Add circuit group reordering

This commit is contained in:
2026-07-30 19:49:27 +02:00
parent 1250f9a6af
commit e702712bd8
11 changed files with 335 additions and 7 deletions
@@ -2,13 +2,17 @@ import { and, eq } from "drizzle-orm";
import {
assertCircuitGroupDeleteProjectCommand,
assertCircuitGroupInsertProjectCommand,
assertCircuitGroupReorderProjectCommand,
assertCircuitGroupUpdateProjectCommand,
circuitGroupDeleteCommandType,
circuitGroupInsertCommandType,
circuitGroupReorderCommandType,
circuitGroupUpdateCommandType,
createCircuitGroupDeleteProjectCommand,
createCircuitGroupInsertProjectCommand,
createCircuitGroupReorderProjectCommand,
createCircuitGroupUpdateProjectCommand,
type CircuitGroupReorderAssignment,
type CircuitGroupSnapshot,
type CircuitGroupStructureProjectCommand,
} from "../../domain/models/circuit-group-structure-project-command.model.js";
@@ -68,6 +72,19 @@ export class CircuitGroupStructureProjectCommandRepository
command.payload.expected
);
}
if (command.type === circuitGroupReorderCommandType) {
assertCircuitGroupReorderProjectCommand(command);
const inverseAssignments = this.reorder(
database,
projectId,
command.payload.circuitListId,
command.payload.assignments
);
return createCircuitGroupReorderProjectCommand(
command.payload.circuitListId,
inverseAssignments
);
}
throw new Error("Unsupported circuit-group structure command.");
}
@@ -168,6 +185,61 @@ export class CircuitGroupStructureProjectCommandRepository
}
}
private reorder(
database: AppDatabase,
projectId: string,
circuitListId: string,
assignments: CircuitGroupReorderAssignment[]
) {
this.assertListOwnership(database, projectId, circuitListId);
const persisted = database
.select({
id: circuitSections.id,
sortOrder: circuitSections.sortOrder,
})
.from(circuitSections)
.where(eq(circuitSections.circuitListId, circuitListId))
.all();
const persistedById = new Map(
persisted.map((group) => [group.id, group])
);
if (
persisted.length !== assignments.length ||
assignments.some((assignment) => {
const group = persistedById.get(assignment.groupId);
return !group || group.sortOrder !== assignment.expectedSortOrder;
})
) {
throw new Error(
"Circuit-group reorder must include every unchanged group in the list."
);
}
for (const assignment of assignments) {
if (assignment.expectedSortOrder === assignment.targetSortOrder) {
continue;
}
const updated = database
.update(circuitSections)
.set({ sortOrder: assignment.targetSortOrder })
.where(
and(
eq(circuitSections.id, assignment.groupId),
eq(circuitSections.circuitListId, circuitListId),
eq(circuitSections.sortOrder, assignment.expectedSortOrder)
)
)
.run();
if (updated.changes !== 1) {
throw new Error("Circuit group changed during reorder.");
}
}
return assignments.map((assignment) => ({
groupId: assignment.groupId,
expectedSortOrder: assignment.targetSortOrder,
targetSortOrder: assignment.expectedSortOrder,
}));
}
private getCurrent(database: AppDatabase, id: string) {
return database
.select()
@@ -8,6 +8,7 @@ import type { SerializedProjectCommand } from "./project-command.model.js";
export const circuitGroupInsertCommandType = "circuit-group.insert" as const;
export const circuitGroupDeleteCommandType = "circuit-group.delete" as const;
export const circuitGroupUpdateCommandType = "circuit-group.update" as const;
export const circuitGroupReorderCommandType = "circuit-group.reorder" as const;
export const circuitGroupStructureCommandSchemaVersion = 1 as const;
export interface CircuitGroupSnapshot {
@@ -30,6 +31,17 @@ interface UpdatePayload {
target: CircuitGroupSnapshot;
}
export interface CircuitGroupReorderAssignment {
groupId: string;
expectedSortOrder: number;
targetSortOrder: number;
}
interface ReorderPayload {
circuitListId: string;
assignments: CircuitGroupReorderAssignment[];
}
export interface CircuitGroupInsertProjectCommand
extends SerializedProjectCommand<SnapshotPayload> {
schemaVersion: typeof circuitGroupStructureCommandSchemaVersion;
@@ -48,10 +60,17 @@ export interface CircuitGroupUpdateProjectCommand
type: typeof circuitGroupUpdateCommandType;
}
export interface CircuitGroupReorderProjectCommand
extends SerializedProjectCommand<ReorderPayload> {
schemaVersion: typeof circuitGroupStructureCommandSchemaVersion;
type: typeof circuitGroupReorderCommandType;
}
export type CircuitGroupStructureProjectCommand =
| CircuitGroupInsertProjectCommand
| CircuitGroupDeleteProjectCommand
| CircuitGroupUpdateProjectCommand;
| CircuitGroupUpdateProjectCommand
| CircuitGroupReorderProjectCommand;
export function createCircuitGroupInsertProjectCommand(
snapshot: CircuitGroupSnapshot
@@ -90,6 +109,19 @@ export function createCircuitGroupUpdateProjectCommand(
return command;
}
export function createCircuitGroupReorderProjectCommand(
circuitListId: string,
assignments: CircuitGroupReorderAssignment[]
): CircuitGroupReorderProjectCommand {
const command: CircuitGroupReorderProjectCommand = {
schemaVersion: circuitGroupStructureCommandSchemaVersion,
type: circuitGroupReorderCommandType,
payload: { circuitListId, assignments },
};
assertCircuitGroupReorderProjectCommand(command);
return command;
}
export function assertCircuitGroupInsertProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is CircuitGroupInsertProjectCommand {
@@ -132,6 +164,44 @@ export function assertCircuitGroupUpdateProjectCommand(
}
}
export function assertCircuitGroupReorderProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is CircuitGroupReorderProjectCommand {
if (
command.schemaVersion !== circuitGroupStructureCommandSchemaVersion ||
command.type !== circuitGroupReorderCommandType ||
!isPlainObject(command.payload) ||
Object.keys(command.payload).length !== 2 ||
typeof command.payload.circuitListId !== "string" ||
!command.payload.circuitListId.trim() ||
!Array.isArray(command.payload.assignments) ||
command.payload.assignments.length === 0
) {
throw new Error("Unsupported circuit-group reorder command.");
}
const groupIds = new Set<string>();
let hasChange = false;
for (const assignment of command.payload.assignments) {
if (
!isPlainObject(assignment) ||
typeof assignment.groupId !== "string" ||
!assignment.groupId.trim() ||
!Number.isFinite(assignment.expectedSortOrder) ||
!Number.isFinite(assignment.targetSortOrder) ||
groupIds.has(assignment.groupId)
) {
throw new Error(
"Circuit-group reorder contains an invalid or duplicate assignment."
);
}
hasChange ||= assignment.expectedSortOrder !== assignment.targetSortOrder;
groupIds.add(assignment.groupId);
}
if (!hasChange) {
throw new Error("Circuit-group reorder must change at least one position.");
}
}
function assertSnapshotCommand(
command: SerializedProjectCommand<unknown>,
type:
@@ -114,9 +114,11 @@ import type {
import {
assertCircuitGroupDeleteProjectCommand,
assertCircuitGroupInsertProjectCommand,
assertCircuitGroupReorderProjectCommand,
assertCircuitGroupUpdateProjectCommand,
circuitGroupDeleteCommandType,
circuitGroupInsertCommandType,
circuitGroupReorderCommandType,
circuitGroupUpdateCommandType,
} from "../models/circuit-group-structure-project-command.model.js";
import type { ProjectLocationStructureProjectCommandStore } from "../ports/project-location-structure-project-command.store.js";
@@ -364,6 +366,13 @@ export class ProjectCommandService implements ProjectCommandExecutor {
command: input.command,
}).revision;
}
case circuitGroupReorderCommandType: {
assertCircuitGroupReorderProjectCommand(input.command);
return this.circuitGroupStructureStore.execute({
...input,
command: input.command,
}).revision;
}
case projectFloorInsertCommandType: {
assertProjectFloorInsertProjectCommand(input.command);
return this.projectLocationStructureStore.execute({
@@ -39,6 +39,7 @@ const commandTypeLabels: Record<string, string> = {
"circuit-group.insert": "Stromkreisgruppe angelegt",
"circuit-group.delete": "Stromkreisgruppe entfernt",
"circuit-group.update": "Stromkreisgruppe bearbeitet",
"circuit-group.reorder": "Stromkreisgruppen sortiert",
"project-floor.insert": "Geschoss angelegt",
"project-floor.delete": "Geschoss entfernt",
"project-room.insert": "Raum angelegt",