diff --git a/Inventory_frontend b/Inventory_frontend index db630e3..40d0753 160000 --- a/Inventory_frontend +++ b/Inventory_frontend @@ -1 +1 @@ -Subproject commit db630e315b2a5bd3ccd89abaa1c3bce1f9a78ef5 +Subproject commit 40d0753637f53092a557c7ebf162b243fa6780e9 diff --git a/migrations/Version20260323100000.php b/migrations/Version20260323100000.php new file mode 100644 index 0000000..a9bc07b --- /dev/null +++ b/migrations/Version20260323100000.php @@ -0,0 +1,91 @@ +addSql(<<<'SQL' + INSERT INTO composant_piece_slots (id, "composantid", "typepieceid", quantity, position, "createdat", "updatedat") + SELECT + 'cl' || substr(md5(random()::text || clock_timestamp()::text || spr.id), 1, 24), + c.id, + spr."typepieceid", + 1, + spr.position, + NOW(), + NOW() + FROM composants c + JOIN skeleton_piece_requirements spr ON spr."modeltypeid" = c."typecomposantid" + WHERE c."typecomposantid" IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM composant_piece_slots cps + WHERE cps."composantid" = c.id AND cps."typepieceid" = spr."typepieceid" + ) + SQL); + + // Product slots + $this->addSql(<<<'SQL' + INSERT INTO composant_product_slots (id, "composantid", "typeproductid", "familycode", position, "createdat", "updatedat") + SELECT + 'cl' || substr(md5(random()::text || clock_timestamp()::text || spr.id), 1, 24), + c.id, + spr."typeproductid", + spr."familycode", + spr.position, + NOW(), + NOW() + FROM composants c + JOIN skeleton_product_requirements spr ON spr."modeltypeid" = c."typecomposantid" + WHERE c."typecomposantid" IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM composant_product_slots cps + WHERE cps."composantid" = c.id AND cps."typeproductid" = spr."typeproductid" + ) + SQL); + + // Subcomponent slots + $this->addSql(<<<'SQL' + INSERT INTO composant_subcomponent_slots (id, "composantid", alias, "familycode", "typecomposantid", position, "createdat", "updatedat") + SELECT + 'cl' || substr(md5(random()::text || clock_timestamp()::text || spr.id), 1, 24), + c.id, + spr.alias, + spr."familycode", + spr."typecomposantid", + spr.position, + NOW(), + NOW() + FROM composants c + JOIN skeleton_subcomponent_requirements spr ON spr."modeltypeid" = c."typecomposantid" + WHERE c."typecomposantid" IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM composant_subcomponent_slots css + WHERE css."composantid" = c.id + AND COALESCE(css."typecomposantid", '') = COALESCE(spr."typecomposantid", '') + AND COALESCE(css.alias, '') = COALESCE(spr.alias, '') + ) + SQL); + } + + public function down(Schema $schema): void + { + // No-op: slots created by this migration are valid data + } +} diff --git a/src/Entity/Composant.php b/src/Entity/Composant.php index 4435b86..9fe6fd5 100644 --- a/src/Entity/Composant.php +++ b/src/Entity/Composant.php @@ -16,6 +16,7 @@ use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Put; use App\Entity\Trait\CuidEntityTrait; use App\Repository\ComposantRepository; +use App\State\ComposantProcessor; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -33,7 +34,7 @@ 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 Post(security: "is_granted('ROLE_GESTIONNAIRE')", processor: ComposantProcessor::class), new Put(security: "is_granted('ROLE_GESTIONNAIRE')"), new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), diff --git a/src/EventSubscriber/UniqueConstraintSubscriber.php b/src/EventSubscriber/UniqueConstraintSubscriber.php index dd5fbb4..91f9f60 100644 --- a/src/EventSubscriber/UniqueConstraintSubscriber.php +++ b/src/EventSubscriber/UniqueConstraintSubscriber.php @@ -16,7 +16,7 @@ final class UniqueConstraintSubscriber implements EventSubscriberInterface public static function getSubscribedEvents(): array { return [ - KernelEvents::EXCEPTION => 'onKernelException', + KernelEvents::EXCEPTION => ['onKernelException', 256], ]; } @@ -28,10 +28,17 @@ final class UniqueConstraintSubscriber implements EventSubscriberInterface return; } + $constraint = $this->detectConstraintName($exception); + $error = match ($constraint) { + 'unique_category_name' => 'Un élément avec ce nom existe déjà dans cette catégorie.', + default => 'Un élément avec cette valeur existe déjà.', + }; + $event->setResponse(new JsonResponse( [ - 'success' => false, - 'error' => 'nom duplique', + 'success' => false, + 'error' => $error, + 'constraint' => $constraint, ], JsonResponse::HTTP_CONFLICT )); @@ -47,4 +54,15 @@ final class UniqueConstraintSubscriber implements EventSubscriberInterface return null; } + + private function detectConstraintName(UniqueConstraintViolationException $exception): ?string + { + $message = $exception->getMessage(); + + if (preg_match('/constraint\s+"([^"]+)"/', $message, $matches)) { + return $matches[1]; + } + + return null; + } } diff --git a/src/State/ComposantProcessor.php b/src/State/ComposantProcessor.php new file mode 100644 index 0000000..eb87125 --- /dev/null +++ b/src/State/ComposantProcessor.php @@ -0,0 +1,110 @@ + $uriVariables + * @param array $context + */ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed + { + if (!$data instanceof Composant) { + return $this->decorated->process($data, $operation, $uriVariables, $context); + } + + $isCreation = $operation instanceof Post; + + // Persist the entity first + $result = $this->decorated->process($data, $operation, $uriVariables, $context); + + // On creation, scaffold slots from the ModelType skeleton + if ($isCreation) { + $this->initializeSlotsFromSkeleton($data); + } + + return $result; + } + + private function initializeSlotsFromSkeleton(Composant $composant): void + { + $modelType = $composant->getTypeComposant(); + + if (!$modelType) { + return; + } + + // Ensure the ModelType's skeleton collections are loaded + $modelType = $this->entityManager->getRepository(ModelType::class)->find($modelType->getId()); + + if (!$modelType) { + return; + } + + $hasNewSlots = false; + + // Piece slots + foreach ($modelType->getSkeletonPieceRequirements() as $req) { + $slot = new ComposantPieceSlot(); + $slot->setComposant($composant); + $slot->setTypePiece($req->getTypePiece()); + $slot->setPosition($req->getPosition()); + $this->entityManager->persist($slot); + $hasNewSlots = true; + } + + // Product slots + foreach ($modelType->getSkeletonProductRequirements() as $req) { + $slot = new ComposantProductSlot(); + $slot->setComposant($composant); + $slot->setTypeProduct($req->getTypeProduct()); + $slot->setFamilyCode($req->getFamilyCode()); + $slot->setPosition($req->getPosition()); + $this->entityManager->persist($slot); + $hasNewSlots = true; + } + + // Subcomponent slots + foreach ($modelType->getSkeletonSubcomponentRequirements() as $req) { + $slot = new ComposantSubcomponentSlot(); + $slot->setComposant($composant); + $slot->setAlias($req->getAlias()); + $slot->setFamilyCode($req->getFamilyCode()); + $slot->setTypeComposant($req->getTypeComposant()); + $slot->setPosition($req->getPosition()); + $this->entityManager->persist($slot); + $hasNewSlots = true; + } + + if ($hasNewSlots) { + $this->entityManager->flush(); + } + } +}