# Piece Quantity — Design Spec

## Context

L'application gère des machines composées de composants et de pièces. Une même pièce (catalogue) peut apparaître dans plusieurs contextes avec des quantités différentes. La quantité doit être portée par la **relation**, pas par l'entité catalogue.

## Scope

- Quantité sur les **pièces directement liées à une machine** (`MachinePieceLink`)
- Quantité sur les **pièces d'un composant** (JSON `structure.pieces` du `Composant`)
- **Hors scope** : quantité sur `MachineComponentLink`, `MachineProductLink`, override de quantité composant au niveau machine, audit logging

## Règles métier

| Contexte | Stockage | Éditable depuis | Visible sur machine |
|----------|----------|-----------------|---------------------|
| Pièce directement sur machine | `MachinePieceLink.quantity` | Page machine | Oui, éditable |
| Pièce d'un composant | `Composant.structure.pieces[].quantity` | Page composant (création + édition) | Oui, lecture seule |

- Type : entier, valeur par défaut = 1, minimum = 1
- Affichage : "×N" après le nom de la pièce, masqué si N = 1
- Quantité = 0 n'est pas valide (utiliser la suppression du lien à la place)

## Backend

### 1. MachinePieceLink — Nouvelle colonne

Ajout d'un champ `quantity` sur l'entité `MachinePieceLink` :

```php
#[ORM\Column(type: Types::INTEGER, options: ['default' => 1])]
private int $quantity = 1;
```

Getter/setter standard. **Pas de `#[Groups]`** — cohérent avec les autres champs de l'entité qui n'en déclarent pas (l'entité n'a pas de `normalizationContext`).

Validation : `#[Assert\GreaterThanOrEqual(1)]`

### 2. Migration SQL

```sql
ALTER TABLE machine_piece_link ADD COLUMN IF NOT EXISTS quantity INTEGER NOT NULL DEFAULT 1;
```

Idempotente avec `IF NOT EXISTS`.

### 3. Composant.structure JSON

Le tableau `pieces` dans le JSON `structure` du Composant accepte une nouvelle clé `quantity`. Pas de migration DB nécessaire — c'est un champ JSON libre.

Avant :
```json
{ "typePieceId": "...", "role": "Filtration" }
```

Après :
```json
{ "typePieceId": "...", "role": "Filtration", "quantity": 4 }
```

Les entrées existantes sans `quantity` sont traitées comme `quantity = 1` (défaut côté frontend et backend).

### 4. MachineStructureController

#### Normalisation (GET)

`normalizePieceLinks()` : inclure `quantity` dans la réponse JSON :
- **Pièce machine directe** (parentLink = null) : `quantity` depuis `MachinePieceLink.quantity`
- **Pièce sous composant** : `quantity` depuis le `structure.pieces` du composant source. Résolution :
  1. Naviguer `MachinePieceLink` → `parentLink` (MachineComponentLink) → `composant` → `structure['pieces']`
  2. Matcher par index de position dans le tableau `pieces` (l'ordre des pièces dans la structure correspond à l'ordre de création des liens)
  3. Fallback : `quantity = 1` si non trouvé

#### PATCH structure

Dans `applyPieceLinks()`, accepter `quantity` au même niveau que `pieceId` dans le payload :

```json
{
  "pieceLinks": [
    { "pieceId": "cl...", "quantity": 4, "overrides": { "nameOverride": "..." } }
  ]
}
```

- `quantity` est appliqué uniquement pour les pièces directement sur la machine (pas de `parentComponentLinkId`)
- Si `parentComponentLinkId` est présent, `quantity` est **ignoré silencieusement** (la valeur vient du composant)

#### Clone

`clonePieceLinks()` doit copier `quantity` depuis le lien source :

```php
$newLink->setQuantity($link->getQuantity());
```

Sans cela, les machines clonées perdraient les quantités (reset à 1).

### 5. Tests

Ajouter dans `MachinePieceLinkTest.php` :
- POST avec `quantity` explicite → vérifier la valeur
- POST sans `quantity` → vérifier défaut = 1
- PATCH `quantity` sur pièce directe → vérifier mise à jour
- GET structure → vérifier `quantity` dans la réponse normalisée
- Clone → vérifier que `quantity` est préservé

## Frontend

### 1. Types TypeScript

Mise à jour de `ComponentModelPiece` dans `shared/types/inventory.ts` — ajout du champ `quantity` :

```typescript
quantity?: number  // défaut 1
```

### 2. Fonctions de sanitization/normalisation à mettre à jour

Ces fonctions énumèrent explicitement les champs à conserver et doivent inclure `quantity` :

- `normalizeStructureForSave()` dans `shared/model/componentStructure.ts` — inclure `quantity` dans le payload backend des pièces
- `sanitizePieceDefinition()` dans `shared/utils/structureAssignmentHelpers.ts` — préserver `quantity`
- `sanitizePieces()` dans `shared/model/componentStructureSanitize.ts` — préserver `quantity` dans la sortie
- `hydratePieces()` / `mapComponentPieces()` — préserver `quantity` lors de l'hydratation

### 3. Pages composant (création + édition)

Dans l'éditeur de structure, chaque pièce du tableau `structure.pieces` affiche un champ input :
- Type : `number`, min = 1, step = 1
- Valeur par défaut : 1
- Style : `input input-bordered input-sm md:input-md` (DaisyUI)
- Position : à côté des champs existants (reference, role)

### 4. Page machine (détail/structure)

- **Pièce directe** (parentLink = null) : affiche "×N" à côté du nom, quantité éditable (input entier)
- **Pièce de composant** : affiche "×N" à côté du nom, lecture seule (pas d'input)
- Si quantité = 1 : rien n'est affiché (pas de bruit visuel)
- Style du label : texte secondaire (`text-base-content/60` ou classe équivalente)
