# Piece Quantity Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a quantity field to pieces — on `MachinePieceLink` for machine-direct pieces, and in `Composant.structure.pieces[]` JSON for composant pieces. **Architecture:** Quantity lives on the relationship, not the catalogue entity. For machine-direct pieces, a new `quantity` integer column on `MachinePieceLink` (default 1). For composant pieces, a `quantity` key in the existing `structure.pieces[]` JSON (default 1). Display: "×N" after piece name, hidden when N=1. **Tech Stack:** Symfony 8 / API Platform, Doctrine ORM, PostgreSQL, Nuxt 4, Vue 3 Composition API, TypeScript, DaisyUI 5 **Spec:** `docs/superpowers/specs/2026-03-12-piece-quantity-design.md` --- ## Chunk 1: Backend ### Task 1: Entity + Migration **Files:** - Modify: `src/Entity/MachinePieceLink.php` - Existing: `migrations/Version20260309150000.php` (already written, untracked) - [ ] **Step 1: Add quantity field to MachinePieceLink entity** In `src/Entity/MachinePieceLink.php`, add after the `prixOverride` field (line 69): ```php #[ORM\Column(type: Types::INTEGER, options: ['default' => 1])] #[Assert\GreaterThanOrEqual(1)] private int $quantity = 1; ``` Add the import at the top if not present: ```php use Symfony\Component\Validator\Constraints as Assert; ``` Add getter/setter after existing methods (before closing brace): ```php public function getQuantity(): int { return $this->quantity; } public function setQuantity(int $quantity): static { $this->quantity = $quantity; return $this; } ``` - [ ] **Step 2: Stage the migration file** The migration `migrations/Version20260309150000.php` already exists (untracked). Verify its content matches: ```php public function up(Schema $schema): void { $this->addSql('ALTER TABLE machine_piece_links ADD COLUMN IF NOT EXISTS quantity INTEGER NOT NULL DEFAULT 1'); } ``` - [ ] **Step 3: Run php-cs-fixer** Run: `make php-cs-fixer-allow-risky` - [ ] **Step 4: Run tests to verify nothing is broken** Run: `make test` Expected: All 167 tests pass (OK, with possible deprecation warnings) - [ ] **Step 5: Commit** ```bash git add src/Entity/MachinePieceLink.php migrations/Version20260309150000.php git commit -m "feat(piece) : add quantity field to MachinePieceLink entity + migration" ``` --- ### Task 2: MachineStructureController — Normalization + PATCH + Clone **Files:** - Modify: `src/Controller/MachineStructureController.php` - [ ] **Step 1: Add quantity to `normalizePieceLinks()`** In `src/Controller/MachineStructureController.php`, method `normalizePieceLinks()` (line ~623-641). Add `'quantity'` to the returned array, after `'overrides'`: ```php 'quantity' => $this->resolvePieceQuantity($link), ``` Add a new private method after `normalizePieceLinks()`: ```php private function resolvePieceQuantity(MachinePieceLink $link): int { $parentLink = $link->getParentLink(); if (!$parentLink) { return $link->getQuantity(); } $composant = $parentLink->getComposant(); $structure = $composant->getStructure(); if (!is_array($structure) || !isset($structure['pieces']) || !is_array($structure['pieces'])) { return 1; } $piece = $link->getPiece(); $typePiece = $piece->getTypePiece(); $typePieceId = $typePiece?->getId(); foreach ($structure['pieces'] as $pieceDef) { if (!is_array($pieceDef)) { continue; } if (isset($pieceDef['typePieceId']) && $pieceDef['typePieceId'] === $typePieceId) { return (int) ($pieceDef['quantity'] ?? 1); } } return 1; } ``` **Note:** Matching is done by `typePieceId`. If a composant has two pieces of the same type, they will get the same quantity (first match). This is an acceptable limitation for now — duplicates of the same piece type in a composant are rare. - [ ] **Step 2: Apply quantity in `applyPieceLinks()`** In method `applyPieceLinks()` (line ~366-422), add quantity application after `$this->applyOverrides($link, $entry['overrides'] ?? null);` (line ~396): ```php if (!isset($entry['parentComponentLinkId']) && !isset($entry['parentLinkId'])) { $quantity = isset($entry['quantity']) ? (int) $entry['quantity'] : $link->getQuantity(); $link->setQuantity(max(1, $quantity)); } ``` **Key behavior:** - Only applies to direct machine pieces (no parent component link) - If `quantity` not in payload: preserves existing value - If `quantity` in payload: sets it, with floor of 1 - For composant-child pieces: quantity is ignored (comes from composant structure) - [ ] **Step 3: Copy quantity in `clonePieceLinks()`** In method `clonePieceLinks()` (line ~233-256), add after `$newLink->setPrixOverride($link->getPrixOverride());` (line ~244): ```php $newLink->setQuantity($link->getQuantity()); ``` - [ ] **Step 4: Run php-cs-fixer** Run: `make php-cs-fixer-allow-risky` - [ ] **Step 5: Run tests** Run: `make test` Expected: All tests pass - [ ] **Step 6: Commit** ```bash git add src/Controller/MachineStructureController.php git commit -m "feat(piece) : add quantity to structure normalization, PATCH and clone" ``` --- ### Task 3: Backend Tests **Files:** - Modify: `tests/Api/Entity/MachinePieceLinkTest.php` - Modify: `tests/AbstractApiTestCase.php` (factory method) - [ ] **Step 1: Update factory method to support quantity** In `tests/AbstractApiTestCase.php`, update `createMachinePieceLink()` to accept an optional quantity parameter: ```php protected function createMachinePieceLink(Machine $machine, Piece $piece, ?MachineComponentLink $parentLink = null, int $quantity = 1): MachinePieceLink { $link = new MachinePieceLink(); $link->setMachine($machine); $link->setPiece($piece); $link->setQuantity($quantity); if (null !== $parentLink) { $link->setParentLink($parentLink); } $em = $this->getEntityManager(); $em->persist($link); $em->flush(); return $link; } ``` - [ ] **Step 2: Add test for POST with explicit quantity** In `tests/Api/Entity/MachinePieceLinkTest.php`, add. Follow the existing test pattern — use `$this->assertJsonContains()` and include the `headers` key with `Content-Type`: ```php public function testPostWithQuantity(): void { $client = $this->createGestionnaireClient(); $machine = $this->createMachine(); $piece = $this->createPiece(); $client->request('POST', '/api/machine_piece_links', [ 'headers' => ['Content-Type' => 'application/ld+json'], 'json' => [ 'machine' => '/api/machines/' . $machine->getId(), 'piece' => '/api/pieces/' . $piece->getId(), 'quantity' => 5, ], ]); $this->assertResponseStatusCodeSame(201); $this->assertJsonContains(['quantity' => 5]); } ``` - [ ] **Step 3: Add test for POST without quantity (default = 1)** ```php public function testPostDefaultQuantity(): void { $client = $this->createGestionnaireClient(); $machine = $this->createMachine(); $piece = $this->createPiece(); $client->request('POST', '/api/machine_piece_links', [ 'headers' => ['Content-Type' => 'application/ld+json'], 'json' => [ 'machine' => '/api/machines/' . $machine->getId(), 'piece' => '/api/pieces/' . $piece->getId(), ], ]); $this->assertResponseStatusCodeSame(201); $this->assertJsonContains(['quantity' => 1]); } ``` - [ ] **Step 4: Run tests** Run: `make test` Expected: All tests pass including new ones - [ ] **Step 5: Run php-cs-fixer** Run: `make php-cs-fixer-allow-risky` - [ ] **Step 6: Commit** ```bash git add tests/Api/Entity/MachinePieceLinkTest.php tests/AbstractApiTestCase.php git commit -m "test(piece) : add quantity tests for MachinePieceLink" ``` --- ## Chunk 2: Frontend ### Task 4: TypeScript Types + Sanitization + Hydration Functions **Files:** - Modify: `frontend/app/shared/types/inventory.ts` - Modify: `frontend/app/shared/model/componentStructure.ts` - Modify: `frontend/app/shared/model/componentStructureSanitize.ts` - Modify: `frontend/app/shared/model/componentStructureHydrate.ts` - Modify: `frontend/app/shared/utils/structureAssignmentHelpers.ts` - [ ] **Step 1: Add `quantity` to `ComponentModelPiece` type** In `frontend/app/shared/types/inventory.ts`, add `quantity` to the `ComponentModelPiece` interface (after `role`, line ~23): ```typescript quantity?: number ``` - [ ] **Step 2: Add `quantity` to `validatePiece()` in same file** In `frontend/app/shared/types/inventory.ts`, in `validatePiece()` (line ~144-172): After `const role = ensureString(value.role)` (line ~161), add: ```typescript const quantity = typeof value.quantity === 'number' && value.quantity >= 1 ? value.quantity : undefined ``` And in the return object, add after the `role` spread: ```typescript ...(quantity ? { quantity } : {}), ``` - [ ] **Step 3: Update `sanitizePieces()` to preserve quantity** In `frontend/app/shared/model/componentStructureSanitize.ts`, in `sanitizePieces()` (~line 130-188). After the existing field extractions, add: ```typescript const quantity = typeof piece?.quantity === 'number' && piece.quantity >= 1 ? piece.quantity : undefined ``` In the result object construction, add alongside existing fields (follow the `if (field) { result.field = field }` pattern used in this function): ```typescript if (quantity !== undefined) { result.quantity = quantity } ``` - [ ] **Step 4: Update `normalizeStructureForSave()` to include quantity** In `frontend/app/shared/model/componentStructure.ts`, in `normalizeStructureForSave()` (~lines 164-179), add in the piece payload mapping after the `reference` check: ```typescript if ((piece as any).quantity !== undefined && (piece as any).quantity >= 1) { payload.quantity = (piece as any).quantity } ``` **Note:** Always send quantity when defined (including 1), so the backend always has an explicit value. - [ ] **Step 5: Update `hydratePieces()` and `mapComponentPieces()` to preserve quantity** In `frontend/app/shared/model/componentStructureHydrate.ts`: In `hydratePieces()` (line ~95-107), add to the mapped object: ```typescript ...(piece?.quantity !== undefined && piece.quantity >= 1 ? { quantity: piece.quantity } : {}), ``` In `mapComponentPieces()` (line ~168-179), add to the mapped object: ```typescript ...(piece?.quantity !== undefined && piece.quantity >= 1 ? { quantity: piece.quantity } : {}), ``` - [ ] **Step 6: Update `sanitizePieceDefinition()` to preserve quantity** In `frontend/app/shared/utils/structureAssignmentHelpers.ts`, in `sanitizePieceDefinition()` (~lines 172-180), add to the `stripNullish()` object: ```typescript quantity: typeof (definition as any).quantity === 'number' ? (definition as any).quantity : null, ``` - [ ] **Step 7: Run lint + typecheck** ```bash cd frontend && npm run lint:fix && npx nuxi typecheck ``` Expected: 0 errors - [ ] **Step 8: Commit** ```bash cd frontend git add app/shared/types/inventory.ts app/shared/model/componentStructure.ts app/shared/model/componentStructureSanitize.ts app/shared/model/componentStructureHydrate.ts app/shared/utils/structureAssignmentHelpers.ts git commit -m "feat(piece) : add quantity field to piece types, sanitization and hydration" ``` --- ### Task 5: Composant Structure Editor — Quantity Input **Files:** - Modify: `frontend/app/components/StructureNodeEditor.vue` (piece section, lines ~229-299) - Modify: `frontend/app/composables/useStructureNodeCrud.ts` (`addPiece()`, lines ~110-118) **Context:** `StructureNodeEditor.vue` renders the composant structure editor. The piece section (lines ~236-293) currently shows only a `select` for `typePieceId` and a delete button. The `addPiece()` function in `useStructureNodeCrud.ts` creates new piece entries with default fields. - [ ] **Step 1: Add default quantity to `addPiece()`** In `frontend/app/composables/useStructureNodeCrud.ts`, in `addPiece()` (line ~110-118), add `quantity: 1` to the pushed object: ```typescript const addPiece = () => { ensureArray('pieces') props.node.pieces!.push({ typePieceId: '', typePieceLabel: '', reference: '', familyCode: '', role: '', quantity: 1, }) } ``` - [ ] **Step 2: Add quantity input in `StructureNodeEditor.vue`** In `frontend/app/components/StructureNodeEditor.vue`, in the piece item rendering section (inside the `v-for` loop for pieces, line ~256-292), add a quantity input next to the existing piece type `select`. Place it after the select and before the delete button: ```vue ``` - [ ] **Step 3: Run lint + typecheck** ```bash cd frontend && npm run lint:fix && npx nuxi typecheck ``` Expected: 0 errors - [ ] **Step 4: Commit** ```bash cd frontend git add app/components/StructureNodeEditor.vue app/composables/useStructureNodeCrud.ts git commit -m "feat(piece) : add quantity input to composant structure editor" ``` --- ### Task 6: Machine Detail Page — Display Quantity **Files:** - Modify: `frontend/app/components/PieceItem.vue` **Context:** `PieceItem.vue` renders each piece in the machine structure view. The piece name is displayed at line ~26 in an `