Expose distribution board copy and delete API

This commit is contained in:
2026-07-31 11:59:31 +02:00
parent 0fd564a930
commit 86f1d42e60
7 changed files with 182 additions and 2 deletions
+5
View File
@@ -114,6 +114,11 @@ export interface DistributionBoardCommandResultDto
distributionBoard: DistributionBoardDto; distributionBoard: DistributionBoardDto;
} }
export interface DistributionBoardDeleteCommandResultDto
extends ProjectCommandResultDto {
distributionBoardId: string;
}
export interface CircuitListDto { export interface CircuitListDto {
id: string; id: string;
projectId: string; projectId: string;
+30
View File
@@ -6,6 +6,7 @@ import type {
CreateGlobalDeviceInput, CreateGlobalDeviceInput,
DistributionBoardDto, DistributionBoardDto,
DistributionBoardCommandResultDto, DistributionBoardCommandResultDto,
DistributionBoardDeleteCommandResultDto,
FloorDto, FloorDto,
GlobalDeviceDto, GlobalDeviceDto,
ProjectCommandDto, ProjectCommandDto,
@@ -331,6 +332,35 @@ export function updateDistributionBoard(
); );
} }
export function copyDistributionBoard(
projectId: string,
distributionBoardId: string,
name: string,
expectedRevision: number
) {
return request<DistributionBoardCommandResultDto>(
`/api/projects/${projectId}/distribution-boards/${distributionBoardId}/copy`,
{
method: "POST",
body: JSON.stringify({ name, expectedRevision }),
}
);
}
export function deleteDistributionBoard(
projectId: string,
distributionBoardId: string,
expectedRevision: number
) {
return request<DistributionBoardDeleteCommandResultDto>(
`/api/projects/${projectId}/distribution-boards/${distributionBoardId}`,
{
method: "DELETE",
body: JSON.stringify({ expectedRevision }),
}
);
}
export function listCircuitLists(projectId: string) { export function listCircuitLists(projectId: string) {
return request<CircuitListDto[]>(`/api/projects/${projectId}/circuit-lists`); return request<CircuitListDto[]>(`/api/projects/${projectId}/circuit-lists`);
} }
@@ -33,6 +33,8 @@ const commandTypeLabels: Record<string, string> = {
"distribution-board.insert": "Verteilung angelegt", "distribution-board.insert": "Verteilung angelegt",
"distribution-board.update": "Verteilung bearbeitet", "distribution-board.update": "Verteilung bearbeitet",
"distribution-board.delete": "Verteilung entfernt", "distribution-board.delete": "Verteilung entfernt",
"distribution-board.insert-subtree": "Verteilung vollständig eingefügt",
"distribution-board.delete-subtree": "Verteilung vollständig entfernt",
"distribution-board-component.insert": "Verteilergerät angelegt", "distribution-board-component.insert": "Verteilergerät angelegt",
"distribution-board-component.delete": "Verteilergerät entfernt", "distribution-board-component.delete": "Verteilergerät entfernt",
"distribution-board-component.update": "Verteilergerät bearbeitet", "distribution-board-component.update": "Verteilergerät bearbeitet",
@@ -5,10 +5,20 @@ import {
createDistributionBoardStructureSnapshot, createDistributionBoardStructureSnapshot,
} from "../../domain/models/distribution-board-structure-project-command.model.js"; } from "../../domain/models/distribution-board-structure-project-command.model.js";
import { import {
createDistributionBoardDeleteSubtreeProjectCommand,
createDistributionBoardInsertSubtreeProjectCommand,
} from "../../domain/models/distribution-board-subtree-project-command.model.js";
import { cloneDistributionBoardSubtree } from "../../domain/models/distribution-board-subtree-snapshot.model.js";
import {
copyDistributionBoardSchema,
createDistributionBoardSchema, createDistributionBoardSchema,
deleteDistributionBoardSchema,
updateDistributionBoardSchema, updateDistributionBoardSchema,
} from "../../shared/validation/project-structure.schemas.js"; } from "../../shared/validation/project-structure.schemas.js";
import { projectCommandService } from "../composition/project-command-stores.js"; import {
distributionBoardSubtreeProjectCommandStore,
projectCommandService,
} from "../composition/project-command-stores.js";
import { distributionBoardRepository } from "../composition/application-repositories.js"; import { distributionBoardRepository } from "../composition/application-repositories.js";
import { respondWithProjectCommandError } from "./project-command.controller.js"; import { respondWithProjectCommandError } from "./project-command.controller.js";
@@ -101,3 +111,85 @@ export async function updateDistributionBoard(
return respondWithProjectCommandError(error, res); return respondWithProjectCommandError(error, res);
} }
} }
export async function copyDistributionBoard(
req: Request,
res: Response
) {
const ids = getDistributionBoardIds(req, res);
if (!ids) {
return;
}
const parsed = copyDistributionBoardSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.flatten() });
}
try {
const source = distributionBoardSubtreeProjectCommandStore.capture(
ids.projectId,
ids.distributionBoardId
);
const copy = cloneDistributionBoardSubtree(source, parsed.data.name);
const result = projectCommandService.executeUser({
projectId: ids.projectId,
expectedRevision: parsed.data.expectedRevision,
description: "Verteilung kopieren",
command:
createDistributionBoardInsertSubtreeProjectCommand(copy),
});
return res.status(201).json({
...result,
distributionBoard: copy.distributionBoard,
});
} catch (error) {
return respondWithProjectCommandError(error, res);
}
}
export async function deleteDistributionBoard(
req: Request,
res: Response
) {
const ids = getDistributionBoardIds(req, res);
if (!ids) {
return;
}
const parsed = deleteDistributionBoardSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.flatten() });
}
try {
const snapshot =
distributionBoardSubtreeProjectCommandStore.capture(
ids.projectId,
ids.distributionBoardId
);
const result = projectCommandService.executeUser({
projectId: ids.projectId,
expectedRevision: parsed.data.expectedRevision,
description: "Verteilung löschen",
command:
createDistributionBoardDeleteSubtreeProjectCommand(snapshot),
});
return res.json({
...result,
distributionBoardId: ids.distributionBoardId,
});
} catch (error) {
return respondWithProjectCommandError(error, res);
}
}
function getDistributionBoardIds(req: Request, res: Response) {
const { projectId, distributionBoardId } = req.params;
if (
typeof projectId !== "string" ||
typeof distributionBoardId !== "string" ||
!projectId.trim() ||
!distributionBoardId.trim()
) {
res.status(400).json({ error: "Invalid distribution board id" });
return null;
}
return { projectId, distributionBoardId };
}
+10
View File
@@ -6,7 +6,9 @@ import {
updateProjectSettings, updateProjectSettings,
} from "../controllers/project.controller.js"; } from "../controllers/project.controller.js";
import { import {
copyDistributionBoard,
createDistributionBoard, createDistributionBoard,
deleteDistributionBoard,
listDistributionBoardsByProject, listDistributionBoardsByProject,
updateDistributionBoard, updateDistributionBoard,
} from "../controllers/distribution-board.controller.js"; } from "../controllers/distribution-board.controller.js";
@@ -56,10 +58,18 @@ projectRouter.post(
projectRouter.put("/:projectId", updateProjectSettings); projectRouter.put("/:projectId", updateProjectSettings);
projectRouter.get("/:projectId/distribution-boards", listDistributionBoardsByProject); projectRouter.get("/:projectId/distribution-boards", listDistributionBoardsByProject);
projectRouter.post("/:projectId/distribution-boards", createDistributionBoard); projectRouter.post("/:projectId/distribution-boards", createDistributionBoard);
projectRouter.post(
"/:projectId/distribution-boards/:distributionBoardId/copy",
copyDistributionBoard
);
projectRouter.put( projectRouter.put(
"/:projectId/distribution-boards/:distributionBoardId", "/:projectId/distribution-boards/:distributionBoardId",
updateDistributionBoard updateDistributionBoard
); );
projectRouter.delete(
"/:projectId/distribution-boards/:distributionBoardId",
deleteDistributionBoard
);
projectRouter.get("/:projectId/circuit-lists", listCircuitListsByProject); projectRouter.get("/:projectId/circuit-lists", listCircuitListsByProject);
projectRouter.get("/:projectId/circuit-lists/:circuitListId/tree", getCircuitTree); projectRouter.get("/:projectId/circuit-lists/:circuitListId/tree", getCircuitTree);
projectRouter.get("/:projectId/floors", listFloorsByProject); projectRouter.get("/:projectId/floors", listFloorsByProject);
@@ -49,6 +49,19 @@ export const updateDistributionBoardSchema = z
}) })
.strict(); .strict();
export const copyDistributionBoardSchema = z
.object({
expectedRevision: expectedProjectRevisionSchema,
name: z.string().trim().min(1).max(200),
})
.strict();
export const deleteDistributionBoardSchema = z
.object({
expectedRevision: expectedProjectRevisionSchema,
})
.strict();
export const createFloorSchema = z export const createFloorSchema = z
.object({ .object({
expectedRevision: expectedProjectRevisionSchema, expectedRevision: expectedProjectRevisionSchema,
@@ -28,7 +28,9 @@ import {
type DistributionBoardUpdateProjectCommand, type DistributionBoardUpdateProjectCommand,
} from "../src/domain/models/distribution-board-project-command.model.js"; } from "../src/domain/models/distribution-board-project-command.model.js";
import { import {
copyDistributionBoard,
createDistributionBoard, createDistributionBoard,
deleteDistributionBoard,
updateDistributionBoard, updateDistributionBoard,
} from "../src/frontend/utils/api.js"; } from "../src/frontend/utils/api.js";
@@ -568,7 +570,7 @@ describe("distribution-board structure project command", () => {
} }
}); });
it("sends distribution-board updates to the revision-safe API", async () => { it("sends distribution-board writes to the revision-safe API", async () => {
const requests: Array<{ url: string; method: string; body: unknown }> = []; const requests: Array<{ url: string; method: string; body: unknown }> = [];
const originalFetch = globalThis.fetch; const originalFetch = globalThis.fetch;
globalThis.fetch = async (input, init) => { globalThis.fetch = async (input, init) => {
@@ -593,6 +595,17 @@ describe("distribution-board structure project command", () => {
}, },
9 9
); );
await copyDistributionBoard(
"project-1",
"board-1",
"UV 1 Kopie",
10
);
await deleteDistributionBoard(
"project-1",
"board-1",
11
);
} finally { } finally {
globalThis.fetch = originalFetch; globalThis.fetch = originalFetch;
} }
@@ -607,6 +620,21 @@ describe("distribution-board structure project command", () => {
expectedRevision: 9, expectedRevision: 9,
}, },
}, },
{
url: "/api/projects/project-1/distribution-boards/board-1/copy",
method: "POST",
body: {
name: "UV 1 Kopie",
expectedRevision: 10,
},
},
{
url: "/api/projects/project-1/distribution-boards/board-1",
method: "DELETE",
body: {
expectedRevision: 11,
},
},
]); ]);
}); });