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