1053 lines
31 KiB
Markdown
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:`
|