feat(catalog) : add admin categories page with MalioDataTable and drawer (first draft)
- Page Nuxt /admin/categories : MalioDataTable + bouton Ajouter - CategoryDrawer : modes creation / consultation / edition (transition auto view -> edit a la 1re modif), validation client RG-1.02/04/05, mapping erreurs server 409 (doublon) et 422 (violations) - CategoryDeleteModal : confirmation suppression (soft delete cote API) - Types Category, CategoryType, User - i18n admin.categories.* (titre, table, form, validation, toasts) - Fix latent : ajout 'categories' a AdminLinkSlug e2e (oubli ERP-47) Logique fetch inline volontaire au M0 — extraction en composables a ERP-50 (ticket 0.8). Aucune persistance d'etat de tableau dans l'URL.
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader>
|
||||
{{ t('admin.categories.title') }}
|
||||
<template #actions>
|
||||
<MalioButton
|
||||
v-if="canManage"
|
||||
:label="t('admin.categories.newCategory')"
|
||||
icon-name="mdi:add-bold"
|
||||
icon-position="left"
|
||||
@click="openCreateDrawer"
|
||||
/>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- Table des categories : tri par defaut sur Nom ASC (RG-1.10).
|
||||
Tri serveur applique a la requete + tri client en miroir pour
|
||||
la pagination front (volumetrie cible <= 300, cf. spec § 4.1). -->
|
||||
<MalioDataTable
|
||||
:columns="columns"
|
||||
:items="categoryItems"
|
||||
:total-items="categories.length"
|
||||
:row-clickable="true"
|
||||
:empty-message="t('admin.categories.noCategories')"
|
||||
@row-click="onRowClick"
|
||||
/>
|
||||
|
||||
<!-- Drawer creation / consultation / edition. -->
|
||||
<CategoryDrawer
|
||||
v-model="drawerOpen"
|
||||
:category="selectedCategory"
|
||||
@saved="onCategorySaved"
|
||||
@delete="onDeleteRequest"
|
||||
/>
|
||||
|
||||
<!-- Modale de confirmation suppression (soft delete cote serveur). -->
|
||||
<CategoryDeleteModal
|
||||
v-model="deleteModalOpen"
|
||||
:category-name="categoryToDelete?.name ?? ''"
|
||||
:loading="deleting"
|
||||
@confirm="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Category } from '~/modules/catalog/types/category'
|
||||
import type { HydraCollection } from '~/shared/utils/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
const api = useApi()
|
||||
const { can } = usePermissions()
|
||||
|
||||
useHead({ title: t('admin.categories.title') })
|
||||
|
||||
const canManage = computed(() => can('catalog.categories.manage'))
|
||||
|
||||
const categories = ref<Category[]>([])
|
||||
const loading = ref(false)
|
||||
const drawerOpen = ref(false)
|
||||
const selectedCategory = ref<Category | null>(null)
|
||||
const deleteModalOpen = ref(false)
|
||||
const categoryToDelete = ref<Category | null>(null)
|
||||
const deleting = ref(false)
|
||||
|
||||
// Colonnes du datatable. Le type est embarque cote API (cf. spec-back § 3.4) —
|
||||
// on aplatit en label lisible pour l'affichage.
|
||||
const columns = [
|
||||
{ key: 'name', label: t('admin.categories.table.name') },
|
||||
{ key: 'typeLabel', label: t('admin.categories.table.type') },
|
||||
]
|
||||
|
||||
const categoryItems = computed(() =>
|
||||
categories.value.map(cat => ({
|
||||
id: cat.id,
|
||||
name: cat.name,
|
||||
typeLabel: cat.categoryType?.label ?? '',
|
||||
})),
|
||||
)
|
||||
|
||||
function getCategoryById(id: number): Category | undefined {
|
||||
return categories.value.find(c => c.id === id)
|
||||
}
|
||||
|
||||
function onRowClick(item: Record<string, unknown>) {
|
||||
const category = getCategoryById(item.id as number)
|
||||
if (category) openEditDrawer(category)
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge la liste des categories. Le serveur exclut les soft-deleted par
|
||||
* defaut (RG-1.08) et trie par name ASC (RG-1.10). Pas de pagination
|
||||
* serveur (RG : volumetrie ≤ 300, pagination front via MalioDataTable).
|
||||
*
|
||||
* Logique inline volontaire au M0 (decision prompt ERP-49) : extraction
|
||||
* en composable `useCategoriesAdmin` au ticket 0.8 (ERP-50).
|
||||
*/
|
||||
async function loadCategories(): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<HydraCollection<Category>>(
|
||||
'/categories',
|
||||
{ itemsPerPage: 999 },
|
||||
{ toast: false },
|
||||
)
|
||||
categories.value = data.member ?? []
|
||||
} catch {
|
||||
// Reset sur echec pour ne pas afficher de donnees stale. Pas de
|
||||
// toast : un user sans permission view recoit 403 et voit une
|
||||
// liste vide propre — le mecanisme de gating se fait cote sidebar.
|
||||
categories.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDrawer() {
|
||||
selectedCategory.value = null
|
||||
drawerOpen.value = true
|
||||
}
|
||||
|
||||
function openEditDrawer(category: Category) {
|
||||
selectedCategory.value = category
|
||||
drawerOpen.value = true
|
||||
}
|
||||
|
||||
function onDeleteRequest() {
|
||||
if (!selectedCategory.value) return
|
||||
categoryToDelete.value = selectedCategory.value
|
||||
deleteModalOpen.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/categories/{id} → soft delete (RG-1.12). Le serveur pose
|
||||
* `deleted_at = now()` et retourne 204. Refresh de la liste a la fin
|
||||
* pour retirer la ligne (l'index unique partiel autorise une recreation
|
||||
* ulterieure avec le meme couple (name, type) — RG-1.07).
|
||||
*/
|
||||
async function handleDelete(): Promise<void> {
|
||||
if (!categoryToDelete.value) return
|
||||
deleting.value = true
|
||||
try {
|
||||
await api.delete(`/categories/${categoryToDelete.value.id}`, {}, {
|
||||
toastSuccessMessage: t('admin.categories.toast.deleted'),
|
||||
})
|
||||
deleteModalOpen.value = false
|
||||
categoryToDelete.value = null
|
||||
drawerOpen.value = false
|
||||
await loadCategories()
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onCategorySaved() {
|
||||
loadCategories()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCategories()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user