Compare commits
90 Commits
14960d5e87
...
v1.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d89c97f0a0 | ||
|
|
7a5dd0b555 | ||
|
|
44d69db560 | ||
|
|
453065c9f0 | ||
|
|
eb85323116 | ||
|
|
2dfa501a65 | ||
|
|
c22f9dbf2b | ||
|
|
27a1b09d62 | ||
|
|
7bbb693924 | ||
|
|
9661fd5d91 | ||
|
|
d9ab583879 | ||
|
|
5d41bda997 | ||
|
|
3d037083c6 | ||
|
|
a3e440c254 | ||
|
|
adc44b99d3 | ||
|
|
60afeb4cfd | ||
|
|
02ff8b1a96 | ||
|
|
2156df22c6 | ||
|
|
cd2a3fac55 | ||
|
|
6300a3588a | ||
|
|
45213103e4 | ||
|
|
91b8b424d6 | ||
|
|
0d1c9277e5 | ||
|
|
db16d26103 | ||
|
|
0eb64d0975 | ||
|
|
39e503ae18 | ||
|
|
70ed354c42 | ||
|
|
ba98ae37f4 | ||
|
|
906d39793f | ||
|
|
f970c1928d | ||
|
|
2a1d966b87 | ||
|
|
a393b62e9f | ||
|
|
1247f72af6 | ||
|
|
6735bf252c | ||
|
|
508066d39f | ||
|
|
70956c204e | ||
|
|
16a7eac0c6 | ||
|
|
37ac08b182 | ||
|
|
5ef80b362e | ||
|
|
78f19daf76 | ||
|
|
6caa4a61df | ||
|
|
bf55034b2e | ||
|
|
ba1114e78b | ||
|
|
5ccc3b30f0 | ||
| 8d83076be0 | |||
|
|
997a3ae822 | ||
|
|
034c193e4b | ||
|
|
4acc8d1c01 | ||
|
|
49ff15f18d | ||
|
|
7a02617d48 | ||
|
|
e52eef0491 | ||
|
|
a5118305d3 | ||
|
|
b51671b1d4 | ||
|
|
1643dcf8c2 | ||
|
|
17ab4cdd16 | ||
|
|
d9182131d9 | ||
|
|
26a7fe64be | ||
|
|
4669dec359 | ||
|
|
3f05fe753e | ||
|
|
a502acd234 | ||
|
|
69b199b6dc | ||
|
|
d5f6749f9e | ||
| ad7918c993 | |||
| 86447000b1 | |||
| 7da5eb917a | |||
| d65eb9ff0f | |||
| 895df7415b | |||
| 9abe9fea7f | |||
| ea45ce9d0a | |||
| 2c3fbb093a | |||
| 3c5fb83673 | |||
| e1dc8850c0 | |||
| 59622580a9 | |||
| bdd1837247 | |||
| 40b4b90ed8 | |||
| d4bdb76fda | |||
| f7fc1bdee2 | |||
| 1a751927fa | |||
| 987aa5c15f | |||
| d2a1cd0cc4 | |||
| 5222a6bbf9 | |||
| 15e0b23f15 | |||
| fab1d25871 | |||
| 037ed782a7 | |||
| de8b05a553 | |||
| 6f9e1ec626 | |||
| 8430e9baef | |||
| fca3104a39 | |||
| 50336694f6 | |||
| c99f76d755 |
6
.env
6
.env
@@ -39,3 +39,9 @@ DEFAULT_URI=http://localhost
|
|||||||
###> nelmio/cors-bundle ###
|
###> nelmio/cors-bundle ###
|
||||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||||
###< nelmio/cors-bundle ###
|
###< nelmio/cors-bundle ###
|
||||||
|
|
||||||
|
###> lexik/jwt-authentication-bundle ###
|
||||||
|
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||||
|
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||||
|
JWT_PASSPHRASE=281e2cd303ed9ba4a4a4074e19eac9cea505cc9d82ce79a448bb8eb00c636ebe
|
||||||
|
###< lexik/jwt-authentication-bundle ###
|
||||||
|
|||||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -23,3 +23,18 @@
|
|||||||
###> docker ###
|
###> docker ###
|
||||||
docker/.env.docker.local
|
docker/.env.docker.local
|
||||||
###< docker ###
|
###< docker ###
|
||||||
|
|
||||||
|
###> lexik/jwt-authentication-bundle ###
|
||||||
|
/config/jwt/*.pem
|
||||||
|
###< lexik/jwt-authentication-bundle ###
|
||||||
|
|
||||||
|
###> migration archives ###
|
||||||
|
/_archives/
|
||||||
|
###< migration archives ###
|
||||||
|
|
||||||
|
###> frontend ###
|
||||||
|
/frontend/node_modules/
|
||||||
|
/frontend/.nuxt/
|
||||||
|
/frontend/.output/
|
||||||
|
/frontend/dist/
|
||||||
|
###< frontend ###
|
||||||
|
|||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "Inventory_frontend"]
|
||||||
|
path = Inventory_frontend
|
||||||
|
url = gitea@gitea.malio.fr:MALIO-DEV/Inventory_frontend.git
|
||||||
0
.idea/ferme.iml → .idea/Inventory.iml
generated
0
.idea/ferme.iml → .idea/Inventory.iml
generated
425
CARNET_DE_BORD.md
Normal file
425
CARNET_DE_BORD.md
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
# 📔 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
|
||||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,15 +1,51 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
Liste des évolutions du projet Ferme
|
## [1.8.0] - 2026-03-03
|
||||||
|
|
||||||
## [0.0.0]
|
### Ajouts
|
||||||
### Parameters
|
- **Stockage documents sur disque** : les documents sont desormais stockes en fichiers sur le systeme de fichiers au lieu de Base64 en base de donnees. Les endpoints `/api/documents/{id}/file` et `/api/documents/{id}/download` servent les fichiers directement.
|
||||||
Ajouter dans le fichier .env
|
- **Commande de migration** `app:migrate-documents-to-filesystem` : migre les documents existants (Base64 → fichiers) avec dry-run, batch-size et limit.
|
||||||
- DEFAULT_URI
|
- **Pagination serveur sur la page Documents** : recherche, tri (date/nom/taille), filtre par rattachement (site/machine/composant/piece/produit), selecteur par page (20/50/100).
|
||||||
- DATABASE_URL
|
- **Compression PDF automatique** : les documents PDF uploades sont compresses automatiquement via Ghostscript. Commande `app:compress-pdf` pour compresser les PDFs existants.
|
||||||
|
- **Nettoyage automatique des fichiers** : suppression du fichier sur disque lors de la suppression d'un document.
|
||||||
|
- **Champ description** sur les entites Piece et Composant, visible dans les catalogues avec popover au survol.
|
||||||
|
|
||||||
### Added
|
### Corrections
|
||||||
|
- Fix normalisation des documents : `fileUrl` et `downloadUrl` toujours exposes dans l'API (meme sans `path` dans le groupe de serialisation).
|
||||||
|
- Fix recursion infinie dans `DocumentNormalizer` (`getSupportedTypes` retourne `false` pour desactiver le cache).
|
||||||
|
- Fix edition de squelettes machines : `deserialize: false` + `validate: false` sur le PUT pour eviter le conflit UniqueEntity et l'interference du deserialiseur avec les collections writableLink.
|
||||||
|
- Fix sites : ajout operation PATCH et correction migration contrainte.
|
||||||
|
- Retrocompatibilite : le controleur de service gere transparentement les anciens documents Base64 et les nouveaux fichiers.
|
||||||
|
|
||||||
### Changed
|
### Migration requise
|
||||||
|
```bash
|
||||||
|
docker compose exec php php bin/console doctrine:migrations:migrate
|
||||||
|
docker compose exec php php bin/console app:migrate-documents-to-filesystem
|
||||||
|
```
|
||||||
|
|
||||||
### Fixed
|
## [1.7.0] - 2026-03-02
|
||||||
|
|
||||||
|
### Ajouts
|
||||||
|
- **Systeme de commentaires / tickets** : les utilisateurs peuvent laisser des commentaires sur les fiches (machines, pieces, composants, produits, categories, squelettes). Les gestionnaires peuvent les resoudre.
|
||||||
|
- **Page commentaires** (`/comments`) : vue centralisee avec filtres (statut, type d'entite), pagination et liens cliquables vers les fiches.
|
||||||
|
- **Badge notifications** : compteur de commentaires ouverts sur l'avatar utilisateur et dans le menu profil (polling 60s).
|
||||||
|
- **Controle d'acces par roles** : ROLE_ADMIN, ROLE_GESTIONNAIRE, ROLE_VIEWER avec permissions granulaires sur toutes les pages.
|
||||||
|
- **Badge de role** dans le dropdown du profil utilisateur.
|
||||||
|
- **Journal d'audit etendu** : audit logging sur machines, constructeurs, types de modeles, documents et conversions.
|
||||||
|
- **Commande `app:init-profile-passwords`** : initialisation en masse des mots de passe et roles.
|
||||||
|
|
||||||
|
### Corrections
|
||||||
|
- Toggle switch pour les champs personnalises booleens (remplace les checkboxes).
|
||||||
|
- Recherche constructeur : filtrage cote client au lieu d'appels API debounce.
|
||||||
|
- Prevention des doublons de noms de constructeurs et de references de pieces (contraintes unique).
|
||||||
|
- Fix creation de squelettes machines : pagination, duplication, champs personnalises.
|
||||||
|
|
||||||
|
### Migration requise
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
- Version initiale avec gestion du parc machines, pieces, composants, produits et categories.
|
||||||
|
|||||||
168
CLAUDE.md
Normal file
168
CLAUDE.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# CLAUDE.md — Inventory Project
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Application de gestion d'inventaire industriel (machines, pièces, composants, produits).
|
||||||
|
Mono-repo avec backend Symfony et frontend Nuxt en submodule git.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
| Layer | Tech | Version |
|
||||||
|
|-------|------|---------|
|
||||||
|
| Backend | Symfony + API Platform | 8.0 / ^4.2 |
|
||||||
|
| PHP | PHP | >=8.4 |
|
||||||
|
| Database | PostgreSQL | 16 |
|
||||||
|
| Frontend | Nuxt (SPA, SSR off) | 4 |
|
||||||
|
| UI | Vue 3 Composition API + TypeScript | 3.5 / 5.7 |
|
||||||
|
| CSS | TailwindCSS 4 + DaisyUI 5 | |
|
||||||
|
| Auth | Session-based (cookies, pas JWT) | |
|
||||||
|
| Containers | Docker Compose | |
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Inventory/ # Backend Symfony (repo principal)
|
||||||
|
├── src/Entity/ # Entités Doctrine (annotations PHP 8 attributes)
|
||||||
|
├── src/Controller/ # Controllers custom (session, comments, audit…)
|
||||||
|
├── src/EventSubscriber/ # Audit subscribers (onFlush)
|
||||||
|
├── config/ # Config Symfony
|
||||||
|
├── migrations/ # Migrations Doctrine (raw SQL PostgreSQL)
|
||||||
|
├── docker/ # Dockerfile + .env.docker
|
||||||
|
├── scripts/ # release.sh, normalize-dump.py
|
||||||
|
├── fixtures/ # SQL fixtures
|
||||||
|
├── tests/ # PHPUnit
|
||||||
|
├── pre-commit, commit-msg # Git hooks
|
||||||
|
├── makefile # Commandes Docker/dev
|
||||||
|
├── VERSION # Source unique de version (semver)
|
||||||
|
├── Inventory_frontend/ # ← SUBMODULE GIT (repo séparé)
|
||||||
|
│ ├── app/pages/ # Pages Nuxt (file-based routing)
|
||||||
|
│ ├── app/components/ # Composants Vue (auto-imported)
|
||||||
|
│ ├── app/composables/ # Composables Vue
|
||||||
|
│ ├── app/shared/ # Types, utils, validation
|
||||||
|
│ ├── app/middleware/ # Auth middleware global
|
||||||
|
│ └── app/services/ # Service layer (wrappers useApi)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker
|
||||||
|
make start # Démarrer les containers
|
||||||
|
make stop # Arrêter
|
||||||
|
make shell # Shell dans le container PHP
|
||||||
|
make install # Install complet (composer + npm + build)
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
make test # PHPUnit
|
||||||
|
docker compose exec php vendor/bin/php-cs-fixer fix # Linter PHP
|
||||||
|
docker compose exec php php bin/console doctrine:migrations:migrate
|
||||||
|
|
||||||
|
# Frontend (dans Inventory_frontend/)
|
||||||
|
npm run dev # Dev server (port 3001)
|
||||||
|
npm run build # Build production
|
||||||
|
npm run lint:fix # ESLint fix
|
||||||
|
npx nuxi typecheck # TypeScript check (0 errors attendu)
|
||||||
|
|
||||||
|
# Release
|
||||||
|
./scripts/release.sh patch # Bump patch version (ou minor/major)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Git Conventions
|
||||||
|
|
||||||
|
### Branches
|
||||||
|
- `master` — production
|
||||||
|
- `develop` — branche principale de dev (cible des PR)
|
||||||
|
- `feat/xxx`, `fix/xxx`, `refactor/xxx` — branches de travail
|
||||||
|
|
||||||
|
### Commit Message Format (enforced by hook)
|
||||||
|
```
|
||||||
|
<type>(<scope optionnel>) : <message>
|
||||||
|
```
|
||||||
|
**Espace obligatoire autour du `:`**. Types autorisés (minuscules) :
|
||||||
|
`build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, `test`, `wip`
|
||||||
|
|
||||||
|
Exemples :
|
||||||
|
- `feat(auth) : add login page`
|
||||||
|
- `fix(machines) : prevent null crash on skeleton creation`
|
||||||
|
|
||||||
|
### Pre-commit Hook
|
||||||
|
1. php-cs-fixer sur les fichiers PHP stagés
|
||||||
|
2. PHPUnit — bloque le commit si tests échouent
|
||||||
|
|
||||||
|
### Submodule Workflow
|
||||||
|
Le frontend est un submodule git. Lors d'un commit frontend :
|
||||||
|
1. Commit dans `Inventory_frontend/` d'abord
|
||||||
|
2. Commit dans le repo principal pour mettre à jour le pointeur submodule
|
||||||
|
3. Push les deux repos
|
||||||
|
|
||||||
|
## Architecture Backend
|
||||||
|
|
||||||
|
### Entités Principales
|
||||||
|
`Machine`, `Piece`, `Composant`, `Product`, `Constructeur`, `Site`, `TypeMachine`, `ModelType`, `CustomField`, `CustomFieldValue`, `Document`, `AuditLog`, `Comment`, `Profile`
|
||||||
|
|
||||||
|
### Patterns
|
||||||
|
- **IDs** : CUID-like strings (`'cl' + bin2hex(random_bytes(12))`), pas d'auto-increment
|
||||||
|
- **ORM** : Attributs PHP 8 (`#[ORM\Column(...)]`, `#[Groups([...])]`)
|
||||||
|
- **Lifecycle** : `#[ORM\HasLifecycleCallbacks]` avec `PrePersist`/`PreUpdate` pour `createdAt`/`updatedAt`
|
||||||
|
- **Sécurité** : `security: "is_granted('ROLE_...')"` sur chaque opération API Platform
|
||||||
|
- **Audit** : Subscribers Doctrine `onFlush` capturent diff + snapshot complet
|
||||||
|
- **Migrations** : Raw SQL PostgreSQL avec `IF NOT EXISTS`/`IF EXISTS` pour idempotence
|
||||||
|
|
||||||
|
### Rôles (hiérarchie)
|
||||||
|
```
|
||||||
|
ROLE_ADMIN → ROLE_GESTIONNAIRE → ROLE_VIEWER → ROLE_USER
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL — ATTENTION
|
||||||
|
- Les noms de colonnes sont **TOUJOURS EN MINUSCULES** dans PG
|
||||||
|
- Doctrine utilise camelCase (`typePieceId`) mais PG stocke `typepieceid`
|
||||||
|
- Le SQL brut doit utiliser les noms lowercase
|
||||||
|
- Tables de jointure many-to-many : colonnes `a` et `b` (ex: `_piececonstructeurs`)
|
||||||
|
|
||||||
|
## Architecture Frontend
|
||||||
|
|
||||||
|
### Patterns
|
||||||
|
- **Composables** : `interface Deps { ... }` + `export function useXxx(deps: Deps)`
|
||||||
|
- **Communication composants** : Props + Events uniquement (pas de provide/inject)
|
||||||
|
- **API** : `useApi.ts` wraps fetch avec `credentials: 'include'` pour les cookies session
|
||||||
|
- **Content-Type** : `application/ld+json` pour POST/PUT, `application/merge-patch+json` pour PATCH
|
||||||
|
- **Auth** : `useProfileSession` + middleware global `profile.global.ts`
|
||||||
|
- **Permissions** : `usePermissions.ts` miroir de la hiérarchie backend côté client
|
||||||
|
- **Auto-imports** : Nuxt auto-importe composants (`components/`) et composables (`composables/`)
|
||||||
|
|
||||||
|
### DaisyUI Classes
|
||||||
|
- Input : `input input-bordered input-sm md:input-md`
|
||||||
|
- Textarea : `textarea textarea-bordered textarea-sm md:textarea-md`
|
||||||
|
- Select : `select select-bordered select-sm md:select-md`
|
||||||
|
- Button : `btn btn-sm md:btn-md btn-primary`
|
||||||
|
|
||||||
|
## Règles Importantes
|
||||||
|
|
||||||
|
### Toujours faire AVANT de modifier du code
|
||||||
|
1. **Lire le fichier** avant de l'éditer — ne jamais proposer de changements sur du code non lu
|
||||||
|
2. **Comprendre le pattern existant** — reproduire le style du fichier (noms, indentation, structure)
|
||||||
|
3. **Vérifier les deux repos** — un changement peut impacter backend ET frontend
|
||||||
|
|
||||||
|
### Après chaque modification
|
||||||
|
1. Backend PHP : `docker compose exec php vendor/bin/php-cs-fixer fix`
|
||||||
|
2. Frontend : `npm run lint:fix` puis `npx nuxi typecheck` si fichiers TS modifiés
|
||||||
|
|
||||||
|
### Ne jamais faire
|
||||||
|
- Ajouter des features non demandées, du code mort, ou des abstractions prématurées
|
||||||
|
- Utiliser `provide/inject` — le codebase utilise Props + Events
|
||||||
|
- Utiliser JWT/tokens — l'auth est session-based
|
||||||
|
- Écrire du SQL avec des noms camelCase — PostgreSQL = lowercase
|
||||||
|
- Committer sans que l'utilisateur le demande explicitement
|
||||||
|
- Force push sans confirmation explicite
|
||||||
|
- Modifier la config git
|
||||||
|
|
||||||
|
### Submodule — Synchronisation
|
||||||
|
Quand les branches `master` et `develop` divergent sur l'un des deux repos, **toujours les synchroniser** :
|
||||||
|
- Main repo : `git checkout master && git merge develop && git push`
|
||||||
|
- Frontend : `git checkout develop && git merge master && git push` (ou l'inverse selon le cas)
|
||||||
|
|
||||||
|
## URLs Locales
|
||||||
|
- API Symfony : `http://localhost:8081/api`
|
||||||
|
- Nuxt dev : `http://localhost:3001`
|
||||||
|
- Adminer (PG) : `http://localhost:5050`
|
||||||
|
- PG direct : `localhost:5433` (user: root, pass: root, db: inventory)
|
||||||
328
DEPLOY.md
Normal file
328
DEPLOY.md
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
# Inventory - Guide de Déploiement & Release
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
inventory.malio-dev.fr/ → Frontend Nuxt (statique)
|
||||||
|
inventory.malio-dev.fr/api → Backend Symfony (PHP-FPM)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Composant | Technologie | Emplacement serveur |
|
||||||
|
|-----------|-------------|---------------------|
|
||||||
|
| Backend | Symfony 8 + API Platform | `/var/www/Inventory/` |
|
||||||
|
| Frontend | Nuxt 4 (statique) | `/var/www/Inventory/Inventory_frontend/.output/public/` |
|
||||||
|
| Base de données | PostgreSQL 16 | `inventory` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prérequis serveur
|
||||||
|
|
||||||
|
- **OS** : Ubuntu/Debian
|
||||||
|
- **PHP** : 8.4 avec extensions : pgsql, intl, zip, gd, mbstring, curl
|
||||||
|
- **Node.js** : 20+
|
||||||
|
- **Nginx**
|
||||||
|
- **PostgreSQL** : 16
|
||||||
|
- **Composer**
|
||||||
|
|
||||||
|
Vérifier :
|
||||||
|
```bash
|
||||||
|
php -v # PHP 8.4+
|
||||||
|
php -m | grep -E 'pgsql|intl|zip|gd|mbstring'
|
||||||
|
node -v # Node 20+
|
||||||
|
nginx -v
|
||||||
|
psql --version
|
||||||
|
composer --version
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Déploiement initial
|
||||||
|
|
||||||
|
### 1. Cloner le projet
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www
|
||||||
|
sudo git clone --recurse-submodules gitea@gitea.malio.fr:MALIO-DEV/Inventory.git Inventory
|
||||||
|
sudo chown -R malio:malio Inventory
|
||||||
|
cd Inventory
|
||||||
|
git checkout master
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Créer la base de données
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -u postgres psql
|
||||||
|
|
||||||
|
CREATE DATABASE inventory OWNER ferme_user;
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE inventory TO ferme_user;
|
||||||
|
\q
|
||||||
|
```
|
||||||
|
|
||||||
|
Importer le dump :
|
||||||
|
```bash
|
||||||
|
# Copier le dump depuis le PC local
|
||||||
|
scp backup_v1.0.0_clean.sql malio@192.168.0.159:/tmp/
|
||||||
|
|
||||||
|
# Importer
|
||||||
|
psql -U ferme_user -h 127.0.0.1 -d inventory -f /tmp/backup_v1.0.0_clean.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configurer le backend Symfony
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/Inventory
|
||||||
|
|
||||||
|
# Installer les dépendances
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
|
||||||
|
# Créer .env.local
|
||||||
|
cat > .env.local << 'EOF'
|
||||||
|
APP_ENV=prod
|
||||||
|
APP_DEBUG=0
|
||||||
|
APP_SECRET=CHANGE_ME
|
||||||
|
|
||||||
|
DATABASE_URL="postgresql://ferme_user:fermerecette@127.0.0.1:5432/inventory?serverVersion=16"
|
||||||
|
|
||||||
|
CORS_ALLOW_ORIGIN='^https?://inventory\.malio-dev\.fr$'
|
||||||
|
|
||||||
|
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||||
|
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||||
|
JWT_PASSPHRASE=inventoryjwt
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Générer APP_SECRET
|
||||||
|
sed -i "s/CHANGE_ME/$(openssl rand -hex 32)/" .env.local
|
||||||
|
|
||||||
|
# Générer les clés JWT
|
||||||
|
mkdir -p config/jwt
|
||||||
|
openssl genrsa -out config/jwt/private.pem -aes256 4096
|
||||||
|
# Passphrase : inventoryjwt
|
||||||
|
openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem
|
||||||
|
chmod 600 config/jwt/private.pem
|
||||||
|
|
||||||
|
# Permissions
|
||||||
|
sudo chown -R www-data:www-data var/
|
||||||
|
sudo chmod -R 775 var/
|
||||||
|
|
||||||
|
# Vider le cache
|
||||||
|
php bin/console cache:clear --env=prod
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Configurer le frontend Nuxt
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/Inventory/Inventory_frontend
|
||||||
|
|
||||||
|
# Permissions
|
||||||
|
sudo chown -R malio:malio .
|
||||||
|
|
||||||
|
# Installer les dépendances
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Créer .env
|
||||||
|
cat > .env << 'EOF'
|
||||||
|
NUXT_PUBLIC_API_BASE_URL=http://inventory.malio-dev.fr/api
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Générer le site statique
|
||||||
|
npx nuxi generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Configurer Nginx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/nginx/sites-available/inventory
|
||||||
|
```
|
||||||
|
|
||||||
|
Contenu :
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name inventory.malio-dev.fr;
|
||||||
|
|
||||||
|
# Gros fichiers (100MB max)
|
||||||
|
client_max_body_size 100M;
|
||||||
|
client_body_timeout 300s;
|
||||||
|
send_timeout 300s;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/inventory-access.log;
|
||||||
|
error_log /var/log/nginx/inventory-error.log;
|
||||||
|
|
||||||
|
# Backend Symfony - /api
|
||||||
|
location /api {
|
||||||
|
root /var/www/Inventory/public;
|
||||||
|
try_files $uri /index.php$is_args$args;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/index\.php(/|$) {
|
||||||
|
fastcgi_pass unix:/run/php/php-fpm.sock;
|
||||||
|
fastcgi_split_path_info ^(.+\.php)(/.*)$;
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/Inventory/public/index.php;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /var/www/Inventory/public;
|
||||||
|
fastcgi_read_timeout 300s;
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Frontend statique
|
||||||
|
location / {
|
||||||
|
root /var/www/Inventory/Inventory_frontend/.output/public;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Activer :
|
||||||
|
```bash
|
||||||
|
sudo ln -s /etc/nginx/sites-available/inventory /etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Vérifier
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://inventory.malio-dev.fr
|
||||||
|
curl http://inventory.malio-dev.fr/api
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mises à jour
|
||||||
|
|
||||||
|
### Mettre à jour l'application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/Inventory
|
||||||
|
|
||||||
|
# Pull les changements
|
||||||
|
git pull
|
||||||
|
git submodule update --init --recursive
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
php bin/console cache:clear --env=prod
|
||||||
|
sudo chown -R www-data:www-data var/
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
cd Inventory_frontend
|
||||||
|
npm install
|
||||||
|
npx nuxi generate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Versioning & Releases
|
||||||
|
|
||||||
|
### Source de vérité
|
||||||
|
|
||||||
|
Le fichier `VERSION` à la racine contient le numéro de version (ex: `1.0.0`).
|
||||||
|
|
||||||
|
Cette version est synchronisée avec :
|
||||||
|
- Le footer de l'application
|
||||||
|
- `config/packages/api_platform.yaml`
|
||||||
|
|
||||||
|
### Créer une release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis le PC de dev
|
||||||
|
./scripts/release.sh patch # 1.0.0 → 1.0.1
|
||||||
|
./scripts/release.sh minor # 1.0.0 → 1.1.0
|
||||||
|
./scripts/release.sh major # 1.0.0 → 2.0.0
|
||||||
|
./scripts/release.sh 2.0.0 # Version exacte
|
||||||
|
```
|
||||||
|
|
||||||
|
Le script :
|
||||||
|
1. Vérifie/commit le submodule frontend
|
||||||
|
2. Met à jour `VERSION` et `api_platform.yaml`
|
||||||
|
3. Commit et tag les deux repos
|
||||||
|
4. Affiche les commandes pour push
|
||||||
|
|
||||||
|
### Pousser la release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Frontend (submodule)
|
||||||
|
cd Inventory_frontend && git push && git push --tags && cd ..
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
git push && git push --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
### Créer la release sur Gitea
|
||||||
|
|
||||||
|
1. Aller sur le dépôt Gitea
|
||||||
|
2. **Releases** > **New Release**
|
||||||
|
3. Sélectionner le tag `vX.Y.Z`
|
||||||
|
4. Ajouter les notes de release
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commandes utiles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Logs Nginx
|
||||||
|
tail -f /var/log/nginx/inventory-error.log
|
||||||
|
|
||||||
|
# Logs Symfony
|
||||||
|
tail -f /var/www/Inventory/var/log/prod.log
|
||||||
|
|
||||||
|
# Vider le cache Symfony
|
||||||
|
php /var/www/Inventory/bin/console cache:clear --env=prod
|
||||||
|
|
||||||
|
# Rebuild frontend
|
||||||
|
cd /var/www/Inventory/Inventory_frontend && npx nuxi generate
|
||||||
|
|
||||||
|
# Status PHP-FPM
|
||||||
|
systemctl status php8.4-fpm
|
||||||
|
|
||||||
|
# Reload Nginx
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backup base de données
|
||||||
|
|
||||||
|
### Export
|
||||||
|
```bash
|
||||||
|
pg_dump -U ferme_user -h 127.0.0.1 -d inventory --no-owner --no-acl --inserts --column-inserts --clean --if-exists > backup_inventory_$(date +%Y%m%d).sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import
|
||||||
|
```bash
|
||||||
|
psql -U ferme_user -h 127.0.0.1 -d inventory -f backup_inventory_YYYYMMDD.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Erreur 502 Bad Gateway
|
||||||
|
```bash
|
||||||
|
# Vérifier PHP-FPM
|
||||||
|
systemctl status php8.4-fpm
|
||||||
|
sudo systemctl restart php8.4-fpm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erreur 403 Forbidden
|
||||||
|
```bash
|
||||||
|
# Vérifier les permissions
|
||||||
|
sudo chown -R www-data:www-data /var/www/Inventory/var/
|
||||||
|
sudo chmod -R 775 /var/www/Inventory/var/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erreur API "No route found"
|
||||||
|
```bash
|
||||||
|
# Vider le cache
|
||||||
|
php /var/www/Inventory/bin/console cache:clear --env=prod
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend ne se met pas à jour
|
||||||
|
```bash
|
||||||
|
# Rebuild
|
||||||
|
cd /var/www/Inventory/Inventory_frontend
|
||||||
|
rm -rf .output
|
||||||
|
npx nuxi generate
|
||||||
|
```
|
||||||
1
Inventory_frontend
Submodule
1
Inventory_frontend
Submodule
Submodule Inventory_frontend added at e88ed5b8f2
1419
MIGRATION_PLAN.md
Normal file
1419
MIGRATION_PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
35
README.md
35
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Projet Ferme
|
# Projet Inventory
|
||||||
|
|
||||||
## Installation du projet
|
## Installation du projet
|
||||||
### Windows
|
### Windows
|
||||||
@@ -12,6 +12,7 @@ Il suffit de suivre cette [doc](https://wiki.malio.fr/bookstack/books/environnem
|
|||||||
### Installation du projet
|
### Installation du projet
|
||||||
Une fois les prérequis installés, il suffit de cloner le projet et de lancer les commandes suivantes
|
Une fois les prérequis installés, il suffit de cloner le projet et de lancer les commandes suivantes
|
||||||
```bash
|
```bash
|
||||||
|
sudo apt install make -y
|
||||||
make start
|
make start
|
||||||
make install
|
make install
|
||||||
```
|
```
|
||||||
@@ -20,7 +21,7 @@ Dans le cas ou le `make start` plante à cause du port de la bdd, il faut modifi
|
|||||||
### Configuration xdebug
|
### Configuration xdebug
|
||||||
Pour configurer xdebug, il faut ajouter un serveur sur phpstorm. <br>
|
Pour configurer xdebug, il faut ajouter un serveur sur phpstorm. <br>
|
||||||
Pour cela, il faut aller dans **Settings > PHP > Servers** <br>
|
Pour cela, il faut aller dans **Settings > PHP > Servers** <br>
|
||||||
* Name : ferme-docker
|
* Name : inventory-docker
|
||||||
* Host : localhost
|
* Host : localhost
|
||||||
* Port : 8080
|
* Port : 8080
|
||||||
* Path : File/Directory -> l'endroit où est stocké votre projet et le path -> /var/www/html
|
* Path : File/Directory -> l'endroit où est stocké votre projet et le path -> /var/www/html
|
||||||
@@ -36,12 +37,42 @@ 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.
|
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.
|
C'est un bdd local dans le docker.
|
||||||
### Frontend
|
### 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
|
Pour le frontend, il suffit de taper la commande suivante qui va lancer le serveur de dev
|
||||||
```bash
|
```bash
|
||||||
make dev-nuxt
|
make dev-nuxt
|
||||||
```
|
```
|
||||||
Le front sera accessible sur http://localhost:3000
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
# Compresser tous les PDFs
|
||||||
|
php bin/console app:compress-pdf
|
||||||
|
```
|
||||||
|
|
||||||
## Commandes utiles
|
## Commandes utiles
|
||||||
Pour restart le container
|
Pour restart le container
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
786
REFACTORING_PLAN.md
Normal file
786
REFACTORING_PLAN.md
Normal file
@@ -0,0 +1,786 @@
|
|||||||
|
# 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
|
||||||
138
RELEASE.md
Normal file
138
RELEASE.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# Guide de Release
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Le projet utilise le [Semantic Versioning](https://semver.org/) (SemVer) : `MAJOR.MINOR.PATCH`
|
||||||
|
|
||||||
|
- **MAJOR** : Changements incompatibles avec les versions précédentes
|
||||||
|
- **MINOR** : Nouvelles fonctionnalités rétrocompatibles
|
||||||
|
- **PATCH** : Corrections de bugs rétrocompatibles
|
||||||
|
|
||||||
|
La version est centralisée dans le fichier `VERSION` à la racine du projet.
|
||||||
|
|
||||||
|
## Créer une release
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
|
|
||||||
|
- Tous les changements doivent être commités
|
||||||
|
- Les tests doivent passer
|
||||||
|
- Être sur la branche à releaser (ex: `main`, `develop`)
|
||||||
|
|
||||||
|
### Utilisation du script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Afficher l'aide et la version actuelle
|
||||||
|
./scripts/release.sh
|
||||||
|
|
||||||
|
# Bump patch : 1.0.0 → 1.0.1
|
||||||
|
./scripts/release.sh patch
|
||||||
|
|
||||||
|
# Bump minor : 1.0.0 → 1.1.0
|
||||||
|
./scripts/release.sh minor
|
||||||
|
|
||||||
|
# Bump major : 1.0.0 → 2.0.0
|
||||||
|
./scripts/release.sh major
|
||||||
|
|
||||||
|
# Version spécifique
|
||||||
|
./scripts/release.sh 2.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Le script :
|
||||||
|
1. Met à jour le fichier `VERSION`
|
||||||
|
2. Met à jour `config/packages/api_platform.yaml`
|
||||||
|
3. Crée un commit `chore(release): vX.Y.Z`
|
||||||
|
4. Crée le tag `vX.Y.Z`
|
||||||
|
|
||||||
|
### Pousser la release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push && git push --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
### Créer la release sur Gitea
|
||||||
|
|
||||||
|
1. Aller sur le dépôt Gitea
|
||||||
|
2. **Releases** > **New Release**
|
||||||
|
3. Sélectionner le tag `vX.Y.Z`
|
||||||
|
4. Titre : `v1.0.0` (ou avec un nom descriptif)
|
||||||
|
5. Description : résumé des changements (voir section Notes de release)
|
||||||
|
|
||||||
|
## Notes de release
|
||||||
|
|
||||||
|
Template pour les notes de release :
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Nouveautés
|
||||||
|
- Feature A
|
||||||
|
- Feature B
|
||||||
|
|
||||||
|
## Corrections
|
||||||
|
- Fix du bug X
|
||||||
|
- Fix du bug Y
|
||||||
|
|
||||||
|
## Changements
|
||||||
|
- Refactoring de Z
|
||||||
|
- Mise à jour des dépendances
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers impactés par le versioning
|
||||||
|
|
||||||
|
| Fichier | Usage |
|
||||||
|
|---------|-------|
|
||||||
|
| `VERSION` | Source unique de vérité |
|
||||||
|
| `config/packages/api_platform.yaml` | Version affichée dans l'API |
|
||||||
|
| `Inventory_frontend/nuxt.config.ts` | Lit VERSION au build |
|
||||||
|
| Footer de l'app | Affiche `v{{ appVersion }}` |
|
||||||
|
|
||||||
|
## Déploiement en production
|
||||||
|
|
||||||
|
### 1. Base de données
|
||||||
|
|
||||||
|
Dump de la base locale :
|
||||||
|
```bash
|
||||||
|
pg_dump -h localhost -p 5433 -U root -d inventory > backup_v1.0.0.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Import en production :
|
||||||
|
```bash
|
||||||
|
psql -h <PROD_HOST> -U <PROD_USER> -d inventory < backup_v1.0.0.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Variables d'environnement production
|
||||||
|
|
||||||
|
Créer un fichier `.env.local` en production avec :
|
||||||
|
|
||||||
|
```env
|
||||||
|
APP_ENV=prod
|
||||||
|
APP_SECRET=<générer avec: openssl rand -hex 32>
|
||||||
|
DATABASE_URL="postgresql://user:password@host:5432/inventory?serverVersion=16"
|
||||||
|
CORS_ALLOW_ORIGIN='^https://votre-domaine\.com$'
|
||||||
|
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||||
|
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||||
|
JWT_PASSPHRASE=<votre-passphrase>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Build production
|
||||||
|
|
||||||
|
Backend :
|
||||||
|
```bash
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
php bin/console cache:clear --env=prod
|
||||||
|
php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend :
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend
|
||||||
|
NUXT_PUBLIC_API_BASE_URL=https://api.votre-domaine.com yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Checklist avant mise en prod
|
||||||
|
|
||||||
|
- [ ] Tests passent
|
||||||
|
- [ ] Migrations DB testées
|
||||||
|
- [ ] Variables d'environnement configurées
|
||||||
|
- [ ] Clés JWT générées
|
||||||
|
- [ ] CORS configuré
|
||||||
|
- [ ] SSL/HTTPS actif
|
||||||
|
- [ ] Backup de la DB prod existante (si upgrade)
|
||||||
2
TODO.md
Normal file
2
TODO.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- Doc: ne pas oublier de mettre `make` dans la documentation.
|
||||||
|
- Note: le probleme d'IP sous WSL, a ajouter dans la doc.
|
||||||
@@ -11,7 +11,7 @@ fi
|
|||||||
|
|
||||||
# Types autorisés (MINUSCULES uniquement)
|
# Types autorisés (MINUSCULES uniquement)
|
||||||
# Optionnel: scope => feat(auth) : ...
|
# Optionnel: scope => feat(auth) : ...
|
||||||
REGEX='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9._-]+\))?\ :\ .+'
|
REGEX='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test|wip)(\([a-z0-9._-]+\))?\ :\ .+'
|
||||||
|
|
||||||
if [[ ! "$FIRST_LINE" =~ $REGEX ]]; then
|
if [[ ! "$FIRST_LINE" =~ $REGEX ]]; then
|
||||||
echo "❌ Message de commit invalide."
|
echo "❌ Message de commit invalide."
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"doctrine/doctrine-bundle": "^3.2",
|
"doctrine/doctrine-bundle": "^3.2",
|
||||||
"doctrine/doctrine-migrations-bundle": "^4.0",
|
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||||
"doctrine/orm": "^3.6",
|
"doctrine/orm": "^3.6",
|
||||||
|
"lexik/jwt-authentication-bundle": "^3.2",
|
||||||
"nelmio/cors-bundle": "^2.6",
|
"nelmio/cors-bundle": "^2.6",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
"phpstan/phpdoc-parser": "^2.3",
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
@@ -27,8 +28,10 @@
|
|||||||
"symfony/security-bundle": "8.0.*",
|
"symfony/security-bundle": "8.0.*",
|
||||||
"symfony/serializer": "8.0.*",
|
"symfony/serializer": "8.0.*",
|
||||||
"symfony/twig-bundle": "8.0.*",
|
"symfony/twig-bundle": "8.0.*",
|
||||||
|
"symfony/uid": "8.0.*",
|
||||||
"symfony/validator": "8.0.*",
|
"symfony/validator": "8.0.*",
|
||||||
"symfony/yaml": "8.0.*"
|
"symfony/yaml": "8.0.*",
|
||||||
|
"vich/uploader-bundle": "^2.9"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
|
|||||||
540
composer.lock
generated
540
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "bab4560dec1d36eec0b0aa2284bd8559",
|
"content-hash": "9e0e35659f9b6ef5c0a60262a36b61f2",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "api-platform/doctrine-common",
|
"name": "api-platform/doctrine-common",
|
||||||
@@ -2361,6 +2361,259 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-10-26T09:35:14+00:00"
|
"time": "2025-10-26T09:35:14+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "jms/metadata",
|
||||||
|
"version": "2.9.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/schmittjoh/metadata.git",
|
||||||
|
"reference": "554319d2e5f0c5d8ccaeffe755eac924e14da330"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/schmittjoh/metadata/zipball/554319d2e5f0c5d8ccaeffe755eac924e14da330",
|
||||||
|
"reference": "554319d2e5f0c5d8ccaeffe755eac924e14da330",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2|^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/cache": "^1.0|^2.0",
|
||||||
|
"doctrine/coding-standard": "^8.0",
|
||||||
|
"mikey179/vfsstream": "^1.6.7",
|
||||||
|
"phpunit/phpunit": "^8.5.42|^9.6.23",
|
||||||
|
"psr/container": "^1.0|^2.0",
|
||||||
|
"symfony/cache": "^3.1|^4.0|^5.0|^6.0|^7.0|^8.0",
|
||||||
|
"symfony/dependency-injection": "^3.1|^4.0|^5.0|^6.0|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Metadata\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Johannes M. Schmitt",
|
||||||
|
"email": "schmittjoh@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Asmir Mustafic",
|
||||||
|
"email": "goetas@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Class/method/property metadata management in PHP",
|
||||||
|
"keywords": [
|
||||||
|
"annotations",
|
||||||
|
"metadata",
|
||||||
|
"xml",
|
||||||
|
"yaml"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/schmittjoh/metadata/issues",
|
||||||
|
"source": "https://github.com/schmittjoh/metadata/tree/2.9.0"
|
||||||
|
},
|
||||||
|
"time": "2025-11-30T20:12:26+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lcobucci/jwt",
|
||||||
|
"version": "5.6.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/lcobucci/jwt.git",
|
||||||
|
"reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e",
|
||||||
|
"reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-openssl": "*",
|
||||||
|
"ext-sodium": "*",
|
||||||
|
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||||
|
"psr/clock": "^1.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"infection/infection": "^0.29",
|
||||||
|
"lcobucci/clock": "^3.2",
|
||||||
|
"lcobucci/coding-standard": "^11.0",
|
||||||
|
"phpbench/phpbench": "^1.2",
|
||||||
|
"phpstan/extension-installer": "^1.2",
|
||||||
|
"phpstan/phpstan": "^1.10.7",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^1.1.3",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.3.10",
|
||||||
|
"phpstan/phpstan-strict-rules": "^1.5.0",
|
||||||
|
"phpunit/phpunit": "^11.1"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"lcobucci/clock": ">= 3.2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Lcobucci\\JWT\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Luís Cobucci",
|
||||||
|
"email": "lcobucci@gmail.com",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
|
||||||
|
"keywords": [
|
||||||
|
"JWS",
|
||||||
|
"jwt"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/lcobucci/jwt/issues",
|
||||||
|
"source": "https://github.com/lcobucci/jwt/tree/5.6.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/lcobucci",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.patreon.com/lcobucci",
|
||||||
|
"type": "patreon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-10-17T11:30:53+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lexik/jwt-authentication-bundle",
|
||||||
|
"version": "v3.2.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/lexik/LexikJWTAuthenticationBundle.git",
|
||||||
|
"reference": "60df75dc70ee6f597929cb2f0812adda591dfa4b"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/lexik/LexikJWTAuthenticationBundle/zipball/60df75dc70ee6f597929cb2f0812adda591dfa4b",
|
||||||
|
"reference": "60df75dc70ee6f597929cb2f0812adda591dfa4b",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-openssl": "*",
|
||||||
|
"lcobucci/jwt": "^5.0",
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/clock": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/config": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/deprecation-contracts": "^2.4|^3.0",
|
||||||
|
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/http-foundation": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/property-access": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/security-bundle": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/translation-contracts": "^1.0|^2.0|^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"api-platform/core": "^3.0|^4.0",
|
||||||
|
"rector/rector": "^1.2",
|
||||||
|
"symfony/browser-kit": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/console": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/dom-crawler": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/filesystem": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/framework-bundle": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/phpunit-bridge": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/var-dumper": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/yaml": "^6.4|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"gesdinet/jwt-refresh-token-bundle": "Implements a refresh token system over Json Web Tokens in Symfony",
|
||||||
|
"spomky-labs/lexik-jose-bridge": "Provides a JWT Token encoder with encryption support"
|
||||||
|
},
|
||||||
|
"type": "symfony-bundle",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Lexik\\Bundle\\JWTAuthenticationBundle\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jeremy Barthe",
|
||||||
|
"email": "j.barthe@lexik.fr",
|
||||||
|
"homepage": "https://github.com/jeremyb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nicolas Cabot",
|
||||||
|
"email": "n.cabot@lexik.fr",
|
||||||
|
"homepage": "https://github.com/slashfan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cedric Girard",
|
||||||
|
"email": "c.girard@lexik.fr",
|
||||||
|
"homepage": "https://github.com/cedric-g"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dev Lexik",
|
||||||
|
"email": "dev@lexik.fr",
|
||||||
|
"homepage": "https://github.com/lexik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Robin Chalas",
|
||||||
|
"email": "robin.chalas@gmail.com",
|
||||||
|
"homepage": "https://github.com/chalasr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lexik Community",
|
||||||
|
"homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle/graphs/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "This bundle provides JWT authentication for your Symfony REST API",
|
||||||
|
"homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle",
|
||||||
|
"keywords": [
|
||||||
|
"Authentication",
|
||||||
|
"JWS",
|
||||||
|
"api",
|
||||||
|
"bundle",
|
||||||
|
"jwt",
|
||||||
|
"rest",
|
||||||
|
"symfony"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/lexik/LexikJWTAuthenticationBundle/issues",
|
||||||
|
"source": "https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v3.2.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/chalasr",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/lexik/jwt-authentication-bundle",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-12-20T17:47:00+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nelmio/cors-bundle",
|
"name": "nelmio/cors-bundle",
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
@@ -4613,6 +4866,92 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-12-31T09:29:34+00:00"
|
"time": "2025-12-31T09:29:34+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/mime",
|
||||||
|
"version": "v8.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/mime.git",
|
||||||
|
"reference": "7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/mime/zipball/7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40",
|
||||||
|
"reference": "7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.4",
|
||||||
|
"symfony/polyfill-intl-idn": "^1.10",
|
||||||
|
"symfony/polyfill-mbstring": "^1.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"egulias/email-validator": "~3.0.0",
|
||||||
|
"phpdocumentor/reflection-docblock": "<3.2.2",
|
||||||
|
"phpdocumentor/type-resolver": "<1.4.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"egulias/email-validator": "^2.1.10|^3.1|^4",
|
||||||
|
"league/html-to-markdown": "^5.0",
|
||||||
|
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
|
||||||
|
"symfony/dependency-injection": "^7.4|^8.0",
|
||||||
|
"symfony/process": "^7.4|^8.0",
|
||||||
|
"symfony/property-access": "^7.4|^8.0",
|
||||||
|
"symfony/property-info": "^7.4|^8.0",
|
||||||
|
"symfony/serializer": "^7.4|^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Mime\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Allows manipulating MIME messages",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"mime",
|
||||||
|
"mime-type"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/mime/tree/v8.0.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-11-16T10:17:21+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/password-hasher",
|
"name": "symfony/password-hasher",
|
||||||
"version": "v8.0.0",
|
"version": "v8.0.0",
|
||||||
@@ -4768,6 +5107,93 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-06-27T09:58:17+00:00"
|
"time": "2025-06-27T09:58:17+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/polyfill-intl-idn",
|
||||||
|
"version": "v1.33.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||||
|
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3",
|
||||||
|
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.2",
|
||||||
|
"symfony/polyfill-intl-normalizer": "^1.10"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-intl": "For best performance"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"thanks": {
|
||||||
|
"url": "https://github.com/symfony/polyfill",
|
||||||
|
"name": "symfony/polyfill"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"bootstrap.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Polyfill\\Intl\\Idn\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Laurent Bassin",
|
||||||
|
"email": "laurent@bassin.info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Trevor Rowbotham",
|
||||||
|
"email": "trevor.rowbotham@pm.me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"compatibility",
|
||||||
|
"idn",
|
||||||
|
"intl",
|
||||||
|
"polyfill",
|
||||||
|
"portable",
|
||||||
|
"shim"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2024-09-10T14:38:51+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-intl-normalizer",
|
"name": "symfony/polyfill-intl-normalizer",
|
||||||
"version": "v1.33.0",
|
"version": "v1.33.0",
|
||||||
@@ -7040,6 +7466,114 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-12-14T11:28:47+00:00"
|
"time": "2025-12-14T11:28:47+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "vich/uploader-bundle",
|
||||||
|
"version": "v2.9.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/dustin10/VichUploaderBundle.git",
|
||||||
|
"reference": "945939a04a33c0b78c5fbb7ead31533d85112df5"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/dustin10/VichUploaderBundle/zipball/945939a04a33c0b78c5fbb7ead31533d85112df5",
|
||||||
|
"reference": "945939a04a33c0b78c5fbb7ead31533d85112df5",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"doctrine/persistence": "^3.0 || ^4.0",
|
||||||
|
"ext-simplexml": "*",
|
||||||
|
"jms/metadata": "^2.4",
|
||||||
|
"php": "^8.1",
|
||||||
|
"symfony/config": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/event-dispatcher-contracts": "^3.1",
|
||||||
|
"symfony/http-foundation": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/mime": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/property-access": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/string": "^5.4 || ^6.0 || ^7.0 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"doctrine/annotations": "<1.12",
|
||||||
|
"league/flysystem": "<2.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dg/bypass-finals": "^1.9",
|
||||||
|
"doctrine/common": "^3.0",
|
||||||
|
"doctrine/doctrine-bundle": "^2.7 || ^3.0",
|
||||||
|
"doctrine/mongodb-odm": "^2.4",
|
||||||
|
"doctrine/orm": "^2.13 || ^3.0",
|
||||||
|
"ext-sqlite3": "*",
|
||||||
|
"knplabs/knp-gaufrette-bundle": "dev-master",
|
||||||
|
"league/flysystem-bundle": "^2.4 || ^3.0",
|
||||||
|
"league/flysystem-memory": "^2.0 || ^3.0",
|
||||||
|
"matthiasnoback/symfony-dependency-injection-test": "^5.1 || ^6.0",
|
||||||
|
"mikey179/vfsstream": "^1.6.11",
|
||||||
|
"phpunit/phpunit": "^10.5 || ^11.5 || ^12.2",
|
||||||
|
"symfony/asset": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/browser-kit": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/doctrine-bridge": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/dom-crawler": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/form": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/phpunit-bridge": "^7.3",
|
||||||
|
"symfony/security-csrf": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/translation": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/twig-bridge": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/twig-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/validator": "^5.4.22 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/var-dumper": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"doctrine/doctrine-bundle": "For integration with Doctrine",
|
||||||
|
"doctrine/mongodb-odm-bundle": "For integration with Doctrine ODM",
|
||||||
|
"doctrine/orm": "For integration with Doctrine ORM",
|
||||||
|
"doctrine/phpcr-odm": "For integration with Doctrine PHPCR",
|
||||||
|
"knplabs/knp-gaufrette-bundle": "For integration with Gaufrette",
|
||||||
|
"league/flysystem-bundle": "For integration with Flysystem",
|
||||||
|
"liip/imagine-bundle": "To generate image thumbnails",
|
||||||
|
"oneup/flysystem-bundle": "For integration with Flysystem",
|
||||||
|
"symfony/asset": "To generate better links",
|
||||||
|
"symfony/form": "To handle uploads in forms",
|
||||||
|
"symfony/yaml": "To use YAML mapping"
|
||||||
|
},
|
||||||
|
"type": "symfony-bundle",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Vich\\UploaderBundle\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Dustin Dobervich",
|
||||||
|
"email": "ddobervich@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Ease file uploads attached to entities",
|
||||||
|
"homepage": "https://github.com/dustin10/VichUploaderBundle",
|
||||||
|
"keywords": [
|
||||||
|
"file uploads",
|
||||||
|
"upload"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/dustin10/VichUploaderBundle/issues",
|
||||||
|
"source": "https://github.com/dustin10/VichUploaderBundle/tree/v2.9.1"
|
||||||
|
},
|
||||||
|
"time": "2025-12-10T08:23:38+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "webmozart/assert",
|
"name": "webmozart/assert",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -10208,7 +10742,7 @@
|
|||||||
],
|
],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"stability-flags": [],
|
"stability-flags": {},
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
@@ -10216,6 +10750,6 @@
|
|||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*"
|
"ext-iconv": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.9.0"
|
"plugin-api-version": "2.9.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
|
||||||
|
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
|
||||||
|
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
|
||||||
|
use Nelmio\CorsBundle\NelmioCorsBundle;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||||
|
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
||||||
|
use Symfony\Bundle\TwigBundle\TwigBundle;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
FrameworkBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
TwigBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
SecurityBundle::class => ['all' => true],
|
||||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
DoctrineBundle::class => ['all' => true],
|
||||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
NelmioCorsBundle::class => ['all' => true],
|
||||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
ApiPlatformBundle::class => ['all' => true],
|
||||||
|
LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
api_platform:
|
api_platform:
|
||||||
title: Hello API Platform
|
title: Hello API Platform
|
||||||
version: 1.0.0
|
version: 1.4.0
|
||||||
defaults:
|
defaults:
|
||||||
stateless: true
|
stateless: false
|
||||||
cache_headers:
|
cache_headers:
|
||||||
vary: ['Content-Type', 'Authorization', 'Origin']
|
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||||
|
pagination_items_per_page: 30
|
||||||
|
pagination_maximum_items_per_page: 200
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
doctrine:
|
doctrine:
|
||||||
dbal:
|
dbal:
|
||||||
url: '%env(resolve:DATABASE_URL)%'
|
url: '%env(resolve:DATABASE_URL)%'
|
||||||
|
mapping_types:
|
||||||
|
modelcategory: string
|
||||||
|
_text: string
|
||||||
|
|
||||||
# IMPORTANT: You MUST configure your server version,
|
# IMPORTANT: You MUST configure your server version,
|
||||||
# either here or in the DATABASE_URL env var (see .env file)
|
# either here or in the DATABASE_URL env var (see .env file)
|
||||||
@@ -9,7 +12,8 @@ doctrine:
|
|||||||
profiling_collect_backtrace: '%kernel.debug%'
|
profiling_collect_backtrace: '%kernel.debug%'
|
||||||
orm:
|
orm:
|
||||||
validate_xml_mapping: true
|
validate_xml_mapping: true
|
||||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
naming_strategy: doctrine.orm.naming_strategy.default
|
||||||
|
quote_strategy: doctrine.orm.quote_strategy.default
|
||||||
identity_generation_preferences:
|
identity_generation_preferences:
|
||||||
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||||
auto_mapping: true
|
auto_mapping: true
|
||||||
|
|||||||
4
config/packages/lexik_jwt_authentication.yaml
Normal file
4
config/packages/lexik_jwt_authentication.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
lexik_jwt_authentication:
|
||||||
|
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
|
||||||
|
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
|
||||||
|
pass_phrase: '%env(JWT_PASSPHRASE)%'
|
||||||
@@ -4,7 +4,8 @@ nelmio_cors:
|
|||||||
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||||
allow_headers: ['Content-Type', 'Authorization']
|
allow_headers: ['Content-Type', 'Authorization']
|
||||||
|
allow_credentials: true
|
||||||
expose_headers: ['Link']
|
expose_headers: ['Link']
|
||||||
max_age: 3600
|
max_age: 3600
|
||||||
paths:
|
paths:
|
||||||
'^/': null
|
'^/api/': ~
|
||||||
|
|||||||
@@ -2,30 +2,63 @@ security:
|
|||||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||||
password_hashers:
|
password_hashers:
|
||||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||||
|
App\Entity\Profile:
|
||||||
|
algorithm: auto
|
||||||
|
|
||||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||||
providers:
|
providers:
|
||||||
users_in_memory: { memory: null }
|
app_user_provider:
|
||||||
|
entity:
|
||||||
|
class: App\Entity\Profile
|
||||||
|
property: email
|
||||||
|
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
# Ensure dev tools and static assets are always allowed
|
# Ensure dev tools and static assets are always allowed
|
||||||
pattern: ^/(_profiler|_wdt|assets|build)/
|
pattern: ^/(_profiler|_wdt|assets|build)/
|
||||||
security: false
|
security: false
|
||||||
|
|
||||||
|
login:
|
||||||
|
pattern: ^/api/login_check
|
||||||
|
stateless: true
|
||||||
|
provider: app_user_provider
|
||||||
|
json_login:
|
||||||
|
check_path: /api/login_check
|
||||||
|
username_path: email
|
||||||
|
password_path: password
|
||||||
|
success_handler: lexik_jwt_authentication.handler.authentication_success
|
||||||
|
failure_handler: lexik_jwt_authentication.handler.authentication_failure
|
||||||
|
|
||||||
|
session_public:
|
||||||
|
pattern: ^/api/session/profiles?$
|
||||||
|
security: false
|
||||||
|
|
||||||
|
api:
|
||||||
|
pattern: ^/api
|
||||||
|
stateless: true
|
||||||
|
custom_authenticators:
|
||||||
|
- App\Security\SessionProfileAuthenticator
|
||||||
|
|
||||||
main:
|
main:
|
||||||
lazy: true
|
lazy: true
|
||||||
provider: users_in_memory
|
provider: app_user_provider
|
||||||
|
|
||||||
# Activate different ways to authenticate:
|
role_hierarchy:
|
||||||
# https://symfony.com/doc/current/security.html#the-firewall
|
ROLE_ADMIN: ROLE_GESTIONNAIRE
|
||||||
|
ROLE_GESTIONNAIRE: ROLE_VIEWER
|
||||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
ROLE_VIEWER: ROLE_USER
|
||||||
# switch_user: true
|
|
||||||
|
|
||||||
# Note: Only the *first* matching rule is applied
|
# Note: Only the *first* matching rule is applied
|
||||||
access_control:
|
access_control:
|
||||||
# - { path: ^/admin, roles: ROLE_ADMIN }
|
- { path: ^/api/session/profile$, roles: PUBLIC_ACCESS }
|
||||||
# - { path: ^/profile, roles: ROLE_USER }
|
- { path: ^/api/session/profiles, roles: PUBLIC_ACCESS, methods: [GET] }
|
||||||
|
- { path: ^/api/admin, roles: ROLE_ADMIN }
|
||||||
|
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/api/test, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/docs, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/contexts, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/\.well-known, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/api, roles: ROLE_VIEWER }
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
security:
|
security:
|
||||||
|
|||||||
@@ -768,6 +768,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* property?: scalar|null|Param, // Default: null
|
* property?: scalar|null|Param, // Default: null
|
||||||
* manager_name?: scalar|null|Param, // Default: null
|
* manager_name?: scalar|null|Param, // Default: null
|
||||||
* },
|
* },
|
||||||
|
* lexik_jwt?: array{
|
||||||
|
* class?: scalar|null|Param, // Default: "Lexik\\Bundle\\JWTAuthenticationBundle\\Security\\User\\JWTUser"
|
||||||
|
* },
|
||||||
* }>,
|
* }>,
|
||||||
* firewalls: array<string, array{ // Default: []
|
* firewalls: array<string, array{ // Default: []
|
||||||
* pattern?: scalar|null|Param,
|
* pattern?: scalar|null|Param,
|
||||||
@@ -826,6 +829,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* provider?: scalar|null|Param,
|
* provider?: scalar|null|Param,
|
||||||
* user?: scalar|null|Param, // Default: "REMOTE_USER"
|
* user?: scalar|null|Param, // Default: "REMOTE_USER"
|
||||||
* },
|
* },
|
||||||
|
* jwt?: array{
|
||||||
|
* provider?: scalar|null|Param, // Default: null
|
||||||
|
* authenticator?: scalar|null|Param, // Default: "lexik_jwt_authentication.security.jwt_authenticator"
|
||||||
|
* },
|
||||||
* login_link?: array{
|
* login_link?: array{
|
||||||
* check_route: scalar|null|Param, // Route that will validate the login link - e.g. "app_login_link_verify".
|
* check_route: scalar|null|Param, // Route that will validate the login link - e.g. "app_login_link_verify".
|
||||||
* check_post_only?: scalar|null|Param, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false
|
* check_post_only?: scalar|null|Param, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false
|
||||||
@@ -1514,6 +1521,91 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* ...<mixed>
|
* ...<mixed>
|
||||||
* },
|
* },
|
||||||
* }
|
* }
|
||||||
|
* @psalm-type LexikJwtAuthenticationConfig = array{
|
||||||
|
* public_key?: scalar|null|Param, // The key used to sign tokens (useless for HMAC). If not set, the key will be automatically computed from the secret key. // Default: null
|
||||||
|
* additional_public_keys?: list<scalar|null|Param>,
|
||||||
|
* secret_key?: scalar|null|Param, // The key used to sign tokens. It can be a raw secret (for HMAC), a raw RSA/ECDSA key or the path to a file itself being plaintext or PEM. // Default: null
|
||||||
|
* pass_phrase?: scalar|null|Param, // The key passphrase (useless for HMAC) // Default: ""
|
||||||
|
* token_ttl?: scalar|null|Param, // Default: 3600
|
||||||
|
* allow_no_expiration?: bool|Param, // Allow tokens without "exp" claim (i.e. indefinitely valid, no lifetime) to be considered valid. Caution: usage of this should be rare. // Default: false
|
||||||
|
* clock_skew?: scalar|null|Param, // Default: 0
|
||||||
|
* encoder?: array{
|
||||||
|
* service?: scalar|null|Param, // Default: "lexik_jwt_authentication.encoder.lcobucci"
|
||||||
|
* signature_algorithm?: scalar|null|Param, // Default: "RS256"
|
||||||
|
* },
|
||||||
|
* user_id_claim?: scalar|null|Param, // Default: "username"
|
||||||
|
* token_extractors?: array{
|
||||||
|
* authorization_header?: bool|array{
|
||||||
|
* enabled?: bool|Param, // Default: true
|
||||||
|
* prefix?: scalar|null|Param, // Default: "Bearer"
|
||||||
|
* name?: scalar|null|Param, // Default: "Authorization"
|
||||||
|
* },
|
||||||
|
* cookie?: bool|array{
|
||||||
|
* enabled?: bool|Param, // Default: false
|
||||||
|
* name?: scalar|null|Param, // Default: "BEARER"
|
||||||
|
* },
|
||||||
|
* query_parameter?: bool|array{
|
||||||
|
* enabled?: bool|Param, // Default: false
|
||||||
|
* name?: scalar|null|Param, // Default: "bearer"
|
||||||
|
* },
|
||||||
|
* split_cookie?: bool|array{
|
||||||
|
* enabled?: bool|Param, // Default: false
|
||||||
|
* cookies?: list<scalar|null|Param>,
|
||||||
|
* },
|
||||||
|
* },
|
||||||
|
* remove_token_from_body_when_cookies_used?: scalar|null|Param, // Default: true
|
||||||
|
* set_cookies?: array<string, array{ // Default: []
|
||||||
|
* lifetime?: scalar|null|Param, // The cookie lifetime. If null, the "token_ttl" option value will be used // Default: null
|
||||||
|
* samesite?: "none"|"lax"|"strict"|Param, // Default: "lax"
|
||||||
|
* path?: scalar|null|Param, // Default: "/"
|
||||||
|
* domain?: scalar|null|Param, // Default: null
|
||||||
|
* secure?: scalar|null|Param, // Default: true
|
||||||
|
* httpOnly?: scalar|null|Param, // Default: true
|
||||||
|
* partitioned?: scalar|null|Param, // Default: false
|
||||||
|
* split?: list<scalar|null|Param>,
|
||||||
|
* }>,
|
||||||
|
* api_platform?: bool|array{ // API Platform compatibility: add check_path in OpenAPI documentation.
|
||||||
|
* enabled?: bool|Param, // Default: false
|
||||||
|
* check_path?: scalar|null|Param, // The login check path to add in OpenAPI. // Default: null
|
||||||
|
* username_path?: scalar|null|Param, // The path to the username in the JSON body. // Default: null
|
||||||
|
* password_path?: scalar|null|Param, // The path to the password in the JSON body. // Default: null
|
||||||
|
* },
|
||||||
|
* access_token_issuance?: bool|array{
|
||||||
|
* enabled?: bool|Param, // Default: false
|
||||||
|
* signature?: array{
|
||||||
|
* algorithm: scalar|null|Param, // The algorithm use to sign the access tokens.
|
||||||
|
* key: scalar|null|Param, // The signature key. It shall be JWK encoded.
|
||||||
|
* },
|
||||||
|
* encryption?: bool|array{
|
||||||
|
* enabled?: bool|Param, // Default: false
|
||||||
|
* key_encryption_algorithm: scalar|null|Param, // The key encryption algorithm is used to encrypt the token.
|
||||||
|
* content_encryption_algorithm: scalar|null|Param, // The key encryption algorithm is used to encrypt the token.
|
||||||
|
* key: scalar|null|Param, // The encryption key. It shall be JWK encoded.
|
||||||
|
* },
|
||||||
|
* },
|
||||||
|
* access_token_verification?: bool|array{
|
||||||
|
* enabled?: bool|Param, // Default: false
|
||||||
|
* signature?: array{
|
||||||
|
* header_checkers?: list<scalar|null|Param>,
|
||||||
|
* claim_checkers?: list<scalar|null|Param>,
|
||||||
|
* mandatory_claims?: list<scalar|null|Param>,
|
||||||
|
* allowed_algorithms?: list<scalar|null|Param>,
|
||||||
|
* keyset: scalar|null|Param, // The signature keyset. It shall be JWKSet encoded.
|
||||||
|
* },
|
||||||
|
* encryption?: bool|array{
|
||||||
|
* enabled?: bool|Param, // Default: false
|
||||||
|
* continue_on_decryption_failure?: bool|Param, // If enable, non-encrypted tokens or tokens that failed during decryption or verification processes are accepted. // Default: false
|
||||||
|
* header_checkers?: list<scalar|null|Param>,
|
||||||
|
* allowed_key_encryption_algorithms?: list<scalar|null|Param>,
|
||||||
|
* allowed_content_encryption_algorithms?: list<scalar|null|Param>,
|
||||||
|
* keyset: scalar|null|Param, // The encryption keyset. It shall be JWKSet encoded.
|
||||||
|
* },
|
||||||
|
* },
|
||||||
|
* blocklist_token?: bool|array{
|
||||||
|
* enabled?: bool|Param, // Default: false
|
||||||
|
* cache?: scalar|null|Param, // Storage to track blocked tokens // Default: "cache.app"
|
||||||
|
* },
|
||||||
|
* }
|
||||||
* @psalm-type ConfigType = array{
|
* @psalm-type ConfigType = array{
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
* parameters?: ParametersConfig,
|
* parameters?: ParametersConfig,
|
||||||
@@ -1525,6 +1617,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||||
* nelmio_cors?: NelmioCorsConfig,
|
* nelmio_cors?: NelmioCorsConfig,
|
||||||
* api_platform?: ApiPlatformConfig,
|
* api_platform?: ApiPlatformConfig,
|
||||||
|
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||||
* "when@dev"?: array{
|
* "when@dev"?: array{
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
* parameters?: ParametersConfig,
|
* parameters?: ParametersConfig,
|
||||||
@@ -1536,6 +1629,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||||
* nelmio_cors?: NelmioCorsConfig,
|
* nelmio_cors?: NelmioCorsConfig,
|
||||||
* api_platform?: ApiPlatformConfig,
|
* api_platform?: ApiPlatformConfig,
|
||||||
|
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||||
* },
|
* },
|
||||||
* "when@prod"?: array{
|
* "when@prod"?: array{
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
@@ -1548,6 +1642,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||||
* nelmio_cors?: NelmioCorsConfig,
|
* nelmio_cors?: NelmioCorsConfig,
|
||||||
* api_platform?: ApiPlatformConfig,
|
* api_platform?: ApiPlatformConfig,
|
||||||
|
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||||
* },
|
* },
|
||||||
* "when@test"?: array{
|
* "when@test"?: array{
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
@@ -1560,6 +1655,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||||
* nelmio_cors?: NelmioCorsConfig,
|
* nelmio_cors?: NelmioCorsConfig,
|
||||||
* api_platform?: ApiPlatformConfig,
|
* api_platform?: ApiPlatformConfig,
|
||||||
|
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||||
* },
|
* },
|
||||||
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
|
|||||||
@@ -7,5 +7,8 @@
|
|||||||
# To list all registered routes, run the following command:
|
# To list all registered routes, run the following command:
|
||||||
# bin/console debug:router
|
# bin/console debug:router
|
||||||
|
|
||||||
|
api_login_check:
|
||||||
|
path: /api/login_check
|
||||||
|
|
||||||
controllers:
|
controllers:
|
||||||
resource: routing.controllers
|
resource: routing.controllers
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
api_platform:
|
api_platform:
|
||||||
resource: .
|
resource: .
|
||||||
type: api_platform
|
type: api_platform
|
||||||
prefix: /
|
prefix: /api
|
||||||
|
|||||||
5
config/routes/routing.controllers.yaml
Normal file
5
config/routes/routing.controllers.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
controllers:
|
||||||
|
resource:
|
||||||
|
path: ../../src/Controller/
|
||||||
|
namespace: App\Controller
|
||||||
|
type: attribute
|
||||||
@@ -21,3 +21,15 @@ services:
|
|||||||
|
|
||||||
# add more service definitions when explicit configuration is needed
|
# add more service definitions when explicit configuration is needed
|
||||||
# please note that last definitions always *replace* previous ones
|
# please note that last definitions always *replace* previous ones
|
||||||
|
|
||||||
|
App\EventSubscriber\ProductAuditSubscriber:
|
||||||
|
tags:
|
||||||
|
- { name: doctrine.event_subscriber }
|
||||||
|
|
||||||
|
App\EventSubscriber\PieceAuditSubscriber:
|
||||||
|
tags:
|
||||||
|
- { name: doctrine.event_subscriber }
|
||||||
|
|
||||||
|
App\EventSubscriber\ComposantAuditSubscriber:
|
||||||
|
tags:
|
||||||
|
- { name: doctrine.event_subscriber }
|
||||||
|
|||||||
61
create_test_user.php
Normal file
61
create_test_user.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
|
||||||
|
|
||||||
|
// Hash the password
|
||||||
|
$factory = new PasswordHasherFactory([
|
||||||
|
'common' => ['algorithm' => 'bcrypt'],
|
||||||
|
'memory-hard' => ['algorithm' => 'argon2i'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$passwordHasher = $factory->getPasswordHasher('common');
|
||||||
|
$hashedPassword = $passwordHasher->hash('admin123');
|
||||||
|
|
||||||
|
// Connect to database
|
||||||
|
$pdo = new PDO(
|
||||||
|
'pgsql:host=db;port=5432;dbname=inventory',
|
||||||
|
'root',
|
||||||
|
'root'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if table exists
|
||||||
|
$tableExists = $pdo->query("SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'profiles')")->fetchColumn();
|
||||||
|
|
||||||
|
if ($tableExists) {
|
||||||
|
echo "Table 'profiles' exists.\n";
|
||||||
|
|
||||||
|
// Check if user exists
|
||||||
|
$userExists = $pdo->prepare('SELECT COUNT(*) FROM profiles WHERE email = ?');
|
||||||
|
$userExists->execute(['admin@admin.com']);
|
||||||
|
|
||||||
|
if ($userExists->fetchColumn() > 0) {
|
||||||
|
echo "User admin@admin.com already exists. Updating password...\n";
|
||||||
|
$stmt = $pdo->prepare('UPDATE profiles SET password = ? WHERE email = ?');
|
||||||
|
$stmt->execute([$hashedPassword, 'admin@admin.com']);
|
||||||
|
echo "Password updated!\n";
|
||||||
|
} else {
|
||||||
|
echo "Creating user admin@admin.com...\n";
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO profiles (id, email, first_name, last_name, is_active, roles, password, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())');
|
||||||
|
$id = 'cl'.substr(strtolower(base_convert(random_bytes(12), 2, 36)), 0, 24);
|
||||||
|
$stmt->execute([
|
||||||
|
$id,
|
||||||
|
'admin@admin.com',
|
||||||
|
'Admin',
|
||||||
|
'User',
|
||||||
|
true,
|
||||||
|
json_encode(['ROLE_USER', 'ROLE_ADMIN']),
|
||||||
|
$hashedPassword,
|
||||||
|
]);
|
||||||
|
echo "User created!\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "Table 'profiles' does not exist yet. Run migrations first.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nTest credentials:\n";
|
||||||
|
echo "Email: admin@admin.com\n";
|
||||||
|
echo "Password: admin123\n";
|
||||||
@@ -14,6 +14,7 @@ services:
|
|||||||
XDEBUG_CLIENT_HOST: ${XDEBUG_CLIENT_HOST:-host.docker.internal}
|
XDEBUG_CLIENT_HOST: ${XDEBUG_CLIENT_HOST:-host.docker.internal}
|
||||||
XDEBUG_CONFIG: client_host=${XDEBUG_CLIENT_HOST:-host.docker.internal} client_port=9003
|
XDEBUG_CONFIG: client_host=${XDEBUG_CLIENT_HOST:-host.docker.internal} client_port=9003
|
||||||
DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
||||||
|
CORS_ALLOW_ORIGIN: ${CORS_ALLOW_ORIGIN}
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/var/www/html
|
- ./:/var/www/html
|
||||||
- ~/.cache:/var/www/.cache # Pour la cache de composer
|
- ~/.cache:/var/www/.cache # Pour la cache de composer
|
||||||
@@ -29,8 +30,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8081:80"
|
||||||
- "3000:3000"
|
- "3001:3000"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
@@ -41,7 +42,20 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- pg_data:/var/lib/postgresql/data
|
- pg_data:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- "${POSTGRES_PORT:-5432}:5432"
|
- "${POSTGRES_PORT:-5433}:5432"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
adminer:
|
||||||
|
container_name: adminer-${DOCKER_APP_NAME}
|
||||||
|
image: adminer:latest
|
||||||
|
environment:
|
||||||
|
ADMINER_DEFAULT_SERVER: db
|
||||||
|
ADMINER_DESIGN: dracula
|
||||||
|
ports:
|
||||||
|
- "${ADMINER_PORT:-5050}:8080"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pg_data:
|
pg_data:
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
DOCKER_APP_NAME=ferme
|
DOCKER_APP_NAME=inventory
|
||||||
DOCKER_PHP_VERSION=8.4.6
|
DOCKER_PHP_VERSION=8.4.6
|
||||||
DOCKER_NODE_VERSION=24.12.0
|
DOCKER_NODE_VERSION=24.12.0
|
||||||
APP_USER=www-data
|
APP_USER=www-data
|
||||||
POSTGRES_DB=ferme
|
POSTGRES_DB=inventory
|
||||||
POSTGRES_USER=root
|
POSTGRES_USER=root
|
||||||
POSTGRES_PASSWORD=root
|
POSTGRES_PASSWORD=root
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
XDEBUG_CLIENT_HOST=host.docker.internal
|
XDEBUG_CLIENT_HOST=host.docker.internal
|
||||||
35
docker/.env.docker.local
Normal file
35
docker/.env.docker.local
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
DOCKER_APP_NAME=inventory
|
||||||
|
DOCKER_PHP_VERSION=8.4.6
|
||||||
|
DOCKER_NODE_VERSION=24.12.0
|
||||||
|
APP_USER=www-data
|
||||||
|
CURRENT_UID=1000
|
||||||
|
CURRENT_GID=1000
|
||||||
|
|
||||||
|
# PostgreSQL
|
||||||
|
POSTGRES_DB=inventory
|
||||||
|
POSTGRES_USER=root
|
||||||
|
POSTGRES_PASSWORD=root
|
||||||
|
#
|
||||||
|
# CORS
|
||||||
|
CORS_ALLOW_ORIGIN=^https?://(localhost|127\\.0\\.0\\.1)(:[0-9]+)?$
|
||||||
|
POSTGRES_PORT=5433
|
||||||
|
|
||||||
|
# pgAdmin
|
||||||
|
PGADMIN_EMAIL=admin@admin.com
|
||||||
|
PGADMIN_PASSWORD=admin
|
||||||
|
PGADMIN_PORT=5050
|
||||||
|
|
||||||
|
# XDebug
|
||||||
|
XDEBUG_CLIENT_HOST=host.docker.internal
|
||||||
|
|
||||||
|
# Symfony (pour future migration)
|
||||||
|
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
|
||||||
|
NESTJS_PORT=3000
|
||||||
|
SESSION_SECRET=changeme_session_secret
|
||||||
|
CORS_ORIGIN=http://localhost:3001
|
||||||
2
docker/pgadmin/pgpass
Normal file
2
docker/pgadmin/pgpass
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
db:5432:inventory:root:root
|
||||||
|
db:5432:*:root:root
|
||||||
15
docker/pgadmin/servers.json
Normal file
15
docker/pgadmin/servers.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"Servers": {
|
||||||
|
"1": {
|
||||||
|
"Name": "Inventory PostgreSQL",
|
||||||
|
"Group": "Servers",
|
||||||
|
"Host": "db",
|
||||||
|
"Port": 5432,
|
||||||
|
"MaintenanceDB": "inventory",
|
||||||
|
"Username": "root",
|
||||||
|
"SSLMode": "prefer",
|
||||||
|
"PassFile": "/var/lib/pgadmin/pgpass",
|
||||||
|
"Comment": "Serveur PostgreSQL du projet Inventory"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
wget \
|
wget \
|
||||||
git \
|
git \
|
||||||
unzip \
|
unzip \
|
||||||
|
qpdf \
|
||||||
&& docker-php-ext-install -j$(nproc) \
|
&& docker-php-ext-install -j$(nproc) \
|
||||||
intl \
|
intl \
|
||||||
zip \
|
zip \
|
||||||
|
|||||||
@@ -1,26 +1,15 @@
|
|||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
DocumentRoot /var/www/html
|
ServerName localhost
|
||||||
|
DocumentRoot /var/www/html/public
|
||||||
|
|
||||||
AliasMatch "^/api(/.*)?" "/var/www/html/public$1"
|
# API Symfony
|
||||||
<Directory /var/www/html/public>
|
<Directory /var/www/html/public>
|
||||||
Options FollowSymLinks
|
Options +FollowSymLinks
|
||||||
AllowOverride All
|
AllowOverride All
|
||||||
Require all granted
|
Require all granted
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
AliasMatch "^(/.*)?" "/var/www/html/frontend/dist$1"
|
# Logs
|
||||||
<Directory /var/www/html/frontend/dist>
|
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||||
AllowOverride All
|
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||||
Order allow,deny
|
|
||||||
Allow from All
|
|
||||||
|
|
||||||
RewriteEngine on
|
|
||||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
|
||||||
RewriteCond %{REQUEST_FILENAME} -d
|
|
||||||
RewriteRule ^ - [L]
|
|
||||||
RewriteRule ^ index.html [L]
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
ErrorLog "${APACHE_LOG_DIR}/error.log"
|
|
||||||
CustomLog "${APACHE_LOG_DIR}/access.log" combined
|
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
|||||||
1092
fixtures/data.sql
Normal file
1092
fixtures/data.sql
Normal file
File diff suppressed because one or more lines are too long
42
fixtures/load.sh
Executable file
42
fixtures/load.sh
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Load fixtures into the database
|
||||||
|
# Usage: ./fixtures/load.sh [--reset]
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
if [ -f "$PROJECT_DIR/.env" ]; then
|
||||||
|
export $(grep -v '^#' "$PROJECT_DIR/.env" | xargs)
|
||||||
|
fi
|
||||||
|
|
||||||
|
DB_USER="${POSTGRES_USER:-root}"
|
||||||
|
DB_NAME="${POSTGRES_DB:-inventory}"
|
||||||
|
CONTAINER="${DB_CONTAINER:-inventory-db-1}"
|
||||||
|
|
||||||
|
echo "Loading fixtures into $DB_NAME..."
|
||||||
|
|
||||||
|
# Check if --reset flag is passed
|
||||||
|
if [ "$1" == "--reset" ]; then
|
||||||
|
echo "Resetting database (truncating all tables)..."
|
||||||
|
docker exec -i "$CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -c "
|
||||||
|
DO \$\$ DECLARE
|
||||||
|
r RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename != 'doctrine_migration_versions') LOOP
|
||||||
|
EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE';
|
||||||
|
END LOOP;
|
||||||
|
END \$\$;
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load fixtures with foreign key checks disabled
|
||||||
|
docker exec -i "$CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" <<EOF
|
||||||
|
SET session_replication_role = replica;
|
||||||
|
$(cat "$SCRIPT_DIR/data.sql")
|
||||||
|
SET session_replication_role = DEFAULT;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Fixtures loaded successfully!"
|
||||||
24
frontend/.gitignore
vendored
24
frontend/.gitignore
vendored
@@ -1,24 +0,0 @@
|
|||||||
# Nuxt dev/build outputs
|
|
||||||
.output
|
|
||||||
.data
|
|
||||||
.nuxt
|
|
||||||
.nitro
|
|
||||||
.cache
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Node dependencies
|
|
||||||
node_modules
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
.DS_Store
|
|
||||||
.fleet
|
|
||||||
.idea
|
|
||||||
|
|
||||||
# Local env files
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
# Nuxt Minimal Starter
|
|
||||||
|
|
||||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
Make sure to install dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# npm
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Server
|
|
||||||
|
|
||||||
Start the development server on `http://localhost:3000`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# npm
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm dev
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn dev
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production
|
|
||||||
|
|
||||||
Build the application for production:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# npm
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm build
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn build
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun run build
|
|
||||||
```
|
|
||||||
|
|
||||||
Locally preview production build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# npm
|
|
||||||
npm run preview
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm preview
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn preview
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun run preview
|
|
||||||
```
|
|
||||||
|
|
||||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<NuxtPage/>
|
|
||||||
</template>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export default defineNuxtConfig({
|
|
||||||
compatibilityDate: '2025-07-15',
|
|
||||||
devtools: { enabled: true },
|
|
||||||
ssr: false,
|
|
||||||
modules: ['@nuxtjs/tailwindcss'],
|
|
||||||
typescript: {
|
|
||||||
strict: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
11892
frontend/package-lock.json
generated
11892
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "frontend",
|
|
||||||
"type": "module",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"build": "nuxt build",
|
|
||||||
"dev": "nuxt dev",
|
|
||||||
"generate": "nuxt generate",
|
|
||||||
"preview": "nuxt preview",
|
|
||||||
"postinstall": "nuxt prepare",
|
|
||||||
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"nuxt": "^4.2.2",
|
|
||||||
"vue": "^3.5.26",
|
|
||||||
"vue-router": "^4.6.4"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="min-h-screen flex items-center justify-center">
|
|
||||||
<h1 class="text-3xl font-bold">Nuxt OK ✅</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,2 +0,0 @@
|
|||||||
User-Agent: *
|
|
||||||
Disallow:
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
// https://nuxt.com/docs/guide/concepts/typescript
|
|
||||||
"files": [],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./.nuxt/tsconfig.app.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./.nuxt/tsconfig.server.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./.nuxt/tsconfig.shared.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./.nuxt/tsconfig.node.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
47
makefile
47
makefile
@@ -19,6 +19,8 @@ EXEC_PHP_ROOT = $(DOCKER) exec -t -u root $(PHP_CONTAINER)
|
|||||||
EXEC_PHP_INTERACTIVE = $(DOCKER) exec -it -u $(APP_USER) $(PHP_CONTAINER)
|
EXEC_PHP_INTERACTIVE = $(DOCKER) exec -it -u $(APP_USER) $(PHP_CONTAINER)
|
||||||
EXEC_PHP_INTERACTIVE_ROOT = $(DOCKER) exec -it -u root $(PHP_CONTAINER)
|
EXEC_PHP_INTERACTIVE_ROOT = $(DOCKER) exec -it -u root $(PHP_CONTAINER)
|
||||||
FILES =
|
FILES =
|
||||||
|
DATA_SQL ?= data.sql
|
||||||
|
DATA_SQL_NORM ?= data_norm.sql
|
||||||
|
|
||||||
#========================================================================================
|
#========================================================================================
|
||||||
|
|
||||||
@@ -31,6 +33,11 @@ start: env-init
|
|||||||
@echo "**** START CONTAINERS ****"
|
@echo "**** START CONTAINERS ****"
|
||||||
@cp --update=none docker/.env.docker docker/.env.docker.local
|
@cp --update=none docker/.env.docker docker/.env.docker.local
|
||||||
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||||
|
@echo ""
|
||||||
|
@echo "URLs disponibles:"
|
||||||
|
@echo "- Symfony API: http://localhost:8081/api"
|
||||||
|
@echo "- Nuxt (Inventory_frontend): http://localhost:3001"
|
||||||
|
@echo "- pgAdmin: http://localhost:5050"
|
||||||
|
|
||||||
# Éteint le container
|
# Éteint le container
|
||||||
stop:
|
stop:
|
||||||
@@ -49,16 +56,16 @@ composer-install:
|
|||||||
$(EXEC_PHP) composer install
|
$(EXEC_PHP) composer install
|
||||||
|
|
||||||
build-nuxtJS:
|
build-nuxtJS:
|
||||||
# $(EXEC_PHP) cp -n frontend/.env.dist frontend/.env.local
|
# $(EXEC_PHP) cp -n Inventory_frontend/.env.dist Inventory_frontend/.env.local
|
||||||
$(EXEC_PHP) sh -lc "cd frontend && npm install && npm run build:dist"
|
$(EXEC_PHP) sh -lc "cd Inventory_frontend && npm install && npm run generate"
|
||||||
|
|
||||||
dev-nuxt:
|
dev-nuxt:
|
||||||
$(EXEC_PHP) sh -c "cd frontend && npm run dev"
|
$(EXEC_PHP) sh -lc "cd Inventory_frontend && npm run dev"
|
||||||
|
|
||||||
delete_built_dir:
|
delete_built_dir:
|
||||||
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||||
$(DOCKER) exec -u root $(PHP_CONTAINER) rm -rf vendor/
|
$(DOCKER) exec -u root $(PHP_CONTAINER) rm -rf vendor/
|
||||||
$(DOCKER) exec -u root $(PHP_CONTAINER) rm -rf frontend/node_modules
|
$(DOCKER) exec -u root $(PHP_CONTAINER) rm -rf Inventory_frontend/node_modules
|
||||||
|
|
||||||
remove_orphans:
|
remove_orphans:
|
||||||
$(DOCKER_COMPOSE) kill
|
$(DOCKER_COMPOSE) kill
|
||||||
@@ -85,6 +92,10 @@ db-restart:
|
|||||||
cache-clear:
|
cache-clear:
|
||||||
$(SYMFONY_CONSOLE) cache:clear
|
$(SYMFONY_CONSOLE) cache:clear
|
||||||
|
|
||||||
|
cache-clear-full:
|
||||||
|
$(SYMFONY_CONSOLE) cache:clear
|
||||||
|
$(EXEC_PHP) rm -rf var/cache/*
|
||||||
|
|
||||||
copy-git-hook:
|
copy-git-hook:
|
||||||
$(EXEC_PHP) cp pre-commit .git/hooks/
|
$(EXEC_PHP) cp pre-commit .git/hooks/
|
||||||
$(EXEC_PHP) cp commit-msg .git/hooks/
|
$(EXEC_PHP) cp commit-msg .git/hooks/
|
||||||
@@ -108,3 +119,31 @@ test:
|
|||||||
|
|
||||||
wait:
|
wait:
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|
||||||
|
# Normalize pgAdmin data-only dump and import into DB
|
||||||
|
import-data:
|
||||||
|
python3 scripts/normalize-dump.py $(DATA_SQL) $(DATA_SQL_NORM) --lower
|
||||||
|
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -v ON_ERROR_STOP=1 -c "SET session_replication_role = replica;"
|
||||||
|
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -v ON_ERROR_STOP=1 < $(DATA_SQL_NORM)
|
||||||
|
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -v ON_ERROR_STOP=1 -c "SET session_replication_role = DEFAULT;"
|
||||||
|
|
||||||
|
# Fixtures management
|
||||||
|
fixtures-dump:
|
||||||
|
@echo "Dumping current database to fixtures/data.sql..."
|
||||||
|
$(DOCKER_COMPOSE) exec -T db pg_dump -U $(POSTGRES_USER) -d $(POSTGRES_DB) \
|
||||||
|
--data-only --inserts --no-owner --no-privileges \
|
||||||
|
--exclude-table=doctrine_migration_versions \
|
||||||
|
| grep -v "^pg_dump:" | grep -v "^\\\\restrict" > fixtures/data.sql
|
||||||
|
@echo "Fixtures saved to fixtures/data.sql"
|
||||||
|
|
||||||
|
fixtures-load:
|
||||||
|
@echo "Loading fixtures from fixtures/data.sql (FK checks disabled)..."
|
||||||
|
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -c "SET session_replication_role = replica;"
|
||||||
|
-$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) < fixtures/data.sql
|
||||||
|
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -c "SET session_replication_role = DEFAULT;"
|
||||||
|
@echo "Fixtures loaded!"
|
||||||
|
|
||||||
|
fixtures-reset:
|
||||||
|
@echo "Resetting database and loading fixtures..."
|
||||||
|
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -c "DO \$$\$$ DECLARE r RECORD; BEGIN FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename != 'doctrine_migration_versions') LOOP EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE'; END LOOP; END \$$\$$;"
|
||||||
|
$(MAKE) fixtures-load
|
||||||
|
|||||||
35
migratebdd.md
Normal file
35
migratebdd.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# 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"
|
||||||
|
```
|
||||||
891
migrations/Version20260125143939.php
Normal file
891
migrations/Version20260125143939.php
Normal file
@@ -0,0 +1,891 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260125143939 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_f95a3199df92e79b') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_f95a3199df92e79b RENAME TO IDX_F95A3199CC8A4CEE';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_f95a3199a3fdb2a7') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_f95a3199a3fdb2a7 RENAME TO IDX_F95A319936799605';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql('ALTER TABLE _composantconstructeurs DROP CONSTRAINT IF EXISTS "_ComposantConstructeurs_A_fkey"');
|
||||||
|
$this->addSql('ALTER TABLE _composantconstructeurs DROP CONSTRAINT IF EXISTS "_ComposantConstructeurs_B_fkey"');
|
||||||
|
$this->addSql('ALTER TABLE _composantconstructeurs ALTER A TYPE VARCHAR(36)');
|
||||||
|
$this->addSql('ALTER TABLE _composantconstructeurs ALTER B TYPE VARCHAR(36)');
|
||||||
|
$this->addSql('ALTER TABLE _composantconstructeurs ADD CONSTRAINT FK_60760125D3D99E8B FOREIGN KEY (A) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE _composantconstructeurs ADD CONSTRAINT FK_607601254AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE _composantconstructeurs ADD PRIMARY KEY (A, B)');
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_5b97d813e8b7be43') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_5b97d813e8b7be43 RENAME TO IDX_60760125D3D99E8B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('_composantconstructeurs_b_index') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX _composantconstructeurs_b_index RENAME TO IDX_607601254AD0CF31';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_6b64d7ff6736d61') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_6b64d7ff6736d61 RENAME TO IDX_6B64D7FF5C4A705F';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_6b64d7fff6bae05f') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_6b64d7fff6bae05f RENAME TO IDX_6B64D7FF633EC4FD';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_6b64d7ffa1dac1c6') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_6b64d7ffa1dac1c6 RENAME TO IDX_6B64D7FF345EE564';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_6b64d7ff96428d73') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_6b64d7ff96428d73 RENAME TO IDX_6B64D7FF3C6A9D1';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_6b64d7ffa3fdb2a7') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_6b64d7ffa3fdb2a7 RENAME TO IDX_6B64D7FF36799605';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_4a48378c158582c3') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_4a48378c158582c3 RENAME TO IDX_4A48378C2F024C2';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_4a48378cdf92e79b') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_4a48378cdf92e79b RENAME TO IDX_4A48378CCC8A4CEE';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_4a48378c4ca601c8') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_4a48378c4ca601c8 RENAME TO IDX_4A48378C169F1CF6';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_4a48378c40c2d03b') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_4a48378c40c2d03b RENAME TO IDX_4A48378C57B7763A';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_a2b07288f6bae05f') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_a2b07288f6bae05f RENAME TO IDX_A2B07288633EC4FD';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_a2b07288a1dac1c6') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_a2b07288a1dac1c6 RENAME TO IDX_A2B07288345EE564';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_a2b0728896428d73') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_a2b0728896428d73 RENAME TO IDX_A2B072883C6A9D1';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_a2b07288a3fdb2a7') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_a2b07288a3fdb2a7 RENAME TO IDX_A2B0728836799605';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_a2b07288fcf7805f') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_a2b07288fcf7805f RENAME TO IDX_A2B072886973A4FD';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_528efe19f6bae05f') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_528efe19f6bae05f RENAME TO IDX_528EFE19633EC4FD';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_528efe19a1dac1c6') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_528efe19a1dac1c6 RENAME TO IDX_528EFE19345EE564';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_528efe197d44d2df') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_528efe197d44d2df RENAME TO IDX_528EFE19EF6CF34B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_528efe19bcced9e3') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_528efe19bcced9e3 RENAME TO IDX_528EFE19C44B383C';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_62941615f6bae05f') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_62941615f6bae05f RENAME TO IDX_62941615633EC4FD';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_6294161596428d73') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_6294161596428d73 RENAME TO IDX_629416153C6A9D1';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_629416157d44d2df') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_629416157d44d2df RENAME TO IDX_62941615EF6CF34B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_6294161532c54aaf') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_6294161532c54aaf RENAME TO IDX_62941615F957D314';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('machine_product_links_machineid_idx') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX machine_product_links_machineid_idx RENAME TO IDX_8CC32259633EC4FD';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('machine_product_links_productid_idx') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX machine_product_links_productid_idx RENAME TO IDX_8CC3225936799605';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_8cc32259357fdbff') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_8cc32259357fdbff RENAME TO IDX_8CC32259B590B209';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_8cc322597d44d2df') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_8cc322597d44d2df RENAME TO IDX_8CC32259EF6CF34B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_8cc32259bcd7dad6') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_8cc32259bcd7dad6 RENAME TO IDX_8CC32259A63AC5DC';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_8cc3225987ceb33f') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_8cc3225987ceb33f RENAME TO IDX_8CC32259937A1D7C';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_f1ce8dedfcf7805f') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_f1ce8dedfcf7805f RENAME TO IDX_F1CE8DED6973A4FD';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_f1ce8ded158582c3') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_f1ce8ded158582c3 RENAME TO IDX_F1CE8DED2F024C2';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql('ALTER TABLE _machineconstructeurs DROP CONSTRAINT IF EXISTS "_MachineConstructeurs_B_fkey"');
|
||||||
|
$this->addSql('ALTER TABLE _machineconstructeurs DROP CONSTRAINT IF EXISTS "_MachineConstructeurs_A_fkey"');
|
||||||
|
$this->addSql('ALTER TABLE _machineconstructeurs ALTER A TYPE VARCHAR(36)');
|
||||||
|
$this->addSql('ALTER TABLE _machineconstructeurs ALTER B TYPE VARCHAR(36)');
|
||||||
|
$this->addSql('ALTER TABLE _machineconstructeurs ADD CONSTRAINT FK_E6A040CCD3D99E8B FOREIGN KEY (A) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE _machineconstructeurs ADD CONSTRAINT FK_E6A040CC4AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE _machineconstructeurs ADD PRIMARY KEY (A, B)');
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_4f225b32e8b7be43') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_4f225b32e8b7be43 RENAME TO IDX_E6A040CCD3D99E8B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('_machineconstructeurs_b_index') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX _machineconstructeurs_b_index RENAME TO IDX_E6A040CC4AD0CF31';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql('ALTER TABLE model_types DROP CONSTRAINT IF EXISTS "ModelType_category_name_key"');
|
||||||
|
$this->addSql('ALTER TABLE model_types DROP CONSTRAINT IF EXISTS "ModelType_code_key"');
|
||||||
|
$this->addSql('ALTER TABLE model_types ALTER id TYPE VARCHAR(36)');
|
||||||
|
$this->addSql('ALTER TABLE model_types ALTER category TYPE VARCHAR(255)');
|
||||||
|
$this->addSql('ALTER TABLE model_types ALTER createdAt DROP DEFAULT');
|
||||||
|
$this->addSql('ALTER TABLE model_types ALTER componentSkeleton TYPE JSON');
|
||||||
|
$this->addSql('ALTER TABLE model_types ALTER pieceSkeleton TYPE JSON');
|
||||||
|
$this->addSql('ALTER TABLE model_types ALTER productSkeleton TYPE JSON');
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_b92d74724ca601c8') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_b92d74724ca601c8 RENAME TO IDX_B92D7472169F1CF6';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_b92d7472a3fdb2a7') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_b92d7472a3fdb2a7 RENAME TO IDX_B92D747236799605';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql('ALTER TABLE _piececonstructeurs DROP CONSTRAINT IF EXISTS "_PieceConstructeurs_A_fkey"');
|
||||||
|
$this->addSql('ALTER TABLE _piececonstructeurs DROP CONSTRAINT IF EXISTS "_PieceConstructeurs_B_fkey"');
|
||||||
|
$this->addSql('ALTER TABLE _piececonstructeurs ALTER A TYPE VARCHAR(36)');
|
||||||
|
$this->addSql('ALTER TABLE _piececonstructeurs ALTER B TYPE VARCHAR(36)');
|
||||||
|
$this->addSql('ALTER TABLE _piececonstructeurs ADD CONSTRAINT FK_E94732E5D3D99E8B FOREIGN KEY (A) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE _piececonstructeurs ADD CONSTRAINT FK_E94732E54AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE _piececonstructeurs ADD PRIMARY KEY (A, B)');
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_77fc120e8b7be43') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_77fc120e8b7be43 RENAME TO IDX_E94732E5D3D99E8B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('_piececonstructeurs_b_index') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX _piececonstructeurs_b_index RENAME TO IDX_E94732E54AD0CF31';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_b3ba5a5a40c2d03b') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_b3ba5a5a40c2d03b RENAME TO IDX_B3BA5A5A57B7763A';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql('ALTER TABLE _productconstructeurs DROP CONSTRAINT IF EXISTS "_ProductConstructeurs_B_fkey"');
|
||||||
|
$this->addSql('ALTER TABLE _productconstructeurs DROP CONSTRAINT IF EXISTS "_ProductConstructeurs_A_fkey"');
|
||||||
|
$this->addSql('ALTER TABLE _productconstructeurs ALTER A TYPE VARCHAR(36)');
|
||||||
|
$this->addSql('ALTER TABLE _productconstructeurs ALTER B TYPE VARCHAR(36)');
|
||||||
|
// Clean orphaned relations before re-adding foreign keys.
|
||||||
|
$this->addSql('DELETE FROM _productconstructeurs WHERE A IS NULL OR B IS NULL');
|
||||||
|
$this->addSql('DELETE FROM _productconstructeurs pc WHERE NOT EXISTS (SELECT 1 FROM products p WHERE p.id = pc.A)');
|
||||||
|
$this->addSql('DELETE FROM _productconstructeurs pc WHERE NOT EXISTS (SELECT 1 FROM constructeurs c WHERE c.id = pc.B)');
|
||||||
|
$this->addSql('ALTER TABLE _productconstructeurs ADD CONSTRAINT FK_CF7403FCD3D99E8B FOREIGN KEY (A) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE _productconstructeurs ADD CONSTRAINT FK_CF7403FC4AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE _productconstructeurs ADD PRIMARY KEY (A, B)');
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_66f61802e8b7be43') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_66f61802e8b7be43 RENAME TO IDX_CF7403FCD3D99E8B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('_productconstructeurs_b_index') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX _productconstructeurs_b_index RENAME TO IDX_CF7403FC4AD0CF31';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql('DROP INDEX IF EXISTS uniq_profiles_email');
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_96958790158582c3') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_96958790158582c3 RENAME TO IDX_969587902F024C2';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_96958790df92e79b') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_96958790df92e79b RENAME TO IDX_96958790CC8A4CEE';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_f609e59e158582c3') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_f609e59e158582c3 RENAME TO IDX_F609E59E2F024C2';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_f609e59e4ca601c8') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_f609e59e4ca601c8 RENAME TO IDX_F609E59E169F1CF6';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_29a51f98158582c3') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_29a51f98158582c3 RENAME TO IDX_29A51F982F024C2';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_29a51f9840c2d03b') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_29a51f9840c2d03b RENAME TO IDX_29A51F9857B7763A';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT IF EXISTS FK_60760125D3D99E8B');
|
||||||
|
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT IF EXISTS FK_607601254AD0CF31');
|
||||||
|
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT IF EXISTS _ComposantConstructeurs_pkey');
|
||||||
|
$this->addSql('ALTER TABLE _ComposantConstructeurs ALTER a TYPE TEXT');
|
||||||
|
$this->addSql('ALTER TABLE _ComposantConstructeurs ALTER b TYPE TEXT');
|
||||||
|
$this->addSql('ALTER TABLE _ComposantConstructeurs ADD CONSTRAINT "_ComposantConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES composants (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE _ComposantConstructeurs ADD CONSTRAINT "_ComposantConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_607601254ad0cf31') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_607601254ad0cf31 RENAME TO "_ComposantConstructeurs_B_index"';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_60760125d3d99e8b') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_60760125d3d99e8b RENAME TO IDX_5B97D813E8B7BE43';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT IF EXISTS FK_E6A040CCD3D99E8B');
|
||||||
|
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT IF EXISTS FK_E6A040CC4AD0CF31');
|
||||||
|
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT IF EXISTS _MachineConstructeurs_pkey');
|
||||||
|
$this->addSql('ALTER TABLE _MachineConstructeurs ALTER a TYPE TEXT');
|
||||||
|
$this->addSql('ALTER TABLE _MachineConstructeurs ALTER b TYPE TEXT');
|
||||||
|
$this->addSql('ALTER TABLE _MachineConstructeurs ADD CONSTRAINT "_MachineConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE _MachineConstructeurs ADD CONSTRAINT "_MachineConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES machines (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_e6a040cc4ad0cf31') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_e6a040cc4ad0cf31 RENAME TO "_MachineConstructeurs_B_index"';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_e6a040ccd3d99e8b') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_e6a040ccd3d99e8b RENAME TO IDX_4F225B32E8B7BE43';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT IF EXISTS FK_E94732E5D3D99E8B');
|
||||||
|
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT IF EXISTS FK_E94732E54AD0CF31');
|
||||||
|
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT IF EXISTS _PieceConstructeurs_pkey');
|
||||||
|
$this->addSql('ALTER TABLE _PieceConstructeurs ALTER a TYPE TEXT');
|
||||||
|
$this->addSql('ALTER TABLE _PieceConstructeurs ALTER b TYPE TEXT');
|
||||||
|
$this->addSql('ALTER TABLE _PieceConstructeurs ADD CONSTRAINT "_PieceConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES pieces (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE _PieceConstructeurs ADD CONSTRAINT "_PieceConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_e94732e54ad0cf31') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_e94732e54ad0cf31 RENAME TO "_PieceConstructeurs_B_index"';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_e94732e5d3d99e8b') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_e94732e5d3d99e8b RENAME TO IDX_77FC120E8B7BE43';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT IF EXISTS FK_CF7403FCD3D99E8B');
|
||||||
|
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT IF EXISTS FK_CF7403FC4AD0CF31');
|
||||||
|
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT IF EXISTS _ProductConstructeurs_pkey');
|
||||||
|
$this->addSql('ALTER TABLE _ProductConstructeurs ALTER a TYPE TEXT');
|
||||||
|
$this->addSql('ALTER TABLE _ProductConstructeurs ALTER b TYPE TEXT');
|
||||||
|
$this->addSql('ALTER TABLE _ProductConstructeurs ADD CONSTRAINT "_ProductConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES products (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE _ProductConstructeurs ADD CONSTRAINT "_ProductConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_cf7403fc4ad0cf31') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_cf7403fc4ad0cf31 RENAME TO "_ProductConstructeurs_B_index"';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_cf7403fcd3d99e8b') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_cf7403fcd3d99e8b RENAME TO IDX_66F61802E8B7BE43';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_f95a319936799605') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_f95a319936799605 RENAME TO IDX_F95A3199A3FDB2A7';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_f95a3199cc8a4cee') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_f95a3199cc8a4cee RENAME TO IDX_F95A3199DF92E79B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_6b64d7ff345ee564') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_6b64d7ff345ee564 RENAME TO IDX_6B64D7FFA1DAC1C6';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_6b64d7ff5c4a705f') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_6b64d7ff5c4a705f RENAME TO IDX_6B64D7FF6736D61';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_6b64d7ff633ec4fd') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_6b64d7ff633ec4fd RENAME TO IDX_6B64D7FFF6BAE05F';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_6b64d7ff3c6a9d1') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_6b64d7ff3c6a9d1 RENAME TO IDX_6B64D7FF96428D73';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_6b64d7ff36799605') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_6b64d7ff36799605 RENAME TO IDX_6B64D7FFA3FDB2A7';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_4a48378c57b7763a') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_4a48378c57b7763a RENAME TO IDX_4A48378C40C2D03B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_4a48378c2f024c2') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_4a48378c2f024c2 RENAME TO IDX_4A48378C158582C3';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_4a48378c169f1cf6') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_4a48378c169f1cf6 RENAME TO IDX_4A48378C4CA601C8';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_4a48378ccc8a4cee') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_4a48378ccc8a4cee RENAME TO IDX_4A48378CDF92E79B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_a2b07288345ee564') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_a2b07288345ee564 RENAME TO IDX_A2B07288A1DAC1C6';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_a2b07288633ec4fd') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_a2b07288633ec4fd RENAME TO IDX_A2B07288F6BAE05F';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_a2b072886973a4fd') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_a2b072886973a4fd RENAME TO IDX_A2B07288FCF7805F';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_a2b072883c6a9d1') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_a2b072883c6a9d1 RENAME TO IDX_A2B0728896428D73';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_a2b0728836799605') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_a2b0728836799605 RENAME TO IDX_A2B07288A3FDB2A7';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_528efe19345ee564') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_528efe19345ee564 RENAME TO IDX_528EFE19A1DAC1C6';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_528efe19633ec4fd') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_528efe19633ec4fd RENAME TO IDX_528EFE19F6BAE05F';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_528efe19ef6cf34b') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_528efe19ef6cf34b RENAME TO IDX_528EFE197D44D2DF';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_528efe19c44b383c') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_528efe19c44b383c RENAME TO IDX_528EFE19BCCED9E3';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_62941615ef6cf34b') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_62941615ef6cf34b RENAME TO IDX_629416157D44D2DF';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_62941615633ec4fd') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_62941615633ec4fd RENAME TO IDX_62941615F6BAE05F';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_629416153c6a9d1') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_629416153c6a9d1 RENAME TO IDX_6294161596428D73';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_62941615f957d314') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_62941615f957d314 RENAME TO IDX_6294161532C54AAF';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_8cc32259633ec4fd') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_8cc32259633ec4fd RENAME TO "machine_product_links_machineId_idx"';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_8cc3225936799605') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_8cc3225936799605 RENAME TO "machine_product_links_productId_idx"';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_8cc32259ef6cf34b') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_8cc32259ef6cf34b RENAME TO IDX_8CC322597D44D2DF';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_8cc32259b590b209') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_8cc32259b590b209 RENAME TO IDX_8CC32259357FDBFF';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_8cc32259a63ac5dc') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_8cc32259a63ac5dc RENAME TO IDX_8CC32259BCD7DAD6';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_8cc32259937a1d7c') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_8cc32259937a1d7c RENAME TO IDX_8CC3225987CEB33F';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_f1ce8ded2f024c2') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_f1ce8ded2f024c2 RENAME TO IDX_F1CE8DED158582C3';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_f1ce8ded6973a4fd') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_f1ce8ded6973a4fd RENAME TO IDX_F1CE8DEDFCF7805F';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql('ALTER TABLE model_types ALTER id TYPE TEXT');
|
||||||
|
$this->addSql('ALTER TABLE model_types ALTER category TYPE VARCHAR');
|
||||||
|
$this->addSql('ALTER TABLE model_types ALTER componentskeleton TYPE JSONB');
|
||||||
|
$this->addSql('ALTER TABLE model_types ALTER pieceskeleton TYPE JSONB');
|
||||||
|
$this->addSql('ALTER TABLE model_types ALTER productskeleton TYPE JSONB');
|
||||||
|
$this->addSql('ALTER TABLE model_types ALTER createdat SET DEFAULT CURRENT_TIMESTAMP');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX "ModelType_category_name_key" ON model_types (category, name)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX "ModelType_code_key" ON model_types (code)');
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_b92d7472169f1cf6') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_b92d7472169f1cf6 RENAME TO IDX_B92D74724CA601C8';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_b92d747236799605') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_b92d747236799605 RENAME TO IDX_B92D7472A3FDB2A7';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_b3ba5a5a57b7763a') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_b3ba5a5a57b7763a RENAME TO IDX_B3BA5A5A40C2D03B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_profiles_email ON profiles (email)');
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_969587902f024c2') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_969587902f024c2 RENAME TO IDX_96958790158582C3';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_96958790cc8a4cee') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_96958790cc8a4cee RENAME TO IDX_96958790DF92E79B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_f609e59e169f1cf6') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_f609e59e169f1cf6 RENAME TO IDX_F609E59E4CA601C8';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_f609e59e2f024c2') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_f609e59e2f024c2 RENAME TO IDX_F609E59E158582C3';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_29a51f9857b7763a') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_29a51f9857b7763a RENAME TO IDX_29A51F9840C2D03B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_29a51f982f024c2') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_29a51f982f024c2 RENAME TO IDX_29A51F98158582C3';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
migrations/Version20260125170000.php
Normal file
41
migrations/Version20260125170000.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260125170000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add audit_logs table to store per-entity history entries.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
CREATE TABLE audit_logs (
|
||||||
|
id VARCHAR(36) NOT NULL,
|
||||||
|
entityType VARCHAR(50) NOT NULL,
|
||||||
|
entityId VARCHAR(36) NOT NULL,
|
||||||
|
action VARCHAR(20) NOT NULL,
|
||||||
|
diff JSON DEFAULT NULL,
|
||||||
|
snapshot JSON DEFAULT NULL,
|
||||||
|
actorProfileId VARCHAR(36) DEFAULT NULL,
|
||||||
|
createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||||
|
PRIMARY KEY(id)
|
||||||
|
)
|
||||||
|
SQL);
|
||||||
|
|
||||||
|
$this->addSql('CREATE INDEX idx_audit_entity ON audit_logs (entityType, entityId)');
|
||||||
|
$this->addSql('CREATE INDEX idx_audit_created_at ON audit_logs (createdAt)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE audit_logs');
|
||||||
|
}
|
||||||
|
}
|
||||||
51
migrations/Version20260302103003.php
Normal file
51
migrations/Version20260302103003.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260302103003 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create comments table + make piece reference unique instead of name';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// Comments table (IF NOT EXISTS in case first attempt partially succeeded)
|
||||||
|
$this->addSql('CREATE TABLE IF NOT EXISTS comments (id VARCHAR(36) NOT NULL, content TEXT NOT NULL, entity_type VARCHAR(50) NOT NULL, entity_id VARCHAR(36) NOT NULL, entity_name VARCHAR(255) DEFAULT NULL, author_id VARCHAR(36) NOT NULL, author_name VARCHAR(255) NOT NULL, status VARCHAR(20) NOT NULL, resolved_by_id VARCHAR(36) DEFAULT NULL, resolved_by_name VARCHAR(255) DEFAULT NULL, resolved_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comment_entity_status ON comments (entity_type, entity_id, status)');
|
||||||
|
$this->addSql('COMMENT ON COLUMN comments.resolved_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN comments.created_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN comments.updated_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
|
||||||
|
// Piece: remove unique constraint on name (it's a constraint, not just an index)
|
||||||
|
$this->addSql('ALTER TABLE pieces DROP CONSTRAINT IF EXISTS uniq_b92d74725e237e06');
|
||||||
|
|
||||||
|
// Deduplicate piece references before adding unique constraint
|
||||||
|
$this->addSql("
|
||||||
|
UPDATE pieces p
|
||||||
|
SET reference = p.reference || '-' || LEFT(p.id, 6)
|
||||||
|
FROM (
|
||||||
|
SELECT id, reference,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY reference ORDER BY createdat) AS rn
|
||||||
|
FROM pieces
|
||||||
|
WHERE reference IS NOT NULL AND reference != ''
|
||||||
|
) dup
|
||||||
|
WHERE p.id = dup.id AND dup.rn > 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS uniq_pieces_reference ON pieces (reference)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE IF EXISTS comments');
|
||||||
|
$this->addSql('DROP INDEX IF EXISTS uniq_pieces_reference');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_b92d74725e237e06 ON pieces (name)');
|
||||||
|
}
|
||||||
|
}
|
||||||
28
migrations/Version20260302120000.php
Normal file
28
migrations/Version20260302120000.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260302120000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add description column to pieces and composants tables';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE pieces ADD COLUMN IF NOT EXISTS description TEXT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE composants ADD COLUMN IF NOT EXISTS description TEXT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE pieces DROP COLUMN IF EXISTS description');
|
||||||
|
$this->addSql('ALTER TABLE composants DROP COLUMN IF EXISTS description');
|
||||||
|
}
|
||||||
|
}
|
||||||
70
public/.htaccess
Normal file
70
public/.htaccess
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Use the front controller as index file. It serves as a fallback solution when
|
||||||
|
# every other rewrite/redirect fails (e.g. in an aliased environment without
|
||||||
|
# mod_rewrite). Additionally, this reduces the matching process for the
|
||||||
|
# start page (path "/") because otherwise Apache will apply the rewriting rules
|
||||||
|
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
|
||||||
|
DirectoryIndex index.php
|
||||||
|
|
||||||
|
# By default, Apache does not evaluate symbolic links if you did not enable this
|
||||||
|
# feature in your server configuration. Uncomment the following line if you
|
||||||
|
# install assets as symlinks or if you experience problems related to symlinks
|
||||||
|
# when compiling LESS/Sass/CoffeeScript assets.
|
||||||
|
# Options +FollowSymlinks
|
||||||
|
|
||||||
|
# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve
|
||||||
|
# to the front controller "/index.php" but be rewritten to "/index.php/index".
|
||||||
|
<IfModule mod_negotiation.c>
|
||||||
|
Options -MultiViews
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
# This Option needs to be enabled for RewriteRule, otherwise it will show an error like
|
||||||
|
# 'Options FollowSymLinks or SymLinksIfOwnerMatch is off which implies that RewriteRule directive is forbidden'
|
||||||
|
Options +FollowSymlinks
|
||||||
|
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Determine the RewriteBase automatically and set it as environment variable.
|
||||||
|
# If you are using Apache aliases to do mass virtual hosting or installed the
|
||||||
|
# project in a subdirectory, the base path will be prepended to allow proper
|
||||||
|
# resolution of the index.php file and to redirect to the correct URI. It will
|
||||||
|
# work in environments without path prefix as well, providing a safe, one-size
|
||||||
|
# fits all solution. But as you do not need it in this case, you can comment
|
||||||
|
# the following 2 lines to eliminate the overhead.
|
||||||
|
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
|
||||||
|
RewriteRule .* - [E=BASE:%1]
|
||||||
|
|
||||||
|
# Sets the HTTP_AUTHORIZATION header removed by Apache
|
||||||
|
RewriteCond %{HTTP:Authorization} .+
|
||||||
|
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
|
||||||
|
|
||||||
|
# Redirect to URI without front controller to prevent duplicate content
|
||||||
|
# (with and without `/index.php`). Only do this redirect on the initial
|
||||||
|
# rewrite by Apache and not on subsequent cycles. Otherwise we would get an
|
||||||
|
# endless redirect loop (request -> rewrite to front controller ->
|
||||||
|
# redirect to URI without front controller -> request -> ...).
|
||||||
|
# So in case you get a "too many redirects" error or you always get redirected
|
||||||
|
# to the start page because your Apache does not expose the REDIRECT_STATUS
|
||||||
|
# environment variable, you have 2 choices:
|
||||||
|
# - disable this feature by commenting the following 2 lines or
|
||||||
|
# - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
|
||||||
|
# following RewriteCond (best solution)
|
||||||
|
RewriteCond %{ENV:REDIRECT_STATUS} =""
|
||||||
|
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
|
||||||
|
|
||||||
|
# If the requested filename exists, simply serve it.
|
||||||
|
# We only want to let Apache serve files and not directories.
|
||||||
|
# Rewrite all other queries to the front controller.
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^ %{ENV:BASE}/index.php [L]
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule !mod_rewrite.c>
|
||||||
|
<IfModule mod_alias.c>
|
||||||
|
# When mod_rewrite is not available, we instruct a temporary redirect of
|
||||||
|
# the start page to the front controller explicitly so that the website
|
||||||
|
# and the generated links can still be used.
|
||||||
|
RedirectMatch 307 ^/$ /index.php/
|
||||||
|
# RedirectTemp cannot be used instead
|
||||||
|
</IfModule>
|
||||||
|
</IfModule>
|
||||||
5
scripts/insert_profiles.sql
Normal file
5
scripts/insert_profiles.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
INSERT INTO public.profiles (id, firstname, lastname, email, isactive, createdat, updatedat)
|
||||||
|
VALUES
|
||||||
|
('admin-default-profile', 'Admin', 'General', 'admin@admin.fr', true, '2025-09-23 13:09:47.804', '2025-09-23 13:09:47.804'),
|
||||||
|
('cmhab2j3x003g47v77xhnm1ff', 'Elodie', 'Souriau', 'elodie@gg.fr', true, '2025-10-28 08:29:25.437', '2025-10-28 08:29:25.437')
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
32
scripts/lowercase-columns.sql
Normal file
32
scripts/lowercase-columns.sql
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
r RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN
|
||||||
|
SELECT table_schema, table_name, column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND column_name <> lower(column_name)
|
||||||
|
ORDER BY table_name, column_name
|
||||||
|
LOOP
|
||||||
|
-- Skip if a lowercase version already exists to avoid collisions.
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns c
|
||||||
|
WHERE c.table_schema = r.table_schema
|
||||||
|
AND c.table_name = r.table_name
|
||||||
|
AND c.column_name = lower(r.column_name)
|
||||||
|
) THEN
|
||||||
|
RAISE NOTICE 'Skip %.%: % -> % (target exists)', r.table_name, r.column_name, r.column_name, lower(r.column_name);
|
||||||
|
ELSE
|
||||||
|
EXECUTE format(
|
||||||
|
'ALTER TABLE %I.%I RENAME COLUMN %I TO %I',
|
||||||
|
r.table_schema,
|
||||||
|
r.table_name,
|
||||||
|
r.column_name,
|
||||||
|
lower(r.column_name)
|
||||||
|
);
|
||||||
|
RAISE NOTICE 'Renamed %.%: % -> %', r.table_name, r.column_name, r.column_name, lower(r.column_name);
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
45
scripts/migrate-inventory-data.sh
Executable file
45
scripts/migrate-inventory-data.sh
Executable file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SOURCE_DB="${SOURCE_DB:-inventory_data}"
|
||||||
|
TARGET_DB="${TARGET_DB:-inventory}"
|
||||||
|
PGHOST="${PGHOST:-localhost}"
|
||||||
|
PGPORT="${PGPORT:-5433}"
|
||||||
|
PGUSER="${PGUSER:-postgres}"
|
||||||
|
PGPASSWORD="${PGPASSWORD:-postgres}"
|
||||||
|
DUMP_FILE="${DUMP_FILE:-/tmp/inventory_data_dump.sql}"
|
||||||
|
|
||||||
|
export PGPASSWORD
|
||||||
|
|
||||||
|
EXCLUDE_TABLES=(
|
||||||
|
"doctrine_migration_versions"
|
||||||
|
"migration_versions"
|
||||||
|
"_prisma_migrations"
|
||||||
|
"profiles"
|
||||||
|
)
|
||||||
|
|
||||||
|
EXCLUDE_ARGS=()
|
||||||
|
for table in "${EXCLUDE_TABLES[@]}"; do
|
||||||
|
EXCLUDE_ARGS+=(--exclude-table-data="$table")
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Dumping data from ${SOURCE_DB}..."
|
||||||
|
pg_dump \
|
||||||
|
--data-only \
|
||||||
|
--inserts \
|
||||||
|
--no-owner \
|
||||||
|
--no-privileges \
|
||||||
|
"${EXCLUDE_ARGS[@]}" \
|
||||||
|
-h "${PGHOST}" \
|
||||||
|
-p "${PGPORT}" \
|
||||||
|
-U "${PGUSER}" \
|
||||||
|
"${SOURCE_DB}" > "${DUMP_FILE}"
|
||||||
|
|
||||||
|
echo "Restoring data into ${TARGET_DB}..."
|
||||||
|
psql \
|
||||||
|
-h "${PGHOST}" \
|
||||||
|
-p "${PGPORT}" \
|
||||||
|
-U "${PGUSER}" \
|
||||||
|
"${TARGET_DB}" < "${DUMP_FILE}"
|
||||||
|
|
||||||
|
echo "Done. Data copied from ${SOURCE_DB} to ${TARGET_DB}."
|
||||||
177
scripts/normalize-dump.py
Normal file
177
scripts/normalize-dump.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
INSERT_RE = re.compile(
|
||||||
|
r"(?P<prefix>INSERT\s+INTO\s+[^;]*?\()(?P<cols>[^)]*)(?P<suffix>\)\s+VALUES)",
|
||||||
|
re.IGNORECASE | re.DOTALL,
|
||||||
|
)
|
||||||
|
TABLE_RE = re.compile(
|
||||||
|
r"(?P<before>INSERT\s+INTO\s+)(?P<table>(?:\"[^\"]+\"\.|[A-Za-z_][\w$]*\.)?\"[^\"]+\"|(?:\"[^\"]+\"\.|[A-Za-z_][\w$]*\.)?[A-Za-z_][\w$]*)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
CREATE_DB_RE = re.compile(r"^CREATE\s+DATABASE\s+.+?;$", re.IGNORECASE | re.MULTILINE)
|
||||||
|
CONNECT_RE = re.compile(r"^\\connect\\s+.+?$", re.IGNORECASE | re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
TABLE_NAME_MAP = {
|
||||||
|
"ModelType": "model_types",
|
||||||
|
"TypeMachine": "type_machines",
|
||||||
|
"TypeMachineComponentRequirement": "type_machine_component_requirements",
|
||||||
|
"TypeMachinePieceRequirement": "type_machine_piece_requirements",
|
||||||
|
"TypeMachineProductRequirement": "type_machine_product_requirements",
|
||||||
|
"MachinePieceLink": "machine_piece_links",
|
||||||
|
"MachineComponentLink": "machine_component_links",
|
||||||
|
"MachineProductLink": "machine_product_links",
|
||||||
|
"Machine": "machines",
|
||||||
|
"Product": "products",
|
||||||
|
"Piece": "pieces",
|
||||||
|
"Composant": "composants",
|
||||||
|
"Profile": "profiles",
|
||||||
|
"CustomField": "custom_fields",
|
||||||
|
"CustomFieldValue": "custom_field_values",
|
||||||
|
"Document": "documents",
|
||||||
|
"Constructeur": "constructeurs",
|
||||||
|
"Site": "sites",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SKIP_TABLES = {
|
||||||
|
"_prisma_migrations",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def to_snake(name: str) -> str:
|
||||||
|
out = []
|
||||||
|
length = len(name)
|
||||||
|
for i, ch in enumerate(name):
|
||||||
|
if ch.isupper():
|
||||||
|
prev = name[i - 1] if i > 0 else ""
|
||||||
|
nxt = name[i + 1] if i + 1 < length else ""
|
||||||
|
if i > 0 and (prev.islower() or prev.isdigit() or (prev.isupper() and nxt.islower())):
|
||||||
|
out.append("_")
|
||||||
|
out.append(ch.lower())
|
||||||
|
else:
|
||||||
|
out.append(ch)
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def to_lower_compact(name: str) -> str:
|
||||||
|
return name.replace("_", "").lower()
|
||||||
|
|
||||||
|
|
||||||
|
def remap_table(ident: str, mode: str) -> str:
|
||||||
|
mapped = TABLE_NAME_MAP.get(ident)
|
||||||
|
if mapped is not None:
|
||||||
|
return mapped
|
||||||
|
if mode == "snake":
|
||||||
|
return to_snake(ident)
|
||||||
|
if mode == "lower":
|
||||||
|
return to_lower_compact(ident)
|
||||||
|
raise ValueError(f"Unsupported mode: {mode}")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_table_ident(prefix: str) -> str | None:
|
||||||
|
match = TABLE_RE.search(prefix)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
table = match.group("table")
|
||||||
|
# Handle quoted schema like "public"."TableName"
|
||||||
|
if '"."' in table:
|
||||||
|
parts = table.split('"."', 1)
|
||||||
|
ident = parts[1].strip('"')
|
||||||
|
elif "." in table:
|
||||||
|
_, ident = table.split(".", 1)
|
||||||
|
ident = ident.strip('"')
|
||||||
|
else:
|
||||||
|
ident = table.strip('"')
|
||||||
|
return ident
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_table_name(prefix: str, mode: str) -> str:
|
||||||
|
def repl(match: re.Match[str]) -> str:
|
||||||
|
table = match.group("table")
|
||||||
|
schema = ""
|
||||||
|
ident = table
|
||||||
|
# Handle quoted schema like "public"."TableName"
|
||||||
|
if '"."' in table:
|
||||||
|
parts = table.split('"."', 1)
|
||||||
|
schema_name = parts[0].strip('"')
|
||||||
|
ident = parts[1].strip('"')
|
||||||
|
schema = f'"{schema_name}".'
|
||||||
|
elif "." in table:
|
||||||
|
schema_part, ident = table.split(".", 1)
|
||||||
|
schema_name = schema_part.strip('"')
|
||||||
|
schema = f'"{schema_name}".'
|
||||||
|
ident = ident.strip('"')
|
||||||
|
else:
|
||||||
|
ident = table.strip('"')
|
||||||
|
mapped = remap_table(ident, mode)
|
||||||
|
return f'{match.group("before")}{schema}"{mapped}"'
|
||||||
|
|
||||||
|
return TABLE_RE.sub(repl, prefix)
|
||||||
|
|
||||||
|
|
||||||
|
def remap_columns(cols: str, mode: str) -> str:
|
||||||
|
def repl(match: re.Match[str]) -> str:
|
||||||
|
name = match.group(1)
|
||||||
|
if mode == "snake":
|
||||||
|
if any(ch.isupper() for ch in name):
|
||||||
|
return f"\"{to_snake(name)}\""
|
||||||
|
return match.group(0)
|
||||||
|
if mode == "lower":
|
||||||
|
return f"\"{to_lower_compact(name)}\""
|
||||||
|
raise ValueError(f"Unsupported mode: {mode}")
|
||||||
|
|
||||||
|
return re.sub(r"\"([^\"]+)\"", repl, cols)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_dump(sql: str, mode: str) -> str:
|
||||||
|
sql = CREATE_DB_RE.sub("", sql)
|
||||||
|
sql = CONNECT_RE.sub("", sql)
|
||||||
|
|
||||||
|
def repl(match: re.Match[str]) -> str:
|
||||||
|
raw_prefix = match.group("prefix")
|
||||||
|
ident = extract_table_ident(raw_prefix)
|
||||||
|
if ident is not None:
|
||||||
|
mapped = remap_table(ident, mode)
|
||||||
|
if ident in SKIP_TABLES or mapped in SKIP_TABLES:
|
||||||
|
return ""
|
||||||
|
prefix = normalize_table_name(raw_prefix, mode)
|
||||||
|
cols = remap_columns(match.group("cols"), mode)
|
||||||
|
return f"{prefix}{cols}{match.group('suffix')}"
|
||||||
|
|
||||||
|
return INSERT_RE.sub(repl, sql)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) not in (3, 4):
|
||||||
|
print("Usage: scripts/normalize-dump.py <input.sql> <output.sql> [--snake|--lower]", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
src = sys.argv[1]
|
||||||
|
dst = sys.argv[2]
|
||||||
|
mode = "lower"
|
||||||
|
if len(sys.argv) == 4:
|
||||||
|
if sys.argv[3] == "--snake":
|
||||||
|
mode = "snake"
|
||||||
|
elif sys.argv[3] == "--lower":
|
||||||
|
mode = "lower"
|
||||||
|
else:
|
||||||
|
print("Invalid mode. Use --snake or --lower.", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
with open(src, "r", encoding="utf-8") as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
normalized = normalize_dump(data, mode)
|
||||||
|
|
||||||
|
with open(dst, "w", encoding="utf-8") as f:
|
||||||
|
f.write(normalized)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
195
scripts/release.sh
Executable file
195
scripts/release.sh
Executable file
@@ -0,0 +1,195 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Couleurs pour l'affichage
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Répertoire racine du projet
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
VERSION_FILE="$PROJECT_ROOT/VERSION"
|
||||||
|
API_PLATFORM_FILE="$PROJECT_ROOT/config/packages/api_platform.yaml"
|
||||||
|
FRONTEND_DIR="$PROJECT_ROOT/Inventory_frontend"
|
||||||
|
|
||||||
|
# Lire la version actuelle
|
||||||
|
current_version=$(cat "$VERSION_FILE" | tr -d '\n')
|
||||||
|
|
||||||
|
# Fonction pour afficher l'aide
|
||||||
|
show_help() {
|
||||||
|
echo -e "${BLUE}Usage:${NC} $0 [version|bump_type]"
|
||||||
|
echo ""
|
||||||
|
echo "Arguments:"
|
||||||
|
echo " version Version spécifique (ex: 1.2.3)"
|
||||||
|
echo " bump_type Type de bump: major, minor, patch"
|
||||||
|
echo ""
|
||||||
|
echo "Exemples:"
|
||||||
|
echo " $0 1.0.0 # Définit la version à 1.0.0"
|
||||||
|
echo " $0 patch # 1.0.0 -> 1.0.1"
|
||||||
|
echo " $0 minor # 1.0.0 -> 1.1.0"
|
||||||
|
echo " $0 major # 1.0.0 -> 2.0.0"
|
||||||
|
echo ""
|
||||||
|
echo -e "Version actuelle: ${GREEN}$current_version${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour bumper la version
|
||||||
|
bump_version() {
|
||||||
|
local version=$1
|
||||||
|
local bump_type=$2
|
||||||
|
|
||||||
|
IFS='.' read -r major minor patch <<< "$version"
|
||||||
|
|
||||||
|
case $bump_type in
|
||||||
|
major)
|
||||||
|
major=$((major + 1))
|
||||||
|
minor=0
|
||||||
|
patch=0
|
||||||
|
;;
|
||||||
|
minor)
|
||||||
|
minor=$((minor + 1))
|
||||||
|
patch=0
|
||||||
|
;;
|
||||||
|
patch)
|
||||||
|
patch=$((patch + 1))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "$major.$minor.$patch"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vérifier les arguments
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
arg=$1
|
||||||
|
|
||||||
|
# Déterminer la nouvelle version
|
||||||
|
case $arg in
|
||||||
|
major|minor|patch)
|
||||||
|
new_version=$(bump_version "$current_version" "$arg")
|
||||||
|
;;
|
||||||
|
-h|--help|help)
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Vérifier le format de version (semver basique)
|
||||||
|
if [[ $arg =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
new_version=$arg
|
||||||
|
else
|
||||||
|
echo -e "${RED}Erreur:${NC} Format de version invalide: $arg"
|
||||||
|
echo "Utilisez le format X.Y.Z (ex: 1.2.3) ou major/minor/patch"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo -e "${BLUE}Release v$new_version${NC}"
|
||||||
|
echo "================================"
|
||||||
|
echo -e "Version actuelle: ${YELLOW}$current_version${NC}"
|
||||||
|
echo -e "Nouvelle version: ${GREEN}$new_version${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Vérifier qu'on n'est pas sur une version identique
|
||||||
|
if [ "$current_version" = "$new_version" ]; then
|
||||||
|
echo -e "${YELLOW}La version est déjà $new_version${NC}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Demander confirmation
|
||||||
|
read -p "Continuer ? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Annulé."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# ÉTAPE 1 : Gérer le submodule frontend
|
||||||
|
# ===========================================
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}[1/6]${NC} Vérification du submodule frontend..."
|
||||||
|
|
||||||
|
cd "$FRONTEND_DIR"
|
||||||
|
|
||||||
|
# Vérifier s'il y a des changements non commités dans le submodule
|
||||||
|
if ! git diff --quiet --exit-code || ! git diff --cached --quiet --exit-code; then
|
||||||
|
echo -e "${YELLOW}Changements détectés dans le submodule frontend${NC}"
|
||||||
|
git status --short
|
||||||
|
echo ""
|
||||||
|
read -p "Commiter ces changements dans le submodule ? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore(release) : prepare v$new_version"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Erreur:${NC} Veuillez d'abord commiter les changements du submodule."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# ÉTAPE 2 : Tag le submodule
|
||||||
|
# ===========================================
|
||||||
|
echo -e "${BLUE}[2/6]${NC} Création du tag v$new_version dans le submodule..."
|
||||||
|
|
||||||
|
# Vérifier si le tag existe déjà
|
||||||
|
if git rev-parse "v$new_version" >/dev/null 2>&1; then
|
||||||
|
echo -e "${YELLOW}Le tag v$new_version existe déjà dans le submodule${NC}"
|
||||||
|
else
|
||||||
|
git tag -a "v$new_version" -m "Release v$new_version"
|
||||||
|
echo -e "${GREEN}Tag v$new_version créé dans le submodule${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Retourner au projet principal
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# ÉTAPE 3 : Mettre à jour VERSION
|
||||||
|
# ===========================================
|
||||||
|
echo -e "${BLUE}[3/6]${NC} Mise à jour du fichier VERSION..."
|
||||||
|
echo "$new_version" > "$VERSION_FILE"
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# ÉTAPE 4 : Mettre à jour api_platform.yaml
|
||||||
|
# ===========================================
|
||||||
|
echo -e "${BLUE}[4/6]${NC} Mise à jour de api_platform.yaml..."
|
||||||
|
sed -i "s/version: .*/version: $new_version/" "$API_PLATFORM_FILE"
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# ÉTAPE 5 : Commit principal (avec mise à jour du submodule)
|
||||||
|
# ===========================================
|
||||||
|
echo -e "${BLUE}[5/6]${NC} Création du commit principal..."
|
||||||
|
git add "$VERSION_FILE" "$API_PLATFORM_FILE" "$FRONTEND_DIR"
|
||||||
|
git commit -m "chore(release) : v$new_version"
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# ÉTAPE 6 : Tag principal
|
||||||
|
# ===========================================
|
||||||
|
echo -e "${BLUE}[6/6]${NC} Création du tag v$new_version..."
|
||||||
|
git tag -a "v$new_version" -m "Release v$new_version"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✓ Release v$new_version préparée avec succès !${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "================================"
|
||||||
|
echo -e "${YELLOW}Prochaines étapes :${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "1. Pousser le submodule frontend :"
|
||||||
|
echo -e " ${BLUE}cd Inventory_frontend && git push && git push --tags && cd ..${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "2. Pousser le projet principal :"
|
||||||
|
echo -e " ${BLUE}git push && git push --tags${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "3. Créer les releases sur Gitea :"
|
||||||
|
echo " - Inventory_frontend : tag v$new_version"
|
||||||
|
echo " - Inventory (backend) : tag v$new_version"
|
||||||
|
echo ""
|
||||||
|
echo "================================"
|
||||||
139
scripts/validate-migration.php
Normal file
139
scripts/validate-migration.php
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
function parseDatabaseUrl(string $url): array
|
||||||
|
{
|
||||||
|
$parts = parse_url($url);
|
||||||
|
if ($parts === false) {
|
||||||
|
throw new RuntimeException('Invalid database URL.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = $parts['host'] ?? 'localhost';
|
||||||
|
$port = isset($parts['port']) ? (int) $parts['port'] : 5432;
|
||||||
|
$user = $parts['user'] ?? '';
|
||||||
|
$pass = $parts['pass'] ?? '';
|
||||||
|
$db = ltrim($parts['path'] ?? '', '/');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'dsn' => sprintf('pgsql:host=%s;port=%d;dbname=%s', $host, $port, $db),
|
||||||
|
'user' => $user,
|
||||||
|
'pass' => $pass,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect(string $url): PDO
|
||||||
|
{
|
||||||
|
$config = parseDatabaseUrl($url);
|
||||||
|
$pdo = new PDO($config['dsn'], $config['user'], $config['pass'], [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceUrl = getenv('SOURCE_DATABASE_URL') ?: '';
|
||||||
|
$targetUrl = getenv('TARGET_DATABASE_URL') ?: '';
|
||||||
|
|
||||||
|
if ($sourceUrl === '' || $targetUrl === '') {
|
||||||
|
fwrite(STDERR, "Usage: SOURCE_DATABASE_URL=... TARGET_DATABASE_URL=... php scripts/validate-migration.php\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tables = [
|
||||||
|
'sites',
|
||||||
|
'type_machines',
|
||||||
|
'machines',
|
||||||
|
'model_types',
|
||||||
|
'composants',
|
||||||
|
'pieces',
|
||||||
|
'products',
|
||||||
|
'constructeurs',
|
||||||
|
'documents',
|
||||||
|
'custom_fields',
|
||||||
|
'custom_field_values',
|
||||||
|
'machine_component_links',
|
||||||
|
'machine_piece_links',
|
||||||
|
'machine_product_links',
|
||||||
|
'type_machine_component_requirements',
|
||||||
|
'type_machine_piece_requirements',
|
||||||
|
'type_machine_product_requirements',
|
||||||
|
'_machineconstructeurs',
|
||||||
|
'_composantconstructeurs',
|
||||||
|
'_piececonstructeurs',
|
||||||
|
'_productconstructeurs',
|
||||||
|
'profiles',
|
||||||
|
];
|
||||||
|
|
||||||
|
$skipTables = array_filter(array_map('trim', explode(',', getenv('SKIP_TABLES') ?: 'profiles')));
|
||||||
|
|
||||||
|
$sourceTableMap = [
|
||||||
|
'model_types' => ['ModelType', 'model_types'],
|
||||||
|
'_machineconstructeurs' => ['_MachineConstructeurs', '_machineconstructeurs'],
|
||||||
|
'_composantconstructeurs' => ['_ComposantConstructeurs', '_composantconstructeurs'],
|
||||||
|
'_piececonstructeurs' => ['_PieceConstructeurs', '_piececonstructeurs'],
|
||||||
|
'_productconstructeurs' => ['_ProductConstructeurs', '_productconstructeurs'],
|
||||||
|
];
|
||||||
|
|
||||||
|
function resolveTable(PDO $db, array $candidates): ?string
|
||||||
|
{
|
||||||
|
foreach ($candidates as $candidate) {
|
||||||
|
$exists = (bool) $db
|
||||||
|
->query(sprintf("SELECT to_regclass('public.%s')", $candidate))
|
||||||
|
->fetchColumn();
|
||||||
|
if ($exists) {
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
$quoted = (bool) $db
|
||||||
|
->query(sprintf("SELECT to_regclass('public.\"%s\"')", $candidate))
|
||||||
|
->fetchColumn();
|
||||||
|
if ($quoted) {
|
||||||
|
return sprintf('"%s"', $candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$source = connect($sourceUrl);
|
||||||
|
$target = connect($targetUrl);
|
||||||
|
|
||||||
|
$hasDifferences = false;
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
if (in_array($table, $skipTables, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$sourceCandidates = $sourceTableMap[$table] ?? [$table];
|
||||||
|
$sourceTable = resolveTable($source, $sourceCandidates);
|
||||||
|
$sourceExists = $sourceTable !== null;
|
||||||
|
$targetExists = (bool) $target
|
||||||
|
->query(sprintf("SELECT to_regclass('public.%s')", $table))
|
||||||
|
->fetchColumn();
|
||||||
|
|
||||||
|
if (!$sourceExists || !$targetExists) {
|
||||||
|
$hasDifferences = true;
|
||||||
|
printf(
|
||||||
|
"%s: source=%s target=%s\n",
|
||||||
|
$table,
|
||||||
|
$sourceExists ? 'exists' : 'missing',
|
||||||
|
$targetExists ? 'exists' : 'missing'
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceCount = (int) $source->query(sprintf('SELECT COUNT(*) FROM public.%s', $sourceTable))->fetchColumn();
|
||||||
|
$targetCount = (int) $target->query(sprintf('SELECT COUNT(*) FROM public.%s', $table))->fetchColumn();
|
||||||
|
|
||||||
|
if ($sourceCount !== $targetCount) {
|
||||||
|
$hasDifferences = true;
|
||||||
|
printf("%s: source=%d target=%d\n", $table, $sourceCount, $targetCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasDifferences) {
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Counts match for all tables.\n";
|
||||||
218
src/Command/CompressPdfCommand.php
Normal file
218
src/Command/CompressPdfCommand.php
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Entity\Document;
|
||||||
|
use App\Repository\DocumentRepository;
|
||||||
|
use App\Service\DocumentStorageService;
|
||||||
|
use App\Service\PdfCompressorService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
use function count;
|
||||||
|
use function strlen;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:compress-pdf',
|
||||||
|
description: 'Compress all PDF documents without quality loss',
|
||||||
|
)]
|
||||||
|
class CompressPdfCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly DocumentRepository $documentRepository,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
private readonly PdfCompressorService $pdfCompressor,
|
||||||
|
private readonly DocumentStorageService $storageService,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be compressed without actually doing it')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$dryRun = $input->getOption('dry-run');
|
||||||
|
|
||||||
|
// Check if qpdf is installed
|
||||||
|
exec('which qpdf', $qpdfPath, $returnCode);
|
||||||
|
if (0 !== $returnCode) {
|
||||||
|
$io->error('qpdf is not installed. Run: sudo apt install qpdf');
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$documents = $this->documentRepository->findBy(['mimeType' => 'application/pdf']);
|
||||||
|
|
||||||
|
if (empty($documents)) {
|
||||||
|
$io->info('No PDF documents found.');
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->title('PDF Compression');
|
||||||
|
$io->text(sprintf('Found %d PDF documents', count($documents)));
|
||||||
|
|
||||||
|
$totalSaved = 0;
|
||||||
|
$compressed = 0;
|
||||||
|
|
||||||
|
foreach ($documents as $document) {
|
||||||
|
$path = $document->getPath();
|
||||||
|
|
||||||
|
if ($this->storageService->isBase64DataUri($path)) {
|
||||||
|
$this->compressBase64Document($document, $path, $dryRun, $io, $totalSaved, $compressed);
|
||||||
|
} else {
|
||||||
|
$this->compressFileDocument($document, $path, $dryRun, $io, $totalSaved, $compressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$dryRun && $compressed > 0) {
|
||||||
|
$this->em->flush();
|
||||||
|
$io->success(sprintf(
|
||||||
|
'Compressed %d/%d PDFs. Total space saved: %s',
|
||||||
|
$compressed,
|
||||||
|
count($documents),
|
||||||
|
$this->formatBytes($totalSaved)
|
||||||
|
));
|
||||||
|
} elseif ($dryRun) {
|
||||||
|
$io->info('Dry run completed. No changes made.');
|
||||||
|
} else {
|
||||||
|
$io->info('No PDFs needed compression.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function compressBase64Document(
|
||||||
|
Document $document,
|
||||||
|
string $path,
|
||||||
|
bool $dryRun,
|
||||||
|
SymfonyStyle $io,
|
||||||
|
int &$totalSaved,
|
||||||
|
int &$compressed,
|
||||||
|
): void {
|
||||||
|
$base64Data = $path;
|
||||||
|
if (str_contains($base64Data, ',')) {
|
||||||
|
$base64Data = explode(',', $base64Data, 2)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdfContent = base64_decode($base64Data, true);
|
||||||
|
if (false === $pdfContent) {
|
||||||
|
$io->warning(sprintf('Failed to decode document: %s', $document->getName()));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalSize = strlen($pdfContent);
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$io->text(sprintf(
|
||||||
|
' [DRY-RUN] Would compress (base64): %s (%s)',
|
||||||
|
$document->getName(),
|
||||||
|
$this->formatBytes($originalSize)
|
||||||
|
));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->pdfCompressor->compressBase64Pdf($path);
|
||||||
|
if (null !== $result) {
|
||||||
|
$document->setPath($result['path']);
|
||||||
|
$document->setSize($result['size']);
|
||||||
|
$totalSaved += $result['saved'];
|
||||||
|
++$compressed;
|
||||||
|
|
||||||
|
$io->text(sprintf(
|
||||||
|
' OK %s: %s → %s (-%s, -%.1f%%)',
|
||||||
|
$document->getName(),
|
||||||
|
$this->formatBytes($result['originalSize']),
|
||||||
|
$this->formatBytes($result['size']),
|
||||||
|
$this->formatBytes($result['saved']),
|
||||||
|
($result['saved'] / $result['originalSize']) * 100
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$io->text(sprintf(
|
||||||
|
' - %s: Already optimal (%s)',
|
||||||
|
$document->getName(),
|
||||||
|
$this->formatBytes($originalSize)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function compressFileDocument(
|
||||||
|
Document $document,
|
||||||
|
string $path,
|
||||||
|
bool $dryRun,
|
||||||
|
SymfonyStyle $io,
|
||||||
|
int &$totalSaved,
|
||||||
|
int &$compressed,
|
||||||
|
): void {
|
||||||
|
$absolutePath = $this->storageService->getAbsolutePath($path);
|
||||||
|
if (!file_exists($absolutePath)) {
|
||||||
|
$io->warning(sprintf('File not found: %s (%s)', $document->getName(), $path));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalSize = filesize($absolutePath);
|
||||||
|
if (false === $originalSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$io->text(sprintf(
|
||||||
|
' [DRY-RUN] Would compress (file): %s (%s)',
|
||||||
|
$document->getName(),
|
||||||
|
$this->formatBytes($originalSize)
|
||||||
|
));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->pdfCompressor->compressFile($absolutePath);
|
||||||
|
if (null !== $result) {
|
||||||
|
$document->setSize($result['size']);
|
||||||
|
$totalSaved += $result['saved'];
|
||||||
|
++$compressed;
|
||||||
|
|
||||||
|
$io->text(sprintf(
|
||||||
|
' OK %s: %s → %s (-%s, -%.1f%%)',
|
||||||
|
$document->getName(),
|
||||||
|
$this->formatBytes($result['originalSize']),
|
||||||
|
$this->formatBytes($result['size']),
|
||||||
|
$this->formatBytes($result['saved']),
|
||||||
|
($result['saved'] / $result['originalSize']) * 100
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$io->text(sprintf(
|
||||||
|
' - %s: Already optimal (%s)',
|
||||||
|
$document->getName(),
|
||||||
|
$this->formatBytes($originalSize)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatBytes(int $bytes): string
|
||||||
|
{
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
$i = 0;
|
||||||
|
while ($bytes >= 1024 && $i < count($units) - 1) {
|
||||||
|
$bytes /= 1024;
|
||||||
|
++$i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($bytes, 2).' '.$units[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/Command/InitProfilePasswordsCommand.php
Normal file
85
src/Command/InitProfilePasswordsCommand.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
|
||||||
|
use function count;
|
||||||
|
use function in_array;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:init-profile-passwords',
|
||||||
|
description: 'Initialize all profile passwords to first letter of firstName + "123"',
|
||||||
|
)]
|
||||||
|
class InitProfilePasswordsCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
$all = $this->profiles->findAll();
|
||||||
|
|
||||||
|
if (0 === count($all)) {
|
||||||
|
$io->warning('Aucun profil trouvé.');
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Promote first profile to ROLE_ADMIN if none exists
|
||||||
|
$hasAdmin = false;
|
||||||
|
foreach ($all as $profile) {
|
||||||
|
if (in_array('ROLE_ADMIN', $profile->getRoles(), true)) {
|
||||||
|
$hasAdmin = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$isFirst = true;
|
||||||
|
$count = 0;
|
||||||
|
foreach ($all as $profile) {
|
||||||
|
// Set password: first letter of firstName + "123"
|
||||||
|
$firstLetter = mb_strtoupper(mb_substr($profile->getFirstName(), 0, 1));
|
||||||
|
$plain = $firstLetter.'123';
|
||||||
|
$hashed = $this->passwordHasher->hashPassword($profile, $plain);
|
||||||
|
$profile->setPassword($hashed);
|
||||||
|
|
||||||
|
// Set roles: first profile → ADMIN, others → VIEWER (minimum to use the app)
|
||||||
|
if (!$hasAdmin && $isFirst) {
|
||||||
|
$profile->setRoles(['ROLE_ADMIN']);
|
||||||
|
$io->writeln(sprintf(' %s %s → mdp: %s — ROLE_ADMIN', $profile->getFirstName(), $profile->getLastName(), $plain));
|
||||||
|
$isFirst = false;
|
||||||
|
} elseif (in_array('ROLE_USER', $profile->getRoles(), true) && !in_array('ROLE_VIEWER', $profile->getRoles(), true) && !in_array('ROLE_GESTIONNAIRE', $profile->getRoles(), true) && !in_array('ROLE_ADMIN', $profile->getRoles(), true)) {
|
||||||
|
$profile->setRoles(['ROLE_VIEWER']);
|
||||||
|
$io->writeln(sprintf(' %s %s → mdp: %s — ROLE_VIEWER', $profile->getFirstName(), $profile->getLastName(), $plain));
|
||||||
|
} else {
|
||||||
|
$io->writeln(sprintf(' %s %s → mdp: %s — %s', $profile->getFirstName(), $profile->getLastName(), $plain, implode(', ', $profile->getRoles())));
|
||||||
|
}
|
||||||
|
|
||||||
|
++$count;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
$io->success(sprintf('%d mot(s) de passe initialisé(s).', $count));
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
218
src/Command/MigrateDocumentsToFilesystemCommand.php
Normal file
218
src/Command/MigrateDocumentsToFilesystemCommand.php
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Repository\DocumentRepository;
|
||||||
|
use App\Service\DocumentStorageService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
use function count;
|
||||||
|
use function strlen;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:migrate-documents-to-filesystem',
|
||||||
|
description: 'Migrate document storage from Base64 in DB to filesystem',
|
||||||
|
)]
|
||||||
|
class MigrateDocumentsToFilesystemCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly DocumentRepository $documentRepository,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
private readonly DocumentStorageService $storageService,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be migrated without making changes')
|
||||||
|
->addOption('batch-size', null, InputOption::VALUE_REQUIRED, 'Number of documents to process before flushing', '50')
|
||||||
|
->addOption('limit', null, InputOption::VALUE_REQUIRED, 'Max documents to migrate (for testing)', '0')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$dryRun = $input->getOption('dry-run');
|
||||||
|
$batchSize = (int) $input->getOption('batch-size');
|
||||||
|
$limit = (int) $input->getOption('limit');
|
||||||
|
|
||||||
|
$io->title('Document Storage Migration: Base64 → Filesystem');
|
||||||
|
|
||||||
|
// Verify storage directory is writable
|
||||||
|
$storageDir = $this->storageService->getStorageDir();
|
||||||
|
if (!$dryRun) {
|
||||||
|
if (!is_dir($storageDir)) {
|
||||||
|
mkdir($storageDir, 0o775, true);
|
||||||
|
}
|
||||||
|
if (!is_writable($storageDir)) {
|
||||||
|
$io->error("Storage directory is not writable: {$storageDir}");
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
$io->text("Storage directory: {$storageDir}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: fetch only IDs of Base64 documents (no heavy path column loaded)
|
||||||
|
$conn = $this->em->getConnection();
|
||||||
|
$ids = $conn->fetchFirstColumn("SELECT id FROM documents WHERE path LIKE 'data:%'");
|
||||||
|
$total = count($ids);
|
||||||
|
$migrated = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$errors = 0;
|
||||||
|
$totalBytes = 0;
|
||||||
|
|
||||||
|
$io->text(sprintf('Found %d documents with Base64 data to migrate', $total));
|
||||||
|
|
||||||
|
if (0 === $total) {
|
||||||
|
$io->success('Nothing to migrate — all documents are already file-based.');
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: process one document at a time to avoid memory exhaustion
|
||||||
|
foreach ($ids as $index => $docId) {
|
||||||
|
if ($limit > 0 && $migrated >= $limit) {
|
||||||
|
$io->text("Reached limit of {$limit} documents.");
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch single row with raw SQL to keep memory flat
|
||||||
|
$row = $conn->fetchAssociative(
|
||||||
|
'SELECT id, name, filename, path, mimetype, size FROM documents WHERE id = ?',
|
||||||
|
[$docId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$row) {
|
||||||
|
++$skipped;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $row['path'];
|
||||||
|
if (!$this->storageService->isBase64DataUri($path)) {
|
||||||
|
++$skipped;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$docName = $row['name'] ?: $row['filename'];
|
||||||
|
$filename = $row['filename'] ?: $row['name'];
|
||||||
|
$mimeType = $row['mimetype'] ?? 'application/octet-stream';
|
||||||
|
|
||||||
|
// Extract binary content from data URI
|
||||||
|
$parts = explode(',', $path, 2);
|
||||||
|
$base64 = $parts[1] ?? '';
|
||||||
|
$content = base64_decode($base64, true);
|
||||||
|
|
||||||
|
// Free the raw row immediately
|
||||||
|
unset($row, $path, $base64, $parts);
|
||||||
|
|
||||||
|
if (false === $content || '' === $content) {
|
||||||
|
$io->warning(sprintf('[%d/%d] Cannot decode: %s (id: %s)', $index + 1, $total, $docName, $docId));
|
||||||
|
++$errors;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileSize = strlen($content);
|
||||||
|
$extension = $this->storageService->extensionFromFilename(
|
||||||
|
$filename ?: ('file.'.$this->storageService->extensionFromMimeType($mimeType))
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$io->text(sprintf(
|
||||||
|
' [DRY-RUN] Would migrate: %s (%s)',
|
||||||
|
$docName,
|
||||||
|
$this->formatBytes($fileSize)
|
||||||
|
));
|
||||||
|
++$migrated;
|
||||||
|
$totalBytes += $fileSize;
|
||||||
|
unset($content);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$relativePath = $this->storageService->store($content, $docId, $extension);
|
||||||
|
unset($content);
|
||||||
|
|
||||||
|
// Update DB directly — avoid loading entity with huge path
|
||||||
|
$conn->executeStatement(
|
||||||
|
'UPDATE documents SET path = ?, size = ? WHERE id = ?',
|
||||||
|
[$relativePath, $fileSize, $docId]
|
||||||
|
);
|
||||||
|
|
||||||
|
++$migrated;
|
||||||
|
$totalBytes += $fileSize;
|
||||||
|
|
||||||
|
$io->text(sprintf(
|
||||||
|
' [OK] %s → %s (%s)',
|
||||||
|
$docName,
|
||||||
|
$relativePath,
|
||||||
|
$this->formatBytes($fileSize)
|
||||||
|
));
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
unset($content);
|
||||||
|
$io->error(sprintf(
|
||||||
|
' [FAIL] %s: %s',
|
||||||
|
$docName,
|
||||||
|
$e->getMessage()
|
||||||
|
));
|
||||||
|
++$errors;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === $migrated % $batchSize) {
|
||||||
|
$io->text(sprintf(' ... %d migrated so far', $migrated));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->newLine();
|
||||||
|
$io->table(
|
||||||
|
['Metric', 'Count'],
|
||||||
|
[
|
||||||
|
['Total documents', (string) $total],
|
||||||
|
['Migrated', (string) $migrated],
|
||||||
|
['Skipped (already file-based)', (string) $skipped],
|
||||||
|
['Errors', (string) $errors],
|
||||||
|
['Total bytes written', $this->formatBytes($totalBytes)],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$io->info('Dry run completed. No changes were made.');
|
||||||
|
} elseif ($errors > 0) {
|
||||||
|
$io->warning(sprintf('Migration completed with %d errors.', $errors));
|
||||||
|
} else {
|
||||||
|
$io->success('Migration completed successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors > 0 ? Command::FAILURE : Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatBytes(int $bytes): string
|
||||||
|
{
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
$i = 0;
|
||||||
|
while ($bytes >= 1024 && $i < count($units) - 1) {
|
||||||
|
$bytes /= 1024;
|
||||||
|
++$i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($bytes, 2).' '.$units[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/Controller/ActivityLogController.php
Normal file
90
src/Controller/ActivityLogController.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\AuditLogRepository;
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
final class ActivityLogController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AuditLogRepository $auditLogs,
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/activity-logs', name: 'api_activity_logs', methods: ['GET'])]
|
||||||
|
public function __invoke(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$page = max(1, $request->query->getInt('page', 1));
|
||||||
|
$itemsPerPage = min(100, max(1, $request->query->getInt('itemsPerPage', 30)));
|
||||||
|
|
||||||
|
$filters = [];
|
||||||
|
if ($entityType = $request->query->get('entityType')) {
|
||||||
|
$filters['entityType'] = $entityType;
|
||||||
|
}
|
||||||
|
if ($action = $request->query->get('action')) {
|
||||||
|
$filters['action'] = $action;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->auditLogs->findAllPaginated($page, $itemsPerPage, $filters);
|
||||||
|
|
||||||
|
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn ($log) => $log->getActorProfileId(),
|
||||||
|
$result['items'],
|
||||||
|
))));
|
||||||
|
|
||||||
|
$actorMap = [];
|
||||||
|
if ([] !== $actorIds) {
|
||||||
|
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||||
|
foreach ($profiles as $profile) {
|
||||||
|
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
|
if ('' === $label) {
|
||||||
|
$label = $profile->getEmail() ?? $profile->getId();
|
||||||
|
}
|
||||||
|
$actorMap[$profile->getId()] = $label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = array_map(
|
||||||
|
static function ($log) use ($actorMap) {
|
||||||
|
$actorId = $log->getActorProfileId();
|
||||||
|
$snapshot = $log->getSnapshot();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $log->getId(),
|
||||||
|
'entityType' => $log->getEntityType(),
|
||||||
|
'entityId' => $log->getEntityId(),
|
||||||
|
'entityName' => $snapshot['name'] ?? null,
|
||||||
|
'entityRef' => $snapshot['reference'] ?? null,
|
||||||
|
'action' => $log->getAction(),
|
||||||
|
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
'actor' => $actorId
|
||||||
|
? [
|
||||||
|
'id' => $actorId,
|
||||||
|
'label' => $actorMap[$actorId] ?? $actorId,
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
'diff' => $log->getDiff(),
|
||||||
|
'snapshot' => $snapshot,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
$result['items'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'items' => array_values($items),
|
||||||
|
'total' => $result['total'],
|
||||||
|
'page' => $page,
|
||||||
|
'itemsPerPage' => $itemsPerPage,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
193
src/Controller/AdminProfileController.php
Normal file
193
src/Controller/AdminProfileController.php
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
use function count;
|
||||||
|
use function in_array;
|
||||||
|
|
||||||
|
#[Route('/api/admin/profiles')]
|
||||||
|
final class AdminProfileController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('', name: 'admin_profiles_list', methods: ['GET'])]
|
||||||
|
public function list(): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
|
||||||
|
$items = $this->profiles->findBy([], ['firstName' => 'ASC']);
|
||||||
|
|
||||||
|
return new JsonResponse(array_map([$this, 'serializeProfile'], $items));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('', name: 'admin_profiles_create', methods: ['POST'])]
|
||||||
|
public function create(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
|
||||||
|
$payload = $request->toArray();
|
||||||
|
$firstName = trim((string) ($payload['firstName'] ?? ''));
|
||||||
|
$lastName = trim((string) ($payload['lastName'] ?? ''));
|
||||||
|
|
||||||
|
if ('' === $firstName || '' === $lastName) {
|
||||||
|
return new JsonResponse(['message' => 'firstName et lastName sont requis.'], JsonResponse::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = trim((string) ($payload['email'] ?? ''));
|
||||||
|
$password = $payload['password'] ?? null;
|
||||||
|
$role = $payload['role'] ?? 'ROLE_VIEWER';
|
||||||
|
|
||||||
|
$allowedRoles = ['ROLE_ADMIN', 'ROLE_GESTIONNAIRE', 'ROLE_VIEWER', 'ROLE_USER'];
|
||||||
|
if (!in_array($role, $allowedRoles, true)) {
|
||||||
|
return new JsonResponse(['message' => 'Role invalide.'], JsonResponse::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile = new Profile();
|
||||||
|
$profile->setFirstName($firstName);
|
||||||
|
$profile->setLastName($lastName);
|
||||||
|
$profile->setIsActive(true);
|
||||||
|
$profile->setRoles([$role]);
|
||||||
|
|
||||||
|
if ('' !== $email) {
|
||||||
|
$profile->setEmail($email);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $password && '' !== $password) {
|
||||||
|
$profile->setPassword(
|
||||||
|
$this->passwordHasher->hashPassword($profile, $password)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->persist($profile);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return new JsonResponse($this->serializeProfile($profile), JsonResponse::HTTP_CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}/role', name: 'admin_profiles_update_role', methods: ['PUT'])]
|
||||||
|
public function updateRole(string $id, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
|
||||||
|
$profile = $this->profiles->find($id);
|
||||||
|
if (!$profile) {
|
||||||
|
return new JsonResponse(['message' => 'Profil introuvable.'], JsonResponse::HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $request->toArray();
|
||||||
|
$role = $payload['role'] ?? null;
|
||||||
|
|
||||||
|
$allowedRoles = ['ROLE_ADMIN', 'ROLE_GESTIONNAIRE', 'ROLE_VIEWER', 'ROLE_USER'];
|
||||||
|
if (!$role || !in_array($role, $allowedRoles, true)) {
|
||||||
|
return new JsonResponse(['message' => 'Role invalide.'], JsonResponse::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent removing the last admin
|
||||||
|
if (in_array('ROLE_ADMIN', $profile->getRoles(), true) && 'ROLE_ADMIN' !== $role) {
|
||||||
|
$adminCount = $this->countAdmins();
|
||||||
|
if ($adminCount <= 1) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Impossible de retirer le dernier administrateur.'],
|
||||||
|
JsonResponse::HTTP_CONFLICT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile->setRoles([$role]);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return new JsonResponse($this->serializeProfile($profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}/password', name: 'admin_profiles_update_password', methods: ['PUT'])]
|
||||||
|
public function updatePassword(string $id, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
|
||||||
|
$profile = $this->profiles->find($id);
|
||||||
|
if (!$profile) {
|
||||||
|
return new JsonResponse(['message' => 'Profil introuvable.'], JsonResponse::HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $request->toArray();
|
||||||
|
$password = $payload['password'] ?? '';
|
||||||
|
|
||||||
|
if ('' === $password) {
|
||||||
|
return new JsonResponse(['message' => 'Le mot de passe est requis.'], JsonResponse::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile->setPassword(
|
||||||
|
$this->passwordHasher->hashPassword($profile, $password)
|
||||||
|
);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return new JsonResponse($this->serializeProfile($profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}/deactivate', name: 'admin_profiles_deactivate', methods: ['PUT'])]
|
||||||
|
public function deactivate(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
|
||||||
|
$profile = $this->profiles->find($id);
|
||||||
|
if (!$profile) {
|
||||||
|
return new JsonResponse(['message' => 'Profil introuvable.'], JsonResponse::HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent deactivating the last admin
|
||||||
|
if (in_array('ROLE_ADMIN', $profile->getRoles(), true)) {
|
||||||
|
$adminCount = $this->countAdmins();
|
||||||
|
if ($adminCount <= 1) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Impossible de desactiver le dernier administrateur.'],
|
||||||
|
JsonResponse::HTTP_CONFLICT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile->setIsActive(false);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return new JsonResponse($this->serializeProfile($profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function serializeProfile(Profile $profile): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $profile->getId(),
|
||||||
|
'firstName' => $profile->getFirstName(),
|
||||||
|
'lastName' => $profile->getLastName(),
|
||||||
|
'email' => $profile->getEmail(),
|
||||||
|
'isActive' => $profile->isActive(),
|
||||||
|
'hasPassword' => null !== $profile->getPassword() && '' !== $profile->getPassword(),
|
||||||
|
'roles' => $profile->getRoles(),
|
||||||
|
'createdAt' => $profile->getCreatedAt()->format('c'),
|
||||||
|
'updatedAt' => $profile->getUpdatedAt()->format('c'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function countAdmins(): int
|
||||||
|
{
|
||||||
|
$all = $this->profiles->findBy(['isActive' => true]);
|
||||||
|
|
||||||
|
return count(array_filter(
|
||||||
|
$all,
|
||||||
|
static fn (Profile $p) => in_array('ROLE_ADMIN', $p->getRoles(), true)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/Controller/CommentController.php
Normal file
145
src/Controller/CommentController.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Comment;
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route('/api/comments')]
|
||||||
|
final class CommentController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('', name: 'api_comments_create', methods: ['POST'])]
|
||||||
|
public function create(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$session = $request->getSession();
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if (!$profileId) {
|
||||||
|
return $this->json(['message' => 'Aucun profil actif.'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile = $this->profiles->find($profileId);
|
||||||
|
if (!$profile) {
|
||||||
|
return $this->json(['message' => 'Profil introuvable.'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = json_decode($request->getContent(), true);
|
||||||
|
if (!is_array($payload)) {
|
||||||
|
return $this->json(['message' => 'Payload JSON invalide.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = trim((string) ($payload['content'] ?? ''));
|
||||||
|
$entityType = trim((string) ($payload['entityType'] ?? ''));
|
||||||
|
$entityId = trim((string) ($payload['entityId'] ?? ''));
|
||||||
|
$entityName = isset($payload['entityName']) ? trim((string) $payload['entityName']) : null;
|
||||||
|
|
||||||
|
if ('' === $content) {
|
||||||
|
return $this->json(['message' => 'Le contenu est requis.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedTypes = ['machine', 'piece', 'composant', 'product', 'piece_category', 'component_category', 'product_category', 'machine_skeleton'];
|
||||||
|
if (!in_array($entityType, $allowedTypes, true)) {
|
||||||
|
return $this->json(['message' => 'Type d\'entité invalide.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('' === $entityId) {
|
||||||
|
return $this->json(['message' => 'L\'identifiant de l\'entité est requis.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$authorName = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
|
if ('' === $authorName) {
|
||||||
|
$authorName = $profile->getEmail() ?? 'Inconnu';
|
||||||
|
}
|
||||||
|
|
||||||
|
$comment = new Comment();
|
||||||
|
$comment->setContent($content);
|
||||||
|
$comment->setEntityType($entityType);
|
||||||
|
$comment->setEntityId($entityId);
|
||||||
|
$comment->setEntityName($entityName);
|
||||||
|
$comment->setAuthorId($profileId);
|
||||||
|
$comment->setAuthorName($authorName);
|
||||||
|
|
||||||
|
$this->entityManager->persist($comment);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $this->json($this->normalize($comment), 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}/resolve', name: 'api_comments_resolve', methods: ['PATCH'])]
|
||||||
|
public function resolve(string $id, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
|
$comment = $this->entityManager->getRepository(Comment::class)->find($id);
|
||||||
|
if (!$comment) {
|
||||||
|
return $this->json(['message' => 'Commentaire introuvable.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$session = $request->getSession();
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
$profile = $profileId ? $this->profiles->find($profileId) : null;
|
||||||
|
|
||||||
|
$resolverName = 'Inconnu';
|
||||||
|
if ($profile) {
|
||||||
|
$resolverName = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
|
if ('' === $resolverName) {
|
||||||
|
$resolverName = $profile->getEmail() ?? 'Inconnu';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$comment->setStatus('resolved');
|
||||||
|
$comment->setResolvedById($profileId);
|
||||||
|
$comment->setResolvedByName($resolverName);
|
||||||
|
$comment->setResolvedAt(new DateTimeImmutable());
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $this->json($this->normalize($comment));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/stats/unresolved-count', name: 'api_comments_unresolved_count', methods: ['GET'])]
|
||||||
|
public function unresolvedCount(): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$count = $this->entityManager->getRepository(Comment::class)
|
||||||
|
->count(['status' => 'open'])
|
||||||
|
;
|
||||||
|
|
||||||
|
return $this->json(['count' => $count]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalize(Comment $comment): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $comment->getId(),
|
||||||
|
'content' => $comment->getContent(),
|
||||||
|
'entityType' => $comment->getEntityType(),
|
||||||
|
'entityId' => $comment->getEntityId(),
|
||||||
|
'entityName' => $comment->getEntityName(),
|
||||||
|
'authorId' => $comment->getAuthorId(),
|
||||||
|
'authorName' => $comment->getAuthorName(),
|
||||||
|
'status' => $comment->getStatus(),
|
||||||
|
'resolvedById' => $comment->getResolvedById(),
|
||||||
|
'resolvedByName' => $comment->getResolvedByName(),
|
||||||
|
'resolvedAt' => $comment->getResolvedAt()?->format(DateTimeInterface::ATOM),
|
||||||
|
'createdAt' => $comment->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
'updatedAt' => $comment->getUpdatedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/Controller/ComposantHistoryController.php
Normal file
82
src/Controller/ComposantHistoryController.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\AuditLogRepository;
|
||||||
|
use App\Repository\ComposantRepository;
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
final class ComposantHistoryController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ComposantRepository $components,
|
||||||
|
private readonly AuditLogRepository $auditLogs,
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/composants/{id}/history', name: 'api_composant_history', methods: ['GET'])]
|
||||||
|
public function __invoke(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$component = $this->components->find($id);
|
||||||
|
if (!$component) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Composant introuvable.'],
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = $this->auditLogs->findEntityHistory('composant', $id, 200);
|
||||||
|
|
||||||
|
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn ($log) => $log->getActorProfileId(),
|
||||||
|
$logs,
|
||||||
|
))));
|
||||||
|
|
||||||
|
$actorMap = [];
|
||||||
|
if ([] !== $actorIds) {
|
||||||
|
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||||
|
foreach ($profiles as $profile) {
|
||||||
|
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
|
if ('' === $label) {
|
||||||
|
$label = $profile->getEmail() ?? $profile->getId();
|
||||||
|
}
|
||||||
|
$actorMap[$profile->getId()] = $label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = array_map(
|
||||||
|
static function ($log) use ($actorMap) {
|
||||||
|
$actorId = $log->getActorProfileId();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $log->getId(),
|
||||||
|
'action' => $log->getAction(),
|
||||||
|
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
'actor' => $actorId
|
||||||
|
? [
|
||||||
|
'id' => $actorId,
|
||||||
|
'label' => $actorMap[$actorId] ?? $actorId,
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
'diff' => $log->getDiff(),
|
||||||
|
'snapshot' => $log->getSnapshot(),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
$logs,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'items' => array_values($items),
|
||||||
|
'total' => count($items),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
304
src/Controller/CustomFieldValueController.php
Normal file
304
src/Controller/CustomFieldValueController.php
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\CustomField;
|
||||||
|
use App\Entity\CustomFieldValue;
|
||||||
|
use App\Repository\ComposantRepository;
|
||||||
|
use App\Repository\CustomFieldRepository;
|
||||||
|
use App\Repository\CustomFieldValueRepository;
|
||||||
|
use App\Repository\MachineRepository;
|
||||||
|
use App\Repository\PieceRepository;
|
||||||
|
use App\Repository\ProductRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route('/api/custom-fields/values')]
|
||||||
|
class CustomFieldValueController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly CustomFieldRepository $customFieldRepository,
|
||||||
|
private readonly CustomFieldValueRepository $customFieldValueRepository,
|
||||||
|
private readonly MachineRepository $machineRepository,
|
||||||
|
private readonly ComposantRepository $composantRepository,
|
||||||
|
private readonly PieceRepository $pieceRepository,
|
||||||
|
private readonly ProductRepository $productRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('', name: 'custom_field_values_create', methods: ['POST'])]
|
||||||
|
public function create(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
|
$payload = $this->decodePayload($request);
|
||||||
|
if ($payload instanceof JsonResponse) {
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
$customField = $this->resolveCustomField($payload);
|
||||||
|
if ($customField instanceof JsonResponse) {
|
||||||
|
return $customField;
|
||||||
|
}
|
||||||
|
|
||||||
|
$target = $this->resolveTarget($payload);
|
||||||
|
if ($target instanceof JsonResponse) {
|
||||||
|
return $target;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = new CustomFieldValue();
|
||||||
|
$value->setCustomField($customField);
|
||||||
|
$value->setValue((string) ($payload['value'] ?? ''));
|
||||||
|
$this->applyTarget($value, $target['type'], $target['entity']);
|
||||||
|
|
||||||
|
$this->entityManager->persist($value);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $this->json($this->normalizeCustomFieldValue($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/upsert', name: 'custom_field_values_upsert', methods: ['POST'])]
|
||||||
|
public function upsert(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
|
$payload = $this->decodePayload($request);
|
||||||
|
if ($payload instanceof JsonResponse) {
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
$customField = $this->resolveCustomField($payload);
|
||||||
|
if ($customField instanceof JsonResponse) {
|
||||||
|
return $customField;
|
||||||
|
}
|
||||||
|
|
||||||
|
$target = $this->resolveTarget($payload);
|
||||||
|
if ($target instanceof JsonResponse) {
|
||||||
|
return $target;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $this->customFieldValueRepository->findOneBy([
|
||||||
|
'customField' => $customField,
|
||||||
|
$target['type'] => $target['entity'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($existing instanceof CustomFieldValue) {
|
||||||
|
$existing->setValue((string) ($payload['value'] ?? ''));
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $this->json($this->normalizeCustomFieldValue($existing));
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = new CustomFieldValue();
|
||||||
|
$value->setCustomField($customField);
|
||||||
|
$value->setValue((string) ($payload['value'] ?? ''));
|
||||||
|
$this->applyTarget($value, $target['type'], $target['entity']);
|
||||||
|
|
||||||
|
$this->entityManager->persist($value);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $this->json($this->normalizeCustomFieldValue($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{entityType}/{entityId}', name: 'custom_field_values_list', methods: ['GET'])]
|
||||||
|
public function listByEntity(string $entityType, string $entityId): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$target = $this->resolveTarget([
|
||||||
|
'entityType' => $entityType,
|
||||||
|
'entityId' => $entityId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($target instanceof JsonResponse) {
|
||||||
|
return $target;
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = $this->customFieldValueRepository->findBy([
|
||||||
|
$target['type'] => $target['entity'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->json(array_map(
|
||||||
|
fn (CustomFieldValue $value) => $this->normalizeCustomFieldValue($value),
|
||||||
|
$values
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}', name: 'custom_field_values_update', methods: ['PATCH'])]
|
||||||
|
public function update(string $id, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
|
$value = $this->customFieldValueRepository->find($id);
|
||||||
|
if (!$value instanceof CustomFieldValue) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Custom field value not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $this->decodePayload($request);
|
||||||
|
if ($payload instanceof JsonResponse) {
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('value', $payload)) {
|
||||||
|
$value->setValue((string) $payload['value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $this->json($this->normalizeCustomFieldValue($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}', name: 'custom_field_values_delete', methods: ['DELETE'])]
|
||||||
|
public function delete(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
|
$value = $this->customFieldValueRepository->find($id);
|
||||||
|
if (!$value instanceof CustomFieldValue) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Custom field value not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->remove($value);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $this->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function decodePayload(Request $request): array|JsonResponse
|
||||||
|
{
|
||||||
|
$payload = json_decode($request->getContent(), true);
|
||||||
|
if (!is_array($payload)) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Invalid JSON payload.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveCustomField(array $payload): CustomField|JsonResponse
|
||||||
|
{
|
||||||
|
$customFieldId = isset($payload['customFieldId']) ? trim((string) $payload['customFieldId']) : '';
|
||||||
|
if ('' !== $customFieldId) {
|
||||||
|
$customField = $this->customFieldRepository->find($customFieldId);
|
||||||
|
if ($customField instanceof CustomField) {
|
||||||
|
return $customField;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json(['success' => false, 'error' => 'Custom field not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$customFieldName = isset($payload['customFieldName']) ? trim((string) $payload['customFieldName']) : '';
|
||||||
|
if ('' === $customFieldName) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'customFieldId or customFieldName is required.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$customField = new CustomField();
|
||||||
|
$customField->setName($customFieldName);
|
||||||
|
$customField->setType((string) ($payload['customFieldType'] ?? 'text'));
|
||||||
|
$customField->setRequired((bool) ($payload['customFieldRequired'] ?? false));
|
||||||
|
|
||||||
|
$options = $payload['customFieldOptions'] ?? null;
|
||||||
|
if (is_array($options)) {
|
||||||
|
$customField->setOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->persist($customField);
|
||||||
|
|
||||||
|
return $customField;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveTarget(array $payload): array|JsonResponse
|
||||||
|
{
|
||||||
|
$entityType = isset($payload['entityType']) ? strtolower((string) $payload['entityType']) : '';
|
||||||
|
$entityId = isset($payload['entityId']) ? trim((string) $payload['entityId']) : '';
|
||||||
|
|
||||||
|
if ('' === $entityType || '' === $entityId) {
|
||||||
|
foreach (['machine', 'composant', 'piece', 'product'] as $candidate) {
|
||||||
|
$key = $candidate.'Id';
|
||||||
|
if (!isset($payload[$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$entityType = $candidate;
|
||||||
|
$entityId = trim((string) $payload[$key]);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('' === $entityType || '' === $entityId) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Entity target is missing.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($entityType) {
|
||||||
|
'machine' => $this->resolveEntity('machine', $entityId, $this->machineRepository),
|
||||||
|
'composant' => $this->resolveEntity('composant', $entityId, $this->composantRepository),
|
||||||
|
'piece' => $this->resolveEntity('piece', $entityId, $this->pieceRepository),
|
||||||
|
'product' => $this->resolveEntity('product', $entityId, $this->productRepository),
|
||||||
|
default => $this->json(['success' => false, 'error' => 'Unsupported entity type.'], 400),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveEntity(string $type, string $id, $repository): array|JsonResponse
|
||||||
|
{
|
||||||
|
$entity = $repository->find($id);
|
||||||
|
if (!$entity) {
|
||||||
|
return $this->json(['success' => false, 'error' => sprintf('%s not found.', $type)], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['type' => $type, 'entity' => $entity];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyTarget(CustomFieldValue $value, string $type, object $entity): void
|
||||||
|
{
|
||||||
|
switch ($type) {
|
||||||
|
case 'machine':
|
||||||
|
$value->setMachine($entity);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'composant':
|
||||||
|
$value->setComposant($entity);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'piece':
|
||||||
|
$value->setPiece($entity);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'product':
|
||||||
|
$value->setProduct($entity);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeCustomFieldValue(CustomFieldValue $value): array
|
||||||
|
{
|
||||||
|
$customField = $value->getCustomField();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $value->getId(),
|
||||||
|
'value' => $value->getValue(),
|
||||||
|
'customFieldId' => $customField->getId(),
|
||||||
|
'customField' => [
|
||||||
|
'id' => $customField->getId(),
|
||||||
|
'name' => $customField->getName(),
|
||||||
|
'type' => $customField->getType(),
|
||||||
|
'required' => $customField->isRequired(),
|
||||||
|
'options' => $customField->getOptions(),
|
||||||
|
'orderIndex' => $customField->getOrderIndex(),
|
||||||
|
],
|
||||||
|
'machineId' => $value->getMachine()?->getId(),
|
||||||
|
'composantId' => $value->getComposant()?->getId(),
|
||||||
|
'pieceId' => $value->getPiece()?->getId(),
|
||||||
|
'productId' => $value->getProduct()?->getId(),
|
||||||
|
'createdAt' => $value->getCreatedAt()->format(DATE_ATOM),
|
||||||
|
'updatedAt' => $value->getUpdatedAt()->format(DATE_ATOM),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/Controller/DocumentQueryController.php
Normal file
129
src/Controller/DocumentQueryController.php
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Document;
|
||||||
|
use App\Repository\ComposantRepository;
|
||||||
|
use App\Repository\DocumentRepository;
|
||||||
|
use App\Repository\MachineRepository;
|
||||||
|
use App\Repository\PieceRepository;
|
||||||
|
use App\Repository\ProductRepository;
|
||||||
|
use App\Repository\SiteRepository;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route('/api/documents')]
|
||||||
|
class DocumentQueryController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly DocumentRepository $documentRepository,
|
||||||
|
private readonly SiteRepository $siteRepository,
|
||||||
|
private readonly MachineRepository $machineRepository,
|
||||||
|
private readonly ComposantRepository $composantRepository,
|
||||||
|
private readonly PieceRepository $pieceRepository,
|
||||||
|
private readonly ProductRepository $productRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/site/{id}', name: 'documents_by_site', methods: ['GET'])]
|
||||||
|
public function listBySite(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$site = $this->siteRepository->find($id);
|
||||||
|
if (!$site) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Site not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$documents = $this->documentRepository->findBy(['site' => $site]);
|
||||||
|
|
||||||
|
return $this->json($this->normalizeDocuments($documents));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/machine/{id}', name: 'documents_by_machine', methods: ['GET'])]
|
||||||
|
public function listByMachine(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$machine = $this->machineRepository->find($id);
|
||||||
|
if (!$machine) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$documents = $this->documentRepository->findBy(['machine' => $machine]);
|
||||||
|
|
||||||
|
return $this->json($this->normalizeDocuments($documents));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/composant/{id}', name: 'documents_by_composant', methods: ['GET'])]
|
||||||
|
public function listByComposant(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$composant = $this->composantRepository->find($id);
|
||||||
|
if (!$composant) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Composant not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$documents = $this->documentRepository->findBy(['composant' => $composant]);
|
||||||
|
|
||||||
|
return $this->json($this->normalizeDocuments($documents));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/piece/{id}', name: 'documents_by_piece', methods: ['GET'])]
|
||||||
|
public function listByPiece(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$piece = $this->pieceRepository->find($id);
|
||||||
|
if (!$piece) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Piece not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$documents = $this->documentRepository->findBy(['piece' => $piece]);
|
||||||
|
|
||||||
|
return $this->json($this->normalizeDocuments($documents));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/product/{id}', name: 'documents_by_product', methods: ['GET'])]
|
||||||
|
public function listByProduct(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$product = $this->productRepository->find($id);
|
||||||
|
if (!$product) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Product not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$documents = $this->documentRepository->findBy(['product' => $product]);
|
||||||
|
|
||||||
|
return $this->json($this->normalizeDocuments($documents));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Document[] $documents
|
||||||
|
*/
|
||||||
|
private function normalizeDocuments(array $documents): array
|
||||||
|
{
|
||||||
|
return array_map(static function (Document $document): array {
|
||||||
|
return [
|
||||||
|
'id' => $document->getId(),
|
||||||
|
'name' => $document->getName(),
|
||||||
|
'filename' => $document->getFilename(),
|
||||||
|
'fileUrl' => '/api/documents/'.$document->getId().'/file',
|
||||||
|
'downloadUrl' => '/api/documents/'.$document->getId().'/download',
|
||||||
|
'mimeType' => $document->getMimeType(),
|
||||||
|
'size' => $document->getSize(),
|
||||||
|
'siteId' => $document->getSite()?->getId(),
|
||||||
|
'machineId' => $document->getMachine()?->getId(),
|
||||||
|
'composantId' => $document->getComposant()?->getId(),
|
||||||
|
'pieceId' => $document->getPiece()?->getId(),
|
||||||
|
'productId' => $document->getProduct()?->getId(),
|
||||||
|
'createdAt' => $document->getCreatedAt()->format(DATE_ATOM),
|
||||||
|
'updatedAt' => $document->getUpdatedAt()->format(DATE_ATOM),
|
||||||
|
];
|
||||||
|
}, $documents);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/Controller/DocumentServeController.php
Normal file
109
src/Controller/DocumentServeController.php
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\DocumentRepository;
|
||||||
|
use App\Service\DocumentStorageService;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
use function strlen;
|
||||||
|
|
||||||
|
#[Route('/api/documents')]
|
||||||
|
class DocumentServeController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly DocumentRepository $documentRepository,
|
||||||
|
private readonly DocumentStorageService $storageService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/{id}/file', name: 'document_serve_file', methods: ['GET'])]
|
||||||
|
public function serve(string $id): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$document = $this->documentRepository->find($id);
|
||||||
|
if (!$document) {
|
||||||
|
return $this->json(['error' => 'Document not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $document->getPath();
|
||||||
|
|
||||||
|
// Backward compatibility: serve Base64 data URIs from DB
|
||||||
|
if ($this->storageService->isBase64DataUri($path)) {
|
||||||
|
$parts = explode(',', $path, 2);
|
||||||
|
$content = base64_decode($parts[1] ?? '', true);
|
||||||
|
if (false === $content) {
|
||||||
|
return $this->json(['error' => 'Invalid document data.'], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response($content, 200, [
|
||||||
|
'Content-Type' => $document->getMimeType(),
|
||||||
|
'Content-Disposition' => ResponseHeaderBag::DISPOSITION_INLINE.'; filename="'.$document->getFilename().'"',
|
||||||
|
'Content-Length' => (string) strlen($content),
|
||||||
|
'Cache-Control' => 'private, max-age=3600',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File-based path: serve from disk
|
||||||
|
$absolutePath = $this->storageService->getAbsolutePath($path);
|
||||||
|
if (!file_exists($absolutePath)) {
|
||||||
|
return $this->json(['error' => 'File not found on disk.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = new BinaryFileResponse($absolutePath);
|
||||||
|
$response->headers->set('Content-Type', $document->getMimeType());
|
||||||
|
$response->setContentDisposition(
|
||||||
|
ResponseHeaderBag::DISPOSITION_INLINE,
|
||||||
|
$document->getFilename()
|
||||||
|
);
|
||||||
|
$response->headers->set('Cache-Control', 'private, max-age=3600');
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}/download', name: 'document_download_file', methods: ['GET'])]
|
||||||
|
public function download(string $id): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$document = $this->documentRepository->find($id);
|
||||||
|
if (!$document) {
|
||||||
|
return $this->json(['error' => 'Document not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $document->getPath();
|
||||||
|
|
||||||
|
if ($this->storageService->isBase64DataUri($path)) {
|
||||||
|
$parts = explode(',', $path, 2);
|
||||||
|
$content = base64_decode($parts[1] ?? '', true);
|
||||||
|
if (false === $content) {
|
||||||
|
return $this->json(['error' => 'Invalid document data.'], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response($content, 200, [
|
||||||
|
'Content-Type' => 'application/octet-stream',
|
||||||
|
'Content-Disposition' => ResponseHeaderBag::DISPOSITION_ATTACHMENT.'; filename="'.$document->getFilename().'"',
|
||||||
|
'Content-Length' => (string) strlen($content),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$absolutePath = $this->storageService->getAbsolutePath($path);
|
||||||
|
if (!file_exists($absolutePath)) {
|
||||||
|
return $this->json(['error' => 'File not found on disk.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = new BinaryFileResponse($absolutePath);
|
||||||
|
$response->setContentDisposition(
|
||||||
|
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
||||||
|
$document->getFilename()
|
||||||
|
);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/Controller/MachineCustomFieldsController.php
Normal file
77
src/Controller/MachineCustomFieldsController.php
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\CustomField;
|
||||||
|
use App\Entity\CustomFieldValue;
|
||||||
|
use App\Entity\Machine;
|
||||||
|
use App\Repository\CustomFieldValueRepository;
|
||||||
|
use App\Repository\MachineRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route('/api/machines')]
|
||||||
|
class MachineCustomFieldsController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly MachineRepository $machineRepository,
|
||||||
|
private readonly CustomFieldValueRepository $customFieldValueRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/{id}/add-custom-fields', name: 'machine_add_custom_fields', methods: ['POST'])]
|
||||||
|
public function addMissingCustomFields(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
|
$machine = $this->machineRepository->find($id);
|
||||||
|
if (!$machine instanceof Machine) {
|
||||||
|
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) {
|
||||||
|
if (!$customField instanceof CustomField) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$existing = $this->customFieldValueRepository->findOneBy([
|
||||||
|
'machine' => $machine,
|
||||||
|
'customField' => $customField,
|
||||||
|
]);
|
||||||
|
if ($existing instanceof CustomFieldValue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = new CustomFieldValue();
|
||||||
|
$value->setMachine($machine);
|
||||||
|
$value->setCustomField($customField);
|
||||||
|
$value->setValue($customField->getDefaultValue() ?? '');
|
||||||
|
$this->entityManager->persist($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$values = $this->customFieldValueRepository->findBy(['machine' => $machine]);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'success' => true,
|
||||||
|
'machineId' => $machine->getId(),
|
||||||
|
'customFieldValues' => array_map(
|
||||||
|
static fn (CustomFieldValue $value) => [
|
||||||
|
'id' => $value->getId(),
|
||||||
|
'value' => $value->getValue(),
|
||||||
|
'customFieldId' => $value->getCustomField()->getId(),
|
||||||
|
],
|
||||||
|
$values
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/Controller/MachineHistoryController.php
Normal file
82
src/Controller/MachineHistoryController.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\AuditLogRepository;
|
||||||
|
use App\Repository\MachineRepository;
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
final class MachineHistoryController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly MachineRepository $machines,
|
||||||
|
private readonly AuditLogRepository $auditLogs,
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/machines/{id}/history', name: 'api_machine_history', methods: ['GET'])]
|
||||||
|
public function __invoke(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$machine = $this->machines->find($id);
|
||||||
|
if (!$machine) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Machine introuvable.'],
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = $this->auditLogs->findEntityHistory('machine', $id, 200);
|
||||||
|
|
||||||
|
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn ($log) => $log->getActorProfileId(),
|
||||||
|
$logs,
|
||||||
|
))));
|
||||||
|
|
||||||
|
$actorMap = [];
|
||||||
|
if ([] !== $actorIds) {
|
||||||
|
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||||
|
foreach ($profiles as $profile) {
|
||||||
|
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
|
if ('' === $label) {
|
||||||
|
$label = $profile->getEmail() ?? $profile->getId();
|
||||||
|
}
|
||||||
|
$actorMap[$profile->getId()] = $label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = array_map(
|
||||||
|
static function ($log) use ($actorMap) {
|
||||||
|
$actorId = $log->getActorProfileId();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $log->getId(),
|
||||||
|
'action' => $log->getAction(),
|
||||||
|
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
'actor' => $actorId
|
||||||
|
? [
|
||||||
|
'id' => $actorId,
|
||||||
|
'label' => $actorMap[$actorId] ?? $actorId,
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
'diff' => $log->getDiff(),
|
||||||
|
'snapshot' => $log->getSnapshot(),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
$logs,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'items' => array_values($items),
|
||||||
|
'total' => count($items),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
770
src/Controller/MachineSkeletonController.php
Normal file
770
src/Controller/MachineSkeletonController.php
Normal file
@@ -0,0 +1,770 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Composant;
|
||||||
|
use App\Entity\CustomField;
|
||||||
|
use App\Entity\Machine;
|
||||||
|
use App\Entity\MachineComponentLink;
|
||||||
|
use App\Entity\MachinePieceLink;
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route('/api/machines')]
|
||||||
|
class MachineSkeletonController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly MachineRepository $machineRepository,
|
||||||
|
private readonly MachineComponentLinkRepository $machineComponentLinkRepository,
|
||||||
|
private readonly MachinePieceLinkRepository $machinePieceLinkRepository,
|
||||||
|
private readonly MachineProductLinkRepository $machineProductLinkRepository,
|
||||||
|
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
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$machine = $this->machineRepository->find($id);
|
||||||
|
if (!$machine instanceof Machine) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$componentLinks = $this->machineComponentLinkRepository->findBy(['machine' => $machine]);
|
||||||
|
$pieceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $machine]);
|
||||||
|
$productLinks = $this->machineProductLinkRepository->findBy(['machine' => $machine]);
|
||||||
|
|
||||||
|
return $this->json($this->normalizeMachineSkeletonResponse(
|
||||||
|
$machine,
|
||||||
|
$componentLinks,
|
||||||
|
$pieceLinks,
|
||||||
|
$productLinks
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}/skeleton', name: 'machine_skeleton_update', methods: ['PATCH'])]
|
||||||
|
public function updateSkeleton(string $id, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
|
$machine = $this->machineRepository->find($id);
|
||||||
|
if (!$machine instanceof Machine) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = json_decode($request->getContent(), true);
|
||||||
|
if (!is_array($payload)) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Invalid JSON payload.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$componentLinksPayload = $this->normalizePayloadList($payload['componentLinks'] ?? []);
|
||||||
|
$pieceLinksPayload = $this->normalizePayloadList($payload['pieceLinks'] ?? []);
|
||||||
|
$productLinksPayload = $this->normalizePayloadList($payload['productLinks'] ?? []);
|
||||||
|
|
||||||
|
$componentLinks = $this->applyComponentLinks($machine, $componentLinksPayload);
|
||||||
|
if ($componentLinks instanceof JsonResponse) {
|
||||||
|
return $componentLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pieceLinks = $this->applyPieceLinks($machine, $pieceLinksPayload, $componentLinks);
|
||||||
|
if ($pieceLinks instanceof JsonResponse) {
|
||||||
|
return $pieceLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
$productLinks = $this->applyProductLinks($machine, $productLinksPayload, $componentLinks, $pieceLinks);
|
||||||
|
if ($productLinks instanceof JsonResponse) {
|
||||||
|
return $productLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $this->json($this->normalizeMachineSkeletonResponse(
|
||||||
|
$machine,
|
||||||
|
$componentLinks,
|
||||||
|
$pieceLinks,
|
||||||
|
$productLinks
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePayloadList(mixed $value): array
|
||||||
|
{
|
||||||
|
if (!is_array($value)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_filter($value, static fn ($item) => is_array($item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyComponentLinks(Machine $machine, array $payload): array|JsonResponse
|
||||||
|
{
|
||||||
|
$existing = $this->indexLinksById($this->machineComponentLinkRepository->findBy(['machine' => $machine]));
|
||||||
|
$keepIds = [];
|
||||||
|
$pendingParents = [];
|
||||||
|
$links = [];
|
||||||
|
|
||||||
|
foreach ($payload as $entry) {
|
||||||
|
$linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
|
||||||
|
$link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineComponentLink();
|
||||||
|
if (!$linkId) {
|
||||||
|
$linkId = $this->generateCuid();
|
||||||
|
}
|
||||||
|
if (!$link->getId()) {
|
||||||
|
$link->setId($linkId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$composantId = $this->resolveIdentifier($entry, ['composantId', 'componentId', 'idComposant']);
|
||||||
|
if (!$composantId) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Composant requis pour le squelette.'], 400);
|
||||||
|
}
|
||||||
|
$composant = $this->composantRepository->find($composantId);
|
||||||
|
if (!$composant instanceof Composant) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Composant introuvable.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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, [
|
||||||
|
'parentComponentLinkId',
|
||||||
|
'parentLinkId',
|
||||||
|
'parentMachineComponentLinkId',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->entityManager->persist($link);
|
||||||
|
$links[$linkId] = $link;
|
||||||
|
$keepIds[] = $linkId;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pendingParents as $linkId => $parentId) {
|
||||||
|
if (!$parentId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isset($links[$linkId])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parent = $links[$parentId] ?? $existing[$parentId] ?? null;
|
||||||
|
if ($parent instanceof MachineComponentLink) {
|
||||||
|
$links[$linkId]->setParentLink($parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->removeMissingLinks($existing, $keepIds);
|
||||||
|
|
||||||
|
return array_values($links);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyPieceLinks(Machine $machine, array $payload, array $componentLinks): array|JsonResponse
|
||||||
|
{
|
||||||
|
$existing = $this->indexLinksById($this->machinePieceLinkRepository->findBy(['machine' => $machine]));
|
||||||
|
$componentIndex = $this->indexLinksById($componentLinks);
|
||||||
|
$keepIds = [];
|
||||||
|
$pendingParents = [];
|
||||||
|
$links = [];
|
||||||
|
|
||||||
|
foreach ($payload as $entry) {
|
||||||
|
$linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
|
||||||
|
$link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachinePieceLink();
|
||||||
|
if (!$linkId) {
|
||||||
|
$linkId = $this->generateCuid();
|
||||||
|
}
|
||||||
|
if (!$link->getId()) {
|
||||||
|
$link->setId($linkId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pieceId = $this->resolveIdentifier($entry, ['pieceId']);
|
||||||
|
if (!$pieceId) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Pièce requise pour le squelette.'], 400);
|
||||||
|
}
|
||||||
|
$piece = $this->pieceRepository->find($pieceId);
|
||||||
|
if (!$piece instanceof Piece) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Pièce introuvable.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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, [
|
||||||
|
'parentComponentLinkId',
|
||||||
|
'parentLinkId',
|
||||||
|
'parentMachineComponentLinkId',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->entityManager->persist($link);
|
||||||
|
$links[$linkId] = $link;
|
||||||
|
$keepIds[] = $linkId;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pendingParents as $linkId => $parentId) {
|
||||||
|
if (!$parentId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isset($links[$linkId])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parent = $componentIndex[$parentId] ?? null;
|
||||||
|
if ($parent instanceof MachineComponentLink) {
|
||||||
|
$links[$linkId]->setParentLink($parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->removeMissingLinks($existing, $keepIds);
|
||||||
|
|
||||||
|
return array_values($links);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyProductLinks(
|
||||||
|
Machine $machine,
|
||||||
|
array $payload,
|
||||||
|
array $componentLinks,
|
||||||
|
array $pieceLinks,
|
||||||
|
): array|JsonResponse {
|
||||||
|
$existing = $this->indexLinksById($this->machineProductLinkRepository->findBy(['machine' => $machine]));
|
||||||
|
$componentIndex = $this->indexLinksById($componentLinks);
|
||||||
|
$pieceIndex = $this->indexLinksById($pieceLinks);
|
||||||
|
$keepIds = [];
|
||||||
|
$pendingParents = [];
|
||||||
|
$links = [];
|
||||||
|
|
||||||
|
foreach ($payload as $entry) {
|
||||||
|
$linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
|
||||||
|
$link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineProductLink();
|
||||||
|
if (!$linkId) {
|
||||||
|
$linkId = $this->generateCuid();
|
||||||
|
}
|
||||||
|
if (!$link->getId()) {
|
||||||
|
$link->setId($linkId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$productId = $this->resolveIdentifier($entry, ['productId']);
|
||||||
|
if (!$productId) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Produit requis pour le squelette.'], 400);
|
||||||
|
}
|
||||||
|
$product = $this->productRepository->find($productId);
|
||||||
|
if (!$product instanceof Product) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Produit introuvable.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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']),
|
||||||
|
'parentLinkId' => $this->resolveIdentifier($entry, ['parentLinkId']),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->entityManager->persist($link);
|
||||||
|
$links[$linkId] = $link;
|
||||||
|
$keepIds[] = $linkId;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pendingParents as $linkId => $parentIds) {
|
||||||
|
if (!isset($links[$linkId])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!empty($parentIds['parentComponentLinkId']) && isset($componentIndex[$parentIds['parentComponentLinkId']])) {
|
||||||
|
$links[$linkId]->setParentComponentLink($componentIndex[$parentIds['parentComponentLinkId']]);
|
||||||
|
}
|
||||||
|
if (!empty($parentIds['parentPieceLinkId']) && isset($pieceIndex[$parentIds['parentPieceLinkId']])) {
|
||||||
|
$links[$linkId]->setParentPieceLink($pieceIndex[$parentIds['parentPieceLinkId']]);
|
||||||
|
}
|
||||||
|
if (!empty($parentIds['parentLinkId']) && isset($links[$parentIds['parentLinkId']])) {
|
||||||
|
$links[$linkId]->setParentLink($links[$parentIds['parentLinkId']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->removeMissingLinks($existing, $keepIds);
|
||||||
|
|
||||||
|
return array_values($links);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeMachineSkeletonResponse(
|
||||||
|
Machine $machine,
|
||||||
|
array $componentLinks,
|
||||||
|
array $pieceLinks,
|
||||||
|
array $productLinks,
|
||||||
|
): array {
|
||||||
|
$normalizedComponentLinks = $this->normalizeComponentLinks($componentLinks);
|
||||||
|
$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;
|
||||||
|
if ($parentId && isset($componentIndex[$parentId])) {
|
||||||
|
$componentIndex[$parentId]['childLinks'][] = $link;
|
||||||
|
$childIds[$link['id']] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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']]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'machine' => $this->normalizeMachine($machine),
|
||||||
|
'componentLinks' => array_values($rootComponents),
|
||||||
|
'pieceLinks' => $normalizedPieceLinks,
|
||||||
|
'productLinks' => $this->normalizeProductLinks($productLinks),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function attachPiecesToComponents(array &$componentIndex, array $pieceLinks): void
|
||||||
|
{
|
||||||
|
foreach ($pieceLinks as $pieceLink) {
|
||||||
|
$parentId = $pieceLink['parentComponentLinkId'] ?? null;
|
||||||
|
if ($parentId && isset($componentIndex[$parentId])) {
|
||||||
|
$componentIndex[$parentId]['pieceLinks'][] = $pieceLink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively attach to child components
|
||||||
|
foreach ($componentIndex as &$component) {
|
||||||
|
if (!empty($component['childLinks'])) {
|
||||||
|
$this->attachPiecesToChildComponents($component['childLinks'], $pieceLinks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function attachPiecesToChildComponents(array &$childLinks, array $pieceLinks): void
|
||||||
|
{
|
||||||
|
foreach ($childLinks as &$child) {
|
||||||
|
$childId = $child['id'] ?? $child['linkId'] ?? null;
|
||||||
|
if ($childId) {
|
||||||
|
foreach ($pieceLinks as $pieceLink) {
|
||||||
|
$parentId = $pieceLink['parentComponentLinkId'] ?? null;
|
||||||
|
if ($parentId === $childId) {
|
||||||
|
$child['pieceLinks'][] = $pieceLink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively process nested children
|
||||||
|
if (!empty($child['childLinks'])) {
|
||||||
|
$this->attachPiecesToChildComponents($child['childLinks'], $pieceLinks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeMachine(Machine $machine): array
|
||||||
|
{
|
||||||
|
$site = $machine->getSite();
|
||||||
|
$typeMachine = $machine->getTypeMachine();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $machine->getId(),
|
||||||
|
'name' => $machine->getName(),
|
||||||
|
'reference' => $machine->getReference(),
|
||||||
|
'prix' => $machine->getPrix(),
|
||||||
|
'siteId' => $site->getId(),
|
||||||
|
'site' => [
|
||||||
|
'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()),
|
||||||
|
'documents' => null,
|
||||||
|
'customFieldValues' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeCustomFields(Collection $customFields): array
|
||||||
|
{
|
||||||
|
$items = [];
|
||||||
|
foreach ($customFields as $customField) {
|
||||||
|
if (!$customField instanceof CustomField) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$items[] = [
|
||||||
|
'id' => $customField->getId(),
|
||||||
|
'name' => $customField->getName(),
|
||||||
|
'type' => $customField->getType(),
|
||||||
|
'required' => $customField->isRequired(),
|
||||||
|
'options' => $customField->getOptions(),
|
||||||
|
'defaultValue' => $customField->getDefaultValue(),
|
||||||
|
'orderIndex' => $customField->getOrderIndex(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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' => [],
|
||||||
|
];
|
||||||
|
}, $links);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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),
|
||||||
|
];
|
||||||
|
}, $links);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeProductLinks(array $links): array
|
||||||
|
{
|
||||||
|
return array_map(function (MachineProductLink $link): array {
|
||||||
|
$product = $link->getProduct();
|
||||||
|
$requirement = $link->getTypeMachineProductRequirement();
|
||||||
|
|
||||||
|
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(),
|
||||||
|
];
|
||||||
|
}, $links);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeComposant(Composant $composant): array
|
||||||
|
{
|
||||||
|
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' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePiece(Piece $piece): array
|
||||||
|
{
|
||||||
|
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' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeProduct(Product $product): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $product->getId(),
|
||||||
|
'name' => $product->getName(),
|
||||||
|
'reference' => $product->getReference(),
|
||||||
|
'supplierPrice' => $product->getSupplierPrice(),
|
||||||
|
'typeProductId' => $product->getTypeProduct()?->getId(),
|
||||||
|
'typeProduct' => $this->normalizeModelType($product->getTypeProduct()),
|
||||||
|
'constructeurs' => $this->normalizeConstructeurs($product->getConstructeurs()),
|
||||||
|
'documents' => [],
|
||||||
|
'customFields' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeModelType(?ModelType $type): ?array
|
||||||
|
{
|
||||||
|
if (!$type instanceof ModelType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeConstructeurs(Collection $constructeurs): array
|
||||||
|
{
|
||||||
|
$items = [];
|
||||||
|
foreach ($constructeurs as $constructeur) {
|
||||||
|
$items[] = [
|
||||||
|
'id' => $constructeur->getId(),
|
||||||
|
'name' => $constructeur->getName(),
|
||||||
|
'email' => $constructeur->getEmail(),
|
||||||
|
'phone' => $constructeur->getPhone(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeOverrides(object $link): ?array
|
||||||
|
{
|
||||||
|
$name = method_exists($link, 'getNameOverride') ? $link->getNameOverride() : null;
|
||||||
|
$reference = method_exists($link, 'getReferenceOverride') ? $link->getReferenceOverride() : null;
|
||||||
|
$prix = method_exists($link, 'getPrixOverride') ? $link->getPrixOverride() : null;
|
||||||
|
|
||||||
|
if (null === $name && null === $reference && null === $prix) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $name,
|
||||||
|
'reference' => $reference,
|
||||||
|
'prix' => $prix,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyOverrides(object $link, mixed $overrides): void
|
||||||
|
{
|
||||||
|
if (!is_array($overrides)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('name', $overrides) && method_exists($link, 'setNameOverride')) {
|
||||||
|
$link->setNameOverride($this->stringOrNull($overrides['name']));
|
||||||
|
}
|
||||||
|
if (array_key_exists('reference', $overrides) && method_exists($link, 'setReferenceOverride')) {
|
||||||
|
$link->setReferenceOverride($this->stringOrNull($overrides['reference']));
|
||||||
|
}
|
||||||
|
if (array_key_exists('prix', $overrides) && method_exists($link, 'setPrixOverride')) {
|
||||||
|
$link->setPrixOverride($this->stringOrNull($overrides['prix']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function stringOrNull(mixed $value): ?string
|
||||||
|
{
|
||||||
|
if (null === $value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$string = trim((string) $value);
|
||||||
|
|
||||||
|
return '' === $string ? null : $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveIdentifier(array $entry, array $keys): ?string
|
||||||
|
{
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (!array_key_exists($key, $entry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$value = $entry[$key];
|
||||||
|
if (null === $value || '' === $value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<array-key, object> $links
|
||||||
|
*
|
||||||
|
* @return array<string, object>
|
||||||
|
*/
|
||||||
|
private function indexLinksById(array $links): array
|
||||||
|
{
|
||||||
|
$indexed = [];
|
||||||
|
foreach ($links as $link) {
|
||||||
|
if (method_exists($link, 'getId') && $link->getId()) {
|
||||||
|
$indexed[$link->getId()] = $link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $indexed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function indexNormalizedLinks(array $links): array
|
||||||
|
{
|
||||||
|
$indexed = [];
|
||||||
|
foreach ($links as $link) {
|
||||||
|
if (is_array($link) && isset($link['id'])) {
|
||||||
|
$indexed[$link['id']] = $link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $indexed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeMissingLinks(array $existing, array $keepIds): void
|
||||||
|
{
|
||||||
|
$keep = array_flip($keepIds);
|
||||||
|
foreach ($existing as $link) {
|
||||||
|
if (!method_exists($link, 'getId')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$id = $link->getId();
|
||||||
|
if ($id && !isset($keep[$id])) {
|
||||||
|
$this->entityManager->remove($link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/Controller/ModelTypeConversionController.php
Normal file
60
src/Controller/ModelTypeConversionController.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\ModelTypeRepository;
|
||||||
|
use App\Service\ModelTypeCategoryConversionService;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
final class ModelTypeConversionController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ModelTypeRepository $modelTypes,
|
||||||
|
private readonly ModelTypeCategoryConversionService $conversionService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/model_types/{id}/conversion-check', name: 'api_model_type_conversion_check', methods: ['GET'])]
|
||||||
|
public function check(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$modelType = $this->modelTypes->find($id);
|
||||||
|
|
||||||
|
if (!$modelType) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Catégorie introuvable.'],
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse($this->conversionService->checkConversion($id));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/model_types/{id}/convert', name: 'api_model_type_convert', methods: ['POST'])]
|
||||||
|
public function convert(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||||
|
|
||||||
|
$modelType = $this->modelTypes->find($id);
|
||||||
|
|
||||||
|
if (!$modelType) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Catégorie introuvable.'],
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->conversionService->convert($id);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
return new JsonResponse($result, Response::HTTP_CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/Controller/PieceHistoryController.php
Normal file
82
src/Controller/PieceHistoryController.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\AuditLogRepository;
|
||||||
|
use App\Repository\PieceRepository;
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
final class PieceHistoryController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly PieceRepository $pieces,
|
||||||
|
private readonly AuditLogRepository $auditLogs,
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/pieces/{id}/history', name: 'api_piece_history', methods: ['GET'])]
|
||||||
|
public function __invoke(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$piece = $this->pieces->find($id);
|
||||||
|
if (!$piece) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Pièce introuvable.'],
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = $this->auditLogs->findEntityHistory('piece', $id, 200);
|
||||||
|
|
||||||
|
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn ($log) => $log->getActorProfileId(),
|
||||||
|
$logs,
|
||||||
|
))));
|
||||||
|
|
||||||
|
$actorMap = [];
|
||||||
|
if ([] !== $actorIds) {
|
||||||
|
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||||
|
foreach ($profiles as $profile) {
|
||||||
|
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
|
if ('' === $label) {
|
||||||
|
$label = $profile->getEmail() ?? $profile->getId();
|
||||||
|
}
|
||||||
|
$actorMap[$profile->getId()] = $label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = array_map(
|
||||||
|
static function ($log) use ($actorMap) {
|
||||||
|
$actorId = $log->getActorProfileId();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $log->getId(),
|
||||||
|
'action' => $log->getAction(),
|
||||||
|
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
'actor' => $actorId
|
||||||
|
? [
|
||||||
|
'id' => $actorId,
|
||||||
|
'label' => $actorMap[$actorId] ?? $actorId,
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
'diff' => $log->getDiff(),
|
||||||
|
'snapshot' => $log->getSnapshot(),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
$logs,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'items' => array_values($items),
|
||||||
|
'total' => count($items),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/Controller/ProductHistoryController.php
Normal file
82
src/Controller/ProductHistoryController.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\AuditLogRepository;
|
||||||
|
use App\Repository\ProductRepository;
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
final class ProductHistoryController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProductRepository $products,
|
||||||
|
private readonly AuditLogRepository $auditLogs,
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/products/{id}/history', name: 'api_product_history', methods: ['GET'])]
|
||||||
|
public function __invoke(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$product = $this->products->find($id);
|
||||||
|
if (!$product) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Produit introuvable.'],
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = $this->auditLogs->findEntityHistory('product', $id, 200);
|
||||||
|
|
||||||
|
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn ($log) => $log->getActorProfileId(),
|
||||||
|
$logs,
|
||||||
|
))));
|
||||||
|
|
||||||
|
$actorMap = [];
|
||||||
|
if ([] !== $actorIds) {
|
||||||
|
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||||
|
foreach ($profiles as $profile) {
|
||||||
|
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
|
if ('' === $label) {
|
||||||
|
$label = $profile->getEmail() ?? $profile->getId();
|
||||||
|
}
|
||||||
|
$actorMap[$profile->getId()] = $label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = array_map(
|
||||||
|
static function ($log) use ($actorMap) {
|
||||||
|
$actorId = $log->getActorProfileId();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $log->getId(),
|
||||||
|
'action' => $log->getAction(),
|
||||||
|
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
'actor' => $actorId
|
||||||
|
? [
|
||||||
|
'id' => $actorId,
|
||||||
|
'label' => $actorMap[$actorId] ?? $actorId,
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
'diff' => $log->getDiff(),
|
||||||
|
'snapshot' => $log->getSnapshot(),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
$logs,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'items' => array_values($items),
|
||||||
|
'total' => count($items),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/Controller/SessionProfileController.php
Normal file
110
src/Controller/SessionProfileController.php
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
final class SessionProfileController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/session/profile', name: 'api_session_profile_get', methods: ['GET'])]
|
||||||
|
public function getActiveProfile(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$session = $request->getSession();
|
||||||
|
if (!$session instanceof SessionInterface) {
|
||||||
|
return new JsonResponse(['message' => 'Session indisponible.'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if (!$profileId) {
|
||||||
|
return new JsonResponse(['message' => 'Aucun profil actif.'], JsonResponse::HTTP_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile = $this->profiles->find($profileId);
|
||||||
|
if (!$profile || !$profile->isActive()) {
|
||||||
|
$session->remove('profileId');
|
||||||
|
|
||||||
|
return new JsonResponse(['message' => 'Profil introuvable ou inactif.'], JsonResponse::HTTP_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'id' => $profile->getId(),
|
||||||
|
'firstName' => $profile->getFirstName(),
|
||||||
|
'lastName' => $profile->getLastName(),
|
||||||
|
'email' => $profile->getEmail(),
|
||||||
|
'isActive' => $profile->isActive(),
|
||||||
|
'roles' => $profile->getRoles(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/session/profile', name: 'api_session_profile_post', methods: ['POST'])]
|
||||||
|
public function activateProfile(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$session = $request->getSession();
|
||||||
|
if (!$session instanceof SessionInterface) {
|
||||||
|
return new JsonResponse(['message' => 'Session indisponible.'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $request->toArray();
|
||||||
|
$profileId = $payload['profileId'] ?? null;
|
||||||
|
|
||||||
|
if (!$profileId) {
|
||||||
|
return new JsonResponse(['message' => 'profileId est requis.'], JsonResponse::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile = $this->profiles->find($profileId);
|
||||||
|
if (!$profile || !$profile->isActive()) {
|
||||||
|
return new JsonResponse(['message' => 'Profil introuvable ou inactif.'], JsonResponse::HTTP_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
$password = $payload['password'] ?? '';
|
||||||
|
if ('' === $password) {
|
||||||
|
return new JsonResponse(['message' => 'Mot de passe requis.'], JsonResponse::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$profile->getPassword()) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Ce profil n\'a pas de mot de passe. Contactez un administrateur.'],
|
||||||
|
JsonResponse::HTTP_FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->passwordHasher->isPasswordValid($profile, $password)) {
|
||||||
|
return new JsonResponse(['message' => 'Mot de passe incorrect.'], JsonResponse::HTTP_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
$session->set('profileId', $profile->getId());
|
||||||
|
$session->set('profileRoles', $profile->getRoles());
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'id' => $profile->getId(),
|
||||||
|
'firstName' => $profile->getFirstName(),
|
||||||
|
'lastName' => $profile->getLastName(),
|
||||||
|
'email' => $profile->getEmail(),
|
||||||
|
'isActive' => $profile->isActive(),
|
||||||
|
'roles' => $profile->getRoles(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/session/profile', name: 'api_session_profile_delete', methods: ['DELETE'])]
|
||||||
|
public function logout(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$session = $request->getSession();
|
||||||
|
if ($session instanceof SessionInterface) {
|
||||||
|
$session->invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse(['success' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/Controller/SessionProfilesController.php
Normal file
37
src/Controller/SessionProfilesController.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
final class SessionProfilesController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/session/profiles', name: 'api_session_profiles_list', methods: ['GET'])]
|
||||||
|
public function list(): JsonResponse
|
||||||
|
{
|
||||||
|
$items = $this->profiles->createQueryBuilder('p')
|
||||||
|
->andWhere('p.isActive = :active')
|
||||||
|
->setParameter('active', true)
|
||||||
|
->orderBy('p.firstName', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult()
|
||||||
|
;
|
||||||
|
|
||||||
|
return new JsonResponse(array_map(static function ($profile): array {
|
||||||
|
return [
|
||||||
|
'id' => $profile->getId(),
|
||||||
|
'firstName' => $profile->getFirstName(),
|
||||||
|
'lastName' => $profile->getLastName(),
|
||||||
|
'hasPassword' => null !== $profile->getPassword() && '' !== $profile->getPassword(),
|
||||||
|
];
|
||||||
|
}, $items));
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/Controller/TestController.php
Normal file
18
src/Controller/TestController.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
class TestController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/api/test', name: 'api_test', methods: ['GET', 'POST'])]
|
||||||
|
public function test(): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->json(['status' => 'ok', 'message' => 'Test endpoint works!']);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/Doctrine/QuoteStrategy/AlwaysQuoteStrategy.php
Normal file
111
src/Doctrine/QuoteStrategy/AlwaysQuoteStrategy.php
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Doctrine\QuoteStrategy;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||||
|
use Doctrine\ORM\Internal\SQLResultCasing;
|
||||||
|
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||||
|
use Doctrine\ORM\Mapping\JoinColumnMapping;
|
||||||
|
use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping;
|
||||||
|
use Doctrine\ORM\Mapping\QuoteStrategy;
|
||||||
|
|
||||||
|
use function array_map;
|
||||||
|
use function array_merge;
|
||||||
|
use function assert;
|
||||||
|
use function explode;
|
||||||
|
use function implode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quote all identifiers to preserve camelCase column names in Postgres.
|
||||||
|
*/
|
||||||
|
final class AlwaysQuoteStrategy implements QuoteStrategy
|
||||||
|
{
|
||||||
|
use SQLResultCasing;
|
||||||
|
|
||||||
|
public function getColumnName(string $fieldName, ClassMetadata $class, AbstractPlatform $platform): string
|
||||||
|
{
|
||||||
|
return $platform->quoteSingleIdentifier($class->fieldMappings[$fieldName]->columnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTableName(ClassMetadata $class, AbstractPlatform $platform): string
|
||||||
|
{
|
||||||
|
$tableName = $platform->quoteSingleIdentifier($class->table['name']);
|
||||||
|
|
||||||
|
if (!empty($class->table['schema'])) {
|
||||||
|
return $platform->quoteSingleIdentifier($class->table['schema']).'.'.$tableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string
|
||||||
|
{
|
||||||
|
return implode('.', array_map(
|
||||||
|
static fn (string $part) => $platform->quoteSingleIdentifier($part),
|
||||||
|
explode('.', $definition['sequenceName']),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getJoinTableName(
|
||||||
|
ManyToManyOwningSideMapping $association,
|
||||||
|
ClassMetadata $class,
|
||||||
|
AbstractPlatform $platform,
|
||||||
|
): string {
|
||||||
|
$schema = '';
|
||||||
|
|
||||||
|
if (isset($association->joinTable->schema)) {
|
||||||
|
$schema = $platform->quoteSingleIdentifier($association->joinTable->schema).'.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema.$platform->quoteSingleIdentifier($association->joinTable->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string
|
||||||
|
{
|
||||||
|
return $platform->quoteSingleIdentifier($joinColumn->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReferencedJoinColumnName(
|
||||||
|
JoinColumnMapping $joinColumn,
|
||||||
|
ClassMetadata $class,
|
||||||
|
AbstractPlatform $platform,
|
||||||
|
): string {
|
||||||
|
return $platform->quoteSingleIdentifier($joinColumn->referencedColumnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform): array
|
||||||
|
{
|
||||||
|
$quotedColumnNames = [];
|
||||||
|
|
||||||
|
foreach ($class->identifier as $fieldName) {
|
||||||
|
if (isset($class->fieldMappings[$fieldName])) {
|
||||||
|
$quotedColumnNames[] = $this->getColumnName($fieldName, $class, $platform);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assoc = $class->associationMappings[$fieldName];
|
||||||
|
assert($assoc->isToOneOwningSide());
|
||||||
|
$joinColumns = $assoc->joinColumns;
|
||||||
|
$assocQuotedColumnNames = array_map(
|
||||||
|
static fn (JoinColumnMapping $joinColumn) => $platform->quoteSingleIdentifier($joinColumn->name),
|
||||||
|
$joinColumns,
|
||||||
|
);
|
||||||
|
|
||||||
|
$quotedColumnNames = array_merge($quotedColumnNames, $assocQuotedColumnNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $quotedColumnNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getColumnAlias(
|
||||||
|
string $columnName,
|
||||||
|
int $counter,
|
||||||
|
AbstractPlatform $platform,
|
||||||
|
?ClassMetadata $class = null,
|
||||||
|
): string {
|
||||||
|
return $this->getSQLResultCasing($platform, $columnName.'_'.$counter);
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/Entity/AuditLog.php
Normal file
117
src/Entity/AuditLog.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\AuditLogRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: AuditLogRepository::class)]
|
||||||
|
#[ORM\Table(name: 'audit_logs')]
|
||||||
|
#[ORM\Index(name: 'idx_audit_entity', columns: ['entityType', 'entityId'])]
|
||||||
|
#[ORM\Index(name: 'idx_audit_created_at', columns: ['createdAt'])]
|
||||||
|
#[ORM\HasLifecycleCallbacks]
|
||||||
|
class AuditLog
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 50)]
|
||||||
|
private string $entityType;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
private string $entityId;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 20)]
|
||||||
|
private string $action;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||||
|
private ?array $diff = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||||
|
private ?array $snapshot = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36, nullable: true)]
|
||||||
|
private ?string $actorProfileId = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $entityType,
|
||||||
|
string $entityId,
|
||||||
|
string $action,
|
||||||
|
?array $diff = null,
|
||||||
|
?array $snapshot = null,
|
||||||
|
?string $actorProfileId = null,
|
||||||
|
) {
|
||||||
|
$this->entityType = $entityType;
|
||||||
|
$this->entityId = $entityId;
|
||||||
|
$this->action = $action;
|
||||||
|
$this->diff = $diff;
|
||||||
|
$this->snapshot = $snapshot;
|
||||||
|
$this->actorProfileId = $actorProfileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ORM\PrePersist]
|
||||||
|
public function initializeAuditLog(): void
|
||||||
|
{
|
||||||
|
if (!isset($this->createdAt)) {
|
||||||
|
$this->createdAt = new DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $this->id) {
|
||||||
|
$this->id = $this->generateCuid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?string
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityType(): string
|
||||||
|
{
|
||||||
|
return $this->entityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityId(): string
|
||||||
|
{
|
||||||
|
return $this->entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAction(): string
|
||||||
|
{
|
||||||
|
return $this->action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDiff(): ?array
|
||||||
|
{
|
||||||
|
return $this->diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSnapshot(): ?array
|
||||||
|
{
|
||||||
|
return $this->snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActorProfileId(): ?string
|
||||||
|
{
|
||||||
|
return $this->actorProfileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
// Keep the same lightweight CUID-like strategy used across the project.
|
||||||
|
return 'cl'.substr(strtolower(base_convert(bin2hex(random_bytes(12)), 16, 36)), 0, 24);
|
||||||
|
}
|
||||||
|
}
|
||||||
235
src/Entity/Comment.php
Normal file
235
src/Entity/Comment.php
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[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'])]
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
],
|
||||||
|
order: ['createdAt' => 'DESC'],
|
||||||
|
paginationClientItemsPerPage: true,
|
||||||
|
paginationMaximumItemsPerPage: 200
|
||||||
|
)]
|
||||||
|
class Comment
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT)]
|
||||||
|
private string $content;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 50, name: 'entity_type')]
|
||||||
|
private string $entityType;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36, name: 'entity_id')]
|
||||||
|
private string $entityId;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'entity_name')]
|
||||||
|
private ?string $entityName = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36, name: 'author_id')]
|
||||||
|
private string $authorId;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, name: 'author_name')]
|
||||||
|
private string $authorName;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 20)]
|
||||||
|
private string $status = 'open';
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36, nullable: true, name: 'resolved_by_id')]
|
||||||
|
private ?string $resolvedById = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'resolved_by_name')]
|
||||||
|
private ?string $resolvedByName = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true, name: 'resolved_at')]
|
||||||
|
private ?DateTimeImmutable $resolvedAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'created_at')]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updated_at')]
|
||||||
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
|
#[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 getContent(): string
|
||||||
|
{
|
||||||
|
return $this->content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setContent(string $content): static
|
||||||
|
{
|
||||||
|
$this->content = $content;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityType(): string
|
||||||
|
{
|
||||||
|
return $this->entityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEntityType(string $entityType): static
|
||||||
|
{
|
||||||
|
$this->entityType = $entityType;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityId(): string
|
||||||
|
{
|
||||||
|
return $this->entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEntityId(string $entityId): static
|
||||||
|
{
|
||||||
|
$this->entityId = $entityId;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityName(): ?string
|
||||||
|
{
|
||||||
|
return $this->entityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEntityName(?string $entityName): static
|
||||||
|
{
|
||||||
|
$this->entityName = $entityName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAuthorId(): string
|
||||||
|
{
|
||||||
|
return $this->authorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAuthorId(string $authorId): static
|
||||||
|
{
|
||||||
|
$this->authorId = $authorId;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAuthorName(): string
|
||||||
|
{
|
||||||
|
return $this->authorName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAuthorName(string $authorName): static
|
||||||
|
{
|
||||||
|
$this->authorName = $authorName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): string
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(string $status): static
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResolvedById(): ?string
|
||||||
|
{
|
||||||
|
return $this->resolvedById;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setResolvedById(?string $resolvedById): static
|
||||||
|
{
|
||||||
|
$this->resolvedById = $resolvedById;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResolvedByName(): ?string
|
||||||
|
{
|
||||||
|
return $this->resolvedByName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setResolvedByName(?string $resolvedByName): static
|
||||||
|
{
|
||||||
|
$this->resolvedByName = $resolvedByName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResolvedAt(): ?DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->resolvedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setResolvedAt(?DateTimeImmutable $resolvedAt): static
|
||||||
|
{
|
||||||
|
$this->resolvedAt = $resolvedAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
312
src/Entity/Composant.php
Normal file
312
src/Entity/Composant.php
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
|
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\ComposantRepository;
|
||||||
|
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\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: ComposantRepository::class)]
|
||||||
|
#[ORM\Table(name: 'composants')]
|
||||||
|
#[ORM\HasLifecycleCallbacks]
|
||||||
|
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeComposant' => 'exact'])]
|
||||||
|
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||||
|
#[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')"),
|
||||||
|
],
|
||||||
|
normalizationContext: ['groups' => ['composant:read']],
|
||||||
|
paginationClientItemsPerPage: true,
|
||||||
|
paginationMaximumItemsPerPage: 200
|
||||||
|
)]
|
||||||
|
class Composant
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
#[Groups(['composant:read', 'document:list'])]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
||||||
|
#[Groups(['composant:read', 'document:list'])]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||||
|
#[Groups(['composant:read'])]
|
||||||
|
private ?string $reference = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
#[Groups(['composant:read'])]
|
||||||
|
private ?string $description = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)]
|
||||||
|
#[Groups(['composant:read'])]
|
||||||
|
private ?string $prix = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||||
|
#[Groups(['composant:read'])]
|
||||||
|
private ?array $structure = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'composants')]
|
||||||
|
#[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: true)]
|
||||||
|
#[Groups(['composant:read'])]
|
||||||
|
private ?ModelType $typeComposant = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'composants')]
|
||||||
|
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||||
|
#[Groups(['composant:read'])]
|
||||||
|
private ?Product $product = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Constructeur>
|
||||||
|
*/
|
||||||
|
#[ORM\ManyToMany(targetEntity: Constructeur::class, inversedBy: 'composants')]
|
||||||
|
#[ORM\JoinTable(
|
||||||
|
name: '_ComposantConstructeurs',
|
||||||
|
joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')],
|
||||||
|
inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||||
|
)]
|
||||||
|
#[Groups(['composant:read'])]
|
||||||
|
private Collection $constructeurs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Document>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'composant', targetEntity: Document::class)]
|
||||||
|
#[Groups(['composant:read'])]
|
||||||
|
private Collection $documents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, CustomFieldValue>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'composant', targetEntity: CustomFieldValue::class)]
|
||||||
|
#[Groups(['composant:read'])]
|
||||||
|
private Collection $customFieldValues;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, MachineComponentLink>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'composant', targetEntity: MachineComponentLink::class)]
|
||||||
|
private Collection $machineLinks;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
|
#[Groups(['composant:read'])]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
|
#[Groups(['composant:read'])]
|
||||||
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->constructeurs = new ArrayCollection();
|
||||||
|
$this->documents = new ArrayCollection();
|
||||||
|
$this->customFieldValues = new ArrayCollection();
|
||||||
|
$this->machineLinks = 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 getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): static
|
||||||
|
{
|
||||||
|
$this->name = mb_strtoupper(mb_substr($name, 0, 1)).mb_substr($name, 1);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReference(): ?string
|
||||||
|
{
|
||||||
|
return $this->reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setReference(?string $reference): static
|
||||||
|
{
|
||||||
|
$this->reference = $reference;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): static
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrix(): ?string
|
||||||
|
{
|
||||||
|
return $this->prix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPrix(?string $prix): static
|
||||||
|
{
|
||||||
|
$this->prix = $prix;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStructure(): ?array
|
||||||
|
{
|
||||||
|
return $this->structure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStructure(?array $structure): static
|
||||||
|
{
|
||||||
|
$this->structure = $structure;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTypeComposant(): ?ModelType
|
||||||
|
{
|
||||||
|
return $this->typeComposant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTypeComposant(?ModelType $typeComposant): static
|
||||||
|
{
|
||||||
|
$this->typeComposant = $typeComposant;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProduct(): ?Product
|
||||||
|
{
|
||||||
|
return $this->product;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setProduct(?Product $product): static
|
||||||
|
{
|
||||||
|
$this->product = $product;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Constructeur>
|
||||||
|
*/
|
||||||
|
public function getConstructeurs(): Collection
|
||||||
|
{
|
||||||
|
return $this->constructeurs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param iterable<Constructeur> $constructeurs
|
||||||
|
*/
|
||||||
|
public function setConstructeurs(iterable $constructeurs): static
|
||||||
|
{
|
||||||
|
$this->constructeurs = new ArrayCollection();
|
||||||
|
|
||||||
|
foreach ($constructeurs as $constructeur) {
|
||||||
|
if ($constructeur instanceof Constructeur && !$this->constructeurs->contains($constructeur)) {
|
||||||
|
$this->constructeurs->add($constructeur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addConstructeur(Constructeur $constructeur): static
|
||||||
|
{
|
||||||
|
if (!$this->constructeurs->contains($constructeur)) {
|
||||||
|
$this->constructeurs->add($constructeur);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeConstructeur(Constructeur $constructeur): static
|
||||||
|
{
|
||||||
|
$this->constructeurs->removeElement($constructeur);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Document>
|
||||||
|
*/
|
||||||
|
public function getDocuments(): Collection
|
||||||
|
{
|
||||||
|
return $this->documents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, CustomFieldValue>
|
||||||
|
*/
|
||||||
|
public function getCustomFieldValues(): Collection
|
||||||
|
{
|
||||||
|
return $this->customFieldValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
171
src/Entity/Constructeur.php
Normal file
171
src/Entity/Constructeur.php
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
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\ConstructeurRepository;
|
||||||
|
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;
|
||||||
|
|
||||||
|
#[UniqueEntity(fields: ['name'], message: 'Un fournisseur avec ce nom existe déjà.')]
|
||||||
|
#[ORM\Entity(repositoryClass: ConstructeurRepository::class)]
|
||||||
|
#[ORM\Table(name: 'constructeurs')]
|
||||||
|
#[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')"),
|
||||||
|
],
|
||||||
|
paginationClientItemsPerPage: true,
|
||||||
|
paginationMaximumItemsPerPage: 200
|
||||||
|
)]
|
||||||
|
class Constructeur
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||||
|
private ?string $email = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||||
|
private ?string $phone = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Machine>
|
||||||
|
*/
|
||||||
|
#[ORM\ManyToMany(targetEntity: Machine::class, mappedBy: 'constructeurs')]
|
||||||
|
private Collection $machines;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Composant>
|
||||||
|
*/
|
||||||
|
#[ORM\ManyToMany(targetEntity: Composant::class, mappedBy: 'constructeurs')]
|
||||||
|
private Collection $composants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Piece>
|
||||||
|
*/
|
||||||
|
#[ORM\ManyToMany(targetEntity: Piece::class, mappedBy: 'constructeurs')]
|
||||||
|
private Collection $pieces;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Product>
|
||||||
|
*/
|
||||||
|
#[ORM\ManyToMany(targetEntity: Product::class, mappedBy: 'constructeurs')]
|
||||||
|
private Collection $products;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->machines = new ArrayCollection();
|
||||||
|
$this->composants = new ArrayCollection();
|
||||||
|
$this->pieces = new ArrayCollection();
|
||||||
|
$this->products = 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 getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmail(): ?string
|
||||||
|
{
|
||||||
|
return $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEmail(?string $email): static
|
||||||
|
{
|
||||||
|
$this->email = $email;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPhone(): ?string
|
||||||
|
{
|
||||||
|
return $this->phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPhone(?string $phone): static
|
||||||
|
{
|
||||||
|
$this->phone = $phone;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
226
src/Entity/CustomField.php
Normal file
226
src/Entity/CustomField.php
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
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\CustomFieldRepository;
|
||||||
|
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\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: CustomFieldRepository::class)]
|
||||||
|
#[ORM\Table(name: 'custom_fields')]
|
||||||
|
#[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 CustomField
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 50)]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
|
private string $type;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
|
private bool $required = false;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'defaultValue')]
|
||||||
|
private ?string $defaultValue = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
|
private ?array $options = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'orderIndex')]
|
||||||
|
#[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: ModelType::class, inversedBy: 'customFields')]
|
||||||
|
#[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
private ?ModelType $typeComposant = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'pieceCustomFields')]
|
||||||
|
#[ORM\JoinColumn(name: 'typePieceId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
private ?ModelType $typePiece = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'productCustomFields')]
|
||||||
|
#[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
private ?ModelType $typeProduct = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, CustomFieldValue>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'customField', targetEntity: CustomFieldValue::class)]
|
||||||
|
private Collection $customFieldValues;
|
||||||
|
|
||||||
|
#[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->customFieldValues = 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 getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setType(string $type): static
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isRequired(): bool
|
||||||
|
{
|
||||||
|
return $this->required;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRequired(bool $required): static
|
||||||
|
{
|
||||||
|
$this->required = $required;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultValue(): ?string
|
||||||
|
{
|
||||||
|
return $this->defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDefaultValue(?string $defaultValue): static
|
||||||
|
{
|
||||||
|
$this->defaultValue = $defaultValue;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOptions(): ?array
|
||||||
|
{
|
||||||
|
return $this->options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOptions(?array $options): static
|
||||||
|
{
|
||||||
|
$this->options = $options;
|
||||||
|
|
||||||
|
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 getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
189
src/Entity/CustomFieldValue.php
Normal file
189
src/Entity/CustomFieldValue.php
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
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\CustomFieldValueRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: CustomFieldValueRepository::class)]
|
||||||
|
#[ORM\Table(name: 'custom_field_values')]
|
||||||
|
#[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 CustomFieldValue
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
|
private string $value;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: CustomField::class, inversedBy: 'customFieldValues')]
|
||||||
|
#[ORM\JoinColumn(name: 'customFieldId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
|
private CustomField $customField;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'customFieldValues')]
|
||||||
|
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
private ?Machine $machine = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'customFieldValues')]
|
||||||
|
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
private ?Composant $composant = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'customFieldValues')]
|
||||||
|
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
private ?Piece $piece = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'customFieldValues')]
|
||||||
|
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
private ?Product $product = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
|
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||||
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
|
#[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 getValue(): string
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setValue(string $value): static
|
||||||
|
{
|
||||||
|
$this->value = $value;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustomField(): CustomField
|
||||||
|
{
|
||||||
|
return $this->customField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCustomField(CustomField $customField): static
|
||||||
|
{
|
||||||
|
$this->customField = $customField;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMachine(): ?Machine
|
||||||
|
{
|
||||||
|
return $this->machine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMachine(?Machine $machine): static
|
||||||
|
{
|
||||||
|
$this->machine = $machine;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getComposant(): ?Composant
|
||||||
|
{
|
||||||
|
return $this->composant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setComposant(?Composant $composant): static
|
||||||
|
{
|
||||||
|
$this->composant = $composant;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPiece(): ?Piece
|
||||||
|
{
|
||||||
|
return $this->piece;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPiece(?Piece $piece): static
|
||||||
|
{
|
||||||
|
$this->piece = $piece;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProduct(): ?Product
|
||||||
|
{
|
||||||
|
return $this->product;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setProduct(?Product $product): static
|
||||||
|
{
|
||||||
|
$this->product = $product;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
276
src/Entity/Document.php
Normal file
276
src/Entity/Document.php
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\ExistsFilter;
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
|
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\DocumentRepository;
|
||||||
|
use App\State\DocumentUploadProcessor;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
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(ExistsFilter::class, properties: ['site', 'machine', 'composant', 'piece', 'product'])]
|
||||||
|
#[ApiFilter(OrderFilter::class, properties: ['createdAt', 'name', 'size'])]
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new GetCollection(
|
||||||
|
security: "is_granted('ROLE_VIEWER')",
|
||||||
|
normalizationContext: ['groups' => ['document:list']],
|
||||||
|
),
|
||||||
|
new Get(
|
||||||
|
security: "is_granted('ROLE_VIEWER')",
|
||||||
|
normalizationContext: ['groups' => ['document:list', 'document:detail']],
|
||||||
|
),
|
||||||
|
new Post(
|
||||||
|
security: "is_granted('ROLE_GESTIONNAIRE')",
|
||||||
|
processor: DocumentUploadProcessor::class,
|
||||||
|
deserialize: false,
|
||||||
|
inputFormats: ['multipart' => ['multipart/form-data']],
|
||||||
|
),
|
||||||
|
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
],
|
||||||
|
paginationClientItemsPerPage: true,
|
||||||
|
paginationMaximumItemsPerPage: 500,
|
||||||
|
order: ['createdAt' => 'DESC']
|
||||||
|
)]
|
||||||
|
class Document
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||||
|
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||||
|
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||||
|
private string $filename;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT)]
|
||||||
|
#[Groups(['document:detail', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||||
|
private string $path;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 100, name: 'mimeType')]
|
||||||
|
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||||
|
private string $mimeType;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::INTEGER)]
|
||||||
|
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||||
|
private int $size;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'documents')]
|
||||||
|
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
|
private ?Machine $machine = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'documents')]
|
||||||
|
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
|
private ?Composant $composant = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'documents')]
|
||||||
|
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
|
private ?Piece $piece = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'documents')]
|
||||||
|
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
|
private ?Product $product = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Site::class, inversedBy: 'documents')]
|
||||||
|
#[ORM\JoinColumn(name: 'siteId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
|
private ?Site $site = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
|
#[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 getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilename(): string
|
||||||
|
{
|
||||||
|
return $this->filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFilename(string $filename): static
|
||||||
|
{
|
||||||
|
$this->filename = $filename;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPath(): string
|
||||||
|
{
|
||||||
|
return $this->path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPath(string $path): static
|
||||||
|
{
|
||||||
|
$this->path = $path;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMimeType(): string
|
||||||
|
{
|
||||||
|
return $this->mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMimeType(string $mimeType): static
|
||||||
|
{
|
||||||
|
$this->mimeType = $mimeType;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSize(): int
|
||||||
|
{
|
||||||
|
return $this->size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSize(int $size): static
|
||||||
|
{
|
||||||
|
$this->size = $size;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMachine(): ?Machine
|
||||||
|
{
|
||||||
|
return $this->machine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMachine(?Machine $machine): static
|
||||||
|
{
|
||||||
|
$this->machine = $machine;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getComposant(): ?Composant
|
||||||
|
{
|
||||||
|
return $this->composant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setComposant(?Composant $composant): static
|
||||||
|
{
|
||||||
|
$this->composant = $composant;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPiece(): ?Piece
|
||||||
|
{
|
||||||
|
return $this->piece;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPiece(?Piece $piece): static
|
||||||
|
{
|
||||||
|
$this->piece = $piece;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProduct(): ?Product
|
||||||
|
{
|
||||||
|
return $this->product;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setProduct(?Product $product): static
|
||||||
|
{
|
||||||
|
$this->product = $product;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSite(): ?Site
|
||||||
|
{
|
||||||
|
return $this->site;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSite(?Site $site): static
|
||||||
|
{
|
||||||
|
$this->site = $site;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
269
src/Entity/Machine.php
Normal file
269
src/Entity/Machine.php
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
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\MachineRepository;
|
||||||
|
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\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: MachineRepository::class)]
|
||||||
|
#[ORM\Table(name: 'machines')]
|
||||||
|
#[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 Machine
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||||
|
private ?string $reference = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)]
|
||||||
|
private ?string $prix = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Site::class, inversedBy: 'machines')]
|
||||||
|
#[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>
|
||||||
|
*/
|
||||||
|
#[ORM\ManyToMany(targetEntity: Constructeur::class, inversedBy: 'machines')]
|
||||||
|
#[ORM\JoinTable(
|
||||||
|
name: '_MachineConstructeurs',
|
||||||
|
joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')],
|
||||||
|
inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||||
|
)]
|
||||||
|
private Collection $constructeurs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, MachineComponentLink>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: MachineComponentLink::class)]
|
||||||
|
private Collection $componentLinks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, MachinePieceLink>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: MachinePieceLink::class)]
|
||||||
|
private Collection $pieceLinks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, MachineProductLink>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: MachineProductLink::class)]
|
||||||
|
private Collection $productLinks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Document>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: Document::class)]
|
||||||
|
private Collection $documents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, CustomFieldValue>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: CustomFieldValue::class)]
|
||||||
|
private Collection $customFieldValues;
|
||||||
|
|
||||||
|
#[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->constructeurs = new ArrayCollection();
|
||||||
|
$this->componentLinks = new ArrayCollection();
|
||||||
|
$this->pieceLinks = new ArrayCollection();
|
||||||
|
$this->productLinks = new ArrayCollection();
|
||||||
|
$this->documents = new ArrayCollection();
|
||||||
|
$this->customFieldValues = 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 getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReference(): ?string
|
||||||
|
{
|
||||||
|
return $this->reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setReference(?string $reference): static
|
||||||
|
{
|
||||||
|
$this->reference = $reference;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrix(): ?string
|
||||||
|
{
|
||||||
|
return $this->prix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPrix(?string $prix): static
|
||||||
|
{
|
||||||
|
$this->prix = $prix;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSite(): Site
|
||||||
|
{
|
||||||
|
return $this->site;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSite(Site $site): static
|
||||||
|
{
|
||||||
|
$this->site = $site;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTypeMachine(): ?TypeMachine
|
||||||
|
{
|
||||||
|
return $this->typeMachine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTypeMachine(?TypeMachine $typeMachine): static
|
||||||
|
{
|
||||||
|
$this->typeMachine = $typeMachine;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Constructeur>
|
||||||
|
*/
|
||||||
|
public function getConstructeurs(): Collection
|
||||||
|
{
|
||||||
|
return $this->constructeurs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, MachineComponentLink>
|
||||||
|
*/
|
||||||
|
public function getComponentLinks(): Collection
|
||||||
|
{
|
||||||
|
return $this->componentLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, MachinePieceLink>
|
||||||
|
*/
|
||||||
|
public function getPieceLinks(): Collection
|
||||||
|
{
|
||||||
|
return $this->pieceLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, MachineProductLink>
|
||||||
|
*/
|
||||||
|
public function getProductLinks(): Collection
|
||||||
|
{
|
||||||
|
return $this->productLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Document>
|
||||||
|
*/
|
||||||
|
public function getDocuments(): Collection
|
||||||
|
{
|
||||||
|
return $this->documents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, CustomFieldValue>
|
||||||
|
*/
|
||||||
|
public function getCustomFieldValues(): Collection
|
||||||
|
{
|
||||||
|
return $this->customFieldValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
214
src/Entity/MachineComponentLink.php
Normal file
214
src/Entity/MachineComponentLink.php
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
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\MachineComponentLinkRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: MachineComponentLinkRepository::class)]
|
||||||
|
#[ORM\Table(name: 'machine_component_links')]
|
||||||
|
#[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 MachineComponentLink
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'componentLinks')]
|
||||||
|
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||||
|
private Machine $machine;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'machineLinks')]
|
||||||
|
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||||
|
private Composant $composant;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: MachineComponentLink::class, inversedBy: 'childLinks')]
|
||||||
|
#[ORM\JoinColumn(name: 'parentLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
private ?MachineComponentLink $parentLink = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, 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>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'parentLink', targetEntity: MachinePieceLink::class)]
|
||||||
|
private Collection $pieceLinks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, MachineProductLink>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'parentComponentLink', targetEntity: MachineProductLink::class)]
|
||||||
|
private Collection $productLinks;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'nameOverride')]
|
||||||
|
private ?string $nameOverride = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'referenceOverride')]
|
||||||
|
private ?string $referenceOverride = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true, name: 'prixOverride')]
|
||||||
|
private ?string $prixOverride = null;
|
||||||
|
|
||||||
|
#[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->childLinks = new ArrayCollection();
|
||||||
|
$this->pieceLinks = new ArrayCollection();
|
||||||
|
$this->productLinks = 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 getMachine(): Machine
|
||||||
|
{
|
||||||
|
return $this->machine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMachine(Machine $machine): static
|
||||||
|
{
|
||||||
|
$this->machine = $machine;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getComposant(): Composant
|
||||||
|
{
|
||||||
|
return $this->composant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setComposant(Composant $composant): static
|
||||||
|
{
|
||||||
|
$this->composant = $composant;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParentLink(): ?MachineComponentLink
|
||||||
|
{
|
||||||
|
return $this->parentLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setParentLink(?MachineComponentLink $parentLink): static
|
||||||
|
{
|
||||||
|
$this->parentLink = $parentLink;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNameOverride(?string $nameOverride): static
|
||||||
|
{
|
||||||
|
$this->nameOverride = $nameOverride;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReferenceOverride(): ?string
|
||||||
|
{
|
||||||
|
return $this->referenceOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setReferenceOverride(?string $referenceOverride): static
|
||||||
|
{
|
||||||
|
$this->referenceOverride = $referenceOverride;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrixOverride(): ?string
|
||||||
|
{
|
||||||
|
return $this->prixOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPrixOverride(?string $prixOverride): static
|
||||||
|
{
|
||||||
|
$this->prixOverride = $prixOverride;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
200
src/Entity/MachinePieceLink.php
Normal file
200
src/Entity/MachinePieceLink.php
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
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\MachinePieceLinkRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: MachinePieceLinkRepository::class)]
|
||||||
|
#[ORM\Table(name: 'machine_piece_links')]
|
||||||
|
#[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 MachinePieceLink
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'pieceLinks')]
|
||||||
|
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||||
|
private Machine $machine;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'machineLinks')]
|
||||||
|
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||||
|
private Piece $piece;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: MachineComponentLink::class, inversedBy: 'pieceLinks')]
|
||||||
|
#[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>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'parentPieceLink', targetEntity: MachineProductLink::class)]
|
||||||
|
private Collection $productLinks;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'nameOverride')]
|
||||||
|
private ?string $nameOverride = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'referenceOverride')]
|
||||||
|
private ?string $referenceOverride = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true, name: 'prixOverride')]
|
||||||
|
private ?string $prixOverride = null;
|
||||||
|
|
||||||
|
#[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->productLinks = 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 getMachine(): Machine
|
||||||
|
{
|
||||||
|
return $this->machine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMachine(Machine $machine): static
|
||||||
|
{
|
||||||
|
$this->machine = $machine;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPiece(): Piece
|
||||||
|
{
|
||||||
|
return $this->piece;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPiece(Piece $piece): static
|
||||||
|
{
|
||||||
|
$this->piece = $piece;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParentLink(): ?MachineComponentLink
|
||||||
|
{
|
||||||
|
return $this->parentLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setParentLink(?MachineComponentLink $parentLink): static
|
||||||
|
{
|
||||||
|
$this->parentLink = $parentLink;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNameOverride(?string $nameOverride): static
|
||||||
|
{
|
||||||
|
$this->nameOverride = $nameOverride;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReferenceOverride(): ?string
|
||||||
|
{
|
||||||
|
return $this->referenceOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setReferenceOverride(?string $referenceOverride): static
|
||||||
|
{
|
||||||
|
$this->referenceOverride = $referenceOverride;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrixOverride(): ?string
|
||||||
|
{
|
||||||
|
return $this->prixOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPrixOverride(?string $prixOverride): static
|
||||||
|
{
|
||||||
|
$this->prixOverride = $prixOverride;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
187
src/Entity/MachineProductLink.php
Normal file
187
src/Entity/MachineProductLink.php
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
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\MachineProductLinkRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: MachineProductLinkRepository::class)]
|
||||||
|
#[ORM\Table(name: 'machine_product_links')]
|
||||||
|
#[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 MachineProductLink
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'productLinks')]
|
||||||
|
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||||
|
private Machine $machine;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'machineLinks')]
|
||||||
|
#[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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, MachineProductLink>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'parentLink', targetEntity: MachineProductLink::class)]
|
||||||
|
private Collection $childLinks;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: MachineComponentLink::class, inversedBy: 'productLinks')]
|
||||||
|
#[ORM\JoinColumn(name: 'parentComponentLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
private ?MachineComponentLink $parentComponentLink = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: MachinePieceLink::class, inversedBy: 'productLinks')]
|
||||||
|
#[ORM\JoinColumn(name: 'parentPieceLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
private ?MachinePieceLink $parentPieceLink = null;
|
||||||
|
|
||||||
|
#[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->childLinks = 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 getMachine(): Machine
|
||||||
|
{
|
||||||
|
return $this->machine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMachine(Machine $machine): static
|
||||||
|
{
|
||||||
|
$this->machine = $machine;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProduct(): Product
|
||||||
|
{
|
||||||
|
return $this->product;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setProduct(Product $product): static
|
||||||
|
{
|
||||||
|
$this->product = $product;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setParentLink(?MachineProductLink $parentLink): static
|
||||||
|
{
|
||||||
|
$this->parentLink = $parentLink;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParentComponentLink(): ?MachineComponentLink
|
||||||
|
{
|
||||||
|
return $this->parentComponentLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setParentComponentLink(?MachineComponentLink $parentComponentLink): static
|
||||||
|
{
|
||||||
|
$this->parentComponentLink = $parentComponentLink;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParentPieceLink(): ?MachinePieceLink
|
||||||
|
{
|
||||||
|
return $this->parentPieceLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setParentPieceLink(?MachinePieceLink $parentPieceLink): static
|
||||||
|
{
|
||||||
|
$this->parentPieceLink = $parentPieceLink;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
352
src/Entity/ModelType.php
Normal file
352
src/Entity/ModelType.php
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
|
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\Enum\ModelCategory;
|
||||||
|
use App\Repository\ModelTypeRepository;
|
||||||
|
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: ModelTypeRepository::class)]
|
||||||
|
#[ORM\Table(name: 'model_types')]
|
||||||
|
#[ORM\UniqueConstraint(name: 'unique_category_name', columns: ['category', 'name'])]
|
||||||
|
#[ORM\HasLifecycleCallbacks]
|
||||||
|
#[ApiFilter(SearchFilter::class, properties: ['category' => 'exact', 'name' => 'ipartial'])]
|
||||||
|
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||||
|
#[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')"),
|
||||||
|
],
|
||||||
|
paginationClientItemsPerPage: true,
|
||||||
|
paginationMaximumItemsPerPage: 200
|
||||||
|
)]
|
||||||
|
class ModelType
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
#[Groups(['type_machine:read', 'model_type:read'])]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 120)]
|
||||||
|
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 60, unique: true)]
|
||||||
|
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
|
||||||
|
private string $code;
|
||||||
|
|
||||||
|
#[ORM\Column(enumType: ModelCategory::class)]
|
||||||
|
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
|
||||||
|
private ModelCategory $category;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
|
||||||
|
private ?string $notes = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
|
||||||
|
private ?string $description = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::JSON, nullable: true, name: 'componentSkeleton')]
|
||||||
|
#[Groups(['model_type:read'])]
|
||||||
|
private ?array $componentSkeleton = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::JSON, nullable: true, name: 'pieceSkeleton')]
|
||||||
|
#[Groups(['model_type:read'])]
|
||||||
|
private ?array $pieceSkeleton = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::JSON, nullable: true, name: 'productSkeleton')]
|
||||||
|
#[Groups(['model_type:read'])]
|
||||||
|
private ?array $productSkeleton = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
|
#[Groups(['model_type:read'])]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
|
#[Groups(['model_type:read'])]
|
||||||
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
|
private ?array $pendingStructure = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Composant>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'typeComposant', targetEntity: Composant::class)]
|
||||||
|
private Collection $composants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Piece>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'typePiece', targetEntity: Piece::class)]
|
||||||
|
private Collection $pieces;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Product>
|
||||||
|
*/
|
||||||
|
#[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>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'typeComposant', targetEntity: CustomField::class)]
|
||||||
|
private Collection $customFields;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, CustomField>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'typePiece', targetEntity: CustomField::class)]
|
||||||
|
private Collection $pieceCustomFields;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, CustomField>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'typeProduct', targetEntity: CustomField::class)]
|
||||||
|
private Collection $productCustomFields;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): static
|
||||||
|
{
|
||||||
|
$this->name = mb_strtoupper(mb_substr($name, 0, 1)).mb_substr($name, 1);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCode(): string
|
||||||
|
{
|
||||||
|
return $this->code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCode(string $code): static
|
||||||
|
{
|
||||||
|
$this->code = $code;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCategory(): ModelCategory
|
||||||
|
{
|
||||||
|
return $this->category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCategory(ModelCategory $category): static
|
||||||
|
{
|
||||||
|
$this->category = $category;
|
||||||
|
|
||||||
|
if (null !== $this->pendingStructure) {
|
||||||
|
$this->applyStructureForCategory($this->pendingStructure, $category);
|
||||||
|
$this->pendingStructure = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNotes(): ?string
|
||||||
|
{
|
||||||
|
return $this->notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNotes(?string $notes): static
|
||||||
|
{
|
||||||
|
$this->notes = $notes;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): static
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getComponentSkeleton(): ?array
|
||||||
|
{
|
||||||
|
return $this->componentSkeleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setComponentSkeleton(?array $componentSkeleton): static
|
||||||
|
{
|
||||||
|
$this->componentSkeleton = $componentSkeleton;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPieceSkeleton(): ?array
|
||||||
|
{
|
||||||
|
return $this->pieceSkeleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPieceSkeleton(?array $pieceSkeleton): static
|
||||||
|
{
|
||||||
|
$this->pieceSkeleton = $pieceSkeleton;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductSkeleton(): ?array
|
||||||
|
{
|
||||||
|
return $this->productSkeleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setProductSkeleton(?array $productSkeleton): static
|
||||||
|
{
|
||||||
|
$this->productSkeleton = $productSkeleton;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Groups(['model_type:read'])]
|
||||||
|
public function getStructure(): ?array
|
||||||
|
{
|
||||||
|
return match ($this->category) {
|
||||||
|
ModelCategory::COMPONENT => $this->componentSkeleton,
|
||||||
|
ModelCategory::PIECE => $this->pieceSkeleton,
|
||||||
|
ModelCategory::PRODUCT => $this->productSkeleton,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Groups(['model_type:write'])]
|
||||||
|
public function setStructure(?array $structure): static
|
||||||
|
{
|
||||||
|
if (!isset($this->category)) {
|
||||||
|
$this->pendingStructure = $structure;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->applyStructureForCategory($structure, $this->category);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyStructureForCategory(?array $structure, ModelCategory $category): void
|
||||||
|
{
|
||||||
|
if (ModelCategory::COMPONENT === $category) {
|
||||||
|
$this->componentSkeleton = $structure;
|
||||||
|
$this->pieceSkeleton = null;
|
||||||
|
$this->productSkeleton = null;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ModelCategory::PIECE === $category) {
|
||||||
|
$this->pieceSkeleton = $structure;
|
||||||
|
$this->componentSkeleton = null;
|
||||||
|
$this->productSkeleton = null;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->productSkeleton = $structure;
|
||||||
|
$this->componentSkeleton = null;
|
||||||
|
$this->pieceSkeleton = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
340
src/Entity/Piece.php
Normal file
340
src/Entity/Piece.php
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
|
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\PieceRepository;
|
||||||
|
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\Attribute\Groups;
|
||||||
|
|
||||||
|
#[UniqueEntity(fields: ['reference'], message: 'Une pièce avec cette référence existe déjà.')]
|
||||||
|
#[ORM\Entity(repositoryClass: PieceRepository::class)]
|
||||||
|
#[ORM\Table(name: 'pieces')]
|
||||||
|
#[ORM\HasLifecycleCallbacks]
|
||||||
|
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typePiece' => 'exact'])]
|
||||||
|
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||||
|
#[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')"),
|
||||||
|
],
|
||||||
|
normalizationContext: ['groups' => ['piece:read']],
|
||||||
|
paginationClientItemsPerPage: true,
|
||||||
|
paginationMaximumItemsPerPage: 200
|
||||||
|
)]
|
||||||
|
class Piece
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
#[Groups(['piece:read', 'document:list'])]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||||
|
#[Groups(['piece:read', 'document:list'])]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, unique: true, nullable: true)]
|
||||||
|
#[Groups(['piece:read'])]
|
||||||
|
private ?string $reference = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
#[Groups(['piece:read'])]
|
||||||
|
private ?string $description = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)]
|
||||||
|
#[Groups(['piece:read'])]
|
||||||
|
private ?string $prix = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'pieces')]
|
||||||
|
#[ORM\JoinColumn(name: 'typePieceId', referencedColumnName: 'id', nullable: true)]
|
||||||
|
#[Groups(['piece:read'])]
|
||||||
|
private ?ModelType $typePiece = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'pieces')]
|
||||||
|
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||||
|
#[Groups(['piece:read'])]
|
||||||
|
private ?Product $product = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::JSON, nullable: true, name: 'productIds')]
|
||||||
|
#[Groups(['piece:read'])]
|
||||||
|
private ?array $productIds = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Constructeur>
|
||||||
|
*/
|
||||||
|
#[ORM\ManyToMany(targetEntity: Constructeur::class, inversedBy: 'pieces')]
|
||||||
|
#[ORM\JoinTable(
|
||||||
|
name: '_PieceConstructeurs',
|
||||||
|
joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')],
|
||||||
|
inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||||
|
)]
|
||||||
|
#[Groups(['piece:read'])]
|
||||||
|
private Collection $constructeurs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Document>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'piece', targetEntity: Document::class)]
|
||||||
|
#[Groups(['piece:read'])]
|
||||||
|
private Collection $documents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, CustomFieldValue>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'piece', targetEntity: CustomFieldValue::class)]
|
||||||
|
#[Groups(['piece:read'])]
|
||||||
|
private Collection $customFieldValues;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, MachinePieceLink>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'piece', targetEntity: MachinePieceLink::class)]
|
||||||
|
private Collection $machineLinks;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
|
#[Groups(['piece:read'])]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
|
#[Groups(['piece:read'])]
|
||||||
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->constructeurs = new ArrayCollection();
|
||||||
|
$this->documents = new ArrayCollection();
|
||||||
|
$this->customFieldValues = new ArrayCollection();
|
||||||
|
$this->machineLinks = 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 getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReference(): ?string
|
||||||
|
{
|
||||||
|
return $this->reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setReference(?string $reference): static
|
||||||
|
{
|
||||||
|
$this->reference = $reference;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): static
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrix(): ?string
|
||||||
|
{
|
||||||
|
return $this->prix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPrix(?string $prix): static
|
||||||
|
{
|
||||||
|
$this->prix = $prix;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTypePiece(): ?ModelType
|
||||||
|
{
|
||||||
|
return $this->typePiece;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTypePiece(?ModelType $typePiece): static
|
||||||
|
{
|
||||||
|
$this->typePiece = $typePiece;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProduct(): ?Product
|
||||||
|
{
|
||||||
|
return $this->product;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setProduct(?Product $product): static
|
||||||
|
{
|
||||||
|
$this->product = $product;
|
||||||
|
|
||||||
|
if ($product && empty($this->productIds)) {
|
||||||
|
$productId = $product->getId();
|
||||||
|
$this->productIds = $productId ? [$productId] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$product && empty($this->productIds)) {
|
||||||
|
$this->productIds = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getProductIds(): array
|
||||||
|
{
|
||||||
|
if (!is_array($this->productIds)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(
|
||||||
|
array_filter(
|
||||||
|
array_map(
|
||||||
|
static fn ($value) => is_string($value) ? trim($value) : '',
|
||||||
|
$this->productIds,
|
||||||
|
),
|
||||||
|
static fn (string $value) => '' !== $value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setProductIds(?array $productIds): static
|
||||||
|
{
|
||||||
|
if (!is_array($productIds)) {
|
||||||
|
$this->productIds = null;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = array_values(
|
||||||
|
array_unique(
|
||||||
|
array_filter(
|
||||||
|
array_map(
|
||||||
|
static fn ($value) => is_string($value) ? trim($value) : '',
|
||||||
|
$productIds,
|
||||||
|
),
|
||||||
|
static fn (string $value) => '' !== $value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->productIds = [] === $normalized ? null : $normalized;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Constructeur>
|
||||||
|
*/
|
||||||
|
public function getConstructeurs(): Collection
|
||||||
|
{
|
||||||
|
return $this->constructeurs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addConstructeur(Constructeur $constructeur): static
|
||||||
|
{
|
||||||
|
if (!$this->constructeurs->contains($constructeur)) {
|
||||||
|
$this->constructeurs->add($constructeur);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeConstructeur(Constructeur $constructeur): static
|
||||||
|
{
|
||||||
|
$this->constructeurs->removeElement($constructeur);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Document>
|
||||||
|
*/
|
||||||
|
public function getDocuments(): Collection
|
||||||
|
{
|
||||||
|
return $this->documents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, CustomFieldValue>
|
||||||
|
*/
|
||||||
|
public function getCustomFieldValues(): Collection
|
||||||
|
{
|
||||||
|
return $this->customFieldValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
261
src/Entity/Product.php
Normal file
261
src/Entity/Product.php
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
|
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\ProductRepository;
|
||||||
|
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\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'])]
|
||||||
|
#[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')"),
|
||||||
|
],
|
||||||
|
normalizationContext: ['groups' => ['product:read']],
|
||||||
|
paginationClientItemsPerPage: true,
|
||||||
|
paginationMaximumItemsPerPage: 200
|
||||||
|
)]
|
||||||
|
class Product
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
#[Groups(['product:read', 'document:list'])]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
||||||
|
#[Groups(['product:read', 'document:list'])]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||||
|
#[Groups(['product:read'])]
|
||||||
|
private ?string $reference = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true, name: 'supplierPrice')]
|
||||||
|
#[Groups(['product:read'])]
|
||||||
|
private ?string $supplierPrice = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'products')]
|
||||||
|
#[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: true)]
|
||||||
|
#[Groups(['product:read'])]
|
||||||
|
private ?ModelType $typeProduct = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Constructeur>
|
||||||
|
*/
|
||||||
|
#[ORM\ManyToMany(targetEntity: Constructeur::class, inversedBy: 'products')]
|
||||||
|
#[ORM\JoinTable(
|
||||||
|
name: '_ProductConstructeurs',
|
||||||
|
joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')],
|
||||||
|
inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||||
|
)]
|
||||||
|
#[Groups(['product:read'])]
|
||||||
|
private Collection $constructeurs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Document>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'product', targetEntity: Document::class)]
|
||||||
|
#[Groups(['product:read'])]
|
||||||
|
private Collection $documents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, CustomFieldValue>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'product', targetEntity: CustomFieldValue::class)]
|
||||||
|
#[Groups(['product:read'])]
|
||||||
|
private Collection $customFieldValues;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Piece>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'product', targetEntity: Piece::class)]
|
||||||
|
private Collection $pieces;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Composant>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'product', targetEntity: Composant::class)]
|
||||||
|
private Collection $composants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, MachineProductLink>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'product', targetEntity: MachineProductLink::class)]
|
||||||
|
private Collection $machineLinks;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
|
#[Groups(['product:read'])]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
|
#[Groups(['product:read'])]
|
||||||
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->constructeurs = new ArrayCollection();
|
||||||
|
$this->documents = new ArrayCollection();
|
||||||
|
$this->customFieldValues = new ArrayCollection();
|
||||||
|
$this->pieces = new ArrayCollection();
|
||||||
|
$this->composants = new ArrayCollection();
|
||||||
|
$this->machineLinks = 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 getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReference(): ?string
|
||||||
|
{
|
||||||
|
return $this->reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setReference(?string $reference): static
|
||||||
|
{
|
||||||
|
$this->reference = $reference;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSupplierPrice(): ?string
|
||||||
|
{
|
||||||
|
return $this->supplierPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSupplierPrice(?string $supplierPrice): static
|
||||||
|
{
|
||||||
|
$this->supplierPrice = $supplierPrice;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTypeProduct(): ?ModelType
|
||||||
|
{
|
||||||
|
return $this->typeProduct;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTypeProduct(?ModelType $typeProduct): static
|
||||||
|
{
|
||||||
|
$this->typeProduct = $typeProduct;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Constructeur>
|
||||||
|
*/
|
||||||
|
public function getConstructeurs(): Collection
|
||||||
|
{
|
||||||
|
return $this->constructeurs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addConstructeur(Constructeur $constructeur): static
|
||||||
|
{
|
||||||
|
if (!$this->constructeurs->contains($constructeur)) {
|
||||||
|
$this->constructeurs->add($constructeur);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeConstructeur(Constructeur $constructeur): static
|
||||||
|
{
|
||||||
|
$this->constructeurs->removeElement($constructeur);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Document>
|
||||||
|
*/
|
||||||
|
public function getDocuments(): Collection
|
||||||
|
{
|
||||||
|
return $this->documents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, CustomFieldValue>
|
||||||
|
*/
|
||||||
|
public function getCustomFieldValues(): Collection
|
||||||
|
{
|
||||||
|
return $this->customFieldValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
258
src/Entity/Profile.php
Normal file
258
src/Entity/Profile.php
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
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\ProfileRepository;
|
||||||
|
use App\State\ProfilePasswordHasher;
|
||||||
|
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\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: ProfileRepository::class)]
|
||||||
|
#[ORM\Table(name: 'profiles')]
|
||||||
|
#[ORM\UniqueConstraint(name: 'UNIQ_email', columns: ['email'])]
|
||||||
|
#[ORM\HasLifecycleCallbacks]
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||||
|
new GetCollection(security: "is_granted('ROLE_ADMIN')"),
|
||||||
|
new Post(
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
denormalizationContext: ['groups' => ['profile:write', 'profile:admin:write']],
|
||||||
|
processor: ProfilePasswordHasher::class,
|
||||||
|
),
|
||||||
|
new Put(
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
denormalizationContext: ['groups' => ['profile:write', 'profile:admin:write']],
|
||||||
|
processor: ProfilePasswordHasher::class,
|
||||||
|
),
|
||||||
|
new Patch(
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
denormalizationContext: ['groups' => ['profile:write', 'profile:admin:write']],
|
||||||
|
processor: ProfilePasswordHasher::class,
|
||||||
|
),
|
||||||
|
new Delete(security: "is_granted('ROLE_ADMIN')"),
|
||||||
|
],
|
||||||
|
normalizationContext: ['groups' => ['profile:read']],
|
||||||
|
denormalizationContext: ['groups' => ['profile:write']]
|
||||||
|
)]
|
||||||
|
class Profile implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: 'string', length: 36)]
|
||||||
|
#[Groups(['profile:read'])]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 180, unique: true, nullable: true)]
|
||||||
|
#[Assert\Email]
|
||||||
|
#[Groups(['profile:read', 'profile:write'])]
|
||||||
|
private ?string $email = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 100, name: 'firstname')]
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Groups(['profile:read', 'profile:write'])]
|
||||||
|
private string $firstName;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 100, name: 'lastname')]
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Groups(['profile:read', 'profile:write'])]
|
||||||
|
private string $lastName;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'boolean', options: ['default' => true], name: 'isactive')]
|
||||||
|
#[Groups(['profile:read', 'profile:write'])]
|
||||||
|
private bool $isActive = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var list<string> The user roles
|
||||||
|
*/
|
||||||
|
#[ORM\Column(type: 'json', options: ['default' => '["ROLE_USER"]'])]
|
||||||
|
#[Groups(['profile:read', 'profile:admin:write'])]
|
||||||
|
private array $roles = ['ROLE_USER'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var null|string The hashed password
|
||||||
|
*/
|
||||||
|
#[ORM\Column(type: 'string', nullable: true)]
|
||||||
|
private ?string $password = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-persisted field used for password hashing via ProfilePasswordHasher.
|
||||||
|
*/
|
||||||
|
#[Groups(['profile:write'])]
|
||||||
|
private ?string $plainPassword = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime_immutable', name: 'createdat')]
|
||||||
|
#[Groups(['profile:read'])]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime_immutable', name: 'updatedat')]
|
||||||
|
#[Groups(['profile:read'])]
|
||||||
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->id = 'cl'.substr(strtolower(base_convert(random_bytes(12), 2, 36)), 0, 24);
|
||||||
|
$this->createdAt = new DateTimeImmutable();
|
||||||
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?string
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmail(): ?string
|
||||||
|
{
|
||||||
|
return $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEmail(?string $email): static
|
||||||
|
{
|
||||||
|
$this->email = $email;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFirstName(): string
|
||||||
|
{
|
||||||
|
return $this->firstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFirstName(string $firstName): static
|
||||||
|
{
|
||||||
|
$this->firstName = $firstName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastName(): string
|
||||||
|
{
|
||||||
|
return $this->lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLastName(string $lastName): static
|
||||||
|
{
|
||||||
|
$this->lastName = $lastName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isActive(): bool
|
||||||
|
{
|
||||||
|
return $this->isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIsActive(bool $isActive): static
|
||||||
|
{
|
||||||
|
$this->isActive = $isActive;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see UserInterface
|
||||||
|
*/
|
||||||
|
public function getUserIdentifier(): string
|
||||||
|
{
|
||||||
|
return (string) $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see UserInterface
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function getRoles(): array
|
||||||
|
{
|
||||||
|
$roles = $this->roles;
|
||||||
|
$roles[] = 'ROLE_USER';
|
||||||
|
|
||||||
|
return array_values(array_unique($roles));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $roles
|
||||||
|
*/
|
||||||
|
public function setRoles(array $roles): static
|
||||||
|
{
|
||||||
|
$this->roles = $roles;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see PasswordAuthenticatedUserInterface
|
||||||
|
*/
|
||||||
|
public function getPassword(): ?string
|
||||||
|
{
|
||||||
|
return $this->password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPassword(?string $password): static
|
||||||
|
{
|
||||||
|
$this->password = $password;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPlainPassword(): ?string
|
||||||
|
{
|
||||||
|
return $this->plainPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPlainPassword(?string $plainPassword): static
|
||||||
|
{
|
||||||
|
$this->plainPassword = $plainPassword;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Groups(['profile:read'])]
|
||||||
|
public function getHasPassword(): bool
|
||||||
|
{
|
||||||
|
return null !== $this->password && '' !== $this->password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see UserInterface
|
||||||
|
*/
|
||||||
|
public function eraseCredentials(): void
|
||||||
|
{
|
||||||
|
$this->plainPassword = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ORM\PrePersist]
|
||||||
|
public function setCreatedAtValue(): void
|
||||||
|
{
|
||||||
|
$this->createdAt = new DateTimeImmutable();
|
||||||
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ORM\PreUpdate]
|
||||||
|
public function setUpdatedAtValue(): void
|
||||||
|
{
|
||||||
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
|
}
|
||||||
|
}
|
||||||
268
src/Entity/Site.php
Normal file
268
src/Entity/Site.php
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
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\SiteRepository;
|
||||||
|
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\Attribute\Groups;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: SiteRepository::class)]
|
||||||
|
#[ORM\Table(name: 'sites')]
|
||||||
|
#[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')"),
|
||||||
|
],
|
||||||
|
paginationClientItemsPerPage: true,
|
||||||
|
paginationMaximumItemsPerPage: 200
|
||||||
|
)]
|
||||||
|
class Site
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
|
private ?string $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||||
|
#[Assert\NotBlank]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, options: ['default' => ''], name: 'contactName')]
|
||||||
|
private string $contactName = '';
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 20, options: ['default' => ''], name: 'contactPhone')]
|
||||||
|
private string $contactPhone = '';
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 500, options: ['default' => ''], name: 'contactAddress')]
|
||||||
|
private string $contactAddress = '';
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 10, options: ['default' => ''], name: 'contactPostalCode')]
|
||||||
|
private string $contactPostalCode = '';
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 100, options: ['default' => ''], name: 'contactCity')]
|
||||||
|
private string $contactCity = '';
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Machine>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(targetEntity: Machine::class, mappedBy: 'site', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||||
|
private Collection $machines;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Document>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(targetEntity: Document::class, mappedBy: 'site', cascade: ['remove'], orphanRemoval: true)]
|
||||||
|
private Collection $documents;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->machines = new ArrayCollection();
|
||||||
|
$this->documents = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ORM\PrePersist]
|
||||||
|
public function setCreatedAtValue(): void
|
||||||
|
{
|
||||||
|
$this->createdAt = new DateTimeImmutable();
|
||||||
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
|
|
||||||
|
// Générer un ID CUID-compatible si nécessaire
|
||||||
|
if (null === $this->id) {
|
||||||
|
$this->id = $this->generateCuid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ORM\PreUpdate]
|
||||||
|
public function setUpdatedAtValue(): void
|
||||||
|
{
|
||||||
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
|
||||||
|
public function getId(): ?string
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setId(string $id): static
|
||||||
|
{
|
||||||
|
$this->id = $id;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContactName(): string
|
||||||
|
{
|
||||||
|
return $this->contactName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setContactName(string $contactName): static
|
||||||
|
{
|
||||||
|
$this->contactName = $contactName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContactPhone(): string
|
||||||
|
{
|
||||||
|
return $this->contactPhone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setContactPhone(string $contactPhone): static
|
||||||
|
{
|
||||||
|
$this->contactPhone = $contactPhone;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContactAddress(): string
|
||||||
|
{
|
||||||
|
return $this->contactAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setContactAddress(string $contactAddress): static
|
||||||
|
{
|
||||||
|
$this->contactAddress = $contactAddress;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContactPostalCode(): string
|
||||||
|
{
|
||||||
|
return $this->contactPostalCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setContactPostalCode(string $contactPostalCode): static
|
||||||
|
{
|
||||||
|
$this->contactPostalCode = $contactPostalCode;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContactCity(): string
|
||||||
|
{
|
||||||
|
return $this->contactCity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setContactCity(string $contactCity): static
|
||||||
|
{
|
||||||
|
$this->contactCity = $contactCity;
|
||||||
|
|
||||||
|
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->setSite($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeMachine(Machine $machine): static
|
||||||
|
{
|
||||||
|
if ($this->machines->removeElement($machine)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($machine->getSite() === $this) {
|
||||||
|
$machine->setSite(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Document>
|
||||||
|
*/
|
||||||
|
public function getDocuments(): Collection
|
||||||
|
{
|
||||||
|
return $this->documents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addDocument(Document $document): static
|
||||||
|
{
|
||||||
|
if (!$this->documents->contains($document)) {
|
||||||
|
$this->documents->add($document);
|
||||||
|
$document->setSite($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeDocument(Document $document): static
|
||||||
|
{
|
||||||
|
if ($this->documents->removeElement($document)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($document->getSite() === $this) {
|
||||||
|
$document->setSite(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
// Génération d'un ID compatible CUID (format: cl + 24 caractères)
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
|
}
|
||||||
390
src/Entity/TypeMachine.php
Normal file
390
src/Entity/TypeMachine.php
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user