feat(loyalty): transaction categories — admin UI + web terminal
Some checks failed
CI / ruff (push) Successful in 27s
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

Admin merchant detail page:
- New "Transaction Categories" section with store selector
- Inline add form, activate/deactivate toggle, delete button
- Categories CRUD via /admin/loyalty/stores/{id}/categories API

Web terminal:
- Loads categories on init via /store/loyalty/categories
- Category pill selector shown in PIN modal before stamp/earn actions
- Selected category_id passed to stamp and points API calls
- Categories are optional (selector hidden when none configured)

4 new i18n keys (EN).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 12:28:55 +02:00
parent 1cf9fea40a
commit ab2daf99bd
5 changed files with 178 additions and 4 deletions

View File

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