# Machine Context Custom Fields **Date** : 2026-04-02 **Statut** : Validé ## Objectif Permettre de définir des champs personnalisés sur un ModelType (catégorie de pièce/composant) qui ne s'affichent et ne sont remplissables que lorsque l'item est lié à une machine. Les valeurs sont propres au lien machine (une même pièce dans deux machines peut avoir des valeurs différentes). ## Périmètre - **Entités concernées** : Composants et Pièces (pas Produits) - **Définition** : Sur le ModelType, avec un flag `machineContextOnly` - **Valeurs** : Stockées par lien (`MachineComponentLink` / `MachinePieceLink`) - **Affichage** : Uniquement dans la vue machine, pas sur les fiches autonomes ## Architecture ### Approche retenue Extension des entités existantes `CustomField` et `CustomFieldValue` avec : - Un flag de filtrage sur la définition - Des FK vers les entités de lien pour les valeurs ### Alternatives écartées - **Entités séparées** (`MachineContextField` / `MachineContextFieldValue`) — trop de duplication de logique - **JSON sur les liens** — contraire au projet de normalisation JSON→tables en cours ## Backend ### 1. Entité `CustomField` Nouveau champ : ```php #[ORM\Column(type: 'boolean', options: ['default' => false])] #[Groups(['customField:read', 'customField:write'])] private bool $machineContextOnly = false; ``` Getter/setter associés. ### 2. Entité `CustomFieldValue` Nouvelles FK nullable : ```php #[ORM\ManyToOne(targetEntity: MachineComponentLink::class, inversedBy: 'contextFieldValues')] #[ORM\JoinColumn(nullable: true)] private ?MachineComponentLink $machineComponentLink = null; #[ORM\ManyToOne(targetEntity: MachinePieceLink::class, inversedBy: 'contextFieldValues')] #[ORM\JoinColumn(nullable: true)] private ?MachinePieceLink $machinePieceLink = null; ``` Contrainte métier : quand `machineComponentLink` est set, `composant` reste aussi set (pour faciliter les queries par composant). Idem pour `machinePieceLink` + `piece`. ### 3. Entités `MachineComponentLink` / `MachinePieceLink` Nouvelle collection : ```php #[ORM\OneToMany(targetEntity: CustomFieldValue::class, mappedBy: 'machineComponentLink', cascade: ['persist', 'remove'], orphanRemoval: true)] private Collection $contextFieldValues; ``` Idem sur `MachinePieceLink` avec `mappedBy: 'machinePieceLink'`. ### 4. Migration ```sql ALTER TABLE custom_field ADD machine_context_only BOOLEAN DEFAULT false NOT NULL; ALTER TABLE custom_field_value ADD machine_component_link_id VARCHAR(36) DEFAULT NULL; ALTER TABLE custom_field_value ADD machine_piece_link_id VARCHAR(36) DEFAULT NULL; ALTER TABLE custom_field_value ADD CONSTRAINT fk_cfv_machine_component_link FOREIGN KEY (machine_component_link_id) REFERENCES machine_component_link(id) ON DELETE CASCADE; ALTER TABLE custom_field_value ADD CONSTRAINT fk_cfv_machine_piece_link FOREIGN KEY (machine_piece_link_id) REFERENCES machine_piece_link(id) ON DELETE CASCADE; CREATE INDEX idx_cfv_machine_component_link ON custom_field_value(machine_component_link_id); CREATE INDEX idx_cfv_machine_piece_link ON custom_field_value(machine_piece_link_id); ``` ### 5. `MachineStructureController` Dans `normalizeComposant()` et `normalizePiece()` : - Récupérer les `CustomField` du ModelType où `machineContextOnly = true` - Récupérer les `CustomFieldValue` liées au lien via `machineComponentLink` / `machinePieceLink` - Ajouter dans la réponse : ```json { "contextCustomFields": [{ "id": "...", "name": "...", "type": "...", ... }], "contextCustomFieldValues": [{ "id": "...", "value": "...", "customField": {...} }] } ``` Séparé des `customFields` / `customFieldValues` globaux existants. ### 6. `CustomFieldValueController` L'upsert existant est étendu pour accepter `machineComponentLink` ou `machinePieceLink` dans le body. Le controller vérifie que si le `CustomField` a `machineContextOnly = true`, un lien machine est obligatoire. ### 7. Clonage machine `MachineStructureController::cloneCustomFields()` doit aussi cloner les `contextFieldValues` des liens, en les rattachant aux nouveaux liens créés lors du clone. ## Frontend ### 1. Page ModelType — Définition des champs Dans l'UI d'édition des custom fields d'un ModelType, ajouter un **toggle/checkbox** "Contexte machine uniquement" sur chaque définition de champ. Cela set `machineContextOnly: true` lors de la sauvegarde. Concerne les custom fields des catégories COMPONENT et PIECE (pas PRODUCT, hors périmètre). ### 2. Vue machine — `ComponentItem.vue` / `PieceItem.vue` Nouvelle section "Champs contextuels" affichée sous les custom fields existants : - Reçoit `contextCustomFields` et `contextCustomFieldValues` en props - Réutilise le composant `CustomFieldDisplay.vue` existant - Mode édition : sur blur/change, appel upsert via `CustomFieldValueController` avec le `machineComponentLinkId` ou `machinePieceLinkId` ### 3. Fiches autonomes pièce/composant Filtrer les champs `machineContextOnly = true` pour ne pas les afficher : - Dans `useEntityCustomFields` : exclure ces champs du `displayedCustomFields` - Dans `useMachineDetailCustomFields` : séparer les champs normaux des champs contextuels ### 4. Transformation des données (`useMachineDetailCustomFields`) `transformComponentCustomFields()` et `transformCustomFields()` : - Extraire `contextCustomFields` / `contextCustomFieldValues` depuis la réponse structure - Les passer en propriétés séparées sur l'objet transformé ## Tests ### Backend - Test unitaire : `CustomField` avec `machineContextOnly = true` est correctement sérialisé - Test API : upsert d'un `CustomFieldValue` avec `machineComponentLink` fonctionne - Test API : upsert d'un `CustomFieldValue` contextuel sans lien machine retourne une erreur - Test API : `/api/machines/{id}/structure` retourne les `contextCustomFields` et `contextCustomFieldValues` - Test API : clone machine copie les valeurs contextuelles ### Frontend - Typecheck : 0 erreurs après modifications - Vérification manuelle : les champs contextuels apparaissent dans la vue machine - Vérification manuelle : les champs contextuels n'apparaissent pas sur les fiches autonomes