Add configurable distribution supply types
This commit is contained in:
@@ -283,17 +283,23 @@ that have remained unchanged. ProjectDevice create, update, delete and
|
|||||||
global-to-project copy API/UI paths use these persistent commands and track the
|
global-to-project copy API/UI paths use these persistent commands and track the
|
||||||
returned project revision. ProjectDevice synchronization/disconnect API and UI
|
returned project revision. ProjectDevice synchronization/disconnect API and UI
|
||||||
paths do the same; their undo action uses the project-wide history endpoint.
|
paths do the same; their undo action uses the project-wide history endpoint.
|
||||||
Project voltage settings use the persistent `project.update-settings` command.
|
Project settings use the persistent `project.update-settings` command. Project
|
||||||
Both voltage defaults change in one revision and Undo/Redo restores them
|
metadata, both voltage defaults and the enabled distribution-board supply types
|
||||||
together; the project PUT route requires `expectedRevision`.
|
change in one revision and Undo/Redo restores them together; the project PUT
|
||||||
|
route requires `expectedRevision`. The system catalog is `AV`, `SV`, `EV`,
|
||||||
|
`USV`, `MSR`, `SiBe`; at least one must be enabled and a type used by a board
|
||||||
|
cannot be disabled.
|
||||||
Distribution-board setup uses `distribution-board.insert` with a complete
|
Distribution-board setup uses `distribution-board.insert` with a complete
|
||||||
stable snapshot of the board, circuit list and four default sections. Its
|
stable snapshot of the board, circuit list and four default sections. Its
|
||||||
inverse removes only the same unchanged and still-empty structure; the POST
|
inverse removes only the same unchanged and still-empty structure; the POST
|
||||||
route requires `expectedRevision` and returns the updated history state.
|
route requires `expectedRevision` and returns the updated history state.
|
||||||
|
Distribution-board floor assignment and a project-enabled supply type use
|
||||||
|
`distribution-board.update`; both values are snapshot/export fields and one
|
||||||
|
persistent undo step.
|
||||||
Floor and room setup use `project-floor.insert` and `project-room.insert` with
|
Floor and room setup use `project-floor.insert` and `project-room.insert` with
|
||||||
complete stable snapshots. Their inverses remove only unchanged records and
|
complete stable snapshots. Their inverses remove only unchanged records and
|
||||||
reject floors with assigned rooms or rooms referenced by device rows or
|
reject floors with assigned rooms/distribution boards or rooms referenced by
|
||||||
retained upgrade data. Both POST routes require `expectedRevision` and return
|
device rows or retained upgrade data. Both POST routes require `expectedRevision` and return
|
||||||
the updated history state.
|
the updated history state.
|
||||||
|
|
||||||
Required operations:
|
Required operations:
|
||||||
|
|||||||
@@ -145,11 +145,14 @@ synchronizes linked circuit rows implicitly.
|
|||||||
|
|
||||||
- `PUT /projects/:projectId`
|
- `PUT /projects/:projectId`
|
||||||
- request:
|
- request:
|
||||||
`{ "expectedRevision": 12, "singlePhaseVoltageV": 230, "threePhaseVoltageV": 400 }`
|
`{ "expectedRevision": 12, "singlePhaseVoltageV": 230, "threePhaseVoltageV": 400, "enabledDistributionBoardSupplyTypes": ["AV", "MSR", "SiBe"], ...projectMetadata }`
|
||||||
- executes `project.update-settings` as one atomic revision
|
- executes `project.update-settings` as one atomic revision
|
||||||
- response:
|
- response:
|
||||||
`{ "project": { ... }, "revision": { ... }, "history": { ... } }`
|
`{ "project": { ... }, "revision": { ... }, "history": { ... } }`
|
||||||
- persistent Undo/Redo restores both voltage values together
|
- persistent Undo/Redo restores metadata, voltage values and the enabled
|
||||||
|
distribution-board supply types together
|
||||||
|
- at least one of `AV`, `SV`, `EV`, `USV`, `MSR`, `SiBe` must be enabled;
|
||||||
|
a supply type currently used by a distribution board cannot be disabled
|
||||||
- unchanged values are rejected without creating a revision; a stale
|
- unchanged values are rejected without creating a revision; a stale
|
||||||
revision returns `409 PROJECT_REVISION_CONFLICT`
|
revision returns `409 PROJECT_REVISION_CONFLICT`
|
||||||
|
|
||||||
@@ -181,7 +184,11 @@ returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`.
|
|||||||
### Distribution Board Setup
|
### Distribution Board Setup
|
||||||
|
|
||||||
- `POST /projects/:projectId/distribution-boards`
|
- `POST /projects/:projectId/distribution-boards`
|
||||||
- body: `{ "name": "UV-02", "expectedRevision": 12 }`
|
- body:
|
||||||
|
`{ "name": "UV-02", "floorId": "floor_1", "supplyType": "SV", "expectedRevision": 12 }`
|
||||||
|
- `floorId` may be `null`; a non-null floor must belong to the project
|
||||||
|
- `supplyType` is one of `AV`, `SV`, `EV`, `USV`, `MSR` or `SiBe` and must
|
||||||
|
be enabled in the project settings
|
||||||
- executes `distribution-board.insert` with stable ids for the distribution
|
- executes `distribution-board.insert` with stable ids for the distribution
|
||||||
board, its circuit list and all four default circuit sections
|
board, its circuit list and all four default circuit sections
|
||||||
- response:
|
- response:
|
||||||
@@ -189,6 +196,11 @@ returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`.
|
|||||||
- persistent Undo removes only the unchanged and still-empty generated
|
- persistent Undo removes only the unchanged and still-empty generated
|
||||||
structure; Redo restores the same ids
|
structure; Redo restores the same ids
|
||||||
- stale revisions return `409 PROJECT_REVISION_CONFLICT`
|
- stale revisions return `409 PROJECT_REVISION_CONFLICT`
|
||||||
|
- `PUT /projects/:projectId/distribution-boards/:distributionBoardId`
|
||||||
|
- body:
|
||||||
|
`{ "floorId": null, "supplyType": "AV", "expectedRevision": 13 }`
|
||||||
|
- executes `distribution-board.update`; floor and supply type are restored
|
||||||
|
together by persistent Undo/Redo
|
||||||
|
|
||||||
### Project Floors and Rooms
|
### Project Floors and Rooms
|
||||||
|
|
||||||
@@ -203,8 +215,9 @@ returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`.
|
|||||||
and must belong to the project when present
|
and must belong to the project when present
|
||||||
- response: `{ "room": { ... }, "revision": { ... }, "history": { ... } }`
|
- response: `{ "room": { ... }, "revision": { ... }, "history": { ... } }`
|
||||||
- Persistent Undo removes only unchanged, unreferenced records. A floor with
|
- Persistent Undo removes only unchanged, unreferenced records. A floor with
|
||||||
assigned rooms and a room referenced by device rows or retained upgrade data
|
assigned rooms or distribution boards and a room referenced by device rows
|
||||||
are rejected instead of silently clearing foreign keys.
|
or retained upgrade data are rejected instead of silently clearing foreign
|
||||||
|
keys.
|
||||||
- Stale revisions return `409 PROJECT_REVISION_CONFLICT`.
|
- Stale revisions return `409 PROJECT_REVISION_CONFLICT`.
|
||||||
|
|
||||||
### Tree Endpoint
|
### Tree Endpoint
|
||||||
|
|||||||
@@ -221,8 +221,11 @@ und Historienstapel teilen dieselbe Transaktion. Create, Import aus der globalen
|
|||||||
Gerätebibliothek und Delete laufen über dieselbe Command-Grenze; ihre Antworten
|
Gerätebibliothek und Delete laufen über dieselbe Command-Grenze; ihre Antworten
|
||||||
liefern Gerät und aktualisierten Historienstand an die Projektseite zurück.
|
liefern Gerät und aktualisierten Historienstand an die Projektseite zurück.
|
||||||
`project.update-settings` versioniert Projektname, interne und externe
|
`project.update-settings` versioniert Projektname, interne und externe
|
||||||
Projektnummer, Bauherr, Beschreibung sowie beide Standardspannungen als eine
|
Projektnummer, Bauherr, Beschreibung, beide Standardspannungen sowie die im
|
||||||
atomare Änderung. Die Projektseite bearbeitet diese Angaben in einem
|
Projekt freigeschalteten Verteiler-Netzarten als eine atomare Änderung. Der
|
||||||
|
Systemkatalog besteht aus `AV`, `SV`, `EV`, `USV`, `MSR` und `SiBe`; mindestens
|
||||||
|
eine Netzart muss aktiv bleiben und eine bereits von einer Verteilung
|
||||||
|
verwendete Netzart kann nicht deaktiviert werden. Die Projektseite bearbeitet diese Angaben in einem
|
||||||
beschrifteten Einstellungsmodal statt in einer permanenten Formularkarte. Der
|
beschrifteten Einstellungsmodal statt in einer permanenten Formularkarte. Der
|
||||||
Store leitet das inverse Kommando aus dem gespeicherten Projekt ab und schreibt
|
Store leitet das inverse Kommando aus dem gespeicherten Projekt ab und schreibt
|
||||||
Werte, Revision und Historienstapel gemeinsam. `PUT /api/projects/:projectId`
|
Werte, Revision und Historienstapel gemeinsam. `PUT /api/projects/:projectId`
|
||||||
@@ -258,11 +261,21 @@ Alle UUIDs entstehen vor dem Command und bleiben über Undo/Redo stabil.
|
|||||||
`distribution-board.delete` ist die persistierte Inverse und entfernt nur den
|
`distribution-board.delete` ist die persistierte Inverse und entfernt nur den
|
||||||
vollständig unveränderten, weiterhin stromkreislosen Block. Controller und
|
vollständig unveränderten, weiterhin stromkreislosen Block. Controller und
|
||||||
Projektseite übergeben die erwartete Projektrevision; der frühere direkte
|
Projektseite übergeben die erwartete Projektrevision; der frühere direkte
|
||||||
Controller-Schreibweg ist entfernt.
|
Controller-Schreibweg ist entfernt. Verteilungen besitzen eine optionale
|
||||||
|
Etagenreferenz sowie eine Netzart aus dem Projektkatalog. Anlage und
|
||||||
|
nachträgliche Bearbeitung prüfen die Projektzugehörigkeit der Etage und die
|
||||||
|
Freigabe der Netzart in den Projekteinstellungen.
|
||||||
|
`distribution-board.update` versioniert Etage und Netzart gemeinsam und stellt
|
||||||
|
beide Werte über dauerhaftes Undo/Redo wieder her. Snapshot-Schema 4 und der
|
||||||
|
portable Projekttransfer enthalten diese Felder; Schema 1/2 sowie gespeicherte
|
||||||
|
Version-1-Strukturcommands werden ohne erfundene Zuordnung hochgestuft.
|
||||||
|
Snapshot-Schema 3 wird mit allen sechs Netzarten als Projektauswahl
|
||||||
|
hochgestuft.
|
||||||
`project-floor.insert` und `project-room.insert` versionieren die Anlage von
|
`project-floor.insert` und `project-room.insert` versionieren die Anlage von
|
||||||
Geschossen und Räumen mit stabilen UUIDs. Die vollständigen Datensätze bilden
|
Geschossen und Räumen mit stabilen UUIDs. Die vollständigen Datensätze bilden
|
||||||
jeweils die persistierte Inverse für Undo/Redo. Ein Geschoss wird durch Undo nur
|
jeweils die persistierte Inverse für Undo/Redo. Ein Geschoss wird durch Undo nur
|
||||||
entfernt, solange ihm kein Raum zugeordnet wurde. Ein Raum wird nur entfernt,
|
entfernt, solange ihm weder ein Raum noch eine Verteilung zugeordnet wurde. Ein
|
||||||
|
Raum wird nur entfernt,
|
||||||
solange weder eine CircuitDeviceRow noch ein aufbewahrter Upgrade-Datensatz auf
|
solange weder eine CircuitDeviceRow noch ein aufbewahrter Upgrade-Datensatz auf
|
||||||
ihn verweist. Beide POST-Endpunkte verlangen `expectedRevision`, liefern den
|
ihn verweist. Beide POST-Endpunkte verlangen `expectedRevision`, liefern den
|
||||||
aktualisierten Historienstand und besitzen keinen direkten Create-Schreibweg
|
aktualisierten Historienstand und besitzen keinen direkten Create-Schreibweg
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ requirements and intended sequencing, not proof of implementation.
|
|||||||
- `SV` (Sicherheitsstromversorgung)
|
- `SV` (Sicherheitsstromversorgung)
|
||||||
- `EV` (Ersatzstromversorgung)
|
- `EV` (Ersatzstromversorgung)
|
||||||
- `USV` (Unterbrechungsfreie Stromversorgung)
|
- `USV` (Unterbrechungsfreie Stromversorgung)
|
||||||
|
- `MSR` (Mess-, Steuerungs- und Regelungstechnik)
|
||||||
|
- `SiBe` (Sicherheitsbeleuchtung)
|
||||||
|
- Let project settings select which catalog supply types are used in a
|
||||||
|
project. Distribution-board dialogs offer only that selection; a type in
|
||||||
|
use cannot be disabled.
|
||||||
- Include both fields in project snapshots and portable project
|
- Include both fields in project snapshots and portable project
|
||||||
export/import.
|
export/import.
|
||||||
- Persist changes through project commands so they remain undoable after a
|
- Persist changes through project commands so they remain undoable after a
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const requiredProjectColumns = [
|
|||||||
"external_project_number",
|
"external_project_number",
|
||||||
"building_owner",
|
"building_owner",
|
||||||
"description",
|
"description",
|
||||||
|
"enabled_distribution_board_supply_types",
|
||||||
];
|
];
|
||||||
const projectColumns = new Set(
|
const projectColumns = new Set(
|
||||||
db.prepare("PRAGMA table_info(projects)").all().map((column) => column.name)
|
db.prepare("PRAGMA table_info(projects)").all().map((column) => column.name)
|
||||||
@@ -60,6 +61,19 @@ const projectColumns = new Set(
|
|||||||
const missingProjectColumns = requiredProjectColumns.filter(
|
const missingProjectColumns = requiredProjectColumns.filter(
|
||||||
(name) => !projectColumns.has(name)
|
(name) => !projectColumns.has(name)
|
||||||
);
|
);
|
||||||
|
const requiredDistributionBoardColumns = ["floor_id", "supply_type"];
|
||||||
|
const distributionBoardColumns = new Set(
|
||||||
|
db
|
||||||
|
.prepare("PRAGMA table_info(distribution_boards)")
|
||||||
|
.all()
|
||||||
|
.map((column) => column.name)
|
||||||
|
);
|
||||||
|
const missingDistributionBoardColumns =
|
||||||
|
requiredDistributionBoardColumns.filter(
|
||||||
|
(name) => !distributionBoardColumns.has(name)
|
||||||
|
);
|
||||||
|
const integrityCheck = db.pragma("integrity_check", { simple: true });
|
||||||
|
const foreignKeyViolations = db.pragma("foreign_key_check");
|
||||||
|
|
||||||
console.log("Database:", dbPath);
|
console.log("Database:", dbPath);
|
||||||
console.log("Required tables:", requiredTables.join(", "));
|
console.log("Required tables:", requiredTables.join(", "));
|
||||||
@@ -67,6 +81,12 @@ console.log("Existing tables:", [...existing].join(", ") || "(none)");
|
|||||||
console.log("Required project-device columns:", requiredProjectDeviceColumns.join(", "));
|
console.log("Required project-device columns:", requiredProjectDeviceColumns.join(", "));
|
||||||
console.log("Required circuit columns:", requiredCircuitColumns.join(", "));
|
console.log("Required circuit columns:", requiredCircuitColumns.join(", "));
|
||||||
console.log("Required project columns:", requiredProjectColumns.join(", "));
|
console.log("Required project columns:", requiredProjectColumns.join(", "));
|
||||||
|
console.log(
|
||||||
|
"Required distribution-board columns:",
|
||||||
|
requiredDistributionBoardColumns.join(", ")
|
||||||
|
);
|
||||||
|
console.log("Integrity:", integrityCheck);
|
||||||
|
console.log("Foreign-key violations:", foreignKeyViolations.length);
|
||||||
|
|
||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
console.error("Missing tables:", missing.join(", "));
|
console.error("Missing tables:", missing.join(", "));
|
||||||
@@ -96,4 +116,17 @@ if (missingProjectColumns.length > 0) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (missingDistributionBoardColumns.length > 0) {
|
||||||
|
console.error(
|
||||||
|
"Missing distribution-board columns:",
|
||||||
|
missingDistributionBoardColumns.join(", ")
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integrityCheck !== "ok" || foreignKeyViolations.length > 0) {
|
||||||
|
console.error("Database integrity or foreign-key verification failed.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Circuit-first schema verification passed.");
|
console.log("Circuit-first schema verification passed.");
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
listRooms,
|
listRooms,
|
||||||
importProjectTransfer,
|
importProjectTransfer,
|
||||||
synchronizeProjectDeviceRows,
|
synchronizeProjectDeviceRows,
|
||||||
|
updateDistributionBoard,
|
||||||
updateProjectDevice,
|
updateProjectDevice,
|
||||||
updateProjectSettings,
|
updateProjectSettings,
|
||||||
} from "../../../frontend/utils/api";
|
} from "../../../frontend/utils/api";
|
||||||
@@ -41,6 +42,10 @@ import {
|
|||||||
projectDeviceSyncFields,
|
projectDeviceSyncFields,
|
||||||
type ProjectDeviceSyncField,
|
type ProjectDeviceSyncField,
|
||||||
} from "../../../shared/constants/project-device-sync-fields";
|
} 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 { ProjectVersionHistory } from "../../../frontend/components/project-version-history";
|
||||||
import {
|
import {
|
||||||
ProjectSettingsModal,
|
ProjectSettingsModal,
|
||||||
@@ -74,6 +79,14 @@ export default function ProjectDetailPage() {
|
|||||||
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
||||||
const [globalDevices, setGlobalDevices] = useState<GlobalDeviceDto[]>([]);
|
const [globalDevices, setGlobalDevices] = useState<GlobalDeviceDto[]>([]);
|
||||||
const [boardName, setBoardName] = useState("");
|
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 [floorName, setFloorName] = useState("");
|
||||||
const [roomNumber, setRoomNumber] = useState("");
|
const [roomNumber, setRoomNumber] = useState("");
|
||||||
const [roomName, setRoomName] = useState("");
|
const [roomName, setRoomName] = useState("");
|
||||||
@@ -158,6 +171,24 @@ export default function ProjectDetailPage() {
|
|||||||
() => new Map(circuitLists.map((circuitList) => [circuitList.distributionBoardId, circuitList])),
|
() => new Map(circuitLists.map((circuitList) => [circuitList.distributionBoardId, circuitList])),
|
||||||
[circuitLists]
|
[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) {
|
function applyProjectRevision(currentRevision: number) {
|
||||||
setProject((current) =>
|
setProject((current) =>
|
||||||
@@ -175,7 +206,11 @@ export default function ProjectDetailPage() {
|
|||||||
try {
|
try {
|
||||||
const result = await createDistributionBoard(
|
const result = await createDistributionBoard(
|
||||||
projectId,
|
projectId,
|
||||||
boardName.trim(),
|
{
|
||||||
|
name: boardName.trim(),
|
||||||
|
floorId: boardFloorId || null,
|
||||||
|
supplyType: boardSupplyType,
|
||||||
|
},
|
||||||
project.currentRevision
|
project.currentRevision
|
||||||
);
|
);
|
||||||
setBoards((current) => [
|
setBoards((current) => [
|
||||||
@@ -185,6 +220,10 @@ export default function ProjectDetailPage() {
|
|||||||
setCircuitLists(await listCircuitLists(projectId));
|
setCircuitLists(await listCircuitLists(projectId));
|
||||||
applyProjectRevision(result.history.currentRevision);
|
applyProjectRevision(result.history.currentRevision);
|
||||||
setBoardName("");
|
setBoardName("");
|
||||||
|
setBoardFloorId("");
|
||||||
|
setBoardSupplyType(
|
||||||
|
project.enabledDistributionBoardSupplyTypes[0]
|
||||||
|
);
|
||||||
setStructureModal(null);
|
setStructureModal(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Verteilung konnte nicht erstellt werden.");
|
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() {
|
async function handleExportProject() {
|
||||||
if (!project) {
|
if (!project) {
|
||||||
return;
|
return;
|
||||||
@@ -596,7 +689,7 @@ export default function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-primary"
|
className="btn btn-sm btn-primary"
|
||||||
onClick={() => setStructureModal("board")}
|
onClick={openBoardCreator}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Verteilung hinzufügen
|
Verteilung hinzufügen
|
||||||
@@ -607,6 +700,8 @@ export default function ProjectDetailPage() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Verteilung</th>
|
<th>Verteilung</th>
|
||||||
|
<th>Etage</th>
|
||||||
|
<th>Netzart</th>
|
||||||
<th className="text-end">Aktionen</th>
|
<th className="text-end">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -616,8 +711,28 @@ export default function ProjectDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<tr key={board.id}>
|
<tr key={board.id}>
|
||||||
<td>{board.name}</td>
|
<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">
|
<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 ? (
|
{circuitList ? (
|
||||||
<Link
|
<Link
|
||||||
className="btn btn-sm btn-primary"
|
className="btn btn-sm btn-primary"
|
||||||
@@ -637,7 +752,7 @@ export default function ProjectDetailPage() {
|
|||||||
})}
|
})}
|
||||||
{!boards.length ? (
|
{!boards.length ? (
|
||||||
<tr>
|
<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.
|
Noch keine Verteilungen vorhanden.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -967,6 +1082,112 @@ export default function ProjectDetailPage() {
|
|||||||
required
|
required
|
||||||
value={boardName}
|
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>
|
</FormModal>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -1082,6 +1303,7 @@ export default function ProjectDetailPage() {
|
|||||||
onImport={handleImportProject}
|
onImport={handleImportProject}
|
||||||
onSave={handleSaveProjectSettings}
|
onSave={handleSaveProjectSettings}
|
||||||
project={project}
|
project={project}
|
||||||
|
usedDistributionBoardSupplyTypes={usedBoardSupplyTypes}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</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,
|
"when": 1785254079435,
|
||||||
"tag": "0016_dashing_darkstar",
|
"tag": "0016_dashing_darkstar",
|
||||||
"breakpoints": true
|
"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,
|
createDistributionBoardInsertProjectCommand,
|
||||||
distributionBoardDeleteCommandType,
|
distributionBoardDeleteCommandType,
|
||||||
distributionBoardInsertCommandType,
|
distributionBoardInsertCommandType,
|
||||||
|
normalizeDistributionBoardStructureProjectCommand,
|
||||||
type DistributionBoardStructureProjectCommand,
|
type DistributionBoardStructureProjectCommand,
|
||||||
type DistributionBoardStructureSnapshot,
|
type DistributionBoardStructureSnapshot,
|
||||||
|
legacyDistributionBoardStructureCommandSchemaVersion,
|
||||||
} from "../../domain/models/distribution-board-structure-project-command.model.js";
|
} 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 {
|
import type {
|
||||||
DistributionBoardStructureProjectCommandStore,
|
DistributionBoardStructureProjectCommandStore,
|
||||||
ExecuteDistributionBoardStructureCommandInput,
|
ExecuteDistributionBoardStructureCommandInput,
|
||||||
@@ -18,6 +28,7 @@ import { circuitLists } from "../schema/circuit-lists.js";
|
|||||||
import { circuitSections } from "../schema/circuit-sections.js";
|
import { circuitSections } from "../schema/circuit-sections.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
|
import { floors } from "../schema/floors.js";
|
||||||
import { projects } from "../schema/projects.js";
|
import { projects } from "../schema/projects.js";
|
||||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.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(
|
private applyCommand(
|
||||||
database: AppDatabase,
|
database: AppDatabase,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
command: DistributionBoardStructureProjectCommand
|
command: DistributionBoardStructureProjectCommand
|
||||||
): DistributionBoardStructureProjectCommand {
|
): DistributionBoardStructureProjectCommand {
|
||||||
if (command.type === distributionBoardInsertCommandType) {
|
const normalized =
|
||||||
assertDistributionBoardInsertProjectCommand(command);
|
normalizeDistributionBoardStructureProjectCommand(command);
|
||||||
return this.insert(
|
if (normalized.type === distributionBoardInsertCommandType) {
|
||||||
|
assertDistributionBoardInsertProjectCommand(normalized);
|
||||||
|
const inverse = this.insert(
|
||||||
database,
|
database,
|
||||||
projectId,
|
projectId,
|
||||||
command.payload.structure
|
normalized.payload.structure
|
||||||
);
|
);
|
||||||
|
return command.schemaVersion ===
|
||||||
|
legacyDistributionBoardStructureCommandSchemaVersion
|
||||||
|
? toLegacyStructureCommand(inverse)
|
||||||
|
: inverse;
|
||||||
}
|
}
|
||||||
if (command.type === distributionBoardDeleteCommandType) {
|
if (normalized.type === distributionBoardDeleteCommandType) {
|
||||||
assertDistributionBoardDeleteProjectCommand(command);
|
assertDistributionBoardDeleteProjectCommand(normalized);
|
||||||
return this.delete(
|
const inverse = this.delete(
|
||||||
database,
|
database,
|
||||||
projectId,
|
projectId,
|
||||||
command.payload.structure
|
normalized.payload.structure
|
||||||
);
|
);
|
||||||
|
return command.schemaVersion ===
|
||||||
|
legacyDistributionBoardStructureCommandSchemaVersion
|
||||||
|
? toLegacyStructureCommand(inverse)
|
||||||
|
: inverse;
|
||||||
}
|
}
|
||||||
throw new Error("Unsupported distribution-board structure command.");
|
throw new Error("Unsupported distribution-board structure command.");
|
||||||
}
|
}
|
||||||
@@ -73,13 +110,33 @@ export class DistributionBoardStructureProjectCommandRepository
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const project = database
|
const project = database
|
||||||
.select({ id: projects.id })
|
.select({
|
||||||
|
id: projects.id,
|
||||||
|
enabledSupplyTypes:
|
||||||
|
projects.enabledDistributionBoardSupplyTypes,
|
||||||
|
})
|
||||||
.from(projects)
|
.from(projects)
|
||||||
.where(eq(projects.id, projectId))
|
.where(eq(projects.id, projectId))
|
||||||
.get();
|
.get();
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error("Project not found.");
|
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
|
const existingBoard = database
|
||||||
.select({ id: distributionBoards.id })
|
.select({ id: distributionBoards.id })
|
||||||
.from(distributionBoards)
|
.from(distributionBoards)
|
||||||
@@ -179,6 +236,139 @@ export class DistributionBoardStructureProjectCommandRepository
|
|||||||
}
|
}
|
||||||
return createDistributionBoardInsertProjectCommand(structure);
|
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(
|
function structureMatches(
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import type { AppDatabase } from "../database-context.js";
|
|||||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||||
import { consumers } from "../schema/consumers.js";
|
import { consumers } from "../schema/consumers.js";
|
||||||
import { floors } from "../schema/floors.js";
|
import { floors } from "../schema/floors.js";
|
||||||
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
import { projects } from "../schema/projects.js";
|
import { projects } from "../schema/projects.js";
|
||||||
import { rooms } from "../schema/rooms.js";
|
import { rooms } from "../schema/rooms.js";
|
||||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.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."
|
"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
|
const deleted = database
|
||||||
.delete(floors)
|
.delete(floors)
|
||||||
.where(
|
.where(
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { eq } from "drizzle-orm";
|
import { and, eq, isNotNull } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
assertProjectSettingsUpdateProjectCommand,
|
assertProjectSettingsUpdateProjectCommand,
|
||||||
createProjectSettingsUpdateProjectCommand,
|
createProjectSettingsUpdateProjectCommand,
|
||||||
legacyProjectSettingsUpdateCommandSchemaVersion,
|
legacyProjectSettingsUpdateCommandSchemaVersion,
|
||||||
|
normalizeProjectSettingsValues,
|
||||||
|
previousProjectSettingsUpdateCommandSchemaVersion,
|
||||||
type ProjectSettingsValues,
|
type ProjectSettingsValues,
|
||||||
} from "../../domain/models/project-settings-project-command.model.js";
|
} from "../../domain/models/project-settings-project-command.model.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -11,6 +13,7 @@ import type {
|
|||||||
} from "../../domain/ports/project-settings-project-command.store.js";
|
} from "../../domain/ports/project-settings-project-command.store.js";
|
||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { projects } from "../schema/projects.js";
|
import { projects } from "../schema/projects.js";
|
||||||
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
|
|
||||||
export class ProjectSettingsProjectCommandRepository
|
export class ProjectSettingsProjectCommandRepository
|
||||||
@@ -39,18 +42,7 @@ export class ProjectSettingsProjectCommandRepository
|
|||||||
if (!current) {
|
if (!current) {
|
||||||
throw new Error("Project not found.");
|
throw new Error("Project not found.");
|
||||||
}
|
}
|
||||||
const target: ProjectSettingsValues =
|
const target = normalizeProjectSettingsValues(input.command, current);
|
||||||
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;
|
|
||||||
if (projectSettingsEqual(current, target)) {
|
if (projectSettingsEqual(current, target)) {
|
||||||
throw new Error("Project settings did not change.");
|
throw new Error("Project settings did not change.");
|
||||||
}
|
}
|
||||||
@@ -66,7 +58,23 @@ export class ProjectSettingsProjectCommandRepository
|
|||||||
threePhaseVoltageV: current.threePhaseVoltageV,
|
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,
|
name: current.name,
|
||||||
internalProjectNumber: current.internalProjectNumber,
|
internalProjectNumber: current.internalProjectNumber,
|
||||||
externalProjectNumber: current.externalProjectNumber,
|
externalProjectNumber: current.externalProjectNumber,
|
||||||
@@ -74,7 +82,10 @@ export class ProjectSettingsProjectCommandRepository
|
|||||||
description: current.description,
|
description: current.description,
|
||||||
singlePhaseVoltageV: current.singlePhaseVoltageV,
|
singlePhaseVoltageV: current.singlePhaseVoltageV,
|
||||||
threePhaseVoltageV: current.threePhaseVoltageV,
|
threePhaseVoltageV: current.threePhaseVoltageV,
|
||||||
|
enabledDistributionBoardSupplyTypes:
|
||||||
|
current.enabledDistributionBoardSupplyTypes,
|
||||||
});
|
});
|
||||||
|
assertDisabledSupplyTypesAreUnused(tx, input.projectId, target);
|
||||||
const updated = tx
|
const updated = tx
|
||||||
.update(projects)
|
.update(projects)
|
||||||
.set(target)
|
.set(target)
|
||||||
@@ -100,5 +111,54 @@ function projectSettingsEqual(
|
|||||||
current.description === target.description &&
|
current.description === target.description &&
|
||||||
current.singlePhaseVoltageV === target.singlePhaseVoltageV &&
|
current.singlePhaseVoltageV === target.singlePhaseVoltageV &&
|
||||||
current.threePhaseVoltageV === target.threePhaseVoltageV
|
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 { ProjectSnapshotNameConflictError } from "../../domain/errors/project-snapshot-name-conflict.error.js";
|
||||||
import { createProjectStateRestoreCommand } from "../../domain/models/project-state-restore-command.model.js";
|
import { createProjectStateRestoreCommand } from "../../domain/models/project-state-restore-command.model.js";
|
||||||
import {
|
import {
|
||||||
|
distributionBoardProjectStateSnapshotSchemaVersion,
|
||||||
deserializeProjectStateSnapshot,
|
deserializeProjectStateSnapshot,
|
||||||
legacyProjectStateSnapshotSchemaVersion,
|
legacyProjectStateSnapshotSchemaVersion,
|
||||||
|
previousProjectStateSnapshotSchemaVersion,
|
||||||
projectStateSnapshotSchemaVersion,
|
projectStateSnapshotSchemaVersion,
|
||||||
} from "../../domain/models/project-state-snapshot.model.js";
|
} from "../../domain/models/project-state-snapshot.model.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -141,6 +143,9 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
stored.schemaVersion !== legacyProjectStateSnapshotSchemaVersion &&
|
stored.schemaVersion !== legacyProjectStateSnapshotSchemaVersion &&
|
||||||
|
stored.schemaVersion !== previousProjectStateSnapshotSchemaVersion &&
|
||||||
|
stored.schemaVersion !==
|
||||||
|
distributionBoardProjectStateSnapshotSchemaVersion &&
|
||||||
stored.schemaVersion !== projectStateSnapshotSchemaVersion
|
stored.schemaVersion !== projectStateSnapshotSchemaVersion
|
||||||
) {
|
) {
|
||||||
throw new Error("Project snapshot schema version is not supported.");
|
throw new Error("Project snapshot schema version is not supported.");
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ function replaceProjectState(
|
|||||||
description: state.project.description,
|
description: state.project.description,
|
||||||
singlePhaseVoltageV: state.project.singlePhaseVoltageV,
|
singlePhaseVoltageV: state.project.singlePhaseVoltageV,
|
||||||
threePhaseVoltageV: state.project.threePhaseVoltageV,
|
threePhaseVoltageV: state.project.threePhaseVoltageV,
|
||||||
|
enabledDistributionBoardSupplyTypes:
|
||||||
|
state.project.enabledDistributionBoardSupplyTypes,
|
||||||
})
|
})
|
||||||
.where(eq(projects.id, state.project.id))
|
.where(eq(projects.id, state.project.id))
|
||||||
.run();
|
.run();
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export function readProjectStateSnapshot(
|
|||||||
description: projects.description,
|
description: projects.description,
|
||||||
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
||||||
threePhaseVoltageV: projects.threePhaseVoltageV,
|
threePhaseVoltageV: projects.threePhaseVoltageV,
|
||||||
|
enabledDistributionBoardSupplyTypes:
|
||||||
|
projects.enabledDistributionBoardSupplyTypes,
|
||||||
currentRevision: projects.currentRevision,
|
currentRevision: projects.currentRevision,
|
||||||
})
|
})
|
||||||
.from(projects)
|
.from(projects)
|
||||||
@@ -182,6 +184,8 @@ export function readProjectStateSnapshot(
|
|||||||
description: project.description,
|
description: project.description,
|
||||||
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||||
threePhaseVoltageV: project.threePhaseVoltageV,
|
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||||
|
enabledDistributionBoardSupplyTypes:
|
||||||
|
project.enabledDistributionBoardSupplyTypes,
|
||||||
},
|
},
|
||||||
distributionBoards: boardRows,
|
distributionBoards: boardRows,
|
||||||
circuitLists: listRows,
|
circuitLists: listRows,
|
||||||
|
|||||||
@@ -74,12 +74,18 @@ export class ProjectTransferRepository implements ProjectTransferStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private parseVerified(value: unknown) {
|
private parseVerified(value: unknown) {
|
||||||
|
const rawPayloadSha256 =
|
||||||
|
isPlainObject(value) &&
|
||||||
|
isPlainObject(value.projectState)
|
||||||
|
? hashProjectStatePayload(JSON.stringify(value.projectState))
|
||||||
|
: null;
|
||||||
const transfer = parseProjectTransferEnvelope(value);
|
const transfer = parseProjectTransferEnvelope(value);
|
||||||
const payloadJson = serializeProjectStateSnapshot(
|
const payloadJson = serializeProjectStateSnapshot(
|
||||||
transfer.projectState
|
transfer.projectState
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
hashProjectStatePayload(payloadJson) !== transfer.payloadSha256
|
hashProjectStatePayload(payloadJson) !== transfer.payloadSha256 &&
|
||||||
|
rawPayloadSha256 !== transfer.payloadSha256
|
||||||
) {
|
) {
|
||||||
throw new Error("Project transfer checksum verification failed.");
|
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(
|
function insertProjectState(
|
||||||
database: AppDatabase,
|
database: AppDatabase,
|
||||||
state: ReturnType<typeof remapProjectState>
|
state: ReturnType<typeof remapProjectState>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { projects } from "../schema/projects.js";
|
|||||||
import type {
|
import type {
|
||||||
CreateProjectInput,
|
CreateProjectInput,
|
||||||
} from "../../shared/validation/project-structure.schemas.js";
|
} from "../../shared/validation/project-structure.schemas.js";
|
||||||
|
import { defaultDistributionBoardSupplyTypes } from "../../shared/constants/distribution-board.js";
|
||||||
|
|
||||||
export class ProjectRepository {
|
export class ProjectRepository {
|
||||||
constructor(private readonly database: AppDatabase) {}
|
constructor(private readonly database: AppDatabase) {}
|
||||||
@@ -26,6 +27,9 @@ export class ProjectRepository {
|
|||||||
description: input.description ?? null,
|
description: input.description ?? null,
|
||||||
singlePhaseVoltageV: input.singlePhaseVoltageV ?? 230,
|
singlePhaseVoltageV: input.singlePhaseVoltageV ?? 230,
|
||||||
threePhaseVoltageV: input.threePhaseVoltageV ?? 400,
|
threePhaseVoltageV: input.threePhaseVoltageV ?? 400,
|
||||||
|
enabledDistributionBoardSupplyTypes: [
|
||||||
|
...defaultDistributionBoardSupplyTypes,
|
||||||
|
],
|
||||||
currentRevision: 0,
|
currentRevision: 0,
|
||||||
};
|
};
|
||||||
await db.insert(projects).values(project);
|
await db.insert(projects).values(project);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
import { floors } from "./floors.js";
|
||||||
import { projects } from "./projects.js";
|
import { projects } from "./projects.js";
|
||||||
|
import type { DistributionBoardSupplyType } from "../../shared/constants/distribution-board.js";
|
||||||
|
|
||||||
export const distributionBoards = sqliteTable("distribution_boards", {
|
export const distributionBoards = sqliteTable("distribution_boards", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
@@ -7,5 +9,9 @@ export const distributionBoards = sqliteTable("distribution_boards", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => projects.id, { onDelete: "cascade" }),
|
.references(() => projects.id, { onDelete: "cascade" }),
|
||||||
name: text("name").notNull(),
|
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 { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
import type { DistributionBoardSupplyType } from "../../shared/constants/distribution-board.js";
|
||||||
|
|
||||||
export const projects = sqliteTable("projects", {
|
export const projects = sqliteTable("projects", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
@@ -9,5 +11,12 @@ export const projects = sqliteTable("projects", {
|
|||||||
description: text("description"),
|
description: text("description"),
|
||||||
singlePhaseVoltageV: integer("single_phase_voltage_v").notNull().default(230),
|
singlePhaseVoltageV: integer("single_phase_voltage_v").notNull().default(230),
|
||||||
threePhaseVoltageV: integer("three_phase_voltage_v").notNull().default(400),
|
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),
|
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 crypto from "node:crypto";
|
||||||
|
import {
|
||||||
|
distributionBoardSupplyTypes,
|
||||||
|
type DistributionBoardSupplyType,
|
||||||
|
} from "../../shared/constants/distribution-board.js";
|
||||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||||
|
|
||||||
export const distributionBoardInsertCommandType =
|
export const distributionBoardInsertCommandType =
|
||||||
"distribution-board.insert" as const;
|
"distribution-board.insert" as const;
|
||||||
export const distributionBoardDeleteCommandType =
|
export const distributionBoardDeleteCommandType =
|
||||||
"distribution-board.delete" as const;
|
"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 = [
|
export const defaultCircuitSectionDefinitions = [
|
||||||
{
|
{
|
||||||
@@ -34,11 +39,23 @@ export const defaultCircuitSectionDefinitions = [
|
|||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
interface LegacyDistributionBoardStructureSnapshot {
|
||||||
|
distributionBoard: {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
circuitList: DistributionBoardStructureSnapshot["circuitList"];
|
||||||
|
sections: DistributionBoardStructureSnapshot["sections"];
|
||||||
|
}
|
||||||
|
|
||||||
export interface DistributionBoardStructureSnapshot {
|
export interface DistributionBoardStructureSnapshot {
|
||||||
distributionBoard: {
|
distributionBoard: {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
floorId: string | null;
|
||||||
|
supplyType: DistributionBoardSupplyType | null;
|
||||||
};
|
};
|
||||||
circuitList: {
|
circuitList: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -56,29 +73,52 @@ export interface DistributionBoardStructureSnapshot {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DistributionBoardStructureCommandPayload {
|
interface DistributionBoardStructureCommandPayload<
|
||||||
structure: DistributionBoardStructureSnapshot;
|
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
|
export interface DistributionBoardInsertProjectCommand
|
||||||
extends SerializedProjectCommand<DistributionBoardStructureCommandPayload> {
|
extends CurrentDistributionBoardStructureProjectCommand {
|
||||||
schemaVersion: typeof distributionBoardStructureCommandSchemaVersion;
|
|
||||||
type: typeof distributionBoardInsertCommandType;
|
type: typeof distributionBoardInsertCommandType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DistributionBoardDeleteProjectCommand
|
export interface DistributionBoardDeleteProjectCommand
|
||||||
extends SerializedProjectCommand<DistributionBoardStructureCommandPayload> {
|
extends CurrentDistributionBoardStructureProjectCommand {
|
||||||
schemaVersion: typeof distributionBoardStructureCommandSchemaVersion;
|
|
||||||
type: typeof distributionBoardDeleteCommandType;
|
type: typeof distributionBoardDeleteCommandType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DistributionBoardStructureProjectCommand =
|
export type DistributionBoardStructureProjectCommand =
|
||||||
| DistributionBoardInsertProjectCommand
|
| DistributionBoardInsertProjectCommand
|
||||||
| DistributionBoardDeleteProjectCommand;
|
| DistributionBoardDeleteProjectCommand
|
||||||
|
| LegacyDistributionBoardStructureProjectCommand;
|
||||||
|
|
||||||
export function createDistributionBoardStructureSnapshot(
|
export function createDistributionBoardStructureSnapshot(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
name: string
|
name: string,
|
||||||
|
input: {
|
||||||
|
floorId?: string | null;
|
||||||
|
supplyType?: DistributionBoardSupplyType | null;
|
||||||
|
} = {}
|
||||||
): DistributionBoardStructureSnapshot {
|
): DistributionBoardStructureSnapshot {
|
||||||
const normalizedName = name.trim();
|
const normalizedName = name.trim();
|
||||||
assertNonEmptyString(projectId, "projectId");
|
assertNonEmptyString(projectId, "projectId");
|
||||||
@@ -89,6 +129,8 @@ export function createDistributionBoardStructureSnapshot(
|
|||||||
id: structureId,
|
id: structureId,
|
||||||
projectId,
|
projectId,
|
||||||
name: normalizedName,
|
name: normalizedName,
|
||||||
|
floorId: input.floorId ?? null,
|
||||||
|
supplyType: input.supplyType ?? null,
|
||||||
},
|
},
|
||||||
circuitList: {
|
circuitList: {
|
||||||
id: structureId,
|
id: structureId,
|
||||||
@@ -132,7 +174,7 @@ export function createDistributionBoardDeleteProjectCommand(
|
|||||||
|
|
||||||
export function assertDistributionBoardInsertProjectCommand(
|
export function assertDistributionBoardInsertProjectCommand(
|
||||||
command: SerializedProjectCommand<unknown>
|
command: SerializedProjectCommand<unknown>
|
||||||
): asserts command is DistributionBoardInsertProjectCommand {
|
): asserts command is DistributionBoardStructureProjectCommand {
|
||||||
assertDistributionBoardStructureProjectCommand(
|
assertDistributionBoardStructureProjectCommand(
|
||||||
command,
|
command,
|
||||||
distributionBoardInsertCommandType
|
distributionBoardInsertCommandType
|
||||||
@@ -141,23 +183,88 @@ export function assertDistributionBoardInsertProjectCommand(
|
|||||||
|
|
||||||
export function assertDistributionBoardDeleteProjectCommand(
|
export function assertDistributionBoardDeleteProjectCommand(
|
||||||
command: SerializedProjectCommand<unknown>
|
command: SerializedProjectCommand<unknown>
|
||||||
): asserts command is DistributionBoardDeleteProjectCommand {
|
): asserts command is DistributionBoardStructureProjectCommand {
|
||||||
assertDistributionBoardStructureProjectCommand(
|
assertDistributionBoardStructureProjectCommand(
|
||||||
command,
|
command,
|
||||||
distributionBoardDeleteCommandType
|
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(
|
export function assertDistributionBoardStructureSnapshot(
|
||||||
structure: unknown
|
structure: unknown
|
||||||
): asserts structure is DistributionBoardStructureSnapshot {
|
): 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) {
|
if (!isPlainObject(structure) || Object.keys(structure).length !== 3) {
|
||||||
throw new Error("Distribution-board structure is invalid.");
|
throw new Error("Distribution-board structure is invalid.");
|
||||||
}
|
}
|
||||||
const { distributionBoard, circuitList, sections } = structure;
|
const { distributionBoard, circuitList, sections } = structure;
|
||||||
if (
|
if (
|
||||||
!isPlainObject(distributionBoard) ||
|
!isPlainObject(distributionBoard) ||
|
||||||
Object.keys(distributionBoard).length !== 3 ||
|
Object.keys(distributionBoard).length !== (legacy ? 3 : 5) ||
|
||||||
!isPlainObject(circuitList) ||
|
!isPlainObject(circuitList) ||
|
||||||
Object.keys(circuitList).length !== 4 ||
|
Object.keys(circuitList).length !== 4 ||
|
||||||
!Array.isArray(sections) ||
|
!Array.isArray(sections) ||
|
||||||
@@ -165,8 +272,15 @@ export function assertDistributionBoardStructureSnapshot(
|
|||||||
) {
|
) {
|
||||||
throw new Error("Distribution-board structure is incomplete.");
|
throw new Error("Distribution-board structure is incomplete.");
|
||||||
}
|
}
|
||||||
for (const [field, value] of Object.entries(distributionBoard)) {
|
for (const field of ["id", "projectId", "name"] as const) {
|
||||||
assertNonEmptyString(value, `distributionBoard.${field}`);
|
assertNonEmptyString(
|
||||||
|
distributionBoard[field],
|
||||||
|
`distributionBoard.${field}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!legacy) {
|
||||||
|
assertNullableId(distributionBoard.floorId, "distributionBoard.floorId");
|
||||||
|
assertNullableSupplyType(distributionBoard.supplyType);
|
||||||
}
|
}
|
||||||
for (const [field, value] of Object.entries(circuitList)) {
|
for (const [field, value] of Object.entries(circuitList)) {
|
||||||
assertNonEmptyString(value, `circuitList.${field}`);
|
assertNonEmptyString(value, `circuitList.${field}`);
|
||||||
@@ -174,22 +288,16 @@ export function assertDistributionBoardStructureSnapshot(
|
|||||||
if (
|
if (
|
||||||
circuitList.projectId !== distributionBoard.projectId ||
|
circuitList.projectId !== distributionBoard.projectId ||
|
||||||
circuitList.distributionBoardId !== distributionBoard.id ||
|
circuitList.distributionBoardId !== distributionBoard.id ||
|
||||||
circuitList.name !==
|
circuitList.name !== `${distributionBoard.name} Stromkreisliste`
|
||||||
`${distributionBoard.name} Stromkreisliste`
|
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error("Distribution board and circuit list are inconsistent.");
|
||||||
"Distribution board and circuit list are inconsistent."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sectionIds = new Set<string>();
|
const sectionIds = new Set<string>();
|
||||||
for (let index = 0; index < sections.length; index += 1) {
|
for (let index = 0; index < sections.length; index += 1) {
|
||||||
const section = sections[index];
|
const section = sections[index];
|
||||||
const expected = defaultCircuitSectionDefinitions[index];
|
const expected = defaultCircuitSectionDefinitions[index];
|
||||||
if (
|
if (!isPlainObject(section) || Object.keys(section).length !== 6) {
|
||||||
!isPlainObject(section) ||
|
|
||||||
Object.keys(section).length !== 6
|
|
||||||
) {
|
|
||||||
throw new Error("Default circuit section is invalid.");
|
throw new Error("Default circuit section is invalid.");
|
||||||
}
|
}
|
||||||
assertNonEmptyString(section.id, "section.id");
|
assertNonEmptyString(section.id, "section.id");
|
||||||
@@ -211,22 +319,23 @@ export function assertDistributionBoardStructureSnapshot(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertDistributionBoardStructureProjectCommand(
|
function assertNullableId(value: unknown, field: string) {
|
||||||
command: SerializedProjectCommand<unknown>,
|
if (value !== null) {
|
||||||
expectedType:
|
assertNonEmptyString(value, field);
|
||||||
| typeof distributionBoardInsertCommandType
|
}
|
||||||
| typeof distributionBoardDeleteCommandType
|
}
|
||||||
) {
|
|
||||||
if (
|
function assertNullableSupplyType(
|
||||||
command.schemaVersion !==
|
value: unknown
|
||||||
distributionBoardStructureCommandSchemaVersion ||
|
): asserts value is DistributionBoardSupplyType | null {
|
||||||
command.type !== expectedType ||
|
if (
|
||||||
!isPlainObject(command.payload) ||
|
value !== null &&
|
||||||
Object.keys(command.payload).length !== 1
|
!distributionBoardSupplyTypes.includes(
|
||||||
) {
|
value as DistributionBoardSupplyType
|
||||||
throw new Error("Unsupported distribution-board structure command.");
|
)
|
||||||
|
) {
|
||||||
|
throw new Error("distributionBoard.supplyType is invalid.");
|
||||||
}
|
}
|
||||||
assertDistributionBoardStructureSnapshot(command.payload.structure);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertNonEmptyString(
|
function assertNonEmptyString(
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||||
|
import {
|
||||||
|
distributionBoardSupplyTypes,
|
||||||
|
type DistributionBoardSupplyType,
|
||||||
|
} from "../../shared/constants/distribution-board.js";
|
||||||
|
|
||||||
export const projectSettingsUpdateCommandType =
|
export const projectSettingsUpdateCommandType =
|
||||||
"project.update-settings" as const;
|
"project.update-settings" as const;
|
||||||
export const legacyProjectSettingsUpdateCommandSchemaVersion = 1 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 {
|
export interface LegacyProjectSettingsValues {
|
||||||
singlePhaseVoltageV: number;
|
singlePhaseVoltageV: number;
|
||||||
threePhaseVoltageV: number;
|
threePhaseVoltageV: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectSettingsValues extends LegacyProjectSettingsValues {
|
export interface PreviousProjectSettingsValues extends LegacyProjectSettingsValues {
|
||||||
name: string;
|
name: string;
|
||||||
internalProjectNumber: string | null;
|
internalProjectNumber: string | null;
|
||||||
externalProjectNumber: string | null;
|
externalProjectNumber: string | null;
|
||||||
@@ -18,6 +23,10 @@ export interface ProjectSettingsValues extends LegacyProjectSettingsValues {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectSettingsValues extends PreviousProjectSettingsValues {
|
||||||
|
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface LegacyProjectSettingsUpdateProjectCommand
|
export interface LegacyProjectSettingsUpdateProjectCommand
|
||||||
extends SerializedProjectCommand<LegacyProjectSettingsValues> {
|
extends SerializedProjectCommand<LegacyProjectSettingsValues> {
|
||||||
schemaVersion: typeof legacyProjectSettingsUpdateCommandSchemaVersion;
|
schemaVersion: typeof legacyProjectSettingsUpdateCommandSchemaVersion;
|
||||||
@@ -30,8 +39,15 @@ export interface CurrentProjectSettingsUpdateProjectCommand
|
|||||||
type: typeof projectSettingsUpdateCommandType;
|
type: typeof projectSettingsUpdateCommandType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PreviousProjectSettingsUpdateProjectCommand
|
||||||
|
extends SerializedProjectCommand<PreviousProjectSettingsValues> {
|
||||||
|
schemaVersion: typeof previousProjectSettingsUpdateCommandSchemaVersion;
|
||||||
|
type: typeof projectSettingsUpdateCommandType;
|
||||||
|
}
|
||||||
|
|
||||||
export type ProjectSettingsUpdateProjectCommand =
|
export type ProjectSettingsUpdateProjectCommand =
|
||||||
| LegacyProjectSettingsUpdateProjectCommand
|
| LegacyProjectSettingsUpdateProjectCommand
|
||||||
|
| PreviousProjectSettingsUpdateProjectCommand
|
||||||
| CurrentProjectSettingsUpdateProjectCommand;
|
| CurrentProjectSettingsUpdateProjectCommand;
|
||||||
|
|
||||||
export function createProjectSettingsUpdateProjectCommand(
|
export function createProjectSettingsUpdateProjectCommand(
|
||||||
@@ -53,6 +69,8 @@ export function assertProjectSettingsUpdateProjectCommand(
|
|||||||
command.type !== projectSettingsUpdateCommandType ||
|
command.type !== projectSettingsUpdateCommandType ||
|
||||||
(command.schemaVersion !==
|
(command.schemaVersion !==
|
||||||
legacyProjectSettingsUpdateCommandSchemaVersion &&
|
legacyProjectSettingsUpdateCommandSchemaVersion &&
|
||||||
|
command.schemaVersion !==
|
||||||
|
previousProjectSettingsUpdateCommandSchemaVersion &&
|
||||||
command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion)
|
command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion)
|
||||||
) {
|
) {
|
||||||
throw new Error("Unsupported project settings update command.");
|
throw new Error("Unsupported project settings update command.");
|
||||||
@@ -66,6 +84,10 @@ export function assertProjectSettingsUpdateProjectCommand(
|
|||||||
assertLegacyValues(command.payload);
|
assertLegacyValues(command.payload);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const expectedKeyCount =
|
||||||
|
command.schemaVersion === previousProjectSettingsUpdateCommandSchemaVersion
|
||||||
|
? 7
|
||||||
|
: 8;
|
||||||
if (
|
if (
|
||||||
!isTrimmedString(command.payload.name, 1, 200) ||
|
!isTrimmedString(command.payload.name, 1, 200) ||
|
||||||
!isNullableTrimmedString(command.payload.internalProjectNumber, 100) ||
|
!isNullableTrimmedString(command.payload.internalProjectNumber, 100) ||
|
||||||
@@ -74,12 +96,61 @@ export function assertProjectSettingsUpdateProjectCommand(
|
|||||||
!isNullableTrimmedString(command.payload.description, 2000) ||
|
!isNullableTrimmedString(command.payload.description, 2000) ||
|
||||||
!isPositiveFiniteNumber(command.payload.singlePhaseVoltageV) ||
|
!isPositiveFiniteNumber(command.payload.singlePhaseVoltageV) ||
|
||||||
!isPositiveFiniteNumber(command.payload.threePhaseVoltageV) ||
|
!isPositiveFiniteNumber(command.payload.threePhaseVoltageV) ||
|
||||||
Object.keys(command.payload).length !== 7
|
Object.keys(command.payload).length !== expectedKeyCount
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Project settings update command contains invalid values."
|
"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(
|
function assertLegacyValues(
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
defaultDistributionBoardSupplyTypes,
|
||||||
|
distributionBoardSupplyTypes,
|
||||||
|
} from "../../shared/constants/distribution-board.js";
|
||||||
|
|
||||||
export const legacyProjectStateSnapshotSchemaVersion = 1 as const;
|
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 idSchema = z.string().trim().min(1);
|
||||||
const nullableStringSchema = z.string().nullable();
|
const nullableStringSchema = z.string().nullable();
|
||||||
@@ -23,7 +29,14 @@ const projectSchema = legacyProjectSchema.extend({
|
|||||||
description: z.string().trim().min(1).max(2000).nullable(),
|
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({
|
.object({
|
||||||
id: idSchema,
|
id: idSchema,
|
||||||
projectId: idSchema,
|
projectId: idSchema,
|
||||||
@@ -31,6 +44,13 @@ const distributionBoardSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
const distributionBoardSchema = legacyDistributionBoardSchema
|
||||||
|
.extend({
|
||||||
|
floorId: idSchema.nullable(),
|
||||||
|
supplyType: z.enum(distributionBoardSupplyTypes).nullable(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
const circuitListSchema = z
|
const circuitListSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: idSchema,
|
id: idSchema,
|
||||||
@@ -140,8 +160,7 @@ const roomSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
const projectStateSnapshotContents = {
|
const commonProjectStateSnapshotContents = {
|
||||||
distributionBoards: z.array(distributionBoardSchema),
|
|
||||||
circuitLists: z.array(circuitListSchema),
|
circuitLists: z.array(circuitListSchema),
|
||||||
circuitSections: z.array(circuitSectionSchema),
|
circuitSections: z.array(circuitSectionSchema),
|
||||||
circuits: z.array(circuitSchema),
|
circuits: z.array(circuitSchema),
|
||||||
@@ -154,15 +173,37 @@ const legacyProjectStateSnapshotSchema = z
|
|||||||
.object({
|
.object({
|
||||||
schemaVersion: z.literal(legacyProjectStateSnapshotSchemaVersion),
|
schemaVersion: z.literal(legacyProjectStateSnapshotSchemaVersion),
|
||||||
project: legacyProjectSchema,
|
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();
|
.strict();
|
||||||
|
|
||||||
export const projectStateSnapshotSchema = z
|
export const projectStateSnapshotSchema = z
|
||||||
.object({
|
.object({
|
||||||
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
|
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
|
||||||
project: projectSchema,
|
project: currentProjectSchema,
|
||||||
...projectStateSnapshotContents,
|
distributionBoards: z.array(distributionBoardSchema),
|
||||||
|
...commonProjectStateSnapshotContents,
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
@@ -176,20 +217,34 @@ export function parseProjectStateSnapshot(
|
|||||||
const version = isPlainObject(value) ? value.schemaVersion : undefined;
|
const version = isPlainObject(value) ? value.schemaVersion : undefined;
|
||||||
const snapshot =
|
const snapshot =
|
||||||
version === legacyProjectStateSnapshotSchemaVersion
|
version === legacyProjectStateSnapshotSchemaVersion
|
||||||
? upgradeLegacyProjectStateSnapshot(
|
? upgradeDistributionBoardProjectStateSnapshot(
|
||||||
legacyProjectStateSnapshotSchema.parse(value)
|
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);
|
assertProjectStateSnapshotRelations(snapshot);
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
function upgradeLegacyProjectStateSnapshot(
|
function upgradeLegacyProjectStateSnapshot(
|
||||||
snapshot: z.infer<typeof legacyProjectStateSnapshotSchema>
|
snapshot: z.infer<typeof legacyProjectStateSnapshotSchema>
|
||||||
): ProjectStateSnapshot {
|
): z.infer<typeof previousProjectStateSnapshotSchema> {
|
||||||
return {
|
return {
|
||||||
...snapshot,
|
...snapshot,
|
||||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
schemaVersion: previousProjectStateSnapshotSchemaVersion,
|
||||||
project: {
|
project: {
|
||||||
...snapshot.project,
|
...snapshot.project,
|
||||||
internalProjectNumber: null,
|
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> {
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
@@ -245,6 +329,23 @@ function assertProjectStateSnapshotRelations(
|
|||||||
|
|
||||||
for (const board of snapshot.distributionBoards) {
|
for (const board of snapshot.distributionBoards) {
|
||||||
assertProjectOwnership(board.projectId, projectId, "distribution board");
|
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) {
|
for (const list of snapshot.circuitLists) {
|
||||||
assertProjectOwnership(list.projectId, projectId, "circuit list");
|
assertProjectOwnership(list.projectId, projectId, "circuit list");
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ export function remapProjectState(
|
|||||||
...board,
|
...board,
|
||||||
id: requiredId(boardIds, board.id),
|
id: requiredId(boardIds, board.id),
|
||||||
projectId: targetProjectId,
|
projectId: targetProjectId,
|
||||||
|
floorId:
|
||||||
|
board.floorId === null
|
||||||
|
? null
|
||||||
|
: requiredId(floorIds, board.floorId),
|
||||||
})),
|
})),
|
||||||
circuitLists: source.circuitLists.map((list) => ({
|
circuitLists: source.circuitLists.map((list) => ({
|
||||||
...list,
|
...list,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { DistributionBoardStructureProjectCommand } from "../models/distribution-board-structure-project-command.model.js";
|
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 {
|
import type {
|
||||||
AppendedProjectRevision,
|
AppendedProjectRevision,
|
||||||
ProjectRevisionSource,
|
ProjectRevisionSource,
|
||||||
@@ -23,4 +24,15 @@ export interface DistributionBoardStructureProjectCommandStore {
|
|||||||
execute(
|
execute(
|
||||||
input: ExecuteDistributionBoardStructureCommandInput
|
input: ExecuteDistributionBoardStructureCommandInput
|
||||||
): ExecutedDistributionBoardStructureCommand;
|
): ExecutedDistributionBoardStructureCommand;
|
||||||
|
executeUpdate(
|
||||||
|
input: Omit<
|
||||||
|
ExecuteDistributionBoardStructureCommandInput,
|
||||||
|
"command"
|
||||||
|
> & {
|
||||||
|
command: DistributionBoardUpdateProjectCommand;
|
||||||
|
}
|
||||||
|
): {
|
||||||
|
revision: AppendedProjectRevision;
|
||||||
|
inverse: DistributionBoardUpdateProjectCommand;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ import {
|
|||||||
circuitDeleteCommandType,
|
circuitDeleteCommandType,
|
||||||
circuitInsertCommandType,
|
circuitInsertCommandType,
|
||||||
} from "../models/circuit-structure-project-command.model.js";
|
} from "../models/circuit-structure-project-command.model.js";
|
||||||
|
import {
|
||||||
|
assertDistributionBoardUpdateProjectCommand,
|
||||||
|
distributionBoardUpdateCommandType,
|
||||||
|
} from "../models/distribution-board-project-command.model.js";
|
||||||
import {
|
import {
|
||||||
assertDistributionBoardDeleteProjectCommand,
|
assertDistributionBoardDeleteProjectCommand,
|
||||||
assertDistributionBoardInsertProjectCommand,
|
assertDistributionBoardInsertProjectCommand,
|
||||||
@@ -285,6 +289,13 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
|||||||
command: input.command,
|
command: input.command,
|
||||||
}).revision;
|
}).revision;
|
||||||
}
|
}
|
||||||
|
case distributionBoardUpdateCommandType: {
|
||||||
|
assertDistributionBoardUpdateProjectCommand(input.command);
|
||||||
|
return this.distributionBoardStructureStore.executeUpdate({
|
||||||
|
...input,
|
||||||
|
command: input.command,
|
||||||
|
}).revision;
|
||||||
|
}
|
||||||
case projectFloorInsertCommandType: {
|
case projectFloorInsertCommandType: {
|
||||||
assertProjectFloorInsertProjectCommand(input.command);
|
assertProjectFloorInsertProjectCommand(input.command);
|
||||||
return this.projectLocationStructureStore.execute({
|
return this.projectLocationStructureStore.execute({
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FormEvent, useState } from "react";
|
import React, { FormEvent, useState } from "react";
|
||||||
import type { ProjectDto } from "../types";
|
import type { ProjectDto } from "../types";
|
||||||
|
import {
|
||||||
|
distributionBoardSupplyTypeLabels,
|
||||||
|
distributionBoardSupplyTypes,
|
||||||
|
type DistributionBoardSupplyType,
|
||||||
|
} from "../../shared/constants/distribution-board";
|
||||||
|
|
||||||
export interface ProjectSettingsInput {
|
export interface ProjectSettingsInput {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -11,6 +16,7 @@ export interface ProjectSettingsInput {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
singlePhaseVoltageV: number;
|
singlePhaseVoltageV: number;
|
||||||
threePhaseVoltageV: number;
|
threePhaseVoltageV: number;
|
||||||
|
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectSettingsModalProps {
|
interface ProjectSettingsModalProps {
|
||||||
@@ -23,6 +29,7 @@ interface ProjectSettingsModalProps {
|
|||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
onSave: (input: ProjectSettingsInput) => Promise<void>;
|
onSave: (input: ProjectSettingsInput) => Promise<void>;
|
||||||
project: ProjectDto;
|
project: ProjectDto;
|
||||||
|
usedDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectSettingsModal({
|
export function ProjectSettingsModal({
|
||||||
@@ -32,6 +39,7 @@ export function ProjectSettingsModal({
|
|||||||
onImport,
|
onImport,
|
||||||
onSave,
|
onSave,
|
||||||
project,
|
project,
|
||||||
|
usedDistributionBoardSupplyTypes,
|
||||||
}: ProjectSettingsModalProps) {
|
}: ProjectSettingsModalProps) {
|
||||||
const [name, setName] = useState(project.name);
|
const [name, setName] = useState(project.name);
|
||||||
const [internalProjectNumber, setInternalProjectNumber] = useState(
|
const [internalProjectNumber, setInternalProjectNumber] = useState(
|
||||||
@@ -52,6 +60,12 @@ export function ProjectSettingsModal({
|
|||||||
const [threePhaseVoltageV, setThreePhaseVoltageV] = useState(
|
const [threePhaseVoltageV, setThreePhaseVoltageV] = useState(
|
||||||
String(project.threePhaseVoltageV)
|
String(project.threePhaseVoltageV)
|
||||||
);
|
);
|
||||||
|
const [
|
||||||
|
enabledDistributionBoardSupplyTypes,
|
||||||
|
setEnabledDistributionBoardSupplyTypes,
|
||||||
|
] = useState<DistributionBoardSupplyType[]>(
|
||||||
|
project.enabledDistributionBoardSupplyTypes
|
||||||
|
);
|
||||||
const [transfer, setTransfer] = useState<unknown>(null);
|
const [transfer, setTransfer] = useState<unknown>(null);
|
||||||
const [transferFilename, setTransferFilename] = useState("");
|
const [transferFilename, setTransferFilename] = useState("");
|
||||||
const [importMode, setImportMode] = useState<"replace" | "duplicate">(
|
const [importMode, setImportMode] = useState<"replace" | "duplicate">(
|
||||||
@@ -70,13 +84,31 @@ export function ProjectSettingsModal({
|
|||||||
description: toNullableString(description),
|
description: toNullableString(description),
|
||||||
singlePhaseVoltageV: Number(singlePhaseVoltageV),
|
singlePhaseVoltageV: Number(singlePhaseVoltageV),
|
||||||
threePhaseVoltageV: Number(threePhaseVoltageV),
|
threePhaseVoltageV: Number(threePhaseVoltageV),
|
||||||
|
enabledDistributionBoardSupplyTypes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValid =
|
const isValid =
|
||||||
name.trim().length > 0 &&
|
name.trim().length > 0 &&
|
||||||
Number(singlePhaseVoltageV) > 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) {
|
async function handleTransferFile(file: File | undefined) {
|
||||||
setTransfer(null);
|
setTransfer(null);
|
||||||
@@ -165,6 +197,53 @@ export function ProjectSettingsModal({
|
|||||||
value={externalProjectNumber}
|
value={externalProjectNumber}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="col-12">
|
||||||
<label className="form-label" htmlFor="building-owner">
|
<label className="form-label" htmlFor="building-owner">
|
||||||
Bauherr
|
Bauherr
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { DistributionBoardSupplyType } from "../shared/constants/distribution-board";
|
||||||
|
|
||||||
export interface ProjectDto {
|
export interface ProjectDto {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -7,6 +9,7 @@ export interface ProjectDto {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
singlePhaseVoltageV: number;
|
singlePhaseVoltageV: number;
|
||||||
threePhaseVoltageV: number;
|
threePhaseVoltageV: number;
|
||||||
|
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
|
||||||
currentRevision: number;
|
currentRevision: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +92,8 @@ export interface DistributionBoardDto {
|
|||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
floorId: string | null;
|
||||||
|
supplyType: DistributionBoardSupplyType | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DistributionBoardCommandResultDto
|
export interface DistributionBoardCommandResultDto
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ export function updateProjectSettings(
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
singlePhaseVoltageV: number;
|
singlePhaseVoltageV: number;
|
||||||
threePhaseVoltageV: number;
|
threePhaseVoltageV: number;
|
||||||
|
enabledDistributionBoardSupplyTypes: ProjectDto["enabledDistributionBoardSupplyTypes"];
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
return request<ProjectSettingsCommandResultDto>(
|
return request<ProjectSettingsCommandResultDto>(
|
||||||
@@ -276,14 +277,36 @@ export function listDistributionBoards(projectId: string) {
|
|||||||
|
|
||||||
export function createDistributionBoard(
|
export function createDistributionBoard(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
name: string,
|
input: {
|
||||||
|
name: string;
|
||||||
|
floorId: string | null;
|
||||||
|
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
|
||||||
|
},
|
||||||
expectedRevision: number
|
expectedRevision: number
|
||||||
) {
|
) {
|
||||||
return request<DistributionBoardCommandResultDto>(
|
return request<DistributionBoardCommandResultDto>(
|
||||||
`/api/projects/${projectId}/distribution-boards`,
|
`/api/projects/${projectId}/distribution-boards`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
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-device.sync-rows": "Projektgerät-Verknüpfungen geändert",
|
||||||
"project.update-settings": "Projekteinstellungen bearbeitet",
|
"project.update-settings": "Projekteinstellungen bearbeitet",
|
||||||
"distribution-board.insert": "Verteilung angelegt",
|
"distribution-board.insert": "Verteilung angelegt",
|
||||||
|
"distribution-board.update": "Verteilung bearbeitet",
|
||||||
"distribution-board.delete": "Verteilung entfernt",
|
"distribution-board.delete": "Verteilung entfernt",
|
||||||
"project-floor.insert": "Geschoss angelegt",
|
"project-floor.insert": "Geschoss angelegt",
|
||||||
"project-floor.delete": "Geschoss entfernt",
|
"project-floor.delete": "Geschoss entfernt",
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
|
import { createDistributionBoardUpdateProjectCommand } from "../../domain/models/distribution-board-project-command.model.js";
|
||||||
import {
|
import {
|
||||||
createDistributionBoardInsertProjectCommand,
|
createDistributionBoardInsertProjectCommand,
|
||||||
createDistributionBoardStructureSnapshot,
|
createDistributionBoardStructureSnapshot,
|
||||||
} from "../../domain/models/distribution-board-structure-project-command.model.js";
|
} 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 { 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";
|
||||||
@@ -31,7 +35,11 @@ export async function createDistributionBoard(req: Request, res: Response) {
|
|||||||
|
|
||||||
const structure = createDistributionBoardStructureSnapshot(
|
const structure = createDistributionBoardStructureSnapshot(
|
||||||
projectId,
|
projectId,
|
||||||
parsed.data.name
|
parsed.data.name,
|
||||||
|
{
|
||||||
|
floorId: parsed.data.floorId,
|
||||||
|
supplyType: parsed.data.supplyType,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
const result = projectCommandService.executeUser({
|
const result = projectCommandService.executeUser({
|
||||||
@@ -49,3 +57,43 @@ export async function createDistributionBoard(req: Request, res: Response) {
|
|||||||
return respondWithProjectCommandError(error, res);
|
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 {
|
import {
|
||||||
createDistributionBoard,
|
createDistributionBoard,
|
||||||
listDistributionBoardsByProject,
|
listDistributionBoardsByProject,
|
||||||
|
updateDistributionBoard,
|
||||||
} from "../controllers/distribution-board.controller.js";
|
} from "../controllers/distribution-board.controller.js";
|
||||||
import { listCircuitListsByProject } from "../controllers/circuit-list.controller.js";
|
import { listCircuitListsByProject } from "../controllers/circuit-list.controller.js";
|
||||||
import { createFloor, listFloorsByProject } from "../controllers/floor.controller.js";
|
import { createFloor, listFloorsByProject } from "../controllers/floor.controller.js";
|
||||||
@@ -55,6 +56,10 @@ 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.put(
|
||||||
|
"/:projectId/distribution-boards/:distributionBoardId",
|
||||||
|
updateDistributionBoard
|
||||||
|
);
|
||||||
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);
|
||||||
|
|||||||
@@ -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 { z } from "zod";
|
||||||
|
import { distributionBoardSupplyTypes } from "../constants/distribution-board.js";
|
||||||
import { expectedProjectRevisionSchema } from "./project-command.schemas.js";
|
import { expectedProjectRevisionSchema } from "./project-command.schemas.js";
|
||||||
|
|
||||||
export const createProjectSchema = z.object({
|
export const createProjectSchema = z.object({
|
||||||
@@ -21,6 +22,12 @@ export const updateProjectSettingsSchema = z
|
|||||||
description: z.string().trim().max(2000).nullable(),
|
description: z.string().trim().max(2000).nullable(),
|
||||||
singlePhaseVoltageV: z.number().positive(),
|
singlePhaseVoltageV: z.number().positive(),
|
||||||
threePhaseVoltageV: 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();
|
.strict();
|
||||||
|
|
||||||
@@ -28,6 +35,16 @@ export const createDistributionBoardSchema = z
|
|||||||
.object({
|
.object({
|
||||||
expectedRevision: expectedProjectRevisionSchema,
|
expectedRevision: expectedProjectRevisionSchema,
|
||||||
name: z.string().trim().min(1),
|
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();
|
.strict();
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,20 @@ import { circuitLists } from "../src/db/schema/circuit-lists.js";
|
|||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
import { circuits } from "../src/db/schema/circuits.js";
|
import { circuits } from "../src/db/schema/circuits.js";
|
||||||
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
|
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
|
||||||
|
import { floors } from "../src/db/schema/floors.js";
|
||||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||||
import { projects } from "../src/db/schema/projects.js";
|
import { projects } from "../src/db/schema/projects.js";
|
||||||
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
||||||
import {
|
import {
|
||||||
createDistributionBoardInsertProjectCommand,
|
createDistributionBoardInsertProjectCommand,
|
||||||
createDistributionBoardStructureSnapshot,
|
createDistributionBoardStructureSnapshot,
|
||||||
|
type DistributionBoardStructureProjectCommand,
|
||||||
} from "../src/domain/models/distribution-board-structure-project-command.model.js";
|
} from "../src/domain/models/distribution-board-structure-project-command.model.js";
|
||||||
import { createDistributionBoard } from "../src/frontend/utils/api.js";
|
import { createDistributionBoardUpdateProjectCommand } from "../src/domain/models/distribution-board-project-command.model.js";
|
||||||
|
import {
|
||||||
|
createDistributionBoard,
|
||||||
|
updateDistributionBoard,
|
||||||
|
} from "../src/frontend/utils/api.js";
|
||||||
|
|
||||||
function createTestDatabase(): DatabaseContext {
|
function createTestDatabase(): DatabaseContext {
|
||||||
const context = createDatabaseContext(":memory:");
|
const context = createDatabaseContext(":memory:");
|
||||||
@@ -50,9 +56,11 @@ describe("distribution-board structure project command", () => {
|
|||||||
it("builds the fixed setup and sends the expected frontend revision", async () => {
|
it("builds the fixed setup and sends the expected frontend revision", async () => {
|
||||||
const structure = createDistributionBoardStructureSnapshot(
|
const structure = createDistributionBoardStructureSnapshot(
|
||||||
"project-1",
|
"project-1",
|
||||||
" UV-02 "
|
" UV-02 ",
|
||||||
|
{ floorId: null, supplyType: "SV" }
|
||||||
);
|
);
|
||||||
assert.equal(structure.distributionBoard.name, "UV-02");
|
assert.equal(structure.distributionBoard.name, "UV-02");
|
||||||
|
assert.equal(structure.distributionBoard.supplyType, "SV");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
structure.circuitList.name,
|
structure.circuitList.name,
|
||||||
"UV-02 Stromkreisliste"
|
"UV-02 Stromkreisliste"
|
||||||
@@ -91,14 +99,27 @@ describe("distribution-board structure project command", () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await createDistributionBoard("project-1", "UV-02", 7);
|
await createDistributionBoard(
|
||||||
|
"project-1",
|
||||||
|
{
|
||||||
|
name: "UV-02",
|
||||||
|
floorId: null,
|
||||||
|
supplyType: "SV",
|
||||||
|
},
|
||||||
|
7
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
}
|
}
|
||||||
assert.deepEqual(requests, [
|
assert.deepEqual(requests, [
|
||||||
{
|
{
|
||||||
url: "/api/projects/project-1/distribution-boards",
|
url: "/api/projects/project-1/distribution-boards",
|
||||||
body: { name: "UV-02", expectedRevision: 7 },
|
body: {
|
||||||
|
name: "UV-02",
|
||||||
|
floorId: null,
|
||||||
|
supplyType: "SV",
|
||||||
|
expectedRevision: 7,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -178,6 +199,239 @@ describe("distribution-board structure project command", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows only supply types enabled in the project", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
context.db
|
||||||
|
.update(projects)
|
||||||
|
.set({
|
||||||
|
enabledDistributionBoardSupplyTypes: ["MSR", "SiBe"],
|
||||||
|
})
|
||||||
|
.where(eq(projects.id, "project-1"))
|
||||||
|
.run();
|
||||||
|
const repository =
|
||||||
|
new DistributionBoardStructureProjectCommandRepository(
|
||||||
|
context.db
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createDistributionBoardInsertProjectCommand(
|
||||||
|
createDistributionBoardStructureSnapshot(
|
||||||
|
"project-1",
|
||||||
|
"UV AV",
|
||||||
|
{ floorId: null, supplyType: "AV" }
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
/nicht aktiviert/
|
||||||
|
);
|
||||||
|
const structure = createDistributionBoardStructureSnapshot(
|
||||||
|
"project-1",
|
||||||
|
"UV SiBe",
|
||||||
|
{ floorId: null, supplyType: "SiBe" }
|
||||||
|
);
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createDistributionBoardInsertProjectCommand(structure),
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
context.db.select().from(distributionBoards).get()?.supplyType,
|
||||||
|
"SiBe"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies stored version-one setup commands with unassigned new fields", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const repository =
|
||||||
|
new DistributionBoardStructureProjectCommandRepository(
|
||||||
|
context.db
|
||||||
|
);
|
||||||
|
const current = createDistributionBoardStructureSnapshot(
|
||||||
|
"project-1",
|
||||||
|
"UV Alt",
|
||||||
|
{ floorId: null, supplyType: null }
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
floorId: _floorId,
|
||||||
|
supplyType: _supplyType,
|
||||||
|
...legacyBoard
|
||||||
|
} = current.distributionBoard;
|
||||||
|
const legacyCommand = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
type: "distribution-board.insert",
|
||||||
|
payload: {
|
||||||
|
structure: {
|
||||||
|
...current,
|
||||||
|
distributionBoard: legacyBoard,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as DistributionBoardStructureProjectCommand;
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: legacyCommand,
|
||||||
|
});
|
||||||
|
const inserted = context.db
|
||||||
|
.select()
|
||||||
|
.from(distributionBoards)
|
||||||
|
.get();
|
||||||
|
assert.equal(inserted?.floorId, null);
|
||||||
|
assert.equal(inserted?.supplyType, null);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates floor and supply type with persisted undo and validates project ownership", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
context.db
|
||||||
|
.insert(floors)
|
||||||
|
.values([
|
||||||
|
{
|
||||||
|
id: "floor-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
name: "EG",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "floor-foreign",
|
||||||
|
projectId: "project-2",
|
||||||
|
name: "Fremd",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.run();
|
||||||
|
const repository =
|
||||||
|
new DistributionBoardStructureProjectCommandRepository(
|
||||||
|
context.db
|
||||||
|
);
|
||||||
|
const structure = createDistributionBoardStructureSnapshot(
|
||||||
|
"project-1",
|
||||||
|
"UV-02",
|
||||||
|
{ floorId: null, supplyType: "AV" }
|
||||||
|
);
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command:
|
||||||
|
createDistributionBoardInsertProjectCommand(structure),
|
||||||
|
});
|
||||||
|
const forward = repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "user",
|
||||||
|
command: createDistributionBoardUpdateProjectCommand(
|
||||||
|
structure.distributionBoard.id,
|
||||||
|
{ floorId: "floor-1", supplyType: "SV" }
|
||||||
|
),
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
context.db
|
||||||
|
.select({
|
||||||
|
floorId: distributionBoards.floorId,
|
||||||
|
supplyType: distributionBoards.supplyType,
|
||||||
|
})
|
||||||
|
.from(distributionBoards)
|
||||||
|
.get(),
|
||||||
|
{ floorId: "floor-1", supplyType: "SV" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const undoStep = new ProjectHistoryRepository(
|
||||||
|
context.db
|
||||||
|
).getNextCommand("project-1", "undo");
|
||||||
|
assert.ok(undoStep);
|
||||||
|
repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 2,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: undoStep.changeSetId,
|
||||||
|
command: forward.inverse,
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
context.db
|
||||||
|
.select({
|
||||||
|
floorId: distributionBoards.floorId,
|
||||||
|
supplyType: distributionBoards.supplyType,
|
||||||
|
})
|
||||||
|
.from(distributionBoards)
|
||||||
|
.get(),
|
||||||
|
{ floorId: null, supplyType: "AV" }
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 3,
|
||||||
|
source: "user",
|
||||||
|
command: createDistributionBoardUpdateProjectCommand(
|
||||||
|
structure.distributionBoard.id,
|
||||||
|
{ floorId: "floor-foreign" }
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
/does not belong/
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(projectRevisions)
|
||||||
|
.all().length,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends distribution-board updates to the revision-safe API", async () => {
|
||||||
|
const requests: Array<{ url: string; method: string; body: unknown }> = [];
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = async (input, init) => {
|
||||||
|
requests.push({
|
||||||
|
url: String(input),
|
||||||
|
method: init?.method ?? "GET",
|
||||||
|
body: JSON.parse(String(init?.body)),
|
||||||
|
});
|
||||||
|
return new Response(JSON.stringify({}), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await updateDistributionBoard(
|
||||||
|
"project-1",
|
||||||
|
"board-1",
|
||||||
|
{ floorId: "floor-1", supplyType: "USV" },
|
||||||
|
9
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
assert.deepEqual(requests, [
|
||||||
|
{
|
||||||
|
url: "/api/projects/project-1/distribution-boards/board-1",
|
||||||
|
method: "PUT",
|
||||||
|
body: {
|
||||||
|
floorId: "floor-1",
|
||||||
|
supplyType: "USV",
|
||||||
|
expectedRevision: 9,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects foreign, stale and changed structures without partial writes", () => {
|
it("rejects foreign, stale and changed structures without partial writes", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -958,6 +958,14 @@ describe("project command service", () => {
|
|||||||
description: null,
|
description: null,
|
||||||
singlePhaseVoltageV: 240,
|
singlePhaseVoltageV: 240,
|
||||||
threePhaseVoltageV: 415,
|
threePhaseVoltageV: 415,
|
||||||
|
enabledDistributionBoardSupplyTypes: [
|
||||||
|
"AV",
|
||||||
|
"SV",
|
||||||
|
"EV",
|
||||||
|
"USV",
|
||||||
|
"MSR",
|
||||||
|
"SiBe",
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
assert.equal(updated.history.currentRevision, 1);
|
assert.equal(updated.history.currentRevision, 1);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
import { ProjectLocationStructureProjectCommandRepository } from "../src/db/repositories/project-location-structure-project-command.repository.js";
|
import { ProjectLocationStructureProjectCommandRepository } from "../src/db/repositories/project-location-structure-project-command.repository.js";
|
||||||
import { consumers } from "../src/db/schema/consumers.js";
|
import { consumers } from "../src/db/schema/consumers.js";
|
||||||
|
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
|
||||||
import { floors } from "../src/db/schema/floors.js";
|
import { floors } from "../src/db/schema/floors.js";
|
||||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||||
import { projects } from "../src/db/schema/projects.js";
|
import { projects } from "../src/db/schema/projects.js";
|
||||||
@@ -183,6 +184,52 @@ describe("project-location structure project command", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not undo a floor assigned to a distribution board", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const repository =
|
||||||
|
new ProjectLocationStructureProjectCommandRepository(context.db);
|
||||||
|
const floor = createProjectFloorSnapshot("project-1", "EG", 0);
|
||||||
|
const inserted = repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createProjectFloorInsertProjectCommand(floor),
|
||||||
|
});
|
||||||
|
context.db
|
||||||
|
.insert(distributionBoards)
|
||||||
|
.values({
|
||||||
|
id: "board-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
name: "UV-01",
|
||||||
|
floorId: floor.id,
|
||||||
|
supplyType: "AV",
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
const undoStep = new ProjectHistoryRepository(
|
||||||
|
context.db
|
||||||
|
).getNextCommand("project-1", "undo");
|
||||||
|
assert.ok(undoStep);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: undoStep.changeSetId,
|
||||||
|
command: inserted.inverse,
|
||||||
|
}),
|
||||||
|
/assigned to a distribution board/
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
context.db.select().from(projectRevisions).all().length,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("creates a stable room and supports persisted undo and redo", () => {
|
it("creates a stable room and supports persisted undo and redo", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { ProjectHistoryRepository } from "../src/db/repositories/project-history
|
|||||||
import { ProjectSettingsProjectCommandRepository } from "../src/db/repositories/project-settings-project-command.repository.js";
|
import { ProjectSettingsProjectCommandRepository } from "../src/db/repositories/project-settings-project-command.repository.js";
|
||||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||||
import { projects } from "../src/db/schema/projects.js";
|
import { projects } from "../src/db/schema/projects.js";
|
||||||
|
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
|
||||||
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
||||||
import { createProjectSettingsUpdateProjectCommand } from "../src/domain/models/project-settings-project-command.model.js";
|
import { createProjectSettingsUpdateProjectCommand } from "../src/domain/models/project-settings-project-command.model.js";
|
||||||
import { updateProjectSettings } from "../src/frontend/utils/api.js";
|
import { updateProjectSettings } from "../src/frontend/utils/api.js";
|
||||||
@@ -43,6 +44,8 @@ function getSettings(context: DatabaseContext) {
|
|||||||
description: projects.description,
|
description: projects.description,
|
||||||
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
||||||
threePhaseVoltageV: projects.threePhaseVoltageV,
|
threePhaseVoltageV: projects.threePhaseVoltageV,
|
||||||
|
enabledDistributionBoardSupplyTypes:
|
||||||
|
projects.enabledDistributionBoardSupplyTypes,
|
||||||
currentRevision: projects.currentRevision,
|
currentRevision: projects.currentRevision,
|
||||||
})
|
})
|
||||||
.from(projects)
|
.from(projects)
|
||||||
@@ -63,6 +66,14 @@ function settings(
|
|||||||
description: null,
|
description: null,
|
||||||
singlePhaseVoltageV: 230,
|
singlePhaseVoltageV: 230,
|
||||||
threePhaseVoltageV: 400,
|
threePhaseVoltageV: 400,
|
||||||
|
enabledDistributionBoardSupplyTypes: [
|
||||||
|
"AV",
|
||||||
|
"SV",
|
||||||
|
"EV",
|
||||||
|
"USV",
|
||||||
|
"MSR",
|
||||||
|
"SiBe",
|
||||||
|
],
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -110,6 +121,14 @@ describe("project settings project-command repository", () => {
|
|||||||
description: null,
|
description: null,
|
||||||
singlePhaseVoltageV: 240,
|
singlePhaseVoltageV: 240,
|
||||||
threePhaseVoltageV: 415,
|
threePhaseVoltageV: 415,
|
||||||
|
enabledDistributionBoardSupplyTypes: [
|
||||||
|
"AV",
|
||||||
|
"SV",
|
||||||
|
"EV",
|
||||||
|
"USV",
|
||||||
|
"MSR",
|
||||||
|
"SiBe",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -128,6 +147,14 @@ describe("project settings project-command repository", () => {
|
|||||||
buildingOwner: "Beispiel Bau GmbH",
|
buildingOwner: "Beispiel Bau GmbH",
|
||||||
singlePhaseVoltageV: 240,
|
singlePhaseVoltageV: 240,
|
||||||
threePhaseVoltageV: 415,
|
threePhaseVoltageV: 415,
|
||||||
|
enabledDistributionBoardSupplyTypes: [
|
||||||
|
"AV",
|
||||||
|
"SV",
|
||||||
|
"EV",
|
||||||
|
"USV",
|
||||||
|
"MSR",
|
||||||
|
"SiBe",
|
||||||
|
],
|
||||||
});
|
});
|
||||||
const forward = repository.executeUpdate({
|
const forward = repository.executeUpdate({
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
@@ -136,13 +163,12 @@ describe("project settings project-command repository", () => {
|
|||||||
command,
|
command,
|
||||||
});
|
});
|
||||||
assert.deepEqual(getSettings(context), {
|
assert.deepEqual(getSettings(context), {
|
||||||
name: "Neuer Projektname",
|
...settings({
|
||||||
internalProjectNumber: "INT-1",
|
name: "Neuer Projektname",
|
||||||
externalProjectNumber: null,
|
buildingOwner: "Beispiel Bau GmbH",
|
||||||
buildingOwner: "Beispiel Bau GmbH",
|
singlePhaseVoltageV: 240,
|
||||||
description: null,
|
threePhaseVoltageV: 415,
|
||||||
singlePhaseVoltageV: 240,
|
}),
|
||||||
threePhaseVoltageV: 415,
|
|
||||||
currentRevision: 1,
|
currentRevision: 1,
|
||||||
});
|
});
|
||||||
assert.deepEqual(forward.inverse.payload, {
|
assert.deepEqual(forward.inverse.payload, {
|
||||||
@@ -153,6 +179,14 @@ describe("project settings project-command repository", () => {
|
|||||||
description: null,
|
description: null,
|
||||||
singlePhaseVoltageV: 230,
|
singlePhaseVoltageV: 230,
|
||||||
threePhaseVoltageV: 400,
|
threePhaseVoltageV: 400,
|
||||||
|
enabledDistributionBoardSupplyTypes: [
|
||||||
|
"AV",
|
||||||
|
"SV",
|
||||||
|
"EV",
|
||||||
|
"USV",
|
||||||
|
"MSR",
|
||||||
|
"SiBe",
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const undoStep = history.getNextCommand("project-1", "undo");
|
const undoStep = history.getNextCommand("project-1", "undo");
|
||||||
@@ -172,6 +206,14 @@ describe("project settings project-command repository", () => {
|
|||||||
description: null,
|
description: null,
|
||||||
singlePhaseVoltageV: 230,
|
singlePhaseVoltageV: 230,
|
||||||
threePhaseVoltageV: 400,
|
threePhaseVoltageV: 400,
|
||||||
|
enabledDistributionBoardSupplyTypes: [
|
||||||
|
"AV",
|
||||||
|
"SV",
|
||||||
|
"EV",
|
||||||
|
"USV",
|
||||||
|
"MSR",
|
||||||
|
"SiBe",
|
||||||
|
],
|
||||||
currentRevision: 2,
|
currentRevision: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,6 +234,14 @@ describe("project settings project-command repository", () => {
|
|||||||
description: null,
|
description: null,
|
||||||
singlePhaseVoltageV: 240,
|
singlePhaseVoltageV: 240,
|
||||||
threePhaseVoltageV: 415,
|
threePhaseVoltageV: 415,
|
||||||
|
enabledDistributionBoardSupplyTypes: [
|
||||||
|
"AV",
|
||||||
|
"SV",
|
||||||
|
"EV",
|
||||||
|
"USV",
|
||||||
|
"MSR",
|
||||||
|
"SiBe",
|
||||||
|
],
|
||||||
currentRevision: 3,
|
currentRevision: 3,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@@ -230,6 +280,51 @@ describe("project settings project-command repository", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("persists the project supply-type selection and rejects disabling an in-use type", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const repository = new ProjectSettingsProjectCommandRepository(
|
||||||
|
context.db
|
||||||
|
);
|
||||||
|
repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createProjectSettingsUpdateProjectCommand({
|
||||||
|
...settings(),
|
||||||
|
enabledDistributionBoardSupplyTypes: ["AV", "MSR", "SiBe"],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
getSettings(context)?.enabledDistributionBoardSupplyTypes,
|
||||||
|
["AV", "MSR", "SiBe"]
|
||||||
|
);
|
||||||
|
|
||||||
|
context.db.insert(distributionBoards).values({
|
||||||
|
id: "board-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
name: "UV SiBe",
|
||||||
|
supplyType: "SiBe",
|
||||||
|
}).run();
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "user",
|
||||||
|
command: createProjectSettingsUpdateProjectCommand({
|
||||||
|
...settings(),
|
||||||
|
enabledDistributionBoardSupplyTypes: ["AV", "MSR"],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
/SiBe/
|
||||||
|
);
|
||||||
|
assert.equal(getSettings(context)?.currentRevision, 1);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects no-op, invalid project and stale revision without partial writes", () => {
|
it("rejects no-op, invalid project and stale revision without partial writes", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
@@ -277,13 +372,7 @@ describe("project settings project-command repository", () => {
|
|||||||
ProjectRevisionConflictError
|
ProjectRevisionConflictError
|
||||||
);
|
);
|
||||||
assert.deepEqual(getSettings(context), {
|
assert.deepEqual(getSettings(context), {
|
||||||
name: "Testprojekt",
|
...settings(),
|
||||||
internalProjectNumber: "INT-1",
|
|
||||||
externalProjectNumber: null,
|
|
||||||
buildingOwner: null,
|
|
||||||
description: null,
|
|
||||||
singlePhaseVoltageV: 230,
|
|
||||||
threePhaseVoltageV: 400,
|
|
||||||
currentRevision: 0,
|
currentRevision: 0,
|
||||||
});
|
});
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -323,13 +412,7 @@ describe("project settings project-command repository", () => {
|
|||||||
/forced project settings history failure/
|
/forced project settings history failure/
|
||||||
);
|
);
|
||||||
assert.deepEqual(getSettings(context), {
|
assert.deepEqual(getSettings(context), {
|
||||||
name: "Testprojekt",
|
...settings(),
|
||||||
internalProjectNumber: "INT-1",
|
|
||||||
externalProjectNumber: null,
|
|
||||||
buildingOwner: null,
|
|
||||||
description: null,
|
|
||||||
singlePhaseVoltageV: 230,
|
|
||||||
threePhaseVoltageV: 400,
|
|
||||||
currentRevision: 0,
|
currentRevision: 0,
|
||||||
});
|
});
|
||||||
assert.equal(
|
assert.equal(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
|||||||
import { circuitLists } from "../src/db/schema/circuit-lists.js";
|
import { circuitLists } from "../src/db/schema/circuit-lists.js";
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
import { circuits } from "../src/db/schema/circuits.js";
|
import { circuits } from "../src/db/schema/circuits.js";
|
||||||
|
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
|
||||||
import { floors } from "../src/db/schema/floors.js";
|
import { floors } from "../src/db/schema/floors.js";
|
||||||
import { projectDevices } from "../src/db/schema/project-devices.js";
|
import { projectDevices } from "../src/db/schema/project-devices.js";
|
||||||
import { projectSnapshots } from "../src/db/schema/project-snapshots.js";
|
import { projectSnapshots } from "../src/db/schema/project-snapshots.js";
|
||||||
@@ -64,6 +65,11 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
sortOrder: 10,
|
sortOrder: 10,
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
context.db
|
||||||
|
.update(distributionBoards)
|
||||||
|
.set({ floorId: "floor-1", supplyType: "SV" })
|
||||||
|
.where(eq(distributionBoards.id, board.id))
|
||||||
|
.run();
|
||||||
context.db
|
context.db
|
||||||
.insert(rooms)
|
.insert(rooms)
|
||||||
.values({
|
.values({
|
||||||
@@ -168,6 +174,8 @@ describe("project snapshot repository", () => {
|
|||||||
);
|
);
|
||||||
assert.equal(state.project.id, "project-1");
|
assert.equal(state.project.id, "project-1");
|
||||||
assert.equal(state.distributionBoards.length, 1);
|
assert.equal(state.distributionBoards.length, 1);
|
||||||
|
assert.equal(state.distributionBoards[0].floorId, "floor-1");
|
||||||
|
assert.equal(state.distributionBoards[0].supplyType, "SV");
|
||||||
assert.equal(state.circuitLists.length, 1);
|
assert.equal(state.circuitLists.length, 1);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
state.circuitSections.length,
|
state.circuitSections.length,
|
||||||
@@ -279,6 +287,13 @@ describe("project snapshot repository", () => {
|
|||||||
delete currentPayload.project.externalProjectNumber;
|
delete currentPayload.project.externalProjectNumber;
|
||||||
delete currentPayload.project.buildingOwner;
|
delete currentPayload.project.buildingOwner;
|
||||||
delete currentPayload.project.description;
|
delete currentPayload.project.description;
|
||||||
|
delete currentPayload.project.enabledDistributionBoardSupplyTypes;
|
||||||
|
for (const board of currentPayload.distributionBoards as Array<
|
||||||
|
Record<string, unknown>
|
||||||
|
>) {
|
||||||
|
delete board.floorId;
|
||||||
|
delete board.supplyType;
|
||||||
|
}
|
||||||
const legacyPayloadJson = JSON.stringify(currentPayload);
|
const legacyPayloadJson = JSON.stringify(currentPayload);
|
||||||
context.db
|
context.db
|
||||||
.update(projectSnapshots)
|
.update(projectSnapshots)
|
||||||
@@ -352,6 +367,14 @@ describe("portable project transfer", () => {
|
|||||||
copied.state.circuits[0].deviceRows[0].roomId,
|
copied.state.circuits[0].deviceRows[0].roomId,
|
||||||
copied.state.rooms[0].id
|
copied.state.rooms[0].id
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
copied.state.distributionBoards[0].floorId,
|
||||||
|
copied.state.floors[0].id
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
copied.state.distributionBoards[0].supplyType,
|
||||||
|
"SV"
|
||||||
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
copied.state.circuits[0].deviceRows[0].legacyConsumerId,
|
copied.state.circuits[0].deviceRows[0].legacyConsumerId,
|
||||||
null
|
null
|
||||||
@@ -390,6 +413,46 @@ describe("portable project transfer", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("imports a checksum-valid version-two project transfer", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const repository = new ProjectTransferRepository(context.db);
|
||||||
|
const current = repository.exportProject("project-1");
|
||||||
|
assert.ok(current);
|
||||||
|
const legacyState = structuredClone(
|
||||||
|
current.projectState
|
||||||
|
) as unknown as Record<string, unknown> & {
|
||||||
|
distributionBoards: Array<Record<string, unknown>>;
|
||||||
|
};
|
||||||
|
legacyState.schemaVersion = 2;
|
||||||
|
delete (
|
||||||
|
legacyState.project as Record<string, unknown>
|
||||||
|
).enabledDistributionBoardSupplyTypes;
|
||||||
|
for (const board of legacyState.distributionBoards) {
|
||||||
|
delete board.floorId;
|
||||||
|
delete board.supplyType;
|
||||||
|
}
|
||||||
|
const legacyPayloadJson = JSON.stringify(legacyState);
|
||||||
|
const duplicate = repository.importDuplicate({
|
||||||
|
...current,
|
||||||
|
projectState: legacyState,
|
||||||
|
payloadSha256: crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(legacyPayloadJson)
|
||||||
|
.digest("hex"),
|
||||||
|
});
|
||||||
|
const copied = readProjectStateSnapshot(
|
||||||
|
context.db,
|
||||||
|
duplicate.projectId
|
||||||
|
);
|
||||||
|
assert.ok(copied);
|
||||||
|
assert.equal(copied.state.distributionBoards[0].floorId, null);
|
||||||
|
assert.equal(copied.state.distributionBoards[0].supplyType, null);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("rolls back a duplicate when a late project-state insert fails", () => {
|
it("rolls back a duplicate when a late project-state insert fails", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ function minimalSnapshot() {
|
|||||||
description: null,
|
description: null,
|
||||||
singlePhaseVoltageV: 230,
|
singlePhaseVoltageV: 230,
|
||||||
threePhaseVoltageV: 400,
|
threePhaseVoltageV: 400,
|
||||||
|
enabledDistributionBoardSupplyTypes: [
|
||||||
|
"AV",
|
||||||
|
"SV",
|
||||||
|
"EV",
|
||||||
|
"USV",
|
||||||
|
"MSR",
|
||||||
|
"SiBe",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
distributionBoards: [],
|
distributionBoards: [],
|
||||||
circuitLists: [],
|
circuitLists: [],
|
||||||
@@ -61,9 +69,65 @@ describe("project state snapshot model", () => {
|
|||||||
externalProjectNumber: null,
|
externalProjectNumber: null,
|
||||||
buildingOwner: null,
|
buildingOwner: null,
|
||||||
description: null,
|
description: null,
|
||||||
|
enabledDistributionBoardSupplyTypes: [
|
||||||
|
"AV",
|
||||||
|
"SV",
|
||||||
|
"EV",
|
||||||
|
"USV",
|
||||||
|
"MSR",
|
||||||
|
"SiBe",
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("upgrades version-two distribution boards without inferred assignments", () => {
|
||||||
|
const previous = {
|
||||||
|
...minimalSnapshot(),
|
||||||
|
schemaVersion: 2 as const,
|
||||||
|
project: {
|
||||||
|
...minimalSnapshot().project,
|
||||||
|
enabledDistributionBoardSupplyTypes: undefined,
|
||||||
|
},
|
||||||
|
distributionBoards: [
|
||||||
|
{
|
||||||
|
id: "board-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
name: "UV 1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
delete previous.project.enabledDistributionBoardSupplyTypes;
|
||||||
|
const snapshot = parseProjectStateSnapshot(previous);
|
||||||
|
assert.deepEqual(snapshot.distributionBoards, [
|
||||||
|
{
|
||||||
|
...previous.distributionBoards[0],
|
||||||
|
floorId: null,
|
||||||
|
supplyType: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("upgrades version-three snapshots with all project supply types", () => {
|
||||||
|
const previous = {
|
||||||
|
...minimalSnapshot(),
|
||||||
|
schemaVersion: 3 as const,
|
||||||
|
project: {
|
||||||
|
...minimalSnapshot().project,
|
||||||
|
enabledDistributionBoardSupplyTypes: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
delete previous.project.enabledDistributionBoardSupplyTypes;
|
||||||
|
const snapshot = parseProjectStateSnapshot(previous);
|
||||||
|
assert.deepEqual(snapshot.project.enabledDistributionBoardSupplyTypes, [
|
||||||
|
"AV",
|
||||||
|
"SV",
|
||||||
|
"EV",
|
||||||
|
"USV",
|
||||||
|
"MSR",
|
||||||
|
"SiBe",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects duplicate ids and cross-project ownership", () => {
|
it("rejects duplicate ids and cross-project ownership", () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() =>
|
() =>
|
||||||
@@ -74,11 +138,15 @@ describe("project state snapshot model", () => {
|
|||||||
id: "board-1",
|
id: "board-1",
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
name: "UV 1",
|
name: "UV 1",
|
||||||
|
floorId: null,
|
||||||
|
supplyType: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "board-1",
|
id: "board-1",
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
name: "UV 2",
|
name: "UV 2",
|
||||||
|
floorId: null,
|
||||||
|
supplyType: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -102,6 +170,22 @@ describe("project state snapshot model", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("rejects invalid references and inconsistent reserve circuits", () => {
|
it("rejects invalid references and inconsistent reserve circuits", () => {
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
parseProjectStateSnapshot({
|
||||||
|
...minimalSnapshot(),
|
||||||
|
distributionBoards: [
|
||||||
|
{
|
||||||
|
id: "board-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
name: "UV 1",
|
||||||
|
floorId: "missing",
|
||||||
|
supplyType: "AV",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
/distribution board floor reference is invalid/
|
||||||
|
);
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() =>
|
() =>
|
||||||
parseProjectStateSnapshot({
|
parseProjectStateSnapshot({
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createElement } from "react";
|
|||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import { ProjectVersionHistory } from "../src/frontend/components/project-version-history.js";
|
import { ProjectVersionHistory } from "../src/frontend/components/project-version-history.js";
|
||||||
import { ProjectDeviceModal } from "../src/frontend/components/project-device-modal.js";
|
import { ProjectDeviceModal } from "../src/frontend/components/project-device-modal.js";
|
||||||
|
import { ProjectSettingsModal } from "../src/frontend/components/project-settings-modal.js";
|
||||||
import type { ProjectRevisionSummaryDto } from "../src/frontend/types.js";
|
import type { ProjectRevisionSummaryDto } from "../src/frontend/types.js";
|
||||||
import {
|
import {
|
||||||
createNamedProjectSnapshot,
|
createNamedProjectSnapshot,
|
||||||
@@ -99,6 +100,40 @@ describe("project version history presentation", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("project settings presentation", () => {
|
||||||
|
it("shows all supply types and marks types already in use", () => {
|
||||||
|
const markup = renderToStaticMarkup(
|
||||||
|
createElement(ProjectSettingsModal, {
|
||||||
|
isSaving: false,
|
||||||
|
onClose: () => undefined,
|
||||||
|
onExport: async () => undefined,
|
||||||
|
onImport: async () => undefined,
|
||||||
|
onSave: async () => undefined,
|
||||||
|
project: {
|
||||||
|
id: "project-1",
|
||||||
|
name: "Projekt",
|
||||||
|
internalProjectNumber: null,
|
||||||
|
externalProjectNumber: null,
|
||||||
|
buildingOwner: null,
|
||||||
|
description: null,
|
||||||
|
singlePhaseVoltageV: 230,
|
||||||
|
threePhaseVoltageV: 400,
|
||||||
|
enabledDistributionBoardSupplyTypes: [
|
||||||
|
"AV",
|
||||||
|
"MSR",
|
||||||
|
"SiBe",
|
||||||
|
],
|
||||||
|
currentRevision: 0,
|
||||||
|
},
|
||||||
|
usedDistributionBoardSupplyTypes: ["SiBe"],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert.match(markup, /MSR/);
|
||||||
|
assert.match(markup, /SiBe/);
|
||||||
|
assert.match(markup, /in Verwendung/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("project device modal presentation", () => {
|
describe("project device modal presentation", () => {
|
||||||
it("renders explicit German labels for every editable device field", () => {
|
it("renders explicit German labels for every editable device field", () => {
|
||||||
const markup = renderToStaticMarkup(
|
const markup = renderToStaticMarkup(
|
||||||
|
|||||||
Reference in New Issue
Block a user