# 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
