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:
Matthieu
2026-03-26 16:51:58 +01:00
parent 9299a46c8b
commit d568961eb3
6 changed files with 1540 additions and 1 deletions

View File

@@ -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;
}
}