[ERP-50] Implémenter les composables useCategoriesAdmin et useCategoryForm (#25)
Auto Tag Develop / tag (push) Successful in 6s

Lien Lesstime : #50

## Résumé
Refacto : extraction de la logique fetch/CRUD inline de la page categories (ERP-49) vers deux composables dédiés, conformément au pattern Starseed (useSidebar / useModules).

- **useCategoriesAdmin** : singleton state (`categories` + `types` + `loading` + `error`). Pré-chargement des types au mount de la page (au lieu d'un fetch par ouverture du drawer). Reset au logout via `onAuthSessionCleared` + appel explicite dans `logout.vue`.
- **useCategoryForm** : state local par form (pas singleton, contrairement à `useCategoriesAdmin`). Valide côté client en miroir des RG back (RG-1.02 / RG-1.04 / RG-1.05), mappe les erreurs 409 (RG-1.07 doublon) et 422 (violations API Platform) sur les bons champs. `submitCreate` / `submitUpdate` / `submitDelete` renvoient la ressource ou `null` pour découpler la décision de fermeture du drawer.

La page et le drawer deviennent purement présentationnels — aucune régression UX attendue (mêmes validations, mêmes toasts, même bascule view → edit via `isDirty` exposé par le composable).

## Décisions
- `useCategoriesAdmin` porte aussi les types (`fetchTypes`), pas seulement `categories` — sinon le drawer continuerait à fetcher tout seul et la refacto n'aurait rien centralisé.
- `buildCreatePayload` retourne `Record<string, unknown>` (pas `CategoryCreateInput`) car la signature `useApi.post(body: AnyObject)` n'accepte pas les types stricts (variance TS).
- Reset au logout : double mécanisme conservé (auto via `onAuthSessionCleared` pour 401, explicite dans `logout.vue` pour logout volontaire — pattern existant Starseed).

## Tests
- `npx nuxi typecheck` ✓ 0 erreur nouvelle (1 erreur pré-existante sur `modules/catalog/nuxt.config.ts` héritée d'ERP-49)
- `make nuxt-test` ✓ 43/43, 0 régression
- PHPUnit ✓ 311/311 (pre-commit)
- Manuel navigateur : à valider (cahier de test consigné dans Lesstime #50)

## ⚠ Note d'intégration
La branche contient encore les 3 commits ERP-49 (`4046910`, `216f388`, `934a12b`) car elle a été créée depuis la branche ERP-49 avant son merge sur develop. Selon l'ordre de merge : soit ERP-49 est mergée d'abord (cette MR ne contiendra plus que le commit ERP-50 après rebase auto), soit cette MR embarque tout l'historique catalog.

Reviewed-on: #25
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #25.
This commit is contained in:
2026-05-29 09:18:29 +00:00
committed by Autin
parent e0d59962d6
commit 58589e93d0
7 changed files with 575 additions and 296 deletions
@@ -18,7 +18,6 @@
(name ASC, RG-1.10). La barre de pagination du MalioDataTable
reste cosmetique tant qu'aucun slice client n'est cable : a
traiter cote @malio/layer-ui le jour ou la volumetrie monte. -->
<MalioDataTable
:columns="columns"
:items="categoryItems"
@@ -48,18 +47,16 @@
<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()
const { categories, fetchAll, fetchTypes } = useCategoriesAdmin()
const { submitDelete } = useCategoryForm()
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)
@@ -90,35 +87,6 @@ function onRowClick(item: Record<string, unknown>) {
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 (volumetrie cible <= 300) ni de slice client — toute la liste
* est rendue d'un coup ; la barre du MalioDataTable est donc cosmetique
* jusqu'a la mise a jour layer-ui (ticket ERP-70).
*
* 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
@@ -136,32 +104,36 @@ function onDeleteRequest() {
}
/**
* 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).
* Soft delete via le composable de form (qui gere toast + erreur). 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()
const ok = await submitDelete(categoryToDelete.value.id)
if (ok) {
deleteModalOpen.value = false
categoryToDelete.value = null
drawerOpen.value = false
await fetchAll()
}
} finally {
deleting.value = false
}
}
function onCategorySaved() {
loadCategories()
fetchAll()
}
// Chargement initial des deux ressources (liste + referentiel des types).
// Le referentiel est pre-charge ici (et pas dans le drawer) pour que le
// select soit pret au moment ou l'utilisateur clique sur « + Ajouter ».
onMounted(() => {
loadCategories()
fetchAll()
fetchTypes()
})
</script>