Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90ad851804 | |||
| c3ad3b68a2 | |||
| 494298f981 |
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '1.9.48'
|
app.version: '1.9.49'
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ export function useMachineCreatePage() {
|
|||||||
siteId: '',
|
siteId: '',
|
||||||
reference: '',
|
reference: '',
|
||||||
cloneFromMachineId: '',
|
cloneFromMachineId: '',
|
||||||
|
// 'full' = clone complet (composants/pièces concrets) ; 'structure' = catégories
|
||||||
|
// uniquement (slots à compléter).
|
||||||
|
cloneMode: 'full' as 'full' | 'structure',
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -57,6 +60,7 @@ export function useMachineCreatePage() {
|
|||||||
result = await cloneMachine(newMachine.cloneFromMachineId, {
|
result = await cloneMachine(newMachine.cloneFromMachineId, {
|
||||||
name: newMachine.name,
|
name: newMachine.name,
|
||||||
siteId: newMachine.siteId,
|
siteId: newMachine.siteId,
|
||||||
|
mode: newMachine.cloneMode,
|
||||||
...(newMachine.reference ? { reference: newMachine.reference } : {}),
|
...(newMachine.reference ? { reference: newMachine.reference } : {}),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export function useMachines() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cloneMachine = async (sourceId: string, data: { name: string; siteId: string; reference?: string }): Promise<ApiResponse> => {
|
const cloneMachine = async (sourceId: string, data: { name: string; siteId: string; reference?: string; mode?: 'full' | 'structure' }): Promise<ApiResponse> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await post(`/machines/${sourceId}/clone`, data)
|
const result = await post(`/machines/${sourceId}/clone`, data)
|
||||||
|
|||||||
@@ -103,6 +103,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Clone mode (visible only when a source machine is selected) -->
|
||||||
|
<div v-if="c.newMachine.cloneFromMachineId" class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Mode de clonage</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex flex-col gap-2 sm:flex-row sm:gap-6">
|
||||||
|
<label class="flex items-start gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
v-model="c.newMachine.cloneMode"
|
||||||
|
type="radio"
|
||||||
|
value="full"
|
||||||
|
class="radio radio-primary radio-sm mt-0.5"
|
||||||
|
:disabled="!canEdit"
|
||||||
|
>
|
||||||
|
<span class="text-sm">
|
||||||
|
Tout cloner
|
||||||
|
<span class="block text-xs text-gray-500">Structure + composants et pièces assignés</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-start gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
v-model="c.newMachine.cloneMode"
|
||||||
|
type="radio"
|
||||||
|
value="structure"
|
||||||
|
class="radio radio-primary radio-sm mt-0.5"
|
||||||
|
:disabled="!canEdit"
|
||||||
|
>
|
||||||
|
<span class="text-sm">
|
||||||
|
Structure seule
|
||||||
|
<span class="block text-xs text-gray-500">Catégories uniquement, slots à compléter</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex justify-end gap-3 pt-4 border-t border-base-200">
|
<div class="flex justify-end gap-3 pt-4 border-t border-base-200">
|
||||||
<NuxtLink to="/machines" class="btn btn-outline btn-sm md:btn-md">
|
<NuxtLink to="/machines" class="btn btn-outline btn-sm md:btn-md">
|
||||||
|
|||||||
@@ -132,6 +132,14 @@ class MachineStructureController extends AbstractController
|
|||||||
return $this->json(['success' => false, 'error' => 'Site introuvable.'], 404);
|
return $this->json(['success' => false, 'error' => 'Site introuvable.'], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clone mode: 'full' copies concrete components/pieces/products; 'structure'
|
||||||
|
// only keeps the slots' categories (modelType) with empty concrete entities.
|
||||||
|
$mode = $payload['mode'] ?? 'full';
|
||||||
|
if (!in_array($mode, ['full', 'structure'], true)) {
|
||||||
|
return $this->json(['success' => false, 'error' => 'mode invalide (valeurs autorisées : full, structure).'], 400);
|
||||||
|
}
|
||||||
|
$structureOnly = 'structure' === $mode;
|
||||||
|
|
||||||
// Create new machine
|
// Create new machine
|
||||||
$newMachine = new Machine();
|
$newMachine = new Machine();
|
||||||
$newMachine->setName($payload['name']);
|
$newMachine->setName($payload['name']);
|
||||||
@@ -156,13 +164,13 @@ class MachineStructureController extends AbstractController
|
|||||||
$this->cloneCustomFields($source, $newMachine);
|
$this->cloneCustomFields($source, $newMachine);
|
||||||
|
|
||||||
// Copy component links (preserving hierarchy)
|
// Copy component links (preserving hierarchy)
|
||||||
$componentLinkMap = $this->cloneComponentLinks($source, $newMachine);
|
$componentLinkMap = $this->cloneComponentLinks($source, $newMachine, $structureOnly);
|
||||||
|
|
||||||
// Copy piece links
|
// Copy piece links
|
||||||
$pieceLinkMap = $this->clonePieceLinks($source, $newMachine, $componentLinkMap);
|
$pieceLinkMap = $this->clonePieceLinks($source, $newMachine, $componentLinkMap, $structureOnly);
|
||||||
|
|
||||||
// Copy product links
|
// Copy product links
|
||||||
$this->cloneProductLinks($source, $newMachine, $componentLinkMap, $pieceLinkMap);
|
$this->cloneProductLinks($source, $newMachine, $componentLinkMap, $pieceLinkMap, $structureOnly);
|
||||||
|
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
@@ -215,7 +223,7 @@ class MachineStructureController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* @return array<string, MachineComponentLink> Map of old link ID → new link
|
* @return array<string, MachineComponentLink> Map of old link ID → new link
|
||||||
*/
|
*/
|
||||||
private function cloneComponentLinks(Machine $source, Machine $target): array
|
private function cloneComponentLinks(Machine $source, Machine $target, bool $structureOnly = false): array
|
||||||
{
|
{
|
||||||
$sourceLinks = $this->machineComponentLinkRepository->findBy(['machine' => $source], ['createdAt' => 'ASC']);
|
$sourceLinks = $this->machineComponentLinkRepository->findBy(['machine' => $source], ['createdAt' => 'ASC']);
|
||||||
$linkMap = [];
|
$linkMap = [];
|
||||||
@@ -224,6 +232,16 @@ class MachineStructureController extends AbstractController
|
|||||||
foreach ($sourceLinks as $link) {
|
foreach ($sourceLinks as $link) {
|
||||||
$newLink = new MachineComponentLink();
|
$newLink = new MachineComponentLink();
|
||||||
$newLink->setMachine($target);
|
$newLink->setMachine($target);
|
||||||
|
|
||||||
|
if ($structureOnly) {
|
||||||
|
// Keep only the slot category; leave the concrete component empty.
|
||||||
|
$newLink->setModelType($link->getModelType() ?? $link->getComposant()?->getTypeComposant());
|
||||||
|
$this->entityManager->persist($newLink);
|
||||||
|
$linkMap[$link->getId()] = $newLink;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$newLink->setComposant($link->getComposant());
|
$newLink->setComposant($link->getComposant());
|
||||||
$newLink->setNameOverride($link->getNameOverride());
|
$newLink->setNameOverride($link->getNameOverride());
|
||||||
$newLink->setReferenceOverride($link->getReferenceOverride());
|
$newLink->setReferenceOverride($link->getReferenceOverride());
|
||||||
@@ -259,7 +277,7 @@ class MachineStructureController extends AbstractController
|
|||||||
*
|
*
|
||||||
* @return array<string, MachinePieceLink> Map of old link ID → new link
|
* @return array<string, MachinePieceLink> Map of old link ID → new link
|
||||||
*/
|
*/
|
||||||
private function clonePieceLinks(Machine $source, Machine $target, array $componentLinkMap): array
|
private function clonePieceLinks(Machine $source, Machine $target, array $componentLinkMap, bool $structureOnly = false): array
|
||||||
{
|
{
|
||||||
$sourceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $source], ['createdAt' => 'ASC']);
|
$sourceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $source], ['createdAt' => 'ASC']);
|
||||||
$linkMap = [];
|
$linkMap = [];
|
||||||
@@ -267,17 +285,27 @@ class MachineStructureController extends AbstractController
|
|||||||
foreach ($sourceLinks as $link) {
|
foreach ($sourceLinks as $link) {
|
||||||
$newLink = new MachinePieceLink();
|
$newLink = new MachinePieceLink();
|
||||||
$newLink->setMachine($target);
|
$newLink->setMachine($target);
|
||||||
$newLink->setPiece($link->getPiece());
|
|
||||||
$newLink->setNameOverride($link->getNameOverride());
|
|
||||||
$newLink->setReferenceOverride($link->getReferenceOverride());
|
|
||||||
$newLink->setPrixOverride($link->getPrixOverride());
|
|
||||||
$newLink->setQuantity($link->getQuantity());
|
|
||||||
|
|
||||||
$parent = $link->getParentLink();
|
$parent = $link->getParentLink();
|
||||||
if ($parent && isset($componentLinkMap[$parent->getId()])) {
|
if ($parent && isset($componentLinkMap[$parent->getId()])) {
|
||||||
$newLink->setParentLink($componentLinkMap[$parent->getId()]);
|
$newLink->setParentLink($componentLinkMap[$parent->getId()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($structureOnly) {
|
||||||
|
// Keep only the slot category; leave the concrete piece empty.
|
||||||
|
$newLink->setModelType($link->getModelType() ?? $link->getPiece()?->getTypePiece());
|
||||||
|
$this->entityManager->persist($newLink);
|
||||||
|
$linkMap[$link->getId()] = $newLink;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newLink->setPiece($link->getPiece());
|
||||||
|
$newLink->setNameOverride($link->getNameOverride());
|
||||||
|
$newLink->setReferenceOverride($link->getReferenceOverride());
|
||||||
|
$newLink->setPrixOverride($link->getPrixOverride());
|
||||||
|
$newLink->setQuantity($link->getQuantity());
|
||||||
|
|
||||||
$this->entityManager->persist($newLink);
|
$this->entityManager->persist($newLink);
|
||||||
|
|
||||||
foreach ($link->getContextFieldValues() as $cfv) {
|
foreach ($link->getContextFieldValues() as $cfv) {
|
||||||
@@ -305,6 +333,7 @@ class MachineStructureController extends AbstractController
|
|||||||
Machine $target,
|
Machine $target,
|
||||||
array $componentLinkMap,
|
array $componentLinkMap,
|
||||||
array $pieceLinkMap,
|
array $pieceLinkMap,
|
||||||
|
bool $structureOnly = false,
|
||||||
): void {
|
): void {
|
||||||
$sourceLinks = $this->machineProductLinkRepository->findBy(['machine' => $source], ['createdAt' => 'ASC']);
|
$sourceLinks = $this->machineProductLinkRepository->findBy(['machine' => $source], ['createdAt' => 'ASC']);
|
||||||
$linkMap = [];
|
$linkMap = [];
|
||||||
@@ -313,7 +342,13 @@ class MachineStructureController extends AbstractController
|
|||||||
foreach ($sourceLinks as $link) {
|
foreach ($sourceLinks as $link) {
|
||||||
$newLink = new MachineProductLink();
|
$newLink = new MachineProductLink();
|
||||||
$newLink->setMachine($target);
|
$newLink->setMachine($target);
|
||||||
$newLink->setProduct($link->getProduct());
|
|
||||||
|
if ($structureOnly) {
|
||||||
|
// Keep only the slot category; leave the concrete product empty.
|
||||||
|
$newLink->setModelType($link->getModelType() ?? $link->getProduct()?->getTypeProduct());
|
||||||
|
} else {
|
||||||
|
$newLink->setProduct($link->getProduct());
|
||||||
|
}
|
||||||
|
|
||||||
$parentComponent = $link->getParentComponentLink();
|
$parentComponent = $link->getParentComponentLink();
|
||||||
if ($parentComponent && isset($componentLinkMap[$parentComponent->getId()])) {
|
if ($parentComponent && isset($componentLinkMap[$parentComponent->getId()])) {
|
||||||
@@ -774,7 +809,7 @@ class MachineStructureController extends AbstractController
|
|||||||
$pieces = [];
|
$pieces = [];
|
||||||
foreach ($composant->getPieceSlots() as $slot) {
|
foreach ($composant->getPieceSlots() as $slot) {
|
||||||
$selectedPiece = $this->ensurePieceExists($slot->getSelectedPiece());
|
$selectedPiece = $this->ensurePieceExists($slot->getSelectedPiece());
|
||||||
$pieceData = [
|
$pieceData = [
|
||||||
'slotId' => $slot->getId(),
|
'slotId' => $slot->getId(),
|
||||||
'typePieceId' => $slot->getTypePiece()?->getId(),
|
'typePieceId' => $slot->getTypePiece()?->getId(),
|
||||||
'typePiece' => $this->normalizeModelType($slot->getTypePiece()),
|
'typePiece' => $this->normalizeModelType($slot->getTypePiece()),
|
||||||
@@ -824,6 +859,7 @@ class MachineStructureController extends AbstractController
|
|||||||
if (null === $piece) {
|
if (null === $piece) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->entityManager->initializeObject($piece);
|
$this->entityManager->initializeObject($piece);
|
||||||
|
|
||||||
@@ -844,6 +880,7 @@ class MachineStructureController extends AbstractController
|
|||||||
if (null === $cf) {
|
if (null === $cf) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->entityManager->initializeObject($cf);
|
$this->entityManager->initializeObject($cf);
|
||||||
|
|
||||||
|
|||||||
@@ -308,4 +308,88 @@ class MachineContextCustomFieldTest extends AbstractApiTestCase
|
|||||||
$this->assertCount(1, $sourceLink['contextCustomFieldValues']);
|
$this->assertCount(1, $sourceLink['contextCustomFieldValues']);
|
||||||
$this->assertSame('1500', $sourceLink['contextCustomFieldValues'][0]['value']);
|
$this->assertSame('1500', $sourceLink['contextCustomFieldValues'][0]['value']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCloneMachineStructureModeKeepsCategoriesWithoutConcreteEntities(): void
|
||||||
|
{
|
||||||
|
$client = $this->createGestionnaireClient();
|
||||||
|
|
||||||
|
$site = $this->createSite('Site Structure');
|
||||||
|
$compType = $this->createModelType('Motor Struct', 'MOTST', ModelCategory::COMPONENT);
|
||||||
|
$pieceType = $this->createModelType('Bearing Struct', 'BRGST', ModelCategory::PIECE);
|
||||||
|
$contextField = $this->createCustomField(
|
||||||
|
name: 'RPM Struct',
|
||||||
|
type: 'number',
|
||||||
|
typeComposant: $compType,
|
||||||
|
machineContextOnly: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
$source = $this->createMachine('Source Struct Machine', $site);
|
||||||
|
$composant = $this->createComposant('Motor ST', 'MOTST-001', $compType);
|
||||||
|
$componentLink = $this->createMachineComponentLink($source, $composant);
|
||||||
|
$piece = $this->createPiece('Bearing ST', 'BRGST-001', $pieceType);
|
||||||
|
$this->createMachinePieceLink($source, $piece, $componentLink);
|
||||||
|
|
||||||
|
$this->createCustomFieldValue(
|
||||||
|
customField: $contextField,
|
||||||
|
value: '4200',
|
||||||
|
composant: $composant,
|
||||||
|
machineComponentLink: $componentLink,
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $client->request('POST', '/api/machines/'.$source->getId().'/clone', [
|
||||||
|
'json' => [
|
||||||
|
'name' => 'Cloned Struct Machine',
|
||||||
|
'siteId' => $site->getId(),
|
||||||
|
'mode' => 'structure',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(201);
|
||||||
|
$data = $response->toArray();
|
||||||
|
|
||||||
|
// Component slot: category preserved, concrete component dropped, no context values.
|
||||||
|
$clonedComponent = $data['componentLinks'][0] ?? null;
|
||||||
|
$this->assertNotNull($clonedComponent, 'Structure clone should expose the component slot');
|
||||||
|
$this->assertTrue($clonedComponent['pendingEntity']);
|
||||||
|
$this->assertNull($clonedComponent['composantId']);
|
||||||
|
$this->assertSame($compType->getId(), $clonedComponent['modelTypeId']);
|
||||||
|
$this->assertCount(0, $clonedComponent['contextCustomFieldValues']);
|
||||||
|
|
||||||
|
// Piece slot: category preserved, concrete piece dropped.
|
||||||
|
$clonedPiece = $data['pieceLinks'][0] ?? null;
|
||||||
|
$this->assertNotNull($clonedPiece, 'Structure clone should expose the piece slot');
|
||||||
|
$this->assertTrue($clonedPiece['pendingEntity']);
|
||||||
|
$this->assertNull($clonedPiece['pieceId']);
|
||||||
|
$this->assertSame($pieceType->getId(), $clonedPiece['modelTypeId']);
|
||||||
|
|
||||||
|
// Source machine stays intact (still has its concrete component).
|
||||||
|
$sourceData = $client->request('GET', '/api/machines/'.$source->getId().'/structure')->toArray();
|
||||||
|
$this->assertFalse($sourceData['componentLinks'][0]['pendingEntity']);
|
||||||
|
$this->assertSame($composant->getId(), $sourceData['componentLinks'][0]['composantId']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCloneMachineFullModeStillCopiesConcreteEntities(): void
|
||||||
|
{
|
||||||
|
$client = $this->createGestionnaireClient();
|
||||||
|
|
||||||
|
$site = $this->createSite('Site Full');
|
||||||
|
$compType = $this->createModelType('Motor Full', 'MOTFL', ModelCategory::COMPONENT);
|
||||||
|
$source = $this->createMachine('Source Full Machine', $site);
|
||||||
|
$composant = $this->createComposant('Motor FL', 'MOTFL-001', $compType);
|
||||||
|
$this->createMachineComponentLink($source, $composant);
|
||||||
|
|
||||||
|
$response = $client->request('POST', '/api/machines/'.$source->getId().'/clone', [
|
||||||
|
'json' => [
|
||||||
|
'name' => 'Cloned Full Machine',
|
||||||
|
'siteId' => $site->getId(),
|
||||||
|
'mode' => 'full',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(201);
|
||||||
|
$clonedComponent = $response->toArray()['componentLinks'][0] ?? null;
|
||||||
|
$this->assertNotNull($clonedComponent);
|
||||||
|
$this->assertFalse($clonedComponent['pendingEntity']);
|
||||||
|
$this->assertSame($composant->getId(), $clonedComponent['composantId']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user