From 3ce40a707f0cad7f8682f956321989a14e82cad4 Mon Sep 17 00:00:00 2001 From: tristan Date: Fri, 29 May 2026 08:59:47 +0000 Subject: [PATCH] =?UTF-8?q?[ERP-49]=20Cr=C3=A9er=20la=20page=20Gestion=20d?= =?UTF-8?q?es=20cat=C3=A9gories=20(datatable=20+=20drawer)=20(#22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Contexte Ticket Lesstime : [#49](https://lesstime.malio.fr/tasks/460) — premier ticket front du M0 (Gestion des catégories). Suit la chaîne back ERP-43..48 mergée sur develop. ## Contenu first draft (Claude Code) - Page Nuxt `/admin/categories` (`MalioDataTable` + bouton `+ Ajouter`) - Composant `` : modes création / consultation / édition, transition auto view → edit à la première modification, validation client miroir RG-1.02 (name requis) / RG-1.04 (longueur 2-120) / RG-1.05 (type requis), mapping erreurs 409 (doublon) et 422 (violations) - Composant `` : confirmation suppression (soft delete RG-1.12) - Types TS `Category`, `CategoryType`, `User` - i18n `admin.categories.*` ajouté dans `fr.json` - Fix latent en passant : ajout de `'categories'` à `AdminLinkSlug` du Page Object e2e (oublié lors d'ERP-47 quand l'item sidebar a été ajouté) ## Décisions marquantes - Logique `fetch` inline dans `categories.vue` (sera extraite en composables `useCategoriesAdmin` + `useCategoryForm` au ticket ERP-50 / 0.8) - Drawer dans composant séparé pour réutilisabilité - Aucun état de tableau persisté dans l'URL (règle ABSOLUE n°6) - Tous les composants formulaires sont `Malio*` (`MalioDataTable`, `MalioInputText`, `MalioSelect`, `MalioButton`, `MalioDrawer`) ## Polish à venir (Tristan) Tristan testera en navigateur et peaufinera : UX, classes Tailwind, animations, icônes, wording de toasts. Les commits de polish suivront sur la même branche. ## Tests - `npx nuxi typecheck` : net 0 nouvelle erreur (mêmes erreurs pré-existantes que sur `develop`, infrastructure auto-import) + 1 latente corrigée (AdminLinkSlug) - `make nuxt-test` : 43/43 passent (0 régression) - Tests manuels navigateur : voir cahier de test du ticket Lesstime #49 ## Note pre-commit hook Le hook a remonté un échec PHPUnit pré-existant sur `develop` (`CategoryDeleteTest::testPatchOnSoftDeletedReturns404` → 401 au lieu de 404, JWT non initialisé en test runner). Aucun PHP touché dans cette MR. Commit avec `--no-verify` autorisé par Tristan. ## Reviewer suggéré Matthieu (back ↔ front + permissions). --------- Co-authored-by: Matthieu Reviewed-on: https://gitea.malio.fr/MALIO-DEV/Starseed/pulls/22 Co-authored-by: tristan Co-committed-by: tristan --- .gitea/workflows/pull-request.yml | 8 + frontend/i18n/locales/fr.json | 33 ++ .../components/CategoryDeleteModal.vue | 48 +++ .../catalog/components/CategoryDrawer.vue | 370 ++++++++++++++++++ frontend/modules/catalog/nuxt.config.ts | 1 + .../catalog/pages/admin/categories.vue | 167 ++++++++ frontend/modules/catalog/types/category.ts | 71 ++++ .../e2e/helpers/pages/SidebarComponent.ts | 2 +- 8 files changed, 699 insertions(+), 1 deletion(-) create mode 100644 frontend/modules/catalog/components/CategoryDeleteModal.vue create mode 100644 frontend/modules/catalog/components/CategoryDrawer.vue create mode 100644 frontend/modules/catalog/nuxt.config.ts create mode 100644 frontend/modules/catalog/pages/admin/categories.vue create mode 100644 frontend/modules/catalog/types/category.ts diff --git a/.gitea/workflows/pull-request.yml b/.gitea/workflows/pull-request.yml index c161627..c0f9425 100644 --- a/.gitea/workflows/pull-request.yml +++ b/.gitea/workflows/pull-request.yml @@ -73,12 +73,20 @@ jobs: run: vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --allow-risky=yes --dry-run --diff - name: Bootstrap test database + # Aligne sur la cible `test-db-setup` du makefile : apres + # `schema:update --force`, on RECREE manuellement l'index unique + # partiel `uq_category_name_type_active` car Doctrine ORM ne sait + # pas exprimer les index fonctionnels partiels (LOWER(name) + WHERE + # deleted_at IS NULL) et `schema:update` les considere comme + # orphelins et les DROP — collisions non detectees, tests d'unicite + # qui attendent 409 recoivent 201. run: | php bin/console doctrine:database:create --env=test --if-not-exists --no-interaction php bin/console doctrine:migrations:migrate --env=test --no-interaction php bin/console doctrine:schema:update --env=test --force --no-interaction php bin/console doctrine:fixtures:load --env=test --no-interaction php bin/console app:sync-permissions --env=test --no-interaction + php bin/console --env=test dbal:run-sql "CREATE UNIQUE INDEX IF NOT EXISTS uq_category_name_type_active ON category (LOWER(name), category_type_id) WHERE deleted_at IS NULL" - name: Run PHPUnit run: php -d memory_limit=512M vendor/bin/phpunit diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index a71d4db..77849a7 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -230,6 +230,39 @@ "updated": "Site mis à jour avec succès", "deleted": "Site supprimé avec succès" } + }, + "categories": { + "title": "Gestion des catégories", + "newCategory": "Ajouter", + "editCategory": "Modifier la catégorie", + "createCategory": "Créer une catégorie", + "viewCategory": "Détail de la catégorie", + "noCategories": "Aucune catégorie pour l'instant.", + "table": { + "name": "Nom", + "type": "Type" + }, + "form": { + "name": "Nom", + "type": "Type de catégorie", + "typePlaceholder": "Sélectionner un type" + }, + "validation": { + "nameRequired": "Le nom est obligatoire.", + "nameLength": "Le nom doit faire entre 2 et 120 caractères.", + "typeRequired": "Le type de catégorie est obligatoire." + }, + "delete": { + "title": "Supprimer la catégorie", + "message": "Êtes-vous sûr de vouloir supprimer la catégorie \"{name}\" ? Cette action est irréversible." + }, + "toast": { + "created": "Catégorie créée avec succès", + "updated": "Catégorie mise à jour avec succès", + "deleted": "Catégorie supprimée avec succès", + "duplicate": "Une catégorie nommée « {name} » existe déjà pour ce type.", + "typesLoadFailed": "Impossible de charger les types de catégorie. Réessayez." + } } } } diff --git a/frontend/modules/catalog/components/CategoryDeleteModal.vue b/frontend/modules/catalog/components/CategoryDeleteModal.vue new file mode 100644 index 0000000..5842904 --- /dev/null +++ b/frontend/modules/catalog/components/CategoryDeleteModal.vue @@ -0,0 +1,48 @@ + + + diff --git a/frontend/modules/catalog/components/CategoryDrawer.vue b/frontend/modules/catalog/components/CategoryDrawer.vue new file mode 100644 index 0000000..73991f0 --- /dev/null +++ b/frontend/modules/catalog/components/CategoryDrawer.vue @@ -0,0 +1,370 @@ + + + diff --git a/frontend/modules/catalog/nuxt.config.ts b/frontend/modules/catalog/nuxt.config.ts new file mode 100644 index 0000000..268da7f --- /dev/null +++ b/frontend/modules/catalog/nuxt.config.ts @@ -0,0 +1 @@ +export default defineNuxtConfig({}) diff --git a/frontend/modules/catalog/pages/admin/categories.vue b/frontend/modules/catalog/pages/admin/categories.vue new file mode 100644 index 0000000..482bce8 --- /dev/null +++ b/frontend/modules/catalog/pages/admin/categories.vue @@ -0,0 +1,167 @@ + + + diff --git a/frontend/modules/catalog/types/category.ts b/frontend/modules/catalog/types/category.ts new file mode 100644 index 0000000..acb154d --- /dev/null +++ b/frontend/modules/catalog/types/category.ts @@ -0,0 +1,71 @@ +/** + * Types front du module Catalog (M0 — Gestion des categories). + * + * Contrats API consommes : + * - GET /api/categories → HydraCollection + * - GET /api/categories/{id} → Category + * - POST /api/categories → body { name, categoryType: IRI } + * - PATCH /api/categories/{id} → body partiel { name?, categoryType?: IRI } + * - DELETE /api/categories/{id} → 204 (soft delete via CategoryProcessor) + * - GET /api/category_types → HydraCollection + * + * Notes : + * - Les IRI sont envoyes en POST/PATCH (ex. "/api/category_types/3"). + * - `categoryType` est embarque (groupe Serializer `category:read` sur les + * proprietes de CategoryType, cf. spec-back § 3.4). + * - `createdBy` / `updatedBy` peuvent etre `null` (hors contexte HTTP, + * ON DELETE SET NULL en BDD). Affichage : libelle "Systeme" si null. + */ + +/** + * Reference legere d'un user, telle qu'embarquee dans Category.createdBy / + * updatedBy. Volontairement minimaliste : on n'a besoin que de l'identifiant + * et de l'username pour l'affichage courant. + */ +export interface User { + id: number + username: string +} + +/** + * Reference du referentiel CategoryType (lecture seule au M0). + */ +export interface CategoryType { + id: number + code: string + label: string +} + +/** + * Categorie metier — telle qu'elle est lue depuis l'API. L'entite porte le + * pattern Timestampable+Blamable (cf. spec-back § 2.8). + */ +export interface Category { + id: number + name: string + categoryType: CategoryType + /** Soft delete : null = active, valeur = supprimee logiquement le {date}. */ + deletedAt: string | null + createdAt: string + updatedAt: string + createdBy: User | null + updatedBy: User | null +} + +/** + * Payload accepte en POST /api/categories. `categoryType` est envoye en + * IRI Hydra (ex. `/api/category_types/3`). + */ +export interface CategoryCreateInput { + name: string + categoryType: string +} + +/** + * Payload accepte en PATCH /api/categories/{id}. Tous les champs sont + * optionnels (modification partielle). + */ +export interface CategoryUpdateInput { + name?: string + categoryType?: string +} diff --git a/frontend/tests/e2e/helpers/pages/SidebarComponent.ts b/frontend/tests/e2e/helpers/pages/SidebarComponent.ts index 9bdae9e..9e7a130 100644 --- a/frontend/tests/e2e/helpers/pages/SidebarComponent.ts +++ b/frontend/tests/e2e/helpers/pages/SidebarComponent.ts @@ -1,6 +1,6 @@ import type { Locator, Page } from '@playwright/test' -export type AdminLinkSlug = 'users' | 'roles' | 'sites' | 'audit-log' +export type AdminLinkSlug = 'users' | 'roles' | 'sites' | 'categories' | 'audit-log' /** * Page Object de la sidebar (MalioSidebar), scope sur les items "admin".