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
- Vue d'ensemble
- Nuxt et Vue — les bases
- L'architecture de l'application
- Les Pages (le routing)
- Les Composables (la logique métier)
- Les Composants (l'interface)
- L'API et les appels HTTP
- L'authentification côté frontend
- Le style (TailwindCSS + DaisyUI)
- Les utilitaires et types
- Flux complet d'une fonctionnalité
- Patterns et conventions du projet
- Les tests
- 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: falsedansnuxt.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+jsonpour POST/PUT etapplication/merge-patch+jsonpour 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 :
- Ajoute le base URL :
/machines→http://localhost:8081/api/machines - Ajoute les headers :
Content-Type: application/ld+json(POST/PUT)Content-Type: application/merge-patch+json(PATCH)
- Inclut les cookies :
credentials: 'include' - Gère les erreurs : parse les erreurs backend et les traduit en français
- 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
- Nuxt auto-importe tout : pas besoin d'
importpour les composants et composables - File-based routing : chaque fichier dans
pages/= une URL - Les composables gèrent la logique : les pages/composants ne font que l'affichage
- useApi.ts centralise les appels HTTP : ne jamais appeler
fetchdirectement - Props + Events pour communiquer entre composants, jamais
provide/inject - DaisyUI pour les composants UI, Tailwind pour le layout
- Les IRIs (
/api/sites/cl...) sont utilisées pour les relations API, pas les IDs simples - Le middleware vérifie automatiquement la session à chaque navigation
- TypeScript est obligatoire : toujours typer les données
- Responsive : toujours penser mobile-first avec les breakpoints
md:,lg: