Files
orion/app/modules/loyalty/static/shared/js/loyalty-card-detail-view.js
Samir Boulahtit dd1f9af811
Some checks failed
CI / ruff (push) Successful in 17s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running
fix(i18n): locale-aware date/number formatting in loyalty factories
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>
2026-05-24 23:03:03 +02:00

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');