Create circuits with rows atomically
This commit is contained in:
@@ -44,6 +44,8 @@ Response sketch:
|
|||||||
|
|
||||||
- `POST /projects/:projectId/circuit-lists/:circuitListId/circuits`
|
- `POST /projects/:projectId/circuit-lists/:circuitListId/circuits`
|
||||||
- create circuit in list/section
|
- create circuit in list/section
|
||||||
|
- `POST /projects/:projectId/circuit-lists/:circuitListId/circuits-with-device-rows`
|
||||||
|
- atomically create one circuit and one or more initial device rows
|
||||||
- `PATCH /circuits/:circuitId`
|
- `PATCH /circuits/:circuitId`
|
||||||
- update circuit-level fields (BMK, protection, cable, reserve flag, etc.)
|
- update circuit-level fields (BMK, protection, cable, reserve flag, etc.)
|
||||||
- `DELETE /circuits/:circuitId`
|
- `DELETE /circuits/:circuitId`
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ Each section has a trailing placeholder row showing `-frei-` in BMK column:
|
|||||||
|
|
||||||
- serves as drop target for creating new circuits from project devices or moved rows
|
- serves as drop target for creating new circuits from project devices or moved rows
|
||||||
- editable placeholder cells can create a new circuit + first row through the same edit command flow
|
- editable placeholder cells can create a new circuit + first row through the same edit command flow
|
||||||
|
- a new circuit and its initial row(s) are created in one SQLite transaction
|
||||||
|
|
||||||
## Add Circuit Behavior
|
## Add Circuit Behavior
|
||||||
|
|
||||||
@@ -68,6 +69,7 @@ Intent is separated by drag source type:
|
|||||||
|
|
||||||
- project device drag:
|
- project device drag:
|
||||||
- drop to section/placeholder -> create new circuit with linked row
|
- drop to section/placeholder -> create new circuit with linked row
|
||||||
|
- the new circuit and linked row are committed atomically
|
||||||
- drop to existing circuit row -> append row to that circuit
|
- drop to existing circuit row -> append row to that circuit
|
||||||
- lighting devices are accepted only by the lighting section
|
- lighting devices are accepted only by the lighting section
|
||||||
- other devices are accepted only by the section matching their phase type
|
- other devices are accepted only by the section matching their phase type
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
|||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
|
import {
|
||||||
|
toCircuitCreateValues,
|
||||||
|
type CircuitCreatePersistenceInput,
|
||||||
|
} from "./circuit.repository.js";
|
||||||
|
|
||||||
export interface CircuitDeviceRowUpdateInput {
|
export interface CircuitDeviceRowUpdateInput {
|
||||||
linkedProjectDeviceId?: string;
|
linkedProjectDeviceId?: string;
|
||||||
@@ -45,6 +49,13 @@ export interface CircuitDeviceRowsBulkMoveInput {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CircuitWithDeviceRowsCreateInput {
|
||||||
|
circuit: CircuitCreatePersistenceInput;
|
||||||
|
deviceRows: Array<
|
||||||
|
Omit<CircuitDeviceRowCreateInput, "circuitId" | "sortOrder"> & { sortOrder?: number }
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
function toUpdateValues(input: CircuitDeviceRowUpdateInput) {
|
function toUpdateValues(input: CircuitDeviceRowUpdateInput) {
|
||||||
return {
|
return {
|
||||||
linkedProjectDeviceId: input.linkedProjectDeviceId ?? null,
|
linkedProjectDeviceId: input.linkedProjectDeviceId ?? null,
|
||||||
@@ -199,6 +210,42 @@ export class CircuitDeviceRowRepository {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createCircuitWithDeviceRowsTransactional(input: CircuitWithDeviceRowsCreateInput) {
|
||||||
|
if (input.deviceRows.length === 0) {
|
||||||
|
throw new Error("Mindestens eine Gerätezeile ist erforderlich.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const circuitId = crypto.randomUUID();
|
||||||
|
const rowIds = input.deviceRows.map(() => crypto.randomUUID());
|
||||||
|
db.transaction((tx) => {
|
||||||
|
tx
|
||||||
|
.insert(circuits)
|
||||||
|
.values(
|
||||||
|
toCircuitCreateValues(circuitId, {
|
||||||
|
...input.circuit,
|
||||||
|
isReserve: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
|
||||||
|
for (let index = 0; index < input.deviceRows.length; index += 1) {
|
||||||
|
const row = input.deviceRows[index];
|
||||||
|
tx
|
||||||
|
.insert(circuitDeviceRows)
|
||||||
|
.values(
|
||||||
|
toCreateValues(rowIds[index], {
|
||||||
|
...row,
|
||||||
|
circuitId,
|
||||||
|
sortOrder: row.sortOrder ?? (index + 1) * 10,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { circuitId, rowIds };
|
||||||
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
rowId: string,
|
rowId: string,
|
||||||
input: CircuitDeviceRowUpdateInput
|
input: CircuitDeviceRowUpdateInput
|
||||||
|
|||||||
@@ -3,6 +3,51 @@ import { and, asc, eq, inArray, ne } from "drizzle-orm";
|
|||||||
import { db } from "../client.js";
|
import { db } from "../client.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
|
|
||||||
|
export interface CircuitCreatePersistenceInput {
|
||||||
|
circuitListId: string;
|
||||||
|
sectionId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
displayName?: string;
|
||||||
|
sortOrder: number;
|
||||||
|
protectionType?: string;
|
||||||
|
protectionRatedCurrent?: number;
|
||||||
|
protectionCharacteristic?: string;
|
||||||
|
cableType?: string;
|
||||||
|
cableCrossSection?: string;
|
||||||
|
cableLength?: number;
|
||||||
|
voltage?: number;
|
||||||
|
controlRequirement?: string;
|
||||||
|
remark?: string;
|
||||||
|
rcdAssignment?: string;
|
||||||
|
terminalDesignation?: string;
|
||||||
|
status?: string;
|
||||||
|
isReserve?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toCircuitCreateValues(id: string, input: CircuitCreatePersistenceInput) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
circuitListId: input.circuitListId,
|
||||||
|
sectionId: input.sectionId,
|
||||||
|
equipmentIdentifier: input.equipmentIdentifier,
|
||||||
|
displayName: input.displayName ?? null,
|
||||||
|
sortOrder: input.sortOrder,
|
||||||
|
protectionType: input.protectionType ?? null,
|
||||||
|
protectionRatedCurrent: input.protectionRatedCurrent ?? null,
|
||||||
|
protectionCharacteristic: input.protectionCharacteristic ?? null,
|
||||||
|
cableType: input.cableType ?? null,
|
||||||
|
cableCrossSection: input.cableCrossSection ?? null,
|
||||||
|
cableLength: input.cableLength ?? null,
|
||||||
|
rcdAssignment: input.rcdAssignment ?? null,
|
||||||
|
terminalDesignation: input.terminalDesignation ?? null,
|
||||||
|
voltage: input.voltage ?? null,
|
||||||
|
controlRequirement: input.controlRequirement ?? null,
|
||||||
|
status: input.status ?? null,
|
||||||
|
isReserve: input.isReserve ? 1 : 0,
|
||||||
|
remark: input.remark ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class CircuitRepository {
|
export class CircuitRepository {
|
||||||
async findById(circuitId: string) {
|
async findById(circuitId: string) {
|
||||||
const [row] = await db.select().from(circuits).where(eq(circuits.id, circuitId)).limit(1);
|
const [row] = await db.select().from(circuits).where(eq(circuits.id, circuitId)).limit(1);
|
||||||
@@ -17,48 +62,9 @@ export class CircuitRepository {
|
|||||||
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(input: {
|
async create(input: CircuitCreatePersistenceInput) {
|
||||||
circuitListId: string;
|
|
||||||
sectionId: string;
|
|
||||||
equipmentIdentifier: string;
|
|
||||||
displayName?: string;
|
|
||||||
sortOrder: number;
|
|
||||||
protectionType?: string;
|
|
||||||
protectionRatedCurrent?: number;
|
|
||||||
protectionCharacteristic?: string;
|
|
||||||
cableType?: string;
|
|
||||||
cableCrossSection?: string;
|
|
||||||
cableLength?: number;
|
|
||||||
voltage?: number;
|
|
||||||
controlRequirement?: string;
|
|
||||||
remark?: string;
|
|
||||||
rcdAssignment?: string;
|
|
||||||
terminalDesignation?: string;
|
|
||||||
status?: string;
|
|
||||||
isReserve?: boolean;
|
|
||||||
}) {
|
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
await db.insert(circuits).values({
|
await db.insert(circuits).values(toCircuitCreateValues(id, input));
|
||||||
id,
|
|
||||||
circuitListId: input.circuitListId,
|
|
||||||
sectionId: input.sectionId,
|
|
||||||
equipmentIdentifier: input.equipmentIdentifier,
|
|
||||||
displayName: input.displayName ?? null,
|
|
||||||
sortOrder: input.sortOrder,
|
|
||||||
protectionType: input.protectionType ?? null,
|
|
||||||
protectionRatedCurrent: input.protectionRatedCurrent ?? null,
|
|
||||||
protectionCharacteristic: input.protectionCharacteristic ?? null,
|
|
||||||
cableType: input.cableType ?? null,
|
|
||||||
cableCrossSection: input.cableCrossSection ?? null,
|
|
||||||
cableLength: input.cableLength ?? null,
|
|
||||||
rcdAssignment: input.rcdAssignment ?? null,
|
|
||||||
terminalDesignation: input.terminalDesignation ?? null,
|
|
||||||
voltage: input.voltage ?? null,
|
|
||||||
controlRequirement: input.controlRequirement ?? null,
|
|
||||||
status: input.status ?? null,
|
|
||||||
isReserve: input.isReserve ? 1 : 0,
|
|
||||||
remark: input.remark ?? null,
|
|
||||||
});
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ProjectDeviceRepository } from "../../db/repositories/project-device.re
|
|||||||
import type {
|
import type {
|
||||||
CreateCircuitDeviceRowInput,
|
CreateCircuitDeviceRowInput,
|
||||||
CreateCircuitInput,
|
CreateCircuitInput,
|
||||||
|
CreateCircuitWithDeviceRowsInput,
|
||||||
MoveCircuitDeviceRowInput,
|
MoveCircuitDeviceRowInput,
|
||||||
MoveCircuitDeviceRowsBulkInput,
|
MoveCircuitDeviceRowsBulkInput,
|
||||||
ReorderSectionCircuitsInput,
|
ReorderSectionCircuitsInput,
|
||||||
@@ -73,6 +74,19 @@ export class CircuitWriteService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validates linked project-device id against owning project of the circuit list.
|
// Validates linked project-device id against owning project of the circuit list.
|
||||||
|
private async assertValidLinkedProjectDeviceForProject(
|
||||||
|
projectId: string,
|
||||||
|
linkedProjectDeviceId?: string
|
||||||
|
) {
|
||||||
|
if (!linkedProjectDeviceId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const device = await this.projectDeviceRepository.findById(projectId, linkedProjectDeviceId);
|
||||||
|
if (!device) {
|
||||||
|
throw new Error("Invalid linked project device id.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async assertValidLinkedProjectDevice(circuitId: string, linkedProjectDeviceId?: string) {
|
private async assertValidLinkedProjectDevice(circuitId: string, linkedProjectDeviceId?: string) {
|
||||||
if (!linkedProjectDeviceId) {
|
if (!linkedProjectDeviceId) {
|
||||||
return;
|
return;
|
||||||
@@ -85,10 +99,7 @@ export class CircuitWriteService {
|
|||||||
if (!list) {
|
if (!list) {
|
||||||
throw new Error("Circuit list not found.");
|
throw new Error("Circuit list not found.");
|
||||||
}
|
}
|
||||||
const device = await this.projectDeviceRepository.findById(list.projectId, linkedProjectDeviceId);
|
await this.assertValidLinkedProjectDeviceForProject(list.projectId, linkedProjectDeviceId);
|
||||||
if (!device) {
|
|
||||||
throw new Error("Invalid linked project device id.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCircuit(projectId: string, circuitListId: string, input: CreateCircuitInput) {
|
async createCircuit(projectId: string, circuitListId: string, input: CreateCircuitInput) {
|
||||||
@@ -121,6 +132,48 @@ export class CircuitWriteService {
|
|||||||
return this.circuitRepository.findById(id);
|
return this.circuitRepository.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createCircuitWithDeviceRows(
|
||||||
|
projectId: string,
|
||||||
|
circuitListId: string,
|
||||||
|
input: CreateCircuitWithDeviceRowsInput
|
||||||
|
) {
|
||||||
|
const list = await this.circuitListRepository.findById(projectId, circuitListId);
|
||||||
|
if (!list) {
|
||||||
|
throw new Error("Circuit list not found in project.");
|
||||||
|
}
|
||||||
|
await this.assertSectionInList(input.circuit.sectionId, circuitListId);
|
||||||
|
await this.assertUniqueEquipmentIdentifier(
|
||||||
|
circuitListId,
|
||||||
|
input.circuit.equipmentIdentifier
|
||||||
|
);
|
||||||
|
for (const row of input.deviceRows) {
|
||||||
|
await this.assertValidLinkedProjectDeviceForProject(
|
||||||
|
list.projectId,
|
||||||
|
row.linkedProjectDeviceId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = this.deviceRowRepository.createCircuitWithDeviceRowsTransactional({
|
||||||
|
circuit: {
|
||||||
|
circuitListId,
|
||||||
|
...input.circuit,
|
||||||
|
isReserve: false,
|
||||||
|
},
|
||||||
|
deviceRows: input.deviceRows,
|
||||||
|
});
|
||||||
|
const circuit = await this.circuitRepository.findById(created.circuitId);
|
||||||
|
const deviceRows = await Promise.all(
|
||||||
|
created.rowIds.map((rowId) => this.deviceRowRepository.findById(rowId))
|
||||||
|
);
|
||||||
|
if (!circuit || deviceRows.some((row) => !row)) {
|
||||||
|
throw new Error("Der angelegte Stromkreis konnte nicht geladen werden.");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
circuit,
|
||||||
|
deviceRows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async updateCircuit(circuitId: string, input: UpdateCircuitInput) {
|
async updateCircuit(circuitId: string, input: UpdateCircuitInput) {
|
||||||
const current = await this.circuitRepository.findById(circuitId);
|
const current = await this.circuitRepository.findById(circuitId);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
import type { VisibleGridRow } from "../utils/circuit-grid-projection";
|
import type { VisibleGridRow } from "../utils/circuit-grid-projection";
|
||||||
import {
|
import {
|
||||||
createCircuit,
|
createCircuit,
|
||||||
|
createCircuitWithDeviceRows,
|
||||||
createCircuitDeviceRow,
|
createCircuitDeviceRow,
|
||||||
deleteCircuitById,
|
deleteCircuitById,
|
||||||
deleteCircuitDeviceRowById,
|
deleteCircuitDeviceRowById,
|
||||||
@@ -1201,28 +1202,41 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
const sortOrder =
|
const sortOrder =
|
||||||
section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
||||||
const isDeviceField = deviceFieldKeys.has(key);
|
const isDeviceField = deviceFieldKeys.has(key);
|
||||||
|
|
||||||
|
if (isDeviceField) {
|
||||||
|
const created = await createCircuitWithDeviceRows(projectId, circuitListId, {
|
||||||
|
circuit: {
|
||||||
|
sectionId,
|
||||||
|
equipmentIdentifier: next.nextIdentifier,
|
||||||
|
displayName: "Neuer Stromkreis",
|
||||||
|
sortOrder,
|
||||||
|
isReserve: false,
|
||||||
|
},
|
||||||
|
deviceRows: [
|
||||||
|
{
|
||||||
|
name: "Manuelles Gerät",
|
||||||
|
displayName: "Manuelles Gerät",
|
||||||
|
phaseType: "single_phase",
|
||||||
|
quantity: 1,
|
||||||
|
powerPerUnit: 0,
|
||||||
|
simultaneityFactor: 1,
|
||||||
|
cosPhi: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const createdCircuit = created.circuit;
|
||||||
|
const createdRow = created.deviceRows[0];
|
||||||
|
await patchDeviceRow(createdRow.id, key, draft);
|
||||||
|
return { rowKey: `circuitCompact:${createdCircuit.id}`, cellKey: key };
|
||||||
|
}
|
||||||
|
|
||||||
const createdCircuit = (await createCircuit(projectId, circuitListId, {
|
const createdCircuit = (await createCircuit(projectId, circuitListId, {
|
||||||
sectionId,
|
sectionId,
|
||||||
equipmentIdentifier: next.nextIdentifier,
|
equipmentIdentifier: next.nextIdentifier,
|
||||||
displayName: "Neuer Stromkreis",
|
displayName: "Neuer Stromkreis",
|
||||||
sortOrder,
|
sortOrder,
|
||||||
isReserve: !isDeviceField,
|
isReserve: true,
|
||||||
})) as CircuitTreeCircuitDto;
|
})) as CircuitTreeCircuitDto;
|
||||||
|
|
||||||
if (isDeviceField) {
|
|
||||||
const createdRow = (await createCircuitDeviceRow(createdCircuit.id, {
|
|
||||||
name: "Manuelles Gerät",
|
|
||||||
displayName: "Manuelles Gerät",
|
|
||||||
phaseType: "single_phase",
|
|
||||||
quantity: 1,
|
|
||||||
powerPerUnit: 0,
|
|
||||||
simultaneityFactor: 1,
|
|
||||||
cosPhi: 1,
|
|
||||||
})) as { id: string };
|
|
||||||
await patchDeviceRow(createdRow.id, key, draft);
|
|
||||||
return { rowKey: `circuitCompact:${createdCircuit.id}`, cellKey: key };
|
|
||||||
}
|
|
||||||
|
|
||||||
await patchCircuit(createdCircuit.id, key, draft);
|
await patchCircuit(createdCircuit.id, key, draft);
|
||||||
return { rowKey: `reserveCircuit:${createdCircuit.id}`, cellKey: key };
|
return { rowKey: `reserveCircuit:${createdCircuit.id}`, cellKey: key };
|
||||||
}
|
}
|
||||||
@@ -1654,28 +1668,33 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
const next = await getNextCircuitIdentifier(sectionId);
|
const next = await getNextCircuitIdentifier(sectionId);
|
||||||
const sortOrder =
|
const sortOrder =
|
||||||
section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
||||||
const createdCircuit = (await createCircuit(projectId, circuitListId, {
|
const created = await createCircuitWithDeviceRows(projectId, circuitListId, {
|
||||||
sectionId,
|
circuit: {
|
||||||
equipmentIdentifier: next.nextIdentifier,
|
sectionId,
|
||||||
displayName: device.displayName || device.name,
|
equipmentIdentifier: next.nextIdentifier,
|
||||||
sortOrder,
|
displayName: device.displayName || device.name,
|
||||||
isReserve: false,
|
sortOrder,
|
||||||
})) as CircuitTreeCircuitDto;
|
isReserve: false,
|
||||||
|
},
|
||||||
const createdRow = (await createCircuitDeviceRow(createdCircuit.id, {
|
deviceRows: [
|
||||||
linkedProjectDeviceId: device.id,
|
{
|
||||||
name: device.name,
|
linkedProjectDeviceId: device.id,
|
||||||
displayName: device.displayName,
|
name: device.name,
|
||||||
phaseType: resolvePhaseType(device),
|
displayName: device.displayName,
|
||||||
connectionKind: device.connectionKind ?? undefined,
|
phaseType: resolvePhaseType(device),
|
||||||
costGroup: device.costGroup ?? undefined,
|
connectionKind: device.connectionKind ?? undefined,
|
||||||
quantity: device.quantity,
|
costGroup: device.costGroup ?? undefined,
|
||||||
powerPerUnit: device.powerPerUnit,
|
quantity: device.quantity,
|
||||||
simultaneityFactor: device.simultaneityFactor,
|
powerPerUnit: device.powerPerUnit,
|
||||||
cosPhi: device.cosPhi ?? undefined,
|
simultaneityFactor: device.simultaneityFactor,
|
||||||
category: device.category ?? undefined,
|
cosPhi: device.cosPhi ?? undefined,
|
||||||
remark: device.remark ?? undefined,
|
category: device.category ?? undefined,
|
||||||
})) as { id: string };
|
remark: device.remark ?? undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const createdCircuit = created.circuit;
|
||||||
|
const createdRow = created.deviceRows[0];
|
||||||
|
|
||||||
setActiveSectionId(sectionId);
|
setActiveSectionId(sectionId);
|
||||||
setTargetCircuitId(createdCircuit.id);
|
setTargetCircuitId(createdCircuit.id);
|
||||||
@@ -1732,7 +1751,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function recreateCircuit(snapshot: CircuitSnapshot) {
|
async function recreateCircuit(snapshot: CircuitSnapshot) {
|
||||||
const createdCircuit = (await createCircuit(projectId, circuitListId, {
|
const circuitInput = {
|
||||||
sectionId: snapshot.sectionId,
|
sectionId: snapshot.sectionId,
|
||||||
equipmentIdentifier: snapshot.equipmentIdentifier,
|
equipmentIdentifier: snapshot.equipmentIdentifier,
|
||||||
displayName: snapshot.displayName,
|
displayName: snapshot.displayName,
|
||||||
@@ -1750,11 +1769,23 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
status: snapshot.status,
|
status: snapshot.status,
|
||||||
isReserve: snapshot.isReserve,
|
isReserve: snapshot.isReserve,
|
||||||
remark: snapshot.remark,
|
remark: snapshot.remark,
|
||||||
})) as CircuitTreeCircuitDto;
|
};
|
||||||
|
|
||||||
const createdRowIds: string[] = [];
|
if (snapshot.deviceRows.length === 0) {
|
||||||
for (const row of snapshot.deviceRows) {
|
const createdCircuit = (await createCircuit(
|
||||||
const created = (await createCircuitDeviceRow(createdCircuit.id, {
|
projectId,
|
||||||
|
circuitListId,
|
||||||
|
circuitInput
|
||||||
|
)) as CircuitTreeCircuitDto;
|
||||||
|
return { circuitId: createdCircuit.id, rowIds: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await createCircuitWithDeviceRows(projectId, circuitListId, {
|
||||||
|
circuit: {
|
||||||
|
...circuitInput,
|
||||||
|
isReserve: false,
|
||||||
|
},
|
||||||
|
deviceRows: snapshot.deviceRows.map((row) => ({
|
||||||
linkedProjectDeviceId: row.linkedProjectDeviceId,
|
linkedProjectDeviceId: row.linkedProjectDeviceId,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
displayName: row.displayName,
|
displayName: row.displayName,
|
||||||
@@ -1773,10 +1804,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
remark: row.remark,
|
remark: row.remark,
|
||||||
overriddenFields: row.overriddenFields,
|
overriddenFields: row.overriddenFields,
|
||||||
sortOrder: row.sortOrder,
|
sortOrder: row.sortOrder,
|
||||||
})) as { id: string };
|
})),
|
||||||
createdRowIds.push(created.id);
|
});
|
||||||
}
|
return {
|
||||||
return { circuitId: createdCircuit.id, rowIds: createdRowIds };
|
circuitId: created.circuit.id,
|
||||||
|
rowIds: created.deviceRows.map((row) => row.id),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDraggedProjectDeviceId(event: DragEvent<HTMLElement>) {
|
function parseDraggedProjectDeviceId(event: DragEvent<HTMLElement>) {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import type {
|
|||||||
RoomDto,
|
RoomDto,
|
||||||
UpdateConsumerInput,
|
UpdateConsumerInput,
|
||||||
CircuitTreeResponseDto,
|
CircuitTreeResponseDto,
|
||||||
|
CircuitTreeCircuitDto,
|
||||||
|
CircuitTreeDeviceRowDto,
|
||||||
CreateCircuitInputDto,
|
CreateCircuitInputDto,
|
||||||
UpdateCircuitInputDto,
|
UpdateCircuitInputDto,
|
||||||
CreateCircuitDeviceRowInputDto,
|
CreateCircuitDeviceRowInputDto,
|
||||||
@@ -98,6 +100,26 @@ export function createCircuit(projectId: string, circuitListId: string, input: C
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createCircuitWithDeviceRows(
|
||||||
|
projectId: string,
|
||||||
|
circuitListId: string,
|
||||||
|
input: {
|
||||||
|
circuit: CreateCircuitInputDto;
|
||||||
|
deviceRows: CreateCircuitDeviceRowInputDto[];
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return request<{
|
||||||
|
circuit: CircuitTreeCircuitDto;
|
||||||
|
deviceRows: CircuitTreeDeviceRowDto[];
|
||||||
|
}>(
|
||||||
|
`/api/projects/${projectId}/circuit-lists/${circuitListId}/circuits-with-device-rows`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateCircuitById(circuitId: string, input: UpdateCircuitInputDto) {
|
export function updateCircuitById(circuitId: string, input: UpdateCircuitInputDto) {
|
||||||
return request(`/api/circuits/${circuitId}`, {
|
return request(`/api/circuits/${circuitId}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
|
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
|
||||||
import { createCircuitSchema, updateCircuitSchema } from "../../shared/validation/circuit.schemas.js";
|
import {
|
||||||
|
createCircuitSchema,
|
||||||
|
createCircuitWithDeviceRowsSchema,
|
||||||
|
updateCircuitSchema,
|
||||||
|
} from "../../shared/validation/circuit.schemas.js";
|
||||||
|
|
||||||
const circuitWriteService = new CircuitWriteService();
|
const circuitWriteService = new CircuitWriteService();
|
||||||
|
|
||||||
@@ -23,6 +27,34 @@ export async function createCircuit(req: Request, res: Response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createCircuitWithDeviceRows(req: Request, res: Response) {
|
||||||
|
const { projectId, circuitListId } = req.params;
|
||||||
|
if (typeof projectId !== "string" || typeof circuitListId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Ungültige Parameter." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = createCircuitWithDeviceRowsSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = await circuitWriteService.createCircuitWithDeviceRows(
|
||||||
|
projectId,
|
||||||
|
circuitListId,
|
||||||
|
parsed.data
|
||||||
|
);
|
||||||
|
return res.status(201).json(created);
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Stromkreis und Gerätezeilen konnten nicht erstellt werden.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateCircuit(req: Request, res: Response) {
|
export async function updateCircuit(req: Request, res: Response) {
|
||||||
const { circuitId } = req.params;
|
const { circuitId } = req.params;
|
||||||
if (typeof circuitId !== "string") {
|
if (typeof circuitId !== "string") {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import {
|
import {
|
||||||
createCircuit,
|
createCircuit,
|
||||||
|
createCircuitWithDeviceRows,
|
||||||
deleteCircuit,
|
deleteCircuit,
|
||||||
getNextCircuitIdentifier,
|
getNextCircuitIdentifier,
|
||||||
updateCircuit,
|
updateCircuit,
|
||||||
@@ -10,6 +11,10 @@ import { createCircuitDeviceRow } from "../controllers/circuit-device-row.contro
|
|||||||
export const circuitRouter = Router();
|
export const circuitRouter = Router();
|
||||||
|
|
||||||
circuitRouter.post("/projects/:projectId/circuit-lists/:circuitListId/circuits", createCircuit);
|
circuitRouter.post("/projects/:projectId/circuit-lists/:circuitListId/circuits", createCircuit);
|
||||||
|
circuitRouter.post(
|
||||||
|
"/projects/:projectId/circuit-lists/:circuitListId/circuits-with-device-rows",
|
||||||
|
createCircuitWithDeviceRows
|
||||||
|
);
|
||||||
circuitRouter.patch("/circuits/:circuitId", updateCircuit);
|
circuitRouter.patch("/circuits/:circuitId", updateCircuit);
|
||||||
circuitRouter.delete("/circuits/:circuitId", deleteCircuit);
|
circuitRouter.delete("/circuits/:circuitId", deleteCircuit);
|
||||||
circuitRouter.get("/circuit-sections/:sectionId/next-identifier", getNextCircuitIdentifier);
|
circuitRouter.get("/circuit-sections/:sectionId/next-identifier", getNextCircuitIdentifier);
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ export const createCircuitDeviceRowSchema = z.object({
|
|||||||
sortOrder: z.number().optional(),
|
sortOrder: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const createCircuitWithDeviceRowsSchema = z.object({
|
||||||
|
circuit: createCircuitSchema,
|
||||||
|
deviceRows: z.array(createCircuitDeviceRowSchema).min(1),
|
||||||
|
});
|
||||||
|
|
||||||
export const updateCircuitDeviceRowSchema = createCircuitDeviceRowSchema.partial();
|
export const updateCircuitDeviceRowSchema = createCircuitDeviceRowSchema.partial();
|
||||||
|
|
||||||
export const moveCircuitDeviceRowSchema = z
|
export const moveCircuitDeviceRowSchema = z
|
||||||
@@ -95,6 +100,7 @@ export const updateSectionEquipmentIdentifiersSchema = z.object({
|
|||||||
export type CreateCircuitInput = z.infer<typeof createCircuitSchema>;
|
export type CreateCircuitInput = z.infer<typeof createCircuitSchema>;
|
||||||
export type UpdateCircuitInput = z.infer<typeof updateCircuitSchema>;
|
export type UpdateCircuitInput = z.infer<typeof updateCircuitSchema>;
|
||||||
export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>;
|
export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>;
|
||||||
|
export type CreateCircuitWithDeviceRowsInput = z.infer<typeof createCircuitWithDeviceRowsSchema>;
|
||||||
export type UpdateCircuitDeviceRowInput = z.infer<typeof updateCircuitDeviceRowSchema>;
|
export type UpdateCircuitDeviceRowInput = z.infer<typeof updateCircuitDeviceRowSchema>;
|
||||||
export type MoveCircuitDeviceRowInput = z.infer<typeof moveCircuitDeviceRowSchema>;
|
export type MoveCircuitDeviceRowInput = z.infer<typeof moveCircuitDeviceRowSchema>;
|
||||||
export type MoveCircuitDeviceRowsBulkInput = z.infer<typeof moveCircuitDeviceRowsBulkSchema>;
|
export type MoveCircuitDeviceRowsBulkInput = z.infer<typeof moveCircuitDeviceRowsBulkSchema>;
|
||||||
|
|||||||
@@ -215,6 +215,85 @@ describe("circuit write service rules", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("creates a circuit and all first device rows through one repository command", async () => {
|
||||||
|
let transactionalCreate: {
|
||||||
|
circuit: { circuitListId: string; sectionId: string; equipmentIdentifier: string };
|
||||||
|
deviceRows: Array<{ linkedProjectDeviceId?: string; name: string }>;
|
||||||
|
} | undefined;
|
||||||
|
const service = new CircuitWriteService({
|
||||||
|
circuitListRepository: {
|
||||||
|
async findById() {
|
||||||
|
return { id: "l1", projectId: "p1" } as never;
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
circuitSectionRepository: {
|
||||||
|
async findById() {
|
||||||
|
return { id: "s1", circuitListId: "l1" } as never;
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
circuitRepository: {
|
||||||
|
async existsByEquipmentIdentifier() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
async findById() {
|
||||||
|
return { id: "c-new", circuitListId: "l1", sectionId: "s1" } as never;
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
projectDeviceRepository: {
|
||||||
|
async findById(projectId: string, deviceId: string) {
|
||||||
|
return projectId === "p1" && deviceId === "pd1" ? ({ id: "pd1" } as never) : null;
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
deviceRowRepository: {
|
||||||
|
createCircuitWithDeviceRowsTransactional(input: typeof transactionalCreate) {
|
||||||
|
transactionalCreate = input;
|
||||||
|
return { circuitId: "c-new", rowIds: ["r1", "r2"] };
|
||||||
|
},
|
||||||
|
async findById(rowId: string) {
|
||||||
|
return { id: rowId, circuitId: "c-new" } as never;
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
const created = await service.createCircuitWithDeviceRows("p1", "l1", {
|
||||||
|
circuit: {
|
||||||
|
sectionId: "s1",
|
||||||
|
equipmentIdentifier: "-2F1",
|
||||||
|
displayName: "Steckdosen",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
deviceRows: [
|
||||||
|
{
|
||||||
|
linkedProjectDeviceId: "pd1",
|
||||||
|
name: "Socket",
|
||||||
|
displayName: "Steckdose",
|
||||||
|
quantity: 1,
|
||||||
|
powerPerUnit: 0.2,
|
||||||
|
simultaneityFactor: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Manual",
|
||||||
|
displayName: "Manuell",
|
||||||
|
quantity: 2,
|
||||||
|
powerPerUnit: 0.1,
|
||||||
|
simultaneityFactor: 0.8,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(transactionalCreate?.circuit.circuitListId, "l1");
|
||||||
|
assert.equal(transactionalCreate?.circuit.sectionId, "s1");
|
||||||
|
assert.equal(transactionalCreate?.circuit.equipmentIdentifier, "-2F1");
|
||||||
|
assert.deepEqual(
|
||||||
|
transactionalCreate?.deviceRows.map((row) => row.name),
|
||||||
|
["Socket", "Manual"]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
created.deviceRows.map((row) => row?.id),
|
||||||
|
["r1", "r2"]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("renumber uses safe bulk identifier update for swapped identifiers", async () => {
|
it("renumber uses safe bulk identifier update for swapped identifiers", async () => {
|
||||||
let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = [];
|
let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = [];
|
||||||
const service = new CircuitWriteService({
|
const service = new CircuitWriteService({
|
||||||
@@ -997,6 +1076,76 @@ describe("circuit write service rules", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("new circuit and initial device rows share one synchronous transaction", () => {
|
||||||
|
const repository = new CircuitDeviceRowRepository();
|
||||||
|
const originalTransaction = (db as unknown as { transaction: unknown }).transaction;
|
||||||
|
|
||||||
|
let callbackReturnedPromise = false;
|
||||||
|
const insertedValues: Array<Record<string, unknown>> = [];
|
||||||
|
(db as unknown as { transaction: (cb: (tx: unknown) => unknown) => void }).transaction = (cb) => {
|
||||||
|
const fakeTx = {
|
||||||
|
insert() {
|
||||||
|
return {
|
||||||
|
values(values: Record<string, unknown>) {
|
||||||
|
insertedValues.push(values);
|
||||||
|
return {
|
||||||
|
run() {
|
||||||
|
return { changes: 1 };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const callbackResult = cb(fakeTx);
|
||||||
|
callbackReturnedPromise = Boolean(
|
||||||
|
callbackResult && typeof (callbackResult as Promise<unknown>).then === "function"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = repository.createCircuitWithDeviceRowsTransactional({
|
||||||
|
circuit: {
|
||||||
|
circuitListId: "l1",
|
||||||
|
sectionId: "s1",
|
||||||
|
equipmentIdentifier: "-2F1",
|
||||||
|
displayName: "Steckdosen",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
deviceRows: [
|
||||||
|
{
|
||||||
|
name: "Socket",
|
||||||
|
displayName: "Steckdose",
|
||||||
|
quantity: 1,
|
||||||
|
powerPerUnit: 0.2,
|
||||||
|
simultaneityFactor: 1,
|
||||||
|
sortOrder: 15,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Manual",
|
||||||
|
displayName: "Manuell",
|
||||||
|
quantity: 2,
|
||||||
|
powerPerUnit: 0.1,
|
||||||
|
simultaneityFactor: 0.8,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(callbackReturnedPromise, false);
|
||||||
|
assert.equal(insertedValues.length, 3);
|
||||||
|
assert.equal(insertedValues[0].id, created.circuitId);
|
||||||
|
assert.equal(insertedValues[0].isReserve, 0);
|
||||||
|
assert.equal(insertedValues[1].id, created.rowIds[0]);
|
||||||
|
assert.equal(insertedValues[1].circuitId, created.circuitId);
|
||||||
|
assert.equal(insertedValues[1].sortOrder, 15);
|
||||||
|
assert.equal(insertedValues[2].id, created.rowIds[1]);
|
||||||
|
assert.equal(insertedValues[2].circuitId, created.circuitId);
|
||||||
|
assert.equal(insertedValues[2].sortOrder, 20);
|
||||||
|
} finally {
|
||||||
|
(db as unknown as { transaction: unknown }).transaction = originalTransaction;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("tracks local edits on linked device rows as overridden fields", async () => {
|
it("tracks local edits on linked device rows as overridden fields", async () => {
|
||||||
let savedOverrides: string | undefined;
|
let savedOverrides: string | undefined;
|
||||||
const current = {
|
const current = {
|
||||||
|
|||||||
Reference in New Issue
Block a user