Files
Inventory/docs/superpowers/specs/2026-04-02-machine-context-custom-fields-design.md
2026-04-03 09:25:07 +02:00

6.1 KiB

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 :

#[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 :

#[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.

Nouvelle collection :

#[ORM\OneToMany(targetEntity: CustomFieldValue::class, mappedBy: 'machineComponentLink', cascade: ['persist', 'remove'], orphanRemoval: true)]
private Collection $contextFieldValues;

Idem sur MachinePieceLink avec mappedBy: 'machinePieceLink'.

4. Migration

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 :
{
  "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