Some checks failed
Dates rendered in English even when the dashboard language was set to French (or any other locale). The 5 shared loyalty Alpine factories hardcoded 'en-US' in every toLocaleDateString / toLocaleString / Intl.NumberFormat call, ignoring the user's selected language. - Add `I18n.locale` getter to static/shared/js/i18n.js that returns the current dashboard language code (en/fr/de/lb). Falls back to 'en' if I18n isn't initialised yet. - Replace 'en-US' with I18n.locale in 5 loyalty shared factories: loyalty-cards-list, loyalty-card-detail-view, loyalty-transactions- list, loyalty-pins-list, loyalty-devices-list. - Also fix a latent bug in loyalty-transactions-list.formatDateTime that called toLocaleDateString with hour/minute opts (silently ignored — same bug previously fixed in loyalty-card-detail-view). Scoped to loyalty per session decision; other modules with the same hardcoded 'en-US' pattern (catalog, billing, etc.) are tracked as a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
217 lines
8.3 KiB
JavaScript
217 lines
8.3 KiB
JavaScript
// app/modules/loyalty/static/shared/js/loyalty-card-detail-view.js
|
|
// Shared Alpine.js data factory for loyalty card detail pages.
|
|
// Used by store, merchant, and admin frontends.
|
|
|
|
const loyaltyCardDetailViewLog = window.LogConfig.loggers.loyaltyCardDetailView || window.LogConfig.createLogger('loyaltyCardDetailView');
|
|
|
|
/**
|
|
* Factory that returns an Alpine.js data object for a single loyalty card detail view.
|
|
*
|
|
* @param {Object} config
|
|
* @param {string} config.apiPrefix - API path prefix, e.g. '/store/loyalty'
|
|
* @param {string} config.backUrl - URL for the "back to list" navigation
|
|
* @param {string} config.currentPage - Alpine currentPage identifier
|
|
* @param {boolean} [config.paginate] - Enable paginated transaction history (default false, loads up to 50)
|
|
* @param {Object} [config.txLabels] - Map of transaction_type -> translated label (default {})
|
|
* @param {Object} [config.txNotes] - Map of raw_note -> translated note (default {})
|
|
*/
|
|
function loyaltyCardDetailView(config) {
|
|
const guardKey = '_loyaltyCardDetailView_' + config.currentPage + '_initialized';
|
|
|
|
return {
|
|
...data(),
|
|
currentPage: config.currentPage,
|
|
|
|
// Data
|
|
cardId: null,
|
|
card: null,
|
|
transactions: [],
|
|
|
|
// Translation maps (defaults to empty so admin/merchant get raw values)
|
|
txLabels: config.txLabels || {},
|
|
txNotes: config.txNotes || {},
|
|
|
|
// Pagination (always present so the shared template's pagination macro
|
|
// resolves; only the wrapper that sets show_pagination renders it)
|
|
pagination: { page: 1, per_page: 20, total: 0 },
|
|
|
|
// State
|
|
loading: false,
|
|
error: null,
|
|
|
|
// Config (exposed for templates)
|
|
_config: config,
|
|
|
|
async init() {
|
|
loyaltyCardDetailViewLog.info('=== LOYALTY CARD DETAIL VIEW INITIALIZING ===', config.currentPage);
|
|
if (window[guardKey]) return;
|
|
window[guardKey] = true;
|
|
|
|
const parentInit = data().init;
|
|
if (parentInit) {
|
|
await parentInit.call(this);
|
|
}
|
|
|
|
this.cardId = this._extractCardIdFromUrl();
|
|
if (!this.cardId) {
|
|
this.error = I18n.t('loyalty.errors.card_not_found');
|
|
loyaltyCardDetailViewLog.error('Could not extract card ID from URL');
|
|
return;
|
|
}
|
|
|
|
if (config.paginate && window.PlatformSettings) {
|
|
try {
|
|
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
|
|
} catch (e) { /* keep default */ }
|
|
}
|
|
|
|
loyaltyCardDetailViewLog.info('Card ID extracted:', this.cardId);
|
|
await this.loadData();
|
|
loyaltyCardDetailViewLog.info('=== LOYALTY CARD DETAIL VIEW INITIALIZATION COMPLETE ===');
|
|
},
|
|
|
|
/**
|
|
* Extract the card ID from the current URL path.
|
|
* Looks for the 'cards' segment and returns the segment immediately after it.
|
|
* e.g. /store/ORION/loyalty/cards/abc123 -> 'abc123'
|
|
*/
|
|
_extractCardIdFromUrl() {
|
|
try {
|
|
const segments = window.location.pathname.split('/').filter(Boolean);
|
|
const cardsIndex = segments.indexOf('cards');
|
|
if (cardsIndex !== -1 && cardsIndex + 1 < segments.length) {
|
|
return segments[cardsIndex + 1];
|
|
}
|
|
} catch (error) {
|
|
loyaltyCardDetailViewLog.error('Error parsing URL for card ID:', error);
|
|
}
|
|
return null;
|
|
},
|
|
|
|
async loadData() {
|
|
this.loading = true;
|
|
this.error = null;
|
|
try {
|
|
await Promise.all([
|
|
this.loadCard(),
|
|
this.loadTransactions()
|
|
]);
|
|
} catch (error) {
|
|
loyaltyCardDetailViewLog.error('Failed to load card data:', error);
|
|
this.error = error.message;
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async loadCard() {
|
|
try {
|
|
const response = await apiClient.get(config.apiPrefix + '/cards/' + this.cardId);
|
|
if (response) {
|
|
this.card = response;
|
|
}
|
|
} catch (error) {
|
|
loyaltyCardDetailViewLog.error('Failed to load card:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
async loadTransactions(page = 1) {
|
|
try {
|
|
let url;
|
|
if (config.paginate) {
|
|
const skip = (page - 1) * this.pagination.per_page;
|
|
url = config.apiPrefix + '/cards/' + this.cardId
|
|
+ '/transactions?skip=' + skip + '&limit=' + this.pagination.per_page;
|
|
} else {
|
|
url = config.apiPrefix + '/cards/' + this.cardId + '/transactions?limit=50';
|
|
}
|
|
const response = await apiClient.get(url);
|
|
if (response) {
|
|
this.transactions = Array.isArray(response)
|
|
? response
|
|
: (response.transactions || []);
|
|
if (config.paginate) {
|
|
this.pagination.total = response.total || 0;
|
|
this.pagination.page = page;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
loyaltyCardDetailViewLog.warn('Failed to load transactions:', error.message);
|
|
}
|
|
},
|
|
|
|
// Standard pagination interface (matches shared pagination macro).
|
|
// Always present; only rendered when the template sets show_pagination.
|
|
get totalPages() {
|
|
return Math.max(1, Math.ceil(this.pagination.total / this.pagination.per_page));
|
|
},
|
|
get startIndex() {
|
|
return this.pagination.total === 0
|
|
? 0
|
|
: (this.pagination.page - 1) * this.pagination.per_page + 1;
|
|
},
|
|
get endIndex() {
|
|
return Math.min(this.pagination.page * this.pagination.per_page, this.pagination.total);
|
|
},
|
|
get pageNumbers() {
|
|
const pages = [];
|
|
const total = this.totalPages;
|
|
const current = this.pagination.page;
|
|
if (total <= 7) {
|
|
for (let i = 1; i <= total; i++) pages.push(i);
|
|
} else {
|
|
pages.push(1);
|
|
if (current > 3) pages.push('...');
|
|
const start = Math.max(2, current - 1);
|
|
const end = Math.min(total - 1, current + 1);
|
|
for (let i = start; i <= end; i++) pages.push(i);
|
|
if (current < total - 2) pages.push('...');
|
|
pages.push(total);
|
|
}
|
|
return pages;
|
|
},
|
|
previousPage() { if (this.pagination.page > 1) this.loadTransactions(this.pagination.page - 1); },
|
|
nextPage() { if (this.pagination.page < this.totalPages) this.loadTransactions(this.pagination.page + 1); },
|
|
goToPage(p) { if (p !== '...' && p >= 1 && p <= this.totalPages) this.loadTransactions(p); },
|
|
|
|
// Formatting helpers
|
|
formatNumber(num) {
|
|
return num == null ? '0' : new Intl.NumberFormat(I18n.locale).format(num);
|
|
},
|
|
|
|
formatDate(dateString) {
|
|
if (!dateString) return 'Never';
|
|
try {
|
|
return new Date(dateString).toLocaleDateString(I18n.locale, {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
} catch (e) {
|
|
return dateString;
|
|
}
|
|
},
|
|
|
|
formatDateTime(dateString) {
|
|
if (!dateString) return 'Never';
|
|
try {
|
|
return new Date(dateString).toLocaleString(I18n.locale, {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
} catch (e) {
|
|
return dateString;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
if (!window.LogConfig.loggers.loyaltyCardDetailView) {
|
|
window.LogConfig.loggers.loyaltyCardDetailView = window.LogConfig.createLogger('loyaltyCardDetailView');
|
|
}
|
|
loyaltyCardDetailViewLog.info('Loyalty card detail view shared module loaded');
|