diff --git a/CLAUDE.md b/CLAUDE.md index 07e141a..9e0b3fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,13 +49,14 @@ Inventory/ # Backend Symfony (repo principal) # Docker make start # Démarrer les containers make stop # Arrêter -make shell # Shell dans le container PHP +make shell # Shell interactif (nécessite un TTY) make install # Install complet (composer + npm + build) # Backend -make test # PHPUnit -docker compose exec php vendor/bin/php-cs-fixer fix # Linter PHP -docker compose exec php php bin/console doctrine:migrations:migrate +make test # PHPUnit (tous les tests) +make test FILES=tests/Api/Entity/MachineTest.php # Un test spécifique +make php-cs-fixer-allow-risky # Linter PHP (cs-fixer) +docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate # Frontend (dans Inventory_frontend/) npm run dev # Dev server (port 3001) @@ -138,13 +139,18 @@ ROLE_ADMIN → ROLE_GESTIONNAIRE → ROLE_VIEWER → ROLE_USER ## Règles Importantes +### CLAUDE.md — Maintenance obligatoire +- **Toujours consulter** ce fichier en début de conversation pour respecter les conventions +- **Mettre à jour** ce fichier quand une nouvelle convention, pattern ou décision architecturale est établie +- **Utiliser comme source de vérité** pour les commandes, patterns et règles du projet + ### Toujours faire AVANT de modifier du code 1. **Lire le fichier** avant de l'éditer — ne jamais proposer de changements sur du code non lu 2. **Comprendre le pattern existant** — reproduire le style du fichier (noms, indentation, structure) 3. **Vérifier les deux repos** — un changement peut impacter backend ET frontend ### Après chaque modification -1. Backend PHP : `docker compose exec php vendor/bin/php-cs-fixer fix` +1. Backend PHP : `make php-cs-fixer-allow-risky` 2. Frontend : `npm run lint:fix` puis `npx nuxi typecheck` si fichiers TS modifiés ### Ne jamais faire @@ -161,6 +167,26 @@ Quand les branches `master` et `develop` divergent sur l'un des deux repos, **to - Main repo : `git checkout master && git merge develop && git push` - Frontend : `git checkout develop && git merge master && git push` (ou l'inverse selon le cas) +## Tests + +### Stack de test +- **PHPUnit 12** + **API Platform Test** (`ApiTestCase`) +- **DAMA DoctrineTestBundle** — wrappe chaque test dans une transaction avec rollback automatique (pas de TRUNCATE) +- Base de test : même PG, env `test` + +### Commandes +```bash +make test # Tous les tests +make test FILES=tests/Api/Entity/MachineTest.php # Un fichier spécifique +make test-setup # Créer/mettre à jour le schéma test +``` + +### Pattern de test +- Hériter de `AbstractApiTestCase` (helpers auth + factories) +- Ne PAS faire de TRUNCATE/cleanup dans tearDown — DAMA s'en occupe par rollback +- Factories : `createProfile()`, `createMachine()`, `createSite()`, etc. +- Auth : `createViewerClient()`, `createGestionnaireClient()`, `createAdminClient()` + ## URLs Locales - API Symfony : `http://localhost:8081/api` - Nuxt dev : `http://localhost:3001` diff --git a/DEPLOY.md b/DEPLOY.md index ed2acc3..464fcae 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,17 +1,29 @@ -# Inventory - Guide de Déploiement & Release +# Inventory — Guide de Déploiement -## Architecture +Guide pour déployer l'application sur un serveur de production. + +## Architecture de production ``` -inventory.malio-dev.fr/ → Frontend Nuxt (statique) -inventory.malio-dev.fr/api → Backend Symfony (PHP-FPM) +inventory.malio-dev.fr/ → Frontend Nuxt (fichiers statiques servis par Nginx) +inventory.malio-dev.fr/api → Backend Symfony (PHP-FPM derrière Nginx) ``` | Composant | Technologie | Emplacement serveur | |-----------|-------------|---------------------| | Backend | Symfony 8 + API Platform | `/var/www/Inventory/` | -| Frontend | Nuxt 4 (statique) | `/var/www/Inventory/Inventory_frontend/.output/public/` | -| Base de données | PostgreSQL 16 | `inventory` | +| Frontend | Nuxt 4 (site statique) | `/var/www/Inventory/Inventory_frontend/.output/public/` | +| Base de données | PostgreSQL 16 | Base `inventory` | + +### Schéma simplifié + +``` +Navigateur + ↓ HTTPS +Nginx (reverse proxy) + ├── /api/* → PHP-FPM (Symfony) → PostgreSQL + └── /* → Fichiers statiques (Nuxt build) +``` --- @@ -24,7 +36,8 @@ inventory.malio-dev.fr/api → Backend Symfony (PHP-FPM) - **PostgreSQL** : 16 - **Composer** -Vérifier : +### Vérification des prérequis + ```bash php -v # PHP 8.4+ php -m | grep -E 'pgsql|intl|zip|gd|mbstring' @@ -73,10 +86,10 @@ psql -U ferme_user -h 127.0.0.1 -d inventory -f /tmp/backup_v1.0.0_clean.sql ```bash cd /var/www/Inventory -# Installer les dépendances +# Installer les dépendances (sans les outils de dev) composer install --no-dev --optimize-autoloader -# Créer .env.local +# Créer le fichier de configuration locale cat > .env.local << 'EOF' APP_ENV=prod APP_DEBUG=0 @@ -85,28 +98,20 @@ APP_SECRET=CHANGE_ME DATABASE_URL="postgresql://ferme_user:fermerecette@127.0.0.1:5432/inventory?serverVersion=16" CORS_ALLOW_ORIGIN='^https?://inventory\.malio-dev\.fr$' - -JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem -JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem -JWT_PASSPHRASE=inventoryjwt EOF -# Générer APP_SECRET +# Générer un secret aléatoire sed -i "s/CHANGE_ME/$(openssl rand -hex 32)/" .env.local -# Générer les clés JWT -mkdir -p config/jwt -openssl genrsa -out config/jwt/private.pem -aes256 4096 -# Passphrase : inventoryjwt -openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem -chmod 600 config/jwt/private.pem - -# Permissions +# Permissions pour le dossier var/ (cache, logs) sudo chown -R www-data:www-data var/ sudo chmod -R 775 var/ # Vider le cache php bin/console cache:clear --env=prod + +# Appliquer les migrations (si première installation ou mise à jour) +php bin/console doctrine:migrations:migrate --no-interaction ``` ### 4. Configurer le frontend Nuxt @@ -120,7 +125,7 @@ sudo chown -R malio:malio . # Installer les dépendances npm install -# Créer .env +# Créer le fichier d'environnement cat > .env << 'EOF' NUXT_PUBLIC_API_BASE_URL=http://inventory.malio-dev.fr/api EOF @@ -141,7 +146,7 @@ server { listen 80; server_name inventory.malio-dev.fr; - # Gros fichiers (100MB max) + # Gros fichiers (100MB max pour les uploads de documents) client_max_body_size 100M; client_body_timeout 300s; send_timeout 300s; @@ -149,12 +154,13 @@ server { access_log /var/log/nginx/inventory-access.log; error_log /var/log/nginx/inventory-error.log; - # Backend Symfony - /api + # Backend Symfony — toutes les requêtes /api location /api { root /var/www/Inventory/public; try_files $uri /index.php$is_args$args; } + # PHP-FPM (exécute le code PHP) location ~ ^/index\.php(/|$) { fastcgi_pass unix:/run/php/php-fpm.sock; fastcgi_split_path_info ^(.+\.php)(/.*)$; @@ -165,27 +171,27 @@ server { internal; } - # Frontend statique + # Frontend statique — tout le reste location / { root /var/www/Inventory/Inventory_frontend/.output/public; index index.html; - try_files $uri $uri/ /index.html; + try_files $uri $uri/ /index.html; # SPA fallback } } ``` -Activer : +Activer le site : ```bash sudo ln -s /etc/nginx/sites-available/inventory /etc/nginx/sites-enabled/ -sudo nginx -t +sudo nginx -t # Vérifier la syntaxe sudo systemctl reload nginx ``` ### 6. Vérifier ```bash -curl http://inventory.malio-dev.fr -curl http://inventory.malio-dev.fr/api +curl http://inventory.malio-dev.fr # Frontend +curl http://inventory.malio-dev.fr/api # API (doc Swagger) ``` --- @@ -197,12 +203,13 @@ curl http://inventory.malio-dev.fr/api ```bash cd /var/www/Inventory -# Pull les changements +# Récupérer les changements git pull git submodule update --init --recursive # Backend composer install --no-dev --optimize-autoloader +php bin/console doctrine:migrations:migrate --no-interaction php bin/console cache:clear --env=prod sudo chown -R www-data:www-data var/ @@ -214,56 +221,76 @@ npx nuxi generate --- -## Versioning & Releases +## Backup base de données -### Source de vérité - -Le fichier `VERSION` à la racine contient le numéro de version (ex: `1.0.0`). - -Cette version est synchronisée avec : -- Le footer de l'application -- `config/packages/api_platform.yaml` - -### Créer une release +### Export (faire un backup) ```bash -# Depuis le PC de dev -./scripts/release.sh patch # 1.0.0 → 1.0.1 -./scripts/release.sh minor # 1.0.0 → 1.1.0 -./scripts/release.sh major # 1.0.0 → 2.0.0 -./scripts/release.sh 2.0.0 # Version exacte +pg_dump -U ferme_user -h 127.0.0.1 -d inventory \ + --no-owner --no-acl --inserts --column-inserts \ + --clean --if-exists > backup_inventory_$(date +%Y%m%d).sql ``` -Le script : -1. Vérifie/commit le submodule frontend -2. Met à jour `VERSION` et `api_platform.yaml` -3. Commit et tag les deux repos -4. Affiche les commandes pour push - -### Pousser la release +### Import (restaurer un backup) ```bash -# Frontend (submodule) -cd Inventory_frontend && git push && git push --tags && cd .. - -# Backend -git push && git push --tags +psql -U ferme_user -h 127.0.0.1 -d inventory -f backup_inventory_YYYYMMDD.sql ``` -### Créer la release sur Gitea - -1. Aller sur le dépôt Gitea -2. **Releases** > **New Release** -3. Sélectionner le tag `vX.Y.Z` -4. Ajouter les notes de release - --- -## Commandes utiles +## Troubleshooting + +### Erreur 502 Bad Gateway + +PHP-FPM ne tourne pas ou est crashé : +```bash +systemctl status php8.4-fpm +sudo systemctl restart php8.4-fpm +``` + +### Erreur 403 Forbidden + +Problème de permissions sur les fichiers : +```bash +sudo chown -R www-data:www-data /var/www/Inventory/var/ +sudo chmod -R 775 /var/www/Inventory/var/ +``` + +### Erreur API "No route found" + +Le cache Symfony est probablement périmé : +```bash +php /var/www/Inventory/bin/console cache:clear --env=prod +``` + +### Frontend ne se met pas à jour + +Les fichiers statiques sont en cache. Rebuilder : +```bash +cd /var/www/Inventory/Inventory_frontend +rm -rf .output +npx nuxi generate +``` + +### L'API retourne 401 sur toutes les requêtes + +La session PHP ne se crée pas correctement. Vérifier : +```bash +# Vérifier que le dossier de sessions existe et est accessible +ls -la /var/lib/php/sessions/ +# Ou vérifier les logs Symfony +tail -f /var/www/Inventory/var/log/prod.log +``` + +--- + +## Commandes utiles en production ```bash # Logs Nginx tail -f /var/log/nginx/inventory-error.log +tail -f /var/log/nginx/inventory-access.log # Logs Symfony tail -f /var/www/Inventory/var/log/prod.log @@ -274,55 +301,12 @@ php /var/www/Inventory/bin/console cache:clear --env=prod # Rebuild frontend cd /var/www/Inventory/Inventory_frontend && npx nuxi generate -# Status PHP-FPM +# Status des services systemctl status php8.4-fpm +systemctl status nginx +systemctl status postgresql -# Reload Nginx +# Redémarrer les services +sudo systemctl restart php8.4-fpm sudo systemctl reload nginx ``` - ---- - -## Backup base de données - -### Export -```bash -pg_dump -U ferme_user -h 127.0.0.1 -d inventory --no-owner --no-acl --inserts --column-inserts --clean --if-exists > backup_inventory_$(date +%Y%m%d).sql -``` - -### Import -```bash -psql -U ferme_user -h 127.0.0.1 -d inventory -f backup_inventory_YYYYMMDD.sql -``` - ---- - -## Troubleshooting - -### Erreur 502 Bad Gateway -```bash -# Vérifier PHP-FPM -systemctl status php8.4-fpm -sudo systemctl restart php8.4-fpm -``` - -### Erreur 403 Forbidden -```bash -# Vérifier les permissions -sudo chown -R www-data:www-data /var/www/Inventory/var/ -sudo chmod -R 775 /var/www/Inventory/var/ -``` - -### Erreur API "No route found" -```bash -# Vider le cache -php /var/www/Inventory/bin/console cache:clear --env=prod -``` - -### Frontend ne se met pas à jour -```bash -# Rebuild -cd /var/www/Inventory/Inventory_frontend -rm -rf .output -npx nuxi generate -``` diff --git a/Inventory_frontend b/Inventory_frontend index 592beb0..165e0a6 160000 --- a/Inventory_frontend +++ b/Inventory_frontend @@ -1 +1 @@ -Subproject commit 592beb0fa75bf061de3f49deb9a3e7cd89611e3b +Subproject commit 165e0a634120bcc0254a3c3b7187dcd8032f764d diff --git a/README.md b/README.md index d35708b..0e31ae7 100644 --- a/README.md +++ b/README.md @@ -2,52 +2,87 @@ Application de gestion d'inventaire industriel pour **Malio**. Gestion complète du parc machines, des pièces, composants, produits, fournisseurs et documents associés, avec traçabilité et contrôle d'accès par rôles. +## C'est quoi ce projet ? + +Inventory est une application web qui permet de gérer un parc de machines industrielles. Concrètement, elle permet de : + +- **Cataloguer** les machines d'une usine, site par site +- **Décomposer** chaque machine en composants, pièces et produits (structure arborescente) +- **Suivre** les fournisseurs/constructeurs de chaque élément +- **Stocker** les documents techniques (PDF, images, fiches techniques) +- **Tracer** toutes les modifications (qui a changé quoi, quand) via un journal d'audit +- **Commenter** les fiches pour collaborer entre équipes +- **Gérer les accès** avec un système de rôles (admin, gestionnaire, lecteur) + +L'application se compose de deux parties : +- Un **backend** (API REST) qui gère les données, la sécurité et la logique métier +- Un **frontend** (interface web) qui affiche les données et permet l'interaction utilisateur + ## Stack technique -| Couche | Technologie | Version | -|--------|-------------|---------| -| Backend | Symfony + API Platform | 8.0 / 4.2 | -| PHP | PHP | >= 8.4 | -| Base de données | PostgreSQL | 16 | -| Frontend | Nuxt (SPA, SSR off) | 4 | -| UI | Vue 3 Composition API + TypeScript | 3.5 / 5.7 | -| CSS | TailwindCSS + DaisyUI | 4 / 5 | -| Conteneurs | Docker Compose | | +| Couche | Technologie | Version | Rôle | +|--------|-------------|---------|------| +| Backend | Symfony + API Platform | 8.0 / 4.2 | API REST, logique métier, sécurité | +| PHP | PHP | >= 8.4 | Langage backend | +| Base de données | PostgreSQL | 16 | Stockage des données | +| Frontend | Nuxt (SPA, SSR off) | 4 | Framework web (rendu côté client) | +| UI | Vue 3 Composition API + TypeScript | 3.5 / 5.7 | Composants d'interface | +| CSS | TailwindCSS + DaisyUI | 4 / 5 | Mise en page et composants visuels | +| Conteneurs | Docker Compose | | Environnement de développement | ## Prérequis -- **Docker** et **Docker Compose** -- **Node.js** >= 20 (via nvm) -- **make** +- **Docker** et **Docker Compose** (pour lancer le projet sans rien installer) +- **Node.js** >= 20 (via [nvm](https://github.com/nvm-sh/nvm)) +- **make** (normalement déjà installé sur Linux/macOS) -### Installation de l'environnement +### Guides d'installation de l'environnement | OS | Documentation | |----|---------------| | Windows | [WSL2 + Ubuntu + Docker](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/windows) | | Linux | [Docker + nvm](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/linux) | -## Installation +## Installation rapide ```bash +# 1. Cloner le projet avec le frontend (submodule) git clone --recurse-submodules cd Inventory + +# 2. Démarrer les conteneurs Docker (PHP, PostgreSQL, Adminer) make start + +# 3. Installer les dépendances et builder le projet make install ``` > Si `make start` échoue sur le port de la BDD, modifier `POSTGRES_PORT` dans `docker/.env.docker.local`. +### Que fait `make install` ? + +1. Installe les dépendances PHP (via Composer) +2. Installe les dépendances Node.js (via npm) +3. Build le frontend Nuxt + +### Premier lancement + +Une fois l'installation terminée, tu peux : + +1. Charger des données de test : `make fixtures-load` +2. Lancer le frontend en mode dev : `make dev-nuxt` +3. Ouvrir l'application : http://localhost:3001 + ## URLs locales -| Service | URL | -|---------|-----| -| API Symfony | http://localhost:8081/api | -| Frontend Nuxt | http://localhost:3001 | -| Adminer (BDD) | http://localhost:5050 | -| PostgreSQL | `localhost:5433` (user: root, pass: root, db: inventory) | +| Service | URL | Description | +|---------|-----|-------------| +| API Symfony | http://localhost:8081/api | Documentation interactive de l'API (Swagger) | +| Frontend Nuxt | http://localhost:3001 | L'application web | +| Adminer (BDD) | http://localhost:5050 | Interface web pour explorer la base de données | +| PostgreSQL | `localhost:5433` | Connexion directe (user: root, pass: root, db: inventory) | -## Commandes +## Commandes utiles ### Docker @@ -56,26 +91,28 @@ make install | `make start` | Démarrer les conteneurs | | `make stop` | Arrêter les conteneurs | | `make restart` | Redémarrer les conteneurs | -| `make shell` | Shell bash dans le conteneur PHP | -| `make reset` | Reset complet (supprime volumes, réinstalle) | +| `make shell` | Ouvrir un terminal dans le conteneur PHP (pour lancer des commandes Symfony) | +| `make reset` | Reset complet (supprime les volumes, réinstalle tout) | ### Backend | Commande | Description | |----------|-------------| | `make test` | Lancer les tests PHPUnit | -| `make php-cs-fixer-allow-risky` | Formatter le code PHP | -| `make cache-clear` | Vider le cache Symfony | -| `make db-reset` | Reset de la BDD (supprime les données) | -| `make fixtures-load` | Charger les fixtures SQL | -| `make fixtures-dump` | Dumper la BDD dans fixtures/data.sql | +| `make test FILES=tests/Api/Entity/MachineTest.php` | Lancer un test spécifique | +| `make test-setup` | Créer/mettre à jour la base de test | +| `make php-cs-fixer-allow-risky` | Formatter le code PHP (indentation, espaces, etc.) | +| `make cache-clear` | Vider le cache Symfony (à faire si tu as des erreurs bizarres) | +| `make db-reset` | Reset de la BDD (supprime toutes les données) | +| `make fixtures-load` | Charger les données de test | +| `make fixtures-dump` | Sauvegarder la BDD actuelle dans fixtures/data.sql | ### Frontend | Commande | Description | |----------|-------------| -| `make dev-nuxt` | Serveur de dev Nuxt | -| `make build-nuxtJS` | Build de production | +| `make dev-nuxt` | Lancer le serveur de dev Nuxt (avec rechargement automatique) | +| `make build-nuxtJS` | Builder le frontend pour la production | ### Release @@ -85,79 +122,115 @@ make install Synchronise automatiquement la version dans `VERSION`, `api_platform.yaml` et `nuxt.config.ts`, crée le tag git et pousse les deux repos. -## Architecture +## Architecture globale + +### Comment ça marche ? + +``` +┌──────────────────┐ HTTP (JSON) ┌──────────────────┐ SQL ┌────────────┐ +│ Frontend │ ◄─────────────────► │ Backend │ ◄──────────► │ PostgreSQL │ +│ (Nuxt/Vue) │ cookies session │ (Symfony/API) │ │ (BDD) │ +│ localhost:3001 │ │ localhost:8081 │ │ port 5433 │ +└──────────────────┘ └──────────────────┘ └────────────┘ + │ │ + Interface web API REST + logique + pour l'utilisateur métier + sécurité +``` + +1. L'utilisateur ouvre le navigateur sur `localhost:3001` +2. Le frontend (Vue/Nuxt) affiche l'interface +3. Quand l'utilisateur fait une action (créer une machine, etc.), le frontend envoie une requête HTTP à l'API backend +4. Le backend valide la requête, vérifie les permissions, exécute la logique métier +5. Le backend lit/écrit dans PostgreSQL et renvoie une réponse JSON +6. Le frontend met à jour l'interface avec les nouvelles données ### Structure du projet ``` Inventory/ # Backend Symfony (repo principal) ├── src/ -│ ├── Entity/ # 20 entités Doctrine (attributs PHP 8) -│ ├── Controller/ # 16 contrôleurs custom -│ ├── EventSubscriber/ # 9 subscribers (audit onFlush) -│ ├── EventListener/ # Listeners documents (cleanup, compression) -│ ├── Command/ # 3 commandes CLI -│ ├── Service/ # 3 services (stockage, conversion, PDF) -│ ├── State/ # 3 processeurs API Platform -│ ├── Repository/ # 19 repositories Doctrine -│ ├── Security/ # Authenticateur session -│ └── Serializer/ # Normalizer custom (Document) -├── config/ # Configuration Symfony -├── migrations/ # 4 migrations Doctrine (SQL PostgreSQL) +│ ├── Entity/ # Les "modèles" de données (Machine, Piece, etc.) +│ ├── Controller/ # Les endpoints API personnalisés +│ ├── EventSubscriber/ # Logique déclenchée automatiquement (audit, etc.) +│ ├── Command/ # Commandes CLI (lancer via php bin/console) +│ ├── Service/ # Services métier (stockage fichiers, PDF, etc.) +│ ├── State/ # Processeurs API Platform (hashage mot de passe, upload) +│ ├── Repository/ # Requêtes BDD personnalisées +│ ├── Security/ # Authentification par session +│ └── Serializer/ # Conversion entité ↔ JSON personnalisée +├── config/ # Configuration Symfony (routes, sécurité, etc.) +├── migrations/ # Scripts de modification de la BDD ├── fixtures/ # Données de test (SQL) +├── tests/ # Tests automatisés (PHPUnit) ├── scripts/ # Utilitaires (release, migration, normalisation) ├── docker/ # Dockerfile + config Docker -├── makefile # Commandes de dev -├── VERSION # Version courante (semver) -└── Inventory_frontend/ # Submodule git (repo séparé) - ├── app/pages/ # 36 pages Nuxt (file-based routing) - ├── app/components/ # 57 composants Vue - ├── app/composables/ # 45 composables - └── app/shared/ # Types, utils, validation +├── makefile # Commandes de dev raccourcies +├── VERSION # Version courante (ex: 1.8.1) +└── Inventory_frontend/ # Submodule git (frontend, repo séparé) + ├── app/pages/ # Les pages de l'app (1 fichier = 1 route URL) + ├── app/components/ # Composants Vue réutilisables + ├── app/composables/ # Logique métier partagée (appels API, états) + ├── app/shared/ # Types TypeScript, utilitaires, validation + ├── app/middleware/ # Vérification de session automatique + └── app/services/ # Couche service (wrappers API) ``` -### Entités principales +### Entités principales (les "tables" de la BDD) -| Entité | Description | -|--------|-------------| -| `Machine` | Machines du parc industriel | -| `Composant` | Composants rattachés aux machines | -| `Piece` | Pièces détachées | -| `Product` | Produits (consommables, outillage) | -| `Site` | Sites physiques / usines | -| `Constructeur` | Fournisseurs / fabricants | -| `TypeMachine` | Types de machines avec squelettes de structure | -| `ModelType` | Catégories (pièce, composant, produit) avec champs personnalisés | -| `CustomField` / `CustomFieldValue` | Champs personnalisés extensibles | -| `Document` | Documents uploadés (stockage fichier + compression PDF) | -| `AuditLog` | Journal d'audit (diff + snapshot) | -| `Comment` | Commentaires / tickets sur les fiches | -| `Profile` | Utilisateurs avec rôles | +| Entité | Description | Exemple | +|--------|-------------|---------| +| `Machine` | Machines du parc industriel | "CNC Mazak 01" | +| `Composant` | Composants fonctionnels d'une machine | "Broche principale" | +| `Piece` | Pièces détachées/de rechange | "Roulement SKF 6205" | +| `Product` | Produits fournisseur (consommables, outillage) | "Huile de coupe X" | +| `Site` | Sites physiques / usines | "Usine de Strasbourg" | +| `Constructeur` | Fournisseurs / fabricants | "SKF", "Mazak" | +| `ModelType` | Catégories avec squelettes de structure | "Type: Moteur électrique" | +| `CustomField` / `CustomFieldValue` | Champs personnalisés (dynamiques) | "Tension : 220V" | +| `Document` | Documents uploadés (PDF, images, etc.) | "Fiche technique CNC.pdf" | +| `AuditLog` | Journal d'audit (historique des modifications) | "Machine X modifiée par Jean" | +| `Comment` | Commentaires / tickets sur les fiches | "Vérifier le roulement" | +| `Profile` | Comptes utilisateurs avec rôles | "admin@malio.fr (ADMIN)" | -### Commandes Symfony +### Structure hiérarchique d'une machine -| Commande | Description | -|----------|-------------| -| `app:compress-pdf` | Compresser les PDFs existants (supporte `--dry-run`) | -| `app:migrate-documents-to-filesystem` | Migrer les documents Base64 vers le système de fichiers | -| `app:init-profile-passwords` | Initialiser mots de passe et rôles en masse | +Une machine peut contenir une arborescence de composants, pièces et produits : + +``` +Machine "CNC Mazak 01" +├── Composant "Broche principale" +│ ├── Pièce "Roulement avant" +│ │ └── Produit "Graisse SKF LGMT2" +│ └── Pièce "Joint d'étanchéité" +├── Composant "Système hydraulique" +│ ├── Pièce "Pompe HP" +│ └── Produit "Huile hydraulique ISO 46" +└── Produit "Filtre à air cabine" +``` ### Rôles et permissions ``` -ROLE_ADMIN → ROLE_GESTIONNAIRE → ROLE_VIEWER → ROLE_USER +ROLE_ADMIN → Tout faire + gérer les utilisateurs + ↓ hérite de +ROLE_GESTIONNAIRE → Créer, modifier, supprimer les données + ↓ hérite de +ROLE_VIEWER → Lecture seule sur toutes les données + ↓ hérite de +ROLE_USER → Accès de base (rôle minimum) ``` -- **ADMIN** : accès complet, gestion des profils -- **GESTIONNAIRE** : CRUD sur toutes les entités, résolution des commentaires -- **VIEWER** : lecture seule sur toutes les entités -- **USER** : accès de base - ### Authentification -Authentification par **session (cookies)**, pas de JWT. Le profil actif est stocké en session côté serveur. +Authentification par **session (cookies)**, pas de JWT. Le profil actif est stocké en session côté serveur. Concrètement : -### Base de données +1. L'utilisateur choisit son profil sur la page de login +2. Il entre son mot de passe +3. Le backend crée une session et envoie un cookie au navigateur +4. À chaque requête suivante, le navigateur envoie automatiquement ce cookie +5. Le backend vérifie le cookie et identifie l'utilisateur + +### Base de données — Points importants PostgreSQL 16 avec les particularités suivantes : - **IDs** : chaînes CUID (`'cl' + bin2hex(random_bytes(12))`), pas d'auto-increment @@ -171,7 +244,7 @@ PostgreSQL 16 avec les particularités suivantes : |---------|-------|------|------| | `web` | PHP 8.4 + Apache + Node | 8081, 3001 | API Symfony + Nuxt dev | | `db` | PostgreSQL 16 Alpine | 5433 | Base de données | -| `adminer` | Adminer | 5050 | Interface web BDD | +| `adminer` | Adminer | 5050 | Interface web pour explorer la BDD | ## Xdebug @@ -191,30 +264,42 @@ Configuration PhpStorm / VSCode : - `develop` : branche principale de dev (cible des PR) - `feat/xxx`, `fix/xxx`, `refactor/xxx` : branches de travail -### Convention de commit +### Convention de commit (enforced par un hook) ``` () : ``` -**Espace obligatoire autour du `:`**. Types : `feat`, `fix`, `perf`, `refactor`, `chore`, `docs`, `test`, `style`, `build`, `ci`, `revert`, `wip`. +**Espace obligatoire autour du `:`**. Types autorisés (minuscules) : +`feat`, `fix`, `perf`, `refactor`, `chore`, `docs`, `test`, `style`, `build`, `ci`, `revert`, `wip` + +Exemples : +``` +feat(machines) : add clone functionality +fix(documents) : prevent duplicate upload +refactor(audit) : merge history controllers +chore(deps) : update composer packages +``` ### Pre-commit hook -1. php-cs-fixer sur les fichiers PHP stagés -2. PHPUnit — bloque le commit si les tests échouent +Le hook `pre-commit` s'exécute automatiquement avant chaque commit : +1. **php-cs-fixer** — Formate automatiquement les fichiers PHP modifiés +2. **PHPUnit** — Lance les tests. Si un test échoue, le commit est bloqué ### Submodule frontend -Le frontend est un **submodule git** dans `Inventory_frontend/`. Workflow : +Le frontend est un **submodule git** dans `Inventory_frontend/` (c'est un repo git séparé, inclus dans le repo principal). Workflow de commit : 1. Commiter dans `Inventory_frontend/` d'abord -2. Commiter dans le repo principal pour mettre à jour le pointeur +2. Commiter dans le repo principal pour mettre à jour le pointeur du submodule 3. Pousser les deux repos -## Documentation complémentaire +## Documentation détaillée -- [DEPLOY.md](DEPLOY.md) : guide de déploiement serveur (Nginx, PHP-FPM, PostgreSQL) -- [RELEASE.md](RELEASE.md) : processus de release et versioning -- [CHANGELOG.md](CHANGELOG.md) : historique des versions -- [Frontend README](Inventory_frontend/README.md) : documentation du frontend Nuxt +- **[docs/BACKEND.md](docs/BACKEND.md)** : guide complet du backend (entités, controllers, API, audit, tests) +- **[docs/FRONTEND.md](docs/FRONTEND.md)** : guide complet du frontend (pages, composables, composants, patterns) +- **[DEPLOY.md](DEPLOY.md)** : guide de déploiement serveur (Nginx, PHP-FPM, PostgreSQL) +- **[RELEASE.md](RELEASE.md)** : processus de release et versioning +- **[CHANGELOG.md](CHANGELOG.md)** : historique des versions +- **[Frontend README](Inventory_frontend/README.md)** : documentation du frontend Nuxt diff --git a/RELEASE.md b/RELEASE.md index d962837..11e19d0 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,12 +1,18 @@ # Guide de Release -## Versioning +## C'est quoi une release ? -Le projet utilise le [Semantic Versioning](https://semver.org/) (SemVer) : `MAJOR.MINOR.PATCH` +Une release c'est une **version officielle** de l'application. Chaque release a un numéro de version (ex: `1.8.1`) et est marquée par un **tag git**. -- **MAJOR** : Changements incompatibles avec les versions précédentes -- **MINOR** : Nouvelles fonctionnalités rétrocompatibles -- **PATCH** : Corrections de bugs rétrocompatibles +## Versioning (Semantic Versioning) + +Le projet utilise le [Semantic Versioning](https://semver.org/) : `MAJOR.MINOR.PATCH` + +| Type | Quand l'utiliser | Exemple | +|------|------------------|---------| +| **PATCH** | Correction de bug, pas de nouvelle fonctionnalité | `1.8.0` → `1.8.1` | +| **MINOR** | Nouvelle fonctionnalité, rétrocompatible | `1.8.1` → `1.9.0` | +| **MAJOR** | Changement majeur, potentiellement incompatible | `1.9.0` → `2.0.0` | La version est centralisée dans le fichier `VERSION` à la racine du projet. @@ -14,9 +20,9 @@ La version est centralisée dans le fichier `VERSION` à la racine du projet. ### Prérequis -- Tous les changements doivent être commités -- Les tests doivent passer -- Être sur la branche à releaser (ex: `main`, `develop`) +- Tous les changements doivent être commités (pas de fichiers modifiés non commités) +- Les tests doivent passer (`make test`) +- Être sur la branche à releaser (généralement `develop` ou `master`) ### Utilisation du script @@ -24,28 +30,38 @@ La version est centralisée dans le fichier `VERSION` à la racine du projet. # Afficher l'aide et la version actuelle ./scripts/release.sh -# Bump patch : 1.0.0 → 1.0.1 +# Bump patch : 1.8.1 → 1.8.2 ./scripts/release.sh patch -# Bump minor : 1.0.0 → 1.1.0 +# Bump minor : 1.8.1 → 1.9.0 ./scripts/release.sh minor -# Bump major : 1.0.0 → 2.0.0 +# Bump major : 1.8.1 → 2.0.0 ./scripts/release.sh major # Version spécifique ./scripts/release.sh 2.0.0 ``` -Le script : -1. Met à jour le fichier `VERSION` -2. Met à jour `config/packages/api_platform.yaml` -3. Crée un commit `chore(release): vX.Y.Z` -4. Crée le tag `vX.Y.Z` +### Que fait le script ? + +1. Vérifie qu'il n'y a pas de changements non commités +2. Vérifie/commit le submodule frontend si nécessaire +3. Met à jour le fichier `VERSION` avec le nouveau numéro +4. Met à jour `config/packages/api_platform.yaml` (version affichée dans l'API) +5. Crée un commit `chore(release) : vX.Y.Z` +6. Crée le tag git `vX.Y.Z` +7. Affiche les commandes pour pousser ### Pousser la release +Après avoir exécuté le script : + ```bash +# Pousser le frontend d'abord (si modifié) +cd Inventory_frontend && git push && git push --tags && cd .. + +# Pousser le backend git push && git push --tags ``` @@ -54,12 +70,21 @@ git push && git push --tags 1. Aller sur le dépôt Gitea 2. **Releases** > **New Release** 3. Sélectionner le tag `vX.Y.Z` -4. Titre : `v1.0.0` (ou avec un nom descriptif) -5. Description : résumé des changements (voir section Notes de release) +4. Titre : `vX.Y.Z` (ou avec un nom descriptif) +5. Description : résumé des changements (copier depuis CHANGELOG.md) + +## Fichiers impactés par le versioning + +| Fichier | Rôle | +|---------|------| +| `VERSION` | Source unique de vérité | +| `config/packages/api_platform.yaml` | Version affichée dans la doc API (Swagger) | +| `Inventory_frontend/nuxt.config.ts` | Lit `VERSION` au build pour l'afficher dans le footer | +| Footer de l'app | Affiche `v{{ appVersion }}` | ## Notes de release -Template pour les notes de release : +Template pour les notes de release (à copier dans Gitea) : ```markdown ## Nouveautés @@ -73,66 +98,25 @@ Template pour les notes de release : ## Changements - Refactoring de Z - Mise à jour des dépendances + +## Migration requise +\`\`\`bash +docker compose exec web php bin/console doctrine:migrations:migrate +\`\`\` ``` -## Fichiers impactés par le versioning +## Déploiement après une release -| Fichier | Usage | -|---------|-------| -| `VERSION` | Source unique de vérité | -| `config/packages/api_platform.yaml` | Version affichée dans l'API | -| `Inventory_frontend/nuxt.config.ts` | Lit VERSION au build | -| Footer de l'app | Affiche `v{{ appVersion }}` | +Voir [DEPLOY.md](DEPLOY.md) pour les instructions de mise à jour en production. -## Déploiement en production - -### 1. Base de données - -Dump de la base locale : -```bash -pg_dump -h localhost -p 5433 -U root -d inventory > backup_v1.0.0.sql -``` - -Import en production : -```bash -psql -h -U -d inventory < backup_v1.0.0.sql -``` - -### 2. Variables d'environnement production - -Créer un fichier `.env.local` en production avec : - -```env -APP_ENV=prod -APP_SECRET= -DATABASE_URL="postgresql://user:password@host:5432/inventory?serverVersion=16" -CORS_ALLOW_ORIGIN='^https://votre-domaine\.com$' -JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem -JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem -JWT_PASSPHRASE= -``` - -### 3. Build production - -Backend : +En résumé : ```bash +# Sur le serveur de production +cd /var/www/Inventory +git pull +git submodule update --init --recursive composer install --no-dev --optimize-autoloader -php bin/console cache:clear --env=prod php bin/console doctrine:migrations:migrate --no-interaction +php bin/console cache:clear --env=prod +cd Inventory_frontend && npm install && npx nuxi generate ``` - -Frontend : -```bash -cd Inventory_frontend -NUXT_PUBLIC_API_BASE_URL=https://api.votre-domaine.com yarn build -``` - -### 4. Checklist avant mise en prod - -- [ ] Tests passent -- [ ] Migrations DB testées -- [ ] Variables d'environnement configurées -- [ ] Clés JWT générées -- [ ] CORS configuré -- [ ] SSL/HTTPS actif -- [ ] Backup de la DB prod existante (si upgrade) diff --git a/docs/BACKEND.md b/docs/BACKEND.md new file mode 100644 index 0000000..a3e101e --- /dev/null +++ b/docs/BACKEND.md @@ -0,0 +1,846 @@ +# Guide Backend — Inventory + +Guide complet du backend Symfony pour comprendre comment tout fonctionne, même si tu débutes. + +## Table des matières + +1. [Vue d'ensemble](#vue-densemble) +2. [Comment fonctionne une API REST](#comment-fonctionne-une-api-rest) +3. [Symfony + API Platform — les bases](#symfony--api-platform--les-bases) +4. [Les Entités (les modèles de données)](#les-entités) +5. [Les Controllers (les endpoints personnalisés)](#les-controllers) +6. [Le système d'audit](#le-système-daudit) +7. [L'authentification par session](#lauthentification-par-session) +8. [Les services](#les-services) +9. [Les migrations de base de données](#les-migrations) +10. [Les tests](#les-tests) +11. [Flux complet d'une requête](#flux-complet-dune-requête) +12. [Commandes Symfony utiles](#commandes-symfony-utiles) + +--- + +## Vue d'ensemble + +Le backend est une **API REST** construite avec : + +- **Symfony 8** : le framework PHP (gère le routing, la sécurité, la config, etc.) +- **API Platform 4.2** : une surcouche qui génère automatiquement les endpoints CRUD à partir des entités +- **Doctrine ORM** : fait le lien entre les objets PHP et les tables PostgreSQL +- **PostgreSQL 16** : la base de données relationnelle + +### Le principe + +Au lieu d'écrire manuellement chaque endpoint (GET /machines, POST /machines, etc.), **API Platform** les génère automatiquement à partir des entités PHP. Tu déclares tes champs, tes relations, tes règles de sécurité directement sur la classe PHP, et API Platform fait le reste. + +--- + +## Comment fonctionne une API REST + +### C'est quoi une API REST ? + +Une API REST c'est un serveur qui répond à des requêtes HTTP (comme un site web, mais au lieu de renvoyer du HTML, il renvoie du JSON). + +### Les verbes HTTP + +| Verbe | Action | Exemple | +|-------|--------|---------| +| `GET` | Lire des données | `GET /api/machines` → liste toutes les machines | +| `POST` | Créer une donnée | `POST /api/machines` + body JSON → crée une machine | +| `PUT` | Remplacer une donnée | `PUT /api/machines/123` + body JSON → remplace la machine 123 | +| `PATCH` | Modifier partiellement | `PATCH /api/machines/123` + body JSON → modifie certains champs | +| `DELETE` | Supprimer | `DELETE /api/machines/123` → supprime la machine 123 | + +### Les codes de réponse HTTP + +| Code | Signification | Quand | +|------|---------------|-------| +| `200` | OK | Requête réussie | +| `201` | Created | Ressource créée avec succès (POST) | +| `204` | No Content | Suppression réussie (DELETE) | +| `400` | Bad Request | Données invalides envoyées | +| `401` | Unauthorized | Pas connecté / session expirée | +| `403` | Forbidden | Connecté mais pas les permissions | +| `404` | Not Found | La ressource n'existe pas | +| `409` | Conflict | Doublon (ex: nom déjà pris) | +| `500` | Server Error | Bug côté serveur | + +### Le format JSON-LD + +L'API utilise **JSON-LD** (JSON Linked Data), une extension de JSON qui ajoute des métadonnées : + +```json +{ + "@context": "/api/contexts/Machine", + "@id": "/api/machines/cl1a2b3c4d5e6f7g8h9i0j1k", + "@type": "Machine", + "id": "cl1a2b3c4d5e6f7g8h9i0j1k", + "name": "CNC Mazak 01", + "reference": "CNM-001", + "prix": "50000.00", + "site": "/api/sites/cl9z8y7x6w5v4u3t2s1r0q", + "createdAt": "2026-01-15T10:30:00+00:00" +} +``` + +Points importants : +- `@id` est l'**IRI** (Internationalized Resource Identifier) : c'est l'identifiant unique de la ressource dans l'API +- Les relations utilisent des IRIs : `"site": "/api/sites/cl9z8..."` au lieu d'un simple ID +- Les collections retournent un format hydra avec pagination : + +```json +{ + "@context": "/api/contexts/Machine", + "@id": "/api/machines", + "@type": "hydra:Collection", + "hydra:totalItems": 42, + "hydra:member": [ + { "@id": "/api/machines/cl...", "name": "CNC 01", ... }, + { "@id": "/api/machines/cl...", "name": "Tour 02", ... } + ] +} +``` + +--- + +## Symfony + API Platform — les bases + +### La structure des fichiers + +``` +src/ +├── Entity/ # Les classes PHP qui représentent les tables de la BDD +├── Controller/ # Les endpoints HTTP personnalisés (quand API Platform ne suffit pas) +├── EventSubscriber/ # Du code qui s'exécute automatiquement quand quelque chose se passe +├── Repository/ # Les requêtes SQL personnalisées +├── Service/ # La logique métier réutilisable +├── State/ # Les processeurs API Platform (interceptent le flux CRUD) +├── Security/ # L'authentification +├── Serializer/ # Personnalisation de la conversion entité ↔ JSON +├── Command/ # Commandes CLI (php bin/console app:xxx) +├── Enum/ # Les énumérations PHP (ex: catégories) +└── OpenApi/ # Personnalisation de la doc Swagger +``` + +### Comment Symfony traite une requête + +``` +Requête HTTP + ↓ +Symfony Router (quel code doit répondre ?) + ↓ +Sécurité (l'utilisateur a-t-il le droit ?) + ↓ +Controller ou API Platform (traitement) + ↓ +Doctrine ORM (lecture/écriture en BDD) + ↓ +Serializer (conversion entité → JSON) + ↓ +Réponse HTTP (JSON envoyé au frontend) +``` + +### Les attributs PHP 8 + +Le projet utilise les **attributs PHP 8** (les `#[...]`) au lieu des annotations (les `@...`). C'est la syntaxe moderne de PHP : + +```php +// Attribut PHP 8 (ce qu'on utilise) ✅ +#[ORM\Column(type: 'string', length: 255)] +private string $name; + +// Annotation (ancien style, on ne l'utilise pas) ❌ +/** @ORM\Column(type="string", length=255) */ +``` + +--- + +## Les Entités + +Les entités sont les classes PHP qui représentent les tables de la base de données. Chaque propriété de la classe correspond à une colonne. + +### Anatomie d'une entité + +Prenons un exemple simplifié : + +```php + ['machine:read']], // ← Quels champs exposer en lecture + denormalizationContext: ['groups' => ['machine:write']], // ← Quels champs accepter en écriture + paginationItemsPerPage: 30, // ← 30 résultats par page +)] +class Machine +{ + #[ORM\Id] + #[ORM\Column(type: 'string', length: 36)] + #[Groups(['machine:read'])] // ← Exposé en lecture uniquement + private string $id; + + #[ORM\Column(type: 'string', length: 255, unique: true)] + #[Groups(['machine:read', 'machine:write'])] // ← Exposé en lecture ET écriture + private string $name; + + #[ORM\Column(type: 'decimal', precision: 10, scale: 2, nullable: true)] + #[Groups(['machine:read', 'machine:write'])] + private ?string $prix = null; + + #[ORM\ManyToOne(targetEntity: Site::class)] // ← Relation : chaque machine appartient à un site + #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] // ← Obligatoire, supprimé en cascade + #[Groups(['machine:read', 'machine:write'])] + private Site $site; + + #[ORM\Column(type: 'datetime_immutable')] + #[Groups(['machine:read'])] // ← Lecture seule (pas dans machine:write) + private \DateTimeImmutable $createdAt; + + // ... getters et setters +} +``` + +### Décryptage des attributs importants + +| Attribut | Signification | +|----------|--------------| +| `#[ORM\Entity]` | Cette classe est stockée en BDD | +| `#[ORM\Column]` | Cette propriété est une colonne | +| `#[ORM\Id]` | C'est la clé primaire | +| `#[ORM\ManyToOne]` | Relation N→1 (plusieurs machines → un site) | +| `#[ORM\OneToMany]` | Relation 1→N (un site → plusieurs machines) | +| `#[ORM\ManyToMany]` | Relation N→N (machines ↔ constructeurs) | +| `#[ApiResource]` | API Platform génère les endpoints CRUD | +| `#[Groups]` | Contrôle quels champs sont visibles/modifiables | +| `security: "is_granted('ROLE_X')"` | Qui a le droit d'utiliser cet endpoint | + +### Le trait CuidEntityTrait + +Toutes les entités utilisent un trait partagé qui génère les IDs et gère les timestamps : + +```php +// src/Entity/Trait/CuidEntityTrait.php +trait CuidEntityTrait +{ + #[ORM\PrePersist] // ← S'exécute automatiquement AVANT l'insertion en BDD + public function generateId(): void + { + if (!isset($this->id)) { + $this->id = 'cl' . bin2hex(random_bytes(12)); // ← Génère un ID unique de 26 chars + } + $this->createdAt = new \DateTimeImmutable(); + $this->updatedAt = new \DateTimeImmutable(); + } + + #[ORM\PreUpdate] // ← S'exécute automatiquement AVANT une mise à jour + public function updateTimestamp(): void + { + $this->updatedAt = new \DateTimeImmutable(); + } +} +``` + +### Les entités du projet + +#### Entités "catalogue" (les éléments qu'on gère) + +| Entité | Table | Champs clés | Relations | +|--------|-------|-------------|-----------| +| **Machine** | `machine` | name, reference, prix | → Site, ↔ Constructeur, → Documents | +| **Composant** | `composant` | name, reference, description, prix, structure (JSON) | → ModelType, → Product, ↔ Constructeur | +| **Piece** | `piece` | name, reference, description, prix, productIds (JSON) | → ModelType, → Product, ↔ Constructeur | +| **Product** | `product` | name, reference, supplierPrice | → ModelType, ↔ Constructeur | + +#### Entités de classification + +| Entité | Table | Champs clés | Rôle | +|--------|-------|-------------|------| +| **Site** | `site` | name, contactName, contactPhone, contactAddress | Regrouper les machines par lieu | +| **Constructeur** | `constructeur` | name, email, phone | Fournisseurs/fabricants partagés | +| **ModelType** | `model_type` | name, code, category (enum), skeletons (JSON) | Catégoriser composants/pièces/produits | + +#### Entités de liaison hiérarchique (structure machine) + +| Entité | Rôle | Relations | +|--------|------|-----------| +| **MachineComponentLink** | Lie un composant à une machine | → Machine, → Composant, → parent (self) | +| **MachinePieceLink** | Lie une pièce à une machine | → Machine, → Piece, → parent composant | +| **MachineProductLink** | Lie un produit à une machine | → Machine, → Product, → parent (flexible) | + +Ces entités permettent la **structure arborescente** : un composant peut contenir des pièces, qui contiennent des produits. + +#### Entités de métadonnées + +| Entité | Rôle | +|--------|------| +| **CustomField** | Définition d'un champ personnalisé (nom, type, options) | +| **CustomFieldValue** | Valeur d'un champ personnalisé pour une entité donnée | +| **Document** | Fichier uploadé (PDF, image) rattaché à une entité | +| **AuditLog** | Entrée du journal d'audit (diff + snapshot) | +| **Comment** | Commentaire/ticket sur une fiche | +| **Profile** | Compte utilisateur (email, rôle, mot de passe hashé) | + +### Les relations entre entités (schéma simplifié) + +``` +Site ──1:N──► Machine ──1:N──► MachineComponentLink ──► Composant + │ │ + │ └──1:N──► MachinePieceLink ──► Piece + │ │ + │ └──1:N──► MachineProductLink ──► Product + │ + └──N:N──► Constructeur (via table de jointure) + +ModelType ──1:N──► Composant / Piece / Product + │ + └──► CustomField ──1:N──► CustomFieldValue + +Machine / Composant / Piece / Product ──1:N──► Document + ──1:N──► CustomFieldValue +``` + +--- + +## Les Controllers + +API Platform génère automatiquement les endpoints CRUD standard. Les controllers personnalisés gèrent les cas plus complexes. + +### Liste des controllers + +#### Authentification (3 controllers) + +**SessionProfileController** (`/api/session/profile`) — Login/Logout + +``` +POST /api/session/profile → Se connecter (payload: { profileId, password }) +GET /api/session/profile → Récupérer le profil connecté +DELETE /api/session/profile → Se déconnecter +``` + +**SessionProfilesController** (`/api/session/profiles`) — Liste des profils + +``` +GET /api/session/profiles → Liste tous les profils actifs (page de login) +``` + +**AdminProfileController** (`/api/admin/profiles`) — Administration des utilisateurs + +``` +GET /api/admin/profiles → Liste tous les profils (ADMIN only) +POST /api/admin/profiles → Créer un profil +PUT /api/admin/profiles/{id}/role → Changer le rôle d'un profil +PUT /api/admin/profiles/{id}/password → Réinitialiser un mot de passe +PUT /api/admin/profiles/{id}/deactivate → Désactiver un profil +``` + +#### Données et logique métier + +**MachineStructureController** — Structure hiérarchique des machines + +``` +GET /api/machines/{id}/structure → Récupérer l'arborescence complète +PATCH /api/machines/{id}/structure → Modifier l'arborescence +POST /api/machines/{id}/clone → Cloner une machine avec toute sa structure +``` + +**MachineCustomFieldsController** — Champs personnalisés machines + +``` +POST /api/machines/{id}/custom-fields/init → Initialiser les champs personnalisés manquants +``` + +**EntityHistoryController** — Historique d'audit par entité + +``` +GET /api/{entityType}/{id}/history → 200 derniers événements d'audit +``` + +**ActivityLogController** — Journal d'activité global + +``` +GET /api/activity-log → Liste paginée avec filtres (entityType, action) +``` + +**CommentController** — Commentaires/tickets + +``` +POST /api/comments → Créer un commentaire +PATCH /api/comments/{id}/resolve → Résoudre un commentaire +GET /api/comments/unresolved-count → Nombre de commentaires non résolus +``` + +**CustomFieldValueController** — Valeurs de champs personnalisés + +``` +POST /api/custom-field-values → Créer/mettre à jour une valeur (upsert) +DELETE /api/custom-field-values/{id} → Supprimer une valeur +``` + +#### Fichiers + +**DocumentQueryController** — Requêter les documents par entité + +``` +GET /api/documents/by-site/{id} → Documents d'un site +GET /api/documents/by-machine/{id} → Documents d'une machine +GET /api/documents/by-composant/{id} → Documents d'un composant +GET /api/documents/by-piece/{id} → Documents d'une pièce +GET /api/documents/by-product/{id} → Documents d'un produit +``` + +**DocumentServeController** — Servir les fichiers + +``` +GET /api/documents/{id}/file → Afficher le fichier (inline) +GET /api/documents/{id}/download → Télécharger le fichier (attachment) +``` + +#### Monitoring + +**HealthCheckController** — Vérification de santé + +``` +GET /api/health → Version, latence BDD, mémoire, version PHP +``` + +### Exemple de controller commenté + +```php +denyAccessUnlessGranted('ROLE_VIEWER'); // ← Vérifie que l'utilisateur est connecté + + $data = json_decode($request->getContent(), true); // ← Parse le body JSON + + $comment = new Comment(); + $comment->setContent($data['content']); + $comment->setEntityType($data['entityType']); + $comment->setEntityId($data['entityId']); + // ... autres champs + + $em->persist($comment); // ← Dit à Doctrine "je veux sauvegarder ça" + $em->flush(); // ← Exécute réellement le INSERT SQL + + return $this->json($comment, 201); // ← Renvoie le commentaire créé avec le code 201 + } +} +``` + +--- + +## Le système d'audit + +Chaque modification sur les entités principales est automatiquement enregistrée dans un journal d'audit. C'est un des points forts de l'application. + +### Comment ça marche ? + +Les **Event Subscribers** de Doctrine interceptent les opérations de base de données **avant** qu'elles soient exécutées (événement `onFlush`). + +``` +L'utilisateur modifie une machine + ↓ +Doctrine détecte le changement + ↓ +onFlush se déclenche + ↓ +Le subscriber calcule le diff (ancien → nouveau) + ↓ +Le subscriber crée un AuditLog avec : + - entityType : "machine" + - entityId : "cl1a2b3c..." + - action : "update" + - diff : { "name": { "from": "CNC 01", "to": "CNC 02" } } + - snapshot : { état complet de la machine } + - actorProfileId : "cl9z8y7x..." (qui a fait la modif) + ↓ +Les deux (machine + audit log) sont sauvegardés en même temps +``` + +### Le diff + +Le diff capture exactement ce qui a changé : + +```json +{ + "name": { "from": "CNC Mazak 01", "to": "CNC Mazak 02" }, + "prix": { "from": "45000.00", "to": "50000.00" }, + "constructeurIds": { + "from": ["cl111...", "cl222..."], + "to": ["cl111...", "cl333..."] + } +} +``` + +### Le snapshot + +Le snapshot capture l'état complet de l'entité au moment de la modification : + +```json +{ + "id": "cl1a2b3c...", + "name": "CNC Mazak 02", + "reference": "CNM-001", + "prix": "50000.00", + "siteId": "cl9z8y7x...", + "constructeurIds": ["cl111...", "cl333..."] +} +``` + +### Les subscribers d'audit + +| Subscriber | Entité | Type | +|------------|--------|------| +| MachineAuditSubscriber | Machine | Complex (avec constructeurs + custom fields) | +| ComposantAuditSubscriber | Composant | Complex | +| PieceAuditSubscriber | Piece | Complex | +| ProductAuditSubscriber | Product | Complex | +| ConstructeurAuditSubscriber | Constructeur | Simple | +| DocumentAuditSubscriber | Document | Simple | +| ModelTypeAuditSubscriber | ModelType | Simple | + +**Simple** = suit seulement les champs de l'entité +**Complex** = suit aussi les relations ManyToMany (constructeurs) et les champs personnalisés + +### AbstractAuditSubscriber + +La classe de base qui contient toute la logique partagée : + +```php +abstract class AbstractAuditSubscriber implements EventSubscriber +{ + // Méthode à implémenter par chaque subscriber + abstract protected function getEntityClass(): string; // Ex: Machine::class + abstract protected function getEntityType(): string; // Ex: 'machine' + abstract protected function buildSnapshot($entity): array; // Construit le snapshot + + // Deux chemins d'exécution : + // 1. onFlushSimple() : pour les entités sans collections ManyToMany + // 2. onFlushComplex() : pour les entités avec constructeurs (détecte les ajouts/suppressions) +} +``` + +### Autres subscribers + +| Subscriber | Rôle | +|------------|------| +| **PieceProductSyncSubscriber** | Synchronise le champ `productIds` sur Piece quand un Product est lié/délié | +| **UniqueConstraintSubscriber** | Capture les erreurs de doublon PostgreSQL et renvoie un message clair | + +--- + +## L'authentification par session + +### Le flux complet + +``` +1. GET /api/session/profiles + → Retourne la liste des profils actifs (nom, prénom, email, hasPassword) + → Le frontend affiche la page de login avec les profils disponibles + +2. POST /api/session/profile + Body: { "profileId": "cl...", "password": "secret" } + → Le backend vérifie le mot de passe + → Si OK : stocke profileId dans la session PHP, retourne le profil + → Si KO : retourne 401 + +3. GET /api/session/profile (à chaque chargement de page) + → Le navigateur envoie le cookie de session automatiquement + → Le backend retrouve le profil via la session + → Retourne le profil connecté ou 401 + +4. DELETE /api/session/profile + → Supprime le profileId de la session + → L'utilisateur est déconnecté +``` + +### La sécurité sur les endpoints + +Chaque endpoint API Platform a une règle de sécurité : + +```php +new GetCollection(security: "is_granted('ROLE_VIEWER')") // Lecture → minimum ROLE_VIEWER +new Post(security: "is_granted('ROLE_GESTIONNAIRE')") // Création → minimum ROLE_GESTIONNAIRE +``` + +Les controllers personnalisés utilisent : +```php +$this->denyAccessUnlessGranted('ROLE_VIEWER'); +``` + +### La hiérarchie des rôles + +Grâce à la hiérarchie, un ADMIN a automatiquement tous les rôles inférieurs : + +``` +ROLE_ADMIN ─── a aussi ──► ROLE_GESTIONNAIRE ──► ROLE_VIEWER ──► ROLE_USER +``` + +Donc `is_granted('ROLE_VIEWER')` accepte aussi les GESTIONNAIRES et les ADMINS. + +--- + +## Les services + +### DocumentStorageService + +Gère le stockage des fichiers sur le système de fichiers : + +```php +// Stocker un fichier uploadé +$path = $storageService->store($uploadedFile, $entityType, $entityId); + +// Supprimer un fichier +$storageService->delete($path); +``` + +Les fichiers sont stockés dans `var/documents/{entityType}/{entityId}/{filename}`. + +### PdfCompressorService + +Compresse les fichiers PDF via Ghostscript pour réduire leur taille : + +```php +$compressorService->compress($filePath); +``` + +### ModelTypeCategoryConversionService + +Permet de convertir la catégorie d'un ModelType (ex: transformer un type "composant" en type "pièce"). + +--- + +## Les migrations + +Les migrations sont des scripts SQL qui modifient la structure de la base de données. Elles sont dans le dossier `migrations/`. + +### Principe + +Quand tu ajoutes un champ à une entité, il faut créer une migration pour mettre à jour la BDD : + +```bash +# Générer une migration à partir des changements détectés +make shell +php bin/console doctrine:migrations:diff + +# Appliquer les migrations +php bin/console doctrine:migrations:migrate +``` + +### Particularités PostgreSQL + +Les migrations utilisent du **SQL brut** avec des gardes pour l'idempotence : + +```sql +-- On peut relancer la migration sans erreur +ALTER TABLE machine ADD COLUMN IF NOT EXISTS description TEXT; +DROP INDEX IF EXISTS idx_machine_name; +CREATE UNIQUE INDEX IF NOT EXISTS idx_machine_name ON machine (name); +``` + +**Attention aux noms de colonnes** : PostgreSQL stocke tout en **minuscules**. Donc `typePieceId` en PHP devient `typepieceid` en SQL. Toujours utiliser des noms lowercase dans le SQL brut. + +--- + +## Les tests + +### Stack de test + +- **PHPUnit 12** : framework de test PHP +- **API Platform Test** : utilitaires pour tester des endpoints API +- **DAMA DoctrineTestBundle** : wrappe chaque test dans une transaction avec rollback automatique (pas besoin de nettoyer la BDD entre les tests) + +### Structure + +``` +tests/ +├── AbstractApiTestCase.php # Classe de base avec helpers +└── Api/ + └── Entity/ + ├── MachineTest.php # Tests des endpoints machine + ├── SiteTest.php # Tests des endpoints site + └── ... +``` + +### Exemple de test + +```php +class MachineTest extends AbstractApiTestCase +{ + public function testCreateMachine(): void + { + // Créer un client HTTP connecté en tant que gestionnaire + $client = $this->createGestionnaireClient(); + + // Créer un site (prérequis) + $site = $this->createSite(); + + // Envoyer une requête POST pour créer une machine + $client->request('POST', '/api/machines', [ + 'json' => [ + 'name' => 'Machine Test', + 'reference' => 'MT-001', + 'site' => '/api/sites/' . $site->getId(), + ], + ]); + + // Vérifier que la réponse est 201 Created + $this->assertResponseStatusCodeSame(201); + + // Vérifier le contenu de la réponse + $this->assertJsonContains([ + 'name' => 'Machine Test', + 'reference' => 'MT-001', + ]); + } +} +``` + +### Helpers disponibles dans AbstractApiTestCase + +| Méthode | Description | +|---------|-------------| +| `createViewerClient()` | Client HTTP connecté avec ROLE_VIEWER | +| `createGestionnaireClient()` | Client HTTP connecté avec ROLE_GESTIONNAIRE | +| `createAdminClient()` | Client HTTP connecté avec ROLE_ADMIN | +| `createProfile()` | Crée un profil utilisateur en BDD | +| `createSite()` | Crée un site en BDD | +| `createMachine()` | Crée une machine en BDD | + +### Lancer les tests + +```bash +make test # Tous les tests +make test FILES=tests/Api/Entity/MachineTest.php # Un fichier +make test-setup # (Re)créer la BDD de test +``` + +--- + +## Flux complet d'une requête + +### Exemple : créer une machine + +``` +1. Le frontend envoie : + POST /api/machines + Content-Type: application/ld+json + Cookie: PHPSESSID=abc123 + { + "name": "CNC Mazak 01", + "reference": "CNM-001", + "prix": "50000.00", + "site": "/api/sites/cl9z8y7x..." + } + +2. Symfony reçoit la requête + → Le routeur identifie : c'est un endpoint API Platform (POST sur Machine) + +3. Sécurité + → Vérifie le cookie de session → retrouve le profil connecté + → Vérifie is_granted('ROLE_GESTIONNAIRE') → OK + +4. Désérialisation (JSON → objet PHP) + → API Platform convertit le JSON en objet Machine + → Le champ "site" (IRI) est résolu en objet Site + → Seuls les champs du groupe 'machine:write' sont acceptés + +5. Validation + → Vérifie les contraintes (name non vide, site existe, etc.) + +6. Persistence (objet PHP → BDD) + → Doctrine déclenche PrePersist (CuidEntityTrait) + → Génère l'ID : "cl" + 24 hex chars aléatoires + → Set createdAt et updatedAt + → Doctrine détecte l'INSERT à faire + +7. Audit (onFlush) + → MachineAuditSubscriber détecte la nouvelle machine + → Crée un AuditLog avec action='create', diff, snapshot + → L'AuditLog est aussi ajouté à la transaction + +8. Flush + → Doctrine exécute les requêtes SQL : + INSERT INTO machine (id, name, reference, ...) VALUES (...) + INSERT INTO audit_log (id, entity_type, entity_id, action, diff, snapshot, ...) VALUES (...) + +9. Sérialisation (objet PHP → JSON) + → API Platform convertit la Machine en JSON-LD + → Seuls les champs du groupe 'machine:read' sont inclus + +10. Réponse + HTTP/1.1 201 Created + { + "@context": "/api/contexts/Machine", + "@id": "/api/machines/cl1a2b3c...", + "@type": "Machine", + "id": "cl1a2b3c...", + "name": "CNC Mazak 01", + ... + } +``` + +--- + +## Commandes Symfony utiles + +Lancer ces commandes dans le conteneur Docker (`make shell` pour y entrer) : + +| Commande | Description | +|----------|-------------| +| `php bin/console debug:router` | Voir toutes les routes disponibles | +| `php bin/console debug:config api_platform` | Voir la config API Platform | +| `php bin/console doctrine:schema:validate` | Vérifier que les entités sont synchronisées avec la BDD | +| `php bin/console doctrine:migrations:diff` | Générer une migration à partir des changements | +| `php bin/console doctrine:migrations:migrate` | Appliquer les migrations | +| `php bin/console cache:clear` | Vider le cache (résout beaucoup de problèmes) | +| `php bin/console app:compress-pdf` | Compresser les PDFs existants | +| `php bin/console app:create-profile` | Créer un profil utilisateur | + +--- + +## Résumé des points clés pour un débutant + +1. **API Platform génère les endpoints CRUD automatiquement** à partir des entités — tu n'as pas besoin d'écrire de controllers pour les opérations standard +2. **Les attributs PHP 8** (`#[...]`) remplacent les annotations et configurent tout : BDD, API, sérialisation, sécurité +3. **Les groupes de sérialisation** (`machine:read`, `machine:write`) contrôlent quels champs sont visibles/modifiables +4. **L'audit est automatique** : chaque modification est tracée sans rien avoir à faire manuellement +5. **L'authentification est par session (cookies)**, pas par tokens JWT +6. **Les IDs sont des CUID** (chaînes aléatoires), pas des auto-increment +7. **PostgreSQL stocke les noms en minuscules** : attention dans le SQL brut +8. **Les tests utilisent des transactions** : chaque test est isolé et la BDD est nettoyée automatiquement diff --git a/docs/FRONTEND.md b/docs/FRONTEND.md new file mode 100644 index 0000000..2cf0a6e --- /dev/null +++ b/docs/FRONTEND.md @@ -0,0 +1,1052 @@ +# Guide Frontend — Inventory + +Guide complet du frontend Nuxt/Vue pour comprendre comment tout fonctionne, même si tu débutes. + +## Table des matières + +1. [Vue d'ensemble](#vue-densemble) +2. [Nuxt et Vue — les bases](#nuxt-et-vue--les-bases) +3. [L'architecture de l'application](#larchitecture-de-lapplication) +4. [Les Pages (le routing)](#les-pages) +5. [Les Composables (la logique métier)](#les-composables) +6. [Les Composants (l'interface)](#les-composants) +7. [L'API et les appels HTTP](#lapi-et-les-appels-http) +8. [L'authentification côté frontend](#lauthentification-côté-frontend) +9. [Le style (TailwindCSS + DaisyUI)](#le-style) +10. [Les utilitaires et types](#les-utilitaires-et-types) +11. [Flux complet d'une fonctionnalité](#flux-complet-dune-fonctionnalité) +12. [Patterns et conventions du projet](#patterns-et-conventions) +13. [Les tests](#les-tests) +14. [Commandes utiles](#commandes-utiles) + +--- + +## Vue d'ensemble + +Le frontend est une **SPA** (Single Page Application) construite avec : + +- **Nuxt 4** : framework basé sur Vue.js qui ajoute le routing automatique, les composables, et plein d'outils +- **Vue 3** : la librairie d'interface (composants, réactivité, etc.) +- **TypeScript** : JavaScript avec des types (réduit les bugs) +- **TailwindCSS 4** : des classes CSS utilitaires (`flex`, `p-4`, `text-lg`, etc.) +- **DaisyUI 5** : des composants visuels prêts à l'emploi (boutons, modales, tableaux, etc.) + +### SPA, c'est quoi ? + +Dans une SPA, le navigateur charge **une seule page HTML** au départ, puis tout se passe en JavaScript : la navigation entre les pages, le chargement de données, etc. Pas de rechargement complet de la page. + +> **SSR est désactivé** (`ssr: false` dans `nuxt.config.ts`). Le rendu se fait entièrement côté client (dans le navigateur). + +--- + +## Nuxt et Vue — les bases + +### Vue 3 en 30 secondes + +Vue utilise des **composants** : des blocs réutilisables qui combinent HTML, JavaScript et CSS. + +```vue + + + +``` + +### Les concepts clés de Vue 3 + +| Concept | Syntaxe | Description | +|---------|---------|-------------| +| **ref** | `const x = ref(0)` | Variable réactive. Quand elle change, l'interface se met à jour | +| **computed** | `const y = computed(() => x.value * 2)` | Valeur calculée, se recalcule automatiquement | +| **v-model** | `` | Lie un input à une variable (bidirectionnel) | +| **v-if** | `
` | Afficher conditionnellement | +| **v-for** | `
` | Boucler sur une liste | +| **@event** | ` +//
Section admin
+``` + +#### useMachines.ts — CRUD machines + +Pattern typique : un composable par domaine métier. + +```typescript +const { + machines, // ref — la liste des machines + loading, // ref — en cours de chargement ? + loaded, // ref — déjà chargé ? + loadMachines, // () => Promise + createMachine, // (data) => Promise + updateMachine, // (id, data) => Promise + deleteMachine, // (id) => Promise + cloneMachine, // (sourceId, data) => Promise +} = useMachines() +``` + +Ce pattern se répète pour `useComposants`, `usePieces`, `useProducts`, `useConstructeurs`, `useSites`, etc. + +#### useDataTable.ts — Table de données générique + +Gère la pagination, le tri, la recherche et les filtres pour toutes les pages catalogue : + +```typescript +const { + sort, // { field: 'name', direction: 'asc' } + pagination, // { currentPage: 1, totalPages: 5, perPage: 30, ... } + handleSort, // (field) => void + handlePageChange, // (page) => void + handlePerPageChange, // (perPage) => void +} = useDataTable({ ... }) +``` + +#### useConfirm.ts — Modale de confirmation + +```typescript +const { confirm } = useConfirm() + +// Afficher une modale et attendre la réponse +const ok = await confirm({ + title: 'Supprimer la machine ?', + message: 'Cette action est irréversible.', +}) +if (ok) { + // l'utilisateur a cliqué "Confirmer" +} +``` + +#### useToast.ts — Notifications + +```typescript +const { showSuccess, showError, showWarning, showInfo } = useToast() + +showSuccess('Machine créée avec succès') +showError('Erreur lors de la suppression') +``` + +#### useMachineDetailData.ts — Le composable le plus complexe + +Gère toute la logique de la page de détail d'une machine : +- Chargement des données (machine, composants, pièces, produits, documents, custom fields) +- Mode édition (toggle) +- Mise à jour des champs +- Ajout/suppression de liens (composant, pièce, produit) +- Upload/suppression de documents + +#### useDocuments.ts — Gestion de fichiers + +```typescript +const { uploadDocuments, deleteDocument, loadDocumentsByMachine } = useDocuments() + +// Upload de fichiers +await uploadDocuments('machine', machineId, fileList) + +// Suppression +await deleteDocument(documentId) +``` + +#### useEntityHistory.ts — Historique d'audit + +```typescript +const { history, loading, loadHistory } = useEntityHistory() + +await loadHistory('machine', machineId) +// history.value → liste des événements d'audit +``` + +### Pattern des composables (convention du projet) + +Certains composables utilisent une injection de dépendances explicite : + +```typescript +// Définir les dépendances nécessaires +interface Deps { + machineId: Ref + onSave: () => void +} + +// Le composable reçoit ses dépendances +export function useMachineDetail(deps: Deps) { + const { machineId, onSave } = deps + + // ... logique utilisant machineId et onSave +} +``` + +--- + +## Les Composants + +### Auto-import par Nuxt + +Tous les composants dans `components/` sont auto-importés. Pas besoin de `import` : + +```vue + + +``` + +La convention de nommage utilise le chemin du dossier : +- `components/common/DataTable.vue` → `` +- `components/machine/InfoCard.vue` → `` +- `components/layout/AppNavbar.vue` → `` + +### Les composants communs (réutilisables) + +#### CommonDataTable + +Le composant central pour tous les tableaux avec pagination, tri et recherche : + +```vue + +``` + +#### CommonConfirmModal + +Modale de confirmation globale (utilisée via `useConfirm()`) : + +```vue + + +``` + +#### CommonSearchSelect + +Dropdown avec recherche intégrée : + +```vue + +``` + +#### CommonPagination + +Composant de pagination : + +```vue + +``` + +### Les composants machine + +| Composant | Rôle | +|-----------|------| +| `MachineDetailHeader` | En-tête avec boutons (éditer, imprimer) | +| `MachineInfoCard` | Carte avec les infos de la machine (nom, référence, constructeur) | +| `MachineDocumentsCard` | Upload et prévisualisation des documents | +| `MachineComponentsCard` | Liste des composants liés (hiérarchique) | +| `MachinePiecesCard` | Liste des pièces liées | +| `MachineProductsCard` | Liste des produits liés | +| `AddEntityToMachineModal` | Modale pour ajouter un composant/pièce/produit | + +### Les composants sites + +| Composant | Rôle | +|-----------|------| +| `SiteCard` | Carte d'affichage d'un site | +| `SiteCreateModal` | Modale de création de site | +| `SiteEditModal` | Modale d'édition de site | +| `SiteContactFormFields` | Champs de formulaire contact (réutilisable) | + +### Les composants model-types + +| Composant | Rôle | +|-----------|------| +| `ManagementView` | Page principale de gestion des types | +| `Table` | Tableau des types/catégories | +| `Toolbar` | Barre de recherche + filtres | +| `ModelTypeForm` | Formulaire d'édition/création | +| `ConversionModal` | Modale de conversion de catégorie | + +### Communication entre composants + +Le projet utilise **exclusivement Props + Events** (pas de `provide/inject`) : + +```vue + + + + + +``` + +--- + +## L'API et les appels HTTP + +### Le composable useApi.ts + +Tous les appels API passent par `useApi.ts`. Voici ce qu'il fait : + +1. **Ajoute le base URL** : `/machines` → `http://localhost:8081/api/machines` +2. **Ajoute les headers** : + - `Content-Type: application/ld+json` (POST/PUT) + - `Content-Type: application/merge-patch+json` (PATCH) +3. **Inclut les cookies** : `credentials: 'include'` +4. **Gère les erreurs** : parse les erreurs backend et les traduit en français +5. **Retourne un objet standardisé** : `{ success, data, error, status }` + +### Les IRIs (Internationalized Resource Identifiers) + +L'API utilise des IRIs pour les relations. Au lieu de passer un simple ID, on passe le chemin complet : + +```typescript +// ❌ Ne pas faire +{ "site": "cl9z8y7x..." } + +// ✅ Faire +{ "site": "/api/sites/cl9z8y7x..." } +``` + +Le fichier `shared/utils/apiRelations.ts` fournit des helpers : + +```typescript +import { toIri, extractRelationId, normalizeRelationIds } from '~/shared/utils/apiRelations' + +// Construire un IRI +toIri('sites', 'cl9z8y7x...') // → "/api/sites/cl9z8y7x..." + +// Extraire l'ID d'un IRI +extractRelationId('/api/sites/cl9z8y7x...') // → "cl9z8y7x..." + +// Convertir les IDs locaux en IRIs dans un payload +normalizeRelationIds({ + siteId: 'cl9z8y7x...', + constructeurIds: ['cl111...', 'cl222...'], +}) +// → { site: "/api/sites/cl9z8y7x...", constructeurs: ["/api/constructeurs/cl111...", ...] } +``` + +### Les collections API (pagination) + +Les réponses de collection suivent le format Hydra : + +```json +{ + "hydra:totalItems": 42, + "hydra:member": [ + { "id": "cl...", "name": "Machine 1" }, + { "id": "cl...", "name": "Machine 2" } + ] +} +``` + +Le helper `extractCollection()` gère les différents formats : + +```typescript +import { extractCollection } from '~/shared/utils/apiHelpers' + +const result = await api.get('/machines?page=1') +const machines = extractCollection(result.data) // → Machine[] +``` + +### Les Content-Types + +| Opération | Content-Type | Quand | +|-----------|-------------|-------| +| POST | `application/ld+json` | Créer une ressource | +| PUT | `application/ld+json` | Remplacer une ressource | +| PATCH | `application/merge-patch+json` | Modifier partiellement | +| Upload fichier | `multipart/form-data` | Envoyer un fichier | + +--- + +## L'authentification côté frontend + +### Le flux complet + +``` +1. L'utilisateur ouvre l'application + ↓ +2. Le middleware profile.global.ts s'exécute + ↓ +3. Il appelle ensureSession() → GET /api/session/profile + ↓ +4a. Si la session est valide → l'utilisateur voit la page demandée +4b. Si pas de session → redirection vers /profiles (page de login) + ↓ +5. L'utilisateur choisit son profil et entre son mot de passe + ↓ +6. POST /api/session/profile → le backend crée la session + ↓ +7. Redirection vers la page d'accueil +``` + +### Le middleware global + +```typescript +// middleware/profile.global.ts +export default defineNuxtRouteMiddleware(async (to) => { + // Ne pas vérifier la session si on est déjà sur la page de login + if (to.path === '/profiles') return + + const { ensureSession, activeProfile } = useProfileSession() + await ensureSession() + + // Pas de session → page de login + if (!activeProfile.value) { + return navigateTo('/profiles') + } + + // Routes admin → vérifier le rôle + if (to.path.startsWith('/admin') && !isAdmin.value) { + return navigateTo('/') + } +}) +``` + +### Les permissions dans les templates + +```vue + + + +``` + +--- + +## Le style + +### TailwindCSS 4 + +TailwindCSS utilise des classes utilitaires au lieu de CSS personnalisé : + +```html + +
+ + +
+``` + +**Classes les plus utilisées :** + +| Catégorie | Classes | Description | +|-----------|---------|-------------| +| Layout | `flex`, `grid`, `block`, `hidden` | Type d'affichage | +| Espacement | `p-4`, `px-2`, `py-6`, `m-2`, `gap-4` | Padding, margin, gap | +| Taille | `w-full`, `h-10`, `max-w-lg`, `min-h-screen` | Largeur, hauteur | +| Texte | `text-lg`, `text-sm`, `font-bold`, `text-gray-500` | Taille, poids, couleur | +| Fond | `bg-white`, `bg-blue-100`, `bg-base-200` | Couleur de fond | +| Bordure | `border`, `rounded-lg`, `border-gray-300` | Bordures | +| Responsive | `md:flex`, `lg:hidden`, `sm:p-2` | Styles conditionnels par taille d'écran | + +### DaisyUI 5 + +DaisyUI ajoute des classes de composants par-dessus Tailwind : + +```html + + + + + + + + + + + + + +
+
+

Titre

+

Contenu

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