Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65fbd38b55 | ||
|
|
37aa755819 | ||
|
|
98caaa148d | ||
|
|
523eed927e | ||
|
|
43bec07bb8 | ||
|
|
0181f18778 | ||
|
|
8e0acf4896 | ||
|
|
aa8e043c83 | ||
|
|
b2aff0e414 | ||
|
|
4072abf7ba | ||
|
|
089ca43404 | ||
|
|
f09c7e4782 | ||
|
|
6a20dcce54 | ||
|
|
6e0be3dbf3 | ||
|
|
f66db3f2f0 | ||
|
|
0864af1439 | ||
|
|
5210e53d73 | ||
|
|
3f07162b94 | ||
|
|
57615b3e9d | ||
|
|
46694d11d9 | ||
|
|
44cfa25eca | ||
|
|
7ea4cc8c12 | ||
|
|
bb300a7ca7 | ||
|
|
556da6e451 | ||
|
|
8871440c9a | ||
|
|
6f1756e82e | ||
|
|
55bed90ac7 | ||
|
|
a6139d7090 | ||
|
|
8ed5f90b63 | ||
|
|
5194543d16 | ||
|
|
c01b71fe06 | ||
|
|
5336dfc09d | ||
|
|
77c5d25cea | ||
|
|
e2326064ba | ||
|
|
100e24725c | ||
|
|
515bae189e | ||
|
|
333f2a88af | ||
|
|
eccbc1bd56 | ||
|
|
2a0809a065 | ||
|
|
f2061abce8 | ||
|
|
42c7072bcd | ||
|
|
1f90f809ac | ||
|
|
a940f53f8a | ||
|
|
c74bdedf9b | ||
|
|
233ee3faf3 | ||
|
|
b8edf1ea95 | ||
|
|
7a7af58074 | ||
|
|
03e6c2432b |
38
CLAUDE.md
38
CLAUDE.md
@@ -64,6 +64,14 @@ npm run build # Build production
|
||||
npm run lint:fix # ESLint fix
|
||||
npx nuxi typecheck # TypeScript check (0 errors attendu)
|
||||
|
||||
# Database / Fixtures
|
||||
make db-reset # Reset database (drop + recreate schema)
|
||||
make fixtures-dump # Dump la DB vers fixtures/data.sql
|
||||
make fixtures-load # Charger les fixtures SQL (désactive FK)
|
||||
make fixtures-reset # Reset DB + recharger fixtures
|
||||
make import-data # Importer les dumps SQL normalisés
|
||||
make cache-clear # Clear cache Symfony
|
||||
|
||||
# Release
|
||||
./scripts/release.sh patch # Bump patch version (ou minor/major)
|
||||
```
|
||||
@@ -101,6 +109,11 @@ Le frontend est un submodule git. Lors d'un commit frontend :
|
||||
### Entités Principales
|
||||
`Machine`, `Piece`, `Composant`, `Product`, `Constructeur`, `Site`, `ModelType`, `CustomField`, `CustomFieldValue`, `Document`, `AuditLog`, `Comment`, `Profile`, `MachineComponentLink`, `MachinePieceLink`, `MachineProductLink`
|
||||
|
||||
#### Entités de normalisation (slots & skeleton requirements)
|
||||
Remplacent les anciennes colonnes JSON `structure` et `productIds` par des tables relationnelles :
|
||||
- **Slots** (données réelles d'un composant) : `ComposantPieceSlot`, `ComposantSubcomponentSlot`, `ComposantProductSlot`
|
||||
- **Skeleton Requirements** (définitions du ModelType) : `SkeletonPieceRequirement`, `SkeletonProductRequirement`, `SkeletonSubcomponentRequirement`
|
||||
|
||||
### Patterns
|
||||
- **IDs** : CUID-like strings (`'cl' + bin2hex(random_bytes(12))`), pas d'auto-increment
|
||||
- **ORM** : Attributs PHP 8 (`#[ORM\Column(...)]`, `#[Groups([...])]`)
|
||||
@@ -110,15 +123,32 @@ Le frontend est un submodule git. Lors d'un commit frontend :
|
||||
- **Migrations** : Raw SQL PostgreSQL avec `IF NOT EXISTS`/`IF EXISTS` pour idempotence
|
||||
|
||||
### Custom Controllers (pas API Platform)
|
||||
- `MachineStructureController` — `/api/machines/{id}/structure` (GET/PATCH) : hiérarchie complète machine avec normalisation JSON manuelle (pas Symfony Serializer). Source principale de données pour la page détail machine.
|
||||
- `MachineStructureController` — `/api/machines/{id}/structure` (GET/PATCH), `/api/machines/{id}/clone` (POST) : hiérarchie complète machine avec normalisation JSON manuelle. Source principale de données pour la page détail machine.
|
||||
- `MachineCustomFieldsController` — `/api/machines/{id}/add-custom-fields` (POST) : initialise les CustomFieldValue manquants pour une machine.
|
||||
- `CustomFieldValueController` — `/api/custom-fields/values/*` : CRUD + upsert pour les valeurs de champs perso.
|
||||
- `ComposantPieceSlotController` — `/api/composant-piece-slots/{id}` (PATCH) : mise à jour des slots pièce d'un composant.
|
||||
- `SessionProfileController` — `/api/session/profile` (GET/POST/DELETE) : auth session (login/logout/current user).
|
||||
- `SessionProfilesController` — `/api/session/profiles` (GET) : liste des profils disponibles pour la session.
|
||||
- `AdminProfileController` — `/api/admin/profiles` : CRUD profils, gestion rôles et mots de passe (ROLE_ADMIN).
|
||||
- `CommentController` — `/api/comments` : création, résolution, compteur non-résolus.
|
||||
- `ActivityLogController` — `/api/activity-logs` (GET) : journal d'activité global.
|
||||
- `EntityHistoryController` — `/api/{entity}/{id}/history` (GET) : historique audit par entité (machines, pièces, composants, produits).
|
||||
- `DocumentQueryController` — `/api/documents/{entity}/{id}` (GET) : documents par site/machine/composant/pièce/produit.
|
||||
- `DocumentServeController` — `/api/documents/{id}/file|download` (GET) : servir/télécharger fichiers.
|
||||
- `ModelTypeConversionController` — `/api/model_types/{id}/conversion-check|convert` : vérification et conversion de ModelType.
|
||||
- `HealthCheckController` — `/api/health` (GET) : health check.
|
||||
|
||||
### Custom Fields — Architecture
|
||||
- **Composants/Pièces/Produits** : définitions dans le JSON `structure` du ModelType
|
||||
- **Composants/Pièces/Produits** : définitions dans les entités `SkeletonPieceRequirement`, `SkeletonProductRequirement`, `SkeletonSubcomponentRequirement` du ModelType (anciennement JSON `structure`, normalisé en tables relationnelles). Les custom fields de ces entités sont définis dans `customFields` JSON sur chaque Skeleton*Requirement.
|
||||
- **Machines** : définitions = entités `CustomField` liées directement via `machineId` FK (pas de ModelType)
|
||||
- Les deux partagent la même entité `CustomFieldValue` pour stocker les valeurs
|
||||
|
||||
### Normalisation JSON → Tables (architecture slots)
|
||||
Les anciennes colonnes JSON `structure` et `productIds` des Composants ont été remplacées par des tables relationnelles :
|
||||
- **ModelType** définit le squelette via `SkeletonPieceRequirement`, `SkeletonProductRequirement`, `SkeletonSubcomponentRequirement`
|
||||
- **Composant** stocke les données réelles via `ComposantPieceSlot`, `ComposantProductSlot`, `ComposantSubcomponentSlot`
|
||||
- Chaque slot référence son skeleton requirement (`skeletonRequirement` FK) + l'entité sélectionnée + position
|
||||
|
||||
### Rôles (hiérarchie)
|
||||
```
|
||||
ROLE_ADMIN → ROLE_GESTIONNAIRE → ROLE_VIEWER → ROLE_USER
|
||||
@@ -193,8 +223,8 @@ make test-setup # Créer/mettre à jour le schéma test
|
||||
### Pattern de test
|
||||
- Hériter de `AbstractApiTestCase` (helpers auth + factories)
|
||||
- Ne PAS faire de TRUNCATE/cleanup dans tearDown — DAMA s'en occupe par rollback
|
||||
- Factories : `createProfile()`, `createMachine()`, `createSite()`, `createComposant()`, `createPiece()`, `createProduct()`, `createCustomField()`, `createCustomFieldValue()`, `createModelType()`, `createMachineComponentLink()`, `createMachinePieceLink()`, `createMachineProductLink()`
|
||||
- Auth : `createViewerClient()`, `createGestionnaireClient()`, `createAdminClient()`
|
||||
- Factories : `createProfile()`, `createMachine()`, `createSite()`, `createComposant()`, `createPiece()`, `createProduct()`, `createConstructeur()`, `createCustomField()`, `createCustomFieldValue()`, `createModelType()`, `createMachineComponentLink()`, `createMachinePieceLink()`, `createMachineProductLink()`, `createComposantPieceSlot()`, `createComposantSubcomponentSlot()`, `createComposantProductSlot()`
|
||||
- Auth : `createViewerClient()`, `createGestionnaireClient()`, `createAdminClient()`, `createUnauthenticatedClient()`
|
||||
|
||||
## URLs Locales
|
||||
- API Symfony : `http://localhost:8081/api`
|
||||
|
||||
Submodule Inventory_frontend updated: b0124c11ba...d4fc0f1fee
@@ -14,6 +14,7 @@
|
||||
"doctrine/orm": "^3.6",
|
||||
"lexik/jwt-authentication-bundle": "^3.2",
|
||||
"nelmio/cors-bundle": "^2.6",
|
||||
"nyholm/psr7": "^1.8",
|
||||
"phpdocumentor/reflection-docblock": "^5.6",
|
||||
"phpstan/phpdoc-parser": "^2.3",
|
||||
"symfony/asset": "8.0.*",
|
||||
@@ -22,8 +23,10 @@
|
||||
"symfony/expression-language": "8.0.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/framework-bundle": "8.0.*",
|
||||
"symfony/mcp-bundle": "^0.6.0",
|
||||
"symfony/property-access": "8.0.*",
|
||||
"symfony/property-info": "8.0.*",
|
||||
"symfony/rate-limiter": "8.0.*",
|
||||
"symfony/runtime": "8.0.*",
|
||||
"symfony/security-bundle": "8.0.*",
|
||||
"symfony/serializer": "8.0.*",
|
||||
|
||||
1033
composer.lock
generated
1033
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
api_platform:
|
||||
title: Inventory API
|
||||
description: API de gestion d'inventaire industriel — machines, pièces, composants, produits.
|
||||
version: 1.8.1
|
||||
version: 1.9.1
|
||||
defaults:
|
||||
stateless: false
|
||||
cache_headers:
|
||||
|
||||
10
config/packages/http_discovery.yaml
Normal file
10
config/packages/http_discovery.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
Psr\Http\Message\RequestFactoryInterface: '@http_discovery.psr17_factory'
|
||||
Psr\Http\Message\ResponseFactoryInterface: '@http_discovery.psr17_factory'
|
||||
Psr\Http\Message\ServerRequestFactoryInterface: '@http_discovery.psr17_factory'
|
||||
Psr\Http\Message\StreamFactoryInterface: '@http_discovery.psr17_factory'
|
||||
Psr\Http\Message\UploadedFileFactoryInterface: '@http_discovery.psr17_factory'
|
||||
Psr\Http\Message\UriFactoryInterface: '@http_discovery.psr17_factory'
|
||||
|
||||
http_discovery.psr17_factory:
|
||||
class: Http\Discovery\Psr17Factory
|
||||
20
config/packages/mcp.yaml.disabled
Normal file
20
config/packages/mcp.yaml.disabled
Normal file
@@ -0,0 +1,20 @@
|
||||
mcp:
|
||||
app: 'inventory'
|
||||
version: '1.0.0'
|
||||
description: 'Inventory MCP Server - Gestion inventaire industriel (machines, pièces, composants, produits)'
|
||||
instructions: |
|
||||
Serveur MCP pour gérer un inventaire industriel.
|
||||
Entités principales : Machine, Composant, Pièce, Produit, Site, Constructeur.
|
||||
Utilisez search_inventory pour chercher dans toutes les entités.
|
||||
Utilisez get_model_type pour comprendre la structure attendue avant de créer un composant ou une pièce.
|
||||
Consultez la resource inventory://schema/entities pour voir le schéma complet.
|
||||
Authentification requise : envoyez X-Profile-Id et X-Profile-Password dans les headers HTTP.
|
||||
client_transports:
|
||||
stdio: true
|
||||
http: true
|
||||
http:
|
||||
path: /_mcp
|
||||
session:
|
||||
store: file
|
||||
directory: '%kernel.cache_dir%/mcp-sessions'
|
||||
ttl: 3600
|
||||
6
config/packages/rate_limiter.yaml.disabled
Normal file
6
config/packages/rate_limiter.yaml.disabled
Normal file
@@ -0,0 +1,6 @@
|
||||
framework:
|
||||
rate_limiter:
|
||||
mcp_auth:
|
||||
policy: sliding_window
|
||||
limit: 5
|
||||
interval: '1 minute'
|
||||
@@ -27,6 +27,13 @@ security:
|
||||
pattern: ^/api/session/profiles?$
|
||||
security: false
|
||||
|
||||
# TODO: re-enable when symfony/ai-mcp-bundle is installed
|
||||
# mcp:
|
||||
# pattern: ^/_mcp
|
||||
# stateless: true
|
||||
# custom_authenticators:
|
||||
# - App\Mcp\Security\McpHeaderAuthenticator
|
||||
|
||||
api:
|
||||
pattern: ^/api
|
||||
stateless: false
|
||||
@@ -49,6 +56,7 @@ security:
|
||||
- { path: ^/api/admin, roles: ROLE_ADMIN }
|
||||
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/api/health$, roles: PUBLIC_ACCESS }
|
||||
# - { path: ^/_mcp, roles: ROLE_USER } # TODO: re-enable with MCP
|
||||
- { path: ^/docs, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/contexts, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/\.well-known, roles: PUBLIC_ACCESS }
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// This file is auto-generated and is for apps only. Bundles SHOULD NOT rely on its content.
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
||||
@@ -1385,7 +1387,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* mercure?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* hub_url?: scalar|null|Param, // The URL sent in the Link HTTP header. If not set, will default to the URL for MercureBundle's default hub. // Default: null
|
||||
* include_type?: bool|Param, // Always include @type in updates (including delete ones). // Default: false
|
||||
* include_type?: bool|Param, // Always include @var in updates (including delete ones). // Default: false
|
||||
* },
|
||||
* messenger?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
@@ -1606,6 +1608,12 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* cache?: scalar|null|Param, // Storage to track blocked tokens // Default: "cache.app"
|
||||
* },
|
||||
* }
|
||||
* @psalm-type DamaDoctrineTestConfig = array{
|
||||
* enable_static_connection?: mixed, // Default: true
|
||||
* enable_static_meta_data_cache?: bool|Param, // Default: true
|
||||
* enable_static_query_cache?: bool|Param, // Default: true
|
||||
* connection_keys?: list<mixed>,
|
||||
* }
|
||||
* @psalm-type ConfigType = array{
|
||||
* imports?: ImportsConfig,
|
||||
* parameters?: ParametersConfig,
|
||||
@@ -1656,6 +1664,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* nelmio_cors?: NelmioCorsConfig,
|
||||
* api_platform?: ApiPlatformConfig,
|
||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||
* dama_doctrine_test?: DamaDoctrineTestConfig,
|
||||
* },
|
||||
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
||||
* imports?: ImportsConfig,
|
||||
|
||||
@@ -18,6 +18,8 @@ services:
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/'
|
||||
exclude:
|
||||
- '../src/Mcp/'
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
@@ -34,7 +36,34 @@ services:
|
||||
tags:
|
||||
- { name: doctrine.event_subscriber }
|
||||
|
||||
# TODO: re-enable when symfony/ai-mcp-bundle is installed
|
||||
# App\Mcp\Security\McpHeaderAuthenticator:
|
||||
# arguments:
|
||||
# $mcpAuthLimiter: '@limiter.mcp_auth'
|
||||
|
||||
App\OpenApi\OpenApiDecorator:
|
||||
decorates: 'api_platform.openapi.factory'
|
||||
arguments:
|
||||
$decorated: '@.inner'
|
||||
|
||||
when@test:
|
||||
services:
|
||||
App\Service\Sync\ProductSyncStrategy:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
public: true
|
||||
|
||||
App\Service\Sync\ComposantSyncStrategy:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
public: true
|
||||
|
||||
App\Service\Sync\PieceSyncStrategy:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
public: true
|
||||
|
||||
App\Service\ModelTypeSyncService:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
public: true
|
||||
|
||||
1067
docs/superpowers/plans/2026-03-12-json-to-tables-normalization.md
Normal file
1067
docs/superpowers/plans/2026-03-12-json-to-tables-normalization.md
Normal file
File diff suppressed because it is too large
Load Diff
546
docs/superpowers/plans/2026-03-12-piece-quantity.md
Normal file
546
docs/superpowers/plans/2026-03-12-piece-quantity.md
Normal file
@@ -0,0 +1,546 @@
|
||||
# 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: `Inventory_frontend/app/shared/types/inventory.ts`
|
||||
- Modify: `Inventory_frontend/app/shared/model/componentStructure.ts`
|
||||
- Modify: `Inventory_frontend/app/shared/model/componentStructureSanitize.ts`
|
||||
- Modify: `Inventory_frontend/app/shared/model/componentStructureHydrate.ts`
|
||||
- Modify: `Inventory_frontend/app/shared/utils/structureAssignmentHelpers.ts`
|
||||
|
||||
- [ ] **Step 1: Add `quantity` to `ComponentModelPiece` type**
|
||||
|
||||
In `Inventory_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 `Inventory_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 `Inventory_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 `Inventory_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 `Inventory_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 `Inventory_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 Inventory_frontend && npm run lint:fix && npx nuxi typecheck
|
||||
```
|
||||
Expected: 0 errors
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
cd Inventory_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: `Inventory_frontend/app/components/StructureNodeEditor.vue` (piece section, lines ~229-299)
|
||||
- Modify: `Inventory_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 `Inventory_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 `Inventory_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 Inventory_frontend && npm run lint:fix && npx nuxi typecheck
|
||||
```
|
||||
Expected: 0 errors
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd Inventory_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: `Inventory_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 `Inventory_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 Inventory_frontend && npm run lint:fix && npx nuxi typecheck
|
||||
```
|
||||
Expected: 0 errors
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd Inventory_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 Inventory_frontend && git push
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update submodule pointer in main repo**
|
||||
|
||||
```bash
|
||||
cd /home/matthieu/dev_malio/Inventory
|
||||
git add Inventory_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
|
||||
1582
docs/superpowers/plans/2026-03-13-modeltype-sync.md
Normal file
1582
docs/superpowers/plans/2026-03-13-modeltype-sync.md
Normal file
File diff suppressed because it is too large
Load Diff
140
docs/superpowers/specs/2026-03-12-piece-quantity-design.md
Normal file
140
docs/superpowers/specs/2026-03-12-piece-quantity-design.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# 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)
|
||||
502
docs/superpowers/specs/2026-03-13-modeltype-sync-design.md
Normal file
502
docs/superpowers/specs/2026-03-13-modeltype-sync-design.md
Normal file
@@ -0,0 +1,502 @@
|
||||
# ModelType Sync — Design Spec
|
||||
|
||||
## Objectif
|
||||
|
||||
Quand un ModelType (catégorie) est modifié (structure, custom fields), propager automatiquement les changements à tous les items liés (Composants, Pièces, Produits). L'utilisateur voit un preview de l'impact et confirme avant que la sync ne s'exécute.
|
||||
|
||||
## Décisions
|
||||
|
||||
| Décision | Choix |
|
||||
|----------|-------|
|
||||
| Scope sync | Composants + Pièces + Produits |
|
||||
| Sync destructive | Avec confirmation (modal frontend) |
|
||||
| Custom fields — ajout | Créer `CustomFieldValue` vides |
|
||||
| Custom fields — suppression | Supprimer avec confirmation |
|
||||
| Custom fields — renommage | Propagation auto (label dans la définition) |
|
||||
| Custom fields — changement de type | Clear les valeurs avec confirmation |
|
||||
| Architecture backend | Strategy pattern (1 strategy par entity type) |
|
||||
| Déclenchement | En deux temps : preview séparé du sync |
|
||||
| Preview timing | AVANT le save (pas de rollback nécessaire) |
|
||||
| Pièces — produits liés | Nouvelle table `PieceProductSlot` remplace la M2M `piece_products` |
|
||||
| `restrictedMode` frontend | Supprimé complètement |
|
||||
| Versioning | `version` INT sur Composant, Pièce, Produit (incrémenté à chaque sync) |
|
||||
| Machines | Aucun changement — elles lisent les slots des composants, la sync met à jour ces slots |
|
||||
| Matching slots | Par `typeXxxId` + `position` (pas de FK vers skeleton requirement) |
|
||||
| Matching custom fields | Par `orderIndex` (propriété stable sur `CustomField`) |
|
||||
| Atomicité PATCH + sync | Wrappé dans une transaction DB côté controller |
|
||||
| Idempotence sync | `execute()` est idempotent — un double appel est un no-op |
|
||||
| Audit | Les opérations de sync sont capturées par les subscribers `onFlush` existants |
|
||||
|
||||
## Endpoints API
|
||||
|
||||
### `POST /api/model_types/{id}/sync-preview`
|
||||
|
||||
Calcule l'impact du diff entre le payload envoyé et l'état actuel des items liés. **Ne persiste rien.**
|
||||
|
||||
**Sécurité :** `ROLE_GESTIONNAIRE`
|
||||
|
||||
**Request body :**
|
||||
```json
|
||||
{
|
||||
"structure": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
Le payload `structure` a le même format que celui envoyé au `PATCH /api/model_types/{id}`.
|
||||
|
||||
**Response :**
|
||||
```json
|
||||
{
|
||||
"modelTypeId": "cl...",
|
||||
"category": "COMPONENT",
|
||||
"itemCount": 12,
|
||||
"additions": {
|
||||
"pieceSlots": 12,
|
||||
"productSlots": 0,
|
||||
"subcomponentSlots": 24,
|
||||
"customFieldValues": 36
|
||||
},
|
||||
"deletions": {
|
||||
"pieceSlots": 0,
|
||||
"productSlots": 12,
|
||||
"subcomponentSlots": 0,
|
||||
"customFieldValues": 0
|
||||
},
|
||||
"modifications": {
|
||||
"customFieldTypeChanges": 12
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Si `additions`, `deletions` et `modifications` sont tous à 0, le frontend skip la modal et sauvegarde directement.
|
||||
|
||||
**Erreurs :**
|
||||
- `404` — ModelType introuvable
|
||||
- `403` — droits insuffisants
|
||||
|
||||
### `POST /api/model_types/{id}/sync`
|
||||
|
||||
Exécute la propagation. Appelé **après** le `PATCH` du ModelType, dans la même requête frontend (PATCH + sync enchaînés).
|
||||
|
||||
**Sécurité :** `ROLE_GESTIONNAIRE`
|
||||
|
||||
**Request body :**
|
||||
```json
|
||||
{
|
||||
"confirmDeletions": true,
|
||||
"confirmTypeChanges": true
|
||||
}
|
||||
```
|
||||
|
||||
Si des suppressions sont nécessaires mais `confirmDeletions` est `false`, le sync **skip les suppressions** (applique uniquement les ajouts). Idem pour `confirmTypeChanges` et les clear de valeurs. Cela permet un sync partiel (ajouts only) sans confirmation.
|
||||
|
||||
**Response :** `200` avec résumé de l'exécution.
|
||||
|
||||
**Erreurs :**
|
||||
- `404` — ModelType introuvable
|
||||
- `403` — droits insuffisants
|
||||
|
||||
## Architecture Backend
|
||||
|
||||
### Strategy Pattern
|
||||
|
||||
```
|
||||
Service/
|
||||
├── ModelTypeSyncService.php # Orchestrateur
|
||||
└── Sync/
|
||||
├── SyncStrategyInterface.php # Interface
|
||||
├── ComposantSyncStrategy.php # Slots pièce/produit/sous-composant + custom fields
|
||||
├── PieceSyncStrategy.php # Slots produit + custom fields
|
||||
└── ProductSyncStrategy.php # Custom fields uniquement
|
||||
```
|
||||
|
||||
### Interface
|
||||
|
||||
```php
|
||||
interface SyncStrategyInterface
|
||||
{
|
||||
public function supports(ModelType $modelType): bool;
|
||||
public function preview(ModelType $modelType, array $newStructure): SyncPreviewResult;
|
||||
public function execute(ModelType $modelType, SyncConfirmation $confirmation): SyncExecutionResult;
|
||||
}
|
||||
```
|
||||
|
||||
**Note sur `execute()` :** Cette méthode est appelée **après** le PATCH du ModelType, donc les skeleton requirements sont déjà mis à jour en base. La strategy compare les skeleton requirements actuels (fraîchement mis à jour) avec les slots existants des items liés. Pas besoin de recevoir `$newStructure` — les relations ORM reflètent déjà le nouvel état.
|
||||
|
||||
### Orchestrateur
|
||||
|
||||
```php
|
||||
class ModelTypeSyncService
|
||||
{
|
||||
/** @param iterable<SyncStrategyInterface> $strategies */
|
||||
public function __construct(private iterable $strategies) {}
|
||||
|
||||
public function preview(ModelType $modelType, array $newStructure): SyncPreviewResult
|
||||
{
|
||||
foreach ($this->strategies as $strategy) {
|
||||
if ($strategy->supports($modelType)) {
|
||||
return $strategy->preview($modelType, $newStructure);
|
||||
}
|
||||
}
|
||||
throw new \LogicException('No strategy found for category: ' . $modelType->getCategory()->value);
|
||||
}
|
||||
|
||||
public function execute(ModelType $modelType, SyncConfirmation $confirmation): SyncExecutionResult
|
||||
{
|
||||
foreach ($this->strategies as $strategy) {
|
||||
if ($strategy->supports($modelType)) {
|
||||
return $strategy->execute($modelType, $confirmation);
|
||||
}
|
||||
}
|
||||
throw new \LogicException('No strategy found for category: ' . $modelType->getCategory()->value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Les strategies sont auto-injectées via `#[AutoconfigureTag('app.sync_strategy')]` et le tagged iterator de Symfony.
|
||||
|
||||
### DTOs
|
||||
|
||||
```php
|
||||
class SyncPreviewResult
|
||||
{
|
||||
public string $modelTypeId;
|
||||
public string $category;
|
||||
public int $itemCount;
|
||||
public array $additions; // ['pieceSlots' => int, 'productSlots' => int, ...]
|
||||
public array $deletions;
|
||||
public array $modifications;
|
||||
}
|
||||
|
||||
class SyncConfirmation
|
||||
{
|
||||
public bool $confirmDeletions = false;
|
||||
public bool $confirmTypeChanges = false;
|
||||
}
|
||||
|
||||
class SyncExecutionResult
|
||||
{
|
||||
public int $itemsUpdated;
|
||||
public array $additions;
|
||||
public array $deletions;
|
||||
public array $modifications;
|
||||
}
|
||||
```
|
||||
|
||||
### Controller
|
||||
|
||||
```php
|
||||
#[Route('/api/model_types/{id}')]
|
||||
class ModelTypeSyncController extends AbstractController
|
||||
{
|
||||
#[Route('/sync-preview', methods: ['POST'])]
|
||||
#[IsGranted('ROLE_GESTIONNAIRE')]
|
||||
public function preview(ModelType $modelType, Request $request): JsonResponse
|
||||
{
|
||||
$structure = json_decode($request->getContent(), true)['structure'] ?? [];
|
||||
$result = $this->syncService->preview($modelType, $structure);
|
||||
return $this->json($result);
|
||||
}
|
||||
|
||||
#[Route('/sync', methods: ['POST'])]
|
||||
#[IsGranted('ROLE_GESTIONNAIRE')]
|
||||
public function sync(ModelType $modelType, Request $request): JsonResponse
|
||||
{
|
||||
$body = json_decode($request->getContent(), true);
|
||||
$confirmation = new SyncConfirmation();
|
||||
$confirmation->confirmDeletions = $body['confirmDeletions'] ?? false;
|
||||
$confirmation->confirmTypeChanges = $body['confirmTypeChanges'] ?? false;
|
||||
$result = $this->syncService->execute($modelType, $confirmation);
|
||||
return $this->json($result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Atomicité PATCH + Sync
|
||||
|
||||
Le frontend enchaîne `PATCH` puis `POST /sync` en deux requêtes HTTP. Le `POST /sync` wrappe toute l'opération dans une transaction DB (`$em->wrapInTransaction()`). Si le sync échoue, les modifications de slots sont rollback. Le PATCH du ModelType (skeleton requirements) reste committée — c'est acceptable car un re-sync est toujours possible.
|
||||
|
||||
En cas d'échec réseau entre le PATCH et le sync, le ModelType est à jour mais les items ne sont pas synchronisés. Le prochain save de la catégorie reproposera le sync-preview, qui détectera les différences.
|
||||
|
||||
## Logique de Diff / Sync
|
||||
|
||||
### Matching des slots
|
||||
|
||||
Pour chaque item lié, on compare ses slots actuels avec les skeleton requirements du ModelType. Le matching se fait par **`typeXxxId`** (le type référencé : `typePieceId`, `typeProductId`, `typeComposantId`) + **`position`**.
|
||||
|
||||
Il n'y a **pas de FK directe** entre un slot et un skeleton requirement. Le lien est implicite via le type + position.
|
||||
|
||||
**Pour le preview :** la strategy parse le `$newStructure` (payload JSON) et le compare aux slots actuels sans toucher à la DB.
|
||||
|
||||
**Pour l'execute :** la strategy lit les skeleton requirements actuels (déjà mis à jour par le PATCH) et les compare aux slots actuels.
|
||||
|
||||
### Règles — Slots (pièce, produit, sous-composant)
|
||||
|
||||
| Cas | Action |
|
||||
|-----|--------|
|
||||
| Skeleton requirement existe, pas de slot correspondant | **Ajouter** slot vide (type + position, `quantity = 1` pour pièces, pas de sélection) |
|
||||
| Slot existe, plus de skeleton requirement | **Supprimer** le slot (si `confirmDeletions`) — sélection perdue |
|
||||
| Les deux existent, position différente | **Mettre à jour** la position du slot |
|
||||
| Slot existe et matche | **Ne rien toucher** — sélection et quantité préservées |
|
||||
|
||||
### Règles — Custom fields
|
||||
|
||||
| Cas | Action |
|
||||
|-----|--------|
|
||||
| Nouveau custom field | **Créer** `CustomFieldValue` vides pour tous les items |
|
||||
| Custom field supprimé | **Supprimer** les `CustomFieldValue` (si `confirmDeletions`) |
|
||||
| Renommé (même `orderIndex`, nom différent) | **Propagation auto** — label dans la définition, valeurs intactes |
|
||||
| Type changé (même `orderIndex`, type différent) | **Clear** les valeurs (si `confirmTypeChanges`) — `CustomFieldValue` conservée, `value` vidée |
|
||||
|
||||
Le matching des custom fields se fait par **`orderIndex`** (propriété stable sur l'entité `CustomField`), pas par index de tableau. Cela évite les faux positifs lors de réordonnancement.
|
||||
|
||||
## Nouvelle Entité — `PieceProductSlot`
|
||||
|
||||
### Contexte — Remplacement de la M2M `piece_products`
|
||||
|
||||
Actuellement, les produits liés aux pièces passent par une relation M2M (`piece_products`, colonnes `a`/`b`). Cette table n'a pas de notion de `position`, `typeProductId`, ou `familyCode`.
|
||||
|
||||
`PieceProductSlot` **remplace** cette M2M pour uniformiser l'architecture avec les slots des Composants. La M2M existante sera conservée temporairement puis supprimée dans une migration future.
|
||||
|
||||
### Table `piece_product_slots`
|
||||
|
||||
| Colonne | Type | Contrainte |
|
||||
|---------|------|------------|
|
||||
| `id` | VARCHAR (CUID) | PK |
|
||||
| `pieceid` | VARCHAR | FK → `pieces.id` CASCADE |
|
||||
| `typeproductid` | VARCHAR | FK → `model_types.id` SET NULL, nullable |
|
||||
| `selectedproductid` | VARCHAR | FK → `products.id` SET NULL, nullable |
|
||||
| `familycode` | VARCHAR(255) | nullable |
|
||||
| `position` | INT | NOT NULL |
|
||||
| `createdat` | TIMESTAMP | NOT NULL |
|
||||
| `updatedat` | TIMESTAMP | NOT NULL |
|
||||
|
||||
### Entité PHP
|
||||
|
||||
```php
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'piece_product_slots')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
class PieceProductSlot
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'string')]
|
||||
private string $id;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'productSlots')]
|
||||
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Piece $piece;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class)]
|
||||
#[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?ModelType $typeProduct = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Product::class)]
|
||||
#[ORM\JoinColumn(name: 'selectedProductId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?Product $selectedProduct = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
||||
private ?string $familyCode = null;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $position = 0;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
}
|
||||
```
|
||||
|
||||
### Relation sur Piece
|
||||
|
||||
```php
|
||||
#[ORM\OneToMany(targetEntity: PieceProductSlot::class, mappedBy: 'piece', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['position' => 'ASC'])]
|
||||
private Collection $productSlots;
|
||||
```
|
||||
|
||||
La relation M2M `$products` existante sur `Piece` sera marquée deprecated puis supprimée dans une migration future.
|
||||
|
||||
### Migration
|
||||
|
||||
1. Créer la table `piece_product_slots`
|
||||
2. Migrer les données existantes de `piece_products` (M2M) → chaque entrée devient un `PieceProductSlot` avec `selectedProductId` renseigné, `typeProductId` déduit du produit sélectionné (`product.typeProduct`), `position` auto-incrémentée
|
||||
3. Conserver `piece_products` temporairement (suppression dans une migration future)
|
||||
4. Mettre à jour le virtual getter `getStructure()` de Piece pour lire les `productSlots`
|
||||
|
||||
## Versioning
|
||||
|
||||
### Nouveau champ sur Composant, Piece, Product
|
||||
|
||||
```php
|
||||
#[ORM\Column(type: 'integer', options: ['default' => 1])]
|
||||
#[Groups(['composant:read'])] // idem pour piece:read, product:read
|
||||
private int $version = 1;
|
||||
```
|
||||
|
||||
### Comportement
|
||||
|
||||
- **Création** d'un item → `version = 1`
|
||||
- **Sync** qui modifie les slots ou custom fields d'un item → `version += 1`
|
||||
- Si la sync n'a aucun impact sur un item particulier (ses slots matchent déjà le skeleton), sa version ne change pas
|
||||
|
||||
### Migration
|
||||
|
||||
```sql
|
||||
ALTER TABLE composants ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 1;
|
||||
ALTER TABLE pieces ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 1;
|
||||
ALTER TABLE products ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 1;
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
### Suppression du `restrictedMode`
|
||||
|
||||
**Fichiers à supprimer :**
|
||||
- `composables/useCategoryEditGuard.ts`
|
||||
- `tests/composables/useCategoryEditGuard.test.ts`
|
||||
|
||||
**Fichiers à modifier (retirer restrictedMode) :**
|
||||
- `pages/component-category/[id]/edit.vue`
|
||||
- `pages/piece-category/[id]/edit.vue`
|
||||
- `pages/product-category/[id]/edit.vue`
|
||||
- `components/StructureNodeEditor.vue`
|
||||
- `components/PieceModelStructureEditor.vue`
|
||||
- `components/ComponentModelStructureEditor.vue`
|
||||
- `components/model-types/ModelTypeForm.vue`
|
||||
- `composables/useStructureNodeCrud.ts`
|
||||
- `composables/useStructureNodeLogic.ts`
|
||||
- `composables/usePieceStructureEditorLogic.ts`
|
||||
- `tests/components/ModelTypeForm.test.ts` (si existant)
|
||||
- `tests/components/PieceModelStructureEditor.test.ts`
|
||||
|
||||
### Nouveau composant — `SyncConfirmationModal.vue`
|
||||
|
||||
Modal DaisyUI qui reçoit un `SyncPreviewResult` et affiche :
|
||||
|
||||
```
|
||||
Cette modification impacte X [composants|pièces|produits] :
|
||||
|
||||
Ajouts :
|
||||
• Y slots pièce à créer
|
||||
• Z valeurs de champs personnalisés à initialiser
|
||||
|
||||
Suppressions :
|
||||
• W slots produit à supprimer (les sélections seront perdues)
|
||||
|
||||
Modifications :
|
||||
• V valeurs de champs à réinitialiser (changement de type)
|
||||
|
||||
[Annuler] [Confirmer la synchronisation]
|
||||
```
|
||||
|
||||
### Nouveau service — `modelTypes.ts`
|
||||
|
||||
Ajout de deux fonctions au service existant :
|
||||
|
||||
```typescript
|
||||
export function syncPreview(id: string, structure: any) {
|
||||
return requestFetch(`/api/model_types/${id}/sync-preview`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ structure }),
|
||||
})
|
||||
}
|
||||
|
||||
export function syncExecute(id: string, confirmation: { confirmDeletions: boolean, confirmTypeChanges: boolean }) {
|
||||
return requestFetch(`/api/model_types/${id}/sync`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(confirmation),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Flow dans les pages d'édition de catégorie
|
||||
|
||||
```typescript
|
||||
const handleSubmit = async (payload) => {
|
||||
// 1. Preview (avant le save)
|
||||
const preview = await syncPreview(id, payload.structure)
|
||||
|
||||
const hasImpact = preview.itemCount > 0 && (
|
||||
Object.values(preview.additions).some(v => v > 0) ||
|
||||
Object.values(preview.deletions).some(v => v > 0) ||
|
||||
Object.values(preview.modifications).some(v => v > 0)
|
||||
)
|
||||
|
||||
// 2. Si impact, demander confirmation
|
||||
if (hasImpact) {
|
||||
pendingPayload.value = payload
|
||||
syncPreviewData.value = preview
|
||||
showSyncModal.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Pas d'impact → save direct (PATCH + sync)
|
||||
await saveAndSync(payload, { confirmDeletions: false, confirmTypeChanges: false })
|
||||
}
|
||||
|
||||
const onSyncConfirmed = async () => {
|
||||
const preview = syncPreviewData.value
|
||||
const needsDeleteConfirm = Object.values(preview.deletions).some(v => v > 0)
|
||||
const needsTypeChangeConfirm = preview.modifications.customFieldTypeChanges > 0
|
||||
|
||||
await saveAndSync(pendingPayload.value, {
|
||||
confirmDeletions: needsDeleteConfirm,
|
||||
confirmTypeChanges: needsTypeChangeConfirm,
|
||||
})
|
||||
}
|
||||
|
||||
const saveAndSync = async (payload, confirmation) => {
|
||||
await updateModelType(id, payload)
|
||||
await syncExecute(id, confirmation)
|
||||
showSuccess('Catégorie mise à jour et synchronisée.')
|
||||
router.push('/...')
|
||||
}
|
||||
```
|
||||
|
||||
## Non-régression
|
||||
|
||||
### Machines
|
||||
|
||||
- Le `MachineStructureController` lit les slots des composants. La sync modifie ces slots → les machines affichent automatiquement la dernière version au prochain chargement.
|
||||
- Aucun changement dans le controller machine.
|
||||
|
||||
### Quantités
|
||||
|
||||
- La sync **ne touche jamais** aux slots qui matchent toujours un skeleton requirement. Les quantités (`ComposantPieceSlot.quantity`) et sélections existantes sont préservées.
|
||||
- Les nouveaux slots ajoutés par la sync ont `quantity = 1` par défaut.
|
||||
- Le `ComposantPieceSlotController` (PATCH quantity) reste inchangé.
|
||||
|
||||
### `PieceProductSlot` — pas de quantité
|
||||
|
||||
Cohérent avec `ComposantProductSlot` qui n'a pas de quantité non plus.
|
||||
|
||||
### Relation M2M `piece_products`
|
||||
|
||||
La M2M existante reste en base pendant la période de transition. Le code frontend/backend qui la lit devra être migré vers les `productSlots`. La M2M sera supprimée dans une migration future une fois que tout le code utilise les slots.
|
||||
|
||||
## Tests
|
||||
|
||||
### Backend — PHPUnit
|
||||
|
||||
- `ModelTypeSyncControllerTest` — tests des endpoints preview et sync (y compris erreurs 403/404, confirmations partielles)
|
||||
- `ComposantSyncStrategyTest` — logique de diff pour composants (ajout, suppression, position update, no-op)
|
||||
- `PieceSyncStrategyTest` — logique de diff pour pièces (ajout/suppression de product slots)
|
||||
- `ProductSyncStrategyTest` — logique de diff pour produits (custom fields only)
|
||||
- `PieceProductSlotTest` — CRUD de la nouvelle entité
|
||||
- Idempotence : vérifier qu'un double appel à `sync` est un no-op
|
||||
- Vérifier la non-régression : `MachineStructureControllerTest` existant doit passer sans modification
|
||||
|
||||
### Frontend — Tests
|
||||
|
||||
- Supprimer `tests/composables/useCategoryEditGuard.test.ts`
|
||||
- Mettre à jour `tests/components/PieceModelStructureEditor.test.ts` (retirer restrictedMode)
|
||||
- Ajouter tests pour le flow sync dans les pages d'édition (preview → modal → confirm → save)
|
||||
|
||||
## Performance
|
||||
|
||||
Pour le volume attendu (dizaines d'items par catégorie, pas de milliers), la sync en PHP avec l'ORM Doctrine est suffisante. Si le volume augmente significativement, les opérations de création/suppression de slots pourront être converties en batch SQL (INSERT ... SELECT, DELETE ... WHERE) sans changer l'architecture (la strategy encapsule la logique).
|
||||
@@ -105,6 +105,114 @@ INSERT INTO public.model_types (id, name, code, category, notes, createdat, upda
|
||||
INSERT INTO public.model_types (id, name, code, category, notes, createdat, updatedat, description, componentskeleton, pieceskeleton, productskeleton) VALUES ('cle48e33ef67853069badfc5f0', 'testcor', 'testcor', 'COMPONENT', 'nnd', '2026-01-25 11:27:47', '2026-01-25 11:27:47', 'nnd', '{"pieces": [{"typePieceId": "cmgs1sco0002k47056yq8eyfq", "typePieceLabel": "Bavette"}], "products": [{"familyCode": "lubrifiant", "typeProductId": "cmhn9zrm5000247s8ds3bmpaf"}], "customFields": [{"key": "uu", "value": {"type": "text", "required": false}}], "subcomponents": [{"alias": "Trémie d''alimentation", "familyCode": "Tremie", "typeComposantId": "cmgs0htn5002d4705tyxxiqb2"}]}', NULL, NULL);
|
||||
|
||||
|
||||
--
|
||||
-- Data for Name: skeleton_piece_requirements; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl0939a6d5cf1e2f42ea338ed9', 'cmgrp0u0h00124705xxyt5fqu', 'cmgrnu6zc000f470565mc8hha', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clc8a5a3ffde2e012115f4a4a6', 'cmgrp0u0h00124705xxyt5fqu', 'cmgrnxlx5000g47059oyj4yuw', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl19977b37375e768874a9bc21', 'cmgrp0u0h00124705xxyt5fqu', 'cmgrohigo000z4705q8yvpih0', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('claa3bd610232237f37b5305f5', 'cmgrp0u0h00124705xxyt5fqu', 'cmgrou6670011470586ipgylm', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl8cc6f7b1c0d2356200710cff', 'cmgrp0u0h00124705xxyt5fqu', 'cmgroij2f00104705t6y33enk', 4, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl6ab668cf6c44971a6e8c15c1', 'cmgrp0u0h00124705xxyt5fqu', 'cmgrnu6zc000f470565mc8hha', 5, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl9d6c1d30a8f1f37d6f5919ca', 'cmgrzrlcc001t47054emo6cfb', 'cmgrzuwkj001u47057u8hej9u', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl89cc18036bb3a12d4d0cd427', 'cmgrzrlcc001t47054emo6cfb', 'cmgrzuwkj001u47057u8hej9u', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl6f6e8f47c6a731bcd9288bba', 'cmgrzrlcc001t47054emo6cfb', 'cmgrzuwkj001u47057u8hej9u', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clc7fe6f18be36b9cd60089a28', 'cmgrzrlcc001t47054emo6cfb', 'cmgroij2f00104705t6y33enk', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl15ff333888d9f160a80a04e5', 'cmgrzrlcc001t47054emo6cfb', 'cmgroij2f00104705t6y33enk', 4, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl82c521ac9e0cae5ba31d2880', 'cmgs0h5ze002c4705szh85svi', 'cmgroij2f00104705t6y33enk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl3eef5c0837bdf0aed9da51b3', 'cmgs0h5ze002c4705szh85svi', 'cmgs0kd5o002f47053b7n8tw6', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl2533d3d3f215df0c180b2c4a', 'cmgs0h5ze002c4705szh85svi', 'cmgrzuwkj001u47057u8hej9u', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl3ff44ebbbd85d8750328183c', 'cmgs0h5ze002c4705szh85svi', 'cmgroij2f00104705t6y33enk', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clbc503ec073758ef2f4cd2a4f', 'cmgs0htn5002d4705tyxxiqb2', 'cmgs13jjp002h4705rjqzz5lh', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl80631b5e0a1ab10d4af65f6f', 'cmgs0htn5002d4705tyxxiqb2', 'cmgs1s4pv002j470567o60oqe', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clc11a5ad82b02a76eca599117', 'cmgs0htn5002d4705tyxxiqb2', 'cmgs1sco0002k47056yq8eyfq', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clbd335edf794794fba25546a6', 'cmgs0htn5002d4705tyxxiqb2', 'cmgs1sco0002k47056yq8eyfq', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl218404db04ffcd7c8e200f0a', 'cmgs0i4je002e4705ndrhe26e', 'cmgroij2f00104705t6y33enk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl31265ec0dcf2fc4e252c2181', 'cmgs0i4je002e4705ndrhe26e', 'cmgrzuwkj001u47057u8hej9u', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl107b717d92362218aa387539', 'cmgs0i4je002e4705ndrhe26e', 'cmgujpyjf002q4705j6hv1nkk', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl5ee1bef2ef4b88f83a680526', 'cmgs0i4je002e4705ndrhe26e', 'cmgs1s4pv002j470567o60oqe', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl49eb01162df881cc87bc281f', 'cmgs0i4je002e4705ndrhe26e', 'cmgrnxlx5000g47059oyj4yuw', 4, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl1369592934d41016018dbbbf', 'cmgs0i4je002e4705ndrhe26e', 'cmgujpyjf002q4705j6hv1nkk', 5, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cld7bf0d8c46d0037cbb16cd69', 'cmgs0i4je002e4705ndrhe26e', 'cmgs1s4pv002j470567o60oqe', 6, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl75660a7f65559f164df06538', 'cmgs0i4je002e4705ndrhe26e', 'cm_motoreducteur_frein_01', 7, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl859ade5b78b6e833d1eb64f8', 'cmgs0i4je002e4705ndrhe26e', 'cm_motoreducteur_frein_01', 8, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cld23bbbeff05e238c6d189da0', 'cmgs0i4je002e4705ndrhe26e', 'cmgukvztv002s4705kqvqjtvg', 9, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl5e7a3eeeb8a6283ce828edf4', 'cmgs0i4je002e4705ndrhe26e', 'cmgukvztv002s4705kqvqjtvg', 10, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl52149961cdaed3f614dd87c1', 'cmgs0i4je002e4705ndrhe26e', 'cmgukxw26002t4705qz4ul929', 11, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cle1d646b02f65c7a5fd276ecf', 'cmgs0i4je002e4705ndrhe26e', 'cmgum1ih0000347ff7bsldmnv', 12, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl07b21cfdc0f2e9d8c11c79ed', 'cmgs0i4je002e4705ndrhe26e', 'cmgulzr7b000247ffpr2vsput', 13, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clb726dc1c94d371d8860ef92d', 'cmgs0i4je002e4705ndrhe26e', 'cmgum1wsl000447ffa109dtag', 14, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cle5605528c6fbb3de1f72b8d8', 'cmgujizjf002o4705kfdea5yw', 'cmgytewe0002447ffup09bscr', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clbd602414e77f0309cfeb92c6', 'cmgujizjf002o4705kfdea5yw', 'cmgytewe0002447ffup09bscr', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl604a3fa207118914f58f5ffb', 'cmgujizjf002o4705kfdea5yw', 'cmgytewe0002447ffup09bscr', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl016fd78557d8ac5ce883208b', 'cmgujizjf002o4705kfdea5yw', 'cmgytewe0002447ffup09bscr', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl29eeddd05188eec4e782aca3', 'cmgujizjf002o4705kfdea5yw', 'cmgytewe0002447ffup09bscr', 4, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl7e7a30ba2126ecfed7f7501c', 'cmgujizjf002o4705kfdea5yw', 'cmgz0qu29004a47ffw1bmjr75', 5, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clb893d014b683a1851425436e', 'cmgujizjf002o4705kfdea5yw', 'cmgz0qu29004a47ffw1bmjr75', 6, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl0b0094ca0a92321f5fa2131c', 'cmgujizjf002o4705kfdea5yw', 'cmgz0v9k4004v47ff8apimo50', 7, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clea857995e00ebb10548095c2', 'cmgujizjf002o4705kfdea5yw', 'cmgz0v9k4004v47ff8apimo50', 8, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl1341b6e156f059e852f719e2', 'cmgujizjf002o4705kfdea5yw', 'cmgz0v9k4004v47ff8apimo50', 9, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cle18462b08b605f31fec6cc14', 'cmgujizjf002o4705kfdea5yw', 'cmgz0v9k4004v47ff8apimo50', 10, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clf3f7ad1fcb6eb3937b96bb8e', 'cmgujizjf002o4705kfdea5yw', 'cmgz0zs4m006447ffq5b20ch3', 11, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clfdd9dfcc06816afd8bf8503a', 'cmgujizjf002o4705kfdea5yw', 'cmgz0zs4m006447ffq5b20ch3', 12, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl4af9588815f4a5a21cf4f979', 'cmgujizjf002o4705kfdea5yw', 'cmgz0zs4m006447ffq5b20ch3', 13, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl0e0d7053e7a0518b50f00481', 'cmgujizjf002o4705kfdea5yw', 'cmgz0zs4m006447ffq5b20ch3', 14, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl4f64c44ed0ffc92a923edccc', 'cmgujizjf002o4705kfdea5yw', 'cmgz17bpz006t47ff58i3j1e1', 15, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clfbdc4656e36b47a8e6a5df68', 'cmgujizjf002o4705kfdea5yw', 'cmgz17bpz006t47ff58i3j1e1', 16, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl4860204e0478d9f4eb806b85', 'cmgujjmpo002p470523lbfqmp', 'cmgum1wsl000447ffa109dtag', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl5e3367975bfef877130e41af', 'cmgujjmpo002p470523lbfqmp', 'cmgum1wsl000447ffa109dtag', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl0c004dd01a3f4a978c8260c9', 'cmh0kmyh1000847s5ciu9agzo', 'cmgujpyjf002q4705j6hv1nkk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl96109b5962d80d95d279fd96', 'cmh0kmyh1000847s5ciu9agzo', 'cmgytewe0002447ffup09bscr', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl5a6e124fa8ec83ae5ef21685', 'cmh20wuye001o47s5auvmq7s8', 'cmgujpyjf002q4705j6hv1nkk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clba2c60a3a4aab6a82eccad61', 'cmh20x49u001q47s5l9ahnvms', 'cmgs1sco0002k47056yq8eyfq', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clbf346fb41ad28af45eb5e7aa', 'cmh20x49u001q47s5l9ahnvms', 'cmgs1sco0002k47056yq8eyfq', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl50ec69f08be67181d3cd5dc3', 'cmh20yrgb001v47s54uxvi6km', 'cmgs1sco0002k47056yq8eyfq', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl84cab14314d4932ef52e9d47', 'cmh20yrgb001v47s54uxvi6km', 'cmgujpyjf002q4705j6hv1nkk', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cle258e4bb4ae12d90aeb49ae7', 'cmh20yrgb001v47s54uxvi6km', 'cmgujpyjf002q4705j6hv1nkk', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl697c36fe5e29f3cfdb209537', 'cmh20yrgb001v47s54uxvi6km', 'cmgujpyjf002q4705j6hv1nkk', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl112cc747d5ac2c68716bd6d4', 'cmh23pwbo002947s5ide6zx7g', 'cmgujpyjf002q4705j6hv1nkk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl62fe319d24840a318d24b682', 'cmkqpdc7a001o1eq6iwqvi3jk', 'cmgujpyjf002q4705j6hv1nkk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl0051b0a075490c406f21213e', 'cmkqq45yq00251eq6k1z0x7kt', 'cmgujpyjf002q4705j6hv1nkk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clc578afcad0f42694f05408f0', 'cmkqqdogo002l1eq6vy26j33g', 'cmgujpyjf002q4705j6hv1nkk', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl0a3a926192f808233c894c43', 'cmkqqdogo002l1eq6vy26j33g', 'cmh9bykt8001j47v7g0oej5dw', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl8a927a5de52db535ba8f062a', 'cmkqqdogo002l1eq6vy26j33g', 'cmhabzypq003h47v7jyjjxst1', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl8f60c43f25ec1c96560209b0', 'cmkqqdogo002l1eq6vy26j33g', 'cmgz0zs4m006447ffq5b20ch3', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('cl5c71200fd7efe31c9764e5d8', 'cmkqqdogo002l1eq6vy26j33g', 'cmkdqtcpv001r1e2wptehmkxi', 4, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_piece_requirements (id, modeltypeid, typepieceid, position, createdat, updatedat) VALUES ('clf6147721769314755e9ff6b8', 'cmkqqdogo002l1eq6vy26j33g', 'cmhbve5h30016475utwgpa32k', 5, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
|
||||
|
||||
--
|
||||
-- Data for Name: skeleton_product_requirements; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
INSERT INTO public.skeleton_product_requirements (id, modeltypeid, typeproductid, familycode, position, createdat, updatedat) VALUES ('cle44c43c22390db28f97b1c17', 'cmkqqdogo002l1eq6vy26j33g', 'cmhn9ze17000147s81dlr4i3v', 'graisse', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
|
||||
|
||||
--
|
||||
-- Data for Name: skeleton_subcomponent_requirements; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl9806dc8b4b42ea0c84b6832b', 'cmgs0i4je002e4705ndrhe26e', NULL, 'Kit', 'kit', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl67fc8aef811eb876a1cde9ed', 'cmgs0i4je002e4705ndrhe26e', NULL, 'Kit', 'kit', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl6cff73d32c4f16b5d4e909ab', 'cmgujjmpo002p470523lbfqmp', NULL, 'Kit', 'kit', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl7f83953a8f78f951f3a3628c', 'cmgz1az8d007g47fflwxk3q95', NULL, 'Contrôleur de rotation', 'controleur-de-rotation', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cle79fbc1bedd4123874a35377', 'cmgz1az8d007g47fflwxk3q95', NULL, 'Kit', 'kit', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl23c07d5454e5f2bc6d7703fa', 'cmgz1az8d007g47fflwxk3q95', NULL, 'Kit', 'kit', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cla3530a303f28a12d97883d82', 'cmgz1az8d007g47fflwxk3q95', NULL, 'Contrôleur de rotation', 'controleur-de-rotation', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('clefbd652fc2561a180f38dd89', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Tête convoyeur à bande', 'tcb', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl70978104735f0e8addcbb494', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Pied convoyeur à bande', 'PCB', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl63db65c869d07fd36d0d3422', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Elément intermédiaire & coude', 'EIC', 2, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('clac7e30f33f20287f76197c9e', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Trémie d''alimentation', 'Tremie', 3, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl081ba5565b6e9320be0c88b0', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Chariot Déverseur', 'Chariot', 4, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl164ed242e2148aac3b30c391', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Commande moteur', 'commande-moteur', 5, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl7aa47c4c4e4070ad1925ee27', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Déport de bande', 'deport-de-bande-et-controleur-de-rotation', 6, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl7c497e23a2deb7e85a2c2abe', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Contrôleur de rotation', 'controleur-de-rotation', 7, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('cl4a4fc72af5a46c4d369f535e', 'cmh0cz8e8000147s5g65vos5e', NULL, 'Contrôleur de rotation', 'controleur-de-rotation', 8, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('clce09e33e0f970116c87057c7', 'cmh0kmyh1000847s5ciu9agzo', 'cmh20z6g4001x47s5b5hturac', 'Paliers', 'paliers', 0, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
INSERT INTO public.skeleton_subcomponent_requirements (id, modeltypeid, typecomposantid, alias, familycode, position, createdat, updatedat) VALUES ('clb5e1e758aaed9688a4e7db60', 'cmh0kmyh1000847s5ciu9agzo', 'cmh4x8m4k000047nko0vwavbg', 'Roulement', 'roulement', 1, '2026-03-12 16:53:05', '2026-03-12 16:53:05');
|
||||
|
||||
|
||||
--
|
||||
-- Data for Name: products; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
@@ -178,6 +286,111 @@ INSERT INTO public.composants (id, name, reference, prix, createdat, updatedat,
|
||||
INSERT INTO public.composants (id, name, reference, prix, createdat, updatedat, typecomposantid, structure, productid) VALUES ('cl9b1583768c7c9fe6cfe93a11', 'testcor', 'll', 3.00, '2026-01-25 11:28:27', '2026-01-25 11:28:27', 'cle48e33ef67853069badfc5f0', '{"path":"root","definition":[],"pieces":[{"path":"root:piece-0","definition":{"typePieceId":"cmgs1sco0002k47056yq8eyfq","typePieceLabel":"Bavette"},"selectedPieceId":"cmgs1tfza002m4705mbl0kwok"}],"products":[{"path":"root:product-0","definition":{"typeProductId":"cmhn9zrm5000247s8ds3bmpaf","familyCode":"lubrifiant"},"selectedProductId":"cmkp97us6007k1e2ws0ogux1m"}],"subcomponents":[{"path":"root:sub-0","definition":{"alias":"Tr\u00e9mie d''alimentation","typeComposantId":"cmgs0htn5002d4705tyxxiqb2","familyCode":"Tremie"},"selectedComponentId":"cmgz5h2s0009v47ff6x26cqry"}]}', 'cmkp97us6007k1e2ws0ogux1m');
|
||||
|
||||
|
||||
--
|
||||
-- Data for Name: composant_piece_slots; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clcb580f2bc13c2f6ffefdfe47', 'cmgz53uvt009s47ff9v0uklr6', 'cmgrnu6zc000f470565mc8hha', 'cmgrnzbku000h4705qrj5eujb', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cleac90912bbe1571ec196c3c9', 'cmgz53uvt009s47ff9v0uklr6', 'cmgrohigo000z4705q8yvpih0', 'cmgrp2sju00144705n8etw7im', 1, 2, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl7b2de7e0f607306317eb6e69', 'cmgz53uvt009s47ff9v0uklr6', 'cmgrou6670011470586ipgylm', 'cmgrp1ry9001347052qn8q2yo', 1, 3, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl8a1a4da80556a964e7267773', 'cmgz53uvt009s47ff9v0uklr6', 'cmgroij2f00104705t6y33enk', 'cmgrp3lhv00194705f1xp8j0m', 1, 4, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl51a23d93f8c8fbf0555d8d4e', 'cmgz53uvt009s47ff9v0uklr6', 'cmgrnu6zc000f470565mc8hha', 'cmgz516t7009n47fft3nfyt34', 1, 5, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clb7caab438a04ccdbe72c7662', 'cmgz5ef4h009t47ffmxveesp0', 'cmgrzuwkj001u47057u8hej9u', 'cmgs07df2001w4705ry79yvbo', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl4e710917a9cc405aea45eae4', 'cmgz5ef4h009t47ffmxveesp0', 'cmgrzuwkj001u47057u8hej9u', 'cmgrzvdmo001v47050tvf2z88', 1, 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl0b03faf21c00093950c06434', 'cmgz5ef4h009t47ffmxveesp0', 'cmgrzuwkj001u47057u8hej9u', 'cmgs08kjb00234705wc5tytxg', 1, 2, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl2151fb8b1d31e27aeea1ba63', 'cmgz5ef4h009t47ffmxveesp0', 'cmgroij2f00104705t6y33enk', 'cmgrp3lhv00194705f1xp8j0m', 1, 3, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl1fb766a77cd4c06b0dd6f68c', 'cmgz5ef4h009t47ffmxveesp0', 'cmgroij2f00104705t6y33enk', 'cmgs0bive00274705zjmiuwzo', 1, 4, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clbb1e23854540497ce8a0cc04', 'cmgz5fsvz009u47ffkrardb1u', 'cmgroij2f00104705t6y33enk', 'cmgrp3lhv00194705f1xp8j0m', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl3410d53157d1426a7c0fee70', 'cmgz5fsvz009u47ffkrardb1u', 'cmgs0kd5o002f47053b7n8tw6', 'cmgs0nyk7002g4705rteyvw7x', 1, 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl6a3fbc2af5b82193cb7eb86c', 'cmgz5fsvz009u47ffkrardb1u', 'cmgrzuwkj001u47057u8hej9u', 'cmgrzvdmo001v47050tvf2z88', 1, 2, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cle60fa4fef139b2e86fc5bf47', 'cmgz5fsvz009u47ffkrardb1u', 'cmgroij2f00104705t6y33enk', 'cmgs0bive00274705zjmiuwzo', 1, 3, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('claf42f4acd9a024263ffb60c0', 'cmgz5h2s0009v47ff6x26cqry', 'cmgs13jjp002h4705rjqzz5lh', 'cmgs14c7a002i4705t1w4qdfx', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl44cc1edd9d00de1a675810d6', 'cmgz5h2s0009v47ff6x26cqry', 'cmgs1s4pv002j470567o60oqe', 'cmgs1swl0002l4705gpyg1yyn', 1, 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl0b8eb4a8defab5cde3bccd3d', 'cmgz5h2s0009v47ff6x26cqry', 'cmgs1sco0002k47056yq8eyfq', 'cmgs1tfza002m4705mbl0kwok', 1, 2, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl86e3c1391165cd65555856c1', 'cmgz5h2s0009v47ff6x26cqry', 'cmgs1sco0002k47056yq8eyfq', 'cmgs1tvrs002n4705gpym7vel', 1, 3, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clbc122534f74362e26882cfd1', 'cmgz79ivv009x47ffeh6of72i', 'cmgroij2f00104705t6y33enk', 'cmgrp3lhv00194705f1xp8j0m', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cla3b28a687250b61e1344dcc3', 'cmgz79ivv009x47ffeh6of72i', 'cmgrzuwkj001u47057u8hej9u', 'cmgum5zm0000547ffzg8ofiqr', 1, 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl8e42ccab069fa0ecd80a2cde', 'cmgz79ivv009x47ffeh6of72i', 'cmgujpyjf002q4705j6hv1nkk', 'cmgum9bn4000847fffbazanc5', 1, 2, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl786b5dc9712d2488ee7d1258', 'cmgz79ivv009x47ffeh6of72i', 'cmgs1s4pv002j470567o60oqe', 'cmgyruhgm000947ffmhhrqdrl', 1, 3, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl168e7708c9d3c91d5a404a5e', 'cmgz79ivv009x47ffeh6of72i', 'cmgujpyjf002q4705j6hv1nkk', 'cmgyrzrbc000h47ffd670wu8j', 1, 5, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl1fd495eaa3cec6667cfab566', 'cmgz79ivv009x47ffeh6of72i', 'cmgs1s4pv002j470567o60oqe', 'cmgys0mgx000i47ffxbftvqt4', 1, 6, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clbf85161d0d8ffef1d74220b6', 'cmgz79ivv009x47ffeh6of72i', 'cm_motoreducteur_frein_01', 'cmgys1osr000j47fftblpdpu2', 1, 7, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cld72b4dce93cb64a7909f3db6', 'cmgz79ivv009x47ffeh6of72i', 'cm_motoreducteur_frein_01', 'cmgys3ugw001147ffq33udxaw', 1, 8, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cle12f0173310ab9c752f8fb00', 'cmgz79ivv009x47ffeh6of72i', 'cmgukvztv002s4705kqvqjtvg', 'cmgys4a2s001847ffhqhz7zcd', 1, 9, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl5782cb522ada0d1de03c4d86', 'cmgz79ivv009x47ffeh6of72i', 'cmgukvztv002s4705kqvqjtvg', 'cmgys6k6b001h47ffuq44ze37', 1, 10, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl5d8c4c8e361ccdbaa041e5b9', 'cmgz79ivv009x47ffeh6of72i', 'cmgukxw26002t4705qz4ul929', 'cmgys7anf001m47ff0ulcp092', 1, 11, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clb1c4e1e8f6a00f1b11f24884', 'cmgz79ivv009x47ffeh6of72i', 'cmgum1ih0000347ff7bsldmnv', 'cmgys8mjl001r47ff5f8z85fs', 1, 12, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl4f4f675958d1667659498076', 'cmgz79ivv009x47ffeh6of72i', 'cmgulzr7b000247ffpr2vsput', 'cmgysatbl001u47ffu55db8gg', 1, 13, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cla3442d7af57410a430dd3c7e', 'cmgz79ivv009x47ffeh6of72i', 'cmgum1wsl000447ffa109dtag', 'cmgysj6wn002347ffgq3f98dr', 1, 14, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl0c706745220aa12658edc03c', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgytewe0002447ffup09bscr', 'cmgytmhw0002547ffzobsmpaa', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clca63f8c3aae40ebd68cc55b7', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgytewe0002447ffup09bscr', 'cmgytqtc8002u47ffum90ylo5', 1, 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cle09fbef03d0edcd66b0a4a0c', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgytewe0002447ffup09bscr', 'cmgyts8s9003747ffd1h8husf', 1, 2, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl57b44902fd7409abccdab4ae', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgytewe0002447ffup09bscr', 'cmgytuf06003k47ffdr8lvp13', 1, 3, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl47ee8065661468bc113d200a', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgytewe0002447ffup09bscr', 'cmgytx2ul003x47ffhdpurtx5', 1, 4, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl6cbbd1f5046e9dfc1a89a358', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0v9k4004v47ff8apimo50', 'cmgz0w66p004w47ffvj6xcxmo', 1, 7, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cle4cb39506f39efaf51af3e9e', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0v9k4004v47ff8apimo50', 'cmgz0xbx5005d47ffjkrafetg', 1, 8, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl8e47336d9326ac471ae821e7', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0v9k4004v47ff8apimo50', 'cmgz0y2aw005m47ff4zkjczei', 1, 9, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl5ae18ec62736f96d273353ec', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0v9k4004v47ff8apimo50', 'cmgz0yt33005v47ffy4p8d28z', 1, 10, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl524831b7cfa901ddc72e1728', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0zs4m006447ffq5b20ch3', 'cmgz10g67006547ffj28sqequ', 1, 11, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl4198321b8039385517c118ba', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0zs4m006447ffq5b20ch3', 'cmgz112hd006e47ffvg37mkoq', 1, 12, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl9fab43583266bb8abfe94d4d', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0zs4m006447ffq5b20ch3', 'cmgz11p1k006j47ffhjqgrnkp', 1, 13, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cled20136adaf599c72facf1a5', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz0zs4m006447ffq5b20ch3', 'cmgz128lz006o47ffwfgtag7e', 1, 14, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clca666296d8db92cabc1299d6', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz17bpz006t47ff58i3j1e1', 'cmgz17w9w006u47ffg6db710j', 1, 15, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl565591562f915536f015cf0a', 'cmgz7fd3l009y47fff1l4g0p0', 'cmgz17bpz006t47ff58i3j1e1', 'cmgz18vw1007947ffr2wg86sa', 1, 16, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl8a5cc24bf84edeaf24b87405', 'cmgz7igun009z47ffhea93fbw', 'cmgum1wsl000447ffa109dtag', 'cmgz1c9wx007h47ffr41untmr', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cle6cc6a2990d51326b7a16ac2', 'cmgz7igun009z47ffhea93fbw', 'cmgum1wsl000447ffa109dtag', 'cmgz1erci007r47ffybtdepul', 1, 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clf8105cc1fa7b62c4616cf509', 'cmh314rnj002q47s5cr3n6445', 'cmgujpyjf002q4705j6hv1nkk', 'cmh313676002d47s5li4e6qt9', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cla7987d4fe2fb2bf8ebe8ce48', 'cmh3jnikd002147zbmmnx2qw8', 'cmgujpyjf002q4705j6hv1nkk', 'cmh31ejnw003b47s548rtk8b1', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clb39042e520687e6c6b51f788', 'cl10c0924d10135c5f515378ac', 'cmgujpyjf002q4705j6hv1nkk', 'cl7b3702f04d24d87e47232a14', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl50fa6580ea24bfbcfb018cac', 'cl10eedbb54a0d2cd0fa3ce9c6', 'cmgujpyjf002q4705j6hv1nkk', 'cle1db7051dbef91fc009073a6', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl2289dce68b996d2c0b56cd49', 'cl36d84884cad86fbc92dba133', 'cmgujpyjf002q4705j6hv1nkk', 'clbf9f0070ebd464b3c309c646', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cla130de58dd731b934e1f9789', 'cl3dbac5194bc192a0589465ba', 'cmgujpyjf002q4705j6hv1nkk', 'cl50fe870a07e42759b37b511f', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl75bfb7413b5df226846ab367', 'cl4660bae41d2af254e6c3b726', 'cmgujpyjf002q4705j6hv1nkk', 'cl4e975566464253882018adcc', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl076c31311e25600aae5d07e2', 'cl54b1b4509971fde475572b29', 'cmgujpyjf002q4705j6hv1nkk', 'cl731386df55fcb9e6a01e0a63', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl4874e08b827307a1313a71b5', 'cl5a8f9656aa7e14c012f30700', 'cmgujpyjf002q4705j6hv1nkk', 'cl5ee293dc7b61feba510082a4', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl6935b95a12cd846b6a3bcbfc', 'cl5b5e336095de8d4ece81b2dc', 'cmgujpyjf002q4705j6hv1nkk', 'cldd656c6092225f53a22badc0', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clf30ffdeac20200df2d190c45', 'cl5e9c6b18bccd38517026dc1c', 'cmgujpyjf002q4705j6hv1nkk', 'clfa3147270e5e66f9b52c425e', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clb8fa63511f6514497e8c1a5f', 'cl7df36c9e7391df3d4ff46102', 'cmgujpyjf002q4705j6hv1nkk', 'cl1406ef19de58fdd1adf40221', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl2251745f51b584dd8bd73aad', 'cl7f254c23161d9c853c3e6d92', 'cmgujpyjf002q4705j6hv1nkk', 'clf16e543545eddd01b20077df', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cle99713d099f9a006931a7863', 'cl8b9b36f5a822aae21edb5a5f', 'cmgujpyjf002q4705j6hv1nkk', 'cl6667d159f6d07ba77fa79b39', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl469909161ea4061d1b514f06', 'cla833681664bb851ca61aca51', 'cmgujpyjf002q4705j6hv1nkk', 'cl8570d729efd017c12a2d5c3d', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('clf9f5aaccf25ef3a54ace17ad', 'clba5633e840726188261145f9', 'cmgujpyjf002q4705j6hv1nkk', 'clafaa71cbf49777fbb8415f19', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cld23a4f8bfc3894842b531b47', 'clbd1e945fb222e1c56dd43941', 'cmgujpyjf002q4705j6hv1nkk', 'clc08fbdcd334ed869772d98ee', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl88ea31be5c6114d3267daaad', 'clbe710810fd7ccd09811957b3', 'cmgujpyjf002q4705j6hv1nkk', 'cmh313676002d47s5li4e6qt9', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cldb0304d3089db7aced359a4f', 'cldd7f161d2cd08ee54e79161e', 'cmgujpyjf002q4705j6hv1nkk', 'cl22c13dbc4d38a1f846323ae6', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_piece_slots (id, composantid, typepieceid, selectedpieceid, quantity, position, createdat, updatedat) VALUES ('cl6e857d06dd103028af452bf6', 'cle98225ad3a32f5d8531950ef', 'cmgujpyjf002q4705j6hv1nkk', 'cl531dde45c3fc64c1a3b16ca0', 1, 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
|
||||
|
||||
--
|
||||
-- Data for Name: composant_subcomponent_slots; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl114d5a6febff03d69286b6b7', 'cmgz79ivv009x47ffeh6of72i', 'Kit', 'kit', NULL, 'cmgz49bm2009547ff7ham94wz', 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl1d648f7c5a3b0ee64b22e69b', 'cmgz79ivv009x47ffeh6of72i', 'Kit', 'kit', NULL, 'cmgz4bczt009647ffzidmc8tc', 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl710c40366df98d7aeb547457', 'cmgz7igun009z47ffhea93fbw', 'Kit', 'kit', NULL, 'cmgz4equ5009747ffq665rpeb', 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl23ac52af05e90d9c7d2daba2', 'cmh0d59v5000347s561ahbept', 'Tête convoyeur à bande', 'tcb', NULL, 'cmgz53uvt009s47ff9v0uklr6', 0, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl5c1f5ff5fb762ffb1c62d02c', 'cmh0d59v5000347s561ahbept', 'Pied convoyeur à bande', 'PCB', NULL, 'cmgz5ef4h009t47ffmxveesp0', 1, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl9a90d9876471267669c5d83b', 'cmh0d59v5000347s561ahbept', 'Elément intermédiaire & coude', 'EIC', NULL, 'cmgz5fsvz009u47ffkrardb1u', 2, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('claeaa69eb72eff09abe7ef3fb', 'cmh0d59v5000347s561ahbept', 'Trémie d''alimentation', 'Tremie', NULL, 'cmgz5h2s0009v47ff6x26cqry', 3, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('clfcb74e5cc092b753c65f74f8', 'cmh0d59v5000347s561ahbept', 'Chariot Déverseur', 'Chariot', NULL, 'cmgz79ivv009x47ffeh6of72i', 4, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl8bb434545f2bca42d73d588e', 'cmh0d59v5000347s561ahbept', 'Commande moteur', 'commande-moteur', NULL, 'cmgz7fd3l009y47fff1l4g0p0', 5, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cle71559358e6346f59657d3d5', 'cmh0d59v5000347s561ahbept', 'Déport de bande', 'deport-de-bande-et-controleur-de-rotation', NULL, 'cmgz7igun009z47ffhea93fbw', 6, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl4a7d21f525c63c0581af5fc6', 'cmh0d59v5000347s561ahbept', 'Contrôleur de rotation', 'controleur-de-rotation', NULL, 'cmgz4qzap009b47ffj7ch6th7', 7, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
INSERT INTO public.composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) VALUES ('cl9f39d9a471c6d1d9ae4c4654', 'cmh0d59v5000347s561ahbept', 'Contrôleur de rotation', 'controleur-de-rotation', NULL, 'cmgz4r99x009c47ffco9f2img', 8, '2026-03-12 17:19:23', '2026-03-12 17:19:23');
|
||||
|
||||
|
||||
--
|
||||
-- Data for Name: composant_product_slots; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Data for Name: piece_products; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Data for Name: constructeurs; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
@@ -257,121 +470,121 @@ INSERT INTO public.machines (id, name, reference, prix, createdat, updatedat, si
|
||||
-- Data for Name: pieces; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgrp1ry9001347052qn8q2yo', 'Lame raclette', 'P40S069915', NULL, '2025-10-15 07:53:07.52', '2025-10-15 07:53:07.52', 'cmgrou6670011470586ipgylm', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgrp46ud001i4705nvphpv0f', 'Palier applique', 'X21000923', NULL, '2025-10-15 07:55:00.132', '2025-10-15 07:55:00.132', 'cmgrnxlx5000g47059oyj4yuw', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgrp2sju00144705n8etw7im', 'Bras tendeur SE18', 'X56654', NULL, '2025-10-15 07:53:54.954', '2025-10-15 12:54:18.646', 'cmgrohigo000z4705q8yvpih0', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs08kjb00234705wc5tytxg', 'Cage d''écureuil de tension', 'W57719', NULL, '2025-10-15 13:06:20.278', '2025-10-15 13:06:20.278', 'cmgrzuwkj001u47057u8hej9u', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh3h3c82001f47zbvfmcu17d', 'Auget tôle', NULL, NULL, '2025-10-23 13:43:37.634', '2025-10-23 13:43:37.634', NULL, NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs0bive00274705zjmiuwzo', 'Rouleau1', 'X24001026', NULL, '2025-10-15 13:08:38.087', '2025-10-15 13:08:38.087', 'cmgroij2f00104705t6y33enk', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs1tfza002m4705mbl0kwok', 'Bavette alimentation', 'P30W07069', NULL, '2025-10-15 13:50:33.766', '2025-10-15 13:50:33.766', 'cmgs1sco0002k47056yq8eyfq', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs1tvrs002n4705gpym7vel', 'Bavette centrage', 'P30W07052', NULL, '2025-10-15 13:50:54.232', '2025-10-15 13:50:54.232', 'cmgs1sco0002k47056yq8eyfq', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgys8mjl001r47ff5f8z85fs', 'Attache rapide 19.05S', 'X10000565', NULL, '2025-10-20 06:56:49.185', '2025-10-20 06:56:49.185', 'cmgum1ih0000347ff7bsldmnv', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgytmhw0002547ffzobsmpaa', 'Moteur éléctrique', 'X50001591', NULL, '2025-10-20 07:35:35.925', '2025-10-20 07:35:35.925', 'cmgytewe0002447ffup09bscr', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgyts8s9003747ffd1h8husf', 'Moteur éléctrique2', 'X50001596', NULL, '2025-10-20 07:40:04.088', '2025-10-20 07:40:04.088', 'cmgytewe0002447ffup09bscr', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz0rm8j004b47ffg2bh2ort', 'Réducteur1', 'X28896', NULL, '2025-10-20 10:55:32.179', '2025-10-20 10:55:32.179', 'cmgz0qu29004a47ffw1bmjr75', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz0se74004o47ffnbbtu66b', 'Réducteur2', 'X15009329', NULL, '2025-10-20 10:56:08.416', '2025-10-20 10:56:08.416', 'cmgz0qu29004a47ffw1bmjr75', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh99o05y001547v7az12sk2n', 'Moteur à flasque', NULL, NULL, '2025-10-27 15:02:21.884', '2025-10-27 15:02:21.884', 'cmgytewe0002447ffup09bscr', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz0w66p004w47ffvj6xcxmo', 'Poulie1', 'X53433', NULL, '2025-10-20 10:59:04.657', '2025-10-20 10:59:26.075', 'cmgz0v9k4004v47ff8apimo50', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz17w9w006u47ffg6db710j', 'Courroie', 'X47067', NULL, '2025-10-20 11:08:11.684', '2025-10-20 11:08:11.684', 'cmgz17bpz006t47ff58i3j1e1', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhaesmf5003v47v7bub04g9p', 'Joint à lèvre', 'J41800-RLX', 44.00, '2025-10-28 10:13:41.607', '2025-10-28 10:13:41.607', 'cmhabzypq003h47v7jyjjxst1', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhbtjqbt0000475ultd24cp0', 'Segment d''arrêt - Circlips', 'S41800-SA2', 37.00, '2025-10-29 09:54:27.209', '2025-10-29 09:54:27.209', 'cmhalh6sa004h47v7y6pnqok2', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz18vw1007947ffr2wg86sa', 'Courroie2', 'X53480', NULL, '2025-10-20 11:08:57.84', '2025-10-20 11:08:57.84', 'cmgz17bpz006t47ff58i3j1e1', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhbv3kj9000t475uzbqpult7', 'Rondelle frein MB20', 'RDLMB20', 7.00, '2025-10-29 10:37:52.437', '2025-10-29 10:37:52.437', 'cmhbuzjrz000s475ue0q2o2xd', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhd56h02002a475uunel89e0', 'Manille', NULL, NULL, '2025-10-30 08:07:50.161', '2025-10-30 08:07:50.161', 'cmhd55caa0029475u1t4vg1i2', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhkr1ulr000147yv73imk2nx', 'Rondelle plate M4 RVS-A2', NULL, NULL, '2025-11-04 15:54:29.296', '2025-11-04 16:49:00.924', 'cmhbuzjrz000s475ue0q2o2xd', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh313676002d47s5li4e6qt9', 'Arbre', NULL, NULL, '2025-10-23 06:15:35.969', '2025-10-23 06:15:35.969', 'cmgujpyjf002q4705j6hv1nkk', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh3jsgfr002n47zbadkkds7r', 'Bavette2', NULL, NULL, '2025-10-23 14:59:08.727', '2025-10-23 14:59:08.727', 'cmgs1sco0002k47056yq8eyfq', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgrp3lhv00194705f1xp8j0m', 'Rouleau', 'X24001025', NULL, '2025-10-15 07:54:32.438', '2025-10-15 07:54:32.438', 'cmgroij2f00104705t6y33enk', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs0nyk7002g4705rteyvw7x', 'Support rouleau inférieur', 'T30S06944', NULL, '2025-10-15 13:18:18.295', '2025-10-15 13:18:18.295', 'cmgs0kd5o002f47053b7n8tw6', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgum5zm0000547ffzg8ofiqr', 'Cage d''écureuil', 'W78517', NULL, '2025-10-17 08:55:43.753', '2025-10-17 08:55:43.753', 'cmgrzuwkj001u47057u8hej9u', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgum9bn4000847fffbazanc5', 'Arbre roue avant', 'H22907', NULL, '2025-10-17 08:58:19.312', '2025-10-17 08:58:19.312', 'cmgujpyjf002q4705j6hv1nkk', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhaf1nsb004347v75uv8gmsi', 'Axe rouleau Promill', NULL, NULL, '2025-10-28 10:20:43.281', '2025-10-28 10:20:43.281', 'cmhaex3ca004247v78ymfpvpd', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhbu3due0007475ung88xfpm', 'Cuvette pour roulement HH 228310', 'C41800-CO2', 498.00, '2025-10-29 10:09:44.122', '2025-10-29 10:09:44.122', 'cmh9bykt8001j47v7g0oej5dw', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgytqtc8002u47ffum90ylo5', 'Moteur éléctrique1', 'X50001593', NULL, '2025-10-20 07:38:57.415', '2025-10-20 07:38:57.415', 'cmgytewe0002447ffup09bscr', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz0xbx5005d47ffjkrafetg', 'Poulie2', 'X53446', NULL, '2025-10-20 10:59:58.743', '2025-10-20 10:59:58.743', 'cmgz0v9k4004v47ff8apimo50', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz1c9wx007h47ffr41untmr', 'Détecteur déport de bande', 'X23100', NULL, '2025-10-20 11:11:35.985', '2025-10-20 11:12:51.466', 'cmgum1wsl000447ffa109dtag', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhbvyr210017475ubrey4eux', 'Ecrou de blocage KM20', 'ECRKM20A', 42.00, '2025-10-29 11:02:07.197', '2025-10-29 11:04:47.071', 'cmhbve5h30016475utwgpa32k', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh4xg7lb000347nkrtvqw3hi', 'Arbre Tapis émotteur', NULL, NULL, '2025-10-24 14:09:18.164', '2025-10-24 14:09:18.164', 'cmgujpyjf002q4705j6hv1nkk', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhaagmno003647v7sfrgsb5v', 'COQUILLE nid d''abeille Promill', 'E41800ASN1P', 574.00, '2025-10-28 08:12:23.603', '2025-10-28 08:12:23.603', 'cmhaa5la2003447v7do7w3s0i', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhaf9o8j004947v7xomube6n', 'Flasque arrière rouleau Promill', 'F07700-001/5759701', 86.71, '2025-10-28 10:26:57.139', '2025-10-28 10:26:57.139', 'cmhaf6jaj004847v7cpq93sq5', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhafarpc004e47v7nqdy97xs', 'Flasque avant rouleau Promill', 'F07700-002/5759601', 115.30, '2025-10-28 10:27:48.288', '2025-10-28 10:27:48.288', 'cmhaf6jaj004847v7cpq93sq5', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhbuapy5000e475utfiwfkcj', 'Cone pour roulement HH228340', 'C41800-CO2', 498.00, '2025-10-29 10:15:26.402', '2025-10-29 10:15:26.402', 'cmh9bykt8001j47v7g0oej5dw', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhd4jjw9001q475u8x4i63jw', 'Graisseur 1/4 rouleaux Promill', NULL, NULL, '2025-10-30 07:50:00.797', '2025-10-30 07:50:00.797', 'cmhd48ipe001p475ul7ejiutq', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhdb5hon002p475uif5ri4vu', 'Douille de serrage', NULL, NULL, '2025-10-30 10:55:02.086', '2025-10-30 10:55:02.086', 'cmhdb4mgx002o475udcnbd71h', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh31ejnw003b47s548rtk8b1', 'Arbre de commande', NULL, NULL, '2025-10-23 06:24:26.634', '2025-11-06 13:36:45.099', 'cmgujpyjf002q4705j6hv1nkk', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgrzvdmo001v47050tvf2z88', 'Cage d''écureuil de pied', 'W78515', NULL, '2025-10-15 12:56:04.801', '2025-10-15 13:05:49.946', 'cmgrzuwkj001u47057u8hej9u', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs14c7a002i4705t1w4qdfx', 'rouleau amortisseur avec axe', 'E1RS07058', NULL, '2025-10-15 13:31:02.469', '2025-10-15 13:31:02.469', 'cmgs13jjp002h4705rjqzz5lh', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgyruhgm000947ffmhhrqdrl', 'Galet avant chariot déverseur', 'H22698', NULL, '2025-10-20 06:45:49.386', '2025-10-20 06:45:49.386', 'cmgs1s4pv002j470567o60oqe', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgyrxcgu000a47ffxvcyyuwm', 'Palier BPF5', 'X21000919', NULL, '2025-10-20 06:48:02.91', '2025-10-20 06:48:02.91', 'cmgrnxlx5000g47059oyj4yuw', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgyrzrbc000h47ffd670wu8j', 'Arbre roue arrière', 'H22908', NULL, '2025-10-20 06:49:55.463', '2025-10-20 06:49:55.463', 'cmgujpyjf002q4705j6hv1nkk', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgys0mgx000i47ffxbftvqt4', 'Galet arrière chariot déverseur', 'H22861', NULL, '2025-10-20 06:50:35.84', '2025-10-20 06:50:35.84', 'cmgs1s4pv002j470567o60oqe', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgysatbl001u47ffu55db8gg', 'Vérin éléctrique', 'X22754', NULL, '2025-10-20 06:58:31.282', '2025-10-20 06:58:31.282', 'cmgulzr7b000247ffpr2vsput', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgytuf06003k47ffdr8lvp13', 'Moteur éléctrique3', 'X50001598', NULL, '2025-10-20 07:41:45.434', '2025-10-20 07:41:45.434', 'cmgytewe0002447ffup09bscr', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz0y2aw005m47ff4zkjczei', 'Poulie3', 'X53450', NULL, '2025-10-20 11:00:32.936', '2025-10-20 11:00:32.936', 'cmgz0v9k4004v47ff8apimo50', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz112hd006e47ffvg37mkoq', 'Moyeu amovible2', 'X11F00653', NULL, '2025-10-20 11:02:53.136', '2025-10-20 11:02:53.136', 'cmgz0zs4m006447ffq5b20ch3', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz1erci007r47ffybtdepul', 'Détecteur déport de bande1', 'X53294', NULL, '2025-10-20 11:13:31.891', '2025-10-20 11:13:31.891', 'cmgum1wsl000447ffa109dtag', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh3eadge001147zbworn1671', 'Bavette 2', NULL, NULL, '2025-10-23 12:25:06.974', '2025-10-23 12:25:06.974', 'cmgs1sco0002k47056yq8eyfq', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh3e95j4000k47zbx53n4tqv', 'Bavette1', NULL, NULL, '2025-10-23 12:24:10.047', '2025-10-23 14:58:45.061', 'cmgs1sco0002k47056yq8eyfq', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgrnzbku000h4705qrj5eujb', 'Tambour de tête', 'H57305', NULL, '2025-10-15 07:23:13.346', '2025-10-15 07:23:13.346', 'cmgrnu6zc000f470565mc8hha', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs07df2001w4705ry79yvbo', 'Cage d''écureuil de pied de tension', 'W58372', NULL, '2025-10-15 13:05:24.397', '2025-10-15 13:05:24.397', 'cmgrzuwkj001u47057u8hej9u', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgs1swl0002l4705gpyg1yyn', 'Galet releveur complet', 'W32440', NULL, '2025-10-15 13:50:08.628', '2025-10-15 13:50:08.628', 'cmgs1s4pv002j470567o60oqe', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgys4a2s001847ffhqhz7zcd', 'Pignon moteur', 'H38143', NULL, '2025-10-20 06:53:26.404', '2025-10-20 06:54:46.835', 'cmgukvztv002s4705kqvqjtvg', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgys6k6b001h47ffuq44ze37', 'Pignon récepteur', 'H47381', NULL, '2025-10-20 06:55:12.803', '2025-10-20 06:55:12.803', 'cmgukvztv002s4705kqvqjtvg', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgys7anf001m47ff0ulcp092', 'Chaîne 19.05S', 'X10000564', NULL, '2025-10-20 06:55:47.115', '2025-10-20 06:56:20.844', 'cmgukxw26002t4705qz4ul929', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgysj6wn002347ffgq3f98dr', 'Détecteur mécanique', 'X60001690', NULL, '2025-10-20 07:05:02.134', '2025-10-20 07:05:02.134', 'cmgum1wsl000447ffa109dtag', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgytx2ul003x47ffhdpurtx5', 'Moteur éléctrique4', 'X50001600', NULL, '2025-10-20 07:43:49.676', '2025-10-20 07:43:49.676', 'cmgytewe0002447ffup09bscr', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz0yt33005v47ffy4p8d28z', 'Poulie4', 'X41745', NULL, '2025-10-20 11:01:07.646', '2025-10-20 11:01:07.646', 'cmgz0v9k4004v47ff8apimo50', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz10g67006547ffj28sqequ', 'Moyeu amovible1', 'X43888', NULL, '2025-10-20 11:02:24.223', '2025-10-20 11:02:24.223', 'cmgz0zs4m006447ffq5b20ch3', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz11p1k006j47ffhjqgrnkp', 'Moyeu amovible3', 'X41739', NULL, '2025-10-20 11:03:22.375', '2025-10-20 11:03:22.375', 'cmgz0zs4m006447ffq5b20ch3', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz128lz006o47ffwfgtag7e', 'Moyeu amovible4', 'X11F00765', NULL, '2025-10-20 11:03:47.735', '2025-10-20 11:03:47.735', 'cmgz0zs4m006447ffq5b20ch3', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgz516t7009n47fft3nfyt34', 'Tambour de tête1', 'H138830', NULL, '2025-10-20 12:54:57.211', '2025-10-20 12:54:57.211', 'cmgrnu6zc000f470565mc8hha', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhae5b7u003i47v7vb4qi81n', 'Joint torique R41', 'JTR41', 2.00, '2025-10-28 09:55:33.999', '2025-10-28 09:55:33.999', 'cmhabzypq003h47v7jyjjxst1', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhallrb1004i47v7855gvfpe', 'Segment d''étanchéïté', 'S41800-SA2', 37.00, '2025-10-28 13:24:18.658', '2025-10-28 13:24:56.572', 'cmhalh6sa004h47v7y6pnqok2', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgys1osr000j47fftblpdpu2', 'Motoréducteur frein', 'X33959', NULL, '2025-10-20 06:51:25.485', '2025-10-20 06:52:25.744', NULL, NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmgys3ugw001147ffq33udxaw', 'Motoréducteur frein.', 'X108273', NULL, '2025-10-20 06:53:06.176', '2025-10-20 06:53:06.176', NULL, NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhbuq97o000l475uf73oiot0', 'Entretoise de roulements', 'E41800-000', 67.00, '2025-10-29 10:27:31.208', '2025-10-29 10:27:31.208', 'cmh9bykt8001j47v7g0oej5dw', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhd4s3u80020475uasnc0mqj', 'Crochet de levage', NULL, NULL, '2025-10-30 07:56:39.92', '2025-10-30 07:56:39.92', 'cmhd4r5bg001z475u0f4tm9yy', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhd4syhb0025475u4sv87dyf', 'Crochet de levage avec manille', NULL, NULL, '2025-10-30 07:57:19.63', '2025-10-30 07:57:19.63', 'cmhd4r5bg001z475u0f4tm9yy', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhdiuklf002u475uadws3h04', 'Courroie XPC', NULL, NULL, '2025-10-30 14:30:29.569', '2025-10-30 14:30:29.569', 'cmgz17bpz006t47ff58i3j1e1', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmh99m3e6000847v75h2m7czn', 'Réducteur emo', NULL, NULL, '2025-10-27 15:00:52.781', '2025-10-27 15:00:52.781', NULL, NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmizudzfy00021e2w2mtd9zv8', 'lame de godet 82', NULL, 192.00, '2025-12-10 10:04:09.262', '2025-12-10 10:05:37.177', 'cmizu3st800001e2waysco15j', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmizv8nzu00081e2wen6ur31b', 'Tapis', 'PF0165295', 3730.67, '2025-12-10 10:28:00.762', '2025-12-10 10:39:59.943', 'cmizup8cv00061e2w2rulkxsn', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmjcixqq300141e2wqkvz0cx6', 'VIS (lame de godet) 82 M14 60mm tête fraisé', NULL, NULL, '2025-12-19 07:04:35.979', '2025-12-19 07:04:35.979', 'cmj025vi7000z1e2wyn3x6msv', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmjcpdwqs00161e2wu4juy4u2', 'Ecrou Ø 14', NULL, NULL, '2025-12-19 10:05:07.973', '2025-12-19 10:05:07.973', 'cmhbve5h30016475utwgpa32k', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmhdattcv002f475ugw514oj3', 'Poulie 8', NULL, NULL, '2025-10-30 10:45:57.343', '2026-01-14 08:04:14.29', 'cmgz0v9k4004v47ff8apimo50', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdqrkez001o1e2wtslqeazi', 'Carter presse', NULL, NULL, '2026-01-14 08:11:13.308', '2026-01-14 08:11:13.308', 'cmkdqqh8w001n1e2wzxkamd1m', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdqw1tq00231e2wxou4eu8z', 'Ecrou HM12', NULL, NULL, '2026-01-14 08:14:42.494', '2026-01-14 08:14:42.494', 'cmhbve5h30016475utwgpa32k', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdrc535002e1e2wjuucdweq', 'Moteur entrainement Presse Promill', NULL, NULL, '2026-01-14 08:27:13.217', '2026-01-14 08:27:13.217', 'cmgytewe0002447ffup09bscr', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdsbrz1002t1e2wqemldbr6', 'Vis HM 14x100', NULL, NULL, '2026-01-14 08:54:55.838', '2026-01-14 08:54:55.838', 'cmkdqtcpv001r1e2wptehmkxi', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdsdqjh00301e2wu2g4ljg2', 'Rondelle plate M24', NULL, NULL, '2026-01-14 08:56:27.294', '2026-01-14 08:56:27.294', 'cmhbuzjrz000s475ue0q2o2xd', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdsp16u003b1e2wetp787yf', 'Rondelle Grower W24', NULL, NULL, '2026-01-14 09:05:14.31', '2026-01-14 09:05:14.31', 'cmhbuzjrz000s475ue0q2o2xd', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdsrew1003m1e2wcuky5m77', 'Ecrou HM24', NULL, NULL, '2026-01-14 09:07:05.377', '2026-01-14 09:07:05.377', 'cmhbve5h30016475utwgpa32k', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdqun4a001s1e2wx123zdy1', 'Vis HM 12x35', NULL, NULL, '2026-01-14 08:13:36.778', '2026-01-14 09:15:22.132', 'cmkdqtcpv001r1e2wptehmkxi', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdt82ma003x1e2w9gkgwybf', 'Vis HM 12x30', NULL, NULL, '2026-01-14 09:20:02.626', '2026-01-14 09:20:02.626', 'cmkdqtcpv001r1e2wptehmkxi', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkdtdh5p00441e2w0g6ye4v5', 'Rondelle Grower W12', NULL, NULL, '2026-01-14 09:24:14.75', '2026-01-14 09:24:14.75', 'cmhbuzjrz000s475ue0q2o2xd', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmke1nimf004f1e2wsurzoeet', 'Vis HM 8x16', NULL, NULL, '2026-01-14 13:16:00.135', '2026-01-14 13:16:00.135', 'cmkdqtcpv001r1e2wptehmkxi', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmke3hgbc004o1e2w02his9k3', 'Vis HM 24x100', NULL, NULL, '2026-01-14 14:07:16.44', '2026-01-14 14:07:16.44', 'cmkdqtcpv001r1e2wptehmkxi', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkp7xsz1006b1e2whn582enn', 'Arbre principal Presse Promill', 'APR 80 101101', NULL, '2026-01-22 08:57:25.741', '2026-01-22 08:57:25.741', 'cmgujpyjf002q4705j6hv1nkk', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkp80cfi006k1e2wha1v14vv', 'Joint Viton', NULL, NULL, '2026-01-22 08:59:24.27', '2026-01-22 08:59:24.27', 'cmhabzypq003h47v7jyjjxst1', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkp84bs0006t1e2wkokejtyn', 'Roulement à rouleaux cylindriques', NULL, NULL, '2026-01-22 09:02:30.048', '2026-01-22 09:02:30.048', 'cmh9bykt8001j47v7g0oej5dw', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkp8mbo900751e2w2746k94i', 'Circlips E260', '390 01126000', NULL, '2026-01-22 09:16:29.721', '2026-01-22 09:16:29.721', 'cmhalh6sa004h47v7y6pnqok2', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkpkaj4400091eq6p6accs62', 'Moyeu central', 'APR 80 101103', NULL, '2026-01-22 14:43:14.884', '2026-01-22 14:43:14.884', 'cmgz0zs4m006447ffq5b20ch3', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkpkwfdh000g1eq6hpqdbqat', 'Roulement à rotule sur rouleaux', '320 41 220001', NULL, '2026-01-22 15:00:16.469', '2026-01-22 15:00:16.469', 'cmh9bykt8001j47v7g0oej5dw', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkpnw6er000s1eq6k57kcnl8', 'Vis CHC M14x40', NULL, NULL, '2026-01-22 16:24:03.699', '2026-01-22 16:24:03.699', 'cmkdqtcpv001r1e2wptehmkxi', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkpo4gxz00121eq6o0ahmizg', 'Joint', '370 20 280000', NULL, '2026-01-22 16:30:30.6', '2026-01-22 16:30:30.6', 'cmhabzypq003h47v7jyjjxst1', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr0nq1a004e1eq6v6ubxlfl', 'Palier tête E1 17', 'SNU516613', NULL, '2026-01-23 15:09:10.414', '2026-01-23 15:09:44.182', 'cmgrnxlx5000g47059oyj4yuw', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr0owbv004l1eq6pzlatzlr', 'Vis HM 14x50', NULL, NULL, '2026-01-23 15:10:05.228', '2026-01-23 15:10:05.228', 'cmkdqtcpv001r1e2wptehmkxi', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkqzl1oa002v1eq6erkt5544', 'BANDE E1 17', NULL, NULL, '2026-01-23 14:39:05.914', '2026-01-23 14:48:36.329', 'cmknus46z00551e2wf7zy706v', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr0fwuo003o1eq69b1idlpw', 'Entretoise porte 2 joints', 'APR 80 101108', NULL, '2026-01-23 15:03:06', '2026-01-23 15:03:06', 'cmhabzypq003h47v7jyjjxst1', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr0koqb003y1eq6a8cx8poy', 'Flasque porte 2 joints', 'APR 25 0304..', NULL, '2026-01-23 15:06:48.755', '2026-01-23 15:06:48.755', 'cmhaf6jaj004847v7cpq93sq5', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr0nmsv00431eq6rcnozdes', 'Rondelle Grower W14', NULL, NULL, '2026-01-23 15:09:06.224', '2026-01-23 15:09:06.224', 'cmhbuzjrz000s475ue0q2o2xd', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr173u9004z1eq6tn4gw3h3', 'Rondelle frein MB44', NULL, NULL, '2026-01-23 15:24:14.77', '2026-01-23 15:24:14.77', 'cmhbuzjrz000s475ue0q2o2xd', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr20cpy005a1eq6nn5kmtys', 'Roulement E1 17', NULL, NULL, '2026-01-23 15:46:59.302', '2026-01-23 15:46:59.302', 'cmh9bykt8001j47v7g0oej5dw', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr253dx005k1eq65vn7evdy', 'Ecrou HM 44T', NULL, NULL, '2026-01-23 15:50:40.485', '2026-01-23 15:50:40.485', 'cmhbve5h30016475utwgpa32k', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr25xz1005v1eq6i0fib4er', 'Manchon E1 17', NULL, NULL, '2026-01-23 15:51:20.125', '2026-01-23 15:51:20.125', 'cmkr24n3x005j1eq6s9xi6obl', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cmkr0qjw5004s1eq6pen63x7j', 'Arbre E1 17', NULL, NULL, '2026-01-23 15:11:22.422', '2026-01-23 15:55:47.227', 'cmgujpyjf002q4705j6hv1nkk', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cl89d9641d47f52c5385f83d5c', 'test', 'four', 33.97, '2026-01-25 10:48:52', '2026-01-25 10:49:48', 'cl880ba34e5789668dd1c3affa', 'cmko9bmrd005m1e2w81v07kiz', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid, productids) VALUES ('cl280f805cc3e6ff4b8bde95e4', 'testjjj', NULL, NULL, '2026-01-25 11:20:44', '2026-01-25 11:20:44', 'cl880ba34e5789668dd1c3affa', 'cmko9bmrd005m1e2w81v07kiz', '["cmko9bmrd005m1e2w81v07kiz","cmkpp4fb3001i1eq6qq74ul2i"]');
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgrp1ry9001347052qn8q2yo', 'Lame raclette', 'P40S069915', NULL, '2025-10-15 07:53:07.52', '2025-10-15 07:53:07.52', 'cmgrou6670011470586ipgylm', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgrp46ud001i4705nvphpv0f', 'Palier applique', 'X21000923', NULL, '2025-10-15 07:55:00.132', '2025-10-15 07:55:00.132', 'cmgrnxlx5000g47059oyj4yuw', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgrp2sju00144705n8etw7im', 'Bras tendeur SE18', 'X56654', NULL, '2025-10-15 07:53:54.954', '2025-10-15 12:54:18.646', 'cmgrohigo000z4705q8yvpih0', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs08kjb00234705wc5tytxg', 'Cage d''écureuil de tension', 'W57719', NULL, '2025-10-15 13:06:20.278', '2025-10-15 13:06:20.278', 'cmgrzuwkj001u47057u8hej9u', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh3h3c82001f47zbvfmcu17d', 'Auget tôle', NULL, NULL, '2025-10-23 13:43:37.634', '2025-10-23 13:43:37.634', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs0bive00274705zjmiuwzo', 'Rouleau1', 'X24001026', NULL, '2025-10-15 13:08:38.087', '2025-10-15 13:08:38.087', 'cmgroij2f00104705t6y33enk', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs1tfza002m4705mbl0kwok', 'Bavette alimentation', 'P30W07069', NULL, '2025-10-15 13:50:33.766', '2025-10-15 13:50:33.766', 'cmgs1sco0002k47056yq8eyfq', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs1tvrs002n4705gpym7vel', 'Bavette centrage', 'P30W07052', NULL, '2025-10-15 13:50:54.232', '2025-10-15 13:50:54.232', 'cmgs1sco0002k47056yq8eyfq', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgys8mjl001r47ff5f8z85fs', 'Attache rapide 19.05S', 'X10000565', NULL, '2025-10-20 06:56:49.185', '2025-10-20 06:56:49.185', 'cmgum1ih0000347ff7bsldmnv', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgytmhw0002547ffzobsmpaa', 'Moteur éléctrique', 'X50001591', NULL, '2025-10-20 07:35:35.925', '2025-10-20 07:35:35.925', 'cmgytewe0002447ffup09bscr', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgyts8s9003747ffd1h8husf', 'Moteur éléctrique2', 'X50001596', NULL, '2025-10-20 07:40:04.088', '2025-10-20 07:40:04.088', 'cmgytewe0002447ffup09bscr', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz0rm8j004b47ffg2bh2ort', 'Réducteur1', 'X28896', NULL, '2025-10-20 10:55:32.179', '2025-10-20 10:55:32.179', 'cmgz0qu29004a47ffw1bmjr75', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz0se74004o47ffnbbtu66b', 'Réducteur2', 'X15009329', NULL, '2025-10-20 10:56:08.416', '2025-10-20 10:56:08.416', 'cmgz0qu29004a47ffw1bmjr75', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh99o05y001547v7az12sk2n', 'Moteur à flasque', NULL, NULL, '2025-10-27 15:02:21.884', '2025-10-27 15:02:21.884', 'cmgytewe0002447ffup09bscr', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz0w66p004w47ffvj6xcxmo', 'Poulie1', 'X53433', NULL, '2025-10-20 10:59:04.657', '2025-10-20 10:59:26.075', 'cmgz0v9k4004v47ff8apimo50', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz17w9w006u47ffg6db710j', 'Courroie', 'X47067', NULL, '2025-10-20 11:08:11.684', '2025-10-20 11:08:11.684', 'cmgz17bpz006t47ff58i3j1e1', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhaesmf5003v47v7bub04g9p', 'Joint à lèvre', 'J41800-RLX', 44.00, '2025-10-28 10:13:41.607', '2025-10-28 10:13:41.607', 'cmhabzypq003h47v7jyjjxst1', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhbtjqbt0000475ultd24cp0', 'Segment d''arrêt - Circlips', 'S41800-SA2', 37.00, '2025-10-29 09:54:27.209', '2025-10-29 09:54:27.209', 'cmhalh6sa004h47v7y6pnqok2', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz18vw1007947ffr2wg86sa', 'Courroie2', 'X53480', NULL, '2025-10-20 11:08:57.84', '2025-10-20 11:08:57.84', 'cmgz17bpz006t47ff58i3j1e1', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhbv3kj9000t475uzbqpult7', 'Rondelle frein MB20', 'RDLMB20', 7.00, '2025-10-29 10:37:52.437', '2025-10-29 10:37:52.437', 'cmhbuzjrz000s475ue0q2o2xd', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhd56h02002a475uunel89e0', 'Manille', NULL, NULL, '2025-10-30 08:07:50.161', '2025-10-30 08:07:50.161', 'cmhd55caa0029475u1t4vg1i2', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhkr1ulr000147yv73imk2nx', 'Rondelle plate M4 RVS-A2', NULL, NULL, '2025-11-04 15:54:29.296', '2025-11-04 16:49:00.924', 'cmhbuzjrz000s475ue0q2o2xd', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh313676002d47s5li4e6qt9', 'Arbre', NULL, NULL, '2025-10-23 06:15:35.969', '2025-10-23 06:15:35.969', 'cmgujpyjf002q4705j6hv1nkk', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh3jsgfr002n47zbadkkds7r', 'Bavette2', NULL, NULL, '2025-10-23 14:59:08.727', '2025-10-23 14:59:08.727', 'cmgs1sco0002k47056yq8eyfq', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgrp3lhv00194705f1xp8j0m', 'Rouleau', 'X24001025', NULL, '2025-10-15 07:54:32.438', '2025-10-15 07:54:32.438', 'cmgroij2f00104705t6y33enk', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs0nyk7002g4705rteyvw7x', 'Support rouleau inférieur', 'T30S06944', NULL, '2025-10-15 13:18:18.295', '2025-10-15 13:18:18.295', 'cmgs0kd5o002f47053b7n8tw6', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgum5zm0000547ffzg8ofiqr', 'Cage d''écureuil', 'W78517', NULL, '2025-10-17 08:55:43.753', '2025-10-17 08:55:43.753', 'cmgrzuwkj001u47057u8hej9u', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgum9bn4000847fffbazanc5', 'Arbre roue avant', 'H22907', NULL, '2025-10-17 08:58:19.312', '2025-10-17 08:58:19.312', 'cmgujpyjf002q4705j6hv1nkk', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhaf1nsb004347v75uv8gmsi', 'Axe rouleau Promill', NULL, NULL, '2025-10-28 10:20:43.281', '2025-10-28 10:20:43.281', 'cmhaex3ca004247v78ymfpvpd', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhbu3due0007475ung88xfpm', 'Cuvette pour roulement HH 228310', 'C41800-CO2', 498.00, '2025-10-29 10:09:44.122', '2025-10-29 10:09:44.122', 'cmh9bykt8001j47v7g0oej5dw', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgytqtc8002u47ffum90ylo5', 'Moteur éléctrique1', 'X50001593', NULL, '2025-10-20 07:38:57.415', '2025-10-20 07:38:57.415', 'cmgytewe0002447ffup09bscr', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz0xbx5005d47ffjkrafetg', 'Poulie2', 'X53446', NULL, '2025-10-20 10:59:58.743', '2025-10-20 10:59:58.743', 'cmgz0v9k4004v47ff8apimo50', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz1c9wx007h47ffr41untmr', 'Détecteur déport de bande', 'X23100', NULL, '2025-10-20 11:11:35.985', '2025-10-20 11:12:51.466', 'cmgum1wsl000447ffa109dtag', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhbvyr210017475ubrey4eux', 'Ecrou de blocage KM20', 'ECRKM20A', 42.00, '2025-10-29 11:02:07.197', '2025-10-29 11:04:47.071', 'cmhbve5h30016475utwgpa32k', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh4xg7lb000347nkrtvqw3hi', 'Arbre Tapis émotteur', NULL, NULL, '2025-10-24 14:09:18.164', '2025-10-24 14:09:18.164', 'cmgujpyjf002q4705j6hv1nkk', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhaagmno003647v7sfrgsb5v', 'COQUILLE nid d''abeille Promill', 'E41800ASN1P', 574.00, '2025-10-28 08:12:23.603', '2025-10-28 08:12:23.603', 'cmhaa5la2003447v7do7w3s0i', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhaf9o8j004947v7xomube6n', 'Flasque arrière rouleau Promill', 'F07700-001/5759701', 86.71, '2025-10-28 10:26:57.139', '2025-10-28 10:26:57.139', 'cmhaf6jaj004847v7cpq93sq5', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhafarpc004e47v7nqdy97xs', 'Flasque avant rouleau Promill', 'F07700-002/5759601', 115.30, '2025-10-28 10:27:48.288', '2025-10-28 10:27:48.288', 'cmhaf6jaj004847v7cpq93sq5', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhbuapy5000e475utfiwfkcj', 'Cone pour roulement HH228340', 'C41800-CO2', 498.00, '2025-10-29 10:15:26.402', '2025-10-29 10:15:26.402', 'cmh9bykt8001j47v7g0oej5dw', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhd4jjw9001q475u8x4i63jw', 'Graisseur 1/4 rouleaux Promill', NULL, NULL, '2025-10-30 07:50:00.797', '2025-10-30 07:50:00.797', 'cmhd48ipe001p475ul7ejiutq', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhdb5hon002p475uif5ri4vu', 'Douille de serrage', NULL, NULL, '2025-10-30 10:55:02.086', '2025-10-30 10:55:02.086', 'cmhdb4mgx002o475udcnbd71h', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh31ejnw003b47s548rtk8b1', 'Arbre de commande', NULL, NULL, '2025-10-23 06:24:26.634', '2025-11-06 13:36:45.099', 'cmgujpyjf002q4705j6hv1nkk', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgrzvdmo001v47050tvf2z88', 'Cage d''écureuil de pied', 'W78515', NULL, '2025-10-15 12:56:04.801', '2025-10-15 13:05:49.946', 'cmgrzuwkj001u47057u8hej9u', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs14c7a002i4705t1w4qdfx', 'rouleau amortisseur avec axe', 'E1RS07058', NULL, '2025-10-15 13:31:02.469', '2025-10-15 13:31:02.469', 'cmgs13jjp002h4705rjqzz5lh', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgyruhgm000947ffmhhrqdrl', 'Galet avant chariot déverseur', 'H22698', NULL, '2025-10-20 06:45:49.386', '2025-10-20 06:45:49.386', 'cmgs1s4pv002j470567o60oqe', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgyrxcgu000a47ffxvcyyuwm', 'Palier BPF5', 'X21000919', NULL, '2025-10-20 06:48:02.91', '2025-10-20 06:48:02.91', 'cmgrnxlx5000g47059oyj4yuw', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgyrzrbc000h47ffd670wu8j', 'Arbre roue arrière', 'H22908', NULL, '2025-10-20 06:49:55.463', '2025-10-20 06:49:55.463', 'cmgujpyjf002q4705j6hv1nkk', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgys0mgx000i47ffxbftvqt4', 'Galet arrière chariot déverseur', 'H22861', NULL, '2025-10-20 06:50:35.84', '2025-10-20 06:50:35.84', 'cmgs1s4pv002j470567o60oqe', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgysatbl001u47ffu55db8gg', 'Vérin éléctrique', 'X22754', NULL, '2025-10-20 06:58:31.282', '2025-10-20 06:58:31.282', 'cmgulzr7b000247ffpr2vsput', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgytuf06003k47ffdr8lvp13', 'Moteur éléctrique3', 'X50001598', NULL, '2025-10-20 07:41:45.434', '2025-10-20 07:41:45.434', 'cmgytewe0002447ffup09bscr', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz0y2aw005m47ff4zkjczei', 'Poulie3', 'X53450', NULL, '2025-10-20 11:00:32.936', '2025-10-20 11:00:32.936', 'cmgz0v9k4004v47ff8apimo50', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz112hd006e47ffvg37mkoq', 'Moyeu amovible2', 'X11F00653', NULL, '2025-10-20 11:02:53.136', '2025-10-20 11:02:53.136', 'cmgz0zs4m006447ffq5b20ch3', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz1erci007r47ffybtdepul', 'Détecteur déport de bande1', 'X53294', NULL, '2025-10-20 11:13:31.891', '2025-10-20 11:13:31.891', 'cmgum1wsl000447ffa109dtag', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh3eadge001147zbworn1671', 'Bavette 2', NULL, NULL, '2025-10-23 12:25:06.974', '2025-10-23 12:25:06.974', 'cmgs1sco0002k47056yq8eyfq', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh3e95j4000k47zbx53n4tqv', 'Bavette1', NULL, NULL, '2025-10-23 12:24:10.047', '2025-10-23 14:58:45.061', 'cmgs1sco0002k47056yq8eyfq', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgrnzbku000h4705qrj5eujb', 'Tambour de tête', 'H57305', NULL, '2025-10-15 07:23:13.346', '2025-10-15 07:23:13.346', 'cmgrnu6zc000f470565mc8hha', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs07df2001w4705ry79yvbo', 'Cage d''écureuil de pied de tension', 'W58372', NULL, '2025-10-15 13:05:24.397', '2025-10-15 13:05:24.397', 'cmgrzuwkj001u47057u8hej9u', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgs1swl0002l4705gpyg1yyn', 'Galet releveur complet', 'W32440', NULL, '2025-10-15 13:50:08.628', '2025-10-15 13:50:08.628', 'cmgs1s4pv002j470567o60oqe', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgys4a2s001847ffhqhz7zcd', 'Pignon moteur', 'H38143', NULL, '2025-10-20 06:53:26.404', '2025-10-20 06:54:46.835', 'cmgukvztv002s4705kqvqjtvg', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgys6k6b001h47ffuq44ze37', 'Pignon récepteur', 'H47381', NULL, '2025-10-20 06:55:12.803', '2025-10-20 06:55:12.803', 'cmgukvztv002s4705kqvqjtvg', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgys7anf001m47ff0ulcp092', 'Chaîne 19.05S', 'X10000564', NULL, '2025-10-20 06:55:47.115', '2025-10-20 06:56:20.844', 'cmgukxw26002t4705qz4ul929', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgysj6wn002347ffgq3f98dr', 'Détecteur mécanique', 'X60001690', NULL, '2025-10-20 07:05:02.134', '2025-10-20 07:05:02.134', 'cmgum1wsl000447ffa109dtag', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgytx2ul003x47ffhdpurtx5', 'Moteur éléctrique4', 'X50001600', NULL, '2025-10-20 07:43:49.676', '2025-10-20 07:43:49.676', 'cmgytewe0002447ffup09bscr', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz0yt33005v47ffy4p8d28z', 'Poulie4', 'X41745', NULL, '2025-10-20 11:01:07.646', '2025-10-20 11:01:07.646', 'cmgz0v9k4004v47ff8apimo50', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz10g67006547ffj28sqequ', 'Moyeu amovible1', 'X43888', NULL, '2025-10-20 11:02:24.223', '2025-10-20 11:02:24.223', 'cmgz0zs4m006447ffq5b20ch3', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz11p1k006j47ffhjqgrnkp', 'Moyeu amovible3', 'X41739', NULL, '2025-10-20 11:03:22.375', '2025-10-20 11:03:22.375', 'cmgz0zs4m006447ffq5b20ch3', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz128lz006o47ffwfgtag7e', 'Moyeu amovible4', 'X11F00765', NULL, '2025-10-20 11:03:47.735', '2025-10-20 11:03:47.735', 'cmgz0zs4m006447ffq5b20ch3', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgz516t7009n47fft3nfyt34', 'Tambour de tête1', 'H138830', NULL, '2025-10-20 12:54:57.211', '2025-10-20 12:54:57.211', 'cmgrnu6zc000f470565mc8hha', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhae5b7u003i47v7vb4qi81n', 'Joint torique R41', 'JTR41', 2.00, '2025-10-28 09:55:33.999', '2025-10-28 09:55:33.999', 'cmhabzypq003h47v7jyjjxst1', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhallrb1004i47v7855gvfpe', 'Segment d''étanchéïté', 'S41800-SA2', 37.00, '2025-10-28 13:24:18.658', '2025-10-28 13:24:56.572', 'cmhalh6sa004h47v7y6pnqok2', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgys1osr000j47fftblpdpu2', 'Motoréducteur frein', 'X33959', NULL, '2025-10-20 06:51:25.485', '2025-10-20 06:52:25.744', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmgys3ugw001147ffq33udxaw', 'Motoréducteur frein.', 'X108273', NULL, '2025-10-20 06:53:06.176', '2025-10-20 06:53:06.176', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhbuq97o000l475uf73oiot0', 'Entretoise de roulements', 'E41800-000', 67.00, '2025-10-29 10:27:31.208', '2025-10-29 10:27:31.208', 'cmh9bykt8001j47v7g0oej5dw', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhd4s3u80020475uasnc0mqj', 'Crochet de levage', NULL, NULL, '2025-10-30 07:56:39.92', '2025-10-30 07:56:39.92', 'cmhd4r5bg001z475u0f4tm9yy', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhd4syhb0025475u4sv87dyf', 'Crochet de levage avec manille', NULL, NULL, '2025-10-30 07:57:19.63', '2025-10-30 07:57:19.63', 'cmhd4r5bg001z475u0f4tm9yy', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhdiuklf002u475uadws3h04', 'Courroie XPC', NULL, NULL, '2025-10-30 14:30:29.569', '2025-10-30 14:30:29.569', 'cmgz17bpz006t47ff58i3j1e1', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmh99m3e6000847v75h2m7czn', 'Réducteur emo', NULL, NULL, '2025-10-27 15:00:52.781', '2025-10-27 15:00:52.781', NULL, NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmizudzfy00021e2w2mtd9zv8', 'lame de godet 82', NULL, 192.00, '2025-12-10 10:04:09.262', '2025-12-10 10:05:37.177', 'cmizu3st800001e2waysco15j', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmizv8nzu00081e2wen6ur31b', 'Tapis', 'PF0165295', 3730.67, '2025-12-10 10:28:00.762', '2025-12-10 10:39:59.943', 'cmizup8cv00061e2w2rulkxsn', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmjcixqq300141e2wqkvz0cx6', 'VIS (lame de godet) 82 M14 60mm tête fraisé', NULL, NULL, '2025-12-19 07:04:35.979', '2025-12-19 07:04:35.979', 'cmj025vi7000z1e2wyn3x6msv', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmjcpdwqs00161e2wu4juy4u2', 'Ecrou Ø 14', NULL, NULL, '2025-12-19 10:05:07.973', '2025-12-19 10:05:07.973', 'cmhbve5h30016475utwgpa32k', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmhdattcv002f475ugw514oj3', 'Poulie 8', NULL, NULL, '2025-10-30 10:45:57.343', '2026-01-14 08:04:14.29', 'cmgz0v9k4004v47ff8apimo50', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdqrkez001o1e2wtslqeazi', 'Carter presse', NULL, NULL, '2026-01-14 08:11:13.308', '2026-01-14 08:11:13.308', 'cmkdqqh8w001n1e2wzxkamd1m', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdqw1tq00231e2wxou4eu8z', 'Ecrou HM12', NULL, NULL, '2026-01-14 08:14:42.494', '2026-01-14 08:14:42.494', 'cmhbve5h30016475utwgpa32k', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdrc535002e1e2wjuucdweq', 'Moteur entrainement Presse Promill', NULL, NULL, '2026-01-14 08:27:13.217', '2026-01-14 08:27:13.217', 'cmgytewe0002447ffup09bscr', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdsbrz1002t1e2wqemldbr6', 'Vis HM 14x100', NULL, NULL, '2026-01-14 08:54:55.838', '2026-01-14 08:54:55.838', 'cmkdqtcpv001r1e2wptehmkxi', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdsdqjh00301e2wu2g4ljg2', 'Rondelle plate M24', NULL, NULL, '2026-01-14 08:56:27.294', '2026-01-14 08:56:27.294', 'cmhbuzjrz000s475ue0q2o2xd', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdsp16u003b1e2wetp787yf', 'Rondelle Grower W24', NULL, NULL, '2026-01-14 09:05:14.31', '2026-01-14 09:05:14.31', 'cmhbuzjrz000s475ue0q2o2xd', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdsrew1003m1e2wcuky5m77', 'Ecrou HM24', NULL, NULL, '2026-01-14 09:07:05.377', '2026-01-14 09:07:05.377', 'cmhbve5h30016475utwgpa32k', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdqun4a001s1e2wx123zdy1', 'Vis HM 12x35', NULL, NULL, '2026-01-14 08:13:36.778', '2026-01-14 09:15:22.132', 'cmkdqtcpv001r1e2wptehmkxi', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdt82ma003x1e2w9gkgwybf', 'Vis HM 12x30', NULL, NULL, '2026-01-14 09:20:02.626', '2026-01-14 09:20:02.626', 'cmkdqtcpv001r1e2wptehmkxi', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkdtdh5p00441e2w0g6ye4v5', 'Rondelle Grower W12', NULL, NULL, '2026-01-14 09:24:14.75', '2026-01-14 09:24:14.75', 'cmhbuzjrz000s475ue0q2o2xd', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmke1nimf004f1e2wsurzoeet', 'Vis HM 8x16', NULL, NULL, '2026-01-14 13:16:00.135', '2026-01-14 13:16:00.135', 'cmkdqtcpv001r1e2wptehmkxi', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmke3hgbc004o1e2w02his9k3', 'Vis HM 24x100', NULL, NULL, '2026-01-14 14:07:16.44', '2026-01-14 14:07:16.44', 'cmkdqtcpv001r1e2wptehmkxi', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkp7xsz1006b1e2whn582enn', 'Arbre principal Presse Promill', 'APR 80 101101', NULL, '2026-01-22 08:57:25.741', '2026-01-22 08:57:25.741', 'cmgujpyjf002q4705j6hv1nkk', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkp80cfi006k1e2wha1v14vv', 'Joint Viton', NULL, NULL, '2026-01-22 08:59:24.27', '2026-01-22 08:59:24.27', 'cmhabzypq003h47v7jyjjxst1', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkp84bs0006t1e2wkokejtyn', 'Roulement à rouleaux cylindriques', NULL, NULL, '2026-01-22 09:02:30.048', '2026-01-22 09:02:30.048', 'cmh9bykt8001j47v7g0oej5dw', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkp8mbo900751e2w2746k94i', 'Circlips E260', '390 01126000', NULL, '2026-01-22 09:16:29.721', '2026-01-22 09:16:29.721', 'cmhalh6sa004h47v7y6pnqok2', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkpkaj4400091eq6p6accs62', 'Moyeu central', 'APR 80 101103', NULL, '2026-01-22 14:43:14.884', '2026-01-22 14:43:14.884', 'cmgz0zs4m006447ffq5b20ch3', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkpkwfdh000g1eq6hpqdbqat', 'Roulement à rotule sur rouleaux', '320 41 220001', NULL, '2026-01-22 15:00:16.469', '2026-01-22 15:00:16.469', 'cmh9bykt8001j47v7g0oej5dw', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkpnw6er000s1eq6k57kcnl8', 'Vis CHC M14x40', NULL, NULL, '2026-01-22 16:24:03.699', '2026-01-22 16:24:03.699', 'cmkdqtcpv001r1e2wptehmkxi', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkpo4gxz00121eq6o0ahmizg', 'Joint', '370 20 280000', NULL, '2026-01-22 16:30:30.6', '2026-01-22 16:30:30.6', 'cmhabzypq003h47v7jyjjxst1', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr0nq1a004e1eq6v6ubxlfl', 'Palier tête E1 17', 'SNU516613', NULL, '2026-01-23 15:09:10.414', '2026-01-23 15:09:44.182', 'cmgrnxlx5000g47059oyj4yuw', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr0owbv004l1eq6pzlatzlr', 'Vis HM 14x50', NULL, NULL, '2026-01-23 15:10:05.228', '2026-01-23 15:10:05.228', 'cmkdqtcpv001r1e2wptehmkxi', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkqzl1oa002v1eq6erkt5544', 'BANDE E1 17', NULL, NULL, '2026-01-23 14:39:05.914', '2026-01-23 14:48:36.329', 'cmknus46z00551e2wf7zy706v', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr0fwuo003o1eq69b1idlpw', 'Entretoise porte 2 joints', 'APR 80 101108', NULL, '2026-01-23 15:03:06', '2026-01-23 15:03:06', 'cmhabzypq003h47v7jyjjxst1', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr0koqb003y1eq6a8cx8poy', 'Flasque porte 2 joints', 'APR 25 0304..', NULL, '2026-01-23 15:06:48.755', '2026-01-23 15:06:48.755', 'cmhaf6jaj004847v7cpq93sq5', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr0nmsv00431eq6rcnozdes', 'Rondelle Grower W14', NULL, NULL, '2026-01-23 15:09:06.224', '2026-01-23 15:09:06.224', 'cmhbuzjrz000s475ue0q2o2xd', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr173u9004z1eq6tn4gw3h3', 'Rondelle frein MB44', NULL, NULL, '2026-01-23 15:24:14.77', '2026-01-23 15:24:14.77', 'cmhbuzjrz000s475ue0q2o2xd', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr20cpy005a1eq6nn5kmtys', 'Roulement E1 17', NULL, NULL, '2026-01-23 15:46:59.302', '2026-01-23 15:46:59.302', 'cmh9bykt8001j47v7g0oej5dw', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr253dx005k1eq65vn7evdy', 'Ecrou HM 44T', NULL, NULL, '2026-01-23 15:50:40.485', '2026-01-23 15:50:40.485', 'cmhbve5h30016475utwgpa32k', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr25xz1005v1eq6i0fib4er', 'Manchon E1 17', NULL, NULL, '2026-01-23 15:51:20.125', '2026-01-23 15:51:20.125', 'cmkr24n3x005j1eq6s9xi6obl', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cmkr0qjw5004s1eq6pen63x7j', 'Arbre E1 17', NULL, NULL, '2026-01-23 15:11:22.422', '2026-01-23 15:55:47.227', 'cmgujpyjf002q4705j6hv1nkk', NULL);
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cl89d9641d47f52c5385f83d5c', 'test', 'four', 33.97, '2026-01-25 10:48:52', '2026-01-25 10:49:48', 'cl880ba34e5789668dd1c3affa', 'cmko9bmrd005m1e2w81v07kiz');
|
||||
INSERT INTO public.pieces (id, name, reference, prix, createdat, updatedat, typepieceid, productid) VALUES ('cl280f805cc3e6ff4b8bde95e4', 'testjjj', NULL, NULL, '2026-01-25 11:20:44', '2026-01-25 11:20:44', 'cl880ba34e5789668dd1c3affa', 'cmko9bmrd005m1e2w81v07kiz');
|
||||
|
||||
|
||||
--
|
||||
|
||||
26
migrations/Version20260309150000.php
Normal file
26
migrations/Version20260309150000.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260309150000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add quantity column to machine_piece_links table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE machine_piece_links ADD COLUMN IF NOT EXISTS quantity INTEGER NOT NULL DEFAULT 1');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE machine_piece_links DROP COLUMN IF EXISTS quantity');
|
||||
}
|
||||
}
|
||||
222
migrations/Version20260312170000.php
Normal file
222
migrations/Version20260312170000.php
Normal file
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260312170000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create skeleton requirement tables (IF NOT EXISTS) and migrate JSON data from ModelType skeleton columns';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// ── Table creation (idempotent) ──────────────────────────────────────
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS skeleton_piece_requirements (
|
||||
id VARCHAR(36) NOT NULL,
|
||||
"modeltypeid" VARCHAR(36) NOT NULL,
|
||||
"typepieceid" VARCHAR(36) NOT NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
"createdat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
"updatedat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS skeleton_product_requirements (
|
||||
id VARCHAR(36) NOT NULL,
|
||||
"modeltypeid" VARCHAR(36) NOT NULL,
|
||||
"typeproductid" VARCHAR(36) NOT NULL,
|
||||
"familycode" VARCHAR(255) DEFAULT NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
"createdat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
"updatedat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS skeleton_subcomponent_requirements (
|
||||
id VARCHAR(36) NOT NULL,
|
||||
"modeltypeid" VARCHAR(36) NOT NULL,
|
||||
alias VARCHAR(255) NOT NULL,
|
||||
"familycode" VARCHAR(255) NOT NULL,
|
||||
"typecomposantid" VARCHAR(36) DEFAULT NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
"createdat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
"updatedat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
SQL);
|
||||
|
||||
// ── Indexes (idempotent) ─────────────────────────────────────────────
|
||||
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_skel_piece_req_model ON skeleton_piece_requirements("modeltypeid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_skel_piece_req_type ON skeleton_piece_requirements("typepieceid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_skel_prod_req_model ON skeleton_product_requirements("modeltypeid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_skel_prod_req_type ON skeleton_product_requirements("typeproductid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_skel_sub_req_model ON skeleton_subcomponent_requirements("modeltypeid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_skel_sub_req_typecomp ON skeleton_subcomponent_requirements("typecomposantid")');
|
||||
|
||||
// ── Foreign keys (idempotent via DO $$ block) ────────────────────────
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_skel_piece_model') THEN
|
||||
ALTER TABLE skeleton_piece_requirements
|
||||
ADD CONSTRAINT fk_skel_piece_model
|
||||
FOREIGN KEY ("modeltypeid") REFERENCES model_types (id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_skel_piece_type') THEN
|
||||
ALTER TABLE skeleton_piece_requirements
|
||||
ADD CONSTRAINT fk_skel_piece_type
|
||||
FOREIGN KEY ("typepieceid") REFERENCES model_types (id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_skel_prod_model') THEN
|
||||
ALTER TABLE skeleton_product_requirements
|
||||
ADD CONSTRAINT fk_skel_prod_model
|
||||
FOREIGN KEY ("modeltypeid") REFERENCES model_types (id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_skel_prod_type') THEN
|
||||
ALTER TABLE skeleton_product_requirements
|
||||
ADD CONSTRAINT fk_skel_prod_type
|
||||
FOREIGN KEY ("typeproductid") REFERENCES model_types (id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_skel_sub_model') THEN
|
||||
ALTER TABLE skeleton_subcomponent_requirements
|
||||
ADD CONSTRAINT fk_skel_sub_model
|
||||
FOREIGN KEY ("modeltypeid") REFERENCES model_types (id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_skel_sub_typecomp') THEN
|
||||
ALTER TABLE skeleton_subcomponent_requirements
|
||||
ADD CONSTRAINT fk_skel_sub_typecomp
|
||||
FOREIGN KEY ("typecomposantid") REFERENCES model_types (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
// ── Data migration: componentSkeleton.pieces → skeleton_piece_requirements ──
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
INSERT INTO skeleton_piece_requirements (id, "modeltypeid", "typepieceid", position, "createdat", "updatedat")
|
||||
SELECT
|
||||
'cl' || encode(gen_random_bytes(12), 'hex'),
|
||||
mt.id,
|
||||
(piece->>'typePieceId'),
|
||||
(ordinality - 1)::int,
|
||||
NOW(), NOW()
|
||||
FROM model_types mt,
|
||||
LATERAL jsonb_array_elements(mt.componentskeleton::jsonb->'pieces') WITH ORDINALITY AS t(piece, ordinality)
|
||||
WHERE mt.category = 'COMPONENT'
|
||||
AND mt.componentskeleton IS NOT NULL
|
||||
AND mt.componentskeleton::jsonb->'pieces' IS NOT NULL
|
||||
AND jsonb_array_length(mt.componentskeleton::jsonb->'pieces') > 0
|
||||
AND NOT EXISTS (SELECT 1 FROM skeleton_piece_requirements spr WHERE spr."modeltypeid" = mt.id)
|
||||
AND EXISTS (SELECT 1 FROM model_types ref WHERE ref.id = (piece->>'typePieceId'))
|
||||
SQL);
|
||||
|
||||
// ── Data migration: componentSkeleton.products → skeleton_product_requirements ──
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
INSERT INTO skeleton_product_requirements (id, "modeltypeid", "typeproductid", "familycode", position, "createdat", "updatedat")
|
||||
SELECT
|
||||
'cl' || encode(gen_random_bytes(12), 'hex'),
|
||||
mt.id,
|
||||
(product->>'typeProductId'),
|
||||
(product->>'familyCode'),
|
||||
(ordinality - 1)::int,
|
||||
NOW(), NOW()
|
||||
FROM model_types mt,
|
||||
LATERAL jsonb_array_elements(mt.componentskeleton::jsonb->'products') WITH ORDINALITY AS t(product, ordinality)
|
||||
WHERE mt.category = 'COMPONENT'
|
||||
AND mt.componentskeleton IS NOT NULL
|
||||
AND mt.componentskeleton::jsonb->'products' IS NOT NULL
|
||||
AND jsonb_array_length(mt.componentskeleton::jsonb->'products') > 0
|
||||
AND NOT EXISTS (SELECT 1 FROM skeleton_product_requirements spr WHERE spr."modeltypeid" = mt.id)
|
||||
AND EXISTS (SELECT 1 FROM model_types ref WHERE ref.id = (product->>'typeProductId'))
|
||||
SQL);
|
||||
|
||||
// ── Data migration: pieceSkeleton.products → skeleton_product_requirements ──
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
INSERT INTO skeleton_product_requirements (id, "modeltypeid", "typeproductid", "familycode", position, "createdat", "updatedat")
|
||||
SELECT
|
||||
'cl' || encode(gen_random_bytes(12), 'hex'),
|
||||
mt.id,
|
||||
(product->>'typeProductId'),
|
||||
(product->>'familyCode'),
|
||||
(ordinality - 1)::int,
|
||||
NOW(), NOW()
|
||||
FROM model_types mt,
|
||||
LATERAL jsonb_array_elements(mt.pieceskeleton::jsonb->'products') WITH ORDINALITY AS t(product, ordinality)
|
||||
WHERE mt.category = 'PIECE'
|
||||
AND mt.pieceskeleton IS NOT NULL
|
||||
AND mt.pieceskeleton::jsonb->'products' IS NOT NULL
|
||||
AND jsonb_array_length(mt.pieceskeleton::jsonb->'products') > 0
|
||||
AND NOT EXISTS (SELECT 1 FROM skeleton_product_requirements spr WHERE spr."modeltypeid" = mt.id)
|
||||
AND EXISTS (SELECT 1 FROM model_types ref WHERE ref.id = (product->>'typeProductId'))
|
||||
SQL);
|
||||
|
||||
// ── Data migration: componentSkeleton.subcomponents → skeleton_subcomponent_requirements ──
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
INSERT INTO skeleton_subcomponent_requirements (id, "modeltypeid", alias, "familycode", "typecomposantid", position, "createdat", "updatedat")
|
||||
SELECT
|
||||
'cl' || encode(gen_random_bytes(12), 'hex'),
|
||||
mt.id,
|
||||
COALESCE(sub->>'alias', ''),
|
||||
COALESCE(sub->>'familyCode', ''),
|
||||
NULLIF(sub->>'typeComposantId', ''),
|
||||
(ordinality - 1)::int,
|
||||
NOW(), NOW()
|
||||
FROM model_types mt,
|
||||
LATERAL jsonb_array_elements(mt.componentskeleton::jsonb->'subcomponents') WITH ORDINALITY AS t(sub, ordinality)
|
||||
WHERE mt.category = 'COMPONENT'
|
||||
AND mt.componentskeleton IS NOT NULL
|
||||
AND mt.componentskeleton::jsonb->'subcomponents' IS NOT NULL
|
||||
AND jsonb_array_length(mt.componentskeleton::jsonb->'subcomponents') > 0
|
||||
AND NOT EXISTS (SELECT 1 FROM skeleton_subcomponent_requirements ssr WHERE ssr."modeltypeid" = mt.id)
|
||||
AND (NULLIF(sub->>'typeComposantId', '') IS NULL OR EXISTS (SELECT 1 FROM model_types ref WHERE ref.id = (sub->>'typeComposantId')))
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE IF EXISTS skeleton_subcomponent_requirements');
|
||||
$this->addSql('DROP TABLE IF EXISTS skeleton_product_requirements');
|
||||
$this->addSql('DROP TABLE IF EXISTS skeleton_piece_requirements');
|
||||
}
|
||||
}
|
||||
47
migrations/Version20260312171810.php
Normal file
47
migrations/Version20260312171810.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260312171810 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create piece_products join table and migrate data from Piece.productIds JSON column';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE IF NOT EXISTS piece_products (piece_id VARCHAR(36) NOT NULL, product_id VARCHAR(36) NOT NULL, PRIMARY KEY (piece_id, product_id))');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS IDX_87C835B5C40FCFA8 ON piece_products (piece_id)');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS IDX_87C835B54584665A ON piece_products (product_id)');
|
||||
$this->addSql('ALTER TABLE piece_products DROP CONSTRAINT IF EXISTS FK_87C835B5C40FCFA8');
|
||||
$this->addSql('ALTER TABLE piece_products ADD CONSTRAINT FK_87C835B5C40FCFA8 FOREIGN KEY (piece_id) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||
$this->addSql('ALTER TABLE piece_products DROP CONSTRAINT IF EXISTS FK_87C835B54584665A');
|
||||
$this->addSql('ALTER TABLE piece_products ADD CONSTRAINT FK_87C835B54584665A FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||
|
||||
// Migrate Piece.productIds JSON array → piece_products join table
|
||||
$this->addSql(<<<'SQL'
|
||||
INSERT INTO piece_products (piece_id, product_id)
|
||||
SELECT DISTINCT p.id, pid.value
|
||||
FROM pieces p,
|
||||
LATERAL jsonb_array_elements_text(p.productids::jsonb) AS pid(value)
|
||||
WHERE p.productids IS NOT NULL
|
||||
AND p.productids::jsonb != '[]'::jsonb
|
||||
AND jsonb_array_length(p.productids::jsonb) > 0
|
||||
AND EXISTS (SELECT 1 FROM products pr WHERE pr.id = pid.value)
|
||||
AND NOT EXISTS (SELECT 1 FROM piece_products pp WHERE pp.piece_id = p.id AND pp.product_id = pid.value)
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE piece_products DROP CONSTRAINT IF EXISTS FK_87C835B5C40FCFA8');
|
||||
$this->addSql('ALTER TABLE piece_products DROP CONSTRAINT IF EXISTS FK_87C835B54584665A');
|
||||
$this->addSql('DROP TABLE IF EXISTS piece_products');
|
||||
}
|
||||
}
|
||||
35
migrations/Version20260312180000.php
Normal file
35
migrations/Version20260312180000.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Drop skeleton JSON columns from model_types — data now lives in
|
||||
* skeleton_piece_requirements, skeleton_product_requirements,
|
||||
* skeleton_subcomponent_requirements and custom_fields tables.
|
||||
*/
|
||||
final class Version20260312180000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Drop componentSkeleton, pieceSkeleton, productSkeleton JSON columns from model_types';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE model_types DROP COLUMN IF EXISTS componentskeleton');
|
||||
$this->addSql('ALTER TABLE model_types DROP COLUMN IF EXISTS pieceskeleton');
|
||||
$this->addSql('ALTER TABLE model_types DROP COLUMN IF EXISTS productskeleton');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE model_types ADD COLUMN IF NOT EXISTS componentskeleton JSON DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE model_types ADD COLUMN IF NOT EXISTS pieceskeleton JSON DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE model_types ADD COLUMN IF NOT EXISTS productskeleton JSON DEFAULT NULL');
|
||||
}
|
||||
}
|
||||
248
migrations/Version20260312190000.php
Normal file
248
migrations/Version20260312190000.php
Normal file
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Create composant slot tables and migrate existing JSON data from composant.structure.
|
||||
*/
|
||||
final class Version20260312190000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create composant_piece_slots, composant_subcomponent_slots, composant_product_slots tables and migrate data from composant.structure JSON';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// ── Table creation (idempotent) ──────────────────────────────────────
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS composant_piece_slots (
|
||||
id VARCHAR(36) NOT NULL,
|
||||
"composantid" VARCHAR(36) NOT NULL,
|
||||
"typepieceid" VARCHAR(36) DEFAULT NULL,
|
||||
"selectedpieceid" VARCHAR(36) DEFAULT NULL,
|
||||
quantity INT NOT NULL DEFAULT 1,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
"createdat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
"updatedat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS composant_subcomponent_slots (
|
||||
id VARCHAR(36) NOT NULL,
|
||||
"composantid" VARCHAR(36) NOT NULL,
|
||||
alias VARCHAR(255) DEFAULT NULL,
|
||||
"familycode" VARCHAR(255) DEFAULT NULL,
|
||||
"typecomposantid" VARCHAR(36) DEFAULT NULL,
|
||||
"selectedcomposantid" VARCHAR(36) DEFAULT NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
"createdat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
"updatedat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS composant_product_slots (
|
||||
id VARCHAR(36) NOT NULL,
|
||||
"composantid" VARCHAR(36) NOT NULL,
|
||||
"typeproductid" VARCHAR(36) DEFAULT NULL,
|
||||
"selectedproductid" VARCHAR(36) DEFAULT NULL,
|
||||
"familycode" VARCHAR(255) DEFAULT NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
"createdat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
"updatedat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
SQL);
|
||||
|
||||
// ── Indexes (idempotent) ─────────────────────────────────────────────
|
||||
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_piece_slot_composant ON composant_piece_slots("composantid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_piece_slot_piece ON composant_piece_slots("selectedpieceid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_piece_slot_type ON composant_piece_slots("typepieceid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_sub_slot_composant ON composant_subcomponent_slots("composantid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_sub_slot_typecomp ON composant_subcomponent_slots("typecomposantid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_sub_slot_selected ON composant_subcomponent_slots("selectedcomposantid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_prod_slot_composant ON composant_product_slots("composantid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_prod_slot_type ON composant_product_slots("typeproductid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_prod_slot_selected ON composant_product_slots("selectedproductid")');
|
||||
|
||||
// ── Foreign keys (idempotent via DO $$ block) ────────────────────────
|
||||
|
||||
// composant_piece_slots FKs
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_piece_slot_composant') THEN
|
||||
ALTER TABLE composant_piece_slots
|
||||
ADD CONSTRAINT fk_comp_piece_slot_composant
|
||||
FOREIGN KEY ("composantid") REFERENCES composants (id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_piece_slot_type') THEN
|
||||
ALTER TABLE composant_piece_slots
|
||||
ADD CONSTRAINT fk_comp_piece_slot_type
|
||||
FOREIGN KEY ("typepieceid") REFERENCES model_types (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_piece_slot_piece') THEN
|
||||
ALTER TABLE composant_piece_slots
|
||||
ADD CONSTRAINT fk_comp_piece_slot_piece
|
||||
FOREIGN KEY ("selectedpieceid") REFERENCES pieces (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
// composant_subcomponent_slots FKs
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_sub_slot_composant') THEN
|
||||
ALTER TABLE composant_subcomponent_slots
|
||||
ADD CONSTRAINT fk_comp_sub_slot_composant
|
||||
FOREIGN KEY ("composantid") REFERENCES composants (id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_sub_slot_typecomp') THEN
|
||||
ALTER TABLE composant_subcomponent_slots
|
||||
ADD CONSTRAINT fk_comp_sub_slot_typecomp
|
||||
FOREIGN KEY ("typecomposantid") REFERENCES model_types (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_sub_slot_selected') THEN
|
||||
ALTER TABLE composant_subcomponent_slots
|
||||
ADD CONSTRAINT fk_comp_sub_slot_selected
|
||||
FOREIGN KEY ("selectedcomposantid") REFERENCES composants (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
// composant_product_slots FKs
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_prod_slot_composant') THEN
|
||||
ALTER TABLE composant_product_slots
|
||||
ADD CONSTRAINT fk_comp_prod_slot_composant
|
||||
FOREIGN KEY ("composantid") REFERENCES composants (id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_prod_slot_type') THEN
|
||||
ALTER TABLE composant_product_slots
|
||||
ADD CONSTRAINT fk_comp_prod_slot_type
|
||||
FOREIGN KEY ("typeproductid") REFERENCES model_types (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_prod_slot_selected') THEN
|
||||
ALTER TABLE composant_product_slots
|
||||
ADD CONSTRAINT fk_comp_prod_slot_selected
|
||||
FOREIGN KEY ("selectedproductid") REFERENCES products (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
// ── Data migration: composant.structure.pieces → composant_piece_slots ──
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
INSERT INTO composant_piece_slots (id, "composantid", "typepieceid", "selectedpieceid", quantity, position, "createdat", "updatedat")
|
||||
SELECT
|
||||
'cl' || encode(gen_random_bytes(12), 'hex'),
|
||||
c.id,
|
||||
NULLIF(piece->'definition'->>'typePieceId', ''),
|
||||
NULLIF(piece->>'selectedPieceId', ''),
|
||||
1,
|
||||
(ordinality - 1)::int,
|
||||
NOW(), NOW()
|
||||
FROM composants c,
|
||||
LATERAL jsonb_array_elements(c.structure::jsonb->'pieces') WITH ORDINALITY AS t(piece, ordinality)
|
||||
WHERE c.structure IS NOT NULL
|
||||
AND (c.structure::jsonb->'pieces') IS NOT NULL
|
||||
AND jsonb_array_length(c.structure::jsonb->'pieces') > 0
|
||||
AND NOT EXISTS (SELECT 1 FROM composant_piece_slots cps WHERE cps."composantid" = c.id)
|
||||
AND (NULLIF(piece->'definition'->>'typePieceId', '') IS NULL OR EXISTS (SELECT 1 FROM model_types mt WHERE mt.id = piece->'definition'->>'typePieceId'))
|
||||
AND (NULLIF(piece->>'selectedPieceId', '') IS NULL OR EXISTS (SELECT 1 FROM pieces p WHERE p.id = piece->>'selectedPieceId'))
|
||||
SQL);
|
||||
|
||||
// ── Data migration: composant.structure.subcomponents → composant_subcomponent_slots ──
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
INSERT INTO composant_subcomponent_slots (id, "composantid", alias, "familycode", "typecomposantid", "selectedcomposantid", position, "createdat", "updatedat")
|
||||
SELECT
|
||||
'cl' || encode(gen_random_bytes(12), 'hex'),
|
||||
c.id,
|
||||
COALESCE(sub->'definition'->>'alias', ''),
|
||||
COALESCE(sub->'definition'->>'familyCode', ''),
|
||||
NULLIF(sub->'definition'->>'typeComposantId', ''),
|
||||
NULLIF(sub->>'selectedComponentId', ''),
|
||||
(ordinality - 1)::int,
|
||||
NOW(), NOW()
|
||||
FROM composants c,
|
||||
LATERAL jsonb_array_elements(c.structure::jsonb->'subcomponents') WITH ORDINALITY AS t(sub, ordinality)
|
||||
WHERE c.structure IS NOT NULL
|
||||
AND (c.structure::jsonb->'subcomponents') IS NOT NULL
|
||||
AND jsonb_array_length(c.structure::jsonb->'subcomponents') > 0
|
||||
AND NOT EXISTS (SELECT 1 FROM composant_subcomponent_slots css WHERE css."composantid" = c.id)
|
||||
AND (NULLIF(sub->'definition'->>'typeComposantId', '') IS NULL OR EXISTS (SELECT 1 FROM model_types mt WHERE mt.id = sub->'definition'->>'typeComposantId'))
|
||||
AND (NULLIF(sub->>'selectedComponentId', '') IS NULL OR EXISTS (SELECT 1 FROM composants sc WHERE sc.id = sub->>'selectedComponentId'))
|
||||
SQL);
|
||||
|
||||
// ── Data migration: composant.structure.products → composant_product_slots ──
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
INSERT INTO composant_product_slots (id, "composantid", "typeproductid", "selectedproductid", "familycode", position, "createdat", "updatedat")
|
||||
SELECT
|
||||
'cl' || encode(gen_random_bytes(12), 'hex'),
|
||||
c.id,
|
||||
NULLIF(prod->'definition'->>'typeProductId', ''),
|
||||
NULLIF(prod->>'selectedProductId', ''),
|
||||
prod->'definition'->>'familyCode',
|
||||
(ordinality - 1)::int,
|
||||
NOW(), NOW()
|
||||
FROM composants c,
|
||||
LATERAL jsonb_array_elements(c.structure::jsonb->'products') WITH ORDINALITY AS t(prod, ordinality)
|
||||
WHERE c.structure IS NOT NULL
|
||||
AND (c.structure::jsonb->'products') IS NOT NULL
|
||||
AND jsonb_array_length(c.structure::jsonb->'products') > 0
|
||||
AND NOT EXISTS (SELECT 1 FROM composant_product_slots cps WHERE cps."composantid" = c.id)
|
||||
AND (NULLIF(prod->'definition'->>'typeProductId', '') IS NULL OR EXISTS (SELECT 1 FROM model_types mt WHERE mt.id = prod->'definition'->>'typeProductId'))
|
||||
AND (NULLIF(prod->>'selectedProductId', '') IS NULL OR EXISTS (SELECT 1 FROM products p WHERE p.id = prod->>'selectedProductId'))
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE IF EXISTS composant_product_slots');
|
||||
$this->addSql('DROP TABLE IF EXISTS composant_subcomponent_slots');
|
||||
$this->addSql('DROP TABLE IF EXISTS composant_piece_slots');
|
||||
}
|
||||
}
|
||||
30
migrations/Version20260312200000.php
Normal file
30
migrations/Version20260312200000.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Drop the legacy productIds JSON column from pieces table.
|
||||
* Data has been migrated to the piece_products join table.
|
||||
*/
|
||||
final class Version20260312200000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Drop legacy productIds JSON column from pieces table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE pieces DROP COLUMN IF EXISTS productids');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE pieces ADD COLUMN productids JSON DEFAULT NULL');
|
||||
}
|
||||
}
|
||||
30
migrations/Version20260312210000.php
Normal file
30
migrations/Version20260312210000.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Drop the legacy structure JSON column from composants table.
|
||||
* Data has been migrated to composant_piece_slots, composant_subcomponent_slots, composant_product_slots tables.
|
||||
*/
|
||||
final class Version20260312210000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Drop legacy structure JSON column from composants table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE composants DROP COLUMN IF EXISTS structure');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE composants ADD COLUMN structure JSON DEFAULT NULL');
|
||||
}
|
||||
}
|
||||
118
migrations/Version20260313124029.php
Normal file
118
migrations/Version20260313124029.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Create piece_product_slots table (mirroring composant_product_slots)
|
||||
* and add version columns to composants, pieces, products.
|
||||
*/
|
||||
final class Version20260313124029 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create piece_product_slots table, add version columns to composants/pieces/products, migrate piece_products data';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// ── Create piece_product_slots table (idempotent) ─────────────────────
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS piece_product_slots (
|
||||
id VARCHAR(36) NOT NULL,
|
||||
"pieceid" VARCHAR(36) NOT NULL,
|
||||
"typeproductid" VARCHAR(36) DEFAULT NULL,
|
||||
"selectedproductid" VARCHAR(36) DEFAULT NULL,
|
||||
"familycode" VARCHAR(255) DEFAULT NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
"createdat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
"updatedat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
SQL);
|
||||
|
||||
// ── Indexes (idempotent) ──────────────────────────────────────────────
|
||||
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_piece_prod_slot_piece ON piece_product_slots ("pieceid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_piece_prod_slot_type ON piece_product_slots ("typeproductid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_piece_prod_slot_selected ON piece_product_slots ("selectedproductid")');
|
||||
$this->addSql('CREATE INDEX IF NOT EXISTS idx_piece_product_slots_piece_pos ON piece_product_slots ("pieceid", position)');
|
||||
|
||||
// ── Foreign keys (idempotent via DO $$ block) ─────────────────────────
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_piece_prod_slot_piece') THEN
|
||||
ALTER TABLE piece_product_slots
|
||||
ADD CONSTRAINT fk_piece_prod_slot_piece
|
||||
FOREIGN KEY ("pieceid") REFERENCES pieces (id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_piece_prod_slot_type') THEN
|
||||
ALTER TABLE piece_product_slots
|
||||
ADD CONSTRAINT fk_piece_prod_slot_type
|
||||
FOREIGN KEY ("typeproductid") REFERENCES model_types (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_piece_prod_slot_selected') THEN
|
||||
ALTER TABLE piece_product_slots
|
||||
ADD CONSTRAINT fk_piece_prod_slot_selected
|
||||
FOREIGN KEY ("selectedproductid") REFERENCES products (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
|
||||
// ── Add version columns (idempotent) ─────────────────────────────────
|
||||
|
||||
$this->addSql('ALTER TABLE composants ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 1');
|
||||
$this->addSql('ALTER TABLE pieces ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 1');
|
||||
$this->addSql('ALTER TABLE products ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 1');
|
||||
|
||||
// ── Data migration: piece_products → piece_product_slots ─────────────
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'piece_products') THEN
|
||||
INSERT INTO piece_product_slots (id, "pieceid", "typeproductid", "selectedproductid", "familycode", position, "createdat", "updatedat")
|
||||
SELECT
|
||||
'cl' || encode(gen_random_bytes(12), 'hex'),
|
||||
pp.piece_id,
|
||||
p.typeproductid,
|
||||
pp.product_id,
|
||||
NULL,
|
||||
ROW_NUMBER() OVER (PARTITION BY pp.piece_id ORDER BY pp.product_id) - 1,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM piece_products pp
|
||||
JOIN products p ON p.id = pp.product_id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM piece_product_slots pps
|
||||
WHERE pps."pieceid" = pp.piece_id AND pps."selectedproductid" = pp.product_id
|
||||
);
|
||||
END IF;
|
||||
END $$
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE IF EXISTS piece_product_slots');
|
||||
$this->addSql('ALTER TABLE composants DROP COLUMN IF EXISTS version');
|
||||
$this->addSql('ALTER TABLE pieces DROP COLUMN IF EXISTS version');
|
||||
$this->addSql('ALTER TABLE products DROP COLUMN IF EXISTS version');
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
<testsuites>
|
||||
<testsuite name="Project Test Suite">
|
||||
<directory>tests</directory>
|
||||
<exclude>tests/Mcp</exclude>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
|
||||
59
src/Controller/ComposantPieceSlotController.php
Normal file
59
src/Controller/ComposantPieceSlotController.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\ComposantPieceSlot;
|
||||
use App\Entity\Piece;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/api/composant-piece-slots')]
|
||||
class ComposantPieceSlotController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
#[Route('/{id}', name: 'composant_piece_slot_patch', methods: ['PATCH'])]
|
||||
public function patch(string $id, Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||
|
||||
$slot = $this->entityManager->find(ComposantPieceSlot::class, $id);
|
||||
if (!$slot) {
|
||||
return $this->json(['success' => false, 'error' => 'Slot not found.'], 404);
|
||||
}
|
||||
|
||||
$payload = json_decode($request->getContent(), true);
|
||||
if (!is_array($payload)) {
|
||||
return $this->json(['success' => false, 'error' => 'Invalid JSON payload.'], 400);
|
||||
}
|
||||
|
||||
if (array_key_exists('quantity', $payload)) {
|
||||
$slot->setQuantity(max(1, (int) $payload['quantity']));
|
||||
}
|
||||
|
||||
if (array_key_exists('selectedPieceId', $payload)) {
|
||||
if (null === $payload['selectedPieceId']) {
|
||||
$slot->setSelectedPiece(null);
|
||||
} else {
|
||||
$piece = $this->entityManager->find(Piece::class, $payload['selectedPieceId']);
|
||||
$slot->setSelectedPiece($piece);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'id' => $slot->getId(),
|
||||
'quantity' => $slot->getQuantity(),
|
||||
'selectedPieceId' => $slot->getSelectedPiece()?->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
54
src/Controller/ComposantProductSlotController.php
Normal file
54
src/Controller/ComposantProductSlotController.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\ComposantProductSlot;
|
||||
use App\Entity\Product;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/api/composant-product-slots')]
|
||||
class ComposantProductSlotController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
#[Route('/{id}', name: 'composant_product_slot_patch', methods: ['PATCH'])]
|
||||
public function patch(string $id, Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||
|
||||
$slot = $this->entityManager->find(ComposantProductSlot::class, $id);
|
||||
if (!$slot) {
|
||||
return $this->json(['success' => false, 'error' => 'Slot not found.'], 404);
|
||||
}
|
||||
|
||||
$payload = json_decode($request->getContent(), true);
|
||||
if (!is_array($payload)) {
|
||||
return $this->json(['success' => false, 'error' => 'Invalid JSON payload.'], 400);
|
||||
}
|
||||
|
||||
if (array_key_exists('selectedProductId', $payload)) {
|
||||
if (null === $payload['selectedProductId']) {
|
||||
$slot->setSelectedProduct(null);
|
||||
} else {
|
||||
$product = $this->entityManager->find(Product::class, $payload['selectedProductId']);
|
||||
$slot->setSelectedProduct($product);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'id' => $slot->getId(),
|
||||
'selectedProductId' => $slot->getSelectedProduct()?->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
54
src/Controller/ComposantSubcomponentSlotController.php
Normal file
54
src/Controller/ComposantSubcomponentSlotController.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Composant;
|
||||
use App\Entity\ComposantSubcomponentSlot;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/api/composant-subcomponent-slots')]
|
||||
class ComposantSubcomponentSlotController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
#[Route('/{id}', name: 'composant_subcomponent_slot_patch', methods: ['PATCH'])]
|
||||
public function patch(string $id, Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||
|
||||
$slot = $this->entityManager->find(ComposantSubcomponentSlot::class, $id);
|
||||
if (!$slot) {
|
||||
return $this->json(['success' => false, 'error' => 'Slot not found.'], 404);
|
||||
}
|
||||
|
||||
$payload = json_decode($request->getContent(), true);
|
||||
if (!is_array($payload)) {
|
||||
return $this->json(['success' => false, 'error' => 'Invalid JSON payload.'], 400);
|
||||
}
|
||||
|
||||
if (array_key_exists('selectedComposantId', $payload)) {
|
||||
if (null === $payload['selectedComposantId']) {
|
||||
$slot->setSelectedComposant(null);
|
||||
} else {
|
||||
$composant = $this->entityManager->find(Composant::class, $payload['selectedComposantId']);
|
||||
$slot->setSelectedComposant($composant);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'id' => $slot->getId(),
|
||||
'selectedComposantId' => $slot->getSelectedComposant()?->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -242,6 +242,7 @@ class MachineStructureController extends AbstractController
|
||||
$newLink->setNameOverride($link->getNameOverride());
|
||||
$newLink->setReferenceOverride($link->getReferenceOverride());
|
||||
$newLink->setPrixOverride($link->getPrixOverride());
|
||||
$newLink->setQuantity($link->getQuantity());
|
||||
|
||||
$parent = $link->getParentLink();
|
||||
if ($parent && isset($componentLinkMap[$parent->getId()])) {
|
||||
@@ -395,6 +396,11 @@ class MachineStructureController extends AbstractController
|
||||
|
||||
$this->applyOverrides($link, $entry['overrides'] ?? null);
|
||||
|
||||
if (!isset($entry['parentComponentLinkId']) && !isset($entry['parentLinkId'])) {
|
||||
$quantity = isset($entry['quantity']) ? (int) $entry['quantity'] : $link->getQuantity();
|
||||
$link->setQuantity(max(1, $quantity));
|
||||
}
|
||||
|
||||
$pendingParents[$linkId] = $this->resolveIdentifier($entry, [
|
||||
'parentComponentLinkId',
|
||||
'parentLinkId',
|
||||
@@ -636,10 +642,31 @@ class MachineStructureController extends AbstractController
|
||||
'parentComponentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||
'overrides' => $this->normalizeOverrides($link),
|
||||
'quantity' => $this->resolvePieceQuantity($link),
|
||||
];
|
||||
}, $links);
|
||||
}
|
||||
|
||||
private function resolvePieceQuantity(MachinePieceLink $link): int
|
||||
{
|
||||
$parentLink = $link->getParentLink();
|
||||
|
||||
if (!$parentLink) {
|
||||
return $link->getQuantity();
|
||||
}
|
||||
|
||||
$composant = $parentLink->getComposant();
|
||||
$piece = $link->getPiece();
|
||||
|
||||
foreach ($composant->getPieceSlots() as $slot) {
|
||||
if ($slot->getSelectedPiece()?->getId() === $piece->getId()) {
|
||||
return $slot->getQuantity();
|
||||
}
|
||||
}
|
||||
|
||||
return $link->getQuantity();
|
||||
}
|
||||
|
||||
private function normalizeProductLinks(array $links): array
|
||||
{
|
||||
return array_map(function (MachineProductLink $link): array {
|
||||
@@ -671,6 +698,7 @@ class MachineStructureController extends AbstractController
|
||||
'typeComposant' => $this->normalizeModelType($type),
|
||||
'productId' => $composant->getProduct()?->getId(),
|
||||
'product' => $composant->getProduct() ? $this->normalizeProduct($composant->getProduct()) : null,
|
||||
'structure' => $this->buildStructureFromSlots($composant),
|
||||
'constructeurs' => $this->normalizeConstructeurs($composant->getConstructeurs()),
|
||||
'documents' => [],
|
||||
'customFields' => $type ? $this->normalizeCustomFieldDefinitions($type->getComponentCustomFields()) : [],
|
||||
@@ -678,6 +706,48 @@ class MachineStructureController extends AbstractController
|
||||
];
|
||||
}
|
||||
|
||||
private function buildStructureFromSlots(Composant $composant): array
|
||||
{
|
||||
$pieces = [];
|
||||
foreach ($composant->getPieceSlots() as $slot) {
|
||||
$pieceData = [
|
||||
'slotId' => $slot->getId(),
|
||||
'typePieceId' => $slot->getTypePiece()?->getId(),
|
||||
'quantity' => $slot->getQuantity(),
|
||||
'selectedPieceId' => $slot->getSelectedPiece()?->getId(),
|
||||
];
|
||||
if ($slot->getSelectedPiece()) {
|
||||
$pieceData['resolvedPiece'] = $this->normalizePiece($slot->getSelectedPiece());
|
||||
}
|
||||
$pieces[] = $pieceData;
|
||||
}
|
||||
|
||||
$subcomponents = [];
|
||||
foreach ($composant->getSubcomponentSlots() as $slot) {
|
||||
$subcomponents[] = [
|
||||
'alias' => $slot->getAlias(),
|
||||
'familyCode' => $slot->getFamilyCode(),
|
||||
'typeComposantId' => $slot->getTypeComposant()?->getId(),
|
||||
'selectedComponentId' => $slot->getSelectedComposant()?->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
$products = [];
|
||||
foreach ($composant->getProductSlots() as $slot) {
|
||||
$products[] = [
|
||||
'typeProductId' => $slot->getTypeProduct()?->getId(),
|
||||
'familyCode' => $slot->getFamilyCode(),
|
||||
'selectedProductId' => $slot->getSelectedProduct()?->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'pieces' => $pieces,
|
||||
'subcomponents' => $subcomponents,
|
||||
'products' => $products,
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizePiece(Piece $piece): array
|
||||
{
|
||||
$type = $piece->getTypePiece();
|
||||
@@ -700,16 +770,19 @@ class MachineStructureController extends AbstractController
|
||||
|
||||
private function normalizeProduct(Product $product): array
|
||||
{
|
||||
$type = $product->getTypeProduct();
|
||||
|
||||
return [
|
||||
'id' => $product->getId(),
|
||||
'name' => $product->getName(),
|
||||
'reference' => $product->getReference(),
|
||||
'supplierPrice' => $product->getSupplierPrice(),
|
||||
'typeProductId' => $product->getTypeProduct()?->getId(),
|
||||
'typeProduct' => $this->normalizeModelType($product->getTypeProduct()),
|
||||
'constructeurs' => $this->normalizeConstructeurs($product->getConstructeurs()),
|
||||
'documents' => [],
|
||||
'customFields' => [],
|
||||
'id' => $product->getId(),
|
||||
'name' => $product->getName(),
|
||||
'reference' => $product->getReference(),
|
||||
'supplierPrice' => $product->getSupplierPrice(),
|
||||
'typeProductId' => $type?->getId(),
|
||||
'typeProduct' => $this->normalizeModelType($type),
|
||||
'constructeurs' => $this->normalizeConstructeurs($product->getConstructeurs()),
|
||||
'documents' => [],
|
||||
'customFields' => $type ? $this->normalizeCustomFieldDefinitions($type->getProductCustomFields()) : [],
|
||||
'customFieldValues' => $this->normalizeCustomFieldValues($product->getCustomFieldValues()),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
74
src/Controller/ModelTypeSyncController.php
Normal file
74
src/Controller/ModelTypeSyncController.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\DTO\SyncConfirmation;
|
||||
use App\Repository\ModelTypeRepository;
|
||||
use App\Service\ModelTypeSyncService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/api/model_types/{id}')]
|
||||
final class ModelTypeSyncController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ModelTypeRepository $modelTypes,
|
||||
private readonly ModelTypeSyncService $syncService,
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
#[Route('/sync-preview', name: 'api_model_type_sync_preview', methods: ['POST'])]
|
||||
public function preview(string $id, Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||
|
||||
$modelType = $this->modelTypes->find($id);
|
||||
|
||||
if (!$modelType) {
|
||||
return new JsonResponse(
|
||||
['message' => 'Catégorie introuvable.'],
|
||||
Response::HTTP_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
$body = json_decode($request->getContent(), true);
|
||||
$structure = $body['structure'] ?? [];
|
||||
|
||||
$result = $this->syncService->preview($modelType, $structure);
|
||||
|
||||
return new JsonResponse($result);
|
||||
}
|
||||
|
||||
#[Route('/sync', name: 'api_model_type_sync', methods: ['POST'])]
|
||||
public function sync(string $id, Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE');
|
||||
|
||||
$modelType = $this->modelTypes->find($id);
|
||||
|
||||
if (!$modelType) {
|
||||
return new JsonResponse(
|
||||
['message' => 'Catégorie introuvable.'],
|
||||
Response::HTTP_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
$body = json_decode($request->getContent(), true);
|
||||
$confirmation = new SyncConfirmation(
|
||||
confirmDeletions: $body['confirmDeletions'] ?? false,
|
||||
confirmTypeChanges: $body['confirmTypeChanges'] ?? false,
|
||||
);
|
||||
|
||||
$result = $this->em->wrapInTransaction(function () use ($modelType, $confirmation) {
|
||||
return $this->syncService->execute($modelType, $confirmation);
|
||||
});
|
||||
|
||||
return new JsonResponse($result);
|
||||
}
|
||||
}
|
||||
13
src/DTO/SyncConfirmation.php
Normal file
13
src/DTO/SyncConfirmation.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\DTO;
|
||||
|
||||
class SyncConfirmation
|
||||
{
|
||||
public function __construct(
|
||||
public readonly bool $confirmDeletions = false,
|
||||
public readonly bool $confirmTypeChanges = false,
|
||||
) {}
|
||||
}
|
||||
27
src/DTO/SyncExecutionResult.php
Normal file
27
src/DTO/SyncExecutionResult.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\DTO;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
class SyncExecutionResult implements JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public readonly int $itemsUpdated,
|
||||
public readonly array $additions = [],
|
||||
public readonly array $deletions = [],
|
||||
public readonly array $modifications = [],
|
||||
) {}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'itemsUpdated' => $this->itemsUpdated,
|
||||
'additions' => $this->additions,
|
||||
'deletions' => $this->deletions,
|
||||
'modifications' => $this->modifications,
|
||||
];
|
||||
}
|
||||
}
|
||||
38
src/DTO/SyncPreviewResult.php
Normal file
38
src/DTO/SyncPreviewResult.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\DTO;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
class SyncPreviewResult implements JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $modelTypeId,
|
||||
public readonly string $category,
|
||||
public readonly int $itemCount,
|
||||
public readonly array $additions = [],
|
||||
public readonly array $deletions = [],
|
||||
public readonly array $modifications = [],
|
||||
) {}
|
||||
|
||||
public function hasImpact(): bool
|
||||
{
|
||||
return array_sum($this->additions) > 0
|
||||
|| array_sum($this->deletions) > 0
|
||||
|| array_sum($this->modifications) > 0;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'modelTypeId' => $this->modelTypeId,
|
||||
'category' => $this->category,
|
||||
'itemCount' => $this->itemCount,
|
||||
'additions' => $this->additions,
|
||||
'deletions' => $this->deletions,
|
||||
'modifications' => $this->modifications,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,6 @@ class Composant
|
||||
#[Groups(['composant:read'])]
|
||||
private ?string $prix = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['composant:read'])]
|
||||
private ?array $structure = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'composants')]
|
||||
#[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: true)]
|
||||
#[Groups(['composant:read'])]
|
||||
@@ -113,6 +109,31 @@ class Composant
|
||||
#[ORM\OneToMany(mappedBy: 'composant', targetEntity: MachineComponentLink::class)]
|
||||
private Collection $machineLinks;
|
||||
|
||||
/**
|
||||
* @var Collection<int, ComposantPieceSlot>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: ComposantPieceSlot::class, mappedBy: 'composant', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['position' => 'ASC'])]
|
||||
private Collection $pieceSlots;
|
||||
|
||||
/**
|
||||
* @var Collection<int, ComposantSubcomponentSlot>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: ComposantSubcomponentSlot::class, mappedBy: 'composant', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['position' => 'ASC'])]
|
||||
private Collection $subcomponentSlots;
|
||||
|
||||
/**
|
||||
* @var Collection<int, ComposantProductSlot>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: ComposantProductSlot::class, mappedBy: 'composant', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['position' => 'ASC'])]
|
||||
private Collection $productSlots;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 1])]
|
||||
#[Groups(['composant:read'])]
|
||||
private int $version = 1;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
#[Groups(['composant:read'])]
|
||||
private DateTimeImmutable $createdAt;
|
||||
@@ -129,6 +150,9 @@ class Composant
|
||||
$this->documents = new ArrayCollection();
|
||||
$this->customFieldValues = new ArrayCollection();
|
||||
$this->machineLinks = new ArrayCollection();
|
||||
$this->pieceSlots = new ArrayCollection();
|
||||
$this->subcomponentSlots = new ArrayCollection();
|
||||
$this->productSlots = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
@@ -179,18 +203,6 @@ class Composant
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStructure(): ?array
|
||||
{
|
||||
return $this->structure;
|
||||
}
|
||||
|
||||
public function setStructure(?array $structure): static
|
||||
{
|
||||
$this->structure = $structure;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeComposant(): ?ModelType
|
||||
{
|
||||
return $this->typeComposant;
|
||||
@@ -270,4 +282,144 @@ class Composant
|
||||
{
|
||||
return $this->customFieldValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, ComposantPieceSlot>
|
||||
*/
|
||||
public function getPieceSlots(): Collection
|
||||
{
|
||||
return $this->pieceSlots;
|
||||
}
|
||||
|
||||
public function addPieceSlot(ComposantPieceSlot $pieceSlot): static
|
||||
{
|
||||
if (!$this->pieceSlots->contains($pieceSlot)) {
|
||||
$this->pieceSlots->add($pieceSlot);
|
||||
$pieceSlot->setComposant($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removePieceSlot(ComposantPieceSlot $pieceSlot): static
|
||||
{
|
||||
$this->pieceSlots->removeElement($pieceSlot);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, ComposantSubcomponentSlot>
|
||||
*/
|
||||
public function getSubcomponentSlots(): Collection
|
||||
{
|
||||
return $this->subcomponentSlots;
|
||||
}
|
||||
|
||||
public function addSubcomponentSlot(ComposantSubcomponentSlot $subcomponentSlot): static
|
||||
{
|
||||
if (!$this->subcomponentSlots->contains($subcomponentSlot)) {
|
||||
$this->subcomponentSlots->add($subcomponentSlot);
|
||||
$subcomponentSlot->setComposant($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeSubcomponentSlot(ComposantSubcomponentSlot $subcomponentSlot): static
|
||||
{
|
||||
$this->subcomponentSlots->removeElement($subcomponentSlot);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, ComposantProductSlot>
|
||||
*/
|
||||
public function getProductSlots(): Collection
|
||||
{
|
||||
return $this->productSlots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual property — rebuilds the legacy structure JSON from slot tables.
|
||||
*
|
||||
* @return null|array{pieces: list<array<string, mixed>>, products: list<array<string, mixed>>, subcomponents: list<array<string, mixed>>}
|
||||
*/
|
||||
#[Groups(['composant:read'])]
|
||||
public function getStructure(): ?array
|
||||
{
|
||||
$pieces = [];
|
||||
foreach ($this->pieceSlots as $slot) {
|
||||
$pieces[] = [
|
||||
'slotId' => $slot->getId(),
|
||||
'typePieceId' => $slot->getTypePiece()?->getId(),
|
||||
'selectedPieceId' => $slot->getSelectedPiece()?->getId(),
|
||||
'quantity' => $slot->getQuantity(),
|
||||
'position' => $slot->getPosition(),
|
||||
];
|
||||
}
|
||||
|
||||
$products = [];
|
||||
foreach ($this->productSlots as $slot) {
|
||||
$products[] = [
|
||||
'slotId' => $slot->getId(),
|
||||
'typeProductId' => $slot->getTypeProduct()?->getId(),
|
||||
'selectedProductId' => $slot->getSelectedProduct()?->getId(),
|
||||
'familyCode' => $slot->getFamilyCode(),
|
||||
'position' => $slot->getPosition(),
|
||||
];
|
||||
}
|
||||
|
||||
$subcomponents = [];
|
||||
foreach ($this->subcomponentSlots as $slot) {
|
||||
$subcomponents[] = [
|
||||
'slotId' => $slot->getId(),
|
||||
'alias' => $slot->getAlias(),
|
||||
'familyCode' => $slot->getFamilyCode(),
|
||||
'typeComposantId' => $slot->getTypeComposant()?->getId(),
|
||||
'selectedComponentId' => $slot->getSelectedComposant()?->getId(),
|
||||
'position' => $slot->getPosition(),
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($pieces) && empty($products) && empty($subcomponents)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'pieces' => $pieces,
|
||||
'products' => $products,
|
||||
'subcomponents' => $subcomponents,
|
||||
];
|
||||
}
|
||||
|
||||
public function addProductSlot(ComposantProductSlot $productSlot): static
|
||||
{
|
||||
if (!$this->productSlots->contains($productSlot)) {
|
||||
$this->productSlots->add($productSlot);
|
||||
$productSlot->setComposant($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeProductSlot(ComposantProductSlot $productSlot): static
|
||||
{
|
||||
$this->productSlots->removeElement($productSlot);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getVersion(): int
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
public function incrementVersion(): static
|
||||
{
|
||||
++$this->version;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
112
src/Entity/ComposantPieceSlot.php
Normal file
112
src/Entity/ComposantPieceSlot.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Entity\Trait\CuidEntityTrait;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'composant_piece_slots')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
class ComposantPieceSlot
|
||||
{
|
||||
use CuidEntityTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'pieceSlots')]
|
||||
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Composant $composant;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class)]
|
||||
#[ORM\JoinColumn(name: 'typePieceId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?ModelType $typePiece = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Piece::class)]
|
||||
#[ORM\JoinColumn(name: 'selectedPieceId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?Piece $selectedPiece = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 1])]
|
||||
private int $quantity = 1;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0])]
|
||||
private int $position = 0;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getComposant(): Composant
|
||||
{
|
||||
return $this->composant;
|
||||
}
|
||||
|
||||
public function setComposant(Composant $composant): static
|
||||
{
|
||||
$this->composant = $composant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypePiece(): ?ModelType
|
||||
{
|
||||
return $this->typePiece;
|
||||
}
|
||||
|
||||
public function setTypePiece(?ModelType $typePiece): static
|
||||
{
|
||||
$this->typePiece = $typePiece;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSelectedPiece(): ?Piece
|
||||
{
|
||||
return $this->selectedPiece;
|
||||
}
|
||||
|
||||
public function setSelectedPiece(?Piece $selectedPiece): static
|
||||
{
|
||||
$this->selectedPiece = $selectedPiece;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getQuantity(): int
|
||||
{
|
||||
return $this->quantity;
|
||||
}
|
||||
|
||||
public function setQuantity(int $quantity): static
|
||||
{
|
||||
$this->quantity = $quantity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function setPosition(int $position): static
|
||||
{
|
||||
$this->position = $position;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
112
src/Entity/ComposantProductSlot.php
Normal file
112
src/Entity/ComposantProductSlot.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Entity\Trait\CuidEntityTrait;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'composant_product_slots')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
class ComposantProductSlot
|
||||
{
|
||||
use CuidEntityTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'productSlots')]
|
||||
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Composant $composant;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class)]
|
||||
#[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?ModelType $typeProduct = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Product::class)]
|
||||
#[ORM\JoinColumn(name: 'selectedProductId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?Product $selectedProduct = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'familyCode')]
|
||||
private ?string $familyCode = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0])]
|
||||
private int $position = 0;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getComposant(): Composant
|
||||
{
|
||||
return $this->composant;
|
||||
}
|
||||
|
||||
public function setComposant(Composant $composant): static
|
||||
{
|
||||
$this->composant = $composant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeProduct(): ?ModelType
|
||||
{
|
||||
return $this->typeProduct;
|
||||
}
|
||||
|
||||
public function setTypeProduct(?ModelType $typeProduct): static
|
||||
{
|
||||
$this->typeProduct = $typeProduct;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSelectedProduct(): ?Product
|
||||
{
|
||||
return $this->selectedProduct;
|
||||
}
|
||||
|
||||
public function setSelectedProduct(?Product $selectedProduct): static
|
||||
{
|
||||
$this->selectedProduct = $selectedProduct;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFamilyCode(): ?string
|
||||
{
|
||||
return $this->familyCode;
|
||||
}
|
||||
|
||||
public function setFamilyCode(?string $familyCode): static
|
||||
{
|
||||
$this->familyCode = $familyCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function setPosition(int $position): static
|
||||
{
|
||||
$this->position = $position;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
127
src/Entity/ComposantSubcomponentSlot.php
Normal file
127
src/Entity/ComposantSubcomponentSlot.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Entity\Trait\CuidEntityTrait;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'composant_subcomponent_slots')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
class ComposantSubcomponentSlot
|
||||
{
|
||||
use CuidEntityTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'subcomponentSlots')]
|
||||
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Composant $composant;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
private ?string $alias = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'familyCode')]
|
||||
private ?string $familyCode = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class)]
|
||||
#[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?ModelType $typeComposant = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Composant::class)]
|
||||
#[ORM\JoinColumn(name: 'selectedComposantId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?Composant $selectedComposant = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0])]
|
||||
private int $position = 0;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getComposant(): Composant
|
||||
{
|
||||
return $this->composant;
|
||||
}
|
||||
|
||||
public function setComposant(Composant $composant): static
|
||||
{
|
||||
$this->composant = $composant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAlias(): ?string
|
||||
{
|
||||
return $this->alias;
|
||||
}
|
||||
|
||||
public function setAlias(?string $alias): static
|
||||
{
|
||||
$this->alias = $alias;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFamilyCode(): ?string
|
||||
{
|
||||
return $this->familyCode;
|
||||
}
|
||||
|
||||
public function setFamilyCode(?string $familyCode): static
|
||||
{
|
||||
$this->familyCode = $familyCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeComposant(): ?ModelType
|
||||
{
|
||||
return $this->typeComposant;
|
||||
}
|
||||
|
||||
public function setTypeComposant(?ModelType $typeComposant): static
|
||||
{
|
||||
$this->typeComposant = $typeComposant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSelectedComposant(): ?Composant
|
||||
{
|
||||
return $this->selectedComposant;
|
||||
}
|
||||
|
||||
public function setSelectedComposant(?Composant $selectedComposant): static
|
||||
{
|
||||
$this->selectedComposant = $selectedComposant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function setPosition(int $position): static
|
||||
{
|
||||
$this->position = $position;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -184,4 +184,40 @@ class CustomField
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeComposant(): ?ModelType
|
||||
{
|
||||
return $this->typeComposant;
|
||||
}
|
||||
|
||||
public function setTypeComposant(?ModelType $typeComposant): static
|
||||
{
|
||||
$this->typeComposant = $typeComposant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypePiece(): ?ModelType
|
||||
{
|
||||
return $this->typePiece;
|
||||
}
|
||||
|
||||
public function setTypePiece(?ModelType $typePiece): static
|
||||
{
|
||||
$this->typePiece = $typePiece;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeProduct(): ?ModelType
|
||||
{
|
||||
return $this->typeProduct;
|
||||
}
|
||||
|
||||
public function setTypeProduct(?ModelType $typeProduct): static
|
||||
{
|
||||
$this->typeProduct = $typeProduct;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +210,22 @@ class Machine
|
||||
return $this->constructeurs;
|
||||
}
|
||||
|
||||
public function addConstructeur(Constructeur $constructeur): static
|
||||
{
|
||||
if (!$this->constructeurs->contains($constructeur)) {
|
||||
$this->constructeurs->add($constructeur);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeConstructeur(Constructeur $constructeur): static
|
||||
{
|
||||
$this->constructeurs->removeElement($constructeur);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, MachineComponentLink>
|
||||
*/
|
||||
|
||||
@@ -18,6 +18,7 @@ use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: MachinePieceLinkRepository::class)]
|
||||
#[ORM\Table(name: 'machine_piece_links')]
|
||||
@@ -68,6 +69,10 @@ class MachinePieceLink
|
||||
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true, name: 'prixOverride')]
|
||||
private ?string $prixOverride = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 1])]
|
||||
#[Assert\GreaterThanOrEqual(1)]
|
||||
private int $quantity = 1;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
@@ -152,4 +157,16 @@ class MachinePieceLink
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getQuantity(): int
|
||||
{
|
||||
return $this->quantity;
|
||||
}
|
||||
|
||||
public function setQuantity(int $quantity): static
|
||||
{
|
||||
$this->quantity = $quantity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use ApiPlatform\Metadata\Put;
|
||||
use App\Entity\Trait\CuidEntityTrait;
|
||||
use App\Enum\ModelCategory;
|
||||
use App\Repository\ModelTypeRepository;
|
||||
use App\State\ModelTypeProcessor;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -35,9 +36,9 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
operations: [
|
||||
new Get(security: "is_granted('ROLE_VIEWER')"),
|
||||
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
|
||||
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
new Post(security: "is_granted('ROLE_GESTIONNAIRE')", processor: ModelTypeProcessor::class),
|
||||
new Put(security: "is_granted('ROLE_GESTIONNAIRE')", processor: ModelTypeProcessor::class),
|
||||
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')", processor: ModelTypeProcessor::class),
|
||||
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
],
|
||||
paginationClientItemsPerPage: true,
|
||||
@@ -72,18 +73,6 @@ class ModelType
|
||||
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true, name: 'componentSkeleton')]
|
||||
#[Groups(['model_type:read', 'composant:read'])]
|
||||
private ?array $componentSkeleton = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true, name: 'pieceSkeleton')]
|
||||
#[Groups(['model_type:read', 'piece:read'])]
|
||||
private ?array $pieceSkeleton = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true, name: 'productSkeleton')]
|
||||
#[Groups(['model_type:read', 'product:read'])]
|
||||
private ?array $productSkeleton = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
#[Groups(['model_type:read'])]
|
||||
private DateTimeImmutable $createdAt;
|
||||
@@ -130,16 +119,40 @@ class ModelType
|
||||
#[ORM\OneToMany(mappedBy: 'typeProduct', targetEntity: CustomField::class)]
|
||||
private Collection $productCustomFields;
|
||||
|
||||
/**
|
||||
* @var Collection<int, SkeletonPieceRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: SkeletonPieceRequirement::class, mappedBy: 'modelType', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['position' => 'ASC'])]
|
||||
private Collection $skeletonPieceRequirements;
|
||||
|
||||
/**
|
||||
* @var Collection<int, SkeletonProductRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: SkeletonProductRequirement::class, mappedBy: 'modelType', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['position' => 'ASC'])]
|
||||
private Collection $skeletonProductRequirements;
|
||||
|
||||
/**
|
||||
* @var Collection<int, SkeletonSubcomponentRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: SkeletonSubcomponentRequirement::class, mappedBy: 'modelType', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['position' => 'ASC'])]
|
||||
private Collection $skeletonSubcomponentRequirements;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
$this->composants = new ArrayCollection();
|
||||
$this->pieces = new ArrayCollection();
|
||||
$this->products = new ArrayCollection();
|
||||
$this->customFields = new ArrayCollection();
|
||||
$this->pieceCustomFields = new ArrayCollection();
|
||||
$this->productCustomFields = new ArrayCollection();
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
$this->composants = new ArrayCollection();
|
||||
$this->pieces = new ArrayCollection();
|
||||
$this->products = new ArrayCollection();
|
||||
$this->customFields = new ArrayCollection();
|
||||
$this->pieceCustomFields = new ArrayCollection();
|
||||
$this->productCustomFields = new ArrayCollection();
|
||||
$this->skeletonPieceRequirements = new ArrayCollection();
|
||||
$this->skeletonProductRequirements = new ArrayCollection();
|
||||
$this->skeletonSubcomponentRequirements = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
@@ -175,11 +188,6 @@ class ModelType
|
||||
{
|
||||
$this->category = $category;
|
||||
|
||||
if (null !== $this->pendingStructure) {
|
||||
$this->applyStructureForCategory($this->pendingStructure, $category);
|
||||
$this->pendingStructure = null;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -207,66 +215,34 @@ class ModelType
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComponentSkeleton(): ?array
|
||||
{
|
||||
return $this->componentSkeleton;
|
||||
}
|
||||
|
||||
public function setComponentSkeleton(?array $componentSkeleton): static
|
||||
{
|
||||
$this->componentSkeleton = $componentSkeleton;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPieceSkeleton(): ?array
|
||||
{
|
||||
return $this->pieceSkeleton;
|
||||
}
|
||||
|
||||
public function setPieceSkeleton(?array $pieceSkeleton): static
|
||||
{
|
||||
$this->pieceSkeleton = $pieceSkeleton;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProductSkeleton(): ?array
|
||||
{
|
||||
return $this->productSkeleton;
|
||||
}
|
||||
|
||||
public function setProductSkeleton(?array $productSkeleton): static
|
||||
{
|
||||
$this->productSkeleton = $productSkeleton;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Groups(['model_type:read', 'product:read', 'composant:read', 'piece:read'])]
|
||||
public function getStructure(): ?array
|
||||
{
|
||||
return match ($this->category) {
|
||||
ModelCategory::COMPONENT => $this->componentSkeleton,
|
||||
ModelCategory::PIECE => $this->pieceSkeleton,
|
||||
ModelCategory::PRODUCT => $this->productSkeleton,
|
||||
ModelCategory::COMPONENT => $this->getComponentStructureFromRelations(),
|
||||
ModelCategory::PIECE => $this->getPieceStructureFromRelations(),
|
||||
ModelCategory::PRODUCT => ['customFields' => $this->serializeCustomFields($this->productCustomFields)],
|
||||
};
|
||||
}
|
||||
|
||||
#[Groups(['model_type:write'])]
|
||||
public function setStructure(?array $structure): static
|
||||
{
|
||||
if (!isset($this->category)) {
|
||||
$this->pendingStructure = $structure;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->applyStructureForCategory($structure, $this->category);
|
||||
$this->pendingStructure = $structure;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPendingStructure(): ?array
|
||||
{
|
||||
return $this->pendingStructure;
|
||||
}
|
||||
|
||||
public function clearPendingStructure(): void
|
||||
{
|
||||
$this->pendingStructure = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, CustomField>
|
||||
*/
|
||||
@@ -291,26 +267,140 @@ class ModelType
|
||||
return $this->productCustomFields;
|
||||
}
|
||||
|
||||
private function applyStructureForCategory(?array $structure, ModelCategory $category): void
|
||||
/**
|
||||
* @return Collection<int, SkeletonPieceRequirement>
|
||||
*/
|
||||
public function getSkeletonPieceRequirements(): Collection
|
||||
{
|
||||
if (ModelCategory::COMPONENT === $category) {
|
||||
$this->componentSkeleton = $structure;
|
||||
$this->pieceSkeleton = null;
|
||||
$this->productSkeleton = null;
|
||||
return $this->skeletonPieceRequirements;
|
||||
}
|
||||
|
||||
return;
|
||||
public function addSkeletonPieceRequirement(SkeletonPieceRequirement $requirement): static
|
||||
{
|
||||
if (!$this->skeletonPieceRequirements->contains($requirement)) {
|
||||
$this->skeletonPieceRequirements->add($requirement);
|
||||
$requirement->setModelType($this);
|
||||
}
|
||||
|
||||
if (ModelCategory::PIECE === $category) {
|
||||
$this->pieceSkeleton = $structure;
|
||||
$this->componentSkeleton = null;
|
||||
$this->productSkeleton = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
return;
|
||||
public function removeSkeletonPieceRequirement(SkeletonPieceRequirement $requirement): static
|
||||
{
|
||||
$this->skeletonPieceRequirements->removeElement($requirement);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, SkeletonProductRequirement>
|
||||
*/
|
||||
public function getSkeletonProductRequirements(): Collection
|
||||
{
|
||||
return $this->skeletonProductRequirements;
|
||||
}
|
||||
|
||||
public function addSkeletonProductRequirement(SkeletonProductRequirement $requirement): static
|
||||
{
|
||||
if (!$this->skeletonProductRequirements->contains($requirement)) {
|
||||
$this->skeletonProductRequirements->add($requirement);
|
||||
$requirement->setModelType($this);
|
||||
}
|
||||
|
||||
$this->productSkeleton = $structure;
|
||||
$this->componentSkeleton = null;
|
||||
$this->pieceSkeleton = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeSkeletonProductRequirement(SkeletonProductRequirement $requirement): static
|
||||
{
|
||||
$this->skeletonProductRequirements->removeElement($requirement);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, SkeletonSubcomponentRequirement>
|
||||
*/
|
||||
public function getSkeletonSubcomponentRequirements(): Collection
|
||||
{
|
||||
return $this->skeletonSubcomponentRequirements;
|
||||
}
|
||||
|
||||
public function addSkeletonSubcomponentRequirement(SkeletonSubcomponentRequirement $requirement): static
|
||||
{
|
||||
if (!$this->skeletonSubcomponentRequirements->contains($requirement)) {
|
||||
$this->skeletonSubcomponentRequirements->add($requirement);
|
||||
$requirement->setModelType($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeSkeletonSubcomponentRequirement(SkeletonSubcomponentRequirement $requirement): static
|
||||
{
|
||||
$this->skeletonSubcomponentRequirements->removeElement($requirement);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function getComponentStructureFromRelations(): array
|
||||
{
|
||||
$structure = ['customFields' => $this->serializeCustomFields($this->customFields), 'pieces' => [], 'products' => [], 'subcomponents' => []];
|
||||
|
||||
foreach ($this->skeletonPieceRequirements as $req) {
|
||||
$structure['pieces'][] = [
|
||||
'typePieceId' => $req->getTypePiece()->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($this->skeletonProductRequirements as $req) {
|
||||
$structure['products'][] = [
|
||||
'typeProductId' => $req->getTypeProduct()->getId(),
|
||||
'familyCode' => $req->getFamilyCode(),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($this->skeletonSubcomponentRequirements as $req) {
|
||||
$structure['subcomponents'][] = [
|
||||
'alias' => $req->getAlias(),
|
||||
'familyCode' => $req->getFamilyCode(),
|
||||
'typeComposantId' => $req->getTypeComposant()?->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
return $structure;
|
||||
}
|
||||
|
||||
private function getPieceStructureFromRelations(): array
|
||||
{
|
||||
return [
|
||||
'customFields' => $this->serializeCustomFields($this->pieceCustomFields),
|
||||
'products' => array_map(fn (SkeletonProductRequirement $req) => [
|
||||
'typeProductId' => $req->getTypeProduct()->getId(),
|
||||
'familyCode' => $req->getFamilyCode(),
|
||||
], $this->skeletonProductRequirements->toArray()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, CustomField> $fields
|
||||
*/
|
||||
private function serializeCustomFields(Collection $fields): array
|
||||
{
|
||||
$items = [];
|
||||
foreach ($fields as $cf) {
|
||||
$items[] = [
|
||||
'id' => $cf->getId(),
|
||||
'name' => $cf->getName(),
|
||||
'type' => $cf->getType(),
|
||||
'required' => $cf->isRequired(),
|
||||
'options' => $cf->getOptions(),
|
||||
'defaultValue' => $cf->getDefaultValue(),
|
||||
'orderIndex' => $cf->getOrderIndex(),
|
||||
];
|
||||
}
|
||||
|
||||
usort($items, static fn (array $a, array $b) => $a['orderIndex'] <=> $b['orderIndex']);
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,10 +79,6 @@ class Piece
|
||||
#[Groups(['piece:read'])]
|
||||
private ?Product $product = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true, name: 'productIds')]
|
||||
#[Groups(['piece:read'])]
|
||||
private ?array $productIds = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Constructeur>
|
||||
*/
|
||||
@@ -109,12 +105,32 @@ class Piece
|
||||
#[Groups(['piece:read'])]
|
||||
private Collection $customFieldValues;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Product>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Product::class, inversedBy: 'linkedPieces')]
|
||||
#[ORM\JoinTable(name: 'piece_products')]
|
||||
#[ORM\JoinColumn(name: 'piece_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[ORM\InverseJoinColumn(name: 'product_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
private Collection $products;
|
||||
|
||||
/**
|
||||
* @var Collection<int, PieceProductSlot>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: PieceProductSlot::class, mappedBy: 'piece', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['position' => 'ASC'])]
|
||||
private Collection $productSlots;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachinePieceLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'piece', targetEntity: MachinePieceLink::class)]
|
||||
private Collection $machineLinks;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 1])]
|
||||
#[Groups(['piece:read'])]
|
||||
private int $version = 1;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
#[Groups(['piece:read'])]
|
||||
private DateTimeImmutable $createdAt;
|
||||
@@ -130,6 +146,8 @@ class Piece
|
||||
$this->constructeurs = new ArrayCollection();
|
||||
$this->documents = new ArrayCollection();
|
||||
$this->customFieldValues = new ArrayCollection();
|
||||
$this->products = new ArrayCollection();
|
||||
$this->productSlots = new ArrayCollection();
|
||||
$this->machineLinks = new ArrayCollection();
|
||||
}
|
||||
|
||||
@@ -202,13 +220,8 @@ class Piece
|
||||
{
|
||||
$this->product = $product;
|
||||
|
||||
if ($product && empty($this->productIds)) {
|
||||
$productId = $product->getId();
|
||||
$this->productIds = $productId ? [$productId] : null;
|
||||
}
|
||||
|
||||
if (!$product && empty($this->productIds)) {
|
||||
$this->productIds = null;
|
||||
if (null !== $product) {
|
||||
$this->addProduct($product);
|
||||
}
|
||||
|
||||
return $this;
|
||||
@@ -217,46 +230,10 @@ class Piece
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
#[Groups(['piece:read'])]
|
||||
public function getProductIds(): array
|
||||
{
|
||||
if (!is_array($this->productIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(
|
||||
array_filter(
|
||||
array_map(
|
||||
static fn ($value) => is_string($value) ? trim($value) : '',
|
||||
$this->productIds,
|
||||
),
|
||||
static fn (string $value) => '' !== $value,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function setProductIds(?array $productIds): static
|
||||
{
|
||||
if (!is_array($productIds)) {
|
||||
$this->productIds = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$normalized = array_values(
|
||||
array_unique(
|
||||
array_filter(
|
||||
array_map(
|
||||
static fn ($value) => is_string($value) ? trim($value) : '',
|
||||
$productIds,
|
||||
),
|
||||
static fn (string $value) => '' !== $value,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$this->productIds = [] === $normalized ? null : $normalized;
|
||||
|
||||
return $this;
|
||||
return $this->products->map(fn (Product $p) => $p->getId())->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -298,4 +275,65 @@ class Piece
|
||||
{
|
||||
return $this->customFieldValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Product>
|
||||
*/
|
||||
public function getProducts(): Collection
|
||||
{
|
||||
return $this->products;
|
||||
}
|
||||
|
||||
public function addProduct(Product $product): static
|
||||
{
|
||||
if (!$this->products->contains($product)) {
|
||||
$this->products->add($product);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeProduct(Product $product): static
|
||||
{
|
||||
$this->products->removeElement($product);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, PieceProductSlot>
|
||||
*/
|
||||
public function getProductSlots(): Collection
|
||||
{
|
||||
return $this->productSlots;
|
||||
}
|
||||
|
||||
public function addProductSlot(PieceProductSlot $slot): static
|
||||
{
|
||||
if (!$this->productSlots->contains($slot)) {
|
||||
$this->productSlots->add($slot);
|
||||
$slot->setPiece($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeProductSlot(PieceProductSlot $slot): static
|
||||
{
|
||||
$this->productSlots->removeElement($slot);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getVersion(): int
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
public function incrementVersion(): static
|
||||
{
|
||||
++$this->version;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
112
src/Entity/PieceProductSlot.php
Normal file
112
src/Entity/PieceProductSlot.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Entity\Trait\CuidEntityTrait;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'piece_product_slots')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
class PieceProductSlot
|
||||
{
|
||||
use CuidEntityTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'productSlots')]
|
||||
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Piece $piece;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class)]
|
||||
#[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?ModelType $typeProduct = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Product::class)]
|
||||
#[ORM\JoinColumn(name: 'selectedProductId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?Product $selectedProduct = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'familyCode')]
|
||||
private ?string $familyCode = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0])]
|
||||
private int $position = 0;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getPiece(): Piece
|
||||
{
|
||||
return $this->piece;
|
||||
}
|
||||
|
||||
public function setPiece(Piece $piece): static
|
||||
{
|
||||
$this->piece = $piece;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeProduct(): ?ModelType
|
||||
{
|
||||
return $this->typeProduct;
|
||||
}
|
||||
|
||||
public function setTypeProduct(?ModelType $typeProduct): static
|
||||
{
|
||||
$this->typeProduct = $typeProduct;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSelectedProduct(): ?Product
|
||||
{
|
||||
return $this->selectedProduct;
|
||||
}
|
||||
|
||||
public function setSelectedProduct(?Product $selectedProduct): static
|
||||
{
|
||||
$this->selectedProduct = $selectedProduct;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFamilyCode(): ?string
|
||||
{
|
||||
return $this->familyCode;
|
||||
}
|
||||
|
||||
public function setFamilyCode(?string $familyCode): static
|
||||
{
|
||||
$this->familyCode = $familyCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function setPosition(int $position): static
|
||||
{
|
||||
$this->position = $position;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -106,12 +106,22 @@ class Product
|
||||
#[ORM\OneToMany(mappedBy: 'product', targetEntity: Composant::class)]
|
||||
private Collection $composants;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Piece>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Piece::class, mappedBy: 'products')]
|
||||
private Collection $linkedPieces;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachineProductLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'product', targetEntity: MachineProductLink::class)]
|
||||
private Collection $machineLinks;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 1])]
|
||||
#[Groups(['product:read'])]
|
||||
private int $version = 1;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
#[Groups(['product:read'])]
|
||||
private DateTimeImmutable $createdAt;
|
||||
@@ -129,6 +139,7 @@ class Product
|
||||
$this->customFieldValues = new ArrayCollection();
|
||||
$this->pieces = new ArrayCollection();
|
||||
$this->composants = new ArrayCollection();
|
||||
$this->linkedPieces = new ArrayCollection();
|
||||
$this->machineLinks = new ArrayCollection();
|
||||
}
|
||||
|
||||
@@ -219,4 +230,24 @@ class Product
|
||||
{
|
||||
return $this->customFieldValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Piece>
|
||||
*/
|
||||
public function getLinkedPieces(): Collection
|
||||
{
|
||||
return $this->linkedPieces;
|
||||
}
|
||||
|
||||
public function getVersion(): int
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
public function incrementVersion(): static
|
||||
{
|
||||
++$this->version;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
81
src/Entity/SkeletonPieceRequirement.php
Normal file
81
src/Entity/SkeletonPieceRequirement.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Entity\Trait\CuidEntityTrait;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'skeleton_piece_requirements')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
class SkeletonPieceRequirement
|
||||
{
|
||||
use CuidEntityTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'skeletonPieceRequirements')]
|
||||
#[ORM\JoinColumn(name: 'modelTypeId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private ModelType $modelType;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class)]
|
||||
#[ORM\JoinColumn(name: 'typePieceId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private ModelType $typePiece;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0])]
|
||||
private int $position = 0;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getModelType(): ModelType
|
||||
{
|
||||
return $this->modelType;
|
||||
}
|
||||
|
||||
public function setModelType(ModelType $modelType): static
|
||||
{
|
||||
$this->modelType = $modelType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypePiece(): ModelType
|
||||
{
|
||||
return $this->typePiece;
|
||||
}
|
||||
|
||||
public function setTypePiece(ModelType $typePiece): static
|
||||
{
|
||||
$this->typePiece = $typePiece;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function setPosition(int $position): static
|
||||
{
|
||||
$this->position = $position;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
96
src/Entity/SkeletonProductRequirement.php
Normal file
96
src/Entity/SkeletonProductRequirement.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Entity\Trait\CuidEntityTrait;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'skeleton_product_requirements')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
class SkeletonProductRequirement
|
||||
{
|
||||
use CuidEntityTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'skeletonProductRequirements')]
|
||||
#[ORM\JoinColumn(name: 'modelTypeId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private ModelType $modelType;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class)]
|
||||
#[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private ModelType $typeProduct;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'familyCode')]
|
||||
private ?string $familyCode = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0])]
|
||||
private int $position = 0;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getModelType(): ModelType
|
||||
{
|
||||
return $this->modelType;
|
||||
}
|
||||
|
||||
public function setModelType(ModelType $modelType): static
|
||||
{
|
||||
$this->modelType = $modelType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeProduct(): ModelType
|
||||
{
|
||||
return $this->typeProduct;
|
||||
}
|
||||
|
||||
public function setTypeProduct(ModelType $typeProduct): static
|
||||
{
|
||||
$this->typeProduct = $typeProduct;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFamilyCode(): ?string
|
||||
{
|
||||
return $this->familyCode;
|
||||
}
|
||||
|
||||
public function setFamilyCode(?string $familyCode): static
|
||||
{
|
||||
$this->familyCode = $familyCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function setPosition(int $position): static
|
||||
{
|
||||
$this->position = $position;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
111
src/Entity/SkeletonSubcomponentRequirement.php
Normal file
111
src/Entity/SkeletonSubcomponentRequirement.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Entity\Trait\CuidEntityTrait;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'skeleton_subcomponent_requirements')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
class SkeletonSubcomponentRequirement
|
||||
{
|
||||
use CuidEntityTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'skeletonSubcomponentRequirements')]
|
||||
#[ORM\JoinColumn(name: 'modelTypeId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private ModelType $modelType;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||
private string $alias;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, name: 'familyCode')]
|
||||
private string $familyCode;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class)]
|
||||
#[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?ModelType $typeComposant = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0])]
|
||||
private int $position = 0;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getModelType(): ModelType
|
||||
{
|
||||
return $this->modelType;
|
||||
}
|
||||
|
||||
public function setModelType(ModelType $modelType): static
|
||||
{
|
||||
$this->modelType = $modelType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAlias(): string
|
||||
{
|
||||
return $this->alias;
|
||||
}
|
||||
|
||||
public function setAlias(string $alias): static
|
||||
{
|
||||
$this->alias = $alias;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFamilyCode(): string
|
||||
{
|
||||
return $this->familyCode;
|
||||
}
|
||||
|
||||
public function setFamilyCode(string $familyCode): static
|
||||
{
|
||||
$this->familyCode = $familyCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeComposant(): ?ModelType
|
||||
{
|
||||
return $this->typeComposant;
|
||||
}
|
||||
|
||||
public function setTypeComposant(?ModelType $typeComposant): static
|
||||
{
|
||||
$this->typeComposant = $typeComposant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function setPosition(int $position): static
|
||||
{
|
||||
$this->position = $position;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,6 @@ final class ComposantAuditSubscriber extends AbstractAuditSubscriber
|
||||
'name' => $this->safeGet($entity, 'getName'),
|
||||
'reference' => $this->safeGet($entity, 'getReference'),
|
||||
'prix' => $this->safeGet($entity, 'getPrix'),
|
||||
'structure' => $this->safeGet($entity, 'getStructure'),
|
||||
'typeComposant' => $this->normalizeValue($this->safeGet($entity, 'getTypeComposant')),
|
||||
'product' => $this->normalizeValue($this->safeGet($entity, 'getProduct')),
|
||||
'constructeurIds' => $this->normalizeCollection($entity->getConstructeurs()),
|
||||
|
||||
@@ -12,7 +12,7 @@ use Doctrine\ORM\Event\PreUpdateEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
/**
|
||||
* Keep the legacy single product relation in sync with the new productIds array.
|
||||
* Keep the legacy single product relation in sync with the ManyToMany products collection.
|
||||
*/
|
||||
final class PieceProductSyncSubscriber implements EventSubscriber
|
||||
{
|
||||
|
||||
100
src/Mcp/Security/McpHeaderAuthenticator.php
Normal file
100
src/Mcp/Security/McpHeaderAuthenticator.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mcp\Security;
|
||||
|
||||
use App\Entity\Profile;
|
||||
use App\Repository\ProfileRepository;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\RateLimiter\RateLimiterFactory;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
|
||||
final class McpHeaderAuthenticator extends AbstractAuthenticator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProfileRepository $profiles,
|
||||
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||
private readonly RateLimiterFactory $mcpAuthLimiter,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
if (!$request->headers->has('X-Profile-Id') || !$request->headers->has('X-Profile-Password')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
$profileId = $request->headers->get('X-Profile-Id', '');
|
||||
$password = $request->headers->get('X-Profile-Password', '');
|
||||
|
||||
$limiter = $this->mcpAuthLimiter->create($request->getClientIp() ?? 'unknown');
|
||||
$limit = $limiter->consume(1);
|
||||
|
||||
if (!$limit->isAccepted()) {
|
||||
$this->logger->warning('MCP auth rate limited', ['ip' => $request->getClientIp()]);
|
||||
|
||||
throw new CustomUserMessageAuthenticationException('Rate limited: too many authentication attempts.');
|
||||
}
|
||||
|
||||
return new SelfValidatingPassport(
|
||||
new UserBadge($profileId, function (string $id) use ($password, $limiter, $request): Profile {
|
||||
$profile = $this->profiles->find($id);
|
||||
|
||||
if (!$profile || !$profile->isActive()) {
|
||||
$this->logger->warning('MCP auth failed: profile not found', ['profileId' => $id]);
|
||||
|
||||
throw new CustomUserMessageAuthenticationException('Authentication failed: invalid credentials.');
|
||||
}
|
||||
|
||||
if (!$this->passwordHasher->isPasswordValid($profile, $password)) {
|
||||
$this->logger->warning('MCP auth failed: invalid password', ['profileId' => $id]);
|
||||
|
||||
throw new CustomUserMessageAuthenticationException('Authentication failed: invalid credentials.');
|
||||
}
|
||||
|
||||
$limiter->reset();
|
||||
|
||||
$this->logger->info('MCP auth success', [
|
||||
'profileId' => $id,
|
||||
'roles' => $profile->getRoles(),
|
||||
'ip' => $request->getClientIp(),
|
||||
]);
|
||||
|
||||
return $profile;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
$statusCode = str_contains($exception->getMessageKey(), 'Rate limited')
|
||||
? Response::HTTP_TOO_MANY_REQUESTS
|
||||
: Response::HTTP_UNAUTHORIZED;
|
||||
|
||||
return new JsonResponse(
|
||||
['message' => $exception->getMessageKey()],
|
||||
$statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -325,8 +325,6 @@ final class ModelTypeCategoryConversionService
|
||||
$this->connection->executeStatement(
|
||||
'UPDATE model_types
|
||||
SET category = :cat,
|
||||
componentskeleton = pieceskeleton,
|
||||
pieceskeleton = NULL,
|
||||
updatedat = :now
|
||||
WHERE id = :id',
|
||||
[
|
||||
@@ -343,8 +341,8 @@ final class ModelTypeCategoryConversionService
|
||||
{
|
||||
// 1. Insert into pieces from composants
|
||||
$count = $this->connection->executeStatement(
|
||||
'INSERT INTO pieces (id, name, reference, prix, productids, typepieceid, productid, createdat, updatedat)
|
||||
SELECT id, name, reference, prix, NULL, typecomposantid, productid, createdat, updatedat
|
||||
'INSERT INTO pieces (id, name, reference, prix, typepieceid, productid, createdat, updatedat)
|
||||
SELECT id, name, reference, prix, typecomposantid, productid, createdat, updatedat
|
||||
FROM composants
|
||||
WHERE typecomposantid = :id',
|
||||
['id' => $modelTypeId],
|
||||
@@ -395,8 +393,6 @@ final class ModelTypeCategoryConversionService
|
||||
$this->connection->executeStatement(
|
||||
'UPDATE model_types
|
||||
SET category = :cat,
|
||||
pieceskeleton = componentskeleton,
|
||||
componentskeleton = NULL,
|
||||
updatedat = :now
|
||||
WHERE id = :id',
|
||||
[
|
||||
|
||||
44
src/Service/ModelTypeSyncService.php
Normal file
44
src/Service/ModelTypeSyncService.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\DTO\SyncConfirmation;
|
||||
use App\DTO\SyncExecutionResult;
|
||||
use App\DTO\SyncPreviewResult;
|
||||
use App\Entity\ModelType;
|
||||
use App\Service\Sync\SyncStrategyInterface;
|
||||
use LogicException;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
|
||||
|
||||
class ModelTypeSyncService
|
||||
{
|
||||
/** @param iterable<SyncStrategyInterface> $strategies */
|
||||
public function __construct(
|
||||
#[AutowireIterator('app.sync_strategy')]
|
||||
private readonly iterable $strategies,
|
||||
) {}
|
||||
|
||||
public function preview(ModelType $modelType, array $newStructure): SyncPreviewResult
|
||||
{
|
||||
foreach ($this->strategies as $strategy) {
|
||||
if ($strategy->supports($modelType)) {
|
||||
return $strategy->preview($modelType, $newStructure);
|
||||
}
|
||||
}
|
||||
|
||||
throw new LogicException('No sync strategy found for category: '.$modelType->getCategory()->value);
|
||||
}
|
||||
|
||||
public function execute(ModelType $modelType, SyncConfirmation $confirmation): SyncExecutionResult
|
||||
{
|
||||
foreach ($this->strategies as $strategy) {
|
||||
if ($strategy->supports($modelType)) {
|
||||
return $strategy->execute($modelType, $confirmation);
|
||||
}
|
||||
}
|
||||
|
||||
throw new LogicException('No sync strategy found for category: '.$modelType->getCategory()->value);
|
||||
}
|
||||
}
|
||||
282
src/Service/SkeletonStructureService.php
Normal file
282
src/Service/SkeletonStructureService.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\CustomField;
|
||||
use App\Entity\ModelType;
|
||||
use App\Entity\SkeletonPieceRequirement;
|
||||
use App\Entity\SkeletonProductRequirement;
|
||||
use App\Entity\SkeletonSubcomponentRequirement;
|
||||
use App\Enum\ModelCategory;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class SkeletonStructureService
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $em) {}
|
||||
|
||||
public function updateSkeletonRequirements(ModelType $modelType, array $structure): void
|
||||
{
|
||||
// Update piece requirements in-place (match by typeId, then update position)
|
||||
$this->syncPieceRequirements($modelType, $structure['pieces'] ?? []);
|
||||
|
||||
// Update product requirements in-place
|
||||
$this->syncProductRequirements($modelType, $structure['products'] ?? []);
|
||||
|
||||
// Update subcomponent requirements in-place
|
||||
$this->syncSubcomponentRequirements($modelType, $structure['subcomponents'] ?? []);
|
||||
|
||||
// Update custom field definitions
|
||||
$this->updateCustomFields($modelType, $structure['customFields'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{typePieceId: string}> $proposedPieces
|
||||
*/
|
||||
private function syncPieceRequirements(ModelType $modelType, array $proposedPieces): void
|
||||
{
|
||||
$existing = $modelType->getSkeletonPieceRequirements()->toArray();
|
||||
|
||||
// Index existing by typeId for matching
|
||||
$existingByTypeId = [];
|
||||
foreach ($existing as $req) {
|
||||
$existingByTypeId[$req->getTypePiece()->getId()][] = $req;
|
||||
}
|
||||
|
||||
$matched = [];
|
||||
$toCreate = [];
|
||||
|
||||
foreach ($proposedPieces as $i => $pieceData) {
|
||||
$typeId = $pieceData['typePieceId'];
|
||||
if (!empty($existingByTypeId[$typeId])) {
|
||||
// Reuse existing requirement, update position
|
||||
$req = array_shift($existingByTypeId[$typeId]);
|
||||
$req->setPosition($i);
|
||||
$matched[spl_object_id($req)] = true;
|
||||
} else {
|
||||
$toCreate[] = ['data' => $pieceData, 'position' => $i];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove unmatched existing requirements
|
||||
foreach ($existing as $req) {
|
||||
if (!isset($matched[spl_object_id($req)])) {
|
||||
$modelType->removeSkeletonPieceRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new requirements
|
||||
foreach ($toCreate as $item) {
|
||||
$req = new SkeletonPieceRequirement();
|
||||
$req->setModelType($modelType);
|
||||
$req->setTypePiece($this->em->getReference(ModelType::class, $item['data']['typePieceId']));
|
||||
$req->setPosition($item['position']);
|
||||
$modelType->addSkeletonPieceRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{typeProductId: string, familyCode?: ?string}> $proposedProducts
|
||||
*/
|
||||
private function syncProductRequirements(ModelType $modelType, array $proposedProducts): void
|
||||
{
|
||||
$existing = $modelType->getSkeletonProductRequirements()->toArray();
|
||||
|
||||
$existingByTypeId = [];
|
||||
foreach ($existing as $req) {
|
||||
$existingByTypeId[$req->getTypeProduct()->getId()][] = $req;
|
||||
}
|
||||
|
||||
$matched = [];
|
||||
$toCreate = [];
|
||||
|
||||
foreach ($proposedProducts as $i => $prodData) {
|
||||
$typeId = $prodData['typeProductId'];
|
||||
if (!empty($existingByTypeId[$typeId])) {
|
||||
$req = array_shift($existingByTypeId[$typeId]);
|
||||
$req->setFamilyCode($prodData['familyCode'] ?? null);
|
||||
$req->setPosition($i);
|
||||
$matched[spl_object_id($req)] = true;
|
||||
} else {
|
||||
$toCreate[] = ['data' => $prodData, 'position' => $i];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($existing as $req) {
|
||||
if (!isset($matched[spl_object_id($req)])) {
|
||||
$modelType->removeSkeletonProductRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($toCreate as $item) {
|
||||
$req = new SkeletonProductRequirement();
|
||||
$req->setModelType($modelType);
|
||||
$req->setTypeProduct($this->em->getReference(ModelType::class, $item['data']['typeProductId']));
|
||||
$req->setFamilyCode($item['data']['familyCode'] ?? null);
|
||||
$req->setPosition($item['position']);
|
||||
$modelType->addSkeletonProductRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{alias?: string, familyCode?: string, typeComposantId?: string}> $proposedSubs
|
||||
*/
|
||||
private function syncSubcomponentRequirements(ModelType $modelType, array $proposedSubs): void
|
||||
{
|
||||
$existing = $modelType->getSkeletonSubcomponentRequirements()->toArray();
|
||||
|
||||
$existingByTypeId = [];
|
||||
foreach ($existing as $req) {
|
||||
$key = $req->getTypeComposant()?->getId() ?? '';
|
||||
$existingByTypeId[$key][] = $req;
|
||||
}
|
||||
|
||||
$matched = [];
|
||||
$toCreate = [];
|
||||
|
||||
foreach ($proposedSubs as $i => $subData) {
|
||||
$typeId = $subData['typeComposantId'] ?? '';
|
||||
if (!empty($existingByTypeId[$typeId])) {
|
||||
$req = array_shift($existingByTypeId[$typeId]);
|
||||
$req->setAlias($subData['alias'] ?? '');
|
||||
$req->setFamilyCode($subData['familyCode'] ?? '');
|
||||
if (!empty($subData['typeComposantId'])) {
|
||||
$req->setTypeComposant($this->em->getReference(ModelType::class, $subData['typeComposantId']));
|
||||
}
|
||||
$req->setPosition($i);
|
||||
$matched[spl_object_id($req)] = true;
|
||||
} else {
|
||||
$toCreate[] = ['data' => $subData, 'position' => $i];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($existing as $req) {
|
||||
if (!isset($matched[spl_object_id($req)])) {
|
||||
$modelType->removeSkeletonSubcomponentRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($toCreate as $item) {
|
||||
$req = new SkeletonSubcomponentRequirement();
|
||||
$req->setModelType($modelType);
|
||||
$req->setAlias($item['data']['alias'] ?? '');
|
||||
$req->setFamilyCode($item['data']['familyCode'] ?? '');
|
||||
if (!empty($item['data']['typeComposantId'])) {
|
||||
$req->setTypeComposant($this->em->getReference(ModelType::class, $item['data']['typeComposantId']));
|
||||
}
|
||||
$req->setPosition($item['position']);
|
||||
$modelType->addSkeletonSubcomponentRequirement($req);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync CustomField entities for this ModelType.
|
||||
* Handles two frontend formats:
|
||||
* - COMPONENT: {key, value: {type, required, options?, defaultValue?}, id?, customFieldId?}
|
||||
* - PIECE/PRODUCT: {name, type, required, options?, orderIndex?, defaultValue?}.
|
||||
*/
|
||||
private function updateCustomFields(ModelType $modelType, array $proposedFields): void
|
||||
{
|
||||
// Determine which FK to use based on category
|
||||
$category = $modelType->getCategory();
|
||||
$fkField = match ($category) {
|
||||
ModelCategory::COMPONENT => 'typeComposant',
|
||||
ModelCategory::PIECE => 'typePiece',
|
||||
ModelCategory::PRODUCT => 'typeProduct',
|
||||
};
|
||||
|
||||
// Load existing custom fields
|
||||
$existingFields = $this->em->getRepository(CustomField::class)->findBy(
|
||||
[$fkField => $modelType],
|
||||
['orderIndex' => 'ASC']
|
||||
);
|
||||
|
||||
// Index existing by ID for matching
|
||||
$existingById = [];
|
||||
foreach ($existingFields as $cf) {
|
||||
$existingById[$cf->getId()] = $cf;
|
||||
}
|
||||
|
||||
$processedIds = [];
|
||||
|
||||
foreach ($proposedFields as $i => $fieldData) {
|
||||
// Normalize both formats to a common shape
|
||||
$normalized = $this->normalizeCustomFieldData($fieldData, $i);
|
||||
|
||||
// Try to match an existing field by ID
|
||||
$existingField = null;
|
||||
$fieldId = $fieldData['customFieldId'] ?? $fieldData['id'] ?? null;
|
||||
if ($fieldId && isset($existingById[$fieldId])) {
|
||||
$existingField = $existingById[$fieldId];
|
||||
}
|
||||
|
||||
if ($existingField) {
|
||||
// Update existing field
|
||||
$existingField->setName($normalized['name']);
|
||||
$existingField->setType($normalized['type']);
|
||||
$existingField->setRequired($normalized['required']);
|
||||
$existingField->setOptions($normalized['options']);
|
||||
$existingField->setDefaultValue($normalized['defaultValue']);
|
||||
$existingField->setOrderIndex($normalized['orderIndex']);
|
||||
$processedIds[$existingField->getId()] = true;
|
||||
} else {
|
||||
// Create new field
|
||||
$cf = new CustomField();
|
||||
$cf->setName($normalized['name']);
|
||||
$cf->setType($normalized['type']);
|
||||
$cf->setRequired($normalized['required']);
|
||||
$cf->setOptions($normalized['options']);
|
||||
$cf->setDefaultValue($normalized['defaultValue']);
|
||||
$cf->setOrderIndex($normalized['orderIndex']);
|
||||
|
||||
match ($category) {
|
||||
ModelCategory::COMPONENT => $cf->setTypeComposant($modelType),
|
||||
ModelCategory::PIECE => $cf->setTypePiece($modelType),
|
||||
ModelCategory::PRODUCT => $cf->setTypeProduct($modelType),
|
||||
};
|
||||
|
||||
$this->em->persist($cf);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove orphaned fields (exist in DB but not in proposed)
|
||||
foreach ($existingFields as $cf) {
|
||||
if (!isset($processedIds[$cf->getId()])) {
|
||||
$this->em->remove($cf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize frontend custom field data to a common shape.
|
||||
*
|
||||
* @return array{name: string, type: string, required: bool, options: ?array, defaultValue: ?string, orderIndex: int}
|
||||
*/
|
||||
private function normalizeCustomFieldData(array $fieldData, int $index): array
|
||||
{
|
||||
// COMPONENT format: {key: "name", value: {type, required, options?, defaultValue?}}
|
||||
if (isset($fieldData['key'], $fieldData['value'])) {
|
||||
$value = $fieldData['value'];
|
||||
|
||||
return [
|
||||
'name' => $fieldData['key'],
|
||||
'type' => $value['type'] ?? 'text',
|
||||
'required' => (bool) ($value['required'] ?? false),
|
||||
'options' => $value['options'] ?? null,
|
||||
'defaultValue' => $value['defaultValue'] ?? null,
|
||||
'orderIndex' => $index,
|
||||
];
|
||||
}
|
||||
|
||||
// PIECE/PRODUCT format: {name, type, required, options?, orderIndex?, defaultValue?}
|
||||
return [
|
||||
'name' => $fieldData['name'] ?? '',
|
||||
'type' => $fieldData['type'] ?? 'text',
|
||||
'required' => (bool) ($fieldData['required'] ?? false),
|
||||
'options' => $fieldData['options'] ?? null,
|
||||
'defaultValue' => $fieldData['defaultValue'] ?? null,
|
||||
'orderIndex' => $fieldData['orderIndex'] ?? $index,
|
||||
];
|
||||
}
|
||||
}
|
||||
447
src/Service/Sync/ComposantSyncStrategy.php
Normal file
447
src/Service/Sync/ComposantSyncStrategy.php
Normal file
@@ -0,0 +1,447 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Sync;
|
||||
|
||||
use App\DTO\SyncConfirmation;
|
||||
use App\DTO\SyncExecutionResult;
|
||||
use App\DTO\SyncPreviewResult;
|
||||
use App\Entity\Composant;
|
||||
use App\Entity\ComposantPieceSlot;
|
||||
use App\Entity\ComposantProductSlot;
|
||||
use App\Entity\ComposantSubcomponentSlot;
|
||||
use App\Entity\CustomField;
|
||||
use App\Entity\CustomFieldValue;
|
||||
use App\Entity\ModelType;
|
||||
use App\Entity\SkeletonPieceRequirement;
|
||||
use App\Entity\SkeletonProductRequirement;
|
||||
use App\Entity\SkeletonSubcomponentRequirement;
|
||||
use App\Enum\ModelCategory;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||
|
||||
#[AutoconfigureTag('app.sync_strategy')]
|
||||
class ComposantSyncStrategy implements SyncStrategyInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
public function supports(ModelType $modelType): bool
|
||||
{
|
||||
return ModelCategory::COMPONENT === $modelType->getCategory();
|
||||
}
|
||||
|
||||
public function preview(ModelType $modelType, array $newStructure): SyncPreviewResult
|
||||
{
|
||||
$composants = $this->em->getRepository(Composant::class)->findBy(['typeComposant' => $modelType]);
|
||||
|
||||
$proposedPieces = $newStructure['pieces'] ?? [];
|
||||
$proposedProducts = $newStructure['products'] ?? [];
|
||||
$proposedSubcomponents = $newStructure['subcomponents'] ?? [];
|
||||
$proposedCustomFields = $newStructure['customFields'] ?? [];
|
||||
|
||||
$addedPieceSlots = 0;
|
||||
$deletedPieceSlots = 0;
|
||||
$addedProductSlots = 0;
|
||||
$deletedProductSlots = 0;
|
||||
$addedSubSlots = 0;
|
||||
$deletedSubSlots = 0;
|
||||
$addedCfValues = 0;
|
||||
$deletedCfValues = 0;
|
||||
|
||||
// Build proposed typeId lists (one entry per requirement, order = position)
|
||||
$proposedPieceTypeIds = [];
|
||||
foreach ($proposedPieces as $pp) {
|
||||
$proposedPieceTypeIds[] = $pp['typePieceId'];
|
||||
}
|
||||
|
||||
$proposedProductTypeIds = [];
|
||||
foreach ($proposedProducts as $pp) {
|
||||
$proposedProductTypeIds[] = $pp['typeProductId'];
|
||||
}
|
||||
|
||||
$proposedSubTypeIds = [];
|
||||
foreach ($proposedSubcomponents as $ps) {
|
||||
$proposedSubTypeIds[] = $ps['typeComposantId'];
|
||||
}
|
||||
|
||||
// Map proposed custom fields by orderIndex (falls back to array index)
|
||||
$proposedCfByOrder = [];
|
||||
foreach ($proposedCustomFields as $i => $pcf) {
|
||||
$order = $pcf['orderIndex'] ?? $i;
|
||||
$proposedCfByOrder[$order] = $pcf;
|
||||
}
|
||||
|
||||
// Get existing custom fields for this model type
|
||||
$existingFields = $this->em->getRepository(CustomField::class)->findBy(
|
||||
['typeComposant' => $modelType],
|
||||
['orderIndex' => 'ASC']
|
||||
);
|
||||
$existingCfByOrder = [];
|
||||
foreach ($existingFields as $field) {
|
||||
$existingCfByOrder[$field->getOrderIndex()] = $field;
|
||||
}
|
||||
|
||||
// Count custom field additions/deletions (definition-level, affects all composants)
|
||||
$cfAdded = 0;
|
||||
$cfDeleted = 0;
|
||||
foreach ($proposedCfByOrder as $orderIndex => $pcf) {
|
||||
if (!isset($existingCfByOrder[$orderIndex])) {
|
||||
++$cfAdded;
|
||||
}
|
||||
}
|
||||
foreach ($existingCfByOrder as $orderIndex => $ef) {
|
||||
if (!isset($proposedCfByOrder[$orderIndex])) {
|
||||
++$cfDeleted;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($composants as $composant) {
|
||||
// Piece slots
|
||||
$pieceSlots = $this->em->getRepository(ComposantPieceSlot::class)->findBy(['composant' => $composant]);
|
||||
$existingPieceTypes = array_map(fn (ComposantPieceSlot $s) => $s->getTypePiece()?->getId() ?? '', $pieceSlots);
|
||||
$result = $this->smartMatchPreview($existingPieceTypes, $proposedPieceTypeIds);
|
||||
$addedPieceSlots += $result['added'];
|
||||
$deletedPieceSlots += $result['deleted'];
|
||||
|
||||
// Product slots
|
||||
$productSlots = $this->em->getRepository(ComposantProductSlot::class)->findBy(['composant' => $composant]);
|
||||
$existingProductTypes = array_map(fn (ComposantProductSlot $s) => $s->getTypeProduct()?->getId() ?? '', $productSlots);
|
||||
$result = $this->smartMatchPreview($existingProductTypes, $proposedProductTypeIds);
|
||||
$addedProductSlots += $result['added'];
|
||||
$deletedProductSlots += $result['deleted'];
|
||||
|
||||
// Subcomponent slots
|
||||
$subSlots = $this->em->getRepository(ComposantSubcomponentSlot::class)->findBy(['composant' => $composant]);
|
||||
$existingSubTypes = array_map(fn (ComposantSubcomponentSlot $s) => $s->getTypeComposant()?->getId() ?? '', $subSlots);
|
||||
$result = $this->smartMatchPreview($existingSubTypes, $proposedSubTypeIds);
|
||||
$addedSubSlots += $result['added'];
|
||||
$deletedSubSlots += $result['deleted'];
|
||||
|
||||
// Custom field values
|
||||
$addedCfValues += $cfAdded;
|
||||
$deletedCfValues += $cfDeleted;
|
||||
}
|
||||
|
||||
$itemCount = count($composants);
|
||||
|
||||
return new SyncPreviewResult(
|
||||
modelTypeId: $modelType->getId(),
|
||||
category: 'component',
|
||||
itemCount: $itemCount,
|
||||
additions: [
|
||||
'pieceSlots' => $addedPieceSlots,
|
||||
'productSlots' => $addedProductSlots,
|
||||
'subcomponentSlots' => $addedSubSlots,
|
||||
'customFieldValues' => $addedCfValues,
|
||||
],
|
||||
deletions: [
|
||||
'pieceSlots' => $deletedPieceSlots,
|
||||
'productSlots' => $deletedProductSlots,
|
||||
'subcomponentSlots' => $deletedSubSlots,
|
||||
'customFieldValues' => $deletedCfValues,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(ModelType $modelType, SyncConfirmation $confirmation): SyncExecutionResult
|
||||
{
|
||||
$composants = $this->em->getRepository(Composant::class)->findBy(['typeComposant' => $modelType]);
|
||||
|
||||
// Load skeleton requirements
|
||||
$pieceReqs = $this->em->getRepository(SkeletonPieceRequirement::class)->findBy(['modelType' => $modelType], ['position' => 'ASC']);
|
||||
$productReqs = $this->em->getRepository(SkeletonProductRequirement::class)->findBy(['modelType' => $modelType], ['position' => 'ASC']);
|
||||
$subReqs = $this->em->getRepository(SkeletonSubcomponentRequirement::class)->findBy(['modelType' => $modelType], ['position' => 'ASC']);
|
||||
$customFields = $this->em->getRepository(CustomField::class)->findBy(
|
||||
['typeComposant' => $modelType],
|
||||
['orderIndex' => 'ASC']
|
||||
);
|
||||
|
||||
$addedPieceSlots = 0;
|
||||
$deletedPieceSlots = 0;
|
||||
$addedProductSlots = 0;
|
||||
$deletedProductSlots = 0;
|
||||
$addedSubSlots = 0;
|
||||
$deletedSubSlots = 0;
|
||||
$addedCfValues = 0;
|
||||
$deletedCfValues = 0;
|
||||
$itemsUpdated = 0;
|
||||
|
||||
foreach ($composants as $composant) {
|
||||
$changed = false;
|
||||
|
||||
// --- Piece slots ---
|
||||
$pieceSlotEntities = $this->em->getRepository(ComposantPieceSlot::class)->findBy(['composant' => $composant]);
|
||||
$existingPieceTypeIds = array_map(fn (ComposantPieceSlot $s) => $s->getTypePiece()?->getId() ?? '', $pieceSlotEntities);
|
||||
$reqPieceTypeIds = array_map(fn (SkeletonPieceRequirement $r) => $r->getTypePiece()->getId(), $pieceReqs);
|
||||
$matchResult = $this->smartMatch($existingPieceTypeIds, $reqPieceTypeIds);
|
||||
|
||||
// Update matched slots (position may have changed)
|
||||
foreach ($matchResult['matched'] as [$slotIdx, $reqIdx]) {
|
||||
$slot = $pieceSlotEntities[$slotIdx];
|
||||
$req = $pieceReqs[$reqIdx];
|
||||
if ($slot->getPosition() !== $req->getPosition()) {
|
||||
$slot->setPosition($req->getPosition());
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new piece slots for unmatched requirements
|
||||
foreach ($matchResult['unmatchedReqs'] as $reqIdx) {
|
||||
$req = $pieceReqs[$reqIdx];
|
||||
$slot = new ComposantPieceSlot();
|
||||
$slot->setComposant($composant);
|
||||
$slot->setTypePiece($req->getTypePiece());
|
||||
$slot->setPosition($req->getPosition());
|
||||
$this->em->persist($slot);
|
||||
++$addedPieceSlots;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
// Delete orphaned piece slots
|
||||
if ($confirmation->confirmDeletions) {
|
||||
foreach ($matchResult['orphanedSlots'] as $slotIdx) {
|
||||
$slot = $pieceSlotEntities[$slotIdx];
|
||||
$composant->removePieceSlot($slot);
|
||||
$this->em->remove($slot);
|
||||
++$deletedPieceSlots;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Product slots ---
|
||||
$productSlotEntities = $this->em->getRepository(ComposantProductSlot::class)->findBy(['composant' => $composant]);
|
||||
$existingProductTypeIds = array_map(fn (ComposantProductSlot $s) => $s->getTypeProduct()?->getId() ?? '', $productSlotEntities);
|
||||
$reqProductTypeIds = array_map(fn (SkeletonProductRequirement $r) => $r->getTypeProduct()->getId(), $productReqs);
|
||||
$matchResult = $this->smartMatch($existingProductTypeIds, $reqProductTypeIds);
|
||||
|
||||
// Update matched slots
|
||||
foreach ($matchResult['matched'] as [$slotIdx, $reqIdx]) {
|
||||
$slot = $productSlotEntities[$slotIdx];
|
||||
$req = $productReqs[$reqIdx];
|
||||
if ($slot->getPosition() !== $req->getPosition()) {
|
||||
$slot->setPosition($req->getPosition());
|
||||
$changed = true;
|
||||
}
|
||||
if ($slot->getFamilyCode() !== $req->getFamilyCode()) {
|
||||
$slot->setFamilyCode($req->getFamilyCode());
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new product slots
|
||||
foreach ($matchResult['unmatchedReqs'] as $reqIdx) {
|
||||
$req = $productReqs[$reqIdx];
|
||||
$slot = new ComposantProductSlot();
|
||||
$slot->setComposant($composant);
|
||||
$slot->setTypeProduct($req->getTypeProduct());
|
||||
$slot->setPosition($req->getPosition());
|
||||
if (null !== $req->getFamilyCode()) {
|
||||
$slot->setFamilyCode($req->getFamilyCode());
|
||||
}
|
||||
$this->em->persist($slot);
|
||||
++$addedProductSlots;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
// Delete orphaned product slots
|
||||
if ($confirmation->confirmDeletions) {
|
||||
foreach ($matchResult['orphanedSlots'] as $slotIdx) {
|
||||
$slot = $productSlotEntities[$slotIdx];
|
||||
$composant->removeProductSlot($slot);
|
||||
$this->em->remove($slot);
|
||||
++$deletedProductSlots;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Subcomponent slots ---
|
||||
$subSlotEntities = $this->em->getRepository(ComposantSubcomponentSlot::class)->findBy(['composant' => $composant]);
|
||||
$existingSubTypeIds = array_map(fn (ComposantSubcomponentSlot $s) => $s->getTypeComposant()?->getId() ?? '', $subSlotEntities);
|
||||
$reqSubTypeIds = array_map(fn (SkeletonSubcomponentRequirement $r) => $r->getTypeComposant()?->getId() ?? '', $subReqs);
|
||||
$matchResult = $this->smartMatch($existingSubTypeIds, $reqSubTypeIds);
|
||||
|
||||
// Update matched slots
|
||||
foreach ($matchResult['matched'] as [$slotIdx, $reqIdx]) {
|
||||
$slot = $subSlotEntities[$slotIdx];
|
||||
$req = $subReqs[$reqIdx];
|
||||
if ($slot->getPosition() !== $req->getPosition()) {
|
||||
$slot->setPosition($req->getPosition());
|
||||
$changed = true;
|
||||
}
|
||||
if ($slot->getAlias() !== $req->getAlias()) {
|
||||
$slot->setAlias($req->getAlias());
|
||||
$changed = true;
|
||||
}
|
||||
if ($slot->getFamilyCode() !== $req->getFamilyCode()) {
|
||||
$slot->setFamilyCode($req->getFamilyCode());
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new subcomponent slots
|
||||
foreach ($matchResult['unmatchedReqs'] as $reqIdx) {
|
||||
$req = $subReqs[$reqIdx];
|
||||
$slot = new ComposantSubcomponentSlot();
|
||||
$slot->setComposant($composant);
|
||||
$slot->setTypeComposant($req->getTypeComposant());
|
||||
$slot->setPosition($req->getPosition());
|
||||
$slot->setAlias($req->getAlias());
|
||||
$slot->setFamilyCode($req->getFamilyCode());
|
||||
$this->em->persist($slot);
|
||||
++$addedSubSlots;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
// Delete orphaned subcomponent slots
|
||||
if ($confirmation->confirmDeletions) {
|
||||
foreach ($matchResult['orphanedSlots'] as $slotIdx) {
|
||||
$slot = $subSlotEntities[$slotIdx];
|
||||
$composant->removeSubcomponentSlot($slot);
|
||||
$this->em->remove($slot);
|
||||
++$deletedSubSlots;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Custom field values ---
|
||||
$existingValues = $this->em->getRepository(CustomFieldValue::class)->findBy([
|
||||
'composant' => $composant,
|
||||
]);
|
||||
|
||||
$existingByFieldId = [];
|
||||
foreach ($existingValues as $cfv) {
|
||||
$existingByFieldId[$cfv->getCustomField()->getId()] = $cfv;
|
||||
}
|
||||
|
||||
// Add missing custom field values
|
||||
foreach ($customFields as $cf) {
|
||||
if (!isset($existingByFieldId[$cf->getId()])) {
|
||||
$cfv = new CustomFieldValue();
|
||||
$cfv->setCustomField($cf);
|
||||
$cfv->setComposant($composant);
|
||||
$cfv->setValue('');
|
||||
$this->em->persist($cfv);
|
||||
++$addedCfValues;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete orphaned custom field values
|
||||
if ($confirmation->confirmDeletions) {
|
||||
$fieldIds = array_map(fn (CustomField $cf) => $cf->getId(), $customFields);
|
||||
foreach ($existingValues as $cfv) {
|
||||
if (!in_array($cfv->getCustomField()->getId(), $fieldIds, true)) {
|
||||
$this->em->remove($cfv);
|
||||
++$deletedCfValues;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($changed) {
|
||||
$composant->incrementVersion();
|
||||
++$itemsUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return new SyncExecutionResult(
|
||||
itemsUpdated: $itemsUpdated,
|
||||
additions: [
|
||||
'pieceSlots' => $addedPieceSlots,
|
||||
'productSlots' => $addedProductSlots,
|
||||
'subcomponentSlots' => $addedSubSlots,
|
||||
'customFieldValues' => $addedCfValues,
|
||||
],
|
||||
deletions: [
|
||||
'pieceSlots' => $deletedPieceSlots,
|
||||
'productSlots' => $deletedProductSlots,
|
||||
'subcomponentSlots' => $deletedSubSlots,
|
||||
'customFieldValues' => $deletedCfValues,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart-match existing slots to proposed requirements by typeId.
|
||||
*
|
||||
* Pass 1: exact match by typeId + position index.
|
||||
* Pass 2: match remaining by typeId only (handles reordering/insertion).
|
||||
*
|
||||
* @param string[] $existingTypeIds typeIds of existing slots (index = slot index)
|
||||
* @param string[] $proposedTypeIds typeIds of proposed requirements (index = req index)
|
||||
*
|
||||
* @return array{matched: list<array{int, int}>, orphanedSlots: int[], unmatchedReqs: int[]}
|
||||
*/
|
||||
private function smartMatch(array $existingTypeIds, array $proposedTypeIds): array
|
||||
{
|
||||
$matchedSlots = [];
|
||||
$matchedReqs = [];
|
||||
$matched = [];
|
||||
|
||||
// Pass 1: exact match where typeId AND position index are identical
|
||||
foreach ($proposedTypeIds as $reqIdx => $reqTypeId) {
|
||||
if (isset($existingTypeIds[$reqIdx]) && $existingTypeIds[$reqIdx] === $reqTypeId && !isset($matchedSlots[$reqIdx])) {
|
||||
$matched[] = [$reqIdx, $reqIdx];
|
||||
$matchedSlots[$reqIdx] = true;
|
||||
$matchedReqs[$reqIdx] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: match remaining by typeId only (preserves selections on reorder)
|
||||
$remainingSlotsByType = [];
|
||||
foreach ($existingTypeIds as $slotIdx => $typeId) {
|
||||
if (!isset($matchedSlots[$slotIdx])) {
|
||||
$remainingSlotsByType[$typeId][] = $slotIdx;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($proposedTypeIds as $reqIdx => $reqTypeId) {
|
||||
if (!isset($matchedReqs[$reqIdx]) && !empty($remainingSlotsByType[$reqTypeId])) {
|
||||
$slotIdx = array_shift($remainingSlotsByType[$reqTypeId]);
|
||||
$matched[] = [$slotIdx, $reqIdx];
|
||||
$matchedSlots[$slotIdx] = true;
|
||||
$matchedReqs[$reqIdx] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect unmatched
|
||||
$orphanedSlots = [];
|
||||
foreach ($existingTypeIds as $slotIdx => $_) {
|
||||
if (!isset($matchedSlots[$slotIdx])) {
|
||||
$orphanedSlots[] = $slotIdx;
|
||||
}
|
||||
}
|
||||
|
||||
$unmatchedReqs = [];
|
||||
foreach ($proposedTypeIds as $reqIdx => $_) {
|
||||
if (!isset($matchedReqs[$reqIdx])) {
|
||||
$unmatchedReqs[] = $reqIdx;
|
||||
}
|
||||
}
|
||||
|
||||
return ['matched' => $matched, 'orphanedSlots' => $orphanedSlots, 'unmatchedReqs' => $unmatchedReqs];
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview version of smart matching — counts additions and deletions.
|
||||
*
|
||||
* @param string[] $existingTypeIds
|
||||
* @param string[] $proposedTypeIds
|
||||
*
|
||||
* @return array{added: int, deleted: int}
|
||||
*/
|
||||
private function smartMatchPreview(array $existingTypeIds, array $proposedTypeIds): array
|
||||
{
|
||||
$result = $this->smartMatch($existingTypeIds, $proposedTypeIds);
|
||||
|
||||
return [
|
||||
'added' => count($result['unmatchedReqs']),
|
||||
'deleted' => count($result['orphanedSlots']),
|
||||
];
|
||||
}
|
||||
}
|
||||
309
src/Service/Sync/PieceSyncStrategy.php
Normal file
309
src/Service/Sync/PieceSyncStrategy.php
Normal file
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Sync;
|
||||
|
||||
use App\DTO\SyncConfirmation;
|
||||
use App\DTO\SyncExecutionResult;
|
||||
use App\DTO\SyncPreviewResult;
|
||||
use App\Entity\CustomField;
|
||||
use App\Entity\CustomFieldValue;
|
||||
use App\Entity\ModelType;
|
||||
use App\Entity\Piece;
|
||||
use App\Entity\PieceProductSlot;
|
||||
use App\Entity\SkeletonProductRequirement;
|
||||
use App\Enum\ModelCategory;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||
|
||||
#[AutoconfigureTag('app.sync_strategy')]
|
||||
class PieceSyncStrategy implements SyncStrategyInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
public function supports(ModelType $modelType): bool
|
||||
{
|
||||
return ModelCategory::PIECE === $modelType->getCategory();
|
||||
}
|
||||
|
||||
public function preview(ModelType $modelType, array $newStructure): SyncPreviewResult
|
||||
{
|
||||
$pieces = $this->em->getRepository(Piece::class)->findBy(['typePiece' => $modelType]);
|
||||
|
||||
$proposedProducts = $newStructure['products'] ?? [];
|
||||
$proposedCustomFields = $newStructure['customFields'] ?? [];
|
||||
|
||||
$addedProductSlots = 0;
|
||||
$deletedProductSlots = 0;
|
||||
$addedCfValues = 0;
|
||||
$deletedCfValues = 0;
|
||||
|
||||
// Build proposed typeId list
|
||||
$proposedProductTypeIds = [];
|
||||
foreach ($proposedProducts as $pp) {
|
||||
$proposedProductTypeIds[] = $pp['typeProductId'];
|
||||
}
|
||||
|
||||
// Map proposed custom fields by orderIndex (falls back to array index)
|
||||
$proposedCfByOrder = [];
|
||||
foreach ($proposedCustomFields as $i => $pcf) {
|
||||
$order = $pcf['orderIndex'] ?? $i;
|
||||
$proposedCfByOrder[$order] = $pcf;
|
||||
}
|
||||
|
||||
// Get existing custom fields for this model type
|
||||
$existingFields = $this->em->getRepository(CustomField::class)->findBy(
|
||||
['typePiece' => $modelType],
|
||||
['orderIndex' => 'ASC']
|
||||
);
|
||||
$existingCfByOrder = [];
|
||||
foreach ($existingFields as $field) {
|
||||
$existingCfByOrder[$field->getOrderIndex()] = $field;
|
||||
}
|
||||
|
||||
// Count custom field additions/deletions (definition-level, affects all pieces)
|
||||
$cfAdded = 0;
|
||||
$cfDeleted = 0;
|
||||
foreach ($proposedCfByOrder as $orderIndex => $pcf) {
|
||||
if (!isset($existingCfByOrder[$orderIndex])) {
|
||||
++$cfAdded;
|
||||
}
|
||||
}
|
||||
foreach ($existingCfByOrder as $orderIndex => $ef) {
|
||||
if (!isset($proposedCfByOrder[$orderIndex])) {
|
||||
++$cfDeleted;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($pieces as $piece) {
|
||||
// Product slots — smart matching by typeId
|
||||
$productSlots = $this->em->getRepository(PieceProductSlot::class)->findBy(['piece' => $piece]);
|
||||
$existingProductTypes = array_map(fn (PieceProductSlot $s) => $s->getTypeProduct()?->getId() ?? '', $productSlots);
|
||||
$result = $this->smartMatchPreview($existingProductTypes, $proposedProductTypeIds);
|
||||
$addedProductSlots += $result['added'];
|
||||
$deletedProductSlots += $result['deleted'];
|
||||
|
||||
// Custom field values
|
||||
$addedCfValues += $cfAdded;
|
||||
$deletedCfValues += $cfDeleted;
|
||||
}
|
||||
|
||||
$itemCount = count($pieces);
|
||||
|
||||
return new SyncPreviewResult(
|
||||
modelTypeId: $modelType->getId(),
|
||||
category: 'piece',
|
||||
itemCount: $itemCount,
|
||||
additions: [
|
||||
'productSlots' => $addedProductSlots,
|
||||
'customFieldValues' => $addedCfValues,
|
||||
],
|
||||
deletions: [
|
||||
'productSlots' => $deletedProductSlots,
|
||||
'customFieldValues' => $deletedCfValues,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(ModelType $modelType, SyncConfirmation $confirmation): SyncExecutionResult
|
||||
{
|
||||
$pieces = $this->em->getRepository(Piece::class)->findBy(['typePiece' => $modelType]);
|
||||
|
||||
// Load skeleton requirements
|
||||
$productReqs = $this->em->getRepository(SkeletonProductRequirement::class)->findBy(['modelType' => $modelType], ['position' => 'ASC']);
|
||||
$customFields = $this->em->getRepository(CustomField::class)->findBy(
|
||||
['typePiece' => $modelType],
|
||||
['orderIndex' => 'ASC']
|
||||
);
|
||||
|
||||
$addedProductSlots = 0;
|
||||
$deletedProductSlots = 0;
|
||||
$addedCfValues = 0;
|
||||
$deletedCfValues = 0;
|
||||
$itemsUpdated = 0;
|
||||
|
||||
foreach ($pieces as $piece) {
|
||||
$changed = false;
|
||||
|
||||
// --- Product slots ---
|
||||
$productSlotEntities = $this->em->getRepository(PieceProductSlot::class)->findBy(['piece' => $piece]);
|
||||
$existingProductTypeIds = array_map(fn (PieceProductSlot $s) => $s->getTypeProduct()?->getId() ?? '', $productSlotEntities);
|
||||
$reqProductTypeIds = array_map(fn (SkeletonProductRequirement $r) => $r->getTypeProduct()->getId(), $productReqs);
|
||||
$matchResult = $this->smartMatch($existingProductTypeIds, $reqProductTypeIds);
|
||||
|
||||
// Update matched slots (position/familyCode may have changed)
|
||||
foreach ($matchResult['matched'] as [$slotIdx, $reqIdx]) {
|
||||
$slot = $productSlotEntities[$slotIdx];
|
||||
$req = $productReqs[$reqIdx];
|
||||
if ($slot->getPosition() !== $req->getPosition()) {
|
||||
$slot->setPosition($req->getPosition());
|
||||
$changed = true;
|
||||
}
|
||||
if ($slot->getFamilyCode() !== $req->getFamilyCode()) {
|
||||
$slot->setFamilyCode($req->getFamilyCode());
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new product slots
|
||||
foreach ($matchResult['unmatchedReqs'] as $reqIdx) {
|
||||
$req = $productReqs[$reqIdx];
|
||||
$slot = new PieceProductSlot();
|
||||
$slot->setPiece($piece);
|
||||
$slot->setTypeProduct($req->getTypeProduct());
|
||||
$slot->setPosition($req->getPosition());
|
||||
if (null !== $req->getFamilyCode()) {
|
||||
$slot->setFamilyCode($req->getFamilyCode());
|
||||
}
|
||||
$this->em->persist($slot);
|
||||
++$addedProductSlots;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
// Delete orphaned product slots
|
||||
if ($confirmation->confirmDeletions) {
|
||||
foreach ($matchResult['orphanedSlots'] as $slotIdx) {
|
||||
$slot = $productSlotEntities[$slotIdx];
|
||||
$piece->removeProductSlot($slot);
|
||||
$this->em->remove($slot);
|
||||
++$deletedProductSlots;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Custom field values ---
|
||||
$existingValues = $this->em->getRepository(CustomFieldValue::class)->findBy([
|
||||
'piece' => $piece,
|
||||
]);
|
||||
|
||||
$existingByFieldId = [];
|
||||
foreach ($existingValues as $cfv) {
|
||||
$existingByFieldId[$cfv->getCustomField()->getId()] = $cfv;
|
||||
}
|
||||
|
||||
// Add missing custom field values
|
||||
foreach ($customFields as $cf) {
|
||||
if (!isset($existingByFieldId[$cf->getId()])) {
|
||||
$cfv = new CustomFieldValue();
|
||||
$cfv->setCustomField($cf);
|
||||
$cfv->setPiece($piece);
|
||||
$cfv->setValue('');
|
||||
$this->em->persist($cfv);
|
||||
++$addedCfValues;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete orphaned custom field values
|
||||
if ($confirmation->confirmDeletions) {
|
||||
$fieldIds = array_map(fn (CustomField $cf) => $cf->getId(), $customFields);
|
||||
foreach ($existingValues as $cfv) {
|
||||
if (!in_array($cfv->getCustomField()->getId(), $fieldIds, true)) {
|
||||
$this->em->remove($cfv);
|
||||
++$deletedCfValues;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($changed) {
|
||||
$piece->incrementVersion();
|
||||
++$itemsUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return new SyncExecutionResult(
|
||||
itemsUpdated: $itemsUpdated,
|
||||
additions: [
|
||||
'productSlots' => $addedProductSlots,
|
||||
'customFieldValues' => $addedCfValues,
|
||||
],
|
||||
deletions: [
|
||||
'productSlots' => $deletedProductSlots,
|
||||
'customFieldValues' => $deletedCfValues,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart-match existing slots to proposed requirements by typeId.
|
||||
*
|
||||
* Pass 1: exact match by typeId + position index.
|
||||
* Pass 2: match remaining by typeId only (handles reordering/insertion).
|
||||
*
|
||||
* @param string[] $existingTypeIds typeIds of existing slots (index = slot index)
|
||||
* @param string[] $proposedTypeIds typeIds of proposed requirements (index = req index)
|
||||
*
|
||||
* @return array{matched: list<array{int, int}>, orphanedSlots: int[], unmatchedReqs: int[]}
|
||||
*/
|
||||
private function smartMatch(array $existingTypeIds, array $proposedTypeIds): array
|
||||
{
|
||||
$matchedSlots = [];
|
||||
$matchedReqs = [];
|
||||
$matched = [];
|
||||
|
||||
// Pass 1: exact match where typeId AND position index are identical
|
||||
foreach ($proposedTypeIds as $reqIdx => $reqTypeId) {
|
||||
if (isset($existingTypeIds[$reqIdx]) && $existingTypeIds[$reqIdx] === $reqTypeId && !isset($matchedSlots[$reqIdx])) {
|
||||
$matched[] = [$reqIdx, $reqIdx];
|
||||
$matchedSlots[$reqIdx] = true;
|
||||
$matchedReqs[$reqIdx] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: match remaining by typeId only (preserves selections on reorder)
|
||||
$remainingSlotsByType = [];
|
||||
foreach ($existingTypeIds as $slotIdx => $typeId) {
|
||||
if (!isset($matchedSlots[$slotIdx])) {
|
||||
$remainingSlotsByType[$typeId][] = $slotIdx;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($proposedTypeIds as $reqIdx => $reqTypeId) {
|
||||
if (!isset($matchedReqs[$reqIdx]) && !empty($remainingSlotsByType[$reqTypeId])) {
|
||||
$slotIdx = array_shift($remainingSlotsByType[$reqTypeId]);
|
||||
$matched[] = [$slotIdx, $reqIdx];
|
||||
$matchedSlots[$slotIdx] = true;
|
||||
$matchedReqs[$reqIdx] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect unmatched
|
||||
$orphanedSlots = [];
|
||||
foreach ($existingTypeIds as $slotIdx => $_) {
|
||||
if (!isset($matchedSlots[$slotIdx])) {
|
||||
$orphanedSlots[] = $slotIdx;
|
||||
}
|
||||
}
|
||||
|
||||
$unmatchedReqs = [];
|
||||
foreach ($proposedTypeIds as $reqIdx => $_) {
|
||||
if (!isset($matchedReqs[$reqIdx])) {
|
||||
$unmatchedReqs[] = $reqIdx;
|
||||
}
|
||||
}
|
||||
|
||||
return ['matched' => $matched, 'orphanedSlots' => $orphanedSlots, 'unmatchedReqs' => $unmatchedReqs];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $existingTypeIds
|
||||
* @param string[] $proposedTypeIds
|
||||
*
|
||||
* @return array{added: int, deleted: int}
|
||||
*/
|
||||
private function smartMatchPreview(array $existingTypeIds, array $proposedTypeIds): array
|
||||
{
|
||||
$result = $this->smartMatch($existingTypeIds, $proposedTypeIds);
|
||||
|
||||
return [
|
||||
'added' => count($result['unmatchedReqs']),
|
||||
'deleted' => count($result['orphanedSlots']),
|
||||
];
|
||||
}
|
||||
}
|
||||
153
src/Service/Sync/ProductSyncStrategy.php
Normal file
153
src/Service/Sync/ProductSyncStrategy.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Sync;
|
||||
|
||||
use App\DTO\SyncConfirmation;
|
||||
use App\DTO\SyncExecutionResult;
|
||||
use App\DTO\SyncPreviewResult;
|
||||
use App\Entity\CustomField;
|
||||
use App\Entity\CustomFieldValue;
|
||||
use App\Entity\ModelType;
|
||||
use App\Entity\Product;
|
||||
use App\Enum\ModelCategory;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||
|
||||
#[AutoconfigureTag('app.sync_strategy')]
|
||||
class ProductSyncStrategy implements SyncStrategyInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
public function supports(ModelType $modelType): bool
|
||||
{
|
||||
return ModelCategory::PRODUCT === $modelType->getCategory();
|
||||
}
|
||||
|
||||
public function preview(ModelType $modelType, array $newStructure): SyncPreviewResult
|
||||
{
|
||||
$products = $this->em->getRepository(Product::class)->findBy(['typeProduct' => $modelType]);
|
||||
$existingFields = $this->em->getRepository(CustomField::class)->findBy(
|
||||
['typeProduct' => $modelType],
|
||||
['orderIndex' => 'ASC']
|
||||
);
|
||||
|
||||
$proposedFields = $newStructure['customFields'] ?? [];
|
||||
|
||||
// Map existing fields by orderIndex
|
||||
$existingByOrder = [];
|
||||
foreach ($existingFields as $field) {
|
||||
$existingByOrder[$field->getOrderIndex()] = $field;
|
||||
}
|
||||
|
||||
// Map proposed fields by orderIndex (falls back to array index)
|
||||
$proposedByOrder = [];
|
||||
foreach ($proposedFields as $i => $pf) {
|
||||
$order = $pf['orderIndex'] ?? $i;
|
||||
$proposedByOrder[$order] = $pf;
|
||||
}
|
||||
|
||||
$addedFields = 0;
|
||||
$deletedFields = 0;
|
||||
$modifiedFields = 0;
|
||||
|
||||
// New fields (in proposed but not in existing)
|
||||
foreach ($proposedByOrder as $orderIndex => $pf) {
|
||||
if (!isset($existingByOrder[$orderIndex])) {
|
||||
++$addedFields;
|
||||
} elseif ($existingByOrder[$orderIndex]->getType() !== ($pf['type'] ?? $pf['value']['type'] ?? null)) {
|
||||
++$modifiedFields;
|
||||
}
|
||||
}
|
||||
|
||||
// Deleted fields (in existing but not in proposed)
|
||||
foreach ($existingByOrder as $orderIndex => $ef) {
|
||||
if (!isset($proposedByOrder[$orderIndex])) {
|
||||
++$deletedFields;
|
||||
}
|
||||
}
|
||||
|
||||
$itemCount = count($products);
|
||||
|
||||
return new SyncPreviewResult(
|
||||
modelTypeId: $modelType->getId(),
|
||||
category: 'product',
|
||||
itemCount: $itemCount,
|
||||
additions: ['customFieldValues' => $addedFields * $itemCount],
|
||||
deletions: ['customFieldValues' => $deletedFields * $itemCount],
|
||||
modifications: ['customFieldValues' => $modifiedFields * $itemCount],
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(ModelType $modelType, SyncConfirmation $confirmation): SyncExecutionResult
|
||||
{
|
||||
$products = $this->em->getRepository(Product::class)->findBy(['typeProduct' => $modelType]);
|
||||
$customFields = $this->em->getRepository(CustomField::class)->findBy(
|
||||
['typeProduct' => $modelType],
|
||||
['orderIndex' => 'ASC']
|
||||
);
|
||||
|
||||
$addedValues = 0;
|
||||
$deletedValues = 0;
|
||||
$modifiedValues = 0;
|
||||
$itemsUpdated = 0;
|
||||
|
||||
foreach ($products as $product) {
|
||||
$changed = false;
|
||||
|
||||
// Get existing custom field values for this product
|
||||
$existingValues = $this->em->getRepository(CustomFieldValue::class)->findBy([
|
||||
'product' => $product,
|
||||
]);
|
||||
|
||||
// Map existing values by custom field ID
|
||||
$existingByFieldId = [];
|
||||
foreach ($existingValues as $cfv) {
|
||||
$existingByFieldId[$cfv->getCustomField()->getId()] = $cfv;
|
||||
}
|
||||
|
||||
// For each custom field defined on the model type, ensure a value exists
|
||||
foreach ($customFields as $cf) {
|
||||
if (!isset($existingByFieldId[$cf->getId()])) {
|
||||
// Create missing custom field value
|
||||
$cfv = new CustomFieldValue();
|
||||
$cfv->setCustomField($cf);
|
||||
$cfv->setProduct($product);
|
||||
$cfv->setValue('');
|
||||
$this->em->persist($cfv);
|
||||
++$addedValues;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete orphaned values if confirmDeletions
|
||||
if ($confirmation->confirmDeletions) {
|
||||
$fieldIds = array_map(fn (CustomField $cf) => $cf->getId(), $customFields);
|
||||
foreach ($existingValues as $cfv) {
|
||||
if (!in_array($cfv->getCustomField()->getId(), $fieldIds, true)) {
|
||||
$this->em->remove($cfv);
|
||||
++$deletedValues;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($changed) {
|
||||
$product->incrementVersion();
|
||||
++$itemsUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return new SyncExecutionResult(
|
||||
itemsUpdated: $itemsUpdated,
|
||||
additions: ['customFieldValues' => $addedValues],
|
||||
deletions: ['customFieldValues' => $deletedValues],
|
||||
modifications: ['customFieldValues' => $modifiedValues],
|
||||
);
|
||||
}
|
||||
}
|
||||
27
src/Service/Sync/SyncStrategyInterface.php
Normal file
27
src/Service/Sync/SyncStrategyInterface.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Sync;
|
||||
|
||||
use App\DTO\SyncConfirmation;
|
||||
use App\DTO\SyncExecutionResult;
|
||||
use App\DTO\SyncPreviewResult;
|
||||
use App\Entity\ModelType;
|
||||
|
||||
interface SyncStrategyInterface
|
||||
{
|
||||
public function supports(ModelType $modelType): bool;
|
||||
|
||||
/**
|
||||
* Compute diff between proposed structure and current items' slots.
|
||||
* Does NOT persist anything.
|
||||
*/
|
||||
public function preview(ModelType $modelType, array $newStructure): SyncPreviewResult;
|
||||
|
||||
/**
|
||||
* Apply sync: compare current skeleton requirements (already persisted)
|
||||
* with items' slots and add/remove as needed.
|
||||
*/
|
||||
public function execute(ModelType $modelType, SyncConfirmation $confirmation): SyncExecutionResult;
|
||||
}
|
||||
47
src/State/ModelTypeProcessor.php
Normal file
47
src/State/ModelTypeProcessor.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Entity\ModelType;
|
||||
use App\Service\SkeletonStructureService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
final class ModelTypeProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
|
||||
private readonly ProcessorInterface $decorated,
|
||||
private readonly SkeletonStructureService $skeletonStructureService,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $uriVariables
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
||||
{
|
||||
if (!$data instanceof ModelType) {
|
||||
return $this->decorated->process($data, $operation, $uriVariables, $context);
|
||||
}
|
||||
|
||||
$pendingStructure = $data->getPendingStructure();
|
||||
|
||||
// Persist the entity first (handles all non-skeleton fields)
|
||||
$result = $this->decorated->process($data, $operation, $uriVariables, $context);
|
||||
|
||||
// If structure was provided in the payload, write it to skeleton relation tables
|
||||
if (null !== $pendingStructure) {
|
||||
$this->skeletonStructureService->updateSkeletonRequirements($data, $pendingStructure);
|
||||
$data->clearPendingStructure();
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
15
symfony.lock
15
symfony.lock
@@ -94,6 +94,18 @@
|
||||
"config/packages/nelmio_cors.yaml"
|
||||
]
|
||||
},
|
||||
"php-http/discovery": {
|
||||
"version": "1.20",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.18",
|
||||
"ref": "f45b5dd173a27873ab19f5e3180b2f661c21de02"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/http_discovery.yaml"
|
||||
]
|
||||
},
|
||||
"phpunit/phpunit": {
|
||||
"version": "12.5",
|
||||
"recipe": {
|
||||
@@ -154,6 +166,9 @@
|
||||
".editorconfig"
|
||||
]
|
||||
},
|
||||
"symfony/mcp-bundle": {
|
||||
"version": "v0.6.0"
|
||||
},
|
||||
"symfony/property-info": {
|
||||
"version": "8.0",
|
||||
"recipe": {
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace App\Tests;
|
||||
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
|
||||
use ApiPlatform\Symfony\Bundle\Test\Client;
|
||||
use App\Entity\Composant;
|
||||
use App\Entity\ComposantPieceSlot;
|
||||
use App\Entity\ComposantProductSlot;
|
||||
use App\Entity\ComposantSubcomponentSlot;
|
||||
use App\Entity\Constructeur;
|
||||
use App\Entity\CustomField;
|
||||
use App\Entity\CustomFieldValue;
|
||||
@@ -16,6 +19,7 @@ use App\Entity\MachinePieceLink;
|
||||
use App\Entity\MachineProductLink;
|
||||
use App\Entity\ModelType;
|
||||
use App\Entity\Piece;
|
||||
use App\Entity\PieceProductSlot;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\Profile;
|
||||
use App\Entity\Site;
|
||||
@@ -233,13 +237,27 @@ abstract class AbstractApiTestCase extends ApiTestCase
|
||||
string $name = 'Custom Field',
|
||||
string $type = 'text',
|
||||
?Machine $machine = null,
|
||||
?ModelType $typeComposant = null,
|
||||
?ModelType $typePiece = null,
|
||||
?ModelType $typeProduct = null,
|
||||
int $orderIndex = 0,
|
||||
): CustomField {
|
||||
$cf = new CustomField();
|
||||
$cf->setName($name);
|
||||
$cf->setType($type);
|
||||
$cf->setOrderIndex($orderIndex);
|
||||
if (null !== $machine) {
|
||||
$cf->setMachine($machine);
|
||||
}
|
||||
if (null !== $typeComposant) {
|
||||
$cf->setTypeComposant($typeComposant);
|
||||
}
|
||||
if (null !== $typePiece) {
|
||||
$cf->setTypePiece($typePiece);
|
||||
}
|
||||
if (null !== $typeProduct) {
|
||||
$cf->setTypeProduct($typeProduct);
|
||||
}
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$em->persist($cf);
|
||||
@@ -284,11 +302,12 @@ abstract class AbstractApiTestCase extends ApiTestCase
|
||||
return $link;
|
||||
}
|
||||
|
||||
protected function createMachinePieceLink(Machine $machine, Piece $piece, ?MachineComponentLink $parentLink = null): MachinePieceLink
|
||||
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);
|
||||
}
|
||||
@@ -313,6 +332,108 @@ abstract class AbstractApiTestCase extends ApiTestCase
|
||||
return $link;
|
||||
}
|
||||
|
||||
protected function createComposantPieceSlot(
|
||||
Composant $composant,
|
||||
?ModelType $typePiece = null,
|
||||
?Piece $selectedPiece = null,
|
||||
int $quantity = 1,
|
||||
int $position = 0,
|
||||
): ComposantPieceSlot {
|
||||
$slot = new ComposantPieceSlot();
|
||||
$slot->setComposant($composant);
|
||||
$slot->setQuantity($quantity);
|
||||
$slot->setPosition($position);
|
||||
if (null !== $typePiece) {
|
||||
$slot->setTypePiece($typePiece);
|
||||
}
|
||||
if (null !== $selectedPiece) {
|
||||
$slot->setSelectedPiece($selectedPiece);
|
||||
}
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$em->persist($slot);
|
||||
$em->flush();
|
||||
|
||||
return $slot;
|
||||
}
|
||||
|
||||
protected function createComposantSubcomponentSlot(
|
||||
Composant $composant,
|
||||
?string $alias = null,
|
||||
?string $familyCode = null,
|
||||
?ModelType $typeComposant = null,
|
||||
?Composant $selectedComposant = null,
|
||||
int $position = 0,
|
||||
): ComposantSubcomponentSlot {
|
||||
$slot = new ComposantSubcomponentSlot();
|
||||
$slot->setComposant($composant);
|
||||
$slot->setAlias($alias);
|
||||
$slot->setFamilyCode($familyCode);
|
||||
$slot->setPosition($position);
|
||||
if (null !== $typeComposant) {
|
||||
$slot->setTypeComposant($typeComposant);
|
||||
}
|
||||
if (null !== $selectedComposant) {
|
||||
$slot->setSelectedComposant($selectedComposant);
|
||||
}
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$em->persist($slot);
|
||||
$em->flush();
|
||||
|
||||
return $slot;
|
||||
}
|
||||
|
||||
protected function createComposantProductSlot(
|
||||
Composant $composant,
|
||||
?ModelType $typeProduct = null,
|
||||
?Product $selectedProduct = null,
|
||||
?string $familyCode = null,
|
||||
int $position = 0,
|
||||
): ComposantProductSlot {
|
||||
$slot = new ComposantProductSlot();
|
||||
$slot->setComposant($composant);
|
||||
$slot->setFamilyCode($familyCode);
|
||||
$slot->setPosition($position);
|
||||
if (null !== $typeProduct) {
|
||||
$slot->setTypeProduct($typeProduct);
|
||||
}
|
||||
if (null !== $selectedProduct) {
|
||||
$slot->setSelectedProduct($selectedProduct);
|
||||
}
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$em->persist($slot);
|
||||
$em->flush();
|
||||
|
||||
return $slot;
|
||||
}
|
||||
|
||||
protected function createPieceProductSlot(
|
||||
Piece $piece,
|
||||
?ModelType $typeProduct = null,
|
||||
?Product $selectedProduct = null,
|
||||
?string $familyCode = null,
|
||||
int $position = 0,
|
||||
): PieceProductSlot {
|
||||
$slot = new PieceProductSlot();
|
||||
$slot->setPiece($piece);
|
||||
$slot->setFamilyCode($familyCode);
|
||||
$slot->setPosition($position);
|
||||
if (null !== $typeProduct) {
|
||||
$slot->setTypeProduct($typeProduct);
|
||||
}
|
||||
if (null !== $selectedProduct) {
|
||||
$slot->setSelectedProduct($selectedProduct);
|
||||
}
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$em->persist($slot);
|
||||
$em->flush();
|
||||
|
||||
return $slot;
|
||||
}
|
||||
|
||||
// ── Assertion helpers ───────────────────────────────────────────
|
||||
|
||||
protected function assertJsonContainsHydraCollection(): void
|
||||
|
||||
267
tests/Api/Controller/ModelTypeSyncControllerTest.php
Normal file
267
tests/Api/Controller/ModelTypeSyncControllerTest.php
Normal file
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Api\Controller;
|
||||
|
||||
use App\Entity\SkeletonPieceRequirement;
|
||||
use App\Entity\SkeletonProductRequirement;
|
||||
use App\Enum\ModelCategory;
|
||||
use App\Tests\AbstractApiTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ModelTypeSyncControllerTest extends AbstractApiTestCase
|
||||
{
|
||||
// ── sync-preview ────────────────────────────────────────────────
|
||||
|
||||
public function testPreviewReturnsNoImpactWhenNoItems(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('POST', '/api/model_types/'.$mt->getId().'/sync-preview', [
|
||||
'json' => [
|
||||
'structure' => [
|
||||
'pieces' => [],
|
||||
'products' => [],
|
||||
'subcomponents' => [],
|
||||
'customFields' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertSame(0, $data['itemCount']);
|
||||
$this->assertSame($mt->getId(), $data['modelTypeId']);
|
||||
}
|
||||
|
||||
public function testPreviewDetectsNewSlots(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE);
|
||||
$this->createComposant('C1', $mt);
|
||||
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('POST', '/api/model_types/'.$mt->getId().'/sync-preview', [
|
||||
'json' => [
|
||||
'structure' => [
|
||||
'pieces' => [['typePieceId' => $pieceType->getId(), 'position' => 0]],
|
||||
'products' => [],
|
||||
'subcomponents' => [],
|
||||
'customFields' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertSame(1, $data['itemCount']);
|
||||
$this->assertSame(1, $data['additions']['pieceSlots']);
|
||||
}
|
||||
|
||||
public function testPreview403ForViewer(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
|
||||
$client = $this->createViewerClient();
|
||||
$client->request('POST', '/api/model_types/'.$mt->getId().'/sync-preview', [
|
||||
'json' => ['structure' => []],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
public function testPreview401ForUnauthenticated(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
|
||||
$client = $this->createUnauthenticatedClient();
|
||||
$client->request('POST', '/api/model_types/'.$mt->getId().'/sync-preview', [
|
||||
'json' => ['structure' => []],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
public function testPreview404ForUnknownModelType(): void
|
||||
{
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('POST', '/api/model_types/nonexistent-id/sync-preview', [
|
||||
'json' => ['structure' => []],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
// ── sync ────────────────────────────────────────────────────────
|
||||
|
||||
public function testSyncAddsSlots(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE);
|
||||
$this->createComposant('C1', $mt);
|
||||
|
||||
// Add a skeleton requirement (simulates a PATCH that already happened)
|
||||
$em = $this->getEntityManager();
|
||||
$req = new SkeletonPieceRequirement();
|
||||
$req->setModelType($mt);
|
||||
$req->setTypePiece($pieceType);
|
||||
$req->setPosition(0);
|
||||
$em->persist($req);
|
||||
$em->flush();
|
||||
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('POST', '/api/model_types/'.$mt->getId().'/sync', [
|
||||
'json' => [
|
||||
'confirmDeletions' => false,
|
||||
'confirmTypeChanges' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertSame(1, $data['itemsUpdated']);
|
||||
$this->assertSame(1, $data['additions']['pieceSlots']);
|
||||
}
|
||||
|
||||
public function testSyncDeletesSlotsWithConfirmation(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE);
|
||||
$composant = $this->createComposant('C1', $mt);
|
||||
$this->createComposantPieceSlot($composant, $pieceType, null, 1, 0);
|
||||
|
||||
// No skeleton requirements → slot is orphaned
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('POST', '/api/model_types/'.$mt->getId().'/sync', [
|
||||
'json' => [
|
||||
'confirmDeletions' => true,
|
||||
'confirmTypeChanges' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertSame(1, $data['deletions']['pieceSlots']);
|
||||
}
|
||||
|
||||
public function testSyncSkipsDeletionsWithoutConfirmation(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE);
|
||||
$composant = $this->createComposant('C1', $mt);
|
||||
$this->createComposantPieceSlot($composant, $pieceType, null, 1, 0);
|
||||
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('POST', '/api/model_types/'.$mt->getId().'/sync', [
|
||||
'json' => [
|
||||
'confirmDeletions' => false,
|
||||
'confirmTypeChanges' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertSame(0, $data['deletions']['pieceSlots']);
|
||||
}
|
||||
|
||||
public function testSync403ForViewer(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
|
||||
$client = $this->createViewerClient();
|
||||
$client->request('POST', '/api/model_types/'.$mt->getId().'/sync', [
|
||||
'json' => ['confirmDeletions' => false, 'confirmTypeChanges' => false],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
public function testSync404ForUnknownModelType(): void
|
||||
{
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('POST', '/api/model_types/nonexistent-id/sync', [
|
||||
'json' => ['confirmDeletions' => false, 'confirmTypeChanges' => false],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
public function testSyncIsIdempotent(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE);
|
||||
$this->createComposant('C1', $mt);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$req = new SkeletonPieceRequirement();
|
||||
$req->setModelType($mt);
|
||||
$req->setTypePiece($pieceType);
|
||||
$req->setPosition(0);
|
||||
$em->persist($req);
|
||||
$em->flush();
|
||||
|
||||
$client = $this->createGestionnaireClient();
|
||||
|
||||
// First sync
|
||||
$client->request('POST', '/api/model_types/'.$mt->getId().'/sync', [
|
||||
'json' => ['confirmDeletions' => false, 'confirmTypeChanges' => false],
|
||||
]);
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data1 = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertSame(1, $data1['itemsUpdated']);
|
||||
|
||||
// Second sync — idempotent, no changes
|
||||
$client->request('POST', '/api/model_types/'.$mt->getId().'/sync', [
|
||||
'json' => ['confirmDeletions' => false, 'confirmTypeChanges' => false],
|
||||
]);
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data2 = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertSame(0, $data2['itemsUpdated']);
|
||||
}
|
||||
|
||||
public function testSyncWorksForPieceCategory(): void
|
||||
{
|
||||
$mt = $this->createModelType('Piece Cat', 'PC-001', ModelCategory::PIECE);
|
||||
$productType = $this->createModelType('Prod Type', 'PD-001', ModelCategory::PRODUCT);
|
||||
$this->createPiece('P1', 'P1-REF', $mt);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$req = new SkeletonProductRequirement();
|
||||
$req->setModelType($mt);
|
||||
$req->setTypeProduct($productType);
|
||||
$req->setPosition(0);
|
||||
$em->persist($req);
|
||||
$em->flush();
|
||||
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('POST', '/api/model_types/'.$mt->getId().'/sync', [
|
||||
'json' => ['confirmDeletions' => false, 'confirmTypeChanges' => false],
|
||||
]);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertSame(1, $data['itemsUpdated']);
|
||||
$this->assertSame(1, $data['additions']['productSlots']);
|
||||
}
|
||||
|
||||
public function testSyncWorksForProductCategory(): void
|
||||
{
|
||||
$mt = $this->createModelType('Prod Cat', 'PD-001', ModelCategory::PRODUCT);
|
||||
$this->createProduct('PR1', 'PR1-REF', $mt);
|
||||
$this->createCustomField('CF1', 'text', typeProduct: $mt, orderIndex: 0);
|
||||
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('POST', '/api/model_types/'.$mt->getId().'/sync', [
|
||||
'json' => ['confirmDeletions' => false, 'confirmTypeChanges' => false],
|
||||
]);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
$this->assertSame(1, $data['itemsUpdated']);
|
||||
$this->assertSame(1, $data['additions']['customFieldValues']);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Api\Entity;
|
||||
|
||||
use App\Entity\Composant;
|
||||
use App\Enum\ModelCategory;
|
||||
use App\Tests\AbstractApiTestCase;
|
||||
|
||||
@@ -135,4 +136,82 @@ class ComposantTest extends AbstractApiTestCase
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertJsonContains(['totalItems' => 1]);
|
||||
}
|
||||
|
||||
public function testComposantPieceSlotsPersistedAndReadable(): void
|
||||
{
|
||||
$pieceType = $this->createModelType('Joint', 'JOINT-CSLOT', ModelCategory::PIECE);
|
||||
$piece = $this->createPiece('Joint slot', 'REF-CSLOT', $pieceType);
|
||||
$composant = $this->createComposant('Composant slots pièces');
|
||||
|
||||
$this->createComposantPieceSlot($composant, $pieceType, $piece, 3, 0);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$em->clear();
|
||||
|
||||
$refetched = $em->find(Composant::class, $composant->getId());
|
||||
$this->assertNotNull($refetched);
|
||||
$this->assertCount(1, $refetched->getPieceSlots());
|
||||
|
||||
$slot = $refetched->getPieceSlots()->first();
|
||||
$this->assertSame($pieceType->getId(), $slot->getTypePiece()->getId());
|
||||
$this->assertSame($piece->getId(), $slot->getSelectedPiece()->getId());
|
||||
$this->assertSame(3, $slot->getQuantity());
|
||||
$this->assertSame(0, $slot->getPosition());
|
||||
}
|
||||
|
||||
public function testComposantSubcomponentSlotsPersistedAndReadable(): void
|
||||
{
|
||||
$subType = $this->createModelType('Sous-pompe', 'SP-001', ModelCategory::COMPONENT);
|
||||
$subComposant = $this->createComposant('Sous-composant');
|
||||
$composant = $this->createComposant('Composant parent');
|
||||
|
||||
$this->createComposantSubcomponentSlot($composant, 'Sous-pompe A', 'SP-FAM', $subType, $subComposant, 0);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$em->clear();
|
||||
|
||||
$refetched = $em->find(Composant::class, $composant->getId());
|
||||
$this->assertNotNull($refetched);
|
||||
$this->assertCount(1, $refetched->getSubcomponentSlots());
|
||||
|
||||
$slot = $refetched->getSubcomponentSlots()->first();
|
||||
$this->assertSame('Sous-pompe A', $slot->getAlias());
|
||||
$this->assertSame('SP-FAM', $slot->getFamilyCode());
|
||||
$this->assertSame($subType->getId(), $slot->getTypeComposant()->getId());
|
||||
$this->assertSame($subComposant->getId(), $slot->getSelectedComposant()->getId());
|
||||
}
|
||||
|
||||
public function testComposantProductSlotsPersistedAndReadable(): void
|
||||
{
|
||||
$productType = $this->createModelType('Huile', 'HUILE-CSLOT', ModelCategory::PRODUCT);
|
||||
$product = $this->createProduct('Huile slot', 'REF-HSLOT', $productType);
|
||||
$composant = $this->createComposant('Composant slots produits');
|
||||
|
||||
$this->createComposantProductSlot($composant, $productType, $product, 'LUB', 0);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$em->clear();
|
||||
|
||||
$refetched = $em->find(Composant::class, $composant->getId());
|
||||
$this->assertNotNull($refetched);
|
||||
$this->assertCount(1, $refetched->getProductSlots());
|
||||
|
||||
$slot = $refetched->getProductSlots()->first();
|
||||
$this->assertSame($productType->getId(), $slot->getTypeProduct()->getId());
|
||||
$this->assertSame($product->getId(), $slot->getSelectedProduct()->getId());
|
||||
$this->assertSame('LUB', $slot->getFamilyCode());
|
||||
}
|
||||
|
||||
public function testComposantDeleteCascadesSlots(): void
|
||||
{
|
||||
$composant = $this->createComposant('Composant cascade');
|
||||
$this->createComposantPieceSlot($composant);
|
||||
$this->createComposantProductSlot($composant);
|
||||
$this->createComposantSubcomponentSlot($composant, 'Sub', 'FAM');
|
||||
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('DELETE', self::iri('composants', $composant->getId()));
|
||||
|
||||
$this->assertResponseStatusCodeSame(204);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,43 @@ class MachinePieceLinkTest extends AbstractApiTestCase
|
||||
]);
|
||||
}
|
||||
|
||||
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' => self::iri('machines', $machine->getId()),
|
||||
'piece' => self::iri('pieces', $piece->getId()),
|
||||
'quantity' => 5,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(201);
|
||||
$this->assertJsonContains(['quantity' => 5]);
|
||||
}
|
||||
|
||||
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' => self::iri('machines', $machine->getId()),
|
||||
'piece' => self::iri('pieces', $piece->getId()),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(201);
|
||||
$this->assertJsonContains(['quantity' => 1]);
|
||||
}
|
||||
|
||||
public function testDelete(): void
|
||||
{
|
||||
$machine = $this->createMachine('Machine A');
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Api\Entity;
|
||||
|
||||
use App\Enum\ModelCategory;
|
||||
use App\Tests\AbstractApiTestCase;
|
||||
|
||||
/**
|
||||
@@ -132,4 +133,173 @@ class MachineTest extends AbstractApiTestCase
|
||||
|
||||
$this->assertResponseStatusCodeSame(422);
|
||||
}
|
||||
|
||||
public function testGetStructureEndpoint(): void
|
||||
{
|
||||
$machine = $this->createMachine('Machine structure');
|
||||
$composant = $this->createComposant('Composant A');
|
||||
$piece = $this->createPiece('Pièce A', 'REF-PA');
|
||||
$product = $this->createProduct('Produit A', 'REF-PRA');
|
||||
|
||||
$compLink = $this->createMachineComponentLink($machine, $composant);
|
||||
$this->createMachinePieceLink($machine, $piece, $compLink, 3);
|
||||
$this->createMachineProductLink($machine, $product);
|
||||
|
||||
$client = $this->createViewerClient();
|
||||
$client->request('GET', '/api/machines/'.$machine->getId().'/structure');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = $client->getResponse()->toArray();
|
||||
|
||||
$this->assertArrayHasKey('machine', $data);
|
||||
$this->assertSame($machine->getId(), $data['machine']['id']);
|
||||
|
||||
$this->assertArrayHasKey('componentLinks', $data);
|
||||
$this->assertCount(1, $data['componentLinks']);
|
||||
$this->assertSame($composant->getId(), $data['componentLinks'][0]['composantId']);
|
||||
|
||||
$this->assertArrayHasKey('pieceLinks', $data);
|
||||
$this->assertCount(1, $data['pieceLinks']);
|
||||
$this->assertSame($piece->getId(), $data['pieceLinks'][0]['pieceId']);
|
||||
|
||||
$this->assertArrayHasKey('productLinks', $data);
|
||||
$this->assertCount(1, $data['productLinks']);
|
||||
$this->assertSame($product->getId(), $data['productLinks'][0]['productId']);
|
||||
}
|
||||
|
||||
public function testGetStructureReturnsComposantSlotsData(): void
|
||||
{
|
||||
$pieceType = $this->createModelType('Joint', 'JOINT-SLOT', ModelCategory::PIECE);
|
||||
$productType = $this->createModelType('Huile', 'HUILE-SLOT', ModelCategory::PRODUCT);
|
||||
$compType = $this->createModelType('Pompe', 'POMPE-SLOT', ModelCategory::COMPONENT);
|
||||
|
||||
$composant = $this->createComposant('Composant avec slots', $compType);
|
||||
$piece = $this->createPiece('Joint sélectionné', 'REF-JS', $pieceType);
|
||||
$product = $this->createProduct('Huile sélectionnée', 'REF-HS', $productType);
|
||||
|
||||
// Create slots on the composant
|
||||
$this->createComposantPieceSlot($composant, $pieceType, $piece, 2, 0);
|
||||
$this->createComposantProductSlot($composant, $productType, $product, 'LUB', 0);
|
||||
$this->createComposantSubcomponentSlot($composant, 'Sous-pompe', 'SP', $compType, null, 0);
|
||||
|
||||
$machine = $this->createMachine('Machine slots');
|
||||
$this->createMachineComponentLink($machine, $composant);
|
||||
|
||||
$client = $this->createViewerClient();
|
||||
$client->request('GET', '/api/machines/'.$machine->getId().'/structure');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = $client->getResponse()->toArray();
|
||||
|
||||
$compData = $data['componentLinks'][0]['composant'];
|
||||
$this->assertArrayHasKey('structure', $compData);
|
||||
|
||||
$structure = $compData['structure'];
|
||||
|
||||
// Piece slots
|
||||
$this->assertCount(1, $structure['pieces']);
|
||||
$this->assertSame($pieceType->getId(), $structure['pieces'][0]['typePieceId']);
|
||||
$this->assertSame(2, $structure['pieces'][0]['quantity']);
|
||||
$this->assertSame($piece->getId(), $structure['pieces'][0]['selectedPieceId']);
|
||||
$this->assertArrayHasKey('resolvedPiece', $structure['pieces'][0]);
|
||||
$this->assertSame($piece->getId(), $structure['pieces'][0]['resolvedPiece']['id']);
|
||||
|
||||
// Product slots
|
||||
$this->assertCount(1, $structure['products']);
|
||||
$this->assertSame($productType->getId(), $structure['products'][0]['typeProductId']);
|
||||
$this->assertSame('LUB', $structure['products'][0]['familyCode']);
|
||||
$this->assertSame($product->getId(), $structure['products'][0]['selectedProductId']);
|
||||
|
||||
// Subcomponent slots
|
||||
$this->assertCount(1, $structure['subcomponents']);
|
||||
$this->assertSame('Sous-pompe', $structure['subcomponents'][0]['alias']);
|
||||
$this->assertSame('SP', $structure['subcomponents'][0]['familyCode']);
|
||||
$this->assertSame($compType->getId(), $structure['subcomponents'][0]['typeComposantId']);
|
||||
}
|
||||
|
||||
public function testGetStructureUnauthenticated(): void
|
||||
{
|
||||
$machine = $this->createMachine('Machine auth');
|
||||
|
||||
$client = $this->createUnauthenticatedClient();
|
||||
$client->request('GET', '/api/machines/'.$machine->getId().'/structure');
|
||||
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
public function testGetStructureNotFound(): void
|
||||
{
|
||||
$client = $this->createViewerClient();
|
||||
$client->request('GET', '/api/machines/nonexistent-id/structure');
|
||||
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
public function testPatchStructureEndpoint(): void
|
||||
{
|
||||
$machine = $this->createMachine('Machine PATCH');
|
||||
$composant = $this->createComposant('Composant PATCH');
|
||||
$piece = $this->createPiece('Pièce PATCH', 'REF-PATCH');
|
||||
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('PATCH', '/api/machines/'.$machine->getId().'/structure', [
|
||||
'headers' => ['Content-Type' => 'application/json'],
|
||||
'json' => [
|
||||
'componentLinks' => [
|
||||
['composantId' => $composant->getId()],
|
||||
],
|
||||
'pieceLinks' => [
|
||||
['pieceId' => $piece->getId(), 'parentComponentLinkId' => null, 'quantity' => 5],
|
||||
],
|
||||
'productLinks' => [],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = $client->getResponse()->toArray();
|
||||
|
||||
$this->assertCount(1, $data['componentLinks']);
|
||||
$this->assertSame($composant->getId(), $data['componentLinks'][0]['composantId']);
|
||||
|
||||
$this->assertCount(1, $data['pieceLinks']);
|
||||
$this->assertSame($piece->getId(), $data['pieceLinks'][0]['pieceId']);
|
||||
$this->assertSame(5, $data['pieceLinks'][0]['quantity']);
|
||||
}
|
||||
|
||||
public function testPatchStructureViewerForbidden(): void
|
||||
{
|
||||
$machine = $this->createMachine('Machine PATCH forbidden');
|
||||
|
||||
$client = $this->createViewerClient();
|
||||
$client->request('PATCH', '/api/machines/'.$machine->getId().'/structure', [
|
||||
'headers' => ['Content-Type' => 'application/json'],
|
||||
'json' => ['componentLinks' => [], 'pieceLinks' => [], 'productLinks' => []],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
public function testPieceQuantityFromComposantSlot(): void
|
||||
{
|
||||
$pieceType = $this->createModelType('Joint', 'JOINT-QTY', ModelCategory::PIECE);
|
||||
$composant = $this->createComposant('Composant qty');
|
||||
$piece = $this->createPiece('Pièce qty', 'REF-QTY', $pieceType);
|
||||
|
||||
// Create a piece slot with quantity 4 on the composant
|
||||
$this->createComposantPieceSlot($composant, $pieceType, $piece, 4, 0);
|
||||
|
||||
$machine = $this->createMachine('Machine qty');
|
||||
$compLink = $this->createMachineComponentLink($machine, $composant);
|
||||
$this->createMachinePieceLink($machine, $piece, $compLink, 1);
|
||||
|
||||
$client = $this->createViewerClient();
|
||||
$client->request('GET', '/api/machines/'.$machine->getId().'/structure');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = $client->getResponse()->toArray();
|
||||
|
||||
// The quantity should come from the composant slot (4), not the link (1)
|
||||
$pieceLinkData = $data['pieceLinks'][0];
|
||||
$this->assertSame(4, $pieceLinkData['quantity']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,4 +152,132 @@ class ModelTypeTest extends AbstractApiTestCase
|
||||
|
||||
$this->assertResponseStatusCodeSame(409);
|
||||
}
|
||||
|
||||
public function testPostComponentWithSkeletonStructure(): void
|
||||
{
|
||||
$pieceType = $this->createModelType('Joint', 'JOINT-001', ModelCategory::PIECE);
|
||||
$productType = $this->createModelType('Huile', 'HUILE-001', ModelCategory::PRODUCT);
|
||||
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('POST', '/api/model_types', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
'json' => [
|
||||
'name' => 'Pompe complète',
|
||||
'code' => 'POMPE-FULL',
|
||||
'category' => 'COMPONENT',
|
||||
'structure' => [
|
||||
'pieces' => [['typePieceId' => $pieceType->getId()]],
|
||||
'products' => [['typeProductId' => $productType->getId(), 'familyCode' => 'LUB']],
|
||||
'subcomponents' => [['alias' => 'Sous-pompe', 'familyCode' => 'SP']],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(201);
|
||||
$data = $client->getResponse()->toArray();
|
||||
|
||||
$this->assertArrayHasKey('structure', $data);
|
||||
$this->assertCount(1, $data['structure']['pieces']);
|
||||
$this->assertSame($pieceType->getId(), $data['structure']['pieces'][0]['typePieceId']);
|
||||
$this->assertCount(1, $data['structure']['products']);
|
||||
$this->assertSame($productType->getId(), $data['structure']['products'][0]['typeProductId']);
|
||||
$this->assertSame('LUB', $data['structure']['products'][0]['familyCode']);
|
||||
$this->assertCount(1, $data['structure']['subcomponents']);
|
||||
$this->assertSame('Sous-pompe', $data['structure']['subcomponents'][0]['alias']);
|
||||
$this->assertSame('SP', $data['structure']['subcomponents'][0]['familyCode']);
|
||||
}
|
||||
|
||||
public function testGetItemReturnsStructureFromRelations(): void
|
||||
{
|
||||
$pieceType = $this->createModelType('Joint', 'JOINT-001', ModelCategory::PIECE);
|
||||
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('POST', '/api/model_types', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
'json' => [
|
||||
'name' => 'Pompe',
|
||||
'code' => 'POMPE-STRUCT',
|
||||
'category' => 'COMPONENT',
|
||||
'structure' => [
|
||||
'pieces' => [['typePieceId' => $pieceType->getId()]],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->assertResponseStatusCodeSame(201);
|
||||
$createdData = $client->getResponse()->toArray();
|
||||
|
||||
// GET the item and verify structure is returned from relations
|
||||
$client->request('GET', self::iri('model_types', $createdData['id']));
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = $client->getResponse()->toArray();
|
||||
|
||||
$this->assertArrayHasKey('structure', $data);
|
||||
$this->assertCount(1, $data['structure']['pieces']);
|
||||
$this->assertSame($pieceType->getId(), $data['structure']['pieces'][0]['typePieceId']);
|
||||
}
|
||||
|
||||
public function testPatchUpdatesSkeletonStructure(): void
|
||||
{
|
||||
$pieceType1 = $this->createModelType('Joint', 'JOINT-001', ModelCategory::PIECE);
|
||||
$pieceType2 = $this->createModelType('Roulement', 'ROUL-001', ModelCategory::PIECE);
|
||||
|
||||
// Create with one piece requirement
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('POST', '/api/model_types', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
'json' => [
|
||||
'name' => 'Pompe',
|
||||
'code' => 'POMPE-UPD',
|
||||
'category' => 'COMPONENT',
|
||||
'structure' => [
|
||||
'pieces' => [['typePieceId' => $pieceType1->getId()]],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->assertResponseStatusCodeSame(201);
|
||||
$createdData = $client->getResponse()->toArray();
|
||||
|
||||
// PATCH with two piece requirements (replaces the old one)
|
||||
$client->request('PATCH', self::iri('model_types', $createdData['id']), [
|
||||
'headers' => ['Content-Type' => 'application/merge-patch+json'],
|
||||
'json' => [
|
||||
'structure' => [
|
||||
'pieces' => [
|
||||
['typePieceId' => $pieceType1->getId()],
|
||||
['typePieceId' => $pieceType2->getId()],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = $client->getResponse()->toArray();
|
||||
$this->assertCount(2, $data['structure']['pieces']);
|
||||
}
|
||||
|
||||
public function testPostPieceTypeWithProductRequirements(): void
|
||||
{
|
||||
$productType = $this->createModelType('Graisse', 'GRAISSE-001', ModelCategory::PRODUCT);
|
||||
|
||||
$client = $this->createGestionnaireClient();
|
||||
$client->request('POST', '/api/model_types', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
'json' => [
|
||||
'name' => 'Roulement',
|
||||
'code' => 'ROUL-PROD',
|
||||
'category' => 'PIECE',
|
||||
'structure' => [
|
||||
'products' => [['typeProductId' => $productType->getId(), 'familyCode' => 'GR']],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(201);
|
||||
$data = $client->getResponse()->toArray();
|
||||
|
||||
$this->assertArrayHasKey('structure', $data);
|
||||
$this->assertCount(1, $data['structure']['products']);
|
||||
$this->assertSame($productType->getId(), $data['structure']['products'][0]['typeProductId']);
|
||||
$this->assertSame('GR', $data['structure']['products'][0]['familyCode']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Api\Entity;
|
||||
|
||||
use App\Entity\Piece;
|
||||
use App\Enum\ModelCategory;
|
||||
use App\Tests\AbstractApiTestCase;
|
||||
|
||||
@@ -142,4 +143,48 @@ class PieceTest extends AbstractApiTestCase
|
||||
|
||||
$this->assertResponseStatusCodeSame(422);
|
||||
}
|
||||
|
||||
public function testGetItemReturnsProductIds(): void
|
||||
{
|
||||
$product1 = $this->createProduct('Huile A', 'HUILE-A');
|
||||
$product2 = $this->createProduct('Graisse B', 'GRAISSE-B');
|
||||
|
||||
$piece = $this->createPiece('Joint avec produits', 'REF-JP');
|
||||
$piece->addProduct($product1);
|
||||
$piece->addProduct($product2);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$em->persist($piece);
|
||||
$em->flush();
|
||||
|
||||
$client = $this->createViewerClient();
|
||||
$client->request('GET', self::iri('pieces', $piece->getId()));
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = $client->getResponse()->toArray();
|
||||
|
||||
$this->assertArrayHasKey('productIds', $data);
|
||||
$this->assertCount(2, $data['productIds']);
|
||||
$this->assertContains($product1->getId(), $data['productIds']);
|
||||
$this->assertContains($product2->getId(), $data['productIds']);
|
||||
}
|
||||
|
||||
public function testPieceProductRelationSurvivesRefetch(): void
|
||||
{
|
||||
$product = $this->createProduct('Huile', 'HUILE-REL');
|
||||
$piece = $this->createPiece('Joint relation', 'REF-REL');
|
||||
$piece->addProduct($product);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$em->persist($piece);
|
||||
$em->flush();
|
||||
$em->clear();
|
||||
|
||||
// Re-fetch the piece to verify relation persisted
|
||||
$refetched = $em->find(Piece::class, $piece->getId());
|
||||
$this->assertNotNull($refetched);
|
||||
$this->assertCount(1, $refetched->getProducts());
|
||||
$this->assertSame($product->getId(), $refetched->getProducts()->first()->getId());
|
||||
$this->assertSame([$product->getId()], $refetched->getProductIds());
|
||||
}
|
||||
}
|
||||
|
||||
178
tests/Api/Service/ComposantSyncStrategyTest.php
Normal file
178
tests/Api/Service/ComposantSyncStrategyTest.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Api\Service;
|
||||
|
||||
use App\DTO\SyncConfirmation;
|
||||
use App\Entity\SkeletonPieceRequirement;
|
||||
use App\Enum\ModelCategory;
|
||||
use App\Service\Sync\ComposantSyncStrategy;
|
||||
use App\Tests\AbstractApiTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ComposantSyncStrategyTest extends AbstractApiTestCase
|
||||
{
|
||||
private ComposantSyncStrategy $strategy;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->strategy = static::getContainer()->get(ComposantSyncStrategy::class);
|
||||
}
|
||||
|
||||
public function testSupportsComponentCategory(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$this->assertTrue($this->strategy->supports($mt));
|
||||
}
|
||||
|
||||
public function testPreviewNoImpactWhenNoComposants(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$result = $this->strategy->preview($mt, ['pieces' => [], 'products' => [], 'subcomponents' => [], 'customFields' => []]);
|
||||
$this->assertSame(0, $result->itemCount);
|
||||
$this->assertFalse($result->hasImpact());
|
||||
}
|
||||
|
||||
public function testPreviewDetectsNewPieceSlot(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE);
|
||||
$this->createComposant('C1', $mt);
|
||||
|
||||
$result = $this->strategy->preview($mt, [
|
||||
'pieces' => [['typePieceId' => $pieceType->getId(), 'position' => 0]],
|
||||
'products' => [],
|
||||
'subcomponents' => [],
|
||||
'customFields' => [],
|
||||
]);
|
||||
|
||||
$this->assertSame(1, $result->itemCount);
|
||||
$this->assertSame(1, $result->additions['pieceSlots']);
|
||||
}
|
||||
|
||||
public function testPreviewDetectsSlotDeletion(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE);
|
||||
$composant = $this->createComposant('C1', $mt);
|
||||
$this->createComposantPieceSlot($composant, $pieceType, null, 1, 0);
|
||||
|
||||
$result = $this->strategy->preview($mt, [
|
||||
'pieces' => [],
|
||||
'products' => [],
|
||||
'subcomponents' => [],
|
||||
'customFields' => [],
|
||||
]);
|
||||
|
||||
$this->assertSame(1, $result->deletions['pieceSlots']);
|
||||
}
|
||||
|
||||
public function testPreviewNoImpactWhenSlotsMatch(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE);
|
||||
$composant = $this->createComposant('C1', $mt);
|
||||
$this->createComposantPieceSlot($composant, $pieceType, null, 1, 0);
|
||||
|
||||
$result = $this->strategy->preview($mt, [
|
||||
'pieces' => [['typePieceId' => $pieceType->getId(), 'position' => 0]],
|
||||
'products' => [],
|
||||
'subcomponents' => [],
|
||||
'customFields' => [],
|
||||
]);
|
||||
|
||||
$this->assertFalse($result->hasImpact());
|
||||
}
|
||||
|
||||
public function testExecuteAddsMissingSlots(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE);
|
||||
$composant = $this->createComposant('C1', $mt);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$req = new SkeletonPieceRequirement();
|
||||
$req->setModelType($mt);
|
||||
$req->setTypePiece($pieceType);
|
||||
$req->setPosition(0);
|
||||
$em->persist($req);
|
||||
$em->flush();
|
||||
|
||||
$result = $this->strategy->execute($mt, new SyncConfirmation());
|
||||
|
||||
$this->assertSame(1, $result->itemsUpdated);
|
||||
$this->assertSame(1, $result->additions['pieceSlots']);
|
||||
|
||||
$em->refresh($composant);
|
||||
$this->assertSame(2, $composant->getVersion());
|
||||
}
|
||||
|
||||
public function testExecutePreservesExistingSelections(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE);
|
||||
$composant = $this->createComposant('C1', $mt);
|
||||
$piece = $this->createPiece('P1', 'P1-REF', $pieceType);
|
||||
$slot = $this->createComposantPieceSlot($composant, $pieceType, $piece, 5, 0);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$req = new SkeletonPieceRequirement();
|
||||
$req->setModelType($mt);
|
||||
$req->setTypePiece($pieceType);
|
||||
$req->setPosition(0);
|
||||
$em->persist($req);
|
||||
$em->flush();
|
||||
|
||||
$result = $this->strategy->execute($mt, new SyncConfirmation());
|
||||
|
||||
// No changes — slot already matches
|
||||
$this->assertSame(0, $result->itemsUpdated);
|
||||
|
||||
// Selection and quantity preserved
|
||||
$em->refresh($slot);
|
||||
$this->assertSame($piece->getId(), $slot->getSelectedPiece()->getId());
|
||||
$this->assertSame(5, $slot->getQuantity());
|
||||
}
|
||||
|
||||
public function testExecuteDeletesSlotsOnlyWithConfirmation(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE);
|
||||
$composant = $this->createComposant('C1', $mt);
|
||||
$this->createComposantPieceSlot($composant, $pieceType, null, 1, 0);
|
||||
|
||||
// No skeleton requirements -> slot should be deleted
|
||||
$result = $this->strategy->execute($mt, new SyncConfirmation(confirmDeletions: false));
|
||||
$this->assertSame(0, $result->deletions['pieceSlots']);
|
||||
|
||||
// With confirmation
|
||||
$result = $this->strategy->execute($mt, new SyncConfirmation(confirmDeletions: true));
|
||||
$this->assertSame(1, $result->deletions['pieceSlots']);
|
||||
}
|
||||
|
||||
public function testExecuteIsIdempotent(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$pieceType = $this->createModelType('Piece Type', 'PT-001', ModelCategory::PIECE);
|
||||
$composant = $this->createComposant('C1', $mt);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$req = new SkeletonPieceRequirement();
|
||||
$req->setModelType($mt);
|
||||
$req->setTypePiece($pieceType);
|
||||
$req->setPosition(0);
|
||||
$em->persist($req);
|
||||
$em->flush();
|
||||
|
||||
$result1 = $this->strategy->execute($mt, new SyncConfirmation());
|
||||
$this->assertSame(1, $result1->additions['pieceSlots']);
|
||||
|
||||
$em->refresh($composant);
|
||||
$result2 = $this->strategy->execute($mt, new SyncConfirmation());
|
||||
$this->assertSame(0, $result2->itemsUpdated);
|
||||
}
|
||||
}
|
||||
103
tests/Api/Service/PieceSyncStrategyTest.php
Normal file
103
tests/Api/Service/PieceSyncStrategyTest.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Api\Service;
|
||||
|
||||
use App\DTO\SyncConfirmation;
|
||||
use App\Entity\SkeletonProductRequirement;
|
||||
use App\Enum\ModelCategory;
|
||||
use App\Service\Sync\PieceSyncStrategy;
|
||||
use App\Tests\AbstractApiTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class PieceSyncStrategyTest extends AbstractApiTestCase
|
||||
{
|
||||
private PieceSyncStrategy $strategy;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->strategy = static::getContainer()->get(PieceSyncStrategy::class);
|
||||
}
|
||||
|
||||
public function testSupportsPieceCategory(): void
|
||||
{
|
||||
$mt = $this->createModelType('Piece Cat', 'PC-001', ModelCategory::PIECE);
|
||||
$this->assertTrue($this->strategy->supports($mt));
|
||||
}
|
||||
|
||||
public function testPreviewDetectsNewProductSlot(): void
|
||||
{
|
||||
$mt = $this->createModelType('Piece Cat', 'PC-001', ModelCategory::PIECE);
|
||||
$productType = $this->createModelType('Product Type', 'PT-001', ModelCategory::PRODUCT);
|
||||
$this->createPiece('P1', 'P1-REF', $mt);
|
||||
|
||||
$result = $this->strategy->preview($mt, [
|
||||
'products' => [['typeProductId' => $productType->getId(), 'position' => 0]],
|
||||
'customFields' => [],
|
||||
]);
|
||||
|
||||
$this->assertSame(1, $result->itemCount);
|
||||
$this->assertSame(1, $result->additions['productSlots']);
|
||||
}
|
||||
|
||||
public function testExecuteAddsProductSlots(): void
|
||||
{
|
||||
$mt = $this->createModelType('Piece Cat', 'PC-001', ModelCategory::PIECE);
|
||||
$productType = $this->createModelType('Product Type', 'PT-001', ModelCategory::PRODUCT);
|
||||
$piece = $this->createPiece('P1', 'P1-REF', $mt);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$req = new SkeletonProductRequirement();
|
||||
$req->setModelType($mt);
|
||||
$req->setTypeProduct($productType);
|
||||
$req->setPosition(0);
|
||||
$em->persist($req);
|
||||
$em->flush();
|
||||
|
||||
$result = $this->strategy->execute($mt, new SyncConfirmation());
|
||||
|
||||
$this->assertSame(1, $result->itemsUpdated);
|
||||
$this->assertSame(1, $result->additions['productSlots']);
|
||||
|
||||
$em->refresh($piece);
|
||||
$this->assertSame(2, $piece->getVersion());
|
||||
$this->assertCount(1, $piece->getProductSlots());
|
||||
}
|
||||
|
||||
public function testExecuteDeletesWithConfirmation(): void
|
||||
{
|
||||
$mt = $this->createModelType('Piece Cat', 'PC-001', ModelCategory::PIECE);
|
||||
$productType = $this->createModelType('Product Type', 'PT-001', ModelCategory::PRODUCT);
|
||||
$piece = $this->createPiece('P1', 'P1-REF', $mt);
|
||||
$this->createPieceProductSlot($piece, $productType, null, null, 0);
|
||||
|
||||
$result = $this->strategy->execute($mt, new SyncConfirmation(confirmDeletions: true));
|
||||
$this->assertSame(1, $result->deletions['productSlots']);
|
||||
}
|
||||
|
||||
public function testExecuteIsIdempotent(): void
|
||||
{
|
||||
$mt = $this->createModelType('Piece Cat', 'PC-001', ModelCategory::PIECE);
|
||||
$productType = $this->createModelType('Product Type', 'PT-001', ModelCategory::PRODUCT);
|
||||
$piece = $this->createPiece('P1', 'P1-REF', $mt);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$req = new SkeletonProductRequirement();
|
||||
$req->setModelType($mt);
|
||||
$req->setTypeProduct($productType);
|
||||
$req->setPosition(0);
|
||||
$em->persist($req);
|
||||
$em->flush();
|
||||
|
||||
$result1 = $this->strategy->execute($mt, new SyncConfirmation());
|
||||
$this->assertSame(1, $result1->additions['productSlots']);
|
||||
|
||||
$em->refresh($piece);
|
||||
$result2 = $this->strategy->execute($mt, new SyncConfirmation());
|
||||
$this->assertSame(0, $result2->itemsUpdated);
|
||||
}
|
||||
}
|
||||
92
tests/Api/Service/ProductSyncStrategyTest.php
Normal file
92
tests/Api/Service/ProductSyncStrategyTest.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Api\Service;
|
||||
|
||||
use App\DTO\SyncConfirmation;
|
||||
use App\Enum\ModelCategory;
|
||||
use App\Service\Sync\ProductSyncStrategy;
|
||||
use App\Tests\AbstractApiTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ProductSyncStrategyTest extends AbstractApiTestCase
|
||||
{
|
||||
private ProductSyncStrategy $strategy;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->strategy = static::getContainer()->get(ProductSyncStrategy::class);
|
||||
}
|
||||
|
||||
public function testSupportsProductCategory(): void
|
||||
{
|
||||
$mt = $this->createModelType('Product Cat', 'PC-001', ModelCategory::PRODUCT);
|
||||
$this->assertTrue($this->strategy->supports($mt));
|
||||
}
|
||||
|
||||
public function testDoesNotSupportComponentCategory(): void
|
||||
{
|
||||
$mt = $this->createModelType('Comp Cat', 'CC-001', ModelCategory::COMPONENT);
|
||||
$this->assertFalse($this->strategy->supports($mt));
|
||||
}
|
||||
|
||||
public function testPreviewNoImpactWhenNoProducts(): void
|
||||
{
|
||||
$mt = $this->createModelType('Product Cat', 'PC-001', ModelCategory::PRODUCT);
|
||||
$result = $this->strategy->preview($mt, ['customFields' => []]);
|
||||
$this->assertSame(0, $result->itemCount);
|
||||
$this->assertFalse($result->hasImpact());
|
||||
}
|
||||
|
||||
public function testPreviewDetectsNewCustomField(): void
|
||||
{
|
||||
$mt = $this->createModelType('Product Cat', 'PC-001', ModelCategory::PRODUCT);
|
||||
$this->createProduct('P1', 'P1-REF', $mt);
|
||||
|
||||
$result = $this->strategy->preview($mt, [
|
||||
'customFields' => [
|
||||
['name' => 'Weight', 'type' => 'text', 'orderIndex' => 0],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSame(1, $result->itemCount);
|
||||
$this->assertSame(1, $result->additions['customFieldValues']);
|
||||
}
|
||||
|
||||
public function testExecuteCreatesCustomFieldValues(): void
|
||||
{
|
||||
$mt = $this->createModelType('Product Cat', 'PC-001', ModelCategory::PRODUCT);
|
||||
$product = $this->createProduct('P1', 'P1-REF', $mt);
|
||||
|
||||
// Create a custom field on the model type
|
||||
$this->createCustomField('Weight', 'text', null, null, null, $mt, 0);
|
||||
|
||||
$result = $this->strategy->execute($mt, new SyncConfirmation());
|
||||
|
||||
$this->assertSame(1, $result->itemsUpdated);
|
||||
$this->assertSame(1, $result->additions['customFieldValues']);
|
||||
|
||||
// Verify version incremented
|
||||
$this->getEntityManager()->refresh($product);
|
||||
$this->assertSame(2, $product->getVersion());
|
||||
}
|
||||
|
||||
public function testExecuteIsIdempotent(): void
|
||||
{
|
||||
$mt = $this->createModelType('Product Cat', 'PC-001', ModelCategory::PRODUCT);
|
||||
$product = $this->createProduct('P1', 'P1-REF', $mt);
|
||||
$cf = $this->createCustomField('Weight', 'text', null, null, null, $mt, 0);
|
||||
|
||||
// First execute
|
||||
$result1 = $this->strategy->execute($mt, new SyncConfirmation());
|
||||
$this->assertSame(1, $result1->additions['customFieldValues']);
|
||||
|
||||
// Second execute — no-op
|
||||
$result2 = $this->strategy->execute($mt, new SyncConfirmation());
|
||||
$this->assertSame(0, $result2->itemsUpdated);
|
||||
}
|
||||
}
|
||||
84
tests/Mcp/Security/McpHeaderAuthenticatorTest.php
Normal file
84
tests/Mcp/Security/McpHeaderAuthenticatorTest.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Mcp\Security;
|
||||
|
||||
use App\Tests\AbstractApiTestCase;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class McpHeaderAuthenticatorTest extends AbstractApiTestCase
|
||||
{
|
||||
public function testMcpEndpointRejectsWithoutCredentials(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('POST', '/_mcp', [
|
||||
'headers' => ['Content-Type' => 'application/json'],
|
||||
'body' => $this->mcpRequest(),
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
public function testMcpEndpointRejectsInvalidPassword(): void
|
||||
{
|
||||
$profile = $this->createProfile(
|
||||
roles: ['ROLE_VIEWER'],
|
||||
password: 'correct-password',
|
||||
);
|
||||
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('POST', '/_mcp', [
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Profile-Id' => $profile->getId(),
|
||||
'X-Profile-Password' => 'wrong-password',
|
||||
],
|
||||
'body' => $this->mcpRequest(),
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
public function testMcpEndpointAcceptsValidCredentials(): void
|
||||
{
|
||||
$profile = $this->createProfile(
|
||||
roles: ['ROLE_VIEWER'],
|
||||
password: 'valid-password',
|
||||
);
|
||||
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('POST', '/_mcp', [
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Profile-Id' => $profile->getId(),
|
||||
'X-Profile-Password' => 'valid-password',
|
||||
],
|
||||
'body' => $this->mcpRequest(),
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(200);
|
||||
}
|
||||
|
||||
private function mcpRequest(array $headers = [], array $body = []): string
|
||||
{
|
||||
$default = [
|
||||
'jsonrpc' => '2.0',
|
||||
'method' => 'initialize',
|
||||
'params' => [
|
||||
'protocolVersion' => '2025-03-26',
|
||||
'capabilities' => new stdClass(),
|
||||
'clientInfo' => ['name' => 'test', 'version' => '1.0'],
|
||||
],
|
||||
'id' => 1,
|
||||
];
|
||||
|
||||
return json_encode(array_merge($default, $body));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user