docs(custom-fields) : add spec and implementation plans for machine context custom fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user