The terminal's selectedCard comes from CardLookupResponse which uses card_id field, but the JS was referencing selectedCard.id (undefined). This caused all terminal transactions to fail with "LoyaltyCard with identifier 'unknown' not found" instead of processing the transaction or showing proper PIN validation errors. 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.card_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.card_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.card_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.card_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.card_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');
|