diff --git a/app/modules/loyalty/locales/en.json b/app/modules/loyalty/locales/en.json index f090b3c3..ecdf7c60 100644 --- a/app/modules/loyalty/locales/en.json +++ b/app/modules/loyalty/locales/en.json @@ -480,6 +480,9 @@ "table_points_earned": "Points Earned", "table_points_redeemed": "Points Redeemed", "table_transactions_30d": "Transactions (30d)", + "transaction_categories": "Transaction Categories", + "select_store": "Select a store...", + "no_categories": "No categories configured. Click Add to create one.", "admin_policy_settings": "Admin Policy Settings", "staff_pin_policy": "Staff PIN Policy", "self_enrollment": "Self Enrollment", @@ -709,6 +712,7 @@ "reward_redeemed": "Reward redeemed: {name}", "card_label": "Card", "confirm": "Confirm", + "select_category": "Category (what was sold)", "pin_authorize_text": "Enter your staff PIN to authorize this transaction", "free_item": "Free item", "reward_label": "Reward", diff --git a/app/modules/loyalty/static/admin/js/loyalty-merchant-detail.js b/app/modules/loyalty/static/admin/js/loyalty-merchant-detail.js index 5804f1b7..879f4c09 100644 --- a/app/modules/loyalty/static/admin/js/loyalty-merchant-detail.js +++ b/app/modules/loyalty/static/admin/js/loyalty-merchant-detail.js @@ -33,6 +33,12 @@ function adminLoyaltyMerchantDetail() { settings: null, locations: [], + // Transaction categories + selectedCategoryStoreId: '', + storeCategories: [], + showAddCategory: false, + newCategoryName: '', + // State loading: false, error: null, @@ -258,6 +264,59 @@ function adminLoyaltyMerchantDetail() { formatNumber(num) { if (num === null || num === undefined) return '0'; return new Intl.NumberFormat('en-US').format(num); + }, + + // Transaction categories + async loadCategoriesForStore() { + if (!this.selectedCategoryStoreId) { + this.storeCategories = []; + return; + } + try { + const response = await apiClient.get(`/admin/loyalty/stores/${this.selectedCategoryStoreId}/categories`); + this.storeCategories = response?.categories || []; + } catch (error) { + loyaltyMerchantDetailLog.warn('Failed to load categories:', error.message); + this.storeCategories = []; + } + }, + + async createCategory() { + if (!this.newCategoryName || !this.selectedCategoryStoreId) return; + try { + await apiClient.post(`/admin/loyalty/stores/${this.selectedCategoryStoreId}/categories`, { + name: this.newCategoryName, + display_order: this.storeCategories.length, + }); + this.newCategoryName = ''; + this.showAddCategory = false; + await this.loadCategoriesForStore(); + Utils.showToast('Category created', 'success'); + } catch (error) { + Utils.showToast(error.message || 'Failed to create category', 'error'); + } + }, + + async toggleCategoryActive(cat) { + try { + await apiClient.patch(`/admin/loyalty/stores/${this.selectedCategoryStoreId}/categories/${cat.id}`, { + is_active: !cat.is_active, + }); + await this.loadCategoriesForStore(); + } catch (error) { + Utils.showToast(error.message || 'Failed to update category', 'error'); + } + }, + + async deleteCategory(catId) { + if (!confirm('Delete this category?')) return; + try { + await apiClient.delete(`/admin/loyalty/stores/${this.selectedCategoryStoreId}/categories/${catId}`); + await this.loadCategoriesForStore(); + Utils.showToast('Category deleted', 'success'); + } catch (error) { + Utils.showToast(error.message || 'Failed to delete category', 'error'); + } } }; } diff --git a/app/modules/loyalty/static/store/js/loyalty-terminal.js b/app/modules/loyalty/static/store/js/loyalty-terminal.js index 6efdbfca..8138e9c2 100644 --- a/app/modules/loyalty/static/store/js/loyalty-terminal.js +++ b/app/modules/loyalty/static/store/js/loyalty-terminal.js @@ -31,6 +31,8 @@ function storeLoyaltyTerminal() { // Transaction inputs earnAmount: null, selectedReward: '', + selectedCategory: null, + categories: [], // PIN entry showPinEntry: false, @@ -63,6 +65,7 @@ function storeLoyaltyTerminal() { } await this.loadData(); + await this.loadCategories(); loyaltyTerminalLog.info('=== LOYALTY TERMINAL INITIALIZATION COMPLETE ==='); }, @@ -279,13 +282,25 @@ function storeLoyaltyTerminal() { } }, + // Load categories for this store + async loadCategories() { + try { + const response = await apiClient.get('/store/loyalty/categories'); + this.categories = (response?.categories || []).filter(c => c.is_active); + loyaltyTerminalLog.info(`Loaded ${this.categories.length} categories`); + } catch (error) { + loyaltyTerminalLog.warn('Failed to load categories:', error.message); + } + }, + // Add stamp async addStamp() { loyaltyTerminalLog.info('Adding stamp...'); await apiClient.post('/store/loyalty/stamp', { card_id: this.selectedCard.id, - staff_pin: this.pinDigits + staff_pin: this.pinDigits, + category_id: this.selectedCategory || undefined, }); Utils.showToast(I18n.t('loyalty.store.terminal.stamp_added'), 'success'); @@ -297,7 +312,7 @@ function storeLoyaltyTerminal() { await apiClient.post('/store/loyalty/stamp/redeem', { card_id: this.selectedCard.id, - staff_pin: this.pinDigits + staff_pin: this.pinDigits, }); Utils.showToast(I18n.t('loyalty.store.terminal.stamps_redeemed'), 'success'); @@ -310,7 +325,8 @@ function storeLoyaltyTerminal() { 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 + staff_pin: this.pinDigits, + category_id: this.selectedCategory || undefined, }); const pointsEarned = response.points_earned || Math.floor(this.earnAmount * (this.program?.points_per_euro || 1)); @@ -329,7 +345,7 @@ function storeLoyaltyTerminal() { await apiClient.post('/store/loyalty/points/redeem', { card_id: this.selectedCard.id, reward_id: this.selectedReward, - staff_pin: this.pinDigits + staff_pin: this.pinDigits, }); Utils.showToast(I18n.t('loyalty.store.terminal.reward_redeemed', {name: reward.name}), 'success'); diff --git a/app/modules/loyalty/templates/loyalty/admin/merchant-detail.html b/app/modules/loyalty/templates/loyalty/admin/merchant-detail.html index a27a8c47..7b1400b8 100644 --- a/app/modules/loyalty/templates/loyalty/admin/merchant-detail.html +++ b/app/modules/loyalty/templates/loyalty/admin/merchant-detail.html @@ -201,6 +201,83 @@ {% endcall %} + +
+ {{ _('loyalty.admin.merchant_detail.no_categories') }} +
+{{ _('loyalty.store.terminal.pin_authorize_text') }}
+ + +