diff --git a/docs/circuit-list-editor-interactions.md b/docs/circuit-list-editor-interactions.md index 7d38d8c..36b008c 100644 --- a/docs/circuit-list-editor-interactions.md +++ b/docs/circuit-list-editor-interactions.md @@ -75,12 +75,12 @@ Intent is separated by drag source type: - drop to placeholder -> create new target circuit and move row(s) - crossing a section boundary requires explicit confirmation - phase type, category, linked project device and local row values remain unchanged + - target creation, row assignments and reserve-state updates use one SQLite transaction - circuit drag (BMK handle): - reorder circuits inside same section only - cross-section reorder is rejected - bulk device row move: - supported via multi-selection + drag - - row assignments, optional target creation and reserve-state updates are committed in one SQLite transaction - multi-circuit move: - supported for same-section selected circuit blocks diff --git a/src/domain/services/circuit-write.service.ts b/src/domain/services/circuit-write.service.ts index 229fb41..cad958c 100644 --- a/src/domain/services/circuit-write.service.ts +++ b/src/domain/services/circuit-write.service.ts @@ -316,7 +316,18 @@ export class CircuitWriteService { throw new Error("Invalid target circuit id."); } - // Placeholder-target move creates a new circuit explicitly; no implicit renumbering of others. + let createTargetCircuit: + | { + circuitListId: string; + sectionId: string; + equipmentIdentifier: string; + displayName: string; + sortOrder: number; + } + | undefined; + + // Placeholder-target move prepares a new circuit explicitly. Its creation and + // the row assignment are committed together without renumbering other circuits. if (!targetCircuit) { if (!input.targetSectionId || !input.createNewCircuit) { throw new Error("Invalid move target."); @@ -326,76 +337,27 @@ export class CircuitWriteService { const sectionCircuits = await this.circuitRepository.listBySection(section.id); const nextSortOrder = sectionCircuits.length > 0 ? Math.max(...sectionCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10; - const createdId = await this.circuitRepository.create({ + createTargetCircuit = { circuitListId: sourceCircuit.circuitListId, sectionId: section.id, equipmentIdentifier: nextIdentifier, displayName: "Neuer Stromkreis", sortOrder: nextSortOrder, - isReserve: false, - }); - targetCircuit = await this.circuitRepository.findById(createdId); - if (!targetCircuit) { - throw new Error("Failed to create target circuit."); - } + }; } - if (targetCircuit.circuitListId !== sourceCircuit.circuitListId) { + if (targetCircuit && targetCircuit.circuitListId !== sourceCircuit.circuitListId) { throw new Error("Target circuit does not belong to same circuit list."); } - if (targetCircuit.id === sourceCircuit.id) { + if (targetCircuit?.id === sourceCircuit.id) { return this.deviceRowRepository.findById(rowId); } - const targetCount = await this.deviceRowRepository.countByCircuit(targetCircuit.id); - await this.deviceRowRepository.moveToCircuit(rowId, targetCircuit.id, (targetCount + 1) * 10); - - const sourceRemaining = await this.deviceRowRepository.countByCircuit(sourceCircuit.id); - // Source circuit becomes reserve when all rows are moved away. - if (sourceRemaining === 0) { - await this.circuitRepository.update(sourceCircuit.id, { - sectionId: sourceCircuit.sectionId, - equipmentIdentifier: sourceCircuit.equipmentIdentifier, - displayName: sourceCircuit.displayName ?? undefined, - sortOrder: sourceCircuit.sortOrder, - protectionType: sourceCircuit.protectionType ?? undefined, - protectionRatedCurrent: sourceCircuit.protectionRatedCurrent ?? undefined, - protectionCharacteristic: sourceCircuit.protectionCharacteristic ?? undefined, - cableType: sourceCircuit.cableType ?? undefined, - cableCrossSection: sourceCircuit.cableCrossSection ?? undefined, - cableLength: sourceCircuit.cableLength ?? undefined, - rcdAssignment: sourceCircuit.rcdAssignment ?? undefined, - terminalDesignation: sourceCircuit.terminalDesignation ?? undefined, - voltage: sourceCircuit.voltage ?? undefined, - controlRequirement: sourceCircuit.controlRequirement ?? undefined, - status: sourceCircuit.status ?? undefined, - isReserve: true, - remark: sourceCircuit.remark ?? undefined, - }); - } - - // Target circuit is no longer reserve once it receives moved rows. - if (Boolean(targetCircuit.isReserve)) { - await this.circuitRepository.update(targetCircuit.id, { - sectionId: targetCircuit.sectionId, - equipmentIdentifier: targetCircuit.equipmentIdentifier, - displayName: targetCircuit.displayName ?? undefined, - sortOrder: targetCircuit.sortOrder, - protectionType: targetCircuit.protectionType ?? undefined, - protectionRatedCurrent: targetCircuit.protectionRatedCurrent ?? undefined, - protectionCharacteristic: targetCircuit.protectionCharacteristic ?? undefined, - cableType: targetCircuit.cableType ?? undefined, - cableCrossSection: targetCircuit.cableCrossSection ?? undefined, - cableLength: targetCircuit.cableLength ?? undefined, - rcdAssignment: targetCircuit.rcdAssignment ?? undefined, - terminalDesignation: targetCircuit.terminalDesignation ?? undefined, - voltage: targetCircuit.voltage ?? undefined, - controlRequirement: targetCircuit.controlRequirement ?? undefined, - status: targetCircuit.status ?? undefined, - isReserve: false, - remark: targetCircuit.remark ?? undefined, - }); - } + this.deviceRowRepository.moveRowsTransactional({ + rows: [{ id: row.id, expectedCircuitId: row.circuitId }], + targetCircuitId: targetCircuit?.id, + createTargetCircuit, + }); return this.deviceRowRepository.findById(rowId); } diff --git a/tests/circuit-write.rules.test.ts b/tests/circuit-write.rules.test.ts index 8c0e62d..d1c5125 100644 --- a/tests/circuit-write.rules.test.ts +++ b/tests/circuit-write.rules.test.ts @@ -299,24 +299,24 @@ describe("circuit write service rules", () => { }); it("moving a device row to another circuit preserves row and toggles reserve flags", async () => { - const updatedReserve: Array<{ id: string; isReserve: boolean }> = []; - const movedCalls: Array<{ rowId: string; targetCircuitId: string; sortOrder: number }> = []; + let transactionalMove: + | { + rows: Array<{ id: string; expectedCircuitId: string }>; + targetCircuitId?: string; + createTargetCircuit?: unknown; + } + | undefined; const service = new CircuitWriteService({ deviceRowRepository: { async findById() { return { id: "r1", circuitId: "c1" } as never; }, - async countByCircuit(circuitId: string) { - if (circuitId === "c2") { - return 2; - } - if (circuitId === "c1") { - return 0; - } - return 0; - }, - async moveToCircuit(rowId: string, targetCircuitId: string, sortOrder: number) { - movedCalls.push({ rowId, targetCircuitId, sortOrder }); + moveRowsTransactional(input: typeof transactionalMove) { + transactionalMove = input; + return { + movedRowIds: input!.rows.map((row) => row.id), + targetCircuitId: input!.targetCircuitId!, + }; }, } as never, circuitRepository: { @@ -340,35 +340,39 @@ describe("circuit write service rules", () => { isReserve: 1, } as never; }, - async update(id: string, payload: { isReserve: boolean }) { - updatedReserve.push({ id, isReserve: payload.isReserve }); - }, } as never, }); await service.moveDeviceRow("r1", { targetCircuitId: "c2" }); - assert.deepEqual(movedCalls, [{ rowId: "r1", targetCircuitId: "c2", sortOrder: 30 }]); - assert.deepEqual(updatedReserve, [ - { id: "c1", isReserve: true }, - { id: "c2", isReserve: false }, - ]); + assert.deepEqual(transactionalMove, { + rows: [{ id: "r1", expectedCircuitId: "c1" }], + targetCircuitId: "c2", + createTargetCircuit: undefined, + }); }); it("moving a device row to placeholder creates a new circuit in target section", async () => { - let createdCircuitPayload: { sectionId: string; equipmentIdentifier: string; isReserve?: boolean } | null = null; + let preparedTarget: + | { + circuitListId: string; + sectionId: string; + equipmentIdentifier: string; + displayName: string; + sortOrder: number; + } + | undefined; const service = new CircuitWriteService({ deviceRowRepository: { async findById() { return { id: "r1", circuitId: "c1" } as never; }, - async countByCircuit(circuitId: string) { - if (circuitId === "c-new") { - return 0; - } - return 1; - }, - async moveToCircuit() { - return; + moveRowsTransactional(input: { rows: Array<{ id: string }>; createTargetCircuit?: typeof preparedTarget }) { + preparedTarget = input.createTargetCircuit; + return { + movedRowIds: input.rows.map((row) => row.id), + targetCircuitId: "c-new", + createdCircuitId: "c-new", + }; }, } as never, circuitRepository: { @@ -383,28 +387,11 @@ describe("circuit write service rules", () => { isReserve: 0, } as never; } - if (circuitId === "c-new") { - return { - id: "c-new", - sectionId: "s2", - circuitListId: "l1", - equipmentIdentifier: "-2F8", - sortOrder: 50, - isReserve: 0, - } as never; - } return null as never; }, async listBySection() { return [{ sortOrder: 40 }] as never[]; }, - async create(payload: { sectionId: string; equipmentIdentifier: string; isReserve?: boolean }) { - createdCircuitPayload = payload; - return "c-new"; - }, - async update() { - return; - }, } as never, circuitSectionRepository: { async findById() { @@ -419,9 +406,44 @@ describe("circuit write service rules", () => { }); await service.moveDeviceRow("r1", { targetSectionId: "s2", createNewCircuit: true }); - assert.equal(createdCircuitPayload?.sectionId, "s2"); - assert.equal(createdCircuitPayload?.equipmentIdentifier, "-2F8"); - assert.equal(createdCircuitPayload?.isReserve, false); + assert.deepEqual(preparedTarget, { + circuitListId: "l1", + sectionId: "s2", + equipmentIdentifier: "-2F8", + displayName: "Neuer Stromkreis", + sortOrder: 50, + }); + }); + + it("moving a device row to its current circuit remains a no-op", async () => { + let transactionalMoveCount = 0; + const service = new CircuitWriteService({ + deviceRowRepository: { + async findById() { + return { id: "r1", circuitId: "c1" } as never; + }, + moveRowsTransactional() { + transactionalMoveCount += 1; + throw new Error("same-circuit move must not write"); + }, + } as never, + circuitRepository: { + async findById() { + return { + id: "c1", + sectionId: "s1", + circuitListId: "l1", + equipmentIdentifier: "-1F1", + sortOrder: 10, + isReserve: 0, + } as never; + }, + } as never, + }); + + const result = await service.moveDeviceRow("r1", { targetCircuitId: "c1" }); + assert.equal(transactionalMoveCount, 0); + assert.equal(result?.id, "r1"); }); it("moving multiple device rows to a circuit preserves input order and toggles reserve", async () => {