feat(permissions) : add role-based UI guards and readonly mode for viewers

- Add usePermissions composable (isAdmin, canEdit, canView)
- Password-protected profile login with modal on profiles page
- Disable all form fields for ROLE_VIEWER across edit/create pages
- Show navigation buttons (Modifier/Consulter) for all roles, hide delete for viewers
- Add readonly prop to ModelTypeForm for category pages
- Disable modal fields (sites, constructeurs) for viewers
- Guard /admin routes in middleware

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-02-26 13:36:42 +01:00
parent 6bed715b7f
commit cc70fe2b29
46 changed files with 946 additions and 423 deletions

229
migration.md Normal file
View File

@@ -0,0 +1,229 @@
# Plan de migration — Réduction de code frontend
> Objectif : réduire ~5 700 LOC sans modifier le fonctionnel.
> Branche : à partir de `refacto/F1-decoupage-mega-composants`
> Statut global : **EN ATTENTE**
---
## Phase 1 — Pages catalogue (3 pages, ~1 200 LOC → ~350 LOC)
### M1.1 · Composant générique `CatalogPage.vue`
- **Motif** : `component-catalog.vue` (348 LOC), `pieces-catalog.vue` (463 LOC) et `product-catalog.vue` (408 LOC) partagent 95 % de structure (recherche, tri, pagination, tableau, suppression, états vides/loading).
- **Différences isolées** : colonnes du tableau, garde de suppression, extraction fournisseur.
- **Plan** :
1. Créer `app/components/common/CatalogPage.vue` acceptant :
- `columns: ColumnDef[]` (nom, clé, slot optionnel)
- `fetchFn: (params) => Promise<PaginatedResult>`
- `deleteFn: (id) => Promise<Result>`
- `deleteGuard?: (item) => string | null` (message bloquant ou null)
- `entityLabel: string`, `createRoute: string`
- Slots nommés pour colonnes custom (`#col-supplier`, etc.)
2. Extraire `supplierDisplayUtils.ts` (pattern `MAX_VISIBLE_SUPPLIERS` dupliqué dans pieces-catalog et product-catalog).
3. Réduire chaque page catalogue à ~80 LOC (config + slots custom).
- **Gain estimé** : ~850 LOC
- **Statut** : `[ ]`
---
## Phase 2 — Composables CRUD génériques (~1 170 LOC → ~400 LOC)
### M2.1 · Factory `useEntityCRUD<T>(config)`
- **Motif** : `usePieces.ts` (240), `useProducts.ts` (305), `useComposants.ts` (231), `useSites.ts` (124) suivent le même pattern CRUD : refs `loading/loaded/error`, `loadItems()` paginé, `create/update/delete` avec mise à jour cache + toast.
- **Différences isolées** : endpoint, normaliseur, enrichissement constructeurs, champs de tri.
- **Plan** :
1. Créer `app/composables/useEntityCRUD.ts` :
```ts
interface EntityCRUDConfig {
endpoint: string
label: string
normalizer?: (item: any) => any
enricher?: (item: any) => Promise<any>
defaultSort?: { field: string; dir: 'asc' | 'desc' }
}
export function useEntityCRUD(config: EntityCRUDConfig)
```
2. Extraire `extractTotal()` dans `apiHelpers.ts` (dupliqué 3×, ~10 LOC chacun).
3. Extraire `buildPaginatedQuery(options)` dans `apiHelpers.ts` (dupliqué 3×, ~15 LOC chacun).
4. Extraire pattern `withResolvedConstructeurs()` dans `useEntityEnricher.ts` (dupliqué 3× dans pieces/products/composants, ~50 LOC chacun).
5. Réduire chaque composable à un appel de factory + méthodes spécifiques.
6. Garder `useMachines.ts` séparé (méthodes spéciales : `reconfigureSkeleton`, `createMachineFromType`).
- **Gain estimé** : ~770 LOC
- **Statut** : `[ ]`
### M2.2 · Helper `withLoadingState()`
- **Motif** : pattern `loading.value = true; try { ... } finally { loading.value = false }` répété 10+ fois dans les composables CRUD.
- **Plan** : créer `app/composables/useLoadingHelper.ts` exportant :
```ts
async function withLoadingState<T>(loading: Ref<boolean>, fn: () => Promise<T>): Promise<T>
```
- **Gain estimé** : ~100 LOC
- **Statut** : `[ ]`
### M2.3 · Fusion `usePersistedValue` + `usePersistedSort`
- **Motif** : même pattern `useCookie()` + `watch()` + JSON parse/stringify.
- **Plan** : fusionner en `usePersistedState<T>(key, fallback, prefix?)`.
- **Gain estimé** : ~30 LOC
- **Statut** : `[ ]`
---
## Phase 3 — Pages edit entités (~2 750 LOC → ~1 200 LOC)
### M3.1 · Composant `HistorySection.vue`
- **Motif** : bloc historique identique (loading/error/empty + itération entries) dans `component/[id]/edit.vue` (L437-503), `pieces/[id]/edit.vue` (L384-450), `product/[id]/edit.vue` (L304-370) — ~67 LOC × 3.
- **Plan** : créer `app/components/common/HistorySection.vue` avec props `entries`, `loading`, `error`.
- **Gain estimé** : ~130 LOC
- **Statut** : `[ ]`
### M3.2 · Composant `DocumentsSection.vue`
- **Motif** : bloc document (upload, liste, preview, download, delete) dupliqué dans les 3 pages edit + `MachineDocumentsCard.vue` + `SiteEditModal.vue` — ~70-180 LOC × 5.
- **Plan** : créer `app/components/common/DocumentsSection.vue` avec props `documents`, `entityId`, `entityType` et events `upload`, `delete`, `preview`.
- **Gain estimé** : ~400 LOC
- **Statut** : `[ ]`
### M3.3 · Composable `useEntityEditForm(config)`
- **Motif** : les 3 pages edit partagent : chargement entité + types + constructeurs, gestion champs custom, normalisation payload, sauvegarde, gestion erreur.
- **Différences** : component a structure display, piece a product selection, product est plus simple.
- **Plan** :
1. Créer `app/composables/useEntityEditForm.ts` gérant le cycle de vie commun (load, save, custom fields sync).
2. Chaque page edit ne garde que ses spécificités.
- **Gain estimé** : ~500 LOC
- **Statut** : `[ ]`
### M3.4 · Réutilisation `customFieldFormUtils.ts` dans `component/create.vue`
- **Motif** : `component/create.vue` (1 266 LOC) réimplémente `resolveFieldName`, `resolveFieldType`, `resolveDefaultValue` déjà dans `customFieldFormUtils.ts`. Aussi 3 fonctions `resolveXxxLabel` quasi-identiques (~18 LOC × 3).
- **Plan** :
1. Remplacer les fonctions locales par les imports de `customFieldFormUtils.ts`.
2. Créer `resolveTypeLabel(entity, typeField, labelField, fallback)` générique.
- **Gain estimé** : ~120 LOC
- **Statut** : `[ ]`
---
## Phase 4 — Décomposition `useMachineDetailData.ts` (1 410 LOC → ~500 LOC)
### M4.1 · Extraire `useMachineDocuments.ts`
- **Motif** : gestion documents (upload, delete, preview, refresh) = ~200 LOC dans le composable monolithique.
- **Gain estimé** : ~150 LOC (après factorisation avec DocumentsSection)
- **Statut** : `[ ]`
### M4.2 · Extraire `useMachineConstructeurs.ts`
- **Motif** : résolution constructeurs avec chaînes de fallback 4 niveaux, `uniqueConstructeurIds`, `resolveConstructeurs` = ~80 LOC.
- **Gain estimé** : ~60 LOC
- **Statut** : `[ ]`
### M4.3 · Fusionner `transformCustomFields` et `transformComponentCustomFields`
- **Motif** : L303-405 et L407-514 — logique quasi-identique de transformation des champs custom, seule la source (machine vs composant) diffère.
- **Plan** : créer `transformEntityCustomFields(entity, fieldSource, config)` paramétrable.
- **Gain estimé** : ~100 LOC
- **Statut** : `[ ]`
### M4.4 · Extraire groupement de requirements
- **Motif** : `componentRequirementGroups`, `pieceRequirementGroups` = computed complexes avec construction de maps et filtres répétitifs.
- **Gain estimé** : ~80 LOC
- **Statut** : `[ ]`
---
## Phase 5 — `StructureNodeEditor.vue` (1 167 LOC → ~600 LOC)
### M5.1 · Composable `useDragDrop.ts`
- **Motif** : 4 handlers drag-drop quasi-identiques (custom fields, pièces, produits, sous-composants) avec chacun `draggingIndex`, `dropTargetIndex`, `reorderClass()`, `handleDragStart/Over/End`.
- **Plan** : créer `useDragDrop<T>(items: Ref<T[]>)` retournant `{ dragging, target, reorderClass, onDragStart, onDragOver, onDragEnd, onDrop }`.
- **Gain estimé** : ~350 LOC
- **Statut** : `[ ]`
### M5.2 · Extraire validation noeud
- **Motif** : `isAssignmentNodeComplete` + logique de validation dispersée.
- **Plan** : déplacer vers `app/shared/utils/structureValidation.ts`.
- **Gain estimé** : ~40 LOC
- **Statut** : `[ ]`
---
## Phase 6 — Micro-duplications restantes (du `micro-dup-report.md`)
### M6.1 · `useControlledModel.ts` (MDUP-004)
- **Motif** : `computed({ get, set })` pour transiter `v-model` entre props et emits — dupliqué dans 6 composants.
- **Gain estimé** : ~60 LOC
- **Statut** : `[ ]`
### M6.2 · `ModalShell.vue` (MDUP-008) + `ModalActions.vue` (MDUP-007)
- **Motif** : squelette de modale DaisyUI (`.modal` + `.modal-box` + titre + footer) dupliqué dans 4+ composants. Pieds de modale « Annuler + Primaire + spinner » dupliqués 5×.
- **Gain estimé** : ~120 LOC
- **Statut** : `[ ]`
### M6.3 · `LoadingButton.vue` (MDUP-010) + `FieldText.vue` (MDUP-009)
- **Motif** : bouton primaire avec spinner (3 occurrences), champ texte simple label+input (5 occurrences).
- **Gain estimé** : ~80 LOC
- **Statut** : `[ ]`
### M6.4 · `createRequirementDefaults` + `useEnsureOptionsLoaded` (MDUP-005, MDUP-006)
- **Motif** : factory de requirement par défaut + `onMounted` identiques dans les sections composant/pièce.
- **Gain estimé** : ~30 LOC
- **Statut** : `[ ]`
---
## Phase 7 — Consolidation custom fields (~1 150 LOC → ~800 LOC)
### M7.1 · Fusionner logique de résolution dans `customFieldUtils.ts`
- **Motif** : `customFieldUtils.ts` (440), `entityCustomFieldLogic.ts` (349), `customFieldFormUtils.ts` (367) contiennent des fonctions de résolution de champs qui se chevauchent (`resolveFieldId`, `resolveFieldName`, génération de clé, déduplication).
- **Plan** : consolider les fonctions dupliquées en gardant la séparation thématique (utils / form / entity) mais en partageant les primitives.
- **Gain estimé** : ~150 LOC
- **Statut** : `[ ]`
---
## Récapitulatif
| Phase | Cible | LOC avant | Gain estimé | Priorité |
|-------|-------|-----------|-------------|----------|
| **P1** | Pages catalogue | ~1 220 | ~850 | Haute |
| **P2** | Composables CRUD | ~1 170 | ~900 | Haute |
| **P3** | Pages edit entités | ~2 750 | ~1 150 | Haute |
| **P4** | useMachineDetailData | ~1 410 | ~390 | Moyenne |
| **P5** | StructureNodeEditor | ~1 167 | ~390 | Moyenne |
| **P6** | Micro-duplications | ~400 | ~290 | Basse |
| **P7** | Custom fields utils | ~1 150 | ~150 | Basse |
| | **Total** | | **~4 120 LOC** | |
### Ordre recommandé
1. **P2** (CRUD generics) — fondation pour P1 et P3
2. **P1** (catalogues) — dépend de P2 pour les fetch functions
3. **P3** (pages edit) — plus gros gain absolu, dépend partiellement de P2
4. **P5** (drag-drop) — indépendant, quick win
5. **P4** (machine detail) — complexe mais fort impact
6. **P6** (micro-dup) — petits gains, faible risque
7. **P7** (custom fields) — délicat, à faire en dernier
### Vérification après chaque phase
```bash
cd Inventory_frontend
npx nuxi typecheck # 0 erreurs
npm run lint:fix # 0 erreurs
npm run build # succès
npx vitest run # 54+ tests pass
```