Add configurable distribution supply types
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
listRooms,
|
||||
importProjectTransfer,
|
||||
synchronizeProjectDeviceRows,
|
||||
updateDistributionBoard,
|
||||
updateProjectDevice,
|
||||
updateProjectSettings,
|
||||
} from "../../../frontend/utils/api";
|
||||
@@ -41,6 +42,10 @@ import {
|
||||
projectDeviceSyncFields,
|
||||
type ProjectDeviceSyncField,
|
||||
} from "../../../shared/constants/project-device-sync-fields";
|
||||
import {
|
||||
distributionBoardSupplyTypeLabels,
|
||||
type DistributionBoardSupplyType,
|
||||
} from "../../../shared/constants/distribution-board";
|
||||
import { ProjectVersionHistory } from "../../../frontend/components/project-version-history";
|
||||
import {
|
||||
ProjectSettingsModal,
|
||||
@@ -74,6 +79,14 @@ export default function ProjectDetailPage() {
|
||||
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
||||
const [globalDevices, setGlobalDevices] = useState<GlobalDeviceDto[]>([]);
|
||||
const [boardName, setBoardName] = useState("");
|
||||
const [boardFloorId, setBoardFloorId] = useState("");
|
||||
const [boardSupplyType, setBoardSupplyType] =
|
||||
useState<DistributionBoardSupplyType>("AV");
|
||||
const [editingBoard, setEditingBoard] =
|
||||
useState<DistributionBoardDto | null>(null);
|
||||
const [editingBoardFloorId, setEditingBoardFloorId] = useState("");
|
||||
const [editingBoardSupplyType, setEditingBoardSupplyType] =
|
||||
useState<DistributionBoardSupplyType>("AV");
|
||||
const [floorName, setFloorName] = useState("");
|
||||
const [roomNumber, setRoomNumber] = useState("");
|
||||
const [roomName, setRoomName] = useState("");
|
||||
@@ -158,6 +171,24 @@ export default function ProjectDetailPage() {
|
||||
() => new Map(circuitLists.map((circuitList) => [circuitList.distributionBoardId, circuitList])),
|
||||
[circuitLists]
|
||||
);
|
||||
const enabledBoardSupplyTypes =
|
||||
project?.enabledDistributionBoardSupplyTypes ?? [];
|
||||
const usedBoardSupplyTypes = useMemo(
|
||||
() =>
|
||||
[
|
||||
...new Set(
|
||||
boards
|
||||
.map((board) => board.supplyType)
|
||||
.filter(
|
||||
(
|
||||
supplyType
|
||||
): supplyType is DistributionBoardSupplyType =>
|
||||
supplyType !== null
|
||||
)
|
||||
),
|
||||
],
|
||||
[boards]
|
||||
);
|
||||
|
||||
function applyProjectRevision(currentRevision: number) {
|
||||
setProject((current) =>
|
||||
@@ -175,7 +206,11 @@ export default function ProjectDetailPage() {
|
||||
try {
|
||||
const result = await createDistributionBoard(
|
||||
projectId,
|
||||
boardName.trim(),
|
||||
{
|
||||
name: boardName.trim(),
|
||||
floorId: boardFloorId || null,
|
||||
supplyType: boardSupplyType,
|
||||
},
|
||||
project.currentRevision
|
||||
);
|
||||
setBoards((current) => [
|
||||
@@ -185,6 +220,10 @@ export default function ProjectDetailPage() {
|
||||
setCircuitLists(await listCircuitLists(projectId));
|
||||
applyProjectRevision(result.history.currentRevision);
|
||||
setBoardName("");
|
||||
setBoardFloorId("");
|
||||
setBoardSupplyType(
|
||||
project.enabledDistributionBoardSupplyTypes[0]
|
||||
);
|
||||
setStructureModal(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Verteilung konnte nicht erstellt werden.");
|
||||
@@ -351,6 +390,60 @@ export default function ProjectDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function openBoardEditor(board: DistributionBoardDto) {
|
||||
setEditingBoard(board);
|
||||
setEditingBoardFloorId(board.floorId ?? "");
|
||||
setEditingBoardSupplyType(
|
||||
board.supplyType ??
|
||||
project?.enabledDistributionBoardSupplyTypes[0] ??
|
||||
"AV"
|
||||
);
|
||||
}
|
||||
|
||||
function openBoardCreator() {
|
||||
setBoardSupplyType(
|
||||
project?.enabledDistributionBoardSupplyTypes[0] ?? "AV"
|
||||
);
|
||||
setStructureModal("board");
|
||||
}
|
||||
|
||||
async function handleUpdateBoard(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!projectId || !project || !editingBoard) {
|
||||
return;
|
||||
}
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await updateDistributionBoard(
|
||||
projectId,
|
||||
editingBoard.id,
|
||||
{
|
||||
floorId: editingBoardFloorId || null,
|
||||
supplyType: editingBoardSupplyType,
|
||||
},
|
||||
project.currentRevision
|
||||
);
|
||||
setBoards((current) =>
|
||||
current.map((board) =>
|
||||
board.id === result.distributionBoard.id
|
||||
? result.distributionBoard
|
||||
: board
|
||||
)
|
||||
);
|
||||
applyProjectRevision(result.history.currentRevision);
|
||||
setEditingBoard(null);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "Verteilung konnte nicht bearbeitet werden."
|
||||
);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExportProject() {
|
||||
if (!project) {
|
||||
return;
|
||||
@@ -596,7 +689,7 @@ export default function ProjectDetailPage() {
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={() => setStructureModal("board")}
|
||||
onClick={openBoardCreator}
|
||||
type="button"
|
||||
>
|
||||
Verteilung hinzufügen
|
||||
@@ -607,6 +700,8 @@ export default function ProjectDetailPage() {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Verteilung</th>
|
||||
<th>Etage</th>
|
||||
<th>Netzart</th>
|
||||
<th className="text-end">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -616,8 +711,28 @@ export default function ProjectDetailPage() {
|
||||
return (
|
||||
<tr key={board.id}>
|
||||
<td>{board.name}</td>
|
||||
<td>
|
||||
{board.floorId
|
||||
? floorById.get(board.floorId)?.name ?? "Unbekannt"
|
||||
: "Ohne Etage"}
|
||||
</td>
|
||||
<td>
|
||||
{board.supplyType
|
||||
? distributionBoardSupplyTypeLabels[
|
||||
board.supplyType
|
||||
]
|
||||
: "Nicht festgelegt"}
|
||||
</td>
|
||||
<td className="text-end">
|
||||
<div className="d-inline-flex gap-2">
|
||||
<div className="d-inline-flex flex-wrap justify-content-end gap-2">
|
||||
<button
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
disabled={isSaving}
|
||||
onClick={() => openBoardEditor(board)}
|
||||
type="button"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
{circuitList ? (
|
||||
<Link
|
||||
className="btn btn-sm btn-primary"
|
||||
@@ -637,7 +752,7 @@ export default function ProjectDetailPage() {
|
||||
})}
|
||||
{!boards.length ? (
|
||||
<tr>
|
||||
<td colSpan={2} className="text-center text-secondary py-4">
|
||||
<td colSpan={4} className="text-center text-secondary py-4">
|
||||
Noch keine Verteilungen vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -967,6 +1082,112 @@ export default function ProjectDetailPage() {
|
||||
required
|
||||
value={boardName}
|
||||
/>
|
||||
<div className="row g-3 mt-0">
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="distribution-board-floor">
|
||||
Etage
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="distribution-board-floor"
|
||||
onChange={(event) => setBoardFloorId(event.target.value)}
|
||||
value={boardFloorId}
|
||||
>
|
||||
<option value="">Ohne Etage</option>
|
||||
{floors.map((floor) => (
|
||||
<option key={floor.id} value={floor.id}>
|
||||
{floor.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<label
|
||||
className="form-label"
|
||||
htmlFor="distribution-board-supply-type"
|
||||
>
|
||||
Netzart
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="distribution-board-supply-type"
|
||||
onChange={(event) =>
|
||||
setBoardSupplyType(
|
||||
event.target.value as DistributionBoardSupplyType
|
||||
)
|
||||
}
|
||||
value={boardSupplyType}
|
||||
>
|
||||
{enabledBoardSupplyTypes.map((supplyType) => (
|
||||
<option key={supplyType} value={supplyType}>
|
||||
{distributionBoardSupplyTypeLabels[supplyType]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</FormModal>
|
||||
) : null}
|
||||
|
||||
{editingBoard ? (
|
||||
<FormModal
|
||||
description={`${editingBoard.name}: Etagenzuordnung und Netzart ändern.`}
|
||||
isSaving={isSaving}
|
||||
onClose={() => setEditingBoard(null)}
|
||||
onSubmit={handleUpdateBoard}
|
||||
submitLabel="Änderungen speichern"
|
||||
title="Verteilung bearbeiten"
|
||||
>
|
||||
<div className="row g-3">
|
||||
<div className="col-12 col-md-6">
|
||||
<label
|
||||
className="form-label"
|
||||
htmlFor="edit-distribution-board-floor"
|
||||
>
|
||||
Etage
|
||||
</label>
|
||||
<select
|
||||
autoFocus
|
||||
className="form-select"
|
||||
id="edit-distribution-board-floor"
|
||||
onChange={(event) =>
|
||||
setEditingBoardFloorId(event.target.value)
|
||||
}
|
||||
value={editingBoardFloorId}
|
||||
>
|
||||
<option value="">Ohne Etage</option>
|
||||
{floors.map((floor) => (
|
||||
<option key={floor.id} value={floor.id}>
|
||||
{floor.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<label
|
||||
className="form-label"
|
||||
htmlFor="edit-distribution-board-supply-type"
|
||||
>
|
||||
Netzart
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="edit-distribution-board-supply-type"
|
||||
onChange={(event) =>
|
||||
setEditingBoardSupplyType(
|
||||
event.target.value as DistributionBoardSupplyType
|
||||
)
|
||||
}
|
||||
value={editingBoardSupplyType}
|
||||
>
|
||||
{enabledBoardSupplyTypes.map((supplyType) => (
|
||||
<option key={supplyType} value={supplyType}>
|
||||
{distributionBoardSupplyTypeLabels[supplyType]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</FormModal>
|
||||
) : null}
|
||||
|
||||
@@ -1082,6 +1303,7 @@ export default function ProjectDetailPage() {
|
||||
onImport={handleImportProject}
|
||||
onSave={handleSaveProjectSettings}
|
||||
project={project}
|
||||
usedDistributionBoardSupplyTypes={usedBoardSupplyTypes}
|
||||
/>
|
||||
) : null}
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `distribution_boards` ADD `floor_id` text REFERENCES floors(id);--> statement-breakpoint
|
||||
ALTER TABLE `distribution_boards` ADD `supply_type` text;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `projects` ADD `enabled_distribution_board_supply_types` text DEFAULT '["AV","SV","EV","USV","MSR","SiBe"]' NOT NULL;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -120,6 +120,20 @@
|
||||
"when": 1785254079435,
|
||||
"tag": "0016_dashing_darkstar",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 17,
|
||||
"version": "6",
|
||||
"when": 1785307189539,
|
||||
"tag": "0017_vengeful_romulus",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 18,
|
||||
"version": "6",
|
||||
"when": 1785308458789,
|
||||
"tag": "0018_fancy_argent",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,9 +6,19 @@ import {
|
||||
createDistributionBoardInsertProjectCommand,
|
||||
distributionBoardDeleteCommandType,
|
||||
distributionBoardInsertCommandType,
|
||||
normalizeDistributionBoardStructureProjectCommand,
|
||||
type DistributionBoardStructureProjectCommand,
|
||||
type DistributionBoardStructureSnapshot,
|
||||
legacyDistributionBoardStructureCommandSchemaVersion,
|
||||
} from "../../domain/models/distribution-board-structure-project-command.model.js";
|
||||
import {
|
||||
assertDistributionBoardUpdateProjectCommand,
|
||||
createDistributionBoardUpdateProjectCommand,
|
||||
type DistributionBoardUpdateField,
|
||||
type DistributionBoardUpdatePatch,
|
||||
type DistributionBoardUpdateProjectCommand,
|
||||
type DistributionBoardUpdateValues,
|
||||
} from "../../domain/models/distribution-board-project-command.model.js";
|
||||
import type {
|
||||
DistributionBoardStructureProjectCommandStore,
|
||||
ExecuteDistributionBoardStructureCommandInput,
|
||||
@@ -18,6 +28,7 @@ import { circuitLists } from "../schema/circuit-lists.js";
|
||||
import { circuitSections } from "../schema/circuit-sections.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||
import { floors } from "../schema/floors.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
|
||||
@@ -35,26 +46,52 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||
);
|
||||
}
|
||||
|
||||
executeUpdate(
|
||||
input: Omit<
|
||||
ExecuteDistributionBoardStructureCommandInput,
|
||||
"command"
|
||||
> & {
|
||||
command: DistributionBoardUpdateProjectCommand;
|
||||
}
|
||||
) {
|
||||
assertDistributionBoardUpdateProjectCommand(input.command);
|
||||
return executeProjectCommandTransaction(
|
||||
this.database,
|
||||
input,
|
||||
(tx) => this.update(tx, input)
|
||||
);
|
||||
}
|
||||
|
||||
private applyCommand(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
command: DistributionBoardStructureProjectCommand
|
||||
): DistributionBoardStructureProjectCommand {
|
||||
if (command.type === distributionBoardInsertCommandType) {
|
||||
assertDistributionBoardInsertProjectCommand(command);
|
||||
return this.insert(
|
||||
const normalized =
|
||||
normalizeDistributionBoardStructureProjectCommand(command);
|
||||
if (normalized.type === distributionBoardInsertCommandType) {
|
||||
assertDistributionBoardInsertProjectCommand(normalized);
|
||||
const inverse = this.insert(
|
||||
database,
|
||||
projectId,
|
||||
command.payload.structure
|
||||
normalized.payload.structure
|
||||
);
|
||||
return command.schemaVersion ===
|
||||
legacyDistributionBoardStructureCommandSchemaVersion
|
||||
? toLegacyStructureCommand(inverse)
|
||||
: inverse;
|
||||
}
|
||||
if (command.type === distributionBoardDeleteCommandType) {
|
||||
assertDistributionBoardDeleteProjectCommand(command);
|
||||
return this.delete(
|
||||
if (normalized.type === distributionBoardDeleteCommandType) {
|
||||
assertDistributionBoardDeleteProjectCommand(normalized);
|
||||
const inverse = this.delete(
|
||||
database,
|
||||
projectId,
|
||||
command.payload.structure
|
||||
normalized.payload.structure
|
||||
);
|
||||
return command.schemaVersion ===
|
||||
legacyDistributionBoardStructureCommandSchemaVersion
|
||||
? toLegacyStructureCommand(inverse)
|
||||
: inverse;
|
||||
}
|
||||
throw new Error("Unsupported distribution-board structure command.");
|
||||
}
|
||||
@@ -73,13 +110,33 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||
);
|
||||
}
|
||||
const project = database
|
||||
.select({ id: projects.id })
|
||||
.select({
|
||||
id: projects.id,
|
||||
enabledSupplyTypes:
|
||||
projects.enabledDistributionBoardSupplyTypes,
|
||||
})
|
||||
.from(projects)
|
||||
.where(eq(projects.id, projectId))
|
||||
.get();
|
||||
if (!project) {
|
||||
throw new Error("Project not found.");
|
||||
}
|
||||
assertSupplyTypeEnabled(
|
||||
project.enabledSupplyTypes,
|
||||
structure.distributionBoard.supplyType
|
||||
);
|
||||
if (structure.distributionBoard.floorId !== null) {
|
||||
const floor = database
|
||||
.select({ projectId: floors.projectId })
|
||||
.from(floors)
|
||||
.where(eq(floors.id, structure.distributionBoard.floorId))
|
||||
.get();
|
||||
if (!floor || floor.projectId !== projectId) {
|
||||
throw new Error(
|
||||
"Distribution-board floor does not belong to project."
|
||||
);
|
||||
}
|
||||
}
|
||||
const existingBoard = database
|
||||
.select({ id: distributionBoards.id })
|
||||
.from(distributionBoards)
|
||||
@@ -179,6 +236,139 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||
}
|
||||
return createDistributionBoardInsertProjectCommand(structure);
|
||||
}
|
||||
|
||||
private update(
|
||||
database: AppDatabase,
|
||||
input: Omit<
|
||||
ExecuteDistributionBoardStructureCommandInput,
|
||||
"command"
|
||||
> & {
|
||||
command: DistributionBoardUpdateProjectCommand;
|
||||
}
|
||||
) {
|
||||
const current = database
|
||||
.select()
|
||||
.from(distributionBoards)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
distributionBoards.id,
|
||||
input.command.payload.distributionBoardId
|
||||
),
|
||||
eq(distributionBoards.projectId, input.projectId)
|
||||
)
|
||||
)
|
||||
.get();
|
||||
if (!current) {
|
||||
throw new Error("Distribution board does not belong to project.");
|
||||
}
|
||||
const project = database
|
||||
.select({
|
||||
enabledSupplyTypes:
|
||||
projects.enabledDistributionBoardSupplyTypes,
|
||||
})
|
||||
.from(projects)
|
||||
.where(eq(projects.id, input.projectId))
|
||||
.get();
|
||||
if (!project) {
|
||||
throw new Error("Project not found.");
|
||||
}
|
||||
const patch = Object.fromEntries(
|
||||
input.command.payload.changes.map((change) => [
|
||||
change.field,
|
||||
change.value,
|
||||
])
|
||||
) as DistributionBoardUpdatePatch;
|
||||
if (patch.floorId !== undefined && patch.floorId !== null) {
|
||||
const floor = database
|
||||
.select({ projectId: floors.projectId })
|
||||
.from(floors)
|
||||
.where(eq(floors.id, patch.floorId))
|
||||
.get();
|
||||
if (!floor || floor.projectId !== input.projectId) {
|
||||
throw new Error(
|
||||
"Distribution-board floor does not belong to project."
|
||||
);
|
||||
}
|
||||
}
|
||||
if (patch.supplyType !== undefined) {
|
||||
assertSupplyTypeEnabled(
|
||||
project.enabledSupplyTypes,
|
||||
patch.supplyType
|
||||
);
|
||||
}
|
||||
const inversePatch = Object.fromEntries(
|
||||
input.command.payload.changes.map((change) => [
|
||||
change.field,
|
||||
getDistributionBoardFieldValue(current, change.field),
|
||||
])
|
||||
) as DistributionBoardUpdatePatch;
|
||||
const inverse = createDistributionBoardUpdateProjectCommand(
|
||||
current.id,
|
||||
inversePatch
|
||||
);
|
||||
const updated = database
|
||||
.update(distributionBoards)
|
||||
.set(patch)
|
||||
.where(
|
||||
and(
|
||||
eq(distributionBoards.id, current.id),
|
||||
eq(distributionBoards.projectId, input.projectId)
|
||||
)
|
||||
)
|
||||
.run();
|
||||
if (updated.changes !== 1) {
|
||||
throw new Error(
|
||||
"Distribution board changed before command execution."
|
||||
);
|
||||
}
|
||||
return inverse;
|
||||
}
|
||||
}
|
||||
|
||||
function assertSupplyTypeEnabled(
|
||||
enabledSupplyTypes: (typeof projects.$inferSelect)["enabledDistributionBoardSupplyTypes"],
|
||||
supplyType: (typeof distributionBoards.$inferSelect)["supplyType"]
|
||||
) {
|
||||
if (
|
||||
supplyType !== null &&
|
||||
!enabledSupplyTypes.includes(supplyType)
|
||||
) {
|
||||
throw new Error(
|
||||
`Die Netzart ${supplyType} ist in den Projekteinstellungen nicht aktiviert.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function toLegacyStructureCommand(
|
||||
command:
|
||||
| ReturnType<typeof createDistributionBoardInsertProjectCommand>
|
||||
| ReturnType<typeof createDistributionBoardDeleteProjectCommand>
|
||||
): DistributionBoardStructureProjectCommand {
|
||||
const {
|
||||
floorId: _floorId,
|
||||
supplyType: _supplyType,
|
||||
...distributionBoard
|
||||
} = command.payload.structure.distributionBoard;
|
||||
return {
|
||||
schemaVersion: legacyDistributionBoardStructureCommandSchemaVersion,
|
||||
type: command.type,
|
||||
payload: {
|
||||
structure: {
|
||||
...command.payload.structure,
|
||||
distributionBoard,
|
||||
},
|
||||
},
|
||||
} as DistributionBoardStructureProjectCommand;
|
||||
}
|
||||
|
||||
function getDistributionBoardFieldValue<
|
||||
TField extends DistributionBoardUpdateField,
|
||||
>(
|
||||
distributionBoard: typeof distributionBoards.$inferSelect,
|
||||
field: TField
|
||||
): DistributionBoardUpdateValues[TField] {
|
||||
return distributionBoard[field] as DistributionBoardUpdateValues[TField];
|
||||
}
|
||||
|
||||
function structureMatches(
|
||||
|
||||
@@ -24,6 +24,7 @@ import type { AppDatabase } from "../database-context.js";
|
||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||
import { consumers } from "../schema/consumers.js";
|
||||
import { floors } from "../schema/floors.js";
|
||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { rooms } from "../schema/rooms.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
@@ -129,6 +130,17 @@ export class ProjectLocationStructureProjectCommandRepository
|
||||
"A project floor with assigned rooms cannot be removed by history."
|
||||
);
|
||||
}
|
||||
const referencedDistributionBoard = database
|
||||
.select({ id: distributionBoards.id })
|
||||
.from(distributionBoards)
|
||||
.where(eq(distributionBoards.floorId, expected.id))
|
||||
.limit(1)
|
||||
.get();
|
||||
if (referencedDistributionBoard) {
|
||||
throw new Error(
|
||||
"A project floor assigned to a distribution board cannot be removed by history."
|
||||
);
|
||||
}
|
||||
const deleted = database
|
||||
.delete(floors)
|
||||
.where(
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq, isNotNull } from "drizzle-orm";
|
||||
import {
|
||||
assertProjectSettingsUpdateProjectCommand,
|
||||
createProjectSettingsUpdateProjectCommand,
|
||||
legacyProjectSettingsUpdateCommandSchemaVersion,
|
||||
normalizeProjectSettingsValues,
|
||||
previousProjectSettingsUpdateCommandSchemaVersion,
|
||||
type ProjectSettingsValues,
|
||||
} from "../../domain/models/project-settings-project-command.model.js";
|
||||
import type {
|
||||
@@ -11,6 +13,7 @@ import type {
|
||||
} from "../../domain/ports/project-settings-project-command.store.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
|
||||
export class ProjectSettingsProjectCommandRepository
|
||||
@@ -39,18 +42,7 @@ export class ProjectSettingsProjectCommandRepository
|
||||
if (!current) {
|
||||
throw new Error("Project not found.");
|
||||
}
|
||||
const target: ProjectSettingsValues =
|
||||
input.command.schemaVersion ===
|
||||
legacyProjectSettingsUpdateCommandSchemaVersion
|
||||
? {
|
||||
name: current.name,
|
||||
internalProjectNumber: current.internalProjectNumber,
|
||||
externalProjectNumber: current.externalProjectNumber,
|
||||
buildingOwner: current.buildingOwner,
|
||||
description: current.description,
|
||||
...input.command.payload,
|
||||
}
|
||||
: input.command.payload;
|
||||
const target = normalizeProjectSettingsValues(input.command, current);
|
||||
if (projectSettingsEqual(current, target)) {
|
||||
throw new Error("Project settings did not change.");
|
||||
}
|
||||
@@ -66,7 +58,23 @@ export class ProjectSettingsProjectCommandRepository
|
||||
threePhaseVoltageV: current.threePhaseVoltageV,
|
||||
},
|
||||
}
|
||||
: createProjectSettingsUpdateProjectCommand({
|
||||
: input.command.schemaVersion ===
|
||||
previousProjectSettingsUpdateCommandSchemaVersion
|
||||
? {
|
||||
schemaVersion:
|
||||
previousProjectSettingsUpdateCommandSchemaVersion,
|
||||
type: input.command.type,
|
||||
payload: {
|
||||
name: current.name,
|
||||
internalProjectNumber: current.internalProjectNumber,
|
||||
externalProjectNumber: current.externalProjectNumber,
|
||||
buildingOwner: current.buildingOwner,
|
||||
description: current.description,
|
||||
singlePhaseVoltageV: current.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: current.threePhaseVoltageV,
|
||||
},
|
||||
}
|
||||
: createProjectSettingsUpdateProjectCommand({
|
||||
name: current.name,
|
||||
internalProjectNumber: current.internalProjectNumber,
|
||||
externalProjectNumber: current.externalProjectNumber,
|
||||
@@ -74,7 +82,10 @@ export class ProjectSettingsProjectCommandRepository
|
||||
description: current.description,
|
||||
singlePhaseVoltageV: current.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: current.threePhaseVoltageV,
|
||||
enabledDistributionBoardSupplyTypes:
|
||||
current.enabledDistributionBoardSupplyTypes,
|
||||
});
|
||||
assertDisabledSupplyTypesAreUnused(tx, input.projectId, target);
|
||||
const updated = tx
|
||||
.update(projects)
|
||||
.set(target)
|
||||
@@ -100,5 +111,54 @@ function projectSettingsEqual(
|
||||
current.description === target.description &&
|
||||
current.singlePhaseVoltageV === target.singlePhaseVoltageV &&
|
||||
current.threePhaseVoltageV === target.threePhaseVoltageV
|
||||
&& sameSupplyTypes(
|
||||
current.enabledDistributionBoardSupplyTypes,
|
||||
target.enabledDistributionBoardSupplyTypes
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function sameSupplyTypes(
|
||||
left: ProjectSettingsValues["enabledDistributionBoardSupplyTypes"],
|
||||
right: ProjectSettingsValues["enabledDistributionBoardSupplyTypes"]
|
||||
) {
|
||||
return (
|
||||
left.length === right.length &&
|
||||
left.every((value, index) => value === right[index])
|
||||
);
|
||||
}
|
||||
|
||||
function assertDisabledSupplyTypesAreUnused(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
target: ProjectSettingsValues
|
||||
) {
|
||||
const used = database
|
||||
.select({ supplyType: distributionBoards.supplyType })
|
||||
.from(distributionBoards)
|
||||
.where(
|
||||
and(
|
||||
eq(distributionBoards.projectId, projectId),
|
||||
isNotNull(distributionBoards.supplyType)
|
||||
)
|
||||
)
|
||||
.all();
|
||||
const disabledUsedTypes = [
|
||||
...new Set(
|
||||
used
|
||||
.map((entry) => entry.supplyType)
|
||||
.filter(
|
||||
(supplyType) =>
|
||||
supplyType !== null &&
|
||||
!target.enabledDistributionBoardSupplyTypes.some(
|
||||
(enabled) => enabled === supplyType
|
||||
)
|
||||
)
|
||||
),
|
||||
];
|
||||
if (disabledUsedTypes.length > 0) {
|
||||
throw new Error(
|
||||
`Verwendete Netzarten können nicht deaktiviert werden: ${disabledUsedTypes.join(", ")}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import { ProjectRevisionConflictError } from "../../domain/errors/project-revisi
|
||||
import { ProjectSnapshotNameConflictError } from "../../domain/errors/project-snapshot-name-conflict.error.js";
|
||||
import { createProjectStateRestoreCommand } from "../../domain/models/project-state-restore-command.model.js";
|
||||
import {
|
||||
distributionBoardProjectStateSnapshotSchemaVersion,
|
||||
deserializeProjectStateSnapshot,
|
||||
legacyProjectStateSnapshotSchemaVersion,
|
||||
previousProjectStateSnapshotSchemaVersion,
|
||||
projectStateSnapshotSchemaVersion,
|
||||
} from "../../domain/models/project-state-snapshot.model.js";
|
||||
import type {
|
||||
@@ -141,6 +143,9 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
|
||||
}
|
||||
if (
|
||||
stored.schemaVersion !== legacyProjectStateSnapshotSchemaVersion &&
|
||||
stored.schemaVersion !== previousProjectStateSnapshotSchemaVersion &&
|
||||
stored.schemaVersion !==
|
||||
distributionBoardProjectStateSnapshotSchemaVersion &&
|
||||
stored.schemaVersion !== projectStateSnapshotSchemaVersion
|
||||
) {
|
||||
throw new Error("Project snapshot schema version is not supported.");
|
||||
|
||||
@@ -194,6 +194,8 @@ function replaceProjectState(
|
||||
description: state.project.description,
|
||||
singlePhaseVoltageV: state.project.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: state.project.threePhaseVoltageV,
|
||||
enabledDistributionBoardSupplyTypes:
|
||||
state.project.enabledDistributionBoardSupplyTypes,
|
||||
})
|
||||
.where(eq(projects.id, state.project.id))
|
||||
.run();
|
||||
|
||||
@@ -38,6 +38,8 @@ export function readProjectStateSnapshot(
|
||||
description: projects.description,
|
||||
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: projects.threePhaseVoltageV,
|
||||
enabledDistributionBoardSupplyTypes:
|
||||
projects.enabledDistributionBoardSupplyTypes,
|
||||
currentRevision: projects.currentRevision,
|
||||
})
|
||||
.from(projects)
|
||||
@@ -182,6 +184,8 @@ export function readProjectStateSnapshot(
|
||||
description: project.description,
|
||||
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||
enabledDistributionBoardSupplyTypes:
|
||||
project.enabledDistributionBoardSupplyTypes,
|
||||
},
|
||||
distributionBoards: boardRows,
|
||||
circuitLists: listRows,
|
||||
|
||||
@@ -74,12 +74,18 @@ export class ProjectTransferRepository implements ProjectTransferStore {
|
||||
}
|
||||
|
||||
private parseVerified(value: unknown) {
|
||||
const rawPayloadSha256 =
|
||||
isPlainObject(value) &&
|
||||
isPlainObject(value.projectState)
|
||||
? hashProjectStatePayload(JSON.stringify(value.projectState))
|
||||
: null;
|
||||
const transfer = parseProjectTransferEnvelope(value);
|
||||
const payloadJson = serializeProjectStateSnapshot(
|
||||
transfer.projectState
|
||||
);
|
||||
if (
|
||||
hashProjectStatePayload(payloadJson) !== transfer.payloadSha256
|
||||
hashProjectStatePayload(payloadJson) !== transfer.payloadSha256 &&
|
||||
rawPayloadSha256 !== transfer.payloadSha256
|
||||
) {
|
||||
throw new Error("Project transfer checksum verification failed.");
|
||||
}
|
||||
@@ -87,6 +93,10 @@ export class ProjectTransferRepository implements ProjectTransferStore {
|
||||
}
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function insertProjectState(
|
||||
database: AppDatabase,
|
||||
state: ReturnType<typeof remapProjectState>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { projects } from "../schema/projects.js";
|
||||
import type {
|
||||
CreateProjectInput,
|
||||
} from "../../shared/validation/project-structure.schemas.js";
|
||||
import { defaultDistributionBoardSupplyTypes } from "../../shared/constants/distribution-board.js";
|
||||
|
||||
export class ProjectRepository {
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
@@ -26,6 +27,9 @@ export class ProjectRepository {
|
||||
description: input.description ?? null,
|
||||
singlePhaseVoltageV: input.singlePhaseVoltageV ?? 230,
|
||||
threePhaseVoltageV: input.threePhaseVoltageV ?? 400,
|
||||
enabledDistributionBoardSupplyTypes: [
|
||||
...defaultDistributionBoardSupplyTypes,
|
||||
],
|
||||
currentRevision: 0,
|
||||
};
|
||||
await db.insert(projects).values(project);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
import { floors } from "./floors.js";
|
||||
import { projects } from "./projects.js";
|
||||
import type { DistributionBoardSupplyType } from "../../shared/constants/distribution-board.js";
|
||||
|
||||
export const distributionBoards = sqliteTable("distribution_boards", {
|
||||
id: text("id").primaryKey(),
|
||||
@@ -7,5 +9,9 @@ export const distributionBoards = sqliteTable("distribution_boards", {
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
floorId: text("floor_id").references(() => floors.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
supplyType: text("supply_type").$type<DistributionBoardSupplyType>(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
import type { DistributionBoardSupplyType } from "../../shared/constants/distribution-board.js";
|
||||
|
||||
export const projects = sqliteTable("projects", {
|
||||
id: text("id").primaryKey(),
|
||||
@@ -9,5 +11,12 @@ export const projects = sqliteTable("projects", {
|
||||
description: text("description"),
|
||||
singlePhaseVoltageV: integer("single_phase_voltage_v").notNull().default(230),
|
||||
threePhaseVoltageV: integer("three_phase_voltage_v").notNull().default(400),
|
||||
enabledDistributionBoardSupplyTypes: text(
|
||||
"enabled_distribution_board_supply_types",
|
||||
{ mode: "json" }
|
||||
)
|
||||
.$type<DistributionBoardSupplyType[]>()
|
||||
.notNull()
|
||||
.default(sql`'["AV","SV","EV","USV","MSR","SiBe"]'`),
|
||||
currentRevision: integer("current_revision").notNull().default(0),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
distributionBoardSupplyTypes,
|
||||
type DistributionBoardSupplyType,
|
||||
} from "../../shared/constants/distribution-board.js";
|
||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||
|
||||
export const distributionBoardUpdateCommandType =
|
||||
"distribution-board.update" as const;
|
||||
export const distributionBoardUpdateCommandSchemaVersion = 1 as const;
|
||||
|
||||
export interface DistributionBoardUpdateValues {
|
||||
floorId: string | null;
|
||||
supplyType: DistributionBoardSupplyType | null;
|
||||
}
|
||||
|
||||
export type DistributionBoardUpdateField =
|
||||
keyof DistributionBoardUpdateValues;
|
||||
export type DistributionBoardUpdatePatch =
|
||||
Partial<DistributionBoardUpdateValues>;
|
||||
|
||||
export interface DistributionBoardUpdateFieldChange<
|
||||
TField extends DistributionBoardUpdateField =
|
||||
DistributionBoardUpdateField,
|
||||
> {
|
||||
field: TField;
|
||||
value: DistributionBoardUpdateValues[TField];
|
||||
}
|
||||
|
||||
export interface DistributionBoardUpdateCommandPayload {
|
||||
distributionBoardId: string;
|
||||
changes: DistributionBoardUpdateFieldChange[];
|
||||
}
|
||||
|
||||
export interface DistributionBoardUpdateProjectCommand
|
||||
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
|
||||
schemaVersion: typeof distributionBoardUpdateCommandSchemaVersion;
|
||||
type: typeof distributionBoardUpdateCommandType;
|
||||
}
|
||||
|
||||
export function createDistributionBoardUpdateProjectCommand(
|
||||
distributionBoardId: string,
|
||||
patch: DistributionBoardUpdatePatch
|
||||
): DistributionBoardUpdateProjectCommand {
|
||||
const command: DistributionBoardUpdateProjectCommand = {
|
||||
schemaVersion: distributionBoardUpdateCommandSchemaVersion,
|
||||
type: distributionBoardUpdateCommandType,
|
||||
payload: {
|
||||
distributionBoardId,
|
||||
changes: Object.entries(patch).map(([field, value]) => ({
|
||||
field: field as DistributionBoardUpdateField,
|
||||
value,
|
||||
})) as DistributionBoardUpdateFieldChange[],
|
||||
},
|
||||
};
|
||||
assertDistributionBoardUpdateProjectCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function assertDistributionBoardUpdateProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is DistributionBoardUpdateProjectCommand {
|
||||
if (
|
||||
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion ||
|
||||
command.type !== distributionBoardUpdateCommandType ||
|
||||
!isPlainObject(command.payload)
|
||||
) {
|
||||
throw new Error("Unsupported distribution-board update command.");
|
||||
}
|
||||
const { distributionBoardId, changes } = command.payload;
|
||||
if (
|
||||
typeof distributionBoardId !== "string" ||
|
||||
!distributionBoardId.trim() ||
|
||||
!Array.isArray(changes) ||
|
||||
changes.length === 0
|
||||
) {
|
||||
throw new Error(
|
||||
"Distribution-board update command requires an id and changes."
|
||||
);
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
for (const change of changes) {
|
||||
if (
|
||||
!isPlainObject(change) ||
|
||||
(change.field !== "floorId" && change.field !== "supplyType") ||
|
||||
seen.has(change.field)
|
||||
) {
|
||||
throw new Error(
|
||||
"Distribution-board update contains an invalid or duplicate field."
|
||||
);
|
||||
}
|
||||
if (change.field === "floorId") {
|
||||
if (
|
||||
change.value !== null &&
|
||||
(typeof change.value !== "string" || !change.value.trim())
|
||||
) {
|
||||
throw new Error("floorId must be a non-empty string or null.");
|
||||
}
|
||||
} else if (
|
||||
change.value !== null &&
|
||||
!distributionBoardSupplyTypes.includes(
|
||||
change.value as DistributionBoardSupplyType
|
||||
)
|
||||
) {
|
||||
throw new Error("supplyType is invalid.");
|
||||
}
|
||||
seen.add(change.field);
|
||||
}
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import crypto from "node:crypto";
|
||||
import {
|
||||
distributionBoardSupplyTypes,
|
||||
type DistributionBoardSupplyType,
|
||||
} from "../../shared/constants/distribution-board.js";
|
||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||
|
||||
export const distributionBoardInsertCommandType =
|
||||
"distribution-board.insert" as const;
|
||||
export const distributionBoardDeleteCommandType =
|
||||
"distribution-board.delete" as const;
|
||||
export const distributionBoardStructureCommandSchemaVersion = 1 as const;
|
||||
export const legacyDistributionBoardStructureCommandSchemaVersion = 1 as const;
|
||||
export const distributionBoardStructureCommandSchemaVersion = 2 as const;
|
||||
|
||||
export const defaultCircuitSectionDefinitions = [
|
||||
{
|
||||
@@ -34,11 +39,23 @@ export const defaultCircuitSectionDefinitions = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface LegacyDistributionBoardStructureSnapshot {
|
||||
distributionBoard: {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
};
|
||||
circuitList: DistributionBoardStructureSnapshot["circuitList"];
|
||||
sections: DistributionBoardStructureSnapshot["sections"];
|
||||
}
|
||||
|
||||
export interface DistributionBoardStructureSnapshot {
|
||||
distributionBoard: {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
floorId: string | null;
|
||||
supplyType: DistributionBoardSupplyType | null;
|
||||
};
|
||||
circuitList: {
|
||||
id: string;
|
||||
@@ -56,29 +73,52 @@ export interface DistributionBoardStructureSnapshot {
|
||||
}>;
|
||||
}
|
||||
|
||||
interface DistributionBoardStructureCommandPayload {
|
||||
structure: DistributionBoardStructureSnapshot;
|
||||
interface DistributionBoardStructureCommandPayload<
|
||||
TStructure = DistributionBoardStructureSnapshot,
|
||||
> {
|
||||
structure: TStructure;
|
||||
}
|
||||
|
||||
interface CurrentDistributionBoardStructureProjectCommand
|
||||
extends SerializedProjectCommand<DistributionBoardStructureCommandPayload> {
|
||||
schemaVersion: typeof distributionBoardStructureCommandSchemaVersion;
|
||||
type:
|
||||
| typeof distributionBoardInsertCommandType
|
||||
| typeof distributionBoardDeleteCommandType;
|
||||
}
|
||||
|
||||
interface LegacyDistributionBoardStructureProjectCommand
|
||||
extends SerializedProjectCommand<
|
||||
DistributionBoardStructureCommandPayload<LegacyDistributionBoardStructureSnapshot>
|
||||
> {
|
||||
schemaVersion: typeof legacyDistributionBoardStructureCommandSchemaVersion;
|
||||
type:
|
||||
| typeof distributionBoardInsertCommandType
|
||||
| typeof distributionBoardDeleteCommandType;
|
||||
}
|
||||
|
||||
export interface DistributionBoardInsertProjectCommand
|
||||
extends SerializedProjectCommand<DistributionBoardStructureCommandPayload> {
|
||||
schemaVersion: typeof distributionBoardStructureCommandSchemaVersion;
|
||||
extends CurrentDistributionBoardStructureProjectCommand {
|
||||
type: typeof distributionBoardInsertCommandType;
|
||||
}
|
||||
|
||||
export interface DistributionBoardDeleteProjectCommand
|
||||
extends SerializedProjectCommand<DistributionBoardStructureCommandPayload> {
|
||||
schemaVersion: typeof distributionBoardStructureCommandSchemaVersion;
|
||||
extends CurrentDistributionBoardStructureProjectCommand {
|
||||
type: typeof distributionBoardDeleteCommandType;
|
||||
}
|
||||
|
||||
export type DistributionBoardStructureProjectCommand =
|
||||
| DistributionBoardInsertProjectCommand
|
||||
| DistributionBoardDeleteProjectCommand;
|
||||
| DistributionBoardDeleteProjectCommand
|
||||
| LegacyDistributionBoardStructureProjectCommand;
|
||||
|
||||
export function createDistributionBoardStructureSnapshot(
|
||||
projectId: string,
|
||||
name: string
|
||||
name: string,
|
||||
input: {
|
||||
floorId?: string | null;
|
||||
supplyType?: DistributionBoardSupplyType | null;
|
||||
} = {}
|
||||
): DistributionBoardStructureSnapshot {
|
||||
const normalizedName = name.trim();
|
||||
assertNonEmptyString(projectId, "projectId");
|
||||
@@ -89,6 +129,8 @@ export function createDistributionBoardStructureSnapshot(
|
||||
id: structureId,
|
||||
projectId,
|
||||
name: normalizedName,
|
||||
floorId: input.floorId ?? null,
|
||||
supplyType: input.supplyType ?? null,
|
||||
},
|
||||
circuitList: {
|
||||
id: structureId,
|
||||
@@ -132,7 +174,7 @@ export function createDistributionBoardDeleteProjectCommand(
|
||||
|
||||
export function assertDistributionBoardInsertProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is DistributionBoardInsertProjectCommand {
|
||||
): asserts command is DistributionBoardStructureProjectCommand {
|
||||
assertDistributionBoardStructureProjectCommand(
|
||||
command,
|
||||
distributionBoardInsertCommandType
|
||||
@@ -141,23 +183,88 @@ export function assertDistributionBoardInsertProjectCommand(
|
||||
|
||||
export function assertDistributionBoardDeleteProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is DistributionBoardDeleteProjectCommand {
|
||||
): asserts command is DistributionBoardStructureProjectCommand {
|
||||
assertDistributionBoardStructureProjectCommand(
|
||||
command,
|
||||
distributionBoardDeleteCommandType
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeDistributionBoardStructureProjectCommand(
|
||||
command: DistributionBoardStructureProjectCommand
|
||||
): DistributionBoardInsertProjectCommand | DistributionBoardDeleteProjectCommand {
|
||||
if (
|
||||
command.schemaVersion ===
|
||||
legacyDistributionBoardStructureCommandSchemaVersion
|
||||
) {
|
||||
const structure: DistributionBoardStructureSnapshot = {
|
||||
...command.payload.structure,
|
||||
distributionBoard: {
|
||||
...command.payload.structure.distributionBoard,
|
||||
floorId: null,
|
||||
supplyType: null,
|
||||
},
|
||||
};
|
||||
return command.type === distributionBoardInsertCommandType
|
||||
? createDistributionBoardInsertProjectCommand(structure)
|
||||
: createDistributionBoardDeleteProjectCommand(structure);
|
||||
}
|
||||
return command as
|
||||
| DistributionBoardInsertProjectCommand
|
||||
| DistributionBoardDeleteProjectCommand;
|
||||
}
|
||||
|
||||
export function assertDistributionBoardStructureSnapshot(
|
||||
structure: unknown
|
||||
): asserts structure is DistributionBoardStructureSnapshot {
|
||||
assertDistributionBoardStructureSnapshotVersion(structure, false);
|
||||
}
|
||||
|
||||
function assertDistributionBoardStructureProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>,
|
||||
expectedType:
|
||||
| typeof distributionBoardInsertCommandType
|
||||
| typeof distributionBoardDeleteCommandType
|
||||
) {
|
||||
if (
|
||||
command.type !== expectedType ||
|
||||
!isPlainObject(command.payload) ||
|
||||
Object.keys(command.payload).length !== 1
|
||||
) {
|
||||
throw new Error("Unsupported distribution-board structure command.");
|
||||
}
|
||||
if (
|
||||
command.schemaVersion ===
|
||||
legacyDistributionBoardStructureCommandSchemaVersion
|
||||
) {
|
||||
assertDistributionBoardStructureSnapshotVersion(
|
||||
command.payload.structure,
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
command.schemaVersion !== distributionBoardStructureCommandSchemaVersion
|
||||
) {
|
||||
throw new Error("Unsupported distribution-board structure command.");
|
||||
}
|
||||
assertDistributionBoardStructureSnapshotVersion(
|
||||
command.payload.structure,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
function assertDistributionBoardStructureSnapshotVersion(
|
||||
structure: unknown,
|
||||
legacy: boolean
|
||||
) {
|
||||
if (!isPlainObject(structure) || Object.keys(structure).length !== 3) {
|
||||
throw new Error("Distribution-board structure is invalid.");
|
||||
}
|
||||
const { distributionBoard, circuitList, sections } = structure;
|
||||
if (
|
||||
!isPlainObject(distributionBoard) ||
|
||||
Object.keys(distributionBoard).length !== 3 ||
|
||||
Object.keys(distributionBoard).length !== (legacy ? 3 : 5) ||
|
||||
!isPlainObject(circuitList) ||
|
||||
Object.keys(circuitList).length !== 4 ||
|
||||
!Array.isArray(sections) ||
|
||||
@@ -165,8 +272,15 @@ export function assertDistributionBoardStructureSnapshot(
|
||||
) {
|
||||
throw new Error("Distribution-board structure is incomplete.");
|
||||
}
|
||||
for (const [field, value] of Object.entries(distributionBoard)) {
|
||||
assertNonEmptyString(value, `distributionBoard.${field}`);
|
||||
for (const field of ["id", "projectId", "name"] as const) {
|
||||
assertNonEmptyString(
|
||||
distributionBoard[field],
|
||||
`distributionBoard.${field}`
|
||||
);
|
||||
}
|
||||
if (!legacy) {
|
||||
assertNullableId(distributionBoard.floorId, "distributionBoard.floorId");
|
||||
assertNullableSupplyType(distributionBoard.supplyType);
|
||||
}
|
||||
for (const [field, value] of Object.entries(circuitList)) {
|
||||
assertNonEmptyString(value, `circuitList.${field}`);
|
||||
@@ -174,22 +288,16 @@ export function assertDistributionBoardStructureSnapshot(
|
||||
if (
|
||||
circuitList.projectId !== distributionBoard.projectId ||
|
||||
circuitList.distributionBoardId !== distributionBoard.id ||
|
||||
circuitList.name !==
|
||||
`${distributionBoard.name} Stromkreisliste`
|
||||
circuitList.name !== `${distributionBoard.name} Stromkreisliste`
|
||||
) {
|
||||
throw new Error(
|
||||
"Distribution board and circuit list are inconsistent."
|
||||
);
|
||||
throw new Error("Distribution board and circuit list are inconsistent.");
|
||||
}
|
||||
|
||||
const sectionIds = new Set<string>();
|
||||
for (let index = 0; index < sections.length; index += 1) {
|
||||
const section = sections[index];
|
||||
const expected = defaultCircuitSectionDefinitions[index];
|
||||
if (
|
||||
!isPlainObject(section) ||
|
||||
Object.keys(section).length !== 6
|
||||
) {
|
||||
if (!isPlainObject(section) || Object.keys(section).length !== 6) {
|
||||
throw new Error("Default circuit section is invalid.");
|
||||
}
|
||||
assertNonEmptyString(section.id, "section.id");
|
||||
@@ -211,22 +319,23 @@ export function assertDistributionBoardStructureSnapshot(
|
||||
}
|
||||
}
|
||||
|
||||
function assertDistributionBoardStructureProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>,
|
||||
expectedType:
|
||||
| typeof distributionBoardInsertCommandType
|
||||
| typeof distributionBoardDeleteCommandType
|
||||
) {
|
||||
if (
|
||||
command.schemaVersion !==
|
||||
distributionBoardStructureCommandSchemaVersion ||
|
||||
command.type !== expectedType ||
|
||||
!isPlainObject(command.payload) ||
|
||||
Object.keys(command.payload).length !== 1
|
||||
) {
|
||||
throw new Error("Unsupported distribution-board structure command.");
|
||||
function assertNullableId(value: unknown, field: string) {
|
||||
if (value !== null) {
|
||||
assertNonEmptyString(value, field);
|
||||
}
|
||||
}
|
||||
|
||||
function assertNullableSupplyType(
|
||||
value: unknown
|
||||
): asserts value is DistributionBoardSupplyType | null {
|
||||
if (
|
||||
value !== null &&
|
||||
!distributionBoardSupplyTypes.includes(
|
||||
value as DistributionBoardSupplyType
|
||||
)
|
||||
) {
|
||||
throw new Error("distributionBoard.supplyType is invalid.");
|
||||
}
|
||||
assertDistributionBoardStructureSnapshot(command.payload.structure);
|
||||
}
|
||||
|
||||
function assertNonEmptyString(
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||
import {
|
||||
distributionBoardSupplyTypes,
|
||||
type DistributionBoardSupplyType,
|
||||
} from "../../shared/constants/distribution-board.js";
|
||||
|
||||
export const projectSettingsUpdateCommandType =
|
||||
"project.update-settings" as const;
|
||||
export const legacyProjectSettingsUpdateCommandSchemaVersion = 1 as const;
|
||||
export const projectSettingsUpdateCommandSchemaVersion = 2 as const;
|
||||
export const previousProjectSettingsUpdateCommandSchemaVersion = 2 as const;
|
||||
export const projectSettingsUpdateCommandSchemaVersion = 3 as const;
|
||||
|
||||
export interface LegacyProjectSettingsValues {
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
}
|
||||
|
||||
export interface ProjectSettingsValues extends LegacyProjectSettingsValues {
|
||||
export interface PreviousProjectSettingsValues extends LegacyProjectSettingsValues {
|
||||
name: string;
|
||||
internalProjectNumber: string | null;
|
||||
externalProjectNumber: string | null;
|
||||
@@ -18,6 +23,10 @@ export interface ProjectSettingsValues extends LegacyProjectSettingsValues {
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface ProjectSettingsValues extends PreviousProjectSettingsValues {
|
||||
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
|
||||
}
|
||||
|
||||
export interface LegacyProjectSettingsUpdateProjectCommand
|
||||
extends SerializedProjectCommand<LegacyProjectSettingsValues> {
|
||||
schemaVersion: typeof legacyProjectSettingsUpdateCommandSchemaVersion;
|
||||
@@ -30,8 +39,15 @@ export interface CurrentProjectSettingsUpdateProjectCommand
|
||||
type: typeof projectSettingsUpdateCommandType;
|
||||
}
|
||||
|
||||
export interface PreviousProjectSettingsUpdateProjectCommand
|
||||
extends SerializedProjectCommand<PreviousProjectSettingsValues> {
|
||||
schemaVersion: typeof previousProjectSettingsUpdateCommandSchemaVersion;
|
||||
type: typeof projectSettingsUpdateCommandType;
|
||||
}
|
||||
|
||||
export type ProjectSettingsUpdateProjectCommand =
|
||||
| LegacyProjectSettingsUpdateProjectCommand
|
||||
| PreviousProjectSettingsUpdateProjectCommand
|
||||
| CurrentProjectSettingsUpdateProjectCommand;
|
||||
|
||||
export function createProjectSettingsUpdateProjectCommand(
|
||||
@@ -53,6 +69,8 @@ export function assertProjectSettingsUpdateProjectCommand(
|
||||
command.type !== projectSettingsUpdateCommandType ||
|
||||
(command.schemaVersion !==
|
||||
legacyProjectSettingsUpdateCommandSchemaVersion &&
|
||||
command.schemaVersion !==
|
||||
previousProjectSettingsUpdateCommandSchemaVersion &&
|
||||
command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion)
|
||||
) {
|
||||
throw new Error("Unsupported project settings update command.");
|
||||
@@ -66,6 +84,10 @@ export function assertProjectSettingsUpdateProjectCommand(
|
||||
assertLegacyValues(command.payload);
|
||||
return;
|
||||
}
|
||||
const expectedKeyCount =
|
||||
command.schemaVersion === previousProjectSettingsUpdateCommandSchemaVersion
|
||||
? 7
|
||||
: 8;
|
||||
if (
|
||||
!isTrimmedString(command.payload.name, 1, 200) ||
|
||||
!isNullableTrimmedString(command.payload.internalProjectNumber, 100) ||
|
||||
@@ -74,12 +96,61 @@ export function assertProjectSettingsUpdateProjectCommand(
|
||||
!isNullableTrimmedString(command.payload.description, 2000) ||
|
||||
!isPositiveFiniteNumber(command.payload.singlePhaseVoltageV) ||
|
||||
!isPositiveFiniteNumber(command.payload.threePhaseVoltageV) ||
|
||||
Object.keys(command.payload).length !== 7
|
||||
Object.keys(command.payload).length !== expectedKeyCount
|
||||
) {
|
||||
throw new Error(
|
||||
"Project settings update command contains invalid values."
|
||||
);
|
||||
}
|
||||
if (
|
||||
command.schemaVersion === projectSettingsUpdateCommandSchemaVersion &&
|
||||
!isValidSupplyTypes(
|
||||
command.payload.enabledDistributionBoardSupplyTypes
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
"Project settings update command contains invalid supply types."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeProjectSettingsValues(
|
||||
command: ProjectSettingsUpdateProjectCommand,
|
||||
current: ProjectSettingsValues
|
||||
): ProjectSettingsValues {
|
||||
if (command.schemaVersion === legacyProjectSettingsUpdateCommandSchemaVersion) {
|
||||
return {
|
||||
...current,
|
||||
...command.payload,
|
||||
};
|
||||
}
|
||||
if (
|
||||
command.schemaVersion === previousProjectSettingsUpdateCommandSchemaVersion
|
||||
) {
|
||||
return {
|
||||
...command.payload,
|
||||
enabledDistributionBoardSupplyTypes:
|
||||
current.enabledDistributionBoardSupplyTypes,
|
||||
};
|
||||
}
|
||||
return command.payload;
|
||||
}
|
||||
|
||||
function isValidSupplyTypes(
|
||||
value: unknown
|
||||
): value is DistributionBoardSupplyType[] {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.length > 0 &&
|
||||
new Set(value).size === value.length &&
|
||||
value.every(
|
||||
(entry) =>
|
||||
typeof entry === "string" &&
|
||||
distributionBoardSupplyTypes.includes(
|
||||
entry as DistributionBoardSupplyType
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function assertLegacyValues(
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
defaultDistributionBoardSupplyTypes,
|
||||
distributionBoardSupplyTypes,
|
||||
} from "../../shared/constants/distribution-board.js";
|
||||
|
||||
export const legacyProjectStateSnapshotSchemaVersion = 1 as const;
|
||||
export const projectStateSnapshotSchemaVersion = 2 as const;
|
||||
export const previousProjectStateSnapshotSchemaVersion = 2 as const;
|
||||
export const distributionBoardProjectStateSnapshotSchemaVersion = 3 as const;
|
||||
export const projectStateSnapshotSchemaVersion = 4 as const;
|
||||
|
||||
const idSchema = z.string().trim().min(1);
|
||||
const nullableStringSchema = z.string().nullable();
|
||||
@@ -23,7 +29,14 @@ const projectSchema = legacyProjectSchema.extend({
|
||||
description: z.string().trim().min(1).max(2000).nullable(),
|
||||
});
|
||||
|
||||
const distributionBoardSchema = z
|
||||
const currentProjectSchema = projectSchema.extend({
|
||||
enabledDistributionBoardSupplyTypes: z
|
||||
.array(z.enum(distributionBoardSupplyTypes))
|
||||
.min(1)
|
||||
.refine((values) => new Set(values).size === values.length),
|
||||
});
|
||||
|
||||
const legacyDistributionBoardSchema = z
|
||||
.object({
|
||||
id: idSchema,
|
||||
projectId: idSchema,
|
||||
@@ -31,6 +44,13 @@ const distributionBoardSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
const distributionBoardSchema = legacyDistributionBoardSchema
|
||||
.extend({
|
||||
floorId: idSchema.nullable(),
|
||||
supplyType: z.enum(distributionBoardSupplyTypes).nullable(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const circuitListSchema = z
|
||||
.object({
|
||||
id: idSchema,
|
||||
@@ -140,8 +160,7 @@ const roomSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
const projectStateSnapshotContents = {
|
||||
distributionBoards: z.array(distributionBoardSchema),
|
||||
const commonProjectStateSnapshotContents = {
|
||||
circuitLists: z.array(circuitListSchema),
|
||||
circuitSections: z.array(circuitSectionSchema),
|
||||
circuits: z.array(circuitSchema),
|
||||
@@ -154,15 +173,37 @@ const legacyProjectStateSnapshotSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(legacyProjectStateSnapshotSchemaVersion),
|
||||
project: legacyProjectSchema,
|
||||
...projectStateSnapshotContents,
|
||||
distributionBoards: z.array(legacyDistributionBoardSchema),
|
||||
...commonProjectStateSnapshotContents,
|
||||
})
|
||||
.strict();
|
||||
|
||||
const previousProjectStateSnapshotSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(previousProjectStateSnapshotSchemaVersion),
|
||||
project: projectSchema,
|
||||
distributionBoards: z.array(legacyDistributionBoardSchema),
|
||||
...commonProjectStateSnapshotContents,
|
||||
})
|
||||
.strict();
|
||||
|
||||
const distributionBoardProjectStateSnapshotSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(
|
||||
distributionBoardProjectStateSnapshotSchemaVersion
|
||||
),
|
||||
project: projectSchema,
|
||||
distributionBoards: z.array(distributionBoardSchema),
|
||||
...commonProjectStateSnapshotContents,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const projectStateSnapshotSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
|
||||
project: projectSchema,
|
||||
...projectStateSnapshotContents,
|
||||
project: currentProjectSchema,
|
||||
distributionBoards: z.array(distributionBoardSchema),
|
||||
...commonProjectStateSnapshotContents,
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -176,20 +217,34 @@ export function parseProjectStateSnapshot(
|
||||
const version = isPlainObject(value) ? value.schemaVersion : undefined;
|
||||
const snapshot =
|
||||
version === legacyProjectStateSnapshotSchemaVersion
|
||||
? upgradeLegacyProjectStateSnapshot(
|
||||
legacyProjectStateSnapshotSchema.parse(value)
|
||||
? upgradeDistributionBoardProjectStateSnapshot(
|
||||
upgradePreviousProjectStateSnapshot(
|
||||
upgradeLegacyProjectStateSnapshot(
|
||||
legacyProjectStateSnapshotSchema.parse(value)
|
||||
)
|
||||
)
|
||||
)
|
||||
: projectStateSnapshotSchema.parse(value);
|
||||
: version === previousProjectStateSnapshotSchemaVersion
|
||||
? upgradeDistributionBoardProjectStateSnapshot(
|
||||
upgradePreviousProjectStateSnapshot(
|
||||
previousProjectStateSnapshotSchema.parse(value)
|
||||
)
|
||||
)
|
||||
: version === distributionBoardProjectStateSnapshotSchemaVersion
|
||||
? upgradeDistributionBoardProjectStateSnapshot(
|
||||
distributionBoardProjectStateSnapshotSchema.parse(value)
|
||||
)
|
||||
: projectStateSnapshotSchema.parse(value);
|
||||
assertProjectStateSnapshotRelations(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function upgradeLegacyProjectStateSnapshot(
|
||||
snapshot: z.infer<typeof legacyProjectStateSnapshotSchema>
|
||||
): ProjectStateSnapshot {
|
||||
): z.infer<typeof previousProjectStateSnapshotSchema> {
|
||||
return {
|
||||
...snapshot,
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
schemaVersion: previousProjectStateSnapshotSchemaVersion,
|
||||
project: {
|
||||
...snapshot.project,
|
||||
internalProjectNumber: null,
|
||||
@@ -200,6 +255,35 @@ function upgradeLegacyProjectStateSnapshot(
|
||||
};
|
||||
}
|
||||
|
||||
function upgradePreviousProjectStateSnapshot(
|
||||
snapshot: z.infer<typeof previousProjectStateSnapshotSchema>
|
||||
): z.infer<typeof distributionBoardProjectStateSnapshotSchema> {
|
||||
return {
|
||||
...snapshot,
|
||||
schemaVersion: distributionBoardProjectStateSnapshotSchemaVersion,
|
||||
distributionBoards: snapshot.distributionBoards.map((board) => ({
|
||||
...board,
|
||||
floorId: null,
|
||||
supplyType: null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function upgradeDistributionBoardProjectStateSnapshot(
|
||||
snapshot: z.infer<typeof distributionBoardProjectStateSnapshotSchema>
|
||||
): ProjectStateSnapshot {
|
||||
return {
|
||||
...snapshot,
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
project: {
|
||||
...snapshot.project,
|
||||
enabledDistributionBoardSupplyTypes: [
|
||||
...defaultDistributionBoardSupplyTypes,
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
@@ -245,6 +329,23 @@ function assertProjectStateSnapshotRelations(
|
||||
|
||||
for (const board of snapshot.distributionBoards) {
|
||||
assertProjectOwnership(board.projectId, projectId, "distribution board");
|
||||
if (board.floorId !== null) {
|
||||
assertReference(
|
||||
floorIds,
|
||||
board.floorId,
|
||||
"distribution board floor"
|
||||
);
|
||||
}
|
||||
if (
|
||||
board.supplyType !== null &&
|
||||
!snapshot.project.enabledDistributionBoardSupplyTypes.includes(
|
||||
board.supplyType
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
"Snapshot distribution board uses a disabled supply type."
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const list of snapshot.circuitLists) {
|
||||
assertProjectOwnership(list.projectId, projectId, "circuit list");
|
||||
|
||||
@@ -94,6 +94,10 @@ export function remapProjectState(
|
||||
...board,
|
||||
id: requiredId(boardIds, board.id),
|
||||
projectId: targetProjectId,
|
||||
floorId:
|
||||
board.floorId === null
|
||||
? null
|
||||
: requiredId(floorIds, board.floorId),
|
||||
})),
|
||||
circuitLists: source.circuitLists.map((list) => ({
|
||||
...list,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DistributionBoardStructureProjectCommand } from "../models/distribution-board-structure-project-command.model.js";
|
||||
import type { DistributionBoardUpdateProjectCommand } from "../models/distribution-board-project-command.model.js";
|
||||
import type {
|
||||
AppendedProjectRevision,
|
||||
ProjectRevisionSource,
|
||||
@@ -23,4 +24,15 @@ export interface DistributionBoardStructureProjectCommandStore {
|
||||
execute(
|
||||
input: ExecuteDistributionBoardStructureCommandInput
|
||||
): ExecutedDistributionBoardStructureCommand;
|
||||
executeUpdate(
|
||||
input: Omit<
|
||||
ExecuteDistributionBoardStructureCommandInput,
|
||||
"command"
|
||||
> & {
|
||||
command: DistributionBoardUpdateProjectCommand;
|
||||
}
|
||||
): {
|
||||
revision: AppendedProjectRevision;
|
||||
inverse: DistributionBoardUpdateProjectCommand;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ import {
|
||||
circuitDeleteCommandType,
|
||||
circuitInsertCommandType,
|
||||
} from "../models/circuit-structure-project-command.model.js";
|
||||
import {
|
||||
assertDistributionBoardUpdateProjectCommand,
|
||||
distributionBoardUpdateCommandType,
|
||||
} from "../models/distribution-board-project-command.model.js";
|
||||
import {
|
||||
assertDistributionBoardDeleteProjectCommand,
|
||||
assertDistributionBoardInsertProjectCommand,
|
||||
@@ -285,6 +289,13 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case distributionBoardUpdateCommandType: {
|
||||
assertDistributionBoardUpdateProjectCommand(input.command);
|
||||
return this.distributionBoardStructureStore.executeUpdate({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case projectFloorInsertCommandType: {
|
||||
assertProjectFloorInsertProjectCommand(input.command);
|
||||
return this.projectLocationStructureStore.execute({
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import React, { FormEvent, useState } from "react";
|
||||
import type { ProjectDto } from "../types";
|
||||
import {
|
||||
distributionBoardSupplyTypeLabels,
|
||||
distributionBoardSupplyTypes,
|
||||
type DistributionBoardSupplyType,
|
||||
} from "../../shared/constants/distribution-board";
|
||||
|
||||
export interface ProjectSettingsInput {
|
||||
name: string;
|
||||
@@ -11,6 +16,7 @@ export interface ProjectSettingsInput {
|
||||
description: string | null;
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
|
||||
}
|
||||
|
||||
interface ProjectSettingsModalProps {
|
||||
@@ -23,6 +29,7 @@ interface ProjectSettingsModalProps {
|
||||
) => Promise<void>;
|
||||
onSave: (input: ProjectSettingsInput) => Promise<void>;
|
||||
project: ProjectDto;
|
||||
usedDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
|
||||
}
|
||||
|
||||
export function ProjectSettingsModal({
|
||||
@@ -32,6 +39,7 @@ export function ProjectSettingsModal({
|
||||
onImport,
|
||||
onSave,
|
||||
project,
|
||||
usedDistributionBoardSupplyTypes,
|
||||
}: ProjectSettingsModalProps) {
|
||||
const [name, setName] = useState(project.name);
|
||||
const [internalProjectNumber, setInternalProjectNumber] = useState(
|
||||
@@ -52,6 +60,12 @@ export function ProjectSettingsModal({
|
||||
const [threePhaseVoltageV, setThreePhaseVoltageV] = useState(
|
||||
String(project.threePhaseVoltageV)
|
||||
);
|
||||
const [
|
||||
enabledDistributionBoardSupplyTypes,
|
||||
setEnabledDistributionBoardSupplyTypes,
|
||||
] = useState<DistributionBoardSupplyType[]>(
|
||||
project.enabledDistributionBoardSupplyTypes
|
||||
);
|
||||
const [transfer, setTransfer] = useState<unknown>(null);
|
||||
const [transferFilename, setTransferFilename] = useState("");
|
||||
const [importMode, setImportMode] = useState<"replace" | "duplicate">(
|
||||
@@ -70,13 +84,31 @@ export function ProjectSettingsModal({
|
||||
description: toNullableString(description),
|
||||
singlePhaseVoltageV: Number(singlePhaseVoltageV),
|
||||
threePhaseVoltageV: Number(threePhaseVoltageV),
|
||||
enabledDistributionBoardSupplyTypes,
|
||||
});
|
||||
}
|
||||
|
||||
const isValid =
|
||||
name.trim().length > 0 &&
|
||||
Number(singlePhaseVoltageV) > 0 &&
|
||||
Number(threePhaseVoltageV) > 0;
|
||||
Number(threePhaseVoltageV) > 0 &&
|
||||
enabledDistributionBoardSupplyTypes.length > 0;
|
||||
|
||||
function toggleSupplyType(supplyType: DistributionBoardSupplyType) {
|
||||
if (
|
||||
enabledDistributionBoardSupplyTypes.includes(supplyType) &&
|
||||
usedDistributionBoardSupplyTypes.includes(supplyType)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setEnabledDistributionBoardSupplyTypes((current) =>
|
||||
current.includes(supplyType)
|
||||
? current.filter((entry) => entry !== supplyType)
|
||||
: distributionBoardSupplyTypes.filter(
|
||||
(entry) => entry === supplyType || current.includes(entry)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function handleTransferFile(file: File | undefined) {
|
||||
setTransfer(null);
|
||||
@@ -165,6 +197,53 @@ export function ProjectSettingsModal({
|
||||
value={externalProjectNumber}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<fieldset>
|
||||
<legend className="form-label mb-1">
|
||||
Verwendete Netzarten
|
||||
</legend>
|
||||
<p className="form-text mt-0">
|
||||
Nur ausgewählte Netzarten stehen bei Verteilungen zur
|
||||
Auswahl. Bereits verwendete Netzarten können nicht
|
||||
deaktiviert werden.
|
||||
</p>
|
||||
<div className="row g-2">
|
||||
{distributionBoardSupplyTypes.map((supplyType) => (
|
||||
<div
|
||||
className="col-12 col-md-6"
|
||||
key={supplyType}
|
||||
>
|
||||
<label className="form-check">
|
||||
<input
|
||||
checked={enabledDistributionBoardSupplyTypes.includes(
|
||||
supplyType
|
||||
)}
|
||||
className="form-check-input"
|
||||
disabled={usedDistributionBoardSupplyTypes.includes(
|
||||
supplyType
|
||||
)}
|
||||
onChange={() => toggleSupplyType(supplyType)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<span className="form-check-label">
|
||||
{distributionBoardSupplyTypeLabels[supplyType]}
|
||||
{usedDistributionBoardSupplyTypes.includes(
|
||||
supplyType
|
||||
)
|
||||
? " (in Verwendung)"
|
||||
: ""}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{enabledDistributionBoardSupplyTypes.length === 0 ? (
|
||||
<div className="text-danger small mt-2">
|
||||
Mindestens eine Netzart muss aktiviert sein.
|
||||
</div>
|
||||
) : null}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label className="form-label" htmlFor="building-owner">
|
||||
Bauherr
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { DistributionBoardSupplyType } from "../shared/constants/distribution-board";
|
||||
|
||||
export interface ProjectDto {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -7,6 +9,7 @@ export interface ProjectDto {
|
||||
description: string | null;
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
|
||||
currentRevision: number;
|
||||
}
|
||||
|
||||
@@ -89,6 +92,8 @@ export interface DistributionBoardDto {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
floorId: string | null;
|
||||
supplyType: DistributionBoardSupplyType | null;
|
||||
}
|
||||
|
||||
export interface DistributionBoardCommandResultDto
|
||||
|
||||
@@ -226,6 +226,7 @@ export function updateProjectSettings(
|
||||
description: string | null;
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
enabledDistributionBoardSupplyTypes: ProjectDto["enabledDistributionBoardSupplyTypes"];
|
||||
}
|
||||
) {
|
||||
return request<ProjectSettingsCommandResultDto>(
|
||||
@@ -276,14 +277,36 @@ export function listDistributionBoards(projectId: string) {
|
||||
|
||||
export function createDistributionBoard(
|
||||
projectId: string,
|
||||
name: string,
|
||||
input: {
|
||||
name: string;
|
||||
floorId: string | null;
|
||||
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
|
||||
},
|
||||
expectedRevision: number
|
||||
) {
|
||||
return request<DistributionBoardCommandResultDto>(
|
||||
`/api/projects/${projectId}/distribution-boards`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name, expectedRevision }),
|
||||
body: JSON.stringify({ ...input, expectedRevision }),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function updateDistributionBoard(
|
||||
projectId: string,
|
||||
distributionBoardId: string,
|
||||
input: {
|
||||
floorId: string | null;
|
||||
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
|
||||
},
|
||||
expectedRevision: number
|
||||
) {
|
||||
return request<DistributionBoardCommandResultDto>(
|
||||
`/api/projects/${projectId}/distribution-boards/${distributionBoardId}`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ ...input, expectedRevision }),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ const commandTypeLabels: Record<string, string> = {
|
||||
"project-device.sync-rows": "Projektgerät-Verknüpfungen geändert",
|
||||
"project.update-settings": "Projekteinstellungen bearbeitet",
|
||||
"distribution-board.insert": "Verteilung angelegt",
|
||||
"distribution-board.update": "Verteilung bearbeitet",
|
||||
"distribution-board.delete": "Verteilung entfernt",
|
||||
"project-floor.insert": "Geschoss angelegt",
|
||||
"project-floor.delete": "Geschoss entfernt",
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { createDistributionBoardUpdateProjectCommand } from "../../domain/models/distribution-board-project-command.model.js";
|
||||
import {
|
||||
createDistributionBoardInsertProjectCommand,
|
||||
createDistributionBoardStructureSnapshot,
|
||||
} from "../../domain/models/distribution-board-structure-project-command.model.js";
|
||||
import { createDistributionBoardSchema } from "../../shared/validation/project-structure.schemas.js";
|
||||
import {
|
||||
createDistributionBoardSchema,
|
||||
updateDistributionBoardSchema,
|
||||
} from "../../shared/validation/project-structure.schemas.js";
|
||||
import { projectCommandService } from "../composition/project-command-stores.js";
|
||||
import { distributionBoardRepository } from "../composition/application-repositories.js";
|
||||
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
||||
@@ -31,7 +35,11 @@ export async function createDistributionBoard(req: Request, res: Response) {
|
||||
|
||||
const structure = createDistributionBoardStructureSnapshot(
|
||||
projectId,
|
||||
parsed.data.name
|
||||
parsed.data.name,
|
||||
{
|
||||
floorId: parsed.data.floorId,
|
||||
supplyType: parsed.data.supplyType,
|
||||
}
|
||||
);
|
||||
try {
|
||||
const result = projectCommandService.executeUser({
|
||||
@@ -49,3 +57,43 @@ export async function createDistributionBoard(req: Request, res: Response) {
|
||||
return respondWithProjectCommandError(error, res);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateDistributionBoard(
|
||||
req: Request,
|
||||
res: Response
|
||||
) {
|
||||
const { projectId, distributionBoardId } = req.params;
|
||||
if (
|
||||
typeof projectId !== "string" ||
|
||||
typeof distributionBoardId !== "string"
|
||||
) {
|
||||
return res.status(400).json({ error: "Invalid distribution board id" });
|
||||
}
|
||||
const parsed = updateDistributionBoardSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
try {
|
||||
const result = projectCommandService.executeUser({
|
||||
projectId,
|
||||
expectedRevision: parsed.data.expectedRevision,
|
||||
description: "Verteilung bearbeiten",
|
||||
command: createDistributionBoardUpdateProjectCommand(
|
||||
distributionBoardId,
|
||||
{
|
||||
floorId: parsed.data.floorId,
|
||||
supplyType: parsed.data.supplyType,
|
||||
}
|
||||
),
|
||||
});
|
||||
const distributionBoard = (
|
||||
await distributionBoardRepository.listByProject(projectId)
|
||||
).find((board) => board.id === distributionBoardId);
|
||||
if (!distributionBoard) {
|
||||
return res.status(404).json({ error: "Distribution board not found" });
|
||||
}
|
||||
return res.json({ ...result, distributionBoard });
|
||||
} catch (error) {
|
||||
return respondWithProjectCommandError(error, res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import {
|
||||
createDistributionBoard,
|
||||
listDistributionBoardsByProject,
|
||||
updateDistributionBoard,
|
||||
} from "../controllers/distribution-board.controller.js";
|
||||
import { listCircuitListsByProject } from "../controllers/circuit-list.controller.js";
|
||||
import { createFloor, listFloorsByProject } from "../controllers/floor.controller.js";
|
||||
@@ -55,6 +56,10 @@ projectRouter.post(
|
||||
projectRouter.put("/:projectId", updateProjectSettings);
|
||||
projectRouter.get("/:projectId/distribution-boards", listDistributionBoardsByProject);
|
||||
projectRouter.post("/:projectId/distribution-boards", createDistributionBoard);
|
||||
projectRouter.put(
|
||||
"/:projectId/distribution-boards/:distributionBoardId",
|
||||
updateDistributionBoard
|
||||
);
|
||||
projectRouter.get("/:projectId/circuit-lists", listCircuitListsByProject);
|
||||
projectRouter.get("/:projectId/circuit-lists/:circuitListId/tree", getCircuitTree);
|
||||
projectRouter.get("/:projectId/floors", listFloorsByProject);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
export const distributionBoardSupplyTypes = [
|
||||
"AV",
|
||||
"SV",
|
||||
"EV",
|
||||
"USV",
|
||||
"MSR",
|
||||
"SiBe",
|
||||
] as const;
|
||||
|
||||
export type DistributionBoardSupplyType =
|
||||
(typeof distributionBoardSupplyTypes)[number];
|
||||
|
||||
export const defaultDistributionBoardSupplyTypes: DistributionBoardSupplyType[] =
|
||||
[...distributionBoardSupplyTypes];
|
||||
|
||||
export const distributionBoardSupplyTypeLabels: Record<
|
||||
DistributionBoardSupplyType,
|
||||
string
|
||||
> = {
|
||||
AV: "AV – Allgemeine Stromversorgung",
|
||||
SV: "SV – Sicherheitsstromversorgung",
|
||||
EV: "EV – Ersatzstromversorgung",
|
||||
USV: "USV – Unterbrechungsfreie Stromversorgung",
|
||||
MSR: "MSR – Mess-, Steuerungs- und Regelungstechnik",
|
||||
SiBe: "SiBe – Sicherheitsbeleuchtung",
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { distributionBoardSupplyTypes } from "../constants/distribution-board.js";
|
||||
import { expectedProjectRevisionSchema } from "./project-command.schemas.js";
|
||||
|
||||
export const createProjectSchema = z.object({
|
||||
@@ -21,6 +22,12 @@ export const updateProjectSettingsSchema = z
|
||||
description: z.string().trim().max(2000).nullable(),
|
||||
singlePhaseVoltageV: z.number().positive(),
|
||||
threePhaseVoltageV: z.number().positive(),
|
||||
enabledDistributionBoardSupplyTypes: z
|
||||
.array(z.enum(distributionBoardSupplyTypes))
|
||||
.min(1)
|
||||
.refine((values) => new Set(values).size === values.length, {
|
||||
message: "Netzarten dürfen nicht mehrfach ausgewählt werden.",
|
||||
}),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -28,6 +35,16 @@ export const createDistributionBoardSchema = z
|
||||
.object({
|
||||
expectedRevision: expectedProjectRevisionSchema,
|
||||
name: z.string().trim().min(1),
|
||||
floorId: z.string().trim().min(1).nullable(),
|
||||
supplyType: z.enum(distributionBoardSupplyTypes),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const updateDistributionBoardSchema = z
|
||||
.object({
|
||||
expectedRevision: expectedProjectRevisionSchema,
|
||||
floorId: z.string().trim().min(1).nullable(),
|
||||
supplyType: z.enum(distributionBoardSupplyTypes),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user