Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51248b7854 | ||
|
|
0e11f4ad2d | ||
|
|
f2539099bc | ||
|
|
e5dc60467e | ||
|
|
fbc0372bd6 | ||
|
|
1483b0075b | ||
|
|
74e88923dc | ||
|
|
ef61d1a0d3 | ||
|
|
3f0fb0d5c2 | ||
|
|
dd1497beac | ||
|
|
7cd8772617 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -32,6 +32,12 @@ docker/.env.docker.local
|
||||
/_archives/
|
||||
###< migration archives ###
|
||||
|
||||
###> temp files ###
|
||||
*.sql
|
||||
*.har
|
||||
FEATURE_IDEAS.md
|
||||
###< temp files ###
|
||||
|
||||
###> frontend ###
|
||||
/frontend/node_modules/
|
||||
/frontend/.nuxt/
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
# 📔 Carnet de Bord - Migration Inventory → Symfony
|
||||
|
||||
**Projet** : Migration backend NestJS/Prisma → Symfony/API Platform
|
||||
**Début** : 2026-01-10
|
||||
**Objectif** : Migrer vers Symfony + JWT + API Platform propre et maintenable
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Convention de liaison des commits (INV)
|
||||
|
||||
- **Format** : `[INV-YYYYMMDD-XX]`
|
||||
- **Usage** : même code dans les commits du backend **et** du frontend + ajout ici pour retrouver le duo rapidement.
|
||||
|
||||
## 🧾 Journal des liaisons INV
|
||||
|
||||
- INV-20260111-01 : ajout du lien submodule `Inventory_frontend` (commit backend : `987aa5c`, commit frontend : `936a73f`)
|
||||
- INV-20260111-02 : alignement front API Platform + sessions (commit backend : `f7fc1bd`, commit frontend : `e99f053`)
|
||||
|
||||
## 🎯 Contexte
|
||||
|
||||
- **Situation initiale** :
|
||||
- `Inventory_backend/` : NestJS + Prisma (fonctionnel, ~11k lignes)
|
||||
- `Inventory_frontend/` : Nuxt 3 (fonctionnel, 105 fichiers)
|
||||
- Base de données PostgreSQL avec données en production
|
||||
|
||||
- **Objectif** :
|
||||
- Backend Symfony 8 + API Platform + JWT
|
||||
- Garder les données existantes (migration Prisma → Doctrine)
|
||||
- Frontend Nuxt connecté au nouveau backend
|
||||
- Docker : 2 backends en parallèle pendant transition
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 1 : Préparation (TERMINÉE - 10/01/2026)
|
||||
|
||||
### Ce qui a été fait
|
||||
|
||||
#### 1. Docker & Infrastructure ✅
|
||||
- **pgAdmin ajouté** au docker-compose.yml
|
||||
- Port : 5050
|
||||
- Login : admin@admin.com / admin
|
||||
- Container : `pgadmin-inventory`
|
||||
- Volume persistant : `pgadmin_data`
|
||||
- **Serveur PostgreSQL pré-configuré** :
|
||||
- Fichier `docker/pgadmin/servers.json` monté automatiquement
|
||||
- Fichier `docker/pgadmin/pgpass` pour authentification sans mot de passe
|
||||
- Connexion automatique à `db:5432/inventory` au démarrage
|
||||
- Nom du serveur : "Inventory PostgreSQL"
|
||||
|
||||
#### 2. Bundles Symfony installés ✅
|
||||
```bash
|
||||
# Versions installées
|
||||
- lexik/jwt-authentication-bundle: v3.2.0
|
||||
- vich/uploader-bundle: v2.9.1
|
||||
- symfony/uid: 8.0.*
|
||||
```
|
||||
|
||||
#### 3. JWT Configuration ✅
|
||||
- **Clés RSA générées** : `config/jwt/private.pem` + `public.pem`
|
||||
- **security.yaml configuré** :
|
||||
- Firewall `login` : pattern `^/api/login_check` avec `json_login`
|
||||
- Firewall `api` : pattern `^/api` avec `jwt` authenticator
|
||||
- Provider : `app_user_provider` (entité Profile via email)
|
||||
- Password hasher : bcrypt auto
|
||||
|
||||
#### 4. Entité Profile créée ✅
|
||||
**Fichier** : `src/Entity/Profile.php`
|
||||
|
||||
**Caractéristiques** :
|
||||
- Implémente `UserInterface` + `PasswordAuthenticatedUserInterface`
|
||||
- Champs :
|
||||
- `id` : string (30 chars, CUID-compatible pour Prisma)
|
||||
- `email` : string unique (username pour JWT)
|
||||
- `password` : string (hashed)
|
||||
- `roles` : array JSON (ROLE_USER par défaut)
|
||||
- `firstName`, `lastName` : string
|
||||
- `isActive` : boolean
|
||||
- `createdAt`, `updatedAt` : DateTimeImmutable
|
||||
- Repository : `ProfileRepository` avec `PasswordUpgraderInterface`
|
||||
- API Platform : endpoints CRUD auto-générés
|
||||
|
||||
#### 5. Base de Données ✅
|
||||
- **Migration créée** : `Version20260110175413`
|
||||
- **Table** : `profiles` créée avec succès
|
||||
- **Utilisateur test créé** :
|
||||
```
|
||||
Email: admin@admin.com
|
||||
Password: admin123
|
||||
Roles: ['ROLE_USER', 'ROLE_ADMIN']
|
||||
```
|
||||
|
||||
#### 6. API Platform ✅
|
||||
- **Endpoint racine** : http://localhost:8081/api/
|
||||
- **Réponse** :
|
||||
```json
|
||||
{
|
||||
"@context": "/api/contexts/Entrypoint",
|
||||
"@id": "/api/",
|
||||
"@type": "Entrypoint",
|
||||
"profile": "/api/profiles"
|
||||
}
|
||||
```
|
||||
- **OpenAPI Docs** : Configurées (à tester)
|
||||
|
||||
#### 7. Configuration Apache ✅
|
||||
- **VirtualHost** : `docker/php/config/vhost.conf`
|
||||
- **DocumentRoot** : `/var/www/html/public`
|
||||
- **AllowOverride** : All (pour `.htaccess`)
|
||||
- **Port** : 8081 (Apache) → accessible depuis l'hôte
|
||||
|
||||
#### 8. Routing Symfony ✅
|
||||
- **Routes définies** :
|
||||
- `/api/login_check` : Login JWT
|
||||
- `/api/test` : Test endpoint (TestController)
|
||||
- `/api/*` : API Platform auto-routes
|
||||
- **Vérification** :
|
||||
```bash
|
||||
php bin/console debug:router api_test
|
||||
php bin/console router:match /api/test --method=GET
|
||||
# ✅ Route found and matches
|
||||
```
|
||||
|
||||
#### 9. .htaccess créé ✅
|
||||
**Fichier** : `public/.htaccess`
|
||||
|
||||
**Contenu** : Symfony standard avec mod_rewrite
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ Problèmes identifiés
|
||||
|
||||
#### 1. Routes inaccessibles via Apache (404)
|
||||
|
||||
**Symptôme** :
|
||||
```bash
|
||||
curl http://localhost:8081/api/test
|
||||
# → 404 Not Found
|
||||
```
|
||||
|
||||
**Tests effectués** :
|
||||
- ✅ Route existe : `php bin/console debug:router api_test`
|
||||
- ✅ Route match : `php bin/console router:match /api/test`
|
||||
- ✅ Symfony fonctionne : `curl http://localhost:8081/api/` → JSON OK
|
||||
- ✅ PHP built-in server OK :
|
||||
```bash
|
||||
php -S localhost:9000
|
||||
curl http://localhost:9000/api/test
|
||||
# → {"status":"ok","message":"Test endpoint works!"}
|
||||
```
|
||||
- ❌ Apache 404 : Depuis l'hôte via port 8081
|
||||
|
||||
**Diagnostic** :
|
||||
- Le problème est **Apache-spécifique**
|
||||
- Symfony/PHP fonctionnent correctement
|
||||
- Le `.htaccess` n'est probablement **PAS lu par Apache**
|
||||
- Hypothèses :
|
||||
1. `AllowOverride All` non pris en compte
|
||||
2. `mod_rewrite` mal configuré
|
||||
3. Ordre des directives Apache incorrect
|
||||
4. Problème de permissions sur `.htaccess`
|
||||
|
||||
**Actions à faire** :
|
||||
- [ ] Vérifier permissions `.htaccess` dans container
|
||||
- [ ] Tester `apache2ctl configtest`
|
||||
- [ ] Activer logs de rewrite : `LogLevel alert rewrite:trace3`
|
||||
- [ ] Tester FallbackResource dans vhost au lieu de `.htaccess`
|
||||
|
||||
#### 2. JWT Login non testé
|
||||
|
||||
**Raison** : Bloqué par problème #1 (routes inaccessibles)
|
||||
|
||||
**Actions à faire** :
|
||||
- [ ] Résoudre problème Apache
|
||||
- [ ] Tester `POST /api/login_check` avec credentials
|
||||
- [ ] Vérifier génération du token JWT
|
||||
- [ ] Tester route protégée avec token
|
||||
|
||||
---
|
||||
|
||||
## 📝 Configuration Actuelle
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
ports:
|
||||
- "8081:80" # Symfony API
|
||||
- "3001:3000" # (prévu pour Nuxt)
|
||||
|
||||
db:
|
||||
ports:
|
||||
- "5433:5432" # PostgreSQL
|
||||
|
||||
pgadmin:
|
||||
ports:
|
||||
- "5050:80" # pgAdmin
|
||||
```
|
||||
|
||||
### Variables d'Environnement
|
||||
|
||||
**Fichier** : `docker/.env.docker.local`
|
||||
|
||||
```env
|
||||
# PostgreSQL
|
||||
POSTGRES_DB=inventory
|
||||
POSTGRES_USER=root
|
||||
POSTGRES_PASSWORD=root
|
||||
POSTGRES_PORT=5433
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 2 : Migration DB + Frontend (TERMINÉE - 10/01/2026)
|
||||
|
||||
### Ce qui a été fait
|
||||
|
||||
#### 1. Entités Doctrine alignées Prisma ✅
|
||||
- **Toutes les entités manquantes** créées (Machine, ModelType, Composant, Piece, Product, Links, Requirements, CustomField, Document, etc.)
|
||||
- **IDs en string(36)** pour compatibilité CUID/UUID
|
||||
- **Colonnes Prisma en camelCase** conservées via `name="..."` (ex: `machineId`, `createdAt`, `supplierPrice`)
|
||||
- **Corrections** :
|
||||
- `Document.path` passé en `TEXT`
|
||||
- `CustomField.options` nullable
|
||||
- `TypeMachineComponentRequirement.required` corrigé
|
||||
|
||||
#### 2. Migration DB inventory-data → inventory ✅
|
||||
- **Dump data-only + normalisation** (conversion des identifiants quoted vers lowercase)
|
||||
- **Mapping table Prisma** : `"ModelType"` → `model_types`
|
||||
- **Exclusions** : `profiles`, `_prisma_migrations`
|
||||
- **Import validé** : `Counts match for all tables.`
|
||||
|
||||
Scripts utiles :
|
||||
```bash
|
||||
scripts/normalize-dump.py
|
||||
scripts/validate-migration.php
|
||||
```
|
||||
|
||||
#### 3. Frontend basculé sur Inventory_frontend ✅
|
||||
- `make dev-nuxt` pointe vers `Inventory_frontend/`
|
||||
- `README.md` mis à jour
|
||||
- **Base API** ajustée : `http://localhost:8081/api`
|
||||
|
||||
Fichiers modifiés :
|
||||
```
|
||||
makefile
|
||||
README.md
|
||||
Inventory_frontend/.env
|
||||
Inventory_frontend/nuxt.config.ts
|
||||
Inventory_frontend/app/services/modelTypes.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# pgAdmin
|
||||
PGADMIN_EMAIL=admin@admin.com
|
||||
PGADMIN_PASSWORD=admin
|
||||
PGADMIN_PORT=5050
|
||||
|
||||
# Symfony
|
||||
APP_ENV=dev
|
||||
APP_SECRET=changeme_super_secret_key_123456789
|
||||
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||
JWT_PASSPHRASE=your_jwt_passphrase_change_me
|
||||
|
||||
# NestJS (pour futur parallèle)
|
||||
NESTJS_PORT=3000
|
||||
SESSION_SECRET=changeme_session_secret
|
||||
CORS_ORIGIN=http://localhost:3001
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Phase 2 : Debugging & Tests (EN COURS)
|
||||
|
||||
### Objectifs
|
||||
- [x] Résoudre problème Apache `.htaccess`
|
||||
- [ ] Tester authentification JWT complète
|
||||
- [ ] Créer endpoint de test public fonctionnel
|
||||
- [ ] Documenter la solution Apache
|
||||
|
||||
### Prochaines étapes
|
||||
1. **Fix Apache** : Logs de debug + test FallbackResource
|
||||
2. **Test JWT** : Login + génération token + route protégée
|
||||
3. **Documentation** : Documenter la config Apache qui fonctionne
|
||||
|
||||
---
|
||||
|
||||
## 📊 Métriques
|
||||
|
||||
### Temps passé
|
||||
- **Phase 1** : ~3h (exploration + setup + debugging)
|
||||
- **Problème Apache** : ~1h30 (en cours)
|
||||
|
||||
### Fichiers créés/modifiés
|
||||
|
||||
**Nouveaux fichiers** :
|
||||
- `src/Entity/Profile.php`
|
||||
- `src/Repository/ProfileRepository.php`
|
||||
- `src/Controller/TestController.php`
|
||||
- `public/.htaccess`
|
||||
- `config/routes/routing.controllers.yaml`
|
||||
- `create_test_user.php` (script utilitaire)
|
||||
- `migrations/Version20260110175413.php`
|
||||
- `docker/pgadmin/servers.json` (config serveur PostgreSQL)
|
||||
- `docker/pgadmin/pgpass` (credentials PostgreSQL)
|
||||
- `CARNET_DE_BORD.md` (ce fichier)
|
||||
|
||||
**Fichiers modifiés** :
|
||||
- `docker-compose.yml` (+ pgAdmin)
|
||||
- `docker/.env.docker.local` (+ variables Symfony/JWT/pgAdmin)
|
||||
- `docker/php/config/vhost.conf` (DocumentRoot → public/)
|
||||
- `config/packages/security.yaml` (JWT firewalls)
|
||||
- `config/routes.yaml` (+ api_login_check)
|
||||
- `composer.json` (+ lexik JWT, vich uploader)
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Leçons Apprises
|
||||
|
||||
### 1. Symfony 8 + API Platform
|
||||
- **Attributs PHP 8** : `use Symfony\Component\Routing\Attribute\Route;` (pas `Annotation`)
|
||||
- **Routes controllers** : Nécessite `config/routes/routing.controllers.yaml` avec `type: attribute`
|
||||
- **API Platform** : Auto-génère les endpoints CRUD avec `#[ApiResource]`
|
||||
|
||||
### 2. JWT Authentication
|
||||
- **3 composants** :
|
||||
1. Firewall `login` : `json_login` intercepte `/api/login_check`
|
||||
2. Firewall `api` : `jwt` vérifie le token sur `/api/*`
|
||||
3. Access control : `PUBLIC_ACCESS` vs `IS_AUTHENTICATED_FULLY`
|
||||
- **username_path** : Permet de mapper `email` au lieu de `username`
|
||||
- **Provider** : Doit être défini dans le firewall `login`
|
||||
|
||||
### 3. Doctrine Migrations
|
||||
- **ID Prisma CUID** : Garder en `string(30)` pour compatibilité
|
||||
- **Lifecycle callbacks** : `#[ORM\PrePersist]` pour `createdAt`/`updatedAt`
|
||||
- **UserInterface** : Nécessite `getUserIdentifier()`, `getRoles()`, `eraseCredentials()`
|
||||
|
||||
### 4. Docker & Apache
|
||||
- **`.htaccess` vs VirtualHost** : Le vhost peut override le `.htaccess`
|
||||
- **AllowOverride All** : Indispensable pour que `.htaccess` fonctionne
|
||||
- **FallbackResource** : Alternative au mod_rewrite dans `.htaccess`
|
||||
- **Debugging** : Tester avec PHP built-in server pour isoler le problème
|
||||
|
||||
---
|
||||
|
||||
## 📚 Ressources Utiles
|
||||
|
||||
### Accès aux Services
|
||||
|
||||
```
|
||||
🌐 pgAdmin: http://localhost:5050
|
||||
└─ Login: admin@admin.com / admin
|
||||
└─ Serveur: "Inventory PostgreSQL" (pré-configuré)
|
||||
└─ Database: inventory
|
||||
└─ Note: Le serveur PostgreSQL est automatiquement connecté au démarrage
|
||||
|
||||
🌐 API Platform: http://localhost:8081/api/
|
||||
└─ Docs: http://localhost:8081/api/docs (à venir)
|
||||
|
||||
🗄️ PostgreSQL: localhost:5433
|
||||
└─ User: root / root
|
||||
└─ Database: inventory
|
||||
```
|
||||
|
||||
### Commandes fréquentes
|
||||
|
||||
```bash
|
||||
# Symfony
|
||||
make shell # Entrer dans le container
|
||||
php bin/console cache:clear # Clear cache
|
||||
make cache-clear-full # Clear cache + purge var/cache
|
||||
php bin/console debug:router # Lister routes
|
||||
php bin/console debug:firewall # Lister firewalls
|
||||
php bin/console doctrine:migrations:migrate # Exécuter migrations
|
||||
|
||||
# Docker
|
||||
make start # Démarrer containers
|
||||
make stop # Arrêter containers
|
||||
docker logs -f php-inventory-apache # Logs Apache
|
||||
docker logs -f pgadmin-inventory # Logs pgAdmin
|
||||
docker exec php-inventory-apache bash # Shell root
|
||||
|
||||
# Tests API
|
||||
curl http://localhost:8081/api/ # Test API Platform
|
||||
curl -X POST http://localhost:8081/api/login_check \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"admin@admin.com","password":"admin123"}'
|
||||
```
|
||||
|
||||
### Documentation
|
||||
- [Lexik JWT Bundle](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst)
|
||||
- [API Platform Security](https://api-platform.com/docs/core/security/)
|
||||
- [Symfony Security](https://symfony.com/doc/current/security.html)
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Historique des Changements
|
||||
|
||||
### 2026-01-15 - Session 3
|
||||
- ✅ Filtre API Platform `category` sur `ModelType`
|
||||
- ✅ Normalisation des structures `ModelType` (structure ↔ skeleton)
|
||||
- ✅ Migration `custom_fields.options` en JSON
|
||||
- ✅ Ajout commande `make cache-clear-full`
|
||||
- ✅ Correctifs frontend: headers API Platform, pagination par catégorie, persistance tri
|
||||
|
||||
### 2026-01-10 - Session 2 (20h30)
|
||||
- ✅ Problème Apache résolu (routes fonctionnelles)
|
||||
- ✅ Phase 2 complète (JWT 100% opérationnel)
|
||||
- ✅ Authentification testée avec succès
|
||||
- ✅ Réorganisation projet (frontend/ + _archives/)
|
||||
- ✅ État des lieux dans MIGRATION_PLAN.md
|
||||
- ✅ 5 commits conventionnels créés
|
||||
- 📊 Base inventory-data analysée (673 lignes)
|
||||
|
||||
### 2026-01-10 - Session 1 (19h00)
|
||||
- ✅ Création projet migration
|
||||
- ✅ Phase 1 complète (pgAdmin, JWT, Profile, migrations)
|
||||
- ⚠️ Problème Apache identifié (routes 404)
|
||||
- 📝 Carnet de bord créé
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour** : 2026-01-15 13:45
|
||||
**Statut** : Phase 3 EN COURS ⚠️ - Migrations et intégration frontend
|
||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,5 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## [1.8.1] - 2026-03-05
|
||||
|
||||
### Refactoring
|
||||
- **Suppression du systeme TypeMachine (squelettes machines)** : les entites `TypeMachine`, `TypeMachineComponentRequirement`, `TypeMachinePieceRequirement`, `TypeMachineProductRequirement` sont supprimees. Les champs personnalises machines sont desormais lies directement a chaque machine (relation `CustomField → Machine`).
|
||||
- **Suppression des pages squelettes machines** : pages `/machine-skeleton`, `/type/[id]`, `/type/edit/[id]` et tous les composants associes (`TypeEditForm`, `MachineSkeletonSummary`, `MachineCreatePreview`, selectors de requirements).
|
||||
- **Simplification de la creation de machines** : plus besoin de selectionner un squelette, ajout direct de composants/pieces/produits.
|
||||
|
||||
### Corrections
|
||||
- **Fix affichage categorie sur les pages edit** : les categories (produit, composant, piece) s'affichent correctement sur les pages d'edition au lieu de "Categorie inconnue". Cause : import `Serializer\Annotation\Groups` obsolete dans `ModelType` (remplace par `Attribute\Groups` pour Symfony 8) + groupes de serialisation manquants (`product:read`, `composant:read`, `piece:read`).
|
||||
- Fix import `Serializer\Annotation\Groups` → `Attribute\Groups` dans `Profile`.
|
||||
- Fix filtre `SearchFilter` : `partial` → `ipartial` sur `Comment.entityName` et `Document.name`/`Document.filename` pour recherche insensible a la casse.
|
||||
|
||||
### Migration requise
|
||||
```bash
|
||||
docker compose exec web php bin/console doctrine:migrations:migrate
|
||||
```
|
||||
|
||||
## [1.8.0] - 2026-03-03
|
||||
|
||||
### Ajouts
|
||||
@@ -46,6 +63,6 @@ docker compose exec php php bin/console doctrine:migrations:migrate
|
||||
docker compose exec php php bin/console app:init-profile-passwords
|
||||
```
|
||||
|
||||
## [1.6.0] - 2026-02-xx
|
||||
## [1.6.0] - 2026-02-12
|
||||
|
||||
- Version initiale avec gestion du parc machines, pieces, composants, produits et categories.
|
||||
|
||||
Submodule Inventory_frontend updated: e88ed5b8f2...32d03b480d
1419
MIGRATION_PLAN.md
1419
MIGRATION_PLAN.md
File diff suppressed because it is too large
Load Diff
264
README.md
264
README.md
@@ -1,92 +1,220 @@
|
||||
# Projet Inventory
|
||||
# Inventory
|
||||
|
||||
## Installation du projet
|
||||
### Windows
|
||||
Pour windows, il faut installer le WSL2, Ubuntu, docker et nvm.
|
||||
Il suffit de suivre cette [doc](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/windows)
|
||||
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.
|
||||
|
||||
### Linux
|
||||
Pour linux, il faut installer docker et nvm.
|
||||
Il suffit de suivre cette [doc](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/linux)
|
||||
## 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 | |
|
||||
|
||||
## Prérequis
|
||||
|
||||
- **Docker** et **Docker Compose**
|
||||
- **Node.js** >= 20 (via nvm)
|
||||
- **make**
|
||||
|
||||
### 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 du projet
|
||||
Une fois les prérequis installés, il suffit de cloner le projet et de lancer les commandes suivantes
|
||||
```bash
|
||||
sudo apt install make -y
|
||||
git clone --recurse-submodules <url-du-repo>
|
||||
cd Inventory
|
||||
make start
|
||||
make install
|
||||
```
|
||||
Dans le cas ou le `make start` plante à cause du port de la bdd, il faut modifier **POSTGRES_PORT** dans le fichier .env.docker.local, remplacer le par un port disponible.
|
||||
|
||||
### Configuration xdebug
|
||||
Pour configurer xdebug, il faut ajouter un serveur sur phpstorm. <br>
|
||||
Pour cela, il faut aller dans **Settings > PHP > Servers** <br>
|
||||
* Name : inventory-docker
|
||||
* Host : localhost
|
||||
* Port : 8080
|
||||
* Path : File/Directory -> l'endroit où est stocké votre projet et le path -> /var/www/html
|
||||
> Si `make start` échoue sur le port de la BDD, modifier `POSTGRES_PORT` dans `docker/.env.docker.local`.
|
||||
|
||||
Pour que xdebug fonctionne sur windows, il faut modifier la variable **XDEBUG_CLIENT_HOST** par votre ip local
|
||||
## 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) |
|
||||
|
||||
## Commandes
|
||||
|
||||
### Docker
|
||||
|
||||
| Commande | Description |
|
||||
|----------|-------------|
|
||||
| `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) |
|
||||
|
||||
## Utilisation du projet
|
||||
### Backend
|
||||
L'api est disponible sur http://localhost:8080/api
|
||||
Pour la bdd toutes les infos sont dans le fichier **docker/.env.docker.local**
|
||||
Vous pouvez modifier le port si nécessaire.
|
||||
|
||||
La bdd est déja pré-configuré dans PhpStorm, il suffit de rentrer les infos du .env.docker.local pour se connecter.
|
||||
C'est un bdd local dans le docker.
|
||||
| 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 |
|
||||
|
||||
### Frontend
|
||||
Le frontend utilise le dossier `Inventory_frontend/`.
|
||||
Pour le frontend, il suffit de taper la commande suivante qui va lancer le serveur de dev
|
||||
|
||||
| Commande | Description |
|
||||
|----------|-------------|
|
||||
| `make dev-nuxt` | Serveur de dev Nuxt |
|
||||
| `make build-nuxtJS` | Build de production |
|
||||
|
||||
### Release
|
||||
|
||||
```bash
|
||||
make dev-nuxt
|
||||
```
|
||||
Le front sera accessible sur http://localhost:3000
|
||||
|
||||
## Compression automatique des PDFs
|
||||
|
||||
Les documents PDF uploadés sont automatiquement compressés sans perte de qualité grâce à **qpdf**.
|
||||
|
||||
### Prérequis
|
||||
```bash
|
||||
# Installation de qpdf (outil système)
|
||||
sudo apt install qpdf
|
||||
|
||||
# Ou dans Docker
|
||||
docker exec -it php-inventory-apache apt update && apt install -y qpdf
|
||||
./scripts/release.sh patch # Bump patch (ou minor / major)
|
||||
```
|
||||
|
||||
### Fonctionnement
|
||||
- À chaque upload de PDF, le système compresse automatiquement le fichier
|
||||
- Compression lossless (sans perte de qualité)
|
||||
- Le PDF est compressé uniquement si la taille diminue
|
||||
- Si qpdf n'est pas installé, le système fonctionne normalement sans compression
|
||||
Synchronise automatiquement la version dans `VERSION`, `api_platform.yaml` et `nuxt.config.ts`, crée le tag git et pousse les deux repos.
|
||||
|
||||
### Compresser les PDFs existants
|
||||
Pour compresser tous les PDFs déjà en base :
|
||||
```bash
|
||||
# Voir ce qui serait compressé (dry-run)
|
||||
php bin/console app:compress-pdf --dry-run
|
||||
## Architecture
|
||||
|
||||
# Compresser tous les PDFs
|
||||
php bin/console app:compress-pdf
|
||||
### 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)
|
||||
├── fixtures/ # Données de test (SQL)
|
||||
├── 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
|
||||
```
|
||||
|
||||
## Commandes utiles
|
||||
Pour restart le container
|
||||
```bash
|
||||
make restart
|
||||
### Entités principales
|
||||
|
||||
| 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 |
|
||||
|
||||
### Commandes Symfony
|
||||
|
||||
| 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 |
|
||||
|
||||
### Rôles et permissions
|
||||
|
||||
```
|
||||
Pour lancer les TU
|
||||
```bash
|
||||
make test
|
||||
ROLE_ADMIN → ROLE_GESTIONNAIRE → ROLE_VIEWER → ROLE_USER
|
||||
```
|
||||
Pour accéder au container et lance des commandes
|
||||
```bash
|
||||
make shell
|
||||
|
||||
- **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.
|
||||
|
||||
### Base de données
|
||||
|
||||
PostgreSQL 16 avec les particularités suivantes :
|
||||
- **IDs** : chaînes CUID (`'cl' + bin2hex(random_bytes(12))`), pas d'auto-increment
|
||||
- **Noms de colonnes** : toujours en **minuscules** dans PostgreSQL (Doctrine map `typePieceId` → `typepieceid`)
|
||||
- **Audit** : les subscribers Doctrine `onFlush` capturent le diff + snapshot complet de chaque modification
|
||||
- **Migrations** : SQL brut avec `IF NOT EXISTS` / `IF EXISTS` pour l'idempotence
|
||||
|
||||
## Services Docker
|
||||
|
||||
| Service | Image | Port | Rôle |
|
||||
|---------|-------|------|------|
|
||||
| `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 |
|
||||
|
||||
## Xdebug
|
||||
|
||||
Configuration PhpStorm / VSCode :
|
||||
- **Serveur** : `inventory-docker`
|
||||
- **Host** : `localhost`
|
||||
- **Port** : `8081`
|
||||
- **Path mapping** : racine du projet → `/var/www/html`
|
||||
|
||||
> Sous WSL, modifier `XDEBUG_CLIENT_HOST` dans `docker/.env.docker.local` avec votre IP locale.
|
||||
|
||||
## Git
|
||||
|
||||
### Branches
|
||||
|
||||
- `master` : production
|
||||
- `develop` : branche principale de dev (cible des PR)
|
||||
- `feat/xxx`, `fix/xxx`, `refactor/xxx` : branches de travail
|
||||
|
||||
### Convention de commit
|
||||
|
||||
```
|
||||
Pour clear le cache Symfony
|
||||
```bash
|
||||
make cache-clear
|
||||
<type>(<scope>) : <message>
|
||||
```
|
||||
|
||||
**Espace obligatoire autour du `:`**. Types : `feat`, `fix`, `perf`, `refactor`, `chore`, `docs`, `test`, `style`, `build`, `ci`, `revert`, `wip`.
|
||||
|
||||
### Pre-commit hook
|
||||
|
||||
1. php-cs-fixer sur les fichiers PHP stagés
|
||||
2. PHPUnit — bloque le commit si les tests échouent
|
||||
|
||||
### Submodule frontend
|
||||
|
||||
Le frontend est un **submodule git** dans `Inventory_frontend/`. Workflow :
|
||||
|
||||
1. Commiter dans `Inventory_frontend/` d'abord
|
||||
2. Commiter dans le repo principal pour mettre à jour le pointeur
|
||||
3. Pousser les deux repos
|
||||
|
||||
## Documentation complémentaire
|
||||
|
||||
- [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
|
||||
|
||||
@@ -1,786 +0,0 @@
|
||||
# Plan de Refactoring - Inventory v1.2.0
|
||||
|
||||
> **Date de creation :** 2026-02-03
|
||||
> **Branche de travail :** `refacto/v1.3.0`
|
||||
> **Base :** `develop` (commit `8d83076`)
|
||||
|
||||
---
|
||||
|
||||
## Legende des statuts
|
||||
|
||||
| Statut | Signification |
|
||||
| ------ | ---------------------- |
|
||||
| `[ ]` | A faire |
|
||||
| `[~]` | En cours |
|
||||
| `[x]` | Termine |
|
||||
| `[!]` | Bloque / besoin d'info |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 - Securite (CRITIQUE)
|
||||
|
||||
> **Priorite :** MAXIMALE - A traiter en premier
|
||||
|
||||
### 1.1 Corriger la configuration de securite
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichier :** `config/packages/security.yaml`
|
||||
- **Probleme :** `PUBLIC_ACCESS` applique a toutes les routes `/api` avant la regle `IS_AUTHENTICATED_FULLY`. Le pattern matching "first match wins" rend potentiellement tout public.
|
||||
- **Action :** Reordonner les regles `access_control` pour que les routes protegees soient listees AVANT les routes publiques.
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
### 1.2 Ajouter les controles d'autorisation sur les controllers
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :**
|
||||
- `src/Controller/MachineSkeletonController.php`
|
||||
- `src/Controller/CustomFieldValueController.php`
|
||||
- `src/Controller/DocumentQueryController.php`
|
||||
- `src/Controller/SessionProfileController.php`
|
||||
- `src/Controller/SessionProfilesController.php`
|
||||
- Tous les `*HistoryController.php`
|
||||
- **Probleme :** Aucun attribut `#[IsGranted]` sur les controllers custom. Pas de RBAC.
|
||||
- **Action :** Ajouter `#[IsGranted('IS_AUTHENTICATED_FULLY')]` sur chaque controller (ou route). Definir des roles si necessaire.
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
### 1.3 Securiser les secrets
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :**
|
||||
- `.env` (JWT_PASSPHRASE en dur, APP_SECRET vide)
|
||||
- `docker/.env.docker` (credentials `root:root`)
|
||||
- **Action :**
|
||||
1. Deplacer `JWT_PASSPHRASE` dans `.env.local` (git-ignore)
|
||||
2. Generer un `APP_SECRET` valide
|
||||
3. Ajouter `.env.local` dans `.gitignore` si pas deja fait
|
||||
4. Documenter la configuration des secrets pour les devs
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 - Elimination de la duplication de code
|
||||
|
||||
> **Priorite :** HAUTE - Impact direct sur la maintenabilite
|
||||
|
||||
### 2.1 Refactorer les 3 Audit Subscribers en un seul generique
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers concernes :**
|
||||
- `src/EventSubscriber/ProductAuditSubscriber.php` (298 LOC)
|
||||
- `src/EventSubscriber/PieceAuditSubscriber.php` (300 LOC)
|
||||
- `src/EventSubscriber/ComposantAuditSubscriber.php` (300 LOC)
|
||||
- **Probleme :** ~900 LOC dupliquees a ~95%. Les methodes `onFlush()`, `buildDiffFromChangeSet()`, `resolveActorProfileId()`, `mergeDiffs()`, `normalizeCollection()` sont identiques. Seules les methodes `snapshot*()` different legerement.
|
||||
- **Action :**
|
||||
1. Creer un `AbstractAuditSubscriber` ou un `GenericAuditSubscriber` parametrable
|
||||
2. Extraire la logique commune (onFlush, buildDiff, resolveActor, mergeDiffs, normalizeCollection)
|
||||
3. Utiliser un systeme de configuration par entite (map `entityClass => entityType + snapshotMethod`)
|
||||
4. Supprimer les 3 fichiers redondants
|
||||
5. Verifier que l'audit fonctionne toujours sur Product, Piece et Composant
|
||||
- **Agent :** -
|
||||
- **Notes :** Tester manuellement les logs d'audit apres refacto.
|
||||
|
||||
### 2.2 Extraire un CuidGenerator utilitaire
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers concernes :** 18 entites contenant `generateCuid()` en prive
|
||||
- **Probleme :** Methode `generateCuid()` dupliquee dans chaque entite. De plus, `AuditLog.php` utilise une variante differente (base_convert).
|
||||
- **Action :**
|
||||
1. Creer `src/Util/CuidGenerator.php` avec une methode statique `generate(): string`
|
||||
2. Uniformiser l'implementation (choisir une seule methode)
|
||||
3. Remplacer tous les appels dans les 18 entites
|
||||
4. Supprimer les methodes privees devenues inutiles
|
||||
- **Agent :** -
|
||||
- **Notes :** Attention a l'inconsistance entre AuditLog et les autres entites.
|
||||
|
||||
### 2.3 Factoriser la logique de liaison dans MachineSkeletonController
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichier :** `src/Controller/MachineSkeletonController.php` (756 LOC)
|
||||
- **Probleme :** Les methodes `applyComponentLinks()`, `applyPieceLinks()`, `applyProductLinks()` sont quasi identiques (~90 LOC chacune).
|
||||
- **Action :**
|
||||
1. Extraire une methode generique `applyLinks(Machine $machine, array $links, string $type)`
|
||||
2. Parametrer par le type d'entite liee (Composant, Piece, Product)
|
||||
3. Reduire le controller a ~400 LOC max
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 - Restructuration des controllers
|
||||
|
||||
> **Priorite :** MOYENNE - Amelioration de la lisibilite et maintenabilite
|
||||
|
||||
### 3.1 Decouper MachineSkeletonController
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichier :** `src/Controller/MachineSkeletonController.php` (756 LOC)
|
||||
- **Action :**
|
||||
1. Extraire la logique metier dans un `MachineSkeletonService`
|
||||
2. Le controller ne doit gerer que la requete/reponse HTTP
|
||||
3. Le service gere la logique de skeleton (get, update, applyLinks)
|
||||
4. Extraire les helpers (`resolveIdentifier`, `indexLinksById`, `applyOverrides`, `normalizeMachineSkeletonResponse`) dans le service
|
||||
- **Agent :** -
|
||||
- **Notes :** Depend de la phase 2.3 (factorisation des liens).
|
||||
|
||||
### 3.2 Ajouter un try-catch et du logging dans les controllers
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :** Tous les controllers dans `src/Controller/`
|
||||
- **Probleme :** Aucun try-catch autour des `flush()` et `persist()`. Pas de logging d'erreurs.
|
||||
- **Action :**
|
||||
1. Ajouter `try-catch` autour des operations Doctrine dans chaque controller
|
||||
2. Logger les erreurs avec le `LoggerInterface` de Symfony (Monolog)
|
||||
3. Retourner des reponses JSON coherentes en cas d'erreur serveur (500)
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
### 3.3 Renforcer la validation des entrees
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :**
|
||||
- `src/Controller/CustomFieldValueController.php`
|
||||
- `src/Controller/MachineSkeletonController.php`
|
||||
- **Probleme :** Pas de validation de longueur max, pas de regex sur les IDs, pas de controle de profondeur JSON.
|
||||
- **Action :**
|
||||
1. Valider le format des IDs (regex CUID : `/^cl[a-f0-9]{24}$/`)
|
||||
2. Ajouter des limites de longueur sur les champs string
|
||||
3. Utiliser le composant Validator de Symfony pour les DTOs si pertinent
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 - Amelioration du stockage
|
||||
|
||||
> **Priorite :** MOYENNE - Performance et scalabilite
|
||||
|
||||
### 4.1 Migrer le stockage PDF de base64 vers le filesystem
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :**
|
||||
- `src/Entity/Document.php`
|
||||
- `src/Command/CompressPdfCommand.php`
|
||||
- `src/Service/PdfCompressorService.php`
|
||||
- **Probleme :** Les PDFs sont stockes en base64 dans la colonne `path` (TEXT) de la BDD. Risque de DoS et mauvaise perf sur des gros fichiers.
|
||||
- **Action :**
|
||||
1. Utiliser `vich/uploader-bundle` (deja installe) pour le stockage fichier
|
||||
2. Configurer un repertoire de stockage (`var/uploads/documents/`)
|
||||
3. Migrer les documents existants (script de migration)
|
||||
4. Adapter `PdfCompressorService` pour lire/ecrire sur le filesystem
|
||||
5. Mettre a jour l'entite Document
|
||||
- **Agent :** -
|
||||
- **Notes :** Prevoir une migration de donnees pour les documents existants.
|
||||
|
||||
### 4.2 Corriger les types de prix (string -> decimal)
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :**
|
||||
- `src/Entity/Machine.php` (`$prix`)
|
||||
- `src/Entity/Product.php` (`$supplierPrice`)
|
||||
- **Probleme :** Les prix sont types `?string` en PHP alors que la colonne est `DECIMAL(10,2)` en BDD.
|
||||
- **Action :**
|
||||
1. Changer le type PHP en `?float` ou utiliser `brick/money`
|
||||
2. Adapter les getters/setters
|
||||
3. Verifier la serialisation API Platform
|
||||
- **Agent :** -
|
||||
- **Notes :** Impact potentiel sur le frontend (format des nombres).
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 - Utilisation du Process Component
|
||||
|
||||
> **Priorite :** BASSE - Bonne pratique
|
||||
|
||||
### 5.1 Remplacer exec() par Symfony Process
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :**
|
||||
- `src/Command/CompressPdfCommand.php` (lignes 42, 98-101)
|
||||
- `src/Service/PdfCompressorService.php` (lignes 37-41)
|
||||
- **Probleme :** Utilisation de `exec()` directe pour appeler `qpdf`.
|
||||
- **Action :**
|
||||
1. Remplacer par `Symfony\Component\Process\Process`
|
||||
2. Gerer le timeout et les erreurs proprement
|
||||
3. Tester que la compression fonctionne toujours
|
||||
- **Agent :** -
|
||||
- **Notes :** `escapeshellarg()` est deja utilise, donc pas de faille de securite immediate.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 - Tests
|
||||
|
||||
> **Priorite :** HAUTE - Indispensable avant toute refacto majeure
|
||||
|
||||
### 6.1 Mettre en place les tests unitaires
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers a creer :**
|
||||
- `tests/Unit/Util/CuidGeneratorTest.php`
|
||||
- `tests/Unit/Entity/MachineTest.php`
|
||||
- `tests/Unit/Entity/ProductTest.php`
|
||||
- `tests/Unit/Service/PdfCompressorServiceTest.php`
|
||||
- **Action :**
|
||||
1. Tester le CuidGenerator (format, unicite)
|
||||
2. Tester les entites (validation, lifecycle callbacks)
|
||||
3. Tester le PdfCompressorService
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
### 6.2 Mettre en place les tests fonctionnels (API)
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers a creer :**
|
||||
- `tests/Functional/Api/MachineTest.php`
|
||||
- `tests/Functional/Api/ProductTest.php`
|
||||
- `tests/Functional/Api/AuthenticationTest.php`
|
||||
- `tests/Functional/Api/MachineSkeletonTest.php`
|
||||
- **Action :**
|
||||
1. Configurer une base de test (SQLite ou PostgreSQL de test)
|
||||
2. Creer des fixtures de test
|
||||
3. Tester les endpoints CRUD
|
||||
4. Tester l'authentification JWT
|
||||
5. Tester les endpoints custom (skeleton, custom fields)
|
||||
- **Agent :** -
|
||||
- **Notes :** Utiliser `ApiTestCase` de API Platform.
|
||||
|
||||
### 6.3 Tests des Audit Subscribers
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers a creer :**
|
||||
- `tests/Unit/EventSubscriber/AuditSubscriberTest.php`
|
||||
- **Action :**
|
||||
1. Tester la creation de logs sur insert/update/delete
|
||||
2. Tester le format des diffs et snapshots
|
||||
3. Tester la resolution de l'acteur
|
||||
- **Agent :** -
|
||||
- **Notes :** A faire APRES la phase 2.1 (refacto des subscribers).
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 - Nett oyage et conventions
|
||||
|
||||
> **Priorite :** BASSE - Polish final
|
||||
|
||||
### 7.1 Supprimer les fichiers inutiles
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers a verifier :**
|
||||
- `frontend/` (dossier legacy ? vs `Inventory_frontend/`)
|
||||
- `src/ApiResource/` (repertoire vide)
|
||||
- Fichiers SQL a la racine (`backup_v1.0.0.sql`, `data_norm.sql`, `fullasse.sql`, `fulldata.sql`)
|
||||
- **Action :** Confirmer avec l'equipe quels fichiers sont obsoletes et les supprimer.
|
||||
- **Agent :** -
|
||||
- **Notes :** Ne pas supprimer sans validation.
|
||||
|
||||
### 7.2 Uniformiser la gestion des null
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :** Toutes les entites dans `src/Entity/`
|
||||
- **Action :** S'assurer que les types nullable sont coherents entre PHP et la BDD (colonnes NOT NULL vs nullable).
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
# FRONTEND (`Inventory_frontend/`)
|
||||
|
||||
---
|
||||
|
||||
## Phase F1 - Decoupage des mega-composants (CRITIQUE)
|
||||
|
||||
> **Priorite :** MAXIMALE - Les fichiers actuels sont inmaintenables
|
||||
|
||||
### F1.1 Decouper `machine/[id].vue` (2989 LOC → 219 LOC)
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichier :** `Inventory_frontend/app/pages/machine/[id].vue`
|
||||
- **Resultat :** Page decomposee en 2 composables + 7 composants. Orchestrateur = 219 LOC.
|
||||
- **Fichiers crees :**
|
||||
- `composables/useMachineDetailData.ts` (1404 LOC) — state + logique metier
|
||||
- `composables/useMachineSkeletonEditor.ts` (843 LOC) — logique skeleton
|
||||
- `components/machine/MachineDetailHeader.vue` (76 LOC)
|
||||
- `components/machine/MachineInfoCard.vue` (185 LOC)
|
||||
- `components/machine/MachineDocumentsCard.vue` (116 LOC)
|
||||
- `components/machine/MachineProductsCard.vue` (62 LOC)
|
||||
- `components/machine/MachineComponentsCard.vue` (53 LOC)
|
||||
- `components/machine/MachinePiecesCard.vue` (34 LOC)
|
||||
- `components/machine/MachineSkeletonSummary.vue` (199 LOC)
|
||||
- **Pattern :** Props + Events (pas de provide/inject). Composables avec injection de dependances (interface Deps).
|
||||
- **Notes :** Typecheck 0 erreurs. Lint OK.
|
||||
|
||||
### F1.2 Decouper `machines/new.vue` (1231 LOC → 196 LOC)
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichier :** `Inventory_frontend/app/pages/machines/new.vue`
|
||||
- **Resultat :** Page decomposee en 1 composable + 5 composants. Orchestrateur = 196 LOC.
|
||||
- **Fichiers crees :**
|
||||
- `composables/useMachineCreatePage.ts` (460 LOC) — state, entity lookups, options, creation
|
||||
- `components/machine/create/RequirementComponentSelector.vue` (126 LOC)
|
||||
- `components/machine/create/RequirementPieceSelector.vue` (130 LOC)
|
||||
- `components/machine/create/RequirementProductSelector.vue` (142 LOC)
|
||||
- `components/machine/create/MachineCreatePreview.vue` (205 LOC)
|
||||
- `components/machine/create/PreviewRequirementGroup.vue` (59 LOC)
|
||||
- **Pattern :** Props + Events. Composable consolide entity lookups, options, label helpers, creation.
|
||||
- **Notes :** Typecheck 0 erreurs. Lint OK. Corrige aussi un bug F1.1 (defineProps dans mauvais script block de MachineSkeletonSummary.vue).
|
||||
|
||||
### F1.3 Decouper les pages de creation/edition (Piece, Component, Product)
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichiers :**
|
||||
- `pages/component/create.vue` (1282 LOC)
|
||||
- `pages/component/[id]/edit.vue` (1629 LOC)
|
||||
- `pages/pieces/create.vue` (817 LOC)
|
||||
- `pages/pieces/[id]/edit.vue` (1327 LOC)
|
||||
- `pages/product/[id]/edit.vue` (936 LOC)
|
||||
- **Probleme :** Formulaires monolithiques avec sections multiples (infos generales, fournisseurs, documents, custom fields, etc.).
|
||||
- **Action :**
|
||||
1. Identifier les sections communes entre create/edit (factoriser)
|
||||
2. Extraire chaque section en composant reutilisable :
|
||||
- `EntityFormGeneral.vue` (nom, reference, description)
|
||||
- `EntityFormSuppliers.vue` (constructeurs)
|
||||
- `EntityFormDocuments.vue` (documents)
|
||||
- `EntityFormCustomFields.vue` (champs personnalises)
|
||||
3. Objectif par page : <400 LOC
|
||||
- **Agent :** -
|
||||
- **Notes :** Les formulaires create et edit partagent beaucoup de code. Factoriser.
|
||||
- **Sous-taches :**
|
||||
- [x] F1.3a Extraire `customFieldFormUtils.ts` (duplique dans 5 fichiers)
|
||||
- [x] F1.3b Extraire `documentDisplayUtils.ts` (duplique dans 3 pages edit)
|
||||
- [x] F1.3c Extraire `historyDisplayUtils.ts` (duplique dans 3 pages edit)
|
||||
- [x] F1.3d Rewire les 5 pages create/edit sur les modules extraits
|
||||
- [x] F1.3e Typecheck + commit F1.3 (erreurs F1.3 corrigees, 120 erreurs preexistantes documentees)
|
||||
|
||||
### F1.4 Reduire PieceItem.vue (1588 LOC) et ComponentItem.vue (1336 LOC)
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichiers :**
|
||||
- `Inventory_frontend/app/components/PieceItem.vue` (1588 → 740 LOC)
|
||||
- `Inventory_frontend/app/components/ComponentItem.vue` (1336 → 585 LOC)
|
||||
- **Probleme :** ~700 LOC de logique dupliquee entre les deux composants (champs personnalises, documents, affichage produit).
|
||||
- **Action realisee :**
|
||||
1. Extraction de la logique pure custom fields dans `shared/utils/entityCustomFieldLogic.ts` (~350 LOC)
|
||||
2. Creation de `composables/useEntityCustomFields.ts` (composable reactif, ~180 LOC)
|
||||
3. Creation de `composables/useEntityDocuments.ts` (CRUD documents + preview, ~120 LOC)
|
||||
4. Creation de `composables/useEntityProductDisplay.ts` (affichage produit, ~100 LOC)
|
||||
5. Import des helpers document depuis `shared/utils/documentDisplayUtils.ts` (existant)
|
||||
6. Rewrite des deux composants pour utiliser les modules partages
|
||||
7. Typecheck 0 erreurs, lint 0 erreurs
|
||||
- **Sous-taches :**
|
||||
- [x] F1.4a Extraire `entityCustomFieldLogic.ts` (fonctions pures)
|
||||
- [x] F1.4b Creer `useEntityCustomFields.ts` (composable reactif)
|
||||
- [x] F1.4c Creer `useEntityDocuments.ts` (composable documents)
|
||||
- [x] F1.4d Creer `useEntityProductDisplay.ts` (composable produit)
|
||||
- [x] F1.4e Rewrite ComponentItem.vue (1336 → 585 LOC, script 900 → 150 LOC)
|
||||
- [x] F1.4f Rewrite PieceItem.vue (1588 → 740 LOC, script 1100 → 255 LOC)
|
||||
- [x] F1.4g Typecheck + lint (0 erreurs)
|
||||
- **Notes :** Les templates restent volumineux (~430-480 LOC) car le contenu UI est dense. Une extraction en sous-composants (DocumentList, ProductDisplay, CustomFieldForm) serait une etape future optionnelle.
|
||||
|
||||
---
|
||||
|
||||
## Phase F2 - Elimination de la duplication frontend
|
||||
|
||||
> **Priorite :** HAUTE - DRY
|
||||
|
||||
### F2.1 Extraire `extractCollection()` dans un utilitaire partage
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichiers concernes :**
|
||||
- `composables/useSites.ts`
|
||||
- `composables/useProducts.ts`
|
||||
- `composables/usePieces.ts`
|
||||
- `composables/useComposants.ts`
|
||||
- `composables/useMachineTypesApi.js`
|
||||
- `composables/useConstructeurs.ts`
|
||||
- `composables/useDocuments.ts`
|
||||
- `composables/useMachineCreateSelections.ts`
|
||||
- `components/ComponentStructureAssignmentNode.vue`
|
||||
- `components/model-types/ManagementView.vue`
|
||||
- **Probleme :** La fonction `extractCollection()` (parsing `hydra:member` / `member` / `items` / `data` / array) etait dupliquee dans 10 fichiers.
|
||||
- **Action :**
|
||||
1. [x] Creer `shared/utils/apiHelpers.ts` avec `extractCollection<T>()` generique
|
||||
2. [x] Remplacer les 10 implementations locales par un import
|
||||
- **Agent :** -
|
||||
- **Notes :** Gere aussi `items` (utilise par ManagementView.vue). `extractRelationId()` et `normalizeRelationIds()` restent dans `shared/apiRelations.ts` (deja partages).
|
||||
|
||||
### F2.2 Fusionner les 3 composables d'historique
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichiers concernes :**
|
||||
- `composables/useComponentHistory.ts` (67 → 13 LOC, thin wrapper)
|
||||
- `composables/usePieceHistory.ts` (67 → 13 LOC, thin wrapper)
|
||||
- `composables/useProductHistory.ts` (67 → 13 LOC, thin wrapper)
|
||||
- `composables/useEntityHistory.ts` (NEW, 65 LOC, logique generique)
|
||||
- **Probleme :** 3 fichiers quasi identiques (seul le endpoint differait).
|
||||
- **Action :**
|
||||
1. [x] Creer `composables/useEntityHistory.ts` parametrable par type d'entite
|
||||
2. [x] Reecrire les 3 fichiers specifiques en wrappers backward-compatible
|
||||
- **Agent :** -
|
||||
- **Notes :** Les wrappers preservent l'API existante (types + fonction), aucun consommateur a modifier.
|
||||
|
||||
### F2.3 Factoriser les composables de types (Component/Piece/Product)
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichiers concernes :**
|
||||
- `composables/useComponentTypes.ts` (165 → 30 LOC, thin wrapper)
|
||||
- `composables/usePieceTypes.ts` (165 → 30 LOC, thin wrapper)
|
||||
- `composables/useProductTypes.ts` (160 → 28 LOC, thin wrapper)
|
||||
- `composables/useEntityTypes.ts` (NEW, 172 LOC, logique generique)
|
||||
- **Probleme :** 3 composables tres similaires pour gerer les categories/types.
|
||||
- **Action :**
|
||||
1. [x] Creer `composables/useEntityTypes.ts` generique (CRUD + singleton state par categorie)
|
||||
2. [x] Reecrire les 3 fichiers specifiques en wrappers avec renommage des champs
|
||||
- **Agent :** -
|
||||
- **Notes :** Les wrappers renomment `types` → `componentTypes`/`pieceTypes`/`productTypes`, preservent `getXxxTypes()` et `isXxxTypeLoading()`. Etat partage via `stateByCategory` map module-level.
|
||||
|
||||
---
|
||||
|
||||
## Phase F3 - Migration TypeScript
|
||||
|
||||
> **Priorite :** HAUTE - Securite du typage
|
||||
|
||||
### F3.1 Definir les types pour les reponses API
|
||||
|
||||
- **Statut :** `[x]` (partiellement — types definis dans chaque composable + `ApiResponse<T>` dans useApi.ts)
|
||||
- **Fichiers :**
|
||||
- `composables/useApi.ts` — `ApiResponse<T>` generique (success/data/error/status)
|
||||
- `composables/useMachines.ts` — `Machine` interface
|
||||
- `composables/useMachineTypesApi.ts` — `MachineType`, `MachineTypeRequirement` interfaces
|
||||
- `composables/useToast.ts` — `Toast`, `ToastType` types
|
||||
- `composables/useProfiles.ts` — `Profile` interface
|
||||
- `composables/useCustomFields.ts` — `CustomFieldValue` interface
|
||||
- **Notes :** Les types sont definis dans chaque composable (colocation). Types entite existants : `Product`, `Piece`, `Composant`, `Constructeur`, `Site`, `Document` dans leurs composables respectifs (.ts). `shared/types/inventory.ts` contient les types de structure de modele.
|
||||
|
||||
### F3.2 Convertir les composables JS en TS
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichiers convertis (7 fichiers JS → TS) :**
|
||||
- [x] `useToast.js` → `useToast.ts` (72 LOC, types: `Toast`, `ToastType`)
|
||||
- [x] `useProfiles.js` → `useProfiles.ts` (68 LOC, type: `Profile`)
|
||||
- [x] `useProfileSession.js` → `useProfileSession.ts` (85 LOC, importe `Profile`)
|
||||
- [x] `useApi.js` → `useApi.ts` (106 LOC → 120 LOC, types: `ApiResponse<T>`, `ApiCallOptions`, ajout `put()`)
|
||||
- [x] `useCustomFields.js` → `useCustomFields.ts` (105 LOC, type: `CustomFieldValue`)
|
||||
- [x] `useMachineTypesApi.js` → `useMachineTypesApi.ts` (173 → 188 LOC, types: `MachineType`, `MachineTypeRequirement`)
|
||||
- [x] `useMachines.js` → `useMachines.ts` (267 LOC, type: `Machine`, utilise `extractCollection`)
|
||||
- **Fichiers deja TS :** `useProducts.ts`, `usePieces.ts`, `useComposants.ts`, `useConstructeurs.ts`, `useSites.ts`, `useDocuments.ts`
|
||||
- **Fichiers JS restants (deprecated) :** `useComponentModels.js`, `usePieceModels.js` (stubs deprecated, a supprimer)
|
||||
- **Notes :** `ApiResponse<T = any>` par defaut `any` pour backward-compat. Les callers existants fonctionnent sans changement ; le nouveau code peut opt-in strict via `get<MyType>()`.
|
||||
|
||||
### F3.3 Eliminer les `any` restants
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichiers concernes :**
|
||||
- `components/ProductSelect.vue` — 1 `any` restant (slot template, incompressible)
|
||||
- `components/model-types/ManagementView.vue` — remplace `data?: any` → `Record<string, unknown>`, `error: any` → `error: unknown`, `item: any` → `item: unknown`
|
||||
- `components/ComponentStructureAssignmentNode.vue` — 12 casts `(definition as any).typePiece/typeProduct` elimines grace a l'extension des types
|
||||
- `components/ComponentModelStructureEditor.vue` — `Promise<any>` → `Promise<unknown>`
|
||||
- `components/model-types/ModelTypeForm.vue` — `(incoming as any).description` → cast `Record<string, unknown>`
|
||||
- `shared/types/inventory.ts` — `ComponentModelPiece.typePiece?` et `ComponentModelProduct.typeProduct?` ajoutes, 3 casts `(value as any)` supprimes
|
||||
- **Probleme :** 20+ usages de `any` type identifies.
|
||||
- **Action :** Etendre les interfaces de types pour supporter les formes alternatives de l'API. Remplacer les `any` par `unknown` ou `Record<string, unknown>` la ou possible.
|
||||
- **Agent :** Claude
|
||||
- **Notes :** ~15 casts `any` elimines. Les `Record<string, any>` restants dans ComponentModelStructureEditor sont justifies (manipulation dynamique interne de custom fields). Typecheck 0 erreurs.
|
||||
|
||||
---
|
||||
|
||||
## Phase F4 - Qualite du code frontend
|
||||
|
||||
> **Priorite :** MOYENNE
|
||||
|
||||
### F4.1 Activer les regles ESLint critiques
|
||||
|
||||
- **Statut :** `[x]` DONE
|
||||
- **Fichier :** `Inventory_frontend/eslint.config.mjs`
|
||||
- **Probleme :** Presque toutes les regles etaient desactivees (`no-console: off`, `no-unused-vars: off`, `no-explicit-any: off`).
|
||||
- **Action realisee :**
|
||||
1. [x] Active `@typescript-eslint/no-explicit-any: warn` (526 warnings — amelioration progressive)
|
||||
2. [x] Active `no-console: warn` avec `allow: ['error']` — 0 violations (deja nettoye en F4.2)
|
||||
3. [x] Active `@typescript-eslint/no-unused-vars: warn` avec ignore `^_` — 0 violations (26 corrigees)
|
||||
4. [x] Corrige les 26 violations `no-unused-vars` : imports inutilises supprimes, variables prefixees `_`, destructurations nettoyees
|
||||
- **Agent :** Claude
|
||||
- **Notes :** 16 fichiers modifies. Regles organisees par categorie (vue, console, typescript, formatting). 0 erreurs, 526 warnings `no-explicit-any` restants (warn, pas bloquant).
|
||||
|
||||
### F4.2 Nettoyer les console.log/console.error
|
||||
|
||||
- **Statut :** `[x]` (console.log supprime, console.error conserve)
|
||||
- **Fichiers modifies :** 8 fichiers (useMachineTypesApi.ts, useSites.ts, type/[id].vue, type/edit/[id].vue, TypeEditPieceRequirementsSection.vue, SearchSelect.vue, app.vue)
|
||||
- **Probleme :** 19 appels `console.log` de debug laisses dans le code de production.
|
||||
- **Action :**
|
||||
1. [x] Supprimer les 19 `console.log` de debug (normalizeRequirementList, page loading, route params, etc.)
|
||||
2. [ ] Les 72 `console.error` restants sont conserves (gestion d'erreur legitime). Migration vers un logger centralise a faire en F4.3.
|
||||
- **Agent :** Claude
|
||||
- **Notes :** 0 `console.log/warn/debug/info` restants dans le frontend.
|
||||
|
||||
### F4.3 Centraliser la gestion d'erreurs API
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichier :** `Inventory_frontend/app/composables/useApi.js` (105 LOC)
|
||||
- **Probleme :** Gestion d'erreur basique (juste un toast). Pas de retry, pas d'intercepteur, erreurs silencieuses dans certains composables.
|
||||
- **Action :**
|
||||
1. Ajouter un systeme de retry configurable (1-3 tentatives)
|
||||
2. Centraliser la gestion des erreurs HTTP (401 -> redirect login, 500 -> message explicite)
|
||||
3. Ajouter des intercepteurs request/response
|
||||
4. Uniformiser le pattern dans tous les composables
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
---
|
||||
|
||||
## Phase F5 - Reduire le fichier modelUtils.ts (1017 LOC)
|
||||
|
||||
> **Priorite :** MOYENNE
|
||||
|
||||
### F5.1 Decouper `shared/modelUtils.ts`
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichier :** `Inventory_frontend/app/shared/modelUtils.ts` (1017 LOC → 37 LOC barrel)
|
||||
- **Probleme :** Fichier utilitaire monolithique de 1017 lignes regroupant toute la logique de manipulation de modeles.
|
||||
- **Action :**
|
||||
1. Identifier les groupes de fonctions (structure, custom fields, requirements, serialization)
|
||||
2. Decouper en 3 modules thematiques :
|
||||
- `shared/model/componentStructure.ts` (~590 LOC) — helpers, sanitize, hydrate, normalize, extract, format pour composants
|
||||
- `shared/model/pieceProductStructure.ts` (~155 LOC) — structure piece/produit (clone, sanitize, hydrate, format)
|
||||
- `shared/model/definitionOverrides.ts` (~50 LOC) — sanitization des overrides de definition
|
||||
3. Re-exporter depuis `shared/modelUtils.ts` (barrel) pour ne pas casser les imports
|
||||
- **Agent :** Claude
|
||||
- **Notes :** 11 fichiers consommateurs inchanges (barrel preserve la retro-compat). Typecheck 0 erreurs.
|
||||
|
||||
---
|
||||
|
||||
## Phase F6 - Tests frontend
|
||||
|
||||
> **Priorite :** HAUTE - Aucun test actuellement
|
||||
|
||||
### F6.1 Configurer Vitest
|
||||
|
||||
- **Statut :** `[x]` DONE
|
||||
- **Fichiers crees :**
|
||||
- `vitest.config.ts` — config Vitest avec happy-dom, alias `~` et `#imports`
|
||||
- `tests/__mocks__/imports.ts` — mock des auto-imports Nuxt (useRuntimeConfig, useRoute, etc.)
|
||||
- `tests/shared/inventory-types.test.ts` — 9 tests smoke (validator, empty structures)
|
||||
- **Action realisee :**
|
||||
1. [x] Installe `vitest`, `@vue/test-utils`, `happy-dom`
|
||||
2. [x] Configure Vitest avec environment happy-dom et resolution d'alias
|
||||
3. [x] Ajoute scripts `test` et `test:watch` dans `package.json`
|
||||
4. [x] Premier test suite : `componentModelStructureValidator` (9 tests, 100% pass)
|
||||
- **Agent :** Claude
|
||||
- **Notes :** `npm test` → 9 tests, 0 failures, <1s. Alias `#imports` pointe vers un mock minimal extensible.
|
||||
|
||||
### F6.2 Tests unitaires des composables
|
||||
|
||||
- **Statut :** `[x]` DONE (base)
|
||||
- **Fichiers crees :**
|
||||
- `tests/shared/apiHelpers.test.ts` — 10 tests (extractCollection, tous formats API)
|
||||
- `tests/shared/modelUtils.test.ts` — 18 tests (isPlainObject, clone, stats, format, piece/product)
|
||||
- `tests/shared/inventory-types.test.ts` — 9 tests (validator, empty structures)
|
||||
- `tests/composables/useToast.test.ts` — 9 tests (add, types, max limit, clear, singleton)
|
||||
- `tests/composables/useConfirm.test.ts` — 8 tests (open, confirm, cancel, options, singleton)
|
||||
- **Action realisee :**
|
||||
1. [x] Teste `extractCollection()` : array, hydra:member, member, items, data, null, undefined
|
||||
2. [x] Teste `useToast` : ajout, types, max 3 toasts, clearAll, removeToast, singleton
|
||||
3. [x] Teste `useConfirm` : open/close, resolve true/false, custom options, singleton state
|
||||
4. [x] Teste `modelUtils` : clone, stats, preview, isPlainObject, piece/product variants
|
||||
5. [x] Teste `componentModelStructureValidator` : valid/invalid, custom fields, subcomponents
|
||||
- **Agent :** Claude
|
||||
- **Notes :** 54 tests, 5 fichiers, 100% pass, <2s. Tests `useApi` et CRUD composables necessitent mock fetch (phase ulterieure).
|
||||
|
||||
### F6.3 Tests de composants
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers a creer :**
|
||||
- `tests/components/Pagination.test.ts`
|
||||
- `tests/components/SearchSelect.test.ts`
|
||||
- `tests/components/MachineHeader.test.ts` (apres F1.1)
|
||||
- **Action :**
|
||||
1. Tester les composants communs (Pagination, SearchSelect)
|
||||
2. Tester le rendu conditionnel et les events
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
---
|
||||
|
||||
## Phase F7 - Ameliorations UX/DX
|
||||
|
||||
> **Priorite :** BASSE - Polish
|
||||
|
||||
### F7.1 Reduire le props drilling
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Probleme :** Props passees sur 3+ niveaux (ex: machine data dans les sous-composants).
|
||||
- **Action :**
|
||||
1. Identifier les cas de props drilling >2 niveaux
|
||||
2. Utiliser `provide/inject` ou des composables partages
|
||||
3. Documenter le pattern choisi
|
||||
- **Agent :** -
|
||||
- **Notes :** A traiter apres F1 (decoupage des composants).
|
||||
|
||||
### F7.2 Remplacer `confirm()` natif par des modales DaisyUI
|
||||
|
||||
- **Statut :** `[x]` DONE
|
||||
- **Probleme :** Les confirmations de suppression utilisaient `window.confirm()` (UI native, non-stylee).
|
||||
- **Action realisee :**
|
||||
1. [x] Cree `composables/useConfirm.ts` — composable promise-based avec etat reactif partage
|
||||
2. [x] Cree `components/common/ConfirmModal.vue` — modale DaisyUI teleportee (backdrop blur, btn-error)
|
||||
3. [x] Monte `ConfirmModal` globalement dans `app.vue`
|
||||
4. [x] Remplace les 10 `confirm()` natifs dans 10 fichiers :
|
||||
- `constructeurs.vue`, `profiles/manage.vue`, `ManagementView.vue`
|
||||
- `product-catalog.vue`, `index.vue`, `machines/index.vue`
|
||||
- `machine-skeleton/index.vue`, `pieces-catalog.vue`, `component-catalog.vue`
|
||||
- `useSiteManagement.ts` (composable — import explicite)
|
||||
- **Agent :** Claude
|
||||
- **Notes :** API : `const { confirm } = useConfirm(); const ok = await confirm({ message: '...' })`. Auto-import Nuxt pour les SFC, import explicite pour les composables.
|
||||
|
||||
### F7.3 Nettoyer `app.vue` (861 LOC)
|
||||
|
||||
- **Statut :** `[x]` DONE
|
||||
- **Fichier :** `Inventory_frontend/app/app.vue` (861 → 49 LOC)
|
||||
- **Probleme :** Le fichier racine contenait le layout principal, la navbar (~676 LOC dupliquee mobile/desktop), et du state management.
|
||||
- **Action realisee :**
|
||||
1. Cree `composables/useNavDropdown.ts` (~65 LOC) — gestion etat dropdowns navbar
|
||||
2. Cree `components/layout/AppNavbar.vue` (~310 LOC) — navbar data-driven avec `v-for` eliminant duplication mobile/desktop
|
||||
3. `app.vue` reecrit en orchestrateur minimal (49 LOC) + converti en TypeScript
|
||||
4. Supprime 4 imports d'icones inutilises
|
||||
- **Agent :** Claude
|
||||
- **Notes :** Approche data-driven : liens et groupes definis comme tableaux types (`NavLink[]`, `NavGroup[]`), rendus par `v-for` pour mobile et desktop
|
||||
|
||||
---
|
||||
|
||||
## Ordre d'execution recommande
|
||||
|
||||
```
|
||||
=== BACKEND === === FRONTEND ===
|
||||
|
||||
Phase 6.1 (Tests unitaires) Phase F6.1 (Config Vitest)
|
||||
| |
|
||||
v v
|
||||
Phase 1 (Securite) Phase F1 (Decoupage mega-composants)
|
||||
| |
|
||||
v v
|
||||
Phase 2 (Duplication backend) Phase F2 (Duplication frontend)
|
||||
| |
|
||||
v v
|
||||
Phase 3 (Controllers) Phase F3 (Migration TypeScript)
|
||||
| |
|
||||
v v
|
||||
Phase 6.2 (Tests API) Phase F4 (Qualite code) + Phase F5 (modelUtils)
|
||||
| |
|
||||
v v
|
||||
Phase 4 (Stockage) Phase F6.2-F6.3 (Tests frontend)
|
||||
| |
|
||||
v v
|
||||
Phase 5 + Phase 7 (Nettoyage) Phase F7 (UX/DX polish)
|
||||
|
|
||||
v
|
||||
Phase 6.3 (Tests audit)
|
||||
```
|
||||
|
||||
> Les colonnes backend et frontend peuvent etre executees **en parallele** par des agents differents.
|
||||
|
||||
---
|
||||
|
||||
## Journal des modifications
|
||||
|
||||
| Date | Phase | Tache | Agent | Statut | Notes |
|
||||
| ---------- | ----- | ------------------------- | --------------- | ------- | ---------------------------------------------- |
|
||||
| 2026-02-03 | - | Creation du plan backend | Claude Opus 4.5 | Termine | Analyse initiale backend (7 phases, 17 taches) |
|
||||
| 2026-02-03 | - | Creation du plan frontend | Claude Opus 4.5 | Termine | Analyse frontend (7 phases, 22 taches) |
|
||||
| | | | | | |
|
||||
|
||||
---
|
||||
|
||||
## Commandes de verification
|
||||
|
||||
> **Contexte :** Le backend tourne dans Docker (`docker compose`), le frontend est en local.
|
||||
> Les commandes ci-dessous sont executees **depuis la racine du projet** (`/home/matthieu/dev_malio/Inventory/`).
|
||||
|
||||
### Frontend (Nuxt 3 / Vue 3 / TypeScript)
|
||||
|
||||
| Commande | Description | Quand l'utiliser |
|
||||
| -------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `npx nuxi typecheck` | Verification des types TypeScript via `vue-tsc` | Apres chaque modification de fichier `.vue` ou `.ts`. C'est la commande principale de validation. |
|
||||
| `npm run lint` | ESLint (config dans `eslint.config.mjs`) | Apres chaque modification pour verifier le style et les erreurs statiques. |
|
||||
| `npm run lint:fix` | ESLint avec auto-fix | Pour corriger automatiquement les erreurs de formatage. |
|
||||
| `npm run build` | Build de production Nuxt (inclut le typecheck) | Avant un commit pour s'assurer que tout compile. Plus lent que `typecheck` seul. |
|
||||
| `npx nuxi prepare` | Regenerer les types auto-generes (`.nuxt/`) | Si les imports auto (composables, components) ne sont pas reconnus par le typecheck. |
|
||||
|
||||
> **Toutes les commandes frontend** sont executees depuis `Inventory_frontend/` :
|
||||
>
|
||||
> ```bash
|
||||
> cd Inventory_frontend && npx nuxi typecheck
|
||||
> ```
|
||||
|
||||
> **Note sur les erreurs pre-existantes :** Il y a ~120 erreurs TypeScript pre-existantes documentees
|
||||
> (anterieures a la refacto). L'objectif est de ne pas en ajouter de nouvelles.
|
||||
> Pour verifier : comparer le nombre d'erreurs avant/apres modification.
|
||||
|
||||
### Backend (Symfony 8 / PHP 8.4)
|
||||
|
||||
| Commande | Description | Quand l'utiliser |
|
||||
| ---------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `vendor/bin/php-cs-fixer fix --dry-run --diff` | Verifie le style PHP (PSR-12 + Symfony) sans modifier | Apres chaque modification PHP. |
|
||||
| `vendor/bin/php-cs-fixer fix` | Corrige automatiquement le style PHP | Avant chaque commit. |
|
||||
| `bin/phpunit` | Lance les tests PHPUnit | Apres chaque modification backend. |
|
||||
| `php bin/console cache:clear` | Vide le cache Symfony | Si des erreurs bizarres apparaissent apres un changement de config. |
|
||||
|
||||
> **Les commandes backend** sont executees **dans le conteneur Docker** :
|
||||
>
|
||||
> ```bash
|
||||
> docker compose exec web vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||
> docker compose exec web bin/phpunit
|
||||
> ```
|
||||
|
||||
### Workflow de verification (checklist par tache)
|
||||
|
||||
```
|
||||
1. Lire les fichiers concernes (AVANT toute modification)
|
||||
2. Effectuer les modifications
|
||||
3. Frontend : npx nuxi typecheck → verifier pas de nouvelles erreurs
|
||||
4. Frontend : npm run lint:fix → corriger le formatage
|
||||
5. Backend : php-cs-fixer fix → corriger le style PHP
|
||||
6. Backend : bin/phpunit → verifier la non-regression
|
||||
7. Commit si tout est OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Regles pour les agents
|
||||
|
||||
1. **Avant de commencer une tache :**
|
||||
- Mettre le statut a `[~]` dans ce fichier
|
||||
- Inscrire son nom/ID dans la colonne "Agent"
|
||||
- Lire les fichiers concernes AVANT de modifier quoi que ce soit
|
||||
|
||||
2. **Pendant le travail :**
|
||||
- Ne modifier QUE les fichiers listes dans la tache
|
||||
- Respecter les conventions existantes (PSR-12, strict_types)
|
||||
- Ne pas introduire de nouvelles dependances sans justification
|
||||
- Lancer `php-cs-fixer` apres les modifications
|
||||
|
||||
3. **Apres avoir termine :**
|
||||
- Mettre le statut a `[x]`
|
||||
- Ajouter une entree dans le "Journal des modifications"
|
||||
- Lancer les tests existants (`make test`) pour verifier la non-regression
|
||||
- Decrire brievement les changements effectues dans "Notes"
|
||||
|
||||
4. **En cas de blocage :**
|
||||
- Mettre le statut a `[!]`
|
||||
- Documenter le blocage dans "Notes"
|
||||
- Ne PAS passer a une autre tache sans signaler le blocage
|
||||
|
||||
5. **Regles specifiques au frontend :**
|
||||
- Ecrire en TypeScript (pas de JS pour les nouveaux fichiers)
|
||||
- Pas de `any` - utiliser des types concrets
|
||||
- Pas de `console.log` - utiliser le logger ou `useToast`
|
||||
- Composants Vue : max 400 LOC par fichier
|
||||
- Utiliser les composants DaisyUI existants (pas de CSS custom)
|
||||
- Tester avec Vitest quand la config est en place
|
||||
|
||||
6. **Regles specifiques au backend :**
|
||||
- `declare(strict_types=1)` obligatoire
|
||||
- Respecter PSR-12 + regles Symfony (php-cs-fixer)
|
||||
- Pas de `exec()` direct - utiliser Symfony Process
|
||||
- Tester avec PHPUnit
|
||||
2
TODO.md
2
TODO.md
@@ -1,2 +0,0 @@
|
||||
- Doc: ne pas oublier de mettre `make` dans la documentation.
|
||||
- Note: le probleme d'IP sous WSL, a ajouter dans la doc.
|
||||
@@ -1,6 +1,6 @@
|
||||
api_platform:
|
||||
title: Hello API Platform
|
||||
version: 1.4.0
|
||||
version: 1.8.1
|
||||
defaults:
|
||||
stateless: false
|
||||
cache_headers:
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# Migration DB (manuel)
|
||||
|
||||
Ce guide explique comment importer un dump SQL venant de pgAdmin dans la base Docker.
|
||||
|
||||
## 1) Export pgAdmin
|
||||
|
||||
Dans pgAdmin:
|
||||
|
||||
- Format: Plain
|
||||
- Options: Use INSERT commands + Use column inserts
|
||||
- Fichier: `data.sql`
|
||||
|
||||
## 2) Normaliser le dump
|
||||
|
||||
Convertit les colonnes camelCase en lowercase compact.
|
||||
|
||||
```bash
|
||||
python3 scripts/normalize-dump.py data.sql data_norm.sql --lower
|
||||
```
|
||||
|
||||
## 3) Importer dans la base Docker
|
||||
|
||||
Utilise `session_replication_role` pour eviter les erreurs de contraintes circulaires.
|
||||
|
||||
```bash
|
||||
docker compose --env-file docker/.env.docker.local exec -T db psql -U root -d inventory -v ON_ERROR_STOP=1 -c "SET session_replication_role = replica;"
|
||||
docker compose --env-file docker/.env.docker.local exec -T db psql -U root -d inventory -v ON_ERROR_STOP=1 < data_norm.sql
|
||||
docker compose --env-file docker/.env.docker.local exec -T db psql -U root -d inventory -v ON_ERROR_STOP=1 -c "SET session_replication_role = DEFAULT;"
|
||||
```
|
||||
|
||||
## 4) Verifier
|
||||
|
||||
```bash
|
||||
docker compose --env-file docker/.env.docker.local exec -T db psql -U $POSTGRES_USER -d $POSTGRES_DB -c "\\dt"
|
||||
```
|
||||
158
migrations/Version20260304120000.php
Normal file
158
migrations/Version20260304120000.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260304120000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Remove TypeMachine skeleton system, link custom fields directly to machines';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// 1. Drop requirement FK columns on link tables
|
||||
$this->addSql('ALTER TABLE machine_component_links DROP COLUMN IF EXISTS typemachinecomponentrequirementid');
|
||||
$this->addSql('ALTER TABLE machine_piece_links DROP COLUMN IF EXISTS typemachinepiecerequirementid');
|
||||
$this->addSql('ALTER TABLE machine_product_links DROP COLUMN IF EXISTS typemachineproductrequirementid');
|
||||
|
||||
// 2. Add machineid column to custom_fields (new direct FK to machines)
|
||||
$this->addSql('ALTER TABLE custom_fields ADD COLUMN IF NOT EXISTS machineid VARCHAR(36) DEFAULT NULL');
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE constraint_name = 'fk_custom_fields_machine' AND table_name = 'custom_fields'
|
||||
) THEN
|
||||
ALTER TABLE custom_fields ADD CONSTRAINT fk_custom_fields_machine
|
||||
FOREIGN KEY (machineid) REFERENCES machines(id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
SQL);
|
||||
|
||||
// 3. Enable pgcrypto for gen_random_bytes (needed for CUID generation)
|
||||
$this->addSql('CREATE EXTENSION IF NOT EXISTS pgcrypto');
|
||||
|
||||
// 4. Migrate existing custom fields: copy from TypeMachine to each linked Machine
|
||||
$this->addSql(<<<'SQL'
|
||||
INSERT INTO custom_fields (id, name, type, required, defaultvalue, options, orderindex, machineid, createdat, updatedat)
|
||||
SELECT
|
||||
'cl' || encode(gen_random_bytes(12), 'hex'),
|
||||
cf.name, cf.type, cf.required, cf.defaultvalue, cf.options, cf.orderindex,
|
||||
m.id,
|
||||
NOW(), NOW()
|
||||
FROM custom_fields cf
|
||||
JOIN machines m ON m.typemachineid = cf.typemachineid
|
||||
WHERE cf.typemachineid IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM custom_fields existing
|
||||
WHERE existing.machineid = m.id AND existing.name = cf.name
|
||||
)
|
||||
SQL);
|
||||
|
||||
// 4. Delete original TypeMachine-linked custom fields (now migrated)
|
||||
$this->addSql('DELETE FROM custom_fields WHERE typemachineid IS NOT NULL');
|
||||
|
||||
// 5. Drop typemachineid column from custom_fields
|
||||
$this->addSql('ALTER TABLE custom_fields DROP COLUMN IF EXISTS typemachineid');
|
||||
|
||||
// 6. Drop typemachineid column from machines
|
||||
$this->addSql('ALTER TABLE machines DROP COLUMN IF EXISTS typemachineid');
|
||||
|
||||
// 7. Drop requirement tables (order matters: these reference type_machines)
|
||||
$this->addSql('DROP TABLE IF EXISTS type_machine_component_requirements');
|
||||
$this->addSql('DROP TABLE IF EXISTS type_machine_piece_requirements');
|
||||
$this->addSql('DROP TABLE IF EXISTS type_machine_product_requirements');
|
||||
|
||||
// 8. Drop type_machines table
|
||||
$this->addSql('DROP TABLE IF EXISTS type_machines');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// Recreate type_machines table
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS type_machines (
|
||||
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
description TEXT DEFAULT NULL,
|
||||
category VARCHAR(255) DEFAULT NULL,
|
||||
maintenancefrequency VARCHAR(255) DEFAULT NULL,
|
||||
components JSON DEFAULT NULL,
|
||||
criticalparts JSON DEFAULT NULL,
|
||||
machinepieces JSON DEFAULT NULL,
|
||||
specifications JSON DEFAULT NULL,
|
||||
createdat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
updatedat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL
|
||||
)
|
||||
SQL);
|
||||
|
||||
// Recreate requirement tables
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS type_machine_component_requirements (
|
||||
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
||||
typemachineid VARCHAR(36) NOT NULL REFERENCES type_machines(id) ON DELETE CASCADE,
|
||||
typecomposantid VARCHAR(36) NOT NULL REFERENCES model_types(id),
|
||||
label VARCHAR(255) DEFAULT NULL,
|
||||
mincount INTEGER DEFAULT 1,
|
||||
maxcount INTEGER DEFAULT NULL,
|
||||
required BOOLEAN DEFAULT true,
|
||||
allownewmodels BOOLEAN DEFAULT true,
|
||||
orderindex INTEGER DEFAULT 0,
|
||||
createdat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
updatedat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL
|
||||
)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS type_machine_piece_requirements (
|
||||
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
||||
typemachineid VARCHAR(36) NOT NULL REFERENCES type_machines(id) ON DELETE CASCADE,
|
||||
typepieceid VARCHAR(36) NOT NULL REFERENCES model_types(id),
|
||||
label VARCHAR(255) DEFAULT NULL,
|
||||
mincount INTEGER DEFAULT 0,
|
||||
maxcount INTEGER DEFAULT NULL,
|
||||
required BOOLEAN DEFAULT false,
|
||||
allownewmodels BOOLEAN DEFAULT true,
|
||||
orderindex INTEGER DEFAULT 0,
|
||||
createdat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
updatedat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL
|
||||
)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS type_machine_product_requirements (
|
||||
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
||||
typemachineid VARCHAR(36) NOT NULL REFERENCES type_machines(id) ON DELETE CASCADE,
|
||||
typeproductid VARCHAR(36) NOT NULL REFERENCES model_types(id),
|
||||
label VARCHAR(255) DEFAULT NULL,
|
||||
mincount INTEGER DEFAULT 0,
|
||||
maxcount INTEGER DEFAULT NULL,
|
||||
required BOOLEAN DEFAULT false,
|
||||
allownewmodels BOOLEAN DEFAULT true,
|
||||
orderindex INTEGER DEFAULT 0,
|
||||
createdat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
updatedat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL
|
||||
)
|
||||
SQL);
|
||||
|
||||
// Re-add typemachineid to machines
|
||||
$this->addSql('ALTER TABLE machines ADD COLUMN IF NOT EXISTS typemachineid VARCHAR(36) DEFAULT NULL');
|
||||
|
||||
// Re-add typemachineid to custom_fields
|
||||
$this->addSql('ALTER TABLE custom_fields ADD COLUMN IF NOT EXISTS typemachineid VARCHAR(36) DEFAULT NULL');
|
||||
|
||||
// Re-add requirement FK columns to link tables
|
||||
$this->addSql('ALTER TABLE machine_component_links ADD COLUMN IF NOT EXISTS typemachinecomponentrequirementid VARCHAR(36) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE machine_piece_links ADD COLUMN IF NOT EXISTS typemachinepiecerequirementid VARCHAR(36) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE machine_product_links ADD COLUMN IF NOT EXISTS typemachineproductrequirementid VARCHAR(36) DEFAULT NULL');
|
||||
|
||||
// Drop machine FK on custom_fields
|
||||
$this->addSql('ALTER TABLE custom_fields DROP COLUMN IF EXISTS machineid');
|
||||
}
|
||||
}
|
||||
@@ -33,12 +33,7 @@ class MachineCustomFieldsController extends AbstractController
|
||||
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
||||
}
|
||||
|
||||
$typeMachine = $machine->getTypeMachine();
|
||||
if (!$typeMachine) {
|
||||
return $this->json(['success' => true, 'machineId' => $machine->getId(), 'customFieldValues' => []]);
|
||||
}
|
||||
|
||||
foreach ($typeMachine->getCustomFields() as $customField) {
|
||||
foreach ($machine->getCustomFields() as $customField) {
|
||||
if (!$customField instanceof CustomField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\Composant;
|
||||
use App\Entity\CustomField;
|
||||
use App\Entity\CustomFieldValue;
|
||||
use App\Entity\Machine;
|
||||
use App\Entity\MachineComponentLink;
|
||||
use App\Entity\MachinePieceLink;
|
||||
@@ -13,9 +14,6 @@ use App\Entity\MachineProductLink;
|
||||
use App\Entity\ModelType;
|
||||
use App\Entity\Piece;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\TypeMachineComponentRequirement;
|
||||
use App\Entity\TypeMachinePieceRequirement;
|
||||
use App\Entity\TypeMachineProductRequirement;
|
||||
use App\Repository\ComposantRepository;
|
||||
use App\Repository\MachineComponentLinkRepository;
|
||||
use App\Repository\MachinePieceLinkRepository;
|
||||
@@ -23,9 +21,6 @@ use App\Repository\MachineProductLinkRepository;
|
||||
use App\Repository\MachineRepository;
|
||||
use App\Repository\PieceRepository;
|
||||
use App\Repository\ProductRepository;
|
||||
use App\Repository\TypeMachineComponentRequirementRepository;
|
||||
use App\Repository\TypeMachinePieceRequirementRepository;
|
||||
use App\Repository\TypeMachineProductRequirementRepository;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@@ -34,7 +29,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/api/machines')]
|
||||
class MachineSkeletonController extends AbstractController
|
||||
class MachineStructureController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
@@ -45,13 +40,10 @@ class MachineSkeletonController extends AbstractController
|
||||
private readonly ComposantRepository $composantRepository,
|
||||
private readonly PieceRepository $pieceRepository,
|
||||
private readonly ProductRepository $productRepository,
|
||||
private readonly TypeMachineComponentRequirementRepository $componentRequirementRepository,
|
||||
private readonly TypeMachinePieceRequirementRepository $pieceRequirementRepository,
|
||||
private readonly TypeMachineProductRequirementRepository $productRequirementRepository,
|
||||
) {}
|
||||
|
||||
#[Route('/{id}/skeleton', name: 'machine_skeleton_get', methods: ['GET'])]
|
||||
public function getSkeleton(string $id): JsonResponse
|
||||
#[Route('/{id}/structure', name: 'machine_structure_get', methods: ['GET'])]
|
||||
public function getStructure(string $id): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||
|
||||
@@ -64,7 +56,7 @@ class MachineSkeletonController extends AbstractController
|
||||
$pieceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $machine]);
|
||||
$productLinks = $this->machineProductLinkRepository->findBy(['machine' => $machine]);
|
||||
|
||||
return $this->json($this->normalizeMachineSkeletonResponse(
|
||||
return $this->json($this->normalizeStructureResponse(
|
||||
$machine,
|
||||
$componentLinks,
|
||||
$pieceLinks,
|
||||
@@ -72,8 +64,8 @@ class MachineSkeletonController extends AbstractController
|
||||
));
|
||||
}
|
||||
|
||||
#[Route('/{id}/skeleton', name: 'machine_skeleton_update', methods: ['PATCH'])]
|
||||
public function updateSkeleton(string $id, Request $request): JsonResponse
|
||||
#[Route('/{id}/structure', name: 'machine_structure_update', methods: ['PATCH'])]
|
||||
public function updateStructure(string $id, Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||
|
||||
@@ -108,7 +100,7 @@ class MachineSkeletonController extends AbstractController
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json($this->normalizeMachineSkeletonResponse(
|
||||
return $this->json($this->normalizeStructureResponse(
|
||||
$machine,
|
||||
$componentLinks,
|
||||
$pieceLinks,
|
||||
@@ -116,6 +108,194 @@ class MachineSkeletonController extends AbstractController
|
||||
));
|
||||
}
|
||||
|
||||
#[Route('/{id}/clone', name: 'machine_clone', methods: ['POST'])]
|
||||
public function cloneMachine(string $id, Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||
|
||||
$source = $this->machineRepository->find($id);
|
||||
if (!$source instanceof Machine) {
|
||||
return $this->json(['success' => false, 'error' => 'Machine source introuvable.'], 404);
|
||||
}
|
||||
|
||||
$payload = json_decode($request->getContent(), true);
|
||||
if (!is_array($payload) || empty($payload['name']) || empty($payload['siteId'])) {
|
||||
return $this->json(['success' => false, 'error' => 'name et siteId sont requis.'], 400);
|
||||
}
|
||||
|
||||
$site = $this->entityManager->getRepository(\App\Entity\Site::class)->find($payload['siteId']);
|
||||
if (!$site) {
|
||||
return $this->json(['success' => false, 'error' => 'Site introuvable.'], 404);
|
||||
}
|
||||
|
||||
// Create new machine
|
||||
$newMachine = new Machine();
|
||||
$newMachine->setName($payload['name']);
|
||||
$newMachine->setSite($site);
|
||||
if (!empty($payload['reference'])) {
|
||||
$newMachine->setReference($payload['reference']);
|
||||
}
|
||||
$newMachine->setPrix($source->getPrix());
|
||||
|
||||
// Copy constructeurs
|
||||
foreach ($source->getConstructeurs() as $constructeur) {
|
||||
$newMachine->getConstructeurs()->add($constructeur);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($newMachine);
|
||||
|
||||
// Copy custom fields and values
|
||||
$this->cloneCustomFields($source, $newMachine);
|
||||
|
||||
// Copy component links (preserving hierarchy)
|
||||
$componentLinkMap = $this->cloneComponentLinks($source, $newMachine);
|
||||
|
||||
// Copy piece links
|
||||
$pieceLinkMap = $this->clonePieceLinks($source, $newMachine, $componentLinkMap);
|
||||
|
||||
// Copy product links
|
||||
$this->cloneProductLinks($source, $newMachine, $componentLinkMap, $pieceLinkMap);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$componentLinks = $this->machineComponentLinkRepository->findBy(['machine' => $newMachine]);
|
||||
$pieceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $newMachine]);
|
||||
$productLinks = $this->machineProductLinkRepository->findBy(['machine' => $newMachine]);
|
||||
|
||||
return $this->json($this->normalizeStructureResponse(
|
||||
$newMachine,
|
||||
$componentLinks,
|
||||
$pieceLinks,
|
||||
$productLinks
|
||||
), 201);
|
||||
}
|
||||
|
||||
private function cloneCustomFields(Machine $source, Machine $target): void
|
||||
{
|
||||
foreach ($source->getCustomFields() as $cf) {
|
||||
$newCf = new CustomField();
|
||||
$newCf->setName($cf->getName());
|
||||
$newCf->setType($cf->getType());
|
||||
$newCf->setRequired($cf->isRequired());
|
||||
$newCf->setDefaultValue($cf->getDefaultValue());
|
||||
$newCf->setOptions($cf->getOptions());
|
||||
$newCf->setOrderIndex($cf->getOrderIndex());
|
||||
$newCf->setMachine($target);
|
||||
$this->entityManager->persist($newCf);
|
||||
}
|
||||
|
||||
foreach ($source->getCustomFieldValues() as $cfv) {
|
||||
$newValue = new CustomFieldValue();
|
||||
$newValue->setMachine($target);
|
||||
$newValue->setCustomField($cfv->getCustomField());
|
||||
$newValue->setValue($cfv->getValue());
|
||||
$this->entityManager->persist($newValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, MachineComponentLink> Map of old link ID → new link
|
||||
*/
|
||||
private function cloneComponentLinks(Machine $source, Machine $target): array
|
||||
{
|
||||
$sourceLinks = $this->machineComponentLinkRepository->findBy(['machine' => $source]);
|
||||
$linkMap = [];
|
||||
|
||||
// First pass: create all links without parent relationships
|
||||
foreach ($sourceLinks as $link) {
|
||||
$newLink = new MachineComponentLink();
|
||||
$newLink->setMachine($target);
|
||||
$newLink->setComposant($link->getComposant());
|
||||
$newLink->setNameOverride($link->getNameOverride());
|
||||
$newLink->setReferenceOverride($link->getReferenceOverride());
|
||||
$newLink->setPrixOverride($link->getPrixOverride());
|
||||
$this->entityManager->persist($newLink);
|
||||
$linkMap[$link->getId()] = $newLink;
|
||||
}
|
||||
|
||||
// Second pass: set parent relationships
|
||||
foreach ($sourceLinks as $link) {
|
||||
$parent = $link->getParentLink();
|
||||
if ($parent && isset($linkMap[$parent->getId()])) {
|
||||
$linkMap[$link->getId()]->setParentLink($linkMap[$parent->getId()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $linkMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, MachineComponentLink> $componentLinkMap
|
||||
*
|
||||
* @return array<string, MachinePieceLink> Map of old link ID → new link
|
||||
*/
|
||||
private function clonePieceLinks(Machine $source, Machine $target, array $componentLinkMap): array
|
||||
{
|
||||
$sourceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $source]);
|
||||
$linkMap = [];
|
||||
|
||||
foreach ($sourceLinks as $link) {
|
||||
$newLink = new MachinePieceLink();
|
||||
$newLink->setMachine($target);
|
||||
$newLink->setPiece($link->getPiece());
|
||||
$newLink->setNameOverride($link->getNameOverride());
|
||||
$newLink->setReferenceOverride($link->getReferenceOverride());
|
||||
$newLink->setPrixOverride($link->getPrixOverride());
|
||||
|
||||
$parent = $link->getParentLink();
|
||||
if ($parent && isset($componentLinkMap[$parent->getId()])) {
|
||||
$newLink->setParentLink($componentLinkMap[$parent->getId()]);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($newLink);
|
||||
$linkMap[$link->getId()] = $newLink;
|
||||
}
|
||||
|
||||
return $linkMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, MachineComponentLink> $componentLinkMap
|
||||
* @param array<string, MachinePieceLink> $pieceLinkMap
|
||||
*/
|
||||
private function cloneProductLinks(
|
||||
Machine $source,
|
||||
Machine $target,
|
||||
array $componentLinkMap,
|
||||
array $pieceLinkMap,
|
||||
): void {
|
||||
$sourceLinks = $this->machineProductLinkRepository->findBy(['machine' => $source]);
|
||||
$linkMap = [];
|
||||
|
||||
// First pass: create all links
|
||||
foreach ($sourceLinks as $link) {
|
||||
$newLink = new MachineProductLink();
|
||||
$newLink->setMachine($target);
|
||||
$newLink->setProduct($link->getProduct());
|
||||
|
||||
$parentComponent = $link->getParentComponentLink();
|
||||
if ($parentComponent && isset($componentLinkMap[$parentComponent->getId()])) {
|
||||
$newLink->setParentComponentLink($componentLinkMap[$parentComponent->getId()]);
|
||||
}
|
||||
|
||||
$parentPiece = $link->getParentPieceLink();
|
||||
if ($parentPiece && isset($pieceLinkMap[$parentPiece->getId()])) {
|
||||
$newLink->setParentPieceLink($pieceLinkMap[$parentPiece->getId()]);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($newLink);
|
||||
$linkMap[$link->getId()] = $newLink;
|
||||
}
|
||||
|
||||
// Second pass: set parent product link relationships
|
||||
foreach ($sourceLinks as $link) {
|
||||
$parent = $link->getParentLink();
|
||||
if ($parent && isset($linkMap[$parent->getId()])) {
|
||||
$linkMap[$link->getId()]->setParentLink($linkMap[$parent->getId()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizePayloadList(mixed $value): array
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
@@ -144,7 +324,7 @@ class MachineSkeletonController extends AbstractController
|
||||
|
||||
$composantId = $this->resolveIdentifier($entry, ['composantId', 'componentId', 'idComposant']);
|
||||
if (!$composantId) {
|
||||
return $this->json(['success' => false, 'error' => 'Composant requis pour le squelette.'], 400);
|
||||
return $this->json(['success' => false, 'error' => 'Composant requis.'], 400);
|
||||
}
|
||||
$composant = $this->composantRepository->find($composantId);
|
||||
if (!$composant instanceof Composant) {
|
||||
@@ -154,14 +334,6 @@ class MachineSkeletonController extends AbstractController
|
||||
$link->setMachine($machine);
|
||||
$link->setComposant($composant);
|
||||
|
||||
$requirementId = $this->resolveIdentifier($entry, ['requirementId', 'typeMachineComponentRequirementId']);
|
||||
if ($requirementId) {
|
||||
$requirement = $this->componentRequirementRepository->find($requirementId);
|
||||
if ($requirement instanceof TypeMachineComponentRequirement) {
|
||||
$link->setTypeMachineComponentRequirement($requirement);
|
||||
}
|
||||
}
|
||||
|
||||
$this->applyOverrides($link, $entry['overrides'] ?? null);
|
||||
|
||||
$pendingParents[$linkId] = $this->resolveIdentifier($entry, [
|
||||
@@ -176,10 +348,7 @@ class MachineSkeletonController extends AbstractController
|
||||
}
|
||||
|
||||
foreach ($pendingParents as $linkId => $parentId) {
|
||||
if (!$parentId) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($links[$linkId])) {
|
||||
if (!$parentId || !isset($links[$linkId])) {
|
||||
continue;
|
||||
}
|
||||
$parent = $links[$parentId] ?? $existing[$parentId] ?? null;
|
||||
@@ -213,7 +382,7 @@ class MachineSkeletonController extends AbstractController
|
||||
|
||||
$pieceId = $this->resolveIdentifier($entry, ['pieceId']);
|
||||
if (!$pieceId) {
|
||||
return $this->json(['success' => false, 'error' => 'Pièce requise pour le squelette.'], 400);
|
||||
return $this->json(['success' => false, 'error' => 'Pièce requise.'], 400);
|
||||
}
|
||||
$piece = $this->pieceRepository->find($pieceId);
|
||||
if (!$piece instanceof Piece) {
|
||||
@@ -223,14 +392,6 @@ class MachineSkeletonController extends AbstractController
|
||||
$link->setMachine($machine);
|
||||
$link->setPiece($piece);
|
||||
|
||||
$requirementId = $this->resolveIdentifier($entry, ['requirementId', 'typeMachinePieceRequirementId']);
|
||||
if ($requirementId) {
|
||||
$requirement = $this->pieceRequirementRepository->find($requirementId);
|
||||
if ($requirement instanceof TypeMachinePieceRequirement) {
|
||||
$link->setTypeMachinePieceRequirement($requirement);
|
||||
}
|
||||
}
|
||||
|
||||
$this->applyOverrides($link, $entry['overrides'] ?? null);
|
||||
|
||||
$pendingParents[$linkId] = $this->resolveIdentifier($entry, [
|
||||
@@ -245,10 +406,7 @@ class MachineSkeletonController extends AbstractController
|
||||
}
|
||||
|
||||
foreach ($pendingParents as $linkId => $parentId) {
|
||||
if (!$parentId) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($links[$linkId])) {
|
||||
if (!$parentId || !isset($links[$linkId])) {
|
||||
continue;
|
||||
}
|
||||
$parent = $componentIndex[$parentId] ?? null;
|
||||
@@ -287,7 +445,7 @@ class MachineSkeletonController extends AbstractController
|
||||
|
||||
$productId = $this->resolveIdentifier($entry, ['productId']);
|
||||
if (!$productId) {
|
||||
return $this->json(['success' => false, 'error' => 'Produit requis pour le squelette.'], 400);
|
||||
return $this->json(['success' => false, 'error' => 'Produit requis.'], 400);
|
||||
}
|
||||
$product = $this->productRepository->find($productId);
|
||||
if (!$product instanceof Product) {
|
||||
@@ -297,14 +455,6 @@ class MachineSkeletonController extends AbstractController
|
||||
$link->setMachine($machine);
|
||||
$link->setProduct($product);
|
||||
|
||||
$requirementId = $this->resolveIdentifier($entry, ['requirementId', 'typeMachineProductRequirementId']);
|
||||
if ($requirementId) {
|
||||
$requirement = $this->productRequirementRepository->find($requirementId);
|
||||
if ($requirement instanceof TypeMachineProductRequirement) {
|
||||
$link->setTypeMachineProductRequirement($requirement);
|
||||
}
|
||||
}
|
||||
|
||||
$pendingParents[$linkId] = [
|
||||
'parentComponentLinkId' => $this->resolveIdentifier($entry, ['parentComponentLinkId']),
|
||||
'parentPieceLinkId' => $this->resolveIdentifier($entry, ['parentPieceLinkId']),
|
||||
@@ -336,7 +486,7 @@ class MachineSkeletonController extends AbstractController
|
||||
return array_values($links);
|
||||
}
|
||||
|
||||
private function normalizeMachineSkeletonResponse(
|
||||
private function normalizeStructureResponse(
|
||||
Machine $machine,
|
||||
array $componentLinks,
|
||||
array $pieceLinks,
|
||||
@@ -346,7 +496,6 @@ class MachineSkeletonController extends AbstractController
|
||||
$componentIndex = $this->indexNormalizedLinks($normalizedComponentLinks);
|
||||
$normalizedPieceLinks = $this->normalizePieceLinks($pieceLinks);
|
||||
|
||||
// Build component hierarchy – track which IDs are children
|
||||
$childIds = [];
|
||||
foreach ($normalizedComponentLinks as $link) {
|
||||
$parentId = $link['parentComponentLinkId'] ?? null;
|
||||
@@ -356,10 +505,8 @@ class MachineSkeletonController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
// Add pieces to components recursively
|
||||
$this->attachPiecesToComponents($componentIndex, $normalizedPieceLinks);
|
||||
|
||||
// Only return root-level components (exclude children already nested)
|
||||
$rootComponents = array_filter(
|
||||
$componentIndex,
|
||||
static fn (array $link) => !isset($childIds[$link['id']]),
|
||||
@@ -382,7 +529,6 @@ class MachineSkeletonController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively attach to child components
|
||||
foreach ($componentIndex as &$component) {
|
||||
if (!empty($component['childLinks'])) {
|
||||
$this->attachPiecesToChildComponents($component['childLinks'], $pieceLinks);
|
||||
@@ -403,7 +549,6 @@ class MachineSkeletonController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process nested children
|
||||
if (!empty($child['childLinks'])) {
|
||||
$this->attachPiecesToChildComponents($child['childLinks'], $pieceLinks);
|
||||
}
|
||||
@@ -412,8 +557,7 @@ class MachineSkeletonController extends AbstractController
|
||||
|
||||
private function normalizeMachine(Machine $machine): array
|
||||
{
|
||||
$site = $machine->getSite();
|
||||
$typeMachine = $machine->getTypeMachine();
|
||||
$site = $machine->getSite();
|
||||
|
||||
return [
|
||||
'id' => $machine->getId(),
|
||||
@@ -425,24 +569,8 @@ class MachineSkeletonController extends AbstractController
|
||||
'id' => $site->getId(),
|
||||
'name' => $site->getName(),
|
||||
],
|
||||
'typeMachineId' => $typeMachine?->getId(),
|
||||
'typeMachine' => $typeMachine ? [
|
||||
'id' => $typeMachine->getId(),
|
||||
'name' => $typeMachine->getName(),
|
||||
'category' => $typeMachine->getCategory(),
|
||||
'description' => $typeMachine->getDescription(),
|
||||
'customFields' => $this->normalizeCustomFields($typeMachine->getCustomFields()),
|
||||
'componentRequirements' => $typeMachine->getComponentRequirements()
|
||||
->map(fn (TypeMachineComponentRequirement $req) => $this->normalizeComponentRequirement($req))
|
||||
->toArray(),
|
||||
'pieceRequirements' => $typeMachine->getPieceRequirements()
|
||||
->map(fn (TypeMachinePieceRequirement $req) => $this->normalizePieceRequirement($req))
|
||||
->toArray(),
|
||||
'productRequirements' => $typeMachine->getProductRequirements()
|
||||
->map(fn (TypeMachineProductRequirement $req) => $this->normalizeProductRequirement($req))
|
||||
->toArray(),
|
||||
] : null,
|
||||
'constructeurs' => $this->normalizeConstructeurs($machine->getConstructeurs()),
|
||||
'customFields' => $this->normalizeCustomFields($machine->getCustomFields()),
|
||||
'documents' => null,
|
||||
'customFieldValues' => null,
|
||||
];
|
||||
@@ -472,26 +600,21 @@ class MachineSkeletonController extends AbstractController
|
||||
private function normalizeComponentLinks(array $links): array
|
||||
{
|
||||
return array_map(function (MachineComponentLink $link): array {
|
||||
$composant = $link->getComposant();
|
||||
$requirement = $link->getTypeMachineComponentRequirement();
|
||||
$parentLink = $link->getParentLink();
|
||||
$parentRequirementId = $parentLink?->getTypeMachineComponentRequirement()?->getId();
|
||||
$composant = $link->getComposant();
|
||||
$parentLink = $link->getParentLink();
|
||||
|
||||
return [
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'composantId' => $composant->getId(),
|
||||
'composant' => $this->normalizeComposant($composant),
|
||||
'typeMachineComponentRequirementId' => $requirement?->getId(),
|
||||
'typeMachineComponentRequirement' => $requirement ? $this->normalizeComponentRequirement($requirement) : null,
|
||||
'parentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||
'parentMachineComponentRequirementId' => $parentRequirementId,
|
||||
'overrides' => $this->normalizeOverrides($link),
|
||||
'childLinks' => [],
|
||||
'pieceLinks' => [],
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'composantId' => $composant->getId(),
|
||||
'composant' => $this->normalizeComposant($composant),
|
||||
'parentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||
'overrides' => $this->normalizeOverrides($link),
|
||||
'childLinks' => [],
|
||||
'pieceLinks' => [],
|
||||
];
|
||||
}, $links);
|
||||
}
|
||||
@@ -499,24 +622,19 @@ class MachineSkeletonController extends AbstractController
|
||||
private function normalizePieceLinks(array $links): array
|
||||
{
|
||||
return array_map(function (MachinePieceLink $link): array {
|
||||
$piece = $link->getPiece();
|
||||
$requirement = $link->getTypeMachinePieceRequirement();
|
||||
$parentLink = $link->getParentLink();
|
||||
$parentRequirementId = $parentLink?->getTypeMachineComponentRequirement()?->getId();
|
||||
$piece = $link->getPiece();
|
||||
$parentLink = $link->getParentLink();
|
||||
|
||||
return [
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'pieceId' => $piece->getId(),
|
||||
'piece' => $this->normalizePiece($piece),
|
||||
'typeMachinePieceRequirementId' => $requirement?->getId(),
|
||||
'typeMachinePieceRequirement' => $requirement ? $this->normalizePieceRequirement($requirement) : null,
|
||||
'parentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||
'parentMachineComponentRequirementId' => $parentRequirementId,
|
||||
'overrides' => $this->normalizeOverrides($link),
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'pieceId' => $piece->getId(),
|
||||
'piece' => $this->normalizePiece($piece),
|
||||
'parentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||
'overrides' => $this->normalizeOverrides($link),
|
||||
];
|
||||
}, $links);
|
||||
}
|
||||
@@ -524,55 +642,58 @@ class MachineSkeletonController extends AbstractController
|
||||
private function normalizeProductLinks(array $links): array
|
||||
{
|
||||
return array_map(function (MachineProductLink $link): array {
|
||||
$product = $link->getProduct();
|
||||
$requirement = $link->getTypeMachineProductRequirement();
|
||||
$product = $link->getProduct();
|
||||
|
||||
return [
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'productId' => $product->getId(),
|
||||
'product' => $this->normalizeProduct($product),
|
||||
'typeMachineProductRequirementId' => $requirement?->getId(),
|
||||
'typeMachineProductRequirement' => $requirement ? $this->normalizeProductRequirement($requirement) : null,
|
||||
'parentLinkId' => $link->getParentLink()?->getId(),
|
||||
'parentComponentLinkId' => $link->getParentComponentLink()?->getId(),
|
||||
'parentPieceLinkId' => $link->getParentPieceLink()?->getId(),
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'productId' => $product->getId(),
|
||||
'product' => $this->normalizeProduct($product),
|
||||
'parentLinkId' => $link->getParentLink()?->getId(),
|
||||
'parentComponentLinkId' => $link->getParentComponentLink()?->getId(),
|
||||
'parentPieceLinkId' => $link->getParentPieceLink()?->getId(),
|
||||
];
|
||||
}, $links);
|
||||
}
|
||||
|
||||
private function normalizeComposant(Composant $composant): array
|
||||
{
|
||||
$type = $composant->getTypeComposant();
|
||||
|
||||
return [
|
||||
'id' => $composant->getId(),
|
||||
'name' => $composant->getName(),
|
||||
'reference' => $composant->getReference(),
|
||||
'prix' => $composant->getPrix(),
|
||||
'typeComposantId' => $composant->getTypeComposant()?->getId(),
|
||||
'typeComposant' => $this->normalizeModelType($composant->getTypeComposant()),
|
||||
'productId' => $composant->getProduct()?->getId(),
|
||||
'product' => $composant->getProduct() ? $this->normalizeProduct($composant->getProduct()) : null,
|
||||
'constructeurs' => $this->normalizeConstructeurs($composant->getConstructeurs()),
|
||||
'documents' => [],
|
||||
'customFields' => [],
|
||||
'id' => $composant->getId(),
|
||||
'name' => $composant->getName(),
|
||||
'reference' => $composant->getReference(),
|
||||
'prix' => $composant->getPrix(),
|
||||
'typeComposantId' => $type?->getId(),
|
||||
'typeComposant' => $this->normalizeModelType($type),
|
||||
'productId' => $composant->getProduct()?->getId(),
|
||||
'product' => $composant->getProduct() ? $this->normalizeProduct($composant->getProduct()) : null,
|
||||
'constructeurs' => $this->normalizeConstructeurs($composant->getConstructeurs()),
|
||||
'documents' => [],
|
||||
'customFields' => $type ? $this->normalizeCustomFieldDefinitions($type->getComponentCustomFields()) : [],
|
||||
'customFieldValues' => $this->normalizeCustomFieldValues($composant->getCustomFieldValues()),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizePiece(Piece $piece): array
|
||||
{
|
||||
$type = $piece->getTypePiece();
|
||||
|
||||
return [
|
||||
'id' => $piece->getId(),
|
||||
'name' => $piece->getName(),
|
||||
'reference' => $piece->getReference(),
|
||||
'prix' => $piece->getPrix(),
|
||||
'typePieceId' => $piece->getTypePiece()?->getId(),
|
||||
'typePiece' => $this->normalizeModelType($piece->getTypePiece()),
|
||||
'productId' => $piece->getProduct()?->getId(),
|
||||
'product' => $piece->getProduct() ? $this->normalizeProduct($piece->getProduct()) : null,
|
||||
'constructeurs' => $this->normalizeConstructeurs($piece->getConstructeurs()),
|
||||
'documents' => [],
|
||||
'customFields' => [],
|
||||
'id' => $piece->getId(),
|
||||
'name' => $piece->getName(),
|
||||
'reference' => $piece->getReference(),
|
||||
'prix' => $piece->getPrix(),
|
||||
'typePieceId' => $type?->getId(),
|
||||
'typePiece' => $this->normalizeModelType($type),
|
||||
'productId' => $piece->getProduct()?->getId(),
|
||||
'product' => $piece->getProduct() ? $this->normalizeProduct($piece->getProduct()) : null,
|
||||
'constructeurs' => $this->normalizeConstructeurs($piece->getConstructeurs()),
|
||||
'documents' => [],
|
||||
'customFields' => $type ? $this->normalizeCustomFieldDefinitions($type->getPieceCustomFields()) : [],
|
||||
'customFieldValues' => $this->normalizeCustomFieldValues($piece->getCustomFieldValues()),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -598,49 +719,11 @@ class MachineSkeletonController extends AbstractController
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $type->getId(),
|
||||
'name' => $type->getName(),
|
||||
'code' => $type->getCode(),
|
||||
'category' => $type->getCategory()->value,
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeComponentRequirement(TypeMachineComponentRequirement $requirement): array
|
||||
{
|
||||
return [
|
||||
'id' => $requirement->getId(),
|
||||
'label' => $requirement->getLabel(),
|
||||
'minCount' => $requirement->getMinCount(),
|
||||
'maxCount' => $requirement->getMaxCount(),
|
||||
'required' => $requirement->isRequired(),
|
||||
'typeComposantId' => $requirement->getTypeComposant()->getId(),
|
||||
'typeComposant' => $this->normalizeModelType($requirement->getTypeComposant()),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizePieceRequirement(TypeMachinePieceRequirement $requirement): array
|
||||
{
|
||||
return [
|
||||
'id' => $requirement->getId(),
|
||||
'label' => $requirement->getLabel(),
|
||||
'minCount' => $requirement->getMinCount(),
|
||||
'maxCount' => $requirement->getMaxCount(),
|
||||
'required' => $requirement->isRequired(),
|
||||
'typePieceId' => $requirement->getTypePiece()->getId(),
|
||||
'typePiece' => $this->normalizeModelType($requirement->getTypePiece()),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeProductRequirement(TypeMachineProductRequirement $requirement): array
|
||||
{
|
||||
return [
|
||||
'id' => $requirement->getId(),
|
||||
'label' => $requirement->getLabel(),
|
||||
'minCount' => $requirement->getMinCount(),
|
||||
'maxCount' => $requirement->getMaxCount(),
|
||||
'required' => $requirement->isRequired(),
|
||||
'typeProductId' => $requirement->getTypeProduct()->getId(),
|
||||
'typeProduct' => $this->normalizeModelType($requirement->getTypeProduct()),
|
||||
'id' => $type->getId(),
|
||||
'name' => $type->getName(),
|
||||
'code' => $type->getCode(),
|
||||
'category' => $type->getCategory()->value,
|
||||
'structure' => $type->getStructure(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -659,6 +742,55 @@ class MachineSkeletonController extends AbstractController
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function normalizeCustomFieldDefinitions(Collection $customFields): array
|
||||
{
|
||||
$items = [];
|
||||
foreach ($customFields as $cf) {
|
||||
if (!$cf instanceof CustomField) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'id' => $cf->getId(),
|
||||
'name' => $cf->getName(),
|
||||
'type' => $cf->getType(),
|
||||
'required' => $cf->isRequired(),
|
||||
'options' => $cf->getOptions(),
|
||||
'defaultValue' => $cf->getDefaultValue(),
|
||||
'orderIndex' => $cf->getOrderIndex(),
|
||||
];
|
||||
}
|
||||
|
||||
usort($items, static fn (array $a, array $b) => $a['orderIndex'] <=> $b['orderIndex']);
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function normalizeCustomFieldValues(Collection $customFieldValues): array
|
||||
{
|
||||
$items = [];
|
||||
foreach ($customFieldValues as $cfv) {
|
||||
if (!$cfv instanceof CustomFieldValue) {
|
||||
continue;
|
||||
}
|
||||
$cf = $cfv->getCustomField();
|
||||
$items[] = [
|
||||
'id' => $cfv->getId(),
|
||||
'value' => $cfv->getValue(),
|
||||
'customField' => [
|
||||
'id' => $cf->getId(),
|
||||
'name' => $cf->getName(),
|
||||
'type' => $cf->getType(),
|
||||
'required' => $cf->isRequired(),
|
||||
'options' => $cf->getOptions(),
|
||||
'defaultValue' => $cf->getDefaultValue(),
|
||||
'orderIndex' => $cf->getOrderIndex(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function normalizeOverrides(object $link): ?array
|
||||
{
|
||||
$name = method_exists($link, 'getNameOverride') ? $link->getNameOverride() : null;
|
||||
@@ -20,8 +20,8 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
#[ORM\Table(name: 'comments')]
|
||||
#[ORM\Index(columns: ['entity_type', 'entity_id', 'status'], name: 'idx_comment_entity_status')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['entityType' => 'exact', 'entityId' => 'exact', 'status' => 'exact'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['createdAt'])]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['entityType' => 'exact', 'entityId' => 'exact', 'status' => 'exact', 'entityName' => 'ipartial'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['createdAt', 'authorName', 'status'])]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||
|
||||
@@ -25,7 +25,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
#[ORM\Entity(repositoryClass: ComposantRepository::class)]
|
||||
#[ORM\Table(name: 'composants')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeComposant' => 'exact'])]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeComposant' => 'exact', 'typeComposant.name' => 'ipartial'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
|
||||
@@ -62,9 +62,9 @@ class CustomField
|
||||
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||
private int $orderIndex = 0;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachine::class, inversedBy: 'customFields')]
|
||||
#[ORM\JoinColumn(name: 'typeMachineId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?TypeMachine $typeMachine = null;
|
||||
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'customFields')]
|
||||
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?Machine $machine = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'customFields')]
|
||||
#[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
@@ -197,14 +197,14 @@ class CustomField
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachine(): ?TypeMachine
|
||||
public function getMachine(): ?Machine
|
||||
{
|
||||
return $this->typeMachine;
|
||||
return $this->machine;
|
||||
}
|
||||
|
||||
public function setTypeMachine(?TypeMachine $typeMachine): static
|
||||
public function setMachine(?Machine $machine): static
|
||||
{
|
||||
$this->typeMachine = $typeMachine;
|
||||
$this->machine = $machine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
#[ORM\Entity(repositoryClass: DocumentRepository::class)]
|
||||
#[ORM\Table(name: 'documents')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'partial', 'filename' => 'partial'])]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'filename' => 'ipartial'])]
|
||||
#[ApiFilter(ExistsFilter::class, properties: ['site', 'machine', 'composant', 'piece', 'product'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['createdAt', 'name', 'size'])]
|
||||
#[ApiResource(
|
||||
|
||||
@@ -53,10 +53,6 @@ class Machine
|
||||
#[ORM\JoinColumn(name: 'siteId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Site $site;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachine::class, inversedBy: 'machines')]
|
||||
#[ORM\JoinColumn(name: 'typeMachineId', referencedColumnName: 'id', nullable: true)]
|
||||
private ?TypeMachine $typeMachine = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Constructeur>
|
||||
*/
|
||||
@@ -92,6 +88,12 @@ class Machine
|
||||
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: Document::class)]
|
||||
private Collection $documents;
|
||||
|
||||
/**
|
||||
* @var Collection<int, CustomField>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: CustomField::class, cascade: ['persist', 'remove'])]
|
||||
private Collection $customFields;
|
||||
|
||||
/**
|
||||
* @var Collection<int, CustomFieldValue>
|
||||
*/
|
||||
@@ -111,6 +113,7 @@ class Machine
|
||||
$this->pieceLinks = new ArrayCollection();
|
||||
$this->productLinks = new ArrayCollection();
|
||||
$this->documents = new ArrayCollection();
|
||||
$this->customFields = new ArrayCollection();
|
||||
$this->customFieldValues = new ArrayCollection();
|
||||
}
|
||||
|
||||
@@ -192,14 +195,31 @@ class Machine
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachine(): ?TypeMachine
|
||||
/**
|
||||
* @return Collection<int, CustomField>
|
||||
*/
|
||||
public function getCustomFields(): Collection
|
||||
{
|
||||
return $this->typeMachine;
|
||||
return $this->customFields;
|
||||
}
|
||||
|
||||
public function setTypeMachine(?TypeMachine $typeMachine): static
|
||||
public function addCustomField(CustomField $customField): static
|
||||
{
|
||||
$this->typeMachine = $typeMachine;
|
||||
if (!$this->customFields->contains($customField)) {
|
||||
$this->customFields->add($customField);
|
||||
$customField->setMachine($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeCustomField(CustomField $customField): static
|
||||
{
|
||||
if ($this->customFields->removeElement($customField)) {
|
||||
if ($customField->getMachine() === $this) {
|
||||
$customField->setMachine(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -55,10 +55,6 @@ class MachineComponentLink
|
||||
#[ORM\OneToMany(mappedBy: 'parentLink', targetEntity: MachineComponentLink::class)]
|
||||
private Collection $childLinks;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachineComponentRequirement::class, inversedBy: 'machineComponentLinks')]
|
||||
#[ORM\JoinColumn(name: 'typeMachineComponentRequirementId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?TypeMachineComponentRequirement $typeMachineComponentRequirement = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachinePieceLink>
|
||||
*/
|
||||
@@ -159,18 +155,6 @@ class MachineComponentLink
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachineComponentRequirement(): ?TypeMachineComponentRequirement
|
||||
{
|
||||
return $this->typeMachineComponentRequirement;
|
||||
}
|
||||
|
||||
public function setTypeMachineComponentRequirement(?TypeMachineComponentRequirement $requirement): static
|
||||
{
|
||||
$this->typeMachineComponentRequirement = $requirement;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNameOverride(): ?string
|
||||
{
|
||||
return $this->nameOverride;
|
||||
|
||||
@@ -49,10 +49,6 @@ class MachinePieceLink
|
||||
#[ORM\JoinColumn(name: 'parentLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?MachineComponentLink $parentLink = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachinePieceRequirement::class, inversedBy: 'machinePieceLinks')]
|
||||
#[ORM\JoinColumn(name: 'typeMachinePieceRequirementId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?TypeMachinePieceRequirement $typeMachinePieceRequirement = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachineProductLink>
|
||||
*/
|
||||
@@ -145,18 +141,6 @@ class MachinePieceLink
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachinePieceRequirement(): ?TypeMachinePieceRequirement
|
||||
{
|
||||
return $this->typeMachinePieceRequirement;
|
||||
}
|
||||
|
||||
public function setTypeMachinePieceRequirement(?TypeMachinePieceRequirement $requirement): static
|
||||
{
|
||||
$this->typeMachinePieceRequirement = $requirement;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNameOverride(): ?string
|
||||
{
|
||||
return $this->nameOverride;
|
||||
|
||||
@@ -45,10 +45,6 @@ class MachineProductLink
|
||||
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Product $product;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachineProductRequirement::class, inversedBy: 'machineProductLinks')]
|
||||
#[ORM\JoinColumn(name: 'typeMachineProductRequirementId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?TypeMachineProductRequirement $typeMachineProductRequirement = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: MachineProductLink::class, inversedBy: 'childLinks')]
|
||||
#[ORM\JoinColumn(name: 'parentLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?MachineProductLink $parentLink = null;
|
||||
@@ -132,18 +128,6 @@ class MachineProductLink
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachineProductRequirement(): ?TypeMachineProductRequirement
|
||||
{
|
||||
return $this->typeMachineProductRequirement;
|
||||
}
|
||||
|
||||
public function setTypeMachineProductRequirement(?TypeMachineProductRequirement $requirement): static
|
||||
{
|
||||
$this->typeMachineProductRequirement = $requirement;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParentLink(): ?MachineProductLink
|
||||
{
|
||||
return $this->parentLink;
|
||||
|
||||
@@ -21,7 +21,7 @@ use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ModelTypeRepository::class)]
|
||||
#[ORM\Table(name: 'model_types')]
|
||||
@@ -45,11 +45,11 @@ class ModelType
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['type_machine:read', 'model_type:read'])]
|
||||
#[Groups(['type_machine:read', 'model_type:read', 'product:read', 'composant:read', 'piece:read'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 120)]
|
||||
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
|
||||
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write', 'product:read', 'composant:read', 'piece:read'])]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 60, unique: true)]
|
||||
@@ -69,15 +69,15 @@ class ModelType
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true, name: 'componentSkeleton')]
|
||||
#[Groups(['model_type:read'])]
|
||||
#[Groups(['model_type:read', 'composant:read'])]
|
||||
private ?array $componentSkeleton = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true, name: 'pieceSkeleton')]
|
||||
#[Groups(['model_type:read'])]
|
||||
#[Groups(['model_type:read', 'piece:read'])]
|
||||
private ?array $pieceSkeleton = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true, name: 'productSkeleton')]
|
||||
#[Groups(['model_type:read'])]
|
||||
#[Groups(['model_type:read', 'product:read'])]
|
||||
private ?array $productSkeleton = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
@@ -108,24 +108,6 @@ class ModelType
|
||||
#[ORM\OneToMany(mappedBy: 'typeProduct', targetEntity: Product::class)]
|
||||
private Collection $products;
|
||||
|
||||
/**
|
||||
* @var Collection<int, TypeMachineComponentRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typeComposant', targetEntity: TypeMachineComponentRequirement::class)]
|
||||
private Collection $componentRequirements;
|
||||
|
||||
/**
|
||||
* @var Collection<int, TypeMachinePieceRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typePiece', targetEntity: TypeMachinePieceRequirement::class)]
|
||||
private Collection $pieceRequirements;
|
||||
|
||||
/**
|
||||
* @var Collection<int, TypeMachineProductRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typeProduct', targetEntity: TypeMachineProductRequirement::class)]
|
||||
private Collection $productRequirements;
|
||||
|
||||
/**
|
||||
* @var Collection<int, CustomField>
|
||||
*/
|
||||
@@ -146,15 +128,12 @@ class ModelType
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->composants = new ArrayCollection();
|
||||
$this->pieces = new ArrayCollection();
|
||||
$this->products = new ArrayCollection();
|
||||
$this->componentRequirements = new ArrayCollection();
|
||||
$this->pieceRequirements = new ArrayCollection();
|
||||
$this->productRequirements = new ArrayCollection();
|
||||
$this->customFields = new ArrayCollection();
|
||||
$this->pieceCustomFields = new ArrayCollection();
|
||||
$this->productCustomFields = new ArrayCollection();
|
||||
$this->composants = new ArrayCollection();
|
||||
$this->pieces = new ArrayCollection();
|
||||
$this->products = new ArrayCollection();
|
||||
$this->customFields = new ArrayCollection();
|
||||
$this->pieceCustomFields = new ArrayCollection();
|
||||
$this->productCustomFields = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
@@ -288,7 +267,7 @@ class ModelType
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Groups(['model_type:read'])]
|
||||
#[Groups(['model_type:read', 'product:read', 'composant:read', 'piece:read'])]
|
||||
public function getStructure(): ?array
|
||||
{
|
||||
return match ($this->category) {
|
||||
@@ -312,6 +291,30 @@ class ModelType
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, CustomField>
|
||||
*/
|
||||
public function getComponentCustomFields(): Collection
|
||||
{
|
||||
return $this->customFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, CustomField>
|
||||
*/
|
||||
public function getPieceCustomFields(): Collection
|
||||
{
|
||||
return $this->pieceCustomFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, CustomField>
|
||||
*/
|
||||
public function getProductCustomFields(): Collection
|
||||
{
|
||||
return $this->productCustomFields;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
|
||||
@@ -27,7 +27,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
#[ORM\Entity(repositoryClass: PieceRepository::class)]
|
||||
#[ORM\Table(name: 'pieces')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typePiece' => 'exact'])]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typePiece' => 'exact', 'typePiece.name' => 'ipartial'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
|
||||
@@ -25,8 +25,8 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
#[ORM\Entity(repositoryClass: ProductRepository::class)]
|
||||
#[ORM\Table(name: 'products')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeProduct' => 'exact'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeProduct' => 'exact', 'typeProduct.name' => 'ipartial'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt', 'supplierPrice'])]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||
|
||||
@@ -17,7 +17,7 @@ use DateTimeImmutable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ProfileRepository::class)]
|
||||
|
||||
@@ -1,390 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\TypeMachineRepository;
|
||||
use App\State\TypeMachinePutProcessor;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TypeMachineRepository::class)]
|
||||
#[ORM\Table(name: 'type_machines')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[UniqueEntity(fields: ['name'], message: 'Ce nom de type de machine existe déjà.')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
new Put(security: "is_granted('ROLE_GESTIONNAIRE')", processor: TypeMachinePutProcessor::class, deserialize: false, validate: false),
|
||||
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
],
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class TypeMachine
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
||||
#[Assert\NotBlank]
|
||||
#[Groups(['type_machine:read', 'type_machine:write', 'machine:read'])]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'type_machine:write'])]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'type_machine:write'])]
|
||||
private ?string $category = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'type_machine:write'])]
|
||||
private ?string $maintenanceFrequency = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'type_machine:write'])]
|
||||
private ?array $components = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'type_machine:write'])]
|
||||
private ?array $criticalParts = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'type_machine:write'])]
|
||||
private ?array $machinePieces = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'type_machine:write'])]
|
||||
private ?array $specifications = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Machine>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Machine::class, mappedBy: 'typeMachine')]
|
||||
private Collection $machines;
|
||||
|
||||
/**
|
||||
* @var Collection<int, CustomField>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: CustomField::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'])]
|
||||
#[ApiProperty(readableLink: true, writableLink: true)]
|
||||
private Collection $customFields;
|
||||
|
||||
/**
|
||||
* @var Collection<int, TypeMachineComponentRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: TypeMachineComponentRequirement::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ApiProperty(readableLink: true, writableLink: true)]
|
||||
private Collection $componentRequirements;
|
||||
|
||||
/**
|
||||
* @var Collection<int, TypeMachinePieceRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: TypeMachinePieceRequirement::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ApiProperty(readableLink: true, writableLink: true)]
|
||||
private Collection $pieceRequirements;
|
||||
|
||||
/**
|
||||
* @var Collection<int, TypeMachineProductRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: TypeMachineProductRequirement::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ApiProperty(readableLink: true, writableLink: true)]
|
||||
private Collection $productRequirements;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = 'cl'.bin2hex(random_bytes(12));
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
$this->machines = new ArrayCollection();
|
||||
$this->customFields = new ArrayCollection();
|
||||
$this->componentRequirements = new ArrayCollection();
|
||||
$this->pieceRequirements = new ArrayCollection();
|
||||
$this->productRequirements = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(?string $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCategory(): ?string
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
public function setCategory(?string $category): static
|
||||
{
|
||||
$this->category = $category;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMaintenanceFrequency(): ?string
|
||||
{
|
||||
return $this->maintenanceFrequency;
|
||||
}
|
||||
|
||||
public function setMaintenanceFrequency(?string $maintenanceFrequency): static
|
||||
{
|
||||
$this->maintenanceFrequency = $maintenanceFrequency;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComponents(): ?array
|
||||
{
|
||||
return $this->components;
|
||||
}
|
||||
|
||||
public function setComponents(?array $components): static
|
||||
{
|
||||
$this->components = $components;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCriticalParts(): ?array
|
||||
{
|
||||
return $this->criticalParts;
|
||||
}
|
||||
|
||||
public function setCriticalParts(?array $criticalParts): static
|
||||
{
|
||||
$this->criticalParts = $criticalParts;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMachinePieces(): ?array
|
||||
{
|
||||
return $this->machinePieces;
|
||||
}
|
||||
|
||||
public function setMachinePieces(?array $machinePieces): static
|
||||
{
|
||||
$this->machinePieces = $machinePieces;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSpecifications(): ?array
|
||||
{
|
||||
return $this->specifications;
|
||||
}
|
||||
|
||||
public function setSpecifications(?array $specifications): static
|
||||
{
|
||||
$this->specifications = $specifications;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Machine>
|
||||
*/
|
||||
public function getMachines(): Collection
|
||||
{
|
||||
return $this->machines;
|
||||
}
|
||||
|
||||
public function addMachine(Machine $machine): static
|
||||
{
|
||||
if (!$this->machines->contains($machine)) {
|
||||
$this->machines->add($machine);
|
||||
$machine->setTypeMachine($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeMachine(Machine $machine): static
|
||||
{
|
||||
if ($this->machines->removeElement($machine)) {
|
||||
if ($machine->getTypeMachine() === $this) {
|
||||
$machine->setTypeMachine(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, CustomField>
|
||||
*/
|
||||
public function getCustomFields(): Collection
|
||||
{
|
||||
return $this->customFields;
|
||||
}
|
||||
|
||||
public function addCustomField(CustomField $customField): static
|
||||
{
|
||||
if (!$this->customFields->contains($customField)) {
|
||||
$this->customFields->add($customField);
|
||||
$customField->setTypeMachine($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeCustomField(CustomField $customField): static
|
||||
{
|
||||
if ($this->customFields->removeElement($customField)) {
|
||||
if ($customField->getTypeMachine() === $this) {
|
||||
$customField->setTypeMachine(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, TypeMachineComponentRequirement>
|
||||
*/
|
||||
public function getComponentRequirements(): Collection
|
||||
{
|
||||
return $this->componentRequirements;
|
||||
}
|
||||
|
||||
public function addComponentRequirement(TypeMachineComponentRequirement $componentRequirement): static
|
||||
{
|
||||
if (!$this->componentRequirements->contains($componentRequirement)) {
|
||||
$this->componentRequirements->add($componentRequirement);
|
||||
$componentRequirement->setTypeMachine($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeComponentRequirement(TypeMachineComponentRequirement $componentRequirement): static
|
||||
{
|
||||
$this->componentRequirements->removeElement($componentRequirement);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, TypeMachinePieceRequirement>
|
||||
*/
|
||||
public function getPieceRequirements(): Collection
|
||||
{
|
||||
return $this->pieceRequirements;
|
||||
}
|
||||
|
||||
public function addPieceRequirement(TypeMachinePieceRequirement $pieceRequirement): static
|
||||
{
|
||||
if (!$this->pieceRequirements->contains($pieceRequirement)) {
|
||||
$this->pieceRequirements->add($pieceRequirement);
|
||||
$pieceRequirement->setTypeMachine($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removePieceRequirement(TypeMachinePieceRequirement $pieceRequirement): static
|
||||
{
|
||||
$this->pieceRequirements->removeElement($pieceRequirement);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, TypeMachineProductRequirement>
|
||||
*/
|
||||
public function getProductRequirements(): Collection
|
||||
{
|
||||
return $this->productRequirements;
|
||||
}
|
||||
|
||||
public function addProductRequirement(TypeMachineProductRequirement $productRequirement): static
|
||||
{
|
||||
if (!$this->productRequirements->contains($productRequirement)) {
|
||||
$this->productRequirements->add($productRequirement);
|
||||
$productRequirement->setTypeMachine($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeProductRequirement(TypeMachineProductRequirement $productRequirement): static
|
||||
{
|
||||
$this->productRequirements->removeElement($productRequirement);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\TypeMachineComponentRequirementRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TypeMachineComponentRequirementRepository::class)]
|
||||
#[ORM\Table(name: 'type_machine_component_requirements')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
]
|
||||
)]
|
||||
class TypeMachineComponentRequirement
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?string $label = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 1], name: 'minCount')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private int $minCount = 1;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true, name: 'maxCount')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?int $maxCount = null;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => true], name: 'required')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private bool $required = true;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => true], name: 'allowNewModels')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private bool $allowNewModels = true;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'orderIndex')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private int $orderIndex = 0;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachine::class, inversedBy: 'componentRequirements')]
|
||||
#[ORM\JoinColumn(name: 'typeMachineId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private TypeMachine $typeMachine;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'componentRequirements')]
|
||||
#[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: false)]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ModelType $typeComposant;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachineComponentLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typeMachineComponentRequirement', targetEntity: MachineComponentLink::class)]
|
||||
private Collection $machineComponentLinks;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->machineComponentLinks = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(?string $label): static
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMinCount(): int
|
||||
{
|
||||
return $this->minCount;
|
||||
}
|
||||
|
||||
public function setMinCount(int $minCount): static
|
||||
{
|
||||
$this->minCount = $minCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMaxCount(): ?int
|
||||
{
|
||||
return $this->maxCount;
|
||||
}
|
||||
|
||||
public function setMaxCount(?int $maxCount): static
|
||||
{
|
||||
$this->maxCount = $maxCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function setRequired(bool $required): static
|
||||
{
|
||||
$this->required = $required;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isAllowNewModels(): bool
|
||||
{
|
||||
return $this->allowNewModels;
|
||||
}
|
||||
|
||||
public function setAllowNewModels(bool $allowNewModels): static
|
||||
{
|
||||
$this->allowNewModels = $allowNewModels;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOrderIndex(): int
|
||||
{
|
||||
return $this->orderIndex;
|
||||
}
|
||||
|
||||
public function setOrderIndex(int $orderIndex): static
|
||||
{
|
||||
$this->orderIndex = $orderIndex;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachine(): TypeMachine
|
||||
{
|
||||
return $this->typeMachine;
|
||||
}
|
||||
|
||||
public function setTypeMachine(TypeMachine $typeMachine): static
|
||||
{
|
||||
$this->typeMachine = $typeMachine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeComposant(): ModelType
|
||||
{
|
||||
return $this->typeComposant;
|
||||
}
|
||||
|
||||
public function setTypeComposant(ModelType $typeComposant): static
|
||||
{
|
||||
$this->typeComposant = $typeComposant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\TypeMachinePieceRequirementRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TypeMachinePieceRequirementRepository::class)]
|
||||
#[ORM\Table(name: 'type_machine_piece_requirements')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
]
|
||||
)]
|
||||
class TypeMachinePieceRequirement
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?string $label = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'minCount')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private int $minCount = 0;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true, name: 'maxCount')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?int $maxCount = null;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private bool $required = false;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => true], name: 'allowNewModels')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private bool $allowNewModels = true;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'orderIndex')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private int $orderIndex = 0;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachine::class, inversedBy: 'pieceRequirements')]
|
||||
#[ORM\JoinColumn(name: 'typeMachineId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private TypeMachine $typeMachine;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'pieceRequirements')]
|
||||
#[ORM\JoinColumn(name: 'typePieceId', referencedColumnName: 'id', nullable: false)]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ModelType $typePiece;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachinePieceLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typeMachinePieceRequirement', targetEntity: MachinePieceLink::class)]
|
||||
private Collection $machinePieceLinks;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->machinePieceLinks = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(?string $label): static
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMinCount(): int
|
||||
{
|
||||
return $this->minCount;
|
||||
}
|
||||
|
||||
public function setMinCount(int $minCount): static
|
||||
{
|
||||
$this->minCount = $minCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMaxCount(): ?int
|
||||
{
|
||||
return $this->maxCount;
|
||||
}
|
||||
|
||||
public function setMaxCount(?int $maxCount): static
|
||||
{
|
||||
$this->maxCount = $maxCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function setRequired(bool $required): static
|
||||
{
|
||||
$this->required = $required;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isAllowNewModels(): bool
|
||||
{
|
||||
return $this->allowNewModels;
|
||||
}
|
||||
|
||||
public function setAllowNewModels(bool $allowNewModels): static
|
||||
{
|
||||
$this->allowNewModels = $allowNewModels;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOrderIndex(): int
|
||||
{
|
||||
return $this->orderIndex;
|
||||
}
|
||||
|
||||
public function setOrderIndex(int $orderIndex): static
|
||||
{
|
||||
$this->orderIndex = $orderIndex;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachine(): TypeMachine
|
||||
{
|
||||
return $this->typeMachine;
|
||||
}
|
||||
|
||||
public function setTypeMachine(TypeMachine $typeMachine): static
|
||||
{
|
||||
$this->typeMachine = $typeMachine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypePiece(): ModelType
|
||||
{
|
||||
return $this->typePiece;
|
||||
}
|
||||
|
||||
public function setTypePiece(ModelType $typePiece): static
|
||||
{
|
||||
$this->typePiece = $typePiece;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\TypeMachineProductRequirementRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TypeMachineProductRequirementRepository::class)]
|
||||
#[ORM\Table(name: 'type_machine_product_requirements')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
]
|
||||
)]
|
||||
class TypeMachineProductRequirement
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?string $label = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'minCount')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private int $minCount = 0;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true, name: 'maxCount')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?int $maxCount = null;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private bool $required = false;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => true], name: 'allowNewModels')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private bool $allowNewModels = true;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'orderIndex')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private int $orderIndex = 0;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachine::class, inversedBy: 'productRequirements')]
|
||||
#[ORM\JoinColumn(name: 'typeMachineId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private TypeMachine $typeMachine;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'productRequirements')]
|
||||
#[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: false)]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ModelType $typeProduct;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachineProductLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typeMachineProductRequirement', targetEntity: MachineProductLink::class)]
|
||||
private Collection $machineProductLinks;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->machineProductLinks = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(?string $label): static
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMinCount(): int
|
||||
{
|
||||
return $this->minCount;
|
||||
}
|
||||
|
||||
public function setMinCount(int $minCount): static
|
||||
{
|
||||
$this->minCount = $minCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMaxCount(): ?int
|
||||
{
|
||||
return $this->maxCount;
|
||||
}
|
||||
|
||||
public function setMaxCount(?int $maxCount): static
|
||||
{
|
||||
$this->maxCount = $maxCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function setRequired(bool $required): static
|
||||
{
|
||||
$this->required = $required;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isAllowNewModels(): bool
|
||||
{
|
||||
return $this->allowNewModels;
|
||||
}
|
||||
|
||||
public function setAllowNewModels(bool $allowNewModels): static
|
||||
{
|
||||
$this->allowNewModels = $allowNewModels;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOrderIndex(): int
|
||||
{
|
||||
return $this->orderIndex;
|
||||
}
|
||||
|
||||
public function setOrderIndex(int $orderIndex): static
|
||||
{
|
||||
$this->orderIndex = $orderIndex;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachine(): TypeMachine
|
||||
{
|
||||
return $this->typeMachine;
|
||||
}
|
||||
|
||||
public function setTypeMachine(TypeMachine $typeMachine): static
|
||||
{
|
||||
$this->typeMachine = $typeMachine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeProduct(): ModelType
|
||||
{
|
||||
return $this->typeProduct;
|
||||
}
|
||||
|
||||
public function setTypeProduct(ModelType $typeProduct): static
|
||||
{
|
||||
$this->typeProduct = $typeProduct;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ use App\Entity\ModelType;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\Profile;
|
||||
use App\Entity\Site;
|
||||
use App\Entity\TypeMachine;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -286,7 +285,6 @@ final class MachineAuditSubscriber implements EventSubscriber
|
||||
'reference' => $machine->getReference(),
|
||||
'prix' => $machine->getPrix(),
|
||||
'site' => $this->normalizeValue($machine->getSite()),
|
||||
'typeMachine' => $this->normalizeValue($machine->getTypeMachine()),
|
||||
'constructeurIds' => $this->normalizeCollection($machine->getConstructeurs()),
|
||||
];
|
||||
}
|
||||
@@ -335,13 +333,6 @@ final class MachineAuditSubscriber implements EventSubscriber
|
||||
];
|
||||
}
|
||||
|
||||
if ($value instanceof TypeMachine) {
|
||||
return [
|
||||
'id' => $value->getId(),
|
||||
'name' => $value->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($value instanceof ModelType) {
|
||||
return [
|
||||
'id' => $value->getId(),
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\TypeMachineComponentRequirement;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<TypeMachineComponentRequirement>
|
||||
*/
|
||||
class TypeMachineComponentRequirementRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, TypeMachineComponentRequirement::class);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\TypeMachinePieceRequirement;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<TypeMachinePieceRequirement>
|
||||
*/
|
||||
class TypeMachinePieceRequirementRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, TypeMachinePieceRequirement::class);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\TypeMachineProductRequirement;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<TypeMachineProductRequirement>
|
||||
*/
|
||||
class TypeMachineProductRequirementRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, TypeMachineProductRequirement::class);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\TypeMachine;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<TypeMachine>
|
||||
*/
|
||||
class TypeMachineRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, TypeMachine::class);
|
||||
}
|
||||
|
||||
public function save(TypeMachine $entity, bool $flush = false): void
|
||||
{
|
||||
$this->getEntityManager()->persist($entity);
|
||||
|
||||
if ($flush) {
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
|
||||
public function remove(TypeMachine $entity, bool $flush = false): void
|
||||
{
|
||||
$this->getEntityManager()->remove($entity);
|
||||
|
||||
if ($flush) {
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,16 +137,6 @@ final class ModelTypeCategoryConversionService
|
||||
$blockers[] = sprintf('%d pièce(s) liée(s) à des machines.', $machineLinked);
|
||||
}
|
||||
|
||||
// Check type machine requirements
|
||||
$requirementCount = (int) $this->connection->fetchOne(
|
||||
'SELECT COUNT(*) FROM type_machine_piece_requirements WHERE typepieceid = :id',
|
||||
['id' => $modelTypeId],
|
||||
);
|
||||
|
||||
if ($requirementCount > 0) {
|
||||
$blockers[] = sprintf('Utilisé dans %d modèle(s) de type de machine.', $requirementCount);
|
||||
}
|
||||
|
||||
// Check name collision with existing composants
|
||||
$collisions = $this->connection->fetchFirstColumn(
|
||||
'SELECT p.name FROM pieces p
|
||||
@@ -210,16 +200,6 @@ final class ModelTypeCategoryConversionService
|
||||
$blockers[] = sprintf('%d composant(s) lié(s) à des machines.', $machineLinked);
|
||||
}
|
||||
|
||||
// Check type machine requirements
|
||||
$requirementCount = (int) $this->connection->fetchOne(
|
||||
'SELECT COUNT(*) FROM type_machine_component_requirements WHERE typecomposantid = :id',
|
||||
['id' => $modelTypeId],
|
||||
);
|
||||
|
||||
if ($requirementCount > 0) {
|
||||
$blockers[] = sprintf('Utilisé dans %d modèle(s) de type de machine.', $requirementCount);
|
||||
}
|
||||
|
||||
// Check if any composant has pieces or sub-components in structure
|
||||
$withStructure = $this->connection->fetchAllAssociative(
|
||||
'SELECT name, structure FROM composants WHERE typecomposantid = :id AND structure IS NOT NULL',
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Entity\CustomField;
|
||||
use App\Entity\ModelType;
|
||||
use App\Entity\TypeMachine;
|
||||
use App\Entity\TypeMachineComponentRequirement;
|
||||
use App\Entity\TypeMachinePieceRequirement;
|
||||
use App\Entity\TypeMachineProductRequirement;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
use function array_key_exists;
|
||||
use function is_string;
|
||||
|
||||
final class TypeMachinePutProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly RequestStack $requestStack,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $uriVariables
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): TypeMachine
|
||||
{
|
||||
$typeMachine = $this->em->getRepository(TypeMachine::class)->find($uriVariables['id']);
|
||||
|
||||
if (!$typeMachine) {
|
||||
throw new NotFoundHttpException('Type de machine non trouvé.');
|
||||
}
|
||||
|
||||
// Guard: cannot edit if machines are linked
|
||||
if (!$typeMachine->getMachines()->isEmpty()) {
|
||||
throw new HttpException(422, 'Ce type de machine ne peut pas être modifié car des machines y sont rattachées.');
|
||||
}
|
||||
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
$payload = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
$this->updateScalarProperties($typeMachine, $payload);
|
||||
|
||||
if (array_key_exists('customFields', $payload)) {
|
||||
$this->replaceCustomFields($typeMachine, $payload['customFields'] ?? []);
|
||||
}
|
||||
|
||||
if (array_key_exists('componentRequirements', $payload)) {
|
||||
$this->replaceComponentRequirements($typeMachine, $payload['componentRequirements'] ?? []);
|
||||
}
|
||||
|
||||
if (array_key_exists('pieceRequirements', $payload)) {
|
||||
$this->replacePieceRequirements($typeMachine, $payload['pieceRequirements'] ?? []);
|
||||
}
|
||||
|
||||
if (array_key_exists('productRequirements', $payload)) {
|
||||
$this->replaceProductRequirements($typeMachine, $payload['productRequirements'] ?? []);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $typeMachine;
|
||||
}
|
||||
|
||||
private function updateScalarProperties(TypeMachine $typeMachine, array $payload): void
|
||||
{
|
||||
if (isset($payload['name'])) {
|
||||
$typeMachine->setName($payload['name']);
|
||||
}
|
||||
|
||||
if (array_key_exists('description', $payload)) {
|
||||
$typeMachine->setDescription($payload['description']);
|
||||
}
|
||||
|
||||
if (array_key_exists('category', $payload)) {
|
||||
$typeMachine->setCategory($payload['category']);
|
||||
}
|
||||
|
||||
if (array_key_exists('maintenanceFrequency', $payload)) {
|
||||
$typeMachine->setMaintenanceFrequency($payload['maintenanceFrequency']);
|
||||
}
|
||||
|
||||
if (array_key_exists('components', $payload)) {
|
||||
$typeMachine->setComponents($payload['components']);
|
||||
}
|
||||
|
||||
if (array_key_exists('criticalParts', $payload)) {
|
||||
$typeMachine->setCriticalParts($payload['criticalParts']);
|
||||
}
|
||||
|
||||
if (array_key_exists('machinePieces', $payload)) {
|
||||
$typeMachine->setMachinePieces($payload['machinePieces']);
|
||||
}
|
||||
|
||||
if (array_key_exists('specifications', $payload)) {
|
||||
$typeMachine->setSpecifications($payload['specifications']);
|
||||
}
|
||||
}
|
||||
|
||||
private function replaceCustomFields(TypeMachine $typeMachine, array $fieldsData): void
|
||||
{
|
||||
foreach ($typeMachine->getCustomFields()->toArray() as $old) {
|
||||
$typeMachine->removeCustomField($old);
|
||||
}
|
||||
|
||||
foreach ($fieldsData as $index => $data) {
|
||||
$field = new CustomField();
|
||||
$field->setName($data['name'] ?? '');
|
||||
$field->setType($data['type'] ?? 'text');
|
||||
$field->setRequired($data['required'] ?? false);
|
||||
$field->setOptions($data['options'] ?? null);
|
||||
$field->setOrderIndex($data['orderIndex'] ?? $index);
|
||||
$typeMachine->addCustomField($field);
|
||||
}
|
||||
}
|
||||
|
||||
private function replaceComponentRequirements(TypeMachine $typeMachine, array $requirementsData): void
|
||||
{
|
||||
foreach ($typeMachine->getComponentRequirements()->toArray() as $old) {
|
||||
$typeMachine->removeComponentRequirement($old);
|
||||
}
|
||||
|
||||
foreach ($requirementsData as $index => $data) {
|
||||
$req = new TypeMachineComponentRequirement();
|
||||
$req->setLabel($data['label'] ?? null);
|
||||
$req->setMinCount($data['minCount'] ?? 1);
|
||||
$req->setMaxCount($data['maxCount'] ?? null);
|
||||
$req->setRequired($data['required'] ?? true);
|
||||
$req->setAllowNewModels($data['allowNewModels'] ?? true);
|
||||
$req->setOrderIndex($data['orderIndex'] ?? $index);
|
||||
|
||||
$modelType = $this->resolveModelType($data['typeComposant'] ?? null);
|
||||
if ($modelType) {
|
||||
$req->setTypeComposant($modelType);
|
||||
}
|
||||
|
||||
$typeMachine->addComponentRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
private function replacePieceRequirements(TypeMachine $typeMachine, array $requirementsData): void
|
||||
{
|
||||
foreach ($typeMachine->getPieceRequirements()->toArray() as $old) {
|
||||
$typeMachine->removePieceRequirement($old);
|
||||
}
|
||||
|
||||
foreach ($requirementsData as $index => $data) {
|
||||
$req = new TypeMachinePieceRequirement();
|
||||
$req->setLabel($data['label'] ?? null);
|
||||
$req->setMinCount($data['minCount'] ?? 0);
|
||||
$req->setMaxCount($data['maxCount'] ?? null);
|
||||
$req->setRequired($data['required'] ?? false);
|
||||
$req->setAllowNewModels($data['allowNewModels'] ?? true);
|
||||
$req->setOrderIndex($data['orderIndex'] ?? $index);
|
||||
|
||||
$modelType = $this->resolveModelType($data['typePiece'] ?? null);
|
||||
if ($modelType) {
|
||||
$req->setTypePiece($modelType);
|
||||
}
|
||||
|
||||
$typeMachine->addPieceRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
private function replaceProductRequirements(TypeMachine $typeMachine, array $requirementsData): void
|
||||
{
|
||||
foreach ($typeMachine->getProductRequirements()->toArray() as $old) {
|
||||
$typeMachine->removeProductRequirement($old);
|
||||
}
|
||||
|
||||
foreach ($requirementsData as $index => $data) {
|
||||
$req = new TypeMachineProductRequirement();
|
||||
$req->setLabel($data['label'] ?? null);
|
||||
$req->setMinCount($data['minCount'] ?? 0);
|
||||
$req->setMaxCount($data['maxCount'] ?? null);
|
||||
$req->setRequired($data['required'] ?? false);
|
||||
$req->setAllowNewModels($data['allowNewModels'] ?? true);
|
||||
$req->setOrderIndex($data['orderIndex'] ?? $index);
|
||||
|
||||
$modelType = $this->resolveModelType($data['typeProduct'] ?? null);
|
||||
if ($modelType) {
|
||||
$req->setTypeProduct($modelType);
|
||||
}
|
||||
|
||||
$typeMachine->addProductRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveModelType(mixed $value): ?ModelType
|
||||
{
|
||||
if (!$value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$id = $value;
|
||||
|
||||
if (is_string($value) && preg_match('#/api/model_types/(.+)$#', $value, $matches)) {
|
||||
$id = $matches[1];
|
||||
}
|
||||
|
||||
return $this->em->getReference(ModelType::class, $id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user