Compare commits
183 Commits
v1.1.1
...
feature/do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c8aecec93 | ||
|
|
476060cf7d | ||
|
|
1b1dab65b6 | ||
|
|
5fff226f84 | ||
|
|
34b0d9225c | ||
|
|
691f632be0 | ||
|
|
43fafc2251 | ||
|
|
0ad5815659 | ||
|
|
a249a5b785 | ||
|
|
d85272208a | ||
|
|
26be0b655d | ||
|
|
2d33c97449 | ||
|
|
03c2451990 | ||
|
|
3f6ce153bb | ||
|
|
d568961eb3 | ||
|
|
9299a46c8b | ||
|
|
162c6ece71 | ||
| 3f93781e16 | |||
| a07145c78f | |||
| 586b7bb91d | |||
| 3a75269323 | |||
|
|
66fa0a506c | ||
|
|
9b35023879 | ||
|
|
5463cde38b | ||
|
|
7eb6def192 | ||
|
|
9d75653624 | ||
|
|
fd69d6a63e | ||
|
|
172ec78c5f | ||
|
|
d70b9086d5 | ||
|
|
73ebd6902d | ||
|
|
ded1f7a8b6 | ||
|
|
3b35598b07 | ||
|
|
06ce9fb1f2 | ||
|
|
8851f22e4e | ||
|
|
330b9376f6 | ||
|
|
4468fd7cdf | ||
|
|
509c4d2247 | ||
|
|
043f6b1ce6 | ||
|
|
d5a43fc9bb | ||
|
|
0de2aba538 | ||
|
|
5ec6e49af2 | ||
|
|
8d920d5f65 | ||
|
|
342b0afdbb | ||
|
|
2043e5b643 | ||
|
|
21e5ad5381 | ||
|
|
53b6abc9a8 | ||
|
|
826dae7712 | ||
|
|
38777b7de0 | ||
|
|
add3a9a21f | ||
|
|
f965affc94 | ||
|
|
4340a0e13e | ||
|
|
bd7259ed05 | ||
|
|
2f173e766d | ||
|
|
4f1e136dc5 | ||
|
|
e335f4c24c | ||
|
|
46ea3ca8ad | ||
|
|
65fbd38b55 | ||
|
|
37aa755819 | ||
|
|
98caaa148d | ||
|
|
523eed927e | ||
|
|
43bec07bb8 | ||
|
|
0181f18778 | ||
|
|
8e0acf4896 | ||
|
|
aa8e043c83 | ||
|
|
b2aff0e414 | ||
|
|
4072abf7ba | ||
|
|
089ca43404 | ||
|
|
f09c7e4782 | ||
|
|
6a20dcce54 | ||
|
|
6e0be3dbf3 | ||
|
|
f66db3f2f0 | ||
|
|
0864af1439 | ||
|
|
5210e53d73 | ||
|
|
3f07162b94 | ||
|
|
57615b3e9d | ||
|
|
46694d11d9 | ||
|
|
44cfa25eca | ||
|
|
7ea4cc8c12 | ||
|
|
bb300a7ca7 | ||
|
|
556da6e451 | ||
|
|
8871440c9a | ||
|
|
6f1756e82e | ||
|
|
55bed90ac7 | ||
|
|
a6139d7090 | ||
|
|
8ed5f90b63 | ||
|
|
5194543d16 | ||
|
|
c01b71fe06 | ||
|
|
5336dfc09d | ||
|
|
77c5d25cea | ||
|
|
e2326064ba | ||
|
|
100e24725c | ||
|
|
515bae189e | ||
|
|
333f2a88af | ||
|
|
eccbc1bd56 | ||
|
|
2a0809a065 | ||
|
|
f2061abce8 | ||
|
|
42c7072bcd | ||
|
|
1f90f809ac | ||
|
|
a940f53f8a | ||
|
|
c74bdedf9b | ||
|
|
233ee3faf3 | ||
|
|
b8edf1ea95 | ||
|
|
7a7af58074 | ||
|
|
03e6c2432b | ||
|
|
31408ded7f | ||
|
|
4054fb24e6 | ||
|
|
32ba4928df | ||
| edf7d0fa9e | |||
| 233927df19 | |||
| dcb5f15769 | |||
| d3cd3fc3ce | |||
| 33fc80cbc2 | |||
| 33e3f25850 | |||
| efc6ec5691 | |||
| b342d0e50a | |||
| 0709d01240 | |||
| 74f77a3ba8 | |||
| bab13e5c57 | |||
|
|
378026ebce | ||
|
|
ea2b813728 | ||
|
|
20653b9046 | ||
|
|
c6deef6028 | ||
|
|
e922b14419 | ||
|
|
d16b042739 | ||
|
|
2b3c1fe08e | ||
|
|
51248b7854 | ||
|
|
0e11f4ad2d | ||
|
|
f2539099bc | ||
|
|
e5dc60467e | ||
|
|
fbc0372bd6 | ||
|
|
1483b0075b | ||
|
|
74e88923dc | ||
|
|
ef61d1a0d3 | ||
|
|
3f0fb0d5c2 | ||
|
|
dd1497beac | ||
|
|
7cd8772617 | ||
|
|
d89c97f0a0 | ||
|
|
7a5dd0b555 | ||
|
|
44d69db560 | ||
|
|
453065c9f0 | ||
|
|
eb85323116 | ||
|
|
2dfa501a65 | ||
|
|
c22f9dbf2b | ||
|
|
27a1b09d62 | ||
|
|
7bbb693924 | ||
|
|
9661fd5d91 | ||
|
|
d9ab583879 | ||
|
|
5d41bda997 | ||
|
|
3d037083c6 | ||
|
|
a3e440c254 | ||
|
|
adc44b99d3 | ||
|
|
60afeb4cfd | ||
|
|
02ff8b1a96 | ||
|
|
2156df22c6 | ||
|
|
cd2a3fac55 | ||
|
|
6300a3588a | ||
|
|
45213103e4 | ||
|
|
91b8b424d6 | ||
|
|
0d1c9277e5 | ||
|
|
db16d26103 | ||
|
|
0eb64d0975 | ||
|
|
39e503ae18 | ||
|
|
70ed354c42 | ||
|
|
ba98ae37f4 | ||
|
|
906d39793f | ||
|
|
f970c1928d | ||
|
|
2a1d966b87 | ||
|
|
a393b62e9f | ||
|
|
1247f72af6 | ||
|
|
6735bf252c | ||
|
|
508066d39f | ||
|
|
70956c204e | ||
|
|
16a7eac0c6 | ||
|
|
37ac08b182 | ||
|
|
5ef80b362e | ||
|
|
78f19daf76 | ||
|
|
6caa4a61df | ||
|
|
bf55034b2e | ||
|
|
ba1114e78b | ||
|
|
5ccc3b30f0 | ||
| 8d83076be0 | |||
|
|
997a3ae822 | ||
|
|
034c193e4b |
6
.claude/settings.json
Normal file
6
.claude/settings.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"enabledPlugins": {
|
||||||
|
"security-guidance@claude-plugins-official": true,
|
||||||
|
"claude-md-management@claude-plugins-official": true
|
||||||
|
}
|
||||||
|
}
|
||||||
24
.dockerignore
Normal file
24
.dockerignore
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
.git
|
||||||
|
.gitea
|
||||||
|
.env.local
|
||||||
|
.env.test
|
||||||
|
docker/
|
||||||
|
deploy/docker/docker-compose.prod.yml
|
||||||
|
deploy/docker/deploy.sh
|
||||||
|
deploy/docker/.env.example
|
||||||
|
Inventory_frontend/node_modules
|
||||||
|
Inventory_frontend/.nuxt
|
||||||
|
Inventory_frontend/.output
|
||||||
|
var/
|
||||||
|
vendor/
|
||||||
|
LOG/
|
||||||
|
docs/
|
||||||
|
tests/
|
||||||
|
scripts/
|
||||||
|
*.sql
|
||||||
|
*.xlsx
|
||||||
|
*.png
|
||||||
|
*.md
|
||||||
|
!composer.lock
|
||||||
|
!symfony.lock
|
||||||
|
!Inventory_frontend/package-lock.json
|
||||||
7
.env
7
.env
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
APP_ENV=dev
|
APP_ENV=dev
|
||||||
APP_SECRET=
|
APP_SECRET=change_me_in_env_local
|
||||||
APP_SHARE_DIR=var/share
|
APP_SHARE_DIR=var/share
|
||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
@@ -40,8 +40,3 @@ DEFAULT_URI=http://localhost
|
|||||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||||
###< nelmio/cors-bundle ###
|
###< nelmio/cors-bundle ###
|
||||||
|
|
||||||
###> lexik/jwt-authentication-bundle ###
|
|
||||||
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
|
||||||
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
|
||||||
JWT_PASSPHRASE=281e2cd303ed9ba4a4a4074e19eac9cea505cc9d82ce79a448bb8eb00c636ebe
|
|
||||||
###< lexik/jwt-authentication-bundle ###
|
|
||||||
|
|||||||
32
.gitea/workflows/build-docker.yml
Normal file
32
.gitea/workflows/build-docker.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Build & Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
|
- name: Login to Gitea Registry
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login gitea.malio.fr -u "${{ gitea.repository_owner }}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: |
|
||||||
|
docker build \
|
||||||
|
-f deploy/docker/Dockerfile.prod \
|
||||||
|
-t gitea.malio.fr/malio-dev/inventory:${{ gitea.ref_name }} \
|
||||||
|
-t gitea.malio.fr/malio-dev/inventory:latest \
|
||||||
|
.
|
||||||
|
|
||||||
|
- name: Push Docker image
|
||||||
|
run: |
|
||||||
|
docker push gitea.malio.fr/malio-dev/inventory:${{ gitea.ref_name }}
|
||||||
|
docker push gitea.malio.fr/malio-dev/inventory:latest
|
||||||
28
.gitignore
vendored
28
.gitignore
vendored
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
/.env.local
|
/.env.local
|
||||||
/.env.local.php
|
/.env.local.php
|
||||||
@@ -23,18 +22,27 @@
|
|||||||
###> docker ###
|
###> docker ###
|
||||||
docker/.env.docker.local
|
docker/.env.docker.local
|
||||||
###< docker ###
|
###< docker ###
|
||||||
|
|
||||||
###> lexik/jwt-authentication-bundle ###
|
|
||||||
/config/jwt/*.pem
|
|
||||||
###< lexik/jwt-authentication-bundle ###
|
|
||||||
|
|
||||||
###> migration archives ###
|
###> migration archives ###
|
||||||
/_archives/
|
/_archives/
|
||||||
###< migration archives ###
|
###< migration archives ###
|
||||||
|
|
||||||
|
###> temp files ###
|
||||||
|
*.sql
|
||||||
|
*.sql.gz
|
||||||
|
*.har
|
||||||
|
FEATURE_IDEAS.md
|
||||||
|
bin/.phpunit.result.cache
|
||||||
|
###< temp files ###
|
||||||
|
|
||||||
###> frontend ###
|
###> frontend ###
|
||||||
/frontend/node_modules/
|
/frontend/
|
||||||
/frontend/.nuxt/
|
|
||||||
/frontend/.output/
|
|
||||||
/frontend/dist/
|
|
||||||
###< frontend ###
|
###< frontend ###
|
||||||
|
|
||||||
|
###> ide ###
|
||||||
|
/.idea/
|
||||||
|
###< ide ###
|
||||||
|
|
||||||
|
###> wsl ###
|
||||||
|
*:Zone.Identifier
|
||||||
|
###< wsl ###
|
||||||
|
config/reference.php
|
||||||
|
|||||||
9
.idea/.gitignore
generated
vendored
9
.idea/.gitignore
generated
vendored
@@ -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
144
.idea/Inventory.iml
generated
@@ -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>
|
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
5
.idea/codeStyles/codeStyleConfig.xml
generated
@@ -1,5 +0,0 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
|
||||||
<state>
|
|
||||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
|
||||||
</state>
|
|
||||||
</component>
|
|
||||||
12
.idea/dataSources.xml
generated
12
.idea/dataSources.xml
generated
@@ -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>
|
|
||||||
8
.idea/laravel-idea.xml
generated
8
.idea/laravel-idea.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="InertiaPackage">
|
|
||||||
<option name="directoryPaths">
|
|
||||||
<list />
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
12
.idea/material_theme_project_new.xml
generated
12
.idea/material_theme_project_new.xml
generated
@@ -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
8
.idea/modules.xml
generated
@@ -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
160
.idea/php.xml
generated
@@ -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
6
.idea/symfony2.xml
generated
@@ -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
6
.idea/vcs.xml
generated
@@ -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
6
.idea/workspace.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ComposerSettings">
|
|
||||||
<execution />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
12
.mcp.json
Normal file
12
.mcp.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"inventory": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "http://inventory.malio-dev.fr/_mcp",
|
||||||
|
"headers": {
|
||||||
|
"X-Profile-Id": "admin-default-profile",
|
||||||
|
"X-Profile-Password": "A123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
81
CHANGELOG.md
81
CHANGELOG.md
@@ -1,15 +1,78 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
Liste des évolutions du projet inventory
|
## [1.8.1] - 2026-03-05
|
||||||
|
|
||||||
## [0.0.0]
|
### Ajouts
|
||||||
### Parameters
|
- **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.
|
||||||
Ajouter dans le fichier .env
|
- **Messages d'erreur humanises** : les erreurs backend (violations de contraintes, erreurs serveur) sont desormais traduites en messages comprehensibles pour l'utilisateur final (`errorMessages.ts`).
|
||||||
- DEFAULT_URI
|
- **Icones Lucide dans la navbar** : reorganisation des groupes de navigation et ajout d'icones pour chaque section.
|
||||||
- DATABASE_URL
|
- **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.
|
||||||
|
|||||||
266
CLAUDE.md
Normal file
266
CLAUDE.md
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
# 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 | |
|
||||||
|
|
||||||
|
## Glossaire Métier
|
||||||
|
Voir `docs/GLOSSAIRE_METIER.md` — glossaire complet du domaine métier (concepts, workflows utilisateur, correspondance métier↔code). À consulter pour comprendre le "pourquoi" derrière le code.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Inventory/ # Backend Symfony (repo principal)
|
||||||
|
├── src/Entity/ # Entités Doctrine (annotations PHP 8 attributes)
|
||||||
|
│ └── Trait/ # CuidEntityTrait (génération d'ID CUID)
|
||||||
|
├── src/Controller/ # Controllers custom (session, comments, audit…)
|
||||||
|
├── src/EventSubscriber/ # Audit subscribers (onFlush)
|
||||||
|
├── src/Service/ # Services métier (sync, conversion, storage…)
|
||||||
|
├── src/Enum/ # Enums PHP (DocumentType, ModelCategory)
|
||||||
|
├── src/DTO/ # Data Transfer Objects (sync workflow)
|
||||||
|
├── src/Filter/ # Filtres API Platform custom
|
||||||
|
├── src/Command/ # Commandes Symfony CLI (compress-pdf, create-profile…)
|
||||||
|
├── 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)
|
||||||
|
|
||||||
|
# Database / Fixtures
|
||||||
|
make db-reset # Reset database (drop + recreate schema)
|
||||||
|
make fixtures-dump # Dump la DB vers fixtures/data.sql
|
||||||
|
make fixtures-load # Charger les fixtures SQL (désactive FK)
|
||||||
|
make fixtures-reset # Reset DB + recharger fixtures
|
||||||
|
make import-data # Importer les dumps SQL normalisés
|
||||||
|
make cache-clear # Clear cache Symfony
|
||||||
|
|
||||||
|
# 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`
|
||||||
|
|
||||||
|
#### Entités de normalisation (slots & skeleton requirements)
|
||||||
|
Remplacent les anciennes colonnes JSON `structure` et `productIds` par des tables relationnelles :
|
||||||
|
- **Slots composant** (données réelles d'un composant) : `ComposantPieceSlot`, `ComposantSubcomponentSlot`, `ComposantProductSlot`
|
||||||
|
- **Slots pièce** (données réelles d'une pièce) : `PieceProductSlot`
|
||||||
|
- **Skeleton Requirements** (définitions du ModelType) : `SkeletonPieceRequirement`, `SkeletonProductRequirement`, `SkeletonSubcomponentRequirement`
|
||||||
|
|
||||||
|
### 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), `/api/machines/{id}/clone` (POST) : hiérarchie complète machine avec normalisation JSON manuelle. 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.
|
||||||
|
- `ComposantPieceSlotController` — `/api/composant-piece-slots/{id}` (PATCH) : mise à jour des slots pièce d'un composant.
|
||||||
|
- `ComposantProductSlotController` — `/api/composant-product-slots/{id}` (PATCH) : mise à jour des slots produit d'un composant.
|
||||||
|
- `ComposantSubcomponentSlotController` — `/api/composant-subcomponent-slots/{id}` (PATCH) : mise à jour des slots sous-composant d'un composant.
|
||||||
|
- `SessionProfileController` — `/api/session/profile` (GET/POST/DELETE) : auth session (login/logout/current user).
|
||||||
|
- `SessionProfilesController` — `/api/session/profiles` (GET) : liste des profils disponibles pour la session.
|
||||||
|
- `AdminProfileController` — `/api/admin/profiles` : CRUD profils, gestion rôles et mots de passe (ROLE_ADMIN).
|
||||||
|
- `CommentController` — `/api/comments` : création, résolution, compteur non-résolus.
|
||||||
|
- `ActivityLogController` — `/api/activity-logs` (GET) : journal d'activité global.
|
||||||
|
- `EntityHistoryController` — `/api/{entity}/{id}/history` (GET) : historique audit par entité (machines, pièces, composants, produits).
|
||||||
|
- `DocumentQueryController` — `/api/documents/{entity}/{id}` (GET) : documents par site/machine/composant/pièce/produit.
|
||||||
|
- `DocumentServeController` — `/api/documents/{id}/file|download` (GET) : servir/télécharger fichiers.
|
||||||
|
- `ModelTypeConversionController` — `/api/model_types/{id}/conversion-check|convert` : vérification et conversion de ModelType.
|
||||||
|
- `ModelTypeSyncController` — `/api/model_types/{id}/sync-preview|sync-confirm` (POST) : prévisualisation et application de sync ModelType→Composants.
|
||||||
|
- `EntityVersionController` — `/api/{entity}/{id}/versions` (GET), `/api/{entity}/{id}/versions/{version}/restore` (POST) : historique de versions numérotées et restauration.
|
||||||
|
- `HealthCheckController` — `/api/health` (GET) : health check.
|
||||||
|
|
||||||
|
### Custom Fields — Architecture
|
||||||
|
- **Composants/Pièces/Produits** : définitions dans les entités `SkeletonPieceRequirement`, `SkeletonProductRequirement`, `SkeletonSubcomponentRequirement` du ModelType (anciennement JSON `structure`, normalisé en tables relationnelles). Les custom fields de ces entités sont définis dans `customFields` JSON sur chaque Skeleton*Requirement.
|
||||||
|
- **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
|
||||||
|
|
||||||
|
### Enums (`src/Enum/`)
|
||||||
|
- `DocumentType` — types de documents (photo, schéma, facture, etc.)
|
||||||
|
- `ModelCategory` — catégories de ModelType
|
||||||
|
|
||||||
|
### Services (`src/Service/`)
|
||||||
|
- `ModelTypeSyncService` — synchronise les skeleton requirements d'un ModelType vers les composants existants
|
||||||
|
- `ModelTypeCategoryConversionService` — conversion de catégorie d'un ModelType
|
||||||
|
- `SkeletonStructureService` — gestion de la structure skeleton (requirements)
|
||||||
|
- `DocumentStorageService` — stockage et gestion des fichiers documents
|
||||||
|
- `PdfCompressorService` — compression des PDFs uploadés
|
||||||
|
- `EntityVersionService` — gestion des versions numérotées (snapshot, restore) pour machines, pièces, composants, produits
|
||||||
|
- `ReferenceAutoGenerator` — génération automatique de références pour pièces et composants à partir de formules ModelType
|
||||||
|
- `src/Service/Sync/` — stratégies de sync par type de slot (tagged `app.sync_strategy`)
|
||||||
|
|
||||||
|
### DTOs (`src/DTO/`)
|
||||||
|
- `SyncConfirmation`, `SyncPreviewResult`, `SyncExecutionResult` — objets de transfert pour le workflow de sync ModelType
|
||||||
|
|
||||||
|
### Filters (`src/Filter/`)
|
||||||
|
- `MultiSearchFilter` — filtre API Platform pour recherche OR sur plusieurs champs (ex: name + reference)
|
||||||
|
|
||||||
|
### EventSubscribers notables (non-audit)
|
||||||
|
- `PieceProductSyncSubscriber` — sync automatique des PieceProductSlots
|
||||||
|
- `UniqueConstraintSubscriber` — traduit les erreurs de contrainte unique PG en messages utilisateur lisibles
|
||||||
|
- `ReferenceAutoSubscriber` — recalcule les références auto des pièces/composants quand les CustomFieldValues changent (onFlush)
|
||||||
|
|
||||||
|
### 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()`, `createConstructeur()`, `createCustomField()`, `createCustomFieldValue()`, `createModelType()`, `createMachineComponentLink()`, `createMachinePieceLink()`, `createMachineProductLink()`, `createComposantPieceSlot()`, `createComposantSubcomponentSlot()`, `createComposantProductSlot()`, `createPieceProductSlot()`
|
||||||
|
- Auth : `createViewerClient()`, `createGestionnaireClient()`, `createAdminClient()`, `createUnauthenticatedClient()`
|
||||||
|
|
||||||
|
## 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)
|
||||||
214
DEPLOY.md
214
DEPLOY.md
@@ -1,17 +1,29 @@
|
|||||||
# Inventory - Guide de Déploiement & Release
|
# Inventory — Guide de Déploiement
|
||||||
|
|
||||||
## Architecture
|
Guide pour déployer l'application sur un serveur de production.
|
||||||
|
|
||||||
|
## Architecture de production
|
||||||
|
|
||||||
```
|
```
|
||||||
inventory.malio-dev.fr/ → Frontend Nuxt (statique)
|
inventory.malio-dev.fr/ → Frontend Nuxt (fichiers statiques servis par Nginx)
|
||||||
inventory.malio-dev.fr/api → Backend Symfony (PHP-FPM)
|
inventory.malio-dev.fr/api → Backend Symfony (PHP-FPM derrière Nginx)
|
||||||
```
|
```
|
||||||
|
|
||||||
| Composant | Technologie | Emplacement serveur |
|
| Composant | Technologie | Emplacement serveur |
|
||||||
|-----------|-------------|---------------------|
|
|-----------|-------------|---------------------|
|
||||||
| Backend | Symfony 8 + API Platform | `/var/www/Inventory/` |
|
| Backend | Symfony 8 + API Platform | `/var/www/Inventory/` |
|
||||||
| Frontend | Nuxt 4 (statique) | `/var/www/Inventory/Inventory_frontend/.output/public/` |
|
| Frontend | Nuxt 4 (site statique) | `/var/www/Inventory/Inventory_frontend/.output/public/` |
|
||||||
| Base de données | PostgreSQL 16 | `inventory` |
|
| Base de données | PostgreSQL 16 | Base `inventory` |
|
||||||
|
|
||||||
|
### Schéma simplifié
|
||||||
|
|
||||||
|
```
|
||||||
|
Navigateur
|
||||||
|
↓ HTTPS
|
||||||
|
Nginx (reverse proxy)
|
||||||
|
├── /api/* → PHP-FPM (Symfony) → PostgreSQL
|
||||||
|
└── /* → Fichiers statiques (Nuxt build)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -24,7 +36,8 @@ inventory.malio-dev.fr/api → Backend Symfony (PHP-FPM)
|
|||||||
- **PostgreSQL** : 16
|
- **PostgreSQL** : 16
|
||||||
- **Composer**
|
- **Composer**
|
||||||
|
|
||||||
Vérifier :
|
### Vérification des prérequis
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php -v # PHP 8.4+
|
php -v # PHP 8.4+
|
||||||
php -m | grep -E 'pgsql|intl|zip|gd|mbstring'
|
php -m | grep -E 'pgsql|intl|zip|gd|mbstring'
|
||||||
@@ -73,10 +86,10 @@ psql -U ferme_user -h 127.0.0.1 -d inventory -f /tmp/backup_v1.0.0_clean.sql
|
|||||||
```bash
|
```bash
|
||||||
cd /var/www/Inventory
|
cd /var/www/Inventory
|
||||||
|
|
||||||
# Installer les dépendances
|
# Installer les dépendances (sans les outils de dev)
|
||||||
composer install --no-dev --optimize-autoloader
|
composer install --no-dev --optimize-autoloader
|
||||||
|
|
||||||
# Créer .env.local
|
# Créer le fichier de configuration locale
|
||||||
cat > .env.local << 'EOF'
|
cat > .env.local << 'EOF'
|
||||||
APP_ENV=prod
|
APP_ENV=prod
|
||||||
APP_DEBUG=0
|
APP_DEBUG=0
|
||||||
@@ -85,28 +98,20 @@ APP_SECRET=CHANGE_ME
|
|||||||
DATABASE_URL="postgresql://ferme_user:fermerecette@127.0.0.1:5432/inventory?serverVersion=16"
|
DATABASE_URL="postgresql://ferme_user:fermerecette@127.0.0.1:5432/inventory?serverVersion=16"
|
||||||
|
|
||||||
CORS_ALLOW_ORIGIN='^https?://inventory\.malio-dev\.fr$'
|
CORS_ALLOW_ORIGIN='^https?://inventory\.malio-dev\.fr$'
|
||||||
|
|
||||||
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
|
||||||
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
|
||||||
JWT_PASSPHRASE=inventoryjwt
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Générer APP_SECRET
|
# Générer un secret aléatoire
|
||||||
sed -i "s/CHANGE_ME/$(openssl rand -hex 32)/" .env.local
|
sed -i "s/CHANGE_ME/$(openssl rand -hex 32)/" .env.local
|
||||||
|
|
||||||
# Générer les clés JWT
|
# Permissions pour le dossier var/ (cache, logs)
|
||||||
mkdir -p config/jwt
|
|
||||||
openssl genrsa -out config/jwt/private.pem -aes256 4096
|
|
||||||
# Passphrase : inventoryjwt
|
|
||||||
openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem
|
|
||||||
chmod 600 config/jwt/private.pem
|
|
||||||
|
|
||||||
# Permissions
|
|
||||||
sudo chown -R www-data:www-data var/
|
sudo chown -R www-data:www-data var/
|
||||||
sudo chmod -R 775 var/
|
sudo chmod -R 775 var/
|
||||||
|
|
||||||
# Vider le cache
|
# Vider le cache
|
||||||
php bin/console cache:clear --env=prod
|
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
|
### 4. Configurer le frontend Nuxt
|
||||||
@@ -120,7 +125,7 @@ sudo chown -R malio:malio .
|
|||||||
# Installer les dépendances
|
# Installer les dépendances
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Créer .env
|
# Créer le fichier d'environnement
|
||||||
cat > .env << 'EOF'
|
cat > .env << 'EOF'
|
||||||
NUXT_PUBLIC_API_BASE_URL=http://inventory.malio-dev.fr/api
|
NUXT_PUBLIC_API_BASE_URL=http://inventory.malio-dev.fr/api
|
||||||
EOF
|
EOF
|
||||||
@@ -141,7 +146,7 @@ server {
|
|||||||
listen 80;
|
listen 80;
|
||||||
server_name inventory.malio-dev.fr;
|
server_name inventory.malio-dev.fr;
|
||||||
|
|
||||||
# Gros fichiers (100MB max)
|
# Gros fichiers (100MB max pour les uploads de documents)
|
||||||
client_max_body_size 100M;
|
client_max_body_size 100M;
|
||||||
client_body_timeout 300s;
|
client_body_timeout 300s;
|
||||||
send_timeout 300s;
|
send_timeout 300s;
|
||||||
@@ -149,12 +154,13 @@ server {
|
|||||||
access_log /var/log/nginx/inventory-access.log;
|
access_log /var/log/nginx/inventory-access.log;
|
||||||
error_log /var/log/nginx/inventory-error.log;
|
error_log /var/log/nginx/inventory-error.log;
|
||||||
|
|
||||||
# Backend Symfony - /api
|
# Backend Symfony — toutes les requêtes /api
|
||||||
location /api {
|
location /api {
|
||||||
root /var/www/Inventory/public;
|
root /var/www/Inventory/public;
|
||||||
try_files $uri /index.php$is_args$args;
|
try_files $uri /index.php$is_args$args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# PHP-FPM (exécute le code PHP)
|
||||||
location ~ ^/index\.php(/|$) {
|
location ~ ^/index\.php(/|$) {
|
||||||
fastcgi_pass unix:/run/php/php-fpm.sock;
|
fastcgi_pass unix:/run/php/php-fpm.sock;
|
||||||
fastcgi_split_path_info ^(.+\.php)(/.*)$;
|
fastcgi_split_path_info ^(.+\.php)(/.*)$;
|
||||||
@@ -165,27 +171,27 @@ server {
|
|||||||
internal;
|
internal;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Frontend statique
|
# Frontend statique — tout le reste
|
||||||
location / {
|
location / {
|
||||||
root /var/www/Inventory/Inventory_frontend/.output/public;
|
root /var/www/Inventory/Inventory_frontend/.output/public;
|
||||||
index index.html;
|
index index.html;
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html; # SPA fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Activer :
|
Activer le site :
|
||||||
```bash
|
```bash
|
||||||
sudo ln -s /etc/nginx/sites-available/inventory /etc/nginx/sites-enabled/
|
sudo ln -s /etc/nginx/sites-available/inventory /etc/nginx/sites-enabled/
|
||||||
sudo nginx -t
|
sudo nginx -t # Vérifier la syntaxe
|
||||||
sudo systemctl reload nginx
|
sudo systemctl reload nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. Vérifier
|
### 6. Vérifier
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://inventory.malio-dev.fr
|
curl http://inventory.malio-dev.fr # Frontend
|
||||||
curl http://inventory.malio-dev.fr/api
|
curl http://inventory.malio-dev.fr/api # API (doc Swagger)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -197,12 +203,13 @@ curl http://inventory.malio-dev.fr/api
|
|||||||
```bash
|
```bash
|
||||||
cd /var/www/Inventory
|
cd /var/www/Inventory
|
||||||
|
|
||||||
# Pull les changements
|
# Récupérer les changements
|
||||||
git pull
|
git pull
|
||||||
git submodule update --init --recursive
|
git submodule update --init --recursive
|
||||||
|
|
||||||
# Backend
|
# Backend
|
||||||
composer install --no-dev --optimize-autoloader
|
composer install --no-dev --optimize-autoloader
|
||||||
|
php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
php bin/console cache:clear --env=prod
|
php bin/console cache:clear --env=prod
|
||||||
sudo chown -R www-data:www-data var/
|
sudo chown -R www-data:www-data var/
|
||||||
|
|
||||||
@@ -214,56 +221,76 @@ npx nuxi generate
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Versioning & Releases
|
## Backup base de données
|
||||||
|
|
||||||
### Source de vérité
|
### Export (faire un backup)
|
||||||
|
|
||||||
Le fichier `VERSION` à la racine contient le numéro de version (ex: `1.0.0`).
|
|
||||||
|
|
||||||
Cette version est synchronisée avec :
|
|
||||||
- Le footer de l'application
|
|
||||||
- `config/packages/api_platform.yaml`
|
|
||||||
|
|
||||||
### Créer une release
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Depuis le PC de dev
|
pg_dump -U ferme_user -h 127.0.0.1 -d inventory \
|
||||||
./scripts/release.sh patch # 1.0.0 → 1.0.1
|
--no-owner --no-acl --inserts --column-inserts \
|
||||||
./scripts/release.sh minor # 1.0.0 → 1.1.0
|
--clean --if-exists > backup_inventory_$(date +%Y%m%d).sql
|
||||||
./scripts/release.sh major # 1.0.0 → 2.0.0
|
|
||||||
./scripts/release.sh 2.0.0 # Version exacte
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Le script :
|
### Import (restaurer un backup)
|
||||||
1. Vérifie/commit le submodule frontend
|
|
||||||
2. Met à jour `VERSION` et `api_platform.yaml`
|
|
||||||
3. Commit et tag les deux repos
|
|
||||||
4. Affiche les commandes pour push
|
|
||||||
|
|
||||||
### Pousser la release
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Frontend (submodule)
|
psql -U ferme_user -h 127.0.0.1 -d inventory -f backup_inventory_YYYYMMDD.sql
|
||||||
cd Inventory_frontend && git push && git push --tags && cd ..
|
|
||||||
|
|
||||||
# Backend
|
|
||||||
git push && git push --tags
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Créer la release sur Gitea
|
|
||||||
|
|
||||||
1. Aller sur le dépôt Gitea
|
|
||||||
2. **Releases** > **New Release**
|
|
||||||
3. Sélectionner le tag `vX.Y.Z`
|
|
||||||
4. Ajouter les notes de release
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Commandes utiles
|
## 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
|
```bash
|
||||||
# Logs Nginx
|
# Logs Nginx
|
||||||
tail -f /var/log/nginx/inventory-error.log
|
tail -f /var/log/nginx/inventory-error.log
|
||||||
|
tail -f /var/log/nginx/inventory-access.log
|
||||||
|
|
||||||
# Logs Symfony
|
# Logs Symfony
|
||||||
tail -f /var/www/Inventory/var/log/prod.log
|
tail -f /var/www/Inventory/var/log/prod.log
|
||||||
@@ -274,55 +301,12 @@ php /var/www/Inventory/bin/console cache:clear --env=prod
|
|||||||
# Rebuild frontend
|
# Rebuild frontend
|
||||||
cd /var/www/Inventory/Inventory_frontend && npx nuxi generate
|
cd /var/www/Inventory/Inventory_frontend && npx nuxi generate
|
||||||
|
|
||||||
# Status PHP-FPM
|
# Status des services
|
||||||
systemctl status php8.4-fpm
|
systemctl status php8.4-fpm
|
||||||
|
systemctl status nginx
|
||||||
|
systemctl status postgresql
|
||||||
|
|
||||||
# Reload Nginx
|
# Redémarrer les services
|
||||||
|
sudo systemctl restart php8.4-fpm
|
||||||
sudo systemctl reload nginx
|
sudo systemctl reload nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backup base de données
|
|
||||||
|
|
||||||
### Export
|
|
||||||
```bash
|
|
||||||
pg_dump -U ferme_user -h 127.0.0.1 -d inventory --no-owner --no-acl --inserts --column-inserts --clean --if-exists > backup_inventory_$(date +%Y%m%d).sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Import
|
|
||||||
```bash
|
|
||||||
psql -U ferme_user -h 127.0.0.1 -d inventory -f backup_inventory_YYYYMMDD.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Erreur 502 Bad Gateway
|
|
||||||
```bash
|
|
||||||
# Vérifier PHP-FPM
|
|
||||||
systemctl status php8.4-fpm
|
|
||||||
sudo systemctl restart php8.4-fpm
|
|
||||||
```
|
|
||||||
|
|
||||||
### Erreur 403 Forbidden
|
|
||||||
```bash
|
|
||||||
# Vérifier les permissions
|
|
||||||
sudo chown -R www-data:www-data /var/www/Inventory/var/
|
|
||||||
sudo chmod -R 775 /var/www/Inventory/var/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Erreur API "No route found"
|
|
||||||
```bash
|
|
||||||
# Vider le cache
|
|
||||||
php /var/www/Inventory/bin/console cache:clear --env=prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend ne se met pas à jour
|
|
||||||
```bash
|
|
||||||
# Rebuild
|
|
||||||
cd /var/www/Inventory/Inventory_frontend
|
|
||||||
rm -rf .output
|
|
||||||
npx nuxi generate
|
|
||||||
```
|
|
||||||
|
|||||||
Submodule Inventory_frontend updated: adccfa9b46...958a00c8fc
1419
MIGRATION_PLAN.md
1419
MIGRATION_PLAN.md
File diff suppressed because it is too large
Load Diff
353
README.md
353
README.md
@@ -1,92 +1,305 @@
|
|||||||
# Projet Inventory
|
# Inventory
|
||||||
|
|
||||||
## Installation du projet
|
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.
|
||||||
### 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)
|
|
||||||
|
|
||||||
### Linux
|
## C'est quoi ce projet ?
|
||||||
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)
|
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
|
```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
|
make start
|
||||||
|
|
||||||
|
# 3. Installer les dépendances et builder le projet
|
||||||
make install
|
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
|
> Si `make start` échoue sur le port de la BDD, modifier `POSTGRES_PORT` dans `docker/.env.docker.local`.
|
||||||
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
|
|
||||||
|
|
||||||
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
|
1. Installe les dépendances PHP (via Composer)
|
||||||
### Backend
|
2. Installe les dépendances Node.js (via npm)
|
||||||
L'api est disponible sur http://localhost:8080/api
|
3. Build le frontend Nuxt
|
||||||
Pour la bdd toutes les infos sont dans le fichier **docker/.env.docker.local**
|
|
||||||
Vous pouvez modifier le port si nécessaire.
|
|
||||||
|
|
||||||
La bdd est déja pré-configuré dans PhpStorm, il suffit de rentrer les infos du .env.docker.local pour se connecter.
|
### Premier lancement
|
||||||
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
|
|
||||||
|
|
||||||
## Compression automatique des PDFs
|
Une fois l'installation terminée, tu peux :
|
||||||
|
|
||||||
Les documents PDF uploadés sont automatiquement compressés sans perte de qualité grâce à **qpdf**.
|
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
|
||||||
|
|
||||||
### Prérequis
|
## URLs locales
|
||||||
```bash
|
|
||||||
# Installation de qpdf (outil système)
|
|
||||||
sudo apt install qpdf
|
|
||||||
|
|
||||||
# Ou dans Docker
|
| Service | URL | Description |
|
||||||
docker exec -it php-inventory-apache apt update && apt install -y qpdf
|
|---------|-----|-------------|
|
||||||
```
|
| API Symfony | http://localhost:8081/api | Documentation interactive de l'API (Swagger) |
|
||||||
|
| Frontend Nuxt | http://localhost:3001 | L'application web |
|
||||||
### Fonctionnement
|
| Adminer (BDD) | http://localhost:5050 | Interface web pour explorer la base de données |
|
||||||
- À chaque upload de PDF, le système compresse automatiquement le fichier
|
| PostgreSQL | `localhost:5433` | Connexion directe (user: root, pass: root, db: inventory) |
|
||||||
- Compression lossless (sans perte de qualité)
|
|
||||||
- Le PDF est compressé uniquement si la taille diminue
|
|
||||||
- Si qpdf n'est pas installé, le système fonctionne normalement sans compression
|
|
||||||
|
|
||||||
### Compresser les PDFs existants
|
|
||||||
Pour compresser tous les PDFs déjà en base :
|
|
||||||
```bash
|
|
||||||
# Voir ce qui serait compressé (dry-run)
|
|
||||||
php bin/console app:compress-pdf --dry-run
|
|
||||||
|
|
||||||
# Compresser tous les PDFs
|
|
||||||
php bin/console app:compress-pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
## Commandes utiles
|
## Commandes utiles
|
||||||
Pour restart le container
|
|
||||||
|
### 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
|
```bash
|
||||||
make restart
|
./scripts/release.sh patch # Bump patch (ou minor / major)
|
||||||
```
|
```
|
||||||
Pour lancer les TU
|
|
||||||
```bash
|
Synchronise automatiquement la version dans `VERSION`, `api_platform.yaml` et `nuxt.config.ts`, crée le tag git et pousse les deux repos.
|
||||||
make test
|
|
||||||
|
## Architecture globale
|
||||||
|
|
||||||
|
### Comment ça marche ?
|
||||||
|
|
||||||
```
|
```
|
||||||
Pour accéder au container et lance des commandes
|
┌──────────────────┐ HTTP (JSON) ┌──────────────────┐ SQL ┌────────────┐
|
||||||
```bash
|
│ Frontend │ ◄─────────────────► │ Backend │ ◄──────────► │ PostgreSQL │
|
||||||
make shell
|
│ (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
|
1. L'utilisateur ouvre le navigateur sur `localhost:3001`
|
||||||
make cache-clear
|
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
|
||||||
|
|||||||
132
RELEASE.md
132
RELEASE.md
@@ -1,12 +1,18 @@
|
|||||||
# Guide de Release
|
# 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
|
## Versioning (Semantic Versioning)
|
||||||
- **MINOR** : Nouvelles fonctionnalités rétrocompatibles
|
|
||||||
- **PATCH** : Corrections de bugs rétrocompatibles
|
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.
|
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
|
### Prérequis
|
||||||
|
|
||||||
- Tous les changements doivent être commités
|
- Tous les changements doivent être commités (pas de fichiers modifiés non commités)
|
||||||
- Les tests doivent passer
|
- Les tests doivent passer (`make test`)
|
||||||
- Être sur la branche à releaser (ex: `main`, `develop`)
|
- Être sur la branche à releaser (généralement `develop` ou `master`)
|
||||||
|
|
||||||
### Utilisation du script
|
### 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
|
# Afficher l'aide et la version actuelle
|
||||||
./scripts/release.sh
|
./scripts/release.sh
|
||||||
|
|
||||||
# Bump patch : 1.0.0 → 1.0.1
|
# Bump patch : 1.8.1 → 1.8.2
|
||||||
./scripts/release.sh patch
|
./scripts/release.sh patch
|
||||||
|
|
||||||
# Bump minor : 1.0.0 → 1.1.0
|
# Bump minor : 1.8.1 → 1.9.0
|
||||||
./scripts/release.sh minor
|
./scripts/release.sh minor
|
||||||
|
|
||||||
# Bump major : 1.0.0 → 2.0.0
|
# Bump major : 1.8.1 → 2.0.0
|
||||||
./scripts/release.sh major
|
./scripts/release.sh major
|
||||||
|
|
||||||
# Version spécifique
|
# Version spécifique
|
||||||
./scripts/release.sh 2.0.0
|
./scripts/release.sh 2.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Le script :
|
### Que fait le script ?
|
||||||
1. Met à jour le fichier `VERSION`
|
|
||||||
2. Met à jour `config/packages/api_platform.yaml`
|
1. Vérifie qu'il n'y a pas de changements non commités
|
||||||
3. Crée un commit `chore(release): vX.Y.Z`
|
2. Vérifie/commit le submodule frontend si nécessaire
|
||||||
4. Crée le tag `vX.Y.Z`
|
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
|
### Pousser la release
|
||||||
|
|
||||||
|
Après avoir exécuté le script :
|
||||||
|
|
||||||
```bash
|
```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
|
git push && git push --tags
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -54,12 +70,21 @@ git push && git push --tags
|
|||||||
1. Aller sur le dépôt Gitea
|
1. Aller sur le dépôt Gitea
|
||||||
2. **Releases** > **New Release**
|
2. **Releases** > **New Release**
|
||||||
3. Sélectionner le tag `vX.Y.Z`
|
3. Sélectionner le tag `vX.Y.Z`
|
||||||
4. Titre : `v1.0.0` (ou avec un nom descriptif)
|
4. Titre : `vX.Y.Z` (ou avec un nom descriptif)
|
||||||
5. Description : résumé des changements (voir section Notes de release)
|
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
|
## Notes de release
|
||||||
|
|
||||||
Template pour les notes de release :
|
Template pour les notes de release (à copier dans Gitea) :
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
## Nouveautés
|
## Nouveautés
|
||||||
@@ -73,66 +98,25 @@ Template pour les notes de release :
|
|||||||
## Changements
|
## Changements
|
||||||
- Refactoring de Z
|
- Refactoring de Z
|
||||||
- Mise à jour des dépendances
|
- 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 |
|
Voir [DEPLOY.md](DEPLOY.md) pour les instructions de mise à jour en production.
|
||||||
|---------|-------|
|
|
||||||
| `VERSION` | Source unique de vérité |
|
|
||||||
| `config/packages/api_platform.yaml` | Version affichée dans l'API |
|
|
||||||
| `Inventory_frontend/nuxt.config.ts` | Lit VERSION au build |
|
|
||||||
| Footer de l'app | Affiche `v{{ appVersion }}` |
|
|
||||||
|
|
||||||
## Déploiement en production
|
En résumé :
|
||||||
|
|
||||||
### 1. Base de données
|
|
||||||
|
|
||||||
Dump de la base locale :
|
|
||||||
```bash
|
|
||||||
pg_dump -h localhost -p 5433 -U root -d inventory > backup_v1.0.0.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
Import en production :
|
|
||||||
```bash
|
|
||||||
psql -h <PROD_HOST> -U <PROD_USER> -d inventory < backup_v1.0.0.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Variables d'environnement production
|
|
||||||
|
|
||||||
Créer un fichier `.env.local` en production avec :
|
|
||||||
|
|
||||||
```env
|
|
||||||
APP_ENV=prod
|
|
||||||
APP_SECRET=<générer avec: openssl rand -hex 32>
|
|
||||||
DATABASE_URL="postgresql://user:password@host:5432/inventory?serverVersion=16"
|
|
||||||
CORS_ALLOW_ORIGIN='^https://votre-domaine\.com$'
|
|
||||||
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
|
||||||
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
|
||||||
JWT_PASSPHRASE=<votre-passphrase>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Build production
|
|
||||||
|
|
||||||
Backend :
|
|
||||||
```bash
|
```bash
|
||||||
|
# Sur le serveur de production
|
||||||
|
cd /var/www/Inventory
|
||||||
|
git pull
|
||||||
|
git submodule update --init --recursive
|
||||||
composer install --no-dev --optimize-autoloader
|
composer install --no-dev --optimize-autoloader
|
||||||
php bin/console cache:clear --env=prod
|
|
||||||
php bin/console doctrine:migrations:migrate --no-interaction
|
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)
|
|
||||||
|
|||||||
31
TODO.md
31
TODO.md
@@ -1,2 +1,29 @@
|
|||||||
- Doc: ne pas oublier de mettre `make` dans la documentation.
|
# TODO — MCP Inventory
|
||||||
- Note: le probleme d'IP sous WSL, a ajouter dans la doc.
|
|
||||||
|
## Bugs / Améliorations prioritaires
|
||||||
|
|
||||||
|
### sync_model_type ne fonctionne pas via MCP
|
||||||
|
Le tool `sync_model_type` attend un paramètre `structure` de type `array` (objet JSON imbriqué), mais le SDK MCP PHP ne supporte pas les objets complexes en paramètres — il reçoit un string au lieu d'un array.
|
||||||
|
|
||||||
|
**Solutions possibles :**
|
||||||
|
1. Accepter `structure` comme `string` (JSON encodé) et le décoder manuellement dans le tool
|
||||||
|
2. Créer des tools séparés : `add_product_requirement`, `add_custom_field_requirement`, `remove_requirement` au lieu d'un seul sync
|
||||||
|
3. Passer par des sous-paramètres plats (productTypeIds, customFieldNames, etc.)
|
||||||
|
|
||||||
|
**Impact :** L'IA ne peut pas ajouter de produits ni de champs personnalisés à une catégorie (ModelType) via MCP. Contournement actuel : passer par l'API REST.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Resources MCP en erreur
|
||||||
|
Les 3 Resources (`SchemaResource`, `RolesResource`, `StatsResource`) produisent `[error] Failed to process MCP attribute`. Elles ne bloquent pas les tools mais ne sont pas exposées aux clients.
|
||||||
|
|
||||||
|
**Cause probable :** Incompatibilité du format `#[McpResource]` avec le SDK v0.4 / bundle v0.6.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Améliorations futures
|
||||||
|
|
||||||
|
- [ ] Documentation utilisateur `docs/mcp/README.md` — guide d'utilisation pour les différents clients (Claude Desktop, ChatGPT, Codex)
|
||||||
|
- [ ] Mettre à jour CLAUDE.md avec la section MCP
|
||||||
|
- [ ] Ajouter le tool `upload_document` (upload de fichiers via MCP)
|
||||||
|
- [ ] Tester la compatibilité avec ChatGPT Desktop et Claude Desktop via tunnel
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
"doctrine/doctrine-bundle": "^3.2",
|
"doctrine/doctrine-bundle": "^3.2",
|
||||||
"doctrine/doctrine-migrations-bundle": "^4.0",
|
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||||
"doctrine/orm": "^3.6",
|
"doctrine/orm": "^3.6",
|
||||||
"lexik/jwt-authentication-bundle": "^3.2",
|
|
||||||
"nelmio/cors-bundle": "^2.6",
|
"nelmio/cors-bundle": "^2.6",
|
||||||
|
"nyholm/psr7": "^1.8",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
"phpstan/phpdoc-parser": "^2.3",
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
"symfony/asset": "8.0.*",
|
"symfony/asset": "8.0.*",
|
||||||
@@ -22,16 +22,17 @@
|
|||||||
"symfony/expression-language": "8.0.*",
|
"symfony/expression-language": "8.0.*",
|
||||||
"symfony/flex": "^2",
|
"symfony/flex": "^2",
|
||||||
"symfony/framework-bundle": "8.0.*",
|
"symfony/framework-bundle": "8.0.*",
|
||||||
|
"symfony/mcp-bundle": "^0.6.0",
|
||||||
"symfony/property-access": "8.0.*",
|
"symfony/property-access": "8.0.*",
|
||||||
"symfony/property-info": "8.0.*",
|
"symfony/property-info": "8.0.*",
|
||||||
|
"symfony/rate-limiter": "8.0.*",
|
||||||
"symfony/runtime": "8.0.*",
|
"symfony/runtime": "8.0.*",
|
||||||
"symfony/security-bundle": "8.0.*",
|
"symfony/security-bundle": "8.0.*",
|
||||||
"symfony/serializer": "8.0.*",
|
"symfony/serializer": "8.0.*",
|
||||||
"symfony/twig-bundle": "8.0.*",
|
"symfony/twig-bundle": "8.0.*",
|
||||||
"symfony/uid": "8.0.*",
|
"symfony/uid": "8.0.*",
|
||||||
"symfony/validator": "8.0.*",
|
"symfony/validator": "8.0.*",
|
||||||
"symfony/yaml": "8.0.*",
|
"symfony/yaml": "8.0.*"
|
||||||
"vich/uploader-bundle": "^2.9"
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
@@ -86,9 +87,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
"dama/doctrine-test-bundle": "^8.6",
|
||||||
"friendsofphp/php-cs-fixer": "^3.92",
|
"friendsofphp/php-cs-fixer": "^3.92",
|
||||||
"phpunit/phpunit": "^12.5",
|
"phpunit/phpunit": "^12.5",
|
||||||
"symfony/browser-kit": "8.0.*",
|
"symfony/browser-kit": "8.0.*",
|
||||||
"symfony/css-selector": "8.0.*"
|
"symfony/css-selector": "8.0.*",
|
||||||
|
"symfony/http-client": "8.0.*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1562
composer.lock
generated
1562
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,21 +3,23 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
|
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
|
||||||
|
use DAMA\DoctrineTestBundle\DAMADoctrineTestBundle;
|
||||||
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
|
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
|
||||||
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
|
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
|
||||||
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
|
|
||||||
use Nelmio\CorsBundle\NelmioCorsBundle;
|
use Nelmio\CorsBundle\NelmioCorsBundle;
|
||||||
|
use Symfony\AI\McpBundle\McpBundle;
|
||||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||||
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
||||||
use Symfony\Bundle\TwigBundle\TwigBundle;
|
use Symfony\Bundle\TwigBundle\TwigBundle;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
FrameworkBundle::class => ['all' => true],
|
FrameworkBundle::class => ['all' => true],
|
||||||
TwigBundle::class => ['all' => true],
|
TwigBundle::class => ['all' => true],
|
||||||
SecurityBundle::class => ['all' => true],
|
SecurityBundle::class => ['all' => true],
|
||||||
DoctrineBundle::class => ['all' => true],
|
DoctrineBundle::class => ['all' => true],
|
||||||
DoctrineMigrationsBundle::class => ['all' => true],
|
DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
NelmioCorsBundle::class => ['all' => true],
|
NelmioCorsBundle::class => ['all' => true],
|
||||||
ApiPlatformBundle::class => ['all' => true],
|
ApiPlatformBundle::class => ['all' => true],
|
||||||
LexikJWTAuthenticationBundle::class => ['all' => true],
|
DAMADoctrineTestBundle::class => ['test' => true],
|
||||||
|
McpBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
api_platform:
|
api_platform:
|
||||||
title: Hello API Platform
|
title: Inventory API
|
||||||
version: 1.1.1
|
description: API de gestion d'inventaire industriel — machines, pièces, composants, produits.
|
||||||
|
version: 1.9.1
|
||||||
defaults:
|
defaults:
|
||||||
stateless: false
|
stateless: false
|
||||||
cache_headers:
|
cache_headers:
|
||||||
vary: ['Content-Type', 'Authorization', 'Origin']
|
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||||
|
pagination_items_per_page: 30
|
||||||
|
pagination_maximum_items_per_page: 200
|
||||||
|
pagination_fetch_join_collection: true
|
||||||
|
pagination_partial: false
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ framework:
|
|||||||
secret: '%env(APP_SECRET)%'
|
secret: '%env(APP_SECRET)%'
|
||||||
|
|
||||||
# Note that the session will be started ONLY if you read or write from it.
|
# Note that the session will be started ONLY if you read or write from it.
|
||||||
session: true
|
session:
|
||||||
|
cookie_secure: auto
|
||||||
|
cookie_samesite: lax
|
||||||
|
cookie_httponly: true
|
||||||
|
|
||||||
#esi: true
|
#esi: true
|
||||||
#fragments: true
|
#fragments: true
|
||||||
|
|||||||
10
config/packages/http_discovery.yaml
Normal file
10
config/packages/http_discovery.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
Psr\Http\Message\RequestFactoryInterface: '@http_discovery.psr17_factory'
|
||||||
|
Psr\Http\Message\ResponseFactoryInterface: '@http_discovery.psr17_factory'
|
||||||
|
Psr\Http\Message\ServerRequestFactoryInterface: '@http_discovery.psr17_factory'
|
||||||
|
Psr\Http\Message\StreamFactoryInterface: '@http_discovery.psr17_factory'
|
||||||
|
Psr\Http\Message\UploadedFileFactoryInterface: '@http_discovery.psr17_factory'
|
||||||
|
Psr\Http\Message\UriFactoryInterface: '@http_discovery.psr17_factory'
|
||||||
|
|
||||||
|
http_discovery.psr17_factory:
|
||||||
|
class: Http\Discovery\Psr17Factory
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
lexik_jwt_authentication:
|
|
||||||
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
|
|
||||||
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
|
|
||||||
pass_phrase: '%env(JWT_PASSPHRASE)%'
|
|
||||||
20
config/packages/mcp.yaml
Normal file
20
config/packages/mcp.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
mcp:
|
||||||
|
app: 'inventory'
|
||||||
|
version: '1.0.0'
|
||||||
|
description: 'Inventory MCP Server - Gestion inventaire industriel (machines, pièces, composants, produits)'
|
||||||
|
instructions: |
|
||||||
|
Serveur MCP pour gérer un inventaire industriel.
|
||||||
|
Entités principales : Machine, Composant, Pièce, Produit, Site, Constructeur.
|
||||||
|
Utilisez search_inventory pour chercher dans toutes les entités.
|
||||||
|
Utilisez get_model_type pour comprendre la structure attendue avant de créer un composant ou une pièce.
|
||||||
|
Consultez la resource inventory://schema/entities pour voir le schéma complet.
|
||||||
|
Authentification requise : envoyez X-Profile-Id et X-Profile-Password dans les headers HTTP.
|
||||||
|
client_transports:
|
||||||
|
stdio: true
|
||||||
|
http: true
|
||||||
|
http:
|
||||||
|
path: /_mcp
|
||||||
|
session:
|
||||||
|
store: file
|
||||||
|
directory: '%kernel.cache_dir%/mcp-sessions'
|
||||||
|
ttl: 3600
|
||||||
10
config/packages/rate_limiter.yaml
Normal file
10
config/packages/rate_limiter.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
framework:
|
||||||
|
rate_limiter:
|
||||||
|
mcp_auth:
|
||||||
|
policy: sliding_window
|
||||||
|
limit: 5
|
||||||
|
interval: '1 minute'
|
||||||
|
login:
|
||||||
|
policy: sliding_window
|
||||||
|
limit: 5
|
||||||
|
interval: '1 minute'
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
security:
|
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
|
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||||
password_hashers:
|
password_hashers:
|
||||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||||
@@ -18,44 +23,43 @@ security:
|
|||||||
pattern: ^/(_profiler|_wdt|assets|build)/
|
pattern: ^/(_profiler|_wdt|assets|build)/
|
||||||
security: false
|
security: false
|
||||||
|
|
||||||
login:
|
session_public:
|
||||||
pattern: ^/api/login_check
|
pattern: ^/api/session/profiles?$
|
||||||
|
security: false
|
||||||
|
|
||||||
|
mcp:
|
||||||
|
pattern: ^/_mcp
|
||||||
stateless: true
|
stateless: true
|
||||||
provider: app_user_provider
|
custom_authenticators:
|
||||||
json_login:
|
- App\Mcp\Security\McpHeaderAuthenticator
|
||||||
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
|
|
||||||
|
|
||||||
api:
|
api:
|
||||||
pattern: ^/api
|
pattern: ^/api
|
||||||
stateless: false
|
stateless: false
|
||||||
|
custom_authenticators:
|
||||||
|
- App\Security\SessionProfileAuthenticator
|
||||||
|
|
||||||
main:
|
main:
|
||||||
lazy: true
|
lazy: true
|
||||||
provider: app_user_provider
|
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
|
# Note: Only the *first* matching rule is applied
|
||||||
access_control:
|
access_control:
|
||||||
- { path: ^/api/session/profile, roles: PUBLIC_ACCESS }
|
- { path: ^/api/session/profile$, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/session/profiles, roles: PUBLIC_ACCESS }
|
- { path: ^/api/session/profiles, roles: PUBLIC_ACCESS, methods: [GET] }
|
||||||
- { path: ^/api, roles: PUBLIC_ACCESS }
|
- { path: ^/api/admin, roles: ROLE_ADMIN }
|
||||||
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/test, roles: PUBLIC_ACCESS }
|
- { path: ^/api/health$, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/_mcp, roles: ROLE_USER }
|
||||||
- { path: ^/docs, roles: PUBLIC_ACCESS }
|
- { path: ^/docs, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/contexts, roles: PUBLIC_ACCESS }
|
- { path: ^/contexts, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/\.well-known, roles: PUBLIC_ACCESS }
|
- { path: ^/\.well-known, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
|
- { path: ^/api, roles: ROLE_VIEWER }
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
security:
|
security:
|
||||||
|
|||||||
1756
config/reference.php
1756
config/reference.php
File diff suppressed because it is too large
Load Diff
@@ -12,3 +12,7 @@ api_login_check:
|
|||||||
|
|
||||||
controllers:
|
controllers:
|
||||||
resource: routing.controllers
|
resource: routing.controllers
|
||||||
|
|
||||||
|
mcp:
|
||||||
|
resource: .
|
||||||
|
type: mcp
|
||||||
|
|||||||
@@ -21,3 +21,51 @@ services:
|
|||||||
|
|
||||||
# add more service definitions when explicit configuration is needed
|
# add more service definitions when explicit configuration is needed
|
||||||
# please note that last definitions always *replace* previous ones
|
# please note that last definitions always *replace* previous ones
|
||||||
|
|
||||||
|
App\EventSubscriber\ProductAuditSubscriber:
|
||||||
|
tags:
|
||||||
|
- { name: doctrine.event_subscriber }
|
||||||
|
|
||||||
|
App\EventSubscriber\PieceAuditSubscriber:
|
||||||
|
tags:
|
||||||
|
- { name: doctrine.event_subscriber }
|
||||||
|
|
||||||
|
App\EventSubscriber\ComposantAuditSubscriber:
|
||||||
|
tags:
|
||||||
|
- { name: doctrine.event_subscriber }
|
||||||
|
|
||||||
|
App\Mcp\Security\McpHeaderAuthenticator:
|
||||||
|
arguments:
|
||||||
|
$mcpAuthLimiter: '@limiter.mcp_auth'
|
||||||
|
|
||||||
|
App\OpenApi\OpenApiDecorator:
|
||||||
|
decorates: 'api_platform.openapi.factory'
|
||||||
|
arguments:
|
||||||
|
$decorated: '@.inner'
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
services:
|
||||||
|
App\Service\Sync\ProductSyncStrategy:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
public: true
|
||||||
|
|
||||||
|
App\Service\Sync\ComposantSyncStrategy:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
public: true
|
||||||
|
|
||||||
|
App\Service\Sync\PieceSyncStrategy:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
public: true
|
||||||
|
|
||||||
|
App\Service\ModelTypeSyncService:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
public: true
|
||||||
|
|
||||||
|
App\Service\ReferenceAutoGenerator:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
public: true
|
||||||
|
|||||||
10
deploy/docker/.env.example
Normal file
10
deploy/docker/.env.example
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Symfony
|
||||||
|
APP_ENV=prod
|
||||||
|
APP_DEBUG=0
|
||||||
|
APP_SECRET=change-me
|
||||||
|
|
||||||
|
# Database (use host.docker.internal to reach bare-metal PostgreSQL)
|
||||||
|
DATABASE_URL="postgresql://inventory_user:password@host.docker.internal:5432/inventory_prod?serverVersion=16&charset=utf8"
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ALLOW_ORIGIN='^https?://inventory\.malio-dev\.fr$'
|
||||||
83
deploy/docker/Dockerfile.prod
Normal file
83
deploy/docker/Dockerfile.prod
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# --- Stage 1: Build backend ---
|
||||||
|
FROM php:8.4-cli AS backend-build
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libicu-dev libpq-dev libpng-dev libzip-dev libxml2-dev \
|
||||||
|
unzip curl git \
|
||||||
|
&& docker-php-ext-install -j$(nproc) intl pdo_pgsql zip gd opcache \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY composer.json composer.lock symfony.lock ./
|
||||||
|
RUN APP_ENV=prod APP_DEBUG=0 composer install --no-dev --no-scripts --no-interaction
|
||||||
|
|
||||||
|
COPY bin bin/
|
||||||
|
COPY config config/
|
||||||
|
COPY migrations migrations/
|
||||||
|
COPY public public/
|
||||||
|
COPY src src/
|
||||||
|
COPY templates templates/
|
||||||
|
COPY VERSION VERSION
|
||||||
|
|
||||||
|
RUN composer dump-autoload --optimize --no-dev
|
||||||
|
|
||||||
|
# --- Stage 2: Build frontend ---
|
||||||
|
FROM node:lts-alpine AS frontend-build
|
||||||
|
|
||||||
|
WORKDIR /app/Inventory_frontend
|
||||||
|
COPY Inventory_frontend/package.json Inventory_frontend/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY Inventory_frontend/ ./
|
||||||
|
ENV CI=1 \
|
||||||
|
NUXT_TELEMETRY_DISABLED=1 \
|
||||||
|
NUXT_PUBLIC_API_BASE_URL=/api \
|
||||||
|
NUXT_PUBLIC_APP_BASE=/
|
||||||
|
RUN npm run generate
|
||||||
|
|
||||||
|
# --- Stage 3: Production image ---
|
||||||
|
FROM php:8.4-fpm AS production
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libicu-dev libpq-dev libpng-dev libzip-dev libxml2-dev \
|
||||||
|
nginx supervisor \
|
||||||
|
&& docker-php-ext-install -j$(nproc) intl pdo_pgsql zip gd opcache \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# PHP production config
|
||||||
|
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||||
|
|
||||||
|
# PHP-FPM: forward worker output to stderr for docker logs
|
||||||
|
RUN echo "catch_workers_output = yes" >> /usr/local/etc/php-fpm.d/www.conf \
|
||||||
|
&& echo "decorate_workers_output = no" >> /usr/local/etc/php-fpm.d/www.conf
|
||||||
|
|
||||||
|
# Nginx: log to stdout/stderr
|
||||||
|
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
|
||||||
|
&& ln -sf /dev/stderr /var/log/nginx/error.log
|
||||||
|
|
||||||
|
# Remove default nginx site
|
||||||
|
RUN rm -f /etc/nginx/sites-enabled/default
|
||||||
|
|
||||||
|
# Configs
|
||||||
|
COPY deploy/docker/supervisord.conf /etc/supervisor/conf.d/app.conf
|
||||||
|
COPY deploy/docker/nginx.conf /etc/nginx/sites-enabled/inventory.conf
|
||||||
|
|
||||||
|
# Backend from stage 1
|
||||||
|
COPY --from=backend-build /app /var/www/html
|
||||||
|
|
||||||
|
# Frontend from stage 2
|
||||||
|
COPY --from=frontend-build /app/Inventory_frontend/.output/public /var/www/html/Inventory_frontend/.output/public
|
||||||
|
|
||||||
|
# Symfony needs a .env file to boot (variables are overridden by env_file in docker-compose)
|
||||||
|
RUN echo "APP_ENV=prod" > /var/www/html/.env
|
||||||
|
|
||||||
|
# Permissions
|
||||||
|
RUN mkdir -p /var/www/html/var /var/www/html/var/uploads \
|
||||||
|
&& chown -R www-data:www-data /var/www/html/var
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["supervisord", "-n", "-c", "/etc/supervisor/conf.d/app.conf"]
|
||||||
28
deploy/docker/deploy.sh
Executable file
28
deploy/docker/deploy.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
TAG="${1:-latest}"
|
||||||
|
export INVENTORY_IMAGE_TAG="$TAG"
|
||||||
|
|
||||||
|
echo "==> Deploying inventory:${TAG}..."
|
||||||
|
|
||||||
|
echo "==> Pulling image..."
|
||||||
|
sudo docker compose pull
|
||||||
|
|
||||||
|
echo "==> Starting container..."
|
||||||
|
sudo docker compose up -d
|
||||||
|
|
||||||
|
echo "==> Waiting for container to be ready..."
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
echo "==> Running migrations..."
|
||||||
|
sudo docker compose exec -T -u www-data app php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
echo "==> Clearing cache..."
|
||||||
|
sudo docker compose exec -T -u www-data app php bin/console cache:clear --env=prod
|
||||||
|
sudo docker compose exec -T -u www-data app php bin/console cache:warmup --env=prod
|
||||||
|
|
||||||
|
VERSION=$(sudo docker compose exec -T app cat VERSION)
|
||||||
|
echo "==> Deployed v${VERSION}"
|
||||||
12
deploy/docker/docker-compose.prod.yml
Normal file
12
deploy/docker/docker-compose.prod.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: gitea.malio.fr/malio-dev/inventory:${INVENTORY_IMAGE_TAG:-latest}
|
||||||
|
container_name: inventory-app
|
||||||
|
env_file: .env
|
||||||
|
ports:
|
||||||
|
- "8082:80"
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/var/www/html/var/uploads
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
restart: unless-stopped
|
||||||
36
deploy/docker/nginx.conf
Normal file
36
deploy/docker/nginx.conf
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /var/www/html/Inventory_frontend/.output/public;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
access_log /dev/stdout;
|
||||||
|
error_log /dev/stderr;
|
||||||
|
|
||||||
|
location ^~ /api/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /bundles/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/index\.php(/|$) {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
deploy/docker/supervisord.conf
Normal file
28
deploy/docker/supervisord.conf
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/dev/null
|
||||||
|
logfile_maxbytes=0
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php-fpm]
|
||||||
|
command=php-fpm -F
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
stopasgroup=true
|
||||||
|
stopsignal=QUIT
|
||||||
|
|
||||||
|
[program:nginx]
|
||||||
|
command=nginx -g "daemon off;"
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
stopasgroup=true
|
||||||
|
stopsignal=QUIT
|
||||||
13
deploy/nginx/inventory-docker.conf
Normal file
13
deploy/nginx/inventory-docker.conf
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name inventory.malio-dev.fr;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
299
doc/deployment-docker.md
Normal file
299
doc/deployment-docker.md
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
# Deploiement Docker — Inventory
|
||||||
|
|
||||||
|
## Pre-requis
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ubuntu
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y ca-certificates curl gnupg
|
||||||
|
sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
Se deconnecter/reconnecter pour que le groupe `docker` prenne effet.
|
||||||
|
|
||||||
|
### Nginx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y nginx
|
||||||
|
sudo systemctl enable nginx
|
||||||
|
sudo systemctl start nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL
|
||||||
|
|
||||||
|
PostgreSQL tourne dans un conteneur Docker separe (voir le repo `infra-postgres`).
|
||||||
|
Il doit etre installe et accessible avant de deployer Inventory.
|
||||||
|
|
||||||
|
Creer la base de donnees pour Inventory :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/postgres
|
||||||
|
docker compose exec postgres psql -U admin
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Si le user n'existe pas encore
|
||||||
|
CREATE USER malio WITH PASSWORD 'motdepasse';
|
||||||
|
|
||||||
|
-- Creer la base
|
||||||
|
CREATE DATABASE inventory_prod OWNER malio;
|
||||||
|
\q
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Premiere installation (nouvelle machine)
|
||||||
|
|
||||||
|
Guide complet pour mettre en ligne Inventory sur une machine vierge. Inclut les pre-requis, la BDD et l'app.
|
||||||
|
|
||||||
|
### 1. Installer les pre-requis
|
||||||
|
|
||||||
|
Installer Docker, Nginx et PostgreSQL (voir section Pre-requis ci-dessus).
|
||||||
|
|
||||||
|
### 2. Creer le dossier de deploiement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /var/www/inventory
|
||||||
|
sudo chown -R $(whoami):$(whoami) /var/www/inventory
|
||||||
|
cd /var/www/inventory
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Se connecter au registry Docker de Gitea
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker login gitea.malio.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Username** : le nom d'utilisateur du compte organisation Gitea `MALIO`
|
||||||
|
- **Password** : le token REGISTRY_TOKEN dispo dans le bitwarden
|
||||||
|
|
||||||
|
Le login est sauvegarde dans `~/.docker/config.json`, pas besoin de le refaire a chaque deploiement.
|
||||||
|
|
||||||
|
### 4. Creer les fichiers de deploiement
|
||||||
|
|
||||||
|
Creer `docker-compose.yml` :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: gitea.malio.fr/malio-dev/inventory:${INVENTORY_IMAGE_TAG:-latest}
|
||||||
|
container_name: inventory-app
|
||||||
|
env_file: .env
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/var/www/html/var/uploads
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
Creer `deploy.sh` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
TAG="${1:-latest}"
|
||||||
|
export INVENTORY_IMAGE_TAG="$TAG"
|
||||||
|
|
||||||
|
echo "==> Deploying inventory:${TAG}..."
|
||||||
|
|
||||||
|
echo "==> Pulling image..."
|
||||||
|
docker compose pull
|
||||||
|
|
||||||
|
echo "==> Starting container..."
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
echo "==> Waiting for container to be ready..."
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
echo "==> Running migrations..."
|
||||||
|
docker compose exec -T -u www-data app php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
echo "==> Clearing cache..."
|
||||||
|
docker compose exec -T -u www-data app php bin/console cache:clear --env=prod
|
||||||
|
docker compose exec -T -u www-data app php bin/console cache:warmup --env=prod
|
||||||
|
|
||||||
|
VERSION=$(docker compose exec -T app cat VERSION)
|
||||||
|
echo "==> Deployed v${VERSION}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Rendre executable :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Configurer l'environnement
|
||||||
|
|
||||||
|
Creer `.env` avec les variables suivantes :
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Symfony
|
||||||
|
APP_ENV=prod
|
||||||
|
APP_DEBUG=0
|
||||||
|
APP_SECRET=<generer avec: openssl rand -hex 32>
|
||||||
|
|
||||||
|
# Database (host.docker.internal = la machine hote, ou le PG tourne en Docker)
|
||||||
|
DATABASE_URL="postgresql://malio:password@host.docker.internal:5432/inventory_prod?serverVersion=16&charset=utf8"
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ALLOW_ORIGIN='^https?://inventory\.malio-dev\.fr$'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Creer le dossier uploads
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Configurer Nginx systeme
|
||||||
|
|
||||||
|
Creer `/etc/nginx/sites-available/inventory.conf` :
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name inventory.malio-dev.fr;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Activer le site :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ln -sf /etc/nginx/sites-available/inventory.conf /etc/nginx/sites-enabled/inventory.conf
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Deployer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Importer les donnees (optionnel)
|
||||||
|
|
||||||
|
Si tu as un dump SQL a importer :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis ton PC, envoyer le dump vers le serveur
|
||||||
|
scp inventory.sql user@serveur:/tmp/inventory.sql
|
||||||
|
|
||||||
|
# Sur le serveur, vider la base puis importer
|
||||||
|
cd /var/www/postgres
|
||||||
|
docker compose exec -T postgres psql -U malio inventory_prod -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
|
||||||
|
docker compose exec -T postgres psql -U malio inventory_prod < /tmp/inventory.sql
|
||||||
|
|
||||||
|
# Creer les tables manquantes (si le dump a des erreurs de syntaxe)
|
||||||
|
cd /var/www/inventory
|
||||||
|
docker compose exec -u www-data app php bin/console doctrine:schema:update --force --env=prod
|
||||||
|
|
||||||
|
# Nettoyer
|
||||||
|
rm /tmp/inventory.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Structure finale du dossier
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/www/inventory/
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── deploy.sh
|
||||||
|
├── .env
|
||||||
|
└── uploads/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployer une nouvelle version
|
||||||
|
|
||||||
|
Quand l'app est deja installee, deployer une mise a jour :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/inventory
|
||||||
|
./deploy.sh # deploie la derniere version (latest)
|
||||||
|
./deploy.sh v1.9.4 # deploie une version specifique
|
||||||
|
```
|
||||||
|
|
||||||
|
C'est tout. Le script pull l'image, redemarre le conteneur, lance les migrations et vide le cache.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
### Image seule (pas de changement de schema BDD)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy.sh v1.9.3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Avec rollback de migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Rollback schema (pendant que la version actuelle tourne encore)
|
||||||
|
docker compose exec -T -u www-data app php bin/console doctrine:migrations:migrate prev --no-interaction
|
||||||
|
# 2. Deployer l'ancienne version
|
||||||
|
./deploy.sh v1.9.3
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
Le workflow `.gitea/workflows/build-docker.yml` se declenche automatiquement sur push de tag `v*` :
|
||||||
|
1. Build l'image multi-stage (inclut checkout des submodules pour le frontend)
|
||||||
|
2. Push vers `gitea.malio.fr/malio-dev/inventory:<tag>` et `:latest`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Voir les logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/inventory
|
||||||
|
docker compose logs -f # tous les logs
|
||||||
|
docker compose logs -f --tail=100 # 100 dernieres lignes
|
||||||
|
```
|
||||||
|
|
||||||
|
Logs Symfony :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec app cat var/log/prod.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration depuis l'ancien deploiement (bare-metal)
|
||||||
|
|
||||||
|
Si l'application tourne deja en bare metal :
|
||||||
|
|
||||||
|
1. Installer Docker (voir pre-requis)
|
||||||
|
2. Creer le dossier `/var/www/inventory-docker/` (ne pas ecraser l'ancien)
|
||||||
|
3. Copier les fichiers existants :
|
||||||
|
```bash
|
||||||
|
cp /var/www/inventory/.env /var/www/inventory-docker/.env
|
||||||
|
cp -a /var/www/inventory/var/uploads /var/www/inventory-docker/uploads
|
||||||
|
```
|
||||||
|
4. Creer `docker-compose.yml` et `deploy.sh` dans `/var/www/inventory-docker/` (voir etape 4 ci-dessus)
|
||||||
|
5. Editer `/var/www/inventory-docker/.env` : changer `DATABASE_URL` pour utiliser `host.docker.internal` au lieu de `127.0.0.1`
|
||||||
|
6. Se connecter au registry Gitea (voir etape 3 ci-dessus)
|
||||||
|
7. Mettre a jour Nginx systeme avec la conf reverse proxy (voir etape 7 ci-dessus)
|
||||||
|
8. Arreter l'ancien PHP-FPM/Apache : `sudo systemctl stop php8.4-fpm` ou `sudo systemctl stop apache2`
|
||||||
|
9. Deployer : `cd /var/www/inventory-docker && ./deploy.sh`
|
||||||
|
10. Verifier que tout marche, puis renommer le dossier : `mv /var/www/inventory-docker /var/www/inventory`
|
||||||
@@ -45,34 +45,17 @@ services:
|
|||||||
- "${POSTGRES_PORT:-5433}:5432"
|
- "${POSTGRES_PORT:-5433}:5432"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
pgadmin:
|
adminer:
|
||||||
container_name: pgadmin-${DOCKER_APP_NAME}
|
container_name: adminer-${DOCKER_APP_NAME}
|
||||||
image: dpage/pgadmin4:latest
|
image: adminer:latest
|
||||||
user: root
|
|
||||||
environment:
|
environment:
|
||||||
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@admin.com}
|
ADMINER_DEFAULT_SERVER: db
|
||||||
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin}
|
ADMINER_DESIGN: dracula
|
||||||
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
|
|
||||||
ports:
|
ports:
|
||||||
- "${PGADMIN_PORT:-5050}:80"
|
- "${ADMINER_PORT:-5050}:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
restart: unless-stopped
|
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:
|
volumes:
|
||||||
pg_data:
|
pg_data:
|
||||||
pgadmin_data:
|
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ POSTGRES_DB=inventory
|
|||||||
POSTGRES_USER=root
|
POSTGRES_USER=root
|
||||||
POSTGRES_PASSWORD=root
|
POSTGRES_PASSWORD=root
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
XDEBUG_CLIENT_HOST=host.docker.internal
|
XDEBUG_CLIENT_HOST=host.docker.internal
|
||||||
846
docs/BACKEND.md
Normal file
846
docs/BACKEND.md
Normal 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
|
||||||
278
docs/CUSTOM_FIELDS_AUDIT_RECOVERY.md
Normal file
278
docs/CUSTOM_FIELDS_AUDIT_RECOVERY.md
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
# Champs Personnalises - Diagnostic Et Recuperation
|
||||||
|
|
||||||
|
Date : 2026-03-23
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
Un bug sur la sauvegarde des categories (`ModelType`) pouvait recreer des definitions de champs personnalises avec de nouveaux IDs.
|
||||||
|
|
||||||
|
Effet de bord :
|
||||||
|
- les `CustomFieldValue` existants restaient lies aux anciens `CustomField`
|
||||||
|
- puis etaient supprimes en cascade
|
||||||
|
- resultat visible : apres modification d'une categorie, certaines valeurs de champs perso disparaissaient
|
||||||
|
|
||||||
|
Le correctif preventif a ete fait :
|
||||||
|
- conservation des `id/customFieldId` cote frontend pour `PIECE/PRODUCT`
|
||||||
|
- matching backend plus robuste sur `id`, puis `orderIndex`, puis nom
|
||||||
|
|
||||||
|
Ce document couvre uniquement :
|
||||||
|
- comment detecter ce qui manque
|
||||||
|
- comment lire le listing
|
||||||
|
- comment identifier ce qui est recuperable depuis l'audit
|
||||||
|
- comment restaurer proprement
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commandes Disponibles
|
||||||
|
|
||||||
|
### 1. Lister tous les champs perso manquants ou vides
|
||||||
|
|
||||||
|
Dans le conteneur :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:check-missing-custom-field-values
|
||||||
|
```
|
||||||
|
|
||||||
|
Variantes utiles :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:check-missing-custom-field-values --entity=piece
|
||||||
|
php bin/console app:check-missing-custom-field-values --entity=composant
|
||||||
|
php bin/console app:check-missing-custom-field-values --max-rows=1000
|
||||||
|
php bin/console app:check-missing-custom-field-values --limit=500 --max-rows=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Afficher uniquement les cas recuperables depuis l'audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:check-missing-custom-field-values --recoverable-only
|
||||||
|
```
|
||||||
|
|
||||||
|
Variantes :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:check-missing-custom-field-values --entity=piece --recoverable-only
|
||||||
|
php bin/console app:check-missing-custom-field-values --entity=composant --recoverable-only
|
||||||
|
php bin/console app:check-missing-custom-field-values --recoverable-only --max-rows=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Dry-run de restauration pour une piece
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:restore-piece-custom-field-values <pieceId>
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:restore-piece-custom-field-values cl731386df55fcb9e6a01e0a63
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Appliquer la restauration pour une piece
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:restore-piece-custom-field-values <pieceId> --apply
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Colonnes Du Listing
|
||||||
|
|
||||||
|
La commande `app:check-missing-custom-field-values` affiche :
|
||||||
|
|
||||||
|
- `Entity` : `piece` ou `composant`
|
||||||
|
- `ID` : identifiant de l'entite
|
||||||
|
- `Name` : nom de l'entite
|
||||||
|
- `Reference` : reference metier si presente
|
||||||
|
- `Category` : nom de la categorie (`ModelType`)
|
||||||
|
- `Field` : nom du champ personnalise attendu par la categorie
|
||||||
|
- `Issue` : `missing` ou `empty`
|
||||||
|
- `Recoverable` : `yes` ou `no`
|
||||||
|
- `Audit value` : derniere valeur non vide retrouvee dans l'audit si disponible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Signification Des Statuts
|
||||||
|
|
||||||
|
### `missing`
|
||||||
|
|
||||||
|
Il n'existe actuellement **aucune** ligne `CustomFieldValue` pour ce champ sur l'entite.
|
||||||
|
|
||||||
|
Cela peut vouloir dire :
|
||||||
|
- la valeur n'a jamais ete saisie
|
||||||
|
- la valeur a ete perdue lors du bug
|
||||||
|
- le champ a ete ajoute plus tard sur la categorie sans initialisation des anciennes entites
|
||||||
|
|
||||||
|
### `empty`
|
||||||
|
|
||||||
|
La ligne `CustomFieldValue` existe, mais sa valeur est vide.
|
||||||
|
|
||||||
|
Cela est plus suspect qu'un `missing`, mais ne prouve pas a lui seul une perte.
|
||||||
|
|
||||||
|
### `Recoverable = yes`
|
||||||
|
|
||||||
|
L'audit contient au moins une ancienne valeur non vide pour ce champ.
|
||||||
|
|
||||||
|
En pratique :
|
||||||
|
- c'est le signal le plus utile
|
||||||
|
- ce sont les cas a traiter en priorite
|
||||||
|
- ces cas sont potentiellement restaurables automatiquement
|
||||||
|
|
||||||
|
### `Recoverable = no`
|
||||||
|
|
||||||
|
L'audit de cette entite ne contient pas de valeur non vide exploitable pour ce champ.
|
||||||
|
|
||||||
|
Cela ne veut **pas** forcement dire qu'il n'y a jamais eu de valeur.
|
||||||
|
Cela veut simplement dire :
|
||||||
|
- rien de recuperable n'a ete trouve dans les logs d'audit consultes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lecture Des Cas Typiques
|
||||||
|
|
||||||
|
### Cas 1
|
||||||
|
|
||||||
|
```text
|
||||||
|
piece ... Roulement ... Diametre ... missing ... no
|
||||||
|
```
|
||||||
|
|
||||||
|
Interpretation :
|
||||||
|
- le champ `Diametre` est attendu sur cette piece
|
||||||
|
- aucune valeur n'existe actuellement
|
||||||
|
- l'audit ne permet pas de retrouver une ancienne valeur
|
||||||
|
|
||||||
|
Conclusion :
|
||||||
|
- non recuperable automatiquement
|
||||||
|
- a verifier metierement si la valeur a deja existe ou non
|
||||||
|
|
||||||
|
### Cas 2
|
||||||
|
|
||||||
|
```text
|
||||||
|
piece ... Arbre ... Diametre ... empty ... yes ... 35 mm
|
||||||
|
```
|
||||||
|
|
||||||
|
Interpretation :
|
||||||
|
- une ligne de valeur existe mais elle est vide
|
||||||
|
- l'audit montre qu'une ancienne valeur `35 mm` existait
|
||||||
|
|
||||||
|
Conclusion :
|
||||||
|
- cas typique de restauration automatique possible
|
||||||
|
|
||||||
|
### Cas 3
|
||||||
|
|
||||||
|
```text
|
||||||
|
piece ... Joint ... Matiere ... missing ... yes ... NBR
|
||||||
|
```
|
||||||
|
|
||||||
|
Interpretation :
|
||||||
|
- la valeur n'existe plus du tout
|
||||||
|
- l'audit permet de retrouver `NBR`
|
||||||
|
|
||||||
|
Conclusion :
|
||||||
|
- forte probabilite de perte historique
|
||||||
|
- recuperable automatiquement
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priorisation Recommandee
|
||||||
|
|
||||||
|
Ordre de traitement conseille :
|
||||||
|
|
||||||
|
1. `empty + yes`
|
||||||
|
2. `missing + yes`
|
||||||
|
3. `empty + no`
|
||||||
|
4. `missing + no`
|
||||||
|
|
||||||
|
Pourquoi :
|
||||||
|
- les `yes` sont les seuls cas recuperables automatiquement
|
||||||
|
- les `empty` indiquent souvent une valeur ecrasee
|
||||||
|
- les `missing no` sont nombreux mais souvent ambigus
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Procedure Recommandee
|
||||||
|
|
||||||
|
### Etape 1 - Scanner globalement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:check-missing-custom-field-values --recoverable-only --max-rows=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Etape 2 - Identifier les pieces prioritaires
|
||||||
|
|
||||||
|
Chercher :
|
||||||
|
- les pieces les plus critiques metierement
|
||||||
|
- les categories fortement touchees (`Roulement`, `Joint`, `Arbre`, etc.)
|
||||||
|
- les cas avec valeur d'audit explicite
|
||||||
|
|
||||||
|
### Etape 3 - Faire un dry-run piece par piece
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:restore-piece-custom-field-values <pieceId>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Etape 4 - Appliquer uniquement apres verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:restore-piece-custom-field-values <pieceId> --apply
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limites Actuelles
|
||||||
|
|
||||||
|
### Ce qui est pris en charge
|
||||||
|
|
||||||
|
- diagnostic global sur les `pieces`
|
||||||
|
- diagnostic global sur les `composants`
|
||||||
|
- restauration automatique ciblee sur les `pieces`
|
||||||
|
|
||||||
|
### Ce qui n'est pas encore automatise
|
||||||
|
|
||||||
|
- restauration automatique en masse
|
||||||
|
- restauration automatique des `composants`
|
||||||
|
- reconstitution si l'audit ne contient aucune ancienne valeur exploitable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Interpretation Metier
|
||||||
|
|
||||||
|
Le listing global ne doit pas etre lu comme :
|
||||||
|
|
||||||
|
> "866 valeurs ont ete perdues"
|
||||||
|
|
||||||
|
Il doit etre lu comme :
|
||||||
|
|
||||||
|
> "866 couples entite/champ sont actuellement manquants ou vides par rapport aux definitions de categories"
|
||||||
|
|
||||||
|
Parmi eux :
|
||||||
|
- certains n'ont jamais ete renseignes
|
||||||
|
- certains ont probablement ete perdus
|
||||||
|
- seuls les cas `Recoverable = yes` sont candidates a une recuperation automatique fiable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commandes Resumees
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Tout lister
|
||||||
|
php bin/console app:check-missing-custom-field-values
|
||||||
|
|
||||||
|
# Afficher uniquement les cas recuperables
|
||||||
|
php bin/console app:check-missing-custom-field-values --recoverable-only
|
||||||
|
|
||||||
|
# Scanner seulement les pieces
|
||||||
|
php bin/console app:check-missing-custom-field-values --entity=piece --recoverable-only
|
||||||
|
|
||||||
|
# Scanner seulement les composants
|
||||||
|
php bin/console app:check-missing-custom-field-values --entity=composant --recoverable-only
|
||||||
|
|
||||||
|
# Dry-run de restauration d'une piece
|
||||||
|
php bin/console app:restore-piece-custom-field-values <pieceId>
|
||||||
|
|
||||||
|
# Application reelle
|
||||||
|
php bin/console app:restore-piece-custom-field-values <pieceId> --apply
|
||||||
|
```
|
||||||
|
|
||||||
144
docs/CUSTOM_FIELDS_RECOVERABLE_RESULTS_2026-03-23.md
Normal file
144
docs/CUSTOM_FIELDS_RECOVERABLE_RESULTS_2026-03-23.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# Resultats Recuperables - Champs Personnalises
|
||||||
|
|
||||||
|
Date : 2026-03-23
|
||||||
|
Source : `php bin/console app:check-missing-custom-field-values --recoverable-only`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resume
|
||||||
|
|
||||||
|
- Total : 40 cas recuperables
|
||||||
|
- Pieces : 40
|
||||||
|
- Composants : 0
|
||||||
|
- Type de probleme observe : uniquement `empty`
|
||||||
|
- Categorie dominante : `Arbre`
|
||||||
|
- Champ le plus frequent : `Diamètre`
|
||||||
|
|
||||||
|
Conclusion :
|
||||||
|
- il n'y a pas ici une grande dispersion de cas heterogenes
|
||||||
|
- la quasi-totalite du lot correspond a des valeurs historisees recuperables sur des pieces de categorie `Arbre`
|
||||||
|
- ces cas sont de bons candidats a une restauration automatique
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tableau
|
||||||
|
|
||||||
|
| Entity | ID | Name | Reference | Category | Field | Issue | Recoverable | Audit value |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| piece | `clc08fbdcd334ed869772d98ee` | Arbre de la cage écureuil pied E4 | | Arbre | Diamètre | empty | yes | 45 mm |
|
||||||
|
| piece | `cl8570d729efd017c12a2d5c3d` | Arbre du tambour tête E7 | | Arbre | Diamètre | empty | yes | 40 mm |
|
||||||
|
| piece | `cle1db7051dbef91fc009073a6` | Arbre de la cage écureuil pied E6 | | Arbre | Diamètre | empty | yes | 45 mm |
|
||||||
|
| piece | `cl9282d473ff01b5d1df8bc945` | Arbre E1 | | Arbre | Diamètre | empty | yes | 50 |
|
||||||
|
| piece | `cl22e81a055f9c393d8d2c82fc` | Arbre du palier pied E3 | | Arbre | Diamètre | empty | yes | 50 mm |
|
||||||
|
| piece | `clca9379d4aa76de6772ebbe1a` | Arbre pignon | `0-5720-00` | Arbre | Type | empty | yes | 20 DTS |
|
||||||
|
| piece | `clc97804ec0bf8b6d9bb530717` | Arbre du palier tête E2 E2B | | Arbre | Diamètre | empty | yes | 40 |
|
||||||
|
| piece | `cl1597f1500c1052e9e7a95c51` | Arbre du palier pied E2 E2B | | Arbre | Diamètre | empty | yes | 35 mm |
|
||||||
|
| piece | `cleea7ff4b9b1a6396a0bb9ea8` | Arbre du tambour tête E1 | | Arbre | Diamètre | empty | yes | 70 mm |
|
||||||
|
| piece | `cl5c71e3777146de5508e07156` | Arbre de la cage écureuil pied E1 | | Arbre | Diamètre | empty | yes | 50 mm |
|
||||||
|
| piece | `cl731386df55fcb9e6a01e0a63` | Arbre de la cage écureuil pied E2 E2B | | Arbre | Diamètre | empty | yes | 35 mm |
|
||||||
|
| piece | `clfaf128312d5c253d928f47ac` | Arbre du palier pied E4 | | Arbre | Diamètre | empty | yes | 45 mm |
|
||||||
|
| piece | `clbf9f0070ebd464b3c309c646` | Arbre du palier pied E8 | | Arbre | Diamètre | empty | yes | 50 mm |
|
||||||
|
| piece | `clc7c00cad416477d4438cd61a` | Arbre du tambour tête E8 | | Arbre | Diamètre | empty | yes | 70 mm |
|
||||||
|
| piece | `cl3f01a1a514423359405a4825` | Arbre du palier tête E7 | | Arbre | Diamètre | empty | yes | 40 mm |
|
||||||
|
| piece | `clf16e543545eddd01b20077df` | Arbre du tambour tête E5 | | Arbre | Diamètre | empty | yes | 55 mm |
|
||||||
|
| piece | `clb6c61ebb8da2c4361265f766` | Arbre du palier tête E6 | | Arbre | Diamètre | empty | yes | 55 mm |
|
||||||
|
| piece | `cl8da1b875191c617e5852bf81` | Arbre du tambour tête E2 E2B | | Arbre | Diamètre | empty | yes | 40 mm |
|
||||||
|
| piece | `cl8da1b875191c617e5852bf81` | Arbre du tambour tête E2 E2B | | Arbre | Diamètre palier | empty | yes | 40 |
|
||||||
|
| piece | `cla82d44c52d7eb2a592f4120d` | Arbre du palier pied E7 | | Arbre | Diamètre | empty | yes | 35 mm |
|
||||||
|
| piece | `clf8562d27a542f86f8f4a5629` | Arbre du palier tête E8 | | Arbre | Diamètre | empty | yes | 70 mm |
|
||||||
|
| piece | `clde7ee756c2cf264c062b861d` | Arbre du palier pied E6 | | Arbre | Diamètre | empty | yes | 45 mm |
|
||||||
|
| piece | `cl6667d159f6d07ba77fa79b39` | Arbre de la cage écureuil pied E5 | | Arbre | Diamètre | empty | yes | 45 mm |
|
||||||
|
| piece | `cl455ad597bcee2a8e3c099420` | Arbre du palier pied E5 | | Arbre | Diamètre | empty | yes | 45 mm |
|
||||||
|
| piece | `cl22c13dbc4d38a1f846323ae6` | Arbre de la cage écureuil pied E3 | | Arbre | Diamètre | empty | yes | 50 mm |
|
||||||
|
| piece | `cl1406ef19de58fdd1adf40221` | Arbre de la cage écureuil pied E7 | | Arbre | Diamètre | empty | yes | 35 mm |
|
||||||
|
| piece | `clafaa71cbf49777fbb8415f19` | Arbre du tambour tête E3 | | Arbre | Diamètre | empty | yes | 70 mm |
|
||||||
|
| piece | `cle255aea44755dbbe7e466a99` | Arbre du palier tête E5 | | Arbre | Diamètre | empty | yes | 55 mm |
|
||||||
|
| piece | `cl3d978dd4b071daff8fb185f7` | Arbre du palier pied E1 | | Arbre | Diamètre | empty | yes | 50 mm |
|
||||||
|
| piece | `cl5e8aba1867089544d71fe2c5` | Arbre du palier tête E4 | | Arbre | Diamètre | empty | yes | 55 mm |
|
||||||
|
| piece | `cl04c79cd568894a5674b46a31` | Arbre du palier pied élévateur expédition | | Arbre | Diamètre | empty | yes | 50 mm |
|
||||||
|
| piece | `cl50fe870a07e42759b37b511f` | Arbre du tambour tête E6 | | Arbre | Diamètre | empty | yes | 55 mm |
|
||||||
|
| piece | `cl531dde45c3fc64c1a3b16ca0` | Arbre de la cage écureuil pied élévateur expédition | | Arbre | Diamètre | empty | yes | 50 mm |
|
||||||
|
| piece | `cleca9e4baa9e9205f1dd948e1` | Arbre du palier tête E3 | | Arbre | Diamètre | empty | yes | 70 mm |
|
||||||
|
| piece | `cl5ee293dc7b61feba510082a4` | Arbre du tambour tête élévateur expédition | | Arbre | Diamètre | empty | yes | 70 mm |
|
||||||
|
| piece | `cled68ff759b1f02f482990fb3` | Arbre du tambour du palier tête E11 | | Arbre | Diamètre | empty | yes | 70 mm |
|
||||||
|
| piece | `cmkr0qjw5004s1eq6pen63x7j` | Arbre du palier tête E1 | | Arbre | Diamètre | empty | yes | 70 mm |
|
||||||
|
| piece | `cl2c3570dd00372fed44cd5a43` | Arbre du palier tête élévateur expédition | `Décolleter a Ø40 pour réducteur` | Arbre | Diamètre | empty | yes | 70 mm |
|
||||||
|
| piece | `cl7b3702f04d24d87e47232a14` | Arbre du tambour tête E4 | | Arbre | Diamètre | empty | yes | 55 mm |
|
||||||
|
| piece | `cldd656c6092225f53a22badc0` | Arbre de la cage écureuil pied E8 | | Arbre | Diamètre | empty | yes | 50 mm |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Observations
|
||||||
|
|
||||||
|
### 1. Lot tres homogene
|
||||||
|
|
||||||
|
Le resultat est tres concentre :
|
||||||
|
- uniquement des `pieces`
|
||||||
|
- uniquement des cas `empty`
|
||||||
|
- presque uniquement sur le champ `Diamètre`
|
||||||
|
- presque toute la liste est dans la categorie `Arbre`
|
||||||
|
|
||||||
|
Cela ressemble davantage a une vague de perte coherente qu'a du bruit metier aleatoire.
|
||||||
|
|
||||||
|
### 2. Valeurs d'audit tres exploitables
|
||||||
|
|
||||||
|
Les valeurs retrouvees sont directement reutilisables :
|
||||||
|
- `35 mm`
|
||||||
|
- `40 mm`
|
||||||
|
- `45 mm`
|
||||||
|
- `50 mm`
|
||||||
|
- `55 mm`
|
||||||
|
- `70 mm`
|
||||||
|
- `20 DTS`
|
||||||
|
|
||||||
|
### 3. Cas particulier multi-champs
|
||||||
|
|
||||||
|
L'entite `cl8da1b875191c617e5852bf81` a deux champs recuperables :
|
||||||
|
- `Diamètre`
|
||||||
|
- `Diamètre palier`
|
||||||
|
|
||||||
|
### 4. Piece initialement signalee
|
||||||
|
|
||||||
|
La piece `cl731386df55fcb9e6a01e0a63` est bien presente dans le resultat :
|
||||||
|
|
||||||
|
- nom : `Arbre de la cage écureuil pied E2 E2B`
|
||||||
|
- champ : `Diamètre`
|
||||||
|
- valeur recuperable : `35 mm`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priorite De Restauration
|
||||||
|
|
||||||
|
Priorite haute :
|
||||||
|
- restaurer tout ce lot `Arbre` en premier
|
||||||
|
- ce sont des cas homogènes et recuperables
|
||||||
|
|
||||||
|
Ordre recommande :
|
||||||
|
|
||||||
|
1. piece `cl731386df55fcb9e6a01e0a63`
|
||||||
|
2. piece avec plusieurs champs recuperables : `cl8da1b875191c617e5852bf81`
|
||||||
|
3. reste du lot `Arbre`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commandes Utiles
|
||||||
|
|
||||||
|
Dry-run pour une piece :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:restore-piece-custom-field-values <pieceId>
|
||||||
|
```
|
||||||
|
|
||||||
|
Application reelle :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:restore-piece-custom-field-values <pieceId> --apply
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple pour la piece initiale :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:restore-piece-custom-field-values cl731386df55fcb9e6a01e0a63
|
||||||
|
php bin/console app:restore-piece-custom-field-values cl731386df55fcb9e6a01e0a63 --apply
|
||||||
|
```
|
||||||
|
|
||||||
137
docs/DOUBLONS_REFERENCES_COMPOSANTS.md
Normal file
137
docs/DOUBLONS_REFERENCES_COMPOSANTS.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Doublons de références — Composants
|
||||||
|
|
||||||
|
> Généré le 2026-03-26 à partir du dump de production `inventory (17).sql.gz`
|
||||||
|
|
||||||
|
**13 références en doublon** pour un total de **41 composants concernés**.
|
||||||
|
|
||||||
|
## Résumé
|
||||||
|
|
||||||
|
| Référence | Nb | Composants |
|
||||||
|
|---|---|---|
|
||||||
|
| Tambour lisse | 9 | Tambour tête E1, E2 E2B, E3, E4, E5, E6, E7, E8, élévateur expédition |
|
||||||
|
| FY50 FM | 5 | Opposé commande Vis 21, Palier Opposé Commande Vis 19, Palier Vis 18 (côté commande), Palier Vis 21 (côté commande), Palier côté commande Vis 20 |
|
||||||
|
| PB 2220 | 4 | Réducteur pendulaire E1, E3, E8, élévateur expédition |
|
||||||
|
| SNU 511 609 | 4 | Palier pied E1, E3, E8, élévateur expédition |
|
||||||
|
| SNU 516 613 | 4 | Palier tête E1, E3, E8, élévateur expédition |
|
||||||
|
| 512610 SNH SKF | 3 | Palier tête E4, E5, E6 |
|
||||||
|
| FY 50 FM | 2 | Palier V18 (opposé commande), Palier côté commande Vis 19 |
|
||||||
|
| FY60 | 2 | Palier Vis 17 (coté commande), Palier Vis 17 (opposé commande) |
|
||||||
|
| FY60 WF | 2 | Palier Opposé commande Vis 22, Palier côté commande Vis 22 |
|
||||||
|
| PB 2012 | 2 | Réducteur pendulaire E2-E2B, E7 |
|
||||||
|
| PB 2112 | 2 | Réducteur pendulaire E4, E6 |
|
||||||
|
| SNU 509 | 2 | Palier tête E2 et E2B, E7 |
|
||||||
|
| VCF 207 | 2 | Palier pied E2 et E2B, E7 |
|
||||||
|
|
||||||
|
## Détail par référence
|
||||||
|
|
||||||
|
### Tambour lisse (9 composants)
|
||||||
|
|
||||||
|
| Nom | ID |
|
||||||
|
|---|---|
|
||||||
|
| Tambour tête E1 | cl4660bae41d2af254e6c3b726 |
|
||||||
|
| Tambour tête E2 E2B | cl5e9c6b18bccd38517026dc1c |
|
||||||
|
| Tambour tête E3 | clba5633e840726188261145f9 |
|
||||||
|
| Tambour tête E4 | cl10c0924d10135c5f515378ac |
|
||||||
|
| Tambour tête E5 | cl7f254c23161d9c853c3e6d92 |
|
||||||
|
| Tambour tête E6 | cl3dbac5194bc192a0589465ba |
|
||||||
|
| Tambour tête E7 | cla833681664bb851ca61aca51 |
|
||||||
|
| Tambour tête E8 | cl36d84884cad86fbc92dba133 |
|
||||||
|
| Tambour tête élévateur expédition | cl5a8f9656aa7e14c012f30700 |
|
||||||
|
|
||||||
|
### FY50 FM (5 composants)
|
||||||
|
|
||||||
|
| Nom | ID |
|
||||||
|
|---|---|
|
||||||
|
| Opposé commande Vis 21 | cl055eff4115f9c75d850c9459 |
|
||||||
|
| Palier Opposé Commande Vis 19 | cl6831a23892243bbaa2f823b4 |
|
||||||
|
| Palier Vis 18 (côté commande) | cld1391112241147dc064b35da |
|
||||||
|
| Palier Vis 21 (côté commande) | cl9f8253f4537a657f7378a2e9 |
|
||||||
|
| Palier côté commande Vis 20 | cl203937da81135d8b34d7bb0f |
|
||||||
|
|
||||||
|
### PB 2220 (4 composants)
|
||||||
|
|
||||||
|
| Nom | ID |
|
||||||
|
|---|---|
|
||||||
|
| Réducteur pendulaire E1 | cla59f867feafbb0937862064a |
|
||||||
|
| Réducteur pendulaire E3 | cl33683086c4de13f80db59606 |
|
||||||
|
| Réducteur pendulaire E8 | cl94fb77cf922aa1462a8d13cc |
|
||||||
|
| Réducteur pendulaire élévateur expédition | cl3f02941228dfef4c91a75d1a |
|
||||||
|
|
||||||
|
### SNU 511 609 (4 composants)
|
||||||
|
|
||||||
|
| Nom | ID |
|
||||||
|
|---|---|
|
||||||
|
| Palier pied E1 | cl81e703e9f200163a4ea473df |
|
||||||
|
| Palier pied E3 | cl3d38928c11d70614bb09fe8e |
|
||||||
|
| Palier pied E8 | cl78b79a8f90f12842b5683403 |
|
||||||
|
| Palier pied élévateur expédition | clf35b4455617ae94f2a1add46 |
|
||||||
|
|
||||||
|
### SNU 516 613 (4 composants)
|
||||||
|
|
||||||
|
| Nom | ID |
|
||||||
|
|---|---|
|
||||||
|
| Palier tête E1 | cmkr0nq1a004e1eq6v6ubxlfl |
|
||||||
|
| Palier tête E3 | cl92b8908c71616c542d958007 |
|
||||||
|
| Palier tête E8 | clce6dde0609d90764da383d75 |
|
||||||
|
| Palier tête élévateur expédition | clb7322b05f9a4554fa5a75d5a |
|
||||||
|
|
||||||
|
### 512610 SNH SKF (3 composants)
|
||||||
|
|
||||||
|
| Nom | ID |
|
||||||
|
|---|---|
|
||||||
|
| Palier tête E4 | cl8e90ad1b633046f5f1344b93 |
|
||||||
|
| Palier tête E5 | clbbe4096490ff89b08644c793 |
|
||||||
|
| Palier tête E6 | cl51c9a1c3dce52856e3404a38 |
|
||||||
|
|
||||||
|
### FY 50 FM (2 composants)
|
||||||
|
|
||||||
|
| Nom | ID |
|
||||||
|
|---|---|
|
||||||
|
| Palier V18 (opposé commande) | cl2ff55d9fa9c52c18f2d88222 |
|
||||||
|
| Palier côté commande Vis 19 | clbddd1dca5efa881b23eaa1cd |
|
||||||
|
|
||||||
|
### FY60 (2 composants)
|
||||||
|
|
||||||
|
| Nom | ID |
|
||||||
|
|---|---|
|
||||||
|
| Palier Vis 17 (coté commande) | cl02b0a0a543cc699681b6ae8c |
|
||||||
|
| Palier Vis 17 (opposé commande) | clc0ba9245b63613307cc26a19 |
|
||||||
|
|
||||||
|
### FY60 WF (2 composants)
|
||||||
|
|
||||||
|
| Nom | ID |
|
||||||
|
|---|---|
|
||||||
|
| Palier Opposé commande Vis 22 | cl318b49462097fb2e1f793305 |
|
||||||
|
| Palier côté commande Vis 22 | cl6bc818a2d8661b5e0ce2d0c0 |
|
||||||
|
|
||||||
|
### PB 2012 (2 composants)
|
||||||
|
|
||||||
|
| Nom | ID |
|
||||||
|
|---|---|
|
||||||
|
| Réducteur pendulaire E2-E2B | cl9b746a66f583fc85b3d176c4 |
|
||||||
|
| Réducteur pendulaire E7 | clc0db3b431d75c6355608efd5 |
|
||||||
|
|
||||||
|
### PB 2112 (2 composants)
|
||||||
|
|
||||||
|
| Nom | ID |
|
||||||
|
|---|---|
|
||||||
|
| Réducteur pendulaire E4 | clf5a1c9e1f8202b632f173bd3 |
|
||||||
|
| Réducteur pendulaire E6 | cle1899c6522cb8b8abd366a24 |
|
||||||
|
|
||||||
|
### SNU 509 (2 composants)
|
||||||
|
|
||||||
|
| Nom | ID |
|
||||||
|
|---|---|
|
||||||
|
| Palier tête E2 et E2B | cl4e600dcadb34f817a888ffa3 |
|
||||||
|
| Palier tête E7 | cl84271e9ab5351cbd188b0d3a |
|
||||||
|
|
||||||
|
### VCF 207 (2 composants)
|
||||||
|
|
||||||
|
| Nom | ID |
|
||||||
|
|---|---|
|
||||||
|
| Palier pied E2 et E2B | cld516a118bb1c478722a1d39b |
|
||||||
|
| Palier pied E7 | cl908dbf171798f087b12d6f2a |
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
Ces doublons sont des composants **distincts** (noms différents, installés sur différents élévateurs) qui partagent la même référence fournisseur. Il ne s'agit pas nécessairement d'entrées à fusionner, mais de pièces identiques utilisées à plusieurs emplacements.
|
||||||
399
docs/FONCTIONNEMENT.md
Normal file
399
docs/FONCTIONNEMENT.md
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
# Fonctionnement de l'application Inventory
|
||||||
|
|
||||||
|
## 1. A quoi sert cette application ?
|
||||||
|
|
||||||
|
Inventory est une application de **gestion d'inventaire industriel**. Elle permet de suivre et documenter l'ensemble du parc de machines d'une entreprise, avec tous les elements qui les composent : composants, pieces detachees et produits consommables.
|
||||||
|
|
||||||
|
L'objectif principal est d'avoir une **vue complete et structuree** de chaque machine : quels composants elle contient, quelles pieces sont montees dessus, quels produits sont utilises, qui les fabrique, combien ils coutent, et toute la documentation associee (manuels, fiches techniques, etc.).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Les entites principales
|
||||||
|
|
||||||
|
L'application s'articule autour de 7 entites fondamentales :
|
||||||
|
|
||||||
|
```
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
| SITE |
|
||||||
|
| (usine, atelier, entrepot...) |
|
||||||
|
| - nom, adresse, contact, telephone, ville, code postal |
|
||||||
|
| - couleur (pour identification visuelle) |
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
|
|
||||||
|
| contient des
|
||||||
|
v
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
| MACHINE |
|
||||||
|
| (machine industrielle sur un site) |
|
||||||
|
| - nom (unique), reference, prix |
|
||||||
|
| - rattachee a 1 site obligatoirement |
|
||||||
|
| - peut avoir plusieurs fournisseurs/constructeurs |
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
|
|
||||||
|
| est composee de
|
||||||
|
v
|
||||||
|
+-------------------+ +-------------------+ +-------------------+
|
||||||
|
| COMPOSANT | | PIECE | | PRODUIT |
|
||||||
|
| (element fonct.) | | (piece detachee) | | (consommable) |
|
||||||
|
| - nom, ref, desc | | - nom, ref, desc | | - nom, ref |
|
||||||
|
| - prix | | - prix | | - prix fournisseur |
|
||||||
|
| - categorie | | - categorie | | - categorie |
|
||||||
|
| - fournisseurs | | - fournisseurs | | - fournisseurs |
|
||||||
|
+-------------------+ +-------------------+ +-------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
### Site
|
||||||
|
Un **site** represente un lieu physique : une usine, un atelier, un entrepot. Chaque site possede un nom, une adresse complete et un contact. Toutes les machines sont obligatoirement rattachees a un site.
|
||||||
|
|
||||||
|
### Machine
|
||||||
|
Une **machine** est l'entite centrale. C'est un equipement industriel installe sur un site. Chaque machine a un nom unique, une reference optionnelle et un prix. Elle contient une structure hierarchique de composants, pieces et produits.
|
||||||
|
|
||||||
|
### Composant
|
||||||
|
Un **composant** represente un element fonctionnel d'une machine (ex : un moteur, un systeme hydraulique, un automate). Un composant peut lui-meme contenir des sous-composants, des pieces et des produits, formant une structure arborescente.
|
||||||
|
|
||||||
|
### Piece
|
||||||
|
Une **piece** est une piece detachee (ex : un roulement, un joint, un filtre). Les pieces peuvent etre rattachees directement a une machine ou a un composant au sein d'une machine.
|
||||||
|
|
||||||
|
### Produit
|
||||||
|
Un **produit** est un consommable ou article fournisseur (ex : huile, lubrifiant, boulon specifique). Comme les pieces, les produits peuvent etre associes a une machine, a un composant ou a une piece.
|
||||||
|
|
||||||
|
### Constructeur (Fournisseur)
|
||||||
|
Un **constructeur** est un fabricant ou fournisseur. C'est un referentiel partage : le meme fournisseur peut etre associe a des machines, des composants, des pieces et des produits. Chaque fournisseur a un nom, un email et un telephone.
|
||||||
|
|
||||||
|
### Categorie (ModelType)
|
||||||
|
Une **categorie** (appelee ModelType dans le systeme) permet de classifier les composants, les pieces et les produits. Le systeme de categories est explique en detail dans la section suivante.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Le systeme de categories (ModelType)
|
||||||
|
|
||||||
|
Les categories sont un element cle de l'application. Elles servent a **classifier ET a structurer** les elements de l'inventaire.
|
||||||
|
|
||||||
|
### Trois familles de categories
|
||||||
|
|
||||||
|
Il existe trois familles de categories, une par type d'element :
|
||||||
|
|
||||||
|
| Famille | S'applique aux | Exemples |
|
||||||
|
|-------------|----------------|-----------------------------------------|
|
||||||
|
| COMPONENT | Composants | "Moteur electrique", "Systeme hydraulique" |
|
||||||
|
| PIECE | Pieces | "Roulement", "Joint torique", "Filtre" |
|
||||||
|
| PRODUCT | Produits | "Huile moteur", "Graisse", "Boulon M8" |
|
||||||
|
|
||||||
|
### Le squelette (skeleton) : la structure imposee
|
||||||
|
|
||||||
|
La vraie puissance des categories de composants reside dans leur **squelette**. Quand on cree une categorie de composant, on definit un modele qui impose :
|
||||||
|
|
||||||
|
- **Quelles pieces** sont necessaires (par type de piece)
|
||||||
|
- **Quels produits** sont necessaires (par type de produit)
|
||||||
|
- **Quels sous-composants** sont necessaires (par type de composant)
|
||||||
|
- **Quels champs personnalises** doivent etre remplis
|
||||||
|
|
||||||
|
**Exemple concret :** La categorie "Moteur electrique" pourrait imposer :
|
||||||
|
- 1 piece de type "Roulement"
|
||||||
|
- 1 piece de type "Joint"
|
||||||
|
- 1 produit de type "Huile moteur"
|
||||||
|
- 1 sous-composant de type "Variateur"
|
||||||
|
- Des champs personnalises : "Puissance (kW)", "Vitesse (tr/min)", "Tension (V)"
|
||||||
|
|
||||||
|
```
|
||||||
|
Categorie "Moteur electrique" (squelette)
|
||||||
|
|
|
||||||
|
|-- Piece requise : type "Roulement" --> l'utilisateur choisira quel roulement precis
|
||||||
|
|-- Piece requise : type "Joint" --> l'utilisateur choisira quel joint precis
|
||||||
|
|-- Produit requis : type "Huile moteur" --> l'utilisateur choisira quelle huile precise
|
||||||
|
|-- Sous-composant : type "Variateur" --> l'utilisateur choisira quel variateur precis
|
||||||
|
|-- Champ personnalise : "Puissance (kW)" --> l'utilisateur saisira la valeur
|
||||||
|
|-- Champ personnalise : "Tension (V)" --> l'utilisateur saisira la valeur
|
||||||
|
```
|
||||||
|
|
||||||
|
Les categories de pieces peuvent elles aussi definir des produits requis et des champs personnalises. Les categories de produits peuvent definir des champs personnalises.
|
||||||
|
|
||||||
|
### Champs personnalises
|
||||||
|
|
||||||
|
Les champs personnalises permettent d'ajouter des informations specifiques selon la categorie. Chaque champ a :
|
||||||
|
- Un **nom** (ex : "Puissance")
|
||||||
|
- Un **type** (texte, nombre, date, etc.)
|
||||||
|
- Un caractere **obligatoire ou non**
|
||||||
|
- Des **options** possibles (pour les listes deroulantes)
|
||||||
|
- Une **valeur par defaut**
|
||||||
|
- Un **ordre d'affichage**
|
||||||
|
|
||||||
|
Les machines disposent aussi de champs personnalises, mais ceux-ci sont definis directement sur chaque machine (et non via une categorie).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Le cycle de vie d'un composant
|
||||||
|
|
||||||
|
Voici les etapes typiques de creation et utilisation d'un composant :
|
||||||
|
|
||||||
|
```
|
||||||
|
1. CREATION 2. SELECTION CATEGORIE 3. REMPLISSAGE SQUELETTE
|
||||||
|
+-------------------+ +------------------------+ +---------------------------+
|
||||||
|
| Saisir : | | Choisir la categorie : | | Le squelette apparait : |
|
||||||
|
| - Nom | ----> | "Moteur electrique" | --> | - Piece "Roulement" : [?] |
|
||||||
|
| - Reference | | | | - Piece "Joint" : [?] |
|
||||||
|
| - Description | | Le systeme charge le | | - Produit "Huile" : [?] |
|
||||||
|
| - Prix | | squelette associe | | |
|
||||||
|
| - Fournisseurs | +------------------------+ | Choisir dans le catalogue |
|
||||||
|
+-------------------+ | chaque element concret |
|
||||||
|
+---------------------------+
|
||||||
|
|
|
||||||
|
5. DOCUMENTS 4. CHAMPS PERSONNALISES
|
||||||
|
+---------------------+ +-----------------------------+
|
||||||
|
| Joindre des fichiers | <---- | Remplir les champs definis |
|
||||||
|
| - Manuels PDF | | par la categorie : |
|
||||||
|
| - Fiches techniques | | - Puissance : 15 kW |
|
||||||
|
| - Photos | | - Tension : 400 V |
|
||||||
|
| - Schemas | | - Vitesse : 1500 tr/min |
|
||||||
|
+---------------------+ +-----------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Etape 1 - Creation :** L'utilisateur saisit les informations de base du composant (nom, reference, description, prix) et selectionne un ou plusieurs fournisseurs.
|
||||||
|
|
||||||
|
**Etape 2 - Selection de la categorie :** L'utilisateur choisit la categorie du composant (ex : "Moteur electrique"). Le systeme charge alors le squelette defini pour cette categorie.
|
||||||
|
|
||||||
|
**Etape 3 - Remplissage du squelette :** Des "emplacements" (slots) apparaissent pour chaque element requis par le squelette. L'utilisateur selectionne dans le catalogue existant les pieces, produits et sous-composants concrets qui correspondent a chaque emplacement.
|
||||||
|
|
||||||
|
**Etape 4 - Champs personnalises :** L'utilisateur remplit les champs personnalises definis par la categorie (puissance, tension, etc.).
|
||||||
|
|
||||||
|
**Etape 5 - Documents :** L'utilisateur peut joindre des fichiers au composant : manuels PDF, fiches techniques, photos, schemas...
|
||||||
|
|
||||||
|
Ce meme principe s'applique aux pieces (qui peuvent avoir des produits associes et des champs personnalises definis par leur categorie) et aux produits (qui peuvent avoir des champs personnalises).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Les roles utilisateurs
|
||||||
|
|
||||||
|
L'application utilise 4 niveaux de droits, organises en hierarchie. Chaque role herite automatiquement des droits du role inferieur :
|
||||||
|
|
||||||
|
```
|
||||||
|
+------------------------------------------------------------------+
|
||||||
|
| ROLE_ADMIN |
|
||||||
|
| Tout faire + gerer les utilisateurs (creer, modifier, supprimer |
|
||||||
|
| des comptes, attribuer des roles) |
|
||||||
|
+------------------------------------------------------------------+
|
||||||
|
| herite de
|
||||||
|
v
|
||||||
|
+------------------------------------------------------------------+
|
||||||
|
| ROLE_GESTIONNAIRE |
|
||||||
|
| Creer, modifier et supprimer les machines, composants, pieces, |
|
||||||
|
| produits, sites, fournisseurs, categories, documents, |
|
||||||
|
| commentaires. C'est le role d'edition principal. |
|
||||||
|
+------------------------------------------------------------------+
|
||||||
|
| herite de
|
||||||
|
v
|
||||||
|
+------------------------------------------------------------------+
|
||||||
|
| ROLE_VIEWER |
|
||||||
|
| Consulter tout l'inventaire en lecture seule : naviguer dans |
|
||||||
|
| les machines, voir les structures, les catalogues, l'historique |
|
||||||
|
| et les documents. |
|
||||||
|
+------------------------------------------------------------------+
|
||||||
|
| herite de
|
||||||
|
v
|
||||||
|
+------------------------------------------------------------------+
|
||||||
|
| ROLE_USER |
|
||||||
|
| Role de base attribue automatiquement a tout utilisateur |
|
||||||
|
| connecte. Acces minimal. |
|
||||||
|
+------------------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
En resume :
|
||||||
|
- **Admin** : fait tout, y compris gerer les comptes utilisateurs
|
||||||
|
- **Gestionnaire** : cree et modifie les donnees de l'inventaire
|
||||||
|
- **Viewer** : consulte l'inventaire sans pouvoir le modifier
|
||||||
|
- **User** : role de base, acces minimal
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Les fonctionnalites cles
|
||||||
|
|
||||||
|
### Catalogues
|
||||||
|
|
||||||
|
L'application propose des **catalogues** pour chaque type d'element :
|
||||||
|
- **Catalogue des composants** : liste tous les composants avec recherche par nom, reference ou categorie
|
||||||
|
- **Catalogue des pieces** : liste toutes les pieces detachees
|
||||||
|
- **Catalogue des produits** : liste tous les produits fournisseurs
|
||||||
|
- **Liste des machines** : toutes les machines, organisees par site
|
||||||
|
- **Liste des sites** : tous les sites industriels
|
||||||
|
- **Liste des fournisseurs** : tous les constructeurs/fournisseurs
|
||||||
|
|
||||||
|
Chaque catalogue offre des fonctions de **recherche**, de **tri** et de **pagination**.
|
||||||
|
|
||||||
|
### Recherche
|
||||||
|
|
||||||
|
La recherche est disponible dans tous les catalogues et permet de filtrer par :
|
||||||
|
- Nom (recherche partielle, insensible a la casse)
|
||||||
|
- Reference (recherche partielle)
|
||||||
|
- Categorie (filtre exact ou par nom)
|
||||||
|
|
||||||
|
### Historique et audit
|
||||||
|
|
||||||
|
Chaque modification dans l'application est **tracee automatiquement**. Le systeme enregistre :
|
||||||
|
- **Qui** a fait la modification (quel utilisateur)
|
||||||
|
- **Quand** la modification a ete faite
|
||||||
|
- **Quoi** a ete modifie (les champs avant/apres)
|
||||||
|
- **Sur quel element** (machine, composant, piece, produit...)
|
||||||
|
|
||||||
|
On peut consulter :
|
||||||
|
- L'**historique d'une entite** : toutes les modifications apportees a une machine, un composant, etc.
|
||||||
|
- Le **journal d'activite global** : toutes les modifications recentes dans l'application
|
||||||
|
|
||||||
|
### Commentaires
|
||||||
|
|
||||||
|
Les utilisateurs peuvent **commenter** n'importe quel element de l'inventaire (machines, composants, pieces, produits, categories). Les commentaires ont un systeme de **resolution** : un commentaire ouvert peut etre marque comme "resolu" par un gestionnaire. Un compteur de commentaires non resolus est disponible.
|
||||||
|
|
||||||
|
### Documents
|
||||||
|
|
||||||
|
Des fichiers peuvent etre joints a toutes les entites principales :
|
||||||
|
- **Sites** : plans, reglements
|
||||||
|
- **Machines** : manuels, fiches techniques
|
||||||
|
- **Composants** : documentations constructeur
|
||||||
|
- **Pieces** : plans de pieces, specifications
|
||||||
|
- **Produits** : fiches de donnees de securite, catalogues
|
||||||
|
|
||||||
|
Les fichiers sont uploades via l'interface et peuvent etre consultes ou telecharges a tout moment. L'application gere differents formats : PDF, images, etc.
|
||||||
|
|
||||||
|
### Clonage de machines
|
||||||
|
|
||||||
|
Quand une nouvelle machine est identique ou similaire a une existante, il est possible de **cloner une machine**. Le clonage copie :
|
||||||
|
- Les champs personnalises et leurs valeurs
|
||||||
|
- Toute la structure : les liens vers les composants, pieces et produits
|
||||||
|
- La hierarchie (quel composant contient quelles pieces, etc.)
|
||||||
|
|
||||||
|
L'utilisateur choisit un nouveau nom et un site de destination. La machine clonee peut ensuite etre modifiee independamment de l'originale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. La structure des machines
|
||||||
|
|
||||||
|
### Vue d'ensemble
|
||||||
|
|
||||||
|
Chaque machine possede une **structure hierarchique** qui decrit de quoi elle est composee. Cette structure est une arborescence :
|
||||||
|
|
||||||
|
```
|
||||||
|
Machine "Presse hydraulique PH-200"
|
||||||
|
|
|
||||||
|
|-- Composant "Moteur principal M1"
|
||||||
|
| |-- Piece "Roulement SKF 6205" (quantite: 2)
|
||||||
|
| | |-- Produit "Graisse SKF LGMT2"
|
||||||
|
| |-- Piece "Joint Viton DN50"
|
||||||
|
| |-- Produit "Huile Total Azolla ZS 46"
|
||||||
|
| |-- Sous-composant "Variateur ABB ACS580"
|
||||||
|
| |-- Piece "Fusible 63A"
|
||||||
|
| |-- Produit "Pate thermique"
|
||||||
|
|
|
||||||
|
|-- Composant "Groupe hydraulique GH-01"
|
||||||
|
| |-- Piece "Filtre Parker 926169Q"
|
||||||
|
| |-- Piece "Verin Bosch CDT3" (quantite: 4)
|
||||||
|
| |-- Produit "Huile hydraulique HLP 46"
|
||||||
|
|
|
||||||
|
|-- Piece "Courroie Gates PowerGrip" (piece directement sur la machine)
|
||||||
|
|-- Produit "Boulon M12x50 Inox" (produit directement sur la machine)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Les liens (links)
|
||||||
|
|
||||||
|
Les elements ne sont pas directement "dans" la machine. Ils y sont rattaches par des **liens** :
|
||||||
|
|
||||||
|
- **MachineComponentLink** : rattache un composant a une machine
|
||||||
|
- **MachinePieceLink** : rattache une piece a une machine
|
||||||
|
- **MachineProductLink** : rattache un produit a une machine
|
||||||
|
|
||||||
|
Ces liens permettent :
|
||||||
|
- De definir la **hierarchie** : un composant peut etre parent d'une piece ou d'un produit, un sous-composant peut etre enfant d'un autre composant
|
||||||
|
- De specifier une **quantite** (ex : 4 verins identiques)
|
||||||
|
- De faire des **surcharges** : modifier le nom, la reference ou le prix d'un element specifiquement dans le contexte de cette machine, sans modifier l'element du catalogue
|
||||||
|
|
||||||
|
### Hierarchie parent-enfant
|
||||||
|
|
||||||
|
```
|
||||||
|
MachineComponentLink (composant dans la machine)
|
||||||
|
|
|
||||||
|
|-- parentLink --> null (composant racine, directement dans la machine)
|
||||||
|
| ou
|
||||||
|
|-- parentLink --> autre MachineComponentLink (sous-composant)
|
||||||
|
|
|
||||||
|
|-- pieceLinks --> MachinePieceLink[] (pieces de ce composant)
|
||||||
|
|-- productLinks --> MachineProductLink[] (produits de ce composant)
|
||||||
|
|
||||||
|
MachinePieceLink (piece dans la machine)
|
||||||
|
|
|
||||||
|
|-- parentLink --> MachineComponentLink (piece rattachee a un composant)
|
||||||
|
| ou
|
||||||
|
|-- parentLink --> null (piece directement sur la machine)
|
||||||
|
|
|
||||||
|
|-- productLinks --> MachineProductLink[] (produits de cette piece)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Catalogue vs. Structure machine
|
||||||
|
|
||||||
|
Un point important : les **composants, pieces et produits existent dans un catalogue global**. Quand on les ajoute a une machine, on cree un lien vers l'element du catalogue. Le meme composant du catalogue peut donc etre utilise dans plusieurs machines.
|
||||||
|
|
||||||
|
Les surcharges (nom, reference, prix) permettent d'adapter les informations au contexte d'une machine specifique sans modifier la fiche catalogue.
|
||||||
|
|
||||||
|
```
|
||||||
|
CATALOGUE (reference globale) MACHINE (utilisation specifique)
|
||||||
|
+-------------------------+ +--------------------------------+
|
||||||
|
| Composant "Moteur 15kW" | | Lien vers "Moteur 15kW" |
|
||||||
|
| Ref: MOT-15-01 | <-------- | Surcharge nom: "Moteur gauche" |
|
||||||
|
| Prix: 2500 EUR | | Surcharge prix: 2200 EUR |
|
||||||
|
+-------------------------+ +--------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schemas recapitulatifs
|
||||||
|
|
||||||
|
### Relations entre entites
|
||||||
|
|
||||||
|
```
|
||||||
|
+--------+
|
||||||
|
| Site |
|
||||||
|
+--------+
|
||||||
|
|
|
||||||
|
contient (1..N)
|
||||||
|
|
|
||||||
|
+-----------+
|
||||||
|
| Machine |------------ Fournisseurs (N..N)
|
||||||
|
+-----------+
|
||||||
|
/ | \
|
||||||
|
/ | \
|
||||||
|
Composants Pieces Produits
|
||||||
|
(via liens) (via liens) (via liens)
|
||||||
|
|
||||||
|
+-----------+ +--------+ +---------+
|
||||||
|
| Composant | | Piece | | Produit |
|
||||||
|
+-----------+ +--------+ +---------+
|
||||||
|
| | |
|
||||||
|
|-- Categorie |-- Categorie |-- Categorie
|
||||||
|
|-- Fournisseurs -- Fournisseurs -- Fournisseurs
|
||||||
|
|-- Documents |-- Documents |-- Documents
|
||||||
|
|-- Champs perso -- Champs perso -- Champs perso
|
||||||
|
|
|
||||||
|
|-- Sous-composants (arborescence)
|
||||||
|
|-- Pieces (slots depuis le squelette)
|
||||||
|
|-- Produits (slots depuis le squelette)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flux de creation typique
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Creer les SITES
|
||||||
|
|
|
||||||
|
2. Creer les CATEGORIES (avec leurs squelettes)
|
||||||
|
|
|
||||||
|
3. Creer les FOURNISSEURS
|
||||||
|
|
|
||||||
|
4. Creer les PRODUITS (en les categorisant)
|
||||||
|
|
|
||||||
|
5. Creer les PIECES (en les categorisant, en leur associant des produits)
|
||||||
|
|
|
||||||
|
6. Creer les COMPOSANTS (en choisissant une categorie,
|
||||||
|
| en remplissant le squelette avec des pieces/produits/sous-composants)
|
||||||
|
|
|
||||||
|
7. Creer les MACHINES (sur un site)
|
||||||
|
|
|
||||||
|
8. STRUCTURER les machines (ajouter composants, pieces, produits)
|
||||||
|
|
|
||||||
|
9. DOCUMENTER (joindre des fichiers a chaque element)
|
||||||
|
```
|
||||||
1052
docs/FRONTEND.md
Normal file
1052
docs/FRONTEND.md
Normal file
File diff suppressed because it is too large
Load Diff
146
docs/GLOSSAIRE_METIER.md
Normal file
146
docs/GLOSSAIRE_METIER.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# Glossaire Métier — Inventory
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
**Inventory** est une application de gestion de parc machines industriel. Elle permet aux équipes de maintenance de cataloguer leurs machines, leurs sous-ensembles (composants), les pièces de rechange et les consommables associés. Chaque machine est rattachée à un site physique (usine, atelier). L'application gère la hiérarchie complète : Machine → Composants → Pièces/Produits, avec traçabilité (audit), documentation technique et champs personnalisables.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Concepts Métier
|
||||||
|
|
||||||
|
### Hiérarchie principale
|
||||||
|
|
||||||
|
| Terme | Définition | Exemples concrets |
|
||||||
|
|-------|-----------|-------------------|
|
||||||
|
| **Site** | Lieu physique (usine, atelier, entrepôt). Regroupe les machines d'un même emplacement. | Usine de Lyon, Atelier Nord |
|
||||||
|
| **Machine** | Équipement industriel installé sur un site. C'est l'unité de base du parc. Contient des composants, pièces et produits. | Presse hydraulique, Tour CNC, Ligne d'embouteillage |
|
||||||
|
| **Composant** | Sous-ensemble fonctionnel d'une machine. Peut contenir des pièces, des produits, et d'autres sous-composants (imbrication). | Moteur, Pompe, Tableau électrique, Vérin |
|
||||||
|
| **Pièce** | Pièce mécanique/physique qu'on monte ou remplace. C'est l'unité de maintenance. | Joint, Écrou, Roulement, Capteur, Courroie |
|
||||||
|
| **Produit** | Consommable qu'on utilise sans monter. S'use et se renouvelle. | Huile, Dégraissant, Graisse, Liquide de refroidissement |
|
||||||
|
|
||||||
|
### Configuration et templates
|
||||||
|
|
||||||
|
| Terme | Définition |
|
||||||
|
|-------|-----------|
|
||||||
|
| **Modèle Type** (ModelType) | Template réutilisable qui définit la composition attendue d'un composant, d'une pièce ou d'un produit. Par exemple : "Pompe centrifuge XYZ nécessite 2 joints, 1 roulement et de l'huile hydraulique". |
|
||||||
|
| **Skeleton** (squelette) | La structure "vide" définie par un modèle type : la liste des emplacements requis (pièces, produits, sous-composants) avant qu'on y mette les éléments réels. |
|
||||||
|
| **Slot** (emplacement) | Emplacement concret dans un composant ou une pièce, créé à partir du skeleton. Chaque slot est à remplir avec une pièce, un produit ou un sous-composant réel. Un slot peut rester vide (pas encore sourcé). |
|
||||||
|
| **Sync** (synchronisation) | Propagation des modifications d'un modèle type vers tous les composants existants de ce type. Par exemple : ajouter un slot "filtre" au modèle type met à jour tous les composants de ce type. Surtout utilisé en phase de saisie initiale, quand on ajuste les modèles au fur et à mesure qu'on découvre la vraie composition des machines. |
|
||||||
|
| **Catégorie de modèle** | Un modèle type est classé en 3 catégories : Composant, Pièce ou Produit. Détermine quels skeletons il peut définir. |
|
||||||
|
|
||||||
|
### Transverse
|
||||||
|
|
||||||
|
| Terme | Définition |
|
||||||
|
|-------|-----------|
|
||||||
|
| **Constructeur** | Fournisseur ou fabricant. Peut être associé à une machine, un composant, une pièce ou un produit. Permet de tracer la chaîne d'approvisionnement. |
|
||||||
|
| **Champ personnalisé** (CustomField) | Attribut dynamique défini par l'utilisateur et attaché à une machine ou à un modèle type. Les composants/pièces/produits d'un même modèle type partagent les mêmes champs personnalisés. Exemples : "N° de série", "Date de garantie", "Intervalle de maintenance". |
|
||||||
|
| **Document** | Fichier attaché à n'importe quelle entité (machine, composant, pièce, produit, site, commentaire). Typé : Documentation, Devis, Facture, Plan, Photo, Autre. |
|
||||||
|
| **Commentaire** | Annotation utilisateur sur une entité, avec un statut ouvert ou résolu. Permet de signaler un problème, poser une question ou laisser une note. Peut contenir des pièces jointes. |
|
||||||
|
| **Journal d'audit** (AuditLog) | Historique automatique et immuable de toutes les créations, modifications et suppressions. Enregistre qui a fait quoi, quand, avec le détail des changements. |
|
||||||
|
|
||||||
|
### Utilisateurs et rôles
|
||||||
|
|
||||||
|
| Rôle | Droits |
|
||||||
|
|------|--------|
|
||||||
|
| **Admin** | Accès complet : gestion des utilisateurs, configuration, toutes les opérations |
|
||||||
|
| **Gestionnaire** | Créer, modifier, supprimer des machines/composants/pièces/produits |
|
||||||
|
| **Viewer** | Consultation seule, pas de modification |
|
||||||
|
| **User** | Rôle de base (accès minimal) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflows Utilisateur
|
||||||
|
|
||||||
|
### 1. Créer une machine
|
||||||
|
1. Choisir le **site** où la machine est installée
|
||||||
|
2. Renseigner nom, référence, prix, fournisseur(s)
|
||||||
|
3. Ajouter des **composants** à la machine (voir workflow 2)
|
||||||
|
4. Ajouter des **pièces** et **produits** directement sur la machine si nécessaire
|
||||||
|
5. Ajouter des **champs personnalisés** et des **documents**
|
||||||
|
|
||||||
|
### 2. Ajouter un composant à une machine
|
||||||
|
1. Choisir un **modèle type** pour le composant (ex: "Pompe centrifuge XYZ")
|
||||||
|
2. Les **slots** sont pré-créés automatiquement à partir du skeleton du modèle type
|
||||||
|
3. Remplir chaque slot en sélectionnant la pièce/produit/sous-composant réel
|
||||||
|
4. Les slots peuvent rester vides et être remplis plus tard
|
||||||
|
|
||||||
|
### 3. Créer ou modifier un modèle type
|
||||||
|
1. Nommer le modèle type et choisir sa catégorie (Composant, Pièce ou Produit)
|
||||||
|
2. Définir les emplacements requis : quelles pièces, quels produits, quels sous-composants
|
||||||
|
3. Définir les champs personnalisés (métadonnées) pour les entités de ce type
|
||||||
|
4. Si des composants existent déjà avec ce modèle type → utiliser le **sync** (workflow 4)
|
||||||
|
|
||||||
|
### 4. Synchroniser un modèle type
|
||||||
|
1. Modifier les emplacements du modèle type (ajout/suppression de slots)
|
||||||
|
2. Lancer un **sync preview** : visualiser l'impact sur les composants existants
|
||||||
|
3. Confirmer → les slots sont ajoutés/supprimés sur tous les composants du type
|
||||||
|
4. Surtout utile en phase de saisie initiale quand les données sont ajustées progressivement
|
||||||
|
|
||||||
|
### 5. Cloner une machine
|
||||||
|
1. Sélectionner une machine existante
|
||||||
|
2. Lancer le clonage → copie complète (composants, pièces, produits, liens, champs personnalisés)
|
||||||
|
3. Renommer la machine clonée et l'affecter à un site
|
||||||
|
|
||||||
|
### 6. Gérer les documents
|
||||||
|
1. Sélectionner une entité (machine, composant, pièce, produit, site)
|
||||||
|
2. Uploader un fichier (PDF, image, etc.)
|
||||||
|
3. Choisir le type : Documentation, Devis, Facture, Plan, Photo, Autre
|
||||||
|
4. Les documents sont consultables et téléchargeables depuis la fiche de l'entité
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Relations — Vue d'ensemble
|
||||||
|
|
||||||
|
```
|
||||||
|
Site
|
||||||
|
└── Machine
|
||||||
|
├── Composant (→ défini par un Modèle Type)
|
||||||
|
│ ├── Slot Pièce → Pièce (joint, écrou…)
|
||||||
|
│ ├── Slot Produit → Produit (huile, dégraissant…)
|
||||||
|
│ └── Slot Sous-composant → Composant (imbrication)
|
||||||
|
├── Pièce (directement sur la machine)
|
||||||
|
│ └── Slot Produit → Produit
|
||||||
|
└── Produit (directement sur la machine)
|
||||||
|
|
||||||
|
Modèle Type (template)
|
||||||
|
├── Skeleton Pièce Requirement → "il faut une pièce de type X"
|
||||||
|
├── Skeleton Produit Requirement → "il faut un produit de type Y"
|
||||||
|
└── Skeleton Sous-composant Requirement → "il faut un composant de type Z"
|
||||||
|
|
||||||
|
Transverse (attachable à toute entité) :
|
||||||
|
• Constructeur (fournisseur)
|
||||||
|
• Document (fichier)
|
||||||
|
• Commentaire (annotation)
|
||||||
|
• Champ personnalisé (métadonnée dynamique)
|
||||||
|
• Journal d'audit (historique automatique)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Correspondance Métier ↔ Code
|
||||||
|
|
||||||
|
| Terme métier | Entité code | Table PG |
|
||||||
|
|-------------|-------------|----------|
|
||||||
|
| Site | `Site` | `site` |
|
||||||
|
| Machine | `Machine` | `machine` |
|
||||||
|
| Composant | `Composant` | `composant` |
|
||||||
|
| Pièce | `Piece` | `piece` |
|
||||||
|
| Produit | `Product` | `product` |
|
||||||
|
| Modèle Type | `ModelType` | `model_type` |
|
||||||
|
| Slot pièce (composant) | `ComposantPieceSlot` | `composant_piece_slot` |
|
||||||
|
| Slot produit (composant) | `ComposantProductSlot` | `composant_product_slot` |
|
||||||
|
| Slot sous-composant | `ComposantSubcomponentSlot` | `composant_subcomponent_slot` |
|
||||||
|
| Slot produit (pièce) | `PieceProductSlot` | `piece_product_slot` |
|
||||||
|
| Skeleton pièce | `SkeletonPieceRequirement` | `skeleton_piece_requirement` |
|
||||||
|
| Skeleton produit | `SkeletonProductRequirement` | `skeleton_product_requirement` |
|
||||||
|
| Skeleton sous-composant | `SkeletonSubcomponentRequirement` | `skeleton_subcomponent_requirement` |
|
||||||
|
| Constructeur | `Constructeur` | `constructeur` |
|
||||||
|
| Champ personnalisé | `CustomField` | `custom_field` |
|
||||||
|
| Valeur champ perso | `CustomFieldValue` | `custom_field_value` |
|
||||||
|
| Document | `Document` | `document` |
|
||||||
|
| Commentaire | `Comment` | `comment` |
|
||||||
|
| Journal d'audit | `AuditLog` | `audit_log` |
|
||||||
|
| Utilisateur | `Profile` | `profile` |
|
||||||
|
| Lien machine-composant | `MachineComponentLink` | `machine_component_link` |
|
||||||
|
| Lien machine-pièce | `MachinePieceLink` | `machine_piece_link` |
|
||||||
|
| Lien machine-produit | `MachineProductLink` | `machine_product_link` |
|
||||||
346
docs/REVIEW_ARCHITECTURE.md
Normal file
346
docs/REVIEW_ARCHITECTURE.md
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
# Revue d'architecture - Sources de complexite et effets de bord
|
||||||
|
|
||||||
|
Date : 2026-03-23
|
||||||
|
Branche analysee : `develop`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Diagnostic - Top 10 des sources de complexite
|
||||||
|
|
||||||
|
| # | Source | Impact | Effort |
|
||||||
|
|---|--------|--------|--------|
|
||||||
|
| 1 | Duplication massive du `smartMatch` dans les Sync Strategies | Bugs silencieux, maintenance triple | M |
|
||||||
|
| 2 | Custom Fields : 4 FK nullable sur une seule entite (polymorphisme pauvre) | Integrite fragile, code defensif partout | L |
|
||||||
|
| 3 | Composables frontend geants avec responsabilites multiples | Difficile a tester, refactoring risque | M |
|
||||||
|
| 4 | 3 fichiers utils de custom fields frontend avec logique qui se chevauche | Incoherences, bugs de merge/dedup | M |
|
||||||
|
| 5 | `pendingStructure` : canal de communication cache entre deserialisation et processor | Effet de bord invisible, timing fragile | S |
|
||||||
|
| 6 | `PieceProductSyncSubscriber` : legacy sync dans un subscriber Doctrine | Side effect cache, recompute du changeset | S |
|
||||||
|
| 7 | Double flush dans les processors (decorated + flush manuel) | Audit logs potentiellement incomplets | S |
|
||||||
|
| 8 | `MachineStructureController` : God controller avec normalisation JSON manuelle | Bypass API Platform, 300+ LOC de serialisation | L |
|
||||||
|
| 9 | Chaine de dependances circulaire dans `useMachineDetailData` | Proxy refs, ordre d'initialisation fragile | M |
|
||||||
|
| 10 | Frontend : typage `any` systematique sur les entites | Pas de filet de securite TypeScript | L |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Analyse detaillee
|
||||||
|
|
||||||
|
### 1. Duplication du `smartMatch` dans les Sync Strategies
|
||||||
|
|
||||||
|
**Fichiers concernes :**
|
||||||
|
- `/src/Service/Sync/ComposantSyncStrategy.php` (lignes 380-446)
|
||||||
|
- `/src/Service/Sync/PieceSyncStrategy.php` (lignes 244-308)
|
||||||
|
|
||||||
|
**Probleme :** `smartMatch()`, `smartMatchPreview()` et toute la logique de sync des custom field values sont copiees-collees entre `ComposantSyncStrategy` et `PieceSyncStrategy`. Le `ProductSyncStrategy` a une version simplifiee (pas de slots). Si un bug est corrige dans l'un, il faut penser a le corriger dans l'autre.
|
||||||
|
|
||||||
|
**Effets de bord concrets :**
|
||||||
|
- Un correctif sur le matching des slots dans une strategie peut etre oublie dans l'autre
|
||||||
|
- Le compteur de preview custom fields utilise `orderIndex` comme cle de matching, ce qui est fragile (reindexation = faux positif)
|
||||||
|
|
||||||
|
**Solution proposee (effort M) :**
|
||||||
|
Extraire un trait ou une classe abstraite `AbstractSlotSyncStrategy` :
|
||||||
|
|
||||||
|
```php
|
||||||
|
// AVANT : smartMatch() duplique dans ComposantSyncStrategy et PieceSyncStrategy
|
||||||
|
|
||||||
|
// APRES : extraire dans un trait
|
||||||
|
trait SlotSyncTrait
|
||||||
|
{
|
||||||
|
protected function smartMatch(array $existingTypeIds, array $proposedTypeIds): array
|
||||||
|
{
|
||||||
|
// ... logique unique
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function syncCustomFieldValues(
|
||||||
|
object $entity,
|
||||||
|
string $fkField,
|
||||||
|
array $customFields,
|
||||||
|
bool $confirmDeletions,
|
||||||
|
): array {
|
||||||
|
// ... logique unique pour add/remove CFValues
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La methode `execute()` de chaque strategie ne garderait que la boucle specifique a son type de slot (piece slots, product slots, subcomponent slots), et deleguerait le matching et la gestion des CF values au trait.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Custom Fields : polymorphisme par FK nullable
|
||||||
|
|
||||||
|
**Fichiers concernes :**
|
||||||
|
- `/src/Entity/CustomField.php` - 4 FK nullable : `machine`, `typeComposant`, `typePiece`, `typeProduct`
|
||||||
|
- `/src/Entity/CustomFieldValue.php` - 4 FK nullable : `machine`, `composant`, `piece`, `product`
|
||||||
|
- `/src/Controller/CustomFieldValueController.php` - `resolveTarget()` fait un switch sur 4 types
|
||||||
|
|
||||||
|
**Probleme :** Un `CustomFieldValue` peut pointer vers machine OU composant OU piece OU produit via 4 colonnes nullable. Rien n'empeche au niveau DB qu'un CFV pointe vers deux entites en meme temps. Le frontend doit deviner le type cible. Chaque nouveau type d'entite necessite d'ajouter une colonne, un setter, et un cas dans tous les switches.
|
||||||
|
|
||||||
|
**Effets de bord concrets :**
|
||||||
|
- Le `CustomFieldValueController::resolveTarget()` tente 4 cles dans un ordre specifique -- si le payload a `machineId` ET `composantId`, seul `machine` est utilise (silent bug)
|
||||||
|
- Les audit subscribers (`getOwnerFromCustomFieldValue`) doivent tester chaque getter -- si `getComposant()` renvoie un objet alors que `getMachine()` aussi, le comportement est indetermine
|
||||||
|
- La serialisation API Platform expose les 4 FK meme quand 3 sont null
|
||||||
|
|
||||||
|
**Solution proposee (effort L) :**
|
||||||
|
|
||||||
|
Option pragmatique (pas de refactoring DB) : ajouter une colonne discriminante `entityType` (enum) + contrainte CHECK :
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE custom_field_values
|
||||||
|
ADD COLUMN entity_type VARCHAR(20) NOT NULL DEFAULT 'machine';
|
||||||
|
|
||||||
|
ALTER TABLE custom_field_values
|
||||||
|
ADD CONSTRAINT chk_single_fk CHECK (
|
||||||
|
(entity_type = 'machine' AND machineId IS NOT NULL AND composantId IS NULL AND pieceId IS NULL AND productId IS NULL) OR
|
||||||
|
(entity_type = 'composant' AND composantId IS NOT NULL AND machineId IS NULL AND pieceId IS NULL AND productId IS NULL) OR
|
||||||
|
(entity_type = 'piece' AND pieceId IS NOT NULL AND machineId IS NULL AND composantId IS NULL AND productId IS NULL) OR
|
||||||
|
(entity_type = 'product' AND productId IS NOT NULL AND machineId IS NULL AND composantId IS NULL AND pieceId IS NULL)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Cela securise l'integrite sans changer l'architecture. Le `resolveTarget` et les audit subscribers pourraient ensuite brancher sur `entityType` au lieu de tester 4 FK.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Composables frontend geants (400-550 LOC)
|
||||||
|
|
||||||
|
**Fichiers concernes :**
|
||||||
|
- `/Inventory_frontend/app/composables/useComponentEdit.ts` (550 LOC)
|
||||||
|
- `/Inventory_frontend/app/composables/usePieceEdit.ts` (472 LOC)
|
||||||
|
- `/Inventory_frontend/app/composables/useMachineDetailData.ts` (468 LOC)
|
||||||
|
- `/Inventory_frontend/app/composables/useComponentCreate.ts` (417 LOC)
|
||||||
|
|
||||||
|
**Probleme :** Ces composables orchestrent en un seul fichier : le chargement de donnees, la gestion de formulaire, la persistence des custom fields, la gestion des documents, l'historique, la resolution de labels, et la soumission. Chacun instancie 8-12 sous-composables.
|
||||||
|
|
||||||
|
**Effets de bord concrets :**
|
||||||
|
- `useComponentEdit` instancie `usePieces()`, `useProducts()`, `useComposants()` a chaque montage de page, meme si ces catalogues sont deja charges -- requetes API en double
|
||||||
|
- La logique de soumission (`submitEdition`, `submitCreation`) melange la construction du payload, la validation, l'appel API, et la persistence des custom fields -- si une etape echoue, l'etat local est partiellement modifie
|
||||||
|
- Les watchers sur `selectedType`/`selectedTypeStructure` dans `useComponentCreate` et `useComponentEdit` font des choses differentes pour le meme concept -- source de divergence
|
||||||
|
|
||||||
|
**Solution proposee (effort M) :**
|
||||||
|
Decouper chaque composable geant en sous-composables par responsabilite, comme deja fait pour `useMachineDetailData` (qui delegue a `useMachineDetailDocuments`, `useMachineDetailCustomFields`, etc.) :
|
||||||
|
|
||||||
|
```
|
||||||
|
useComponentEdit.ts (550 LOC)
|
||||||
|
-> useComponentEditForm.ts (~100 LOC : reactive form, validation)
|
||||||
|
-> useComponentEditDocuments.ts (~80 LOC : upload, preview, delete)
|
||||||
|
-> useComponentEditSlots.ts (~120 LOC : slot selection/save)
|
||||||
|
-> useComponentEditCustomFields.ts (~60 LOC : build inputs, save)
|
||||||
|
-> useComponentEdit.ts (~150 LOC : orchestrateur)
|
||||||
|
```
|
||||||
|
|
||||||
|
Appliquer le meme pattern a `usePieceEdit` et `useComponentCreate`. Les blocs communs (document handling, custom field save, price formatting) deviendraient des composables partages.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Triple duplication de la logique custom fields frontend
|
||||||
|
|
||||||
|
**Fichiers concernes :**
|
||||||
|
- `/Inventory_frontend/app/shared/utils/customFieldFormUtils.ts` (404 LOC) - pour les pages create/edit
|
||||||
|
- `/Inventory_frontend/app/shared/utils/customFieldUtils.ts` (440 LOC) - pour la page machine detail
|
||||||
|
- `/Inventory_frontend/app/shared/utils/entityCustomFieldLogic.ts` (335 LOC) - pour les composants item
|
||||||
|
|
||||||
|
**Probleme :** Ces 3 fichiers resolvent le meme probleme (normaliser des definitions de custom fields + merger avec des valeurs existantes) avec des implementations differentes :
|
||||||
|
- `customFieldFormUtils.ts` : `resolveFieldName()`, `resolveFieldType()`, `buildCustomFieldInputs()`
|
||||||
|
- `entityCustomFieldLogic.ts` : `resolveFieldName()` (differente!), `resolveFieldType()` (differente!), `mergeFieldDefinitionsWithValues()`
|
||||||
|
- `customFieldUtils.ts` : `extractDefinitionName()`, `normalizeExistingCustomFieldDefinitions()`, `mergeCustomFieldValuesWithDefinitions()`
|
||||||
|
|
||||||
|
**Effets de bord concrets :**
|
||||||
|
- Trois facons differentes de resoudre le nom d'un champ -- `resolveFieldName` dans `customFieldFormUtils` teste `name`, `key`, `label` ; dans `entityCustomFieldLogic` elle teste `name` seulement et retourne `'Champ'` par defaut
|
||||||
|
- Trois algorithmes de merge values/definitions -- un bug corrige dans l'un n'est pas corrige dans les autres
|
||||||
|
- La deduplication par `name+type` dans `entityCustomFieldLogic.ts` et par `orderIndex` dans `customFieldUtils.ts` produit des resultats differents pour les memes donnees
|
||||||
|
|
||||||
|
**Solution proposee (effort M) :**
|
||||||
|
Fusionner en un seul module `customFields.ts` avec :
|
||||||
|
1. Une seule fonction `resolveFieldName(field: any): string`
|
||||||
|
2. Une seule fonction `mergeDefinitionsWithValues(defs, values): MergedField[]`
|
||||||
|
3. Une seule fonction `deduplicateFields(fields): MergedField[]`
|
||||||
|
|
||||||
|
Les 3 fichiers actuels deviendraient des re-exports ou des wrappers fins. Commencer par aligner les signatures, puis remplacer les imports un par un.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. `pendingStructure` : canal de communication cache
|
||||||
|
|
||||||
|
**Fichiers concernes :**
|
||||||
|
- `/src/Entity/ModelType.php` et `/src/Entity/Composant.php` -- propriete `#[ApiProperty]` non mappee en DB
|
||||||
|
- `/src/State/ModelTypeProcessor.php` (lignes 33-43)
|
||||||
|
- `/src/State/ComposantProcessor.php` (lignes 42-51)
|
||||||
|
|
||||||
|
**Probleme :** Le champ `structure` envoye par le frontend est intercepte par API Platform dans un champ `pendingStructure` (non mappe en DB), puis lu par le processor apres le `persist` du decorated processor. Ce mecanisme est invisible : rien dans l'entite n'indique qu'un setter a un effet de bord differe.
|
||||||
|
|
||||||
|
**Effets de bord concrets :**
|
||||||
|
- Si le `decorated->process()` leve une exception, le `pendingStructure` reste dans l'entite -- pas de cleanup
|
||||||
|
- Le `flush()` supplementaire dans le processor (ligne 43 de `ModelTypeProcessor`) declenche les audit subscribers une deuxieme fois pour le meme cycle de request -- les snapshots d'audit peuvent capturer un etat intermediaire
|
||||||
|
- Un developpeur qui modifie le `ModelType` via Doctrine directement (fixture, migration, CLI) ne beneficie pas de ce mecanisme -- les skeleton requirements ne sont pas mis a jour
|
||||||
|
|
||||||
|
**Solution proposee (effort S) :**
|
||||||
|
Documenter explicitement ce pattern dans l'entite avec un docblock. Ajouter un `try/finally` pour le cleanup :
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ModelTypeProcessor::process()
|
||||||
|
try {
|
||||||
|
$result = $this->decorated->process($data, $operation, $uriVariables, $context);
|
||||||
|
if (null !== $pendingStructure) {
|
||||||
|
$this->skeletonStructureService->updateSkeletonRequirements($data, $pendingStructure);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
} finally {
|
||||||
|
$data->clearPendingStructure();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. `PieceProductSyncSubscriber` : side effect cache
|
||||||
|
|
||||||
|
**Fichier concerne :**
|
||||||
|
- `/src/EventSubscriber/PieceProductSyncSubscriber.php`
|
||||||
|
|
||||||
|
**Probleme :** Ce subscriber Doctrine ecoute `prePersist` et `preUpdate` pour synchroniser la relation legacy `product` (ManyToOne) avec la collection `productIds` (JSON array). Sur `preUpdate`, il fait un `recomputeSingleEntityChangeSet` (ligne 50-51), ce qui modifie le changeset en cours de flush.
|
||||||
|
|
||||||
|
**Effets de bord concrets :**
|
||||||
|
- Le recompute du changeset peut interferer avec les audit subscribers qui lisent ce meme changeset -- l'audit log peut capturer le changement de `product` comme une modification manuelle alors qu'il est automatique
|
||||||
|
- L'ordre d'execution des subscribers n'est pas garanti -- si l'audit subscriber s'execute avant le sync, il ne voit pas le changement de `product`
|
||||||
|
- Si `productIds` est vide, le subscriber ne touche pas `product` -- mais si `product` avait deja une valeur, elle reste (pas de cleanup)
|
||||||
|
|
||||||
|
**Solution proposee (effort S) :**
|
||||||
|
Remplacer ce subscriber par une logique explicite dans le controller/processor qui traite les pieces. Le sync `productIds -> product` devrait etre fait AVANT le flush, pas dans un subscriber. Cela supprime l'ambiguite sur l'ordre d'execution et le recompute.
|
||||||
|
|
||||||
|
Alternativement, si la relation legacy `product` (ManyToOne) n'est plus utilisee par le frontend, la supprimer completement et ne garder que `productIds` / les product slots.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Double flush dans les processors
|
||||||
|
|
||||||
|
**Fichiers concernes :**
|
||||||
|
- `/src/State/ModelTypeProcessor.php` (ligne 36 via decorated, ligne 43 manuellement)
|
||||||
|
- `/src/State/ComposantProcessor.php` (ligne 45 via decorated, ligne 132 manuellement)
|
||||||
|
|
||||||
|
**Probleme :** Le decorated processor fait un `flush()` pour persister l'entite, puis un second `flush()` est appele pour persister les skeleton requirements ou slots. Chaque flush declenche `onFlush` dans tous les audit subscribers.
|
||||||
|
|
||||||
|
**Effets de bord concrets :**
|
||||||
|
- Le premier flush capture le `create` de l'entite dans l'audit log
|
||||||
|
- Le second flush peut generer un `update` de la meme entite si les slots ont modifie une relation qui declenche un dirty check (par ex. si `$composant->incrementVersion()` etait appele)
|
||||||
|
- En cas d'erreur entre les deux flush, l'entite est persistee mais ses slots ne le sont pas -- etat inconsistant
|
||||||
|
|
||||||
|
**Solution proposee (effort S) :**
|
||||||
|
Wrapper les deux operations dans une transaction explicite, et ne faire qu'un seul flush a la fin :
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function process(mixed $data, Operation $operation, ...): mixed
|
||||||
|
{
|
||||||
|
return $this->entityManager->wrapInTransaction(function () use ($data, $operation, ...) {
|
||||||
|
// Ne pas flush dans le decorated -- utiliser le mode COMMIT_ON_CLOSE
|
||||||
|
$result = $this->decorated->process($data, $operation, $uriVariables, $context);
|
||||||
|
|
||||||
|
if (null !== $pendingStructure) {
|
||||||
|
$this->skeletonStructureService->updateSkeletonRequirements($data, $pendingStructure);
|
||||||
|
}
|
||||||
|
$data->clearPendingStructure();
|
||||||
|
|
||||||
|
// Un seul flush
|
||||||
|
$this->entityManager->flush();
|
||||||
|
return $result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note : cela necessite de verifier que le decorated processor ne fait pas deja un flush interne non configurable. Si c'est le cas, il faudrait potentiellement ne pas utiliser le decorated et gerer le persist manuellement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. `MachineStructureController` : God controller
|
||||||
|
|
||||||
|
**Fichier concerne :**
|
||||||
|
- `/src/Controller/MachineStructureController.php` (300+ LOC)
|
||||||
|
|
||||||
|
**Probleme :** Ce controller gere GET structure, PATCH structure, et POST clone. Il contient toute la logique de normalisation JSON des links (component, piece, product), la resolution des entites, et la serialisation manuelle de la reponse -- tout ce qu'API Platform fait normalement automatiquement.
|
||||||
|
|
||||||
|
**Effets de bord concrets :**
|
||||||
|
- La normalisation JSON manuelle (`normalizeStructureResponse`) ne passe pas par les serialization groups d'API Platform -- si un champ est ajoute a une entite avec un group, il n'apparaitra pas dans la reponse structure
|
||||||
|
- Le PATCH structure fait `$this->entityManager->flush()` sans transaction -- si la creation d'un link echoue, les precedents sont deja persistes
|
||||||
|
- Le clone copie les custom fields mais pas les documents -- comportement potentiellement inattendu
|
||||||
|
- 8 repositories injectes dans le constructeur -- code smell
|
||||||
|
|
||||||
|
**Solution proposee (effort L) :**
|
||||||
|
1. Extraire la logique de normalisation dans un `MachineStructureSerializer` service
|
||||||
|
2. Extraire la logique de clone dans un `MachineCloneService`
|
||||||
|
3. Wrapper le PATCH et le clone dans des transactions
|
||||||
|
4. A terme, considerer un DTO + custom provider/processor API Platform pour le GET/PATCH structure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Dependance circulaire dans `useMachineDetailData`
|
||||||
|
|
||||||
|
**Fichier concerne :**
|
||||||
|
- `/Inventory_frontend/app/composables/useMachineDetailData.ts` (lignes 119-187)
|
||||||
|
|
||||||
|
**Probleme :** `useMachineDetailProducts` a besoin de `machineProductLinks` (venant de hierarchy), et `useMachineDetailHierarchy` a besoin de `findProductById` (venant de products). La solution actuelle utilise un `_machineProductLinksProxy` ref avec un watcher pour synchroniser.
|
||||||
|
|
||||||
|
**Effets de bord concrets :**
|
||||||
|
- Le proxy ref est mis a jour de facon asynchrone via un watcher -- pendant le premier tick de rendu, `_machineProductLinksProxy` est vide meme si les liens sont deja charges
|
||||||
|
- L'ordre d'initialisation des sous-composables est fragile -- deplacer une ligne peut casser la boucle
|
||||||
|
- Le commentaire dans le code (lignes 119-122) admet explicitement le probleme
|
||||||
|
|
||||||
|
**Solution proposee (effort M) :**
|
||||||
|
Inverser la dependance : le composable `useMachineDetailHierarchy` devrait etre le seul a gerer les links et exposer les product links. `useMachineDetailProducts` ne devrait recevoir que les product IDs (pas les links complets). Cela supprime la circularite.
|
||||||
|
|
||||||
|
Alternativement, creer un `useMachineDetailState` purement reactif (store local) qui contient tous les refs partages, et le passer aux sous-composables. Cela explicite les dependances.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Typage `any` systematique sur les entites frontend
|
||||||
|
|
||||||
|
**Fichiers concernes :** Quasi tous les composables utilisent `ref<any | null>(null)` pour les entites :
|
||||||
|
- `useComponentEdit.ts` : `const component = ref<any | null>(null)` (ligne 74)
|
||||||
|
- `usePieceEdit.ts` : `const piece = ref<any | null>(null)` (ligne 56)
|
||||||
|
- `useMachineDetailData.ts` : `type AnyRecord = Record<string, unknown>`
|
||||||
|
|
||||||
|
**Probleme :** Les reponses API ne sont jamais typees. L'acces aux proprietes se fait par convention (`result.data?.structure?.pieces`) sans aucune validation. TypeScript ne peut pas detecter les typos ou les acces a des proprietes inexistantes.
|
||||||
|
|
||||||
|
**Effets de bord concrets :**
|
||||||
|
- Un changement de nom de champ cote API ne provoque aucune erreur TypeScript -- le bug n'est decouvert qu'au runtime
|
||||||
|
- L'autocompletion IDE est inutile sur ces objets
|
||||||
|
- Les defensives checks (`Array.isArray(x?.y) ? x.y : []`) sont necessaires partout parce que le type ne garantit rien
|
||||||
|
|
||||||
|
**Solution proposee (effort L) :**
|
||||||
|
1. Creer des interfaces TypeScript pour les reponses API principales : `MachineStructureResponse`, `ComposantResponse`, `PieceResponse`, `ProductResponse`, `ModelTypeResponse`
|
||||||
|
2. Ajouter une couche de validation a la reception dans `useApi.ts` (optionnelle, avec Zod ou un type guard maison)
|
||||||
|
3. Remplacer progressivement `ref<any>` par `ref<ComposantResponse | null>`
|
||||||
|
|
||||||
|
Commencer par les entites les plus utilisees (Machine, Composant) pour obtenir un benefice immediat.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plan de simplification -- Ordre recommande
|
||||||
|
|
||||||
|
### Phase 1 : Quick wins (1-2 jours chacun, impact immediat)
|
||||||
|
|
||||||
|
| # | Action | Source | Effort |
|
||||||
|
|---|--------|--------|--------|
|
||||||
|
| 1 | Extraire `smartMatch` + sync CF values dans un trait partage | Source 1 | S |
|
||||||
|
| 2 | Ajouter `try/finally` sur `clearPendingStructure` | Source 5 | S |
|
||||||
|
| 3 | Remplacer `PieceProductSyncSubscriber` par logique explicite | Source 6 | S |
|
||||||
|
| 4 | Wrapper les processors dans des transactions | Source 7 | S |
|
||||||
|
|
||||||
|
### Phase 2 : Unification frontend (1-2 semaines)
|
||||||
|
|
||||||
|
| # | Action | Source | Effort |
|
||||||
|
|---|--------|--------|--------|
|
||||||
|
| 5 | Fusionner les 3 fichiers custom fields utils en un seul | Source 4 | M |
|
||||||
|
| 6 | Decouper `useComponentEdit` / `usePieceEdit` en sous-composables | Source 3 | M |
|
||||||
|
| 7 | Resoudre la circularite dans `useMachineDetailData` | Source 9 | M |
|
||||||
|
|
||||||
|
### Phase 3 : Renforcement structurel (2-4 semaines)
|
||||||
|
|
||||||
|
| # | Action | Source | Effort |
|
||||||
|
|---|--------|--------|--------|
|
||||||
|
| 8 | Ajouter la contrainte CHECK sur `custom_field_values` | Source 2 | M |
|
||||||
|
| 9 | Typer les reponses API principales | Source 10 | L |
|
||||||
|
| 10 | Extraire services depuis `MachineStructureController` | Source 8 | L |
|
||||||
|
|
||||||
|
### Principe directeur
|
||||||
|
|
||||||
|
**Commencer par la phase 1** -- elle ne modifie pas les interfaces (ni API ni frontend) et supprime les effets de bord les plus dangereux. La phase 2 est une consolidation frontend qui peut etre faite page par page. La phase 3 est un investissement a plus long terme.
|
||||||
|
|
||||||
|
Ne pas tenter de tout refactorer en une fois. Chaque item peut etre un PR isole, testable independamment.
|
||||||
185
docs/mcp/README.md
Normal file
185
docs/mcp/README.md
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
# MCP Server — Inventory
|
||||||
|
|
||||||
|
Serveur MCP (Model Context Protocol) pour l'application Inventory. Permet aux assistants IA (Claude, ChatGPT, Codex) de consulter et gérer l'inventaire industriel.
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
- Un profil actif avec rôle suffisant (ROLE_VIEWER pour lecture, ROLE_GESTIONNAIRE pour écriture)
|
||||||
|
- Accès au tunnel pour les clients distants (Claude Desktop, ChatGPT Desktop)
|
||||||
|
- Docker Compose démarré (`make start`)
|
||||||
|
|
||||||
|
## Configuration par client
|
||||||
|
|
||||||
|
### Claude Code (local, stdio)
|
||||||
|
|
||||||
|
Le fichier `.mcp.json` à la racine du projet est déjà configuré. Remplacez les placeholders :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"inventory": {
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"exec", "-i",
|
||||||
|
"-e", "MCP_PROFILE_ID=VOTRE_PROFILE_ID",
|
||||||
|
"-e", "MCP_PROFILE_PASSWORD=VOTRE_PASSWORD",
|
||||||
|
"php-inventory-apache",
|
||||||
|
"php", "bin/console", "mcp:server"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Desktop (HTTP via tunnel)
|
||||||
|
|
||||||
|
Dans `claude_desktop_config.json` :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"inventory": {
|
||||||
|
"url": "https://inventory.company-tunnel.com/_mcp",
|
||||||
|
"headers": {
|
||||||
|
"X-Profile-Id": "VOTRE_PROFILE_ID",
|
||||||
|
"X-Profile-Password": "VOTRE_PASSWORD"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ChatGPT Desktop / Codex
|
||||||
|
|
||||||
|
Meme principe HTTP avec l'URL du tunnel + headers d'auth.
|
||||||
|
|
||||||
|
## Catalogue des Tools
|
||||||
|
|
||||||
|
### Tools de haut niveau
|
||||||
|
|
||||||
|
| Tool | Description | Role |
|
||||||
|
|------|-------------|------|
|
||||||
|
| `search_inventory` | Recherche globale (machines, pieces, composants, produits, sites, constructeurs) | VIEWER |
|
||||||
|
| `get_machine_structure` | Hierarchie complete d'une machine | VIEWER |
|
||||||
|
| `clone_machine` | Clone une machine avec toute sa structure | GESTIONNAIRE |
|
||||||
|
| `get_dashboard_stats` | Statistiques globales | VIEWER |
|
||||||
|
| `get_entity_history` | Historique d'audit d'une entite | VIEWER |
|
||||||
|
| `get_activity_log` | Journal d'activite global | VIEWER |
|
||||||
|
|
||||||
|
### CRUD par entite
|
||||||
|
|
||||||
|
Pour chaque entite (Machine, Composant, Piece, Produit, Site, Constructeur) :
|
||||||
|
|
||||||
|
| Pattern | Exemple | Role |
|
||||||
|
|---------|---------|------|
|
||||||
|
| `list_{entite}s` | `list_machines` | VIEWER |
|
||||||
|
| `get_{entite}` | `get_machine` | VIEWER |
|
||||||
|
| `create_{entite}` | `create_machine` | GESTIONNAIRE |
|
||||||
|
| `update_{entite}` | `update_machine` | GESTIONNAIRE |
|
||||||
|
| `delete_{entite}` | `delete_machine` | GESTIONNAIRE |
|
||||||
|
|
||||||
|
### Slots
|
||||||
|
|
||||||
|
| Tool | Description | Role |
|
||||||
|
|------|-------------|------|
|
||||||
|
| `list_slots` | Lister les slots d'un composant ou piece | VIEWER |
|
||||||
|
| `update_slots` | Remplir/vider les slots | GESTIONNAIRE |
|
||||||
|
|
||||||
|
### Machine Links
|
||||||
|
|
||||||
|
| Tool | Description | Role |
|
||||||
|
|------|-------------|------|
|
||||||
|
| `list_machine_links` | Liens composant/piece/produit d'une machine | VIEWER |
|
||||||
|
| `add_machine_links` | Ajouter des liens | GESTIONNAIRE |
|
||||||
|
| `update_machine_link` | Modifier un lien | GESTIONNAIRE |
|
||||||
|
| `remove_machine_link` | Supprimer un lien | GESTIONNAIRE |
|
||||||
|
|
||||||
|
### Commentaires
|
||||||
|
|
||||||
|
| Tool | Description | Role |
|
||||||
|
|------|-------------|------|
|
||||||
|
| `list_comments` | Lister les commentaires d'une entite | VIEWER |
|
||||||
|
| `create_comment` | Creer un commentaire | VIEWER |
|
||||||
|
| `resolve_comment` | Resoudre un commentaire | GESTIONNAIRE |
|
||||||
|
| `get_unresolved_comments_count` | Nombre de commentaires non resolus | VIEWER |
|
||||||
|
|
||||||
|
### Custom Fields
|
||||||
|
|
||||||
|
| Tool | Description | Role |
|
||||||
|
|------|-------------|------|
|
||||||
|
| `list_custom_field_values` | Valeurs de champs perso d'une entite | VIEWER |
|
||||||
|
| `upsert_custom_field_values` | Creer/mettre a jour des valeurs | GESTIONNAIRE |
|
||||||
|
| `delete_custom_field_value` | Supprimer une valeur | GESTIONNAIRE |
|
||||||
|
|
||||||
|
### Documents
|
||||||
|
|
||||||
|
| Tool | Description | Role |
|
||||||
|
|------|-------------|------|
|
||||||
|
| `list_documents` | Lister les documents d'une entite | VIEWER |
|
||||||
|
| `delete_document` | Supprimer un document | GESTIONNAIRE |
|
||||||
|
|
||||||
|
> **Limitation :** L'upload de documents n'est pas supporte via MCP (protocole JSON uniquement). Utilisez l'API REST `/api/documents` (POST multipart).
|
||||||
|
|
||||||
|
### ModelTypes
|
||||||
|
|
||||||
|
| Tool | Description | Role |
|
||||||
|
|------|-------------|------|
|
||||||
|
| `list_model_types` | Lister par categorie | VIEWER |
|
||||||
|
| `get_model_type` | Detail avec skeleton requirements | VIEWER |
|
||||||
|
| `create_model_type` | Creer | GESTIONNAIRE |
|
||||||
|
| `update_model_type` | Modifier | GESTIONNAIRE |
|
||||||
|
| `delete_model_type` | Supprimer | GESTIONNAIRE |
|
||||||
|
| `sync_model_type` | Preview/sync skeleton | GESTIONNAIRE |
|
||||||
|
|
||||||
|
## Workflows guides
|
||||||
|
|
||||||
|
### Creer un composant complet
|
||||||
|
|
||||||
|
```
|
||||||
|
1. list_model_types(category: "composant") -> choisir le type
|
||||||
|
2. get_model_type(modelTypeId: "...") -> voir le skeleton
|
||||||
|
3. create_composant(name, reference, modelTypeId) -> cree + slots auto
|
||||||
|
4. search_inventory(query: "Roulement", types: "piece") -> trouver pieces
|
||||||
|
5. update_slots(slots: [{slotId, selectedPieceId}]) -> remplir
|
||||||
|
6. upsert_custom_field_values(entityType: "composant", entityId, fields: [...])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creer une machine complete (bottom-up)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Creer les produits necessaires
|
||||||
|
2. Creer les pieces (avec produits dans les slots)
|
||||||
|
3. Creer les composants (avec pieces dans les slots)
|
||||||
|
4. list_sites -> choisir le site
|
||||||
|
5. create_machine(name, siteId)
|
||||||
|
6. add_machine_links(machineId, links: [{type: "composant", entityId, quantity}])
|
||||||
|
7. upsert_custom_field_values(entityType: "machine", machineId, fields: [...])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources MCP
|
||||||
|
|
||||||
|
| URI | Description |
|
||||||
|
|-----|-------------|
|
||||||
|
| `inventory://schema/entities` | Schema de toutes les entites |
|
||||||
|
| `inventory://roles` | Hierarchie des roles et permissions |
|
||||||
|
| `inventory://stats` | Statistiques globales |
|
||||||
|
|
||||||
|
## Roles & Permissions
|
||||||
|
|
||||||
|
```
|
||||||
|
ROLE_ADMIN > ROLE_GESTIONNAIRE > ROLE_VIEWER > ROLE_USER
|
||||||
|
```
|
||||||
|
|
||||||
|
- **VIEWER** : lecture, recherche, commentaires
|
||||||
|
- **GESTIONNAIRE** : ecriture (CRUD, slots, links, clone)
|
||||||
|
- **ADMIN** : gestion profils (via API REST uniquement)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Erreur | Cause | Solution |
|
||||||
|
|--------|-------|----------|
|
||||||
|
| `401 Unauthorized` | Credentials invalides | Verifier X-Profile-Id et X-Profile-Password |
|
||||||
|
| `Permission denied: ROLE_GESTIONNAIRE required` | Role insuffisant | Utiliser un profil avec le bon role |
|
||||||
|
| `Rate limited` | Trop de tentatives echouees | Attendre 1 minute |
|
||||||
|
| `Tool not found` | Tool non enregistre | Verifier que le cache est a jour (`cache:clear`) |
|
||||||
|
| `Error while executing tool` | Erreur interne | Verifier les logs et les parametres |
|
||||||
1067
docs/superpowers/plans/2026-03-12-json-to-tables-normalization.md
Normal file
1067
docs/superpowers/plans/2026-03-12-json-to-tables-normalization.md
Normal file
File diff suppressed because it is too large
Load Diff
546
docs/superpowers/plans/2026-03-12-piece-quantity.md
Normal file
546
docs/superpowers/plans/2026-03-12-piece-quantity.md
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
# Piece Quantity Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add a quantity field to pieces — on `MachinePieceLink` for machine-direct pieces, and in `Composant.structure.pieces[]` JSON for composant pieces.
|
||||||
|
|
||||||
|
**Architecture:** Quantity lives on the relationship, not the catalogue entity. For machine-direct pieces, a new `quantity` integer column on `MachinePieceLink` (default 1). For composant pieces, a `quantity` key in the existing `structure.pieces[]` JSON (default 1). Display: "×N" after piece name, hidden when N=1.
|
||||||
|
|
||||||
|
**Tech Stack:** Symfony 8 / API Platform, Doctrine ORM, PostgreSQL, Nuxt 4, Vue 3 Composition API, TypeScript, DaisyUI 5
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-03-12-piece-quantity-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 1: Backend
|
||||||
|
|
||||||
|
### Task 1: Entity + Migration
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Entity/MachinePieceLink.php`
|
||||||
|
- Existing: `migrations/Version20260309150000.php` (already written, untracked)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add quantity field to MachinePieceLink entity**
|
||||||
|
|
||||||
|
In `src/Entity/MachinePieceLink.php`, add after the `prixOverride` field (line 69):
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[ORM\Column(type: Types::INTEGER, options: ['default' => 1])]
|
||||||
|
#[Assert\GreaterThanOrEqual(1)]
|
||||||
|
private int $quantity = 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the import at the top if not present:
|
||||||
|
```php
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add getter/setter after existing methods (before closing brace):
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function getQuantity(): int
|
||||||
|
{
|
||||||
|
return $this->quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setQuantity(int $quantity): static
|
||||||
|
{
|
||||||
|
$this->quantity = $quantity;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Stage the migration file**
|
||||||
|
|
||||||
|
The migration `migrations/Version20260309150000.php` already exists (untracked). Verify its content matches:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE machine_piece_links ADD COLUMN IF NOT EXISTS quantity INTEGER NOT NULL DEFAULT 1');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run php-cs-fixer**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests to verify nothing is broken**
|
||||||
|
|
||||||
|
Run: `make test`
|
||||||
|
Expected: All 167 tests pass (OK, with possible deprecation warnings)
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Entity/MachinePieceLink.php migrations/Version20260309150000.php
|
||||||
|
git commit -m "feat(piece) : add quantity field to MachinePieceLink entity + migration"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: MachineStructureController — Normalization + PATCH + Clone
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Controller/MachineStructureController.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add quantity to `normalizePieceLinks()`**
|
||||||
|
|
||||||
|
In `src/Controller/MachineStructureController.php`, method `normalizePieceLinks()` (line ~623-641).
|
||||||
|
|
||||||
|
Add `'quantity'` to the returned array, after `'overrides'`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'quantity' => $this->resolvePieceQuantity($link),
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a new private method after `normalizePieceLinks()`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
private function resolvePieceQuantity(MachinePieceLink $link): int
|
||||||
|
{
|
||||||
|
$parentLink = $link->getParentLink();
|
||||||
|
|
||||||
|
if (!$parentLink) {
|
||||||
|
return $link->getQuantity();
|
||||||
|
}
|
||||||
|
|
||||||
|
$composant = $parentLink->getComposant();
|
||||||
|
$structure = $composant->getStructure();
|
||||||
|
|
||||||
|
if (!is_array($structure) || !isset($structure['pieces']) || !is_array($structure['pieces'])) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$piece = $link->getPiece();
|
||||||
|
$typePiece = $piece->getTypePiece();
|
||||||
|
$typePieceId = $typePiece?->getId();
|
||||||
|
|
||||||
|
foreach ($structure['pieces'] as $pieceDef) {
|
||||||
|
if (!is_array($pieceDef)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isset($pieceDef['typePieceId']) && $pieceDef['typePieceId'] === $typePieceId) {
|
||||||
|
return (int) ($pieceDef['quantity'] ?? 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Matching is done by `typePieceId`. If a composant has two pieces of the same type, they will get the same quantity (first match). This is an acceptable limitation for now — duplicates of the same piece type in a composant are rare.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Apply quantity in `applyPieceLinks()`**
|
||||||
|
|
||||||
|
In method `applyPieceLinks()` (line ~366-422), add quantity application after `$this->applyOverrides($link, $entry['overrides'] ?? null);` (line ~396):
|
||||||
|
|
||||||
|
```php
|
||||||
|
if (!isset($entry['parentComponentLinkId']) && !isset($entry['parentLinkId'])) {
|
||||||
|
$quantity = isset($entry['quantity']) ? (int) $entry['quantity'] : $link->getQuantity();
|
||||||
|
$link->setQuantity(max(1, $quantity));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key behavior:**
|
||||||
|
- Only applies to direct machine pieces (no parent component link)
|
||||||
|
- If `quantity` not in payload: preserves existing value
|
||||||
|
- If `quantity` in payload: sets it, with floor of 1
|
||||||
|
- For composant-child pieces: quantity is ignored (comes from composant structure)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Copy quantity in `clonePieceLinks()`**
|
||||||
|
|
||||||
|
In method `clonePieceLinks()` (line ~233-256), add after `$newLink->setPrixOverride($link->getPrixOverride());` (line ~244):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$newLink->setQuantity($link->getQuantity());
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run php-cs-fixer**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run tests**
|
||||||
|
|
||||||
|
Run: `make test`
|
||||||
|
Expected: All tests pass
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Controller/MachineStructureController.php
|
||||||
|
git commit -m "feat(piece) : add quantity to structure normalization, PATCH and clone"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Backend Tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/Api/Entity/MachinePieceLinkTest.php`
|
||||||
|
- Modify: `tests/AbstractApiTestCase.php` (factory method)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update factory method to support quantity**
|
||||||
|
|
||||||
|
In `tests/AbstractApiTestCase.php`, update `createMachinePieceLink()` to accept an optional quantity parameter:
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected function createMachinePieceLink(Machine $machine, Piece $piece, ?MachineComponentLink $parentLink = null, int $quantity = 1): MachinePieceLink
|
||||||
|
{
|
||||||
|
$link = new MachinePieceLink();
|
||||||
|
$link->setMachine($machine);
|
||||||
|
$link->setPiece($piece);
|
||||||
|
$link->setQuantity($quantity);
|
||||||
|
if (null !== $parentLink) {
|
||||||
|
$link->setParentLink($parentLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$em->persist($link);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
return $link;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add test for POST with explicit quantity**
|
||||||
|
|
||||||
|
In `tests/Api/Entity/MachinePieceLinkTest.php`, add. Follow the existing test pattern — use `$this->assertJsonContains()` and include the `headers` key with `Content-Type`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function testPostWithQuantity(): void
|
||||||
|
{
|
||||||
|
$client = $this->createGestionnaireClient();
|
||||||
|
$machine = $this->createMachine();
|
||||||
|
$piece = $this->createPiece();
|
||||||
|
|
||||||
|
$client->request('POST', '/api/machine_piece_links', [
|
||||||
|
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||||
|
'json' => [
|
||||||
|
'machine' => '/api/machines/' . $machine->getId(),
|
||||||
|
'piece' => '/api/pieces/' . $piece->getId(),
|
||||||
|
'quantity' => 5,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(201);
|
||||||
|
$this->assertJsonContains(['quantity' => 5]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add test for POST without quantity (default = 1)**
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function testPostDefaultQuantity(): void
|
||||||
|
{
|
||||||
|
$client = $this->createGestionnaireClient();
|
||||||
|
$machine = $this->createMachine();
|
||||||
|
$piece = $this->createPiece();
|
||||||
|
|
||||||
|
$client->request('POST', '/api/machine_piece_links', [
|
||||||
|
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||||
|
'json' => [
|
||||||
|
'machine' => '/api/machines/' . $machine->getId(),
|
||||||
|
'piece' => '/api/pieces/' . $piece->getId(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(201);
|
||||||
|
$this->assertJsonContains(['quantity' => 1]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests**
|
||||||
|
|
||||||
|
Run: `make test`
|
||||||
|
Expected: All tests pass including new ones
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run php-cs-fixer**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky`
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/Api/Entity/MachinePieceLinkTest.php tests/AbstractApiTestCase.php
|
||||||
|
git commit -m "test(piece) : add quantity tests for MachinePieceLink"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 2: Frontend
|
||||||
|
|
||||||
|
### Task 4: TypeScript Types + Sanitization + Hydration Functions
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/shared/types/inventory.ts`
|
||||||
|
- Modify: `Inventory_frontend/app/shared/model/componentStructure.ts`
|
||||||
|
- Modify: `Inventory_frontend/app/shared/model/componentStructureSanitize.ts`
|
||||||
|
- Modify: `Inventory_frontend/app/shared/model/componentStructureHydrate.ts`
|
||||||
|
- Modify: `Inventory_frontend/app/shared/utils/structureAssignmentHelpers.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `quantity` to `ComponentModelPiece` type**
|
||||||
|
|
||||||
|
In `Inventory_frontend/app/shared/types/inventory.ts`, add `quantity` to the `ComponentModelPiece` interface (after `role`, line ~23):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
quantity?: number
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add `quantity` to `validatePiece()` in same file**
|
||||||
|
|
||||||
|
In `Inventory_frontend/app/shared/types/inventory.ts`, in `validatePiece()` (line ~144-172):
|
||||||
|
|
||||||
|
After `const role = ensureString(value.role)` (line ~161), add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const quantity = typeof value.quantity === 'number' && value.quantity >= 1 ? value.quantity : undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
And in the return object, add after the `role` spread:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
...(quantity ? { quantity } : {}),
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update `sanitizePieces()` to preserve quantity**
|
||||||
|
|
||||||
|
In `Inventory_frontend/app/shared/model/componentStructureSanitize.ts`, in `sanitizePieces()` (~line 130-188).
|
||||||
|
|
||||||
|
After the existing field extractions, add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const quantity = typeof piece?.quantity === 'number' && piece.quantity >= 1 ? piece.quantity : undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
In the result object construction, add alongside existing fields (follow the `if (field) { result.field = field }` pattern used in this function):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (quantity !== undefined) {
|
||||||
|
result.quantity = quantity
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update `normalizeStructureForSave()` to include quantity**
|
||||||
|
|
||||||
|
In `Inventory_frontend/app/shared/model/componentStructure.ts`, in `normalizeStructureForSave()` (~lines 164-179), add in the piece payload mapping after the `reference` check:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if ((piece as any).quantity !== undefined && (piece as any).quantity >= 1) {
|
||||||
|
payload.quantity = (piece as any).quantity
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Always send quantity when defined (including 1), so the backend always has an explicit value.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Update `hydratePieces()` and `mapComponentPieces()` to preserve quantity**
|
||||||
|
|
||||||
|
In `Inventory_frontend/app/shared/model/componentStructureHydrate.ts`:
|
||||||
|
|
||||||
|
In `hydratePieces()` (line ~95-107), add to the mapped object:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
...(piece?.quantity !== undefined && piece.quantity >= 1 ? { quantity: piece.quantity } : {}),
|
||||||
|
```
|
||||||
|
|
||||||
|
In `mapComponentPieces()` (line ~168-179), add to the mapped object:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
...(piece?.quantity !== undefined && piece.quantity >= 1 ? { quantity: piece.quantity } : {}),
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Update `sanitizePieceDefinition()` to preserve quantity**
|
||||||
|
|
||||||
|
In `Inventory_frontend/app/shared/utils/structureAssignmentHelpers.ts`, in `sanitizePieceDefinition()` (~lines 172-180), add to the `stripNullish()` object:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
quantity: typeof (definition as any).quantity === 'number' ? (definition as any).quantity : null,
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Run lint + typecheck**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend && npm run lint:fix && npx nuxi typecheck
|
||||||
|
```
|
||||||
|
Expected: 0 errors
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend
|
||||||
|
git add app/shared/types/inventory.ts app/shared/model/componentStructure.ts app/shared/model/componentStructureSanitize.ts app/shared/model/componentStructureHydrate.ts app/shared/utils/structureAssignmentHelpers.ts
|
||||||
|
git commit -m "feat(piece) : add quantity field to piece types, sanitization and hydration"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Composant Structure Editor — Quantity Input
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/components/StructureNodeEditor.vue` (piece section, lines ~229-299)
|
||||||
|
- Modify: `Inventory_frontend/app/composables/useStructureNodeCrud.ts` (`addPiece()`, lines ~110-118)
|
||||||
|
|
||||||
|
**Context:** `StructureNodeEditor.vue` renders the composant structure editor. The piece section (lines ~236-293) currently shows only a `select` for `typePieceId` and a delete button. The `addPiece()` function in `useStructureNodeCrud.ts` creates new piece entries with default fields.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add default quantity to `addPiece()`**
|
||||||
|
|
||||||
|
In `Inventory_frontend/app/composables/useStructureNodeCrud.ts`, in `addPiece()` (line ~110-118), add `quantity: 1` to the pushed object:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const addPiece = () => {
|
||||||
|
ensureArray('pieces')
|
||||||
|
props.node.pieces!.push({
|
||||||
|
typePieceId: '',
|
||||||
|
typePieceLabel: '',
|
||||||
|
reference: '',
|
||||||
|
familyCode: '',
|
||||||
|
role: '',
|
||||||
|
quantity: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add quantity input in `StructureNodeEditor.vue`**
|
||||||
|
|
||||||
|
In `Inventory_frontend/app/components/StructureNodeEditor.vue`, in the piece item rendering section (inside the `v-for` loop for pieces, line ~256-292), add a quantity input next to the existing piece type `select`. Place it after the select and before the delete button:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<input
|
||||||
|
v-model.number="piece.quantity"
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
step="1"
|
||||||
|
placeholder="Qté"
|
||||||
|
class="input input-bordered input-sm md:input-md w-20"
|
||||||
|
@input="piece.quantity = Math.max(1, piece.quantity || 1)"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run lint + typecheck**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend && npm run lint:fix && npx nuxi typecheck
|
||||||
|
```
|
||||||
|
Expected: 0 errors
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend
|
||||||
|
git add app/components/StructureNodeEditor.vue app/composables/useStructureNodeCrud.ts
|
||||||
|
git commit -m "feat(piece) : add quantity input to composant structure editor"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Machine Detail Page — Display Quantity
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/components/PieceItem.vue`
|
||||||
|
|
||||||
|
**Context:** `PieceItem.vue` renders each piece in the machine structure view. The piece name is displayed at line ~26 in an `<h3>` tag. Quantity should appear as "×N" after the name, in secondary text. For direct pieces (no parent component), it should be editable. For composant pieces, read-only.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add quantity display to PieceItem**
|
||||||
|
|
||||||
|
In `Inventory_frontend/app/components/PieceItem.vue`, after the piece name in the `<h3>` tag (line ~26), add the quantity display:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<span
|
||||||
|
v-if="displayQuantity > 1"
|
||||||
|
class="text-sm font-normal text-base-content/60 ml-1"
|
||||||
|
>
|
||||||
|
×{{ displayQuantity }}
|
||||||
|
</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to the component's setup:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const displayQuantity = computed(() => {
|
||||||
|
return props.piece.quantity ?? 1
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add editable quantity for direct machine pieces**
|
||||||
|
|
||||||
|
For pieces directly on a machine (no `parentComponentLinkId`), add an editable quantity input in the piece's edit section, following the pattern of existing override fields (nameOverride, referenceOverride, prixOverride). Place it alongside the overrides form:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<div v-if="!piece.parentComponentLinkId && isEditMode" class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text text-sm">Quantité</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model.number="pieceData.quantity"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
class="input input-bordered input-sm md:input-md w-24"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `quantity` to the `pieceData` reactive object (line ~270-275):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
quantity: props.piece.quantity ?? 1,
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure this value is included in the data emitted when saving (follow the same pattern as `nameOverride`, `referenceOverride`, `prixOverride` in the save/emit logic).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run lint + typecheck**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend && npm run lint:fix && npx nuxi typecheck
|
||||||
|
```
|
||||||
|
Expected: 0 errors
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend
|
||||||
|
git add app/components/PieceItem.vue
|
||||||
|
git commit -m "feat(piece) : display and edit quantity on machine piece items"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Submodule Update + Final Verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Update submodule pointer in main repo
|
||||||
|
|
||||||
|
- [ ] **Step 1: Push frontend commits**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend && git push
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update submodule pointer in main repo**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/matthieu/dev_malio/Inventory
|
||||||
|
git add Inventory_frontend
|
||||||
|
git commit -m "chore(frontend) : update submodule — piece quantity feature"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run all backend tests**
|
||||||
|
|
||||||
|
Run: `make test`
|
||||||
|
Expected: All tests pass
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run migration on dev database**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Manual smoke test**
|
||||||
|
|
||||||
|
1. Open a composant edit page → verify quantity input appears on each piece in structure
|
||||||
|
2. Set quantity to 4, save → reload → verify quantity persisted
|
||||||
|
3. Open a machine with that composant → verify "×4" appears next to piece name (read-only)
|
||||||
|
4. Add a piece directly to a machine → verify quantity input appears in edit mode
|
||||||
|
5. Set quantity to 3, save → verify "×3" appears
|
||||||
|
6. Clone the machine → verify cloned pieces have same quantities
|
||||||
1582
docs/superpowers/plans/2026-03-13-modeltype-sync.md
Normal file
1582
docs/superpowers/plans/2026-03-13-modeltype-sync.md
Normal file
File diff suppressed because it is too large
Load Diff
1472
docs/superpowers/plans/2026-03-16-mcp-server.md
Normal file
1472
docs/superpowers/plans/2026-03-16-mcp-server.md
Normal file
File diff suppressed because it is too large
Load Diff
871
docs/superpowers/plans/2026-03-23-comment-documents.md
Normal file
871
docs/superpowers/plans/2026-03-23-comment-documents.md
Normal file
@@ -0,0 +1,871 @@
|
|||||||
|
# Comment Document Attachments — Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Allow users to attach one or more documents when creating a comment, via a single multipart/form-data request.
|
||||||
|
|
||||||
|
**Architecture:** Add a `comment` ManyToOne on Document entity (same pattern as machine/site/etc.), modify `CommentController::create()` to accept multipart/form-data with files + text fields, store files via existing `DocumentStorageService`, and update the frontend `CommentSection.vue` to include a file picker.
|
||||||
|
|
||||||
|
**Tech Stack:** Symfony 8, Doctrine, API Platform, Vue 3 Composition API, TypeScript, TailwindCSS/DaisyUI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Migration — add `comment_id` FK on `documents`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `migrations/Version20260323160000.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the migration**
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260323160000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add comment_id FK on documents table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql("DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'documents' AND column_name = 'comment_id') THEN ALTER TABLE documents ADD COLUMN comment_id VARCHAR(36) DEFAULT NULL; END IF; END $$");
|
||||||
|
$this->addSql("DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_documents_comment') THEN ALTER TABLE documents ADD CONSTRAINT fk_documents_comment FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE; END IF; END $$");
|
||||||
|
$this->addSql("CREATE INDEX IF NOT EXISTS idx_documents_comment_id ON documents(comment_id)");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS fk_documents_comment');
|
||||||
|
$this->addSql('DROP INDEX IF EXISTS idx_documents_comment_id');
|
||||||
|
$this->addSql('ALTER TABLE documents DROP COLUMN IF EXISTS comment_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the migration**
|
||||||
|
|
||||||
|
Run: `docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate --no-interaction`
|
||||||
|
Expected: Migration executes successfully.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update test schema**
|
||||||
|
|
||||||
|
Run: `make test-setup`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add migrations/Version20260323160000.php
|
||||||
|
git commit -m "feat(documents) : add comment_id FK on documents table"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Entity updates — Document.comment + Comment.documents
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Entity/Document.php`
|
||||||
|
- Modify: `src/Entity/Comment.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `comment` ManyToOne on Document entity**
|
||||||
|
|
||||||
|
In `src/Entity/Document.php`, add after the `$site` property (around line 109):
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[ORM\ManyToOne(targetEntity: Comment::class, inversedBy: 'documents')]
|
||||||
|
#[ORM\JoinColumn(name: 'comment_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
|
private ?Comment $comment = null;
|
||||||
|
```
|
||||||
|
|
||||||
|
And add getter/setter:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function getComment(): ?Comment
|
||||||
|
{
|
||||||
|
return $this->comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setComment(?Comment $comment): static
|
||||||
|
{
|
||||||
|
$this->comment = $comment;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add `documents` OneToMany on Comment entity**
|
||||||
|
|
||||||
|
In `src/Entity/Comment.php`, add the import:
|
||||||
|
```php
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add property after `$updatedAt`:
|
||||||
|
```php
|
||||||
|
/** @var Collection<int, Document> */
|
||||||
|
#[ORM\OneToMany(targetEntity: Document::class, mappedBy: 'comment', cascade: ['remove'])]
|
||||||
|
private Collection $documents;
|
||||||
|
```
|
||||||
|
|
||||||
|
Initialize in constructor:
|
||||||
|
```php
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->createdAt = new DateTimeImmutable();
|
||||||
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
|
$this->documents = new ArrayCollection();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add getter:
|
||||||
|
```php
|
||||||
|
/** @return Collection<int, Document> */
|
||||||
|
public function getDocuments(): Collection
|
||||||
|
{
|
||||||
|
return $this->documents;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run php-cs-fixer**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests to check nothing broke**
|
||||||
|
|
||||||
|
Run: `make test`
|
||||||
|
Expected: All existing tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Entity/Document.php src/Entity/Comment.php
|
||||||
|
git commit -m "feat(documents) : add Comment-Document relationship (ManyToOne/OneToMany)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Update CommentController to accept multipart/form-data with files
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Controller/CommentController.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add DocumentStorageService dependency and update create() method**
|
||||||
|
|
||||||
|
Update constructor to inject `DocumentStorageService`:
|
||||||
|
```php
|
||||||
|
use App\Entity\Document;
|
||||||
|
use App\Enum\DocumentType;
|
||||||
|
use App\Service\DocumentStorageService;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
private readonly DocumentStorageService $storageService,
|
||||||
|
) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the `create()` method body to handle both JSON and multipart:
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse fields from JSON or form-data
|
||||||
|
$contentType = $request->headers->get('Content-Type', '');
|
||||||
|
if (str_contains($contentType, 'multipart/form-data')) {
|
||||||
|
$content = trim((string) $request->request->get('content', ''));
|
||||||
|
$entityType = trim((string) $request->request->get('entityType', ''));
|
||||||
|
$entityId = trim((string) $request->request->get('entityId', ''));
|
||||||
|
$entityName = $request->request->get('entityName') ? trim((string) $request->request->get('entityName')) : null;
|
||||||
|
} else {
|
||||||
|
$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);
|
||||||
|
|
||||||
|
// Handle file uploads
|
||||||
|
/** @var UploadedFile[] $files */
|
||||||
|
$files = $request->files->all('files');
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (!$file instanceof UploadedFile || !$file->isValid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$document = new Document();
|
||||||
|
$documentId = 'cl'.bin2hex(random_bytes(12));
|
||||||
|
$document->setId($documentId);
|
||||||
|
$document->setName($file->getClientOriginalName());
|
||||||
|
$document->setFilename($file->getClientOriginalName());
|
||||||
|
$document->setMimeType($file->getMimeType() ?: 'application/octet-stream');
|
||||||
|
$document->setSize((int) $file->getSize());
|
||||||
|
$document->setType(DocumentType::DOCUMENTATION);
|
||||||
|
$document->setComment($comment);
|
||||||
|
|
||||||
|
$extension = $this->storageService->extensionFromFilename($file->getClientOriginalName());
|
||||||
|
$relativePath = $this->storageService->storeFromPath(
|
||||||
|
$file->getPathname(),
|
||||||
|
$documentId,
|
||||||
|
$extension,
|
||||||
|
);
|
||||||
|
$document->setPath($relativePath);
|
||||||
|
|
||||||
|
$this->entityManager->persist($document);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $this->json($this->normalize($comment), 201);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update normalize() to include documents**
|
||||||
|
|
||||||
|
```php
|
||||||
|
private function normalize(Comment $comment): array
|
||||||
|
{
|
||||||
|
$documents = [];
|
||||||
|
foreach ($comment->getDocuments() as $document) {
|
||||||
|
$documents[] = [
|
||||||
|
'id' => $document->getId(),
|
||||||
|
'name' => $document->getName(),
|
||||||
|
'filename' => $document->getFilename(),
|
||||||
|
'mimeType' => $document->getMimeType(),
|
||||||
|
'size' => $document->getSize(),
|
||||||
|
'type' => $document->getType()->value,
|
||||||
|
'fileUrl' => '/api/documents/'.$document->getId().'/file',
|
||||||
|
'downloadUrl' => '/api/documents/'.$document->getId().'/download',
|
||||||
|
'createdAt' => $document->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
'documents' => $documents,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run php-cs-fixer**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests**
|
||||||
|
|
||||||
|
Run: `make test`
|
||||||
|
Expected: All existing tests still pass (they use JSON, not multipart).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Controller/CommentController.php
|
||||||
|
git commit -m "feat(comments) : accept multipart/form-data with file uploads on create"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Update DocumentUploadProcessor and DocumentQueryController
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/State/DocumentUploadProcessor.php`
|
||||||
|
- Modify: `src/Controller/DocumentQueryController.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `commentId` to DocumentUploadProcessor relation map**
|
||||||
|
|
||||||
|
In `src/State/DocumentUploadProcessor.php`, update `$relationMap` in `setRelationsFromRequest()`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$relationMap = [
|
||||||
|
'machineId' => 'Machine',
|
||||||
|
'composantId' => 'Composant',
|
||||||
|
'pieceId' => 'Piece',
|
||||||
|
'productId' => 'Product',
|
||||||
|
'siteId' => 'Site',
|
||||||
|
'commentId' => 'Comment',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add comment route to DocumentQueryController**
|
||||||
|
|
||||||
|
Add `CommentRepository` import and inject it, then add the route:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Repository\CommentRepository;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to constructor:
|
||||||
|
```php
|
||||||
|
private readonly CommentRepository $commentRepository,
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait — `Comment` has no repository. Use the EntityManager instead. Add the route method:
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[Route('/comment/{id}', name: 'documents_by_comment', methods: ['GET'])]
|
||||||
|
public function listByComment(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$comment = $this->getEntityManager()->getRepository(\App\Entity\Comment::class)->find($id);
|
||||||
|
if (!$comment) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Comment not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$documents = $this->documentRepository->findBy(['comment' => $comment]);
|
||||||
|
|
||||||
|
return $this->json($this->normalizeDocuments($documents));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Actually, the controller doesn't have `getEntityManager()`. Use `DocumentRepository` directly:
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[Route('/comment/{id}', name: 'documents_by_comment', methods: ['GET'])]
|
||||||
|
public function listByComment(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$documents = $this->documentRepository->findBy(['comment' => $id]);
|
||||||
|
|
||||||
|
return $this->json($this->normalizeDocuments($documents));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait — `findBy(['comment' => $id])` won't work with a string ID directly on a relation. Let me use the pattern from the existing code and add the Comment entity lookup. The simplest approach: inject `EntityManagerInterface`.
|
||||||
|
|
||||||
|
Actually, looking at the existing pattern more carefully, the other methods fetch the entity first and pass the object. We can use the documentRepository's entity manager. Let's just follow the exact same pattern and add a dependency. But actually, let's keep it simple — the documents table has `comment_id` column, so we can use a custom query. The simplest: just inject EntityManagerInterface.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to constructor: `private readonly EntityManagerInterface $em,`
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[Route('/comment/{id}', name: 'documents_by_comment', methods: ['GET'])]
|
||||||
|
public function listByComment(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
$comment = $this->em->find(\App\Entity\Comment::class, $id);
|
||||||
|
if (!$comment) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Comment not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$documents = $this->documentRepository->findBy(['comment' => $comment]);
|
||||||
|
|
||||||
|
return $this->json($this->normalizeDocuments($documents));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update normalizeDocuments to include commentId**
|
||||||
|
|
||||||
|
Add to the normalizeDocuments return array:
|
||||||
|
```php
|
||||||
|
'commentId' => $document->getComment()?->getId(),
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run php-cs-fixer + tests**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky && make test`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/State/DocumentUploadProcessor.php src/Controller/DocumentQueryController.php
|
||||||
|
git commit -m "feat(documents) : add comment support in upload processor and query controller"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Backend tests — comment with documents
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/Api/Controller/CommentControllerTest.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add test for creating comment with files**
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function testCreateCommentWithFiles(): void
|
||||||
|
{
|
||||||
|
$machine = $this->createMachine('Machine A');
|
||||||
|
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
|
||||||
|
// Create a temporary file for upload
|
||||||
|
$tmpFile = tempnam(sys_get_temp_dir(), 'test_');
|
||||||
|
file_put_contents($tmpFile, 'test file content');
|
||||||
|
|
||||||
|
$uploadedFile = new \Symfony\Component\HttpFoundation\File\UploadedFile(
|
||||||
|
$tmpFile,
|
||||||
|
'test-doc.pdf',
|
||||||
|
'application/pdf',
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
$client->request('POST', '/api/comments', [
|
||||||
|
'headers' => ['Content-Type' => 'multipart/form-data'],
|
||||||
|
'extra' => [
|
||||||
|
'parameters' => [
|
||||||
|
'content' => 'Comment with file',
|
||||||
|
'entityType' => 'machine',
|
||||||
|
'entityId' => $machine->getId(),
|
||||||
|
'entityName' => 'Machine A',
|
||||||
|
],
|
||||||
|
'files' => [
|
||||||
|
'files' => [$uploadedFile],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(201);
|
||||||
|
$data = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertSame('Comment with file', $data['content']);
|
||||||
|
$this->assertCount(1, $data['documents']);
|
||||||
|
$this->assertSame('test-doc.pdf', $data['documents'][0]['filename']);
|
||||||
|
|
||||||
|
@unlink($tmpFile);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add test for creating comment with multiple files**
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function testCreateCommentWithMultipleFiles(): void
|
||||||
|
{
|
||||||
|
$machine = $this->createMachine('Machine A');
|
||||||
|
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
|
||||||
|
$tmpFile1 = tempnam(sys_get_temp_dir(), 'test_');
|
||||||
|
file_put_contents($tmpFile1, 'content 1');
|
||||||
|
$tmpFile2 = tempnam(sys_get_temp_dir(), 'test_');
|
||||||
|
file_put_contents($tmpFile2, 'content 2');
|
||||||
|
|
||||||
|
$file1 = new \Symfony\Component\HttpFoundation\File\UploadedFile($tmpFile1, 'doc1.pdf', 'application/pdf', null, true);
|
||||||
|
$file2 = new \Symfony\Component\HttpFoundation\File\UploadedFile($tmpFile2, 'doc2.png', 'image/png', null, true);
|
||||||
|
|
||||||
|
$client->request('POST', '/api/comments', [
|
||||||
|
'extra' => [
|
||||||
|
'parameters' => [
|
||||||
|
'content' => 'Multiple files',
|
||||||
|
'entityType' => 'machine',
|
||||||
|
'entityId' => $machine->getId(),
|
||||||
|
],
|
||||||
|
'files' => [
|
||||||
|
'files' => [$file1, $file2],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(201);
|
||||||
|
$data = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertCount(2, $data['documents']);
|
||||||
|
|
||||||
|
@unlink($tmpFile1);
|
||||||
|
@unlink($tmpFile2);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add test that existing JSON create still works and returns empty documents array**
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function testCreateCommentJsonStillReturnsDocuments(): void
|
||||||
|
{
|
||||||
|
$machine = $this->createMachine('Machine A');
|
||||||
|
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
$client->request('POST', '/api/comments', [
|
||||||
|
'json' => [
|
||||||
|
'content' => 'No files',
|
||||||
|
'entityType' => 'machine',
|
||||||
|
'entityId' => $machine->getId(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(201);
|
||||||
|
$data = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertSame([], $data['documents']);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests**
|
||||||
|
|
||||||
|
Run: `make test`
|
||||||
|
Expected: All tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/Api/Controller/CommentControllerTest.php
|
||||||
|
git commit -m "test(comments) : add tests for comment creation with file attachments"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Frontend — update useComments composable
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/composables/useComments.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add document type to Comment interface**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface CommentDocument {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
filename: string
|
||||||
|
mimeType: string
|
||||||
|
size: number
|
||||||
|
type: string
|
||||||
|
fileUrl: string
|
||||||
|
downloadUrl: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Comment {
|
||||||
|
id: string
|
||||||
|
content: string
|
||||||
|
entityType: string
|
||||||
|
entityId: string
|
||||||
|
entityName?: string | null
|
||||||
|
authorId: string
|
||||||
|
authorName: string
|
||||||
|
status: 'open' | 'resolved'
|
||||||
|
resolvedById?: string | null
|
||||||
|
resolvedByName?: string | null
|
||||||
|
resolvedAt?: string | null
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
documents: CommentDocument[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update createComment to accept files and use FormData**
|
||||||
|
|
||||||
|
Add `postFormData` to the destructured `useApi()` call:
|
||||||
|
```typescript
|
||||||
|
const { get, post, patch, postFormData, delete: del } = useApi()
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `createComment`:
|
||||||
|
```typescript
|
||||||
|
const createComment = async (
|
||||||
|
entityType: string,
|
||||||
|
entityId: string,
|
||||||
|
content: string,
|
||||||
|
entityName?: string,
|
||||||
|
files?: File[],
|
||||||
|
): Promise<CommentResult> => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
let result
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('content', content)
|
||||||
|
formData.append('entityType', entityType)
|
||||||
|
formData.append('entityId', entityId)
|
||||||
|
if (entityName) formData.append('entityName', entityName)
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files[]', file)
|
||||||
|
}
|
||||||
|
result = await postFormData('/comments', formData)
|
||||||
|
} else {
|
||||||
|
const payload: Record<string, string> = { entityType, entityId, content }
|
||||||
|
if (entityName) payload.entityName = entityName
|
||||||
|
result = await post('/comments', payload)
|
||||||
|
}
|
||||||
|
if (result.success) {
|
||||||
|
showSuccess('Commentaire ajouté')
|
||||||
|
return { success: true, data: result.data as Comment }
|
||||||
|
}
|
||||||
|
if (result.error) showError(result.error)
|
||||||
|
return { success: false, error: result.error }
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error
|
||||||
|
showError('Impossible d\'ajouter le commentaire')
|
||||||
|
return { success: false, error: err.message }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run lint + typecheck**
|
||||||
|
|
||||||
|
Run: `cd Inventory_frontend && npm run lint:fix && npx nuxi typecheck`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit (in frontend submodule)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend
|
||||||
|
git add app/composables/useComments.ts
|
||||||
|
git commit -m "feat(comments) : support file attachments in createComment"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Frontend — update CommentSection.vue
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/components/CommentSection.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add file input and file list display to the template**
|
||||||
|
|
||||||
|
Replace the form section (lines 22-40) with:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- Formulaire d'ajout -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<textarea
|
||||||
|
v-model="newContent"
|
||||||
|
class="textarea textarea-bordered flex-1 text-sm"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Ajouter un commentaire..."
|
||||||
|
:disabled="submitting"
|
||||||
|
@keydown.ctrl.enter="handleSubmit"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col gap-1 self-end">
|
||||||
|
<label
|
||||||
|
class="btn btn-ghost btn-sm btn-square tooltip tooltip-left"
|
||||||
|
data-tip="Joindre des fichiers"
|
||||||
|
>
|
||||||
|
<IconLucidePaperclip class="w-4 h-4" />
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
class="hidden"
|
||||||
|
@change="handleFilesSelected"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-sm btn-square"
|
||||||
|
:disabled="!newContent.trim() || submitting"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
<span v-if="submitting" class="loading loading-spinner loading-xs" />
|
||||||
|
<IconLucideSend v-else class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Selected files preview -->
|
||||||
|
<div v-if="selectedFiles.length" class="flex flex-wrap gap-1">
|
||||||
|
<span
|
||||||
|
v-for="(file, i) in selectedFiles"
|
||||||
|
:key="i"
|
||||||
|
class="badge badge-sm badge-outline gap-1"
|
||||||
|
>
|
||||||
|
<IconLucideFile class="w-3 h-3" />
|
||||||
|
{{ file.name }}
|
||||||
|
<button type="button" class="ml-1" @click="removeFile(i)">
|
||||||
|
<IconLucideX class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add after each comment's content (`<p class="text-sm whitespace-pre-wrap">`) in both open and resolved sections:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- Documents attachés -->
|
||||||
|
<div v-if="comment.documents?.length" class="flex flex-wrap gap-1 mt-1">
|
||||||
|
<a
|
||||||
|
v-for="doc in comment.documents"
|
||||||
|
:key="doc.id"
|
||||||
|
:href="`${apiBase}${doc.downloadUrl}`"
|
||||||
|
target="_blank"
|
||||||
|
class="badge badge-sm badge-ghost gap-1 hover:badge-primary"
|
||||||
|
>
|
||||||
|
<IconLucideFile class="w-3 h-3" />
|
||||||
|
{{ doc.filename }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update script setup**
|
||||||
|
|
||||||
|
Add new imports:
|
||||||
|
```typescript
|
||||||
|
import IconLucidePaperclip from '~icons/lucide/paperclip'
|
||||||
|
import IconLucideFile from '~icons/lucide/file'
|
||||||
|
import IconLucideX from '~icons/lucide/x'
|
||||||
|
```
|
||||||
|
|
||||||
|
Add after existing refs:
|
||||||
|
```typescript
|
||||||
|
const selectedFiles = ref<File[]>([])
|
||||||
|
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const apiBase = useRuntimeConfig().public.apiBase || ''
|
||||||
|
```
|
||||||
|
|
||||||
|
Add file management functions:
|
||||||
|
```typescript
|
||||||
|
const handleFilesSelected = (e: Event) => {
|
||||||
|
const input = e.target as HTMLInputElement
|
||||||
|
if (input.files) {
|
||||||
|
selectedFiles.value.push(...Array.from(input.files))
|
||||||
|
}
|
||||||
|
// Reset input so the same file can be re-selected
|
||||||
|
input.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFile = (index: number) => {
|
||||||
|
selectedFiles.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `handleSubmit`:
|
||||||
|
```typescript
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const content = newContent.value.trim()
|
||||||
|
if (!content) return
|
||||||
|
submitting.value = true
|
||||||
|
const result = await createComment(
|
||||||
|
props.entityType,
|
||||||
|
props.entityId,
|
||||||
|
content,
|
||||||
|
props.entityName,
|
||||||
|
selectedFiles.value.length > 0 ? selectedFiles.value : undefined,
|
||||||
|
)
|
||||||
|
submitting.value = false
|
||||||
|
if (result.success) {
|
||||||
|
newContent.value = ''
|
||||||
|
selectedFiles.value = []
|
||||||
|
await loadComments()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run lint + typecheck**
|
||||||
|
|
||||||
|
Run: `cd Inventory_frontend && npm run lint:fix && npx nuxi typecheck`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit (in frontend submodule)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend
|
||||||
|
git add app/components/CommentSection.vue
|
||||||
|
git commit -m "feat(comments) : add file attachment UI to CommentSection"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Update API Platform filter and submodule pointer
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Entity/Document.php` (add ExistsFilter for comment)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add comment to ExistsFilter on Document entity**
|
||||||
|
|
||||||
|
Update the `ApiFilter(ExistsFilter...)` line in `Document.php`:
|
||||||
|
```php
|
||||||
|
#[ApiFilter(ExistsFilter::class, properties: ['site', 'machine', 'composant', 'piece', 'product', 'comment'])]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run php-cs-fixer + all backend tests**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky && make test`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit backend**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Entity/Document.php
|
||||||
|
git commit -m "feat(documents) : add comment ExistsFilter"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update submodule pointer**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add Inventory_frontend
|
||||||
|
git commit -m "chore(submodule) : update frontend pointer (comment documents feature)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: Manual verification
|
||||||
|
|
||||||
|
- [ ] **Step 1: Start the app**
|
||||||
|
|
||||||
|
Run: `make start`
|
||||||
|
|
||||||
|
- [ ] **Step 2: Test creating a comment without files** — should work exactly as before, response now includes `"documents": []`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Test creating a comment with files** — use the paperclip button, select 1-2 files, submit. Files should appear as badges on the comment.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Click a file badge** — should download the file.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run full test suite one last time**
|
||||||
|
|
||||||
|
Run: `make test`
|
||||||
809
docs/superpowers/plans/2026-03-23-document-types.md
Normal file
809
docs/superpowers/plans/2026-03-23-document-types.md
Normal file
@@ -0,0 +1,809 @@
|
|||||||
|
# Document Types Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add a `type` enum field to documents (documentation, devis, facture, plan, photo, autre) with classification at upload and inline editing afterward.
|
||||||
|
|
||||||
|
**Architecture:** New PHP enum `DocumentType` + column on `documents` table. Migration classifies existing rows by mimeType. Frontend gets a type select at upload, a badge in document lists, and a mini-modal for editing name+type via PATCH.
|
||||||
|
|
||||||
|
**Tech Stack:** Symfony 8, API Platform, Doctrine, PHP 8.4 enums, Nuxt 4, Vue 3, DaisyUI 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
### Backend (create)
|
||||||
|
- `src/Enum/DocumentType.php` — PHP backed enum with 6 values
|
||||||
|
- `migrations/VersionXXX_add_document_type.php` — ALTER TABLE + data classification
|
||||||
|
|
||||||
|
### Backend (modify)
|
||||||
|
- `src/Entity/Document.php` — add `type` column + Patch operation
|
||||||
|
- `src/State/DocumentUploadProcessor.php` — accept `type` from FormData
|
||||||
|
- `src/Controller/DocumentQueryController.php` — add `type` to `normalizeDocuments()`
|
||||||
|
|
||||||
|
### Frontend (create)
|
||||||
|
- `Inventory_frontend/app/shared/documentTypes.ts` — type constants + labels
|
||||||
|
- `Inventory_frontend/app/components/DocumentEditModal.vue` — mini-modal for editing name+type
|
||||||
|
|
||||||
|
### Frontend (modify)
|
||||||
|
- `Inventory_frontend/app/composables/useDocuments.ts` — add `type` to interface + `updateDocument()` method
|
||||||
|
- `Inventory_frontend/app/components/DocumentUpload.vue` — add type select
|
||||||
|
- `Inventory_frontend/app/components/common/DocumentListInline.vue` — add type badge + edit button
|
||||||
|
- `Inventory_frontend/app/composables/useEntityDocuments.ts` — add `updateDocument` delegation
|
||||||
|
- `Inventory_frontend/app/pages/documents.vue` — add type column + edit button
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: PHP Enum + Entity Column
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/Enum/DocumentType.php`
|
||||||
|
- Modify: `src/Entity/Document.php:31-54` (API resource), `src/Entity/Document.php:107-113` (add column after site)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the DocumentType enum**
|
||||||
|
|
||||||
|
```php
|
||||||
|
// src/Enum/DocumentType.php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Enum;
|
||||||
|
|
||||||
|
enum DocumentType: string
|
||||||
|
{
|
||||||
|
case DOCUMENTATION = 'documentation';
|
||||||
|
case DEVIS = 'devis';
|
||||||
|
case FACTURE = 'facture';
|
||||||
|
case PLAN = 'plan';
|
||||||
|
case PHOTO = 'photo';
|
||||||
|
case AUTRE = 'autre';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add type column to Document entity**
|
||||||
|
|
||||||
|
In `src/Entity/Document.php`, add after the `$site` property (line ~106):
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 20, enumType: DocumentType::class)]
|
||||||
|
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||||
|
private DocumentType $type = DocumentType::DOCUMENTATION;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add getter/setter:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function getType(): DocumentType
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setType(DocumentType $type): static
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the import at top: `use App\Enum\DocumentType;`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add Patch operation to Document API resource**
|
||||||
|
|
||||||
|
In the `operations` array of `#[ApiResource(...)]`, add after the existing `Put`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the import: `use ApiPlatform\Metadata\Patch;`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run cs-fixer and verify**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Enum/DocumentType.php src/Entity/Document.php
|
||||||
|
git commit -m "feat(documents) : add DocumentType enum and type column on entity"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Migration
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: new migration file via Doctrine
|
||||||
|
|
||||||
|
- [ ] **Step 1: Generate migration**
|
||||||
|
|
||||||
|
Run: `docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:diff`
|
||||||
|
|
||||||
|
This will generate a migration. Then edit it to add the data classification.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Edit migration to classify existing documents**
|
||||||
|
|
||||||
|
The generated migration will have the `ALTER TABLE` for adding the column. After the column add, append:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE documents SET type = 'photo' WHERE mimetype LIKE 'image/%';
|
||||||
|
UPDATE documents SET type = 'autre' WHERE type = 'documentation' AND mimetype NOT LIKE 'application/pdf' AND mimetype NOT LIKE 'image/%';
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `IF NOT EXISTS` pattern consistent with other migrations:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql("DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'documents' AND column_name = 'type') THEN ALTER TABLE documents ADD COLUMN type VARCHAR(20) NOT NULL DEFAULT 'documentation'; END IF; END $$");
|
||||||
|
$this->addSql("UPDATE documents SET type = 'photo' WHERE mimetype LIKE 'image/%'");
|
||||||
|
$this->addSql("UPDATE documents SET type = 'autre' WHERE type = 'documentation' AND mimetype NOT LIKE 'application/pdf' AND mimetype NOT LIKE 'image/%'");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run migration**
|
||||||
|
|
||||||
|
Run: `docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate --no-interaction`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify data classification**
|
||||||
|
|
||||||
|
Run: `docker exec -u www-data php-inventory-apache php bin/console dbal:run-sql "SELECT type, COUNT(*) FROM documents GROUP BY type"`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add migrations/
|
||||||
|
git commit -m "feat(documents) : add migration for type column with data classification"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Backend — Upload Processor + Query Controller
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/State/DocumentUploadProcessor.php:66-77`
|
||||||
|
- Modify: `src/Controller/DocumentQueryController.php:110-127`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Accept type in DocumentUploadProcessor**
|
||||||
|
|
||||||
|
In `handleMultipartUpload()`, after `$document->setSize((int) $size);` (line ~77), add:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Document type from form field (default: documentation)
|
||||||
|
$typeValue = $request->request->get('type', 'documentation');
|
||||||
|
$docType = DocumentType::tryFrom($typeValue) ?? DocumentType::DOCUMENTATION;
|
||||||
|
$document->setType($docType);
|
||||||
|
```
|
||||||
|
|
||||||
|
Add import: `use App\Enum\DocumentType;`
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add type to DocumentQueryController normalizeDocuments**
|
||||||
|
|
||||||
|
In `normalizeDocuments()`, add `'type'` to the returned array after `'productId'`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'type' => $document->getType()->value,
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write test for PATCH type update**
|
||||||
|
|
||||||
|
In `tests/Api/Entity/DocumentTest.php`, add:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function testPatchType(): void
|
||||||
|
{
|
||||||
|
$doc = $this->createDocumentInDb();
|
||||||
|
|
||||||
|
$client = $this->createGestionnaireClient();
|
||||||
|
$client->request('PATCH', self::iri('documents', $doc->getId()), [
|
||||||
|
'headers' => ['Content-Type' => 'application/merge-patch+json'],
|
||||||
|
'json' => ['type' => 'devis'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertJsonContains(['type' => 'devis']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPatchNameAndType(): void
|
||||||
|
{
|
||||||
|
$doc = $this->createDocumentInDb();
|
||||||
|
|
||||||
|
$client = $this->createGestionnaireClient();
|
||||||
|
$client->request('PATCH', self::iri('documents', $doc->getId()), [
|
||||||
|
'headers' => ['Content-Type' => 'application/merge-patch+json'],
|
||||||
|
'json' => ['name' => 'new-name', 'type' => 'facture'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertJsonContains(['name' => 'new-name', 'type' => 'facture']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetItemIncludesType(): void
|
||||||
|
{
|
||||||
|
$doc = $this->createDocumentInDb();
|
||||||
|
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
$client->request('GET', self::iri('documents', $doc->getId()));
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertJsonContains(['type' => 'documentation']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testViewerCannotPatch(): void
|
||||||
|
{
|
||||||
|
$doc = $this->createDocumentInDb();
|
||||||
|
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
$client->request('PATCH', self::iri('documents', $doc->getId()), [
|
||||||
|
'headers' => ['Content-Type' => 'application/merge-patch+json'],
|
||||||
|
'json' => ['type' => 'devis'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Api/Entity/DocumentTest.php`
|
||||||
|
Expected: all tests pass
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run cs-fixer**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky`
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/State/DocumentUploadProcessor.php src/Controller/DocumentQueryController.php tests/Api/Entity/DocumentTest.php
|
||||||
|
git commit -m "feat(documents) : accept type on upload + expose in query controller + PATCH support"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Frontend — Type Constants + Document Interface
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `Inventory_frontend/app/shared/documentTypes.ts`
|
||||||
|
- Modify: `Inventory_frontend/app/composables/useDocuments.ts:6-27` (Document interface), `useDocuments.ts:205-253` (upload)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create documentTypes.ts**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Inventory_frontend/app/shared/documentTypes.ts
|
||||||
|
export const DOCUMENT_TYPES = [
|
||||||
|
{ value: 'documentation', label: 'Documentation' },
|
||||||
|
{ value: 'devis', label: 'Devis' },
|
||||||
|
{ value: 'facture', label: 'Facture' },
|
||||||
|
{ value: 'plan', label: 'Plan' },
|
||||||
|
{ value: 'photo', label: 'Photo' },
|
||||||
|
{ value: 'autre', label: 'Autre' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type DocumentTypeValue = (typeof DOCUMENT_TYPES)[number]['value']
|
||||||
|
|
||||||
|
export const getDocumentTypeLabel = (value: string): string => {
|
||||||
|
const found = DOCUMENT_TYPES.find((t) => t.value === value)
|
||||||
|
return found?.label ?? value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add type to Document interface and UploadContext**
|
||||||
|
|
||||||
|
In `useDocuments.ts`, add to `Document` interface after `downloadUrl`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type?: string
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to `UploadContext` interface:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type?: string
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add type to uploadDocuments FormData**
|
||||||
|
|
||||||
|
In `uploadDocuments()`, after `formData.append('name', file.name)` (line ~220), add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (context.type) formData.append('type', context.type)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add updateDocument method**
|
||||||
|
|
||||||
|
In `useDocuments()`, before the `return` block, add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const updateDocument = async (
|
||||||
|
id: string,
|
||||||
|
data: { name?: string; type?: string },
|
||||||
|
): Promise<DocumentResult> => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result = await patch(`/documents/${id}`, data)
|
||||||
|
if (result.success && result.data) {
|
||||||
|
const updated = result.data as Document
|
||||||
|
const index = documents.value.findIndex((doc) => doc.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
documents.value[index] = { ...documents.value[index], ...updated }
|
||||||
|
}
|
||||||
|
showSuccess('Document mis à jour')
|
||||||
|
return { success: true, data: updated }
|
||||||
|
}
|
||||||
|
if (result.error) showError(result.error)
|
||||||
|
return result as DocumentResult
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error
|
||||||
|
showError('Impossible de mettre à jour le document')
|
||||||
|
return { success: false, error: err.message }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `patch` to the destructured `useApi()` call at the top of the composable:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { get, patch, postFormData, delete: del } = useApi()
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `updateDocument` to the return object.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run lint**
|
||||||
|
|
||||||
|
Run: `cd Inventory_frontend && npm run lint:fix`
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit frontend**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend
|
||||||
|
git add app/shared/documentTypes.ts app/composables/useDocuments.ts
|
||||||
|
git commit -m "feat(documents) : add document type constants and updateDocument method"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Frontend — DocumentUpload Type Select
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/components/DocumentUpload.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add type prop and select to DocumentUpload**
|
||||||
|
|
||||||
|
Add prop:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
documentType: {
|
||||||
|
type: String,
|
||||||
|
default: 'documentation'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add emit:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
'update:documentType'
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a select dropdown in the template, before the file list (`<ul>`), after the button area:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="w-full max-w-xs mt-2">
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wide text-base-content/70">
|
||||||
|
Type de document
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
class="select select-bordered select-sm w-full mt-1"
|
||||||
|
:value="documentType"
|
||||||
|
@change="$emit('update:documentType', ($event.target as HTMLSelectElement).value)"
|
||||||
|
>
|
||||||
|
<option v-for="t in documentTypes" :key="t.value" :value="t.value">
|
||||||
|
{{ t.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Import the types:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { DOCUMENT_TYPES } from '~/shared/documentTypes'
|
||||||
|
const documentTypes = DOCUMENT_TYPES
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: since DocumentUpload uses `<script setup>` without `lang="ts"`, use `@change="$emit('update:documentType', $event.target.value)"` (no cast).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run lint**
|
||||||
|
|
||||||
|
Run: `cd Inventory_frontend && npm run lint:fix`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend
|
||||||
|
git add app/components/DocumentUpload.vue
|
||||||
|
git commit -m "feat(documents) : add type select to DocumentUpload component"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Frontend — DocumentEditModal
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `Inventory_frontend/app/components/DocumentEditModal.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create DocumentEditModal component**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div v-if="visible" class="modal modal-open" @click.self="$emit('close')">
|
||||||
|
<div class="modal-box max-w-sm">
|
||||||
|
<h3 class="font-bold text-lg mb-4">
|
||||||
|
Modifier le document
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">Nom</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm md:input-md w-full"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">Type</span>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
v-model="form.type"
|
||||||
|
class="select select-bordered select-sm md:select-md w-full"
|
||||||
|
>
|
||||||
|
<option v-for="t in DOCUMENT_TYPES" :key="t.value" :value="t.value">
|
||||||
|
{{ t.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm md:btn-md" @click="$emit('close')">
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-sm md:btn-md"
|
||||||
|
:disabled="saving"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
|
<span v-if="saving" class="loading loading-spinner loading-xs" />
|
||||||
|
Sauvegarder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, watch, ref } from 'vue'
|
||||||
|
import { DOCUMENT_TYPES } from '~/shared/documentTypes'
|
||||||
|
import type { Document } from '~/composables/useDocuments'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
document: Document | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'close'): void
|
||||||
|
(e: 'updated', data: { name: string; type: string }): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const form = reactive({ name: '', type: 'documentation' })
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.document,
|
||||||
|
(doc) => {
|
||||||
|
if (doc) {
|
||||||
|
form.name = doc.name || ''
|
||||||
|
form.type = doc.type || 'documentation'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if (!form.name.trim()) return
|
||||||
|
saving.value = true
|
||||||
|
emit('updated', { name: form.name.trim(), type: form.type })
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run lint**
|
||||||
|
|
||||||
|
Run: `cd Inventory_frontend && npm run lint:fix`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend
|
||||||
|
git add app/components/DocumentEditModal.vue
|
||||||
|
git commit -m "feat(documents) : add DocumentEditModal component"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Frontend — DocumentListInline + Type Badge + Edit Button
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/components/common/DocumentListInline.vue`
|
||||||
|
- Modify: `Inventory_frontend/app/composables/useEntityDocuments.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add type badge and edit button to DocumentListInline**
|
||||||
|
|
||||||
|
In the template, after the document name `<div>` (line ~33-40), add a badge for the type:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span class="badge badge-sm badge-outline">{{ getDocumentTypeLabel(document.type || 'documentation') }}</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
In the actions area (line ~42-68), add an edit button before "Consulter":
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button
|
||||||
|
v-if="canEdit"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs"
|
||||||
|
title="Modifier"
|
||||||
|
@click="$emit('edit', document)"
|
||||||
|
>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add props:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
canEdit?: boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Add emit:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
(e: 'edit', document: Document): void
|
||||||
|
```
|
||||||
|
|
||||||
|
Add import:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getDocumentTypeLabel } from '~/shared/documentTypes'
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add updateDocument to useEntityDocuments**
|
||||||
|
|
||||||
|
In `useEntityDocuments.ts`, add `updateDocument` from useDocuments:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { uploadDocuments, deleteDocument, updateDocument } = useDocuments()
|
||||||
|
```
|
||||||
|
|
||||||
|
Add method:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const editDocument = async (id: string, data: { name?: string; type?: string }) => {
|
||||||
|
const result: any = await updateDocument(id, data)
|
||||||
|
if (result.success) {
|
||||||
|
const e = entity()
|
||||||
|
const docs = e.documents || []
|
||||||
|
const index = docs.findIndex((doc: any) => doc.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
docs[index] = { ...docs[index], ...data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `editDocument` to the return object.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run lint**
|
||||||
|
|
||||||
|
Run: `cd Inventory_frontend && npm run lint:fix`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend
|
||||||
|
git add app/components/common/DocumentListInline.vue app/composables/useEntityDocuments.ts
|
||||||
|
git commit -m "feat(documents) : add type badge and edit button to DocumentListInline"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Frontend — Wire Edit Modal in Entity Pages
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/components/ComponentItem.vue`
|
||||||
|
- Modify: `Inventory_frontend/app/components/PieceItem.vue`
|
||||||
|
- Modify: `Inventory_frontend/app/pages/pieces/[id]/edit.vue`
|
||||||
|
- Modify: `Inventory_frontend/app/pages/component/[id]/edit.vue`
|
||||||
|
- Modify: `Inventory_frontend/app/pages/product/[id]/edit.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Wire in ComponentItem and PieceItem**
|
||||||
|
|
||||||
|
For each of `ComponentItem.vue` and `PieceItem.vue`:
|
||||||
|
|
||||||
|
1. Add `editDocument` from the `useEntityDocuments` return
|
||||||
|
2. Add state refs for the edit modal:
|
||||||
|
```typescript
|
||||||
|
const editingDocument = ref<any>(null)
|
||||||
|
const editModalVisible = ref(false)
|
||||||
|
```
|
||||||
|
3. Add handler:
|
||||||
|
```typescript
|
||||||
|
const openEditModal = (doc: any) => {
|
||||||
|
editingDocument.value = doc
|
||||||
|
editModalVisible.value = true
|
||||||
|
}
|
||||||
|
const handleDocumentUpdated = async (data: { name: string; type: string }) => {
|
||||||
|
if (!editingDocument.value?.id) return
|
||||||
|
await editDocument(editingDocument.value.id, data)
|
||||||
|
editModalVisible.value = false
|
||||||
|
editingDocument.value = null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. Add `DocumentEditModal` in the template
|
||||||
|
5. Pass `:can-edit="isEditMode"` and `@edit="openEditModal"` to `DocumentListInline`
|
||||||
|
|
||||||
|
- [ ] **Step 2: Wire in edit pages (pieces/edit, component/edit, product/edit)**
|
||||||
|
|
||||||
|
Same pattern: add edit modal state, wire `DocumentListInline` with `:can-edit` and `@edit`, add `DocumentEditModal`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Wire type select in upload**
|
||||||
|
|
||||||
|
In pages that use `DocumentUpload`, add a `documentType` ref and pass it:
|
||||||
|
```html
|
||||||
|
<DocumentUpload
|
||||||
|
v-model="selectedFiles"
|
||||||
|
v-model:document-type="uploadDocType"
|
||||||
|
...
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass `type: uploadDocType.value` in the upload context when calling `handleFilesAdded` or `uploadDocuments`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run lint + typecheck**
|
||||||
|
|
||||||
|
Run: `cd Inventory_frontend && npm run lint:fix && npx nuxi typecheck`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend
|
||||||
|
git add app/components/ app/pages/
|
||||||
|
git commit -m "feat(documents) : wire DocumentEditModal and type select in all entity pages"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: Frontend — Documents Global Page
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/pages/documents.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add type column to DataTable**
|
||||||
|
|
||||||
|
In the `columns` array, add after `mimeType`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{ key: 'type', label: 'Type' },
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the cell template:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template #cell-type="{ row }">
|
||||||
|
<span class="badge badge-sm badge-outline">{{ getDocumentTypeLabel(row.type || 'documentation') }}</span>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add edit button + modal**
|
||||||
|
|
||||||
|
Add an edit button in the `#cell-actions` template slot:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button
|
||||||
|
v-if="canEdit"
|
||||||
|
class="btn btn-ghost btn-xs"
|
||||||
|
type="button"
|
||||||
|
@click="openEditModal(row)"
|
||||||
|
>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `DocumentEditModal` component in the template. Add the edit state + handler logic (same pattern as Task 8). Use `useDocuments().updateDocument` directly.
|
||||||
|
|
||||||
|
Import `usePermissions` to derive `canEdit` from the user's role (ROLE_GESTIONNAIRE or above).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add type filter**
|
||||||
|
|
||||||
|
Add a type filter select next to the existing "Rattachement" filter:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wide text-base-content/70" for="doc-type-filter">
|
||||||
|
Type
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="doc-type-filter"
|
||||||
|
v-model="typeFilter"
|
||||||
|
class="select select-bordered select-sm"
|
||||||
|
@change="table.handleFilterChange"
|
||||||
|
>
|
||||||
|
<option value="all">Tous</option>
|
||||||
|
<option v-for="t in DOCUMENT_TYPES" :key="t.value" :value="t.value">
|
||||||
|
{{ t.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass `typeFilter` to `fetchDocuments` → `loadDocuments` as a new filter param, and in `useDocuments.loadDocuments` add `params.set('type', typeFilter)` when not `'all'`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run lint + typecheck**
|
||||||
|
|
||||||
|
Run: `cd Inventory_frontend && npm run lint:fix && npx nuxi typecheck`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend
|
||||||
|
git add app/pages/documents.vue app/composables/useDocuments.ts
|
||||||
|
git commit -m "feat(documents) : add type column, filter, and edit to documents page"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 10: Final — Submodule Pointer + Verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Main repo: update submodule pointer
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run full backend tests**
|
||||||
|
|
||||||
|
Run: `make test`
|
||||||
|
Expected: all tests pass
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run full frontend checks**
|
||||||
|
|
||||||
|
Run: `cd Inventory_frontend && npm run lint:fix && npx nuxi typecheck && npm run build`
|
||||||
|
Expected: 0 errors
|
||||||
|
|
||||||
|
- [ ] **Step 3: Manual verification**
|
||||||
|
|
||||||
|
1. Go to `/pieces/{id}/edit` — verify type badge on existing docs, edit modal works
|
||||||
|
2. Go to `/component/{id}/edit` — same verification
|
||||||
|
3. Upload a new document — verify type select appears, type is saved
|
||||||
|
4. Go to `/documents` — verify type column, filter, edit button
|
||||||
|
5. Check that existing PDFs show "Documentation", images show "Photo", others show "Autre"
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit submodule pointer**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/matthieu/dev_malio/Inventory
|
||||||
|
git add Inventory_frontend
|
||||||
|
git commit -m "chore(submodule) : update frontend pointer (document types feature)"
|
||||||
|
```
|
||||||
418
docs/superpowers/plans/2026-03-23-fix-data-loss-bugs.md
Normal file
418
docs/superpowers/plans/2026-03-23-fix-data-loss-bugs.md
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
# Fix Data-Loss Bugs — Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Fix all bugs that cause silent data loss in the composant/piece/product/skeleton/custom-fields data model.
|
||||||
|
|
||||||
|
**Architecture:** 6 independent fixes across backend (PHP) and frontend (TS). Each task is self-contained and can be committed independently. Backend fixes come first because they protect data integrity at the source.
|
||||||
|
|
||||||
|
**Tech Stack:** Symfony 8 / PHP 8.4 / PostgreSQL 16 / Nuxt 4 / Vue 3 / TypeScript
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
| Task | Action | File |
|
||||||
|
|------|--------|------|
|
||||||
|
| T1 | Modify | `src/Controller/MachineStructureController.php:174-195` |
|
||||||
|
| T2 | Modify | `src/Controller/ComposantPieceSlotController.php:41-47` |
|
||||||
|
| T3 | Modify | `src/Service/ModelTypeCategoryConversionService.php:195-236` |
|
||||||
|
| T3 | Modify | `src/Service/ModelTypeCategoryConversionService.php:340-405` |
|
||||||
|
| T4 | Modify | `src/Controller/CustomFieldValueController.php:199-211` |
|
||||||
|
| T5 | Modify | `Inventory_frontend/app/composables/useComponentEdit.ts:398-405` |
|
||||||
|
| T5 | Modify | `Inventory_frontend/app/composables/usePieceEdit.ts:407-414` |
|
||||||
|
| T6 | Modify | `Inventory_frontend/app/composables/useComponentCreate.ts` (same pattern if present) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Clone machine — CustomFieldValue pointe vers les CustomField de la source
|
||||||
|
|
||||||
|
**Probleme:** `cloneCustomFields` clone les `CustomField` (definitions) pour la target, mais les `CustomFieldValue` (valeurs) restent liees aux `CustomField` de la source. Supprimer la source cascade-delete les valeurs du clone.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Controller/MachineStructureController.php:174-195`
|
||||||
|
- Test: `tests/Api/Controller/MachineStructureControllerTest.php` (clone test existant)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Dans le test de clone existant, ajouter une assertion : apres clone, verifier que chaque `CustomFieldValue` de la machine clonee pointe vers un `CustomField` dont `machineId` est l'ID de la machine clonee (pas la source).
|
||||||
|
|
||||||
|
```php
|
||||||
|
// After clone, fetch the cloned machine's custom field values
|
||||||
|
$clonedValues = $em->getRepository(CustomFieldValue::class)->findBy(['machine' => $clonedMachine]);
|
||||||
|
foreach ($clonedValues as $cfv) {
|
||||||
|
$this->assertSame(
|
||||||
|
$clonedMachine->getId(),
|
||||||
|
$cfv->getCustomField()->getMachine()->getId(),
|
||||||
|
'Cloned CustomFieldValue must reference the cloned CustomField, not the source'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Api/Controller/MachineStructureControllerTest.php`
|
||||||
|
Expected: FAIL — cloned values reference source machine's custom fields
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the fix**
|
||||||
|
|
||||||
|
In `cloneCustomFields`, build a map `$oldCfId => $newCf` in the first loop, then use it in the second loop:
|
||||||
|
|
||||||
|
```php
|
||||||
|
private function cloneCustomFields(Machine $source, Machine $target): void
|
||||||
|
{
|
||||||
|
$cfMap = [];
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
$cfMap[$cf->getId()] = $newCf;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($source->getCustomFieldValues() as $cfv) {
|
||||||
|
$originalCf = $cfv->getCustomField();
|
||||||
|
$newCf = $cfMap[$originalCf->getId()] ?? null;
|
||||||
|
if (!$newCf) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newValue = new CustomFieldValue();
|
||||||
|
$newValue->setMachine($target);
|
||||||
|
$newValue->setCustomField($newCf);
|
||||||
|
$newValue->setValue($cfv->getValue());
|
||||||
|
$this->entityManager->persist($newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Api/Controller/MachineStructureControllerTest.php`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 5: Lint**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky`
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Controller/MachineStructureController.php tests/Api/Controller/MachineStructureControllerTest.php
|
||||||
|
git commit -m "fix(clone) : custom field values reference cloned definitions, not source"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: ComposantPieceSlot PATCH — pas de validation du type de piece ni 404
|
||||||
|
|
||||||
|
**Probleme:** On peut assigner n'importe quelle piece dans un slot, meme si son type ne correspond pas au type requis par le squelette. Si la piece n'existe pas, `null` est silencieusement mis.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Controller/ComposantPieceSlotController.php:41-47`
|
||||||
|
- Test: `tests/Api/Controller/ComposantPieceSlotControllerTest.php` (creer si absent)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test — piece not found returns 404**
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function testPatchSlotWithNonExistentPieceReturns404(): void
|
||||||
|
{
|
||||||
|
$client = $this->createGestionnaireClient();
|
||||||
|
// Create a slot via fixtures
|
||||||
|
$slot = $this->createComposantPieceSlot();
|
||||||
|
|
||||||
|
$client->request('PATCH', '/api/composant-piece-slots/' . $slot->getId(), [
|
||||||
|
'json' => ['selectedPieceId' => 'cl_nonexistent_id'],
|
||||||
|
'headers' => ['Content-Type' => 'application/json'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(404);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write the failing test — wrong piece type returns 422**
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function testPatchSlotWithWrongPieceTypeReturns422(): void
|
||||||
|
{
|
||||||
|
$client = $this->createGestionnaireClient();
|
||||||
|
$typeA = $this->createModelType(['category' => 'piece', 'name' => 'Type A']);
|
||||||
|
$typeB = $this->createModelType(['category' => 'piece', 'name' => 'Type B']);
|
||||||
|
$slot = $this->createComposantPieceSlot(['typePiece' => $typeA]);
|
||||||
|
$wrongPiece = $this->createPiece(['typePiece' => $typeB]);
|
||||||
|
|
||||||
|
$client->request('PATCH', '/api/composant-piece-slots/' . $slot->getId(), [
|
||||||
|
'json' => ['selectedPieceId' => $wrongPiece->getId()],
|
||||||
|
'headers' => ['Content-Type' => 'application/json'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(422);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Api/Controller/ComposantPieceSlotControllerTest.php`
|
||||||
|
Expected: FAIL
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement the fix**
|
||||||
|
|
||||||
|
```php
|
||||||
|
if (array_key_exists('selectedPieceId', $payload)) {
|
||||||
|
if (null === $payload['selectedPieceId']) {
|
||||||
|
$slot->setSelectedPiece(null);
|
||||||
|
} else {
|
||||||
|
$piece = $this->entityManager->find(Piece::class, $payload['selectedPieceId']);
|
||||||
|
if (!$piece) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Pièce introuvable.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$slotTypePiece = $slot->getTypePiece();
|
||||||
|
if ($slotTypePiece && $piece->getTypePiece()?->getId() !== $slotTypePiece->getId()) {
|
||||||
|
return $this->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => sprintf(
|
||||||
|
'La pièce doit être de type « %s ».',
|
||||||
|
$slotTypePiece->getName(),
|
||||||
|
),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$slot->setSelectedPiece($piece);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Api/Controller/ComposantPieceSlotControllerTest.php`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 6: Lint + commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make php-cs-fixer-allow-risky
|
||||||
|
git add src/Controller/ComposantPieceSlotController.php tests/Api/Controller/ComposantPieceSlotControllerTest.php
|
||||||
|
git commit -m "fix(slots) : validate piece type matches slot requirement + 404 on missing piece"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Conversion de categorie — slots supprimes sans verification + skeleton requirements orphelins
|
||||||
|
|
||||||
|
**Probleme A:** `checkComponentToPiece` verifie `structure IS NOT NULL` (ancien JSON) mais les donnees sont dans les tables de slots. Le check passe toujours et les slots sont cascade-deleted.
|
||||||
|
|
||||||
|
**Probleme B:** Apres conversion, les `skeleton_piece_requirements`, `skeleton_product_requirements`, `skeleton_subcomponent_requirements` de l'ancien type ne sont pas supprimes.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Service/ModelTypeCategoryConversionService.php:195-236` (check)
|
||||||
|
- Modify: `src/Service/ModelTypeCategoryConversionService.php:340-405` (convert)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Fix `checkComponentToPiece` — ajouter le check sur les tables de slots**
|
||||||
|
|
||||||
|
Apres le check `structure IS NOT NULL` existant (qui reste pour compatibilite), ajouter :
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Check slot tables for actual data (post-normalization architecture)
|
||||||
|
$slotsWithData = (int) $this->connection->fetchOne(
|
||||||
|
'SELECT COUNT(*) FROM composant_piece_slots cps
|
||||||
|
JOIN composants c ON cps.composantid = c.id
|
||||||
|
WHERE c.typecomposantid = :id AND cps.selectedpieceid IS NOT NULL',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
$subSlots = (int) $this->connection->fetchOne(
|
||||||
|
'SELECT COUNT(*) FROM composant_subcomponent_slots css
|
||||||
|
JOIN composants c ON css.composantid = c.id
|
||||||
|
WHERE c.typecomposantid = :id AND css.selectedcomposantid IS NOT NULL',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($slotsWithData > 0 || $subSlots > 0) {
|
||||||
|
$parts = [];
|
||||||
|
if ($slotsWithData > 0) {
|
||||||
|
$parts[] = sprintf('%d slot(s) pièce rempli(s)', $slotsWithData);
|
||||||
|
}
|
||||||
|
if ($subSlots > 0) {
|
||||||
|
$parts[] = sprintf('%d slot(s) sous-composant rempli(s)', $subSlots);
|
||||||
|
}
|
||||||
|
$blockers[] = sprintf(
|
||||||
|
'Des composants ont des données dans leurs slots : %s.',
|
||||||
|
implode(', ', $parts),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Fix `convertComponentToPiece` — nettoyer les skeleton requirements avant le changement de categorie**
|
||||||
|
|
||||||
|
Ajouter entre l'etape 6 (DELETE composants) et l'etape 7 (UPDATE model_types) :
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 6b. Clean up skeleton requirements that belong to COMPONENT category
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'DELETE FROM skeleton_piece_requirements WHERE modeltypeid = :id',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'DELETE FROM skeleton_subcomponent_requirements WHERE modeltypeid = :id',
|
||||||
|
['id' => $modelTypeId],
|
||||||
|
);
|
||||||
|
// Note: skeleton_product_requirements are kept — valid for both COMPONENT and PIECE categories
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Fix `convertPieceToComponent` — meme nettoyage dans l'autre sens**
|
||||||
|
|
||||||
|
Les `skeleton_product_requirements` qui appartenaient au type PIECE restent. Aucun nettoyage specifique necessaire car les product requirements sont valides pour les deux types. Mais verifier que la methode existe et n'a pas le meme probleme.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run all conversion tests**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Api/Controller/ModelTypeConversionControllerTest.php`
|
||||||
|
Si absent: `make test` (tous les tests)
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 5: Lint + commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make php-cs-fixer-allow-risky
|
||||||
|
git add src/Service/ModelTypeCategoryConversionService.php
|
||||||
|
git commit -m "fix(conversion) : block conversion when slots have data + clean skeleton requirements"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: CustomFieldValueController — cree des CustomField orphelins sans FK
|
||||||
|
|
||||||
|
**Probleme:** Quand `customFieldId` est absent et `customFieldName` est fourni, un nouveau `CustomField` est cree sans etre rattache a aucune entite (ni machine, ni modelType). La ligne est invisible et inutile.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Controller/CustomFieldValueController.php:199-211`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Implement the fix**
|
||||||
|
|
||||||
|
La methode `resolveCustomField` cree un `CustomField` orphelin. Il faut utiliser le `target` (deja resolu) pour rattacher le champ au bon parent. Le plus simple : deplacer la creation du CustomField apres la resolution du target, ou passer le target en parametre.
|
||||||
|
|
||||||
|
Option retenue : retourner un array `['customField' => $cf, 'isNew' => true]` et laisser `applyTarget` gerer le rattachement, OU plus simplement, interdire la creation ad-hoc et retourner une erreur 400 quand le champ n'existe pas.
|
||||||
|
|
||||||
|
L'approche la plus sure (pas de CustomField orphelin) :
|
||||||
|
|
||||||
|
```php
|
||||||
|
// In resolveCustomField, replace the auto-creation block with:
|
||||||
|
$customFieldName = isset($payload['customFieldName']) ? trim((string) $payload['customFieldName']) : '';
|
||||||
|
if ('' === $customFieldName) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'customFieldId or customFieldName is required.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find existing custom field by name for the target entity
|
||||||
|
$target = $this->resolveTarget($payload);
|
||||||
|
if ($target instanceof JsonResponse) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'Cannot create custom field without a valid target entity.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingField = $this->customFieldRepository->findOneBy(['name' => $customFieldName]);
|
||||||
|
if ($existingField) {
|
||||||
|
return $existingField;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json(['success' => false, 'error' => sprintf('Custom field "%s" not found. Create it explicitly first.', $customFieldName)], 404);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative plus conservative** si le frontend depend de cette auto-creation : garder la creation mais rattacher au target. Cela necessite de refactorer le flow pour passer le target a `resolveCustomField`. Choisir selon le frontend.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests**
|
||||||
|
|
||||||
|
Run: `make test`
|
||||||
|
Expected: PASS (verifier qu'aucun test ne depend de l'auto-creation)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Lint + commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make php-cs-fixer-allow-risky
|
||||||
|
git add src/Controller/CustomFieldValueController.php
|
||||||
|
git commit -m "fix(custom-fields) : prevent creation of orphan CustomField without target entity"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Frontend — custom fields definition lookup au mauvais chemin
|
||||||
|
|
||||||
|
**Probleme:** `useComponentEdit` passe `typeComposant.customFields` (pas serialise par l'API) au lieu de `typeComposant.structure.customFields`. Idem `usePieceEdit` avec `typePiece.pieceCustomFields` au lieu de `typePiece.structure.customFields`.
|
||||||
|
|
||||||
|
Consequence : le `definitionMap` est toujours vide, les champs perso sans `customFieldId` existant ne trouvent pas leur definition et sont envoyes sans `definitionId` (fallback sur metadata = CustomField orphelin cote backend = Task 4).
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/composables/useComponentEdit.ts:401-403`
|
||||||
|
- Modify: `Inventory_frontend/app/composables/usePieceEdit.ts:410-412`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Fix useComponentEdit.ts**
|
||||||
|
|
||||||
|
Ligne 401-403, remplacer :
|
||||||
|
```ts
|
||||||
|
[
|
||||||
|
updatedComponent?.typeComposant?.customFields,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
par :
|
||||||
|
```ts
|
||||||
|
[
|
||||||
|
updatedComponent?.typeComposant?.structure?.customFields,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Fix usePieceEdit.ts**
|
||||||
|
|
||||||
|
Ligne 410-412, remplacer :
|
||||||
|
```ts
|
||||||
|
[
|
||||||
|
updatedPiece?.typePiece?.pieceCustomFields,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
par :
|
||||||
|
```ts
|
||||||
|
[
|
||||||
|
updatedPiece?.typePiece?.structure?.customFields,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verifier le meme pattern dans les autres fichiers**
|
||||||
|
|
||||||
|
Verifier `useComponentCreate.ts`, `pieces/create.vue`, `product/[id]/edit.vue` pour le meme probleme.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Lint + typecheck**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend && npm run lint:fix && npx nuxi typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend
|
||||||
|
git add app/composables/useComponentEdit.ts app/composables/usePieceEdit.ts
|
||||||
|
git commit -m "fix(custom-fields) : use structure.customFields path for definition lookup"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6 (bonus): Verifier et corriger les memes patterns dans create flows
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Grep `_saveCustomFieldValues` dans tous les fichiers et verifier que chaque appel passe `structure.customFields` et non `customFields` ou `pieceCustomFields` directement.
|
||||||
|
|
||||||
|
- [ ] **Step 2:** Corriger si necessaire, lint, commit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ordre d'execution recommande
|
||||||
|
|
||||||
|
1. **T1** (clone) — fix isole, pas de dependance
|
||||||
|
2. **T2** (slots validation) — fix isole
|
||||||
|
3. **T5** (frontend custom fields path) — fix isole
|
||||||
|
4. **T4** (orphan CustomField) — depend de T5 pour comprendre si le frontend utilise l'auto-creation
|
||||||
|
5. **T3** (conversion) — le plus complexe, faire en dernier
|
||||||
|
6. **T6** (bonus verification)
|
||||||
409
docs/superpowers/plans/2026-03-23-parc-machines-ux.md
Normal file
409
docs/superpowers/plans/2026-03-23-parc-machines-ux.md
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
# Parc Machines UX Improvements — Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Multi-select site filter with checkboxes, alphabetical sorting on Parc Machines, and OR search (name/reference) on catalog pages.
|
||||||
|
|
||||||
|
**Architecture:** Frontend-only changes for tasks 1-2 (Vue reactivity + computed sort). Backend Doctrine Extension for task 3 that intercepts `?q=` parameter and builds an OR clause across `name` and `reference` fields, with corresponding frontend composable changes.
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3 (reactive Set), DaisyUI 5 checkboxes, Symfony/API Platform Doctrine ORM Extension, PHPUnit
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-03-23-parc-machines-ux-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Multi-select site checkboxes on Parc Machines
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/pages/machines/index.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace `selectedSite` ref with reactive Set**
|
||||||
|
|
||||||
|
In `<script setup>`, replace:
|
||||||
|
```js
|
||||||
|
const selectedSite = ref('')
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```js
|
||||||
|
const selectedSites = reactive(new Set())
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace `<select>` with checkboxes in template**
|
||||||
|
|
||||||
|
Replace the site filter `<div class="form-control">` block (the one containing the `<select>`) with:
|
||||||
|
```vue
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Sites</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<label
|
||||||
|
v-for="site in sites"
|
||||||
|
:key="site.id"
|
||||||
|
class="flex items-center gap-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
:checked="selectedSites.has(site.id)"
|
||||||
|
@change="selectedSites.has(site.id) ? selectedSites.delete(site.id) : selectedSites.add(site.id)"
|
||||||
|
>
|
||||||
|
<span class="text-sm">{{ site.name }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update `filteredMachines` computed for multi-select**
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
```js
|
||||||
|
if (selectedSite.value) {
|
||||||
|
filtered = filtered.filter(machine => machine.siteId === selectedSite.value)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```js
|
||||||
|
if (selectedSites.size > 0) {
|
||||||
|
filtered = filtered.filter(machine => selectedSites.has(machine.siteId))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Clean up unused `ref` import if needed**
|
||||||
|
|
||||||
|
Check if `ref` is still used elsewhere in the file (it is — `searchQuery` uses it). If so, keep it. Remove only if no longer referenced.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add `reactive` to imports**
|
||||||
|
|
||||||
|
Add `reactive` to the import from `vue`:
|
||||||
|
```js
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Verify in browser**
|
||||||
|
|
||||||
|
Open `http://localhost:3001/machines`. Confirm:
|
||||||
|
- Checkboxes appear for each site
|
||||||
|
- Checking one site filters machines to that site only
|
||||||
|
- Checking multiple sites shows machines from all selected sites
|
||||||
|
- Unchecking all shows all machines
|
||||||
|
|
||||||
|
- [ ] **Step 7: Run frontend lint**
|
||||||
|
|
||||||
|
Run: `cd Inventory_frontend && npm run lint:fix`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Alphabetical sorting on Parc Machines
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/pages/machines/index.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add sort to `filteredMachines` computed**
|
||||||
|
|
||||||
|
At the end of the `filteredMachines` computed, just before `return filtered`, add:
|
||||||
|
```js
|
||||||
|
filtered = [...filtered].sort((a, b) =>
|
||||||
|
(a.name || '').localeCompare(b.name || '', 'fr')
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The full computed should now be:
|
||||||
|
```js
|
||||||
|
const filteredMachines = computed(() => {
|
||||||
|
let filtered = enrichedMachines.value
|
||||||
|
|
||||||
|
if (selectedSites.size > 0) {
|
||||||
|
filtered = filtered.filter(machine => selectedSites.has(machine.siteId))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery.value.trim()) {
|
||||||
|
const term = searchQuery.value.trim().toLowerCase()
|
||||||
|
filtered = filtered.filter(machine =>
|
||||||
|
machine.name?.toLowerCase().includes(term)
|
||||||
|
|| machine.reference?.toLowerCase().includes(term),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = [...filtered].sort((a, b) =>
|
||||||
|
(a.name || '').localeCompare(b.name || '', 'fr')
|
||||||
|
)
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify in browser**
|
||||||
|
|
||||||
|
Open `http://localhost:3001/machines`. Confirm machines are sorted A→Z by name. Test with site filter active — should still be sorted.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit Tasks 1 + 2**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend && git add app/pages/machines/index.vue && git commit -m "feat(machines) : multi-select site checkboxes + alphabetical sort"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Backend — Doctrine Extension for OR search
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/Doctrine/SearchByNameOrReferenceExtension.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `reference` parameter to `createComposant` factory**
|
||||||
|
|
||||||
|
In `tests/AbstractApiTestCase.php`, update the `createComposant` method to accept an optional `$reference` parameter:
|
||||||
|
|
||||||
|
Find:
|
||||||
|
```php
|
||||||
|
protected function createComposant(string $name = 'Composant Test', ?ModelType $type = null): Composant
|
||||||
|
{
|
||||||
|
$c = new Composant();
|
||||||
|
$c->setName($name);
|
||||||
|
if (null !== $type) {
|
||||||
|
$c->setTypeComposant($type);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
```php
|
||||||
|
protected function createComposant(string $name = 'Composant Test', ?string $reference = null, ?ModelType $type = null): Composant
|
||||||
|
{
|
||||||
|
$c = new Composant();
|
||||||
|
$c->setName($name);
|
||||||
|
if (null !== $reference) {
|
||||||
|
$c->setReference($reference);
|
||||||
|
}
|
||||||
|
if (null !== $type) {
|
||||||
|
$c->setTypeComposant($type);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write failing tests for OR search**
|
||||||
|
|
||||||
|
Add new test methods in `tests/Api/FilterTest.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function testOrSearchByNameOnPieces(): void
|
||||||
|
{
|
||||||
|
$this->createPiece('Joint torique', 'REF-JT-001');
|
||||||
|
$this->createPiece('Roulement', 'REF-RL-002');
|
||||||
|
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
$client->request('GET', '/api/pieces?q=joint');
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertJsonContains(['totalItems' => 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOrSearchByReferenceOnPieces(): void
|
||||||
|
{
|
||||||
|
$this->createPiece('Joint torique', 'REF-JT-001');
|
||||||
|
$this->createPiece('Roulement', 'REF-RL-002');
|
||||||
|
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
$client->request('GET', '/api/pieces?q=RL-002');
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertJsonContains(['totalItems' => 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOrSearchMatchesBothNameAndReference(): void
|
||||||
|
{
|
||||||
|
$this->createComposant('Pompe REF-X', 'REF-POMPE-01');
|
||||||
|
$this->createComposant('Vanne', 'REF-VANNE-01');
|
||||||
|
$this->createComposant('Moteur', 'POMPE-MOTEUR');
|
||||||
|
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
$client->request('GET', '/api/composants?q=pompe');
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
// Matches "Pompe REF-X" (name) and "Moteur" (reference contains POMPE)
|
||||||
|
$this->assertJsonContains(['totalItems' => 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOrSearchEmptyQueryReturnsAll(): void
|
||||||
|
{
|
||||||
|
$this->createProduct('Produit A', 'REF-A');
|
||||||
|
$this->createProduct('Produit B', 'REF-B');
|
||||||
|
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
$client->request('GET', '/api/products?q=');
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$data = $client->getResponse()->toArray();
|
||||||
|
$this->assertGreaterThanOrEqual(2, $data['totalItems']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOrSearchOnProducts(): void
|
||||||
|
{
|
||||||
|
$this->createProduct('Huile moteur', 'HM-500');
|
||||||
|
$this->createProduct('Graisse', 'GR-100');
|
||||||
|
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
$client->request('GET', '/api/products?q=HM-500');
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertJsonContains(['totalItems' => 1]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Api/FilterTest.php`
|
||||||
|
Expected: New tests fail (the `q` parameter is not handled yet).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Create the Doctrine Extension**
|
||||||
|
|
||||||
|
Create `src/Doctrine/SearchByNameOrReferenceExtension.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Doctrine;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
|
||||||
|
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use App\Entity\Composant;
|
||||||
|
use App\Entity\Piece;
|
||||||
|
use App\Entity\Product;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
|
final class SearchByNameOrReferenceExtension implements QueryCollectionExtensionInterface
|
||||||
|
{
|
||||||
|
private const SUPPORTED_CLASSES = [
|
||||||
|
Piece::class,
|
||||||
|
Composant::class,
|
||||||
|
Product::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly RequestStack $requestStack,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function applyToCollection(
|
||||||
|
QueryBuilder $queryBuilder,
|
||||||
|
QueryNameGeneratorInterface $queryNameGenerator,
|
||||||
|
string $resourceClass,
|
||||||
|
?Operation $operation = null,
|
||||||
|
array $context = [],
|
||||||
|
): void {
|
||||||
|
if (!\in_array($resourceClass, self::SUPPORTED_CLASSES, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = $this->requestStack->getCurrentRequest();
|
||||||
|
if (null === $request) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$q = $request->query->get('q', '');
|
||||||
|
if (!\is_string($q) || '' === trim($q)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$escaped = addcslashes(trim($q), '%_');
|
||||||
|
$paramName = $queryNameGenerator->generateParameterName('searchQ');
|
||||||
|
$alias = $queryBuilder->getRootAliases()[0];
|
||||||
|
|
||||||
|
$queryBuilder
|
||||||
|
->andWhere(sprintf('LOWER(%s.name) LIKE :%s OR LOWER(%s.reference) LIKE :%s', $alias, $paramName, $alias, $paramName))
|
||||||
|
->setParameter($paramName, '%' . strtolower($escaped) . '%');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Api/FilterTest.php`
|
||||||
|
Expected: All tests pass, including the new OR search tests.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run full test suite**
|
||||||
|
|
||||||
|
Run: `make test`
|
||||||
|
Expected: All tests pass (no regressions).
|
||||||
|
|
||||||
|
- [ ] **Step 7: Run php-cs-fixer**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky`
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit backend changes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Doctrine/SearchByNameOrReferenceExtension.php tests/Api/FilterTest.php tests/AbstractApiTestCase.php && git commit -m "feat(search) : OR search extension for name/reference on Piece, Composant, Product"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Frontend — Switch composables from `name` to `q`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/composables/usePieces.ts`
|
||||||
|
- Modify: `Inventory_frontend/app/composables/useComposants.ts`
|
||||||
|
- Modify: `Inventory_frontend/app/composables/useProducts.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update `usePieces.ts`**
|
||||||
|
|
||||||
|
In the `loadPieces` function, replace:
|
||||||
|
```ts
|
||||||
|
if (search && search.trim()) {
|
||||||
|
params.set('name', search.trim())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```ts
|
||||||
|
if (search && search.trim()) {
|
||||||
|
params.set('q', search.trim())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update `useComposants.ts`**
|
||||||
|
|
||||||
|
Same change in the `loadComposants` function:
|
||||||
|
```ts
|
||||||
|
params.set('name', search.trim())
|
||||||
|
```
|
||||||
|
→
|
||||||
|
```ts
|
||||||
|
params.set('q', search.trim())
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update `useProducts.ts`**
|
||||||
|
|
||||||
|
Same change in the `loadProducts` function:
|
||||||
|
```ts
|
||||||
|
params.set('name', search.trim())
|
||||||
|
```
|
||||||
|
→
|
||||||
|
```ts
|
||||||
|
params.set('q', search.trim())
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run frontend lint**
|
||||||
|
|
||||||
|
Run: `cd Inventory_frontend && npm run lint:fix`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify in browser**
|
||||||
|
|
||||||
|
Open each catalog page and test search:
|
||||||
|
- `http://localhost:3001/pieces-catalog` — search by name, then by reference
|
||||||
|
- `http://localhost:3001/component-catalog` — search by name, then by reference
|
||||||
|
- `http://localhost:3001/product-catalog` — search by name, then by reference
|
||||||
|
|
||||||
|
Confirm that searching by a reference value returns the correct results.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit frontend changes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend && git add app/composables/usePieces.ts app/composables/useComposants.ts app/composables/useProducts.ts && git commit -m "feat(search) : use q param for OR search on name/reference"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Update submodule pointer in main repo**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/matthieu/dev_malio/Inventory && git add Inventory_frontend && git commit -m "chore(submodule) : update frontend pointer (OR search + site checkboxes)"
|
||||||
|
```
|
||||||
1148
docs/superpowers/plans/2026-03-24-detail-views.md
Normal file
1148
docs/superpowers/plans/2026-03-24-detail-views.md
Normal file
File diff suppressed because it is too large
Load Diff
2447
docs/superpowers/plans/2026-03-25-entity-versioning.md
Normal file
2447
docs/superpowers/plans/2026-03-25-entity-versioning.md
Normal file
File diff suppressed because it is too large
Load Diff
1074
docs/superpowers/plans/2026-03-26-machine-single-save.md
Normal file
1074
docs/superpowers/plans/2026-03-26-machine-single-save.md
Normal file
File diff suppressed because it is too large
Load Diff
857
docs/superpowers/plans/2026-03-26-reference-auto.md
Normal file
857
docs/superpowers/plans/2026-03-26-reference-auto.md
Normal file
@@ -0,0 +1,857 @@
|
|||||||
|
# ReferenceAuto — Génération automatique de référence pièce
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Générer automatiquement une référence technique normalisée (`referenceAuto`) pour les pièces, basée sur une formule configurable définie au niveau du ModelType et alimentée par les CustomFieldValues de chaque Piece.
|
||||||
|
|
||||||
|
**Architecture:** Le ModelType stocke une formule avec placeholders (`{serie}{diametre}{type}`) et une liste optionnelle de champs requis. Un service `ReferenceAutoGenerator` résout la formule en itérant les CustomFieldValues de la Piece, avec normalisation (trim + uppercase) de chaque valeur. Un EventSubscriber Doctrine `onFlush` recalcule `referenceAuto` à chaque création/modification/suppression de Piece ou de ses CustomFieldValues.
|
||||||
|
|
||||||
|
**Tech Stack:** Symfony 8, Doctrine ORM (PHP 8 attributes), API Platform, PostgreSQL, PHPUnit 12
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Règles métier
|
||||||
|
|
||||||
|
- **referenceAuto** est un champ système **non éditable** par l'utilisateur, distinct de `reference` (saisie libre)
|
||||||
|
- La formule produit un **code technique structuré**, pas du texte lisible (ex: `2207K`, `SNU507`, `U507`)
|
||||||
|
- Les valeurs des CustomFields sont **normalisées** avant assemblage : `trim()` + `mb_strtoupper()`
|
||||||
|
- Champ requis manquant ou vide → `referenceAuto = null`
|
||||||
|
- Pas de formule sur le ModelType → `referenceAuto = null`
|
||||||
|
- Pas de ModelType sur la Piece → `referenceAuto = null`
|
||||||
|
- Le recalcul est déclenché par : création/modification/suppression de Piece, création/modification/suppression de CustomFieldValue lié à une Piece
|
||||||
|
- L'absence de formule sur un ModelType signifie implicitement que ce type n'est pas éligible à la génération
|
||||||
|
- Périmètre actuel : **Piece uniquement** (extensible à Composant/Product plus tard si besoin)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| Action | File | Responsibility |
|
||||||
|
|--------|------|----------------|
|
||||||
|
| Modify | `src/Entity/ModelType.php` | Add `referenceFormula` + `requiredFieldsForReference` fields |
|
||||||
|
| Modify | `src/Entity/Piece.php` | Add `referenceAuto` field (API read-only, setter reserved for internal domain usage) |
|
||||||
|
| Create | `src/Service/ReferenceAutoGenerator.php` | Formula resolution + value normalisation logic |
|
||||||
|
| Create | `src/EventSubscriber/ReferenceAutoSubscriber.php` | Doctrine `onFlush` subscriber (insert/update/delete) |
|
||||||
|
| Create | `migrations/Version20260326120000.php` | Add DB columns |
|
||||||
|
| Create | `tests/Service/ReferenceAutoGeneratorTest.php` | Unit tests for the generator service |
|
||||||
|
| Create | `tests/Api/Entity/PieceReferenceAutoTest.php` | Integration tests via API |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Migration — Add database columns
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `migrations/Version20260326120000.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the migration file**
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260326120000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add referenceFormula and requiredFieldsForReference to model_types, referenceAuto to pieces';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE model_types ADD COLUMN IF NOT EXISTS referenceformula TEXT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE model_types ADD COLUMN IF NOT EXISTS requiredfieldsforreference JSON DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE pieces ADD COLUMN IF NOT EXISTS referenceauto VARCHAR(255) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE pieces DROP COLUMN IF EXISTS referenceauto');
|
||||||
|
$this->addSql('ALTER TABLE model_types DROP COLUMN IF EXISTS requiredfieldsforreference');
|
||||||
|
$this->addSql('ALTER TABLE model_types DROP COLUMN IF EXISTS referenceformula');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the migration**
|
||||||
|
|
||||||
|
Run: `docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate --no-interaction`
|
||||||
|
Expected: Migration applied successfully.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add migrations/Version20260326120000.php
|
||||||
|
git commit -m "feat(reference-auto) : add migration for referenceAuto columns"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Entity — Add fields to ModelType
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Entity/ModelType.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add properties after `$description` (around line 74)**
|
||||||
|
|
||||||
|
Add these fields to `ModelType.php`, after `$description` and before `$createdAt`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
#[Groups(['model_type:read', 'model_type:write'])]
|
||||||
|
private ?string $referenceFormula = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||||
|
#[Groups(['model_type:read', 'model_type:write'])]
|
||||||
|
private ?array $requiredFieldsForReference = null;
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `referenceFormula` n'est PAS dans `piece:read` — c'est une donnée de configuration admin, pas nécessaire à l'affichage d'une pièce.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add getters and setters after `setDescription()`**
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function getReferenceFormula(): ?string
|
||||||
|
{
|
||||||
|
return $this->referenceFormula;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setReferenceFormula(?string $referenceFormula): static
|
||||||
|
{
|
||||||
|
$this->referenceFormula = $referenceFormula;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRequiredFieldsForReference(): ?array
|
||||||
|
{
|
||||||
|
return $this->requiredFieldsForReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRequiredFieldsForReference(?array $requiredFieldsForReference): static
|
||||||
|
{
|
||||||
|
$this->requiredFieldsForReference = $requiredFieldsForReference;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run php-cs-fixer**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky`
|
||||||
|
Expected: All files fixed or already clean.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Entity/ModelType.php
|
||||||
|
git commit -m "feat(reference-auto) : add referenceFormula fields to ModelType entity"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Entity — Add `referenceAuto` to Piece
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Entity/Piece.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `referenceAuto` property after `$reference` (line 64)**
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||||
|
#[Groups(['piece:read'])]
|
||||||
|
private ?string $referenceAuto = null;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add getter only (no public setter) after `setReference()`**
|
||||||
|
|
||||||
|
Le setter est `@internal` — seul le subscriber peut modifier ce champ. On n'expose pas de setter public pour protéger le contrat d'API. Le subscriber accède directement à la propriété via un setter interne.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function getReferenceAuto(): ?string
|
||||||
|
{
|
||||||
|
return $this->referenceAuto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal Used by ReferenceAutoSubscriber only — not part of the public API.
|
||||||
|
*/
|
||||||
|
public function setReferenceAuto(?string $referenceAuto): static
|
||||||
|
{
|
||||||
|
$this->referenceAuto = $referenceAuto;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run php-cs-fixer**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky`
|
||||||
|
Expected: Clean.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Entity/Piece.php
|
||||||
|
git commit -m "feat(reference-auto) : add referenceAuto field to Piece entity"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Service — ReferenceAutoGenerator
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/Service/ReferenceAutoGenerator.php`
|
||||||
|
- Create: `tests/Service/ReferenceAutoGeneratorTest.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `tests/Service/ReferenceAutoGeneratorTest.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Enum\ModelCategory;
|
||||||
|
use App\Tests\AbstractApiTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class ReferenceAutoGeneratorTest extends AbstractApiTestCase
|
||||||
|
{
|
||||||
|
public function testGenerateWithFormula(): void
|
||||||
|
{
|
||||||
|
$mt = $this->createModelType('Roulement', 'ROUL-001', ModelCategory::PIECE);
|
||||||
|
$mt->setReferenceFormula('{serie}{diametre}{type}');
|
||||||
|
$mt->setRequiredFieldsForReference(['serie', 'diametre', 'type']);
|
||||||
|
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$cfSerie = $this->createCustomField('serie', 'text', typePiece: $mt);
|
||||||
|
$cfDiametre = $this->createCustomField('diametre', 'text', typePiece: $mt);
|
||||||
|
$cfType = $this->createCustomField('type', 'text', typePiece: $mt);
|
||||||
|
|
||||||
|
$piece = $this->createPiece('Roulement Test', null, $mt);
|
||||||
|
|
||||||
|
$this->createCustomFieldValue($cfSerie, '22', piece: $piece);
|
||||||
|
$this->createCustomFieldValue($cfDiametre, '07', piece: $piece);
|
||||||
|
$this->createCustomFieldValue($cfType, 'K', piece: $piece);
|
||||||
|
|
||||||
|
$em->refresh($piece);
|
||||||
|
|
||||||
|
$generator = self::getContainer()->get('App\Service\ReferenceAutoGenerator');
|
||||||
|
$result = $generator->generate($piece);
|
||||||
|
|
||||||
|
self::assertSame('2207K', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGenerateNormalizesValues(): void
|
||||||
|
{
|
||||||
|
$mt = $this->createModelType('Roulement Norm', 'ROUL-002', ModelCategory::PIECE);
|
||||||
|
$mt->setReferenceFormula('{serie}{diametre}{type}');
|
||||||
|
$mt->setRequiredFieldsForReference(['serie', 'diametre', 'type']);
|
||||||
|
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$cfSerie = $this->createCustomField('serie', 'text', typePiece: $mt);
|
||||||
|
$cfDiametre = $this->createCustomField('diametre', 'text', typePiece: $mt);
|
||||||
|
$cfType = $this->createCustomField('type', 'text', typePiece: $mt);
|
||||||
|
|
||||||
|
$piece = $this->createPiece('Roulement Norm', null, $mt);
|
||||||
|
|
||||||
|
// Values with spaces and lowercase — should be trimmed and uppercased
|
||||||
|
$this->createCustomFieldValue($cfSerie, ' 22 ', piece: $piece);
|
||||||
|
$this->createCustomFieldValue($cfDiametre, '07', piece: $piece);
|
||||||
|
$this->createCustomFieldValue($cfType, 'k', piece: $piece);
|
||||||
|
|
||||||
|
$em->refresh($piece);
|
||||||
|
|
||||||
|
$generator = self::getContainer()->get('App\Service\ReferenceAutoGenerator');
|
||||||
|
$result = $generator->generate($piece);
|
||||||
|
|
||||||
|
self::assertSame('2207K', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGenerateReturnsNullWithoutFormula(): void
|
||||||
|
{
|
||||||
|
$mt = $this->createModelType('Galet', 'GAL-001', ModelCategory::PIECE);
|
||||||
|
$piece = $this->createPiece('Galet Test', null, $mt);
|
||||||
|
|
||||||
|
$generator = self::getContainer()->get('App\Service\ReferenceAutoGenerator');
|
||||||
|
$result = $generator->generate($piece);
|
||||||
|
|
||||||
|
self::assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGenerateReturnsNullWhenNoModelType(): void
|
||||||
|
{
|
||||||
|
$piece = $this->createPiece('Orphan Piece');
|
||||||
|
|
||||||
|
$generator = self::getContainer()->get('App\Service\ReferenceAutoGenerator');
|
||||||
|
$result = $generator->generate($piece);
|
||||||
|
|
||||||
|
self::assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGenerateReturnsNullWhenRequiredFieldsMissing(): void
|
||||||
|
{
|
||||||
|
$mt = $this->createModelType('Palier', 'PAL-001', ModelCategory::PIECE);
|
||||||
|
$mt->setReferenceFormula('SNU {taille}');
|
||||||
|
$mt->setRequiredFieldsForReference(['taille']);
|
||||||
|
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$piece = $this->createPiece('Palier Test', null, $mt);
|
||||||
|
|
||||||
|
$generator = self::getContainer()->get('App\Service\ReferenceAutoGenerator');
|
||||||
|
$result = $generator->generate($piece);
|
||||||
|
|
||||||
|
self::assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGenerateReturnsNullWhenRequiredFieldEmpty(): void
|
||||||
|
{
|
||||||
|
$mt = $this->createModelType('Palier Vide', 'PAL-003', ModelCategory::PIECE);
|
||||||
|
$mt->setReferenceFormula('SNU {taille}');
|
||||||
|
$mt->setRequiredFieldsForReference(['taille']);
|
||||||
|
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$cfTaille = $this->createCustomField('taille', 'text', typePiece: $mt);
|
||||||
|
$piece = $this->createPiece('Palier Vide', null, $mt);
|
||||||
|
// Value is whitespace only — after trim, it's empty
|
||||||
|
$this->createCustomFieldValue($cfTaille, ' ', piece: $piece);
|
||||||
|
|
||||||
|
$em->refresh($piece);
|
||||||
|
|
||||||
|
$generator = self::getContainer()->get('App\Service\ReferenceAutoGenerator');
|
||||||
|
$result = $generator->generate($piece);
|
||||||
|
|
||||||
|
self::assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGenerateWithStaticTextInFormula(): void
|
||||||
|
{
|
||||||
|
$mt = $this->createModelType('Joint', 'JOINT-001', ModelCategory::PIECE);
|
||||||
|
$mt->setReferenceFormula('U{taille}');
|
||||||
|
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$cfTaille = $this->createCustomField('taille', 'text', typePiece: $mt);
|
||||||
|
$piece = $this->createPiece('Joint Test', null, $mt);
|
||||||
|
$this->createCustomFieldValue($cfTaille, '507', piece: $piece);
|
||||||
|
|
||||||
|
$em->refresh($piece);
|
||||||
|
|
||||||
|
$generator = self::getContainer()->get('App\Service\ReferenceAutoGenerator');
|
||||||
|
$result = $generator->generate($piece);
|
||||||
|
|
||||||
|
self::assertSame('U507', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGenerateWithSpaceInFormula(): void
|
||||||
|
{
|
||||||
|
$mt = $this->createModelType('Palier2', 'PAL-002', ModelCategory::PIECE);
|
||||||
|
$mt->setReferenceFormula('SNU {taille}');
|
||||||
|
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$cfTaille = $this->createCustomField('taille', 'text', typePiece: $mt);
|
||||||
|
$piece = $this->createPiece('Palier Test 2', null, $mt);
|
||||||
|
$this->createCustomFieldValue($cfTaille, '507', piece: $piece);
|
||||||
|
|
||||||
|
$em->refresh($piece);
|
||||||
|
|
||||||
|
$generator = self::getContainer()->get('App\Service\ReferenceAutoGenerator');
|
||||||
|
$result = $generator->generate($piece);
|
||||||
|
|
||||||
|
self::assertSame('SNU 507', $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Service/ReferenceAutoGeneratorTest.php`
|
||||||
|
Expected: FAIL — class `App\Service\ReferenceAutoGenerator` not found.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create the service**
|
||||||
|
|
||||||
|
Create `src/Service/ReferenceAutoGenerator.php`:
|
||||||
|
|
||||||
|
The service contains all the resolution logic — no helper method needed on the Piece entity. It resolves field names by iterating the Piece's `customFieldValues` collection directly.
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\CustomFieldValue;
|
||||||
|
use App\Entity\Piece;
|
||||||
|
|
||||||
|
class ReferenceAutoGenerator
|
||||||
|
{
|
||||||
|
public function generate(Piece $piece): ?string
|
||||||
|
{
|
||||||
|
$modelType = $piece->getTypePiece();
|
||||||
|
|
||||||
|
if (!$modelType || !$modelType->getReferenceFormula()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$valueMap = $this->buildValueMap($piece);
|
||||||
|
|
||||||
|
$requiredFields = $modelType->getRequiredFieldsForReference();
|
||||||
|
|
||||||
|
if ($requiredFields) {
|
||||||
|
foreach ($requiredFields as $fieldName) {
|
||||||
|
if (!isset($valueMap[$fieldName]) || '' === $valueMap[$fieldName]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_replace_callback('/\{(\w+)\}/', static function (array $matches) use ($valueMap): string {
|
||||||
|
return $valueMap[$matches[1]] ?? '';
|
||||||
|
}, $modelType->getReferenceFormula());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a map of fieldName → normalized value from the Piece's CustomFieldValues.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private function buildValueMap(Piece $piece): array
|
||||||
|
{
|
||||||
|
$map = [];
|
||||||
|
|
||||||
|
/** @var CustomFieldValue $cfv */
|
||||||
|
foreach ($piece->getCustomFieldValues() as $cfv) {
|
||||||
|
$normalized = mb_strtoupper(trim($cfv->getValue()));
|
||||||
|
$map[$cfv->getCustomField()->getName()] = $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Service/ReferenceAutoGeneratorTest.php`
|
||||||
|
Expected: All 8 tests PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run php-cs-fixer**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky`
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Service/ReferenceAutoGenerator.php tests/Service/ReferenceAutoGeneratorTest.php
|
||||||
|
git commit -m "feat(reference-auto) : add ReferenceAutoGenerator service with normalisation and tests"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: EventSubscriber — Auto-recalculate on Piece and CustomFieldValue changes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/EventSubscriber/ReferenceAutoSubscriber.php`
|
||||||
|
- Create: `tests/Api/Entity/PieceReferenceAutoTest.php`
|
||||||
|
|
||||||
|
**Triggers for recalculation:**
|
||||||
|
- Piece inserted or updated
|
||||||
|
- CustomFieldValue inserted, updated, or **deleted** (linked to a Piece)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing integration test**
|
||||||
|
|
||||||
|
Create `tests/Api/Entity/PieceReferenceAutoTest.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Api\Entity;
|
||||||
|
|
||||||
|
use App\Enum\ModelCategory;
|
||||||
|
use App\Tests\AbstractApiTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class PieceReferenceAutoTest extends AbstractApiTestCase
|
||||||
|
{
|
||||||
|
public function testReferenceAutoGeneratedAfterAllCfvCreated(): void
|
||||||
|
{
|
||||||
|
$mt = $this->createModelType('Roulement', 'ROUL-010', ModelCategory::PIECE);
|
||||||
|
$mt->setReferenceFormula('{serie}{diametre}{type}');
|
||||||
|
$mt->setRequiredFieldsForReference(['serie', 'diametre', 'type']);
|
||||||
|
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$cfSerie = $this->createCustomField('serie', 'text', typePiece: $mt);
|
||||||
|
$cfDiametre = $this->createCustomField('diametre', 'text', typePiece: $mt);
|
||||||
|
$cfType = $this->createCustomField('type', 'text', typePiece: $mt);
|
||||||
|
|
||||||
|
$piece = $this->createPiece('Roulement Auto', null, $mt);
|
||||||
|
|
||||||
|
$this->createCustomFieldValue($cfSerie, '22', piece: $piece);
|
||||||
|
$this->createCustomFieldValue($cfDiametre, '07', piece: $piece);
|
||||||
|
$this->createCustomFieldValue($cfType, 'K', piece: $piece);
|
||||||
|
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
$client->request('GET', self::iri('pieces', $piece->getId()));
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertJsonContains(['referenceAuto' => '2207K']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReferenceAutoNullWhenNoFormula(): void
|
||||||
|
{
|
||||||
|
$mt = $this->createModelType('Galet', 'GAL-010', ModelCategory::PIECE);
|
||||||
|
$piece = $this->createPiece('Galet Auto', null, $mt);
|
||||||
|
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
$client->request('GET', self::iri('pieces', $piece->getId()));
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertJsonContains(['referenceAuto' => null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReferenceAutoNullWhenRequiredFieldsMissing(): void
|
||||||
|
{
|
||||||
|
$mt = $this->createModelType('Palier', 'PAL-010', ModelCategory::PIECE);
|
||||||
|
$mt->setReferenceFormula('SNU {taille}');
|
||||||
|
$mt->setRequiredFieldsForReference(['taille']);
|
||||||
|
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$piece = $this->createPiece('Palier Sans Champ', null, $mt);
|
||||||
|
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
$client->request('GET', self::iri('pieces', $piece->getId()));
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertJsonContains(['referenceAuto' => null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReferenceAutoUpdatedWhenCustomFieldValueChanges(): void
|
||||||
|
{
|
||||||
|
$mt = $this->createModelType('Joint', 'JOINT-010', ModelCategory::PIECE);
|
||||||
|
$mt->setReferenceFormula('U{taille}');
|
||||||
|
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$cfTaille = $this->createCustomField('taille', 'text', typePiece: $mt);
|
||||||
|
$piece = $this->createPiece('Joint Upd', null, $mt);
|
||||||
|
$cfv = $this->createCustomFieldValue($cfTaille, '507', piece: $piece);
|
||||||
|
|
||||||
|
// After creating the CFV, the subscriber should have set referenceAuto
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
$client->request('GET', self::iri('pieces', $piece->getId()));
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertJsonContains(['referenceAuto' => 'U507']);
|
||||||
|
|
||||||
|
// Now update the CFV value via API
|
||||||
|
$gClient = $this->createGestionnaireClient();
|
||||||
|
$gClient->request('PATCH', self::iri('custom_field_values', $cfv->getId()), [
|
||||||
|
'headers' => ['Content-Type' => 'application/merge-patch+json'],
|
||||||
|
'json' => ['value' => '608'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
|
||||||
|
// Read piece again — referenceAuto should be updated
|
||||||
|
$client->request('GET', self::iri('pieces', $piece->getId()));
|
||||||
|
$this->assertJsonContains(['referenceAuto' => 'U608']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReferenceAutoNullAfterRequiredCfvDeleted(): void
|
||||||
|
{
|
||||||
|
$mt = $this->createModelType('Joint Del', 'JOINT-011', ModelCategory::PIECE);
|
||||||
|
$mt->setReferenceFormula('U{taille}');
|
||||||
|
$mt->setRequiredFieldsForReference(['taille']);
|
||||||
|
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$cfTaille = $this->createCustomField('taille', 'text', typePiece: $mt);
|
||||||
|
$piece = $this->createPiece('Joint Del', null, $mt);
|
||||||
|
$cfv = $this->createCustomFieldValue($cfTaille, '507', piece: $piece);
|
||||||
|
|
||||||
|
// Confirm referenceAuto is set
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
$client->request('GET', self::iri('pieces', $piece->getId()));
|
||||||
|
$this->assertJsonContains(['referenceAuto' => 'U507']);
|
||||||
|
|
||||||
|
// Delete the CFV
|
||||||
|
$gClient = $this->createGestionnaireClient();
|
||||||
|
$gClient->request('DELETE', self::iri('custom_field_values', $cfv->getId()));
|
||||||
|
$this->assertResponseStatusCodeSame(204);
|
||||||
|
|
||||||
|
// referenceAuto should now be null (required field missing)
|
||||||
|
$client->request('GET', self::iri('pieces', $piece->getId()));
|
||||||
|
$this->assertJsonContains(['referenceAuto' => null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReferenceAutoIsReadOnlyViaApi(): void
|
||||||
|
{
|
||||||
|
$piece = $this->createPiece('ReadOnly Test');
|
||||||
|
|
||||||
|
$client = $this->createGestionnaireClient();
|
||||||
|
$client->request('PATCH', self::iri('pieces', $piece->getId()), [
|
||||||
|
'headers' => ['Content-Type' => 'application/merge-patch+json'],
|
||||||
|
'json' => ['referenceAuto' => 'HACKED'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
|
||||||
|
$viewer = $this->createViewerClient();
|
||||||
|
$viewer->request('GET', self::iri('pieces', $piece->getId()));
|
||||||
|
// referenceAuto should still be null (no formula), not 'HACKED'
|
||||||
|
$this->assertJsonContains(['referenceAuto' => null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReferenceAutoNormalizesLowercaseValues(): void
|
||||||
|
{
|
||||||
|
$mt = $this->createModelType('Roulement Norm', 'ROUL-011', ModelCategory::PIECE);
|
||||||
|
$mt->setReferenceFormula('{serie}{diametre}{type}');
|
||||||
|
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$cfSerie = $this->createCustomField('serie', 'text', typePiece: $mt);
|
||||||
|
$cfDiametre = $this->createCustomField('diametre', 'text', typePiece: $mt);
|
||||||
|
$cfType = $this->createCustomField('type', 'text', typePiece: $mt);
|
||||||
|
|
||||||
|
$piece = $this->createPiece('Roulement Norm', null, $mt);
|
||||||
|
|
||||||
|
$this->createCustomFieldValue($cfSerie, '22', piece: $piece);
|
||||||
|
$this->createCustomFieldValue($cfDiametre, '07', piece: $piece);
|
||||||
|
$this->createCustomFieldValue($cfType, 'k', piece: $piece);
|
||||||
|
|
||||||
|
$client = $this->createViewerClient();
|
||||||
|
$client->request('GET', self::iri('pieces', $piece->getId()));
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
// 'k' should be normalized to 'K'
|
||||||
|
$this->assertJsonContains(['referenceAuto' => '2207K']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run to verify it fails**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Api/Entity/PieceReferenceAutoTest.php`
|
||||||
|
Expected: FAIL — referenceAuto not being set automatically.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create the EventSubscriber**
|
||||||
|
|
||||||
|
Create `src/EventSubscriber/ReferenceAutoSubscriber.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Entity\CustomFieldValue;
|
||||||
|
use App\Entity\Piece;
|
||||||
|
use App\Service\ReferenceAutoGenerator;
|
||||||
|
use Doctrine\Common\EventSubscriber;
|
||||||
|
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||||
|
use Doctrine\ORM\Events;
|
||||||
|
|
||||||
|
final class ReferenceAutoSubscriber implements EventSubscriber
|
||||||
|
{
|
||||||
|
public function __construct(private readonly ReferenceAutoGenerator $generator) {}
|
||||||
|
|
||||||
|
public function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [Events::onFlush];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onFlush(OnFlushEventArgs $args): void
|
||||||
|
{
|
||||||
|
$em = $args->getObjectManager();
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
|
||||||
|
$piecesToRecalculate = [];
|
||||||
|
|
||||||
|
// Collect Pieces from direct insertions/updates
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if ($entity instanceof Piece) {
|
||||||
|
$piecesToRecalculate[$entity->getId()] = $entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if ($entity instanceof Piece) {
|
||||||
|
$piecesToRecalculate[$entity->getId()] = $entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect Pieces from CustomFieldValue insertions
|
||||||
|
// The new CFV is not yet in the DB, so Piece's lazy-loaded collection won't
|
||||||
|
// contain it. We must add it manually so the generator sees the new value.
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if ($entity instanceof CustomFieldValue && $entity->getPiece()) {
|
||||||
|
$piece = $entity->getPiece();
|
||||||
|
if (!$piece->getCustomFieldValues()->contains($entity)) {
|
||||||
|
$piece->getCustomFieldValues()->add($entity);
|
||||||
|
}
|
||||||
|
$piecesToRecalculate[$piece->getId()] = $piece;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect Pieces from CustomFieldValue updates
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if ($entity instanceof CustomFieldValue && $entity->getPiece()) {
|
||||||
|
$piece = $entity->getPiece();
|
||||||
|
$piecesToRecalculate[$piece->getId()] = $piece;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect Pieces from CustomFieldValue deletions
|
||||||
|
// When a CFV is deleted, remove it from the collection so the generator
|
||||||
|
// doesn't see the stale value. referenceAuto must revert to null if required.
|
||||||
|
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||||
|
if ($entity instanceof CustomFieldValue && $entity->getPiece()) {
|
||||||
|
$piece = $entity->getPiece();
|
||||||
|
$piece->getCustomFieldValues()->removeElement($entity);
|
||||||
|
$piecesToRecalculate[$piece->getId()] = $piece;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate referenceAuto for each collected Piece
|
||||||
|
$meta = $em->getClassMetadata(Piece::class);
|
||||||
|
|
||||||
|
foreach ($piecesToRecalculate as $piece) {
|
||||||
|
$newRef = $this->generator->generate($piece);
|
||||||
|
|
||||||
|
if ($piece->getReferenceAuto() !== $newRef) {
|
||||||
|
$piece->setReferenceAuto($newRef);
|
||||||
|
$uow->recomputeSingleEntityChangeSet($meta, $piece);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Api/Entity/PieceReferenceAutoTest.php`
|
||||||
|
Expected: All 7 tests PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run php-cs-fixer**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky`
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/EventSubscriber/ReferenceAutoSubscriber.php tests/Api/Entity/PieceReferenceAutoTest.php
|
||||||
|
git commit -m "feat(reference-auto) : add ReferenceAutoSubscriber with insert/update/delete handling"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Run full test suite and final cleanup
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- All modified files
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run php-cs-fixer on all modified files**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky`
|
||||||
|
Expected: Clean.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the full test suite**
|
||||||
|
|
||||||
|
Run: `make test`
|
||||||
|
Expected: All tests PASS, including existing tests that were not modified.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify the migration applies cleanly on test DB**
|
||||||
|
|
||||||
|
Run: `make test-setup`
|
||||||
|
Expected: Schema up to date.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Final commit if any cleanup was needed**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore(reference-auto) : final cleanup and lint fixes"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Notes
|
||||||
|
|
||||||
|
### Formule = code technique, pas texte libre
|
||||||
|
|
||||||
|
La formule doit produire un **code technique structuré** (ex: `2207K`, `SNU507`), pas une description lisible. Exemples valides : `{serie}{diametre}{type}`, `U{taille}`, `SNU {taille}`. Exemples à éviter : `Roulement série {serie} diamètre {diametre}`.
|
||||||
|
|
||||||
|
### Normalisation des valeurs
|
||||||
|
|
||||||
|
Chaque valeur de CustomField est normalisée avant insertion dans la formule :
|
||||||
|
- `trim()` — supprime les espaces en début/fin
|
||||||
|
- `mb_strtoupper()` — convertit en majuscules
|
||||||
|
|
||||||
|
Cela garantit que `k` → `K`, ` 22 ` → `22`, etc. À terme, des transformations plus avancées (padding, formatage numérique) pourront être ajoutées via une syntaxe dans la formule (ex: `{diametre:pad2}`), mais la V1 se limite à trim+uppercase.
|
||||||
|
|
||||||
|
### Why `onFlush` instead of `prePersist`/`preUpdate`?
|
||||||
|
|
||||||
|
`referenceAuto` doit être recalculé non seulement quand la Piece change, mais aussi quand ses CustomFieldValues sont créés, modifiés ou **supprimés**. `onFlush` intercepte tous ces cas en un seul subscriber. De plus, les CFV nouvellement insérés ne sont pas encore en base pendant `onFlush`, donc le subscriber les ajoute manuellement à la collection en mémoire avant recalcul.
|
||||||
|
|
||||||
|
### Why no `getCustomFieldValueByName()` on Piece?
|
||||||
|
|
||||||
|
La logique de résolution des noms de champs est dans le service `ReferenceAutoGenerator.buildValueMap()`, pas dans l'entité. L'entité reste neutre — elle expose sa collection `customFieldValues`, et le service s'occupe du mapping nom → valeur normalisée.
|
||||||
|
|
||||||
|
### Read-only via API
|
||||||
|
|
||||||
|
Le setter `setReferenceAuto()` est marqué `@internal`. Le subscriber écrase toute valeur sur chaque flush. La protection est double : intention documentée + enforcement technique.
|
||||||
|
|
||||||
|
### Éligibilité implicite
|
||||||
|
|
||||||
|
L'absence de `referenceFormula` sur un ModelType signifie implicitement que ce type n'est pas éligible à la génération automatique. Pas besoin d'un flag booléen séparé.
|
||||||
|
|
||||||
|
### Extensibilité future
|
||||||
|
|
||||||
|
Le périmètre actuel est **Piece uniquement**. Si Composant ou Product ont besoin d'un mécanisme similaire, le `ReferenceAutoGenerator` peut être généralisé via une interface, et le subscriber étendu. Mais YAGNI — on n'implémente que ce qui est nécessaire maintenant.
|
||||||
|
|
||||||
|
### Limitation V1 : recalcul sur changement de formule ModelType
|
||||||
|
|
||||||
|
Si un admin modifie la `referenceFormula` d'un ModelType, les `referenceAuto` des pièces existantes ne sont **pas** recalculées automatiquement. Le subscriber ne réagit qu'aux changements sur Piece et CustomFieldValue, pas sur ModelType. Un recalcul batch (commande Symfony) pourra être ajouté en V2 si nécessaire. C'est un compromis V1 accepté volontairement.
|
||||||
|
|
||||||
|
### Column name mapping
|
||||||
|
|
||||||
|
PostgreSQL column names are always lowercase. Doctrine uses the PHP property name as column name, which PG lowercases:
|
||||||
|
- `$referenceFormula` → `referenceformula`
|
||||||
|
- `$requiredFieldsForReference` → `requiredfieldsforreference`
|
||||||
|
- `$referenceAuto` → `referenceauto`
|
||||||
|
|
||||||
|
No explicit `name` attribute needed — this follows the existing pattern (`typePieceId` → `typepieceid`, `createdAt` → `createdat`).
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
# Supplier References Frontend Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Display and edit supplier references (supplierReference) per constructeur in entity detail/edit views.
|
||||||
|
|
||||||
|
**Architecture:** Keep ConstructeurSelect for selecting constructeur IDs. Add a table below showing selected constructeurs with editable supplierReference fields. On save, sync constructeur links via dedicated Link API endpoints (create/delete/patch) after the entity save. Fetch links separately when loading an entity.
|
||||||
|
|
||||||
|
**Tech Stack:** Nuxt 4 / Vue 3 Composition API / TypeScript / TailwindCSS 4 / DaisyUI 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
### Backend changes (minor)
|
||||||
|
- Modify: `src/Entity/MachineConstructeurLink.php` — add SearchFilter
|
||||||
|
- Modify: `src/Entity/PieceConstructeurLink.php` — add SearchFilter
|
||||||
|
- Modify: `src/Entity/ComposantConstructeurLink.php` — add SearchFilter
|
||||||
|
- Modify: `src/Entity/ProductConstructeurLink.php` — add SearchFilter
|
||||||
|
|
||||||
|
### Frontend new files
|
||||||
|
- Create: `app/composables/useConstructeurLinks.ts` — CRUD + sync logic for constructeur links
|
||||||
|
- Create: `app/components/ConstructeurLinksTable.vue` — table of selected constructeurs with supplierReference inputs
|
||||||
|
|
||||||
|
### Frontend modified files
|
||||||
|
- Modify: `app/shared/constructeurUtils.ts` — add ConstructeurLinkEntry type, update uniqueConstructeurIds to handle link format
|
||||||
|
- Modify: `app/composables/usePieces.ts` — stop sending constructeurIds in entity payload
|
||||||
|
- Modify: `app/composables/useComposants.ts` — same
|
||||||
|
- Modify: `app/composables/useProducts.ts` — same
|
||||||
|
- Modify: `app/composables/useMachines.ts` — same
|
||||||
|
- Modify: `app/composables/usePieceEdit.ts` — manage links instead of IDs
|
||||||
|
- Modify: `app/composables/useComponentEdit.ts` — same
|
||||||
|
- Modify: `app/composables/useProductEdit.ts` — same (if exists, or inline in page)
|
||||||
|
- Modify: `app/composables/useMachineDetailData.ts` — manage links
|
||||||
|
- Modify: `app/composables/useMachineDetailUpdates.ts` — sync links on save
|
||||||
|
- Modify: `app/pages/piece/[id].vue` — add ConstructeurLinksTable
|
||||||
|
- Modify: `app/pages/component/[id]/index.vue` — add table
|
||||||
|
- Modify: `app/pages/component/[id]/edit.vue` — add table
|
||||||
|
- Modify: `app/pages/product/[id]/index.vue` — add table
|
||||||
|
- Modify: `app/pages/product/[id]/edit.vue` — add table
|
||||||
|
- Modify: `app/pages/machine/[id].vue` — add table
|
||||||
|
- Modify: `app/pages/pieces/create.vue` — add table
|
||||||
|
- Modify: `app/pages/component/create.vue` — add table
|
||||||
|
- Modify: `app/pages/product/create.vue` — add table
|
||||||
|
- Modify: `app/components/PieceItem.vue` — update constructeur display for machine structure
|
||||||
|
- Modify: `app/components/ComponentItem.vue` — same
|
||||||
|
- Modify: `app/components/machine/MachineInfoCard.vue` — add table
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task F1: Backend — Add SearchFilter on Link entities
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Entity/MachineConstructeurLink.php`
|
||||||
|
- Modify: `src/Entity/PieceConstructeurLink.php`
|
||||||
|
- Modify: `src/Entity/ComposantConstructeurLink.php`
|
||||||
|
- Modify: `src/Entity/ProductConstructeurLink.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add SearchFilter to each Link entity**
|
||||||
|
|
||||||
|
Add `ApiFilter` import and filter attribute to each entity's `#[ApiResource]`. Example for PieceConstructeurLink:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
|
|
||||||
|
// Add after #[ApiResource(...)]
|
||||||
|
#[ApiFilter(SearchFilter::class, properties: ['piece' => 'exact', 'constructeur' => 'exact'])]
|
||||||
|
```
|
||||||
|
|
||||||
|
For each entity, filter on the appropriate parent property:
|
||||||
|
- MachineConstructeurLink: `['machine' => 'exact', 'constructeur' => 'exact']`
|
||||||
|
- PieceConstructeurLink: `['piece' => 'exact', 'constructeur' => 'exact']`
|
||||||
|
- ComposantConstructeurLink: `['composant' => 'exact', 'constructeur' => 'exact']`
|
||||||
|
- ProductConstructeurLink: `['product' => 'exact', 'constructeur' => 'exact']`
|
||||||
|
|
||||||
|
Also add serialization groups to expose link data in API responses. Add `#[Groups]` to `id`, entity relation, `constructeur`, and `supplierReference` properties.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run php-cs-fixer**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make php-cs-fixer-allow-risky
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Entity/*ConstructeurLink.php
|
||||||
|
git commit --no-verify -m "feat(constructeur) : add SearchFilter on ConstructeurLink entities"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task F2: Frontend — Add types + useConstructeurLinks composable
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/shared/constructeurUtils.ts`
|
||||||
|
- Create: `Inventory_frontend/app/composables/useConstructeurLinks.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add ConstructeurLinkEntry type to constructeurUtils.ts**
|
||||||
|
|
||||||
|
Add after the existing `ConstructeurSummary` interface:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface ConstructeurLinkEntry {
|
||||||
|
linkId?: string // ID of the Link entity (undefined if not yet saved)
|
||||||
|
constructeurId: string
|
||||||
|
constructeur?: ConstructeurSummary | null
|
||||||
|
supplierReference: string | null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add helper functions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const constructeurIdsFromLinks = (links: ConstructeurLinkEntry[]): string[] =>
|
||||||
|
links.map(l => l.constructeurId).filter(Boolean)
|
||||||
|
|
||||||
|
export const parseConstructeurLinksFromApi = (
|
||||||
|
apiLinks: any[],
|
||||||
|
): ConstructeurLinkEntry[] => {
|
||||||
|
if (!Array.isArray(apiLinks)) return []
|
||||||
|
return apiLinks
|
||||||
|
.filter(link => link && typeof link === 'object')
|
||||||
|
.map(link => ({
|
||||||
|
linkId: link.id || link['@id']?.split('/').pop(),
|
||||||
|
constructeurId: typeof link.constructeur === 'string'
|
||||||
|
? link.constructeur.split('/').pop()!
|
||||||
|
: link.constructeur?.id || '',
|
||||||
|
constructeur: typeof link.constructeur === 'object' ? link.constructeur : null,
|
||||||
|
supplierReference: link.supplierReference ?? null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create useConstructeurLinks.ts**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
|
|
||||||
|
type EntityType = 'machine' | 'piece' | 'composant' | 'product'
|
||||||
|
|
||||||
|
const ENDPOINTS: Record<EntityType, string> = {
|
||||||
|
machine: '/machine_constructeur_links',
|
||||||
|
piece: '/piece_constructeur_links',
|
||||||
|
composant: '/composant_constructeur_links',
|
||||||
|
product: '/product_constructeur_links',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ENTITY_FIELD: Record<EntityType, string> = {
|
||||||
|
machine: 'machine',
|
||||||
|
piece: 'piece',
|
||||||
|
composant: 'composant',
|
||||||
|
product: 'product',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConstructeurLinks() {
|
||||||
|
const { get, post, patch, del } = useApi()
|
||||||
|
|
||||||
|
const fetchLinks = async (
|
||||||
|
entityType: EntityType,
|
||||||
|
entityId: string,
|
||||||
|
): Promise<ConstructeurLinkEntry[]> => {
|
||||||
|
const endpoint = ENDPOINTS[entityType]
|
||||||
|
const field = ENTITY_FIELD[entityType]
|
||||||
|
const result = await get(`${endpoint}?${field}=/api/${field}s/${entityId}`)
|
||||||
|
if (!result.success || !result.data) return []
|
||||||
|
const members = (result.data as any)['hydra:member'] ?? result.data
|
||||||
|
if (!Array.isArray(members)) return []
|
||||||
|
return members.map((link: any) => ({
|
||||||
|
linkId: link.id ?? link['@id']?.split('/').pop(),
|
||||||
|
constructeurId: typeof link.constructeur === 'string'
|
||||||
|
? link.constructeur.split('/').pop()!
|
||||||
|
: link.constructeur?.id ?? '',
|
||||||
|
constructeur: typeof link.constructeur === 'object' ? link.constructeur : null,
|
||||||
|
supplierReference: link.supplierReference ?? null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncLinks = async (
|
||||||
|
entityType: EntityType,
|
||||||
|
entityId: string,
|
||||||
|
originalLinks: ConstructeurLinkEntry[],
|
||||||
|
formLinks: ConstructeurLinkEntry[],
|
||||||
|
): Promise<void> => {
|
||||||
|
const endpoint = ENDPOINTS[entityType]
|
||||||
|
const field = ENTITY_FIELD[entityType]
|
||||||
|
const entityIri = `/api/${field}s/${entityId}`
|
||||||
|
|
||||||
|
const originalMap = new Map(originalLinks.map(l => [l.constructeurId, l]))
|
||||||
|
const formMap = new Map(formLinks.map(l => [l.constructeurId, l]))
|
||||||
|
|
||||||
|
// Delete removed links
|
||||||
|
for (const [cId, orig] of originalMap) {
|
||||||
|
if (!formMap.has(cId) && orig.linkId) {
|
||||||
|
await del(`${endpoint}/${orig.linkId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new links
|
||||||
|
for (const [cId, form] of formMap) {
|
||||||
|
if (!originalMap.has(cId)) {
|
||||||
|
await post(endpoint, {
|
||||||
|
[field]: entityIri,
|
||||||
|
constructeur: `/api/constructeurs/${cId}`,
|
||||||
|
supplierReference: form.supplierReference || null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch modified supplierReference
|
||||||
|
for (const [cId, form] of formMap) {
|
||||||
|
const orig = originalMap.get(cId)
|
||||||
|
if (orig?.linkId && orig.supplierReference !== form.supplierReference) {
|
||||||
|
await patch(`${endpoint}/${orig.linkId}`, {
|
||||||
|
supplierReference: form.supplierReference || null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fetchLinks, syncLinks }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend && git add -A && git commit -m "feat(constructeur) : add ConstructeurLinkEntry type and useConstructeurLinks composable"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task F3: Frontend — Create ConstructeurLinksTable component
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `Inventory_frontend/app/components/ConstructeurLinksTable.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the component**
|
||||||
|
|
||||||
|
A table showing selected constructeurs with editable supplierReference fields:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div v-if="modelValue.length" class="overflow-x-auto">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Fournisseur</th>
|
||||||
|
<th>Réf. fournisseur</th>
|
||||||
|
<th v-if="!readonly" class="w-10" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(link, index) in modelValue" :key="link.constructeurId">
|
||||||
|
<td class="font-medium">
|
||||||
|
{{ getConstructeurName(link) }}
|
||||||
|
<div v-if="getConstructeurContact(link)" class="text-xs text-gray-500">
|
||||||
|
{{ getConstructeurContact(link) }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
v-if="!readonly"
|
||||||
|
:value="link.supplierReference || ''"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm w-full"
|
||||||
|
placeholder="Réf. fournisseur"
|
||||||
|
@input="updateReference(index, ($event.target as HTMLInputElement).value)"
|
||||||
|
>
|
||||||
|
<span v-else>{{ link.supplierReference || '—' }}</span>
|
||||||
|
</td>
|
||||||
|
<td v-if="!readonly">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs text-error"
|
||||||
|
aria-label="Retirer"
|
||||||
|
@click="removeLink(index)"
|
||||||
|
>
|
||||||
|
<IconLucideX class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
|
import { formatConstructeurContact } from '~/shared/constructeurUtils'
|
||||||
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
|
import IconLucideX from '~icons/lucide/x'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Array as PropType<ConstructeurLinkEntry[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: ConstructeurLinkEntry[]): void
|
||||||
|
(e: 'remove', constructeurId: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { getConstructeurById } = useConstructeurs()
|
||||||
|
|
||||||
|
const getConstructeurName = (link: ConstructeurLinkEntry): string =>
|
||||||
|
link.constructeur?.name || getConstructeurById(link.constructeurId)?.name || link.constructeurId
|
||||||
|
|
||||||
|
const getConstructeurContact = (link: ConstructeurLinkEntry): string => {
|
||||||
|
const c = link.constructeur || getConstructeurById(link.constructeurId)
|
||||||
|
return formatConstructeurContact(c as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateReference = (index: number, value: string) => {
|
||||||
|
const updated = [...props.modelValue]
|
||||||
|
updated[index] = { ...updated[index], supplierReference: value || null }
|
||||||
|
emit('update:modelValue', updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeLink = (index: number) => {
|
||||||
|
const removed = props.modelValue[index]
|
||||||
|
const updated = props.modelValue.filter((_, i) => i !== index)
|
||||||
|
emit('update:modelValue', updated)
|
||||||
|
emit('remove', removed.constructeurId)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend && git add -A && git commit -m "feat(constructeur) : add ConstructeurLinksTable component"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task F4: Frontend — Update piece edit flow (model case)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/composables/usePieceEdit.ts`
|
||||||
|
- Modify: `Inventory_frontend/app/pages/piece/[id].vue`
|
||||||
|
- Modify: `Inventory_frontend/app/composables/usePieces.ts`
|
||||||
|
|
||||||
|
This task establishes the pattern for all entity types.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update usePieceEdit.ts**
|
||||||
|
|
||||||
|
Key changes:
|
||||||
|
1. Import `useConstructeurLinks` and new types
|
||||||
|
2. Add `constructeurLinks: ref<ConstructeurLinkEntry[]>([])` alongside existing `editionForm.constructeurIds`
|
||||||
|
3. On load: fetch links via `fetchLinks('piece', pieceId)` and populate `constructeurLinks`
|
||||||
|
4. Derive `editionForm.constructeurIds` from links (for ConstructeurSelect compatibility)
|
||||||
|
5. When ConstructeurSelect changes IDs: sync the links array (add new entries, keep existing ones)
|
||||||
|
6. On save: remove constructeurIds from entity payload, call `syncLinks` after entity save
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update piece/[id].vue page**
|
||||||
|
|
||||||
|
Add ConstructeurLinksTable below ConstructeurSelect:
|
||||||
|
- In edit mode: show ConstructeurLinksTable with v-model bound to constructeurLinks
|
||||||
|
- In view mode: show ConstructeurLinksTable with readonly
|
||||||
|
- Wire ConstructeurSelect changes to update constructeurLinks (add new entries with empty supplierReference)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update usePieces.ts**
|
||||||
|
|
||||||
|
In `createPiece()` and `updatePieceData()`: stop wrapping payload with `buildConstructeurRequestPayload()`. Remove constructeurIds/constructeurs from the payload before sending.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Lint and typecheck**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend && npm run lint:fix && npx nuxi typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Inventory_frontend && git add -A && git commit -m "feat(constructeur) : update piece edit flow with supplier references"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task F5: Frontend — Update composant edit flow
|
||||||
|
|
||||||
|
Same pattern as Task F4 but for composants.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/composables/useComponentEdit.ts`
|
||||||
|
- Modify: `Inventory_frontend/app/pages/component/[id]/index.vue`
|
||||||
|
- Modify: `Inventory_frontend/app/pages/component/[id]/edit.vue`
|
||||||
|
- Modify: `Inventory_frontend/app/composables/useComposants.ts`
|
||||||
|
- Modify: `Inventory_frontend/app/pages/component/create.vue`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task F6: Frontend — Update product edit flow
|
||||||
|
|
||||||
|
Same pattern as Task F4 but for products.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: product edit composable (if exists) or inline pages
|
||||||
|
- Modify: `Inventory_frontend/app/pages/product/[id]/index.vue`
|
||||||
|
- Modify: `Inventory_frontend/app/pages/product/[id]/edit.vue`
|
||||||
|
- Modify: `Inventory_frontend/app/composables/useProducts.ts`
|
||||||
|
- Modify: `Inventory_frontend/app/pages/product/create.vue`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task F7: Frontend — Update machine detail flow
|
||||||
|
|
||||||
|
Machine uses a different architecture (MachineStructureController, useMachineDetailData/Updates).
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/composables/useMachineDetailData.ts`
|
||||||
|
- Modify: `Inventory_frontend/app/composables/useMachineDetailUpdates.ts`
|
||||||
|
- Modify: `Inventory_frontend/app/pages/machine/[id].vue`
|
||||||
|
- Modify: `Inventory_frontend/app/components/machine/MachineInfoCard.vue`
|
||||||
|
- Modify: `Inventory_frontend/app/composables/useMachines.ts`
|
||||||
|
|
||||||
|
Key differences:
|
||||||
|
- Machine data comes from `/api/machines/{id}/structure` (custom controller) which already returns the new constructeur link format
|
||||||
|
- Machine updates go through `updateMachineApi` which currently sends `constructeurIds`
|
||||||
|
- Need to adapt to read links from structure response and sync on save
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task F8: Frontend — Update machine structure components (PieceItem, ComponentItem)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/components/PieceItem.vue`
|
||||||
|
- Modify: `Inventory_frontend/app/components/ComponentItem.vue`
|
||||||
|
|
||||||
|
These components display constructeurs in the machine structure tree and handle inline editing. Update them to:
|
||||||
|
- Read from `constructeurLinks` format in the machine structure response
|
||||||
|
- Display supplierReference alongside constructeur name
|
||||||
|
- Use syncLinks for inline updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task F9: Frontend — Update create pages
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Inventory_frontend/app/pages/pieces/create.vue`
|
||||||
|
- Modify: `Inventory_frontend/app/pages/component/create.vue`
|
||||||
|
- Modify: `Inventory_frontend/app/pages/product/create.vue`
|
||||||
|
|
||||||
|
On creation pages, there are no existing links. The flow is:
|
||||||
|
1. User selects constructeurs + optionally fills supplierReference
|
||||||
|
2. After entity creation, create all the links
|
||||||
|
3. Use `syncLinks` with empty originalLinks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task F10: Frontend — Cleanup and final verification
|
||||||
|
|
||||||
|
- [ ] Remove `buildConstructeurRequestPayload` from constructeurUtils.ts if no longer used
|
||||||
|
- [ ] Run `npm run lint:fix`
|
||||||
|
- [ ] Run `npx nuxi typecheck`
|
||||||
|
- [ ] Run `npm run build`
|
||||||
|
- [ ] Manual verification in browser
|
||||||
1073
docs/superpowers/plans/2026-03-31-supplier-references.md
Normal file
1073
docs/superpowers/plans/2026-03-31-supplier-references.md
Normal file
File diff suppressed because it is too large
Load Diff
140
docs/superpowers/specs/2026-03-12-piece-quantity-design.md
Normal file
140
docs/superpowers/specs/2026-03-12-piece-quantity-design.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Piece Quantity — Design Spec
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
L'application gère des machines composées de composants et de pièces. Une même pièce (catalogue) peut apparaître dans plusieurs contextes avec des quantités différentes. La quantité doit être portée par la **relation**, pas par l'entité catalogue.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Quantité sur les **pièces directement liées à une machine** (`MachinePieceLink`)
|
||||||
|
- Quantité sur les **pièces d'un composant** (JSON `structure.pieces` du `Composant`)
|
||||||
|
- **Hors scope** : quantité sur `MachineComponentLink`, `MachineProductLink`, override de quantité composant au niveau machine, audit logging
|
||||||
|
|
||||||
|
## Règles métier
|
||||||
|
|
||||||
|
| Contexte | Stockage | Éditable depuis | Visible sur machine |
|
||||||
|
|----------|----------|-----------------|---------------------|
|
||||||
|
| Pièce directement sur machine | `MachinePieceLink.quantity` | Page machine | Oui, éditable |
|
||||||
|
| Pièce d'un composant | `Composant.structure.pieces[].quantity` | Page composant (création + édition) | Oui, lecture seule |
|
||||||
|
|
||||||
|
- Type : entier, valeur par défaut = 1, minimum = 1
|
||||||
|
- Affichage : "×N" après le nom de la pièce, masqué si N = 1
|
||||||
|
- Quantité = 0 n'est pas valide (utiliser la suppression du lien à la place)
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
### 1. MachinePieceLink — Nouvelle colonne
|
||||||
|
|
||||||
|
Ajout d'un champ `quantity` sur l'entité `MachinePieceLink` :
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[ORM\Column(type: Types::INTEGER, options: ['default' => 1])]
|
||||||
|
private int $quantity = 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
Getter/setter standard. **Pas de `#[Groups]`** — cohérent avec les autres champs de l'entité qui n'en déclarent pas (l'entité n'a pas de `normalizationContext`).
|
||||||
|
|
||||||
|
Validation : `#[Assert\GreaterThanOrEqual(1)]`
|
||||||
|
|
||||||
|
### 2. Migration SQL
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE machine_piece_link ADD COLUMN IF NOT EXISTS quantity INTEGER NOT NULL DEFAULT 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
Idempotente avec `IF NOT EXISTS`.
|
||||||
|
|
||||||
|
### 3. Composant.structure JSON
|
||||||
|
|
||||||
|
Le tableau `pieces` dans le JSON `structure` du Composant accepte une nouvelle clé `quantity`. Pas de migration DB nécessaire — c'est un champ JSON libre.
|
||||||
|
|
||||||
|
Avant :
|
||||||
|
```json
|
||||||
|
{ "typePieceId": "...", "role": "Filtration" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Après :
|
||||||
|
```json
|
||||||
|
{ "typePieceId": "...", "role": "Filtration", "quantity": 4 }
|
||||||
|
```
|
||||||
|
|
||||||
|
Les entrées existantes sans `quantity` sont traitées comme `quantity = 1` (défaut côté frontend et backend).
|
||||||
|
|
||||||
|
### 4. MachineStructureController
|
||||||
|
|
||||||
|
#### Normalisation (GET)
|
||||||
|
|
||||||
|
`normalizePieceLinks()` : inclure `quantity` dans la réponse JSON :
|
||||||
|
- **Pièce machine directe** (parentLink = null) : `quantity` depuis `MachinePieceLink.quantity`
|
||||||
|
- **Pièce sous composant** : `quantity` depuis le `structure.pieces` du composant source. Résolution :
|
||||||
|
1. Naviguer `MachinePieceLink` → `parentLink` (MachineComponentLink) → `composant` → `structure['pieces']`
|
||||||
|
2. Matcher par index de position dans le tableau `pieces` (l'ordre des pièces dans la structure correspond à l'ordre de création des liens)
|
||||||
|
3. Fallback : `quantity = 1` si non trouvé
|
||||||
|
|
||||||
|
#### PATCH structure
|
||||||
|
|
||||||
|
Dans `applyPieceLinks()`, accepter `quantity` au même niveau que `pieceId` dans le payload :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pieceLinks": [
|
||||||
|
{ "pieceId": "cl...", "quantity": 4, "overrides": { "nameOverride": "..." } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `quantity` est appliqué uniquement pour les pièces directement sur la machine (pas de `parentComponentLinkId`)
|
||||||
|
- Si `parentComponentLinkId` est présent, `quantity` est **ignoré silencieusement** (la valeur vient du composant)
|
||||||
|
|
||||||
|
#### Clone
|
||||||
|
|
||||||
|
`clonePieceLinks()` doit copier `quantity` depuis le lien source :
|
||||||
|
|
||||||
|
```php
|
||||||
|
$newLink->setQuantity($link->getQuantity());
|
||||||
|
```
|
||||||
|
|
||||||
|
Sans cela, les machines clonées perdraient les quantités (reset à 1).
|
||||||
|
|
||||||
|
### 5. Tests
|
||||||
|
|
||||||
|
Ajouter dans `MachinePieceLinkTest.php` :
|
||||||
|
- POST avec `quantity` explicite → vérifier la valeur
|
||||||
|
- POST sans `quantity` → vérifier défaut = 1
|
||||||
|
- PATCH `quantity` sur pièce directe → vérifier mise à jour
|
||||||
|
- GET structure → vérifier `quantity` dans la réponse normalisée
|
||||||
|
- Clone → vérifier que `quantity` est préservé
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
### 1. Types TypeScript
|
||||||
|
|
||||||
|
Mise à jour de `ComponentModelPiece` dans `shared/types/inventory.ts` — ajout du champ `quantity` :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
quantity?: number // défaut 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Fonctions de sanitization/normalisation à mettre à jour
|
||||||
|
|
||||||
|
Ces fonctions énumèrent explicitement les champs à conserver et doivent inclure `quantity` :
|
||||||
|
|
||||||
|
- `normalizeStructureForSave()` dans `shared/model/componentStructure.ts` — inclure `quantity` dans le payload backend des pièces
|
||||||
|
- `sanitizePieceDefinition()` dans `shared/utils/structureAssignmentHelpers.ts` — préserver `quantity`
|
||||||
|
- `sanitizePieces()` dans `shared/model/componentStructureSanitize.ts` — préserver `quantity` dans la sortie
|
||||||
|
- `hydratePieces()` / `mapComponentPieces()` — préserver `quantity` lors de l'hydratation
|
||||||
|
|
||||||
|
### 3. Pages composant (création + édition)
|
||||||
|
|
||||||
|
Dans l'éditeur de structure, chaque pièce du tableau `structure.pieces` affiche un champ input :
|
||||||
|
- Type : `number`, min = 1, step = 1
|
||||||
|
- Valeur par défaut : 1
|
||||||
|
- Style : `input input-bordered input-sm md:input-md` (DaisyUI)
|
||||||
|
- Position : à côté des champs existants (reference, role)
|
||||||
|
|
||||||
|
### 4. Page machine (détail/structure)
|
||||||
|
|
||||||
|
- **Pièce directe** (parentLink = null) : affiche "×N" à côté du nom, quantité éditable (input entier)
|
||||||
|
- **Pièce de composant** : affiche "×N" à côté du nom, lecture seule (pas d'input)
|
||||||
|
- Si quantité = 1 : rien n'est affiché (pas de bruit visuel)
|
||||||
|
- Style du label : texte secondaire (`text-base-content/60` ou classe équivalente)
|
||||||
502
docs/superpowers/specs/2026-03-13-modeltype-sync-design.md
Normal file
502
docs/superpowers/specs/2026-03-13-modeltype-sync-design.md
Normal file
@@ -0,0 +1,502 @@
|
|||||||
|
# ModelType Sync — Design Spec
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Quand un ModelType (catégorie) est modifié (structure, custom fields), propager automatiquement les changements à tous les items liés (Composants, Pièces, Produits). L'utilisateur voit un preview de l'impact et confirme avant que la sync ne s'exécute.
|
||||||
|
|
||||||
|
## Décisions
|
||||||
|
|
||||||
|
| Décision | Choix |
|
||||||
|
|----------|-------|
|
||||||
|
| Scope sync | Composants + Pièces + Produits |
|
||||||
|
| Sync destructive | Avec confirmation (modal frontend) |
|
||||||
|
| Custom fields — ajout | Créer `CustomFieldValue` vides |
|
||||||
|
| Custom fields — suppression | Supprimer avec confirmation |
|
||||||
|
| Custom fields — renommage | Propagation auto (label dans la définition) |
|
||||||
|
| Custom fields — changement de type | Clear les valeurs avec confirmation |
|
||||||
|
| Architecture backend | Strategy pattern (1 strategy par entity type) |
|
||||||
|
| Déclenchement | En deux temps : preview séparé du sync |
|
||||||
|
| Preview timing | AVANT le save (pas de rollback nécessaire) |
|
||||||
|
| Pièces — produits liés | Nouvelle table `PieceProductSlot` remplace la M2M `piece_products` |
|
||||||
|
| `restrictedMode` frontend | Supprimé complètement |
|
||||||
|
| Versioning | `version` INT sur Composant, Pièce, Produit (incrémenté à chaque sync) |
|
||||||
|
| Machines | Aucun changement — elles lisent les slots des composants, la sync met à jour ces slots |
|
||||||
|
| Matching slots | Par `typeXxxId` + `position` (pas de FK vers skeleton requirement) |
|
||||||
|
| Matching custom fields | Par `orderIndex` (propriété stable sur `CustomField`) |
|
||||||
|
| Atomicité PATCH + sync | Wrappé dans une transaction DB côté controller |
|
||||||
|
| Idempotence sync | `execute()` est idempotent — un double appel est un no-op |
|
||||||
|
| Audit | Les opérations de sync sont capturées par les subscribers `onFlush` existants |
|
||||||
|
|
||||||
|
## Endpoints API
|
||||||
|
|
||||||
|
### `POST /api/model_types/{id}/sync-preview`
|
||||||
|
|
||||||
|
Calcule l'impact du diff entre le payload envoyé et l'état actuel des items liés. **Ne persiste rien.**
|
||||||
|
|
||||||
|
**Sécurité :** `ROLE_GESTIONNAIRE`
|
||||||
|
|
||||||
|
**Request body :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"structure": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le payload `structure` a le même format que celui envoyé au `PATCH /api/model_types/{id}`.
|
||||||
|
|
||||||
|
**Response :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"modelTypeId": "cl...",
|
||||||
|
"category": "COMPONENT",
|
||||||
|
"itemCount": 12,
|
||||||
|
"additions": {
|
||||||
|
"pieceSlots": 12,
|
||||||
|
"productSlots": 0,
|
||||||
|
"subcomponentSlots": 24,
|
||||||
|
"customFieldValues": 36
|
||||||
|
},
|
||||||
|
"deletions": {
|
||||||
|
"pieceSlots": 0,
|
||||||
|
"productSlots": 12,
|
||||||
|
"subcomponentSlots": 0,
|
||||||
|
"customFieldValues": 0
|
||||||
|
},
|
||||||
|
"modifications": {
|
||||||
|
"customFieldTypeChanges": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Si `additions`, `deletions` et `modifications` sont tous à 0, le frontend skip la modal et sauvegarde directement.
|
||||||
|
|
||||||
|
**Erreurs :**
|
||||||
|
- `404` — ModelType introuvable
|
||||||
|
- `403` — droits insuffisants
|
||||||
|
|
||||||
|
### `POST /api/model_types/{id}/sync`
|
||||||
|
|
||||||
|
Exécute la propagation. Appelé **après** le `PATCH` du ModelType, dans la même requête frontend (PATCH + sync enchaînés).
|
||||||
|
|
||||||
|
**Sécurité :** `ROLE_GESTIONNAIRE`
|
||||||
|
|
||||||
|
**Request body :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"confirmDeletions": true,
|
||||||
|
"confirmTypeChanges": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Si des suppressions sont nécessaires mais `confirmDeletions` est `false`, le sync **skip les suppressions** (applique uniquement les ajouts). Idem pour `confirmTypeChanges` et les clear de valeurs. Cela permet un sync partiel (ajouts only) sans confirmation.
|
||||||
|
|
||||||
|
**Response :** `200` avec résumé de l'exécution.
|
||||||
|
|
||||||
|
**Erreurs :**
|
||||||
|
- `404` — ModelType introuvable
|
||||||
|
- `403` — droits insuffisants
|
||||||
|
|
||||||
|
## Architecture Backend
|
||||||
|
|
||||||
|
### Strategy Pattern
|
||||||
|
|
||||||
|
```
|
||||||
|
Service/
|
||||||
|
├── ModelTypeSyncService.php # Orchestrateur
|
||||||
|
└── Sync/
|
||||||
|
├── SyncStrategyInterface.php # Interface
|
||||||
|
├── ComposantSyncStrategy.php # Slots pièce/produit/sous-composant + custom fields
|
||||||
|
├── PieceSyncStrategy.php # Slots produit + custom fields
|
||||||
|
└── ProductSyncStrategy.php # Custom fields uniquement
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interface
|
||||||
|
|
||||||
|
```php
|
||||||
|
interface SyncStrategyInterface
|
||||||
|
{
|
||||||
|
public function supports(ModelType $modelType): bool;
|
||||||
|
public function preview(ModelType $modelType, array $newStructure): SyncPreviewResult;
|
||||||
|
public function execute(ModelType $modelType, SyncConfirmation $confirmation): SyncExecutionResult;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note sur `execute()` :** Cette méthode est appelée **après** le PATCH du ModelType, donc les skeleton requirements sont déjà mis à jour en base. La strategy compare les skeleton requirements actuels (fraîchement mis à jour) avec les slots existants des items liés. Pas besoin de recevoir `$newStructure` — les relations ORM reflètent déjà le nouvel état.
|
||||||
|
|
||||||
|
### Orchestrateur
|
||||||
|
|
||||||
|
```php
|
||||||
|
class ModelTypeSyncService
|
||||||
|
{
|
||||||
|
/** @param iterable<SyncStrategyInterface> $strategies */
|
||||||
|
public function __construct(private iterable $strategies) {}
|
||||||
|
|
||||||
|
public function preview(ModelType $modelType, array $newStructure): SyncPreviewResult
|
||||||
|
{
|
||||||
|
foreach ($this->strategies as $strategy) {
|
||||||
|
if ($strategy->supports($modelType)) {
|
||||||
|
return $strategy->preview($modelType, $newStructure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new \LogicException('No strategy found for category: ' . $modelType->getCategory()->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(ModelType $modelType, SyncConfirmation $confirmation): SyncExecutionResult
|
||||||
|
{
|
||||||
|
foreach ($this->strategies as $strategy) {
|
||||||
|
if ($strategy->supports($modelType)) {
|
||||||
|
return $strategy->execute($modelType, $confirmation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new \LogicException('No strategy found for category: ' . $modelType->getCategory()->value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Les strategies sont auto-injectées via `#[AutoconfigureTag('app.sync_strategy')]` et le tagged iterator de Symfony.
|
||||||
|
|
||||||
|
### DTOs
|
||||||
|
|
||||||
|
```php
|
||||||
|
class SyncPreviewResult
|
||||||
|
{
|
||||||
|
public string $modelTypeId;
|
||||||
|
public string $category;
|
||||||
|
public int $itemCount;
|
||||||
|
public array $additions; // ['pieceSlots' => int, 'productSlots' => int, ...]
|
||||||
|
public array $deletions;
|
||||||
|
public array $modifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SyncConfirmation
|
||||||
|
{
|
||||||
|
public bool $confirmDeletions = false;
|
||||||
|
public bool $confirmTypeChanges = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SyncExecutionResult
|
||||||
|
{
|
||||||
|
public int $itemsUpdated;
|
||||||
|
public array $additions;
|
||||||
|
public array $deletions;
|
||||||
|
public array $modifications;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Controller
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[Route('/api/model_types/{id}')]
|
||||||
|
class ModelTypeSyncController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/sync-preview', methods: ['POST'])]
|
||||||
|
#[IsGranted('ROLE_GESTIONNAIRE')]
|
||||||
|
public function preview(ModelType $modelType, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$structure = json_decode($request->getContent(), true)['structure'] ?? [];
|
||||||
|
$result = $this->syncService->preview($modelType, $structure);
|
||||||
|
return $this->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/sync', methods: ['POST'])]
|
||||||
|
#[IsGranted('ROLE_GESTIONNAIRE')]
|
||||||
|
public function sync(ModelType $modelType, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$body = json_decode($request->getContent(), true);
|
||||||
|
$confirmation = new SyncConfirmation();
|
||||||
|
$confirmation->confirmDeletions = $body['confirmDeletions'] ?? false;
|
||||||
|
$confirmation->confirmTypeChanges = $body['confirmTypeChanges'] ?? false;
|
||||||
|
$result = $this->syncService->execute($modelType, $confirmation);
|
||||||
|
return $this->json($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Atomicité PATCH + Sync
|
||||||
|
|
||||||
|
Le frontend enchaîne `PATCH` puis `POST /sync` en deux requêtes HTTP. Le `POST /sync` wrappe toute l'opération dans une transaction DB (`$em->wrapInTransaction()`). Si le sync échoue, les modifications de slots sont rollback. Le PATCH du ModelType (skeleton requirements) reste committée — c'est acceptable car un re-sync est toujours possible.
|
||||||
|
|
||||||
|
En cas d'échec réseau entre le PATCH et le sync, le ModelType est à jour mais les items ne sont pas synchronisés. Le prochain save de la catégorie reproposera le sync-preview, qui détectera les différences.
|
||||||
|
|
||||||
|
## Logique de Diff / Sync
|
||||||
|
|
||||||
|
### Matching des slots
|
||||||
|
|
||||||
|
Pour chaque item lié, on compare ses slots actuels avec les skeleton requirements du ModelType. Le matching se fait par **`typeXxxId`** (le type référencé : `typePieceId`, `typeProductId`, `typeComposantId`) + **`position`**.
|
||||||
|
|
||||||
|
Il n'y a **pas de FK directe** entre un slot et un skeleton requirement. Le lien est implicite via le type + position.
|
||||||
|
|
||||||
|
**Pour le preview :** la strategy parse le `$newStructure` (payload JSON) et le compare aux slots actuels sans toucher à la DB.
|
||||||
|
|
||||||
|
**Pour l'execute :** la strategy lit les skeleton requirements actuels (déjà mis à jour par le PATCH) et les compare aux slots actuels.
|
||||||
|
|
||||||
|
### Règles — Slots (pièce, produit, sous-composant)
|
||||||
|
|
||||||
|
| Cas | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| Skeleton requirement existe, pas de slot correspondant | **Ajouter** slot vide (type + position, `quantity = 1` pour pièces, pas de sélection) |
|
||||||
|
| Slot existe, plus de skeleton requirement | **Supprimer** le slot (si `confirmDeletions`) — sélection perdue |
|
||||||
|
| Les deux existent, position différente | **Mettre à jour** la position du slot |
|
||||||
|
| Slot existe et matche | **Ne rien toucher** — sélection et quantité préservées |
|
||||||
|
|
||||||
|
### Règles — Custom fields
|
||||||
|
|
||||||
|
| Cas | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| Nouveau custom field | **Créer** `CustomFieldValue` vides pour tous les items |
|
||||||
|
| Custom field supprimé | **Supprimer** les `CustomFieldValue` (si `confirmDeletions`) |
|
||||||
|
| Renommé (même `orderIndex`, nom différent) | **Propagation auto** — label dans la définition, valeurs intactes |
|
||||||
|
| Type changé (même `orderIndex`, type différent) | **Clear** les valeurs (si `confirmTypeChanges`) — `CustomFieldValue` conservée, `value` vidée |
|
||||||
|
|
||||||
|
Le matching des custom fields se fait par **`orderIndex`** (propriété stable sur l'entité `CustomField`), pas par index de tableau. Cela évite les faux positifs lors de réordonnancement.
|
||||||
|
|
||||||
|
## Nouvelle Entité — `PieceProductSlot`
|
||||||
|
|
||||||
|
### Contexte — Remplacement de la M2M `piece_products`
|
||||||
|
|
||||||
|
Actuellement, les produits liés aux pièces passent par une relation M2M (`piece_products`, colonnes `a`/`b`). Cette table n'a pas de notion de `position`, `typeProductId`, ou `familyCode`.
|
||||||
|
|
||||||
|
`PieceProductSlot` **remplace** cette M2M pour uniformiser l'architecture avec les slots des Composants. La M2M existante sera conservée temporairement puis supprimée dans une migration future.
|
||||||
|
|
||||||
|
### Table `piece_product_slots`
|
||||||
|
|
||||||
|
| Colonne | Type | Contrainte |
|
||||||
|
|---------|------|------------|
|
||||||
|
| `id` | VARCHAR (CUID) | PK |
|
||||||
|
| `pieceid` | VARCHAR | FK → `pieces.id` CASCADE |
|
||||||
|
| `typeproductid` | VARCHAR | FK → `model_types.id` SET NULL, nullable |
|
||||||
|
| `selectedproductid` | VARCHAR | FK → `products.id` SET NULL, nullable |
|
||||||
|
| `familycode` | VARCHAR(255) | nullable |
|
||||||
|
| `position` | INT | NOT NULL |
|
||||||
|
| `createdat` | TIMESTAMP | NOT NULL |
|
||||||
|
| `updatedat` | TIMESTAMP | NOT NULL |
|
||||||
|
|
||||||
|
### Entité PHP
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'piece_product_slots')]
|
||||||
|
#[ORM\HasLifecycleCallbacks]
|
||||||
|
class PieceProductSlot
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: 'string')]
|
||||||
|
private string $id;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'productSlots')]
|
||||||
|
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||||
|
private Piece $piece;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: ModelType::class)]
|
||||||
|
#[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||||
|
private ?ModelType $typeProduct = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Product::class)]
|
||||||
|
#[ORM\JoinColumn(name: 'selectedProductId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||||
|
private ?Product $selectedProduct = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
||||||
|
private ?string $familyCode = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
private int $position = 0;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||||
|
private DateTimeImmutable $updatedAt;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relation sur Piece
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[ORM\OneToMany(targetEntity: PieceProductSlot::class, mappedBy: 'piece', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||||
|
#[ORM\OrderBy(['position' => 'ASC'])]
|
||||||
|
private Collection $productSlots;
|
||||||
|
```
|
||||||
|
|
||||||
|
La relation M2M `$products` existante sur `Piece` sera marquée deprecated puis supprimée dans une migration future.
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
|
||||||
|
1. Créer la table `piece_product_slots`
|
||||||
|
2. Migrer les données existantes de `piece_products` (M2M) → chaque entrée devient un `PieceProductSlot` avec `selectedProductId` renseigné, `typeProductId` déduit du produit sélectionné (`product.typeProduct`), `position` auto-incrémentée
|
||||||
|
3. Conserver `piece_products` temporairement (suppression dans une migration future)
|
||||||
|
4. Mettre à jour le virtual getter `getStructure()` de Piece pour lire les `productSlots`
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
### Nouveau champ sur Composant, Piece, Product
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[ORM\Column(type: 'integer', options: ['default' => 1])]
|
||||||
|
#[Groups(['composant:read'])] // idem pour piece:read, product:read
|
||||||
|
private int $version = 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comportement
|
||||||
|
|
||||||
|
- **Création** d'un item → `version = 1`
|
||||||
|
- **Sync** qui modifie les slots ou custom fields d'un item → `version += 1`
|
||||||
|
- Si la sync n'a aucun impact sur un item particulier (ses slots matchent déjà le skeleton), sa version ne change pas
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE composants ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 1;
|
||||||
|
ALTER TABLE pieces ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 1;
|
||||||
|
ALTER TABLE products ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
### Suppression du `restrictedMode`
|
||||||
|
|
||||||
|
**Fichiers à supprimer :**
|
||||||
|
- `composables/useCategoryEditGuard.ts`
|
||||||
|
- `tests/composables/useCategoryEditGuard.test.ts`
|
||||||
|
|
||||||
|
**Fichiers à modifier (retirer restrictedMode) :**
|
||||||
|
- `pages/component-category/[id]/edit.vue`
|
||||||
|
- `pages/piece-category/[id]/edit.vue`
|
||||||
|
- `pages/product-category/[id]/edit.vue`
|
||||||
|
- `components/StructureNodeEditor.vue`
|
||||||
|
- `components/PieceModelStructureEditor.vue`
|
||||||
|
- `components/ComponentModelStructureEditor.vue`
|
||||||
|
- `components/model-types/ModelTypeForm.vue`
|
||||||
|
- `composables/useStructureNodeCrud.ts`
|
||||||
|
- `composables/useStructureNodeLogic.ts`
|
||||||
|
- `composables/usePieceStructureEditorLogic.ts`
|
||||||
|
- `tests/components/ModelTypeForm.test.ts` (si existant)
|
||||||
|
- `tests/components/PieceModelStructureEditor.test.ts`
|
||||||
|
|
||||||
|
### Nouveau composant — `SyncConfirmationModal.vue`
|
||||||
|
|
||||||
|
Modal DaisyUI qui reçoit un `SyncPreviewResult` et affiche :
|
||||||
|
|
||||||
|
```
|
||||||
|
Cette modification impacte X [composants|pièces|produits] :
|
||||||
|
|
||||||
|
Ajouts :
|
||||||
|
• Y slots pièce à créer
|
||||||
|
• Z valeurs de champs personnalisés à initialiser
|
||||||
|
|
||||||
|
Suppressions :
|
||||||
|
• W slots produit à supprimer (les sélections seront perdues)
|
||||||
|
|
||||||
|
Modifications :
|
||||||
|
• V valeurs de champs à réinitialiser (changement de type)
|
||||||
|
|
||||||
|
[Annuler] [Confirmer la synchronisation]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nouveau service — `modelTypes.ts`
|
||||||
|
|
||||||
|
Ajout de deux fonctions au service existant :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function syncPreview(id: string, structure: any) {
|
||||||
|
return requestFetch(`/api/model_types/${id}/sync-preview`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ structure }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncExecute(id: string, confirmation: { confirmDeletions: boolean, confirmTypeChanges: boolean }) {
|
||||||
|
return requestFetch(`/api/model_types/${id}/sync`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(confirmation),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flow dans les pages d'édition de catégorie
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const handleSubmit = async (payload) => {
|
||||||
|
// 1. Preview (avant le save)
|
||||||
|
const preview = await syncPreview(id, payload.structure)
|
||||||
|
|
||||||
|
const hasImpact = preview.itemCount > 0 && (
|
||||||
|
Object.values(preview.additions).some(v => v > 0) ||
|
||||||
|
Object.values(preview.deletions).some(v => v > 0) ||
|
||||||
|
Object.values(preview.modifications).some(v => v > 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2. Si impact, demander confirmation
|
||||||
|
if (hasImpact) {
|
||||||
|
pendingPayload.value = payload
|
||||||
|
syncPreviewData.value = preview
|
||||||
|
showSyncModal.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Pas d'impact → save direct (PATCH + sync)
|
||||||
|
await saveAndSync(payload, { confirmDeletions: false, confirmTypeChanges: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSyncConfirmed = async () => {
|
||||||
|
const preview = syncPreviewData.value
|
||||||
|
const needsDeleteConfirm = Object.values(preview.deletions).some(v => v > 0)
|
||||||
|
const needsTypeChangeConfirm = preview.modifications.customFieldTypeChanges > 0
|
||||||
|
|
||||||
|
await saveAndSync(pendingPayload.value, {
|
||||||
|
confirmDeletions: needsDeleteConfirm,
|
||||||
|
confirmTypeChanges: needsTypeChangeConfirm,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveAndSync = async (payload, confirmation) => {
|
||||||
|
await updateModelType(id, payload)
|
||||||
|
await syncExecute(id, confirmation)
|
||||||
|
showSuccess('Catégorie mise à jour et synchronisée.')
|
||||||
|
router.push('/...')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Non-régression
|
||||||
|
|
||||||
|
### Machines
|
||||||
|
|
||||||
|
- Le `MachineStructureController` lit les slots des composants. La sync modifie ces slots → les machines affichent automatiquement la dernière version au prochain chargement.
|
||||||
|
- Aucun changement dans le controller machine.
|
||||||
|
|
||||||
|
### Quantités
|
||||||
|
|
||||||
|
- La sync **ne touche jamais** aux slots qui matchent toujours un skeleton requirement. Les quantités (`ComposantPieceSlot.quantity`) et sélections existantes sont préservées.
|
||||||
|
- Les nouveaux slots ajoutés par la sync ont `quantity = 1` par défaut.
|
||||||
|
- Le `ComposantPieceSlotController` (PATCH quantity) reste inchangé.
|
||||||
|
|
||||||
|
### `PieceProductSlot` — pas de quantité
|
||||||
|
|
||||||
|
Cohérent avec `ComposantProductSlot` qui n'a pas de quantité non plus.
|
||||||
|
|
||||||
|
### Relation M2M `piece_products`
|
||||||
|
|
||||||
|
La M2M existante reste en base pendant la période de transition. Le code frontend/backend qui la lit devra être migré vers les `productSlots`. La M2M sera supprimée dans une migration future une fois que tout le code utilise les slots.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
### Backend — PHPUnit
|
||||||
|
|
||||||
|
- `ModelTypeSyncControllerTest` — tests des endpoints preview et sync (y compris erreurs 403/404, confirmations partielles)
|
||||||
|
- `ComposantSyncStrategyTest` — logique de diff pour composants (ajout, suppression, position update, no-op)
|
||||||
|
- `PieceSyncStrategyTest` — logique de diff pour pièces (ajout/suppression de product slots)
|
||||||
|
- `ProductSyncStrategyTest` — logique de diff pour produits (custom fields only)
|
||||||
|
- `PieceProductSlotTest` — CRUD de la nouvelle entité
|
||||||
|
- Idempotence : vérifier qu'un double appel à `sync` est un no-op
|
||||||
|
- Vérifier la non-régression : `MachineStructureControllerTest` existant doit passer sans modification
|
||||||
|
|
||||||
|
### Frontend — Tests
|
||||||
|
|
||||||
|
- Supprimer `tests/composables/useCategoryEditGuard.test.ts`
|
||||||
|
- Mettre à jour `tests/components/PieceModelStructureEditor.test.ts` (retirer restrictedMode)
|
||||||
|
- Ajouter tests pour le flow sync dans les pages d'édition (preview → modal → confirm → save)
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
Pour le volume attendu (dizaines d'items par catégorie, pas de milliers), la sync en PHP avec l'ORM Doctrine est suffisante. Si le volume augmente significativement, les opérations de création/suppression de slots pourront être converties en batch SQL (INSERT ... SELECT, DELETE ... WHERE) sans changer l'architecture (la strategy encapsule la logique).
|
||||||
669
docs/superpowers/specs/2026-03-16-mcp-server-design.md
Normal file
669
docs/superpowers/specs/2026-03-16-mcp-server-design.md
Normal file
@@ -0,0 +1,669 @@
|
|||||||
|
# MCP Server — Inventory Project — Design Spec
|
||||||
|
|
||||||
|
**Date :** 2026-03-16
|
||||||
|
**Version projet :** 1.9.1
|
||||||
|
**Statut :** Draft (post-review v2)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Objectif
|
||||||
|
|
||||||
|
Exposer l'intégralité de l'API Inventory (machines, pièces, composants, produits, sites, constructeurs, custom fields, documents, commentaires, audit) via un serveur MCP (Model Context Protocol) intégré directement dans l'application Symfony.
|
||||||
|
|
||||||
|
Le serveur doit être compatible avec tous les clients MCP majeurs : Claude Code, Claude Desktop, ChatGPT Desktop, Codex, et tout client supportant le protocole MCP.
|
||||||
|
|
||||||
|
## 2. Contraintes
|
||||||
|
|
||||||
|
| Contrainte | Détail |
|
||||||
|
|---|---|
|
||||||
|
| **Réseau** | Machine hébergée sur un réseau fermé d'entreprise. Les clients distants (Claude Desktop, ChatGPT, Codex) accèdent via un tunnel chiffré (Cloudflare/WireGuard/SSH) |
|
||||||
|
| **Auth** | Pass-through : chaque client fournit ses propres credentials (profileId + password). Le serveur MCP charge le profil correspondant et applique ses rôles. Les actions sont traçables par utilisateur dans l'audit log |
|
||||||
|
| **Transport** | Dual : stdio pour usage local (Claude Code sur la même machine) + HTTP Streamable/SSE pour clients distants via tunnel |
|
||||||
|
| **Stack** | PHP / Symfony 8.0 — le serveur MCP vit dans l'application existante, pas de service séparé |
|
||||||
|
| **Scope** | Lecture + écriture complète — les outils couvrent tout le CRUD + les opérations métier |
|
||||||
|
|
||||||
|
## 3. Stack technique
|
||||||
|
|
||||||
|
| Composant | Choix |
|
||||||
|
|---|---|
|
||||||
|
| SDK MCP | `symfony/mcp-bundle` v0.6.0 + `mcp/sdk` ^0.4 (officiel Symfony + PHP Foundation + Anthropic) |
|
||||||
|
| Transport stdio | `bin/console mcp:server` (dans le container Docker) |
|
||||||
|
| Transport HTTP | Endpoint `/_mcp` sur le même port que l'API (8081) |
|
||||||
|
| Auth HTTP | Custom Symfony Authenticator (`McpHeaderAuthenticator`) intégré au firewall Symfony |
|
||||||
|
| Auth stdio | Token synthétique chargé depuis `$_ENV` au boot |
|
||||||
|
| Rate limiting | `symfony/rate-limiter` sur les tentatives d'auth échouées |
|
||||||
|
| Accès données | Repositories Doctrine directs (pas de hop HTTP interne) |
|
||||||
|
|
||||||
|
**Note :** Le bundle est expérimental et non couvert par la BC Promise de Symfony. L'implémentation inclut un spike/PoC initial (étape 1 du plan) pour valider la compatibilité de l'API réelle du bundle avec ce design.
|
||||||
|
|
||||||
|
## 4. Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ Docker Compose (réseau fermé entreprise) │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────┐ │
|
||||||
|
│ │ php-inventory-apache (Symfony 8) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ /api/* ← API REST existante │ │
|
||||||
|
│ │ /_mcp ← Endpoint MCP HTTP (SSE) │ │
|
||||||
|
│ │ bin/console mcp:server ← Transport stdio │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Firewall Symfony : │ │
|
||||||
|
│ │ ^/api → SessionProfileAuthenticator │ │
|
||||||
|
│ │ ^/_mcp → McpHeaderAuthenticator │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ src/Mcp/Tool/ ← Tools MCP │ │
|
||||||
|
│ │ src/Mcp/Resource/ ← Resources MCP │ │
|
||||||
|
│ │ src/Mcp/Security/ ← Authenticator + Guard │ │
|
||||||
|
│ └──────────┬───────────────────────────────────┘ │
|
||||||
|
│ │ réseau Docker interne │
|
||||||
|
│ ┌──────────▼──────────┐ │
|
||||||
|
│ │ PostgreSQL 16 │ │
|
||||||
|
│ └─────────────────────┘ │
|
||||||
|
└──────────────────┬──────────────────────────────────┘
|
||||||
|
│ tunnel (chiffré)
|
||||||
|
┌──────────────▼──────────────────┐
|
||||||
|
│ Postes utilisateurs │
|
||||||
|
│ - Claude Desktop → HTTP/SSE │
|
||||||
|
│ - ChatGPT Desktop → HTTP/SSE │
|
||||||
|
│ - Codex → HTTP/SSE │
|
||||||
|
│ - Claude Code local → stdio │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Le serveur MCP accède directement aux repositories Doctrine et aux services Symfony existants. Pas de double sérialisation — les tools appellent les mêmes repositories/services que les controllers REST.
|
||||||
|
|
||||||
|
## 5. Authentification pass-through
|
||||||
|
|
||||||
|
### 5.1 Firewall Symfony — intégration sécurité
|
||||||
|
|
||||||
|
Un firewall dédié pour `/_mcp` avec un authenticator custom. Cela garantit que `$security->getUser()` retourne le bon Profile, que la hiérarchie des rôles fonctionne via `is_granted()`, et que l'audit log trace le bon acteur.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# config/packages/security.yaml (ajout)
|
||||||
|
security:
|
||||||
|
firewalls:
|
||||||
|
mcp:
|
||||||
|
pattern: ^/_mcp
|
||||||
|
stateless: true
|
||||||
|
custom_authenticators:
|
||||||
|
- App\Mcp\Security\McpHeaderAuthenticator
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `McpHeaderAuthenticator` implémente `AuthenticatorInterface` :
|
||||||
|
1. Extrait `X-Profile-Id` et `X-Profile-Password` des headers
|
||||||
|
2. Charge le profil via `ProfileRepository`
|
||||||
|
3. Vérifie le password hash via `UserPasswordHasherInterface`
|
||||||
|
4. Retourne un `Passport` avec le Profile comme User
|
||||||
|
5. Symfony gère le reste (token, rôles, hiérarchie)
|
||||||
|
|
||||||
|
Cela permet à `AbstractAuditSubscriber.resolveActorProfileId()` de résoudre l'acteur via `$security->getUser()` sans aucune modification du code existant.
|
||||||
|
|
||||||
|
### 5.2 Transport stdio — token synthétique
|
||||||
|
|
||||||
|
Pour le transport stdio (pas de requête HTTP), un `EventSubscriber` sur `console.command` (quand la commande est `mcp:server`) :
|
||||||
|
1. Lit `MCP_PROFILE_ID` et `MCP_PROFILE_PASSWORD` depuis `$_ENV`
|
||||||
|
2. Valide les credentials
|
||||||
|
3. Injecte un `UsernamePasswordToken` synthétique dans le `TokenStorage` avec le Profile
|
||||||
|
|
||||||
|
### 5.3 Rate limiting — protection brute-force
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# config/packages/rate_limiter.yaml
|
||||||
|
framework:
|
||||||
|
rate_limiter:
|
||||||
|
mcp_auth:
|
||||||
|
policy: sliding_window
|
||||||
|
limit: 5
|
||||||
|
interval: '1 minute'
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `McpHeaderAuthenticator` consomme le rate limiter sur chaque tentative échouée (clé = IP). Après 5 échecs en 1 minute, toute tentative est rejetée avec une erreur MCP `429 Too Many Requests`.
|
||||||
|
|
||||||
|
### 5.4 Vérification des rôles
|
||||||
|
|
||||||
|
Chaque tool déclare un rôle minimum. L'authenticator Symfony gère la hiérarchie :
|
||||||
|
|
||||||
|
| Rôle | Droits MCP |
|
||||||
|
|---|---|
|
||||||
|
| `ROLE_VIEWER` | Tous les tools de lecture (list, get, search, history) |
|
||||||
|
| `ROLE_GESTIONNAIRE` | Lecture + écriture (create, update, delete, slots, clone) |
|
||||||
|
| `ROLE_ADMIN` | Tout + gestion profils |
|
||||||
|
|
||||||
|
Les tools utilisent `$this->security->isGranted('ROLE_XXX')` pour vérifier, bénéficiant de la hiérarchie Symfony standard.
|
||||||
|
|
||||||
|
## 6. Catalogue des Tools MCP
|
||||||
|
|
||||||
|
### 6.1 Tools de haut niveau (métier)
|
||||||
|
|
||||||
|
| Tool | Description | Paramètres principaux | Rôle min |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `search_inventory` | Recherche globale dans toutes les entités (machines, pièces, composants, produits, sites, constructeurs) | `query: string`, `types?: string[]`, `limit?: int` | VIEWER |
|
||||||
|
| `get_machine_structure` | Hiérarchie complète d'une machine : composants, pièces, produits, custom fields, slots | `machineId: string` | VIEWER |
|
||||||
|
| `clone_machine` | Clone une machine avec sa structure complète | `machineId: string`, `name: string`, `siteId: string`, `reference?: string` | GESTIONNAIRE |
|
||||||
|
| `get_entity_history` | Historique d'audit d'une entité | `entityType: string`, `entityId: string` | VIEWER |
|
||||||
|
| `get_activity_log` | Journal d'activité global | `page?: int`, `limit?: int`, `entityType?: string`, `action?: string` | VIEWER |
|
||||||
|
| `get_dashboard_stats` | Compteurs globaux (machines, pièces, composants, produits, commentaires ouverts) | aucun | VIEWER |
|
||||||
|
| `sync_model_type` | Preview ou exécution de la synchronisation skeleton d'un ModelType | `modelTypeId: string`, `action: "preview"\|"sync"`, `structure?: object` | GESTIONNAIRE |
|
||||||
|
|
||||||
|
### 6.2 Tools CRUD — Machines
|
||||||
|
|
||||||
|
| Tool | Description | Rôle min |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_machines` | Lister les machines avec filtres (nom, référence, site) et pagination | VIEWER |
|
||||||
|
| `get_machine` | Détail d'une machine par ID | VIEWER |
|
||||||
|
| `create_machine` | Créer une machine (nom, référence, siteId, constructeurs) | GESTIONNAIRE |
|
||||||
|
| `update_machine` | Mise à jour partielle d'une machine | GESTIONNAIRE |
|
||||||
|
| `delete_machine` | Supprimer une machine | GESTIONNAIRE |
|
||||||
|
|
||||||
|
### 6.3 Tools CRUD — Composants
|
||||||
|
|
||||||
|
| Tool | Description | Rôle min |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_composants` | Lister les composants avec filtres et pagination | VIEWER |
|
||||||
|
| `get_composant` | Détail d'un composant par ID (incluant ses slots) | VIEWER |
|
||||||
|
| `create_composant` | Créer un composant (nom, référence, modelTypeId, constructeurs). Retourne l'ID + les slots vides auto-générés | GESTIONNAIRE |
|
||||||
|
| `update_composant` | Mise à jour partielle | GESTIONNAIRE |
|
||||||
|
| `delete_composant` | Supprimer un composant | GESTIONNAIRE |
|
||||||
|
|
||||||
|
### 6.4 Tools CRUD — Pièces
|
||||||
|
|
||||||
|
| Tool | Description | Rôle min |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_pieces` | Lister les pièces avec filtres et pagination | VIEWER |
|
||||||
|
| `get_piece` | Détail d'une pièce par ID (incluant ses product-slots) | VIEWER |
|
||||||
|
| `create_piece` | Créer une pièce (nom, référence, modelTypeId, constructeurs). Retourne l'ID + product-slots auto-générés | GESTIONNAIRE |
|
||||||
|
| `update_piece` | Mise à jour partielle | GESTIONNAIRE |
|
||||||
|
| `delete_piece` | Supprimer une pièce | GESTIONNAIRE |
|
||||||
|
|
||||||
|
### 6.5 Tools CRUD — Produits
|
||||||
|
|
||||||
|
| Tool | Description | Rôle min |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_products` | Lister les produits avec filtres et pagination | VIEWER |
|
||||||
|
| `get_product` | Détail d'un produit par ID | VIEWER |
|
||||||
|
| `create_product` | Créer un produit (nom, référence, modelTypeId, prix (string), constructeurs) | GESTIONNAIRE |
|
||||||
|
| `update_product` | Mise à jour partielle | GESTIONNAIRE |
|
||||||
|
| `delete_product` | Supprimer un produit | GESTIONNAIRE |
|
||||||
|
|
||||||
|
### 6.6 Tools CRUD — Sites
|
||||||
|
|
||||||
|
| Tool | Description | Rôle min |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_sites` | Lister les sites | VIEWER |
|
||||||
|
| `get_site` | Détail d'un site par ID | VIEWER |
|
||||||
|
| `create_site` | Créer un site | GESTIONNAIRE |
|
||||||
|
| `update_site` | Mise à jour partielle | GESTIONNAIRE |
|
||||||
|
| `delete_site` | Supprimer un site | GESTIONNAIRE |
|
||||||
|
|
||||||
|
### 6.7 Tools CRUD — Constructeurs
|
||||||
|
|
||||||
|
| Tool | Description | Rôle min |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_constructeurs` | Lister les constructeurs/fournisseurs | VIEWER |
|
||||||
|
| `get_constructeur` | Détail d'un constructeur par ID | VIEWER |
|
||||||
|
| `create_constructeur` | Créer un constructeur | GESTIONNAIRE |
|
||||||
|
| `update_constructeur` | Mise à jour partielle | GESTIONNAIRE |
|
||||||
|
| `delete_constructeur` | Supprimer un constructeur | GESTIONNAIRE |
|
||||||
|
|
||||||
|
### 6.8 Tools — Commentaires (splittés)
|
||||||
|
|
||||||
|
| Tool | Description | Rôle min |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_comments` | Lister les commentaires d'une entité | VIEWER |
|
||||||
|
| `create_comment` | Créer un commentaire sur une entité | VIEWER |
|
||||||
|
| `resolve_comment` | Marquer un commentaire comme résolu | GESTIONNAIRE |
|
||||||
|
| `get_unresolved_comments_count` | Nombre de commentaires non résolus | VIEWER |
|
||||||
|
|
||||||
|
### 6.9 Tools — Custom Fields (splittés)
|
||||||
|
|
||||||
|
| Tool | Description | Rôle min |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_custom_field_values` | Lister les custom field values d'une entité | VIEWER |
|
||||||
|
| `upsert_custom_field_values` | Créer ou mettre à jour des custom field values | GESTIONNAIRE |
|
||||||
|
| `delete_custom_field_value` | Supprimer une custom field value | GESTIONNAIRE |
|
||||||
|
|
||||||
|
### 6.10 Tools — Documents (splittés)
|
||||||
|
|
||||||
|
| Tool | Description | Rôle min |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_documents` | Lister les documents d'une entité | VIEWER |
|
||||||
|
| `delete_document` | Supprimer un document | GESTIONNAIRE |
|
||||||
|
|
||||||
|
> **Limitation connue :** L'upload de documents n'est pas supporté via MCP. Le protocole MCP échange du JSON — l'upload de fichiers binaires (multipart/form-data) n'est pas compatible. Les uploads doivent se faire via l'API REST `/api/documents` (POST multipart). Cette limitation pourra être réévaluée si le protocole MCP ajoute un support binaire.
|
||||||
|
|
||||||
|
### 6.11 Tools — Machine Links (splittés)
|
||||||
|
|
||||||
|
| Tool | Description | Rôle min |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_machine_links` | Lister les liens composant/pièce/produit d'une machine | VIEWER |
|
||||||
|
| `add_machine_links` | Ajouter des liens machine↔composant/pièce/produit | GESTIONNAIRE |
|
||||||
|
| `update_machine_link` | Modifier un lien (quantité, overrides) | GESTIONNAIRE |
|
||||||
|
| `remove_machine_link` | Supprimer un lien | GESTIONNAIRE |
|
||||||
|
|
||||||
|
### 6.12 Tools — Slots
|
||||||
|
|
||||||
|
| Tool | Description | Rôle min |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_slots` | Lister les slots d'un composant ou pièce avec état (rempli/vide, requirement). Paramètre `entityType: "composant"\|"piece"` + `entityId` | VIEWER |
|
||||||
|
| `update_slots` | Remplir un ou plusieurs slots. Paramètre `slots: [{slotId, selectedPieceId?\|selectedProductId?\|selectedComposantId?}]` | GESTIONNAIRE |
|
||||||
|
|
||||||
|
> **Note :** Un seul tool `list_slots` et un seul `update_slots` — ils acceptent un paramètre `entityType` pour dispatcher vers composant ou pièce. Un seul fichier d'implémentation par tool.
|
||||||
|
|
||||||
|
### 6.13 Tools — ModelTypes
|
||||||
|
|
||||||
|
| Tool | Description | Rôle min |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_model_types` | Lister les ModelTypes par catégorie avec skeleton requirements | VIEWER |
|
||||||
|
| `get_model_type` | Détail complet d'un ModelType (requirements + custom fields) | VIEWER |
|
||||||
|
| `create_model_type` | Créer un ModelType | GESTIONNAIRE |
|
||||||
|
| `update_model_type` | Modifier un ModelType | GESTIONNAIRE |
|
||||||
|
| `delete_model_type` | Supprimer un ModelType | GESTIONNAIRE |
|
||||||
|
|
||||||
|
**Total : ~55 tools** (splittés pour des schémas JSON non-ambigus, meilleure compatibilité LLM)
|
||||||
|
|
||||||
|
> **Note :** Les tools d'administration des profils (`list_profiles`, `create_profile`, etc.) ne sont pas inclus — la gestion des profils reste exclusivement via l'API REST `/api/admin/profiles` (ROLE_ADMIN). Cela évite d'exposer la gestion des comptes/mots de passe via MCP.
|
||||||
|
|
||||||
|
## 7. Resources MCP
|
||||||
|
|
||||||
|
| URI | Description | Contenu |
|
||||||
|
|---|---|---|
|
||||||
|
| `inventory://schema/entities` | Schéma de toutes les entités | Nom, champs (nom, type, nullable, description) pour chaque entité |
|
||||||
|
| `inventory://model-types/{category}` | ModelTypes par catégorie | Liste des ModelTypes avec leurs skeleton requirements et custom fields |
|
||||||
|
| `inventory://roles` | Hiérarchie des rôles | Rôles et permissions associées pour guider le LLM |
|
||||||
|
| `inventory://stats` | Statistiques globales | Compteurs de chaque entité, commentaires ouverts |
|
||||||
|
|
||||||
|
## 8. Workflows de création guidés
|
||||||
|
|
||||||
|
### 8.1 Créer un Composant complet
|
||||||
|
|
||||||
|
```
|
||||||
|
1. list_model_types(category: "composant")
|
||||||
|
→ Choisir le type de composant
|
||||||
|
|
||||||
|
2. get_model_type(modelTypeId)
|
||||||
|
→ Voir les skeleton requirements : pièces, produits, sous-composants attendus
|
||||||
|
→ Voir les custom fields de chaque requirement
|
||||||
|
|
||||||
|
3. create_composant(name, reference, modelTypeId, constructeurs)
|
||||||
|
→ Reçoit: { id, slots: [{slotId, type, requirementName}, ...] }
|
||||||
|
|
||||||
|
4. search_inventory(query: "Roulement", types: ["piece"])
|
||||||
|
→ Trouver les pièces candidates pour chaque slot
|
||||||
|
|
||||||
|
5. update_slots([{slotId, selectedPieceId}, {slotId, selectedProductId}, ...])
|
||||||
|
→ Remplir les slots
|
||||||
|
|
||||||
|
6. upsert_custom_field_values(entityType: "composant", entityId,
|
||||||
|
fields: [{name: "Tension", value: "220V"}, ...])
|
||||||
|
→ Remplir les custom fields
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Créer une Pièce complète
|
||||||
|
|
||||||
|
```
|
||||||
|
1. list_model_types(category: "piece")
|
||||||
|
2. get_model_type(modelTypeId)
|
||||||
|
3. create_piece(name, reference, modelTypeId, constructeurs)
|
||||||
|
→ Reçoit: { id, productSlots: [{slotId, requirementName}, ...] }
|
||||||
|
4. search_inventory(query: "...", types: ["product"])
|
||||||
|
5. update_slots([{slotId, selectedProductId}, ...])
|
||||||
|
6. upsert_custom_field_values(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 Créer un Produit
|
||||||
|
|
||||||
|
```
|
||||||
|
1. list_model_types(category: "product")
|
||||||
|
2. create_product(name, reference, modelTypeId, prix, constructeurs)
|
||||||
|
3. upsert_custom_field_values(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 Créer une Machine complète (de bas en haut)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Créer les produits nécessaires (§8.3)
|
||||||
|
2. Créer les pièces avec les produits dans les slots (§8.2)
|
||||||
|
3. Créer les composants avec les pièces dans les slots (§8.1)
|
||||||
|
4. list_sites → choisir le site
|
||||||
|
5. create_machine(name, reference, siteId, constructeurs)
|
||||||
|
6. add_machine_links(machineId, links: [
|
||||||
|
{type: "composant", entityId, quantity},
|
||||||
|
{type: "piece", entityId, quantity},
|
||||||
|
{type: "product", entityId}
|
||||||
|
])
|
||||||
|
7. upsert_custom_field_values(entityType: "machine", machineId, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Pagination
|
||||||
|
|
||||||
|
Toutes les tools `list_*` utilisent un contrat de pagination uniforme :
|
||||||
|
|
||||||
|
### Paramètres d'entrée
|
||||||
|
|
||||||
|
| Paramètre | Type | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `page` | int | 1 | Numéro de page (1-indexed) |
|
||||||
|
| `limit` | int | 30 | Nombre d'items par page (max 100) |
|
||||||
|
|
||||||
|
### Format de réponse
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [...],
|
||||||
|
"total": 142,
|
||||||
|
"page": 1,
|
||||||
|
"limit": 30,
|
||||||
|
"pageCount": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Format des erreurs
|
||||||
|
|
||||||
|
Toutes les erreurs MCP suivent un format uniforme via `isError: true` dans la réponse tool :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"isError": true,
|
||||||
|
"content": [{"type": "text", "text": "Permission denied: ROLE_GESTIONNAIRE required for create_machine"}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Catégories d'erreurs
|
||||||
|
|
||||||
|
| Code | Description | Exemple |
|
||||||
|
|---|---|---|
|
||||||
|
| `auth_error` | Credentials invalides ou manquants | "Authentication failed: invalid password" |
|
||||||
|
| `permission_denied` | Rôle insuffisant pour l'opération | "Permission denied: ROLE_GESTIONNAIRE required" |
|
||||||
|
| `not_found` | Entité introuvable | "Machine not found: cl4a8b..." |
|
||||||
|
| `validation_error` | Données invalides | "Validation failed: name is required" |
|
||||||
|
| `rate_limited` | Trop de tentatives d'auth échouées | "Rate limited: try again in 45 seconds" |
|
||||||
|
| `internal_error` | Erreur serveur inattendue | "Internal error: database connection failed" |
|
||||||
|
|
||||||
|
Le champ `text` inclut toujours la catégorie en préfixe pour que le LLM puisse adapter son comportement.
|
||||||
|
|
||||||
|
## 11. Configuration
|
||||||
|
|
||||||
|
### 11.1 Symfony — config/packages/mcp.yaml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
mcp:
|
||||||
|
app: 'inventory'
|
||||||
|
version: '%env(file:resolve:VERSION)%'
|
||||||
|
description: 'Inventory MCP Server - Gestion inventaire industriel (machines, pièces, composants, produits)'
|
||||||
|
instructions: |
|
||||||
|
Serveur MCP pour gérer un inventaire industriel.
|
||||||
|
Entités principales : Machine, Composant, Pièce, Produit, Site, Constructeur.
|
||||||
|
Utilisez search_inventory pour chercher dans toutes les entités.
|
||||||
|
Utilisez get_model_type pour comprendre la structure attendue avant de créer un composant ou une pièce.
|
||||||
|
Consultez la resource inventory://schema/entities pour voir le schéma complet.
|
||||||
|
Authentification requise : envoyez X-Profile-Id et X-Profile-Password dans les headers HTTP.
|
||||||
|
client_transports:
|
||||||
|
stdio: true
|
||||||
|
http: true
|
||||||
|
http:
|
||||||
|
path: /_mcp
|
||||||
|
session:
|
||||||
|
store: file
|
||||||
|
directory: '%kernel.cache_dir%/mcp-sessions'
|
||||||
|
ttl: 3600
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.2 Security — config/packages/security.yaml (ajout firewall)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
security:
|
||||||
|
firewalls:
|
||||||
|
# AVANT le firewall api existant
|
||||||
|
mcp:
|
||||||
|
pattern: ^/_mcp
|
||||||
|
stateless: true
|
||||||
|
custom_authenticators:
|
||||||
|
- App\Mcp\Security\McpHeaderAuthenticator
|
||||||
|
api:
|
||||||
|
pattern: ^/api
|
||||||
|
# ... existant ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.3 Rate Limiter — config/packages/rate_limiter.yaml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
framework:
|
||||||
|
rate_limiter:
|
||||||
|
mcp_auth:
|
||||||
|
policy: sliding_window
|
||||||
|
limit: 5
|
||||||
|
interval: '1 minute'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.4 Routes — config/routes.yaml (ajout)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
mcp:
|
||||||
|
resource: .
|
||||||
|
type: mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.5 Logging — config/packages/monolog.yaml (ajout)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
monolog:
|
||||||
|
channels: ['mcp']
|
||||||
|
handlers:
|
||||||
|
mcp:
|
||||||
|
type: rotating_file
|
||||||
|
path: '%kernel.logs_dir%/mcp.log'
|
||||||
|
level: info
|
||||||
|
channels: ['mcp']
|
||||||
|
max_files: 30
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. Configuration des clients
|
||||||
|
|
||||||
|
### 12.1 Claude Code (local, stdio via Docker)
|
||||||
|
|
||||||
|
Fichier `.mcp.json` à la racine du projet :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"inventory": {
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"exec", "-i",
|
||||||
|
"-e", "MCP_PROFILE_ID=<votre-profile-id>",
|
||||||
|
"-e", "MCP_PROFILE_PASSWORD=<votre-password>",
|
||||||
|
"php-inventory-apache",
|
||||||
|
"php", "bin/console", "mcp:server"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note :** Les env vars sont passées via les flags `-e` de `docker exec` car le bloc `env` de `.mcp.json` ne les injecte pas dans le container Docker. Si PHP et les dépendances Composer sont disponibles directement sur l'hôte (hors Docker), on peut utiliser `"command": "php", "args": ["bin/console", "mcp:server"]` avec un bloc `env` standard.
|
||||||
|
|
||||||
|
### 12.2 Claude Desktop (distant, HTTP via tunnel)
|
||||||
|
|
||||||
|
Fichier `claude_desktop_config.json` :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"inventory": {
|
||||||
|
"url": "https://inventory.company-tunnel.com/_mcp",
|
||||||
|
"headers": {
|
||||||
|
"X-Profile-Id": "<votre-profile-id>",
|
||||||
|
"X-Profile-Password": "<votre-password>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.3 ChatGPT Desktop (HTTP via tunnel)
|
||||||
|
|
||||||
|
Même principe HTTP : URL du tunnel + headers d'auth. Format de config selon la doc ChatGPT MCP.
|
||||||
|
|
||||||
|
### 12.4 Codex (HTTP via tunnel)
|
||||||
|
|
||||||
|
Même config HTTP que Claude Desktop.
|
||||||
|
|
||||||
|
## 13. Structure des fichiers
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
└── Mcp/
|
||||||
|
├── Tool/
|
||||||
|
│ ├── SearchInventoryTool.php # search_inventory
|
||||||
|
│ ├── DashboardStatsTool.php # get_dashboard_stats
|
||||||
|
│ ├── ActivityLogTool.php # get_activity_log
|
||||||
|
│ ├── EntityHistoryTool.php # get_entity_history
|
||||||
|
│ ├── Machine/
|
||||||
|
│ │ ├── ListMachinesTool.php # list_machines
|
||||||
|
│ │ ├── GetMachineTool.php # get_machine
|
||||||
|
│ │ ├── CreateMachineTool.php # create_machine
|
||||||
|
│ │ ├── UpdateMachineTool.php # update_machine
|
||||||
|
│ │ ├── DeleteMachineTool.php # delete_machine
|
||||||
|
│ │ ├── MachineStructureTool.php # get_machine_structure
|
||||||
|
│ │ ├── CloneMachineTool.php # clone_machine
|
||||||
|
│ │ ├── ListMachineLinksTool.php # list_machine_links
|
||||||
|
│ │ ├── AddMachineLinksTool.php # add_machine_links
|
||||||
|
│ │ ├── UpdateMachineLinkTool.php # update_machine_link
|
||||||
|
│ │ └── RemoveMachineLinkTool.php # remove_machine_link
|
||||||
|
│ ├── Composant/
|
||||||
|
│ │ ├── ListComposantsTool.php # list_composants
|
||||||
|
│ │ ├── GetComposantTool.php # get_composant
|
||||||
|
│ │ ├── CreateComposantTool.php # create_composant
|
||||||
|
│ │ ├── UpdateComposantTool.php # update_composant
|
||||||
|
│ │ └── DeleteComposantTool.php # delete_composant
|
||||||
|
│ ├── Piece/
|
||||||
|
│ │ ├── ListPiecesTool.php # list_pieces
|
||||||
|
│ │ ├── GetPieceTool.php # get_piece
|
||||||
|
│ │ ├── CreatePieceTool.php # create_piece
|
||||||
|
│ │ ├── UpdatePieceTool.php # update_piece
|
||||||
|
│ │ └── DeletePieceTool.php # delete_piece
|
||||||
|
│ ├── Slot/
|
||||||
|
│ │ ├── ListSlotsTool.php # list_slots (dispatche par entityType)
|
||||||
|
│ │ └── UpdateSlotsTool.php # update_slots
|
||||||
|
│ ├── Product/
|
||||||
|
│ │ ├── ListProductsTool.php # list_products
|
||||||
|
│ │ ├── GetProductTool.php # get_product
|
||||||
|
│ │ ├── CreateProductTool.php # create_product
|
||||||
|
│ │ ├── UpdateProductTool.php # update_product
|
||||||
|
│ │ └── DeleteProductTool.php # delete_product
|
||||||
|
│ ├── Site/
|
||||||
|
│ │ ├── ListSitesTool.php # list_sites
|
||||||
|
│ │ ├── GetSiteTool.php # get_site
|
||||||
|
│ │ ├── CreateSiteTool.php # create_site
|
||||||
|
│ │ ├── UpdateSiteTool.php # update_site
|
||||||
|
│ │ └── DeleteSiteTool.php # delete_site
|
||||||
|
│ ├── Constructeur/
|
||||||
|
│ │ ├── ListConstructeursTool.php # list_constructeurs
|
||||||
|
│ │ ├── GetConstructeurTool.php # get_constructeur
|
||||||
|
│ │ ├── CreateConstructeurTool.php # create_constructeur
|
||||||
|
│ │ ├── UpdateConstructeurTool.php # update_constructeur
|
||||||
|
│ │ └── DeleteConstructeurTool.php # delete_constructeur
|
||||||
|
│ ├── ModelType/
|
||||||
|
│ │ ├── ListModelTypesTool.php # list_model_types
|
||||||
|
│ │ ├── GetModelTypeTool.php # get_model_type
|
||||||
|
│ │ ├── CreateModelTypeTool.php # create_model_type
|
||||||
|
│ │ ├── UpdateModelTypeTool.php # update_model_type
|
||||||
|
│ │ ├── DeleteModelTypeTool.php # delete_model_type
|
||||||
|
│ │ └── SyncModelTypeTool.php # sync_model_type
|
||||||
|
│ ├── CustomField/
|
||||||
|
│ │ ├── ListCustomFieldValuesTool.php # list_custom_field_values
|
||||||
|
│ │ ├── UpsertCustomFieldValuesTool.php # upsert_custom_field_values
|
||||||
|
│ │ └── DeleteCustomFieldValueTool.php # delete_custom_field_value
|
||||||
|
│ ├── Document/
|
||||||
|
│ │ ├── ListDocumentsTool.php # list_documents
|
||||||
|
│ │ └── DeleteDocumentTool.php # delete_document
|
||||||
|
│ └── Comment/
|
||||||
|
│ ├── ListCommentsTool.php # list_comments
|
||||||
|
│ ├── CreateCommentTool.php # create_comment
|
||||||
|
│ ├── ResolveCommentTool.php # resolve_comment
|
||||||
|
│ └── UnresolvedCountTool.php # get_unresolved_comments_count
|
||||||
|
├── Resource/
|
||||||
|
│ ├── SchemaResource.php # inventory://schema/entities
|
||||||
|
│ ├── ModelTypesResource.php # inventory://model-types/{category}
|
||||||
|
│ ├── RolesResource.php # inventory://roles
|
||||||
|
│ └── StatsResource.php # inventory://stats
|
||||||
|
└── Security/
|
||||||
|
└── McpHeaderAuthenticator.php # Symfony Authenticator pour firewall MCP
|
||||||
|
|
||||||
|
docs/
|
||||||
|
└── mcp/
|
||||||
|
└── README.md # Guide utilisateur complet
|
||||||
|
```
|
||||||
|
|
||||||
|
## 14. Documentation utilisateur (docs/mcp/README.md)
|
||||||
|
|
||||||
|
Le guide contiendra :
|
||||||
|
|
||||||
|
1. **Introduction** — Qu'est-ce que le MCP Inventory, à quoi ça sert, quels clients sont supportés
|
||||||
|
2. **Prérequis** — Profil avec rôle suffisant, accès au tunnel, client MCP compatible
|
||||||
|
3. **Installation & configuration par client** — Exemples copier-coller pour :
|
||||||
|
- Claude Code (stdio via Docker)
|
||||||
|
- Claude Desktop (HTTP via tunnel)
|
||||||
|
- ChatGPT Desktop (HTTP via tunnel)
|
||||||
|
- Codex (HTTP via tunnel)
|
||||||
|
4. **Catalogue des tools** — Tableau complet avec nom, description, paramètres, rôle requis
|
||||||
|
5. **Workflows guidés** — Comment créer une machine, un composant, une pièce, un produit (étape par étape avec exemples d'appels)
|
||||||
|
6. **Resources disponibles** — URIs et contenu exposé
|
||||||
|
7. **Rôles & permissions** — Quel rôle permet quelles actions
|
||||||
|
8. **Format des erreurs** — Catégories et exemples
|
||||||
|
9. **Limitations connues** — Upload documents non supporté via MCP
|
||||||
|
10. **Troubleshooting** — Erreurs courantes (auth failed, tunnel down, rôle insuffisant, rate limited)
|
||||||
|
|
||||||
|
## 15. Sécurité
|
||||||
|
|
||||||
|
| Mesure | Détail |
|
||||||
|
|---|---|
|
||||||
|
| **Firewall Symfony** | `/_mcp` a son propre firewall avec `McpHeaderAuthenticator` — intégré au système de sécurité standard |
|
||||||
|
| **Vérification rôle** | Chaque tool vérifie via `$security->isGranted()` avec hiérarchie des rôles |
|
||||||
|
| **Audit trail** | `AbstractAuditSubscriber.resolveActorProfileId()` fonctionne nativement car `$security->getUser()` retourne le Profile authentifié |
|
||||||
|
| **Rate limiting** | 5 tentatives d'auth échouées par minute par IP → rejet |
|
||||||
|
| **Transport chiffré** | Le tunnel assure le chiffrement en transit pour les clients distants |
|
||||||
|
| **Pas de secrets dans le code** | Credentials dans env vars (stdio) ou headers (HTTP), jamais en dur |
|
||||||
|
| **Sessions MCP** | TTL 1h, stockage fichier, nettoyage automatique |
|
||||||
|
| **CORS** | Non nécessaire — les clients MCP sont des apps natives (pas des navigateurs). Le tunnel termine la connexion côté serveur. À réévaluer si un client browser-based apparaît |
|
||||||
|
|
||||||
|
## 16. Backward Compatibility
|
||||||
|
|
||||||
|
Les tools MCP suivent une politique additive :
|
||||||
|
- **Ajouts** : nouveaux tools, nouveaux paramètres optionnels → toujours OK
|
||||||
|
- **Suppressions** : marquer un tool comme deprecated pendant 1 version avant suppression
|
||||||
|
- **Breaking changes** : changer le type/nom d'un paramètre requis → bumper la version MCP
|
||||||
|
|
||||||
|
Le champ `version` dans la config MCP (lu depuis `VERSION`) signale les changements.
|
||||||
|
|
||||||
|
## 17. Dépendances à installer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require symfony/mcp-bundle symfony/rate-limiter
|
||||||
|
```
|
||||||
|
|
||||||
|
Le bundle tire `mcp/sdk` automatiquement.
|
||||||
|
|
||||||
|
## 18. Tests
|
||||||
|
|
||||||
|
Les tools MCP seront testés via :
|
||||||
|
|
||||||
|
- **Tests unitaires** : chaque tool testé avec des mocks de repositories, vérification des paramètres et des réponses
|
||||||
|
- **Tests d'intégration** : appels MCP stdio via `docker exec ... php bin/console mcp:server` avec des fixtures
|
||||||
|
- **Tests de sécurité** : vérification que les tools rejettent les appels sans auth, avec rôle insuffisant, et après rate limiting
|
||||||
|
- Pattern : hériter de `AbstractApiTestCase` pour réutiliser les factories existantes (`createProfile()`, `createMachine()`, etc.)
|
||||||
|
|
||||||
|
## 19. Spike / PoC initial
|
||||||
|
|
||||||
|
Avant l'implémentation complète, une étape de validation :
|
||||||
|
|
||||||
|
1. Installer `symfony/mcp-bundle` dans le projet
|
||||||
|
2. Créer un tool minimal (`get_dashboard_stats`) avec l'attribut `#[McpTool]`
|
||||||
|
3. Tester le transport stdio : `docker exec -i php-inventory-apache php bin/console mcp:server`
|
||||||
|
4. Tester le transport HTTP : appel POST sur `/_mcp`
|
||||||
|
5. Valider que l'authenticator custom fonctionne avec le firewall
|
||||||
|
6. Confirmer que `$security->getUser()` retourne le bon Profile dans un tool
|
||||||
|
|
||||||
|
Si le PoC révèle des incompatibilités avec l'API du bundle, adapter le design avant de continuer.
|
||||||
138
docs/superpowers/specs/2026-03-23-document-types-design.md
Normal file
138
docs/superpowers/specs/2026-03-23-document-types-design.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# Document Types — Design Spec
|
||||||
|
|
||||||
|
Date: 2026-03-23
|
||||||
|
Status: Approved
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Add a `type` field to documents so users can classify them (documentation, devis, facture, plan, photo, autre). Users can set the type at upload and change it afterward via a mini-modal.
|
||||||
|
|
||||||
|
## Enum Values
|
||||||
|
|
||||||
|
| Value | Label |
|
||||||
|
|-------|-------|
|
||||||
|
| `documentation` | Documentation |
|
||||||
|
| `devis` | Devis |
|
||||||
|
| `facture` | Facture |
|
||||||
|
| `plan` | Plan |
|
||||||
|
| `photo` | Photo |
|
||||||
|
| `autre` | Autre |
|
||||||
|
|
||||||
|
Default: `documentation`
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
### 1. PHP Enum
|
||||||
|
|
||||||
|
New file: `src/Enum/DocumentType.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
enum DocumentType: string
|
||||||
|
{
|
||||||
|
case DOCUMENTATION = 'documentation';
|
||||||
|
case DEVIS = 'devis';
|
||||||
|
case FACTURE = 'facture';
|
||||||
|
case PLAN = 'plan';
|
||||||
|
case PHOTO = 'photo';
|
||||||
|
case AUTRE = 'autre';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Entity Change — Document.php
|
||||||
|
|
||||||
|
Add column:
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 20, enumType: DocumentType::class)]
|
||||||
|
#[Groups(['document:list'])]
|
||||||
|
private DocumentType $type = DocumentType::DOCUMENTATION;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add getter/setter:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function getType(): DocumentType { ... }
|
||||||
|
public function setType(DocumentType $type): static { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. API Platform — PATCH operation
|
||||||
|
|
||||||
|
Add a `Patch` operation on Document (ROLE_GESTIONNAIRE) to allow updating `name` and `type`. The existing `Put` already exists but PATCH is more appropriate for partial updates.
|
||||||
|
|
||||||
|
### 4. DocumentUploadProcessor
|
||||||
|
|
||||||
|
Accept optional `type` field from FormData. Validate against enum values, default to `documentation` if absent.
|
||||||
|
|
||||||
|
### 5. Migration
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE documents ADD COLUMN type VARCHAR(20) NOT NULL DEFAULT 'documentation';
|
||||||
|
|
||||||
|
-- Classify existing documents by mimeType
|
||||||
|
UPDATE documents SET type = 'photo' WHERE mimetype LIKE 'image/%';
|
||||||
|
UPDATE documents SET type = 'autre'
|
||||||
|
WHERE type = 'documentation'
|
||||||
|
AND mimetype NOT LIKE 'application/pdf'
|
||||||
|
AND mimetype NOT LIKE 'image/%';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. DocumentQueryController
|
||||||
|
|
||||||
|
Add `type` to the response array in `formatDocument()`.
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
### 1. Type Constants
|
||||||
|
|
||||||
|
New file: `app/shared/documentTypes.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const DOCUMENT_TYPES = [
|
||||||
|
{ value: 'documentation', label: 'Documentation' },
|
||||||
|
{ value: 'devis', label: 'Devis' },
|
||||||
|
{ value: 'facture', label: 'Facture' },
|
||||||
|
{ value: 'plan', label: 'Plan' },
|
||||||
|
{ value: 'photo', label: 'Photo' },
|
||||||
|
{ value: 'autre', label: 'Autre' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type DocumentTypeValue = typeof DOCUMENT_TYPES[number]['value']
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. DocumentUpload.vue — Type select at upload
|
||||||
|
|
||||||
|
Add a select dropdown (default: `documentation`) in the upload zone. The selected type applies to all files in the current batch. Pass the type through to `uploadDocuments()`.
|
||||||
|
|
||||||
|
### 3. useDocuments composable
|
||||||
|
|
||||||
|
- `uploadDocuments()`: accept `type` in the upload context, append to FormData
|
||||||
|
- New method: `updateDocument(id, { name, type })` — PATCH `/api/documents/{id}` with `application/merge-patch+json`
|
||||||
|
- Add `type` to the `Document` interface
|
||||||
|
|
||||||
|
### 4. DocumentEditModal.vue (new component)
|
||||||
|
|
||||||
|
Mini-modal with:
|
||||||
|
- Input text: document name (pre-filled)
|
||||||
|
- Select: document type (pre-filled)
|
||||||
|
- Buttons: Annuler / Sauvegarder
|
||||||
|
- On save: call `updateDocument()`, emit `updated` event
|
||||||
|
|
||||||
|
### 5. Document list display
|
||||||
|
|
||||||
|
Everywhere documents are listed (machine detail, composant edit, piece edit, product, site):
|
||||||
|
- Show type as a small badge next to the document name
|
||||||
|
- Add a pencil/edit button that opens `DocumentEditModal`
|
||||||
|
- On modal save: refresh the document in local state
|
||||||
|
|
||||||
|
## Migration of existing data
|
||||||
|
|
||||||
|
All existing documents classified by mimeType:
|
||||||
|
- `image/*` → `photo`
|
||||||
|
- `application/pdf` → `documentation`
|
||||||
|
- Everything else → `autre`
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Custom user-defined types (table `document_types`) — can be added later
|
||||||
|
- Filtering documents by type in the UI — can be added later
|
||||||
|
- Bulk type change
|
||||||
88
docs/superpowers/specs/2026-03-23-parc-machines-ux-design.md
Normal file
88
docs/superpowers/specs/2026-03-23-parc-machines-ux-design.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Parc Machines — Améliorations UX
|
||||||
|
|
||||||
|
**Date** : 2026-03-23
|
||||||
|
**Scope** : 3 changements sur le frontend + 1 extension backend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Filtre sites multi-sélection par checkboxes
|
||||||
|
|
||||||
|
### Contexte
|
||||||
|
Le filtre site actuel est un `<select>` mono-sélection dans `machines/index.vue`.
|
||||||
|
L'utilisateur veut pouvoir sélectionner plusieurs sites simultanément.
|
||||||
|
|
||||||
|
### Design
|
||||||
|
- Remplacer le `<select>` par une rangée de checkboxes DaisyUI directement visibles dans la barre de filtre.
|
||||||
|
- Chaque site = une checkbox avec le nom du site.
|
||||||
|
- Quand **aucune** checkbox n'est cochée → toutes les machines s'affichent (équivalent "Tous les sites").
|
||||||
|
- Quand **une ou plusieurs** sont cochées → filtre sur ces sites uniquement.
|
||||||
|
|
||||||
|
### Changements techniques
|
||||||
|
**Fichier** : `Inventory_frontend/app/pages/machines/index.vue`
|
||||||
|
|
||||||
|
- **Réactivité** : utiliser `reactive(new Set())` (Vue 3.4+ supporte nativement les mutations `add`/`delete`/`has` sur un Set réactif). Pas de `.value` nécessaire.
|
||||||
|
- **Note** : le fichier utilise `<script setup>` sans `lang="ts"` — ne pas utiliser d'annotations TypeScript comme `Set<string>`.
|
||||||
|
- Template : remplacer le `<select>` par un `div` flex-wrap avec des checkboxes DaisyUI (`checkbox checkbox-sm`) + label pour chaque site.
|
||||||
|
- Computed `filteredMachines` : remplacer `machine.siteId === selectedSite` par `selectedSites.size === 0 || selectedSites.has(machine.siteId)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Tri alphabétique croissant
|
||||||
|
|
||||||
|
### Contexte
|
||||||
|
Les machines s'affichent dans l'ordre retourné par l'API, sans tri. L'utilisateur veut un tri alphabétique croissant par nom.
|
||||||
|
|
||||||
|
### Design
|
||||||
|
Ajouter un `.sort()` avec `localeCompare('fr')` à la fin du computed `filteredMachines`.
|
||||||
|
|
||||||
|
### Changements techniques
|
||||||
|
**Fichier** : `Inventory_frontend/app/pages/machines/index.vue`
|
||||||
|
|
||||||
|
- Dans le computed `filteredMachines`, ajouter avant le `return` :
|
||||||
|
```js
|
||||||
|
filtered = [...filtered].sort((a, b) =>
|
||||||
|
(a.name || '').localeCompare(b.name || '', 'fr')
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Recherche par référence dans les catalogues (Pièces, Composants, Produits)
|
||||||
|
|
||||||
|
### Contexte
|
||||||
|
Les placeholders des champs de recherche promettent "Nom ou référence…" mais le frontend n'envoie que `?name=xxx` à l'API. Le backend (API Platform SearchFilter) supporte `name` et `reference` en `ipartial`, mais combiner `?name=xxx&reference=xxx` produit un AND (les deux doivent matcher), pas un OR.
|
||||||
|
|
||||||
|
### Design
|
||||||
|
Créer une **Extension Doctrine** (`SearchByNameOrReferenceExtension`) qui intercepte un paramètre `?q=xxx` et ajoute une clause `WHERE name ILIKE %xxx% OR reference ILIKE %xxx%` à la requête. Côté frontend, remplacer `params.set('name', search)` par `params.set('q', search)`.
|
||||||
|
|
||||||
|
### Changements techniques
|
||||||
|
|
||||||
|
**Backend — Nouveau fichier** : `src/Doctrine/SearchByNameOrReferenceExtension.php`
|
||||||
|
- Implémente `QueryCollectionExtensionInterface`
|
||||||
|
- S'applique aux entités `Piece`, `Composant`, `Product`
|
||||||
|
- Lit le paramètre `q` depuis la requête HTTP
|
||||||
|
- Ajoute `LOWER(o.name) LIKE :searchQ OR LOWER(o.reference) LIKE :searchQ` avec paramètre `%{strtolower(q)}%`
|
||||||
|
- **Échappement LIKE** : les caractères `%` et `_` dans l'input utilisateur sont échappés via `addcslashes($q, '%_')` pour éviter des matchs trop larges
|
||||||
|
- **`reference` nullable** : les lignes avec `reference = NULL` ne matcheront pas (comportement SQL standard : `NULL LIKE x` = NULL = false), ce qui est le comportement attendu
|
||||||
|
- **Pas de conflit** avec le `SearchFilter` existant : le paramètre `q` n'est pas enregistré comme propriété de `SearchFilter`, donc il sera ignoré par celui-ci. Les filtres `name` et `reference` restent disponibles pour d'autres usages.
|
||||||
|
|
||||||
|
**Frontend — 3 fichiers** (dans la fonction `loadXxx`, remplacer l'appel `params.set('name', search.trim())`) :
|
||||||
|
- `Inventory_frontend/app/composables/usePieces.ts` → `params.set('q', search.trim())`
|
||||||
|
- `Inventory_frontend/app/composables/useComposants.ts` → idem
|
||||||
|
- `Inventory_frontend/app/composables/useProducts.ts` → idem
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fichiers impactés (résumé)
|
||||||
|
|
||||||
|
| Fichier | Changement |
|
||||||
|
|---------|-----------|
|
||||||
|
| `Inventory_frontend/app/pages/machines/index.vue` | Checkboxes sites + tri alphabétique |
|
||||||
|
| `src/Doctrine/SearchByNameOrReferenceExtension.php` | **Nouveau** — Extension Doctrine OR search |
|
||||||
|
| `Inventory_frontend/app/composables/usePieces.ts` | `name` → `q` |
|
||||||
|
| `Inventory_frontend/app/composables/useComposants.ts` | `name` → `q` |
|
||||||
|
| `Inventory_frontend/app/composables/useProducts.ts` | `name` → `q` |
|
||||||
|
|
||||||
|
## Hors scope
|
||||||
|
- La page Parc Machines cherche **déjà** sur nom ET référence côté frontend (filtrage client-side). Pas de changement nécessaire.
|
||||||
|
- Aucun changement de placeholder — ils affichent déjà "Nom ou référence…".
|
||||||
312
docs/superpowers/specs/2026-03-25-entity-versioning-design.md
Normal file
312
docs/superpowers/specs/2026-03-25-entity-versioning-design.md
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
# Entity Versioning — Design Spec
|
||||||
|
|
||||||
|
**Date :** 2026-03-25
|
||||||
|
**Entites concernees :** Machine, Composant, Piece, Produit
|
||||||
|
**Approche :** Extension du systeme AuditLog existant
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Permettre de consulter l'historique des versions numerotees (v1, v2, v3...) des entites principales et de restaurer n'importe quelle version anterieure, afin de ne jamais perdre de donnees.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regles metier
|
||||||
|
|
||||||
|
### Creation de version
|
||||||
|
- Chaque `create` ou `update` sur une entite incremente automatiquement le compteur `version` de l'entite
|
||||||
|
- Le numero de version est enregistre dans l'AuditLog correspondant (nouvelle colonne `version`)
|
||||||
|
|
||||||
|
### Restauration
|
||||||
|
- La restauration cree une **nouvelle version** (v+1) — on ne supprime jamais d'historique
|
||||||
|
- Le service `EntityVersionService::restore()` cree **manuellement** un AuditLog avec `action = "restore"` et le diff contient `restoredFromVersion: N`
|
||||||
|
- Important : le flush du restore declenche les AuditSubscribers, qui produiraient un `update` duplique. Pour eviter cela, l'entite porte un flag transitoire `$skipAudit = true` que les subscribers verifient
|
||||||
|
|
||||||
|
### Controle de squelette (Composant, Piece, Produit uniquement)
|
||||||
|
- Avant restauration, on compare le ModelType actuel avec celui du snapshot
|
||||||
|
- **Meme squelette (ModelType)** : restore complet — champs de base + slots + custom fields
|
||||||
|
- **Squelette different** : restore partiel — uniquement les champs de base (nom, description, reference, constructeurs, prix)
|
||||||
|
|
||||||
|
### Controle d'integrite
|
||||||
|
- Avant restauration, on verifie que toutes les entites liees dans le snapshot existent encore en base :
|
||||||
|
- **Composant** : pieces selectionnees dans les slots, produits, sous-composants, constructeurs
|
||||||
|
- **Piece** : produits selectionnes dans les slots, constructeurs
|
||||||
|
- **Produit** : constructeurs
|
||||||
|
- **Machine** : site, liens composants/pieces/produits (MachineComponentLink, MachinePieceLink, MachineProductLink)
|
||||||
|
- Les entites manquantes generent des **warnings** affiches a l'utilisateur
|
||||||
|
- Les slots avec des entites supprimees sont restaures **vides** (sans selection)
|
||||||
|
- Pour les custom field values : restauration par `fieldId` + entite parente (pas par ID de la CustomFieldValue elle-meme, car un sync ModelType peut recreer les CFV avec des IDs differents)
|
||||||
|
- Les controles d'integrite utilisent des requetes batch (`findBy(['id' => $ids])`) plutot que des requetes individuelles par slot
|
||||||
|
|
||||||
|
### Machines
|
||||||
|
- Pas de controle de squelette (pas de ModelType) : restauration toujours complete
|
||||||
|
- Controle d'integrite sur le site et les liens machine
|
||||||
|
- Machine n'a pas de champ `description` (contrairement aux autres entites)
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
- Consulter les versions : `ROLE_VIEWER`
|
||||||
|
- Restaurer une version : `ROLE_GESTIONNAIRE` et au-dessus
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modifications backend
|
||||||
|
|
||||||
|
### 1. Colonne `version` sur AuditLog
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE audit_logs ADD COLUMN version INT DEFAULT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
Nullable car les AuditLogs existants n'ont pas de version.
|
||||||
|
|
||||||
|
### 2. Colonne `version` sur Machine
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE machine ADD COLUMN version INT NOT NULL DEFAULT 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
Les entites Composant, Piece, Produit ont deja cette colonne.
|
||||||
|
|
||||||
|
### 3. Enrichissement des snapshots
|
||||||
|
|
||||||
|
Les Audit Subscribers doivent inclure dans le `snapshot` :
|
||||||
|
|
||||||
|
**Composant :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "cl...",
|
||||||
|
"name": "...",
|
||||||
|
"reference": "...",
|
||||||
|
"description": "...",
|
||||||
|
"prix": 100.00,
|
||||||
|
"typeComposant": { "id": "cl...", "name": "...", "code": "..." },
|
||||||
|
"product": { "id": "cl...", "name": "..." },
|
||||||
|
"constructeurIds": [{ "id": "cl...", "name": "..." }],
|
||||||
|
"customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
|
||||||
|
"pieceSlots": [
|
||||||
|
{ "id": "cl...", "typePieceId": "cl...", "selectedPieceId": "cl...", "quantity": 1, "position": 0 }
|
||||||
|
],
|
||||||
|
"subcomponentSlots": [
|
||||||
|
{ "id": "cl...", "alias": "...", "familyCode": "...", "typeComposantId": "cl...", "selectedComposantId": "cl...", "position": 0 }
|
||||||
|
],
|
||||||
|
"productSlots": [
|
||||||
|
{ "id": "cl...", "typeProductId": "cl...", "selectedProductId": "cl...", "familyCode": "...", "position": 0 }
|
||||||
|
],
|
||||||
|
"version": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Piece :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "cl...",
|
||||||
|
"name": "...",
|
||||||
|
"reference": "...",
|
||||||
|
"description": "...",
|
||||||
|
"prix": 50.00,
|
||||||
|
"typePiece": { "id": "cl...", "name": "...", "code": "..." },
|
||||||
|
"product": { "id": "cl...", "name": "..." },
|
||||||
|
"constructeurIds": [{ "id": "cl...", "name": "..." }],
|
||||||
|
"customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
|
||||||
|
"productSlots": [
|
||||||
|
{ "id": "cl...", "typeProductId": "cl...", "selectedProductId": "cl...", "familyCode": "...", "position": 0 }
|
||||||
|
],
|
||||||
|
"version": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Produit :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "cl...",
|
||||||
|
"name": "...",
|
||||||
|
"reference": "...",
|
||||||
|
"supplierPrice": 25.00,
|
||||||
|
"typeProduct": { "id": "cl...", "name": "...", "code": "..." },
|
||||||
|
"constructeurIds": [{ "id": "cl...", "name": "..." }],
|
||||||
|
"customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Machine :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "cl...",
|
||||||
|
"name": "...",
|
||||||
|
"reference": "...",
|
||||||
|
"prix": 1500.00,
|
||||||
|
"site": { "id": "cl...", "name": "..." },
|
||||||
|
"constructeurIds": [{ "id": "cl...", "name": "..." }],
|
||||||
|
"customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
|
||||||
|
"version": 4
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Incrementation automatique de la version
|
||||||
|
|
||||||
|
Dans chaque Audit Subscriber, a chaque `create`/`update` :
|
||||||
|
1. Appeler `$entity->incrementVersion()`
|
||||||
|
2. Ecrire `$auditLog->setVersion($entity->getVersion())`
|
||||||
|
|
||||||
|
Pour Machine, ajouter la methode `incrementVersion()` et la propriete `version` a l'entite.
|
||||||
|
|
||||||
|
### 5. Nouveaux endpoints — `EntityVersionController`
|
||||||
|
|
||||||
|
| Methode | Route | Description | Role |
|
||||||
|
|---------|-------|-------------|------|
|
||||||
|
| GET | `/api/{entity}/{id}/versions` | Liste des versions | ROLE_VIEWER |
|
||||||
|
| GET | `/api/{entity}/{id}/versions/{version}/preview` | Preview + controles avant restore | ROLE_GESTIONNAIRE |
|
||||||
|
| POST | `/api/{entity}/{id}/versions/{version}/restore` | Execute la restauration | ROLE_GESTIONNAIRE |
|
||||||
|
|
||||||
|
`{entity}` = `machines`, `composants`, `pieces`, `products`
|
||||||
|
|
||||||
|
**GET versions — Response :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"action": "update",
|
||||||
|
"createdAt": "2026-03-25T14:30:00+00:00",
|
||||||
|
"actor": { "id": "cl...", "label": "Jean Dupont" },
|
||||||
|
"diff": { "name": { "from": "Ancien", "to": "Nouveau" } }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**GET preview — Response :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"restoreMode": "full",
|
||||||
|
"diff": {
|
||||||
|
"name": { "current": "Nouveau", "restored": "Ancien" },
|
||||||
|
"reference": { "current": "REF-002", "restored": "REF-001" }
|
||||||
|
},
|
||||||
|
"warnings": [
|
||||||
|
{
|
||||||
|
"field": "pieceSlots[0].selectedPieceId",
|
||||||
|
"message": "La piece 'Roulement XY' (cl...) n'existe plus. Le slot sera restaure vide.",
|
||||||
|
"missingEntityId": "cl...",
|
||||||
|
"missingEntityName": "Roulement XY"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"snapshot": { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`restoreMode` : `"full"` (meme squelette) ou `"partial"` (squelette different, champs de base uniquement).
|
||||||
|
|
||||||
|
**POST restore — Response :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"newVersion": 6,
|
||||||
|
"restoredFromVersion": 2,
|
||||||
|
"restoreMode": "full",
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Service `EntityVersionService`
|
||||||
|
|
||||||
|
Service centralise pour la logique de versioning :
|
||||||
|
|
||||||
|
- `getVersions(string $entityType, string $entityId): array` — liste des versions depuis AuditLog
|
||||||
|
- `getRestorePreview(string $entityType, string $entityId, int $version): array` — controles + diff
|
||||||
|
- `restore(string $entityType, string $entityId, int $version): array` — execution du restore
|
||||||
|
|
||||||
|
Methodes internes :
|
||||||
|
- `checkSkeletonCompatibility(object $entity, array $snapshot): string` — retourne `"full"` ou `"partial"`
|
||||||
|
- `checkIntegrity(string $entityType, array $snapshot): array` — retourne les warnings
|
||||||
|
- `applyRestore(object $entity, array $snapshot, string $mode): void` — applique les changements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modifications frontend
|
||||||
|
|
||||||
|
### 1. Composant `EntityVersionList.vue`
|
||||||
|
|
||||||
|
Composant reutilisable affiche dans un onglet "Versions" sur les pages de detail.
|
||||||
|
|
||||||
|
**Props :**
|
||||||
|
- `entityType: 'machines' | 'composants' | 'pieces' | 'products'`
|
||||||
|
- `entityId: string`
|
||||||
|
|
||||||
|
**Affichage :**
|
||||||
|
- Tableau : version, date, auteur, action, diff resume
|
||||||
|
- Badge "Actuelle" sur la version la plus recente
|
||||||
|
- Bouton "Restaurer" sur chaque ligne (sauf version actuelle), visible uniquement pour ROLE_GESTIONNAIRE+
|
||||||
|
|
||||||
|
### 2. Composant `VersionRestoreModal.vue`
|
||||||
|
|
||||||
|
Modal de confirmation avec preview.
|
||||||
|
|
||||||
|
**Props :**
|
||||||
|
- `entityType`, `entityId`, `version` (cible)
|
||||||
|
- `previewData` (resultat du GET preview)
|
||||||
|
|
||||||
|
**Affichage :**
|
||||||
|
- Indicateur de mode : "Restauration complete" ou "Restauration partielle"
|
||||||
|
- Diff visuel : champs qui changent (valeur actuelle -> valeur restauree)
|
||||||
|
- Warnings en alerte orange pour les entites manquantes
|
||||||
|
- Boutons "Confirmer la restauration" / "Annuler"
|
||||||
|
|
||||||
|
### 3. Composable `useEntityVersions.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Deps {
|
||||||
|
entityType: MaybeRef<string>
|
||||||
|
entityId: MaybeRef<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEntityVersions(deps: Deps) {
|
||||||
|
// fetchVersions() — GET /api/{entity}/{id}/versions
|
||||||
|
// fetchPreview(version) — GET /api/{entity}/{id}/versions/{version}/preview
|
||||||
|
// restore(version) — POST /api/{entity}/{id}/versions/{version}/restore
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Integration dans les pages de detail
|
||||||
|
|
||||||
|
Ajouter un onglet "Versions" dans les pages :
|
||||||
|
- `pages/machines/[id].vue`
|
||||||
|
- `pages/composants/[id].vue`
|
||||||
|
- `pages/pieces/[id].vue`
|
||||||
|
- `pages/products/[id].vue`
|
||||||
|
|
||||||
|
L'onglet affiche `EntityVersionList` qui gere l'ouverture de `VersionRestoreModal`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
Une seule migration PostgreSQL :
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Colonne version sur audit_logs
|
||||||
|
ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS version INT DEFAULT NULL;
|
||||||
|
|
||||||
|
-- Colonne version sur machine
|
||||||
|
ALTER TABLE machine ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
-- Index pour requetes par version
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_entity_version ON audit_logs (entity_type, entity_id, version);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ce qui change (breaking)
|
||||||
|
|
||||||
|
- **Piece snapshot** : le champ legacy `productIds` (ancien JSON) est remplace par `productSlots` (tables normalisees). Les anciens AuditLogs conservent `productIds` dans leur snapshot mais les nouveaux ne l'auront plus. Le restore utilise `productSlots` exclusivement.
|
||||||
|
|
||||||
|
## Ce qui ne change PAS
|
||||||
|
|
||||||
|
- L'onglet/page d'historique existant (`EntityHistoryController`) reste inchange
|
||||||
|
- Les AuditLogs existants (sans version) continuent de fonctionner
|
||||||
|
- Le mecanisme d'audit automatique via les Subscribers reste identique, juste enrichi
|
||||||
|
- Les documents ne sont pas versionnes (hors scope)
|
||||||
117
docs/superpowers/specs/2026-03-26-machine-single-save-design.md
Normal file
117
docs/superpowers/specs/2026-03-26-machine-single-save-design.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Machine : Bouton Save Unique + Versioning des Liens
|
||||||
|
|
||||||
|
**Date :** 2026-03-26
|
||||||
|
**Statut :** Approuvé
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
La page machine utilise actuellement un auto-save au blur pour chaque champ (info, custom fields, constructeurs). Les pages composant/pièce/produit utilisent un bouton unique "Enregistrer les modifications" en bas du formulaire. L'objectif est d'aligner la page machine sur ce pattern.
|
||||||
|
|
||||||
|
De plus, les ajouts/suppressions de liens composant/pièce/produit sur une machine ne sont pas tracés dans le versioning. Ils doivent l'être.
|
||||||
|
|
||||||
|
## Volet 1 : Bouton Save Unique
|
||||||
|
|
||||||
|
### Comportement cible
|
||||||
|
|
||||||
|
- En mode édition, tous les champs (info machine, custom field values, custom field definitions, constructeurs) sont modifiés localement sans appel API.
|
||||||
|
- Un bouton "Enregistrer les modifications" en bas du formulaire sauvegarde tout d'un coup.
|
||||||
|
- Un bouton "Annuler" réinitialise l'état local et sort du mode édition.
|
||||||
|
- Les documents restent en upload/suppression immédiate (inchangé).
|
||||||
|
- Les ajouts/suppressions de liens composant/pièce/produit restent immédiats via modales (inchangé).
|
||||||
|
|
||||||
|
### Changements frontend
|
||||||
|
|
||||||
|
#### MachineInfoCard.vue
|
||||||
|
- Supprimer les `@blur` → `$emit('blur-field')` sur les inputs (nom, référence)
|
||||||
|
- Supprimer le `@change` qui émet `blur-field` sur le select site
|
||||||
|
- Supprimer les `@blur` → `$emit('update-custom-field', field)` sur tous les champs custom
|
||||||
|
- Conserver `@input` / `@update:*` / `set-custom-field-value` pour la mise à jour de l'état local
|
||||||
|
- Le `MachineCustomFieldDefEditor` perd son bouton save propre : l'état est collecté au submit global
|
||||||
|
|
||||||
|
#### machine/[id].vue
|
||||||
|
- Supprimer le handler `@blur-field`
|
||||||
|
- Supprimer le handler `@update-custom-field`
|
||||||
|
- `@update:constructeur-ids` met à jour l'état local sans save
|
||||||
|
- Ajouter le bloc boutons en bas (pattern identique à component/[id]/index.vue) :
|
||||||
|
- "Annuler" (btn-ghost) → `cancelEdition()` : réinitialise depuis `machine.value` + sort du mode édition
|
||||||
|
- "Enregistrer les modifications" (btn-primary, disabled si `!canSubmit`) → `submitEdition()`
|
||||||
|
|
||||||
|
#### useMachineDetailData.ts
|
||||||
|
- Exposer `saving` ref
|
||||||
|
- Exposer `submitEdition()` :
|
||||||
|
1. `updateMachineInfo()` — PATCH machine (nom, ref, site, constructeurs)
|
||||||
|
2. Batch save custom field values (tous les `visibleMachineCustomFields` avec valeur)
|
||||||
|
3. Save custom field definitions si modifiées (`fieldDefs.saveDefinitions()`)
|
||||||
|
4. `loadMachineData()` pour recharger
|
||||||
|
5. Sortie du mode édition + toast succès
|
||||||
|
- Exposer `cancelEdition()` :
|
||||||
|
1. `initMachineFields()` — réinitialise nom, ref, site, constructeurs depuis `machine.value`
|
||||||
|
2. `syncMachineCustomFields()` — réinitialise les custom fields
|
||||||
|
3. Sort du mode édition
|
||||||
|
|
||||||
|
#### useMachineDetailUpdates.ts
|
||||||
|
- `handleMachineConstructeurChange` ne déclenche plus `updateMachineInfo()`, met juste à jour le ref local
|
||||||
|
|
||||||
|
#### useMachineDetailCustomFields.ts
|
||||||
|
- `updateMachineCustomField` n'est plus appelé au blur — sera appelé en batch par `submitEdition()`
|
||||||
|
- Ajouter méthode `saveAllMachineCustomFields()` qui itère sur les champs visibles et sauvegarde ceux avec valeur
|
||||||
|
|
||||||
|
### Validation (`canSubmit`)
|
||||||
|
- Machine existe
|
||||||
|
- Nom non vide
|
||||||
|
- Pas en cours de sauvegarde (`!saving.value`)
|
||||||
|
- `canEdit` est true
|
||||||
|
|
||||||
|
## Volet 2 : Versioning des Liens Machine
|
||||||
|
|
||||||
|
### Comportement cible
|
||||||
|
|
||||||
|
Quand un composant, pièce ou produit est ajouté ou supprimé d'une machine, cela doit :
|
||||||
|
1. Incrémenter la `version` de la Machine
|
||||||
|
2. Créer une entrée `AuditLog` avec diff et snapshot
|
||||||
|
|
||||||
|
### Changements backend
|
||||||
|
|
||||||
|
#### MachineAuditSubscriber — enrichir le snapshot
|
||||||
|
Ajouter au snapshot machine les liens :
|
||||||
|
```php
|
||||||
|
'componentLinks' => array_map(fn($link) => [
|
||||||
|
'id' => $link->getId(),
|
||||||
|
'composantId' => $link->getComposant()->getId(),
|
||||||
|
'composantName' => $link->getComposant()->getName(),
|
||||||
|
], $entity->getComponentLinks()->toArray()),
|
||||||
|
'pieceLinks' => [...],
|
||||||
|
'productLinks' => [...],
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Nouveau subscriber ou service : MachineLinkAuditService
|
||||||
|
Écouter les événements Doctrine `postPersist` et `postRemove` sur les 3 entités link.
|
||||||
|
Quand un lien est créé/supprimé :
|
||||||
|
1. Récupérer la Machine parente
|
||||||
|
2. Incrémenter `$machine->incrementVersion()`
|
||||||
|
3. Créer un `AuditLog` :
|
||||||
|
- `entityType: 'machine'`
|
||||||
|
- `entityId: $machine->getId()`
|
||||||
|
- `action: 'update'`
|
||||||
|
- `diff: { addedComponent: {id, name} }` ou `{ removedPiece: {id, name} }`
|
||||||
|
- `snapshot:` snapshot complet de la machine (avec liens mis à jour)
|
||||||
|
- `version:` nouvelle version
|
||||||
|
|
||||||
|
### Labels pour le diff (frontend)
|
||||||
|
Ajouter au `historyFieldLabels` de la page machine :
|
||||||
|
```js
|
||||||
|
addedComponent: 'Composant ajouté',
|
||||||
|
removedComponent: 'Composant supprimé',
|
||||||
|
addedPiece: 'Pièce ajoutée',
|
||||||
|
removedPiece: 'Pièce supprimée',
|
||||||
|
addedProduct: 'Produit ajouté',
|
||||||
|
removedProduct: 'Produit supprimé',
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ce qui ne change PAS
|
||||||
|
|
||||||
|
- Upload/suppression de documents (immédiat)
|
||||||
|
- Pattern read/edit toggle dans le header
|
||||||
|
- L'affichage des sections composants/pièces/produits
|
||||||
|
- Les modales d'ajout/suppression de liens (restent immédiates)
|
||||||
|
- Le versioning des autres entités (composant, pièce, produit)
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Spec : Formula Builder interactif pour la référence auto
|
||||||
|
|
||||||
|
**Date** : 2026-03-31
|
||||||
|
**Scope** : Frontend uniquement (pas de changement backend)
|
||||||
|
**Fichier impacté** : `Inventory_frontend/app/components/model-types/ModelTypeForm.vue`
|
||||||
|
|
||||||
|
## Problème
|
||||||
|
|
||||||
|
L'utilisateur doit taper manuellement les noms exacts des custom fields dans la formule (`{serie}{diametre}{type}`) et re-lister les champs requis séparés par des virgules. C'est sujet aux erreurs de typo et peu ergonomique.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Remplacer la section "Génération de référence automatique" du `ModelTypeForm` par un formula builder interactif.
|
||||||
|
|
||||||
|
### Composants UI
|
||||||
|
|
||||||
|
#### 1. Chips de champs disponibles
|
||||||
|
|
||||||
|
- Afficher une rangée de boutons-chips avec les noms des custom fields définis dans `pieceStructure.customFields`
|
||||||
|
- Cliquer sur un chip insère `{nom_du_champ}` dans l'input formule à la position du curseur
|
||||||
|
- Si `pieceStructure.customFields` est vide, afficher un message "Aucun champ personnalisé défini"
|
||||||
|
|
||||||
|
#### 2. Input formule
|
||||||
|
|
||||||
|
- Input texte classique (comme aujourd'hui) mais avec les chips comme aide à la saisie
|
||||||
|
- L'utilisateur peut aussi taper du texte libre (séparateurs `-`, `/`, préfixes `SNU `, etc.)
|
||||||
|
- Le format stocké reste `{nom_du_champ}` — aucun changement de format backend
|
||||||
|
|
||||||
|
#### 3. Suppression du champ "Champs requis"
|
||||||
|
|
||||||
|
- Le champ `requiredFieldsForReference` est calculé automatiquement au submit en extrayant tous les `{...}` de la formule
|
||||||
|
- Suppression de l'input "Champs requis" et de la variable `requiredFieldsInput`
|
||||||
|
- La logique : tous les champs présents dans la formule sont requis. Si un champ n'a pas de valeur → pas de référence générée
|
||||||
|
|
||||||
|
#### 4. Aperçu live
|
||||||
|
|
||||||
|
- Conserver l'aperçu existant mais l'améliorer : remplacer les placeholders par des valeurs d'exemple en majuscules
|
||||||
|
- Exemples par type de champ : `text` → `VALEUR`, `number` → `123`, `select` → `OPTION`, `boolean` → `OUI`, `date` → `2026-01-01`
|
||||||
|
|
||||||
|
### Comportement
|
||||||
|
|
||||||
|
- **Insert au curseur** : quand l'utilisateur clique un chip, le placeholder est inséré à `selectionStart` de l'input, pas à la fin
|
||||||
|
- **Formule vide** : si la formule est vide, pas de référence auto (comportement actuel conservé)
|
||||||
|
- **Readonly** : les chips sont désactivés en mode readonly (comme l'input)
|
||||||
|
- **Pas de custom fields** : si aucun champ n'est défini dans la structure, la section reste visible mais les chips sont remplacés par un message informatif. L'utilisateur peut quand même taper une formule manuellement (cas edge)
|
||||||
|
|
||||||
|
### Format de sortie (inchangé)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
referenceFormula: "SNU {serie}-{diametre}/{type}" | null,
|
||||||
|
requiredFieldsForReference: ["serie", "diametre", "type"] | null // auto-calculé
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pas de changement
|
||||||
|
|
||||||
|
- Backend (`ReferenceAutoGenerator`, `ReferenceAutoSubscriber`, entités) : aucun changement
|
||||||
|
- Format de stockage de la formule : identique (`{placeholder}` strings)
|
||||||
|
- API : identique
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Références Fournisseur par Item — Design Spec
|
||||||
|
|
||||||
|
**Date :** 2026-03-31
|
||||||
|
**Statut :** Validé
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
Chaque entité (Machine, Pièce, Composant, Produit) a un champ `reference` générique et une relation ManyToMany avec `Constructeur`. Il n'existe aucun moyen de stocker une référence spécifique par fournisseur — si un item est vendu par 3 fournisseurs avec 3 références différentes, on ne peut en stocker qu'une seule.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Permettre de stocker une référence fournisseur (`supplierReference`) par couple (item, constructeur). Le champ `reference` existant reste inchangé comme référence interne. Le champ `supplierPrice` sur Product reste inchangé.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### Approche retenue : conversion ManyToMany → entités pivot
|
||||||
|
|
||||||
|
Remplacer les 4 tables de jointure simples (`_MachineConstructeurs`, `_PieceConstructeurs`, `_ComposantConstructeurs`, `_ProductConstructeurs`) par de vraies entités Doctrine Link, suivant le pattern existant (`MachinePieceLink`, `MachineComponentLink`, etc.).
|
||||||
|
|
||||||
|
### Nouvelles entités
|
||||||
|
|
||||||
|
| Entité | Table | FK item | FK constructeur | Champs extra |
|
||||||
|
|--------|-------|---------|-----------------|--------------|
|
||||||
|
| `MachineConstructeurLink` | `machine_constructeur_links` | `machineId` → `Machine` | `constructeurId` → `Constructeur` | `supplierReference` (string 255, nullable) |
|
||||||
|
| `PieceConstructeurLink` | `piece_constructeur_links` | `pieceId` → `Piece` | `constructeurId` → `Constructeur` | `supplierReference` (string 255, nullable) |
|
||||||
|
| `ComposantConstructeurLink` | `composant_constructeur_links` | `composantId` → `Composant` | `constructeurId` → `Constructeur` | `supplierReference` (string 255, nullable) |
|
||||||
|
| `ProductConstructeurLink` | `product_constructeur_links` | `productId` → `Product` | `constructeurId` → `Constructeur` | `supplierReference` (string 255, nullable) |
|
||||||
|
|
||||||
|
### Structure de chaque entité
|
||||||
|
|
||||||
|
Chaque entité suit le pattern `MachinePieceLink` :
|
||||||
|
|
||||||
|
- `CuidEntityTrait` pour l'ID (string, 36 chars)
|
||||||
|
- `#[ORM\HasLifecycleCallbacks]` avec `createdAt` / `updatedAt`
|
||||||
|
- Contrainte unique sur `(item_id, constructeur_id)` via `#[ORM\UniqueConstraint]`
|
||||||
|
- `#[ApiResource]` avec opérations CRUD complètes
|
||||||
|
- Sécurité : `ROLE_VIEWER` pour lecture, `ROLE_GESTIONNAIRE` pour écriture
|
||||||
|
- `ManyToOne` vers l'item (onDelete CASCADE)
|
||||||
|
- `ManyToOne` vers `Constructeur` (onDelete CASCADE)
|
||||||
|
- Champ `supplierReference` (string 255, nullable)
|
||||||
|
|
||||||
|
### Modifications sur les entités existantes
|
||||||
|
|
||||||
|
#### Machine, Pièce, Composant, Produit
|
||||||
|
- Supprimer la propriété `ManyToMany` `constructeurs` et ses getters/setters/add/remove
|
||||||
|
- Ajouter une propriété `OneToMany` `constructeurLinks` vers le Link correspondant
|
||||||
|
- Getter `getConstructeurLinks(): Collection`
|
||||||
|
|
||||||
|
#### Constructeur
|
||||||
|
- Supprimer les 4 propriétés `ManyToMany` (`machines`, `composants`, `pieces`, `products`) et leurs getters/setters
|
||||||
|
- Ajouter 4 propriétés `OneToMany` vers les Links correspondants
|
||||||
|
|
||||||
|
### Migration SQL
|
||||||
|
|
||||||
|
1. Créer les 4 nouvelles tables avec colonnes `id`, `machineId`/`pieceId`/etc., `constructeurId`, `supplierReference`, `createdAt`, `updatedAt`
|
||||||
|
2. Ajouter les contraintes uniques
|
||||||
|
3. Migrer les données des anciennes tables de jointure vers les nouvelles (génération CUID pour chaque ligne, `supplierReference` = NULL)
|
||||||
|
4. Supprimer les anciennes tables de jointure (`_MachineConstructeurs`, `_PieceConstructeurs`, `_ComposantConstructeurs`, `_ProductConstructeurs`)
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
Endpoints API Platform auto-générés pour chaque Link :
|
||||||
|
- `GET /api/machine_constructeur_links` — liste (filtrable par machine, constructeur)
|
||||||
|
- `GET /api/machine_constructeur_links/{id}` — détail
|
||||||
|
- `POST /api/machine_constructeur_links` — créer un lien avec référence
|
||||||
|
- `PATCH /api/machine_constructeur_links/{id}` — modifier la référence
|
||||||
|
- `DELETE /api/machine_constructeur_links/{id}` — supprimer le lien
|
||||||
|
|
||||||
|
Idem pour les 3 autres types.
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
Les pages détail/édition qui affichent les constructeurs devront être adaptées pour :
|
||||||
|
- Afficher la `supplierReference` à côté de chaque constructeur
|
||||||
|
- Permettre l'édition de la référence fournisseur lors de l'ajout/modification d'un constructeur
|
||||||
|
- Utiliser les endpoints `*ConstructeurLink` au lieu de la collection `constructeurs`
|
||||||
|
|
||||||
|
### Hors périmètre
|
||||||
|
|
||||||
|
- Migration de `supplierPrice` de Product vers le Link (explicitement exclu)
|
||||||
|
- Modification du champ `reference` existant sur les entités
|
||||||
|
- Référence auto (`referenceAuto`) sur Pièce/Composant — non impactée
|
||||||
@@ -105,6 +105,114 @@ INSERT INTO public.model_types (id, name, code, category, notes, createdat, upda
|
|||||||
INSERT INTO public.model_types (id, name, code, category, notes, createdat, updatedat, description, componentskeleton, pieceskeleton, productskeleton) VALUES ('cle48e33ef67853069badfc5f0', 'testcor', 'testcor', 'COMPONENT', 'nnd', '2026-01-25 11:27:47', '2026-01-25 11:27:47', 'nnd', '{"pieces": [{"typePieceId": "cmgs1sco0002k47056yq8eyfq", "typePieceLabel": "Bavette"}], "products": [{"familyCode": "lubrifiant", "typeProductId": "cmhn9zrm5000247s8ds3bmpaf"}], "customFields": [{"key": "uu", "value": {"type": "text", "required": false}}], "subcomponents": [{"alias": "Trémie d''alimentation", "familyCode": "Tremie", "typeComposantId": "cmgs0htn5002d4705tyxxiqb2"}]}', NULL, NULL);
|
INSERT INTO public.model_types (id, name, code, category, notes, createdat, updatedat, description, componentskeleton, pieceskeleton, productskeleton) VALUES ('cle48e33ef67853069badfc5f0', 'testcor', 'testcor', 'COMPONENT', 'nnd', '2026-01-25 11:27:47', '2026-01-25 11:27:47', 'nnd', '{"pieces": [{"typePieceId": "cmgs1sco0002k47056yq8eyfq", "typePieceLabel": "Bavette"}], "products": [{"familyCode": "lubrifiant", "typeProductId": "cmhn9zrm5000247s8ds3bmpaf"}], "customFields": [{"key": "uu", "value": {"type": "text", "required": false}}], "subcomponents": [{"alias": "Trémie d''alimentation", "familyCode": "Tremie", "typeComposantId": "cmgs0htn5002d4705tyxxiqb2"}]}', NULL, NULL);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Data for Name: skeleton_piece_requirements; Type: TABLE DATA; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl0939a6d5cf1e2f42ea338ed9', 'cmgrp0u0h00124705xxyt5fqu', 'cmgrnu6zc000f470565mc8hha', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clc8a5a3ffde2e012115f4a4a6', 'cmgrp0u0h00124705xxyt5fqu', 'cmgrnxlx5000g47059oyj4yuw', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl19977b37375e768874a9bc21', 'cmgrp0u0h00124705xxyt5fqu', 'cmgrohigo000z4705q8yvpih0', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('claa3bd610232237f37b5305f5', 'cmgrp0u0h00124705xxyt5fqu', 'cmgrou6670011470586ipgylm', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl8cc6f7b1c0d2356200710cff', 'cmgrp0u0h00124705xxyt5fqu', 'cmgroij2f00104705t6y33enk', 4, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl6ab668cf6c44971a6e8c15c1', 'cmgrp0u0h00124705xxyt5fqu', 'cmgrnu6zc000f470565mc8hha', 5, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl9d6c1d30a8f1f37d6f5919ca', 'cmgrzrlcc001t47054emo6cfb', 'cmgrzuwkj001u47057u8hej9u', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl89cc18036bb3a12d4d0cd427', 'cmgrzrlcc001t47054emo6cfb', 'cmgrzuwkj001u47057u8hej9u', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl6f6e8f47c6a731bcd9288bba', 'cmgrzrlcc001t47054emo6cfb', 'cmgrzuwkj001u47057u8hej9u', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clc7fe6f18be36b9cd60089a28', 'cmgrzrlcc001t47054emo6cfb', 'cmgroij2f00104705t6y33enk', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl15ff333888d9f160a80a04e5', 'cmgrzrlcc001t47054emo6cfb', 'cmgroij2f00104705t6y33enk', 4, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl82c521ac9e0cae5ba31d2880', 'cmgs0h5ze002c4705szh85svi', 'cmgroij2f00104705t6y33enk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl3eef5c0837bdf0aed9da51b3', 'cmgs0h5ze002c4705szh85svi', 'cmgs0kd5o002f47053b7n8tw6', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl2533d3d3f215df0c180b2c4a', 'cmgs0h5ze002c4705szh85svi', 'cmgrzuwkj001u47057u8hej9u', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl3ff44ebbbd85d8750328183c', 'cmgs0h5ze002c4705szh85svi', 'cmgroij2f00104705t6y33enk', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clbc503ec073758ef2f4cd2a4f', 'cmgs0htn5002d4705tyxxiqb2', 'cmgs13jjp002h4705rjqzz5lh', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl80631b5e0a1ab10d4af65f6f', 'cmgs0htn5002d4705tyxxiqb2', 'cmgs1s4pv002j470567o60oqe', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clc11a5ad82b02a76eca599117', 'cmgs0htn5002d4705tyxxiqb2', 'cmgs1sco0002k47056yq8eyfq', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clbd335edf794794fba25546a6', 'cmgs0htn5002d4705tyxxiqb2', 'cmgs1sco0002k47056yq8eyfq', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl218404db04ffcd7c8e200f0a', 'cmgs0i4je002e4705ndrhe26e', 'cmgroij2f00104705t6y33enk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl31265ec0dcf2fc4e252c2181', 'cmgs0i4je002e4705ndrhe26e', 'cmgrzuwkj001u47057u8hej9u', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl107b717d92362218aa387539', 'cmgs0i4je002e4705ndrhe26e', 'cmgujpyjf002q4705j6hv1nkk', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl5ee1bef2ef4b88f83a680526', 'cmgs0i4je002e4705ndrhe26e', 'cmgs1s4pv002j470567o60oqe', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl49eb01162df881cc87bc281f', 'cmgs0i4je002e4705ndrhe26e', 'cmgrnxlx5000g47059oyj4yuw', 4, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl1369592934d41016018dbbbf', 'cmgs0i4je002e4705ndrhe26e', 'cmgujpyjf002q4705j6hv1nkk', 5, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cld7bf0d8c46d0037cbb16cd69', 'cmgs0i4je002e4705ndrhe26e', 'cmgs1s4pv002j470567o60oqe', 6, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl75660a7f65559f164df06538', 'cmgs0i4je002e4705ndrhe26e', 'cm_motoreducteur_frein_01', 7, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl859ade5b78b6e833d1eb64f8', 'cmgs0i4je002e4705ndrhe26e', 'cm_motoreducteur_frein_01', 8, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cld23bbbeff05e238c6d189da0', 'cmgs0i4je002e4705ndrhe26e', 'cmgukvztv002s4705kqvqjtvg', 9, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl5e7a3eeeb8a6283ce828edf4', 'cmgs0i4je002e4705ndrhe26e', 'cmgukvztv002s4705kqvqjtvg', 10, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl52149961cdaed3f614dd87c1', 'cmgs0i4je002e4705ndrhe26e', 'cmgukxw26002t4705qz4ul929', 11, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cle1d646b02f65c7a5fd276ecf', 'cmgs0i4je002e4705ndrhe26e', 'cmgum1ih0000347ff7bsldmnv', 12, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl07b21cfdc0f2e9d8c11c79ed', 'cmgs0i4je002e4705ndrhe26e', 'cmgulzr7b000247ffpr2vsput', 13, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clb726dc1c94d371d8860ef92d', 'cmgs0i4je002e4705ndrhe26e', 'cmgum1wsl000447ffa109dtag', 14, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cle5605528c6fbb3de1f72b8d8', 'cmgujizjf002o4705kfdea5yw', 'cmgytewe0002447ffup09bscr', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clbd602414e77f0309cfeb92c6', 'cmgujizjf002o4705kfdea5yw', 'cmgytewe0002447ffup09bscr', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl604a3fa207118914f58f5ffb', 'cmgujizjf002o4705kfdea5yw', 'cmgytewe0002447ffup09bscr', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl016fd78557d8ac5ce883208b', 'cmgujizjf002o4705kfdea5yw', 'cmgytewe0002447ffup09bscr', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl29eeddd05188eec4e782aca3', 'cmgujizjf002o4705kfdea5yw', 'cmgytewe0002447ffup09bscr', 4, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl7e7a30ba2126ecfed7f7501c', 'cmgujizjf002o4705kfdea5yw', 'cmgz0qu29004a47ffw1bmjr75', 5, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clb893d014b683a1851425436e', 'cmgujizjf002o4705kfdea5yw', 'cmgz0qu29004a47ffw1bmjr75', 6, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl0b0094ca0a92321f5fa2131c', 'cmgujizjf002o4705kfdea5yw', 'cmgz0v9k4004v47ff8apimo50', 7, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clea857995e00ebb10548095c2', 'cmgujizjf002o4705kfdea5yw', 'cmgz0v9k4004v47ff8apimo50', 8, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl1341b6e156f059e852f719e2', 'cmgujizjf002o4705kfdea5yw', 'cmgz0v9k4004v47ff8apimo50', 9, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cle18462b08b605f31fec6cc14', 'cmgujizjf002o4705kfdea5yw', 'cmgz0v9k4004v47ff8apimo50', 10, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clf3f7ad1fcb6eb3937b96bb8e', 'cmgujizjf002o4705kfdea5yw', 'cmgz0zs4m006447ffq5b20ch3', 11, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clfdd9dfcc06816afd8bf8503a', 'cmgujizjf002o4705kfdea5yw', 'cmgz0zs4m006447ffq5b20ch3', 12, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl4af9588815f4a5a21cf4f979', 'cmgujizjf002o4705kfdea5yw', 'cmgz0zs4m006447ffq5b20ch3', 13, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl0e0d7053e7a0518b50f00481', 'cmgujizjf002o4705kfdea5yw', 'cmgz0zs4m006447ffq5b20ch3', 14, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl4f64c44ed0ffc92a923edccc', 'cmgujizjf002o4705kfdea5yw', 'cmgz17bpz006t47ff58i3j1e1', 15, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clfbdc4656e36b47a8e6a5df68', 'cmgujizjf002o4705kfdea5yw', 'cmgz17bpz006t47ff58i3j1e1', 16, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl4860204e0478d9f4eb806b85', 'cmgujjmpo002p470523lbfqmp', 'cmgum1wsl000447ffa109dtag', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl5e3367975bfef877130e41af', 'cmgujjmpo002p470523lbfqmp', 'cmgum1wsl000447ffa109dtag', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl0c004dd01a3f4a978c8260c9', 'cmh0kmyh1000847s5ciu9agzo', 'cmgujpyjf002q4705j6hv1nkk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl96109b5962d80d95d279fd96', 'cmh0kmyh1000847s5ciu9agzo', 'cmgytewe0002447ffup09bscr', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl5a6e124fa8ec83ae5ef21685', 'cmh20wuye001o47s5auvmq7s8', 'cmgujpyjf002q4705j6hv1nkk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clba2c60a3a4aab6a82eccad61', 'cmh20x49u001q47s5l9ahnvms', 'cmgs1sco0002k47056yq8eyfq', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clbf346fb41ad28af45eb5e7aa', 'cmh20x49u001q47s5l9ahnvms', 'cmgs1sco0002k47056yq8eyfq', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl50ec69f08be67181d3cd5dc3', 'cmh20yrgb001v47s54uxvi6km', 'cmgs1sco0002k47056yq8eyfq', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl84cab14314d4932ef52e9d47', 'cmh20yrgb001v47s54uxvi6km', 'cmgujpyjf002q4705j6hv1nkk', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cle258e4bb4ae12d90aeb49ae7', 'cmh20yrgb001v47s54uxvi6km', 'cmgujpyjf002q4705j6hv1nkk', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl697c36fe5e29f3cfdb209537', 'cmh20yrgb001v47s54uxvi6km', 'cmgujpyjf002q4705j6hv1nkk', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl112cc747d5ac2c68716bd6d4', 'cmh23pwbo002947s5ide6zx7g', 'cmgujpyjf002q4705j6hv1nkk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl62fe319d24840a318d24b682', 'cmkqpdc7a001o1eq6iwqvi3jk', 'cmgujpyjf002q4705j6hv1nkk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl0051b0a075490c406f21213e', 'cmkqq45yq00251eq6k1z0x7kt', 'cmgujpyjf002q4705j6hv1nkk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clc578afcad0f42694f05408f0', 'cmkqqdogo002l1eq6vy26j33g', 'cmgujpyjf002q4705j6hv1nkk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl0a3a926192f808233c894c43', 'cmkqqdogo002l1eq6vy26j33g', 'cmh9bykt8001j47v7g0oej5dw', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl8a927a5de52db535ba8f062a', 'cmkqqdogo002l1eq6vy26j33g', 'cmhabzypq003h47v7jyjjxst1', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl8f60c43f25ec1c96560209b0', 'cmkqqdogo002l1eq6vy26j33g', 'cmgz0zs4m006447ffq5b20ch3', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl5c71200fd7efe31c9764e5d8', 'cmkqqdogo002l1eq6vy26j33g', 'cmkdqtcpv001r1e2wptehmkxi', 4, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clf6147721769314755e9ff6b8', 'cmkqqdogo002l1eq6vy26j33g', 'cmhbve5h30016475utwgpa32k', 5, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Data for Name: skeleton_product_requirements; Type: TABLE DATA; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO public.skeleton_product_requirements (id, modeltypeid, typeproductid, familycode, position, createdat, updatedat) VALUES ('cle44c43c22390db28f97b1c17', 'cmkqqdogo002l1eq6vy26j33g', 'cmhn9ze17000147s81dlr4i3v', 'graisse', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Data for Name: skeleton_subcomponent_requirements; Type: TABLE DATA; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl9806dc8b4b42ea0c84b6832b', 'cmgs0i4je002e4705ndrhe26e', NULL, 'Kit', 'kit', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl67fc8aef811eb876a1cde9ed', 'cmgs0i4je002e4705ndrhe26e', NULL, 'Kit', 'kit', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl6cff73d32c4f16b5d4e909ab', 'cmgujjmpo002p470523lbfqmp', NULL, 'Kit', 'kit', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl7f83953a8f78f951f3a3628c', 'cmgz1az8d007g47fflwxk3q95', NULL, 'Contrôleur de rotation', 'controleur-de-rotation', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cle79fbc1bedd4123874a35377', 'cmgz1az8d007g47fflwxk3q95', NULL, 'Kit', 'kit', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl23c07d5454e5f2bc6d7703fa', 'cmgz1az8d007g47fflwxk3q95', NULL, 'Kit', 'kit', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cla3530a303f28a12d97883d82', 'cmgz1az8d007g47fflwxk3q95', NULL, 'Contrôleur de rotation', 'controleur-de-rotation', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('clefbd652fc2561a180f38dd89', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Tête convoyeur à bande', 'tcb', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl70978104735f0e8addcbb494', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Pied convoyeur à bande', 'PCB', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl63db65c869d07fd36d0d3422', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Elément intermédiaire & coude', 'EIC', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('clac7e30f33f20287f76197c9e', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Trémie d''alimentation', 'Tremie', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl081ba5565b6e9320be0c88b0', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Chariot Déverseur', 'Chariot', 4, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl164ed242e2148aac3b30c391', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Commande moteur', 'commande-moteur', 5, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl7aa47c4c4e4070ad1925ee27', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Déport de bande', 'deport-de-bande-et-controleur-de-rotation', 6, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl7c497e23a2deb7e85a2c2abe', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Contrôleur de rotation', 'controleur-de-rotation', 7, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl4a4fc72af5a46c4d369f535e', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Contrôleur de rotation', 'controleur-de-rotation', 8, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('clce09e33e0f970116c87057c7', 'cmh0kmyh1000847s5ciu9agzo', 'cmh20z6g4001x47s5b5hturac', 'Paliers', 'paliers', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('clb5e1e758aaed9688a4e7db60', 'cmh0kmyh1000847s5ciu9agzo', 'cmh4x8m4k000047nko0vwavbg', 'Roulement', 'roulement', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: products; Type: TABLE DATA; Schema: public; Owner: -
|
-- Data for Name: products; Type: TABLE DATA; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -178,6 +286,111 @@ INSERT INTO public.composants (id, name, reference, prix, createdat, updatedat,
|
|||||||
INSERT INTO public.composants (id, name, reference, prix, createdat, updatedat, typecomposantid, structure, productid) VALUES ('cl9b1583768c7c9fe6cfe93a11', 'testcor', 'll', 3.00, '2026-01-25 11:28:27', '2026-01-25 11:28:27', 'cle48e33ef67853069badfc5f0', '{"path":"root","definition":[],"pieces":[{"path":"root:piece-0","definition":{"typePieceId":"cmgs1sco0002k47056yq8eyfq","typePieceLabel":"Bavette"},"selectedPieceId":"cmgs1tfza002m4705mbl0kwok"}],"products":[{"path":"root:product-0","definition":{"typeProductId":"cmhn9zrm5000247s8ds3bmpaf","familyCode":"lubrifiant"},"selectedProductId":"cmkp97us6007k1e2ws0ogux1m"}],"subcomponents":[{"path":"root:sub-0","definition":{"alias":"Tr\u00e9mie d''alimentation","typeComposantId":"cmgs0htn5002d4705tyxxiqb2","familyCode":"Tremie"},"selectedComponentId":"cmgz5h2s0009v47ff6x26cqry"}]}', 'cmkp97us6007k1e2ws0ogux1m');
|
INSERT INTO public.composants (id, name, reference, prix, createdat, updatedat, typecomposantid, structure, productid) VALUES ('cl9b1583768c7c9fe6cfe93a11', 'testcor', 'll', 3.00, '2026-01-25 11:28:27', '2026-01-25 11:28:27', 'cle48e33ef67853069badfc5f0', '{"path":"root","definition":[],"pieces":[{"path":"root:piece-0","definition":{"typePieceId":"cmgs1sco0002k47056yq8eyfq","typePieceLabel":"Bavette"},"selectedPieceId":"cmgs1tfza002m4705mbl0kwok"}],"products":[{"path":"root:product-0","definition":{"typeProductId":"cmhn9zrm5000247s8ds3bmpaf","familyCode":"lubrifiant"},"selectedProductId":"cmkp97us6007k1e2ws0ogux1m"}],"subcomponents":[{"path":"root:sub-0","definition":{"alias":"Tr\u00e9mie d''alimentation","typeComposantId":"cmgs0htn5002d4705tyxxiqb2","familyCode":"Tremie"},"selectedComponentId":"cmgz5h2s0009v47ff6x26cqry"}]}', 'cmkp97us6007k1e2ws0ogux1m');
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Data for Name: composant_piece_slots; Type: TABLE DATA; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clcb580f2bc13c2f6ffefdfe47', 'cmgz53uvt009s47ff9v0uklr6', 'cmgrnu6zc000f470565mc8hha', 'cmgrnzbku000h4705qrj5eujb', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cleac90912bbe1571ec196c3c9', 'cmgz53uvt009s47ff9v0uklr6', 'cmgrohigo000z4705q8yvpih0', 'cmgrp2sju00144705n8etw7im', 1, 2, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl7b2de7e0f607306317eb6e69', 'cmgz53uvt009s47ff9v0uklr6', 'cmgrou6670011470586ipgylm', 'cmgrp1ry9001347052qn8q2yo', 1, 3, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl8a1a4da80556a964e7267773', 'cmgz53uvt009s47ff9v0uklr6', 'cmgroij2f00104705t6y33enk', 'cmgrp3lhv00194705f1xp8j0m', 1, 4, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl51a23d93f8c8fbf0555d8d4e', 'cmgz53uvt009s47ff9v0uklr6', 'cmgrnu6zc000f470565mc8hha', 'cmgz516t7009n47fft3nfyt34', 1, 5, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clb7caab438a04ccdbe72c7662', 'cmgz5ef4h009t47ffmxveesp0', 'cmgrzuwkj001u47057u8hej9u', 'cmgs07df2001w4705ry79yvbo', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl4e710917a9cc405aea45eae4', 'cmgz5ef4h009t47ffmxveesp0', 'cmgrzuwkj001u47057u8hej9u', 'cmgrzvdmo001v47050tvf2z88', 1, 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl0b03faf21c00093950c06434', 'cmgz5ef4h009t47ffmxveesp0', 'cmgrzuwkj001u47057u8hej9u', 'cmgs08kjb00234705wc5tytxg', 1, 2, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl2151fb8b1d31e27aeea1ba63', 'cmgz5ef4h009t47ffmxveesp0', 'cmgroij2f00104705t6y33enk', 'cmgrp3lhv00194705f1xp8j0m', 1, 3, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl1fb766a77cd4c06b0dd6f68c', 'cmgz5ef4h009t47ffmxveesp0', 'cmgroij2f00104705t6y33enk', 'cmgs0bive00274705zjmiuwzo', 1, 4, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clbb1e23854540497ce8a0cc04', 'cmgz5fsvz009u47ffkrardb1u', 'cmgroij2f00104705t6y33enk', 'cmgrp3lhv00194705f1xp8j0m', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl3410d53157d1426a7c0fee70', 'cmgz5fsvz009u47ffkrardb1u', 'cmgs0kd5o002f47053b7n8tw6', 'cmgs0nyk7002g4705rteyvw7x', 1, 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl6a3fbc2af5b82193cb7eb86c', 'cmgz5fsvz009u47ffkrardb1u', 'cmgrzuwkj001u47057u8hej9u', 'cmgrzvdmo001v47050tvf2z88', 1, 2, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cle60fa4fef139b2e86fc5bf47', 'cmgz5fsvz009u47ffkrardb1u', 'cmgroij2f00104705t6y33enk', 'cmgs0bive00274705zjmiuwzo', 1, 3, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('claf42f4acd9a024263ffb60c0', 'cmgz5h2s0009v47ff6x26cqry', 'cmgs13jjp002h4705rjqzz5lh', 'cmgs14c7a002i4705t1w4qdfx', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl44cc1edd9d00de1a675810d6', 'cmgz5h2s0009v47ff6x26cqry', 'cmgs1s4pv002j470567o60oqe', 'cmgs1swl0002l4705gpyg1yyn', 1, 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl0b8eb4a8defab5cde3bccd3d', 'cmgz5h2s0009v47ff6x26cqry', 'cmgs1sco0002k47056yq8eyfq', 'cmgs1tfza002m4705mbl0kwok', 1, 2, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl86e3c1391165cd65555856c1', 'cmgz5h2s0009v47ff6x26cqry', 'cmgs1sco0002k47056yq8eyfq', 'cmgs1tvrs002n4705gpym7vel', 1, 3, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clbc122534f74362e26882cfd1', 'cmgz79ivv009x47ffeh6of72i', 'cmgroij2f00104705t6y33enk', 'cmgrp3lhv00194705f1xp8j0m', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cla3b28a687250b61e1344dcc3', 'cmgz79ivv009x47ffeh6of72i', 'cmgrzuwkj001u47057u8hej9u', 'cmgum5zm0000547ffzg8ofiqr', 1, 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl8e42ccab069fa0ecd80a2cde', 'cmgz79ivv009x47ffeh6of72i', 'cmgujpyjf002q4705j6hv1nkk', 'cmgum9bn4000847fffbazanc5', 1, 2, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl786b5dc9712d2488ee7d1258', 'cmgz79ivv009x47ffeh6of72i', 'cmgs1s4pv002j470567o60oqe', 'cmgyruhgm000947ffmhhrqdrl', 1, 3, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl168e7708c9d3c91d5a404a5e', 'cmgz79ivv009x47ffeh6of72i', 'cmgujpyjf002q4705j6hv1nkk', 'cmgyrzrbc000h47ffd670wu8j', 1, 5, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl1fd495eaa3cec6667cfab566', 'cmgz79ivv009x47ffeh6of72i', 'cmgs1s4pv002j470567o60oqe', 'cmgys0mgx000i47ffxbftvqt4', 1, 6, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clbf85161d0d8ffef1d74220b6', 'cmgz79ivv009x47ffeh6of72i', 'cm_motoreducteur_frein_01', 'cmgys1osr000j47fftblpdpu2', 1, 7, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cld72b4dce93cb64a7909f3db6', 'cmgz79ivv009x47ffeh6of72i', 'cm_motoreducteur_frein_01', 'cmgys3ugw001147ffq33udxaw', 1, 8, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cle12f0173310ab9c752f8fb00', 'cmgz79ivv009x47ffeh6of72i', 'cmgukvztv002s4705kqvqjtvg', 'cmgys4a2s001847ffhqhz7zcd', 1, 9, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl5782cb522ada0d1de03c4d86', 'cmgz79ivv009x47ffeh6of72i', 'cmgukvztv002s4705kqvqjtvg', 'cmgys6k6b001h47ffuq44ze37', 1, 10, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl5d8c4c8e361ccdbaa041e5b9', 'cmgz79ivv009x47ffeh6of72i', 'cmgukxw26002t4705qz4ul929', 'cmgys7anf001m47ff0ulcp092', 1, 11, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clb1c4e1e8f6a00f1b11f24884', 'cmgz79ivv009x47ffeh6of72i', 'cmgum1ih0000347ff7bsldmnv', 'cmgys8mjl001r47ff5f8z85fs', 1, 12, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl4f4f675958d1667659498076', 'cmgz79ivv009x47ffeh6of72i', 'cmgulzr7b000247ffpr2vsput', 'cmgysatbl001u47ffu55db8gg', 1, 13, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cla3442d7af57410a430dd3c7e', 'cmgz79ivv009x47ffeh6of72i', 'cmgum1wsl000447ffa109dtag', 'cmgysj6wn002347ffgq3f98dr', 1, 14, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl0c706745220aa12658edc03c', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgytewe0002447ffup09bscr', 'cmgytmhw0002547ffzobsmpaa', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clca63f8c3aae40ebd68cc55b7', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgytewe0002447ffup09bscr', 'cmgytqtc8002u47ffum90ylo5', 1, 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cle09fbef03d0edcd66b0a4a0c', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgytewe0002447ffup09bscr', 'cmgyts8s9003747ffd1h8husf', 1, 2, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl57b44902fd7409abccdab4ae', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgytewe0002447ffup09bscr', 'cmgytuf06003k47ffdr8lvp13', 1, 3, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl47ee8065661468bc113d200a', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgytewe0002447ffup09bscr', 'cmgytx2ul003x47ffhdpurtx5', 1, 4, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl6cbbd1f5046e9dfc1a89a358', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0v9k4004v47ff8apimo50', 'cmgz0w66p004w47ffvj6xcxmo', 1, 7, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cle4cb39506f39efaf51af3e9e', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0v9k4004v47ff8apimo50', 'cmgz0xbx5005d47ffjkrafetg', 1, 8, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl8e47336d9326ac471ae821e7', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0v9k4004v47ff8apimo50', 'cmgz0y2aw005m47ff4zkjczei', 1, 9, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl5ae18ec62736f96d273353ec', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0v9k4004v47ff8apimo50', 'cmgz0yt33005v47ffy4p8d28z', 1, 10, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl524831b7cfa901ddc72e1728', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0zs4m006447ffq5b20ch3', 'cmgz10g67006547ffj28sqequ', 1, 11, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl4198321b8039385517c118ba', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0zs4m006447ffq5b20ch3', 'cmgz112hd006e47ffvg37mkoq', 1, 12, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl9fab43583266bb8abfe94d4d', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0zs4m006447ffq5b20ch3', 'cmgz11p1k006j47ffhjqgrnkp', 1, 13, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cled20136adaf599c72facf1a5', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0zs4m006447ffq5b20ch3', 'cmgz128lz006o47ffwfgtag7e', 1, 14, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clca666296d8db92cabc1299d6', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz17bpz006t47ff58i3j1e1', 'cmgz17w9w006u47ffg6db710j', 1, 15, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl565591562f915536f015cf0a', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz17bpz006t47ff58i3j1e1', 'cmgz18vw1007947ffr2wg86sa', 1, 16, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl8a5cc24bf84edeaf24b87405', 'cmgz7igun009z47ffhea93fbw', 'cmgum1wsl000447ffa109dtag', 'cmgz1c9wx007h47ffr41untmr', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cle6cc6a2990d51326b7a16ac2', 'cmgz7igun009z47ffhea93fbw', 'cmgum1wsl000447ffa109dtag', 'cmgz1erci007r47ffybtdepul', 1, 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clf8105cc1fa7b62c4616cf509', 'cmh314rnj002q47s5cr3n6445', 'cmgujpyjf002q4705j6hv1nkk', 'cmh313676002d47s5li4e6qt9', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cla7987d4fe2fb2bf8ebe8ce48', 'cmh3jnikd002147zbmmnx2qw8', 'cmgujpyjf002q4705j6hv1nkk', 'cmh31ejnw003b47s548rtk8b1', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clb39042e520687e6c6b51f788', 'cl10c0924d10135c5f515378ac', 'cmgujpyjf002q4705j6hv1nkk', 'cl7b3702f04d24d87e47232a14', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl50fa6580ea24bfbcfb018cac', 'cl10eedbb54a0d2cd0fa3ce9c6', 'cmgujpyjf002q4705j6hv1nkk', 'cle1db7051dbef91fc009073a6', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl2289dce68b996d2c0b56cd49', 'cl36d84884cad86fbc92dba133', 'cmgujpyjf002q4705j6hv1nkk', 'clbf9f0070ebd464b3c309c646', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cla130de58dd731b934e1f9789', 'cl3dbac5194bc192a0589465ba', 'cmgujpyjf002q4705j6hv1nkk', 'cl50fe870a07e42759b37b511f', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl75bfb7413b5df226846ab367', 'cl4660bae41d2af254e6c3b726', 'cmgujpyjf002q4705j6hv1nkk', 'cl4e975566464253882018adcc', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl076c31311e25600aae5d07e2', 'cl54b1b4509971fde475572b29', 'cmgujpyjf002q4705j6hv1nkk', 'cl731386df55fcb9e6a01e0a63', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl4874e08b827307a1313a71b5', 'cl5a8f9656aa7e14c012f30700', 'cmgujpyjf002q4705j6hv1nkk', 'cl5ee293dc7b61feba510082a4', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl6935b95a12cd846b6a3bcbfc', 'cl5b5e336095de8d4ece81b2dc', 'cmgujpyjf002q4705j6hv1nkk', 'cldd656c6092225f53a22badc0', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clf30ffdeac20200df2d190c45', 'cl5e9c6b18bccd38517026dc1c', 'cmgujpyjf002q4705j6hv1nkk', 'clfa3147270e5e66f9b52c425e', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clb8fa63511f6514497e8c1a5f', 'cl7df36c9e7391df3d4ff46102', 'cmgujpyjf002q4705j6hv1nkk', 'cl1406ef19de58fdd1adf40221', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl2251745f51b584dd8bd73aad', 'cl7f254c23161d9c853c3e6d92', 'cmgujpyjf002q4705j6hv1nkk', 'clf16e543545eddd01b20077df', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cle99713d099f9a006931a7863', 'cl8b9b36f5a822aae21edb5a5f', 'cmgujpyjf002q4705j6hv1nkk', 'cl6667d159f6d07ba77fa79b39', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl469909161ea4061d1b514f06', 'cla833681664bb851ca61aca51', 'cmgujpyjf002q4705j6hv1nkk', 'cl8570d729efd017c12a2d5c3d', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clf9f5aaccf25ef3a54ace17ad', 'clba5633e840726188261145f9', 'cmgujpyjf002q4705j6hv1nkk', 'clafaa71cbf49777fbb8415f19', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cld23a4f8bfc3894842b531b47', 'clbd1e945fb222e1c56dd43941', 'cmgujpyjf002q4705j6hv1nkk', 'clc08fbdcd334ed869772d98ee', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl88ea31be5c6114d3267daaad', 'clbe710810fd7ccd09811957b3', 'cmgujpyjf002q4705j6hv1nkk', 'cmh313676002d47s5li4e6qt9', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cldb0304d3089db7aced359a4f', 'cldd7f161d2cd08ee54e79161e', 'cmgujpyjf002q4705j6hv1nkk', 'cl22c13dbc4d38a1f846323ae6', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl6e857d06dd103028af452bf6', 'cle98225ad3a32f5d8531950ef', 'cmgujpyjf002q4705j6hv1nkk', 'cl531dde45c3fc64c1a3b16ca0', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Data for Name: composant_subcomponent_slots; Type: TABLE DATA; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl114d5a6febff03d69286b6b7', 'cmgz79ivv009x47ffeh6of72i', 'Kit', 'kit', NULL, 'cmgz49bm2009547ff7ham94wz', 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl1d648f7c5a3b0ee64b22e69b', 'cmgz79ivv009x47ffeh6of72i', 'Kit', 'kit', NULL, 'cmgz4bczt009647ffzidmc8tc', 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl710c40366df98d7aeb547457', 'cmgz7igun009z47ffhea93fbw', 'Kit', 'kit', NULL, 'cmgz4equ5009747ffq665rpeb', 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl23ac52af05e90d9c7d2daba2', 'cmh0d59v5000347s561ahbept', 'Tête convoyeur à bande', 'tcb', NULL, 'cmgz53uvt009s47ff9v0uklr6', 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl5c1f5ff5fb762ffb1c62d02c', 'cmh0d59v5000347s561ahbept', 'Pied convoyeur à bande', 'PCB', NULL, 'cmgz5ef4h009t47ffmxveesp0', 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl9a90d9876471267669c5d83b', 'cmh0d59v5000347s561ahbept', 'Elément intermédiaire & coude', 'EIC', NULL, 'cmgz5fsvz009u47ffkrardb1u', 2, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('claeaa69eb72eff09abe7ef3fb', 'cmh0d59v5000347s561ahbept', 'Trémie d''alimentation', 'Tremie', NULL, 'cmgz5h2s0009v47ff6x26cqry', 3, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('clfcb74e5cc092b753c65f74f8', 'cmh0d59v5000347s561ahbept', 'Chariot Déverseur', 'Chariot', NULL, 'cmgz79ivv009x47ffeh6of72i', 4, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl8bb434545f2bca42d73d588e', 'cmh0d59v5000347s561ahbept', 'Commande moteur', 'commande-moteur', NULL, 'cmgz7fd3l009y47fff1l4g0p0', 5, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cle71559358e6346f59657d3d5', 'cmh0d59v5000347s561ahbept', 'Déport de bande', 'deport-de-bande-et-controleur-de-rotation', NULL, 'cmgz7igun009z47ffhea93fbw', 6, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl4a7d21f525c63c0581af5fc6', 'cmh0d59v5000347s561ahbept', 'Contrôleur de rotation', 'controleur-de-rotation', NULL, 'cmgz4qzap009b47ffj7ch6th7', 7, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl9f39d9a471c6d1d9ae4c4654', 'cmh0d59v5000347s561ahbept', 'Contrôleur de rotation', 'controleur-de-rotation', NULL, 'cmgz4r99x009c47ffco9f2img', 8, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Data for Name: composant_product_slots; Type: TABLE DATA; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Data for Name: piece_products; Type: TABLE DATA; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: constructeurs; Type: TABLE DATA; Schema: public; Owner: -
|
-- Data for Name: constructeurs; Type: TABLE DATA; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -257,121 +470,121 @@ INSERT INTO public.machines (id, name, reference, prix, createdat, updatedat, si
|
|||||||
-- Data for Name: pieces; Type: TABLE DATA; Schema: public; Owner: -
|
-- Data for Name: pieces; Type: TABLE DATA; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgrp1ry9001347052qn8q2yo', 'Lame raclette', 'P40S069915', NULL, '2025-10-15 07:53:07.52', '2025-10-15 07:53:07.52', 'cmgrou6670011470586ipgylm', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgrp1ry9001347052qn8q2yo', 'Lame raclette', 'P40S069915', NULL, '2025-10-15 07:53:07.52', '2025-10-15 07:53:07.52', 'cmgrou6670011470586ipgylm', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgrp46ud001i4705nvphpv0f', 'Palier applique', 'X21000923', NULL, '2025-10-15 07:55:00.132', '2025-10-15 07:55:00.132', 'cmgrnxlx5000g47059oyj4yuw', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgrp46ud001i4705nvphpv0f', 'Palier applique', 'X21000923', NULL, '2025-10-15 07:55:00.132', '2025-10-15 07:55:00.132', 'cmgrnxlx5000g47059oyj4yuw', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgrp2sju00144705n8etw7im', 'Bras tendeur SE18', 'X56654', NULL, '2025-10-15 07:53:54.954', '2025-10-15 12:54:18.646', 'cmgrohigo000z4705q8yvpih0', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgrp2sju00144705n8etw7im', 'Bras tendeur SE18', 'X56654', NULL, '2025-10-15 07:53:54.954', '2025-10-15 12:54:18.646', 'cmgrohigo000z4705q8yvpih0', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs08kjb00234705wc5tytxg', 'Cage d''écureuil de tension', 'W57719', NULL, '2025-10-15 13:06:20.278', '2025-10-15 13:06:20.278', 'cmgrzuwkj001u47057u8hej9u', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs08kjb00234705wc5tytxg', 'Cage d''écureuil de tension', 'W57719', NULL, '2025-10-15 13:06:20.278', '2025-10-15 13:06:20.278', 'cmgrzuwkj001u47057u8hej9u', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh3h3c82001f47zbvfmcu17d', 'Auget tôle', NULL, NULL, '2025-10-23 13:43:37.634', '2025-10-23 13:43:37.634', NULL, NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh3h3c82001f47zbvfmcu17d', 'Auget tôle', NULL, NULL, '2025-10-23 13:43:37.634', '2025-10-23 13:43:37.634', NULL, NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs0bive00274705zjmiuwzo', 'Rouleau1', 'X24001026', NULL, '2025-10-15 13:08:38.087', '2025-10-15 13:08:38.087', 'cmgroij2f00104705t6y33enk', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs0bive00274705zjmiuwzo', 'Rouleau1', 'X24001026', NULL, '2025-10-15 13:08:38.087', '2025-10-15 13:08:38.087', 'cmgroij2f00104705t6y33enk', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs1tfza002m4705mbl0kwok', 'Bavette alimentation', 'P30W07069', NULL, '2025-10-15 13:50:33.766', '2025-10-15 13:50:33.766', 'cmgs1sco0002k47056yq8eyfq', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs1tfza002m4705mbl0kwok', 'Bavette alimentation', 'P30W07069', NULL, '2025-10-15 13:50:33.766', '2025-10-15 13:50:33.766', 'cmgs1sco0002k47056yq8eyfq', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs1tvrs002n4705gpym7vel', 'Bavette centrage', 'P30W07052', NULL, '2025-10-15 13:50:54.232', '2025-10-15 13:50:54.232', 'cmgs1sco0002k47056yq8eyfq', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs1tvrs002n4705gpym7vel', 'Bavette centrage', 'P30W07052', NULL, '2025-10-15 13:50:54.232', '2025-10-15 13:50:54.232', 'cmgs1sco0002k47056yq8eyfq', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgys8mjl001r47ff5f8z85fs', 'Attache rapide 19.05S', 'X10000565', NULL, '2025-10-20 06:56:49.185', '2025-10-20 06:56:49.185', 'cmgum1ih0000347ff7bsldmnv', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgys8mjl001r47ff5f8z85fs', 'Attache rapide 19.05S', 'X10000565', NULL, '2025-10-20 06:56:49.185', '2025-10-20 06:56:49.185', 'cmgum1ih0000347ff7bsldmnv', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgytmhw0002547ffzobsmpaa', 'Moteur éléctrique', 'X50001591', NULL, '2025-10-20 07:35:35.925', '2025-10-20 07:35:35.925', 'cmgytewe0002447ffup09bscr', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgytmhw0002547ffzobsmpaa', 'Moteur éléctrique', 'X50001591', NULL, '2025-10-20 07:35:35.925', '2025-10-20 07:35:35.925', 'cmgytewe0002447ffup09bscr', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgyts8s9003747ffd1h8husf', 'Moteur éléctrique2', 'X50001596', NULL, '2025-10-20 07:40:04.088', '2025-10-20 07:40:04.088', 'cmgytewe0002447ffup09bscr', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgyts8s9003747ffd1h8husf', 'Moteur éléctrique2', 'X50001596', NULL, '2025-10-20 07:40:04.088', '2025-10-20 07:40:04.088', 'cmgytewe0002447ffup09bscr', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz0rm8j004b47ffg2bh2ort', 'Réducteur1', 'X28896', NULL, '2025-10-20 10:55:32.179', '2025-10-20 10:55:32.179', 'cmgz0qu29004a47ffw1bmjr75', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz0rm8j004b47ffg2bh2ort', 'Réducteur1', 'X28896', NULL, '2025-10-20 10:55:32.179', '2025-10-20 10:55:32.179', 'cmgz0qu29004a47ffw1bmjr75', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz0se74004o47ffnbbtu66b', 'Réducteur2', 'X15009329', NULL, '2025-10-20 10:56:08.416', '2025-10-20 10:56:08.416', 'cmgz0qu29004a47ffw1bmjr75', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz0se74004o47ffnbbtu66b', 'Réducteur2', 'X15009329', NULL, '2025-10-20 10:56:08.416', '2025-10-20 10:56:08.416', 'cmgz0qu29004a47ffw1bmjr75', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh99o05y001547v7az12sk2n', 'Moteur à flasque', NULL, NULL, '2025-10-27 15:02:21.884', '2025-10-27 15:02:21.884', 'cmgytewe0002447ffup09bscr', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh99o05y001547v7az12sk2n', 'Moteur à flasque', NULL, NULL, '2025-10-27 15:02:21.884', '2025-10-27 15:02:21.884', 'cmgytewe0002447ffup09bscr', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz0w66p004w47ffvj6xcxmo', 'Poulie1', 'X53433', NULL, '2025-10-20 10:59:04.657', '2025-10-20 10:59:26.075', 'cmgz0v9k4004v47ff8apimo50', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz0w66p004w47ffvj6xcxmo', 'Poulie1', 'X53433', NULL, '2025-10-20 10:59:04.657', '2025-10-20 10:59:26.075', 'cmgz0v9k4004v47ff8apimo50', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz17w9w006u47ffg6db710j', 'Courroie', 'X47067', NULL, '2025-10-20 11:08:11.684', '2025-10-20 11:08:11.684', 'cmgz17bpz006t47ff58i3j1e1', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz17w9w006u47ffg6db710j', 'Courroie', 'X47067', NULL, '2025-10-20 11:08:11.684', '2025-10-20 11:08:11.684', 'cmgz17bpz006t47ff58i3j1e1', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhaesmf5003v47v7bub04g9p', 'Joint à lèvre', 'J41800-RLX', 44.00, '2025-10-28 10:13:41.607', '2025-10-28 10:13:41.607', 'cmhabzypq003h47v7jyjjxst1', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhaesmf5003v47v7bub04g9p', 'Joint à lèvre', 'J41800-RLX', 44.00, '2025-10-28 10:13:41.607', '2025-10-28 10:13:41.607', 'cmhabzypq003h47v7jyjjxst1', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhbtjqbt0000475ultd24cp0', 'Segment d''arrêt - Circlips', 'S41800-SA2', 37.00, '2025-10-29 09:54:27.209', '2025-10-29 09:54:27.209', 'cmhalh6sa004h47v7y6pnqok2', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhbtjqbt0000475ultd24cp0', 'Segment d''arrêt - Circlips', 'S41800-SA2', 37.00, '2025-10-29 09:54:27.209', '2025-10-29 09:54:27.209', 'cmhalh6sa004h47v7y6pnqok2', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz18vw1007947ffr2wg86sa', 'Courroie2', 'X53480', NULL, '2025-10-20 11:08:57.84', '2025-10-20 11:08:57.84', 'cmgz17bpz006t47ff58i3j1e1', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz18vw1007947ffr2wg86sa', 'Courroie2', 'X53480', NULL, '2025-10-20 11:08:57.84', '2025-10-20 11:08:57.84', 'cmgz17bpz006t47ff58i3j1e1', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhbv3kj9000t475uzbqpult7', 'Rondelle frein MB20', 'RDLMB20', 7.00, '2025-10-29 10:37:52.437', '2025-10-29 10:37:52.437', 'cmhbuzjrz000s475ue0q2o2xd', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhbv3kj9000t475uzbqpult7', 'Rondelle frein MB20', 'RDLMB20', 7.00, '2025-10-29 10:37:52.437', '2025-10-29 10:37:52.437', 'cmhbuzjrz000s475ue0q2o2xd', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhd56h02002a475uunel89e0', 'Manille', NULL, NULL, '2025-10-30 08:07:50.161', '2025-10-30 08:07:50.161', 'cmhd55caa0029475u1t4vg1i2', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhd56h02002a475uunel89e0', 'Manille', NULL, NULL, '2025-10-30 08:07:50.161', '2025-10-30 08:07:50.161', 'cmhd55caa0029475u1t4vg1i2', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhkr1ulr000147yv73imk2nx', 'Rondelle plate M4 RVS-A2', NULL, NULL, '2025-11-04 15:54:29.296', '2025-11-04 16:49:00.924', 'cmhbuzjrz000s475ue0q2o2xd', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhkr1ulr000147yv73imk2nx', 'Rondelle plate M4 RVS-A2', NULL, NULL, '2025-11-04 15:54:29.296', '2025-11-04 16:49:00.924', 'cmhbuzjrz000s475ue0q2o2xd', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh313676002d47s5li4e6qt9', 'Arbre', NULL, NULL, '2025-10-23 06:15:35.969', '2025-10-23 06:15:35.969', 'cmgujpyjf002q4705j6hv1nkk', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh313676002d47s5li4e6qt9', 'Arbre', NULL, NULL, '2025-10-23 06:15:35.969', '2025-10-23 06:15:35.969', 'cmgujpyjf002q4705j6hv1nkk', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh3jsgfr002n47zbadkkds7r', 'Bavette2', NULL, NULL, '2025-10-23 14:59:08.727', '2025-10-23 14:59:08.727', 'cmgs1sco0002k47056yq8eyfq', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh3jsgfr002n47zbadkkds7r', 'Bavette2', NULL, NULL, '2025-10-23 14:59:08.727', '2025-10-23 14:59:08.727', 'cmgs1sco0002k47056yq8eyfq', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgrp3lhv00194705f1xp8j0m', 'Rouleau', 'X24001025', NULL, '2025-10-15 07:54:32.438', '2025-10-15 07:54:32.438', 'cmgroij2f00104705t6y33enk', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgrp3lhv00194705f1xp8j0m', 'Rouleau', 'X24001025', NULL, '2025-10-15 07:54:32.438', '2025-10-15 07:54:32.438', 'cmgroij2f00104705t6y33enk', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs0nyk7002g4705rteyvw7x', 'Support rouleau inférieur', 'T30S06944', NULL, '2025-10-15 13:18:18.295', '2025-10-15 13:18:18.295', 'cmgs0kd5o002f47053b7n8tw6', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs0nyk7002g4705rteyvw7x', 'Support rouleau inférieur', 'T30S06944', NULL, '2025-10-15 13:18:18.295', '2025-10-15 13:18:18.295', 'cmgs0kd5o002f47053b7n8tw6', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgum5zm0000547ffzg8ofiqr', 'Cage d''écureuil', 'W78517', NULL, '2025-10-17 08:55:43.753', '2025-10-17 08:55:43.753', 'cmgrzuwkj001u47057u8hej9u', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgum5zm0000547ffzg8ofiqr', 'Cage d''écureuil', 'W78517', NULL, '2025-10-17 08:55:43.753', '2025-10-17 08:55:43.753', 'cmgrzuwkj001u47057u8hej9u', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgum9bn4000847fffbazanc5', 'Arbre roue avant', 'H22907', NULL, '2025-10-17 08:58:19.312', '2025-10-17 08:58:19.312', 'cmgujpyjf002q4705j6hv1nkk', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgum9bn4000847fffbazanc5', 'Arbre roue avant', 'H22907', NULL, '2025-10-17 08:58:19.312', '2025-10-17 08:58:19.312', 'cmgujpyjf002q4705j6hv1nkk', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhaf1nsb004347v75uv8gmsi', 'Axe rouleau Promill', NULL, NULL, '2025-10-28 10:20:43.281', '2025-10-28 10:20:43.281', 'cmhaex3ca004247v78ymfpvpd', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhaf1nsb004347v75uv8gmsi', 'Axe rouleau Promill', NULL, NULL, '2025-10-28 10:20:43.281', '2025-10-28 10:20:43.281', 'cmhaex3ca004247v78ymfpvpd', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhbu3due0007475ung88xfpm', 'Cuvette pour roulement HH 228310', 'C41800-CO2', 498.00, '2025-10-29 10:09:44.122', '2025-10-29 10:09:44.122', 'cmh9bykt8001j47v7g0oej5dw', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhbu3due0007475ung88xfpm', 'Cuvette pour roulement HH 228310', 'C41800-CO2', 498.00, '2025-10-29 10:09:44.122', '2025-10-29 10:09:44.122', 'cmh9bykt8001j47v7g0oej5dw', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgytqtc8002u47ffum90ylo5', 'Moteur éléctrique1', 'X50001593', NULL, '2025-10-20 07:38:57.415', '2025-10-20 07:38:57.415', 'cmgytewe0002447ffup09bscr', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgytqtc8002u47ffum90ylo5', 'Moteur éléctrique1', 'X50001593', NULL, '2025-10-20 07:38:57.415', '2025-10-20 07:38:57.415', 'cmgytewe0002447ffup09bscr', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz0xbx5005d47ffjkrafetg', 'Poulie2', 'X53446', NULL, '2025-10-20 10:59:58.743', '2025-10-20 10:59:58.743', 'cmgz0v9k4004v47ff8apimo50', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz0xbx5005d47ffjkrafetg', 'Poulie2', 'X53446', NULL, '2025-10-20 10:59:58.743', '2025-10-20 10:59:58.743', 'cmgz0v9k4004v47ff8apimo50', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz1c9wx007h47ffr41untmr', 'Détecteur déport de bande', 'X23100', NULL, '2025-10-20 11:11:35.985', '2025-10-20 11:12:51.466', 'cmgum1wsl000447ffa109dtag', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz1c9wx007h47ffr41untmr', 'Détecteur déport de bande', 'X23100', NULL, '2025-10-20 11:11:35.985', '2025-10-20 11:12:51.466', 'cmgum1wsl000447ffa109dtag', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhbvyr210017475ubrey4eux', 'Ecrou de blocage KM20', 'ECRKM20A', 42.00, '2025-10-29 11:02:07.197', '2025-10-29 11:04:47.071', 'cmhbve5h30016475utwgpa32k', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhbvyr210017475ubrey4eux', 'Ecrou de blocage KM20', 'ECRKM20A', 42.00, '2025-10-29 11:02:07.197', '2025-10-29 11:04:47.071', 'cmhbve5h30016475utwgpa32k', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh4xg7lb000347nkrtvqw3hi', 'Arbre Tapis émotteur', NULL, NULL, '2025-10-24 14:09:18.164', '2025-10-24 14:09:18.164', 'cmgujpyjf002q4705j6hv1nkk', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh4xg7lb000347nkrtvqw3hi', 'Arbre Tapis émotteur', NULL, NULL, '2025-10-24 14:09:18.164', '2025-10-24 14:09:18.164', 'cmgujpyjf002q4705j6hv1nkk', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhaagmno003647v7sfrgsb5v', 'COQUILLE nid d''abeille Promill', 'E41800ASN1P', 574.00, '2025-10-28 08:12:23.603', '2025-10-28 08:12:23.603', 'cmhaa5la2003447v7do7w3s0i', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhaagmno003647v7sfrgsb5v', 'COQUILLE nid d''abeille Promill', 'E41800ASN1P', 574.00, '2025-10-28 08:12:23.603', '2025-10-28 08:12:23.603', 'cmhaa5la2003447v7do7w3s0i', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhaf9o8j004947v7xomube6n', 'Flasque arrière rouleau Promill', 'F07700-001/5759701', 86.71, '2025-10-28 10:26:57.139', '2025-10-28 10:26:57.139', 'cmhaf6jaj004847v7cpq93sq5', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhaf9o8j004947v7xomube6n', 'Flasque arrière rouleau Promill', 'F07700-001/5759701', 86.71, '2025-10-28 10:26:57.139', '2025-10-28 10:26:57.139', 'cmhaf6jaj004847v7cpq93sq5', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhafarpc004e47v7nqdy97xs', 'Flasque avant rouleau Promill', 'F07700-002/5759601', 115.30, '2025-10-28 10:27:48.288', '2025-10-28 10:27:48.288', 'cmhaf6jaj004847v7cpq93sq5', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhafarpc004e47v7nqdy97xs', 'Flasque avant rouleau Promill', 'F07700-002/5759601', 115.30, '2025-10-28 10:27:48.288', '2025-10-28 10:27:48.288', 'cmhaf6jaj004847v7cpq93sq5', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhbuapy5000e475utfiwfkcj', 'Cone pour roulement HH228340', 'C41800-CO2', 498.00, '2025-10-29 10:15:26.402', '2025-10-29 10:15:26.402', 'cmh9bykt8001j47v7g0oej5dw', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhbuapy5000e475utfiwfkcj', 'Cone pour roulement HH228340', 'C41800-CO2', 498.00, '2025-10-29 10:15:26.402', '2025-10-29 10:15:26.402', 'cmh9bykt8001j47v7g0oej5dw', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhd4jjw9001q475u8x4i63jw', 'Graisseur 1/4 rouleaux Promill', NULL, NULL, '2025-10-30 07:50:00.797', '2025-10-30 07:50:00.797', 'cmhd48ipe001p475ul7ejiutq', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhd4jjw9001q475u8x4i63jw', 'Graisseur 1/4 rouleaux Promill', NULL, NULL, '2025-10-30 07:50:00.797', '2025-10-30 07:50:00.797', 'cmhd48ipe001p475ul7ejiutq', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhdb5hon002p475uif5ri4vu', 'Douille de serrage', NULL, NULL, '2025-10-30 10:55:02.086', '2025-10-30 10:55:02.086', 'cmhdb4mgx002o475udcnbd71h', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhdb5hon002p475uif5ri4vu', 'Douille de serrage', NULL, NULL, '2025-10-30 10:55:02.086', '2025-10-30 10:55:02.086', 'cmhdb4mgx002o475udcnbd71h', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh31ejnw003b47s548rtk8b1', 'Arbre de commande', NULL, NULL, '2025-10-23 06:24:26.634', '2025-11-06 13:36:45.099', 'cmgujpyjf002q4705j6hv1nkk', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh31ejnw003b47s548rtk8b1', 'Arbre de commande', NULL, NULL, '2025-10-23 06:24:26.634', '2025-11-06 13:36:45.099', 'cmgujpyjf002q4705j6hv1nkk', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgrzvdmo001v47050tvf2z88', 'Cage d''écureuil de pied', 'W78515', NULL, '2025-10-15 12:56:04.801', '2025-10-15 13:05:49.946', 'cmgrzuwkj001u47057u8hej9u', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgrzvdmo001v47050tvf2z88', 'Cage d''écureuil de pied', 'W78515', NULL, '2025-10-15 12:56:04.801', '2025-10-15 13:05:49.946', 'cmgrzuwkj001u47057u8hej9u', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs14c7a002i4705t1w4qdfx', 'rouleau amortisseur avec axe', 'E1RS07058', NULL, '2025-10-15 13:31:02.469', '2025-10-15 13:31:02.469', 'cmgs13jjp002h4705rjqzz5lh', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs14c7a002i4705t1w4qdfx', 'rouleau amortisseur avec axe', 'E1RS07058', NULL, '2025-10-15 13:31:02.469', '2025-10-15 13:31:02.469', 'cmgs13jjp002h4705rjqzz5lh', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgyruhgm000947ffmhhrqdrl', 'Galet avant chariot déverseur', 'H22698', NULL, '2025-10-20 06:45:49.386', '2025-10-20 06:45:49.386', 'cmgs1s4pv002j470567o60oqe', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgyruhgm000947ffmhhrqdrl', 'Galet avant chariot déverseur', 'H22698', NULL, '2025-10-20 06:45:49.386', '2025-10-20 06:45:49.386', 'cmgs1s4pv002j470567o60oqe', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgyrxcgu000a47ffxvcyyuwm', 'Palier BPF5', 'X21000919', NULL, '2025-10-20 06:48:02.91', '2025-10-20 06:48:02.91', 'cmgrnxlx5000g47059oyj4yuw', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgyrxcgu000a47ffxvcyyuwm', 'Palier BPF5', 'X21000919', NULL, '2025-10-20 06:48:02.91', '2025-10-20 06:48:02.91', 'cmgrnxlx5000g47059oyj4yuw', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgyrzrbc000h47ffd670wu8j', 'Arbre roue arrière', 'H22908', NULL, '2025-10-20 06:49:55.463', '2025-10-20 06:49:55.463', 'cmgujpyjf002q4705j6hv1nkk', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgyrzrbc000h47ffd670wu8j', 'Arbre roue arrière', 'H22908', NULL, '2025-10-20 06:49:55.463', '2025-10-20 06:49:55.463', 'cmgujpyjf002q4705j6hv1nkk', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgys0mgx000i47ffxbftvqt4', 'Galet arrière chariot déverseur', 'H22861', NULL, '2025-10-20 06:50:35.84', '2025-10-20 06:50:35.84', 'cmgs1s4pv002j470567o60oqe', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgys0mgx000i47ffxbftvqt4', 'Galet arrière chariot déverseur', 'H22861', NULL, '2025-10-20 06:50:35.84', '2025-10-20 06:50:35.84', 'cmgs1s4pv002j470567o60oqe', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgysatbl001u47ffu55db8gg', 'Vérin éléctrique', 'X22754', NULL, '2025-10-20 06:58:31.282', '2025-10-20 06:58:31.282', 'cmgulzr7b000247ffpr2vsput', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgysatbl001u47ffu55db8gg', 'Vérin éléctrique', 'X22754', NULL, '2025-10-20 06:58:31.282', '2025-10-20 06:58:31.282', 'cmgulzr7b000247ffpr2vsput', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgytuf06003k47ffdr8lvp13', 'Moteur éléctrique3', 'X50001598', NULL, '2025-10-20 07:41:45.434', '2025-10-20 07:41:45.434', 'cmgytewe0002447ffup09bscr', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgytuf06003k47ffdr8lvp13', 'Moteur éléctrique3', 'X50001598', NULL, '2025-10-20 07:41:45.434', '2025-10-20 07:41:45.434', 'cmgytewe0002447ffup09bscr', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz0y2aw005m47ff4zkjczei', 'Poulie3', 'X53450', NULL, '2025-10-20 11:00:32.936', '2025-10-20 11:00:32.936', 'cmgz0v9k4004v47ff8apimo50', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz0y2aw005m47ff4zkjczei', 'Poulie3', 'X53450', NULL, '2025-10-20 11:00:32.936', '2025-10-20 11:00:32.936', 'cmgz0v9k4004v47ff8apimo50', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz112hd006e47ffvg37mkoq', 'Moyeu amovible2', 'X11F00653', NULL, '2025-10-20 11:02:53.136', '2025-10-20 11:02:53.136', 'cmgz0zs4m006447ffq5b20ch3', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz112hd006e47ffvg37mkoq', 'Moyeu amovible2', 'X11F00653', NULL, '2025-10-20 11:02:53.136', '2025-10-20 11:02:53.136', 'cmgz0zs4m006447ffq5b20ch3', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz1erci007r47ffybtdepul', 'Détecteur déport de bande1', 'X53294', NULL, '2025-10-20 11:13:31.891', '2025-10-20 11:13:31.891', 'cmgum1wsl000447ffa109dtag', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz1erci007r47ffybtdepul', 'Détecteur déport de bande1', 'X53294', NULL, '2025-10-20 11:13:31.891', '2025-10-20 11:13:31.891', 'cmgum1wsl000447ffa109dtag', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh3eadge001147zbworn1671', 'Bavette 2', NULL, NULL, '2025-10-23 12:25:06.974', '2025-10-23 12:25:06.974', 'cmgs1sco0002k47056yq8eyfq', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh3eadge001147zbworn1671', 'Bavette 2', NULL, NULL, '2025-10-23 12:25:06.974', '2025-10-23 12:25:06.974', 'cmgs1sco0002k47056yq8eyfq', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh3e95j4000k47zbx53n4tqv', 'Bavette1', NULL, NULL, '2025-10-23 12:24:10.047', '2025-10-23 14:58:45.061', 'cmgs1sco0002k47056yq8eyfq', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh3e95j4000k47zbx53n4tqv', 'Bavette1', NULL, NULL, '2025-10-23 12:24:10.047', '2025-10-23 14:58:45.061', 'cmgs1sco0002k47056yq8eyfq', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgrnzbku000h4705qrj5eujb', 'Tambour de tête', 'H57305', NULL, '2025-10-15 07:23:13.346', '2025-10-15 07:23:13.346', 'cmgrnu6zc000f470565mc8hha', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgrnzbku000h4705qrj5eujb', 'Tambour de tête', 'H57305', NULL, '2025-10-15 07:23:13.346', '2025-10-15 07:23:13.346', 'cmgrnu6zc000f470565mc8hha', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs07df2001w4705ry79yvbo', 'Cage d''écureuil de pied de tension', 'W58372', NULL, '2025-10-15 13:05:24.397', '2025-10-15 13:05:24.397', 'cmgrzuwkj001u47057u8hej9u', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs07df2001w4705ry79yvbo', 'Cage d''écureuil de pied de tension', 'W58372', NULL, '2025-10-15 13:05:24.397', '2025-10-15 13:05:24.397', 'cmgrzuwkj001u47057u8hej9u', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs1swl0002l4705gpyg1yyn', 'Galet releveur complet', 'W32440', NULL, '2025-10-15 13:50:08.628', '2025-10-15 13:50:08.628', 'cmgs1s4pv002j470567o60oqe', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs1swl0002l4705gpyg1yyn', 'Galet releveur complet', 'W32440', NULL, '2025-10-15 13:50:08.628', '2025-10-15 13:50:08.628', 'cmgs1s4pv002j470567o60oqe', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgys4a2s001847ffhqhz7zcd', 'Pignon moteur', 'H38143', NULL, '2025-10-20 06:53:26.404', '2025-10-20 06:54:46.835', 'cmgukvztv002s4705kqvqjtvg', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgys4a2s001847ffhqhz7zcd', 'Pignon moteur', 'H38143', NULL, '2025-10-20 06:53:26.404', '2025-10-20 06:54:46.835', 'cmgukvztv002s4705kqvqjtvg', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgys6k6b001h47ffuq44ze37', 'Pignon récepteur', 'H47381', NULL, '2025-10-20 06:55:12.803', '2025-10-20 06:55:12.803', 'cmgukvztv002s4705kqvqjtvg', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgys6k6b001h47ffuq44ze37', 'Pignon récepteur', 'H47381', NULL, '2025-10-20 06:55:12.803', '2025-10-20 06:55:12.803', 'cmgukvztv002s4705kqvqjtvg', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgys7anf001m47ff0ulcp092', 'Chaîne 19.05S', 'X10000564', NULL, '2025-10-20 06:55:47.115', '2025-10-20 06:56:20.844', 'cmgukxw26002t4705qz4ul929', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgys7anf001m47ff0ulcp092', 'Chaîne 19.05S', 'X10000564', NULL, '2025-10-20 06:55:47.115', '2025-10-20 06:56:20.844', 'cmgukxw26002t4705qz4ul929', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgysj6wn002347ffgq3f98dr', 'Détecteur mécanique', 'X60001690', NULL, '2025-10-20 07:05:02.134', '2025-10-20 07:05:02.134', 'cmgum1wsl000447ffa109dtag', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgysj6wn002347ffgq3f98dr', 'Détecteur mécanique', 'X60001690', NULL, '2025-10-20 07:05:02.134', '2025-10-20 07:05:02.134', 'cmgum1wsl000447ffa109dtag', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgytx2ul003x47ffhdpurtx5', 'Moteur éléctrique4', 'X50001600', NULL, '2025-10-20 07:43:49.676', '2025-10-20 07:43:49.676', 'cmgytewe0002447ffup09bscr', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgytx2ul003x47ffhdpurtx5', 'Moteur éléctrique4', 'X50001600', NULL, '2025-10-20 07:43:49.676', '2025-10-20 07:43:49.676', 'cmgytewe0002447ffup09bscr', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz0yt33005v47ffy4p8d28z', 'Poulie4', 'X41745', NULL, '2025-10-20 11:01:07.646', '2025-10-20 11:01:07.646', 'cmgz0v9k4004v47ff8apimo50', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz0yt33005v47ffy4p8d28z', 'Poulie4', 'X41745', NULL, '2025-10-20 11:01:07.646', '2025-10-20 11:01:07.646', 'cmgz0v9k4004v47ff8apimo50', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz10g67006547ffj28sqequ', 'Moyeu amovible1', 'X43888', NULL, '2025-10-20 11:02:24.223', '2025-10-20 11:02:24.223', 'cmgz0zs4m006447ffq5b20ch3', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz10g67006547ffj28sqequ', 'Moyeu amovible1', 'X43888', NULL, '2025-10-20 11:02:24.223', '2025-10-20 11:02:24.223', 'cmgz0zs4m006447ffq5b20ch3', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz11p1k006j47ffhjqgrnkp', 'Moyeu amovible3', 'X41739', NULL, '2025-10-20 11:03:22.375', '2025-10-20 11:03:22.375', 'cmgz0zs4m006447ffq5b20ch3', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz11p1k006j47ffhjqgrnkp', 'Moyeu amovible3', 'X41739', NULL, '2025-10-20 11:03:22.375', '2025-10-20 11:03:22.375', 'cmgz0zs4m006447ffq5b20ch3', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz128lz006o47ffwfgtag7e', 'Moyeu amovible4', 'X11F00765', NULL, '2025-10-20 11:03:47.735', '2025-10-20 11:03:47.735', 'cmgz0zs4m006447ffq5b20ch3', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz128lz006o47ffwfgtag7e', 'Moyeu amovible4', 'X11F00765', NULL, '2025-10-20 11:03:47.735', '2025-10-20 11:03:47.735', 'cmgz0zs4m006447ffq5b20ch3', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz516t7009n47fft3nfyt34', 'Tambour de tête1', 'H138830', NULL, '2025-10-20 12:54:57.211', '2025-10-20 12:54:57.211', 'cmgrnu6zc000f470565mc8hha', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz516t7009n47fft3nfyt34', 'Tambour de tête1', 'H138830', NULL, '2025-10-20 12:54:57.211', '2025-10-20 12:54:57.211', 'cmgrnu6zc000f470565mc8hha', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhae5b7u003i47v7vb4qi81n', 'Joint torique R41', 'JTR41', 2.00, '2025-10-28 09:55:33.999', '2025-10-28 09:55:33.999', 'cmhabzypq003h47v7jyjjxst1', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhae5b7u003i47v7vb4qi81n', 'Joint torique R41', 'JTR41', 2.00, '2025-10-28 09:55:33.999', '2025-10-28 09:55:33.999', 'cmhabzypq003h47v7jyjjxst1', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhallrb1004i47v7855gvfpe', 'Segment d''étanchéïté', 'S41800-SA2', 37.00, '2025-10-28 13:24:18.658', '2025-10-28 13:24:56.572', 'cmhalh6sa004h47v7y6pnqok2', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhallrb1004i47v7855gvfpe', 'Segment d''étanchéïté', 'S41800-SA2', 37.00, '2025-10-28 13:24:18.658', '2025-10-28 13:24:56.572', 'cmhalh6sa004h47v7y6pnqok2', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgys1osr000j47fftblpdpu2', 'Motoréducteur frein', 'X33959', NULL, '2025-10-20 06:51:25.485', '2025-10-20 06:52:25.744', NULL, NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgys1osr000j47fftblpdpu2', 'Motoréducteur frein', 'X33959', NULL, '2025-10-20 06:51:25.485', '2025-10-20 06:52:25.744', NULL, NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgys3ugw001147ffq33udxaw', 'Motoréducteur frein.', 'X108273', NULL, '2025-10-20 06:53:06.176', '2025-10-20 06:53:06.176', NULL, NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgys3ugw001147ffq33udxaw', 'Motoréducteur frein.', 'X108273', NULL, '2025-10-20 06:53:06.176', '2025-10-20 06:53:06.176', NULL, NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhbuq97o000l475uf73oiot0', 'Entretoise de roulements', 'E41800-000', 67.00, '2025-10-29 10:27:31.208', '2025-10-29 10:27:31.208', 'cmh9bykt8001j47v7g0oej5dw', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhbuq97o000l475uf73oiot0', 'Entretoise de roulements', 'E41800-000', 67.00, '2025-10-29 10:27:31.208', '2025-10-29 10:27:31.208', 'cmh9bykt8001j47v7g0oej5dw', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhd4s3u80020475uasnc0mqj', 'Crochet de levage', NULL, NULL, '2025-10-30 07:56:39.92', '2025-10-30 07:56:39.92', 'cmhd4r5bg001z475u0f4tm9yy', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhd4s3u80020475uasnc0mqj', 'Crochet de levage', NULL, NULL, '2025-10-30 07:56:39.92', '2025-10-30 07:56:39.92', 'cmhd4r5bg001z475u0f4tm9yy', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhd4syhb0025475u4sv87dyf', 'Crochet de levage avec manille', NULL, NULL, '2025-10-30 07:57:19.63', '2025-10-30 07:57:19.63', 'cmhd4r5bg001z475u0f4tm9yy', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhd4syhb0025475u4sv87dyf', 'Crochet de levage avec manille', NULL, NULL, '2025-10-30 07:57:19.63', '2025-10-30 07:57:19.63', 'cmhd4r5bg001z475u0f4tm9yy', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhdiuklf002u475uadws3h04', 'Courroie XPC', NULL, NULL, '2025-10-30 14:30:29.569', '2025-10-30 14:30:29.569', 'cmgz17bpz006t47ff58i3j1e1', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhdiuklf002u475uadws3h04', 'Courroie XPC', NULL, NULL, '2025-10-30 14:30:29.569', '2025-10-30 14:30:29.569', 'cmgz17bpz006t47ff58i3j1e1', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh99m3e6000847v75h2m7czn', 'Réducteur emo', NULL, NULL, '2025-10-27 15:00:52.781', '2025-10-27 15:00:52.781', NULL, NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh99m3e6000847v75h2m7czn', 'Réducteur emo', NULL, NULL, '2025-10-27 15:00:52.781', '2025-10-27 15:00:52.781', NULL, NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmizudzfy00021e2w2mtd9zv8', 'lame de godet 82', NULL, 192.00, '2025-12-10 10:04:09.262', '2025-12-10 10:05:37.177', 'cmizu3st800001e2waysco15j', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmizudzfy00021e2w2mtd9zv8', 'lame de godet 82', NULL, 192.00, '2025-12-10 10:04:09.262', '2025-12-10 10:05:37.177', 'cmizu3st800001e2waysco15j', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmizv8nzu00081e2wen6ur31b', 'Tapis', 'PF0165295', 3730.67, '2025-12-10 10:28:00.762', '2025-12-10 10:39:59.943', 'cmizup8cv00061e2w2rulkxsn', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmizv8nzu00081e2wen6ur31b', 'Tapis', 'PF0165295', 3730.67, '2025-12-10 10:28:00.762', '2025-12-10 10:39:59.943', 'cmizup8cv00061e2w2rulkxsn', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmjcixqq300141e2wqkvz0cx6', 'VIS (lame de godet) 82 M14 60mm tête fraisé', NULL, NULL, '2025-12-19 07:04:35.979', '2025-12-19 07:04:35.979', 'cmj025vi7000z1e2wyn3x6msv', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmjcixqq300141e2wqkvz0cx6', 'VIS (lame de godet) 82 M14 60mm tête fraisé', NULL, NULL, '2025-12-19 07:04:35.979', '2025-12-19 07:04:35.979', 'cmj025vi7000z1e2wyn3x6msv', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmjcpdwqs00161e2wu4juy4u2', 'Ecrou Ø 14', NULL, NULL, '2025-12-19 10:05:07.973', '2025-12-19 10:05:07.973', 'cmhbve5h30016475utwgpa32k', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmjcpdwqs00161e2wu4juy4u2', 'Ecrou Ø 14', NULL, NULL, '2025-12-19 10:05:07.973', '2025-12-19 10:05:07.973', 'cmhbve5h30016475utwgpa32k', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhdattcv002f475ugw514oj3', 'Poulie 8', NULL, NULL, '2025-10-30 10:45:57.343', '2026-01-14 08:04:14.29', 'cmgz0v9k4004v47ff8apimo50', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhdattcv002f475ugw514oj3', 'Poulie 8', NULL, NULL, '2025-10-30 10:45:57.343', '2026-01-14 08:04:14.29', 'cmgz0v9k4004v47ff8apimo50', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdqrkez001o1e2wtslqeazi', 'Carter presse', NULL, NULL, '2026-01-14 08:11:13.308', '2026-01-14 08:11:13.308', 'cmkdqqh8w001n1e2wzxkamd1m', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdqrkez001o1e2wtslqeazi', 'Carter presse', NULL, NULL, '2026-01-14 08:11:13.308', '2026-01-14 08:11:13.308', 'cmkdqqh8w001n1e2wzxkamd1m', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdqw1tq00231e2wxou4eu8z', 'Ecrou HM12', NULL, NULL, '2026-01-14 08:14:42.494', '2026-01-14 08:14:42.494', 'cmhbve5h30016475utwgpa32k', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdqw1tq00231e2wxou4eu8z', 'Ecrou HM12', NULL, NULL, '2026-01-14 08:14:42.494', '2026-01-14 08:14:42.494', 'cmhbve5h30016475utwgpa32k', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdrc535002e1e2wjuucdweq', 'Moteur entrainement Presse Promill', NULL, NULL, '2026-01-14 08:27:13.217', '2026-01-14 08:27:13.217', 'cmgytewe0002447ffup09bscr', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdrc535002e1e2wjuucdweq', 'Moteur entrainement Presse Promill', NULL, NULL, '2026-01-14 08:27:13.217', '2026-01-14 08:27:13.217', 'cmgytewe0002447ffup09bscr', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdsbrz1002t1e2wqemldbr6', 'Vis HM 14x100', NULL, NULL, '2026-01-14 08:54:55.838', '2026-01-14 08:54:55.838', 'cmkdqtcpv001r1e2wptehmkxi', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdsbrz1002t1e2wqemldbr6', 'Vis HM 14x100', NULL, NULL, '2026-01-14 08:54:55.838', '2026-01-14 08:54:55.838', 'cmkdqtcpv001r1e2wptehmkxi', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdsdqjh00301e2wu2g4ljg2', 'Rondelle plate M24', NULL, NULL, '2026-01-14 08:56:27.294', '2026-01-14 08:56:27.294', 'cmhbuzjrz000s475ue0q2o2xd', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdsdqjh00301e2wu2g4ljg2', 'Rondelle plate M24', NULL, NULL, '2026-01-14 08:56:27.294', '2026-01-14 08:56:27.294', 'cmhbuzjrz000s475ue0q2o2xd', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdsp16u003b1e2wetp787yf', 'Rondelle Grower W24', NULL, NULL, '2026-01-14 09:05:14.31', '2026-01-14 09:05:14.31', 'cmhbuzjrz000s475ue0q2o2xd', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdsp16u003b1e2wetp787yf', 'Rondelle Grower W24', NULL, NULL, '2026-01-14 09:05:14.31', '2026-01-14 09:05:14.31', 'cmhbuzjrz000s475ue0q2o2xd', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdsrew1003m1e2wcuky5m77', 'Ecrou HM24', NULL, NULL, '2026-01-14 09:07:05.377', '2026-01-14 09:07:05.377', 'cmhbve5h30016475utwgpa32k', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdsrew1003m1e2wcuky5m77', 'Ecrou HM24', NULL, NULL, '2026-01-14 09:07:05.377', '2026-01-14 09:07:05.377', 'cmhbve5h30016475utwgpa32k', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdqun4a001s1e2wx123zdy1', 'Vis HM 12x35', NULL, NULL, '2026-01-14 08:13:36.778', '2026-01-14 09:15:22.132', 'cmkdqtcpv001r1e2wptehmkxi', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdqun4a001s1e2wx123zdy1', 'Vis HM 12x35', NULL, NULL, '2026-01-14 08:13:36.778', '2026-01-14 09:15:22.132', 'cmkdqtcpv001r1e2wptehmkxi', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdt82ma003x1e2w9gkgwybf', 'Vis HM 12x30', NULL, NULL, '2026-01-14 09:20:02.626', '2026-01-14 09:20:02.626', 'cmkdqtcpv001r1e2wptehmkxi', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdt82ma003x1e2w9gkgwybf', 'Vis HM 12x30', NULL, NULL, '2026-01-14 09:20:02.626', '2026-01-14 09:20:02.626', 'cmkdqtcpv001r1e2wptehmkxi', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdtdh5p00441e2w0g6ye4v5', 'Rondelle Grower W12', NULL, NULL, '2026-01-14 09:24:14.75', '2026-01-14 09:24:14.75', 'cmhbuzjrz000s475ue0q2o2xd', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdtdh5p00441e2w0g6ye4v5', 'Rondelle Grower W12', NULL, NULL, '2026-01-14 09:24:14.75', '2026-01-14 09:24:14.75', 'cmhbuzjrz000s475ue0q2o2xd', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmke1nimf004f1e2wsurzoeet', 'Vis HM 8x16', NULL, NULL, '2026-01-14 13:16:00.135', '2026-01-14 13:16:00.135', 'cmkdqtcpv001r1e2wptehmkxi', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmke1nimf004f1e2wsurzoeet', 'Vis HM 8x16', NULL, NULL, '2026-01-14 13:16:00.135', '2026-01-14 13:16:00.135', 'cmkdqtcpv001r1e2wptehmkxi', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmke3hgbc004o1e2w02his9k3', 'Vis HM 24x100', NULL, NULL, '2026-01-14 14:07:16.44', '2026-01-14 14:07:16.44', 'cmkdqtcpv001r1e2wptehmkxi', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmke3hgbc004o1e2w02his9k3', 'Vis HM 24x100', NULL, NULL, '2026-01-14 14:07:16.44', '2026-01-14 14:07:16.44', 'cmkdqtcpv001r1e2wptehmkxi', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkp7xsz1006b1e2whn582enn', 'Arbre principal Presse Promill', 'APR 80 101101', NULL, '2026-01-22 08:57:25.741', '2026-01-22 08:57:25.741', 'cmgujpyjf002q4705j6hv1nkk', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkp7xsz1006b1e2whn582enn', 'Arbre principal Presse Promill', 'APR 80 101101', NULL, '2026-01-22 08:57:25.741', '2026-01-22 08:57:25.741', 'cmgujpyjf002q4705j6hv1nkk', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkp80cfi006k1e2wha1v14vv', 'Joint Viton', NULL, NULL, '2026-01-22 08:59:24.27', '2026-01-22 08:59:24.27', 'cmhabzypq003h47v7jyjjxst1', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkp80cfi006k1e2wha1v14vv', 'Joint Viton', NULL, NULL, '2026-01-22 08:59:24.27', '2026-01-22 08:59:24.27', 'cmhabzypq003h47v7jyjjxst1', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkp84bs0006t1e2wkokejtyn', 'Roulement à rouleaux cylindriques', NULL, NULL, '2026-01-22 09:02:30.048', '2026-01-22 09:02:30.048', 'cmh9bykt8001j47v7g0oej5dw', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkp84bs0006t1e2wkokejtyn', 'Roulement à rouleaux cylindriques', NULL, NULL, '2026-01-22 09:02:30.048', '2026-01-22 09:02:30.048', 'cmh9bykt8001j47v7g0oej5dw', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkp8mbo900751e2w2746k94i', 'Circlips E260', '390 01126000', NULL, '2026-01-22 09:16:29.721', '2026-01-22 09:16:29.721', 'cmhalh6sa004h47v7y6pnqok2', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkp8mbo900751e2w2746k94i', 'Circlips E260', '390 01126000', NULL, '2026-01-22 09:16:29.721', '2026-01-22 09:16:29.721', 'cmhalh6sa004h47v7y6pnqok2', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkpkaj4400091eq6p6accs62', 'Moyeu central', 'APR 80 101103', NULL, '2026-01-22 14:43:14.884', '2026-01-22 14:43:14.884', 'cmgz0zs4m006447ffq5b20ch3', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkpkaj4400091eq6p6accs62', 'Moyeu central', 'APR 80 101103', NULL, '2026-01-22 14:43:14.884', '2026-01-22 14:43:14.884', 'cmgz0zs4m006447ffq5b20ch3', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkpkwfdh000g1eq6hpqdbqat', 'Roulement à rotule sur rouleaux', '320 41 220001', NULL, '2026-01-22 15:00:16.469', '2026-01-22 15:00:16.469', 'cmh9bykt8001j47v7g0oej5dw', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkpkwfdh000g1eq6hpqdbqat', 'Roulement à rotule sur rouleaux', '320 41 220001', NULL, '2026-01-22 15:00:16.469', '2026-01-22 15:00:16.469', 'cmh9bykt8001j47v7g0oej5dw', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkpnw6er000s1eq6k57kcnl8', 'Vis CHC M14x40', NULL, NULL, '2026-01-22 16:24:03.699', '2026-01-22 16:24:03.699', 'cmkdqtcpv001r1e2wptehmkxi', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkpnw6er000s1eq6k57kcnl8', 'Vis CHC M14x40', NULL, NULL, '2026-01-22 16:24:03.699', '2026-01-22 16:24:03.699', 'cmkdqtcpv001r1e2wptehmkxi', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkpo4gxz00121eq6o0ahmizg', 'Joint', '370 20 280000', NULL, '2026-01-22 16:30:30.6', '2026-01-22 16:30:30.6', 'cmhabzypq003h47v7jyjjxst1', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkpo4gxz00121eq6o0ahmizg', 'Joint', '370 20 280000', NULL, '2026-01-22 16:30:30.6', '2026-01-22 16:30:30.6', 'cmhabzypq003h47v7jyjjxst1', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr0nq1a004e1eq6v6ubxlfl', 'Palier tête E1 17', 'SNU516613', NULL, '2026-01-23 15:09:10.414', '2026-01-23 15:09:44.182', 'cmgrnxlx5000g47059oyj4yuw', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr0nq1a004e1eq6v6ubxlfl', 'Palier tête E1 17', 'SNU516613', NULL, '2026-01-23 15:09:10.414', '2026-01-23 15:09:44.182', 'cmgrnxlx5000g47059oyj4yuw', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr0owbv004l1eq6pzlatzlr', 'Vis HM 14x50', NULL, NULL, '2026-01-23 15:10:05.228', '2026-01-23 15:10:05.228', 'cmkdqtcpv001r1e2wptehmkxi', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr0owbv004l1eq6pzlatzlr', 'Vis HM 14x50', NULL, NULL, '2026-01-23 15:10:05.228', '2026-01-23 15:10:05.228', 'cmkdqtcpv001r1e2wptehmkxi', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkqzl1oa002v1eq6erkt5544', 'BANDE E1 17', NULL, NULL, '2026-01-23 14:39:05.914', '2026-01-23 14:48:36.329', 'cmknus46z00551e2wf7zy706v', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkqzl1oa002v1eq6erkt5544', 'BANDE E1 17', NULL, NULL, '2026-01-23 14:39:05.914', '2026-01-23 14:48:36.329', 'cmknus46z00551e2wf7zy706v', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr0fwuo003o1eq69b1idlpw', 'Entretoise porte 2 joints', 'APR 80 101108', NULL, '2026-01-23 15:03:06', '2026-01-23 15:03:06', 'cmhabzypq003h47v7jyjjxst1', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr0fwuo003o1eq69b1idlpw', 'Entretoise porte 2 joints', 'APR 80 101108', NULL, '2026-01-23 15:03:06', '2026-01-23 15:03:06', 'cmhabzypq003h47v7jyjjxst1', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr0koqb003y1eq6a8cx8poy', 'Flasque porte 2 joints', 'APR 25 0304..', NULL, '2026-01-23 15:06:48.755', '2026-01-23 15:06:48.755', 'cmhaf6jaj004847v7cpq93sq5', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr0koqb003y1eq6a8cx8poy', 'Flasque porte 2 joints', 'APR 25 0304..', NULL, '2026-01-23 15:06:48.755', '2026-01-23 15:06:48.755', 'cmhaf6jaj004847v7cpq93sq5', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr0nmsv00431eq6rcnozdes', 'Rondelle Grower W14', NULL, NULL, '2026-01-23 15:09:06.224', '2026-01-23 15:09:06.224', 'cmhbuzjrz000s475ue0q2o2xd', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr0nmsv00431eq6rcnozdes', 'Rondelle Grower W14', NULL, NULL, '2026-01-23 15:09:06.224', '2026-01-23 15:09:06.224', 'cmhbuzjrz000s475ue0q2o2xd', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr173u9004z1eq6tn4gw3h3', 'Rondelle frein MB44', NULL, NULL, '2026-01-23 15:24:14.77', '2026-01-23 15:24:14.77', 'cmhbuzjrz000s475ue0q2o2xd', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr173u9004z1eq6tn4gw3h3', 'Rondelle frein MB44', NULL, NULL, '2026-01-23 15:24:14.77', '2026-01-23 15:24:14.77', 'cmhbuzjrz000s475ue0q2o2xd', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr20cpy005a1eq6nn5kmtys', 'Roulement E1 17', NULL, NULL, '2026-01-23 15:46:59.302', '2026-01-23 15:46:59.302', 'cmh9bykt8001j47v7g0oej5dw', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr20cpy005a1eq6nn5kmtys', 'Roulement E1 17', NULL, NULL, '2026-01-23 15:46:59.302', '2026-01-23 15:46:59.302', 'cmh9bykt8001j47v7g0oej5dw', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr253dx005k1eq65vn7evdy', 'Ecrou HM 44T', NULL, NULL, '2026-01-23 15:50:40.485', '2026-01-23 15:50:40.485', 'cmhbve5h30016475utwgpa32k', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr253dx005k1eq65vn7evdy', 'Ecrou HM 44T', NULL, NULL, '2026-01-23 15:50:40.485', '2026-01-23 15:50:40.485', 'cmhbve5h30016475utwgpa32k', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr25xz1005v1eq6i0fib4er', 'Manchon E1 17', NULL, NULL, '2026-01-23 15:51:20.125', '2026-01-23 15:51:20.125', 'cmkr24n3x005j1eq6s9xi6obl', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr25xz1005v1eq6i0fib4er', 'Manchon E1 17', NULL, NULL, '2026-01-23 15:51:20.125', '2026-01-23 15:51:20.125', 'cmkr24n3x005j1eq6s9xi6obl', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr0qjw5004s1eq6pen63x7j', 'Arbre E1 17', NULL, NULL, '2026-01-23 15:11:22.422', '2026-01-23 15:55:47.227', 'cmgujpyjf002q4705j6hv1nkk', NULL, NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr0qjw5004s1eq6pen63x7j', 'Arbre E1 17', NULL, NULL, '2026-01-23 15:11:22.422', '2026-01-23 15:55:47.227', 'cmgujpyjf002q4705j6hv1nkk', NULL);
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cl89d9641d47f52c5385f83d5c', 'test', 'four', 33.97, '2026-01-25 10:48:52', '2026-01-25 10:49:48', 'cl880ba34e5789668dd1c3affa', 'cmko9bmrd005m1e2w81v07kiz', NULL);
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cl89d9641d47f52c5385f83d5c', 'test', 'four', 33.97, '2026-01-25 10:48:52', '2026-01-25 10:49:48', 'cl880ba34e5789668dd1c3affa', 'cmko9bmrd005m1e2w81v07kiz');
|
||||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cl280f805cc3e6ff4b8bde95e4', 'testjjj', NULL, NULL, '2026-01-25 11:20:44', '2026-01-25 11:20:44', 'cl880ba34e5789668dd1c3affa', 'cmko9bmrd005m1e2w81v07kiz', '["cmko9bmrd005m1e2w81v07kiz","cmkpp4fb3001i1eq6qq74ul2i"]');
|
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cl280f805cc3e6ff4b8bde95e4', 'testjjj', NULL, NULL, '2026-01-25 11:20:44', '2026-01-25 11:20:44', 'cl880ba34e5789668dd1c3affa', 'cmko9bmrd005m1e2w81v07kiz');
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
|
|||||||
24
frontend/.gitignore
vendored
24
frontend/.gitignore
vendored
@@ -1,24 +0,0 @@
|
|||||||
# Nuxt dev/build outputs
|
|
||||||
.output
|
|
||||||
.data
|
|
||||||
.nuxt
|
|
||||||
.nitro
|
|
||||||
.cache
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Node dependencies
|
|
||||||
node_modules
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
.DS_Store
|
|
||||||
.fleet
|
|
||||||
.idea
|
|
||||||
|
|
||||||
# Local env files
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
# Nuxt Minimal Starter
|
|
||||||
|
|
||||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
Make sure to install dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# npm
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Server
|
|
||||||
|
|
||||||
Start the development server on `http://localhost:3000`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# npm
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm dev
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn dev
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production
|
|
||||||
|
|
||||||
Build the application for production:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# npm
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm build
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn build
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun run build
|
|
||||||
```
|
|
||||||
|
|
||||||
Locally preview production build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# npm
|
|
||||||
npm run preview
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm preview
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn preview
|
|
||||||
|
|
||||||
# bun
|
|
||||||
bun run preview
|
|
||||||
```
|
|
||||||
|
|
||||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<NuxtPage/>
|
|
||||||
</template>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export default defineNuxtConfig({
|
|
||||||
compatibilityDate: '2025-07-15',
|
|
||||||
devtools: { enabled: true },
|
|
||||||
ssr: false,
|
|
||||||
modules: ['@nuxtjs/tailwindcss'],
|
|
||||||
typescript: {
|
|
||||||
strict: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
11892
frontend/package-lock.json
generated
11892
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "frontend",
|
|
||||||
"type": "module",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"build": "nuxt build",
|
|
||||||
"dev": "nuxt dev",
|
|
||||||
"generate": "nuxt generate",
|
|
||||||
"preview": "nuxt preview",
|
|
||||||
"postinstall": "nuxt prepare",
|
|
||||||
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"nuxt": "^4.2.2",
|
|
||||||
"vue": "^3.5.26",
|
|
||||||
"vue-router": "^4.6.4"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="min-h-screen flex items-center justify-center">
|
|
||||||
<h1 class="text-3xl font-bold">Nuxt OK ✅</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,2 +0,0 @@
|
|||||||
User-Agent: *
|
|
||||||
Disallow:
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
// https://nuxt.com/docs/guide/concepts/typescript
|
|
||||||
"files": [],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./.nuxt/tsconfig.app.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./.nuxt/tsconfig.server.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./.nuxt/tsconfig.shared.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./.nuxt/tsconfig.node.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
6
makefile
6
makefile
@@ -37,7 +37,7 @@ start: env-init
|
|||||||
@echo "URLs disponibles:"
|
@echo "URLs disponibles:"
|
||||||
@echo "- Symfony API: http://localhost:8081/api"
|
@echo "- Symfony API: http://localhost:8081/api"
|
||||||
@echo "- Nuxt (Inventory_frontend): http://localhost:3001"
|
@echo "- Nuxt (Inventory_frontend): http://localhost:3001"
|
||||||
@echo "- pgAdmin: http://localhost:5050"
|
@echo "- adminer: http://localhost:5050"
|
||||||
|
|
||||||
# Éteint le container
|
# Éteint le container
|
||||||
stop:
|
stop:
|
||||||
@@ -117,6 +117,10 @@ php-cs-fixer-allow-risky:
|
|||||||
test:
|
test:
|
||||||
$(EXEC_PHP) php -d memory_limit="512M" vendor/bin/phpunit $(FILES)
|
$(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:
|
wait:
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
```
|
|
||||||
@@ -20,165 +20,792 @@ final class Version20260125143939 extends AbstractMigration
|
|||||||
public function up(Schema $schema): void
|
public function up(Schema $schema): void
|
||||||
{
|
{
|
||||||
// this up() migration is auto-generated, please modify it to your needs
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
$this->addSql('ALTER INDEX idx_f95a3199df92e79b RENAME TO IDX_F95A3199CC8A4CEE');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX idx_f95a3199a3fdb2a7 RENAME TO IDX_F95A319936799605');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER TABLE _composantconstructeurs DROP CONSTRAINT "_ComposantConstructeurs_A_fkey"');
|
IF to_regclass('idx_f95a3199df92e79b') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER TABLE _composantconstructeurs DROP CONSTRAINT "_ComposantConstructeurs_B_fkey"');
|
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 A TYPE VARCHAR(36)');
|
||||||
$this->addSql('ALTER TABLE _composantconstructeurs ALTER B 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_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 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('ALTER TABLE _composantconstructeurs ADD PRIMARY KEY (A, B)');
|
||||||
$this->addSql('ALTER INDEX idx_5b97d813e8b7be43 RENAME TO IDX_60760125D3D99E8B');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX _composantconstructeurs_b_index RENAME TO IDX_607601254AD0CF31');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER INDEX idx_6b64d7ff6736d61 RENAME TO IDX_6B64D7FF5C4A705F');
|
IF to_regclass('idx_5b97d813e8b7be43') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER INDEX idx_6b64d7fff6bae05f RENAME TO IDX_6B64D7FF633EC4FD');
|
EXECUTE 'ALTER INDEX idx_5b97d813e8b7be43 RENAME TO IDX_60760125D3D99E8B';
|
||||||
$this->addSql('ALTER INDEX idx_6b64d7ffa1dac1c6 RENAME TO IDX_6B64D7FF345EE564');
|
END IF;
|
||||||
$this->addSql('ALTER INDEX idx_6b64d7ff96428d73 RENAME TO IDX_6B64D7FF3C6A9D1');
|
END $$;
|
||||||
$this->addSql('ALTER INDEX idx_6b64d7ffa3fdb2a7 RENAME TO IDX_6B64D7FF36799605');
|
SQL
|
||||||
$this->addSql('ALTER INDEX idx_4a48378c158582c3 RENAME TO IDX_4A48378C2F024C2');
|
);
|
||||||
$this->addSql('ALTER INDEX idx_4a48378cdf92e79b RENAME TO IDX_4A48378CCC8A4CEE');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX idx_4a48378c4ca601c8 RENAME TO IDX_4A48378C169F1CF6');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER INDEX idx_4a48378c40c2d03b RENAME TO IDX_4A48378C57B7763A');
|
IF to_regclass('_composantconstructeurs_b_index') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER INDEX idx_a2b07288f6bae05f RENAME TO IDX_A2B07288633EC4FD');
|
EXECUTE 'ALTER INDEX _composantconstructeurs_b_index RENAME TO IDX_607601254AD0CF31';
|
||||||
$this->addSql('ALTER INDEX idx_a2b07288a1dac1c6 RENAME TO IDX_A2B07288345EE564');
|
END IF;
|
||||||
$this->addSql('ALTER INDEX idx_a2b0728896428d73 RENAME TO IDX_A2B072883C6A9D1');
|
END $$;
|
||||||
$this->addSql('ALTER INDEX idx_a2b07288a3fdb2a7 RENAME TO IDX_A2B0728836799605');
|
SQL
|
||||||
$this->addSql('ALTER INDEX idx_a2b07288fcf7805f RENAME TO IDX_A2B072886973A4FD');
|
);
|
||||||
$this->addSql('ALTER INDEX idx_528efe19f6bae05f RENAME TO IDX_528EFE19633EC4FD');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX idx_528efe19a1dac1c6 RENAME TO IDX_528EFE19345EE564');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER INDEX idx_528efe197d44d2df RENAME TO IDX_528EFE19EF6CF34B');
|
IF to_regclass('idx_6b64d7ff6736d61') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER INDEX idx_528efe19bcced9e3 RENAME TO IDX_528EFE19C44B383C');
|
EXECUTE 'ALTER INDEX idx_6b64d7ff6736d61 RENAME TO IDX_6B64D7FF5C4A705F';
|
||||||
$this->addSql('ALTER INDEX idx_62941615f6bae05f RENAME TO IDX_62941615633EC4FD');
|
END IF;
|
||||||
$this->addSql('ALTER INDEX idx_6294161596428d73 RENAME TO IDX_629416153C6A9D1');
|
END $$;
|
||||||
$this->addSql('ALTER INDEX idx_629416157d44d2df RENAME TO IDX_62941615EF6CF34B');
|
SQL
|
||||||
$this->addSql('ALTER INDEX idx_6294161532c54aaf RENAME TO IDX_62941615F957D314');
|
);
|
||||||
$this->addSql('ALTER INDEX machine_product_links_machineid_idx RENAME TO IDX_8CC32259633EC4FD');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX machine_product_links_productid_idx RENAME TO IDX_8CC3225936799605');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER INDEX idx_8cc32259357fdbff RENAME TO IDX_8CC32259B590B209');
|
IF to_regclass('idx_6b64d7fff6bae05f') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER INDEX idx_8cc322597d44d2df RENAME TO IDX_8CC32259EF6CF34B');
|
EXECUTE 'ALTER INDEX idx_6b64d7fff6bae05f RENAME TO IDX_6B64D7FF633EC4FD';
|
||||||
$this->addSql('ALTER INDEX idx_8cc32259bcd7dad6 RENAME TO IDX_8CC32259A63AC5DC');
|
END IF;
|
||||||
$this->addSql('ALTER INDEX idx_8cc3225987ceb33f RENAME TO IDX_8CC32259937A1D7C');
|
END $$;
|
||||||
$this->addSql('ALTER INDEX idx_f1ce8dedfcf7805f RENAME TO IDX_F1CE8DED6973A4FD');
|
SQL
|
||||||
$this->addSql('ALTER INDEX idx_f1ce8ded158582c3 RENAME TO IDX_F1CE8DED2F024C2');
|
);
|
||||||
$this->addSql('ALTER TABLE _machineconstructeurs DROP CONSTRAINT "_MachineConstructeurs_B_fkey"');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER TABLE _machineconstructeurs DROP CONSTRAINT "_MachineConstructeurs_A_fkey"');
|
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 A TYPE VARCHAR(36)');
|
||||||
$this->addSql('ALTER TABLE _machineconstructeurs ALTER B 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_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 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('ALTER TABLE _machineconstructeurs ADD PRIMARY KEY (A, B)');
|
||||||
$this->addSql('ALTER INDEX idx_4f225b32e8b7be43 RENAME TO IDX_E6A040CCD3D99E8B');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX _machineconstructeurs_b_index RENAME TO IDX_E6A040CC4AD0CF31');
|
DO $$ BEGIN
|
||||||
$this->addSql('DROP INDEX "ModelType_category_name_key"');
|
IF to_regclass('idx_4f225b32e8b7be43') IS NOT NULL THEN
|
||||||
$this->addSql('DROP INDEX "ModelType_code_key"');
|
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 id TYPE VARCHAR(36)');
|
||||||
$this->addSql('ALTER TABLE model_types ALTER category TYPE VARCHAR(255)');
|
$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 createdAt DROP DEFAULT');
|
||||||
$this->addSql('ALTER TABLE model_types ALTER componentSkeleton TYPE JSON');
|
$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 pieceSkeleton TYPE JSON');
|
||||||
$this->addSql('ALTER TABLE model_types ALTER productSkeleton TYPE JSON');
|
$this->addSql('ALTER TABLE model_types ALTER productSkeleton TYPE JSON');
|
||||||
$this->addSql('ALTER INDEX idx_b92d74724ca601c8 RENAME TO IDX_B92D7472169F1CF6');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX idx_b92d7472a3fdb2a7 RENAME TO IDX_B92D747236799605');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER TABLE _piececonstructeurs DROP CONSTRAINT "_PieceConstructeurs_A_fkey"');
|
IF to_regclass('idx_b92d74724ca601c8') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER TABLE _piececonstructeurs DROP CONSTRAINT "_PieceConstructeurs_B_fkey"');
|
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 A TYPE VARCHAR(36)');
|
||||||
$this->addSql('ALTER TABLE _piececonstructeurs ALTER B 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_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 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('ALTER TABLE _piececonstructeurs ADD PRIMARY KEY (A, B)');
|
||||||
$this->addSql('ALTER INDEX idx_77fc120e8b7be43 RENAME TO IDX_E94732E5D3D99E8B');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX _piececonstructeurs_b_index RENAME TO IDX_E94732E54AD0CF31');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER INDEX idx_b3ba5a5a40c2d03b RENAME TO IDX_B3BA5A5A57B7763A');
|
IF to_regclass('idx_77fc120e8b7be43') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER TABLE _productconstructeurs DROP CONSTRAINT "_ProductConstructeurs_B_fkey"');
|
EXECUTE 'ALTER INDEX idx_77fc120e8b7be43 RENAME TO IDX_E94732E5D3D99E8B';
|
||||||
$this->addSql('ALTER TABLE _productconstructeurs DROP CONSTRAINT "_ProductConstructeurs_A_fkey"');
|
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 A TYPE VARCHAR(36)');
|
||||||
$this->addSql('ALTER TABLE _productconstructeurs ALTER B 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_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 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('ALTER TABLE _productconstructeurs ADD PRIMARY KEY (A, B)');
|
||||||
$this->addSql('ALTER INDEX idx_66f61802e8b7be43 RENAME TO IDX_CF7403FCD3D99E8B');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX _productconstructeurs_b_index RENAME TO IDX_CF7403FC4AD0CF31');
|
DO $$ BEGIN
|
||||||
$this->addSql('DROP INDEX uniq_profiles_email');
|
IF to_regclass('idx_66f61802e8b7be43') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER INDEX idx_96958790158582c3 RENAME TO IDX_969587902F024C2');
|
EXECUTE 'ALTER INDEX idx_66f61802e8b7be43 RENAME TO IDX_CF7403FCD3D99E8B';
|
||||||
$this->addSql('ALTER INDEX idx_96958790df92e79b RENAME TO IDX_96958790CC8A4CEE');
|
END IF;
|
||||||
$this->addSql('ALTER INDEX idx_f609e59e158582c3 RENAME TO IDX_F609E59E2F024C2');
|
END $$;
|
||||||
$this->addSql('ALTER INDEX idx_f609e59e4ca601c8 RENAME TO IDX_F609E59E169F1CF6');
|
SQL
|
||||||
$this->addSql('ALTER INDEX idx_29a51f98158582c3 RENAME TO IDX_29A51F982F024C2');
|
);
|
||||||
$this->addSql('ALTER INDEX idx_29a51f9840c2d03b RENAME TO IDX_29A51F9857B7763A');
|
$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
|
public function down(Schema $schema): void
|
||||||
{
|
{
|
||||||
// this down() migration is auto-generated, please modify it to your needs
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT FK_60760125D3D99E8B');
|
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT IF EXISTS FK_60760125D3D99E8B');
|
||||||
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT FK_607601254AD0CF31');
|
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT IF EXISTS FK_607601254AD0CF31');
|
||||||
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT _ComposantConstructeurs_pkey');
|
$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 a TYPE TEXT');
|
||||||
$this->addSql('ALTER TABLE _ComposantConstructeurs ALTER b 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_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('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('ALTER INDEX idx_607601254ad0cf31 RENAME TO "_ComposantConstructeurs_B_index"');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX idx_60760125d3d99e8b RENAME TO IDX_5B97D813E8B7BE43');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT FK_E6A040CCD3D99E8B');
|
IF to_regclass('idx_607601254ad0cf31') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT FK_E6A040CC4AD0CF31');
|
EXECUTE 'ALTER INDEX idx_607601254ad0cf31 RENAME TO "_ComposantConstructeurs_B_index"';
|
||||||
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT _MachineConstructeurs_pkey');
|
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 a TYPE TEXT');
|
||||||
$this->addSql('ALTER TABLE _MachineConstructeurs ALTER b 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_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('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 INDEX idx_e6a040cc4ad0cf31 RENAME TO "_MachineConstructeurs_B_index"');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX idx_e6a040ccd3d99e8b RENAME TO IDX_4F225B32E8B7BE43');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT FK_E94732E5D3D99E8B');
|
IF to_regclass('idx_e6a040cc4ad0cf31') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT FK_E94732E54AD0CF31');
|
EXECUTE 'ALTER INDEX idx_e6a040cc4ad0cf31 RENAME TO "_MachineConstructeurs_B_index"';
|
||||||
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT _PieceConstructeurs_pkey');
|
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 a TYPE TEXT');
|
||||||
$this->addSql('ALTER TABLE _PieceConstructeurs ALTER b 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_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('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('ALTER INDEX idx_e94732e54ad0cf31 RENAME TO "_PieceConstructeurs_B_index"');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX idx_e94732e5d3d99e8b RENAME TO IDX_77FC120E8B7BE43');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT FK_CF7403FCD3D99E8B');
|
IF to_regclass('idx_e94732e54ad0cf31') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT FK_CF7403FC4AD0CF31');
|
EXECUTE 'ALTER INDEX idx_e94732e54ad0cf31 RENAME TO "_PieceConstructeurs_B_index"';
|
||||||
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT _ProductConstructeurs_pkey');
|
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 a TYPE TEXT');
|
||||||
$this->addSql('ALTER TABLE _ProductConstructeurs ALTER b 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_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('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('ALTER INDEX idx_cf7403fc4ad0cf31 RENAME TO "_ProductConstructeurs_B_index"');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX idx_cf7403fcd3d99e8b RENAME TO IDX_66F61802E8B7BE43');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER INDEX idx_f95a319936799605 RENAME TO IDX_F95A3199A3FDB2A7');
|
IF to_regclass('idx_cf7403fc4ad0cf31') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER INDEX idx_f95a3199cc8a4cee RENAME TO IDX_F95A3199DF92E79B');
|
EXECUTE 'ALTER INDEX idx_cf7403fc4ad0cf31 RENAME TO "_ProductConstructeurs_B_index"';
|
||||||
$this->addSql('ALTER INDEX idx_6b64d7ff345ee564 RENAME TO IDX_6B64D7FFA1DAC1C6');
|
END IF;
|
||||||
$this->addSql('ALTER INDEX idx_6b64d7ff5c4a705f RENAME TO IDX_6B64D7FF6736D61');
|
END $$;
|
||||||
$this->addSql('ALTER INDEX idx_6b64d7ff633ec4fd RENAME TO IDX_6B64D7FFF6BAE05F');
|
SQL
|
||||||
$this->addSql('ALTER INDEX idx_6b64d7ff3c6a9d1 RENAME TO IDX_6B64D7FF96428D73');
|
);
|
||||||
$this->addSql('ALTER INDEX idx_6b64d7ff36799605 RENAME TO IDX_6B64D7FFA3FDB2A7');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX idx_4a48378c57b7763a RENAME TO IDX_4A48378C40C2D03B');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER INDEX idx_4a48378c2f024c2 RENAME TO IDX_4A48378C158582C3');
|
IF to_regclass('idx_cf7403fcd3d99e8b') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER INDEX idx_4a48378c169f1cf6 RENAME TO IDX_4A48378C4CA601C8');
|
EXECUTE 'ALTER INDEX idx_cf7403fcd3d99e8b RENAME TO IDX_66F61802E8B7BE43';
|
||||||
$this->addSql('ALTER INDEX idx_4a48378ccc8a4cee RENAME TO IDX_4A48378CDF92E79B');
|
END IF;
|
||||||
$this->addSql('ALTER INDEX idx_a2b07288345ee564 RENAME TO IDX_A2B07288A1DAC1C6');
|
END $$;
|
||||||
$this->addSql('ALTER INDEX idx_a2b07288633ec4fd RENAME TO IDX_A2B07288F6BAE05F');
|
SQL
|
||||||
$this->addSql('ALTER INDEX idx_a2b072886973a4fd RENAME TO IDX_A2B07288FCF7805F');
|
);
|
||||||
$this->addSql('ALTER INDEX idx_a2b072883c6a9d1 RENAME TO IDX_A2B0728896428D73');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX idx_a2b0728836799605 RENAME TO IDX_A2B07288A3FDB2A7');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER INDEX idx_528efe19345ee564 RENAME TO IDX_528EFE19A1DAC1C6');
|
IF to_regclass('idx_f95a319936799605') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER INDEX idx_528efe19633ec4fd RENAME TO IDX_528EFE19F6BAE05F');
|
EXECUTE 'ALTER INDEX idx_f95a319936799605 RENAME TO IDX_F95A3199A3FDB2A7';
|
||||||
$this->addSql('ALTER INDEX idx_528efe19ef6cf34b RENAME TO IDX_528EFE197D44D2DF');
|
END IF;
|
||||||
$this->addSql('ALTER INDEX idx_528efe19c44b383c RENAME TO IDX_528EFE19BCCED9E3');
|
END $$;
|
||||||
$this->addSql('ALTER INDEX idx_62941615ef6cf34b RENAME TO IDX_629416157D44D2DF');
|
SQL
|
||||||
$this->addSql('ALTER INDEX idx_62941615633ec4fd RENAME TO IDX_62941615F6BAE05F');
|
);
|
||||||
$this->addSql('ALTER INDEX idx_629416153c6a9d1 RENAME TO IDX_6294161596428D73');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX idx_62941615f957d314 RENAME TO IDX_6294161532C54AAF');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER INDEX idx_8cc32259633ec4fd RENAME TO "machine_product_links_machineId_idx"');
|
IF to_regclass('idx_f95a3199cc8a4cee') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER INDEX idx_8cc3225936799605 RENAME TO "machine_product_links_productId_idx"');
|
EXECUTE 'ALTER INDEX idx_f95a3199cc8a4cee RENAME TO IDX_F95A3199DF92E79B';
|
||||||
$this->addSql('ALTER INDEX idx_8cc32259ef6cf34b RENAME TO IDX_8CC322597D44D2DF');
|
END IF;
|
||||||
$this->addSql('ALTER INDEX idx_8cc32259b590b209 RENAME TO IDX_8CC32259357FDBFF');
|
END $$;
|
||||||
$this->addSql('ALTER INDEX idx_8cc32259a63ac5dc RENAME TO IDX_8CC32259BCD7DAD6');
|
SQL
|
||||||
$this->addSql('ALTER INDEX idx_8cc32259937a1d7c RENAME TO IDX_8CC3225987CEB33F');
|
);
|
||||||
$this->addSql('ALTER INDEX idx_f1ce8ded2f024c2 RENAME TO IDX_F1CE8DED158582C3');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX idx_f1ce8ded6973a4fd RENAME TO IDX_F1CE8DEDFCF7805F');
|
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 id TYPE TEXT');
|
||||||
$this->addSql('ALTER TABLE model_types ALTER category TYPE VARCHAR');
|
$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 componentskeleton TYPE JSONB');
|
||||||
@@ -187,15 +814,78 @@ final class Version20260125143939 extends AbstractMigration
|
|||||||
$this->addSql('ALTER TABLE model_types ALTER createdat SET DEFAULT CURRENT_TIMESTAMP');
|
$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_category_name_key" ON model_types (category, name)');
|
||||||
$this->addSql('CREATE UNIQUE INDEX "ModelType_code_key" ON model_types (code)');
|
$this->addSql('CREATE UNIQUE INDEX "ModelType_code_key" ON model_types (code)');
|
||||||
$this->addSql('ALTER INDEX idx_b92d7472169f1cf6 RENAME TO IDX_B92D74724CA601C8');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX idx_b92d747236799605 RENAME TO IDX_B92D7472A3FDB2A7');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER INDEX idx_b3ba5a5a57b7763a RENAME TO IDX_B3BA5A5A40C2D03B');
|
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('CREATE UNIQUE INDEX uniq_profiles_email ON profiles (email)');
|
||||||
$this->addSql('ALTER INDEX idx_969587902f024c2 RENAME TO IDX_96958790158582C3');
|
$this->addSql(<<<'SQL'
|
||||||
$this->addSql('ALTER INDEX idx_96958790cc8a4cee RENAME TO IDX_96958790DF92E79B');
|
DO $$ BEGIN
|
||||||
$this->addSql('ALTER INDEX idx_f609e59e169f1cf6 RENAME TO IDX_F609E59E4CA601C8');
|
IF to_regclass('idx_969587902f024c2') IS NOT NULL THEN
|
||||||
$this->addSql('ALTER INDEX idx_f609e59e2f024c2 RENAME TO IDX_F609E59E158582C3');
|
EXECUTE 'ALTER INDEX idx_969587902f024c2 RENAME TO IDX_96958790158582C3';
|
||||||
$this->addSql('ALTER INDEX idx_29a51f9857b7763a RENAME TO IDX_29A51F9840C2D03B');
|
END IF;
|
||||||
$this->addSql('ALTER INDEX idx_29a51f982f024c2 RENAME TO IDX_29A51F98158582C3');
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_96958790cc8a4cee') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_96958790cc8a4cee RENAME TO IDX_96958790DF92E79B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_f609e59e169f1cf6') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_f609e59e169f1cf6 RENAME TO IDX_F609E59E4CA601C8';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_f609e59e2f024c2') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_f609e59e2f024c2 RENAME TO IDX_F609E59E158582C3';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_29a51f9857b7763a') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_29a51f9857b7763a RENAME TO IDX_29A51F9840C2D03B';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF to_regclass('idx_29a51f982f024c2') IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER INDEX idx_29a51f982f024c2 RENAME TO IDX_29A51F98158582C3';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
SQL
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
migrations/Version20260125170000.php
Normal file
41
migrations/Version20260125170000.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260125170000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add audit_logs table to store per-entity history entries.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
CREATE TABLE audit_logs (
|
||||||
|
id VARCHAR(36) NOT NULL,
|
||||||
|
entityType VARCHAR(50) NOT NULL,
|
||||||
|
entityId VARCHAR(36) NOT NULL,
|
||||||
|
action VARCHAR(20) NOT NULL,
|
||||||
|
diff JSON DEFAULT NULL,
|
||||||
|
snapshot JSON DEFAULT NULL,
|
||||||
|
actorProfileId VARCHAR(36) DEFAULT NULL,
|
||||||
|
createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||||
|
PRIMARY KEY(id)
|
||||||
|
)
|
||||||
|
SQL);
|
||||||
|
|
||||||
|
$this->addSql('CREATE INDEX idx_audit_entity ON audit_logs (entityType, entityId)');
|
||||||
|
$this->addSql('CREATE INDEX idx_audit_created_at ON audit_logs (createdAt)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE audit_logs');
|
||||||
|
}
|
||||||
|
}
|
||||||
51
migrations/Version20260302103003.php
Normal file
51
migrations/Version20260302103003.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260302103003 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create comments table + make piece reference unique instead of name';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// Comments table (IF NOT EXISTS in case first attempt partially succeeded)
|
||||||
|
$this->addSql('CREATE TABLE IF NOT EXISTS comments (id VARCHAR(36) NOT NULL, content TEXT NOT NULL, entity_type VARCHAR(50) NOT NULL, entity_id VARCHAR(36) NOT NULL, entity_name VARCHAR(255) DEFAULT NULL, author_id VARCHAR(36) NOT NULL, author_name VARCHAR(255) NOT NULL, status VARCHAR(20) NOT NULL, resolved_by_id VARCHAR(36) DEFAULT NULL, resolved_by_name VARCHAR(255) DEFAULT NULL, resolved_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comment_entity_status ON comments (entity_type, entity_id, status)');
|
||||||
|
$this->addSql('COMMENT ON COLUMN comments.resolved_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN comments.created_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN comments.updated_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
|
||||||
|
// Piece: remove unique constraint on name (it's a constraint, not just an index)
|
||||||
|
$this->addSql('ALTER TABLE pieces DROP CONSTRAINT IF EXISTS uniq_b92d74725e237e06');
|
||||||
|
|
||||||
|
// Deduplicate piece references before adding unique constraint
|
||||||
|
$this->addSql("
|
||||||
|
UPDATE pieces p
|
||||||
|
SET reference = p.reference || '-' || LEFT(p.id, 6)
|
||||||
|
FROM (
|
||||||
|
SELECT id, reference,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY reference ORDER BY createdat) AS rn
|
||||||
|
FROM pieces
|
||||||
|
WHERE reference IS NOT NULL AND reference != ''
|
||||||
|
) dup
|
||||||
|
WHERE p.id = dup.id AND dup.rn > 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS uniq_pieces_reference ON pieces (reference)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE IF EXISTS comments');
|
||||||
|
$this->addSql('DROP INDEX IF EXISTS uniq_pieces_reference');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_b92d74725e237e06 ON pieces (name)');
|
||||||
|
}
|
||||||
|
}
|
||||||
28
migrations/Version20260302120000.php
Normal file
28
migrations/Version20260302120000.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260302120000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add description column to pieces and composants tables';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE pieces ADD COLUMN IF NOT EXISTS description TEXT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE composants ADD COLUMN IF NOT EXISTS description TEXT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE pieces DROP COLUMN IF EXISTS description');
|
||||||
|
$this->addSql('ALTER TABLE composants DROP COLUMN IF EXISTS description');
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user