diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..1da8c3d --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,6 @@ +{ + "enabledPlugins": { + "security-guidance@claude-plugins-official": true, + "claude-md-management@claude-plugins-official": true + } +} diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore index f580e83..3f48653 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ - ###> symfony/framework-bundle ### /.env.local /.env.local.php @@ -23,19 +22,16 @@ ###> docker ### docker/.env.docker.local ###< docker ### - -###> lexik/jwt-authentication-bundle ### -/config/jwt/*.pem -###< lexik/jwt-authentication-bundle ### - ###> migration archives ### /_archives/ ###< migration archives ### ###> temp files ### *.sql +*.sql.gz *.har FEATURE_IDEAS.md +bin/.phpunit.result.cache ###< temp files ### ###> frontend ### diff --git a/CLAUDE.md b/CLAUDE.md index b0234f9..84034bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,6 +26,7 @@ Voir `docs/GLOSSAIRE_METIER.md` — glossaire complet du domaine métier (concep ``` 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…) @@ -148,6 +149,7 @@ Remplacent les anciennes colonnes JSON `structure` et `productIds` par des table - `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 @@ -165,6 +167,8 @@ Remplacent les anciennes colonnes JSON `structure` et `productIds` par des table - `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/`) @@ -176,6 +180,7 @@ Remplacent les anciennes colonnes JSON `structure` et `productIds` par des table ### 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) ``` diff --git a/Inventory_frontend b/Inventory_frontend index c82c21c..958a00c 160000 --- a/Inventory_frontend +++ b/Inventory_frontend @@ -1 +1 @@ -Subproject commit c82c21c0cdf069349411393f9d73199ca9879797 +Subproject commit 958a00c8fc1a67ea5d66c0c73f6a84c7561aea4e diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..df82bd4 --- /dev/null +++ b/TODO.md @@ -0,0 +1,29 @@ +# TODO — MCP Inventory + +## 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 diff --git a/composer.json b/composer.json index 4b4392d..112619d 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,6 @@ "doctrine/doctrine-bundle": "^3.2", "doctrine/doctrine-migrations-bundle": "^4.0", "doctrine/orm": "^3.6", - "lexik/jwt-authentication-bundle": "^3.2", "nelmio/cors-bundle": "^2.6", "nyholm/psr7": "^1.8", "phpdocumentor/reflection-docblock": "^5.6", @@ -33,8 +32,7 @@ "symfony/twig-bundle": "8.0.*", "symfony/uid": "8.0.*", "symfony/validator": "8.0.*", - "symfony/yaml": "8.0.*", - "vich/uploader-bundle": "^2.9" + "symfony/yaml": "8.0.*" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index cbcceec..f931e3c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b15a7808211e724ca29dd78602df3aab", + "content-hash": "2db01f705a09cf38007a2baa3b078e49", "packages": [ { "name": "api-platform/doctrine-common", @@ -2361,259 +2361,6 @@ }, "time": "2025-10-26T09:35:14+00:00" }, - { - "name": "jms/metadata", - "version": "2.9.0", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/metadata.git", - "reference": "554319d2e5f0c5d8ccaeffe755eac924e14da330" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/554319d2e5f0c5d8ccaeffe755eac924e14da330", - "reference": "554319d2e5f0c5d8ccaeffe755eac924e14da330", - "shasum": "" - }, - "require": { - "php": "^7.2|^8.0" - }, - "require-dev": { - "doctrine/cache": "^1.0|^2.0", - "doctrine/coding-standard": "^8.0", - "mikey179/vfsstream": "^1.6.7", - "phpunit/phpunit": "^8.5.42|^9.6.23", - "psr/container": "^1.0|^2.0", - "symfony/cache": "^3.1|^4.0|^5.0|^6.0|^7.0|^8.0", - "symfony/dependency-injection": "^3.1|^4.0|^5.0|^6.0|^7.0|^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Metadata\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com" - }, - { - "name": "Asmir Mustafic", - "email": "goetas@gmail.com" - } - ], - "description": "Class/method/property metadata management in PHP", - "keywords": [ - "annotations", - "metadata", - "xml", - "yaml" - ], - "support": { - "issues": "https://github.com/schmittjoh/metadata/issues", - "source": "https://github.com/schmittjoh/metadata/tree/2.9.0" - }, - "time": "2025-11-30T20:12:26+00:00" - }, - { - "name": "lcobucci/jwt", - "version": "5.6.0", - "source": { - "type": "git", - "url": "https://github.com/lcobucci/jwt.git", - "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e", - "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e", - "shasum": "" - }, - "require": { - "ext-openssl": "*", - "ext-sodium": "*", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", - "psr/clock": "^1.0" - }, - "require-dev": { - "infection/infection": "^0.29", - "lcobucci/clock": "^3.2", - "lcobucci/coding-standard": "^11.0", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.10.7", - "phpstan/phpstan-deprecation-rules": "^1.1.3", - "phpstan/phpstan-phpunit": "^1.3.10", - "phpstan/phpstan-strict-rules": "^1.5.0", - "phpunit/phpunit": "^11.1" - }, - "suggest": { - "lcobucci/clock": ">= 3.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "Lcobucci\\JWT\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Luís Cobucci", - "email": "lcobucci@gmail.com", - "role": "Developer" - } - ], - "description": "A simple library to work with JSON Web Token and JSON Web Signature", - "keywords": [ - "JWS", - "jwt" - ], - "support": { - "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.6.0" - }, - "funding": [ - { - "url": "https://github.com/lcobucci", - "type": "github" - }, - { - "url": "https://www.patreon.com/lcobucci", - "type": "patreon" - } - ], - "time": "2025-10-17T11:30:53+00:00" - }, - { - "name": "lexik/jwt-authentication-bundle", - "version": "v3.2.0", - "source": { - "type": "git", - "url": "https://github.com/lexik/LexikJWTAuthenticationBundle.git", - "reference": "60df75dc70ee6f597929cb2f0812adda591dfa4b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/lexik/LexikJWTAuthenticationBundle/zipball/60df75dc70ee6f597929cb2f0812adda591dfa4b", - "reference": "60df75dc70ee6f597929cb2f0812adda591dfa4b", - "shasum": "" - }, - "require": { - "ext-openssl": "*", - "lcobucci/jwt": "^5.0", - "php": ">=8.2", - "symfony/clock": "^6.4|^7.0|^8.0", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/deprecation-contracts": "^2.4|^3.0", - "symfony/event-dispatcher": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/property-access": "^6.4|^7.0|^8.0", - "symfony/security-bundle": "^6.4|^7.0|^8.0", - "symfony/translation-contracts": "^1.0|^2.0|^3.0" - }, - "require-dev": { - "api-platform/core": "^3.0|^4.0", - "rector/rector": "^1.2", - "symfony/browser-kit": "^6.4|^7.0|^8.0", - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/dom-crawler": "^6.4|^7.0|^8.0", - "symfony/filesystem": "^6.4|^7.0|^8.0", - "symfony/framework-bundle": "^6.4|^7.0|^8.0", - "symfony/phpunit-bridge": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0", - "symfony/yaml": "^6.4|^7.0|^8.0" - }, - "suggest": { - "gesdinet/jwt-refresh-token-bundle": "Implements a refresh token system over Json Web Tokens in Symfony", - "spomky-labs/lexik-jose-bridge": "Provides a JWT Token encoder with encryption support" - }, - "type": "symfony-bundle", - "autoload": { - "psr-4": { - "Lexik\\Bundle\\JWTAuthenticationBundle\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jeremy Barthe", - "email": "j.barthe@lexik.fr", - "homepage": "https://github.com/jeremyb" - }, - { - "name": "Nicolas Cabot", - "email": "n.cabot@lexik.fr", - "homepage": "https://github.com/slashfan" - }, - { - "name": "Cedric Girard", - "email": "c.girard@lexik.fr", - "homepage": "https://github.com/cedric-g" - }, - { - "name": "Dev Lexik", - "email": "dev@lexik.fr", - "homepage": "https://github.com/lexik" - }, - { - "name": "Robin Chalas", - "email": "robin.chalas@gmail.com", - "homepage": "https://github.com/chalasr" - }, - { - "name": "Lexik Community", - "homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle/graphs/contributors" - } - ], - "description": "This bundle provides JWT authentication for your Symfony REST API", - "homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle", - "keywords": [ - "Authentication", - "JWS", - "api", - "bundle", - "jwt", - "rest", - "symfony" - ], - "support": { - "issues": "https://github.com/lexik/LexikJWTAuthenticationBundle/issues", - "source": "https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v3.2.0" - }, - "funding": [ - { - "url": "https://github.com/chalasr", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/lexik/jwt-authentication-bundle", - "type": "tidelift" - } - ], - "time": "2025-12-20T17:47:00+00:00" - }, { "name": "mcp/sdk", "version": "v0.4.0", @@ -5594,92 +5341,6 @@ ], "time": "2026-03-04T16:39:24+00:00" }, - { - "name": "symfony/mime", - "version": "v8.0.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/mime.git", - "reference": "7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40", - "reference": "7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40", - "shasum": "" - }, - "require": { - "php": ">=8.4", - "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0" - }, - "conflict": { - "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0" - }, - "require-dev": { - "egulias/email-validator": "^2.1.10|^3.1|^4", - "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/process": "^7.4|^8.0", - "symfony/property-access": "^7.4|^8.0", - "symfony/property-info": "^7.4|^8.0", - "symfony/serializer": "^7.4|^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Mime\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Allows manipulating MIME messages", - "homepage": "https://symfony.com", - "keywords": [ - "mime", - "mime-type" - ], - "support": { - "source": "https://github.com/symfony/mime/tree/v8.0.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-11-16T10:17:21+00:00" - }, { "name": "symfony/options-resolver", "version": "v8.0.0", @@ -5906,93 +5567,6 @@ ], "time": "2025-06-27T09:58:17+00:00" }, - { - "name": "symfony/polyfill-intl-idn", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", - "shasum": "" - }, - "require": { - "php": ">=7.2", - "symfony/polyfill-intl-normalizer": "^1.10" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" - }, - { - "name": "Trevor Rowbotham", - "email": "trevor.rowbotham@pm.me" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "idn", - "intl", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-10T14:38:51+00:00" - }, { "name": "symfony/polyfill-intl-normalizer", "version": "v1.33.0", @@ -8426,114 +8000,6 @@ ], "time": "2025-12-14T11:28:47+00:00" }, - { - "name": "vich/uploader-bundle", - "version": "v2.9.1", - "source": { - "type": "git", - "url": "https://github.com/dustin10/VichUploaderBundle.git", - "reference": "945939a04a33c0b78c5fbb7ead31533d85112df5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/dustin10/VichUploaderBundle/zipball/945939a04a33c0b78c5fbb7ead31533d85112df5", - "reference": "945939a04a33c0b78c5fbb7ead31533d85112df5", - "shasum": "" - }, - "require": { - "doctrine/persistence": "^3.0 || ^4.0", - "ext-simplexml": "*", - "jms/metadata": "^2.4", - "php": "^8.1", - "symfony/config": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/event-dispatcher-contracts": "^3.1", - "symfony/http-foundation": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/mime": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/property-access": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/string": "^5.4 || ^6.0 || ^7.0 || ^8.0" - }, - "conflict": { - "doctrine/annotations": "<1.12", - "league/flysystem": "<2.0" - }, - "require-dev": { - "dg/bypass-finals": "^1.9", - "doctrine/common": "^3.0", - "doctrine/doctrine-bundle": "^2.7 || ^3.0", - "doctrine/mongodb-odm": "^2.4", - "doctrine/orm": "^2.13 || ^3.0", - "ext-sqlite3": "*", - "knplabs/knp-gaufrette-bundle": "dev-master", - "league/flysystem-bundle": "^2.4 || ^3.0", - "league/flysystem-memory": "^2.0 || ^3.0", - "matthiasnoback/symfony-dependency-injection-test": "^5.1 || ^6.0", - "mikey179/vfsstream": "^1.6.11", - "phpunit/phpunit": "^10.5 || ^11.5 || ^12.2", - "symfony/asset": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/browser-kit": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/doctrine-bridge": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/dom-crawler": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/form": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/phpunit-bridge": "^7.3", - "symfony/security-csrf": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/translation": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/twig-bridge": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/twig-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/validator": "^5.4.22 || ^6.0 || ^7.0 || ^8.0", - "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0" - }, - "suggest": { - "doctrine/doctrine-bundle": "For integration with Doctrine", - "doctrine/mongodb-odm-bundle": "For integration with Doctrine ODM", - "doctrine/orm": "For integration with Doctrine ORM", - "doctrine/phpcr-odm": "For integration with Doctrine PHPCR", - "knplabs/knp-gaufrette-bundle": "For integration with Gaufrette", - "league/flysystem-bundle": "For integration with Flysystem", - "liip/imagine-bundle": "To generate image thumbnails", - "oneup/flysystem-bundle": "For integration with Flysystem", - "symfony/asset": "To generate better links", - "symfony/form": "To handle uploads in forms", - "symfony/yaml": "To use YAML mapping" - }, - "type": "symfony-bundle", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Vich\\UploaderBundle\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Dustin Dobervich", - "email": "ddobervich@gmail.com" - } - ], - "description": "Ease file uploads attached to entities", - "homepage": "https://github.com/dustin10/VichUploaderBundle", - "keywords": [ - "file uploads", - "upload" - ], - "support": { - "issues": "https://github.com/dustin10/VichUploaderBundle/issues", - "source": "https://github.com/dustin10/VichUploaderBundle/tree/v2.9.1" - }, - "time": "2025-12-10T08:23:38+00:00" - }, { "name": "webmozart/assert", "version": "2.0.0", diff --git a/config/bundles.php b/config/bundles.php index 861ea7a..186ae7d 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -6,7 +6,6 @@ use ApiPlatform\Symfony\Bundle\ApiPlatformBundle; use DAMA\DoctrineTestBundle\DAMADoctrineTestBundle; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle; -use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle; use Nelmio\CorsBundle\NelmioCorsBundle; use Symfony\AI\McpBundle\McpBundle; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; @@ -14,14 +13,13 @@ use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\TwigBundle\TwigBundle; return [ - FrameworkBundle::class => ['all' => true], - TwigBundle::class => ['all' => true], - SecurityBundle::class => ['all' => true], - DoctrineBundle::class => ['all' => true], - DoctrineMigrationsBundle::class => ['all' => true], - NelmioCorsBundle::class => ['all' => true], - ApiPlatformBundle::class => ['all' => true], - LexikJWTAuthenticationBundle::class => ['all' => true], - DAMADoctrineTestBundle::class => ['test' => true], - McpBundle::class => ['all' => true], + FrameworkBundle::class => ['all' => true], + TwigBundle::class => ['all' => true], + SecurityBundle::class => ['all' => true], + DoctrineBundle::class => ['all' => true], + DoctrineMigrationsBundle::class => ['all' => true], + NelmioCorsBundle::class => ['all' => true], + ApiPlatformBundle::class => ['all' => true], + DAMADoctrineTestBundle::class => ['test' => true], + McpBundle::class => ['all' => true], ]; diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 7e1ee1f..f0c5c6d 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -3,7 +3,10 @@ framework: secret: '%env(APP_SECRET)%' # 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 #fragments: true diff --git a/config/packages/lexik_jwt_authentication.yaml b/config/packages/lexik_jwt_authentication.yaml deleted file mode 100644 index edfb69d..0000000 --- a/config/packages/lexik_jwt_authentication.yaml +++ /dev/null @@ -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)%' diff --git a/config/packages/rate_limiter.yaml b/config/packages/rate_limiter.yaml index 87b497b..39bcbec 100644 --- a/config/packages/rate_limiter.yaml +++ b/config/packages/rate_limiter.yaml @@ -4,3 +4,7 @@ framework: policy: sliding_window limit: 5 interval: '1 minute' + login: + policy: sliding_window + limit: 5 + interval: '1 minute' diff --git a/docs/CUSTOM_FIELDS_AUDIT_RECOVERY.md b/docs/CUSTOM_FIELDS_AUDIT_RECOVERY.md new file mode 100644 index 0000000..32e199a --- /dev/null +++ b/docs/CUSTOM_FIELDS_AUDIT_RECOVERY.md @@ -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 +``` + +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 --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 +``` + +### Etape 4 - Appliquer uniquement apres verification + +```bash +php bin/console app:restore-piece-custom-field-values --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 + +# Application reelle +php bin/console app:restore-piece-custom-field-values --apply +``` + diff --git a/docs/CUSTOM_FIELDS_RECOVERABLE_RESULTS_2026-03-23.md b/docs/CUSTOM_FIELDS_RECOVERABLE_RESULTS_2026-03-23.md new file mode 100644 index 0000000..3ada2c1 --- /dev/null +++ b/docs/CUSTOM_FIELDS_RECOVERABLE_RESULTS_2026-03-23.md @@ -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 +``` + +Application reelle : + +```bash +php bin/console app:restore-piece-custom-field-values --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 +``` + diff --git a/docs/DOUBLONS_REFERENCES_COMPOSANTS.md b/docs/DOUBLONS_REFERENCES_COMPOSANTS.md new file mode 100644 index 0000000..8948825 --- /dev/null +++ b/docs/DOUBLONS_REFERENCES_COMPOSANTS.md @@ -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. diff --git a/docs/FONCTIONNEMENT.md b/docs/FONCTIONNEMENT.md new file mode 100644 index 0000000..9ea239d --- /dev/null +++ b/docs/FONCTIONNEMENT.md @@ -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) +``` diff --git a/docs/GLOSSAIRE_METIER.md b/docs/GLOSSAIRE_METIER.md new file mode 100644 index 0000000..ff757df --- /dev/null +++ b/docs/GLOSSAIRE_METIER.md @@ -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` | diff --git a/docs/REVIEW_ARCHITECTURE.md b/docs/REVIEW_ARCHITECTURE.md new file mode 100644 index 0000000..5440a1e --- /dev/null +++ b/docs/REVIEW_ARCHITECTURE.md @@ -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(null)` pour les entites : +- `useComponentEdit.ts` : `const component = ref(null)` (ligne 74) +- `usePieceEdit.ts` : `const piece = ref(null)` (ligne 56) +- `useMachineDetailData.ts` : `type AnyRecord = Record` + +**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` par `ref` + +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. diff --git a/docs/superpowers/plans/2026-03-23-comment-documents.md b/docs/superpowers/plans/2026-03-23-comment-documents.md new file mode 100644 index 0000000..cb716a4 --- /dev/null +++ b/docs/superpowers/plans/2026-03-23-comment-documents.md @@ -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 +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 */ +#[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 */ +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 => { + 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 = { 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 + +
+
+