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

31 KiB

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
  2. Nuxt et Vue — les bases
  3. L'architecture de l'application
  4. Les Pages (le routing)
  5. Les Composables (la logique métier)
  6. Les Composants (l'interface)
  7. L'API et les appels HTTP
  8. L'authentification côté frontend
  9. Le style (TailwindCSS + DaisyUI)
  10. Les utilitaires et types
  11. Flux complet d'une fonctionnalité
  12. Patterns et conventions du projet
  13. Les tests
  14. 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.

<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 :

<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 :

<!-- 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

<!-- 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.

// 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

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

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.

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 :

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

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

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

const { uploadDocuments, deleteDocument, loadDocumentsByMachine } = useDocuments()

// Upload de fichiers
await uploadDocuments('machine', machineId, fileList)

// Suppression
await deleteDocument(documentId)

useEntityHistory.ts — Historique d'audit

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 :

// 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 :

<!-- 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 :

<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()) :

<!-- Utilisé dans app.vue, piloté par useConfirm -->
<CommonConfirmModal />

CommonSearchSelect

Dropdown avec recherche intégrée :

<CommonSearchSelect
  v-model="selectedSiteId"
  :options="sites"
  label-key="name"
  value-key="id"
  placeholder="Choisir un site"
/>

CommonPagination

Composant de pagination :

<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) :

<!-- 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 : /machineshttp://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 :

// ❌ Ne pas faire
{ "site": "cl9z8y7x..." }

// ✅ Faire
{ "site": "/api/sites/cl9z8y7x..." }

Le fichier shared/utils/apiRelations.ts fournit des helpers :

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 :

{
  "hydra:totalItems": 42,
  "hydra:member": [
    { "id": "cl...", "name": "Machine 1" },
    { "id": "cl...", "name": "Machine 2" }
  ]
}

Le helper extractCollection() gère les différents formats :

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

// 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

<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é :

<!-- 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 :

<!-- 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 :

<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 :

// 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/)

// 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

<!--  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

// ✅ 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 :

<!-- 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)

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 :

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)

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: