feat(machine) : single save button + link versioning with restore
Backend: - Enrich machine snapshot with componentLinks/pieceLinks/productLinks - Detect link add/remove in MachineAuditSubscriber onFlush - Add link diff comparison in restore preview - Add link restoration in applyRestore for machines - Add integrity warnings for missing linked entities Frontend (submodule update): - Single save button replacing auto-save-on-blur - Link versioning display in version list and restore modal Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Submodule Inventory_frontend updated: 767c9a7424...a7415964a7
1074
docs/superpowers/plans/2026-03-26-machine-single-save.md
Normal file
1074
docs/superpowers/plans/2026-03-26-machine-single-save.md
Normal file
File diff suppressed because it is too large
Load Diff
117
docs/superpowers/specs/2026-03-26-machine-single-save-design.md
Normal file
117
docs/superpowers/specs/2026-03-26-machine-single-save-design.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Machine : Bouton Save Unique + Versioning des Liens
|
||||
|
||||
**Date :** 2026-03-26
|
||||
**Statut :** Approuvé
|
||||
|
||||
## Contexte
|
||||
|
||||
La page machine utilise actuellement un auto-save au blur pour chaque champ (info, custom fields, constructeurs). Les pages composant/pièce/produit utilisent un bouton unique "Enregistrer les modifications" en bas du formulaire. L'objectif est d'aligner la page machine sur ce pattern.
|
||||
|
||||
De plus, les ajouts/suppressions de liens composant/pièce/produit sur une machine ne sont pas tracés dans le versioning. Ils doivent l'être.
|
||||
|
||||
## Volet 1 : Bouton Save Unique
|
||||
|
||||
### Comportement cible
|
||||
|
||||
- En mode édition, tous les champs (info machine, custom field values, custom field definitions, constructeurs) sont modifiés localement sans appel API.
|
||||
- Un bouton "Enregistrer les modifications" en bas du formulaire sauvegarde tout d'un coup.
|
||||
- Un bouton "Annuler" réinitialise l'état local et sort du mode édition.
|
||||
- Les documents restent en upload/suppression immédiate (inchangé).
|
||||
- Les ajouts/suppressions de liens composant/pièce/produit restent immédiats via modales (inchangé).
|
||||
|
||||
### Changements frontend
|
||||
|
||||
#### MachineInfoCard.vue
|
||||
- Supprimer les `@blur` → `$emit('blur-field')` sur les inputs (nom, référence)
|
||||
- Supprimer le `@change` qui émet `blur-field` sur le select site
|
||||
- Supprimer les `@blur` → `$emit('update-custom-field', field)` sur tous les champs custom
|
||||
- Conserver `@input` / `@update:*` / `set-custom-field-value` pour la mise à jour de l'état local
|
||||
- Le `MachineCustomFieldDefEditor` perd son bouton save propre : l'état est collecté au submit global
|
||||
|
||||
#### machine/[id].vue
|
||||
- Supprimer le handler `@blur-field`
|
||||
- Supprimer le handler `@update-custom-field`
|
||||
- `@update:constructeur-ids` met à jour l'état local sans save
|
||||
- Ajouter le bloc boutons en bas (pattern identique à component/[id]/index.vue) :
|
||||
- "Annuler" (btn-ghost) → `cancelEdition()` : réinitialise depuis `machine.value` + sort du mode édition
|
||||
- "Enregistrer les modifications" (btn-primary, disabled si `!canSubmit`) → `submitEdition()`
|
||||
|
||||
#### useMachineDetailData.ts
|
||||
- Exposer `saving` ref
|
||||
- Exposer `submitEdition()` :
|
||||
1. `updateMachineInfo()` — PATCH machine (nom, ref, site, constructeurs)
|
||||
2. Batch save custom field values (tous les `visibleMachineCustomFields` avec valeur)
|
||||
3. Save custom field definitions si modifiées (`fieldDefs.saveDefinitions()`)
|
||||
4. `loadMachineData()` pour recharger
|
||||
5. Sortie du mode édition + toast succès
|
||||
- Exposer `cancelEdition()` :
|
||||
1. `initMachineFields()` — réinitialise nom, ref, site, constructeurs depuis `machine.value`
|
||||
2. `syncMachineCustomFields()` — réinitialise les custom fields
|
||||
3. Sort du mode édition
|
||||
|
||||
#### useMachineDetailUpdates.ts
|
||||
- `handleMachineConstructeurChange` ne déclenche plus `updateMachineInfo()`, met juste à jour le ref local
|
||||
|
||||
#### useMachineDetailCustomFields.ts
|
||||
- `updateMachineCustomField` n'est plus appelé au blur — sera appelé en batch par `submitEdition()`
|
||||
- Ajouter méthode `saveAllMachineCustomFields()` qui itère sur les champs visibles et sauvegarde ceux avec valeur
|
||||
|
||||
### Validation (`canSubmit`)
|
||||
- Machine existe
|
||||
- Nom non vide
|
||||
- Pas en cours de sauvegarde (`!saving.value`)
|
||||
- `canEdit` est true
|
||||
|
||||
## Volet 2 : Versioning des Liens Machine
|
||||
|
||||
### Comportement cible
|
||||
|
||||
Quand un composant, pièce ou produit est ajouté ou supprimé d'une machine, cela doit :
|
||||
1. Incrémenter la `version` de la Machine
|
||||
2. Créer une entrée `AuditLog` avec diff et snapshot
|
||||
|
||||
### Changements backend
|
||||
|
||||
#### MachineAuditSubscriber — enrichir le snapshot
|
||||
Ajouter au snapshot machine les liens :
|
||||
```php
|
||||
'componentLinks' => array_map(fn($link) => [
|
||||
'id' => $link->getId(),
|
||||
'composantId' => $link->getComposant()->getId(),
|
||||
'composantName' => $link->getComposant()->getName(),
|
||||
], $entity->getComponentLinks()->toArray()),
|
||||
'pieceLinks' => [...],
|
||||
'productLinks' => [...],
|
||||
```
|
||||
|
||||
#### Nouveau subscriber ou service : MachineLinkAuditService
|
||||
Écouter les événements Doctrine `postPersist` et `postRemove` sur les 3 entités link.
|
||||
Quand un lien est créé/supprimé :
|
||||
1. Récupérer la Machine parente
|
||||
2. Incrémenter `$machine->incrementVersion()`
|
||||
3. Créer un `AuditLog` :
|
||||
- `entityType: 'machine'`
|
||||
- `entityId: $machine->getId()`
|
||||
- `action: 'update'`
|
||||
- `diff: { addedComponent: {id, name} }` ou `{ removedPiece: {id, name} }`
|
||||
- `snapshot:` snapshot complet de la machine (avec liens mis à jour)
|
||||
- `version:` nouvelle version
|
||||
|
||||
### Labels pour le diff (frontend)
|
||||
Ajouter au `historyFieldLabels` de la page machine :
|
||||
```js
|
||||
addedComponent: 'Composant ajouté',
|
||||
removedComponent: 'Composant supprimé',
|
||||
addedPiece: 'Pièce ajoutée',
|
||||
removedPiece: 'Pièce supprimée',
|
||||
addedProduct: 'Produit ajouté',
|
||||
removedProduct: 'Produit supprimé',
|
||||
```
|
||||
|
||||
## Ce qui ne change PAS
|
||||
|
||||
- Upload/suppression de documents (immédiat)
|
||||
- Pattern read/edit toggle dans le header
|
||||
- L'affichage des sections composants/pièces/produits
|
||||
- Les modales d'ajout/suppression de liens (restent immédiates)
|
||||
- Le versioning des autres entités (composant, pièce, produit)
|
||||
30
migrations/Version20260326120000.php
Normal file
30
migrations/Version20260326120000.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260326120000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add referenceFormula and requiredFieldsForReference to model_types, referenceAuto to pieces';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE model_types ADD COLUMN IF NOT EXISTS referenceformula TEXT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE model_types ADD COLUMN IF NOT EXISTS requiredfieldsforreference JSON DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE pieces ADD COLUMN IF NOT EXISTS referenceauto VARCHAR(255) DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE pieces DROP COLUMN IF EXISTS referenceauto');
|
||||
$this->addSql('ALTER TABLE model_types DROP COLUMN IF EXISTS requiredfieldsforreference');
|
||||
$this->addSql('ALTER TABLE model_types DROP COLUMN IF EXISTS referenceformula');
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,38 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\AuditLog;
|
||||
use App\Entity\CustomFieldValue;
|
||||
use App\Entity\Machine;
|
||||
use App\Entity\MachineComponentLink;
|
||||
use App\Entity\MachinePieceLink;
|
||||
use App\Entity\MachineProductLink;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
|
||||
#[AsDoctrineListener(event: Events::onFlush)]
|
||||
final class MachineAuditSubscriber extends AbstractAuditSubscriber
|
||||
{
|
||||
public function onFlush(OnFlushEventArgs $args): void
|
||||
{
|
||||
// Let parent handle regular Machine entity changes (fields, collections, custom fields)
|
||||
parent::onFlush($args);
|
||||
|
||||
// Now handle link entity changes
|
||||
$em = $args->getObjectManager();
|
||||
if (!$em instanceof EntityManagerInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uow = $em->getUnitOfWork();
|
||||
$actorProfileId = $this->resolveActorProfileId();
|
||||
|
||||
$this->processLinkChanges($em, $uow, $actorProfileId);
|
||||
}
|
||||
|
||||
protected function supports(object $entity): bool
|
||||
{
|
||||
return $entity instanceof Machine;
|
||||
@@ -46,6 +70,34 @@ final class MachineAuditSubscriber extends AbstractAuditSubscriber
|
||||
];
|
||||
}
|
||||
|
||||
$componentLinks = [];
|
||||
foreach ($entity->getComponentLinks() as $link) {
|
||||
$componentLinks[] = [
|
||||
'id' => $link->getId(),
|
||||
'composantId' => $link->getComposant()->getId(),
|
||||
'composantName' => $link->getComposant()->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
$pieceLinks = [];
|
||||
foreach ($entity->getPieceLinks() as $link) {
|
||||
$pieceLinks[] = [
|
||||
'id' => $link->getId(),
|
||||
'pieceId' => $link->getPiece()->getId(),
|
||||
'pieceName' => $link->getPiece()->getName(),
|
||||
'quantity' => $link->getQuantity(),
|
||||
];
|
||||
}
|
||||
|
||||
$productLinks = [];
|
||||
foreach ($entity->getProductLinks() as $link) {
|
||||
$productLinks[] = [
|
||||
'id' => $link->getId(),
|
||||
'productId' => $link->getProduct()->getId(),
|
||||
'productName' => $link->getProduct()->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $entity->getId(),
|
||||
'name' => $this->safeGet($entity, 'getName'),
|
||||
@@ -54,7 +106,108 @@ final class MachineAuditSubscriber extends AbstractAuditSubscriber
|
||||
'site' => $this->normalizeValue($this->safeGet($entity, 'getSite')),
|
||||
'constructeurIds' => $this->normalizeCollection($entity->getConstructeurs()),
|
||||
'customFieldValues' => $customFieldValues,
|
||||
'componentLinks' => $componentLinks,
|
||||
'pieceLinks' => $pieceLinks,
|
||||
'productLinks' => $productLinks,
|
||||
'version' => $this->safeGet($entity, 'getVersion'),
|
||||
];
|
||||
}
|
||||
|
||||
private function processLinkChanges(EntityManagerInterface $em, UnitOfWork $uow, ?string $actorProfileId): void
|
||||
{
|
||||
$machineChanges = [];
|
||||
|
||||
// Detect inserted links
|
||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||
$info = $this->extractLinkInfo($entity, 'added');
|
||||
if (null === $info) {
|
||||
continue;
|
||||
}
|
||||
$machineId = (string) $info['machine']->getId();
|
||||
if ('' === $machineId) {
|
||||
continue;
|
||||
}
|
||||
$machineChanges[$machineId] ??= ['machine' => $info['machine'], 'diffs' => []];
|
||||
$machineChanges[$machineId]['diffs'][$info['diffKey']] = [
|
||||
'from' => null,
|
||||
'to' => $info['diffValue'],
|
||||
];
|
||||
}
|
||||
|
||||
// Detect deleted links
|
||||
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||
$info = $this->extractLinkInfo($entity, 'removed');
|
||||
if (null === $info) {
|
||||
continue;
|
||||
}
|
||||
$machineId = (string) $info['machine']->getId();
|
||||
if ('' === $machineId) {
|
||||
continue;
|
||||
}
|
||||
$machineChanges[$machineId] ??= ['machine' => $info['machine'], 'diffs' => []];
|
||||
$machineChanges[$machineId]['diffs'][$info['diffKey']] = [
|
||||
'from' => $info['diffValue'],
|
||||
'to' => null,
|
||||
];
|
||||
}
|
||||
|
||||
// Create audit logs for each affected machine
|
||||
foreach ($machineChanges as $machineId => $change) {
|
||||
$machine = $change['machine'];
|
||||
$diff = $change['diffs'];
|
||||
|
||||
if ([] === $diff) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$version = $this->incrementEntityVersion($machine, $em, $uow);
|
||||
$snapshot = $this->snapshotEntity($machine);
|
||||
|
||||
$this->persistAuditLog(
|
||||
$em,
|
||||
new AuditLog('machine', $machineId, 'update', $diff, $snapshot, $actorProfileId, $version),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|array{machine: Machine, diffKey: string, diffValue: array{id: string, name: string}}
|
||||
*/
|
||||
private function extractLinkInfo(object $entity, string $action): ?array
|
||||
{
|
||||
if ($entity instanceof MachineComponentLink) {
|
||||
return [
|
||||
'machine' => $entity->getMachine(),
|
||||
'diffKey' => $action.'Component',
|
||||
'diffValue' => [
|
||||
'id' => $entity->getComposant()->getId(),
|
||||
'name' => $entity->getComposant()->getName(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ($entity instanceof MachinePieceLink) {
|
||||
return [
|
||||
'machine' => $entity->getMachine(),
|
||||
'diffKey' => $action.'Piece',
|
||||
'diffValue' => [
|
||||
'id' => $entity->getPiece()->getId(),
|
||||
'name' => $entity->getPiece()->getName(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ($entity instanceof MachineProductLink) {
|
||||
return [
|
||||
'machine' => $entity->getMachine(),
|
||||
'diffKey' => $action.'Product',
|
||||
'diffValue' => [
|
||||
'id' => $entity->getProduct()->getId(),
|
||||
'name' => $entity->getProduct()->getName(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ use App\Entity\ComposantPieceSlot;
|
||||
use App\Entity\ComposantProductSlot;
|
||||
use App\Entity\ComposantSubcomponentSlot;
|
||||
use App\Entity\Machine;
|
||||
use App\Entity\MachineComponentLink;
|
||||
use App\Entity\MachinePieceLink;
|
||||
use App\Entity\MachineProductLink;
|
||||
use App\Entity\Piece;
|
||||
use App\Entity\PieceProductSlot;
|
||||
use App\Entity\Product;
|
||||
@@ -295,6 +298,40 @@ final class EntityVersionService
|
||||
return $warnings;
|
||||
}
|
||||
|
||||
// Machine: check link references
|
||||
if ('machine' === $entityType) {
|
||||
$linkChecks = [
|
||||
['key' => 'componentLinks', 'refKey' => 'composantId', 'nameKey' => 'composantName', 'label' => 'composant', 'repo' => $this->composants],
|
||||
['key' => 'pieceLinks', 'refKey' => 'pieceId', 'nameKey' => 'pieceName', 'label' => 'pièce', 'repo' => $this->pieces],
|
||||
['key' => 'productLinks', 'refKey' => 'productId', 'nameKey' => 'productName', 'label' => 'produit', 'repo' => $this->products],
|
||||
];
|
||||
|
||||
foreach ($linkChecks as $check) {
|
||||
$links = $snapshot[$check['key']] ?? [];
|
||||
$refIds = [];
|
||||
foreach ($links as $link) {
|
||||
$refId = $link[$check['refKey']] ?? null;
|
||||
if (null !== $refId) {
|
||||
$refIds[$refId] = $link[$check['nameKey']] ?? $refId;
|
||||
}
|
||||
}
|
||||
if ([] === $refIds) {
|
||||
continue;
|
||||
}
|
||||
$foundIds = array_map(fn ($e) => $e->getId(), $check['repo']->findBy(['id' => array_keys($refIds)]));
|
||||
foreach ($refIds as $id => $name) {
|
||||
if (!in_array($id, $foundIds, true)) {
|
||||
$warnings[] = [
|
||||
'field' => $check['key'],
|
||||
'message' => sprintf("Le %s '%s' n'existe plus. Le lien ne sera pas restauré.", $check['label'], $name),
|
||||
'missingEntityId' => $id,
|
||||
'missingEntityName' => $name,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Full mode: check slot references (batch queries per slot type)
|
||||
$slotChecks = match ($entityType) {
|
||||
'composant' => [
|
||||
@@ -385,6 +422,48 @@ final class EntityVersionService
|
||||
}
|
||||
}
|
||||
|
||||
// Machine: link diffs
|
||||
if ('machine' === $entityType) {
|
||||
$linkTypes = [
|
||||
'componentLinks' => ['idKey' => 'composantId', 'nameKey' => 'composantName', 'getter' => 'getComponentLinks', 'entityGetter' => 'getComposant'],
|
||||
'pieceLinks' => ['idKey' => 'pieceId', 'nameKey' => 'pieceName', 'getter' => 'getPieceLinks', 'entityGetter' => 'getPiece'],
|
||||
'productLinks' => ['idKey' => 'productId', 'nameKey' => 'productName', 'getter' => 'getProductLinks', 'entityGetter' => 'getProduct'],
|
||||
];
|
||||
|
||||
foreach ($linkTypes as $key => $config) {
|
||||
$currentIds = [];
|
||||
$currentNames = [];
|
||||
if (method_exists($entity, $config['getter'])) {
|
||||
foreach ($entity->{$config['getter']}() as $link) {
|
||||
$linked = $link->{$config['entityGetter']}();
|
||||
$currentIds[] = $linked->getId();
|
||||
$currentNames[$linked->getId()] = $linked->getName();
|
||||
}
|
||||
}
|
||||
|
||||
$snapshotIds = [];
|
||||
$snapshotNames = [];
|
||||
foreach ($snapshot[$key] ?? [] as $entry) {
|
||||
$id = $entry[$config['idKey']] ?? null;
|
||||
if ($id) {
|
||||
$snapshotIds[] = $id;
|
||||
$snapshotNames[$id] = $entry[$config['nameKey']] ?? $id;
|
||||
}
|
||||
}
|
||||
|
||||
sort($currentIds);
|
||||
sort($snapshotIds);
|
||||
if ($currentIds !== $snapshotIds) {
|
||||
$currentDisplay = array_map(fn ($id) => $currentNames[$id] ?? $id, $currentIds);
|
||||
$restoredDisplay = array_map(fn ($id) => $snapshotNames[$id] ?? $id, $snapshotIds);
|
||||
$diff[$key] = [
|
||||
'current' => [] !== $currentDisplay ? implode(', ', $currentDisplay) : '(aucun)',
|
||||
'restored' => [] !== $restoredDisplay ? implode(', ', $restoredDisplay) : '(aucun)',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom field values diff
|
||||
$snapshotCfvs = $snapshot['customFieldValues'] ?? [];
|
||||
if ([] !== $snapshotCfvs && method_exists($entity, 'getCustomFieldValues')) {
|
||||
@@ -453,6 +532,11 @@ final class EntityVersionService
|
||||
default => null,
|
||||
};
|
||||
|
||||
// Machine: restore links
|
||||
if ('machine' === $entityType) {
|
||||
$this->restoreMachineLinks($entity, $snapshot);
|
||||
}
|
||||
|
||||
// Full mode: restore custom field values
|
||||
$this->restoreCustomFieldValues($entityType, $entity, $snapshot);
|
||||
}
|
||||
@@ -490,6 +574,62 @@ final class EntityVersionService
|
||||
}
|
||||
}
|
||||
|
||||
private function restoreMachineLinks(Machine $machine, array $snapshot): void
|
||||
{
|
||||
// Remove all existing links
|
||||
foreach ($machine->getProductLinks()->toArray() as $link) {
|
||||
$this->em->remove($link);
|
||||
}
|
||||
foreach ($machine->getPieceLinks()->toArray() as $link) {
|
||||
$this->em->remove($link);
|
||||
}
|
||||
foreach ($machine->getComponentLinks()->toArray() as $link) {
|
||||
$this->em->remove($link);
|
||||
}
|
||||
|
||||
// Flush removals to avoid FK conflicts
|
||||
$this->em->flush();
|
||||
|
||||
// Recreate component links
|
||||
foreach ($snapshot['componentLinks'] ?? [] as $data) {
|
||||
$composant = !empty($data['composantId']) ? $this->composants->find($data['composantId']) : null;
|
||||
if (null === $composant) {
|
||||
continue;
|
||||
}
|
||||
$link = new MachineComponentLink();
|
||||
$link->setMachine($machine);
|
||||
$link->setComposant($composant);
|
||||
$this->em->persist($link);
|
||||
}
|
||||
|
||||
// Recreate piece links
|
||||
foreach ($snapshot['pieceLinks'] ?? [] as $data) {
|
||||
$piece = !empty($data['pieceId']) ? $this->pieces->find($data['pieceId']) : null;
|
||||
if (null === $piece) {
|
||||
continue;
|
||||
}
|
||||
$link = new MachinePieceLink();
|
||||
$link->setMachine($machine);
|
||||
$link->setPiece($piece);
|
||||
if (isset($data['quantity']) && $data['quantity'] > 0) {
|
||||
$link->setQuantity((int) $data['quantity']);
|
||||
}
|
||||
$this->em->persist($link);
|
||||
}
|
||||
|
||||
// Recreate product links
|
||||
foreach ($snapshot['productLinks'] ?? [] as $data) {
|
||||
$product = !empty($data['productId']) ? $this->products->find($data['productId']) : null;
|
||||
if (null === $product) {
|
||||
continue;
|
||||
}
|
||||
$link = new MachineProductLink();
|
||||
$link->setMachine($machine);
|
||||
$link->setProduct($product);
|
||||
$this->em->persist($link);
|
||||
}
|
||||
}
|
||||
|
||||
private function restoreComposantSlots(Composant $entity, array $snapshot): void
|
||||
{
|
||||
// Clear existing slots
|
||||
@@ -672,6 +812,31 @@ final class EntityVersionService
|
||||
$snapshot['site'] = $site ? ['id' => $site->getId(), 'name' => $site->getName()] : null;
|
||||
}
|
||||
|
||||
// Machine: links
|
||||
if ('machine' === $entityType) {
|
||||
$snapshot['componentLinks'] = [];
|
||||
foreach ($entity->getComponentLinks() as $link) {
|
||||
$snapshot['componentLinks'][] = [
|
||||
'id' => $link->getId(), 'composantId' => $link->getComposant()->getId(),
|
||||
'composantName' => $link->getComposant()->getName(),
|
||||
];
|
||||
}
|
||||
$snapshot['pieceLinks'] = [];
|
||||
foreach ($entity->getPieceLinks() as $link) {
|
||||
$snapshot['pieceLinks'][] = [
|
||||
'id' => $link->getId(), 'pieceId' => $link->getPiece()->getId(),
|
||||
'pieceName' => $link->getPiece()->getName(), 'quantity' => $link->getQuantity(),
|
||||
];
|
||||
}
|
||||
$snapshot['productLinks'] = [];
|
||||
foreach ($entity->getProductLinks() as $link) {
|
||||
$snapshot['productLinks'][] = [
|
||||
'id' => $link->getId(), 'productId' => $link->getProduct()->getId(),
|
||||
'productName' => $link->getProduct()->getName(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Composant/Piece: product
|
||||
if (in_array($entityType, ['composant', 'piece'], true) && method_exists($entity, 'getProduct')) {
|
||||
$product = $entity->getProduct();
|
||||
|
||||
Reference in New Issue
Block a user