feat(machines) : allow category-only links on machine structure
Enable adding a component, piece, or product to a machine by selecting only the category (ModelType) without a specific entity. The link displays a red "À remplir" badge; clicking it reopens the modal pre-filled with the category so the user can associate an item later. Backend: entity FKs made nullable on the 3 link tables, modelType FK added, controller/audit/version/MCP normalization adapted for null entities. Frontend: modal accepts category-only confirm, page handles fill mode, hierarchy builder creates pending nodes, display components show clickable badge with event propagation through the full hierarchy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -623,17 +623,21 @@ class MachineStructureController extends AbstractController
|
||||
{
|
||||
return array_map(function (MachineComponentLink $link): array {
|
||||
$composant = $link->getComposant();
|
||||
$modelType = $link->getModelType();
|
||||
$parentLink = $link->getParentLink();
|
||||
|
||||
return [
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'composantId' => $composant->getId(),
|
||||
'composant' => $this->normalizeComposant($composant),
|
||||
'composantId' => $composant?->getId(),
|
||||
'composant' => $composant ? $this->normalizeComposant($composant) : null,
|
||||
'modelTypeId' => $modelType?->getId(),
|
||||
'modelType' => $modelType ? $this->normalizeModelType($modelType) : null,
|
||||
'pendingEntity' => null === $composant,
|
||||
'parentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()?->getId(),
|
||||
'overrides' => $this->normalizeOverrides($link),
|
||||
'childLinks' => [],
|
||||
'pieceLinks' => [],
|
||||
@@ -645,19 +649,23 @@ class MachineStructureController extends AbstractController
|
||||
{
|
||||
return array_map(function (MachinePieceLink $link): array {
|
||||
$piece = $link->getPiece();
|
||||
$modelType = $link->getModelType();
|
||||
$parentLink = $link->getParentLink();
|
||||
|
||||
return [
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'pieceId' => $piece->getId(),
|
||||
'piece' => $this->normalizePiece($piece),
|
||||
'pieceId' => $piece?->getId(),
|
||||
'piece' => $piece ? $this->normalizePiece($piece) : null,
|
||||
'modelTypeId' => $modelType?->getId(),
|
||||
'modelType' => $modelType ? $this->normalizeModelType($modelType) : null,
|
||||
'pendingEntity' => null === $piece,
|
||||
'parentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()?->getId(),
|
||||
'overrides' => $this->normalizeOverrides($link),
|
||||
'quantity' => $this->resolvePieceQuantity($link),
|
||||
'quantity' => $piece ? $this->resolvePieceQuantity($link) : 1,
|
||||
];
|
||||
}, $links);
|
||||
}
|
||||
@@ -665,13 +673,16 @@ class MachineStructureController extends AbstractController
|
||||
private function resolvePieceQuantity(MachinePieceLink $link): int
|
||||
{
|
||||
$parentLink = $link->getParentLink();
|
||||
$piece = $link->getPiece();
|
||||
|
||||
if (!$parentLink) {
|
||||
if (!$parentLink || !$piece) {
|
||||
return $link->getQuantity();
|
||||
}
|
||||
|
||||
$composant = $parentLink->getComposant();
|
||||
$piece = $link->getPiece();
|
||||
if (!$composant) {
|
||||
return $link->getQuantity();
|
||||
}
|
||||
|
||||
foreach ($composant->getPieceSlots() as $slot) {
|
||||
if ($slot->getSelectedPiece()?->getId() === $piece->getId()) {
|
||||
@@ -685,14 +696,18 @@ class MachineStructureController extends AbstractController
|
||||
private function normalizeProductLinks(array $links): array
|
||||
{
|
||||
return array_map(function (MachineProductLink $link): array {
|
||||
$product = $link->getProduct();
|
||||
$product = $link->getProduct();
|
||||
$modelType = $link->getModelType();
|
||||
|
||||
return [
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'productId' => $product->getId(),
|
||||
'product' => $this->normalizeProduct($product),
|
||||
'productId' => $product?->getId(),
|
||||
'product' => $product ? $this->normalizeProduct($product) : null,
|
||||
'modelTypeId' => $modelType?->getId(),
|
||||
'modelType' => $modelType ? $this->normalizeModelType($modelType) : null,
|
||||
'pendingEntity' => null === $product,
|
||||
'parentLinkId' => $link->getParentLink()?->getId(),
|
||||
'parentComponentLinkId' => $link->getParentComponentLink()?->getId(),
|
||||
'parentPieceLinkId' => $link->getParentPieceLink()?->getId(),
|
||||
|
||||
@@ -46,8 +46,12 @@ class MachineComponentLink
|
||||
private Machine $machine;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'machineLinks')]
|
||||
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Composant $composant;
|
||||
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?Composant $composant = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class)]
|
||||
#[ORM\JoinColumn(name: 'modelTypeId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?ModelType $modelType = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: MachineComponentLink::class, inversedBy: 'childLinks')]
|
||||
#[ORM\JoinColumn(name: 'parentLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
@@ -107,18 +111,30 @@ class MachineComponentLink
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComposant(): Composant
|
||||
public function getComposant(): ?Composant
|
||||
{
|
||||
return $this->composant;
|
||||
}
|
||||
|
||||
public function setComposant(Composant $composant): static
|
||||
public function setComposant(?Composant $composant): static
|
||||
{
|
||||
$this->composant = $composant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModelType(): ?ModelType
|
||||
{
|
||||
return $this->modelType;
|
||||
}
|
||||
|
||||
public function setModelType(?ModelType $modelType): static
|
||||
{
|
||||
$this->modelType = $modelType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParentLink(): ?MachineComponentLink
|
||||
{
|
||||
return $this->parentLink;
|
||||
|
||||
@@ -47,8 +47,12 @@ class MachinePieceLink
|
||||
private Machine $machine;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'machineLinks')]
|
||||
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Piece $piece;
|
||||
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?Piece $piece = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class)]
|
||||
#[ORM\JoinColumn(name: 'modelTypeId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?ModelType $modelType = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: MachineComponentLink::class, inversedBy: 'pieceLinks')]
|
||||
#[ORM\JoinColumn(name: 'parentLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
@@ -98,18 +102,30 @@ class MachinePieceLink
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPiece(): Piece
|
||||
public function getPiece(): ?Piece
|
||||
{
|
||||
return $this->piece;
|
||||
}
|
||||
|
||||
public function setPiece(Piece $piece): static
|
||||
public function setPiece(?Piece $piece): static
|
||||
{
|
||||
$this->piece = $piece;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModelType(): ?ModelType
|
||||
{
|
||||
return $this->modelType;
|
||||
}
|
||||
|
||||
public function setModelType(?ModelType $modelType): static
|
||||
{
|
||||
$this->modelType = $modelType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParentLink(): ?MachineComponentLink
|
||||
{
|
||||
return $this->parentLink;
|
||||
|
||||
@@ -46,8 +46,12 @@ class MachineProductLink
|
||||
private Machine $machine;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'machineLinks')]
|
||||
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Product $product;
|
||||
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?Product $product = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class)]
|
||||
#[ORM\JoinColumn(name: 'modelTypeId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?ModelType $modelType = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: MachineProductLink::class, inversedBy: 'childLinks')]
|
||||
#[ORM\JoinColumn(name: 'parentLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
@@ -92,18 +96,30 @@ class MachineProductLink
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProduct(): Product
|
||||
public function getProduct(): ?Product
|
||||
{
|
||||
return $this->product;
|
||||
}
|
||||
|
||||
public function setProduct(Product $product): static
|
||||
public function setProduct(?Product $product): static
|
||||
{
|
||||
$this->product = $product;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModelType(): ?ModelType
|
||||
{
|
||||
return $this->modelType;
|
||||
}
|
||||
|
||||
public function setModelType(?ModelType $modelType): static
|
||||
{
|
||||
$this->modelType = $modelType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParentLink(): ?MachineProductLink
|
||||
{
|
||||
return $this->parentLink;
|
||||
|
||||
@@ -74,18 +74,20 @@ final class MachineAuditSubscriber extends AbstractAuditSubscriber
|
||||
foreach ($entity->getComponentLinks() as $link) {
|
||||
$componentLinks[] = [
|
||||
'id' => $link->getId(),
|
||||
'composantId' => $link->getComposant()->getId(),
|
||||
'composantName' => $link->getComposant()->getName(),
|
||||
'composantId' => $link->getComposant()?->getId(),
|
||||
'composantName' => $link->getComposant()?->getName(),
|
||||
'modelTypeId' => $link->getModelType()?->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
$pieceLinks = [];
|
||||
foreach ($entity->getPieceLinks() as $link) {
|
||||
$pieceLinks[] = [
|
||||
'id' => $link->getId(),
|
||||
'pieceId' => $link->getPiece()->getId(),
|
||||
'pieceName' => $link->getPiece()->getName(),
|
||||
'quantity' => $link->getQuantity(),
|
||||
'id' => $link->getId(),
|
||||
'pieceId' => $link->getPiece()?->getId(),
|
||||
'pieceName' => $link->getPiece()?->getName(),
|
||||
'quantity' => $link->getQuantity(),
|
||||
'modelTypeId' => $link->getModelType()?->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -93,8 +95,9 @@ final class MachineAuditSubscriber extends AbstractAuditSubscriber
|
||||
foreach ($entity->getProductLinks() as $link) {
|
||||
$productLinks[] = [
|
||||
'id' => $link->getId(),
|
||||
'productId' => $link->getProduct()->getId(),
|
||||
'productName' => $link->getProduct()->getName(),
|
||||
'productId' => $link->getProduct()?->getId(),
|
||||
'productName' => $link->getProduct()?->getName(),
|
||||
'modelTypeId' => $link->getModelType()?->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -187,8 +190,8 @@ final class MachineAuditSubscriber extends AbstractAuditSubscriber
|
||||
'machine' => $entity->getMachine(),
|
||||
'diffKey' => $action.'Component',
|
||||
'diffValue' => [
|
||||
'id' => $entity->getComposant()->getId(),
|
||||
'name' => $entity->getComposant()->getName(),
|
||||
'id' => $entity->getComposant()?->getId() ?? $entity->getModelType()?->getId(),
|
||||
'name' => $entity->getComposant()?->getName() ?? $entity->getModelType()?->getName() ?? 'Catégorie seule',
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -198,8 +201,8 @@ final class MachineAuditSubscriber extends AbstractAuditSubscriber
|
||||
'machine' => $entity->getMachine(),
|
||||
'diffKey' => $action.'Piece',
|
||||
'diffValue' => [
|
||||
'id' => $entity->getPiece()->getId(),
|
||||
'name' => $entity->getPiece()->getName(),
|
||||
'id' => $entity->getPiece()?->getId() ?? $entity->getModelType()?->getId(),
|
||||
'name' => $entity->getPiece()?->getName() ?? $entity->getModelType()?->getName() ?? 'Catégorie seule',
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -209,8 +212,8 @@ final class MachineAuditSubscriber extends AbstractAuditSubscriber
|
||||
'machine' => $entity->getMachine(),
|
||||
'diffKey' => $action.'Product',
|
||||
'diffValue' => [
|
||||
'id' => $entity->getProduct()->getId(),
|
||||
'name' => $entity->getProduct()->getName(),
|
||||
'id' => $entity->getProduct()?->getId() ?? $entity->getModelType()?->getId(),
|
||||
'name' => $entity->getProduct()?->getName() ?? $entity->getModelType()?->getName() ?? 'Catégorie seule',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -164,11 +164,11 @@ class MachineStructureTool
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'composantId' => $composant->getId(),
|
||||
'composant' => $this->normalizeComposant($composant),
|
||||
'composantId' => $composant?->getId(),
|
||||
'composant' => $composant ? $this->normalizeComposant($composant) : null,
|
||||
'parentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()?->getId(),
|
||||
'overrides' => $this->normalizeOverrides($link),
|
||||
'childLinks' => [],
|
||||
'pieceLinks' => [],
|
||||
@@ -189,13 +189,13 @@ class MachineStructureTool
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'pieceId' => $piece->getId(),
|
||||
'piece' => $this->normalizePiece($piece),
|
||||
'pieceId' => $piece?->getId(),
|
||||
'piece' => $piece ? $this->normalizePiece($piece) : null,
|
||||
'parentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()?->getId(),
|
||||
'overrides' => $this->normalizeOverrides($link),
|
||||
'quantity' => $this->resolvePieceQuantity($link),
|
||||
'quantity' => $piece ? $this->resolvePieceQuantity($link) : 1,
|
||||
];
|
||||
}, $links);
|
||||
}
|
||||
|
||||
@@ -869,22 +869,25 @@ final class EntityVersionService
|
||||
$snapshot['componentLinks'] = [];
|
||||
foreach ($entity->getComponentLinks() as $link) {
|
||||
$snapshot['componentLinks'][] = [
|
||||
'id' => $link->getId(), 'composantId' => $link->getComposant()->getId(),
|
||||
'composantName' => $link->getComposant()->getName(),
|
||||
'id' => $link->getId(), 'composantId' => $link->getComposant()?->getId(),
|
||||
'composantName' => $link->getComposant()?->getName(),
|
||||
'modelTypeId' => $link->getModelType()?->getId(),
|
||||
];
|
||||
}
|
||||
$snapshot['pieceLinks'] = [];
|
||||
foreach ($entity->getPieceLinks() as $link) {
|
||||
$snapshot['pieceLinks'][] = [
|
||||
'id' => $link->getId(), 'pieceId' => $link->getPiece()->getId(),
|
||||
'pieceName' => $link->getPiece()->getName(), 'quantity' => $link->getQuantity(),
|
||||
'id' => $link->getId(), 'pieceId' => $link->getPiece()?->getId(),
|
||||
'pieceName' => $link->getPiece()?->getName(), 'quantity' => $link->getQuantity(),
|
||||
'modelTypeId' => $link->getModelType()?->getId(),
|
||||
];
|
||||
}
|
||||
$snapshot['productLinks'] = [];
|
||||
foreach ($entity->getProductLinks() as $link) {
|
||||
$snapshot['productLinks'][] = [
|
||||
'id' => $link->getId(), 'productId' => $link->getProduct()->getId(),
|
||||
'productName' => $link->getProduct()->getName(),
|
||||
'id' => $link->getId(), 'productId' => $link->getProduct()?->getId(),
|
||||
'productName' => $link->getProduct()?->getName(),
|
||||
'modelTypeId' => $link->getModelType()?->getId(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user