feat(loyalty): cross-persona page alignment with shared components
Align loyalty pages across admin, merchant, and store personas so each sees the same page set scoped to their access level. Admin acts as a superset of merchant with "on behalf" capabilities. New pages: - Store: Staff PINs management (CRUD) - Merchant: Cards, Card Detail, Transactions, Staff PINs (CRUD), Settings (read-only) - Admin: Merchant Cards, Card Detail, Transactions, PINs (read-only) Architecture: - 4 shared Jinja2 partials (cards-list, card-detail, transactions, pins) - 4 shared JS factory modules parameterized by apiPrefix/scope - Persona templates are thin wrappers including shared partials - PinDetailResponse schema for cross-store PIN listings API: 17 new endpoints (11 merchant, 6 admin on-behalf) Tests: 38 new integration tests, arch-check green i18n: ~130 new keys across en/fr/de/lb Docs: pages-and-navigation.md with full page matrix Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
// app/modules/loyalty/static/admin/js/loyalty-merchant-card-detail.js
|
||||
// Admin wrapper for shared loyalty card detail view (on-behalf view).
|
||||
|
||||
const adminMerchantCardDetailLog = window.LogConfig.loggers.adminMerchantCardDetail || window.LogConfig.createLogger('adminMerchantCardDetail');
|
||||
|
||||
function adminMerchantCardDetail() {
|
||||
// Extract merchant_id and card_id from URL: /admin/loyalty/merchants/{merchant_id}/cards/{card_id}
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const merchantsIndex = pathParts.indexOf('merchants');
|
||||
const merchantId = merchantsIndex !== -1 ? pathParts[merchantsIndex + 1] : null;
|
||||
|
||||
return loyaltyCardDetailView({
|
||||
apiPrefix: '/admin/loyalty/merchants/' + merchantId,
|
||||
backUrl: '/admin/loyalty/merchants/' + merchantId + '/cards',
|
||||
currentPage: 'loyalty-programs',
|
||||
});
|
||||
}
|
||||
|
||||
if (!window.LogConfig.loggers.adminMerchantCardDetail) {
|
||||
window.LogConfig.loggers.adminMerchantCardDetail = window.LogConfig.createLogger('adminMerchantCardDetail');
|
||||
}
|
||||
adminMerchantCardDetailLog.info('Admin merchant card detail module loaded');
|
||||
@@ -0,0 +1,23 @@
|
||||
// app/modules/loyalty/static/admin/js/loyalty-merchant-cards.js
|
||||
// Admin wrapper for shared loyalty cards list (on-behalf view).
|
||||
|
||||
const adminMerchantCardsLog = window.LogConfig.loggers.adminMerchantCards || window.LogConfig.createLogger('adminMerchantCards');
|
||||
|
||||
function adminMerchantCards() {
|
||||
// Extract merchant_id from URL: /admin/loyalty/merchants/{merchant_id}/cards
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const merchantsIndex = pathParts.indexOf('merchants');
|
||||
const merchantId = merchantsIndex !== -1 ? pathParts[merchantsIndex + 1] : null;
|
||||
|
||||
return loyaltyCardsList({
|
||||
apiPrefix: '/admin/loyalty/merchants/' + merchantId,
|
||||
baseUrl: '/admin/loyalty/merchants/' + merchantId + '/cards',
|
||||
showStoreFilter: true,
|
||||
currentPage: 'loyalty-programs',
|
||||
});
|
||||
}
|
||||
|
||||
if (!window.LogConfig.loggers.adminMerchantCards) {
|
||||
window.LogConfig.loggers.adminMerchantCards = window.LogConfig.createLogger('adminMerchantCards');
|
||||
}
|
||||
adminMerchantCardsLog.info('Admin merchant cards module loaded');
|
||||
23
app/modules/loyalty/static/admin/js/loyalty-merchant-pins.js
Normal file
23
app/modules/loyalty/static/admin/js/loyalty-merchant-pins.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// app/modules/loyalty/static/admin/js/loyalty-merchant-pins.js
|
||||
// Admin wrapper for shared loyalty PINs list (on-behalf read-only view).
|
||||
|
||||
const adminMerchantPinsLog = window.LogConfig.loggers.adminMerchantPins || window.LogConfig.createLogger('adminMerchantPins');
|
||||
|
||||
function adminMerchantPins() {
|
||||
// Extract merchant_id from URL: /admin/loyalty/merchants/{merchant_id}/pins
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const merchantsIndex = pathParts.indexOf('merchants');
|
||||
const merchantId = merchantsIndex !== -1 ? pathParts[merchantsIndex + 1] : null;
|
||||
|
||||
return loyaltyPinsList({
|
||||
apiPrefix: '/admin/loyalty/merchants/' + merchantId,
|
||||
showStoreFilter: true,
|
||||
showCrud: false,
|
||||
currentPage: 'loyalty-programs',
|
||||
});
|
||||
}
|
||||
|
||||
if (!window.LogConfig.loggers.adminMerchantPins) {
|
||||
window.LogConfig.loggers.adminMerchantPins = window.LogConfig.createLogger('adminMerchantPins');
|
||||
}
|
||||
adminMerchantPinsLog.info('Admin merchant pins module loaded');
|
||||
@@ -0,0 +1,22 @@
|
||||
// app/modules/loyalty/static/admin/js/loyalty-merchant-transactions.js
|
||||
// Admin wrapper for shared loyalty transactions list (on-behalf view).
|
||||
|
||||
const adminMerchantTransactionsLog = window.LogConfig.loggers.adminMerchantTransactions || window.LogConfig.createLogger('adminMerchantTransactions');
|
||||
|
||||
function adminMerchantTransactions() {
|
||||
// Extract merchant_id from URL: /admin/loyalty/merchants/{merchant_id}/transactions
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const merchantsIndex = pathParts.indexOf('merchants');
|
||||
const merchantId = merchantsIndex !== -1 ? pathParts[merchantsIndex + 1] : null;
|
||||
|
||||
return loyaltyTransactionsList({
|
||||
apiPrefix: '/admin/loyalty/merchants/' + merchantId,
|
||||
showStoreFilter: true,
|
||||
currentPage: 'loyalty-programs',
|
||||
});
|
||||
}
|
||||
|
||||
if (!window.LogConfig.loggers.adminMerchantTransactions) {
|
||||
window.LogConfig.loggers.adminMerchantTransactions = window.LogConfig.createLogger('adminMerchantTransactions');
|
||||
}
|
||||
adminMerchantTransactionsLog.info('Admin merchant transactions module loaded');
|
||||
@@ -0,0 +1,17 @@
|
||||
// app/modules/loyalty/static/merchant/js/loyalty-card-detail.js
|
||||
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
||||
|
||||
const merchantCardDetailLog = window.LogConfig.loggers.merchantCardDetail || window.LogConfig.createLogger('merchantCardDetail');
|
||||
|
||||
function merchantLoyaltyCardDetail() {
|
||||
return loyaltyCardDetailView({
|
||||
apiPrefix: '/merchants/loyalty',
|
||||
backUrl: '/merchants/loyalty/cards',
|
||||
currentPage: 'loyalty-cards',
|
||||
});
|
||||
}
|
||||
|
||||
if (!window.LogConfig.loggers.merchantCardDetail) {
|
||||
window.LogConfig.loggers.merchantCardDetail = window.LogConfig.createLogger('merchantCardDetail');
|
||||
}
|
||||
merchantCardDetailLog.info('Merchant loyalty card detail module loaded');
|
||||
18
app/modules/loyalty/static/merchant/js/loyalty-cards.js
Normal file
18
app/modules/loyalty/static/merchant/js/loyalty-cards.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// app/modules/loyalty/static/merchant/js/loyalty-cards.js
|
||||
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
||||
|
||||
const merchantCardsLog = window.LogConfig.loggers.merchantCards || window.LogConfig.createLogger('merchantCards');
|
||||
|
||||
function merchantLoyaltyCards() {
|
||||
return loyaltyCardsList({
|
||||
apiPrefix: '/merchants/loyalty',
|
||||
baseUrl: '/merchants/loyalty/cards',
|
||||
showStoreFilter: true,
|
||||
currentPage: 'loyalty-cards',
|
||||
});
|
||||
}
|
||||
|
||||
if (!window.LogConfig.loggers.merchantCards) {
|
||||
window.LogConfig.loggers.merchantCards = window.LogConfig.createLogger('merchantCards');
|
||||
}
|
||||
merchantCardsLog.info('Merchant loyalty cards module loaded');
|
||||
@@ -0,0 +1,53 @@
|
||||
// app/modules/loyalty/static/merchant/js/loyalty-merchant-settings.js
|
||||
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
||||
|
||||
const merchantSettingsViewLog = window.LogConfig.loggers.merchantSettingsView || window.LogConfig.createLogger('merchantSettingsView');
|
||||
|
||||
function merchantLoyaltyMerchantSettings() {
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'loyalty-settings',
|
||||
|
||||
settings: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
async init() {
|
||||
merchantSettingsViewLog.info('=== MERCHANT LOYALTY SETTINGS VIEW PAGE INITIALIZING ===');
|
||||
if (window._merchantLoyaltyMerchantSettingsInitialized) return;
|
||||
window._merchantLoyaltyMerchantSettingsInitialized = true;
|
||||
|
||||
this.loadMenuConfig();
|
||||
await this.loadSettings();
|
||||
merchantSettingsViewLog.info('=== MERCHANT LOYALTY SETTINGS VIEW PAGE INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
async loadSettings() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await apiClient.get('/merchants/loyalty/settings');
|
||||
if (response) {
|
||||
this.settings = response;
|
||||
merchantSettingsViewLog.info('Settings loaded');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
merchantSettingsViewLog.info('No settings found');
|
||||
this.settings = null;
|
||||
} else {
|
||||
merchantSettingsViewLog.error('Failed to load settings:', error);
|
||||
this.error = error.message || 'Failed to load settings';
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!window.LogConfig.loggers.merchantSettingsView) {
|
||||
window.LogConfig.loggers.merchantSettingsView = window.LogConfig.createLogger('merchantSettingsView');
|
||||
}
|
||||
merchantSettingsViewLog.info('Merchant loyalty settings view module loaded');
|
||||
18
app/modules/loyalty/static/merchant/js/loyalty-pins.js
Normal file
18
app/modules/loyalty/static/merchant/js/loyalty-pins.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// app/modules/loyalty/static/merchant/js/loyalty-pins.js
|
||||
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
||||
|
||||
const merchantPinsLog = window.LogConfig.loggers.merchantPins || window.LogConfig.createLogger('merchantPins');
|
||||
|
||||
function merchantLoyaltyPins() {
|
||||
return loyaltyPinsList({
|
||||
apiPrefix: '/merchants/loyalty',
|
||||
showStoreFilter: true,
|
||||
showCrud: true,
|
||||
currentPage: 'loyalty-pins',
|
||||
});
|
||||
}
|
||||
|
||||
if (!window.LogConfig.loggers.merchantPins) {
|
||||
window.LogConfig.loggers.merchantPins = window.LogConfig.createLogger('merchantPins');
|
||||
}
|
||||
merchantPinsLog.info('Merchant loyalty pins module loaded');
|
||||
@@ -0,0 +1,17 @@
|
||||
// app/modules/loyalty/static/merchant/js/loyalty-transactions.js
|
||||
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
||||
|
||||
const merchantTransactionsLog = window.LogConfig.loggers.merchantTransactions || window.LogConfig.createLogger('merchantTransactions');
|
||||
|
||||
function merchantLoyaltyTransactions() {
|
||||
return loyaltyTransactionsList({
|
||||
apiPrefix: '/merchants/loyalty',
|
||||
showStoreFilter: true,
|
||||
currentPage: 'loyalty-transactions',
|
||||
});
|
||||
}
|
||||
|
||||
if (!window.LogConfig.loggers.merchantTransactions) {
|
||||
window.LogConfig.loggers.merchantTransactions = window.LogConfig.createLogger('merchantTransactions');
|
||||
}
|
||||
merchantTransactionsLog.info('Merchant loyalty transactions module loaded');
|
||||
156
app/modules/loyalty/static/shared/js/loyalty-card-detail-view.js
Normal file
156
app/modules/loyalty/static/shared/js/loyalty-card-detail-view.js
Normal file
@@ -0,0 +1,156 @@
|
||||
// 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
|
||||
*/
|
||||
function loyaltyCardDetailView(config) {
|
||||
const guardKey = '_loyaltyCardDetailView_' + config.currentPage + '_initialized';
|
||||
|
||||
return {
|
||||
...data(),
|
||||
currentPage: config.currentPage,
|
||||
|
||||
// Data
|
||||
cardId: null,
|
||||
card: null,
|
||||
transactions: [],
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Extract cardId from URL: find 'cards' segment and take the next segment
|
||||
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;
|
||||
}
|
||||
|
||||
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() {
|
||||
try {
|
||||
const response = await apiClient.get(
|
||||
config.apiPrefix + '/cards/' + this.cardId + '/transactions?limit=50'
|
||||
);
|
||||
if (response) {
|
||||
this.transactions = Array.isArray(response)
|
||||
? response
|
||||
: (response.transactions || []);
|
||||
}
|
||||
} catch (error) {
|
||||
loyaltyCardDetailViewLog.warn('Failed to load transactions:', error.message);
|
||||
}
|
||||
},
|
||||
|
||||
// Formatting helpers
|
||||
formatNumber(num) {
|
||||
return num == null ? '0' : new Intl.NumberFormat('en-US').format(num);
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return 'Never';
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
},
|
||||
|
||||
formatDateTime(dateString) {
|
||||
if (!dateString) return 'Never';
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
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');
|
||||
247
app/modules/loyalty/static/shared/js/loyalty-cards-list.js
Normal file
247
app/modules/loyalty/static/shared/js/loyalty-cards-list.js
Normal file
@@ -0,0 +1,247 @@
|
||||
// app/modules/loyalty/static/shared/js/loyalty-cards-list.js
|
||||
// Shared Alpine.js data factory for loyalty cards listing pages.
|
||||
// Used by store, merchant, and admin frontends.
|
||||
|
||||
const loyaltyCardsListLog = window.LogConfig.loggers.loyaltyCardsList || window.LogConfig.createLogger('loyaltyCardsList');
|
||||
|
||||
/**
|
||||
* Factory that returns an Alpine.js data object for loyalty card listing.
|
||||
*
|
||||
* @param {Object} config
|
||||
* @param {string} config.apiPrefix - API path prefix, e.g. '/store/loyalty'
|
||||
* @param {string} config.baseUrl - Base URL for card detail links, e.g. '/store/ORION/loyalty/cards'
|
||||
* @param {boolean} config.showStoreFilter - Whether to show the store/location dropdown filter
|
||||
* @param {string} config.currentPage - Alpine currentPage identifier
|
||||
*/
|
||||
function loyaltyCardsList(config) {
|
||||
const guardKey = '_loyaltyCardsList_' + config.currentPage + '_initialized';
|
||||
|
||||
return {
|
||||
...data(),
|
||||
currentPage: config.currentPage,
|
||||
|
||||
// Data
|
||||
cards: [],
|
||||
program: null,
|
||||
stats: {
|
||||
total_cards: 0,
|
||||
active_cards: 0,
|
||||
new_this_month: 0,
|
||||
total_points_balance: 0
|
||||
},
|
||||
locations: [],
|
||||
|
||||
// Filters
|
||||
filters: {
|
||||
search: '',
|
||||
status: '',
|
||||
store_id: ''
|
||||
},
|
||||
|
||||
// Pagination
|
||||
pagination: {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
pages: 0
|
||||
},
|
||||
|
||||
// State
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
// Config (exposed for templates)
|
||||
_config: config,
|
||||
|
||||
async init() {
|
||||
loyaltyCardsListLog.info('=== LOYALTY CARDS LIST INITIALIZING ===', config.currentPage);
|
||||
if (window[guardKey]) return;
|
||||
window[guardKey] = true;
|
||||
|
||||
const parentInit = data().init;
|
||||
if (parentInit) {
|
||||
await parentInit.call(this);
|
||||
}
|
||||
|
||||
if (window.PlatformSettings) {
|
||||
try {
|
||||
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
|
||||
} catch (error) {
|
||||
loyaltyCardsListLog.warn('Failed to get rows per page setting:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
await this.loadData();
|
||||
loyaltyCardsListLog.info('=== LOYALTY CARDS LIST INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
await this.loadProgram();
|
||||
if (this.program) {
|
||||
const parallel = [this.loadCards(), this.loadStats()];
|
||||
if (config.showStoreFilter) {
|
||||
parallel.push(this.loadLocations());
|
||||
}
|
||||
await Promise.all(parallel);
|
||||
}
|
||||
} catch (error) {
|
||||
loyaltyCardsListLog.error('Failed to load data:', error);
|
||||
this.error = error.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadProgram() {
|
||||
try {
|
||||
const response = await apiClient.get(config.apiPrefix + '/program');
|
||||
if (response) this.program = response;
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async loadCards() {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('skip', (this.pagination.page - 1) * this.pagination.per_page);
|
||||
params.append('limit', this.pagination.per_page);
|
||||
if (this.filters.search) params.append('search', this.filters.search);
|
||||
if (this.filters.status) params.append('is_active', this.filters.status === 'active');
|
||||
if (this.filters.store_id) params.append('store_id', this.filters.store_id);
|
||||
|
||||
const response = await apiClient.get(config.apiPrefix + '/cards?' + params);
|
||||
if (response) {
|
||||
this.cards = response.cards || [];
|
||||
this.pagination.total = response.total || 0;
|
||||
this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page);
|
||||
}
|
||||
} catch (error) {
|
||||
loyaltyCardsListLog.error('Failed to load cards:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async loadStats() {
|
||||
try {
|
||||
const response = await apiClient.get(config.apiPrefix + '/stats');
|
||||
if (response) {
|
||||
this.stats = {
|
||||
total_cards: response.total_cards || 0,
|
||||
active_cards: response.active_cards || 0,
|
||||
new_this_month: response.new_this_month || 0,
|
||||
total_points_balance: response.total_points_balance || 0
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
loyaltyCardsListLog.warn('Failed to load stats:', error.message);
|
||||
}
|
||||
},
|
||||
|
||||
async loadLocations() {
|
||||
try {
|
||||
const response = await apiClient.get(config.apiPrefix + '/locations');
|
||||
if (response) {
|
||||
this.locations = Array.isArray(response) ? response : (response.locations || []);
|
||||
}
|
||||
} catch (error) {
|
||||
loyaltyCardsListLog.warn('Failed to load locations:', error.message);
|
||||
}
|
||||
},
|
||||
|
||||
debouncedSearch() {
|
||||
if (this._searchTimeout) clearTimeout(this._searchTimeout);
|
||||
this._searchTimeout = setTimeout(() => {
|
||||
this.pagination.page = 1;
|
||||
this.loadCards();
|
||||
}, 300);
|
||||
},
|
||||
|
||||
applyFilter() {
|
||||
this.pagination.page = 1;
|
||||
this.loadCards();
|
||||
},
|
||||
|
||||
// Pagination helpers
|
||||
get totalPages() {
|
||||
return this.pagination.pages;
|
||||
},
|
||||
|
||||
get startIndex() {
|
||||
return this.pagination.total === 0
|
||||
? 0
|
||||
: (this.pagination.page - 1) * this.pagination.per_page + 1;
|
||||
},
|
||||
|
||||
get endIndex() {
|
||||
const end = this.pagination.page * this.pagination.per_page;
|
||||
return end > this.pagination.total ? this.pagination.total : end;
|
||||
},
|
||||
|
||||
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.pagination.page--;
|
||||
this.loadCards();
|
||||
}
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
if (this.pagination.page < this.totalPages) {
|
||||
this.pagination.page++;
|
||||
this.loadCards();
|
||||
}
|
||||
},
|
||||
|
||||
goToPage(num) {
|
||||
if (num !== '...' && num >= 1 && num <= this.totalPages) {
|
||||
this.pagination.page = num;
|
||||
this.loadCards();
|
||||
}
|
||||
},
|
||||
|
||||
// Formatting helpers
|
||||
formatNumber(num) {
|
||||
return num == null ? '0' : new Intl.NumberFormat('en-US').format(num);
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return 'Never';
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!window.LogConfig.loggers.loyaltyCardsList) {
|
||||
window.LogConfig.loggers.loyaltyCardsList = window.LogConfig.createLogger('loyaltyCardsList');
|
||||
}
|
||||
loyaltyCardsListLog.info('Loyalty cards list shared module loaded');
|
||||
283
app/modules/loyalty/static/shared/js/loyalty-pins-list.js
Normal file
283
app/modules/loyalty/static/shared/js/loyalty-pins-list.js
Normal file
@@ -0,0 +1,283 @@
|
||||
// app/modules/loyalty/static/shared/js/loyalty-pins-list.js
|
||||
// Shared Alpine.js data factory for loyalty staff PIN listing pages.
|
||||
// Used by store, merchant, and admin frontends.
|
||||
|
||||
const loyaltyPinsListLog = window.LogConfig.loggers.loyaltyPinsList || window.LogConfig.createLogger('loyaltyPinsList');
|
||||
|
||||
/**
|
||||
* Factory that returns an Alpine.js data object for loyalty PIN management.
|
||||
*
|
||||
* @param {Object} config
|
||||
* @param {string} config.apiPrefix - API path prefix, e.g. '/store/loyalty'
|
||||
* @param {boolean} config.showStoreFilter - Whether to show the store/location dropdown filter
|
||||
* @param {boolean} config.showCrud - Whether to show create/edit/delete UI
|
||||
* @param {string} config.currentPage - Alpine currentPage identifier
|
||||
*/
|
||||
function loyaltyPinsList(config) {
|
||||
const guardKey = '_loyaltyPinsList_' + config.currentPage + '_initialized';
|
||||
|
||||
return {
|
||||
...data(),
|
||||
currentPage: config.currentPage,
|
||||
|
||||
// Data
|
||||
pins: [],
|
||||
program: null,
|
||||
locations: [],
|
||||
|
||||
// Filters
|
||||
filters: {
|
||||
store_id: '',
|
||||
status: ''
|
||||
},
|
||||
|
||||
// Stats (computed from pins array)
|
||||
stats: {
|
||||
total: 0,
|
||||
active: 0,
|
||||
locked: 0
|
||||
},
|
||||
|
||||
// State
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
// Modal state
|
||||
showCreateModal: false,
|
||||
showEditModal: false,
|
||||
showDeleteModal: false,
|
||||
editingPin: null,
|
||||
deletingPin: null,
|
||||
|
||||
// Form state
|
||||
pinForm: {
|
||||
name: '',
|
||||
staff_id: '',
|
||||
pin: '',
|
||||
store_id: ''
|
||||
},
|
||||
|
||||
// Action state
|
||||
saving: false,
|
||||
deleting: false,
|
||||
|
||||
// Config (exposed for templates)
|
||||
_config: config,
|
||||
|
||||
async init() {
|
||||
loyaltyPinsListLog.info('=== LOYALTY PINS LIST INITIALIZING ===', config.currentPage);
|
||||
if (window[guardKey]) return;
|
||||
window[guardKey] = true;
|
||||
|
||||
const parentInit = data().init;
|
||||
if (parentInit) {
|
||||
await parentInit.call(this);
|
||||
}
|
||||
|
||||
await this.loadData();
|
||||
loyaltyPinsListLog.info('=== LOYALTY PINS LIST INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
await this.loadProgram();
|
||||
if (this.program) {
|
||||
const parallel = [this.loadPins()];
|
||||
if (config.showStoreFilter) {
|
||||
parallel.push(this.loadLocations());
|
||||
}
|
||||
await Promise.all(parallel);
|
||||
}
|
||||
} catch (error) {
|
||||
loyaltyPinsListLog.error('Failed to load data:', error);
|
||||
this.error = error.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadProgram() {
|
||||
try {
|
||||
const response = await apiClient.get(config.apiPrefix + '/program');
|
||||
if (response) this.program = response;
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async loadPins() {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.filters.store_id) params.append('store_id', this.filters.store_id);
|
||||
if (this.filters.status) params.append('is_active', this.filters.status === 'active');
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = config.apiPrefix + '/pins' + (queryString ? '?' + queryString : '');
|
||||
const response = await apiClient.get(url);
|
||||
if (response) {
|
||||
this.pins = Array.isArray(response) ? response : (response.pins || []);
|
||||
this.computeStats();
|
||||
}
|
||||
} catch (error) {
|
||||
loyaltyPinsListLog.error('Failed to load pins:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async loadLocations() {
|
||||
try {
|
||||
const response = await apiClient.get(config.apiPrefix + '/locations');
|
||||
if (response) {
|
||||
this.locations = Array.isArray(response) ? response : (response.locations || []);
|
||||
}
|
||||
} catch (error) {
|
||||
loyaltyPinsListLog.warn('Failed to load locations:', error.message);
|
||||
}
|
||||
},
|
||||
|
||||
computeStats() {
|
||||
this.stats.total = this.pins.length;
|
||||
this.stats.active = this.pins.filter(p => p.is_active && !p.is_locked).length;
|
||||
this.stats.locked = this.pins.filter(p => p.is_locked).length;
|
||||
},
|
||||
|
||||
applyFilter() {
|
||||
this.loadPins();
|
||||
},
|
||||
|
||||
// ---- CRUD operations (only used when config.showCrud is true) ----
|
||||
|
||||
openCreateModal() {
|
||||
this.pinForm = {
|
||||
name: '',
|
||||
staff_id: '',
|
||||
pin: '',
|
||||
store_id: ''
|
||||
};
|
||||
this.showCreateModal = true;
|
||||
},
|
||||
|
||||
openEditModal(pin) {
|
||||
this.editingPin = pin;
|
||||
this.pinForm = {
|
||||
name: pin.name || '',
|
||||
staff_id: pin.staff_id || '',
|
||||
pin: '',
|
||||
store_id: pin.store_id || ''
|
||||
};
|
||||
this.showEditModal = true;
|
||||
},
|
||||
|
||||
openDeleteModal(pin) {
|
||||
this.deletingPin = pin;
|
||||
this.showDeleteModal = true;
|
||||
},
|
||||
|
||||
async createPin() {
|
||||
this.saving = true;
|
||||
try {
|
||||
const payload = { ...this.pinForm };
|
||||
if (!payload.store_id) delete payload.store_id;
|
||||
|
||||
await apiClient.post(config.apiPrefix + '/pins', payload);
|
||||
Utils.showToast(I18n.t('loyalty.toasts.pin_created'), 'success');
|
||||
this.showCreateModal = false;
|
||||
await this.loadPins();
|
||||
} catch (error) {
|
||||
loyaltyPinsListLog.error('Failed to create pin:', error);
|
||||
Utils.showToast(error.message || I18n.t('loyalty.toasts.pin_create_error'), 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
async updatePin() {
|
||||
if (!this.editingPin) return;
|
||||
this.saving = true;
|
||||
try {
|
||||
const payload = { ...this.pinForm };
|
||||
// Only include pin field if a new pin was entered
|
||||
if (!payload.pin) delete payload.pin;
|
||||
if (!payload.store_id) delete payload.store_id;
|
||||
|
||||
await apiClient.patch(
|
||||
config.apiPrefix + '/pins/' + this.editingPin.id,
|
||||
payload
|
||||
);
|
||||
Utils.showToast(I18n.t('loyalty.toasts.pin_updated'), 'success');
|
||||
this.showEditModal = false;
|
||||
this.editingPin = null;
|
||||
await this.loadPins();
|
||||
} catch (error) {
|
||||
loyaltyPinsListLog.error('Failed to update pin:', error);
|
||||
Utils.showToast(error.message || I18n.t('loyalty.toasts.pin_update_error'), 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deletePin() {
|
||||
if (!this.deletingPin) return;
|
||||
this.deleting = true;
|
||||
try {
|
||||
await apiClient.delete(config.apiPrefix + '/pins/' + this.deletingPin.id);
|
||||
Utils.showToast(I18n.t('loyalty.toasts.pin_deleted'), 'success');
|
||||
this.showDeleteModal = false;
|
||||
this.deletingPin = null;
|
||||
await this.loadPins();
|
||||
} catch (error) {
|
||||
loyaltyPinsListLog.error('Failed to delete pin:', error);
|
||||
Utils.showToast(error.message || I18n.t('loyalty.toasts.pin_delete_error'), 'error');
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
}
|
||||
},
|
||||
|
||||
async unlockPin(pin) {
|
||||
try {
|
||||
await apiClient.post(config.apiPrefix + '/pins/' + pin.id + '/unlock');
|
||||
Utils.showToast(I18n.t('loyalty.toasts.pin_unlocked'), 'success');
|
||||
await this.loadPins();
|
||||
} catch (error) {
|
||||
loyaltyPinsListLog.error('Failed to unlock pin:', error);
|
||||
Utils.showToast(error.message || I18n.t('loyalty.toasts.pin_unlock_error'), 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// Formatting helpers
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return 'Never';
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
},
|
||||
|
||||
formatDateTime(dateString) {
|
||||
if (!dateString) return 'Never';
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!window.LogConfig.loggers.loyaltyPinsList) {
|
||||
window.LogConfig.loggers.loyaltyPinsList = window.LogConfig.createLogger('loyaltyPinsList');
|
||||
}
|
||||
loyaltyPinsListLog.info('Loyalty pins list shared module loaded');
|
||||
@@ -0,0 +1,248 @@
|
||||
// app/modules/loyalty/static/shared/js/loyalty-transactions-list.js
|
||||
// Shared Alpine.js data factory for loyalty transactions listing pages.
|
||||
// Used by store, merchant, and admin frontends.
|
||||
|
||||
const loyaltyTransactionsListLog = window.LogConfig.loggers.loyaltyTransactionsList || window.LogConfig.createLogger('loyaltyTransactionsList');
|
||||
|
||||
/**
|
||||
* Factory that returns an Alpine.js data object for loyalty transaction listing.
|
||||
*
|
||||
* @param {Object} config
|
||||
* @param {string} config.apiPrefix - API path prefix, e.g. '/store/loyalty'
|
||||
* @param {boolean} config.showStoreFilter - Whether to show the store/location dropdown filter
|
||||
* @param {string} config.currentPage - Alpine currentPage identifier
|
||||
*/
|
||||
function loyaltyTransactionsList(config) {
|
||||
const guardKey = '_loyaltyTransactionsList_' + config.currentPage + '_initialized';
|
||||
|
||||
return {
|
||||
...data(),
|
||||
currentPage: config.currentPage,
|
||||
|
||||
// Data
|
||||
transactions: [],
|
||||
locations: [],
|
||||
|
||||
// Filters
|
||||
filters: {
|
||||
search: '',
|
||||
type: '',
|
||||
store_id: ''
|
||||
},
|
||||
|
||||
// Pagination
|
||||
pagination: {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
pages: 0
|
||||
},
|
||||
|
||||
// State
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
// Config (exposed for templates)
|
||||
_config: config,
|
||||
|
||||
async init() {
|
||||
loyaltyTransactionsListLog.info('=== LOYALTY TRANSACTIONS LIST INITIALIZING ===', config.currentPage);
|
||||
if (window[guardKey]) return;
|
||||
window[guardKey] = true;
|
||||
|
||||
const parentInit = data().init;
|
||||
if (parentInit) {
|
||||
await parentInit.call(this);
|
||||
}
|
||||
|
||||
if (window.PlatformSettings) {
|
||||
try {
|
||||
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
|
||||
} catch (error) {
|
||||
loyaltyTransactionsListLog.warn('Failed to get rows per page setting:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
await this.loadData();
|
||||
loyaltyTransactionsListLog.info('=== LOYALTY TRANSACTIONS LIST INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
const parallel = [this.loadTransactions()];
|
||||
if (config.showStoreFilter) {
|
||||
parallel.push(this.loadLocations());
|
||||
}
|
||||
await Promise.all(parallel);
|
||||
} catch (error) {
|
||||
loyaltyTransactionsListLog.error('Failed to load data:', error);
|
||||
this.error = error.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadTransactions() {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('skip', (this.pagination.page - 1) * this.pagination.per_page);
|
||||
params.append('limit', this.pagination.per_page);
|
||||
if (this.filters.search) params.append('search', this.filters.search);
|
||||
if (this.filters.type) params.append('type', this.filters.type);
|
||||
if (this.filters.store_id) params.append('store_id', this.filters.store_id);
|
||||
|
||||
const response = await apiClient.get(config.apiPrefix + '/transactions?' + params);
|
||||
if (response) {
|
||||
this.transactions = response.transactions || [];
|
||||
this.pagination.total = response.total || 0;
|
||||
this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page);
|
||||
}
|
||||
} catch (error) {
|
||||
loyaltyTransactionsListLog.error('Failed to load transactions:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async loadLocations() {
|
||||
try {
|
||||
const response = await apiClient.get(config.apiPrefix + '/locations');
|
||||
if (response) {
|
||||
this.locations = Array.isArray(response) ? response : (response.locations || []);
|
||||
}
|
||||
} catch (error) {
|
||||
loyaltyTransactionsListLog.warn('Failed to load locations:', error.message);
|
||||
}
|
||||
},
|
||||
|
||||
debouncedSearch() {
|
||||
if (this._searchTimeout) clearTimeout(this._searchTimeout);
|
||||
this._searchTimeout = setTimeout(() => {
|
||||
this.pagination.page = 1;
|
||||
this.loadTransactions();
|
||||
}, 300);
|
||||
},
|
||||
|
||||
applyFilter() {
|
||||
this.pagination.page = 1;
|
||||
this.loadTransactions();
|
||||
},
|
||||
|
||||
// Pagination helpers
|
||||
get totalPages() {
|
||||
return this.pagination.pages;
|
||||
},
|
||||
|
||||
get startIndex() {
|
||||
return this.pagination.total === 0
|
||||
? 0
|
||||
: (this.pagination.page - 1) * this.pagination.per_page + 1;
|
||||
},
|
||||
|
||||
get endIndex() {
|
||||
const end = this.pagination.page * this.pagination.per_page;
|
||||
return end > this.pagination.total ? this.pagination.total : end;
|
||||
},
|
||||
|
||||
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.pagination.page--;
|
||||
this.loadTransactions();
|
||||
}
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
if (this.pagination.page < this.totalPages) {
|
||||
this.pagination.page++;
|
||||
this.loadTransactions();
|
||||
}
|
||||
},
|
||||
|
||||
goToPage(num) {
|
||||
if (num !== '...' && num >= 1 && num <= this.totalPages) {
|
||||
this.pagination.page = num;
|
||||
this.loadTransactions();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns Tailwind CSS classes for a transaction type badge.
|
||||
* @param {string} type - Transaction type (e.g. 'earn', 'redeem', 'bonus', 'adjustment', 'expire')
|
||||
* @returns {string} CSS class string
|
||||
*/
|
||||
getTransactionBadgeClass(type) {
|
||||
switch (type) {
|
||||
case 'earn':
|
||||
return 'bg-green-100 text-green-800';
|
||||
case 'redeem':
|
||||
return 'bg-blue-100 text-blue-800';
|
||||
case 'bonus':
|
||||
return 'bg-purple-100 text-purple-800';
|
||||
case 'adjustment':
|
||||
return 'bg-yellow-100 text-yellow-800';
|
||||
case 'expire':
|
||||
return 'bg-red-100 text-red-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
},
|
||||
|
||||
// Formatting helpers
|
||||
formatNumber(num) {
|
||||
return num == null ? '0' : new Intl.NumberFormat('en-US').format(num);
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return 'Never';
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
},
|
||||
|
||||
formatDateTime(dateString) {
|
||||
if (!dateString) return 'Never';
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!window.LogConfig.loggers.loyaltyTransactionsList) {
|
||||
window.LogConfig.loggers.loyaltyTransactionsList = window.LogConfig.createLogger('loyaltyTransactionsList');
|
||||
}
|
||||
loyaltyTransactionsListLog.info('Loyalty transactions list shared module loaded');
|
||||
18
app/modules/loyalty/static/store/js/loyalty-pins.js
Normal file
18
app/modules/loyalty/static/store/js/loyalty-pins.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// app/modules/loyalty/static/store/js/loyalty-pins.js
|
||||
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
||||
|
||||
const storePinsLog = window.LogConfig.loggers.storePins || window.LogConfig.createLogger('storePins');
|
||||
|
||||
function storeLoyaltyPins() {
|
||||
return loyaltyPinsList({
|
||||
apiPrefix: '/store/loyalty',
|
||||
showStoreFilter: false,
|
||||
showCrud: true,
|
||||
currentPage: 'pins',
|
||||
});
|
||||
}
|
||||
|
||||
if (!window.LogConfig.loggers.storePins) {
|
||||
window.LogConfig.loggers.storePins = window.LogConfig.createLogger('storePins');
|
||||
}
|
||||
storePinsLog.info('Store loyalty pins module loaded');
|
||||
Reference in New Issue
Block a user