feat(loyalty): inline edit for transaction categories in admin
Some checks failed
Some checks failed
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:
@@ -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}`, {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user