The store and merchant init-alpine.js derive currentPage from the URL's last segment (e.g., /loyalty/program -> 'program'). Loyalty menu items used prefixed IDs like 'loyalty-program' which never matched, so sidebar items never highlighted. Fixed by renaming all store/merchant menu item IDs and JS currentPage values to match URL segments: program, cards, analytics, transactions, pins, settings — consistent with how every other module works. Also reverted the init-alpine.js guard that broke storeCode extraction, and added missing loyalty.common.contact_admin_setup translation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
343 lines
12 KiB
JavaScript
343 lines
12 KiB
JavaScript
// app/modules/loyalty/static/store/js/loyalty-terminal.js
|
|
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
|
|
|
// Use centralized logger
|
|
const loyaltyTerminalLog = window.LogConfig.loggers.loyaltyTerminal || window.LogConfig.createLogger('loyaltyTerminal');
|
|
|
|
// ============================================
|
|
// STORE LOYALTY TERMINAL FUNCTION
|
|
// ============================================
|
|
function storeLoyaltyTerminal() {
|
|
return {
|
|
// Inherit base layout functionality
|
|
...data(),
|
|
|
|
// Page identifier
|
|
currentPage: 'terminal',
|
|
|
|
// Program state
|
|
program: null,
|
|
availableRewards: [],
|
|
|
|
// Customer lookup
|
|
searchQuery: '',
|
|
lookingUp: false,
|
|
selectedCard: null,
|
|
|
|
// Transaction inputs
|
|
earnAmount: null,
|
|
selectedReward: '',
|
|
|
|
// PIN entry
|
|
showPinEntry: false,
|
|
pinDigits: '',
|
|
pendingAction: null, // 'earn' or 'redeem'
|
|
processing: false,
|
|
|
|
// Recent transactions
|
|
recentTransactions: [],
|
|
|
|
// State
|
|
loading: false,
|
|
error: null,
|
|
|
|
// Initialize
|
|
async init() {
|
|
loyaltyTerminalLog.info('=== LOYALTY TERMINAL INITIALIZING ===');
|
|
|
|
// Prevent multiple initializations
|
|
if (window._loyaltyTerminalInitialized) {
|
|
loyaltyTerminalLog.warn('Loyalty terminal already initialized, skipping...');
|
|
return;
|
|
}
|
|
window._loyaltyTerminalInitialized = true;
|
|
|
|
// IMPORTANT: Call parent init first to set storeCode from URL
|
|
const parentInit = data().init;
|
|
if (parentInit) {
|
|
await parentInit.call(this);
|
|
}
|
|
|
|
await this.loadData();
|
|
|
|
loyaltyTerminalLog.info('=== LOYALTY TERMINAL INITIALIZATION COMPLETE ===');
|
|
},
|
|
|
|
// Load initial data
|
|
async loadData() {
|
|
this.loading = true;
|
|
this.error = null;
|
|
|
|
try {
|
|
await Promise.all([
|
|
this.loadProgram(),
|
|
this.loadRecentTransactions()
|
|
]);
|
|
} catch (error) {
|
|
loyaltyTerminalLog.error('Failed to load data:', error);
|
|
this.error = error.message || 'Failed to load terminal';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
// Load program info
|
|
async loadProgram() {
|
|
try {
|
|
loyaltyTerminalLog.info('Loading program info...');
|
|
const response = await apiClient.get('/store/loyalty/program');
|
|
|
|
if (response) {
|
|
this.program = response;
|
|
this.availableRewards = response.points_rewards || [];
|
|
loyaltyTerminalLog.info('Program loaded:', this.program.display_name);
|
|
}
|
|
} catch (error) {
|
|
if (error.status === 404) {
|
|
loyaltyTerminalLog.info('No program configured');
|
|
this.program = null;
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
},
|
|
|
|
// Load recent transactions
|
|
async loadRecentTransactions() {
|
|
try {
|
|
loyaltyTerminalLog.info('Loading recent transactions...');
|
|
const response = await apiClient.get('/store/loyalty/transactions?limit=10');
|
|
|
|
if (response && response.transactions) {
|
|
this.recentTransactions = response.transactions;
|
|
loyaltyTerminalLog.info(`Loaded ${this.recentTransactions.length} transactions`);
|
|
}
|
|
} catch (error) {
|
|
loyaltyTerminalLog.warn('Failed to load transactions:', error.message);
|
|
// Don't throw - transactions are optional
|
|
}
|
|
},
|
|
|
|
// Look up customer
|
|
async lookupCustomer() {
|
|
if (!this.searchQuery) return;
|
|
|
|
this.lookingUp = true;
|
|
this.selectedCard = null;
|
|
|
|
try {
|
|
loyaltyTerminalLog.info('Looking up customer:', this.searchQuery);
|
|
const response = await apiClient.get(`/store/loyalty/cards/lookup?q=${encodeURIComponent(this.searchQuery)}`);
|
|
|
|
if (response) {
|
|
this.selectedCard = response;
|
|
loyaltyTerminalLog.info('Customer found:', this.selectedCard.customer_name);
|
|
this.searchQuery = '';
|
|
}
|
|
} catch (error) {
|
|
if (error.status === 404) {
|
|
Utils.showToast(I18n.t('loyalty.store.terminal.customer_not_found'), 'warning');
|
|
} else {
|
|
Utils.showToast(I18n.t('loyalty.store.terminal.error_lookup', {message: error.message}), 'error');
|
|
}
|
|
loyaltyTerminalLog.error('Lookup failed:', error);
|
|
} finally {
|
|
this.lookingUp = false;
|
|
}
|
|
},
|
|
|
|
// Clear selected customer
|
|
clearCustomer() {
|
|
this.selectedCard = null;
|
|
this.earnAmount = null;
|
|
this.selectedReward = '';
|
|
},
|
|
|
|
// Get selected reward points
|
|
getSelectedRewardPoints() {
|
|
if (!this.selectedReward) return 0;
|
|
const reward = this.availableRewards.find(r => r.id === this.selectedReward);
|
|
return reward ? reward.points_required : 0;
|
|
},
|
|
|
|
// Show PIN modal
|
|
showPinModal(action) {
|
|
this.pendingAction = action;
|
|
this.pinDigits = '';
|
|
this.showPinEntry = true;
|
|
},
|
|
|
|
// PIN entry methods
|
|
addPinDigit(digit) {
|
|
if (this.pinDigits.length < 4) {
|
|
this.pinDigits += digit.toString();
|
|
}
|
|
},
|
|
|
|
removePinDigit() {
|
|
this.pinDigits = this.pinDigits.slice(0, -1);
|
|
},
|
|
|
|
cancelPinEntry() {
|
|
this.showPinEntry = false;
|
|
this.pinDigits = '';
|
|
this.pendingAction = null;
|
|
},
|
|
|
|
// Submit transaction
|
|
async submitTransaction() {
|
|
if (this.pinDigits.length !== 4) return;
|
|
|
|
this.processing = true;
|
|
|
|
try {
|
|
if (this.pendingAction === 'stamp') {
|
|
await this.addStamp();
|
|
} else if (this.pendingAction === 'redeemStamps') {
|
|
await this.redeemStamps();
|
|
} else if (this.pendingAction === 'earn') {
|
|
await this.earnPoints();
|
|
} else if (this.pendingAction === 'redeem') {
|
|
await this.redeemReward();
|
|
}
|
|
|
|
// Close modal and refresh
|
|
this.showPinEntry = false;
|
|
this.pinDigits = '';
|
|
this.pendingAction = null;
|
|
|
|
// Refresh customer card and transactions
|
|
if (this.selectedCard) {
|
|
await this.refreshCard();
|
|
}
|
|
await this.loadRecentTransactions();
|
|
|
|
} catch (error) {
|
|
Utils.showToast(I18n.t('loyalty.store.terminal.transaction_failed', {message: error.message}), 'error');
|
|
loyaltyTerminalLog.error('Transaction failed:', error);
|
|
} finally {
|
|
this.processing = false;
|
|
}
|
|
},
|
|
|
|
// Add stamp
|
|
async addStamp() {
|
|
loyaltyTerminalLog.info('Adding stamp...');
|
|
|
|
await apiClient.post('/store/loyalty/stamp', {
|
|
card_id: this.selectedCard.id,
|
|
staff_pin: this.pinDigits
|
|
});
|
|
|
|
Utils.showToast(I18n.t('loyalty.store.terminal.stamp_added'), 'success');
|
|
},
|
|
|
|
// Redeem stamps
|
|
async redeemStamps() {
|
|
loyaltyTerminalLog.info('Redeeming stamps...');
|
|
|
|
await apiClient.post('/store/loyalty/stamp/redeem', {
|
|
card_id: this.selectedCard.id,
|
|
staff_pin: this.pinDigits
|
|
});
|
|
|
|
Utils.showToast(I18n.t('loyalty.store.terminal.stamps_redeemed'), 'success');
|
|
},
|
|
|
|
// Earn points
|
|
async earnPoints() {
|
|
loyaltyTerminalLog.info('Earning points...', { amount: this.earnAmount });
|
|
|
|
const response = await apiClient.post('/store/loyalty/points/earn', {
|
|
card_id: this.selectedCard.id,
|
|
purchase_amount_cents: Math.round(this.earnAmount * 100),
|
|
staff_pin: this.pinDigits
|
|
});
|
|
|
|
const pointsEarned = response.points_earned || Math.floor(this.earnAmount * (this.program?.points_per_euro || 1));
|
|
Utils.showToast(I18n.t('loyalty.store.terminal.x_points_awarded', {points: pointsEarned}), 'success');
|
|
|
|
this.earnAmount = null;
|
|
},
|
|
|
|
// Redeem reward
|
|
async redeemReward() {
|
|
const reward = this.availableRewards.find(r => r.id === this.selectedReward);
|
|
if (!reward) return;
|
|
|
|
loyaltyTerminalLog.info('Redeeming reward...', { reward: reward.name });
|
|
|
|
await apiClient.post('/store/loyalty/points/redeem', {
|
|
card_id: this.selectedCard.id,
|
|
reward_id: this.selectedReward,
|
|
staff_pin: this.pinDigits
|
|
});
|
|
|
|
Utils.showToast(I18n.t('loyalty.store.terminal.reward_redeemed', {name: reward.name}), 'success');
|
|
|
|
this.selectedReward = '';
|
|
},
|
|
|
|
// Refresh card data
|
|
async refreshCard() {
|
|
try {
|
|
const response = await apiClient.get(`/store/loyalty/cards/${this.selectedCard.id}`);
|
|
if (response) {
|
|
this.selectedCard = response;
|
|
}
|
|
} catch (error) {
|
|
loyaltyTerminalLog.warn('Failed to refresh card:', error.message);
|
|
}
|
|
},
|
|
|
|
// Format number
|
|
getTransactionLabel(tx) {
|
|
const type = tx.transaction_type;
|
|
if (type) {
|
|
return I18n.t('loyalty.transactions.' + type, {defaultValue: type.replace(/_/g, ' ')});
|
|
}
|
|
return I18n.t('loyalty.common.unknown');
|
|
},
|
|
|
|
getTransactionColor(tx) {
|
|
const type = tx.transaction_type || '';
|
|
if (['card_created', 'welcome_bonus', 'stamp_earned', 'points_earned'].includes(type)) {
|
|
return 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100';
|
|
}
|
|
if (['stamp_redeemed', 'points_redeemed'].includes(type)) {
|
|
return 'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100';
|
|
}
|
|
if (['stamp_voided', 'points_voided', 'points_expired', 'card_deactivated'].includes(type)) {
|
|
return 'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100';
|
|
}
|
|
return 'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100';
|
|
},
|
|
|
|
formatNumber(num) {
|
|
if (num === null || num === undefined) return '0';
|
|
return new Intl.NumberFormat('en-US').format(num);
|
|
},
|
|
|
|
// Format time
|
|
formatTime(dateString) {
|
|
if (!dateString) return '-';
|
|
try {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleTimeString('en-US', {
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
} catch (e) {
|
|
return dateString;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Register logger
|
|
if (!window.LogConfig.loggers.loyaltyTerminal) {
|
|
window.LogConfig.loggers.loyaltyTerminal = window.LogConfig.createLogger('loyaltyTerminal');
|
|
}
|
|
|
|
loyaltyTerminalLog.info('Loyalty terminal module loaded');
|