feat(loyalty): inline edit for transaction categories in admin
Some checks failed
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running
CI / ruff (push) Successful in 21s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled

Category list now has a pencil edit button that expands inline with
name + FR/DE/LB translation fields. Save updates via PATCH API.
View mode shows translations summary next to the name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 19:27:55 +02:00
parent eafa086c73
commit 51bcc9f874
2 changed files with 95 additions and 15 deletions

View File

@@ -39,6 +39,8 @@ function adminLoyaltyMerchantDetail() {
showAddCategory: false,
newCategoryName: '',
newCategoryTranslations: { fr: '', de: '', lb: '' },
editingCategoryId: null,
editCategoryData: { name: '', translations: { fr: '', de: '', lb: '' } },
// State
loading: false,
@@ -307,6 +309,38 @@ function adminLoyaltyMerchantDetail() {
}
},
startEditCategory(cat) {
this.editingCategoryId = cat.id;
this.editCategoryData = {
name: cat.name,
translations: {
fr: cat.name_translations?.fr || '',
de: cat.name_translations?.de || '',
lb: cat.name_translations?.lb || '',
},
};
},
async saveEditCategory(catId) {
if (!this.editCategoryData.name) return;
try {
const translations = { en: this.editCategoryData.name };
for (const [lang, val] of Object.entries(this.editCategoryData.translations)) {
if (val) translations[lang] = val;
}
await apiClient.patch(`/admin/loyalty/stores/${this.selectedCategoryStoreId}/categories/${catId}`, {
name: this.editCategoryData.name,
name_translations: Object.keys(translations).length > 0 ? translations : null,
});
this.editingCategoryId = null;
await this.loadCategoriesForStore();
Utils.showToast('Category updated', 'success');
} catch (error) {
Utils.showToast(error.message || 'Failed to update category', 'error');
}
},
async toggleCategoryActive(cat) {
try {
await apiClient.patch(`/admin/loyalty/stores/${this.selectedCategoryStoreId}/categories/${cat.id}`, {

View File

@@ -269,22 +269,68 @@
<!-- Categories table -->
<div class="space-y-2">
<template x-for="cat in storeCategories" :key="cat.id">
<div class="flex items-center justify-between p-3 border border-gray-200 dark:border-gray-700 rounded-lg">
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300" x-text="cat.name"></span>
<span x-show="!cat.is_active" class="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-500 dark:bg-gray-700">{{ _('loyalty.common.inactive') }}</span>
<div class="border border-gray-200 dark:border-gray-700 rounded-lg">
<!-- View mode -->
<div x-show="editingCategoryId !== cat.id" class="flex items-center justify-between p-3">
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300" x-text="cat.name"></span>
<template x-if="cat.name_translations">
<span class="text-xs text-gray-400" x-text="Object.entries(cat.name_translations || {}).filter(([k,v]) => v && k !== 'en').map(([k,v]) => k.toUpperCase() + ': ' + v).join(' · ')"></span>
</template>
<span x-show="!cat.is_active" class="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-500 dark:bg-gray-700">{{ _('loyalty.common.inactive') }}</span>
</div>
<div class="flex items-center gap-2">
<button @click="startEditCategory(cat)" type="button"
aria-label="{{ _('loyalty.common.edit') }}"
class="text-purple-500 hover:text-purple-700">
<span x-html="$icon('pencil', 'w-4 h-4')"></span>
</button>
<button @click="toggleCategoryActive(cat)" type="button"
:aria-label="cat.is_active ? '{{ _('loyalty.common.deactivate') }}' : '{{ _('loyalty.common.activate') }}'"
class="text-sm" :class="cat.is_active ? 'text-orange-500 hover:text-orange-700' : 'text-green-500 hover:text-green-700'">
<span x-html="$icon(cat.is_active ? 'pause' : 'play', 'w-4 h-4')"></span>
</button>
<button @click="deleteCategory(cat.id)" type="button"
aria-label="{{ _('loyalty.common.delete') }}"
class="text-red-500 hover:text-red-700">
<span x-html="$icon('trash', 'w-4 h-4')"></span>
</button>
</div>
</div>
<div class="flex items-center gap-2">
<button @click="toggleCategoryActive(cat)" type="button"
:aria-label="cat.is_active ? '{{ _('loyalty.common.deactivate') }}' : '{{ _('loyalty.common.activate') }}'"
class="text-sm" :class="cat.is_active ? 'text-orange-500 hover:text-orange-700' : 'text-green-500 hover:text-green-700'">
<span x-html="$icon(cat.is_active ? 'pause' : 'play', 'w-4 h-4')"></span>
</button>
<button @click="deleteCategory(cat.id)" type="button"
aria-label="{{ _('loyalty.common.delete') }}"
class="text-red-500 hover:text-red-700">
<span x-html="$icon('trash', 'w-4 h-4')"></span>
</button>
<!-- Edit mode -->
<div x-show="editingCategoryId === cat.id" class="p-3 bg-purple-50 dark:bg-purple-900/20">
<div class="grid gap-2 md:grid-cols-2 mb-3">
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Name (default)</label>
<input type="text" x-model="editCategoryData.name" maxlength="100"
class="w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-300">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">French (FR)</label>
<input type="text" x-model="editCategoryData.translations.fr" maxlength="100"
class="w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-300">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">German (DE)</label>
<input type="text" x-model="editCategoryData.translations.de" maxlength="100"
class="w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-300">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Luxembourgish (LB)</label>
<input type="text" x-model="editCategoryData.translations.lb" maxlength="100"
class="w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-300">
</div>
</div>
<div class="flex justify-end gap-2">
<button @click="editingCategoryId = null" type="button"
class="px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600">
{{ _('loyalty.common.cancel') }}
</button>
<button @click="saveEditCategory(cat.id)" :disabled="!editCategoryData.name"
class="px-3 py-1.5 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50">
{{ _('loyalty.common.save') }}
</button>
</div>
</div>
</div>
</template>