Files
Inventory/docs/FRONTEND.md
2026-03-08 13:47:46 +01:00

1053 lines
31 KiB
Markdown

# 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
<script setup lang="ts">
// La partie logique (JavaScript/TypeScript)
import { ref, computed } from 'vue'
const count = ref(0) // ref() = variable réactive
const doubled = computed(() => count.value * 2) // computed() = valeur calculée
function increment() {
count.value++ // Modifier un ref → l'interface se met à jour
}
</script>
<template>
<!-- La partie visuelle (HTML) -->
<div>
<p>Compteur : {{ count }}</p> <!-- {{ }} = afficher une valeur -->
<p>Double : {{ doubled }}</p>
<button @click="increment">+1</button> <!-- @click = événement au clic -->
</div>
</template>
```
### 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** | `<input v-model="name">` | Lie un input à une variable (bidirectionnel) |
| **v-if** | `<div v-if="show">` | Afficher conditionnellement |
| **v-for** | `<div v-for="item in items">` | Boucler sur une liste |
| **@event** | `<button @click="fn">` | Écouter un événement |
| **:prop** | `<Comp :title="myTitle">` | Passer une valeur à un composant enfant |
| **emit** | `emit('save', data)` | Envoyer un événement au composant parent |
### Ce que Nuxt ajoute par rapport à Vue seul
| Fonctionnalité | Description |
|----------------|-------------|
| **File-based routing** | Chaque fichier dans `pages/` crée automatiquement une route URL |
| **Auto-imports** | Les composants et composables sont importés automatiquement |
| **Middleware** | Code qui s'exécute avant chaque navigation (ex: vérifier l'auth) |
| **useRoute / navigateTo** | Utilitaires de navigation fournis par Nuxt |
| **nuxt.config.ts** | Configuration centralisée du projet |
---
## L'architecture de l'application
### Structure des dossiers
```
app/
├── pages/ # Les pages (1 fichier = 1 URL)
│ ├── index.vue # → /
│ ├── machines/
│ │ └── index.vue # → /machines
│ ├── machine/
│ │ └── [id].vue # → /machine/abc123 (paramètre dynamique)
│ └── ...
├── components/ # Les composants réutilisables (auto-importés)
│ ├── common/ # Composants génériques (DataTable, Pagination, Modal)
│ ├── form/ # Champs de formulaire (email, téléphone)
│ ├── layout/ # Navbar
│ ├── machine/ # Composants spécifiques aux machines
│ ├── model-types/ # Composants de gestion des types
│ └── sites/ # Composants de gestion des sites
├── composables/ # La logique métier (appels API, gestion d'état)
│ ├── useApi.ts # Wrapper HTTP centralisé
│ ├── useMachines.ts # CRUD machines
│ ├── useProfileSession.ts # Gestion de session
│ └── ... # Un composable par domaine
├── shared/ # Types, utilitaires, validation
│ ├── types/ # Interfaces TypeScript
│ ├── utils/ # Fonctions helpers
│ ├── validation/ # Validation email, téléphone
│ └── model/ # Structures de données
├── middleware/ # Code exécuté avant chaque navigation
│ └── profile.global.ts # Vérifie que l'utilisateur est connecté
├── services/ # Couche service
│ └── modelTypes.ts # Service spécialisé
├── utils/ # Fonctions de formatage
│ └── ...
├── assets/ # Fichiers CSS, images
│ └── app.css # Styles globaux
└── app.vue # Le composant racine de l'application
```
### Le composant racine : app.vue
C'est le "cadre" de l'application. Tout ce qui est affiché passe par ce fichier :
```vue
<template>
<div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<!-- La barre de navigation (toujours visible) -->
<LayoutAppNavbar @open-settings="showSettings = true" @logout="logout" />
<!-- Le contenu de la page (change selon l'URL) -->
<NuxtPage />
<!-- Les éléments globaux (toujours disponibles) -->
<ToastContainer /> <!-- Notifications en bas à droite -->
<CommonConfirmModal /> <!-- Modale de confirmation -->
<DisplaySettings /> <!-- Paramètres d'affichage -->
<footer>© Malio 2025 · v{{ appVersion }}</footer>
</div>
</template>
```
---
## Les Pages
### Le routing basé sur les fichiers
Nuxt crée automatiquement les routes à partir de la structure des fichiers dans `pages/` :
| Fichier | URL | Description |
|---------|-----|-------------|
| `pages/index.vue` | `/` | Page d'accueil (dashboard) |
| `pages/profiles/index.vue` | `/profiles` | Page de login |
| `pages/machines/index.vue` | `/machines` | Catalogue des machines |
| `pages/machines/new.vue` | `/machines/new` | Création d'une machine |
| `pages/machine/[id].vue` | `/machine/abc123` | Détail d'une machine (id dynamique) |
| `pages/component-catalog.vue` | `/component-catalog` | Catalogue des composants |
| `pages/component/create.vue` | `/component/create` | Création d'un composant |
| `pages/component/[id]/edit.vue` | `/component/abc123/edit` | Édition d'un composant |
| `pages/pieces-catalog.vue` | `/pieces-catalog` | Catalogue des pièces |
| `pages/product-catalog.vue` | `/product-catalog` | Catalogue des produits |
| `pages/sites.vue` | `/sites` | Gestion des sites |
| `pages/constructeurs.vue` | `/constructeurs` | Gestion des fournisseurs |
| `pages/models/index.vue` | `/models` | Gestion des types/catégories |
| `pages/documents.vue` | `/documents` | Navigateur de documents |
| `pages/comments.vue` | `/comments` | Commentaires/tickets |
| `pages/activity-log.vue` | `/activity-log` | Journal d'activité |
| `pages/admin/index.vue` | `/admin` | Administration (profils) |
| `pages/changelog.vue` | `/changelog` | Historique des versions |
### Les paramètres dynamiques
Les crochets `[id]` dans le nom du fichier créent un paramètre dynamique :
```vue
<!-- pages/machine/[id].vue -->
<script setup lang="ts">
const route = useRoute()
const machineId = route.params.id // → "abc123" si l'URL est /machine/abc123
</script>
```
### Anatomie d'une page typique
```vue
<!-- pages/machines/index.vue Catalogue des machines -->
<script setup lang="ts">
// 1. Utiliser les composables pour la logique
const { machines, loading, loadMachines } = useMachines()
const { canEdit } = usePermissions()
// 2. Charger les données au montage
onMounted(() => {
loadMachines()
})
// 3. Logique de la page
function handleCreate() {
navigateTo('/machines/new')
}
</script>
<template>
<!-- 4. L'interface -->
<div class="container mx-auto p-4">
<PageHero title="Machines" subtitle="Catalogue des machines du parc">
<button v-if="canEdit" class="btn btn-primary" @click="handleCreate">
<IconLucidePlus class="w-5 h-5" />
Nouvelle machine
</button>
</PageHero>
<!-- Composant DataTable pour afficher les données -->
<CommonDataTable
:items="machines"
:loading="loading"
:columns="columns"
/>
</div>
</template>
```
---
## Les Composables
### C'est quoi un composable ?
Un composable est une **fonction qui encapsule de la logique réutilisable**. C'est le pattern central du projet. Au lieu de mettre toute la logique dans les pages/composants, on la met dans des composables.
### Pourquoi ?
- **Réutilisation** : la même logique peut être utilisée dans plusieurs pages
- **Séparation** : la page s'occupe de l'affichage, le composable de la logique
- **Testabilité** : plus facile à tester isolément
### Les composables principaux
#### useApi.ts — Le wrapper HTTP
C'est le composable le plus important : il centralise **tous les appels API**.
```typescript
// Utilisation dans un autre composable
const api = useApi()
// GET : récupérer des données
const result = await api.get<Machine[]>('/machines')
if (result.success) {
console.log(result.data) // → les machines
}
// POST : créer une donnée
const result = await api.post<Machine>('/machines', {
name: 'CNC 01',
reference: 'CNM-001',
site: '/api/sites/cl...',
})
// PATCH : modifier partiellement
const result = await api.patch<Machine>('/machines/cl...', {
name: 'CNC 02',
})
// DELETE : supprimer
const result = await api.delete('/machines/cl...')
```
**Points clés :**
- Ajoute automatiquement `credentials: 'include'` (cookies de session)
- Utilise `application/ld+json` pour POST/PUT et `application/merge-patch+json` pour PATCH
- Gère les erreurs et affiche des toasts automatiquement
- Retourne toujours un objet `{ success, data?, error?, status? }`
#### useProfileSession.ts — Gestion de session
```typescript
const { activeProfile, sessionLoaded, loading } = useProfileSession()
// Vérifier la session (appelé automatiquement par le middleware)
await ensureSession()
// Se connecter
await activateProfile('cl...profileId', 'password123')
// Se déconnecter
await logout()
```
#### usePermissions.ts — Vérification des droits
```typescript
const { isAdmin, canEdit, canView } = usePermissions()
// Utilisation dans le template
// <button v-if="canEdit">Modifier</button>
// <div v-if="isAdmin">Section admin</div>
```
#### useMachines.ts — CRUD machines
Pattern typique : un composable par domaine métier.
```typescript
const {
machines, // ref<Machine[]> — la liste des machines
loading, // ref<boolean> — en cours de chargement ?
loaded, // ref<boolean> — déjà chargé ?
loadMachines, // () => Promise<void>
createMachine, // (data) => Promise<Machine | null>
updateMachine, // (id, data) => Promise<boolean>
deleteMachine, // (id) => Promise<boolean>
cloneMachine, // (sourceId, data) => Promise<Machine | null>
} = 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<string>
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
<!-- Le composant CommonDataTable est importé automatiquement -->
<template>
<CommonDataTable :items="items" />
</template>
```
La convention de nommage utilise le chemin du dossier :
- `components/common/DataTable.vue``<CommonDataTable />`
- `components/machine/InfoCard.vue``<MachineInfoCard />`
- `components/layout/AppNavbar.vue``<LayoutAppNavbar />`
### Les composants communs (réutilisables)
#### CommonDataTable
Le composant central pour tous les tableaux avec pagination, tri et recherche :
```vue
<CommonDataTable
:items="machines"
:columns="[
{ key: 'name', label: 'Nom', sortable: true },
{ key: 'reference', label: 'Référence', sortable: true },
{ key: 'prix', label: 'Prix', sortable: true },
]"
:loading="loading"
:pagination="pagination"
@sort="handleSort"
@page-change="handlePageChange"
/>
```
#### CommonConfirmModal
Modale de confirmation globale (utilisée via `useConfirm()`) :
```vue
<!-- Utilisé dans app.vue, piloté par useConfirm -->
<CommonConfirmModal />
```
#### CommonSearchSelect
Dropdown avec recherche intégrée :
```vue
<CommonSearchSelect
v-model="selectedSiteId"
:options="sites"
label-key="name"
value-key="id"
placeholder="Choisir un site"
/>
```
#### CommonPagination
Composant de pagination :
```vue
<CommonPagination
:current-page="pagination.currentPage"
:total-pages="pagination.totalPages"
@page-change="handlePageChange"
/>
```
### 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
<!-- Parent (page) -->
<template>
<MachineInfoCard
:machine="machine" <!-- Props : données passées à l'enfant -->
:edit-mode="isEditing"
@save="handleSave" <!-- Events : l'enfant notifie le parent -->
@cancel="isEditing = false"
/>
</template>
<!-- Enfant (composant) -->
<script setup lang="ts">
// Déclarer les props reçues
const props = defineProps<{
machine: Machine
editMode: boolean
}>()
// Déclarer les événements émis
const emit = defineEmits<{
save: [data: Partial<Machine>]
cancel: []
}>()
function handleSave() {
emit('save', { name: newName.value }) // Notifier le parent
}
</script>
```
---
## 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
<script setup>
const { isAdmin, canEdit, canView } = usePermissions()
</script>
<template>
<!-- Bouton visible uniquement pour les gestionnaires et admins -->
<button v-if="canEdit" class="btn btn-primary">
Modifier
</button>
<!-- Section visible uniquement pour les admins -->
<div v-if="isAdmin">
<h2>Administration</h2>
<!-- ... -->
</div>
</template>
```
---
## Le style
### TailwindCSS 4
TailwindCSS utilise des classes utilitaires au lieu de CSS personnalisé :
```html
<!-- Au lieu d'écrire du CSS personnalisé... -->
<div style="display: flex; padding: 16px; gap: 8px; background: white; border-radius: 8px;">
<!-- ...on utilise des classes Tailwind -->
<div class="flex p-4 gap-2 bg-white rounded-lg">
```
**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
<!-- Bouton -->
<button class="btn btn-primary btn-sm md:btn-md">Sauvegarder</button>
<!-- Input -->
<input class="input input-bordered input-sm md:input-md" placeholder="Nom">
<!-- Select -->
<select class="select select-bordered select-sm md:select-md">
<option>Option 1</option>
</select>
<!-- Textarea -->
<textarea class="textarea textarea-bordered textarea-sm md:textarea-md"></textarea>
<!-- Carte -->
<div class="card bg-base-100 shadow-md">
<div class="card-body">
<h2 class="card-title">Titre</h2>
<p>Contenu</p>
<div class="card-actions justify-end">
<button class="btn btn-primary">Action</button>
</div>
</div>
</div>
<!-- Modale -->
<dialog class="modal modal-open">
<div class="modal-box">
<h3>Titre</h3>
<p>Contenu</p>
<div class="modal-action">
<button class="btn">Fermer</button>
</div>
</div>
</dialog>
<!-- Badge -->
<span class="badge badge-primary badge-sm">Actif</span>
<!-- Tableau -->
<table class="table table-sm">
<thead><tr><th>Nom</th><th>Email</th></tr></thead>
<tbody><tr><td>Jean</td><td>jean@mail.com</td></tr></tbody>
</table>
<!-- Loading -->
<span class="loading loading-spinner loading-lg"></span>
```
### 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
<IconLucidePlus class="w-5 h-5" />
<IconLucideTrash class="w-4 h-4 text-error" />
<IconLucideEdit class="w-4 h-4" />
<IconLucideChevronDown class="w-4 h-4" />
<IconLucideFactory class="w-6 h-6" />
```
---
## 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 :
<MachineInfoCard :machine="machine" :edit-mode="isEditing" @save="handleSave" />
<MachineComponentsCard :components="components" />
<MachineDocumentsCard :documents="documents" @upload="handleUpload" />
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
<!-- Correct -->
<ChildComponent :data="myData" @save="handleSave" />
<!-- Interdit dans ce projet -->
<!-- provide('data', myData) dans le parent -->
<!-- inject('data') dans l'enfant -->
```
### 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
<!-- Petit sur mobile, normal sur desktop -->
<input class="input input-bordered input-sm md:input-md">
<button class="btn btn-sm md:btn-md btn-primary">
<!-- Caché sur mobile, visible sur desktop -->
<div class="hidden md:block">Visible seulement sur desktop</div>
<!-- Grille responsive -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
```
---
## Les tests
### Tests unitaires (Vitest)
```bash
npm run test # Lancer tous les tests
npm run test:watch # Mode watch (relance à chaque modification)
```
Les tests sont dans `tests/` et utilisent Vitest avec happy-dom :
```typescript
import { describe, it, expect } from 'vitest'
describe('extractCollection', () => {
it('should extract hydra:member', () => {
const data = { 'hydra:member': [{ id: '1' }] }
expect(extractCollection(data)).toEqual([{ id: '1' }])
})
})
```
### Tests E2E (Playwright)
```bash
npm run test:e2e # Lancer les tests end-to-end
```
Les tests E2E simulent un navigateur réel et testent l'application de bout en bout.
---
## Commandes utiles
| Commande | Description |
|----------|-------------|
| `npm run dev` | Serveur de développement avec rechargement automatique |
| `npm run build` | Build de production |
| `npm run lint:fix` | Corriger les erreurs de style automatiquement |
| `npx nuxi typecheck` | Vérifier les types TypeScript (doit retourner 0 erreur) |
| `npm run test` | Tests unitaires |
| `npm run test:e2e` | Tests E2E |
---
## Résumé des points clés pour un débutant
1. **Nuxt auto-importe tout** : pas besoin d'`import` pour les composants et composables
2. **File-based routing** : chaque fichier dans `pages/` = une URL
3. **Les composables gèrent la logique** : les pages/composants ne font que l'affichage
4. **useApi.ts centralise les appels HTTP** : ne jamais appeler `fetch` directement
5. **Props + Events** pour communiquer entre composants, jamais `provide/inject`
6. **DaisyUI** pour les composants UI, **Tailwind** pour le layout
7. **Les IRIs** (`/api/sites/cl...`) sont utilisées pour les relations API, pas les IDs simples
8. **Le middleware** vérifie automatiquement la session à chaque navigation
9. **TypeScript** est obligatoire : toujours typer les données
10. **Responsive** : toujours penser mobile-first avec les breakpoints `md:`, `lg:`