diff --git a/src/Controller/ComposantPieceSlotController.php b/src/Controller/ComposantPieceSlotController.php index 522a6f6..6af2f9b 100644 --- a/src/Controller/ComposantPieceSlotController.php +++ b/src/Controller/ComposantPieceSlotController.php @@ -43,6 +43,18 @@ class ComposantPieceSlotController extends AbstractController $slot->setSelectedPiece(null); } else { $piece = $this->entityManager->find(Piece::class, $payload['selectedPieceId']); + if (!$piece) { + return $this->json(['success' => false, 'error' => 'Pièce introuvable.'], 404); + } + + $slotTypePiece = $slot->getTypePiece(); + if ($slotTypePiece && $piece->getTypePiece()?->getId() !== $slotTypePiece->getId()) { + return $this->json([ + 'success' => false, + 'error' => sprintf('La pièce doit être de type « %s ».', $slotTypePiece->getName()), + ], 422); + } + $slot->setSelectedPiece($piece); } } diff --git a/src/Controller/CustomFieldValueController.php b/src/Controller/CustomFieldValueController.php index 162ee4f..54ee926 100644 --- a/src/Controller/CustomFieldValueController.php +++ b/src/Controller/CustomFieldValueController.php @@ -196,19 +196,16 @@ class CustomFieldValueController extends AbstractController return $this->json(['success' => false, 'error' => 'customFieldId or customFieldName is required.'], 400); } - $customField = new CustomField(); - $customField->setName($customFieldName); - $customField->setType((string) ($payload['customFieldType'] ?? 'text')); - $customField->setRequired((bool) ($payload['customFieldRequired'] ?? false)); - - $options = $payload['customFieldOptions'] ?? null; - if (is_array($options)) { - $customField->setOptions($options); + // Try to find an existing custom field by name instead of creating an orphan + $existing = $this->customFieldRepository->findOneBy(['name' => $customFieldName]); + if ($existing instanceof CustomField) { + return $existing; } - $this->entityManager->persist($customField); - - return $customField; + return $this->json([ + 'success' => false, + 'error' => sprintf('Custom field "%s" not found. Create it explicitly first.', $customFieldName), + ], 404); } private function resolveTarget(array $payload): array|JsonResponse diff --git a/src/Controller/MachineStructureController.php b/src/Controller/MachineStructureController.php index 20c8d1c..1986d32 100644 --- a/src/Controller/MachineStructureController.php +++ b/src/Controller/MachineStructureController.php @@ -173,6 +173,8 @@ class MachineStructureController extends AbstractController private function cloneCustomFields(Machine $source, Machine $target): void { + $cfMap = []; + foreach ($source->getCustomFields() as $cf) { $newCf = new CustomField(); $newCf->setName($cf->getName()); @@ -183,12 +185,20 @@ class MachineStructureController extends AbstractController $newCf->setOrderIndex($cf->getOrderIndex()); $newCf->setMachine($target); $this->entityManager->persist($newCf); + + $cfMap[$cf->getId()] = $newCf; } foreach ($source->getCustomFieldValues() as $cfv) { + $originalCf = $cfv->getCustomField(); + $newCf = $cfMap[$originalCf->getId()] ?? null; + if (!$newCf) { + continue; + } + $newValue = new CustomFieldValue(); $newValue->setMachine($target); - $newValue->setCustomField($cfv->getCustomField()); + $newValue->setCustomField($newCf); $newValue->setValue($cfv->getValue()); $this->entityManager->persist($newValue); } diff --git a/src/Service/ModelTypeCategoryConversionService.php b/src/Service/ModelTypeCategoryConversionService.php index e91bf27..8942b5d 100644 --- a/src/Service/ModelTypeCategoryConversionService.php +++ b/src/Service/ModelTypeCategoryConversionService.php @@ -235,6 +235,45 @@ final class ModelTypeCategoryConversionService } } + // Check slot tables for actual data (post-normalization architecture) + $filledPieceSlots = (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM composant_piece_slots cps + JOIN composants c ON cps.composantid = c.id + WHERE c.typecomposantid = :id AND cps.selectedpieceid IS NOT NULL', + ['id' => $modelTypeId], + ); + + $filledSubSlots = (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM composant_subcomponent_slots css + JOIN composants c ON css.composantid = c.id + WHERE c.typecomposantid = :id AND css.selectedcomposantid IS NOT NULL', + ['id' => $modelTypeId], + ); + + $filledProductSlots = (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM composant_product_slots cps + JOIN composants c ON cps.composantid = c.id + WHERE c.typecomposantid = :id AND cps.selectedproductid IS NOT NULL', + ['id' => $modelTypeId], + ); + + if ($filledPieceSlots > 0 || $filledSubSlots > 0 || $filledProductSlots > 0) { + $parts = []; + if ($filledPieceSlots > 0) { + $parts[] = sprintf('%d slot(s) pièce', $filledPieceSlots); + } + if ($filledSubSlots > 0) { + $parts[] = sprintf('%d slot(s) sous-composant', $filledSubSlots); + } + if ($filledProductSlots > 0) { + $parts[] = sprintf('%d slot(s) produit', $filledProductSlots); + } + $blockers[] = sprintf( + 'Des composants ont des données dans leurs slots : %s.', + implode(', ', $parts), + ); + } + // Check name collision with existing pieces $collisions = $this->connection->fetchFirstColumn( 'SELECT c.name FROM composants c @@ -383,12 +422,22 @@ final class ModelTypeCategoryConversionService ['id' => $modelTypeId], ); - // 6. Delete original composants + // 6. Delete original composants (cascades to slot tables) $this->connection->executeStatement( 'DELETE FROM composants WHERE typecomposantid = :id', ['id' => $modelTypeId], ); + // 6b. Clean up skeleton requirements that only apply to COMPONENT category + $this->connection->executeStatement( + 'DELETE FROM skeleton_piece_requirements WHERE modeltypeid = :id', + ['id' => $modelTypeId], + ); + $this->connection->executeStatement( + 'DELETE FROM skeleton_subcomponent_requirements WHERE modeltypeid = :id', + ['id' => $modelTypeId], + ); + // 7. Update ModelType $this->connection->executeStatement( 'UPDATE model_types