# Guide Frontend — Inventory Guide complet du frontend Nuxt/Vue pour comprendre comment tout fonctionne, même si tu débutes. ## Table des matières 1. [Vue d'ensemble](#vue-densemble) 2. [Nuxt et Vue — les bases](#nuxt-et-vue--les-bases) 3. [L'architecture de l'application](#larchitecture-de-lapplication) 4. [Les Pages (le routing)](#les-pages) 5. [Les Composables (la logique métier)](#les-composables) 6. [Les Composants (l'interface)](#les-composants) 7. [L'API et les appels HTTP](#lapi-et-les-appels-http) 8. [L'authentification côté frontend](#lauthentification-côté-frontend) 9. [Le style (TailwindCSS + DaisyUI)](#le-style) 10. [Les utilitaires et types](#les-utilitaires-et-types) 11. [Flux complet d'une fonctionnalité](#flux-complet-dune-fonctionnalité) 12. [Patterns et conventions du projet](#patterns-et-conventions) 13. [Les tests](#les-tests) 14. [Commandes utiles](#commandes-utiles) --- ## Vue d'ensemble Le frontend est une **SPA** (Single Page Application) construite avec : - **Nuxt 4** : framework basé sur Vue.js qui ajoute le routing automatique, les composables, et plein d'outils - **Vue 3** : la librairie d'interface (composants, réactivité, etc.) - **TypeScript** : JavaScript avec des types (réduit les bugs) - **TailwindCSS 4** : des classes CSS utilitaires (`flex`, `p-4`, `text-lg`, etc.) - **DaisyUI 5** : des composants visuels prêts à l'emploi (boutons, modales, tableaux, etc.) ### SPA, c'est quoi ? Dans une SPA, le navigateur charge **une seule page HTML** au départ, puis tout se passe en JavaScript : la navigation entre les pages, le chargement de données, etc. Pas de rechargement complet de la page. > **SSR est désactivé** (`ssr: false` dans `nuxt.config.ts`). Le rendu se fait entièrement côté client (dans le navigateur). --- ## Nuxt et Vue — les bases ### Vue 3 en 30 secondes Vue utilise des **composants** : des blocs réutilisables qui combinent HTML, JavaScript et CSS. ```vue ``` ### Les concepts clés de Vue 3 | Concept | Syntaxe | Description | |---------|---------|-------------| | **ref** | `const x = ref(0)` | Variable réactive. Quand elle change, l'interface se met à jour | | **computed** | `const y = computed(() => x.value * 2)` | Valeur calculée, se recalcule automatiquement | | **v-model** | `` | Lie un input à une variable (bidirectionnel) | | **v-if** | `
` | Afficher conditionnellement | | **v-for** | `
` | Boucler sur une liste | | **@event** | ` //
Section admin
``` #### useMachines.ts — CRUD machines Pattern typique : un composable par domaine métier. ```typescript const { machines, // ref — la liste des machines loading, // ref — en cours de chargement ? loaded, // ref — déjà chargé ? loadMachines, // () => Promise createMachine, // (data) => Promise updateMachine, // (id, data) => Promise deleteMachine, // (id) => Promise cloneMachine, // (sourceId, data) => Promise } = useMachines() ``` Ce pattern se répète pour `useComposants`, `usePieces`, `useProducts`, `useConstructeurs`, `useSites`, etc. #### useDataTable.ts — Table de données générique Gère la pagination, le tri, la recherche et les filtres pour toutes les pages catalogue : ```typescript const { sort, // { field: 'name', direction: 'asc' } pagination, // { currentPage: 1, totalPages: 5, perPage: 30, ... } handleSort, // (field) => void handlePageChange, // (page) => void handlePerPageChange, // (perPage) => void } = useDataTable({ ... }) ``` #### useConfirm.ts — Modale de confirmation ```typescript const { confirm } = useConfirm() // Afficher une modale et attendre la réponse const ok = await confirm({ title: 'Supprimer la machine ?', message: 'Cette action est irréversible.', }) if (ok) { // l'utilisateur a cliqué "Confirmer" } ``` #### useToast.ts — Notifications ```typescript const { showSuccess, showError, showWarning, showInfo } = useToast() showSuccess('Machine créée avec succès') showError('Erreur lors de la suppression') ``` #### useMachineDetailData.ts — Le composable le plus complexe Gère toute la logique de la page de détail d'une machine : - Chargement des données (machine, composants, pièces, produits, documents, custom fields) - Mode édition (toggle) - Mise à jour des champs - Ajout/suppression de liens (composant, pièce, produit) - Upload/suppression de documents #### useDocuments.ts — Gestion de fichiers ```typescript const { uploadDocuments, deleteDocument, loadDocumentsByMachine } = useDocuments() // Upload de fichiers await uploadDocuments('machine', machineId, fileList) // Suppression await deleteDocument(documentId) ``` #### useEntityHistory.ts — Historique d'audit ```typescript const { history, loading, loadHistory } = useEntityHistory() await loadHistory('machine', machineId) // history.value → liste des événements d'audit ``` ### Pattern des composables (convention du projet) Certains composables utilisent une injection de dépendances explicite : ```typescript // Définir les dépendances nécessaires interface Deps { machineId: Ref onSave: () => void } // Le composable reçoit ses dépendances export function useMachineDetail(deps: Deps) { const { machineId, onSave } = deps // ... logique utilisant machineId et onSave } ``` --- ## Les Composants ### Auto-import par Nuxt Tous les composants dans `components/` sont auto-importés. Pas besoin de `import` : ```vue ``` La convention de nommage utilise le chemin du dossier : - `components/common/DataTable.vue` → `` - `components/machine/InfoCard.vue` → `` - `components/layout/AppNavbar.vue` → `` ### Les composants communs (réutilisables) #### CommonDataTable Le composant central pour tous les tableaux avec pagination, tri et recherche : ```vue ``` #### CommonConfirmModal Modale de confirmation globale (utilisée via `useConfirm()`) : ```vue ``` #### CommonSearchSelect Dropdown avec recherche intégrée : ```vue ``` #### CommonPagination Composant de pagination : ```vue ``` ### Les composants machine | Composant | Rôle | |-----------|------| | `MachineDetailHeader` | En-tête avec boutons (éditer, imprimer) | | `MachineInfoCard` | Carte avec les infos de la machine (nom, référence, constructeur) | | `MachineDocumentsCard` | Upload et prévisualisation des documents | | `MachineComponentsCard` | Liste des composants liés (hiérarchique) | | `MachinePiecesCard` | Liste des pièces liées | | `MachineProductsCard` | Liste des produits liés | | `AddEntityToMachineModal` | Modale pour ajouter un composant/pièce/produit | ### Les composants sites | Composant | Rôle | |-----------|------| | `SiteCard` | Carte d'affichage d'un site | | `SiteCreateModal` | Modale de création de site | | `SiteEditModal` | Modale d'édition de site | | `SiteContactFormFields` | Champs de formulaire contact (réutilisable) | ### Les composants model-types | Composant | Rôle | |-----------|------| | `ManagementView` | Page principale de gestion des types | | `Table` | Tableau des types/catégories | | `Toolbar` | Barre de recherche + filtres | | `ModelTypeForm` | Formulaire d'édition/création | | `ConversionModal` | Modale de conversion de catégorie | ### Communication entre composants Le projet utilise **exclusivement Props + Events** (pas de `provide/inject`) : ```vue ``` --- ## L'API et les appels HTTP ### Le composable useApi.ts Tous les appels API passent par `useApi.ts`. Voici ce qu'il fait : 1. **Ajoute le base URL** : `/machines` → `http://localhost:8081/api/machines` 2. **Ajoute les headers** : - `Content-Type: application/ld+json` (POST/PUT) - `Content-Type: application/merge-patch+json` (PATCH) 3. **Inclut les cookies** : `credentials: 'include'` 4. **Gère les erreurs** : parse les erreurs backend et les traduit en français 5. **Retourne un objet standardisé** : `{ success, data, error, status }` ### Les IRIs (Internationalized Resource Identifiers) L'API utilise des IRIs pour les relations. Au lieu de passer un simple ID, on passe le chemin complet : ```typescript // ❌ Ne pas faire { "site": "cl9z8y7x..." } // ✅ Faire { "site": "/api/sites/cl9z8y7x..." } ``` Le fichier `shared/utils/apiRelations.ts` fournit des helpers : ```typescript import { toIri, extractRelationId, normalizeRelationIds } from '~/shared/utils/apiRelations' // Construire un IRI toIri('sites', 'cl9z8y7x...') // → "/api/sites/cl9z8y7x..." // Extraire l'ID d'un IRI extractRelationId('/api/sites/cl9z8y7x...') // → "cl9z8y7x..." // Convertir les IDs locaux en IRIs dans un payload normalizeRelationIds({ siteId: 'cl9z8y7x...', constructeurIds: ['cl111...', 'cl222...'], }) // → { site: "/api/sites/cl9z8y7x...", constructeurs: ["/api/constructeurs/cl111...", ...] } ``` ### Les collections API (pagination) Les réponses de collection suivent le format Hydra : ```json { "hydra:totalItems": 42, "hydra:member": [ { "id": "cl...", "name": "Machine 1" }, { "id": "cl...", "name": "Machine 2" } ] } ``` Le helper `extractCollection()` gère les différents formats : ```typescript import { extractCollection } from '~/shared/utils/apiHelpers' const result = await api.get('/machines?page=1') const machines = extractCollection(result.data) // → Machine[] ``` ### Les Content-Types | Opération | Content-Type | Quand | |-----------|-------------|-------| | POST | `application/ld+json` | Créer une ressource | | PUT | `application/ld+json` | Remplacer une ressource | | PATCH | `application/merge-patch+json` | Modifier partiellement | | Upload fichier | `multipart/form-data` | Envoyer un fichier | --- ## L'authentification côté frontend ### Le flux complet ``` 1. L'utilisateur ouvre l'application ↓ 2. Le middleware profile.global.ts s'exécute ↓ 3. Il appelle ensureSession() → GET /api/session/profile ↓ 4a. Si la session est valide → l'utilisateur voit la page demandée 4b. Si pas de session → redirection vers /profiles (page de login) ↓ 5. L'utilisateur choisit son profil et entre son mot de passe ↓ 6. POST /api/session/profile → le backend crée la session ↓ 7. Redirection vers la page d'accueil ``` ### Le middleware global ```typescript // middleware/profile.global.ts export default defineNuxtRouteMiddleware(async (to) => { // Ne pas vérifier la session si on est déjà sur la page de login if (to.path === '/profiles') return const { ensureSession, activeProfile } = useProfileSession() await ensureSession() // Pas de session → page de login if (!activeProfile.value) { return navigateTo('/profiles') } // Routes admin → vérifier le rôle if (to.path.startsWith('/admin') && !isAdmin.value) { return navigateTo('/') } }) ``` ### Les permissions dans les templates ```vue ``` --- ## Le style ### TailwindCSS 4 TailwindCSS utilise des classes utilitaires au lieu de CSS personnalisé : ```html
``` **Classes les plus utilisées :** | Catégorie | Classes | Description | |-----------|---------|-------------| | Layout | `flex`, `grid`, `block`, `hidden` | Type d'affichage | | Espacement | `p-4`, `px-2`, `py-6`, `m-2`, `gap-4` | Padding, margin, gap | | Taille | `w-full`, `h-10`, `max-w-lg`, `min-h-screen` | Largeur, hauteur | | Texte | `text-lg`, `text-sm`, `font-bold`, `text-gray-500` | Taille, poids, couleur | | Fond | `bg-white`, `bg-blue-100`, `bg-base-200` | Couleur de fond | | Bordure | `border`, `rounded-lg`, `border-gray-300` | Bordures | | Responsive | `md:flex`, `lg:hidden`, `sm:p-2` | Styles conditionnels par taille d'écran | ### DaisyUI 5 DaisyUI ajoute des classes de composants par-dessus Tailwind : ```html

Titre

Contenu

Actif
NomEmail
Jeanjean@mail.com
``` ### Les icônes Lucide Le projet utilise les icônes Lucide via `unplugin-icons`. Elles sont auto-importées avec le préfixe `Icon` : ```vue ``` --- ## Les utilitaires et types ### Types TypeScript (`shared/types/`) Les interfaces définissent la forme des données : ```typescript // shared/types/inventory.ts interface Machine { id: string name: string reference: string prix: string | null site: string // IRI du site constructeurs: string[] // IRIs des constructeurs createdAt: string updatedAt: string } interface Site { id: string name: string contactName: string | null contactPhone: string | null contactAddress: string | null contactPostalCode: string | null contactCity: string | null } // Types de colonnes pour DataTable interface DataTableSort { field: string direction: 'asc' | 'desc' } interface DataTablePagination { currentPage: number totalPages: number totalItems: number perPage: number } ``` ### Utilitaires (`shared/utils/`) | Fichier | Rôle | |---------|------| | `apiRelations.ts` | Conversion ID ↔ IRI pour les relations API | | `apiHelpers.ts` | Extraction de collections API (hydra) | | `errorMessages.ts` | Traduction des erreurs backend en français | | `customFieldUtils.ts` | Formatage et logique des champs personnalisés | | `documentDisplayUtils.ts` | Prévisualisation, icônes, tailles de fichiers | | `productDisplayUtils.ts` | Affichage produit (référence, prix, fournisseurs) | | `deleteImpactUtils.ts` | Analyse d'impact avant suppression | | `historyDisplayUtils.ts` | Formatage du journal d'audit | ### Validation (`shared/validation/`) ```typescript // shared/validation/email.ts export function isValidEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) } // shared/validation/phone.ts export function isValidPhone(phone: string): boolean { return /^[\d\s\-+().]{6,20}$/.test(phone) } ``` --- ## Flux complet d'une fonctionnalité ### Exemple : afficher et modifier une machine ``` 1. L'utilisateur clique sur une machine dans le catalogue → navigateTo('/machine/cl1a2b3c...') 2. Le middleware profile.global.ts vérifie la session → OK 3. La page machine/[id].vue se monte : const route = useRoute() const machineId = route.params.id // → "cl1a2b3c..." 4. Le composable useMachineDetailData(machineId) est appelé : → GET /api/machines/cl1a2b3c... → données de la machine → GET /api/machines/cl1a2b3c.../structure → composants, pièces, produits → GET /api/documents/by-machine/cl1a2b3c... → documents → GET /api/custom-field-values?machine=... → champs personnalisés 5. Les données arrivent dans des refs réactifs : machine.value = { id: "cl1a2b3c...", name: "CNC 01", ... } components.value = [...] documents.value = [...] 6. Le template affiche les composants : 7. L'utilisateur clique sur "Modifier" → isEditing = true → Les champs deviennent éditables (inputs au lieu de texte) 8. L'utilisateur modifie le nom et clique "Sauvegarder" → handleSave({ name: "CNC 02" }) → PATCH /api/machines/cl1a2b3c... { "name": "CNC 02" } → Toast "Machine mise à jour" → machine.value.name = "CNC 02" → l'interface se met à jour ``` --- ## Patterns et conventions ### 1. Un composable par domaine ``` useMachines.ts → CRUD machines useComposants.ts → CRUD composants usePieces.ts → CRUD pièces useProducts.ts → CRUD produits useSites.ts → CRUD sites useConstructeurs.ts → CRUD constructeurs ``` ### 2. Props + Events, jamais provide/inject ```vue ``` ### 3. État dans les composables, pas dans les composants ```typescript // ✅ La logique est dans le composable const { machines, loadMachines } = useMachines() // ❌ Ne pas faire d'appels API directement dans le template/composant ``` ### 4. Pas de Pinia/Vuex Le projet utilise des refs dans les composables au lieu d'un store global. C'est plus simple et suffisant pour cette application. ### 5. Nommage | Type | Convention | Exemple | |------|------------|---------| | Composable | `use` + PascalCase | `useMachines`, `useProfileSession` | | Composant | PascalCase | `DataTable`, `MachineInfoCard` | | Page | kebab-case | `machines/index.vue`, `activity-log.vue` | | Type/Interface | PascalCase | `Machine`, `DataTableSort` | | Fonction | camelCase | `loadMachines`, `handleSave` | ### 6. Responsive design Utiliser les breakpoints Tailwind pour adapter l'affichage : ```html