Compare commits

...

90 Commits

Author SHA1 Message Date
b5d5ce0d8e Actualiser README.md 2026-03-15 07:15:49 +00:00
Matthieu
03e6c2432b fix(machine) : add addConstructeur/removeConstructeur methods + fix fournisseur display
API Platform silently ignored the constructeurs field on PATCH because
Machine was missing the add/remove methods (unlike Composant, Piece, Product).
Also fixes the read-only fournisseur display overflow in MachineInfoCard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 18:00:09 +01:00
Matthieu
31408ded7f chore : bump version to 1.9.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:52:21 +01:00
Matthieu
4054fb24e6 feat(site) : add color field to sites + frontend submodule update
- Add color VARCHAR(7) column to sites entity
- Migration with IF NOT EXISTS for idempotence
- Update reference config
- Frontend: site color picker, dark mode, card styling improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:51:55 +01:00
Matthieu
32ba4928df chore(frontend) : update submodule — site edit + card buttons alignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:11:55 +01:00
edf7d0fa9e chore(frontend) : update submodule — changelog v1.9.0 2026-03-09 13:04:54 +01:00
233927df19 chore : sync frontend submodule and update reference config
Update frontend submodule pointer to latest UI refactor.
Update config/reference.php with Symfony auto-generated changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 00:14:49 +01:00
dcb5f15769 docs : update CLAUDE.md with custom controllers, fields architecture and factories
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 18:43:19 +01:00
d3cd3fc3ce feat(machine) : add custom field management on machine detail page
- Fix: return customFieldValues in structure endpoint (was hardcoded null)
- Frontend: add editor to create/edit/delete custom field definitions
- Tests: add integration tests for structure values + definition CRUD

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 18:36:14 +01:00
33fc80cbc2 fix(security) : disable session migration on API firewall
Symfony's default session_fixation_strategy (migrate) regenerated the
session ID on every authenticated request, breaking concurrent API calls
from the SPA — only the first request succeeded, all others got 401.
The login controller already calls $session->migrate(true) explicitly,
so disabling automatic migration is safe.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 14:30:38 +01:00
33e3f25850 docs : update project documentation and frontend submodule pointer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 13:47:46 +01:00
efc6ec5691 test(api) : add comprehensive API test suite (161 tests)
- Add AbstractApiTestCase with auth helpers and entity factories
- Add tests for all entities: Machine, Piece, Composant, Product, Site,
  ModelType, Constructeur, CustomField, CustomFieldValue, Document,
  MachineComponentLink, MachinePieceLink, MachineProductLink, Profile
- Add controller tests: CommentController, EntityHistory
- Add HealthCheck, Filter, Pagination, Validation, Session tests
- Test auth (401), authorization (403), CRUD, and edge cases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 13:42:56 +01:00
b342d0e50a fix(security) : harden auth, session, document access and health endpoint
- Remove orphaned PUBLIC_ACCESS rule for deleted /api/test route
- Remove JWT login firewall (app is session-based only)
- Set APP_SECRET placeholder (real value must be in .env.local)
- Remove JWT env vars from .env
- Add session regeneration on login (prevent session fixation)
- Remove Document.path from API serialization groups (prevent path leak)
- Restrict health check details to ROLE_ADMIN (anonymes get status only)
- Add path traversal guard in DocumentStorageService
- Convert CreateProfileCommand password to interactive hidden prompt
- Restrict Profile Get endpoint to ROLE_ADMIN
- Change api firewall to stateless: false (matches session-based auth)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 13:42:09 +01:00
0709d01240 chore(config) : add DAMA test bundle, update API Platform config, improve makefile
- Register DAMADoctrineTestBundle for test env (transaction rollback)
- Update API Platform title/description, add pagination defaults
- Configure services for new controllers and commands
- Update makefile targets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 13:39:44 +01:00
74f77a3ba8 refactor(backend) : extract CuidEntityTrait, abstract audit subscriber, merge history controllers
- Extract shared ID generation + timestamps into CuidEntityTrait used by all entities
- Create AbstractAuditSubscriber to deduplicate audit logic across 7 subscribers
- Merge per-entity history controllers into single EntityHistoryController
- Delete redundant ComposantHistory/MachineHistory/PieceHistory/ProductHistoryController
- Add OpenApiDecorator for API documentation customization
- Disable failOnDeprecation in PHPUnit (vendor API Platform deprecation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 13:39:03 +01:00
bab13e5c57 chore : clean project config — untrack .idea/, gitignore Zone.Identifier and frontend/, blank JWT secret
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 02:01:54 +01:00
Matthieu
378026ebce chore(frontend) : update submodule — add buttons repositioned
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:32:56 +01:00
Matthieu
ea2b813728 chore(frontend) : update submodule — product delete confirmation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:07:06 +01:00
Matthieu
20653b9046 docs(changelog) : add delete confirmation dialog entry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:59:48 +01:00
Matthieu
c6deef6028 chore(frontend) : update submodule — delete confirmation dialog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:59:10 +01:00
Matthieu
e922b14419 feat(api) : add /api/health endpoint for monitoring
- Returns status, version, timestamp, PHP version, DB latency and memory usage
- Accessible without authentication (PUBLIC_ACCESS)
- Returns 200 when healthy, 503 when degraded (DB down)
2026-03-06 09:51:09 +01:00
Matthieu
d16b042739 chore(frontend) : update submodule — changelog v1.8.1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:39:07 +01:00
Matthieu
2b3c1fe08e docs(changelog) : complete v1.8.1 changelog with all frontend changes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:36:37 +01:00
Matthieu
51248b7854 chore(release) : v1.8.1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:27:29 +01:00
Matthieu
0e11f4ad2d refactor(api) : remove TypeMachine skeleton system, fix ModelType serialization
- Remove TypeMachine, TypeMachineComponentRequirement, TypeMachinePieceRequirement,
  TypeMachineProductRequirement entities and related repositories/state processor
- Replace MachineSkeletonController with MachineStructureController
- Link CustomField directly to Machine instead of TypeMachine
- Add migration to drop TypeMachine tables and migrate custom fields to machines
- Fix ModelType serialization: Annotation\Groups → Attribute\Groups (Symfony 8 compat)
  and add product:read, composant:read, piece:read groups for embedded category display
- Fix Profile: same Annotation → Attribute import
- Fix SearchFilter: partial → ipartial on Comment and Document
- Update frontend submodule (remove skeleton pages/components, simplify machine creation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:26:16 +01:00
Matthieu
f2539099bc chore(frontend) : update submodule — DataTable global + filtres server-side
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 16:07:03 +01:00
Matthieu
e5dc60467e feat(api) : ajout filtres SearchFilter ipartial sur noms de types et commentaires
- Piece : typePiece.name ipartial
- Composant : typeComposant.name ipartial
- Product : typeProduct.name ipartial + OrderFilter supplierPrice
- Comment : entityName partial + OrderFilter authorName, status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 16:05:24 +01:00
Matthieu
fbc0372bd6 docs(readme) : comprehensive project documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 10:49:35 +01:00
Matthieu
1483b0075b chore(frontend) : update submodule — README
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 10:45:53 +01:00
Matthieu
74e88923dc chore(frontend) : update submodule — README
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 10:38:19 +01:00
Matthieu
ef61d1a0d3 chore : remove obsolete docs and update submodule
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 10:10:40 +01:00
Matthieu
3f0fb0d5c2 chore : remove stale TODO.md and temp files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 10:06:08 +01:00
Matthieu
dd1497beac chore : bump v1.8.0, update changelog, gitignore and submodule
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 10:01:00 +01:00
Matthieu
7cd8772617 chore(frontend) : update submodule — navbar reorder and icons
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:31:36 +01:00
Matthieu
d89c97f0a0 feat(documents) : filesystem storage, server-side pagination and PDF compression
- Add DocumentStorageService for file-based storage (replaces Base64 in DB)
- Add DocumentServeController with /file and /download endpoints
- Add DocumentUploadProcessor using FormData + filesystem storage
- Add DocumentNormalizer exposing fileUrl/downloadUrl on all responses
- Add DocumentFileCleanupListener for automatic file deletion
- Add MigrateDocumentsToFilesystemCommand (Base64 → files, memory-safe)
- Add ApiFilter (SearchFilter, ExistsFilter, OrderFilter) on Document entity
- Add PdfCompressorService + refactor CompressPdfCommand for batch processing
- Fix TypeMachine PUT: deserialize=false + validate=false to prevent
  UniqueEntity false positive and writableLink collection interference
- Update CHANGELOG for v1.8.0
- Update frontend submodule

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:18:55 +01:00
Matthieu
7a5dd0b555 feat(skeleton) : add custom PUT processor and edit guard for linked machines
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 10:13:45 +01:00
Matthieu
44d69db560 chore(frontend) : update submodule — description field on catalog forms
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:35:57 +01:00
Matthieu
453065c9f0 feat(entities) : add description field to Piece and Composant
Add nullable TEXT description column to both pieces and composants
tables with corresponding Doctrine entity mappings, getters/setters
and serialization groups.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:35:37 +01:00
Matthieu
eb85323116 chore(frontend) : update submodule — fix site edit modal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 16:33:34 +01:00
Matthieu
2dfa501a65 fix(sites) : add PATCH operation and fix migration constraint drop
Add Patch operation to Site entity (was only Put, causing 405 errors).
Fix migration to use ALTER TABLE DROP CONSTRAINT instead of DROP INDEX
for the piece name unique constraint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 16:33:22 +01:00
Matthieu
c22f9dbf2b chore(release) : bump version to 1.7.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 14:36:58 +01:00
Matthieu
27a1b09d62 chore(frontend) : update submodule — comments system and constructeur fixes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 14:06:31 +01:00
Matthieu
7bbb693924 feat(comments) : add comment entity, controller and migration
Create Comment entity with API Platform annotations (GET, PATCH, DELETE).
Add CommentController with POST (create), PATCH (resolve) and GET
(unresolved count) endpoints. Add migration for comments table and
piece reference unique index.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 14:06:25 +01:00
Matthieu
9661fd5d91 fix(entities) : add unique constraints for constructeur name and piece reference
Add UniqueEntity validation on Constructeur.name and Piece.reference.
Move unique DB constraint from piece name to piece reference column.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 14:06:19 +01:00
Matthieu
d9ab583879 chore(frontend) : update submodule — package-lock.json
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:02:17 +01:00
Matthieu
5d41bda997 fix(ui) : replace checkbox with toggle switch for boolean custom fields
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 09:56:56 +01:00
Matthieu
3d037083c6 feat(ui) : display role badge in profile dropdown
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 09:42:09 +01:00
Matthieu
a3e440c254 feat(permissions) : add role-based access control system
Backend:
- Add role hierarchy (ADMIN > GESTIONNAIRE > VIEWER > USER) in security.yaml
- Add password authentication on profile activation (SessionProfileController)
- Add SessionProfileAuthenticator with stateless API firewall
- Add ProfilePasswordHasher state processor for API Platform
- Add security annotations on all 18 API Platform entities
- Add denyAccessUnlessGranted on all 13 custom controllers
- Add AdminProfileController for profile/role management (/api/admin/profiles)
- Add InitProfilePasswordsCommand for initial admin setup
- Simplify SessionProfilesController to list-only (removed create/delete)

Frontend (submodule update):
- Add usePermissions composable (isAdmin, canEdit, canView, isGranted)
- Add password login modal on profiles page
- Add admin backoffice page for profile management
- Disable all form fields for ROLE_VIEWER across all edit/create pages
- Show navigation buttons for all roles, hide destructive actions for viewers
- Add readonly mode to ModelTypeForm and site/constructeur modals
- Guard /admin routes in middleware
- Configure Vite proxy for API requests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 13:37:12 +01:00
Matthieu
adc44b99d3 fix(machines) : fix skeleton creation — pagination, duplication, custom fields
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:40:09 +01:00
Matthieu
60afeb4cfd chore(frontend) : update submodule — Playwright e2e setup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 09:07:37 +01:00
Matthieu
02ff8b1a96 feat(audit) : extend audit logging to machines, constructeurs, model types, documents and conversions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 14:51:26 +01:00
Matthieu
2156df22c6 chore(release) : bump version to 1.6.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 14:27:47 +01:00
Matthieu
cd2a3fac55 feat(categories) : add bidirectional piece/component category conversion
Backend service and controller for converting piece categories to component
categories (and vice-versa). Uses raw SQL in a transaction to preserve IDs
and transfer all related data (documents, custom fields, constructeurs).
Includes php-cs-fixer formatting pass on existing controllers/entities.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 14:27:07 +01:00
Matthieu
6300a3588a chore(docker) : replace pgAdmin with Adminer for lighter DB management
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 12:10:52 +01:00
Matthieu
45213103e4 Merge branch 'develop' into master — fix documents OOM 2026-02-11 17:16:41 +01:00
Matthieu
91b8b424d6 fix(documents) : add serialization groups to prevent OOM on collection endpoint
The path field (base64 data URIs) is now excluded from GetCollection
via document:list group. Individual GET returns path via document:detail
group. Related entities expose id+name in document:list for attachment
display. Frontend lazy-loads path on download/preview click.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 17:16:27 +01:00
Matthieu
0d1c9277e5 Merge branch 'develop' into master — changelog page 2026-02-11 17:01:53 +01:00
Matthieu
db16d26103 chore(frontend) : update submodule — changelog page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 17:01:45 +01:00
Matthieu
0eb64d0975 Merge branch 'develop' into master — v1.5.0 2026-02-11 16:51:22 +01:00
Matthieu
39e503ae18 chore(release) : bump version to 1.5.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 16:50:59 +01:00
Matthieu
70ed354c42 Merge branch 'fix/filtres-listes' into develop 2026-02-11 16:50:48 +01:00
Matthieu
ba98ae37f4 feat(entity) : auto-capitalize first letter of names on Composant and ModelType
Update setName() to use mb_strtoupper on the first character so that
category and component names always start with an uppercase letter.
Also update frontend submodule with URL state preservation and back
button improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 16:48:46 +01:00
Matthieu
906d39793f fix(filters) : repair broken filters on catalog and document pages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 15:33:20 +01:00
Matthieu
f970c1928d fix(api) : cap pagination to 200 items/page to prevent OOM in production
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 11:11:09 +01:00
Matthieu
2a1d966b87 chore(frontend) : update submodule — smart cache on composables
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 09:18:50 +01:00
Matthieu
a393b62e9f chore(frontend) : update submodule — Malio brand colors
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 09:06:30 +01:00
Matthieu
1247f72af6 chore(frontend) : update submodule — activity log + clickable catalog types
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 08:54:26 +01:00
Matthieu
6735bf252c feat(activity-log) : add paginated activity log endpoint and store constructeur names in audit
- New GET /api/activity-logs endpoint with pagination and filters
  (entityType, action) for the global activity log page
- Add findAllPaginated() to AuditLogRepository
- normalizeCollection() now stores {id, name} objects instead of
  bare IDs so constructeur changes display readable names

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 08:54:19 +01:00
Matthieu
508066d39f fix(frontend) : update submodule with custom field display fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 16:48:12 +01:00
Matthieu
70956c204e fix(audit) : inject Security for actor resolution + track custom field changes
- Inject Security service into all 3 audit subscribers to resolve
  actor profile from authenticated user (fixes "Par Inconnu" issue)
- Add CustomFieldValue tracking: insertions, updates, and deletions
  on custom field values now produce audit log entries on the parent
  entity (composant, piece, product) with field name prefix
  "customField:{name}"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 16:48:07 +01:00
Matthieu
16a7eac0c6 chore(release) : v1.4.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 15:59:55 +01:00
Matthieu
37ac08b182 chore(frontend) : update submodule — edit pages optimization
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 15:58:50 +01:00
Matthieu
5ef80b362e perf(api) : add serialization groups to CustomFieldValue and CustomField
Expose customField definitions (id, name, type, required, options, orderIndex)
inline in entity responses, eliminating separate API calls for custom field data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 15:58:43 +01:00
Matthieu
78f19daf76 chore(release) : v1.3.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 14:20:29 +01:00
Matthieu
6caa4a61df chore(frontend) : update submodule — API optimizations, cache invalidation, tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 14:19:24 +01:00
Matthieu
bf55034b2e chore(frontend) : complete frontend refactoring (F1-F7)
Update frontend submodule with 14 conventional commits covering:
- F1.1-F1.4: Decompose mega-components (machine detail/create, ComponentItem/PieceItem)
- F2.1-F2.3: Extract shared helpers (extractCollection, history, types)
- F3.2-F3.3: Migrate composables to TypeScript, eliminate explicit any
- F4.1-F4.2: Enable strict ESLint rules, remove debug console.logs
- F5.1: Split modelUtils into thematic modules
- F6.1-F6.2: Configure Vitest with 54 unit tests
- F7.2-F7.3: DaisyUI confirm modal, extract AppNavbar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 11:20:55 +01:00
Matthieu
ba1114e78b chore(frontend): update refactor plan and remove legacy frontend 2026-02-06 17:17:29 +01:00
Matthieu
5ccc3b30f0 docs : add comprehensive refactoring plan (backend + frontend)
14 phases, 39 tasks covering:
- Backend: security, code duplication, controllers, storage, tests
- Frontend: mega-component split, duplication, TypeScript migration, tests

Includes agent tracking system with status, journal, and rules.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 17:34:43 +01:00
8d83076be0 chore(release) : v1.2.0 2026-01-29 19:55:29 +01:00
Matthieu
997a3ae822 chore(frontend): update submodule to history UI 2026-01-25 21:22:24 +01:00
Matthieu
034c193e4b feat(audit): add history tracking and bump version to 1.1.2 2026-01-25 21:19:42 +01:00
Matthieu
4acc8d1c01 chore(release) : v1.1.1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:07:45 +01:00
Matthieu
49ff15f18d fix : correct commit message format in release script
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:07:08 +01:00
Matthieu
7a02617d48 chore : add qpdf to Docker image for PDF compression
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:06:08 +01:00
Matthieu
e52eef0491 docs : add PDF compression documentation to README
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:03:36 +01:00
Matthieu
a5118305d3 feat : automatic PDF compression on upload
- Add PdfCompressorService for lossless compression with qpdf
- Add DocumentPdfCompressorListener for automatic compression on persist/update
- Add app:compress-pdf command for batch compression of existing PDFs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:02:26 +01:00
Matthieu
b51671b1d4 chore(release) : v1.1.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 15:58:09 +01:00
Matthieu
1643dcf8c2 fix : case-insensitive search filters for all entities 2026-01-25 15:54:07 +01:00
Matthieu
17ab4cdd16 chore : update fixtures with current database data 2026-01-25 15:44:22 +01:00
Matthieu
d9182131d9 chore : reset migrations to single initial schema + add deployment guide 2026-01-25 15:41:06 +01:00
156 changed files with 14196 additions and 19339 deletions

7
.env
View File

@@ -16,7 +16,7 @@
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=
APP_SECRET=change_me_in_env_local
APP_SHARE_DIR=var/share
###< symfony/framework-bundle ###
@@ -40,8 +40,3 @@ DEFAULT_URI=http://localhost
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
###< 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 ###

19
.gitignore vendored
View File

@@ -32,9 +32,20 @@ docker/.env.docker.local
/_archives/
###< migration archives ###
###> temp files ###
*.sql
*.har
FEATURE_IDEAS.md
###< temp files ###
###> frontend ###
/frontend/node_modules/
/frontend/.nuxt/
/frontend/.output/
/frontend/dist/
/frontend/
###< frontend ###
###> ide ###
/.idea/
###< ide ###
###> wsl ###
*:Zone.Identifier
###< wsl ###

9
.idea/.gitignore generated vendored
View File

@@ -1,9 +0,0 @@
# Default ignored files
/shelf/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

144
.idea/Inventory.iml generated
View File

@@ -1,144 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="App\" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="App\Tests\" />
<excludeFolder url="file://$MODULE_DIR$/public/bundles" />
<excludeFolder url="file://$MODULE_DIR$/var" />
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/doctrine-common" />
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/doctrine-orm" />
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/documentation" />
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/http-cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/hydra" />
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/json-schema" />
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/jsonld" />
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/metadata" />
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/openapi" />
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/serializer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/state" />
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/symfony" />
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/validator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/clue/ndjson-react" />
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/collections" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/common" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/dbal" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/deprecations" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/event-manager" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/inflector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/instantiator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/lexer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/migrations" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/orm" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/persistence" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/sql-formatter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/evenement/evenement" />
<excludeFolder url="file://$MODULE_DIR$/vendor/fidry/cpu-core-counter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/friendsofphp/php-cs-fixer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nelmio/cors-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-common" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-docblock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/type-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpstan/phpdoc-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/clock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/event-dispatcher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/link" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/log" />
<excludeFolder url="file://$MODULE_DIR$/vendor/react/cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/react/child-process" />
<excludeFolder url="file://$MODULE_DIR$/vendor/react/dns" />
<excludeFolder url="file://$MODULE_DIR$/vendor/react/event-loop" />
<excludeFolder url="file://$MODULE_DIR$/vendor/react/promise" />
<excludeFolder url="file://$MODULE_DIR$/vendor/react/socket" />
<excludeFolder url="file://$MODULE_DIR$/vendor/react/stream" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/diff" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/asset" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/cache-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/clock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/config" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/console" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dependency-injection" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/deprecation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/doctrine-bridge" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dotenv" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/error-handler" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/expression-language" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/filesystem" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/finder" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/flex" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/framework-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-foundation" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-kernel" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/options-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/password-hasher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php85" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-uuid" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/process" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-access" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-info" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/routing" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/runtime" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-core" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-csrf" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-http" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/serializer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/stopwatch" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/string" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/twig-bridge" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/twig-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/type-info" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/uid" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/validator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-dumper" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-exporter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/web-link" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/yaml" />
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/twig" />
<excludeFolder url="file://$MODULE_DIR$/vendor/webmozart/assert" />
<excludeFolder url="file://$MODULE_DIR$/vendor/willdurand/negotiation" />
<excludeFolder url="file://$MODULE_DIR$/vendor/myclabs/deep-copy" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nikic/php-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/manifest" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/version" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-code-coverage" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-file-iterator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-invoker" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-text-template" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-timer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/phpunit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/cli-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/comparator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/complexity" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/environment" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/exporter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/global-state" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/lines-of-code" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-enumerator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-reflector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/recursion-context" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/type" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/version" />
<excludeFolder url="file://$MODULE_DIR$/vendor/staabm/side-effects-detector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/browser-kit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/css-selector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dom-crawler" />
<excludeFolder url="file://$MODULE_DIR$/vendor/theseer/tokenizer" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

12
.idea/dataSources.xml generated
View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="ferme" uuid="f407a514-c6b4-4b26-9555-445a85892502">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/ferme</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="InertiaPackage">
<option name="directoryPaths">
<list />
</option>
</component>
</project>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-70fca0d0:19b8da49b68:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ferme.iml" filepath="$PROJECT_DIR$/.idea/ferme.iml" />
</modules>
</component>
</project>

160
.idea/php.xml generated
View File

@@ -1,160 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpIncludePathManager">
<include_path>
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
<path value="$PROJECT_DIR$/vendor/doctrine/common" />
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/vendor/api-platform/jsonld" />
<path value="$PROJECT_DIR$/vendor/api-platform/hydra" />
<path value="$PROJECT_DIR$/vendor/willdurand/negotiation" />
<path value="$PROJECT_DIR$/vendor/api-platform/http-cache" />
<path value="$PROJECT_DIR$/vendor/api-platform/serializer" />
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-orm" />
<path value="$PROJECT_DIR$/vendor/api-platform/validator" />
<path value="$PROJECT_DIR$/vendor/api-platform/state" />
<path value="$PROJECT_DIR$/vendor/api-platform/metadata" />
<path value="$PROJECT_DIR$/vendor/api-platform/symfony" />
<path value="$PROJECT_DIR$/vendor/api-platform/openapi" />
<path value="$PROJECT_DIR$/vendor/api-platform/documentation" />
<path value="$PROJECT_DIR$/vendor/friendsofphp/php-cs-fixer" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
<path value="$PROJECT_DIR$/vendor/api-platform/json-schema" />
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-common" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
<path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/psr/log" />
<path value="$PROJECT_DIR$/vendor/psr/link" />
<path value="$PROJECT_DIR$/vendor/psr/cache" />
<path value="$PROJECT_DIR$/vendor/psr/clock" />
<path value="$PROJECT_DIR$/vendor/clue/ndjson-react" />
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/psr/container" />
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/vendor/react/promise" />
<path value="$PROJECT_DIR$/vendor/twig/twig" />
<path value="$PROJECT_DIR$/vendor/fidry/cpu-core-counter" />
<path value="$PROJECT_DIR$/vendor/react/stream" />
<path value="$PROJECT_DIR$/vendor/react/child-process" />
<path value="$PROJECT_DIR$/vendor/react/cache" />
<path value="$PROJECT_DIR$/vendor/react/event-loop" />
<path value="$PROJECT_DIR$/vendor/nelmio/cors-bundle" />
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
<path value="$PROJECT_DIR$/vendor/react/socket" />
<path value="$PROJECT_DIR$/vendor/react/dns" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
<path value="$PROJECT_DIR$/vendor/symfony/console" />
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
<path value="$PROJECT_DIR$/vendor/symfony/web-link" />
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/process" />
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
<path value="$PROJECT_DIR$/vendor/symfony/config" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/vendor/symfony/string" />
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
<path value="$PROJECT_DIR$/vendor/symfony/expression-language" />
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.4" />
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PhpUnit">
<phpunit_settings>
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" />
</phpunit_settings>
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

6
.idea/symfony2.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Symfony2PluginSettings">
<option name="pluginEnabled" value="true" />
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

6
.idea/workspace.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ComposerSettings">
<execution />
</component>
</project>

View File

@@ -1,425 +0,0 @@
# 📔 Carnet de Bord - Migration Inventory → Symfony
**Projet** : Migration backend NestJS/Prisma → Symfony/API Platform
**Début** : 2026-01-10
**Objectif** : Migrer vers Symfony + JWT + API Platform propre et maintenable
---
## 🔗 Convention de liaison des commits (INV)
- **Format** : `[INV-YYYYMMDD-XX]`
- **Usage** : même code dans les commits du backend **et** du frontend + ajout ici pour retrouver le duo rapidement.
## 🧾 Journal des liaisons INV
- INV-20260111-01 : ajout du lien submodule `Inventory_frontend` (commit backend : `987aa5c`, commit frontend : `936a73f`)
- INV-20260111-02 : alignement front API Platform + sessions (commit backend : `f7fc1bd`, commit frontend : `e99f053`)
## 🎯 Contexte
- **Situation initiale** :
- `Inventory_backend/` : NestJS + Prisma (fonctionnel, ~11k lignes)
- `Inventory_frontend/` : Nuxt 3 (fonctionnel, 105 fichiers)
- Base de données PostgreSQL avec données en production
- **Objectif** :
- Backend Symfony 8 + API Platform + JWT
- Garder les données existantes (migration Prisma → Doctrine)
- Frontend Nuxt connecté au nouveau backend
- Docker : 2 backends en parallèle pendant transition
---
## ✅ Phase 1 : Préparation (TERMINÉE - 10/01/2026)
### Ce qui a été fait
#### 1. Docker & Infrastructure ✅
- **pgAdmin ajouté** au docker-compose.yml
- Port : 5050
- Login : admin@admin.com / admin
- Container : `pgadmin-inventory`
- Volume persistant : `pgadmin_data`
- **Serveur PostgreSQL pré-configuré** :
- Fichier `docker/pgadmin/servers.json` monté automatiquement
- Fichier `docker/pgadmin/pgpass` pour authentification sans mot de passe
- Connexion automatique à `db:5432/inventory` au démarrage
- Nom du serveur : "Inventory PostgreSQL"
#### 2. Bundles Symfony installés ✅
```bash
# Versions installées
- lexik/jwt-authentication-bundle: v3.2.0
- vich/uploader-bundle: v2.9.1
- symfony/uid: 8.0.*
```
#### 3. JWT Configuration ✅
- **Clés RSA générées** : `config/jwt/private.pem` + `public.pem`
- **security.yaml configuré** :
- Firewall `login` : pattern `^/api/login_check` avec `json_login`
- Firewall `api` : pattern `^/api` avec `jwt` authenticator
- Provider : `app_user_provider` (entité Profile via email)
- Password hasher : bcrypt auto
#### 4. Entité Profile créée ✅
**Fichier** : `src/Entity/Profile.php`
**Caractéristiques** :
- Implémente `UserInterface` + `PasswordAuthenticatedUserInterface`
- Champs :
- `id` : string (30 chars, CUID-compatible pour Prisma)
- `email` : string unique (username pour JWT)
- `password` : string (hashed)
- `roles` : array JSON (ROLE_USER par défaut)
- `firstName`, `lastName` : string
- `isActive` : boolean
- `createdAt`, `updatedAt` : DateTimeImmutable
- Repository : `ProfileRepository` avec `PasswordUpgraderInterface`
- API Platform : endpoints CRUD auto-générés
#### 5. Base de Données ✅
- **Migration créée** : `Version20260110175413`
- **Table** : `profiles` créée avec succès
- **Utilisateur test créé** :
```
Email: admin@admin.com
Password: admin123
Roles: ['ROLE_USER', 'ROLE_ADMIN']
```
#### 6. API Platform ✅
- **Endpoint racine** : http://localhost:8081/api/
- **Réponse** :
```json
{
"@context": "/api/contexts/Entrypoint",
"@id": "/api/",
"@type": "Entrypoint",
"profile": "/api/profiles"
}
```
- **OpenAPI Docs** : Configurées (à tester)
#### 7. Configuration Apache ✅
- **VirtualHost** : `docker/php/config/vhost.conf`
- **DocumentRoot** : `/var/www/html/public`
- **AllowOverride** : All (pour `.htaccess`)
- **Port** : 8081 (Apache) → accessible depuis l'hôte
#### 8. Routing Symfony ✅
- **Routes définies** :
- `/api/login_check` : Login JWT
- `/api/test` : Test endpoint (TestController)
- `/api/*` : API Platform auto-routes
- **Vérification** :
```bash
php bin/console debug:router api_test
php bin/console router:match /api/test --method=GET
# ✅ Route found and matches
```
#### 9. .htaccess créé ✅
**Fichier** : `public/.htaccess`
**Contenu** : Symfony standard avec mod_rewrite
---
### ⚠️ Problèmes identifiés
#### 1. Routes inaccessibles via Apache (404)
**Symptôme** :
```bash
curl http://localhost:8081/api/test
# → 404 Not Found
```
**Tests effectués** :
- ✅ Route existe : `php bin/console debug:router api_test`
- ✅ Route match : `php bin/console router:match /api/test`
- ✅ Symfony fonctionne : `curl http://localhost:8081/api/` → JSON OK
- ✅ PHP built-in server OK :
```bash
php -S localhost:9000
curl http://localhost:9000/api/test
# → {"status":"ok","message":"Test endpoint works!"}
```
- ❌ Apache 404 : Depuis l'hôte via port 8081
**Diagnostic** :
- Le problème est **Apache-spécifique**
- Symfony/PHP fonctionnent correctement
- Le `.htaccess` n'est probablement **PAS lu par Apache**
- Hypothèses :
1. `AllowOverride All` non pris en compte
2. `mod_rewrite` mal configuré
3. Ordre des directives Apache incorrect
4. Problème de permissions sur `.htaccess`
**Actions à faire** :
- [ ] Vérifier permissions `.htaccess` dans container
- [ ] Tester `apache2ctl configtest`
- [ ] Activer logs de rewrite : `LogLevel alert rewrite:trace3`
- [ ] Tester FallbackResource dans vhost au lieu de `.htaccess`
#### 2. JWT Login non testé
**Raison** : Bloqué par problème #1 (routes inaccessibles)
**Actions à faire** :
- [ ] Résoudre problème Apache
- [ ] Tester `POST /api/login_check` avec credentials
- [ ] Vérifier génération du token JWT
- [ ] Tester route protégée avec token
---
## 📝 Configuration Actuelle
### Docker Compose
```yaml
services:
web:
ports:
- "8081:80" # Symfony API
- "3001:3000" # (prévu pour Nuxt)
db:
ports:
- "5433:5432" # PostgreSQL
pgadmin:
ports:
- "5050:80" # pgAdmin
```
### Variables d'Environnement
**Fichier** : `docker/.env.docker.local`
```env
# PostgreSQL
POSTGRES_DB=inventory
POSTGRES_USER=root
POSTGRES_PASSWORD=root
POSTGRES_PORT=5433
---
## ✅ Phase 2 : Migration DB + Frontend (TERMINÉE - 10/01/2026)
### Ce qui a été fait
#### 1. Entités Doctrine alignées Prisma ✅
- **Toutes les entités manquantes** créées (Machine, ModelType, Composant, Piece, Product, Links, Requirements, CustomField, Document, etc.)
- **IDs en string(36)** pour compatibilité CUID/UUID
- **Colonnes Prisma en camelCase** conservées via `name="..."` (ex: `machineId`, `createdAt`, `supplierPrice`)
- **Corrections** :
- `Document.path` passé en `TEXT`
- `CustomField.options` nullable
- `TypeMachineComponentRequirement.required` corrigé
#### 2. Migration DB inventory-data → inventory ✅
- **Dump data-only + normalisation** (conversion des identifiants quoted vers lowercase)
- **Mapping table Prisma** : `"ModelType"` → `model_types`
- **Exclusions** : `profiles`, `_prisma_migrations`
- **Import validé** : `Counts match for all tables.`
Scripts utiles :
```bash
scripts/normalize-dump.py
scripts/validate-migration.php
```
#### 3. Frontend basculé sur Inventory_frontend ✅
- `make dev-nuxt` pointe vers `Inventory_frontend/`
- `README.md` mis à jour
- **Base API** ajustée : `http://localhost:8081/api`
Fichiers modifiés :
```
makefile
README.md
Inventory_frontend/.env
Inventory_frontend/nuxt.config.ts
Inventory_frontend/app/services/modelTypes.ts
```
---
# pgAdmin
PGADMIN_EMAIL=admin@admin.com
PGADMIN_PASSWORD=admin
PGADMIN_PORT=5050
# Symfony
APP_ENV=dev
APP_SECRET=changeme_super_secret_key_123456789
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=your_jwt_passphrase_change_me
# NestJS (pour futur parallèle)
NESTJS_PORT=3000
SESSION_SECRET=changeme_session_secret
CORS_ORIGIN=http://localhost:3001
```
---
## 🚧 Phase 2 : Debugging & Tests (EN COURS)
### Objectifs
- [x] Résoudre problème Apache `.htaccess`
- [ ] Tester authentification JWT complète
- [ ] Créer endpoint de test public fonctionnel
- [ ] Documenter la solution Apache
### Prochaines étapes
1. **Fix Apache** : Logs de debug + test FallbackResource
2. **Test JWT** : Login + génération token + route protégée
3. **Documentation** : Documenter la config Apache qui fonctionne
---
## 📊 Métriques
### Temps passé
- **Phase 1** : ~3h (exploration + setup + debugging)
- **Problème Apache** : ~1h30 (en cours)
### Fichiers créés/modifiés
**Nouveaux fichiers** :
- `src/Entity/Profile.php`
- `src/Repository/ProfileRepository.php`
- `src/Controller/TestController.php`
- `public/.htaccess`
- `config/routes/routing.controllers.yaml`
- `create_test_user.php` (script utilitaire)
- `migrations/Version20260110175413.php`
- `docker/pgadmin/servers.json` (config serveur PostgreSQL)
- `docker/pgadmin/pgpass` (credentials PostgreSQL)
- `CARNET_DE_BORD.md` (ce fichier)
**Fichiers modifiés** :
- `docker-compose.yml` (+ pgAdmin)
- `docker/.env.docker.local` (+ variables Symfony/JWT/pgAdmin)
- `docker/php/config/vhost.conf` (DocumentRoot → public/)
- `config/packages/security.yaml` (JWT firewalls)
- `config/routes.yaml` (+ api_login_check)
- `composer.json` (+ lexik JWT, vich uploader)
---
## 🎓 Leçons Apprises
### 1. Symfony 8 + API Platform
- **Attributs PHP 8** : `use Symfony\Component\Routing\Attribute\Route;` (pas `Annotation`)
- **Routes controllers** : Nécessite `config/routes/routing.controllers.yaml` avec `type: attribute`
- **API Platform** : Auto-génère les endpoints CRUD avec `#[ApiResource]`
### 2. JWT Authentication
- **3 composants** :
1. Firewall `login` : `json_login` intercepte `/api/login_check`
2. Firewall `api` : `jwt` vérifie le token sur `/api/*`
3. Access control : `PUBLIC_ACCESS` vs `IS_AUTHENTICATED_FULLY`
- **username_path** : Permet de mapper `email` au lieu de `username`
- **Provider** : Doit être défini dans le firewall `login`
### 3. Doctrine Migrations
- **ID Prisma CUID** : Garder en `string(30)` pour compatibilité
- **Lifecycle callbacks** : `#[ORM\PrePersist]` pour `createdAt`/`updatedAt`
- **UserInterface** : Nécessite `getUserIdentifier()`, `getRoles()`, `eraseCredentials()`
### 4. Docker & Apache
- **`.htaccess` vs VirtualHost** : Le vhost peut override le `.htaccess`
- **AllowOverride All** : Indispensable pour que `.htaccess` fonctionne
- **FallbackResource** : Alternative au mod_rewrite dans `.htaccess`
- **Debugging** : Tester avec PHP built-in server pour isoler le problème
---
## 📚 Ressources Utiles
### Accès aux Services
```
🌐 pgAdmin: http://localhost:5050
└─ Login: admin@admin.com / admin
└─ Serveur: "Inventory PostgreSQL" (pré-configuré)
└─ Database: inventory
└─ Note: Le serveur PostgreSQL est automatiquement connecté au démarrage
🌐 API Platform: http://localhost:8081/api/
└─ Docs: http://localhost:8081/api/docs (à venir)
🗄️ PostgreSQL: localhost:5433
└─ User: root / root
└─ Database: inventory
```
### Commandes fréquentes
```bash
# Symfony
make shell # Entrer dans le container
php bin/console cache:clear # Clear cache
make cache-clear-full # Clear cache + purge var/cache
php bin/console debug:router # Lister routes
php bin/console debug:firewall # Lister firewalls
php bin/console doctrine:migrations:migrate # Exécuter migrations
# Docker
make start # Démarrer containers
make stop # Arrêter containers
docker logs -f php-inventory-apache # Logs Apache
docker logs -f pgadmin-inventory # Logs pgAdmin
docker exec php-inventory-apache bash # Shell root
# Tests API
curl http://localhost:8081/api/ # Test API Platform
curl -X POST http://localhost:8081/api/login_check \
-H "Content-Type: application/json" \
-d '{"email":"admin@admin.com","password":"admin123"}'
```
### Documentation
- [Lexik JWT Bundle](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst)
- [API Platform Security](https://api-platform.com/docs/core/security/)
- [Symfony Security](https://symfony.com/doc/current/security.html)
---
## 🔄 Historique des Changements
### 2026-01-15 - Session 3
- ✅ Filtre API Platform `category` sur `ModelType`
- ✅ Normalisation des structures `ModelType` (structure ↔ skeleton)
- ✅ Migration `custom_fields.options` en JSON
- ✅ Ajout commande `make cache-clear-full`
- ✅ Correctifs frontend: headers API Platform, pagination par catégorie, persistance tri
### 2026-01-10 - Session 2 (20h30)
- ✅ Problème Apache résolu (routes fonctionnelles)
- ✅ Phase 2 complète (JWT 100% opérationnel)
- ✅ Authentification testée avec succès
- ✅ Réorganisation projet (frontend/ + _archives/)
- ✅ État des lieux dans MIGRATION_PLAN.md
- ✅ 5 commits conventionnels créés
- 📊 Base inventory-data analysée (673 lignes)
### 2026-01-10 - Session 1 (19h00)
- ✅ Création projet migration
- ✅ Phase 1 complète (pgAdmin, JWT, Profile, migrations)
- ⚠️ Problème Apache identifié (routes 404)
- 📝 Carnet de bord créé
---
**Dernière mise à jour** : 2026-01-15 13:45
**Statut** : Phase 3 EN COURS ⚠️ - Migrations et intégration frontend

View File

@@ -1,15 +1,78 @@
# Changelog
Liste des évolutions du projet inventory
## [1.8.1] - 2026-03-05
## [0.0.0]
### Parameters
Ajouter dans le fichier .env
- DEFAULT_URI
- DATABASE_URL
### Ajouts
- **Composant DataTable generique** : nouveau composant `DataTable.vue` + composable `useDataTable.ts` avec tri, recherche, pagination et filtres server-side. Toutes les pages catalogue (composants, pieces, produits, documents, constructeurs, commentaires, journal d'audit, admin) migrees vers ce composant partage.
- **Messages d'erreur humanises** : les erreurs backend (violations de contraintes, erreurs serveur) sont desormais traduites en messages comprehensibles pour l'utilisateur final (`errorMessages.ts`).
- **Icones Lucide dans la navbar** : reorganisation des groupes de navigation et ajout d'icones pour chaque section.
- **Modal d'ajout d'entites aux machines** (`AddEntityToMachineModal.vue`) : ajout direct de composants, pieces et produits depuis la fiche machine.
- **Filtres SearchFilter ipartial** sur les noms de types de modeles et commentaires cote API.
### Added
### Refactoring
- **Suppression du systeme TypeMachine (squelettes machines)** : les entites `TypeMachine`, `TypeMachineComponentRequirement`, `TypeMachinePieceRequirement`, `TypeMachineProductRequirement` sont supprimees avec leurs repositories et state processors. Les champs personnalises machines sont desormais lies directement a chaque machine (relation `CustomField → Machine`).
- **Suppression des pages squelettes machines** : pages `/machine-skeleton`, `/type/[id]`, `/type/edit/[id]` et tous les composants associes (`TypeEditForm`, `MachineSkeletonSummary`, `MachineCreatePreview`, selectors de requirements, `useMachineTypesApi`, `useMachineSkeletonEditor`, `useMachineCreateSelections`, `useMachineCreatePreview`).
- **Simplification de la creation de machines** : plus besoin de selectionner un squelette, ajout direct de composants/pieces/produits.
- **Refactoring MachineStructureController** : remplacement de `MachineSkeletonController` par `MachineStructureController` avec gestion directe de la structure machine.
- **Migration de toutes les tables vers DataTable** : suppression du code de tableau duplique dans chaque page au profit du composant generique.
### Changed
### Corrections
- **Suppression catalogue avec confirmation** : la suppression d'une piece ou d'un composant dans le catalogue affiche desormais une modale de confirmation listant les elements qui seront supprimes en cascade (documents, liaisons machine, valeurs de champs personnalises) au lieu de bloquer la suppression.
- **Fix affichage categorie sur les pages edit** : les categories (produit, composant, piece) s'affichent correctement sur les pages d'edition au lieu de "Categorie inconnue". Cause : import `Serializer\Annotation\Groups` obsolete dans `ModelType` (remplace par `Attribute\Groups` pour Symfony 8) + groupes de serialisation manquants (`product:read`, `composant:read`, `piece:read`).
- Fix import `Serializer\Annotation\Groups``Attribute\Groups` dans `Profile`.
- Fix filtre `SearchFilter` : `partial``ipartial` sur `Comment.entityName` et `Document.name`/`Document.filename` pour recherche insensible a la casse.
### Fixed
### Migration requise
```bash
docker compose exec web php bin/console doctrine:migrations:migrate
```
## [1.8.0] - 2026-03-03
### Ajouts
- **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.
- **Commande de migration** `app:migrate-documents-to-filesystem` : migre les documents existants (Base64 → fichiers) avec dry-run, batch-size et limit.
- **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).
- **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.
### 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.
### 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
```
## [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-12
- Version initiale avec gestion du parc machines, pieces, composants, produits et categories.

203
CLAUDE.md Normal file
View File

@@ -0,0 +1,203 @@
# 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 interactif (nécessite un TTY)
make install # Install complet (composer + npm + build)
# Backend
make test # PHPUnit (tous les tests)
make test FILES=tests/Api/Entity/MachineTest.php # Un test spécifique
make php-cs-fixer-allow-risky # Linter PHP (cs-fixer)
docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate
# Frontend (dans Inventory_frontend/)
npm run dev # Dev server (port 3001)
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`, `ModelType`, `CustomField`, `CustomFieldValue`, `Document`, `AuditLog`, `Comment`, `Profile`, `MachineComponentLink`, `MachinePieceLink`, `MachineProductLink`
### 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
### Custom Controllers (pas API Platform)
- `MachineStructureController``/api/machines/{id}/structure` (GET/PATCH) : hiérarchie complète machine avec normalisation JSON manuelle (pas Symfony Serializer). Source principale de données pour la page détail machine.
- `MachineCustomFieldsController``/api/machines/{id}/add-custom-fields` (POST) : initialise les CustomFieldValue manquants pour une machine.
- `CustomFieldValueController``/api/custom-fields/values/*` : CRUD + upsert pour les valeurs de champs perso.
### Custom Fields — Architecture
- **Composants/Pièces/Produits** : définitions dans le JSON `structure` du ModelType
- **Machines** : définitions = entités `CustomField` liées directement via `machineId` FK (pas de ModelType)
- Les deux partagent la même entité `CustomFieldValue` pour stocker les valeurs
### 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
### CLAUDE.md — Maintenance obligatoire
- **Toujours consulter** ce fichier en début de conversation pour respecter les conventions
- **Mettre à jour** ce fichier quand une nouvelle convention, pattern ou décision architecturale est établie
- **Utiliser comme source de vérité** pour les commandes, patterns et règles du projet
### Toujours faire AVANT de modifier du code
1. **Lire le fichier** avant de l'éditer — ne jamais proposer de changements sur du code non lu
2. **Comprendre le pattern existant** — reproduire le style du fichier (noms, indentation, structure)
3. **Vérifier les deux repos** — un changement peut impacter backend ET frontend
### Après chaque modification
1. Backend PHP : `make php-cs-fixer-allow-risky`
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)
## Tests
### Stack de test
- **PHPUnit 12** + **API Platform Test** (`ApiTestCase`)
- **DAMA DoctrineTestBundle** — wrappe chaque test dans une transaction avec rollback automatique (pas de TRUNCATE)
- Base de test : même PG, env `test`
### Commandes
Voir section "Key Commands". Commande additionnelle :
```bash
make test-setup # Créer/mettre à jour le schéma test
```
### Pattern de test
- Hériter de `AbstractApiTestCase` (helpers auth + factories)
- Ne PAS faire de TRUNCATE/cleanup dans tearDown — DAMA s'en occupe par rollback
- Factories : `createProfile()`, `createMachine()`, `createSite()`, `createComposant()`, `createPiece()`, `createProduct()`, `createCustomField()`, `createCustomFieldValue()`, `createModelType()`, `createMachineComponentLink()`, `createMachinePieceLink()`, `createMachineProductLink()`
- Auth : `createViewerClient()`, `createGestionnaireClient()`, `createAdminClient()`
## 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)

312
DEPLOY.md Normal file
View File

@@ -0,0 +1,312 @@
# Inventory — Guide de Déploiement
Guide pour déployer l'application sur un serveur de production.
## Architecture de production
```
inventory.malio-dev.fr/ → Frontend Nuxt (fichiers statiques servis par Nginx)
inventory.malio-dev.fr/api → Backend Symfony (PHP-FPM derrière Nginx)
```
| Composant | Technologie | Emplacement serveur |
|-----------|-------------|---------------------|
| Backend | Symfony 8 + API Platform | `/var/www/Inventory/` |
| Frontend | Nuxt 4 (site statique) | `/var/www/Inventory/Inventory_frontend/.output/public/` |
| Base de données | PostgreSQL 16 | Base `inventory` |
### Schéma simplifié
```
Navigateur
↓ HTTPS
Nginx (reverse proxy)
├── /api/* → PHP-FPM (Symfony) → PostgreSQL
└── /* → Fichiers statiques (Nuxt build)
```
---
## 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érification des prérequis
```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 (sans les outils de dev)
composer install --no-dev --optimize-autoloader
# Créer le fichier de configuration locale
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$'
EOF
# Générer un secret aléatoire
sed -i "s/CHANGE_ME/$(openssl rand -hex 32)/" .env.local
# Permissions pour le dossier var/ (cache, logs)
sudo chown -R www-data:www-data var/
sudo chmod -R 775 var/
# Vider le cache
php bin/console cache:clear --env=prod
# Appliquer les migrations (si première installation ou mise à jour)
php bin/console doctrine:migrations:migrate --no-interaction
```
### 4. Configurer le frontend Nuxt
```bash
cd /var/www/Inventory/Inventory_frontend
# Permissions
sudo chown -R malio:malio .
# Installer les dépendances
npm install
# Créer le fichier d'environnement
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 pour les uploads de documents)
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 — toutes les requêtes /api
location /api {
root /var/www/Inventory/public;
try_files $uri /index.php$is_args$args;
}
# PHP-FPM (exécute le code PHP)
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
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 — tout le reste
location / {
root /var/www/Inventory/Inventory_frontend/.output/public;
index index.html;
try_files $uri $uri/ /index.html; # SPA fallback
}
}
```
Activer le site :
```bash
sudo ln -s /etc/nginx/sites-available/inventory /etc/nginx/sites-enabled/
sudo nginx -t # Vérifier la syntaxe
sudo systemctl reload nginx
```
### 6. Vérifier
```bash
curl http://inventory.malio-dev.fr # Frontend
curl http://inventory.malio-dev.fr/api # API (doc Swagger)
```
---
## Mises à jour
### Mettre à jour l'application
```bash
cd /var/www/Inventory
# Récupérer les changements
git pull
git submodule update --init --recursive
# Backend
composer install --no-dev --optimize-autoloader
php bin/console doctrine:migrations:migrate --no-interaction
php bin/console cache:clear --env=prod
sudo chown -R www-data:www-data var/
# Frontend
cd Inventory_frontend
npm install
npx nuxi generate
```
---
## Backup base de données
### Export (faire un backup)
```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 (restaurer un backup)
```bash
psql -U ferme_user -h 127.0.0.1 -d inventory -f backup_inventory_YYYYMMDD.sql
```
---
## Troubleshooting
### Erreur 502 Bad Gateway
PHP-FPM ne tourne pas ou est crashé :
```bash
systemctl status php8.4-fpm
sudo systemctl restart php8.4-fpm
```
### Erreur 403 Forbidden
Problème de permissions sur les fichiers :
```bash
sudo chown -R www-data:www-data /var/www/Inventory/var/
sudo chmod -R 775 /var/www/Inventory/var/
```
### Erreur API "No route found"
Le cache Symfony est probablement périmé :
```bash
php /var/www/Inventory/bin/console cache:clear --env=prod
```
### Frontend ne se met pas à jour
Les fichiers statiques sont en cache. Rebuilder :
```bash
cd /var/www/Inventory/Inventory_frontend
rm -rf .output
npx nuxi generate
```
### L'API retourne 401 sur toutes les requêtes
La session PHP ne se crée pas correctement. Vérifier :
```bash
# Vérifier que le dossier de sessions existe et est accessible
ls -la /var/lib/php/sessions/
# Ou vérifier les logs Symfony
tail -f /var/www/Inventory/var/log/prod.log
```
---
## Commandes utiles en production
```bash
# Logs Nginx
tail -f /var/log/nginx/inventory-error.log
tail -f /var/log/nginx/inventory-access.log
# Logs Symfony
tail -f /var/www/Inventory/var/log/prod.log
# 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 des services
systemctl status php8.4-fpm
systemctl status nginx
systemctl status postgresql
# Redémarrer les services
sudo systemctl restart php8.4-fpm
sudo systemctl reload nginx
```

File diff suppressed because it is too large Load Diff

332
README.md
View File

@@ -1,63 +1,305 @@
# Projet Inventory
# InventoryTEST
## Installation du projet
### Windows
Pour windows, il faut installer le WSL2, Ubuntu, docker et nvm.
Il suffit de suivre cette [doc](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/windows)
Application de gestion d'inventaire industriel pour **Malio**. Gestion complète du parc machines, des pièces, composants, produits, fournisseurs et documents associés, avec traçabilité et contrôle d'accès par rôles.
### Linux
Pour linux, il faut installer docker et nvm.
Il suffit de suivre cette [doc](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/linux)
## C'est quoi ce projet ?
Inventory est une application web qui permet de gérer un parc de machines industrielles. Concrètement, elle permet de :
- **Cataloguer** les machines d'une usine, site par site
- **Décomposer** chaque machine en composants, pièces et produits (structure arborescente)
- **Suivre** les fournisseurs/constructeurs de chaque élément
- **Stocker** les documents techniques (PDF, images, fiches techniques)
- **Tracer** toutes les modifications (qui a changé quoi, quand) via un journal d'audit
- **Commenter** les fiches pour collaborer entre équipes
- **Gérer les accès** avec un système de rôles (admin, gestionnaire, lecteur)
L'application se compose de deux parties :
- Un **backend** (API REST) qui gère les données, la sécurité et la logique métier
- Un **frontend** (interface web) qui affiche les données et permet l'interaction utilisateur
## Stack technique
| Couche | Technologie | Version | Rôle |
|--------|-------------|---------|------|
| Backend | Symfony + API Platform | 8.0 / 4.2 | API REST, logique métier, sécurité |
| PHP | PHP | >= 8.4 | Langage backend |
| Base de données | PostgreSQL | 16 | Stockage des données |
| Frontend | Nuxt (SPA, SSR off) | 4 | Framework web (rendu côté client) |
| UI | Vue 3 Composition API + TypeScript | 3.5 / 5.7 | Composants d'interface |
| CSS | TailwindCSS + DaisyUI | 4 / 5 | Mise en page et composants visuels |
| Conteneurs | Docker Compose | | Environnement de développement |
## Prérequis
- **Docker** et **Docker Compose** (pour lancer le projet sans rien installer)
- **Node.js** >= 20 (via [nvm](https://github.com/nvm-sh/nvm))
- **make** (normalement déjà installé sur Linux/macOS)
### Guides d'installation de l'environnement
| OS | Documentation |
|----|---------------|
| Windows | [WSL2 + Ubuntu + Docker](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/windows) |
| Linux | [Docker + nvm](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/linux) |
## Installation rapide
### Installation du projet
Une fois les prérequis installés, il suffit de cloner le projet et de lancer les commandes suivantes
```bash
sudo apt install make -y
# 1. Cloner le projet avec le frontend (submodule)
git clone --recurse-submodules <url-du-repo>
cd Inventory
# 2. Démarrer les conteneurs Docker (PHP, PostgreSQL, Adminer)
make start
# 3. Installer les dépendances et builder le projet
make install
```
Dans le cas ou le `make start` plante à cause du port de la bdd, il faut modifier **POSTGRES_PORT** dans le fichier .env.docker.local, remplacer le par un port disponible.
### Configuration xdebug
Pour configurer xdebug, il faut ajouter un serveur sur phpstorm. <br>
Pour cela, il faut aller dans **Settings > PHP > Servers** <br>
* Name : inventory-docker
* Host : localhost
* Port : 8080
* Path : File/Directory -> l'endroit où est stocké votre projet et le path -> /var/www/html
> Si `make start` échoue sur le port de la BDD, modifier `POSTGRES_PORT` dans `docker/.env.docker.local`.
Pour que xdebug fonctionne sur windows, il faut modifier la variable **XDEBUG_CLIENT_HOST** par votre ip local
### Que fait `make install` ?
## Utilisation du projet
### Backend
L'api est disponible sur http://localhost:8080/api
Pour la bdd toutes les infos sont dans le fichier **docker/.env.docker.local**
Vous pouvez modifier le port si nécessaire.
1. Installe les dépendances PHP (via Composer)
2. Installe les dépendances Node.js (via npm)
3. Build le frontend Nuxt
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.
### 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
```bash
make dev-nuxt
```
Le front sera accessible sur http://localhost:3000
### Premier lancement
Une fois l'installation terminée, tu peux :
1. Charger des données de test : `make fixtures-load`
2. Lancer le frontend en mode dev : `make dev-nuxt`
3. Ouvrir l'application : http://localhost:3001
## URLs locales
| Service | URL | Description |
|---------|-----|-------------|
| API Symfony | http://localhost:8081/api | Documentation interactive de l'API (Swagger) |
| Frontend Nuxt | http://localhost:3001 | L'application web |
| Adminer (BDD) | http://localhost:5050 | Interface web pour explorer la base de données |
| PostgreSQL | `localhost:5433` | Connexion directe (user: root, pass: root, db: inventory) |
## Commandes utiles
Pour restart le container
### Docker
| Commande | Description |
|----------|-------------|
| `make start` | Démarrer les conteneurs |
| `make stop` | Arrêter les conteneurs |
| `make restart` | Redémarrer les conteneurs |
| `make shell` | Ouvrir un terminal dans le conteneur PHP (pour lancer des commandes Symfony) |
| `make reset` | Reset complet (supprime les volumes, réinstalle tout) |
### Backend
| Commande | Description |
|----------|-------------|
| `make test` | Lancer les tests PHPUnit |
| `make test FILES=tests/Api/Entity/MachineTest.php` | Lancer un test spécifique |
| `make test-setup` | Créer/mettre à jour la base de test |
| `make php-cs-fixer-allow-risky` | Formatter le code PHP (indentation, espaces, etc.) |
| `make cache-clear` | Vider le cache Symfony (à faire si tu as des erreurs bizarres) |
| `make db-reset` | Reset de la BDD (supprime toutes les données) |
| `make fixtures-load` | Charger les données de test |
| `make fixtures-dump` | Sauvegarder la BDD actuelle dans fixtures/data.sql |
### Frontend
| Commande | Description |
|----------|-------------|
| `make dev-nuxt` | Lancer le serveur de dev Nuxt (avec rechargement automatique) |
| `make build-nuxtJS` | Builder le frontend pour la production |
### Release
```bash
make restart
./scripts/release.sh patch # Bump patch (ou minor / major)
```
Pour lancer les TU
```bash
make test
Synchronise automatiquement la version dans `VERSION`, `api_platform.yaml` et `nuxt.config.ts`, crée le tag git et pousse les deux repos.
## Architecture globale
### Comment ça marche ?
```
Pour accéder au container et lance des commandes
```bash
make shell
┌──────────────────┐ HTTP (JSON) ┌──────────────────┐ SQL ┌────────────┐
│ Frontend │ ◄─────────────────► │ Backend │ ◄──────────► │ PostgreSQL │
│ (Nuxt/Vue) │ cookies session │ (Symfony/API) │ │ (BDD) │
│ localhost:3001 │ │ localhost:8081 │ │ port 5433 │
└──────────────────┘ └──────────────────┘ └────────────┘
│ │
Interface web API REST + logique
pour l'utilisateur métier + sécurité
```
Pour clear le cache Symfony
```bash
make cache-clear
1. L'utilisateur ouvre le navigateur sur `localhost:3001`
2. Le frontend (Vue/Nuxt) affiche l'interface
3. Quand l'utilisateur fait une action (créer une machine, etc.), le frontend envoie une requête HTTP à l'API backend
4. Le backend valide la requête, vérifie les permissions, exécute la logique métier
5. Le backend lit/écrit dans PostgreSQL et renvoie une réponse JSON
6. Le frontend met à jour l'interface avec les nouvelles données
### Structure du projet
```
Inventory/ # Backend Symfony (repo principal)
├── src/
│ ├── Entity/ # Les "modèles" de données (Machine, Piece, etc.)
│ ├── Controller/ # Les endpoints API personnalisés
│ ├── EventSubscriber/ # Logique déclenchée automatiquement (audit, etc.)
│ ├── Command/ # Commandes CLI (lancer via php bin/console)
│ ├── Service/ # Services métier (stockage fichiers, PDF, etc.)
│ ├── State/ # Processeurs API Platform (hashage mot de passe, upload)
│ ├── Repository/ # Requêtes BDD personnalisées
│ ├── Security/ # Authentification par session
│ └── Serializer/ # Conversion entité ↔ JSON personnalisée
├── config/ # Configuration Symfony (routes, sécurité, etc.)
├── migrations/ # Scripts de modification de la BDD
├── fixtures/ # Données de test (SQL)
├── tests/ # Tests automatisés (PHPUnit)
├── scripts/ # Utilitaires (release, migration, normalisation)
├── docker/ # Dockerfile + config Docker
├── makefile # Commandes de dev raccourcies
├── VERSION # Version courante (ex: 1.8.1)
└── Inventory_frontend/ # Submodule git (frontend, repo séparé)
├── app/pages/ # Les pages de l'app (1 fichier = 1 route URL)
├── app/components/ # Composants Vue réutilisables
├── app/composables/ # Logique métier partagée (appels API, états)
├── app/shared/ # Types TypeScript, utilitaires, validation
├── app/middleware/ # Vérification de session automatique
└── app/services/ # Couche service (wrappers API)
```
### Entités principales (les "tables" de la BDD)
| Entité | Description | Exemple |
|--------|-------------|---------|
| `Machine` | Machines du parc industriel | "CNC Mazak 01" |
| `Composant` | Composants fonctionnels d'une machine | "Broche principale" |
| `Piece` | Pièces détachées/de rechange | "Roulement SKF 6205" |
| `Product` | Produits fournisseur (consommables, outillage) | "Huile de coupe X" |
| `Site` | Sites physiques / usines | "Usine de Strasbourg" |
| `Constructeur` | Fournisseurs / fabricants | "SKF", "Mazak" |
| `ModelType` | Catégories avec squelettes de structure | "Type: Moteur électrique" |
| `CustomField` / `CustomFieldValue` | Champs personnalisés (dynamiques) | "Tension : 220V" |
| `Document` | Documents uploadés (PDF, images, etc.) | "Fiche technique CNC.pdf" |
| `AuditLog` | Journal d'audit (historique des modifications) | "Machine X modifiée par Jean" |
| `Comment` | Commentaires / tickets sur les fiches | "Vérifier le roulement" |
| `Profile` | Comptes utilisateurs avec rôles | "admin@malio.fr (ADMIN)" |
### Structure hiérarchique d'une machine
Une machine peut contenir une arborescence de composants, pièces et produits :
```
Machine "CNC Mazak 01"
├── Composant "Broche principale"
│ ├── Pièce "Roulement avant"
│ │ └── Produit "Graisse SKF LGMT2"
│ └── Pièce "Joint d'étanchéité"
├── Composant "Système hydraulique"
│ ├── Pièce "Pompe HP"
│ └── Produit "Huile hydraulique ISO 46"
└── Produit "Filtre à air cabine"
```
### Rôles et permissions
```
ROLE_ADMIN → Tout faire + gérer les utilisateurs
↓ hérite de
ROLE_GESTIONNAIRE → Créer, modifier, supprimer les données
↓ hérite de
ROLE_VIEWER → Lecture seule sur toutes les données
↓ hérite de
ROLE_USER → Accès de base (rôle minimum)
```
### Authentification
Authentification par **session (cookies)**, pas de JWT. Le profil actif est stocké en session côté serveur. Concrètement :
1. L'utilisateur choisit son profil sur la page de login
2. Il entre son mot de passe
3. Le backend crée une session et envoie un cookie au navigateur
4. À chaque requête suivante, le navigateur envoie automatiquement ce cookie
5. Le backend vérifie le cookie et identifie l'utilisateur
### Base de données — Points importants
PostgreSQL 16 avec les particularités suivantes :
- **IDs** : chaînes CUID (`'cl' + bin2hex(random_bytes(12))`), pas d'auto-increment
- **Noms de colonnes** : toujours en **minuscules** dans PostgreSQL (Doctrine map `typePieceId``typepieceid`)
- **Audit** : les subscribers Doctrine `onFlush` capturent le diff + snapshot complet de chaque modification
- **Migrations** : SQL brut avec `IF NOT EXISTS` / `IF EXISTS` pour l'idempotence
## Services Docker
| Service | Image | Port | Rôle |
|---------|-------|------|------|
| `web` | PHP 8.4 + Apache + Node | 8081, 3001 | API Symfony + Nuxt dev |
| `db` | PostgreSQL 16 Alpine | 5433 | Base de données |
| `adminer` | Adminer | 5050 | Interface web pour explorer la BDD |
## Xdebug
Configuration PhpStorm / VSCode :
- **Serveur** : `inventory-docker`
- **Host** : `localhost`
- **Port** : `8081`
- **Path mapping** : racine du projet → `/var/www/html`
> Sous WSL, modifier `XDEBUG_CLIENT_HOST` dans `docker/.env.docker.local` avec votre IP locale.
## Git
### Branches
- `master` : production
- `develop` : branche principale de dev (cible des PR)
- `feat/xxx`, `fix/xxx`, `refactor/xxx` : branches de travail
### Convention de commit (enforced par un hook)
```
<type>(<scope>) : <message>
```
**Espace obligatoire autour du `:`**. Types autorisés (minuscules) :
`feat`, `fix`, `perf`, `refactor`, `chore`, `docs`, `test`, `style`, `build`, `ci`, `revert`, `wip`
Exemples :
```
feat(machines) : add clone functionality
fix(documents) : prevent duplicate upload
refactor(audit) : merge history controllers
chore(deps) : update composer packages
```
### Pre-commit hook
Le hook `pre-commit` s'exécute automatiquement avant chaque commit :
1. **php-cs-fixer** — Formate automatiquement les fichiers PHP modifiés
2. **PHPUnit** — Lance les tests. Si un test échoue, le commit est bloqué
### Submodule frontend
Le frontend est un **submodule git** dans `Inventory_frontend/` (c'est un repo git séparé, inclus dans le repo principal). Workflow de commit :
1. Commiter dans `Inventory_frontend/` d'abord
2. Commiter dans le repo principal pour mettre à jour le pointeur du submodule
3. Pousser les deux repos
## Documentation détaillée
- **[docs/BACKEND.md](docs/BACKEND.md)** : guide complet du backend (entités, controllers, API, audit, tests)
- **[docs/FRONTEND.md](docs/FRONTEND.md)** : guide complet du frontend (pages, composables, composants, patterns)
- **[DEPLOY.md](DEPLOY.md)** : guide de déploiement serveur (Nginx, PHP-FPM, PostgreSQL)
- **[RELEASE.md](RELEASE.md)** : processus de release et versioning
- **[CHANGELOG.md](CHANGELOG.md)** : historique des versions
- **[Frontend README](Inventory_frontend/README.md)** : documentation du frontend Nuxt

View File

@@ -1,12 +1,18 @@
# Guide de Release
## Versioning
## C'est quoi une release ?
Le projet utilise le [Semantic Versioning](https://semver.org/) (SemVer) : `MAJOR.MINOR.PATCH`
Une release c'est une **version officielle** de l'application. Chaque release a un numéro de version (ex: `1.8.1`) et est marquée par un **tag git**.
- **MAJOR** : Changements incompatibles avec les versions précédentes
- **MINOR** : Nouvelles fonctionnalités rétrocompatibles
- **PATCH** : Corrections de bugs rétrocompatibles
## Versioning (Semantic Versioning)
Le projet utilise le [Semantic Versioning](https://semver.org/) : `MAJOR.MINOR.PATCH`
| Type | Quand l'utiliser | Exemple |
|------|------------------|---------|
| **PATCH** | Correction de bug, pas de nouvelle fonctionnalité | `1.8.0``1.8.1` |
| **MINOR** | Nouvelle fonctionnalité, rétrocompatible | `1.8.1``1.9.0` |
| **MAJOR** | Changement majeur, potentiellement incompatible | `1.9.0``2.0.0` |
La version est centralisée dans le fichier `VERSION` à la racine du projet.
@@ -14,9 +20,9 @@ La version est centralisée dans le fichier `VERSION` à la racine du projet.
### Prérequis
- Tous les changements doivent être commités
- Les tests doivent passer
- Être sur la branche à releaser (ex: `main`, `develop`)
- Tous les changements doivent être commités (pas de fichiers modifiés non commités)
- Les tests doivent passer (`make test`)
- Être sur la branche à releaser (généralement `develop` ou `master`)
### Utilisation du script
@@ -24,28 +30,38 @@ La version est centralisée dans le fichier `VERSION` à la racine du projet.
# Afficher l'aide et la version actuelle
./scripts/release.sh
# Bump patch : 1.0.0 → 1.0.1
# Bump patch : 1.8.1 → 1.8.2
./scripts/release.sh patch
# Bump minor : 1.0.0 → 1.1.0
# Bump minor : 1.8.1 → 1.9.0
./scripts/release.sh minor
# Bump major : 1.0.0 → 2.0.0
# Bump major : 1.8.1 → 2.0.0
./scripts/release.sh major
# Version spécifique
./scripts/release.sh 2.0.0
```
Le script :
1. Met à jour le fichier `VERSION`
2. Met à jour `config/packages/api_platform.yaml`
3. Crée un commit `chore(release): vX.Y.Z`
4. Crée le tag `vX.Y.Z`
### Que fait le script ?
1. Vérifie qu'il n'y a pas de changements non commités
2. Vérifie/commit le submodule frontend si nécessaire
3. Met à jour le fichier `VERSION` avec le nouveau numéro
4. Met à jour `config/packages/api_platform.yaml` (version affichée dans l'API)
5. Crée un commit `chore(release) : vX.Y.Z`
6. Crée le tag git `vX.Y.Z`
7. Affiche les commandes pour pousser
### Pousser la release
Après avoir exécuté le script :
```bash
# Pousser le frontend d'abord (si modifié)
cd Inventory_frontend && git push && git push --tags && cd ..
# Pousser le backend
git push && git push --tags
```
@@ -54,12 +70,21 @@ git push && git push --tags
1. Aller sur le dépôt Gitea
2. **Releases** > **New Release**
3. Sélectionner le tag `vX.Y.Z`
4. Titre : `v1.0.0` (ou avec un nom descriptif)
5. Description : résumé des changements (voir section Notes de release)
4. Titre : `vX.Y.Z` (ou avec un nom descriptif)
5. Description : résumé des changements (copier depuis CHANGELOG.md)
## Fichiers impactés par le versioning
| Fichier | Rôle |
|---------|------|
| `VERSION` | Source unique de vérité |
| `config/packages/api_platform.yaml` | Version affichée dans la doc API (Swagger) |
| `Inventory_frontend/nuxt.config.ts` | Lit `VERSION` au build pour l'afficher dans le footer |
| Footer de l'app | Affiche `v{{ appVersion }}` |
## Notes de release
Template pour les notes de release :
Template pour les notes de release (à copier dans Gitea) :
```markdown
## Nouveautés
@@ -73,66 +98,25 @@ Template pour les notes de release :
## Changements
- Refactoring de Z
- Mise à jour des dépendances
## Migration requise
\`\`\`bash
docker compose exec web php bin/console doctrine:migrations:migrate
\`\`\`
```
## Fichiers impactés par le versioning
## Déploiement après une release
| Fichier | Usage |
|---------|-------|
| `VERSION` | Source unique de vérité |
| `config/packages/api_platform.yaml` | Version affichée dans l'API |
| `Inventory_frontend/nuxt.config.ts` | Lit VERSION au build |
| Footer de l'app | Affiche `v{{ appVersion }}` |
Voir [DEPLOY.md](DEPLOY.md) pour les instructions de mise à jour en production.
## Déploiement en production
### 1. Base de données
Dump de la base locale :
```bash
pg_dump -h localhost -p 5433 -U root -d inventory > backup_v1.0.0.sql
```
Import en production :
```bash
psql -h <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 :
En résumé :
```bash
# Sur le serveur de production
cd /var/www/Inventory
git pull
git submodule update --init --recursive
composer install --no-dev --optimize-autoloader
php bin/console cache:clear --env=prod
php bin/console doctrine:migrations:migrate --no-interaction
php bin/console cache:clear --env=prod
cd Inventory_frontend && npm install && npx nuxi generate
```
Frontend :
```bash
cd Inventory_frontend
NUXT_PUBLIC_API_BASE_URL=https://api.votre-domaine.com yarn build
```
### 4. Checklist avant mise en prod
- [ ] Tests passent
- [ ] Migrations DB testées
- [ ] Variables d'environnement configurées
- [ ] Clés JWT générées
- [ ] CORS configuré
- [ ] SSL/HTTPS actif
- [ ] Backup de la DB prod existante (si upgrade)

View File

@@ -1,2 +0,0 @@
- Doc: ne pas oublier de mettre `make` dans la documentation.
- Note: le probleme d'IP sous WSL, a ajouter dans la doc.

View File

@@ -1 +1 @@
1.0.0
1.9.0

View File

@@ -86,9 +86,11 @@
}
},
"require-dev": {
"dama/doctrine-test-bundle": "^8.6",
"friendsofphp/php-cs-fixer": "^3.92",
"phpunit/phpunit": "^12.5",
"symfony/browser-kit": "8.0.*",
"symfony/css-selector": "8.0.*"
"symfony/css-selector": "8.0.*",
"symfony/http-client": "8.0.*"
}
}

257
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9e0e35659f9b6ef5c0a60262a36b61f2",
"content-hash": "97c89001351c3dcf060e2b9b5f37a8a6",
"packages": [
{
"name": "api-platform/doctrine-common",
@@ -7980,6 +7980,75 @@
],
"time": "2024-05-06T16:37:16+00:00"
},
{
"name": "dama/doctrine-test-bundle",
"version": "v8.6.0",
"source": {
"type": "git",
"url": "https://github.com/dmaicher/doctrine-test-bundle.git",
"reference": "f7e3487643e685432f7e27c50cac64e9f8c515a4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/f7e3487643e685432f7e27c50cac64e9f8c515a4",
"reference": "f7e3487643e685432f7e27c50cac64e9f8c515a4",
"shasum": ""
},
"require": {
"doctrine/dbal": "^3.3 || ^4.0",
"doctrine/doctrine-bundle": "^2.11.0 || ^3.0",
"php": ">= 8.2",
"psr/cache": "^2.0 || ^3.0",
"symfony/cache": "^6.4 || ^7.3 || ^8.0",
"symfony/framework-bundle": "^6.4 || ^7.3 || ^8.0"
},
"conflict": {
"phpunit/phpunit": "<11.0"
},
"require-dev": {
"behat/behat": "^3.0",
"friendsofphp/php-cs-fixer": "^3.27",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^11.5.41|| ^12.3.14",
"symfony/dotenv": "^6.4 || ^7.3 || ^8.0",
"symfony/process": "^6.4 || ^7.3 || ^8.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
}
},
"autoload": {
"psr-4": {
"DAMA\\DoctrineTestBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "David Maicher",
"email": "mail@dmaicher.de"
}
],
"description": "Symfony bundle to isolate doctrine database tests and improve test performance",
"keywords": [
"doctrine",
"isolation",
"performance",
"symfony",
"testing",
"tests"
],
"support": {
"issues": "https://github.com/dmaicher/doctrine-test-bundle/issues",
"source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.6.0"
},
"time": "2026-01-21T07:39:44+00:00"
},
{
"name": "evenement/evenement",
"version": "v3.0.2",
@@ -10344,16 +10413,16 @@
},
{
"name": "symfony/browser-kit",
"version": "v8.0.3",
"version": "v8.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/browser-kit.git",
"reference": "efc7cc6d442b80c8cb0ad0b29bdb0c9418cee9ee"
"reference": "0d998c101e1920fc68572209d1316fec0db728ef"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/browser-kit/zipball/efc7cc6d442b80c8cb0ad0b29bdb0c9418cee9ee",
"reference": "efc7cc6d442b80c8cb0ad0b29bdb0c9418cee9ee",
"url": "https://api.github.com/repos/symfony/browser-kit/zipball/0d998c101e1920fc68572209d1316fec0db728ef",
"reference": "0d998c101e1920fc68572209d1316fec0db728ef",
"shasum": ""
},
"require": {
@@ -10392,7 +10461,7 @@
"description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/browser-kit/tree/v8.0.3"
"source": "https://github.com/symfony/browser-kit/tree/v8.0.4"
},
"funding": [
{
@@ -10412,7 +10481,7 @@
"type": "tidelift"
}
],
"time": "2025-12-16T08:10:18+00:00"
"time": "2026-01-13T13:06:50+00:00"
},
{
"name": "symfony/css-selector",
@@ -10553,6 +10622,180 @@
],
"time": "2025-12-06T17:00:47+00:00"
},
{
"name": "symfony/http-client",
"version": "v8.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "ade9bd433450382f0af154661fc8e72758b4de36"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/ade9bd433450382f0af154661fc8e72758b4de36",
"reference": "ade9bd433450382f0af154661fc8e72758b4de36",
"shasum": ""
},
"require": {
"php": ">=8.4",
"psr/log": "^1|^2|^3",
"symfony/http-client-contracts": "~3.4.4|^3.5.2",
"symfony/service-contracts": "^2.5|^3"
},
"conflict": {
"amphp/amp": "<3",
"php-http/discovery": "<1.15"
},
"provide": {
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "1.0",
"symfony/http-client-implementation": "3.0"
},
"require-dev": {
"amphp/http-client": "^5.3.2",
"amphp/http-tunnel": "^2.0",
"guzzlehttp/promises": "^1.4|^2.0",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
"symfony/cache": "^7.4|^8.0",
"symfony/dependency-injection": "^7.4|^8.0",
"symfony/http-kernel": "^7.4|^8.0",
"symfony/messenger": "^7.4|^8.0",
"symfony/process": "^7.4|^8.0",
"symfony/rate-limiter": "^7.4|^8.0",
"symfony/stopwatch": "^7.4|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
"homepage": "https://symfony.com",
"keywords": [
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v8.0.7"
},
"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": "2026-03-06T13:17:40+00:00"
},
{
"name": "symfony/http-client-contracts",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client-contracts.git",
"reference": "75d7043853a42837e68111812f4d964b01e5101c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c",
"reference": "75d7043853a42837e68111812f4d964b01e5101c",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.6-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to HTTP clients",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-04-29T11:18:49+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v8.0.0",

View File

@@ -3,6 +3,7 @@
declare(strict_types=1);
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
use DAMA\DoctrineTestBundle\DAMADoctrineTestBundle;
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
@@ -20,4 +21,5 @@ return [
NelmioCorsBundle::class => ['all' => true],
ApiPlatformBundle::class => ['all' => true],
LexikJWTAuthenticationBundle::class => ['all' => true],
DAMADoctrineTestBundle::class => ['test' => true],
];

View File

@@ -1,7 +1,12 @@
api_platform:
title: Hello API Platform
version: 1.0.0
title: Inventory API
description: API de gestion d'inventaire industriel — machines, pièces, composants, produits.
version: 1.8.1
defaults:
stateless: false
cache_headers:
vary: ['Content-Type', 'Authorization', 'Origin']
pagination_items_per_page: 30
pagination_maximum_items_per_page: 200
pagination_fetch_join_collection: true
pagination_partial: false

View File

@@ -1,4 +1,9 @@
security:
# Login controller already calls $session->migrate(true) on login.
# Keeping 'migrate' would regenerate the session ID on every authenticated
# API request, which breaks concurrent requests from the SPA (race condition).
session_fixation_strategy: none
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
@@ -18,44 +23,36 @@ security:
pattern: ^/(_profiler|_wdt|assets|build)/
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_profile:
pattern: ^/api/session
stateless: false
session_api:
pattern: ^/api/(sites|machines|documents|profiles)
stateless: false
session_public:
pattern: ^/api/session/profiles?$
security: false
api:
pattern: ^/api
stateless: false
custom_authenticators:
- App\Security\SessionProfileAuthenticator
main:
lazy: true
provider: app_user_provider
role_hierarchy:
ROLE_ADMIN: ROLE_GESTIONNAIRE
ROLE_GESTIONNAIRE: ROLE_VIEWER
ROLE_VIEWER: ROLE_USER
# Note: Only the *first* matching rule is applied
access_control:
- { path: ^/api/session/profile, roles: PUBLIC_ACCESS }
- { path: ^/api/session/profiles, roles: PUBLIC_ACCESS }
- { path: ^/api, roles: PUBLIC_ACCESS }
- { path: ^/api/session/profile$, roles: PUBLIC_ACCESS }
- { 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: ^/api/health$, roles: PUBLIC_ACCESS }
- { path: ^/docs, roles: PUBLIC_ACCESS }
- { path: ^/contexts, roles: PUBLIC_ACCESS }
- { path: ^/\.well-known, roles: PUBLIC_ACCESS }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/api, roles: ROLE_VIEWER }
when@test:
security:

View File

@@ -21,3 +21,20 @@ services:
# add more service definitions when explicit configuration is needed
# 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 }
App\OpenApi\OpenApiDecorator:
decorates: 'api_platform.openapi.factory'
arguments:
$decorated: '@.inner'

View File

@@ -45,34 +45,17 @@ services:
- "${POSTGRES_PORT:-5433}:5432"
restart: unless-stopped
pgadmin:
container_name: pgadmin-${DOCKER_APP_NAME}
image: dpage/pgadmin4:latest
user: root
adminer:
container_name: adminer-${DOCKER_APP_NAME}
image: adminer:latest
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@admin.com}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin}
PGADMIN_CONFIG_SERVER_MODE: 'False'
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
PGADMIN_SERVER_JSON_FILE: '/pgadmin4/servers.json'
volumes:
- pgadmin_data:/var/lib/pgadmin
- ./docker/pgadmin/servers.json:/pgadmin4/servers.json:ro
- ./docker/pgadmin/pgpass:/pgadmin4/pgpass:ro
ADMINER_DEFAULT_SERVER: db
ADMINER_DESIGN: dracula
ports:
- "${PGADMIN_PORT:-5050}:80"
- "${ADMINER_PORT:-5050}:8080"
depends_on:
- db
restart: unless-stopped
entrypoint: >
/bin/sh -c "
mkdir -p /var/lib/pgadmin &&
cp /pgadmin4/pgpass /var/lib/pgadmin/pgpass &&
chmod 600 /var/lib/pgadmin/pgpass &&
chown 5050:5050 /var/lib/pgadmin/pgpass &&
/entrypoint.sh
"
volumes:
pg_data:
pgadmin_data:

View File

@@ -6,4 +6,4 @@ POSTGRES_DB=inventory
POSTGRES_USER=root
POSTGRES_PASSWORD=root
POSTGRES_PORT=5432
XDEBUG_CLIENT_HOST=host.docker.internal
XDEBUG_CLIENT_HOST=host.docker.internal

View File

@@ -33,6 +33,7 @@ RUN apt-get update && apt-get install -y \
wget \
git \
unzip \
qpdf \
&& docker-php-ext-install -j$(nproc) \
intl \
zip \

846
docs/BACKEND.md Normal file
View File

@@ -0,0 +1,846 @@
# Guide Backend — Inventory
Guide complet du backend Symfony pour comprendre comment tout fonctionne, même si tu débutes.
## Table des matières
1. [Vue d'ensemble](#vue-densemble)
2. [Comment fonctionne une API REST](#comment-fonctionne-une-api-rest)
3. [Symfony + API Platform — les bases](#symfony--api-platform--les-bases)
4. [Les Entités (les modèles de données)](#les-entités)
5. [Les Controllers (les endpoints personnalisés)](#les-controllers)
6. [Le système d'audit](#le-système-daudit)
7. [L'authentification par session](#lauthentification-par-session)
8. [Les services](#les-services)
9. [Les migrations de base de données](#les-migrations)
10. [Les tests](#les-tests)
11. [Flux complet d'une requête](#flux-complet-dune-requête)
12. [Commandes Symfony utiles](#commandes-symfony-utiles)
---
## Vue d'ensemble
Le backend est une **API REST** construite avec :
- **Symfony 8** : le framework PHP (gère le routing, la sécurité, la config, etc.)
- **API Platform 4.2** : une surcouche qui génère automatiquement les endpoints CRUD à partir des entités
- **Doctrine ORM** : fait le lien entre les objets PHP et les tables PostgreSQL
- **PostgreSQL 16** : la base de données relationnelle
### Le principe
Au lieu d'écrire manuellement chaque endpoint (GET /machines, POST /machines, etc.), **API Platform** les génère automatiquement à partir des entités PHP. Tu déclares tes champs, tes relations, tes règles de sécurité directement sur la classe PHP, et API Platform fait le reste.
---
## Comment fonctionne une API REST
### C'est quoi une API REST ?
Une API REST c'est un serveur qui répond à des requêtes HTTP (comme un site web, mais au lieu de renvoyer du HTML, il renvoie du JSON).
### Les verbes HTTP
| Verbe | Action | Exemple |
|-------|--------|---------|
| `GET` | Lire des données | `GET /api/machines` → liste toutes les machines |
| `POST` | Créer une donnée | `POST /api/machines` + body JSON → crée une machine |
| `PUT` | Remplacer une donnée | `PUT /api/machines/123` + body JSON → remplace la machine 123 |
| `PATCH` | Modifier partiellement | `PATCH /api/machines/123` + body JSON → modifie certains champs |
| `DELETE` | Supprimer | `DELETE /api/machines/123` → supprime la machine 123 |
### Les codes de réponse HTTP
| Code | Signification | Quand |
|------|---------------|-------|
| `200` | OK | Requête réussie |
| `201` | Created | Ressource créée avec succès (POST) |
| `204` | No Content | Suppression réussie (DELETE) |
| `400` | Bad Request | Données invalides envoyées |
| `401` | Unauthorized | Pas connecté / session expirée |
| `403` | Forbidden | Connecté mais pas les permissions |
| `404` | Not Found | La ressource n'existe pas |
| `409` | Conflict | Doublon (ex: nom déjà pris) |
| `500` | Server Error | Bug côté serveur |
### Le format JSON-LD
L'API utilise **JSON-LD** (JSON Linked Data), une extension de JSON qui ajoute des métadonnées :
```json
{
"@context": "/api/contexts/Machine",
"@id": "/api/machines/cl1a2b3c4d5e6f7g8h9i0j1k",
"@type": "Machine",
"id": "cl1a2b3c4d5e6f7g8h9i0j1k",
"name": "CNC Mazak 01",
"reference": "CNM-001",
"prix": "50000.00",
"site": "/api/sites/cl9z8y7x6w5v4u3t2s1r0q",
"createdAt": "2026-01-15T10:30:00+00:00"
}
```
Points importants :
- `@id` est l'**IRI** (Internationalized Resource Identifier) : c'est l'identifiant unique de la ressource dans l'API
- Les relations utilisent des IRIs : `"site": "/api/sites/cl9z8..."` au lieu d'un simple ID
- Les collections retournent un format hydra avec pagination :
```json
{
"@context": "/api/contexts/Machine",
"@id": "/api/machines",
"@type": "hydra:Collection",
"hydra:totalItems": 42,
"hydra:member": [
{ "@id": "/api/machines/cl...", "name": "CNC 01", ... },
{ "@id": "/api/machines/cl...", "name": "Tour 02", ... }
]
}
```
---
## Symfony + API Platform — les bases
### La structure des fichiers
```
src/
├── Entity/ # Les classes PHP qui représentent les tables de la BDD
├── Controller/ # Les endpoints HTTP personnalisés (quand API Platform ne suffit pas)
├── EventSubscriber/ # Du code qui s'exécute automatiquement quand quelque chose se passe
├── Repository/ # Les requêtes SQL personnalisées
├── Service/ # La logique métier réutilisable
├── State/ # Les processeurs API Platform (interceptent le flux CRUD)
├── Security/ # L'authentification
├── Serializer/ # Personnalisation de la conversion entité ↔ JSON
├── Command/ # Commandes CLI (php bin/console app:xxx)
├── Enum/ # Les énumérations PHP (ex: catégories)
└── OpenApi/ # Personnalisation de la doc Swagger
```
### Comment Symfony traite une requête
```
Requête HTTP
Symfony Router (quel code doit répondre ?)
Sécurité (l'utilisateur a-t-il le droit ?)
Controller ou API Platform (traitement)
Doctrine ORM (lecture/écriture en BDD)
Serializer (conversion entité → JSON)
Réponse HTTP (JSON envoyé au frontend)
```
### Les attributs PHP 8
Le projet utilise les **attributs PHP 8** (les `#[...]`) au lieu des annotations (les `@...`). C'est la syntaxe moderne de PHP :
```php
// Attribut PHP 8 (ce qu'on utilise) ✅
#[ORM\Column(type: 'string', length: 255)]
private string $name;
// Annotation (ancien style, on ne l'utilise pas) ❌
/** @ORM\Column(type="string", length=255) */
```
---
## Les Entités
Les entités sont les classes PHP qui représentent les tables de la base de données. Chaque propriété de la classe correspond à une colonne.
### Anatomie d'une entité
Prenons un exemple simplifié :
```php
<?php
// src/Entity/Machine.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: MachineRepository::class)] // ← Lié à une table en BDD
#[ORM\HasLifecycleCallbacks] // ← Active les hooks PrePersist/PreUpdate
#[ApiResource( // ← Génère les endpoints API
operations: [
new GetCollection(security: "is_granted('ROLE_VIEWER')"), // GET /api/machines
new Get(security: "is_granted('ROLE_VIEWER')"), // GET /api/machines/{id}
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), // POST /api/machines
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), // PATCH /api/machines/{id}
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), // DELETE /api/machines/{id}
],
normalizationContext: ['groups' => ['machine:read']], // ← Quels champs exposer en lecture
denormalizationContext: ['groups' => ['machine:write']], // ← Quels champs accepter en écriture
paginationItemsPerPage: 30, // ← 30 résultats par page
)]
class Machine
{
#[ORM\Id]
#[ORM\Column(type: 'string', length: 36)]
#[Groups(['machine:read'])] // ← Exposé en lecture uniquement
private string $id;
#[ORM\Column(type: 'string', length: 255, unique: true)]
#[Groups(['machine:read', 'machine:write'])] // ← Exposé en lecture ET écriture
private string $name;
#[ORM\Column(type: 'decimal', precision: 10, scale: 2, nullable: true)]
#[Groups(['machine:read', 'machine:write'])]
private ?string $prix = null;
#[ORM\ManyToOne(targetEntity: Site::class)] // ← Relation : chaque machine appartient à un site
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] // ← Obligatoire, supprimé en cascade
#[Groups(['machine:read', 'machine:write'])]
private Site $site;
#[ORM\Column(type: 'datetime_immutable')]
#[Groups(['machine:read'])] // ← Lecture seule (pas dans machine:write)
private \DateTimeImmutable $createdAt;
// ... getters et setters
}
```
### Décryptage des attributs importants
| Attribut | Signification |
|----------|--------------|
| `#[ORM\Entity]` | Cette classe est stockée en BDD |
| `#[ORM\Column]` | Cette propriété est une colonne |
| `#[ORM\Id]` | C'est la clé primaire |
| `#[ORM\ManyToOne]` | Relation N→1 (plusieurs machines → un site) |
| `#[ORM\OneToMany]` | Relation 1→N (un site → plusieurs machines) |
| `#[ORM\ManyToMany]` | Relation N→N (machines ↔ constructeurs) |
| `#[ApiResource]` | API Platform génère les endpoints CRUD |
| `#[Groups]` | Contrôle quels champs sont visibles/modifiables |
| `security: "is_granted('ROLE_X')"` | Qui a le droit d'utiliser cet endpoint |
### Le trait CuidEntityTrait
Toutes les entités utilisent un trait partagé qui génère les IDs et gère les timestamps :
```php
// src/Entity/Trait/CuidEntityTrait.php
trait CuidEntityTrait
{
#[ORM\PrePersist] // ← S'exécute automatiquement AVANT l'insertion en BDD
public function generateId(): void
{
if (!isset($this->id)) {
$this->id = 'cl' . bin2hex(random_bytes(12)); // ← Génère un ID unique de 26 chars
}
$this->createdAt = new \DateTimeImmutable();
$this->updatedAt = new \DateTimeImmutable();
}
#[ORM\PreUpdate] // ← S'exécute automatiquement AVANT une mise à jour
public function updateTimestamp(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
}
```
### Les entités du projet
#### Entités "catalogue" (les éléments qu'on gère)
| Entité | Table | Champs clés | Relations |
|--------|-------|-------------|-----------|
| **Machine** | `machine` | name, reference, prix | → Site, ↔ Constructeur, → Documents |
| **Composant** | `composant` | name, reference, description, prix, structure (JSON) | → ModelType, → Product, ↔ Constructeur |
| **Piece** | `piece` | name, reference, description, prix, productIds (JSON) | → ModelType, → Product, ↔ Constructeur |
| **Product** | `product` | name, reference, supplierPrice | → ModelType, ↔ Constructeur |
#### Entités de classification
| Entité | Table | Champs clés | Rôle |
|--------|-------|-------------|------|
| **Site** | `site` | name, contactName, contactPhone, contactAddress | Regrouper les machines par lieu |
| **Constructeur** | `constructeur` | name, email, phone | Fournisseurs/fabricants partagés |
| **ModelType** | `model_type` | name, code, category (enum), skeletons (JSON) | Catégoriser composants/pièces/produits |
#### Entités de liaison hiérarchique (structure machine)
| Entité | Rôle | Relations |
|--------|------|-----------|
| **MachineComponentLink** | Lie un composant à une machine | → Machine, → Composant, → parent (self) |
| **MachinePieceLink** | Lie une pièce à une machine | → Machine, → Piece, → parent composant |
| **MachineProductLink** | Lie un produit à une machine | → Machine, → Product, → parent (flexible) |
Ces entités permettent la **structure arborescente** : un composant peut contenir des pièces, qui contiennent des produits.
#### Entités de métadonnées
| Entité | Rôle |
|--------|------|
| **CustomField** | Définition d'un champ personnalisé (nom, type, options) |
| **CustomFieldValue** | Valeur d'un champ personnalisé pour une entité donnée |
| **Document** | Fichier uploadé (PDF, image) rattaché à une entité |
| **AuditLog** | Entrée du journal d'audit (diff + snapshot) |
| **Comment** | Commentaire/ticket sur une fiche |
| **Profile** | Compte utilisateur (email, rôle, mot de passe hashé) |
### Les relations entre entités (schéma simplifié)
```
Site ──1:N──► Machine ──1:N──► MachineComponentLink ──► Composant
│ │
│ └──1:N──► MachinePieceLink ──► Piece
│ │
│ └──1:N──► MachineProductLink ──► Product
└──N:N──► Constructeur (via table de jointure)
ModelType ──1:N──► Composant / Piece / Product
└──► CustomField ──1:N──► CustomFieldValue
Machine / Composant / Piece / Product ──1:N──► Document
──1:N──► CustomFieldValue
```
---
## Les Controllers
API Platform génère automatiquement les endpoints CRUD standard. Les controllers personnalisés gèrent les cas plus complexes.
### Liste des controllers
#### Authentification (3 controllers)
**SessionProfileController** (`/api/session/profile`) — Login/Logout
```
POST /api/session/profile → Se connecter (payload: { profileId, password })
GET /api/session/profile → Récupérer le profil connecté
DELETE /api/session/profile → Se déconnecter
```
**SessionProfilesController** (`/api/session/profiles`) — Liste des profils
```
GET /api/session/profiles → Liste tous les profils actifs (page de login)
```
**AdminProfileController** (`/api/admin/profiles`) — Administration des utilisateurs
```
GET /api/admin/profiles → Liste tous les profils (ADMIN only)
POST /api/admin/profiles → Créer un profil
PUT /api/admin/profiles/{id}/role → Changer le rôle d'un profil
PUT /api/admin/profiles/{id}/password → Réinitialiser un mot de passe
PUT /api/admin/profiles/{id}/deactivate → Désactiver un profil
```
#### Données et logique métier
**MachineStructureController** — Structure hiérarchique des machines
```
GET /api/machines/{id}/structure → Récupérer l'arborescence complète
PATCH /api/machines/{id}/structure → Modifier l'arborescence
POST /api/machines/{id}/clone → Cloner une machine avec toute sa structure
```
**MachineCustomFieldsController** — Champs personnalisés machines
```
POST /api/machines/{id}/custom-fields/init → Initialiser les champs personnalisés manquants
```
**EntityHistoryController** — Historique d'audit par entité
```
GET /api/{entityType}/{id}/history → 200 derniers événements d'audit
```
**ActivityLogController** — Journal d'activité global
```
GET /api/activity-log → Liste paginée avec filtres (entityType, action)
```
**CommentController** — Commentaires/tickets
```
POST /api/comments → Créer un commentaire
PATCH /api/comments/{id}/resolve → Résoudre un commentaire
GET /api/comments/unresolved-count → Nombre de commentaires non résolus
```
**CustomFieldValueController** — Valeurs de champs personnalisés
```
POST /api/custom-field-values → Créer/mettre à jour une valeur (upsert)
DELETE /api/custom-field-values/{id} → Supprimer une valeur
```
#### Fichiers
**DocumentQueryController** — Requêter les documents par entité
```
GET /api/documents/by-site/{id} → Documents d'un site
GET /api/documents/by-machine/{id} → Documents d'une machine
GET /api/documents/by-composant/{id} → Documents d'un composant
GET /api/documents/by-piece/{id} → Documents d'une pièce
GET /api/documents/by-product/{id} → Documents d'un produit
```
**DocumentServeController** — Servir les fichiers
```
GET /api/documents/{id}/file → Afficher le fichier (inline)
GET /api/documents/{id}/download → Télécharger le fichier (attachment)
```
#### Monitoring
**HealthCheckController** — Vérification de santé
```
GET /api/health → Version, latence BDD, mémoire, version PHP
```
### Exemple de controller commenté
```php
<?php
// src/Controller/CommentController.php
namespace App\Controller;
use App\Entity\Comment;
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;
class CommentController extends AbstractController
{
#[Route('/api/comments', methods: ['POST'])] // ← Définit l'URL et le verbe HTTP
public function create(
Request $request, // ← La requête HTTP entrante
EntityManagerInterface $em, // ← Pour écrire en BDD (injecté automatiquement)
): JsonResponse {
$this->denyAccessUnlessGranted('ROLE_VIEWER'); // ← Vérifie que l'utilisateur est connecté
$data = json_decode($request->getContent(), true); // ← Parse le body JSON
$comment = new Comment();
$comment->setContent($data['content']);
$comment->setEntityType($data['entityType']);
$comment->setEntityId($data['entityId']);
// ... autres champs
$em->persist($comment); // ← Dit à Doctrine "je veux sauvegarder ça"
$em->flush(); // ← Exécute réellement le INSERT SQL
return $this->json($comment, 201); // ← Renvoie le commentaire créé avec le code 201
}
}
```
---
## Le système d'audit
Chaque modification sur les entités principales est automatiquement enregistrée dans un journal d'audit. C'est un des points forts de l'application.
### Comment ça marche ?
Les **Event Subscribers** de Doctrine interceptent les opérations de base de données **avant** qu'elles soient exécutées (événement `onFlush`).
```
L'utilisateur modifie une machine
Doctrine détecte le changement
onFlush se déclenche
Le subscriber calcule le diff (ancien → nouveau)
Le subscriber crée un AuditLog avec :
- entityType : "machine"
- entityId : "cl1a2b3c..."
- action : "update"
- diff : { "name": { "from": "CNC 01", "to": "CNC 02" } }
- snapshot : { état complet de la machine }
- actorProfileId : "cl9z8y7x..." (qui a fait la modif)
Les deux (machine + audit log) sont sauvegardés en même temps
```
### Le diff
Le diff capture exactement ce qui a changé :
```json
{
"name": { "from": "CNC Mazak 01", "to": "CNC Mazak 02" },
"prix": { "from": "45000.00", "to": "50000.00" },
"constructeurIds": {
"from": ["cl111...", "cl222..."],
"to": ["cl111...", "cl333..."]
}
}
```
### Le snapshot
Le snapshot capture l'état complet de l'entité au moment de la modification :
```json
{
"id": "cl1a2b3c...",
"name": "CNC Mazak 02",
"reference": "CNM-001",
"prix": "50000.00",
"siteId": "cl9z8y7x...",
"constructeurIds": ["cl111...", "cl333..."]
}
```
### Les subscribers d'audit
| Subscriber | Entité | Type |
|------------|--------|------|
| MachineAuditSubscriber | Machine | Complex (avec constructeurs + custom fields) |
| ComposantAuditSubscriber | Composant | Complex |
| PieceAuditSubscriber | Piece | Complex |
| ProductAuditSubscriber | Product | Complex |
| ConstructeurAuditSubscriber | Constructeur | Simple |
| DocumentAuditSubscriber | Document | Simple |
| ModelTypeAuditSubscriber | ModelType | Simple |
**Simple** = suit seulement les champs de l'entité
**Complex** = suit aussi les relations ManyToMany (constructeurs) et les champs personnalisés
### AbstractAuditSubscriber
La classe de base qui contient toute la logique partagée :
```php
abstract class AbstractAuditSubscriber implements EventSubscriber
{
// Méthode à implémenter par chaque subscriber
abstract protected function getEntityClass(): string; // Ex: Machine::class
abstract protected function getEntityType(): string; // Ex: 'machine'
abstract protected function buildSnapshot($entity): array; // Construit le snapshot
// Deux chemins d'exécution :
// 1. onFlushSimple() : pour les entités sans collections ManyToMany
// 2. onFlushComplex() : pour les entités avec constructeurs (détecte les ajouts/suppressions)
}
```
### Autres subscribers
| Subscriber | Rôle |
|------------|------|
| **PieceProductSyncSubscriber** | Synchronise le champ `productIds` sur Piece quand un Product est lié/délié |
| **UniqueConstraintSubscriber** | Capture les erreurs de doublon PostgreSQL et renvoie un message clair |
---
## L'authentification par session
### Le flux complet
```
1. GET /api/session/profiles
→ Retourne la liste des profils actifs (nom, prénom, email, hasPassword)
→ Le frontend affiche la page de login avec les profils disponibles
2. POST /api/session/profile
Body: { "profileId": "cl...", "password": "secret" }
→ Le backend vérifie le mot de passe
→ Si OK : stocke profileId dans la session PHP, retourne le profil
→ Si KO : retourne 401
3. GET /api/session/profile (à chaque chargement de page)
→ Le navigateur envoie le cookie de session automatiquement
→ Le backend retrouve le profil via la session
→ Retourne le profil connecté ou 401
4. DELETE /api/session/profile
→ Supprime le profileId de la session
→ L'utilisateur est déconnecté
```
### La sécurité sur les endpoints
Chaque endpoint API Platform a une règle de sécurité :
```php
new GetCollection(security: "is_granted('ROLE_VIEWER')") // Lecture → minimum ROLE_VIEWER
new Post(security: "is_granted('ROLE_GESTIONNAIRE')") // Création → minimum ROLE_GESTIONNAIRE
```
Les controllers personnalisés utilisent :
```php
$this->denyAccessUnlessGranted('ROLE_VIEWER');
```
### La hiérarchie des rôles
Grâce à la hiérarchie, un ADMIN a automatiquement tous les rôles inférieurs :
```
ROLE_ADMIN ─── a aussi ──► ROLE_GESTIONNAIRE ──► ROLE_VIEWER ──► ROLE_USER
```
Donc `is_granted('ROLE_VIEWER')` accepte aussi les GESTIONNAIRES et les ADMINS.
---
## Les services
### DocumentStorageService
Gère le stockage des fichiers sur le système de fichiers :
```php
// Stocker un fichier uploadé
$path = $storageService->store($uploadedFile, $entityType, $entityId);
// Supprimer un fichier
$storageService->delete($path);
```
Les fichiers sont stockés dans `var/documents/{entityType}/{entityId}/{filename}`.
### PdfCompressorService
Compresse les fichiers PDF via Ghostscript pour réduire leur taille :
```php
$compressorService->compress($filePath);
```
### ModelTypeCategoryConversionService
Permet de convertir la catégorie d'un ModelType (ex: transformer un type "composant" en type "pièce").
---
## Les migrations
Les migrations sont des scripts SQL qui modifient la structure de la base de données. Elles sont dans le dossier `migrations/`.
### Principe
Quand tu ajoutes un champ à une entité, il faut créer une migration pour mettre à jour la BDD :
```bash
# Générer une migration à partir des changements détectés
make shell
php bin/console doctrine:migrations:diff
# Appliquer les migrations
php bin/console doctrine:migrations:migrate
```
### Particularités PostgreSQL
Les migrations utilisent du **SQL brut** avec des gardes pour l'idempotence :
```sql
-- On peut relancer la migration sans erreur
ALTER TABLE machine ADD COLUMN IF NOT EXISTS description TEXT;
DROP INDEX IF EXISTS idx_machine_name;
CREATE UNIQUE INDEX IF NOT EXISTS idx_machine_name ON machine (name);
```
**Attention aux noms de colonnes** : PostgreSQL stocke tout en **minuscules**. Donc `typePieceId` en PHP devient `typepieceid` en SQL. Toujours utiliser des noms lowercase dans le SQL brut.
---
## Les tests
### Stack de test
- **PHPUnit 12** : framework de test PHP
- **API Platform Test** : utilitaires pour tester des endpoints API
- **DAMA DoctrineTestBundle** : wrappe chaque test dans une transaction avec rollback automatique (pas besoin de nettoyer la BDD entre les tests)
### Structure
```
tests/
├── AbstractApiTestCase.php # Classe de base avec helpers
└── Api/
└── Entity/
├── MachineTest.php # Tests des endpoints machine
├── SiteTest.php # Tests des endpoints site
└── ...
```
### Exemple de test
```php
class MachineTest extends AbstractApiTestCase
{
public function testCreateMachine(): void
{
// Créer un client HTTP connecté en tant que gestionnaire
$client = $this->createGestionnaireClient();
// Créer un site (prérequis)
$site = $this->createSite();
// Envoyer une requête POST pour créer une machine
$client->request('POST', '/api/machines', [
'json' => [
'name' => 'Machine Test',
'reference' => 'MT-001',
'site' => '/api/sites/' . $site->getId(),
],
]);
// Vérifier que la réponse est 201 Created
$this->assertResponseStatusCodeSame(201);
// Vérifier le contenu de la réponse
$this->assertJsonContains([
'name' => 'Machine Test',
'reference' => 'MT-001',
]);
}
}
```
### Helpers disponibles dans AbstractApiTestCase
| Méthode | Description |
|---------|-------------|
| `createViewerClient()` | Client HTTP connecté avec ROLE_VIEWER |
| `createGestionnaireClient()` | Client HTTP connecté avec ROLE_GESTIONNAIRE |
| `createAdminClient()` | Client HTTP connecté avec ROLE_ADMIN |
| `createProfile()` | Crée un profil utilisateur en BDD |
| `createSite()` | Crée un site en BDD |
| `createMachine()` | Crée une machine en BDD |
### Lancer les tests
```bash
make test # Tous les tests
make test FILES=tests/Api/Entity/MachineTest.php # Un fichier
make test-setup # (Re)créer la BDD de test
```
---
## Flux complet d'une requête
### Exemple : créer une machine
```
1. Le frontend envoie :
POST /api/machines
Content-Type: application/ld+json
Cookie: PHPSESSID=abc123
{
"name": "CNC Mazak 01",
"reference": "CNM-001",
"prix": "50000.00",
"site": "/api/sites/cl9z8y7x..."
}
2. Symfony reçoit la requête
→ Le routeur identifie : c'est un endpoint API Platform (POST sur Machine)
3. Sécurité
→ Vérifie le cookie de session → retrouve le profil connecté
→ Vérifie is_granted('ROLE_GESTIONNAIRE') → OK
4. Désérialisation (JSON → objet PHP)
→ API Platform convertit le JSON en objet Machine
→ Le champ "site" (IRI) est résolu en objet Site
→ Seuls les champs du groupe 'machine:write' sont acceptés
5. Validation
→ Vérifie les contraintes (name non vide, site existe, etc.)
6. Persistence (objet PHP → BDD)
→ Doctrine déclenche PrePersist (CuidEntityTrait)
→ Génère l'ID : "cl" + 24 hex chars aléatoires
→ Set createdAt et updatedAt
→ Doctrine détecte l'INSERT à faire
7. Audit (onFlush)
→ MachineAuditSubscriber détecte la nouvelle machine
→ Crée un AuditLog avec action='create', diff, snapshot
→ L'AuditLog est aussi ajouté à la transaction
8. Flush
→ Doctrine exécute les requêtes SQL :
INSERT INTO machine (id, name, reference, ...) VALUES (...)
INSERT INTO audit_log (id, entity_type, entity_id, action, diff, snapshot, ...) VALUES (...)
9. Sérialisation (objet PHP → JSON)
→ API Platform convertit la Machine en JSON-LD
→ Seuls les champs du groupe 'machine:read' sont inclus
10. Réponse
HTTP/1.1 201 Created
{
"@context": "/api/contexts/Machine",
"@id": "/api/machines/cl1a2b3c...",
"@type": "Machine",
"id": "cl1a2b3c...",
"name": "CNC Mazak 01",
...
}
```
---
## Commandes Symfony utiles
Lancer ces commandes dans le conteneur Docker (`make shell` pour y entrer) :
| Commande | Description |
|----------|-------------|
| `php bin/console debug:router` | Voir toutes les routes disponibles |
| `php bin/console debug:config api_platform` | Voir la config API Platform |
| `php bin/console doctrine:schema:validate` | Vérifier que les entités sont synchronisées avec la BDD |
| `php bin/console doctrine:migrations:diff` | Générer une migration à partir des changements |
| `php bin/console doctrine:migrations:migrate` | Appliquer les migrations |
| `php bin/console cache:clear` | Vider le cache (résout beaucoup de problèmes) |
| `php bin/console app:compress-pdf` | Compresser les PDFs existants |
| `php bin/console app:create-profile` | Créer un profil utilisateur |
---
## Résumé des points clés pour un débutant
1. **API Platform génère les endpoints CRUD automatiquement** à partir des entités — tu n'as pas besoin d'écrire de controllers pour les opérations standard
2. **Les attributs PHP 8** (`#[...]`) remplacent les annotations et configurent tout : BDD, API, sérialisation, sécurité
3. **Les groupes de sérialisation** (`machine:read`, `machine:write`) contrôlent quels champs sont visibles/modifiables
4. **L'audit est automatique** : chaque modification est tracée sans rien avoir à faire manuellement
5. **L'authentification est par session (cookies)**, pas par tokens JWT
6. **Les IDs sont des CUID** (chaînes aléatoires), pas des auto-increment
7. **PostgreSQL stocke les noms en minuscules** : attention dans le SQL brut
8. **Les tests utilisent des transactions** : chaque test est isolé et la BDD est nettoyée automatiquement

1052
docs/FRONTEND.md Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

24
frontend/.gitignore vendored
View File

@@ -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

View File

@@ -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.

View File

@@ -1,3 +0,0 @@
<template>
<NuxtPage/>
</template>

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -1,2 +0,0 @@
User-Agent: *
Disallow:

View File

@@ -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"
}
]
}

View File

@@ -37,7 +37,7 @@ start: env-init
@echo "URLs disponibles:"
@echo "- Symfony API: http://localhost:8081/api"
@echo "- Nuxt (Inventory_frontend): http://localhost:3001"
@echo "- pgAdmin: http://localhost:5050"
@echo "- adminer: http://localhost:5050"
# Éteint le container
stop:
@@ -117,6 +117,10 @@ php-cs-fixer-allow-risky:
test:
$(EXEC_PHP) php -d memory_limit="512M" vendor/bin/phpunit $(FILES)
test-setup:
$(SYMFONY_CONSOLE) doctrine:database:create --if-not-exists --env=test
$(SYMFONY_CONSOLE) doctrine:schema:update --force --env=test
wait:
sleep 10

View File

@@ -1,35 +0,0 @@
# Migration DB (manuel)
Ce guide explique comment importer un dump SQL venant de pgAdmin dans la base Docker.
## 1) Export pgAdmin
Dans pgAdmin:
- Format: Plain
- Options: Use INSERT commands + Use column inserts
- Fichier: `data.sql`
## 2) Normaliser le dump
Convertit les colonnes camelCase en lowercase compact.
```bash
python3 scripts/normalize-dump.py data.sql data_norm.sql --lower
```
## 3) Importer dans la base Docker
Utilise `session_replication_role` pour eviter les erreurs de contraintes circulaires.
```bash
docker compose --env-file docker/.env.docker.local exec -T db psql -U root -d inventory -v ON_ERROR_STOP=1 -c "SET session_replication_role = replica;"
docker compose --env-file docker/.env.docker.local exec -T db psql -U root -d inventory -v ON_ERROR_STOP=1 < data_norm.sql
docker compose --env-file docker/.env.docker.local exec -T db psql -U root -d inventory -v ON_ERROR_STOP=1 -c "SET session_replication_role = DEFAULT;"
```
## 4) Verifier
```bash
docker compose --env-file docker/.env.docker.local exec -T db psql -U $POSTGRES_USER -d $POSTGRES_DB -c "\\dt"
```

View File

@@ -1,229 +0,0 @@
<?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 Version20260110202855 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('CREATE TABLE composants (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, reference VARCHAR(255) DEFAULT NULL, prix NUMERIC(10, 2) DEFAULT NULL, structure JSON DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, typeComposantId VARCHAR(36) DEFAULT NULL, productId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_F95A31995E237E06 ON composants (name)');
$this->addSql('CREATE INDEX IDX_F95A3199CC8A4CEE ON composants (typeComposantId)');
$this->addSql('CREATE INDEX IDX_F95A319936799605 ON composants (productId)');
$this->addSql('CREATE TABLE _ComposantConstructeurs (A VARCHAR(36) NOT NULL, B VARCHAR(36) NOT NULL, PRIMARY KEY (A, B))');
$this->addSql('CREATE INDEX IDX_60760125D3D99E8B ON _ComposantConstructeurs (A)');
$this->addSql('CREATE INDEX IDX_607601254AD0CF31 ON _ComposantConstructeurs (B)');
$this->addSql('CREATE TABLE constructeurs (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, email VARCHAR(255) DEFAULT NULL, phone VARCHAR(255) DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_CC8D6F55E237E06 ON constructeurs (name)');
$this->addSql('CREATE TABLE custom_field_values (id VARCHAR(36) NOT NULL, value VARCHAR(255) NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, customFieldId VARCHAR(36) NOT NULL, machineId VARCHAR(36) DEFAULT NULL, composantId VARCHAR(36) DEFAULT NULL, pieceId VARCHAR(36) DEFAULT NULL, productId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_6B64D7FF5C4A705F ON custom_field_values (customFieldId)');
$this->addSql('CREATE INDEX IDX_6B64D7FF633EC4FD ON custom_field_values (machineId)');
$this->addSql('CREATE INDEX IDX_6B64D7FF345EE564 ON custom_field_values (composantId)');
$this->addSql('CREATE INDEX IDX_6B64D7FF3C6A9D1 ON custom_field_values (pieceId)');
$this->addSql('CREATE INDEX IDX_6B64D7FF36799605 ON custom_field_values (productId)');
$this->addSql('CREATE TABLE custom_fields (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, type VARCHAR(50) NOT NULL, required BOOLEAN DEFAULT false NOT NULL, defaultValue VARCHAR(255) DEFAULT NULL, options JSON NOT NULL, orderIndex INT DEFAULT 0 NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, typeMachineId VARCHAR(36) DEFAULT NULL, typeComposantId VARCHAR(36) DEFAULT NULL, typePieceId VARCHAR(36) DEFAULT NULL, typeProductId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_4A48378C2F024C2 ON custom_fields (typeMachineId)');
$this->addSql('CREATE INDEX IDX_4A48378CCC8A4CEE ON custom_fields (typeComposantId)');
$this->addSql('CREATE INDEX IDX_4A48378C169F1CF6 ON custom_fields (typePieceId)');
$this->addSql('CREATE INDEX IDX_4A48378C57B7763A ON custom_fields (typeProductId)');
$this->addSql('CREATE TABLE documents (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, filename VARCHAR(255) NOT NULL, path VARCHAR(500) NOT NULL, mimeType VARCHAR(100) NOT NULL, size INT NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, machineId VARCHAR(36) DEFAULT NULL, composantId VARCHAR(36) DEFAULT NULL, pieceId VARCHAR(36) DEFAULT NULL, productId VARCHAR(36) DEFAULT NULL, siteId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_A2B07288633EC4FD ON documents (machineId)');
$this->addSql('CREATE INDEX IDX_A2B07288345EE564 ON documents (composantId)');
$this->addSql('CREATE INDEX IDX_A2B072883C6A9D1 ON documents (pieceId)');
$this->addSql('CREATE INDEX IDX_A2B0728836799605 ON documents (productId)');
$this->addSql('CREATE INDEX IDX_A2B072886973A4FD ON documents (siteId)');
$this->addSql('CREATE TABLE machine_component_links (id VARCHAR(36) NOT NULL, nameOverride VARCHAR(255) DEFAULT NULL, referenceOverride VARCHAR(255) DEFAULT NULL, prixOverride NUMERIC(10, 2) DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, machineId VARCHAR(36) NOT NULL, composantId VARCHAR(36) NOT NULL, parentLinkId VARCHAR(36) DEFAULT NULL, typeMachineComponentRequirementId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_528EFE19633EC4FD ON machine_component_links (machineId)');
$this->addSql('CREATE INDEX IDX_528EFE19345EE564 ON machine_component_links (composantId)');
$this->addSql('CREATE INDEX IDX_528EFE19EF6CF34B ON machine_component_links (parentLinkId)');
$this->addSql('CREATE INDEX IDX_528EFE19C44B383C ON machine_component_links (typeMachineComponentRequirementId)');
$this->addSql('CREATE TABLE machine_piece_links (id VARCHAR(36) NOT NULL, nameOverride VARCHAR(255) DEFAULT NULL, referenceOverride VARCHAR(255) DEFAULT NULL, prixOverride NUMERIC(10, 2) DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, machineId VARCHAR(36) NOT NULL, pieceId VARCHAR(36) NOT NULL, parentLinkId VARCHAR(36) DEFAULT NULL, typeMachinePieceRequirementId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_62941615633EC4FD ON machine_piece_links (machineId)');
$this->addSql('CREATE INDEX IDX_629416153C6A9D1 ON machine_piece_links (pieceId)');
$this->addSql('CREATE INDEX IDX_62941615EF6CF34B ON machine_piece_links (parentLinkId)');
$this->addSql('CREATE INDEX IDX_62941615F957D314 ON machine_piece_links (typeMachinePieceRequirementId)');
$this->addSql('CREATE TABLE machine_product_links (id VARCHAR(36) NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, machineId VARCHAR(36) NOT NULL, productId VARCHAR(36) NOT NULL, typeMachineProductRequirementId VARCHAR(36) DEFAULT NULL, parentLinkId VARCHAR(36) DEFAULT NULL, parentComponentLinkId VARCHAR(36) DEFAULT NULL, parentPieceLinkId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_8CC32259633EC4FD ON machine_product_links (machineId)');
$this->addSql('CREATE INDEX IDX_8CC3225936799605 ON machine_product_links (productId)');
$this->addSql('CREATE INDEX IDX_8CC32259B590B209 ON machine_product_links (typeMachineProductRequirementId)');
$this->addSql('CREATE INDEX IDX_8CC32259EF6CF34B ON machine_product_links (parentLinkId)');
$this->addSql('CREATE INDEX IDX_8CC32259A63AC5DC ON machine_product_links (parentComponentLinkId)');
$this->addSql('CREATE INDEX IDX_8CC32259937A1D7C ON machine_product_links (parentPieceLinkId)');
$this->addSql('CREATE TABLE machines (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, reference VARCHAR(255) DEFAULT NULL, prix NUMERIC(10, 2) DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, siteId VARCHAR(36) NOT NULL, typeMachineId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_F1CE8DED5E237E06 ON machines (name)');
$this->addSql('CREATE INDEX IDX_F1CE8DED6973A4FD ON machines (siteId)');
$this->addSql('CREATE INDEX IDX_F1CE8DED2F024C2 ON machines (typeMachineId)');
$this->addSql('CREATE TABLE _MachineConstructeurs (A VARCHAR(36) NOT NULL, B VARCHAR(36) NOT NULL, PRIMARY KEY (A, B))');
$this->addSql('CREATE INDEX IDX_E6A040CCD3D99E8B ON _MachineConstructeurs (A)');
$this->addSql('CREATE INDEX IDX_E6A040CC4AD0CF31 ON _MachineConstructeurs (B)');
$this->addSql('CREATE TABLE model_types (id VARCHAR(36) NOT NULL, name VARCHAR(120) NOT NULL, code VARCHAR(60) NOT NULL, category VARCHAR(255) NOT NULL, notes TEXT DEFAULT NULL, description TEXT DEFAULT NULL, componentSkeleton JSON DEFAULT NULL, pieceSkeleton JSON DEFAULT NULL, productSkeleton JSON DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6773A9C777153098 ON model_types (code)');
$this->addSql('CREATE UNIQUE INDEX unique_category_name ON model_types (category, name)');
$this->addSql('CREATE TABLE pieces (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, reference VARCHAR(255) DEFAULT NULL, prix NUMERIC(10, 2) DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, typePieceId VARCHAR(36) DEFAULT NULL, productId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B92D74725E237E06 ON pieces (name)');
$this->addSql('CREATE INDEX IDX_B92D7472169F1CF6 ON pieces (typePieceId)');
$this->addSql('CREATE INDEX IDX_B92D747236799605 ON pieces (productId)');
$this->addSql('CREATE TABLE _PieceConstructeurs (A VARCHAR(36) NOT NULL, B VARCHAR(36) NOT NULL, PRIMARY KEY (A, B))');
$this->addSql('CREATE INDEX IDX_E94732E5D3D99E8B ON _PieceConstructeurs (A)');
$this->addSql('CREATE INDEX IDX_E94732E54AD0CF31 ON _PieceConstructeurs (B)');
$this->addSql('CREATE TABLE products (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, reference VARCHAR(255) DEFAULT NULL, supplierPrice NUMERIC(10, 2) DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, typeProductId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B3BA5A5A5E237E06 ON products (name)');
$this->addSql('CREATE INDEX IDX_B3BA5A5A57B7763A ON products (typeProductId)');
$this->addSql('CREATE TABLE _ProductConstructeurs (A VARCHAR(36) NOT NULL, B VARCHAR(36) NOT NULL, PRIMARY KEY (A, B))');
$this->addSql('CREATE INDEX IDX_CF7403FCD3D99E8B ON _ProductConstructeurs (A)');
$this->addSql('CREATE INDEX IDX_CF7403FC4AD0CF31 ON _ProductConstructeurs (B)');
$this->addSql('CREATE TABLE profiles (id VARCHAR(36) NOT NULL, email VARCHAR(180) DEFAULT NULL, firstName VARCHAR(100) NOT NULL, lastName VARCHAR(100) NOT NULL, isActive BOOLEAN DEFAULT true NOT NULL, roles JSON DEFAULT \'["ROLE_USER"]\' NOT NULL, password VARCHAR(255) DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_email ON profiles (email)');
$this->addSql('CREATE TABLE sites (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, contactName VARCHAR(255) DEFAULT \'\' NOT NULL, contactPhone VARCHAR(20) DEFAULT \'\' NOT NULL, contactAddress VARCHAR(500) DEFAULT \'\' NOT NULL, contactPostalCode VARCHAR(10) DEFAULT \'\' NOT NULL, contactCity VARCHAR(100) DEFAULT \'\' NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE TABLE type_machine_component_requirements (id VARCHAR(36) NOT NULL, label VARCHAR(255) DEFAULT NULL, minCount INT DEFAULT 1 NOT NULL, maxCount INT DEFAULT NULL, allowNewModels BOOLEAN DEFAULT true NOT NULL, allow_new_models BOOLEAN DEFAULT true NOT NULL, orderIndex INT DEFAULT 0 NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, typeMachineId VARCHAR(36) NOT NULL, typeComposantId VARCHAR(36) NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_969587902F024C2 ON type_machine_component_requirements (typeMachineId)');
$this->addSql('CREATE INDEX IDX_96958790CC8A4CEE ON type_machine_component_requirements (typeComposantId)');
$this->addSql('CREATE TABLE type_machine_piece_requirements (id VARCHAR(36) NOT NULL, label VARCHAR(255) DEFAULT NULL, minCount INT DEFAULT 0 NOT NULL, maxCount INT DEFAULT NULL, required BOOLEAN DEFAULT false NOT NULL, allowNewModels BOOLEAN DEFAULT true NOT NULL, orderIndex INT DEFAULT 0 NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, typeMachineId VARCHAR(36) NOT NULL, typePieceId VARCHAR(36) NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_F609E59E2F024C2 ON type_machine_piece_requirements (typeMachineId)');
$this->addSql('CREATE INDEX IDX_F609E59E169F1CF6 ON type_machine_piece_requirements (typePieceId)');
$this->addSql('CREATE TABLE type_machine_product_requirements (id VARCHAR(36) NOT NULL, label VARCHAR(255) DEFAULT NULL, minCount INT DEFAULT 0 NOT NULL, maxCount INT DEFAULT NULL, required BOOLEAN DEFAULT false NOT NULL, allowNewModels BOOLEAN DEFAULT true NOT NULL, orderIndex INT DEFAULT 0 NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, typeMachineId VARCHAR(36) NOT NULL, typeProductId VARCHAR(36) NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_29A51F982F024C2 ON type_machine_product_requirements (typeMachineId)');
$this->addSql('CREATE INDEX IDX_29A51F9857B7763A ON type_machine_product_requirements (typeProductId)');
$this->addSql('CREATE TABLE type_machines (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, maintenanceFrequency VARCHAR(255) DEFAULT NULL, maintenance_frequency VARCHAR(255) DEFAULT NULL, criticalParts JSON DEFAULT NULL, machinePieces JSON DEFAULT NULL, machine_pieces JSON DEFAULT NULL, specifications JSON DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_3C31AA115E237E06 ON type_machines (name)');
$this->addSql('ALTER TABLE composants ADD CONSTRAINT FK_F95A3199CC8A4CEE FOREIGN KEY (typeComposantId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE composants ADD CONSTRAINT FK_F95A319936799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE SET NULL NOT DEFERRABLE');
$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 custom_field_values ADD CONSTRAINT FK_6B64D7FF5C4A705F FOREIGN KEY (customFieldId) REFERENCES custom_fields (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT FK_6B64D7FF633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT FK_6B64D7FF345EE564 FOREIGN KEY (composantId) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT FK_6B64D7FF3C6A9D1 FOREIGN KEY (pieceId) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT FK_6B64D7FF36799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT FK_4A48378C2F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT FK_4A48378CCC8A4CEE FOREIGN KEY (typeComposantId) REFERENCES model_types (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT FK_4A48378C169F1CF6 FOREIGN KEY (typePieceId) REFERENCES model_types (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT FK_4A48378C57B7763A FOREIGN KEY (typeProductId) REFERENCES model_types (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B07288633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B07288345EE564 FOREIGN KEY (composantId) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B072883C6A9D1 FOREIGN KEY (pieceId) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B0728836799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B072886973A4FD FOREIGN KEY (siteId) REFERENCES sites (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT FK_528EFE19633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT FK_528EFE19345EE564 FOREIGN KEY (composantId) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT FK_528EFE19EF6CF34B FOREIGN KEY (parentLinkId) REFERENCES machine_component_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT FK_528EFE19C44B383C FOREIGN KEY (typeMachineComponentRequirementId) REFERENCES type_machine_component_requirements (id) ON DELETE SET NULL NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT FK_62941615633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT FK_629416153C6A9D1 FOREIGN KEY (pieceId) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT FK_62941615EF6CF34B FOREIGN KEY (parentLinkId) REFERENCES machine_component_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT FK_62941615F957D314 FOREIGN KEY (typeMachinePieceRequirementId) REFERENCES type_machine_piece_requirements (id) ON DELETE SET NULL NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC3225936799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259B590B209 FOREIGN KEY (typeMachineProductRequirementId) REFERENCES type_machine_product_requirements (id) ON DELETE SET NULL NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259EF6CF34B FOREIGN KEY (parentLinkId) REFERENCES machine_product_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259A63AC5DC FOREIGN KEY (parentComponentLinkId) REFERENCES machine_component_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259937A1D7C FOREIGN KEY (parentPieceLinkId) REFERENCES machine_piece_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machines ADD CONSTRAINT FK_F1CE8DED6973A4FD FOREIGN KEY (siteId) REFERENCES sites (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machines ADD CONSTRAINT FK_F1CE8DED2F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) NOT DEFERRABLE');
$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 pieces ADD CONSTRAINT FK_B92D7472169F1CF6 FOREIGN KEY (typePieceId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE pieces ADD CONSTRAINT FK_B92D747236799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE SET NULL NOT DEFERRABLE');
$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 products ADD CONSTRAINT FK_B3BA5A5A57B7763A FOREIGN KEY (typeProductId) REFERENCES model_types (id) NOT DEFERRABLE');
$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 type_machine_component_requirements ADD CONSTRAINT FK_969587902F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE type_machine_component_requirements ADD CONSTRAINT FK_96958790CC8A4CEE FOREIGN KEY (typeComposantId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE type_machine_piece_requirements ADD CONSTRAINT FK_F609E59E2F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE type_machine_piece_requirements ADD CONSTRAINT FK_F609E59E169F1CF6 FOREIGN KEY (typePieceId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE type_machine_product_requirements ADD CONSTRAINT FK_29A51F982F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE type_machine_product_requirements ADD CONSTRAINT FK_29A51F9857B7763A FOREIGN KEY (typeProductId) REFERENCES model_types (id) NOT DEFERRABLE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE composants DROP CONSTRAINT FK_F95A3199CC8A4CEE');
$this->addSql('ALTER TABLE composants DROP CONSTRAINT FK_F95A319936799605');
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT FK_60760125D3D99E8B');
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT FK_607601254AD0CF31');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT FK_6B64D7FF5C4A705F');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT FK_6B64D7FF633EC4FD');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT FK_6B64D7FF345EE564');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT FK_6B64D7FF3C6A9D1');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT FK_6B64D7FF36799605');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT FK_4A48378C2F024C2');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT FK_4A48378CCC8A4CEE');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT FK_4A48378C169F1CF6');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT FK_4A48378C57B7763A');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT FK_A2B07288633EC4FD');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT FK_A2B07288345EE564');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT FK_A2B072883C6A9D1');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT FK_A2B0728836799605');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT FK_A2B072886973A4FD');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT FK_528EFE19633EC4FD');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT FK_528EFE19345EE564');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT FK_528EFE19EF6CF34B');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT FK_528EFE19C44B383C');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT FK_62941615633EC4FD');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT FK_629416153C6A9D1');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT FK_62941615EF6CF34B');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT FK_62941615F957D314');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT FK_8CC32259633EC4FD');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT FK_8CC3225936799605');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT FK_8CC32259B590B209');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT FK_8CC32259EF6CF34B');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT FK_8CC32259A63AC5DC');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT FK_8CC32259937A1D7C');
$this->addSql('ALTER TABLE machines DROP CONSTRAINT FK_F1CE8DED6973A4FD');
$this->addSql('ALTER TABLE machines DROP CONSTRAINT FK_F1CE8DED2F024C2');
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT FK_E6A040CCD3D99E8B');
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT FK_E6A040CC4AD0CF31');
$this->addSql('ALTER TABLE pieces DROP CONSTRAINT FK_B92D7472169F1CF6');
$this->addSql('ALTER TABLE pieces DROP CONSTRAINT FK_B92D747236799605');
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT FK_E94732E5D3D99E8B');
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT FK_E94732E54AD0CF31');
$this->addSql('ALTER TABLE products DROP CONSTRAINT FK_B3BA5A5A57B7763A');
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT FK_CF7403FCD3D99E8B');
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT FK_CF7403FC4AD0CF31');
$this->addSql('ALTER TABLE type_machine_component_requirements DROP CONSTRAINT FK_969587902F024C2');
$this->addSql('ALTER TABLE type_machine_component_requirements DROP CONSTRAINT FK_96958790CC8A4CEE');
$this->addSql('ALTER TABLE type_machine_piece_requirements DROP CONSTRAINT FK_F609E59E2F024C2');
$this->addSql('ALTER TABLE type_machine_piece_requirements DROP CONSTRAINT FK_F609E59E169F1CF6');
$this->addSql('ALTER TABLE type_machine_product_requirements DROP CONSTRAINT FK_29A51F982F024C2');
$this->addSql('ALTER TABLE type_machine_product_requirements DROP CONSTRAINT FK_29A51F9857B7763A');
$this->addSql('DROP TABLE composants');
$this->addSql('DROP TABLE _ComposantConstructeurs');
$this->addSql('DROP TABLE constructeurs');
$this->addSql('DROP TABLE custom_field_values');
$this->addSql('DROP TABLE custom_fields');
$this->addSql('DROP TABLE documents');
$this->addSql('DROP TABLE machine_component_links');
$this->addSql('DROP TABLE machine_piece_links');
$this->addSql('DROP TABLE machine_product_links');
$this->addSql('DROP TABLE machines');
$this->addSql('DROP TABLE _MachineConstructeurs');
$this->addSql('DROP TABLE model_types');
$this->addSql('DROP TABLE pieces');
$this->addSql('DROP TABLE _PieceConstructeurs');
$this->addSql('DROP TABLE products');
$this->addSql('DROP TABLE _ProductConstructeurs');
$this->addSql('DROP TABLE profiles');
$this->addSql('DROP TABLE sites');
$this->addSql('DROP TABLE type_machine_component_requirements');
$this->addSql('DROP TABLE type_machine_piece_requirements');
$this->addSql('DROP TABLE type_machine_product_requirements');
$this->addSql('DROP TABLE type_machines');
}
}

View File

@@ -1,682 +0,0 @@
<?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 Version20260120181438 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('CREATE TABLE IF NOT EXISTS model_types (id VARCHAR(36) NOT NULL, name VARCHAR(120) NOT NULL, code VARCHAR(60) NOT NULL, category VARCHAR(255) NOT NULL, notes TEXT DEFAULT NULL, description TEXT DEFAULT NULL, componentSkeleton JSON DEFAULT NULL, pieceSkeleton JSON DEFAULT NULL, productSkeleton JSON DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS UNIQ_6773A9C777153098 ON model_types (code)');
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS unique_category_name ON model_types (category, name)');
$this->addSql('DROP TABLE IF EXISTS "ModelType"');
$this->addSql('DROP TABLE IF EXISTS _prisma_migrations');
$this->addSql('ALTER TABLE composants DROP CONSTRAINT IF EXISTS "composants_productId_fkey"');
$this->addSql('ALTER TABLE composants DROP CONSTRAINT IF EXISTS "composants_typeComposantId_fkey"');
$this->addSql('ALTER TABLE composants ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE composants ALTER name TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE composants ALTER reference TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE composants ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE composants ALTER typeComposantId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE composants ALTER structure TYPE JSON');
$this->addSql('ALTER TABLE composants ALTER productId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE composants DROP CONSTRAINT IF EXISTS FK_F95A3199CC8A4CEE');
$this->addSql('ALTER TABLE composants ADD CONSTRAINT FK_F95A3199CC8A4CEE FOREIGN KEY (typeComposantId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE composants DROP CONSTRAINT IF EXISTS FK_F95A319936799605');
$this->addSql('ALTER TABLE composants ADD CONSTRAINT FK_F95A319936799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE SET NULL NOT DEFERRABLE');
$this->addSql('ALTER INDEX IF EXISTS composants_name_key RENAME TO UNIQ_F95A31995E237E06');
$this->addSql('ALTER INDEX IF EXISTS idx_f95a31999fd7f38f RENAME TO IDX_F95A3199CC8A4CEE');
$this->addSql('ALTER INDEX IF EXISTS idx_f95a319921c3ccfc RENAME TO IDX_F95A319936799605');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS "_ComposantConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS "_ComposantConstructeurs_B_fkey"');
$this->addSql('DROP INDEX IF EXISTS "_ComposantConstructeurs_AB_unique"');
$this->addSql('ALTER TABLE IF EXISTS "$1" ALTER A TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE IF EXISTS "$1" ALTER B TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS FK_60760125D3D99E8B');
$this->addSql('ALTER TABLE IF EXISTS "$1" ADD CONSTRAINT FK_60760125D3D99E8B FOREIGN KEY (A) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS FK_607601254AD0CF31');
$this->addSql('ALTER TABLE IF EXISTS "$1" ADD CONSTRAINT FK_607601254AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS _ComposantConstructeurs_pkey');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS _ComposantConstructeurs_pkey');
$this->addSql('ALTER TABLE IF EXISTS "$1" ADD PRIMARY KEY (A, B)');
$this->addSql('ALTER INDEX IF EXISTS idx_60760125f88a743c RENAME TO IDX_60760125D3D99E8B');
$this->addSql('ALTER INDEX IF EXISTS _composantconstructeurs_b_index RENAME TO IDX_607601254AD0CF31');
$this->addSql('ALTER TABLE constructeurs ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE constructeurs ALTER name TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE constructeurs ALTER email TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE constructeurs ALTER phone TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE constructeurs ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER INDEX IF EXISTS constructeurs_name_key RENAME TO UNIQ_CC8D6F55E237E06');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS "custom_field_values_composantId_fkey"');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS "custom_field_values_customFieldId_fkey"');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS "custom_field_values_machineId_fkey"');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS "custom_field_values_pieceId_fkey"');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS "custom_field_values_productId_fkey"');
$this->addSql('ALTER TABLE custom_field_values ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE custom_field_values ALTER value TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE custom_field_values ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE custom_field_values ALTER customFieldId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE custom_field_values ALTER machineId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE custom_field_values ALTER composantId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE custom_field_values ALTER pieceId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE custom_field_values ALTER productId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS FK_6B64D7FF5C4A705F');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT FK_6B64D7FF5C4A705F FOREIGN KEY (customFieldId) REFERENCES custom_fields (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS FK_6B64D7FF633EC4FD');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT FK_6B64D7FF633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS FK_6B64D7FF345EE564');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT FK_6B64D7FF345EE564 FOREIGN KEY (composantId) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS FK_6B64D7FF3C6A9D1');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT FK_6B64D7FF3C6A9D1 FOREIGN KEY (pieceId) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS FK_6B64D7FF36799605');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT FK_6B64D7FF36799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER INDEX IF EXISTS idx_6b64d7ffdac15d53 RENAME TO IDX_6B64D7FF5C4A705F');
$this->addSql('ALTER INDEX IF EXISTS idx_6b64d7ff92f0f180 RENAME TO IDX_6B64D7FF633EC4FD');
$this->addSql('ALTER INDEX IF EXISTS idx_6b64d7ffea0a2b9e RENAME TO IDX_6B64D7FF345EE564');
$this->addSql('ALTER INDEX IF EXISTS idx_6b64d7ffd999eb60 RENAME TO IDX_6B64D7FF3C6A9D1');
$this->addSql('ALTER INDEX IF EXISTS idx_6b64d7ff21c3ccfc RENAME TO IDX_6B64D7FF36799605');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT IF EXISTS "custom_fields_typeComposantId_fkey"');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT IF EXISTS "custom_fields_typeMachineId_fkey"');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT IF EXISTS "custom_fields_typePieceId_fkey"');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT IF EXISTS "custom_fields_typeProductId_fkey"');
$this->addSql('ALTER TABLE custom_fields ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE custom_fields ALTER name TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE custom_fields ALTER type TYPE VARCHAR(50)');
$this->addSql('ALTER TABLE custom_fields ALTER defaultValue TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE custom_fields ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE custom_fields ALTER typeMachineId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE custom_fields ALTER typeComposantId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE custom_fields ALTER typePieceId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE custom_fields ALTER options TYPE JSON USING array_to_json(options)');
$this->addSql('ALTER TABLE custom_fields ALTER typeProductId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT IF EXISTS FK_4A48378C2F024C2');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT FK_4A48378C2F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT IF EXISTS FK_4A48378CCC8A4CEE');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT FK_4A48378CCC8A4CEE FOREIGN KEY (typeComposantId) REFERENCES model_types (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT IF EXISTS FK_4A48378C169F1CF6');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT FK_4A48378C169F1CF6 FOREIGN KEY (typePieceId) REFERENCES model_types (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT IF EXISTS FK_4A48378C57B7763A');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT FK_4A48378C57B7763A FOREIGN KEY (typeProductId) REFERENCES model_types (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER INDEX IF EXISTS idx_4a48378c542108fe RENAME TO IDX_4A48378C2F024C2');
$this->addSql('ALTER INDEX IF EXISTS idx_4a48378c9fd7f38f RENAME TO IDX_4A48378CCC8A4CEE');
$this->addSql('ALTER INDEX IF EXISTS idx_4a48378cf429180f RENAME TO IDX_4A48378C169F1CF6');
$this->addSql('ALTER INDEX IF EXISTS idx_4a48378ce7123582 RENAME TO IDX_4A48378C57B7763A');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS "documents_composantId_fkey"');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS "documents_machineId_fkey"');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS "documents_pieceId_fkey"');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS "documents_productId_fkey"');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS "documents_siteId_fkey"');
$this->addSql('ALTER TABLE documents ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE documents ALTER name TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE documents ALTER filename TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE documents ALTER mimeType TYPE VARCHAR(100)');
$this->addSql('ALTER TABLE documents ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE documents ALTER machineId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE documents ALTER composantId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE documents ALTER pieceId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE documents ALTER siteId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE documents ALTER productId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS FK_A2B07288633EC4FD');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B07288633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS FK_A2B07288345EE564');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B07288345EE564 FOREIGN KEY (composantId) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS FK_A2B072883C6A9D1');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B072883C6A9D1 FOREIGN KEY (pieceId) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS FK_A2B0728836799605');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B0728836799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS FK_A2B072886973A4FD');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B072886973A4FD FOREIGN KEY (siteId) REFERENCES sites (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER INDEX IF EXISTS idx_a2b0728892f0f180 RENAME TO IDX_A2B07288633EC4FD');
$this->addSql('ALTER INDEX IF EXISTS idx_a2b07288ea0a2b9e RENAME TO IDX_A2B07288345EE564');
$this->addSql('ALTER INDEX IF EXISTS idx_a2b07288d999eb60 RENAME TO IDX_A2B072883C6A9D1');
$this->addSql('ALTER INDEX IF EXISTS idx_a2b0728821c3ccfc RENAME TO IDX_A2B0728836799605');
$this->addSql('ALTER INDEX IF EXISTS idx_a2b07288871a3650 RENAME TO IDX_A2B072886973A4FD');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT IF EXISTS "machine_component_links_composantId_fkey"');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT IF EXISTS "machine_component_links_machineId_fkey"');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT IF EXISTS "machine_component_links_parentLinkId_fkey"');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT IF EXISTS "machine_component_links_typeMachineComponentRequirementId_fkey"');
$this->addSql('ALTER TABLE machine_component_links ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_component_links ALTER machineId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_component_links ALTER composantId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_component_links ALTER parentLinkId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_component_links ALTER typeMachineComponentRequirementId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_component_links ALTER nameOverride TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE machine_component_links ALTER referenceOverride TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE machine_component_links ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT IF EXISTS FK_528EFE19633EC4FD');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT FK_528EFE19633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT IF EXISTS FK_528EFE19345EE564');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT FK_528EFE19345EE564 FOREIGN KEY (composantId) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT IF EXISTS FK_528EFE19EF6CF34B');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT FK_528EFE19EF6CF34B FOREIGN KEY (parentLinkId) REFERENCES machine_component_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT IF EXISTS FK_528EFE19C44B383C');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT FK_528EFE19C44B383C FOREIGN KEY (typeMachineComponentRequirementId) REFERENCES type_machine_component_requirements (id) ON DELETE SET NULL NOT DEFERRABLE');
$this->addSql('ALTER INDEX IF EXISTS idx_528efe1992f0f180 RENAME TO IDX_528EFE19633EC4FD');
$this->addSql('ALTER INDEX IF EXISTS idx_528efe19ea0a2b9e RENAME TO IDX_528EFE19345EE564');
$this->addSql('ALTER INDEX IF EXISTS idx_528efe191446d9b2 RENAME TO IDX_528EFE19EF6CF34B');
$this->addSql('ALTER INDEX IF EXISTS idx_528efe19bbf9038c RENAME TO IDX_528EFE19C44B383C');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT IF EXISTS "machine_piece_links_machineId_fkey"');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT IF EXISTS "machine_piece_links_parentLinkId_fkey"');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT IF EXISTS "machine_piece_links_pieceId_fkey"');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT IF EXISTS "machine_piece_links_typeMachinePieceRequirementId_fkey"');
$this->addSql('ALTER TABLE machine_piece_links ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_piece_links ALTER machineId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_piece_links ALTER pieceId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_piece_links ALTER parentLinkId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_piece_links ALTER typeMachinePieceRequirementId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_piece_links ALTER nameOverride TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE machine_piece_links ALTER referenceOverride TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE machine_piece_links ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT IF EXISTS FK_62941615633EC4FD');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT FK_62941615633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT IF EXISTS FK_629416153C6A9D1');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT FK_629416153C6A9D1 FOREIGN KEY (pieceId) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT IF EXISTS FK_62941615EF6CF34B');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT FK_62941615EF6CF34B FOREIGN KEY (parentLinkId) REFERENCES machine_component_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT IF EXISTS FK_62941615F957D314');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT FK_62941615F957D314 FOREIGN KEY (typeMachinePieceRequirementId) REFERENCES type_machine_piece_requirements (id) ON DELETE SET NULL NOT DEFERRABLE');
$this->addSql('ALTER INDEX IF EXISTS idx_6294161592f0f180 RENAME TO IDX_62941615633EC4FD');
$this->addSql('ALTER INDEX IF EXISTS idx_62941615d999eb60 RENAME TO IDX_629416153C6A9D1');
$this->addSql('ALTER INDEX IF EXISTS idx_629416151446d9b2 RENAME TO IDX_62941615EF6CF34B');
$this->addSql('ALTER INDEX IF EXISTS idx_629416156e0f7201 RENAME TO IDX_62941615F957D314');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS "machine_product_links_machineId_fkey"');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS "machine_product_links_parentComponentLinkId_fkey"');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS "machine_product_links_parentLinkId_fkey"');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS "machine_product_links_parentPieceLinkId_fkey"');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS "machine_product_links_productId_fkey"');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS "machine_product_links_typeMachineProductRequirementId_fkey"');
$this->addSql('ALTER TABLE machine_product_links ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_product_links ALTER machineId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_product_links ALTER productId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_product_links ALTER typeMachineProductRequirementId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_product_links ALTER parentLinkId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_product_links ALTER parentComponentLinkId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_product_links ALTER parentPieceLinkId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machine_product_links ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS FK_8CC32259633EC4FD');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS FK_8CC3225936799605');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC3225936799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS FK_8CC32259B590B209');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259B590B209 FOREIGN KEY (typeMachineProductRequirementId) REFERENCES type_machine_product_requirements (id) ON DELETE SET NULL NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS FK_8CC32259EF6CF34B');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259EF6CF34B FOREIGN KEY (parentLinkId) REFERENCES machine_product_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS FK_8CC32259A63AC5DC');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259A63AC5DC FOREIGN KEY (parentComponentLinkId) REFERENCES machine_component_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS FK_8CC32259937A1D7C');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259937A1D7C FOREIGN KEY (parentPieceLinkId) REFERENCES machine_piece_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER INDEX IF EXISTS machine_product_links_machineid_idx RENAME TO IDX_8CC32259633EC4FD');
$this->addSql('ALTER INDEX IF EXISTS machine_product_links_productid_idx RENAME TO IDX_8CC3225936799605');
$this->addSql('ALTER INDEX IF EXISTS idx_8cc32259187fc99c RENAME TO IDX_8CC32259B590B209');
$this->addSql('ALTER INDEX IF EXISTS idx_8cc322591446d9b2 RENAME TO IDX_8CC32259EF6CF34B');
$this->addSql('ALTER INDEX IF EXISTS idx_8cc32259bd5b4086 RENAME TO IDX_8CC32259A63AC5DC');
$this->addSql('ALTER INDEX IF EXISTS idx_8cc32259b1619fa4 RENAME TO IDX_8CC32259937A1D7C');
$this->addSql('ALTER TABLE machines DROP CONSTRAINT IF EXISTS "machines_siteId_fkey"');
$this->addSql('ALTER TABLE machines DROP CONSTRAINT IF EXISTS "machines_typeMachineId_fkey"');
$this->addSql('ALTER TABLE machines ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machines ALTER name TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE machines ALTER reference TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE machines ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE machines ALTER siteId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machines ALTER typeMachineId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE machines DROP CONSTRAINT IF EXISTS FK_F1CE8DED6973A4FD');
$this->addSql('ALTER TABLE machines ADD CONSTRAINT FK_F1CE8DED6973A4FD FOREIGN KEY (siteId) REFERENCES sites (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machines DROP CONSTRAINT IF EXISTS FK_F1CE8DED2F024C2');
$this->addSql('ALTER TABLE machines ADD CONSTRAINT FK_F1CE8DED2F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) NOT DEFERRABLE');
$this->addSql('ALTER INDEX IF EXISTS machines_name_key RENAME TO UNIQ_F1CE8DED5E237E06');
$this->addSql('ALTER INDEX IF EXISTS idx_f1ce8ded871a3650 RENAME TO IDX_F1CE8DED6973A4FD');
$this->addSql('ALTER INDEX IF EXISTS idx_f1ce8ded542108fe RENAME TO IDX_F1CE8DED2F024C2');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS "_MachineConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS "_MachineConstructeurs_B_fkey"');
$this->addSql('DROP INDEX IF EXISTS "_MachineConstructeurs_AB_unique"');
$this->addSql('ALTER TABLE IF EXISTS "$1" ALTER A TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE IF EXISTS "$1" ALTER B TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS FK_E6A040CCD3D99E8B');
$this->addSql('ALTER TABLE IF EXISTS "$1" ADD CONSTRAINT FK_E6A040CCD3D99E8B FOREIGN KEY (A) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS FK_E6A040CC4AD0CF31');
$this->addSql('ALTER TABLE IF EXISTS "$1" ADD CONSTRAINT FK_E6A040CC4AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS _MachineConstructeurs_pkey');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS _MachineConstructeurs_pkey');
$this->addSql('ALTER TABLE IF EXISTS "$1" ADD PRIMARY KEY (A, B)');
$this->addSql('ALTER INDEX IF EXISTS idx_e6a040ccf88a743c RENAME TO IDX_E6A040CCD3D99E8B');
$this->addSql('ALTER INDEX IF EXISTS _machineconstructeurs_b_index RENAME TO IDX_E6A040CC4AD0CF31');
$this->addSql('ALTER TABLE pieces DROP CONSTRAINT IF EXISTS "pieces_productId_fkey"');
$this->addSql('ALTER TABLE pieces DROP CONSTRAINT IF EXISTS "pieces_typePieceId_fkey"');
$this->addSql('ALTER TABLE pieces ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE pieces ALTER name TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE pieces ALTER reference TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE pieces ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE pieces ALTER typePieceId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE pieces ALTER productId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE pieces DROP CONSTRAINT IF EXISTS FK_B92D7472169F1CF6');
$this->addSql('ALTER TABLE pieces ADD CONSTRAINT FK_B92D7472169F1CF6 FOREIGN KEY (typePieceId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE pieces DROP CONSTRAINT IF EXISTS FK_B92D747236799605');
$this->addSql('ALTER TABLE pieces ADD CONSTRAINT FK_B92D747236799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE SET NULL NOT DEFERRABLE');
$this->addSql('ALTER INDEX IF EXISTS pieces_name_key RENAME TO UNIQ_B92D74725E237E06');
$this->addSql('ALTER INDEX IF EXISTS idx_b92d7472f429180f RENAME TO IDX_B92D7472169F1CF6');
$this->addSql('ALTER INDEX IF EXISTS idx_b92d747221c3ccfc RENAME TO IDX_B92D747236799605');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS "_PieceConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS "_PieceConstructeurs_B_fkey"');
$this->addSql('DROP INDEX IF EXISTS "_PieceConstructeurs_AB_unique"');
$this->addSql('ALTER TABLE IF EXISTS "$1" ALTER A TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE IF EXISTS "$1" ALTER B TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS FK_E94732E5D3D99E8B');
$this->addSql('ALTER TABLE IF EXISTS "$1" ADD CONSTRAINT FK_E94732E5D3D99E8B FOREIGN KEY (A) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS FK_E94732E54AD0CF31');
$this->addSql('ALTER TABLE IF EXISTS "$1" ADD CONSTRAINT FK_E94732E54AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS _PieceConstructeurs_pkey');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS _PieceConstructeurs_pkey');
$this->addSql('ALTER TABLE IF EXISTS "$1" ADD PRIMARY KEY (A, B)');
$this->addSql('ALTER INDEX IF EXISTS idx_e94732e5f88a743c RENAME TO IDX_E94732E5D3D99E8B');
$this->addSql('ALTER INDEX IF EXISTS _piececonstructeurs_b_index RENAME TO IDX_E94732E54AD0CF31');
$this->addSql('ALTER TABLE products DROP CONSTRAINT IF EXISTS "products_typeProductId_fkey"');
$this->addSql('ALTER TABLE products ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE products ALTER name TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE products ALTER reference TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE products ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE products ALTER typeProductId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE products DROP CONSTRAINT IF EXISTS FK_B3BA5A5A57B7763A');
$this->addSql('ALTER TABLE products ADD CONSTRAINT FK_B3BA5A5A57B7763A FOREIGN KEY (typeProductId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER INDEX IF EXISTS products_name_key RENAME TO UNIQ_B3BA5A5A5E237E06');
$this->addSql('ALTER INDEX IF EXISTS idx_b3ba5a5ae7123582 RENAME TO IDX_B3BA5A5A57B7763A');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS "_ProductConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS "_ProductConstructeurs_B_fkey"');
$this->addSql('DROP INDEX IF EXISTS "_ProductConstructeurs_AB_unique"');
$this->addSql('ALTER TABLE IF EXISTS "$1" ALTER A TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE IF EXISTS "$1" ALTER B TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS FK_CF7403FCD3D99E8B');
$this->addSql('ALTER TABLE IF EXISTS "$1" ADD CONSTRAINT FK_CF7403FCD3D99E8B FOREIGN KEY (A) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS FK_CF7403FC4AD0CF31');
$this->addSql('ALTER TABLE IF EXISTS "$1" ADD CONSTRAINT FK_CF7403FC4AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS _ProductConstructeurs_pkey');
$this->addSql('ALTER TABLE IF EXISTS "$1" DROP CONSTRAINT IF EXISTS _ProductConstructeurs_pkey');
$this->addSql('ALTER TABLE IF EXISTS "$1" ADD PRIMARY KEY (A, B)');
$this->addSql('ALTER INDEX IF EXISTS idx_cf7403fcf88a743c RENAME TO IDX_CF7403FCD3D99E8B');
$this->addSql('ALTER INDEX IF EXISTS _productconstructeurs_b_index RENAME TO IDX_CF7403FC4AD0CF31');
$this->addSql('ALTER TABLE profiles ADD COLUMN IF NOT EXISTS email VARCHAR(180) DEFAULT NULL');
$this->addSql('ALTER TABLE profiles ADD COLUMN IF NOT EXISTS roles JSON DEFAULT \'["ROLE_USER"]\' NOT NULL');
$this->addSql('ALTER TABLE profiles ADD COLUMN IF NOT EXISTS password VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE profiles ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE profiles ALTER firstname TYPE VARCHAR(100)');
$this->addSql('ALTER TABLE profiles ALTER lastname TYPE VARCHAR(100)');
$this->addSql('ALTER TABLE profiles ALTER createdat DROP DEFAULT');
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS UNIQ_email ON profiles (email)');
$this->addSql('ALTER TABLE sites ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE sites ALTER name TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE sites ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE sites ALTER contactName TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE sites ALTER contactPhone TYPE VARCHAR(20)');
$this->addSql('ALTER TABLE sites ALTER contactAddress TYPE VARCHAR(500)');
$this->addSql('ALTER TABLE sites ALTER contactPostalCode TYPE VARCHAR(10)');
$this->addSql('ALTER TABLE sites ALTER contactCity TYPE VARCHAR(100)');
$this->addSql('ALTER TABLE type_machine_component_requirements DROP CONSTRAINT IF EXISTS "type_machine_component_requirements_typeComposantId_fkey"');
$this->addSql('ALTER TABLE type_machine_component_requirements DROP CONSTRAINT IF EXISTS "type_machine_component_requirements_typeMachineId_fkey"');
$this->addSql('ALTER TABLE type_machine_component_requirements ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE type_machine_component_requirements ALTER label TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE type_machine_component_requirements ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE type_machine_component_requirements ALTER typeMachineId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE type_machine_component_requirements ALTER typeComposantId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE type_machine_component_requirements DROP CONSTRAINT IF EXISTS FK_969587902F024C2');
$this->addSql('ALTER TABLE type_machine_component_requirements ADD CONSTRAINT FK_969587902F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE type_machine_component_requirements DROP CONSTRAINT IF EXISTS FK_96958790CC8A4CEE');
$this->addSql('ALTER TABLE type_machine_component_requirements ADD CONSTRAINT FK_96958790CC8A4CEE FOREIGN KEY (typeComposantId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER INDEX IF EXISTS idx_96958790542108fe RENAME TO IDX_969587902F024C2');
$this->addSql('ALTER INDEX IF EXISTS idx_969587909fd7f38f RENAME TO IDX_96958790CC8A4CEE');
$this->addSql('ALTER TABLE type_machine_piece_requirements DROP CONSTRAINT IF EXISTS "type_machine_piece_requirements_typeMachineId_fkey"');
$this->addSql('ALTER TABLE type_machine_piece_requirements DROP CONSTRAINT IF EXISTS "type_machine_piece_requirements_typePieceId_fkey"');
$this->addSql('ALTER TABLE type_machine_piece_requirements ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE type_machine_piece_requirements ALTER label TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE type_machine_piece_requirements ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE type_machine_piece_requirements ALTER typeMachineId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE type_machine_piece_requirements ALTER typePieceId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE type_machine_piece_requirements DROP CONSTRAINT IF EXISTS FK_F609E59E2F024C2');
$this->addSql('ALTER TABLE type_machine_piece_requirements ADD CONSTRAINT FK_F609E59E2F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE type_machine_piece_requirements DROP CONSTRAINT IF EXISTS FK_F609E59E169F1CF6');
$this->addSql('ALTER TABLE type_machine_piece_requirements ADD CONSTRAINT FK_F609E59E169F1CF6 FOREIGN KEY (typePieceId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER INDEX IF EXISTS idx_f609e59e542108fe RENAME TO IDX_F609E59E2F024C2');
$this->addSql('ALTER INDEX IF EXISTS idx_f609e59ef429180f RENAME TO IDX_F609E59E169F1CF6');
$this->addSql('ALTER TABLE type_machine_product_requirements DROP CONSTRAINT IF EXISTS "type_machine_product_requirements_typeMachineId_fkey"');
$this->addSql('ALTER TABLE type_machine_product_requirements DROP CONSTRAINT IF EXISTS "type_machine_product_requirements_typeProductId_fkey"');
$this->addSql('ALTER TABLE type_machine_product_requirements ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE type_machine_product_requirements ALTER label TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE type_machine_product_requirements ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE type_machine_product_requirements ALTER typeMachineId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE type_machine_product_requirements ALTER typeProductId TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE type_machine_product_requirements DROP CONSTRAINT IF EXISTS FK_29A51F982F024C2');
$this->addSql('ALTER TABLE type_machine_product_requirements ADD CONSTRAINT FK_29A51F982F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE type_machine_product_requirements DROP CONSTRAINT IF EXISTS FK_29A51F9857B7763A');
$this->addSql('ALTER TABLE type_machine_product_requirements ADD CONSTRAINT FK_29A51F9857B7763A FOREIGN KEY (typeProductId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER INDEX IF EXISTS idx_29a51f98542108fe RENAME TO IDX_29A51F982F024C2');
$this->addSql('ALTER INDEX IF EXISTS idx_29a51f98e7123582 RENAME TO IDX_29A51F9857B7763A');
$this->addSql('ALTER TABLE type_machines ADD COLUMN IF NOT EXISTS category VARCHAR(255)');
$this->addSql('ALTER TABLE type_machines ADD COLUMN IF NOT EXISTS maintenanceFrequency VARCHAR(255)');
$this->addSql('ALTER TABLE type_machines ADD COLUMN IF NOT EXISTS components JSON');
$this->addSql('ALTER TABLE type_machines ADD COLUMN IF NOT EXISTS criticalParts JSON');
$this->addSql('ALTER TABLE type_machines ADD COLUMN IF NOT EXISTS specifications JSON');
$this->addSql('ALTER TABLE type_machines ADD COLUMN IF NOT EXISTS machinePieces JSON');
$this->addSql('ALTER TABLE type_machines ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE type_machines ALTER name TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE type_machines ALTER category TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE type_machines ALTER maintenanceFrequency TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE type_machines ALTER components TYPE JSON');
$this->addSql('ALTER TABLE type_machines ALTER criticalParts TYPE JSON');
$this->addSql('ALTER TABLE type_machines ALTER specifications TYPE JSON');
$this->addSql('ALTER TABLE type_machines ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE type_machines ALTER machinePieces TYPE JSON');
$this->addSql('ALTER INDEX IF EXISTS type_machines_name_key RENAME TO UNIQ_3C31AA115E237E06');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE "ModelType" (id TEXT NOT NULL, name VARCHAR(120) NOT NULL, code VARCHAR(60) NOT NULL, category VARCHAR NOT NULL, notes TEXT DEFAULT NULL, "createdAt" TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, description TEXT DEFAULT NULL, "componentSkeleton" JSONB DEFAULT NULL, "pieceSkeleton" JSONB DEFAULT NULL, "productSkeleton" JSONB DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS "ModelType_code_key" ON "ModelType" (code)');
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS "ModelType_category_name_key" ON "ModelType" (category, name)');
$this->addSql('CREATE TABLE _prisma_migrations (id VARCHAR(36) NOT NULL, checksum VARCHAR(64) NOT NULL, finished_at TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL, migration_name VARCHAR(255) NOT NULL, logs TEXT DEFAULT NULL, rolled_back_at TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL, started_at TIMESTAMP(0) WITH TIME ZONE DEFAULT \'now()\' NOT NULL, applied_steps_count INT DEFAULT 0 NOT NULL, PRIMARY KEY (id))');
$this->addSql('DROP TABLE model_types');
$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('CREATE UNIQUE INDEX IF NOT EXISTS "_ComposantConstructeurs_AB_unique" ON _ComposantConstructeurs ("A", "B")');
$this->addSql('ALTER INDEX IF EXISTS idx_607601254ad0cf31 RENAME TO "_ComposantConstructeurs_B_index"');
$this->addSql('ALTER INDEX IF EXISTS idx_60760125d3d99e8b RENAME TO IDX_60760125F88A743C');
$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_A_fkey" FOREIGN KEY ("A") REFERENCES machines (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$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('CREATE UNIQUE INDEX IF NOT EXISTS "_MachineConstructeurs_AB_unique" ON _MachineConstructeurs ("A", "B")');
$this->addSql('ALTER INDEX IF EXISTS idx_e6a040cc4ad0cf31 RENAME TO "_MachineConstructeurs_B_index"');
$this->addSql('ALTER INDEX IF EXISTS idx_e6a040ccd3d99e8b RENAME TO IDX_E6A040CCF88A743C');
$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('CREATE UNIQUE INDEX IF NOT EXISTS "_PieceConstructeurs_AB_unique" ON _PieceConstructeurs ("A", "B")');
$this->addSql('ALTER INDEX IF EXISTS idx_e94732e54ad0cf31 RENAME TO "_PieceConstructeurs_B_index"');
$this->addSql('ALTER INDEX IF EXISTS idx_e94732e5d3d99e8b RENAME TO IDX_E94732E5F88A743C');
$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_A_fkey" FOREIGN KEY ("A") REFERENCES products (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE _ProductConstructeurs ADD CONSTRAINT "_ProductConstructeurs_B_fkey" FOREIGN KEY ("B") REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS "_ProductConstructeurs_AB_unique" ON _ProductConstructeurs ("A", "B")');
$this->addSql('ALTER INDEX IF EXISTS idx_cf7403fc4ad0cf31 RENAME TO "_ProductConstructeurs_B_index"');
$this->addSql('ALTER INDEX IF EXISTS idx_cf7403fcd3d99e8b RENAME TO IDX_CF7403FCF88A743C');
$this->addSql('ALTER TABLE composants DROP CONSTRAINT IF EXISTS FK_F95A3199CC8A4CEE');
$this->addSql('ALTER TABLE composants DROP CONSTRAINT IF EXISTS FK_F95A319936799605');
$this->addSql('ALTER TABLE composants ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE composants ALTER name TYPE TEXT');
$this->addSql('ALTER TABLE composants ALTER reference TYPE TEXT');
$this->addSql('ALTER TABLE composants ALTER structure TYPE JSONB');
$this->addSql('ALTER TABLE composants ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE composants ALTER "typeComposantId" TYPE TEXT');
$this->addSql('ALTER TABLE composants ALTER "productId" TYPE TEXT');
$this->addSql('ALTER TABLE composants ADD CONSTRAINT "composants_productId_fkey" FOREIGN KEY ("productId") REFERENCES products (id) ON UPDATE CASCADE ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE composants ADD CONSTRAINT "composants_typeComposantId_fkey" FOREIGN KEY ("typeComposantId") REFERENCES "ModelType" (id) ON UPDATE CASCADE ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER INDEX IF EXISTS uniq_f95a31995e237e06 RENAME TO composants_name_key');
$this->addSql('ALTER INDEX IF EXISTS idx_f95a319936799605 RENAME TO IDX_F95A319921C3CCFC');
$this->addSql('ALTER INDEX IF EXISTS idx_f95a3199cc8a4cee RENAME TO IDX_F95A31999FD7F38F');
$this->addSql('ALTER TABLE constructeurs ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE constructeurs ALTER name TYPE TEXT');
$this->addSql('ALTER TABLE constructeurs ALTER email TYPE TEXT');
$this->addSql('ALTER TABLE constructeurs ALTER phone TYPE TEXT');
$this->addSql('ALTER TABLE constructeurs ALTER "createdAt" SET DEFAULT \'now()\'');
$this->addSql('ALTER INDEX IF EXISTS uniq_cc8d6f55e237e06 RENAME TO constructeurs_name_key');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS FK_6B64D7FF5C4A705F');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS FK_6B64D7FF633EC4FD');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS FK_6B64D7FF345EE564');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS FK_6B64D7FF3C6A9D1');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS FK_6B64D7FF36799605');
$this->addSql('ALTER TABLE custom_field_values ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE custom_field_values ALTER value TYPE TEXT');
$this->addSql('ALTER TABLE custom_field_values ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE custom_field_values ALTER "customFieldId" TYPE TEXT');
$this->addSql('ALTER TABLE custom_field_values ALTER "machineId" TYPE TEXT');
$this->addSql('ALTER TABLE custom_field_values ALTER "composantId" TYPE TEXT');
$this->addSql('ALTER TABLE custom_field_values ALTER "pieceId" TYPE TEXT');
$this->addSql('ALTER TABLE custom_field_values ALTER "productId" TYPE TEXT');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT "custom_field_values_composantId_fkey" FOREIGN KEY ("composantId") REFERENCES composants (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT "custom_field_values_customFieldId_fkey" FOREIGN KEY ("customFieldId") REFERENCES custom_fields (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT "custom_field_values_machineId_fkey" FOREIGN KEY ("machineId") REFERENCES machines (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT "custom_field_values_pieceId_fkey" FOREIGN KEY ("pieceId") REFERENCES pieces (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT "custom_field_values_productId_fkey" FOREIGN KEY ("productId") REFERENCES products (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER INDEX IF EXISTS idx_6b64d7ff345ee564 RENAME TO IDX_6B64D7FFEA0A2B9E');
$this->addSql('ALTER INDEX IF EXISTS idx_6b64d7ff5c4a705f RENAME TO IDX_6B64D7FFDAC15D53');
$this->addSql('ALTER INDEX IF EXISTS idx_6b64d7ff633ec4fd RENAME TO IDX_6B64D7FF92F0F180');
$this->addSql('ALTER INDEX IF EXISTS idx_6b64d7ff3c6a9d1 RENAME TO IDX_6B64D7FFD999EB60');
$this->addSql('ALTER INDEX IF EXISTS idx_6b64d7ff36799605 RENAME TO IDX_6B64D7FF21C3CCFC');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT IF EXISTS FK_4A48378C2F024C2');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT IF EXISTS FK_4A48378CCC8A4CEE');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT IF EXISTS FK_4A48378C169F1CF6');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT IF EXISTS FK_4A48378C57B7763A');
$this->addSql('ALTER TABLE custom_fields ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE custom_fields ALTER name TYPE TEXT');
$this->addSql('ALTER TABLE custom_fields ALTER type TYPE TEXT');
$this->addSql('ALTER TABLE custom_fields ALTER "defaultValue" TYPE TEXT');
$this->addSql('ALTER TABLE custom_fields ALTER options TYPE VARCHAR');
$this->addSql('ALTER TABLE custom_fields ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE custom_fields ALTER "typeMachineId" TYPE TEXT');
$this->addSql('ALTER TABLE custom_fields ALTER "typeComposantId" TYPE TEXT');
$this->addSql('ALTER TABLE custom_fields ALTER "typePieceId" TYPE TEXT');
$this->addSql('ALTER TABLE custom_fields ALTER "typeProductId" TYPE TEXT');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT "custom_fields_typeComposantId_fkey" FOREIGN KEY ("typeComposantId") REFERENCES "ModelType" (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT "custom_fields_typeMachineId_fkey" FOREIGN KEY ("typeMachineId") REFERENCES type_machines (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT "custom_fields_typePieceId_fkey" FOREIGN KEY ("typePieceId") REFERENCES "ModelType" (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT "custom_fields_typeProductId_fkey" FOREIGN KEY ("typeProductId") REFERENCES "ModelType" (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER INDEX IF EXISTS idx_4a48378ccc8a4cee RENAME TO IDX_4A48378C9FD7F38F');
$this->addSql('ALTER INDEX IF EXISTS idx_4a48378c2f024c2 RENAME TO IDX_4A48378C542108FE');
$this->addSql('ALTER INDEX IF EXISTS idx_4a48378c169f1cf6 RENAME TO IDX_4A48378CF429180F');
$this->addSql('ALTER INDEX IF EXISTS idx_4a48378c57b7763a RENAME TO IDX_4A48378CE7123582');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS FK_A2B07288633EC4FD');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS FK_A2B07288345EE564');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS FK_A2B072883C6A9D1');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS FK_A2B0728836799605');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS FK_A2B072886973A4FD');
$this->addSql('ALTER TABLE documents ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE documents ALTER name TYPE TEXT');
$this->addSql('ALTER TABLE documents ALTER filename TYPE TEXT');
$this->addSql('ALTER TABLE documents ALTER "mimeType" TYPE TEXT');
$this->addSql('ALTER TABLE documents ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE documents ALTER "machineId" TYPE TEXT');
$this->addSql('ALTER TABLE documents ALTER "composantId" TYPE TEXT');
$this->addSql('ALTER TABLE documents ALTER "pieceId" TYPE TEXT');
$this->addSql('ALTER TABLE documents ALTER "productId" TYPE TEXT');
$this->addSql('ALTER TABLE documents ALTER "siteId" TYPE TEXT');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT "documents_composantId_fkey" FOREIGN KEY ("composantId") REFERENCES composants (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT "documents_machineId_fkey" FOREIGN KEY ("machineId") REFERENCES machines (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT "documents_pieceId_fkey" FOREIGN KEY ("pieceId") REFERENCES pieces (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT "documents_productId_fkey" FOREIGN KEY ("productId") REFERENCES products (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT "documents_siteId_fkey" FOREIGN KEY ("siteId") REFERENCES sites (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER INDEX IF EXISTS idx_a2b07288345ee564 RENAME TO IDX_A2B07288EA0A2B9E');
$this->addSql('ALTER INDEX IF EXISTS idx_a2b07288633ec4fd RENAME TO IDX_A2B0728892F0F180');
$this->addSql('ALTER INDEX IF EXISTS idx_a2b072883c6a9d1 RENAME TO IDX_A2B07288D999EB60');
$this->addSql('ALTER INDEX IF EXISTS idx_a2b0728836799605 RENAME TO IDX_A2B0728821C3CCFC');
$this->addSql('ALTER INDEX IF EXISTS idx_a2b072886973a4fd RENAME TO IDX_A2B07288871A3650');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT IF EXISTS FK_528EFE19633EC4FD');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT IF EXISTS FK_528EFE19345EE564');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT IF EXISTS FK_528EFE19EF6CF34B');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT IF EXISTS FK_528EFE19C44B383C');
$this->addSql('ALTER TABLE machine_component_links ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE machine_component_links ALTER "nameOverride" TYPE TEXT');
$this->addSql('ALTER TABLE machine_component_links ALTER "referenceOverride" TYPE TEXT');
$this->addSql('ALTER TABLE machine_component_links ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE machine_component_links ALTER "machineId" TYPE TEXT');
$this->addSql('ALTER TABLE machine_component_links ALTER "composantId" TYPE TEXT');
$this->addSql('ALTER TABLE machine_component_links ALTER "parentLinkId" TYPE TEXT');
$this->addSql('ALTER TABLE machine_component_links ALTER "typeMachineComponentRequirementId" TYPE TEXT');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT "machine_component_links_composantId_fkey" FOREIGN KEY ("composantId") REFERENCES composants (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT "machine_component_links_machineId_fkey" FOREIGN KEY ("machineId") REFERENCES machines (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT "machine_component_links_parentLinkId_fkey" FOREIGN KEY ("parentLinkId") REFERENCES machine_component_links (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT "machine_component_links_typeMachineComponentRequirementId_fkey" FOREIGN KEY ("typeMachineComponentRequirementId") REFERENCES type_machine_component_requirements (id) ON UPDATE CASCADE ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER INDEX IF EXISTS idx_528efe19345ee564 RENAME TO IDX_528EFE19EA0A2B9E');
$this->addSql('ALTER INDEX IF EXISTS idx_528efe19633ec4fd RENAME TO IDX_528EFE1992F0F180');
$this->addSql('ALTER INDEX IF EXISTS idx_528efe19ef6cf34b RENAME TO IDX_528EFE191446D9B2');
$this->addSql('ALTER INDEX IF EXISTS idx_528efe19c44b383c RENAME TO IDX_528EFE19BBF9038C');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT IF EXISTS FK_62941615633EC4FD');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT IF EXISTS FK_629416153C6A9D1');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT IF EXISTS FK_62941615EF6CF34B');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT IF EXISTS FK_62941615F957D314');
$this->addSql('ALTER TABLE machine_piece_links ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE machine_piece_links ALTER "nameOverride" TYPE TEXT');
$this->addSql('ALTER TABLE machine_piece_links ALTER "referenceOverride" TYPE TEXT');
$this->addSql('ALTER TABLE machine_piece_links ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE machine_piece_links ALTER "machineId" TYPE TEXT');
$this->addSql('ALTER TABLE machine_piece_links ALTER "pieceId" TYPE TEXT');
$this->addSql('ALTER TABLE machine_piece_links ALTER "parentLinkId" TYPE TEXT');
$this->addSql('ALTER TABLE machine_piece_links ALTER "typeMachinePieceRequirementId" TYPE TEXT');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT "machine_piece_links_machineId_fkey" FOREIGN KEY ("machineId") REFERENCES machines (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT "machine_piece_links_parentLinkId_fkey" FOREIGN KEY ("parentLinkId") REFERENCES machine_component_links (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT "machine_piece_links_pieceId_fkey" FOREIGN KEY ("pieceId") REFERENCES pieces (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT "machine_piece_links_typeMachinePieceRequirementId_fkey" FOREIGN KEY ("typeMachinePieceRequirementId") REFERENCES type_machine_piece_requirements (id) ON UPDATE CASCADE ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER INDEX IF EXISTS idx_62941615633ec4fd RENAME TO IDX_6294161592F0F180');
$this->addSql('ALTER INDEX IF EXISTS idx_62941615ef6cf34b RENAME TO IDX_629416151446D9B2');
$this->addSql('ALTER INDEX IF EXISTS idx_629416153c6a9d1 RENAME TO IDX_62941615D999EB60');
$this->addSql('ALTER INDEX IF EXISTS idx_62941615f957d314 RENAME TO IDX_629416156E0F7201');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS FK_8CC32259633EC4FD');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS FK_8CC3225936799605');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS FK_8CC32259B590B209');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS FK_8CC32259EF6CF34B');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS FK_8CC32259A63AC5DC');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT IF EXISTS FK_8CC32259937A1D7C');
$this->addSql('ALTER TABLE machine_product_links ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE machine_product_links ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE machine_product_links ALTER "machineId" TYPE TEXT');
$this->addSql('ALTER TABLE machine_product_links ALTER "productId" TYPE TEXT');
$this->addSql('ALTER TABLE machine_product_links ALTER "typeMachineProductRequirementId" TYPE TEXT');
$this->addSql('ALTER TABLE machine_product_links ALTER "parentLinkId" TYPE TEXT');
$this->addSql('ALTER TABLE machine_product_links ALTER "parentComponentLinkId" TYPE TEXT');
$this->addSql('ALTER TABLE machine_product_links ALTER "parentPieceLinkId" TYPE TEXT');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT "machine_product_links_machineId_fkey" FOREIGN KEY ("machineId") REFERENCES machines (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT "machine_product_links_parentComponentLinkId_fkey" FOREIGN KEY ("parentComponentLinkId") REFERENCES machine_component_links (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT "machine_product_links_parentLinkId_fkey" FOREIGN KEY ("parentLinkId") REFERENCES machine_product_links (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT "machine_product_links_parentPieceLinkId_fkey" FOREIGN KEY ("parentPieceLinkId") REFERENCES machine_piece_links (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT "machine_product_links_productId_fkey" FOREIGN KEY ("productId") REFERENCES products (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT "machine_product_links_typeMachineProductRequirementId_fkey" FOREIGN KEY ("typeMachineProductRequirementId") REFERENCES type_machine_product_requirements (id) ON UPDATE CASCADE ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER INDEX IF EXISTS idx_8cc32259633ec4fd RENAME TO "machine_product_links_machineId_idx"');
$this->addSql('ALTER INDEX IF EXISTS idx_8cc3225936799605 RENAME TO "machine_product_links_productId_idx"');
$this->addSql('ALTER INDEX IF EXISTS idx_8cc32259a63ac5dc RENAME TO IDX_8CC32259BD5B4086');
$this->addSql('ALTER INDEX IF EXISTS idx_8cc32259ef6cf34b RENAME TO IDX_8CC322591446D9B2');
$this->addSql('ALTER INDEX IF EXISTS idx_8cc32259937a1d7c RENAME TO IDX_8CC32259B1619FA4');
$this->addSql('ALTER INDEX IF EXISTS idx_8cc32259b590b209 RENAME TO IDX_8CC32259187FC99C');
$this->addSql('ALTER TABLE machines DROP CONSTRAINT IF EXISTS FK_F1CE8DED6973A4FD');
$this->addSql('ALTER TABLE machines DROP CONSTRAINT IF EXISTS FK_F1CE8DED2F024C2');
$this->addSql('ALTER TABLE machines ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE machines ALTER name TYPE TEXT');
$this->addSql('ALTER TABLE machines ALTER reference TYPE TEXT');
$this->addSql('ALTER TABLE machines ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE machines ALTER "siteId" TYPE TEXT');
$this->addSql('ALTER TABLE machines ALTER "typeMachineId" TYPE TEXT');
$this->addSql('ALTER TABLE machines ADD CONSTRAINT "machines_siteId_fkey" FOREIGN KEY ("siteId") REFERENCES sites (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE machines ADD CONSTRAINT "machines_typeMachineId_fkey" FOREIGN KEY ("typeMachineId") REFERENCES type_machines (id) ON UPDATE CASCADE ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER INDEX IF EXISTS uniq_f1ce8ded5e237e06 RENAME TO machines_name_key');
$this->addSql('ALTER INDEX IF EXISTS idx_f1ce8ded6973a4fd RENAME TO IDX_F1CE8DED871A3650');
$this->addSql('ALTER INDEX IF EXISTS idx_f1ce8ded2f024c2 RENAME TO IDX_F1CE8DED542108FE');
$this->addSql('ALTER TABLE pieces DROP CONSTRAINT IF EXISTS FK_B92D7472169F1CF6');
$this->addSql('ALTER TABLE pieces DROP CONSTRAINT IF EXISTS FK_B92D747236799605');
$this->addSql('ALTER TABLE pieces ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE pieces ALTER name TYPE TEXT');
$this->addSql('ALTER TABLE pieces ALTER reference TYPE TEXT');
$this->addSql('ALTER TABLE pieces ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE pieces ALTER "typePieceId" TYPE TEXT');
$this->addSql('ALTER TABLE pieces ALTER "productId" TYPE TEXT');
$this->addSql('ALTER TABLE pieces ADD CONSTRAINT "pieces_productId_fkey" FOREIGN KEY ("productId") REFERENCES products (id) ON UPDATE CASCADE ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE pieces ADD CONSTRAINT "pieces_typePieceId_fkey" FOREIGN KEY ("typePieceId") REFERENCES "ModelType" (id) ON UPDATE CASCADE ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER INDEX IF EXISTS uniq_b92d74725e237e06 RENAME TO pieces_name_key');
$this->addSql('ALTER INDEX IF EXISTS idx_b92d747236799605 RENAME TO IDX_B92D747221C3CCFC');
$this->addSql('ALTER INDEX IF EXISTS idx_b92d7472169f1cf6 RENAME TO IDX_B92D7472F429180F');
$this->addSql('ALTER TABLE products DROP CONSTRAINT IF EXISTS FK_B3BA5A5A57B7763A');
$this->addSql('ALTER TABLE products ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE products ALTER name TYPE TEXT');
$this->addSql('ALTER TABLE products ALTER reference TYPE TEXT');
$this->addSql('ALTER TABLE products ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE products ALTER "typeProductId" TYPE TEXT');
$this->addSql('ALTER TABLE products ADD CONSTRAINT "products_typeProductId_fkey" FOREIGN KEY ("typeProductId") REFERENCES "ModelType" (id) ON UPDATE CASCADE ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER INDEX IF EXISTS uniq_b3ba5a5a5e237e06 RENAME TO products_name_key');
$this->addSql('ALTER INDEX IF EXISTS idx_b3ba5a5a57b7763a RENAME TO IDX_B3BA5A5AE7123582');
$this->addSql('DROP INDEX IF EXISTS UNIQ_email');
$this->addSql('ALTER TABLE profiles DROP COLUMN IF EXISTS email');
$this->addSql('ALTER TABLE profiles DROP COLUMN IF EXISTS roles');
$this->addSql('ALTER TABLE profiles DROP COLUMN IF EXISTS password');
$this->addSql('ALTER TABLE profiles ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE profiles ALTER "firstName" TYPE TEXT');
$this->addSql('ALTER TABLE profiles ALTER "lastName" TYPE TEXT');
$this->addSql('ALTER TABLE profiles ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE sites ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE sites ALTER name TYPE TEXT');
$this->addSql('ALTER TABLE sites ALTER "contactName" TYPE TEXT');
$this->addSql('ALTER TABLE sites ALTER "contactPhone" TYPE TEXT');
$this->addSql('ALTER TABLE sites ALTER "contactAddress" TYPE TEXT');
$this->addSql('ALTER TABLE sites ALTER "contactPostalCode" TYPE TEXT');
$this->addSql('ALTER TABLE sites ALTER "contactCity" TYPE TEXT');
$this->addSql('ALTER TABLE sites ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE type_machine_component_requirements DROP CONSTRAINT IF EXISTS FK_969587902F024C2');
$this->addSql('ALTER TABLE type_machine_component_requirements DROP CONSTRAINT IF EXISTS FK_96958790CC8A4CEE');
$this->addSql('ALTER TABLE type_machine_component_requirements ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE type_machine_component_requirements ALTER label TYPE TEXT');
$this->addSql('ALTER TABLE type_machine_component_requirements ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE type_machine_component_requirements ALTER "typeMachineId" TYPE TEXT');
$this->addSql('ALTER TABLE type_machine_component_requirements ALTER "typeComposantId" TYPE TEXT');
$this->addSql('ALTER TABLE type_machine_component_requirements ADD CONSTRAINT "type_machine_component_requirements_typeComposantId_fkey" FOREIGN KEY ("typeComposantId") REFERENCES "ModelType" (id) ON UPDATE CASCADE ON DELETE RESTRICT NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE type_machine_component_requirements ADD CONSTRAINT "type_machine_component_requirements_typeMachineId_fkey" FOREIGN KEY ("typeMachineId") REFERENCES type_machines (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER INDEX IF EXISTS idx_96958790cc8a4cee RENAME TO IDX_969587909FD7F38F');
$this->addSql('ALTER INDEX IF EXISTS idx_969587902f024c2 RENAME TO IDX_96958790542108FE');
$this->addSql('ALTER TABLE type_machine_piece_requirements DROP CONSTRAINT IF EXISTS FK_F609E59E2F024C2');
$this->addSql('ALTER TABLE type_machine_piece_requirements DROP CONSTRAINT IF EXISTS FK_F609E59E169F1CF6');
$this->addSql('ALTER TABLE type_machine_piece_requirements ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE type_machine_piece_requirements ALTER label TYPE TEXT');
$this->addSql('ALTER TABLE type_machine_piece_requirements ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE type_machine_piece_requirements ALTER "typeMachineId" TYPE TEXT');
$this->addSql('ALTER TABLE type_machine_piece_requirements ALTER "typePieceId" TYPE TEXT');
$this->addSql('ALTER TABLE type_machine_piece_requirements ADD CONSTRAINT "type_machine_piece_requirements_typeMachineId_fkey" FOREIGN KEY ("typeMachineId") REFERENCES type_machines (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE type_machine_piece_requirements ADD CONSTRAINT "type_machine_piece_requirements_typePieceId_fkey" FOREIGN KEY ("typePieceId") REFERENCES "ModelType" (id) ON UPDATE CASCADE ON DELETE RESTRICT NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER INDEX IF EXISTS idx_f609e59e2f024c2 RENAME TO IDX_F609E59E542108FE');
$this->addSql('ALTER INDEX IF EXISTS idx_f609e59e169f1cf6 RENAME TO IDX_F609E59EF429180F');
$this->addSql('ALTER TABLE type_machine_product_requirements DROP CONSTRAINT IF EXISTS FK_29A51F982F024C2');
$this->addSql('ALTER TABLE type_machine_product_requirements DROP CONSTRAINT IF EXISTS FK_29A51F9857B7763A');
$this->addSql('ALTER TABLE type_machine_product_requirements ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE type_machine_product_requirements ALTER label TYPE TEXT');
$this->addSql('ALTER TABLE type_machine_product_requirements ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER TABLE type_machine_product_requirements ALTER "typeMachineId" TYPE TEXT');
$this->addSql('ALTER TABLE type_machine_product_requirements ALTER "typeProductId" TYPE TEXT');
$this->addSql('ALTER TABLE type_machine_product_requirements ADD CONSTRAINT "type_machine_product_requirements_typeMachineId_fkey" FOREIGN KEY ("typeMachineId") REFERENCES type_machines (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE type_machine_product_requirements ADD CONSTRAINT "type_machine_product_requirements_typeProductId_fkey" FOREIGN KEY ("typeProductId") REFERENCES "ModelType" (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER INDEX IF EXISTS idx_29a51f982f024c2 RENAME TO IDX_29A51F98542108FE');
$this->addSql('ALTER INDEX IF EXISTS idx_29a51f9857b7763a RENAME TO IDX_29A51F98E7123582');
$this->addSql('ALTER TABLE type_machines ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE type_machines ALTER name TYPE TEXT');
$this->addSql('ALTER TABLE type_machines ALTER category TYPE TEXT');
$this->addSql('ALTER TABLE type_machines ALTER "maintenanceFrequency" TYPE TEXT');
$this->addSql('ALTER TABLE type_machines ALTER components TYPE JSONB');
$this->addSql('ALTER TABLE type_machines ALTER "criticalParts" TYPE JSONB');
$this->addSql('ALTER TABLE type_machines ALTER "machinePieces" TYPE JSONB');
$this->addSql('ALTER TABLE type_machines ALTER specifications TYPE JSONB');
$this->addSql('ALTER TABLE type_machines ALTER "createdAt" SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('ALTER INDEX IF EXISTS uniq_3c31aa115e237e06 RENAME TO type_machines_name_key');
}
}

View 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
);
}
}

View 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');
}
}

View 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)');
}
}

View 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');
}
}

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260304120000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Remove TypeMachine skeleton system, link custom fields directly to machines';
}
public function up(Schema $schema): void
{
// 1. Drop requirement FK columns on link tables
$this->addSql('ALTER TABLE machine_component_links DROP COLUMN IF EXISTS typemachinecomponentrequirementid');
$this->addSql('ALTER TABLE machine_piece_links DROP COLUMN IF EXISTS typemachinepiecerequirementid');
$this->addSql('ALTER TABLE machine_product_links DROP COLUMN IF EXISTS typemachineproductrequirementid');
// 2. Add machineid column to custom_fields (new direct FK to machines)
$this->addSql('ALTER TABLE custom_fields ADD COLUMN IF NOT EXISTS machineid VARCHAR(36) DEFAULT NULL');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'fk_custom_fields_machine' AND table_name = 'custom_fields'
) THEN
ALTER TABLE custom_fields ADD CONSTRAINT fk_custom_fields_machine
FOREIGN KEY (machineid) REFERENCES machines(id) ON DELETE CASCADE;
END IF;
END $$;
SQL);
// 3. Enable pgcrypto for gen_random_bytes (needed for CUID generation)
$this->addSql('CREATE EXTENSION IF NOT EXISTS pgcrypto');
// 4. Migrate existing custom fields: copy from TypeMachine to each linked Machine
$this->addSql(<<<'SQL'
INSERT INTO custom_fields (id, name, type, required, defaultvalue, options, orderindex, machineid, createdat, updatedat)
SELECT
'cl' || encode(gen_random_bytes(12), 'hex'),
cf.name, cf.type, cf.required, cf.defaultvalue, cf.options, cf.orderindex,
m.id,
NOW(), NOW()
FROM custom_fields cf
JOIN machines m ON m.typemachineid = cf.typemachineid
WHERE cf.typemachineid IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM custom_fields existing
WHERE existing.machineid = m.id AND existing.name = cf.name
)
SQL);
// 4. Delete original TypeMachine-linked custom fields (now migrated)
$this->addSql('DELETE FROM custom_fields WHERE typemachineid IS NOT NULL');
// 5. Drop typemachineid column from custom_fields
$this->addSql('ALTER TABLE custom_fields DROP COLUMN IF EXISTS typemachineid');
// 6. Drop typemachineid column from machines
$this->addSql('ALTER TABLE machines DROP COLUMN IF EXISTS typemachineid');
// 7. Drop requirement tables (order matters: these reference type_machines)
$this->addSql('DROP TABLE IF EXISTS type_machine_component_requirements');
$this->addSql('DROP TABLE IF EXISTS type_machine_piece_requirements');
$this->addSql('DROP TABLE IF EXISTS type_machine_product_requirements');
// 8. Drop type_machines table
$this->addSql('DROP TABLE IF EXISTS type_machines');
}
public function down(Schema $schema): void
{
// Recreate type_machines table
$this->addSql(<<<'SQL'
CREATE TABLE IF NOT EXISTS type_machines (
id VARCHAR(36) NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT DEFAULT NULL,
category VARCHAR(255) DEFAULT NULL,
maintenancefrequency VARCHAR(255) DEFAULT NULL,
components JSON DEFAULT NULL,
criticalparts JSON DEFAULT NULL,
machinepieces JSON DEFAULT NULL,
specifications JSON DEFAULT NULL,
createdat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
updatedat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL
)
SQL);
// Recreate requirement tables
$this->addSql(<<<'SQL'
CREATE TABLE IF NOT EXISTS type_machine_component_requirements (
id VARCHAR(36) NOT NULL PRIMARY KEY,
typemachineid VARCHAR(36) NOT NULL REFERENCES type_machines(id) ON DELETE CASCADE,
typecomposantid VARCHAR(36) NOT NULL REFERENCES model_types(id),
label VARCHAR(255) DEFAULT NULL,
mincount INTEGER DEFAULT 1,
maxcount INTEGER DEFAULT NULL,
required BOOLEAN DEFAULT true,
allownewmodels BOOLEAN DEFAULT true,
orderindex INTEGER DEFAULT 0,
createdat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
updatedat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL
)
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE IF NOT EXISTS type_machine_piece_requirements (
id VARCHAR(36) NOT NULL PRIMARY KEY,
typemachineid VARCHAR(36) NOT NULL REFERENCES type_machines(id) ON DELETE CASCADE,
typepieceid VARCHAR(36) NOT NULL REFERENCES model_types(id),
label VARCHAR(255) DEFAULT NULL,
mincount INTEGER DEFAULT 0,
maxcount INTEGER DEFAULT NULL,
required BOOLEAN DEFAULT false,
allownewmodels BOOLEAN DEFAULT true,
orderindex INTEGER DEFAULT 0,
createdat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
updatedat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL
)
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE IF NOT EXISTS type_machine_product_requirements (
id VARCHAR(36) NOT NULL PRIMARY KEY,
typemachineid VARCHAR(36) NOT NULL REFERENCES type_machines(id) ON DELETE CASCADE,
typeproductid VARCHAR(36) NOT NULL REFERENCES model_types(id),
label VARCHAR(255) DEFAULT NULL,
mincount INTEGER DEFAULT 0,
maxcount INTEGER DEFAULT NULL,
required BOOLEAN DEFAULT false,
allownewmodels BOOLEAN DEFAULT true,
orderindex INTEGER DEFAULT 0,
createdat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
updatedat TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL
)
SQL);
// Re-add typemachineid to machines
$this->addSql('ALTER TABLE machines ADD COLUMN IF NOT EXISTS typemachineid VARCHAR(36) DEFAULT NULL');
// Re-add typemachineid to custom_fields
$this->addSql('ALTER TABLE custom_fields ADD COLUMN IF NOT EXISTS typemachineid VARCHAR(36) DEFAULT NULL');
// Re-add requirement FK columns to link tables
$this->addSql('ALTER TABLE machine_component_links ADD COLUMN IF NOT EXISTS typemachinecomponentrequirementid VARCHAR(36) DEFAULT NULL');
$this->addSql('ALTER TABLE machine_piece_links ADD COLUMN IF NOT EXISTS typemachinepiecerequirementid VARCHAR(36) DEFAULT NULL');
$this->addSql('ALTER TABLE machine_product_links ADD COLUMN IF NOT EXISTS typemachineproductrequirementid VARCHAR(36) DEFAULT NULL');
// Drop machine FK on custom_fields
$this->addSql('ALTER TABLE custom_fields DROP COLUMN IF EXISTS machineid');
}
}

View File

@@ -7,21 +7,20 @@ namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260125102000 extends AbstractMigration
final class Version20260309120000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add productIds JSON column to pieces to support multiple product requirements.';
return 'Add color column to sites table';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE pieces ADD productIds JSON DEFAULT NULL');
$this->addSql("ALTER TABLE sites ADD COLUMN IF NOT EXISTS color VARCHAR(7) NOT NULL DEFAULT ''");
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE pieces DROP productIds');
$this->addSql('ALTER TABLE sites DROP COLUMN IF EXISTS color');
}
}

View File

@@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261120120000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add email column to profiles when missing.';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE profiles ADD COLUMN IF NOT EXISTS email VARCHAR(180) DEFAULT NULL');
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS uniq_profiles_email ON profiles (email)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX IF EXISTS uniq_profiles_email');
$this->addSql('ALTER TABLE profiles DROP COLUMN IF EXISTS email');
}
}

View File

@@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261120123000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Ensure profiles.email exists (camelCase schema).';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS email VARCHAR(180) DEFAULT NULL');
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS uniq_profiles_email ON public.profiles (email)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX IF EXISTS uniq_profiles_email');
$this->addSql('ALTER TABLE public.profiles DROP COLUMN IF EXISTS email');
}
}

View File

@@ -1,94 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261120124000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Normalize profiles columns to camelCase (quoted identifiers).';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'firstname'
) THEN
ALTER TABLE public.profiles RENAME COLUMN firstname TO "firstName";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'lastname'
) THEN
ALTER TABLE public.profiles RENAME COLUMN lastname TO "lastName";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'isactive'
) THEN
ALTER TABLE public.profiles RENAME COLUMN isactive TO "isActive";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'createdat'
) THEN
ALTER TABLE public.profiles RENAME COLUMN createdat TO "createdAt";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'updatedat'
) THEN
ALTER TABLE public.profiles RENAME COLUMN updatedat TO "updatedAt";
END IF;
END $$;
SQL);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'firstName'
) THEN
ALTER TABLE public.profiles RENAME COLUMN "firstName" TO firstname;
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'lastName'
) THEN
ALTER TABLE public.profiles RENAME COLUMN "lastName" TO lastname;
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'isActive'
) THEN
ALTER TABLE public.profiles RENAME COLUMN "isActive" TO isactive;
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'createdAt'
) THEN
ALTER TABLE public.profiles RENAME COLUMN "createdAt" TO createdat;
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'updatedAt'
) THEN
ALTER TABLE public.profiles RENAME COLUMN "updatedAt" TO updatedat;
END IF;
END $$;
SQL);
}
}

View File

@@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261120125000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add missing profile auth columns (email, roles, password).';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS email VARCHAR(180) DEFAULT NULL');
$this->addSql('ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS roles JSON DEFAULT \'["ROLE_USER"]\' NOT NULL');
$this->addSql('ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS password VARCHAR(255) DEFAULT NULL');
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS uniq_profiles_email ON public.profiles (email)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX IF EXISTS uniq_profiles_email');
$this->addSql('ALTER TABLE public.profiles DROP COLUMN IF EXISTS password');
$this->addSql('ALTER TABLE public.profiles DROP COLUMN IF EXISTS roles');
$this->addSql('ALTER TABLE public.profiles DROP COLUMN IF EXISTS email');
}
}

View File

@@ -1,66 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261120131000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Normalize public schema identifiers to lowercase (tables + columns).';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
DO $$
DECLARE
r RECORD;
BEGIN
-- Special-case legacy table name from Prisma.
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'ModelType'
) THEN
EXECUTE 'ALTER TABLE public."ModelType" RENAME TO model_types';
END IF;
-- Rename columns containing uppercase letters.
FOR r IN
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'public' AND column_name ~ '[A-Z]'
LOOP
EXECUTE format(
'ALTER TABLE public.%I RENAME COLUMN %I TO %I',
r.table_name,
r.column_name,
lower(r.column_name)
);
END LOOP;
-- Rename tables containing uppercase letters.
FOR r IN
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public' AND table_name ~ '[A-Z]'
LOOP
EXECUTE format(
'ALTER TABLE public.%I RENAME TO %I',
r.table_name,
lower(r.table_name)
);
END LOOP;
END $$;
SQL);
}
public function down(Schema $schema): void
{
// Irreversible: cannot restore original casing reliably.
}
}

View File

@@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261120140000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Convert custom_fields.options from text[] to json.';
}
public function up(Schema $schema): void
{
$this->addSql("ALTER TABLE custom_fields ALTER COLUMN options TYPE JSON USING to_json(options)");
}
public function down(Schema $schema): void
{
$this->addSql("ALTER TABLE custom_fields ALTER COLUMN options TYPE TEXT[] USING ARRAY(SELECT json_array_elements_text(options))");
}
}

View File

@@ -4,7 +4,7 @@
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
failOnDeprecation="true"
failOnDeprecation="false"
failOnNotice="true"
failOnWarning="true"
bootstrap="tests/bootstrap.php"
@@ -40,5 +40,6 @@
</source>
<extensions>
<bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" />
</extensions>
</phpunit>

View File

@@ -128,7 +128,7 @@ if ! git diff --quiet --exit-code || ! git diff --cached --quiet --exit-code; th
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
git add -A
git commit -m "chore(release): prepare v$new_version"
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
@@ -168,7 +168,7 @@ sed -i "s/version: .*/version: $new_version/" "$API_PLATFORM_FILE"
# ===========================================
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"
git commit -m "chore(release) : v$new_version"
# ===========================================
# ÉTAPE 6 : Tag principal

View 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];
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Entity\Profile;
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\InputArgument;
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 Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use function in_array;
#[AsCommand(
name: 'app:create-profile',
description: 'Create a new profile with the given credentials',
)]
class CreateProfileCommand extends Command
{
public function __construct(
private readonly ProfileRepository $profiles,
private readonly EntityManagerInterface $em,
private readonly UserPasswordHasherInterface $passwordHasher,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('firstName', InputArgument::REQUIRED, 'First name')
->addArgument('lastName', InputArgument::REQUIRED, 'Last name')
->addOption('email', null, InputOption::VALUE_REQUIRED, 'Email address')
->addOption('role', null, InputOption::VALUE_REQUIRED, 'Role (ROLE_ADMIN, ROLE_GESTIONNAIRE, ROLE_VIEWER)', 'ROLE_VIEWER')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$firstName = $input->getArgument('firstName');
$lastName = $input->getArgument('lastName');
$email = $input->getOption('email');
$password = $io->askHidden('Password');
if (null === $password || '' === $password) {
$io->error('Le mot de passe est requis.');
return Command::FAILURE;
}
$role = $input->getOption('role');
$allowedRoles = ['ROLE_ADMIN', 'ROLE_GESTIONNAIRE', 'ROLE_VIEWER', 'ROLE_USER'];
if (!in_array($role, $allowedRoles, true)) {
$io->error('Role invalide. Roles autorisés : '.implode(', ', $allowedRoles));
return Command::FAILURE;
}
if (null !== $email && '' !== $email) {
$existing = $this->profiles->findOneBy(['email' => $email]);
if (null !== $existing) {
$io->error('Un profil avec cet email existe déjà.');
return Command::FAILURE;
}
}
$profile = new Profile();
$profile->setFirstName($firstName);
$profile->setLastName($lastName);
$profile->setRoles([$role]);
$profile->setIsActive(true);
if (null !== $email && '' !== $email) {
$profile->setEmail($email);
}
$profile->setPassword(
$this->passwordHasher->hashPassword($profile, $password)
);
$this->em->persist($profile);
$this->em->flush();
$io->success(sprintf(
'Profil créé : %s %s (ID: %s, Role: %s)',
$firstName,
$lastName,
$profile->getId(),
$role,
));
return Command::SUCCESS;
}
}

View 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;
}
}

View 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];
}
}

View 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,
]);
}
}

View 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)
));
}
}

View 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),
];
}
}

View File

@@ -29,12 +29,13 @@ class CustomFieldValueController extends AbstractController
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;
@@ -64,6 +65,8 @@ class CustomFieldValueController extends AbstractController
#[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;
@@ -80,7 +83,7 @@ class CustomFieldValueController extends AbstractController
}
$existing = $this->customFieldValueRepository->findOneBy([
'customField' => $customField,
'customField' => $customField,
$target['type'] => $target['entity'],
]);
@@ -105,9 +108,11 @@ class CustomFieldValueController extends AbstractController
#[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,
'entityId' => $entityId,
]);
if ($target instanceof JsonResponse) {
@@ -127,6 +132,8 @@ class CustomFieldValueController extends AbstractController
#[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);
@@ -149,6 +156,8 @@ class CustomFieldValueController extends AbstractController
#[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);
@@ -173,7 +182,7 @@ class CustomFieldValueController extends AbstractController
private function resolveCustomField(array $payload): CustomField|JsonResponse
{
$customFieldId = isset($payload['customFieldId']) ? trim((string) $payload['customFieldId']) : '';
if ($customFieldId !== '') {
if ('' !== $customFieldId) {
$customField = $this->customFieldRepository->find($customFieldId);
if ($customField instanceof CustomField) {
return $customField;
@@ -183,7 +192,7 @@ class CustomFieldValueController extends AbstractController
}
$customFieldName = isset($payload['customFieldName']) ? trim((string) $payload['customFieldName']) : '';
if ($customFieldName === '') {
if ('' === $customFieldName) {
return $this->json(['success' => false, 'error' => 'customFieldId or customFieldName is required.'], 400);
}
@@ -205,30 +214,31 @@ class CustomFieldValueController extends AbstractController
private function resolveTarget(array $payload): array|JsonResponse
{
$entityType = isset($payload['entityType']) ? strtolower((string) $payload['entityType']) : '';
$entityId = isset($payload['entityId']) ? trim((string) $payload['entityId']) : '';
$entityId = isset($payload['entityId']) ? trim((string) $payload['entityId']) : '';
if ($entityType === '' || $entityId === '') {
if ('' === $entityType || '' === $entityId) {
foreach (['machine', 'composant', 'piece', 'product'] as $candidate) {
$key = $candidate . 'Id';
$key = $candidate.'Id';
if (!isset($payload[$key])) {
continue;
}
$entityType = $candidate;
$entityId = trim((string) $payload[$key]);
$entityId = trim((string) $payload[$key]);
break;
}
}
if ($entityType === '' || $entityId === '') {
if ('' === $entityType || '' === $entityId) {
return $this->json(['success' => false, 'error' => 'Entity target is missing.'], 400);
}
return match ($entityType) {
'machine' => $this->resolveEntity('machine', $entityId, $this->machineRepository),
'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),
'piece' => $this->resolveEntity('piece', $entityId, $this->pieceRepository),
'product' => $this->resolveEntity('product', $entityId, $this->productRepository),
default => $this->json(['success' => false, 'error' => 'Unsupported entity type.'], 400),
};
}
@@ -247,15 +257,22 @@ class CustomFieldValueController extends AbstractController
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;
}
}
@@ -265,23 +282,23 @@ class CustomFieldValueController extends AbstractController
$customField = $value->getCustomField();
return [
'id' => $value->getId(),
'value' => $value->getValue(),
'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(),
'customField' => [
'id' => $customField->getId(),
'name' => $customField->getName(),
'type' => $customField->getType(),
'required' => $customField->isRequired(),
'options' => $customField->getOptions(),
'orderIndex' => $customField->getOrderIndex(),
],
'machineId' => $value->getMachine()?->getId(),
'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),
'pieceId' => $value->getPiece()?->getId(),
'productId' => $value->getProduct()?->getId(),
'createdAt' => $value->getCreatedAt()->format(DATE_ATOM),
'updatedAt' => $value->getUpdatedAt()->format(DATE_ATOM),
];
}
}

View File

@@ -25,12 +25,13 @@ class DocumentQueryController extends AbstractController
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);
@@ -44,6 +45,8 @@ class DocumentQueryController extends AbstractController
#[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);
@@ -57,6 +60,8 @@ class DocumentQueryController extends AbstractController
#[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);
@@ -70,6 +75,8 @@ class DocumentQueryController extends AbstractController
#[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);
@@ -83,6 +90,8 @@ class DocumentQueryController extends AbstractController
#[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);
@@ -100,19 +109,20 @@ class DocumentQueryController extends AbstractController
{
return array_map(static function (Document $document): array {
return [
'id' => $document->getId(),
'name' => $document->getName(),
'filename' => $document->getFilename(),
'path' => $document->getPath(),
'mimeType' => $document->getMimeType(),
'size' => $document->getSize(),
'siteId' => $document->getSite()?->getId(),
'machineId' => $document->getMachine()?->getId(),
'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),
'pieceId' => $document->getPiece()?->getId(),
'productId' => $document->getProduct()?->getId(),
'createdAt' => $document->getCreatedAt()->format(DATE_ATOM),
'updatedAt' => $document->getUpdatedAt()->format(DATE_ATOM),
];
}, $documents);
}

View 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;
}
}

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Repository\AuditLogRepository;
use App\Repository\ComposantRepository;
use App\Repository\MachineRepository;
use App\Repository\PieceRepository;
use App\Repository\ProductRepository;
use App\Repository\ProfileRepository;
use DateTimeInterface;
use Doctrine\ORM\EntityRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class EntityHistoryController extends AbstractController
{
/** @var array<string, array{repo: EntityRepository<object>, label: string}> */
private readonly array $entityConfig;
public function __construct(
MachineRepository $machines,
PieceRepository $pieces,
ComposantRepository $composants,
ProductRepository $products,
private readonly AuditLogRepository $auditLogs,
private readonly ProfileRepository $profiles,
) {
$this->entityConfig = [
'machine' => ['repo' => $machines, 'label' => 'Machine introuvable.'],
'piece' => ['repo' => $pieces, 'label' => 'Pièce introuvable.'],
'composant' => ['repo' => $composants, 'label' => 'Composant introuvable.'],
'product' => ['repo' => $products, 'label' => 'Produit introuvable.'],
];
}
#[Route('/api/machines/{id}/history', name: 'api_machine_history', methods: ['GET'])]
public function machineHistory(string $id): JsonResponse
{
return $this->entityHistory('machine', $id);
}
#[Route('/api/pieces/{id}/history', name: 'api_piece_history', methods: ['GET'])]
public function pieceHistory(string $id): JsonResponse
{
return $this->entityHistory('piece', $id);
}
#[Route('/api/composants/{id}/history', name: 'api_composant_history', methods: ['GET'])]
public function composantHistory(string $id): JsonResponse
{
return $this->entityHistory('composant', $id);
}
#[Route('/api/products/{id}/history', name: 'api_product_history', methods: ['GET'])]
public function productHistory(string $id): JsonResponse
{
return $this->entityHistory('product', $id);
}
private function entityHistory(string $type, string $id): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$config = $this->entityConfig[$type];
$entity = $config['repo']->find($id);
if (!$entity) {
return new JsonResponse(
['message' => $config['label']],
Response::HTTP_NOT_FOUND,
);
}
$logs = $this->auditLogs->findEntityHistory($type, $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),
]);
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\DBAL\Connection;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
use Throwable;
class HealthCheckController extends AbstractController
{
#[Route('/api/health', name: 'api_health', methods: ['GET'])]
public function __invoke(Connection $connection): JsonResponse
{
$dbOk = false;
try {
$start = hrtime(true);
$connection->executeQuery('SELECT 1');
$dbLatency = round((hrtime(true) - $start) / 1e6, 1);
$dbOk = true;
} catch (Throwable) {
$dbLatency = null;
}
$healthy = $dbOk;
$data = ['status' => $healthy ? 'ok' : 'degraded'];
if ($this->isGranted('ROLE_ADMIN')) {
$version = '0.0.0';
$versionFile = $this->getParameter('kernel.project_dir').'/VERSION';
if (file_exists($versionFile)) {
$version = trim(file_get_contents($versionFile));
}
$data += [
'version' => $version,
'timestamp' => new DateTimeImmutable()->format(DateTimeInterface::ATOM),
'php' => PHP_VERSION,
'checks' => [
'database' => [
'status' => $dbOk ? 'ok' : 'down',
'latency_ms' => $dbLatency,
],
],
'memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 1),
];
}
return $this->json($data, $healthy ? 200 : 503);
}
}

View File

@@ -21,28 +21,24 @@ class MachineCustomFieldsController extends AbstractController
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) {
foreach ($machine->getCustomFields() as $customField) {
if (!$customField instanceof CustomField) {
continue;
}
$existing = $this->customFieldValueRepository->findOneBy([
'machine' => $machine,
'machine' => $machine,
'customField' => $customField,
]);
if ($existing instanceof CustomFieldValue) {
@@ -61,12 +57,12 @@ class MachineCustomFieldsController extends AbstractController
$values = $this->customFieldValueRepository->findBy(['machine' => $machine]);
return $this->json([
'success' => true,
'machineId' => $machine->getId(),
'success' => true,
'machineId' => $machine->getId(),
'customFieldValues' => array_map(
static fn (CustomFieldValue $value) => [
'id' => $value->getId(),
'value' => $value->getValue(),
'id' => $value->getId(),
'value' => $value->getValue(),
'customFieldId' => $value->getCustomField()->getId(),
],
$values

View File

@@ -1,756 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Composant;
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
{
$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
{
$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
foreach ($normalizedComponentLinks as &$link) {
$parentId = $link['parentComponentLinkId'] ?? null;
if ($parentId && isset($componentIndex[$parentId])) {
$componentIndex[$parentId]['childLinks'][] = &$link;
}
}
unset($link);
// Add pieces to components recursively
$this->attachPiecesToComponents($componentIndex, $normalizedPieceLinks);
return [
'machine' => $this->normalizeMachine($machine),
'componentLinks' => array_values($componentIndex),
'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 ($name === null && $reference === null && $prix === null) {
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 ($value === null) {
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 ($value === null || $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));
}
}

View File

@@ -0,0 +1,903 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Composant;
use App\Entity\CustomField;
use App\Entity\CustomFieldValue;
use App\Entity\Machine;
use App\Entity\MachineComponentLink;
use App\Entity\MachinePieceLink;
use App\Entity\MachineProductLink;
use App\Entity\ModelType;
use App\Entity\Piece;
use App\Entity\Product;
use App\Entity\Site;
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 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 MachineStructureController 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,
) {}
#[Route('/{id}/structure', name: 'machine_structure_get', methods: ['GET'])]
public function getStructure(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->normalizeStructureResponse(
$machine,
$componentLinks,
$pieceLinks,
$productLinks
));
}
#[Route('/{id}/structure', name: 'machine_structure_update', methods: ['PATCH'])]
public function updateStructure(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->normalizeStructureResponse(
$machine,
$componentLinks,
$pieceLinks,
$productLinks
));
}
#[Route('/{id}/clone', name: 'machine_clone', methods: ['POST'])]
public function cloneMachine(string $id, Request $request): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
$source = $this->machineRepository->find($id);
if (!$source instanceof Machine) {
return $this->json(['success' => false, 'error' => 'Machine source introuvable.'], 404);
}
$payload = json_decode($request->getContent(), true);
if (!is_array($payload) || empty($payload['name']) || empty($payload['siteId'])) {
return $this->json(['success' => false, 'error' => 'name et siteId sont requis.'], 400);
}
$site = $this->entityManager->getRepository(Site::class)->find($payload['siteId']);
if (!$site) {
return $this->json(['success' => false, 'error' => 'Site introuvable.'], 404);
}
// Create new machine
$newMachine = new Machine();
$newMachine->setName($payload['name']);
$newMachine->setSite($site);
if (!empty($payload['reference'])) {
$newMachine->setReference($payload['reference']);
}
$newMachine->setPrix($source->getPrix());
// Copy constructeurs
foreach ($source->getConstructeurs() as $constructeur) {
$newMachine->getConstructeurs()->add($constructeur);
}
$this->entityManager->persist($newMachine);
// Copy custom fields and values
$this->cloneCustomFields($source, $newMachine);
// Copy component links (preserving hierarchy)
$componentLinkMap = $this->cloneComponentLinks($source, $newMachine);
// Copy piece links
$pieceLinkMap = $this->clonePieceLinks($source, $newMachine, $componentLinkMap);
// Copy product links
$this->cloneProductLinks($source, $newMachine, $componentLinkMap, $pieceLinkMap);
$this->entityManager->flush();
$componentLinks = $this->machineComponentLinkRepository->findBy(['machine' => $newMachine]);
$pieceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $newMachine]);
$productLinks = $this->machineProductLinkRepository->findBy(['machine' => $newMachine]);
return $this->json($this->normalizeStructureResponse(
$newMachine,
$componentLinks,
$pieceLinks,
$productLinks
), 201);
}
private function cloneCustomFields(Machine $source, Machine $target): void
{
foreach ($source->getCustomFields() as $cf) {
$newCf = new CustomField();
$newCf->setName($cf->getName());
$newCf->setType($cf->getType());
$newCf->setRequired($cf->isRequired());
$newCf->setDefaultValue($cf->getDefaultValue());
$newCf->setOptions($cf->getOptions());
$newCf->setOrderIndex($cf->getOrderIndex());
$newCf->setMachine($target);
$this->entityManager->persist($newCf);
}
foreach ($source->getCustomFieldValues() as $cfv) {
$newValue = new CustomFieldValue();
$newValue->setMachine($target);
$newValue->setCustomField($cfv->getCustomField());
$newValue->setValue($cfv->getValue());
$this->entityManager->persist($newValue);
}
}
/**
* @return array<string, MachineComponentLink> Map of old link ID → new link
*/
private function cloneComponentLinks(Machine $source, Machine $target): array
{
$sourceLinks = $this->machineComponentLinkRepository->findBy(['machine' => $source]);
$linkMap = [];
// First pass: create all links without parent relationships
foreach ($sourceLinks as $link) {
$newLink = new MachineComponentLink();
$newLink->setMachine($target);
$newLink->setComposant($link->getComposant());
$newLink->setNameOverride($link->getNameOverride());
$newLink->setReferenceOverride($link->getReferenceOverride());
$newLink->setPrixOverride($link->getPrixOverride());
$this->entityManager->persist($newLink);
$linkMap[$link->getId()] = $newLink;
}
// Second pass: set parent relationships
foreach ($sourceLinks as $link) {
$parent = $link->getParentLink();
if ($parent && isset($linkMap[$parent->getId()])) {
$linkMap[$link->getId()]->setParentLink($linkMap[$parent->getId()]);
}
}
return $linkMap;
}
/**
* @param array<string, MachineComponentLink> $componentLinkMap
*
* @return array<string, MachinePieceLink> Map of old link ID → new link
*/
private function clonePieceLinks(Machine $source, Machine $target, array $componentLinkMap): array
{
$sourceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $source]);
$linkMap = [];
foreach ($sourceLinks as $link) {
$newLink = new MachinePieceLink();
$newLink->setMachine($target);
$newLink->setPiece($link->getPiece());
$newLink->setNameOverride($link->getNameOverride());
$newLink->setReferenceOverride($link->getReferenceOverride());
$newLink->setPrixOverride($link->getPrixOverride());
$parent = $link->getParentLink();
if ($parent && isset($componentLinkMap[$parent->getId()])) {
$newLink->setParentLink($componentLinkMap[$parent->getId()]);
}
$this->entityManager->persist($newLink);
$linkMap[$link->getId()] = $newLink;
}
return $linkMap;
}
/**
* @param array<string, MachineComponentLink> $componentLinkMap
* @param array<string, MachinePieceLink> $pieceLinkMap
*/
private function cloneProductLinks(
Machine $source,
Machine $target,
array $componentLinkMap,
array $pieceLinkMap,
): void {
$sourceLinks = $this->machineProductLinkRepository->findBy(['machine' => $source]);
$linkMap = [];
// First pass: create all links
foreach ($sourceLinks as $link) {
$newLink = new MachineProductLink();
$newLink->setMachine($target);
$newLink->setProduct($link->getProduct());
$parentComponent = $link->getParentComponentLink();
if ($parentComponent && isset($componentLinkMap[$parentComponent->getId()])) {
$newLink->setParentComponentLink($componentLinkMap[$parentComponent->getId()]);
}
$parentPiece = $link->getParentPieceLink();
if ($parentPiece && isset($pieceLinkMap[$parentPiece->getId()])) {
$newLink->setParentPieceLink($pieceLinkMap[$parentPiece->getId()]);
}
$this->entityManager->persist($newLink);
$linkMap[$link->getId()] = $newLink;
}
// Second pass: set parent product link relationships
foreach ($sourceLinks as $link) {
$parent = $link->getParentLink();
if ($parent && isset($linkMap[$parent->getId()])) {
$linkMap[$link->getId()]->setParentLink($linkMap[$parent->getId()]);
}
}
}
private function normalizePayloadList(mixed $value): array
{
if (!is_array($value)) {
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.'], 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);
$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 || !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.'], 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);
$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 || !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.'], 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);
$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 normalizeStructureResponse(
Machine $machine,
array $componentLinks,
array $pieceLinks,
array $productLinks,
): array {
$normalizedComponentLinks = $this->normalizeComponentLinks($componentLinks);
$componentIndex = $this->indexNormalizedLinks($normalizedComponentLinks);
$normalizedPieceLinks = $this->normalizePieceLinks($pieceLinks);
$childIds = [];
foreach ($normalizedComponentLinks as $link) {
$parentId = $link['parentComponentLinkId'] ?? null;
if ($parentId && isset($componentIndex[$parentId])) {
$componentIndex[$parentId]['childLinks'][] = $link;
$childIds[$link['id']] = true;
}
}
$this->attachPiecesToComponents($componentIndex, $normalizedPieceLinks);
$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;
}
}
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;
}
}
}
if (!empty($child['childLinks'])) {
$this->attachPiecesToChildComponents($child['childLinks'], $pieceLinks);
}
}
}
private function normalizeMachine(Machine $machine): array
{
$site = $machine->getSite();
return [
'id' => $machine->getId(),
'name' => $machine->getName(),
'reference' => $machine->getReference(),
'prix' => $machine->getPrix(),
'siteId' => $site->getId(),
'site' => [
'id' => $site->getId(),
'name' => $site->getName(),
],
'constructeurs' => $this->normalizeConstructeurs($machine->getConstructeurs()),
'customFields' => $this->normalizeCustomFields($machine->getCustomFields()),
'documents' => null,
'customFieldValues' => $this->normalizeCustomFieldValues($machine->getCustomFieldValues()),
];
}
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();
$parentLink = $link->getParentLink();
return [
'id' => $link->getId(),
'linkId' => $link->getId(),
'machineId' => $link->getMachine()->getId(),
'composantId' => $composant->getId(),
'composant' => $this->normalizeComposant($composant),
'parentLinkId' => $parentLink?->getId(),
'parentComponentLinkId' => $parentLink?->getId(),
'parentComponentId' => $parentLink?->getComposant()->getId(),
'overrides' => $this->normalizeOverrides($link),
'childLinks' => [],
'pieceLinks' => [],
];
}, $links);
}
private function normalizePieceLinks(array $links): array
{
return array_map(function (MachinePieceLink $link): array {
$piece = $link->getPiece();
$parentLink = $link->getParentLink();
return [
'id' => $link->getId(),
'linkId' => $link->getId(),
'machineId' => $link->getMachine()->getId(),
'pieceId' => $piece->getId(),
'piece' => $this->normalizePiece($piece),
'parentLinkId' => $parentLink?->getId(),
'parentComponentLinkId' => $parentLink?->getId(),
'parentComponentId' => $parentLink?->getComposant()->getId(),
'overrides' => $this->normalizeOverrides($link),
];
}, $links);
}
private function normalizeProductLinks(array $links): array
{
return array_map(function (MachineProductLink $link): array {
$product = $link->getProduct();
return [
'id' => $link->getId(),
'linkId' => $link->getId(),
'machineId' => $link->getMachine()->getId(),
'productId' => $product->getId(),
'product' => $this->normalizeProduct($product),
'parentLinkId' => $link->getParentLink()?->getId(),
'parentComponentLinkId' => $link->getParentComponentLink()?->getId(),
'parentPieceLinkId' => $link->getParentPieceLink()?->getId(),
];
}, $links);
}
private function normalizeComposant(Composant $composant): array
{
$type = $composant->getTypeComposant();
return [
'id' => $composant->getId(),
'name' => $composant->getName(),
'reference' => $composant->getReference(),
'prix' => $composant->getPrix(),
'typeComposantId' => $type?->getId(),
'typeComposant' => $this->normalizeModelType($type),
'productId' => $composant->getProduct()?->getId(),
'product' => $composant->getProduct() ? $this->normalizeProduct($composant->getProduct()) : null,
'constructeurs' => $this->normalizeConstructeurs($composant->getConstructeurs()),
'documents' => [],
'customFields' => $type ? $this->normalizeCustomFieldDefinitions($type->getComponentCustomFields()) : [],
'customFieldValues' => $this->normalizeCustomFieldValues($composant->getCustomFieldValues()),
];
}
private function normalizePiece(Piece $piece): array
{
$type = $piece->getTypePiece();
return [
'id' => $piece->getId(),
'name' => $piece->getName(),
'reference' => $piece->getReference(),
'prix' => $piece->getPrix(),
'typePieceId' => $type?->getId(),
'typePiece' => $this->normalizeModelType($type),
'productId' => $piece->getProduct()?->getId(),
'product' => $piece->getProduct() ? $this->normalizeProduct($piece->getProduct()) : null,
'constructeurs' => $this->normalizeConstructeurs($piece->getConstructeurs()),
'documents' => [],
'customFields' => $type ? $this->normalizeCustomFieldDefinitions($type->getPieceCustomFields()) : [],
'customFieldValues' => $this->normalizeCustomFieldValues($piece->getCustomFieldValues()),
];
}
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,
'structure' => $type->getStructure(),
];
}
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 normalizeCustomFieldDefinitions(Collection $customFields): array
{
$items = [];
foreach ($customFields as $cf) {
if (!$cf instanceof CustomField) {
continue;
}
$items[] = [
'id' => $cf->getId(),
'name' => $cf->getName(),
'type' => $cf->getType(),
'required' => $cf->isRequired(),
'options' => $cf->getOptions(),
'defaultValue' => $cf->getDefaultValue(),
'orderIndex' => $cf->getOrderIndex(),
];
}
usort($items, static fn (array $a, array $b) => $a['orderIndex'] <=> $b['orderIndex']);
return $items;
}
private function normalizeCustomFieldValues(Collection $customFieldValues): array
{
$items = [];
foreach ($customFieldValues as $cfv) {
if (!$cfv instanceof CustomFieldValue) {
continue;
}
$cf = $cfv->getCustomField();
$items[] = [
'id' => $cfv->getId(),
'value' => $cfv->getValue(),
'customField' => [
'id' => $cf->getId(),
'name' => $cf->getName(),
'type' => $cf->getType(),
'required' => $cf->isRequired(),
'options' => $cf->getOptions(),
'defaultValue' => $cf->getDefaultValue(),
'orderIndex' => $cf->getOrderIndex(),
],
];
}
return $items;
}
private function normalizeOverrides(object $link): ?array
{
$name = method_exists($link, 'getNameOverride') ? $link->getNameOverride() : null;
$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));
}
}

View 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);
}
}

View File

@@ -8,13 +8,15 @@ 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)
{
}
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
@@ -32,16 +34,17 @@ final class SessionProfileController
$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(),
'id' => $profile->getId(),
'firstName' => $profile->getFirstName(),
'lastName' => $profile->getLastName(),
'email' => $profile->getEmail(),
'isActive' => $profile->isActive(),
'roles' => $profile->getRoles(),
'lastName' => $profile->getLastName(),
'email' => $profile->getEmail(),
'isActive' => $profile->isActive(),
'roles' => $profile->getRoles(),
]);
}
@@ -53,7 +56,7 @@ final class SessionProfileController
return new JsonResponse(['message' => 'Session indisponible.'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR);
}
$payload = $request->toArray();
$payload = $request->toArray();
$profileId = $payload['profileId'] ?? null;
if (!$profileId) {
@@ -65,15 +68,33 @@ final class SessionProfileController
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->migrate(true);
$session->set('profileId', $profile->getId());
$session->set('profileRoles', $profile->getRoles());
return new JsonResponse([
'id' => $profile->getId(),
'id' => $profile->getId(),
'firstName' => $profile->getFirstName(),
'lastName' => $profile->getLastName(),
'email' => $profile->getEmail(),
'isActive' => $profile->isActive(),
'roles' => $profile->getRoles(),
'lastName' => $profile->getLastName(),
'email' => $profile->getEmail(),
'isActive' => $profile->isActive(),
'roles' => $profile->getRoles(),
]);
}

View File

@@ -4,20 +4,15 @@ declare(strict_types=1);
namespace App\Controller;
use App\Entity\Profile;
use App\Repository\ProfileRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
final class SessionProfilesController
{
public function __construct(
private readonly ProfileRepository $profiles,
private readonly EntityManagerInterface $entityManager
) {
}
) {}
#[Route('/api/session/profiles', name: 'api_session_profiles_list', methods: ['GET'])]
public function list(): JsonResponse
@@ -27,54 +22,16 @@ final class SessionProfilesController
->setParameter('active', true)
->orderBy('p.firstName', 'ASC')
->getQuery()
->getResult();
->getResult()
;
return new JsonResponse(array_map([$this, 'serializeProfile'], $items));
}
#[Route('/api/session/profiles', name: 'api_session_profiles_create', methods: ['POST'])]
public function create(Request $request): JsonResponse
{
$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);
}
$profile = new Profile();
$profile->setFirstName($firstName);
$profile->setLastName($lastName);
$profile->setIsActive(true);
$this->entityManager->persist($profile);
$this->entityManager->flush();
return new JsonResponse($this->serializeProfile($profile), JsonResponse::HTTP_CREATED);
}
#[Route('/api/session/profiles/{id}', name: 'api_session_profiles_delete', methods: ['DELETE'])]
public function delete(string $id): JsonResponse
{
$profile = $this->profiles->find($id);
if (!$profile) {
return new JsonResponse(['message' => 'Profil introuvable.'], JsonResponse::HTTP_NOT_FOUND);
}
$profile->setIsActive(false);
$this->entityManager->flush();
return new JsonResponse(['success' => true]);
}
private function serializeProfile(Profile $profile): array
{
return [
'id' => $profile->getId(),
'firstName' => $profile->getFirstName(),
'lastName' => $profile->getLastName(),
'isActive' => $profile->isActive(),
];
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));
}
}

View File

@@ -1,18 +0,0 @@
<?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!']);
}
}

View File

@@ -33,8 +33,8 @@ final class AlwaysQuoteStrategy implements QuoteStrategy
{
$tableName = $platform->quoteSingleIdentifier($class->table['name']);
if (! empty($class->table['schema'])) {
return $platform->quoteSingleIdentifier($class->table['schema']) . '.' . $tableName;
if (!empty($class->table['schema'])) {
return $platform->quoteSingleIdentifier($class->table['schema']).'.'.$tableName;
}
return $tableName;
@@ -56,10 +56,10 @@ final class AlwaysQuoteStrategy implements QuoteStrategy
$schema = '';
if (isset($association->joinTable->schema)) {
$schema = $platform->quoteSingleIdentifier($association->joinTable->schema) . '.';
$schema = $platform->quoteSingleIdentifier($association->joinTable->schema).'.';
}
return $schema . $platform->quoteSingleIdentifier($association->joinTable->name);
return $schema.$platform->quoteSingleIdentifier($association->joinTable->name);
}
public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string
@@ -82,12 +82,13 @@ final class AlwaysQuoteStrategy implements QuoteStrategy
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;
$joinColumns = $assoc->joinColumns;
$assocQuotedColumnNames = array_map(
static fn (JoinColumnMapping $joinColumn) => $platform->quoteSingleIdentifier($joinColumn->name),
$joinColumns,
@@ -103,8 +104,8 @@ final class AlwaysQuoteStrategy implements QuoteStrategy
string $columnName,
int $counter,
AbstractPlatform $platform,
ClassMetadata|null $class = null,
?ClassMetadata $class = null,
): string {
return $this->getSQLResultCasing($platform, $columnName . '_' . $counter);
return $this->getSQLResultCasing($platform, $columnName.'_'.$counter);
}
}

117
src/Entity/AuditLog.php Normal file
View 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);
}
}

207
src/Entity/Comment.php Normal file
View File

@@ -0,0 +1,207 @@
<?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 App\Entity\Trait\CuidEntityTrait;
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', 'entityName' => 'ipartial'])]
#[ApiFilter(OrderFilter::class, properties: ['createdAt', 'authorName', 'status'])]
#[ApiResource(
description: 'Commentaires et annotations. Permet aux utilisateurs de commenter les machines, pièces, composants, produits et catégories. Les commentaires peuvent être marqués comme résolus.',
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
{
use CuidEntityTrait;
#[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;
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
}
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;
}
}

View File

@@ -8,6 +8,13 @@ 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\Entity\Trait\CuidEntityTrait;
use App\Repository\ComposantRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
@@ -19,28 +26,43 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: ComposantRepository::class)]
#[ORM\Table(name: 'composants')]
#[ORM\HasLifecycleCallbacks]
#[ApiFilter(SearchFilter::class, properties: ['name' => 'partial', 'reference' => 'partial', 'typeComposant' => 'exact'])]
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeComposant' => 'exact', 'typeComposant.name' => 'ipartial'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[ApiResource(
description: 'Composants du catalogue. Un composant représente un élément fonctionnel rattaché à une machine, avec un type, des fournisseurs et des documents.',
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: 500
paginationMaximumItemsPerPage: 200
)]
class Composant
{
use CuidEntityTrait;
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['composant:read'])]
#[Groups(['composant:read', 'document:list'])]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
#[Groups(['composant:read'])]
#[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;
@@ -101,42 +123,14 @@ class Composant
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
$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;
@@ -144,7 +138,7 @@ class Composant
public function setName(string $name): static
{
$this->name = $name;
$this->name = mb_strtoupper(mb_substr($name, 0, 1)).mb_substr($name, 1);
return $this;
}
@@ -161,6 +155,18 @@ class Composant
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;
@@ -264,19 +270,4 @@ class Composant
{
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));
}
}

View File

@@ -5,28 +5,50 @@ 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\Entity\Trait\CuidEntityTrait;
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;
use Symfony\Component\Validator\Constraints as Assert;
#[UniqueEntity(fields: ['name'], message: 'Un fournisseur avec ce nom existe déjà.')]
#[ORM\Entity(repositoryClass: ConstructeurRepository::class)]
#[ORM\Table(name: 'constructeurs')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource(
description: 'Fournisseurs et constructeurs. Référentiel partagé entre les machines, pièces, composants et produits pour identifier les fabricants et distributeurs.',
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: 500
paginationMaximumItemsPerPage: 200
)]
class Constructeur
{
use CuidEntityTrait;
#[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;
#[Assert\NotBlank(message: 'Le nom est obligatoire.')]
private ?string $name = null;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
private ?string $email = null;
@@ -66,43 +88,15 @@ class Constructeur
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
$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
public function getName(): ?string
{
return $this->name;
}
@@ -137,19 +131,4 @@ class Constructeur
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));
}
}

View File

@@ -5,43 +5,70 @@ 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\Entity\Trait\CuidEntityTrait;
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]
#[ApiResource(
description: 'Définitions de champs personnalisés. Permet de créer des champs dynamiques (texte, nombre, date, etc.) applicables aux machines, pièces, composants et produits.',
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
{
use CuidEntityTrait;
#[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: Machine::class, inversedBy: 'customFields')]
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
private ?Machine $machine = null;
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'customFields')]
#[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
@@ -62,51 +89,18 @@ class CustomField
private Collection $customFieldValues;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private \DateTimeImmutable $createdAt;
private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
private \DateTimeImmutable $updatedAt;
private DateTimeImmutable $updatedAt;
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
$this->customFieldValues = new ArrayCollection();
}
#[ORM\PrePersist]
public function setCreatedAtValue(): void
{
$now = new \DateTimeImmutable();
$this->createdAt = $now;
$this->updatedAt = $now;
if ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
@@ -179,25 +173,15 @@ class CustomField
return $this;
}
public function getTypeMachine(): ?TypeMachine
public function getMachine(): ?Machine
{
return $this->typeMachine;
return $this->machine;
}
public function setTypeMachine(?TypeMachine $typeMachine): static
public function setMachine(?Machine $machine): static
{
$this->typeMachine = $typeMachine;
$this->machine = $machine;
return $this;
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): \DateTimeImmutable
{
return $this->updatedAt;
}
}

View File

@@ -5,25 +5,49 @@ 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\Entity\Trait\CuidEntityTrait;
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]
#[ApiResource(
description: 'Valeurs des champs personnalisés. Stocke la valeur concrète d\'un champ personnalisé pour une entité donnée (machine, pièce, composant ou produit).',
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
{
use CuidEntityTrait;
#[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')]
@@ -43,44 +67,17 @@ class CustomFieldValue
private ?Product $product = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private \DateTimeImmutable $createdAt;
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
private \DateTimeImmutable $updatedAt;
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
private DateTimeImmutable $updatedAt;
#[ORM\PrePersist]
public function setCreatedAtValue(): void
public function __construct()
{
$now = new \DateTimeImmutable();
$this->createdAt = $now;
$this->updatedAt = $now;
if ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
public function getId(): ?string
{
return $this->id;
}
public function setId(string $id): static
{
$this->id = $id;
return $this;
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
}
public function getValue(): string
@@ -154,14 +151,4 @@ class CustomFieldValue
return $this;
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): \DateTimeImmutable
{
return $this->updatedAt;
}
}

View File

@@ -4,8 +4,20 @@ 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\Entity\Trait\CuidEntityTrait;
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;
@@ -13,93 +25,97 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: DocumentRepository::class)]
#[ORM\Table(name: 'documents')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'filename' => 'ipartial'])]
#[ApiFilter(ExistsFilter::class, properties: ['site', 'machine', 'composant', 'piece', 'product'])]
#[ApiFilter(OrderFilter::class, properties: ['createdAt', 'name', 'size'])]
#[ApiResource(
description: 'Documents et fichiers. Gestion des fichiers joints (PDF, images, etc.) rattachés aux machines, pièces, composants, produits ou sites. Upload via multipart/form-data.',
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
{
use CuidEntityTrait;
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $name;
#[ORM\Column(type: Types::STRING, length: 255)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $filename;
#[ORM\Column(type: Types::TEXT)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $path;
#[ORM\Column(type: Types::STRING, length: 100, name: 'mimeType')]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $mimeType;
#[ORM\Column(type: Types::INTEGER)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
#[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')]
private \DateTimeImmutable $createdAt;
#[Groups(['document:list'])]
private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
private \DateTimeImmutable $updatedAt;
private DateTimeImmutable $updatedAt;
#[ORM\PrePersist]
public function setCreatedAtValue(): void
public function __construct()
{
$now = new \DateTimeImmutable();
$this->createdAt = $now;
$this->updatedAt = $now;
if ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
public function getId(): ?string
{
return $this->id;
}
public function setId(string $id): static
{
$this->id = $id;
return $this;
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
}
public function getName(): string
@@ -221,14 +237,4 @@ class Document
return $this;
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): \DateTimeImmutable
{
return $this->updatedAt;
}
}

View File

@@ -5,23 +5,46 @@ 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\Entity\Trait\CuidEntityTrait;
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;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: MachineRepository::class)]
#[ORM\Table(name: 'machines')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource]
#[ApiResource(
description: 'Machines industrielles rattachées à un site. Chaque machine possède une structure hiérarchique de composants, pièces et produits, ainsi que des champs personnalisés et des documents.',
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
{
use CuidEntityTrait;
#[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)]
@@ -32,11 +55,8 @@ class Machine
#[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;
#[Assert\NotNull(message: 'Le site est obligatoire.')]
private ?Site $site = null;
/**
* @var Collection<int, Constructeur>
@@ -73,6 +93,12 @@ class Machine
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: Document::class)]
private Collection $documents;
/**
* @var Collection<int, CustomField>
*/
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: CustomField::class, cascade: ['persist', 'remove'])]
private Collection $customFields;
/**
* @var Collection<int, CustomFieldValue>
*/
@@ -80,56 +106,25 @@ class Machine
private Collection $customFieldValues;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private \DateTimeImmutable $createdAt;
private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
private \DateTimeImmutable $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();
$now = new DateTimeImmutable();
$this->createdAt = $now;
$this->updatedAt = $now;
$this->constructeurs = new ArrayCollection();
$this->componentLinks = new ArrayCollection();
$this->pieceLinks = new ArrayCollection();
$this->productLinks = new ArrayCollection();
$this->documents = new ArrayCollection();
$this->customFields = new ArrayCollection();
$this->customFieldValues = new ArrayCollection();
}
#[ORM\PrePersist]
public function setCreatedAtValue(): void
{
$now = new \DateTimeImmutable();
$this->createdAt = $now;
$this->updatedAt = $now;
if ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
@@ -166,26 +161,43 @@ class Machine
return $this;
}
public function getSite(): Site
public function getSite(): ?Site
{
return $this->site;
}
public function setSite(Site $site): static
public function setSite(?Site $site): static
{
$this->site = $site;
return $this;
}
public function getTypeMachine(): ?TypeMachine
/**
* @return Collection<int, CustomField>
*/
public function getCustomFields(): Collection
{
return $this->typeMachine;
return $this->customFields;
}
public function setTypeMachine(?TypeMachine $typeMachine): static
public function addCustomField(CustomField $customField): static
{
$this->typeMachine = $typeMachine;
if (!$this->customFields->contains($customField)) {
$this->customFields->add($customField);
$customField->setMachine($this);
}
return $this;
}
public function removeCustomField(CustomField $customField): static
{
if ($this->customFields->removeElement($customField)) {
if ($customField->getMachine() === $this) {
$customField->setMachine(null);
}
}
return $this;
}
@@ -198,6 +210,22 @@ class Machine
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, MachineComponentLink>
*/
@@ -237,14 +265,4 @@ class Machine
{
return $this->customFieldValues;
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): \DateTimeImmutable
{
return $this->updatedAt;
}
}

View File

@@ -5,7 +5,15 @@ 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\Entity\Trait\CuidEntityTrait;
use App\Repository\MachineComponentLinkRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
@@ -14,9 +22,21 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MachineComponentLinkRepository::class)]
#[ORM\Table(name: 'machine_component_links')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource]
#[ApiResource(
description: 'Liaisons machinecomposant. Représente le rattachement d\'un composant à une machine, avec quantité, position dans la hiérarchie et surcharges éventuelles (nom, référence).',
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
{
use CuidEntityTrait;
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
private ?string $id = null;
@@ -39,10 +59,6 @@ class MachineComponentLink
#[ORM\OneToMany(mappedBy: 'parentLink', targetEntity: MachineComponentLink::class)]
private Collection $childLinks;
#[ORM\ManyToOne(targetEntity: TypeMachineComponentRequirement::class, inversedBy: 'machineComponentLinks')]
#[ORM\JoinColumn(name: 'typeMachineComponentRequirementId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
private ?TypeMachineComponentRequirement $typeMachineComponentRequirement = null;
/**
* @var Collection<int, MachinePieceLink>
*/
@@ -65,53 +81,20 @@ class MachineComponentLink
private ?string $prixOverride = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private \DateTimeImmutable $createdAt;
private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
private \DateTimeImmutable $updatedAt;
private DateTimeImmutable $updatedAt;
public function __construct()
{
$this->childLinks = new ArrayCollection();
$this->pieceLinks = new ArrayCollection();
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
$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 ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
@@ -148,18 +131,6 @@ class MachineComponentLink
return $this;
}
public function getTypeMachineComponentRequirement(): ?TypeMachineComponentRequirement
{
return $this->typeMachineComponentRequirement;
}
public function setTypeMachineComponentRequirement(?TypeMachineComponentRequirement $requirement): static
{
$this->typeMachineComponentRequirement = $requirement;
return $this;
}
public function getNameOverride(): ?string
{
return $this->nameOverride;

View File

@@ -5,7 +5,15 @@ 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\Entity\Trait\CuidEntityTrait;
use App\Repository\MachinePieceLinkRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
@@ -14,9 +22,21 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MachinePieceLinkRepository::class)]
#[ORM\Table(name: 'machine_piece_links')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource]
#[ApiResource(
description: 'Liaisons machinepièce. Représente le rattachement d\'une pièce à une machine, avec quantité, position dans la hiérarchie et surcharges éventuelles (nom, référence, prix).',
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
{
use CuidEntityTrait;
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
private ?string $id = null;
@@ -33,10 +53,6 @@ class MachinePieceLink
#[ORM\JoinColumn(name: 'parentLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
private ?MachineComponentLink $parentLink = null;
#[ORM\ManyToOne(targetEntity: TypeMachinePieceRequirement::class, inversedBy: 'machinePieceLinks')]
#[ORM\JoinColumn(name: 'typeMachinePieceRequirementId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
private ?TypeMachinePieceRequirement $typeMachinePieceRequirement = null;
/**
* @var Collection<int, MachineProductLink>
*/
@@ -53,51 +69,18 @@ class MachinePieceLink
private ?string $prixOverride = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private \DateTimeImmutable $createdAt;
private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
private \DateTimeImmutable $updatedAt;
private DateTimeImmutable $updatedAt;
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
$this->productLinks = new ArrayCollection();
}
#[ORM\PrePersist]
public function setCreatedAtValue(): void
{
$now = new \DateTimeImmutable();
$this->createdAt = $now;
$this->updatedAt = $now;
if ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
@@ -134,18 +117,6 @@ class MachinePieceLink
return $this;
}
public function getTypeMachinePieceRequirement(): ?TypeMachinePieceRequirement
{
return $this->typeMachinePieceRequirement;
}
public function setTypeMachinePieceRequirement(?TypeMachinePieceRequirement $requirement): static
{
$this->typeMachinePieceRequirement = $requirement;
return $this;
}
public function getNameOverride(): ?string
{
return $this->nameOverride;

View File

@@ -5,7 +5,15 @@ 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\Entity\Trait\CuidEntityTrait;
use App\Repository\MachineProductLinkRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
@@ -14,9 +22,21 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MachineProductLinkRepository::class)]
#[ORM\Table(name: 'machine_product_links')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource]
#[ApiResource(
description: 'Liaisons machineproduit. Représente le rattachement d\'un produit à une machine, avec quantité, position dans la hiérarchie et surcharges éventuelles (nom, référence, prix).',
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
{
use CuidEntityTrait;
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
private ?string $id = null;
@@ -29,10 +49,6 @@ class MachineProductLink
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
private Product $product;
#[ORM\ManyToOne(targetEntity: TypeMachineProductRequirement::class, inversedBy: 'machineProductLinks')]
#[ORM\JoinColumn(name: 'typeMachineProductRequirementId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
private ?TypeMachineProductRequirement $typeMachineProductRequirement = null;
#[ORM\ManyToOne(targetEntity: MachineProductLink::class, inversedBy: 'childLinks')]
#[ORM\JoinColumn(name: 'parentLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
private ?MachineProductLink $parentLink = null;
@@ -52,51 +68,18 @@ class MachineProductLink
private ?MachinePieceLink $parentPieceLink = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private \DateTimeImmutable $createdAt;
private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
private \DateTimeImmutable $updatedAt;
private DateTimeImmutable $updatedAt;
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
$this->childLinks = new ArrayCollection();
}
#[ORM\PrePersist]
public function setCreatedAtValue(): void
{
$now = new \DateTimeImmutable();
$this->createdAt = $now;
$this->updatedAt = $now;
if ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
@@ -121,18 +104,6 @@ class MachineProductLink
return $this;
}
public function getTypeMachineProductRequirement(): ?TypeMachineProductRequirement
{
return $this->typeMachineProductRequirement;
}
public function setTypeMachineProductRequirement(?TypeMachineProductRequirement $requirement): static
{
$this->typeMachineProductRequirement = $requirement;
return $this;
}
public function getParentLink(): ?MachineProductLink
{
return $this->parentLink;

View File

@@ -4,9 +4,17 @@ 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\Entity\Trait\CuidEntityTrait;
use App\Enum\ModelCategory;
use App\Repository\ModelTypeRepository;
use DateTimeImmutable;
@@ -14,26 +22,38 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: ModelTypeRepository::class)]
#[ORM\Table(name: 'model_types')]
#[ORM\UniqueConstraint(name: 'unique_category_name', columns: ['category', 'name'])]
#[ORM\HasLifecycleCallbacks]
#[ApiFilter(SearchFilter::class, properties: ['category' => 'exact'])]
#[ApiFilter(SearchFilter::class, properties: ['category' => 'exact', 'name' => 'ipartial'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[ApiResource(
description: 'Types et catégories. Référentiel de classification pour les machines, pièces, composants et produits. Chaque type appartient à une catégorie (machine, piece, composant, product) et peut être converti.',
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: 500
paginationMaximumItemsPerPage: 200
)]
class ModelType
{
use CuidEntityTrait;
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['type_machine:read', 'model_type:read'])]
#[Groups(['type_machine:read', 'model_type:read', 'product:read', 'composant:read', 'piece:read'])]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 120)]
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write', 'product:read', 'composant:read', 'piece:read'])]
private string $name;
#[ORM\Column(type: Types::STRING, length: 60, unique: true)]
@@ -53,15 +73,15 @@ class ModelType
private ?string $description = null;
#[ORM\Column(type: Types::JSON, nullable: true, name: 'componentSkeleton')]
#[Groups(['model_type:read'])]
#[Groups(['model_type:read', 'composant:read'])]
private ?array $componentSkeleton = null;
#[ORM\Column(type: Types::JSON, nullable: true, name: 'pieceSkeleton')]
#[Groups(['model_type:read'])]
#[Groups(['model_type:read', 'piece:read'])]
private ?array $pieceSkeleton = null;
#[ORM\Column(type: Types::JSON, nullable: true, name: 'productSkeleton')]
#[Groups(['model_type:read'])]
#[Groups(['model_type:read', 'product:read'])]
private ?array $productSkeleton = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
@@ -92,24 +112,6 @@ class ModelType
#[ORM\OneToMany(mappedBy: 'typeProduct', targetEntity: Product::class)]
private Collection $products;
/**
* @var Collection<int, TypeMachineComponentRequirement>
*/
#[ORM\OneToMany(mappedBy: 'typeComposant', targetEntity: TypeMachineComponentRequirement::class)]
private Collection $componentRequirements;
/**
* @var Collection<int, TypeMachinePieceRequirement>
*/
#[ORM\OneToMany(mappedBy: 'typePiece', targetEntity: TypeMachinePieceRequirement::class)]
private Collection $pieceRequirements;
/**
* @var Collection<int, TypeMachineProductRequirement>
*/
#[ORM\OneToMany(mappedBy: 'typeProduct', targetEntity: TypeMachineProductRequirement::class)]
private Collection $productRequirements;
/**
* @var Collection<int, CustomField>
*/
@@ -130,45 +132,14 @@ class ModelType
public function __construct()
{
$this->composants = new ArrayCollection();
$this->pieces = new ArrayCollection();
$this->products = new ArrayCollection();
$this->componentRequirements = new ArrayCollection();
$this->pieceRequirements = new ArrayCollection();
$this->productRequirements = new ArrayCollection();
$this->customFields = new ArrayCollection();
$this->pieceCustomFields = new ArrayCollection();
$this->productCustomFields = new ArrayCollection();
}
#[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;
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
$this->composants = new ArrayCollection();
$this->pieces = new ArrayCollection();
$this->products = new ArrayCollection();
$this->customFields = new ArrayCollection();
$this->pieceCustomFields = new ArrayCollection();
$this->productCustomFields = new ArrayCollection();
}
public function getName(): string
@@ -178,7 +149,7 @@ class ModelType
public function setName(string $name): static
{
$this->name = $name;
$this->name = mb_strtoupper(mb_substr($name, 0, 1)).mb_substr($name, 1);
return $this;
}
@@ -272,7 +243,7 @@ class ModelType
return $this;
}
#[Groups(['model_type:read'])]
#[Groups(['model_type:read', 'product:read', 'composant:read', 'piece:read'])]
public function getStructure(): ?array
{
return match ($this->category) {
@@ -296,19 +267,28 @@ class ModelType
return $this;
}
public function getCreatedAt(): DateTimeImmutable
/**
* @return Collection<int, CustomField>
*/
public function getComponentCustomFields(): Collection
{
return $this->createdAt;
return $this->customFields;
}
public function getUpdatedAt(): DateTimeImmutable
/**
* @return Collection<int, CustomField>
*/
public function getPieceCustomFields(): Collection
{
return $this->updatedAt;
return $this->pieceCustomFields;
}
private function generateCuid(): string
/**
* @return Collection<int, CustomField>
*/
public function getProductCustomFields(): Collection
{
return 'cl'.bin2hex(random_bytes(12));
return $this->productCustomFields;
}
private function applyStructureForCategory(?array $structure, ModelCategory $category): void

View File

@@ -8,39 +8,63 @@ 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\Entity\Trait\CuidEntityTrait;
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' => 'partial', 'reference' => 'partial', 'typePiece' => 'exact'])]
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typePiece' => 'exact', 'typePiece.name' => 'ipartial'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[ApiResource(
description: 'Pièces détachées du catalogue. Une pièce peut être rattachée à plusieurs machines et possède un type, des fournisseurs, des documents et un produit associé.',
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: 500
paginationMaximumItemsPerPage: 200
)]
class Piece
{
use CuidEntityTrait;
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['piece:read'])]
#[Groups(['piece:read', 'document:list'])]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
#[Groups(['piece:read'])]
#[ORM\Column(type: Types::STRING, length: 255)]
#[Groups(['piece:read', 'document:list'])]
private string $name;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
#[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;
@@ -101,42 +125,14 @@ class Piece
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
$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;
@@ -161,6 +157,18 @@ class Piece
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;
@@ -195,7 +203,7 @@ class Piece
$this->product = $product;
if ($product && empty($this->productIds)) {
$productId = $product->getId();
$productId = $product->getId();
$this->productIds = $productId ? [$productId] : null;
}
@@ -221,7 +229,7 @@ class Piece
static fn ($value) => is_string($value) ? trim($value) : '',
$this->productIds,
),
static fn (string $value) => $value !== '',
static fn (string $value) => '' !== $value,
),
);
}
@@ -241,12 +249,12 @@ class Piece
static fn ($value) => is_string($value) ? trim($value) : '',
$productIds,
),
static fn (string $value) => $value !== '',
static fn (string $value) => '' !== $value,
),
),
);
$this->productIds = $normalized === [] ? null : $normalized;
$this->productIds = [] === $normalized ? null : $normalized;
return $this;
}
@@ -290,19 +298,4 @@ class Piece
{
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));
}
}

View File

@@ -8,6 +8,13 @@ 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\Entity\Trait\CuidEntityTrait;
use App\Repository\ProductRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
@@ -19,22 +26,33 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: ProductRepository::class)]
#[ORM\Table(name: 'products')]
#[ORM\HasLifecycleCallbacks]
#[ApiFilter(SearchFilter::class, properties: ['name' => 'partial', 'reference' => 'partial', 'typeProduct' => 'exact'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeProduct' => 'exact', 'typeProduct.name' => 'ipartial'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt', 'supplierPrice'])]
#[ApiResource(
description: 'Produits du catalogue fournisseur. Un produit possède une référence, un prix indicatif, un type, des fournisseurs et des documents. Il peut être lié à des machines.',
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: 500
paginationMaximumItemsPerPage: 200
)]
class Product
{
use CuidEntityTrait;
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['product:read'])]
#[Groups(['product:read', 'document:list'])]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
#[Groups(['product:read'])]
#[Groups(['product:read', 'document:list'])]
private string $name;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
@@ -104,6 +122,8 @@ class Product
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
$this->constructeurs = new ArrayCollection();
$this->documents = new ArrayCollection();
$this->customFieldValues = new ArrayCollection();
@@ -112,36 +132,6 @@ class Product
$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;
@@ -229,19 +219,4 @@ class Product
{
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));
}
}

View File

@@ -8,14 +8,16 @@ 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\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: ProfileRepository::class)]
@@ -23,12 +25,26 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ORM\UniqueConstraint(name: 'UNIQ_email', columns: ['email'])]
#[ORM\HasLifecycleCallbacks]
#[ApiResource(
description: 'Profils utilisateurs. Chaque profil possède un rôle (Admin, Gestionnaire, Viewer, User), un email unique et un mot de passe. Gère l\'authentification par session.',
operations: [
new Get(),
new GetCollection(),
new Post(),
new Put(),
new Delete(),
new Get(security: "is_granted('ROLE_ADMIN')"),
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']]
@@ -63,16 +79,21 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface
* @var list<string> The user roles
*/
#[ORM\Column(type: 'json', options: ['default' => '["ROLE_USER"]'])]
#[Groups(['profile:read', 'profile:write'])]
#[Groups(['profile:read', 'profile:admin:write'])]
private array $roles = ['ROLE_USER'];
/**
* @var string The hashed password
* @var null|string The hashed password
*/
#[ORM\Column(type: 'string', nullable: true)]
#[Groups(['profile:write'])]
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;
@@ -83,8 +104,7 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface
public function __construct()
{
// Générer un CUID-like ID pour compatibilité avec Prisma
$this->id = 'cl'.substr(strtolower(base_convert(random_bytes(12), 2, 36)), 0, 24);
$this->id = 'cl'.bin2hex(random_bytes(12));
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
}
@@ -157,11 +177,10 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles = $this->roles;
$roles[] = 'ROLE_USER';
return array_unique($roles);
return array_values(array_unique($roles));
}
/**
@@ -182,20 +201,37 @@ class Profile implements UserInterface, PasswordAuthenticatedUserInterface
return $this->password;
}
public function setPassword(string $password): static
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
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
$this->plainPassword = null;
}
public function getCreatedAt(): DateTimeImmutable

View File

@@ -8,38 +8,47 @@ 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\Entity\Trait\CuidEntityTrait;
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(
description: 'Sites industriels. Chaque site regroupe des machines et peut avoir ses propres documents. Un site possède un nom, une adresse et des coordonnées de contact.',
operations: [
new Get(),
new GetCollection(),
new Post(),
new Put(),
new Delete(),
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: 500
paginationMaximumItemsPerPage: 200
)]
class Site
{
use CuidEntityTrait;
#[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')]
@@ -57,6 +66,9 @@ class Site
#[ORM\Column(type: Types::STRING, length: 100, options: ['default' => ''], name: 'contactCity')]
private string $contactCity = '';
#[ORM\Column(type: Types::STRING, length: 7, options: ['default' => ''], name: 'color')]
private string $color = '';
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private DateTimeImmutable $createdAt;
@@ -76,41 +88,11 @@ class Site
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;
$this->machines = new ArrayCollection();
$this->documents = new ArrayCollection();
}
public function getName(): string
@@ -185,14 +167,16 @@ class Site
return $this;
}
public function getCreatedAt(): DateTimeImmutable
public function getColor(): string
{
return $this->createdAt;
return $this->color;
}
public function getUpdatedAt(): DateTimeImmutable
public function setColor(string $color): static
{
return $this->updatedAt;
$this->color = $color;
return $this;
}
/**
@@ -254,10 +238,4 @@ class Site
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));
}
}

Some files were not shown because too many files have changed in this diff Show More