Address review findings: drop Groups attribute, add clone logic, specify PATCH payload format, list frontend functions to update, add validation and test cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
141 lines
5.3 KiB
Markdown
141 lines
5.3 KiB
Markdown
# 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)
|