Files
Inventory/docs/superpowers/plans/2026-03-12-piece-quantity.md
Matthieu be859e57db refactor : rename Inventory_frontend to frontend in docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:20:19 +02:00

547 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<input
v-model.number="piece.quantity"
type="number"
:min="1"
step="1"
placeholder="Qté"
class="input input-bordered input-sm md:input-md w-20"
@input="piece.quantity = Math.max(1, piece.quantity || 1)"
/>
```
- [ ] **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 `<h3>` tag. Quantity should appear as "×N" after the name, in secondary text. For direct pieces (no parent component), it should be editable. For composant pieces, read-only.
- [ ] **Step 1: Add quantity display to PieceItem**
In `frontend/app/components/PieceItem.vue`, after the piece name in the `<h3>` tag (line ~26), add the quantity display:
```vue
<span
v-if="displayQuantity > 1"
class="text-sm font-normal text-base-content/60 ml-1"
>
×{{ displayQuantity }}
</span>
```
Add to the component's setup:
```typescript
const displayQuantity = computed(() => {
return props.piece.quantity ?? 1
})
```
- [ ] **Step 2: Add editable quantity for direct machine pieces**
For pieces directly on a machine (no `parentComponentLinkId`), add an editable quantity input in the piece's edit section, following the pattern of existing override fields (nameOverride, referenceOverride, prixOverride). Place it alongside the overrides form:
```vue
<div v-if="!piece.parentComponentLinkId && isEditMode" class="form-control">
<label class="label">
<span class="label-text text-sm">Quantité</span>
</label>
<input
v-model.number="pieceData.quantity"
type="number"
min="1"
step="1"
class="input input-bordered input-sm md:input-md w-24"
/>
</div>
```
Add `quantity` to the `pieceData` reactive object (line ~270-275):
```typescript
quantity: props.piece.quantity ?? 1,
```
Ensure this value is included in the data emitted when saving (follow the same pattern as `nameOverride`, `referenceOverride`, `prixOverride` in the save/emit logic).
- [ ] **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/PieceItem.vue
git commit -m "feat(piece) : display and edit quantity on machine piece items"
```
---
### Task 7: Submodule Update + Final Verification
**Files:**
- Update submodule pointer in main repo
- [ ] **Step 1: Push frontend commits**
```bash
cd frontend && git push
```
- [ ] **Step 2: Update submodule pointer in main repo**
```bash
cd /home/matthieu/dev_malio/Inventory
git add frontend
git commit -m "chore(frontend) : update submodule — piece quantity feature"
```
- [ ] **Step 3: Run all backend tests**
Run: `make test`
Expected: All tests pass
- [ ] **Step 4: Run migration on dev database**
```bash
docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate --no-interaction
```
- [ ] **Step 5: Manual smoke test**
1. Open a composant edit page → verify quantity input appears on each piece in structure
2. Set quantity to 4, save → reload → verify quantity persisted
3. Open a machine with that composant → verify "×4" appears next to piece name (read-only)
4. Add a piece directly to a machine → verify quantity input appears in edit mode
5. Set quantity to 3, save → verify "×3" appears
6. Clone the machine → verify cloned pieces have same quantities