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

16 KiB
Raw Blame History

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):

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

Add the import at the top if not present:

use Symfony\Component\Validator\Constraints as Assert;

Add getter/setter after existing methods (before closing brace):

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:

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
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':

'quantity' => $this->resolvePieceQuantity($link),

Add a new private method after normalizePieceLinks():

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):

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):

$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
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:

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:

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)
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
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):

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:

const quantity = typeof value.quantity === 'number' && value.quantity >= 1 ? value.quantity : undefined

And in the return object, add after the role spread:

...(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:

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):

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:

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:

...(piece?.quantity !== undefined && piece.quantity >= 1 ? { quantity: piece.quantity } : {}),

In mapComponentPieces() (line ~168-179), add to the mapped object:

...(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:

quantity: typeof (definition as any).quantity === 'number' ? (definition as any).quantity : null,
  • Step 7: Run lint + typecheck
cd frontend && npm run lint:fix && npx nuxi typecheck

Expected: 0 errors

  • Step 8: Commit
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:

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:

<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
cd frontend && npm run lint:fix && npx nuxi typecheck

Expected: 0 errors

  • Step 4: Commit
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:

<span
  v-if="displayQuantity > 1"
  class="text-sm font-normal text-base-content/60 ml-1"
>
  ×{{ displayQuantity }}
</span>

Add to the component's setup:

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:

<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):

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
cd frontend && npm run lint:fix && npx nuxi typecheck

Expected: 0 errors

  • Step 4: Commit
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

cd frontend && git push
  • Step 2: Update submodule pointer in main repo
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
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