diff --git a/AGENTS.md b/AGENTS.md index bcd2ca6..51b6616 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -250,14 +250,11 @@ not change the project revision or undo/redo stacks. Restoring a server-stored snapshot verifies its checksum and the current-state hash, replaces supported project data atomically and records a new `restore` revision with a complete inverse command. Restore can therefore be undone and redone after a restart. -Current snapshot schema version 7 additionally stores the project buildings -and `floors[].buildingId` (see `docs/building-structure.md`); versions 1 to 6 -are upgraded with no buildings and `buildingId = null`. Version 6 stores the -distribution-board info fields `boardNumber`, `location`, `fedFrom` and -`remark` and the project cover fields `siteAddress`, `workStage`, -`projectLead` and `projectEditor`; all older supported versions are upgraded -with `null` for these fields. -Stored named and automatic snapshots of every supported version (1 to 7) +Current snapshot schema version 6 additionally stores the distribution-board +info fields `boardNumber`, `location`, `fedFrom` and `remark` and the project +cover fields `siteAddress`, `workStage`, `projectLead` and `projectEditor`; +all older supported versions are upgraded with `null` for these fields. +Stored named and automatic snapshots of every supported version (1 to 6) remain restorable; they are upgraded while parsing. Version 5 stores the manual quantity share of every CircuitDeviceRow. Version 4 snapshots are upgraded with `manualQuantity = quantity`; they already contain the complete confirmed @@ -273,7 +270,7 @@ project graph. Project-wide Revit CSV configuration uses `external-csv-configuration.update`. Its complete expected/target snapshot is persisted through the shared project-command transaction, so create, update, -delete, Undo and Redo are revision-safe. Snapshot schema 5-7, restore and portable +delete, Undo and Redo are revision-safe. Snapshot schema 5/6, restore and portable project duplication preserve the configuration; duplication remaps its internal id and project link. The project page exposes the Revit CSV modal. It edits the complete diff --git a/docs/README.md b/docs/README.md index cdddb67..395b1ac 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,8 +14,6 @@ ausdrücklich getrennt und dürfen nicht als bereits implementiert verstanden we Drag-and-drop, Sortierung, Filterung und Undo/Redo - [Seitenansicht und PDF-Export](print-view.md) – Deckblatt, Verteilerübersicht, Verteilerpläne, Büro-Logo -- [Gebäudestruktur](building-structure.md) – Gebäude → Etagen → Räume, - Befehle, Snapshot-Schema 7, eigene Seite - [Editor-API](circuit-list-editor-api.md) – unterstützte Circuit-First-Endpunkte - [Bekannte Einschränkungen](circuit-list-editor-known-limitations.md) – bewusst noch nicht implementierte oder begrenzte Funktionen diff --git a/docs/building-structure.md b/docs/building-structure.md deleted file mode 100644 index 1c1a455..0000000 --- a/docs/building-structure.md +++ /dev/null @@ -1,79 +0,0 @@ -# Gebäudestruktur (Gebäude → Etagen → Räume) - -Projekte mit mehreren Gebäuden werden über eine eigene Ebene **Gebäude** -gegliedert. Etagen können einem Gebäude zugeordnet sein, Räume und Verteilungen -hängen wie bisher an der Etage. Das Gebäude eines Raums oder einer Verteilung -ergibt sich also aus der Etage; eine zweite, konkurrierende Zuordnung gibt es -nicht. Abstimmung mit Julian: Issue #6. - -## Datenmodell - -| Tabelle | Feld | Bedeutung | -|---|---|---| -| `buildings` | `id`, `project_id` (FK, cascade), `name`, `sort_order` | Gebäude eines Projekts | -| `floors` | `building_id` (nullable FK → `buildings`, `ON DELETE set null`) | optionales übergeordnetes Gebäude | - -Migration `0009_buildings`. Bestehende Etagen behalten `building_id = null` und -erscheinen in der Oberfläche als „Ohne Gebäude“. Es gibt keine Zwangsmigration -und kein automatisch angelegtes Standardgebäude. - -## Befehle, Undo/Redo - -Alle Änderungen laufen über die bestehende Orts-Struktur -(`ProjectLocationStructureProjectCommandRepository`) mit Revision, Inverse und -persistentem Undo/Redo: - -- `project-building.insert` / `project-building.delete` mit vollständigem - Gebäude-Snapshot (Schema-Version 1) -- `project-building.update`: ändert nur den Namen -- Löschen eines Gebäudes wird abgelehnt, solange Etagen zugeordnet sind - („Das Gebäude kann nicht gelöscht werden, solange Etagen zugeordnet sind.“) -- Etagenbefehle `project-floor.insert/.update/.delete` haben seit - Schema-Version **2** das Feld `buildingId`. `project-floor.update` darf Name - und/oder Gebäude ändern, nicht aber `sortOrder`. Ein referenziertes Gebäude - muss zum Projekt gehören. -- Version-1-Etagenbefehle aus bestehender Historie bleiben ausführbar. Sie - stammen aus der Zeit vor den Gebäuden und werden vor der Ausführung mit - `buildingId = null` auf Version 2 gehoben; auch die daraus neu gespeicherten - Vorwärts- und Inversbefehle sind Version 2. Das ist bei linearer Historie - exakt: Wird ein Gebäude später zugeordnet, liegt dieser neuere Befehl im - Undo-Stapel vor dem alten Befehl und wird zuerst zurückgenommen. -- Raumbefehle bleiben unverändert (Version 1). - -## Snapshots und Transfer - -Projekt-Snapshot-Schema **7** enthält `buildings` und `floors[].buildingId`. -Alle älteren unterstützten Versionen (1–6) werden beim Einlesen mit leerer -Gebäudeliste und `buildingId = null` hochgestuft; gespeicherte Sicherungspunkte -bleiben wiederherstellbar. Die Relationen werden geprüft (Gebäude gehört zum -Projekt, `buildingId` verweist auf ein Gebäude des Snapshots). Restore löscht -und schreibt Gebäude zusammen mit den Etagen; der Projekt-Transfer vergibt neue -Gebäude-IDs und stellt die Etagenverweise um. - -## API - -- `GET /api/projects/:projectId/buildings` – Gebäude sortiert nach - `sortOrder`, Name -- `POST /api/projects/:projectId/buildings` – `{ "name": "Haus A", "expectedRevision": 3 }` -- `PUT /api/projects/:projectId/buildings/:buildingId` – `{ "name": "Haus B", "expectedRevision": 4 }` -- `DELETE /api/projects/:projectId/buildings/:buildingId` – `{ "expectedRevision": 5 }` -- `POST/PUT /api/projects/:projectId/floors[/:floorId]` akzeptieren zusätzlich - optional `buildingId` (`string | null`). Fehlt das Feld, bleibt beim Bearbeiten - das bisherige Gebäude erhalten; ältere Clients funktionieren unverändert. - -## Oberfläche - -Gebäude, Etagen und Räume werden auf der eigenen Seite -`/projects/:projectId/struktur` („Gebäudestruktur“) gepflegt, erreichbar über -die Seitenleiste und die Kennzahlen der Projektübersicht. Drei Spalten -Gebäude → Etagen → Räume filtern sich gegenseitig: Klick auf ein Gebäude zeigt -nur dessen Etagen, Klick auf eine Etage nur deren Räume, „Alle“ hebt den Filter -auf. Hinzufügen, Bearbeiten und Löschen laufen über Icon-Buttons wie in der -Stromkreisliste. - -## Offen - -- Revit: Das Gebäude kann später aus einem Revit-Parameter kommen und über die - bestehende CSV-Zuordnung gemappt werden (Frage an Julian in Issue #6). -- Sortierung von Gebäuden per Drag-and-drop ist noch nicht umgesetzt; neue - Gebäude werden hinten angehängt. diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index 156be62..d325944 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -257,27 +257,15 @@ returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`. `distribution-board.delete-subtree`. Persistent Undo/Redo restores or removes the same complete subtree, including protection devices. -### Project Buildings, Floors and Rooms +### Project Floors and Rooms -- `GET /projects/:projectId/buildings` -- `POST /projects/:projectId/buildings` - - body: `{ "name": "Haus A", "expectedRevision": 12 }` - - executes `project-building.insert` with a stable building id - - response: `{ "building": { ... }, "revision": { ... }, "history": { ... } }` -- `PUT /projects/:projectId/buildings/:buildingId` - - body: `{ "name": "Haus B", "expectedRevision": 13 }` (name only) -- `DELETE /projects/:projectId/buildings/:buildingId` - - body: `{ "expectedRevision": 14 }` - - rejects buildings with assigned floors - `POST /projects/:projectId/floors` - - body: `{ "name": "EG", "buildingId": "building_1", "expectedRevision": 13 }` - (`buildingId` optional, `null` = no building) - - executes `project-floor.insert` (command version 2) with a stable floor id + - body: `{ "name": "EG", "expectedRevision": 13 }` + - executes `project-floor.insert` with a stable floor id - response: `{ "floor": { ... }, "revision": { ... }, "history": { ... } }` - `PUT /projects/:projectId/floors/:floorId` - - body: `{ "name": "1. OG", "buildingId": null, "expectedRevision": 14 }` - - executes an exact persistent floor update of name and/or building; a - missing `buildingId` keeps the current building + - body: `{ "name": "1. OG", "expectedRevision": 14 }` + - executes an exact persistent floor update - `DELETE /projects/:projectId/floors/:floorId` - body: `{ "expectedRevision": 15 }` - rejects floors with assigned rooms or distribution boards diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 191c3f4..fe55413 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -246,11 +246,9 @@ Werte, Revision und Historienstapel gemeinsam. `PUT /api/projects/:projectId` verlangt deshalb `expectedRevision`, liefert Projekt plus aktualisierten Historienstand und besitzt keinen separaten direkten Settings-Schreibweg mehr. Kommando- und Snapshot-Payloads müssen dem aktuellen Baseline-Schema entsprechen. -Die Projektseite zeigt Verteilungen als kompakte Bestandsübersicht; ihre -versionierten Erstellwege öffnen beschriftete Modals statt dauerhafter -Eingabezeilen. Gebäude, Etagen und Räume werden auf der eigenen Seite -„Gebäudestruktur“ (`/projects/:projectId/struktur`) gepflegt; Etagen können -einem Gebäude zugeordnet sein (`docs/building-structure.md`). Projektgeräte werden als durchsuchbare, +Die Projektseite zeigt Verteilungen, Etagen und Räume als kompakte +Bestandsübersichten; ihre versionierten Erstellwege öffnen beschriftete Modals +statt dauerhafter Eingabezeilen. Projektgeräte werden als durchsuchbare, fünfspaltige Übersicht dargestellt. Manuelle Anlage, Übernahme aus der globalen Bibliothek und vollständige Bearbeitung erfolgen in einem gemeinsamen Modal. Die anschließende Vorschau verknüpfter Stromkreiszeilen bleibt davon getrennt, @@ -424,8 +422,6 @@ Derselbe Befehl versioniert die Felder der Verteiler-Infokarte Infokarte liegt im Bearbeiten-Dialog der Verteilung; die Werte sind lokale Planungswerte und sollen später über den Revit-Abgleich nur nach Vorschau und ausdrücklicher Bestätigung vorbelegt werden. `fedFrom` ist vorerst Freitext. -Snapshot-Schema 7 enthält zusätzlich die Gebäude und die Gebäudezuordnung -der Etagen; ältere Versionen werden ohne Gebäude hochgestuft. Snapshot-Schema 6 enthält diese Felder und die Deckblattangaben des Projekts; ältere Versionen werden mit `null` hochgestuft. Snapshot-Schema 5 enthält zusätzlich den manuellen Mengenanteil jeder diff --git a/docs/project-history-and-external-model-architecture.md b/docs/project-history-and-external-model-architecture.md index a7c1300..4c0a8ea 100644 --- a/docs/project-history-and-external-model-architecture.md +++ b/docs/project-history-and-external-model-architecture.md @@ -275,10 +275,6 @@ Completed foundation: - `project-floor.insert` and `project-room.insert` persist complete location records with stable ids; their inverses reject changed or referenced records so Undo never clears room assignments or device-row links implicitly -- `project-building.insert/.update/.delete` persist complete building records; - deleting a building with assigned floors is rejected. Floor commands carry - `buildingId` since command version 2; version 1 floor commands from older - history are upgraded with `buildingId = null` before execution - the floor and room POST routes require the current expected revision and the project page tracks the returned history state diff --git a/scripts/db-verify-circuit-schema.js b/scripts/db-verify-circuit-schema.js index 947767a..8d27b1d 100644 --- a/scripts/db-verify-circuit-schema.js +++ b/scripts/db-verify-circuit-schema.js @@ -5,7 +5,6 @@ const dbPath = path.resolve("data", "leistungsbilanz.db"); const db = new Database(dbPath, { readonly: true }); const requiredTables = [ - "buildings", "circuit_sections", "circuits", "circuit_device_rows", @@ -101,13 +100,6 @@ const projectColumns = new Set( const missingProjectColumns = requiredProjectColumns.filter( (name) => !projectColumns.has(name) ); -const requiredFloorColumns = ["building_id"]; -const floorColumns = new Set( - db.prepare("PRAGMA table_info(floors)").all().map((column) => column.name) -); -const missingFloorColumns = requiredFloorColumns.filter( - (name) => !floorColumns.has(name) -); const requiredDistributionBoardColumns = [ "floor_id", "supply_type", @@ -163,7 +155,6 @@ console.log( requiredCircuitSectionColumns.join(", ") ); console.log("Required project columns:", requiredProjectColumns.join(", ")); -console.log("Required floor columns:", requiredFloorColumns.join(", ")); console.log( "Required distribution-board columns:", requiredDistributionBoardColumns.join(", ") @@ -180,11 +171,6 @@ if (missing.length > 0) { process.exit(1); } -if (missingFloorColumns.length > 0) { - console.error("Missing floor columns:", missingFloorColumns.join(", ")); - process.exit(1); -} - if (missingProjectDeviceColumns.length > 0) { console.error("Missing project-device columns:", missingProjectDeviceColumns.join(", ")); process.exit(1); diff --git a/src/app/globals.css b/src/app/globals.css index 0c40c5c..22da5df 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -220,7 +220,7 @@ a { --bs-alert-border-color: #205a38; } -/* ── App-Shell / Seitenleiste (alle Seiten außer der Seitenansicht) ──── */ +/* ── App-Shell / Seitenleiste (nur auf der Projektseite) ────────────────── */ .app-shell { display: grid; @@ -1461,144 +1461,3 @@ a.kpi:hover { .sidebar-collapsed .sidebar-label { display: none; } - -/* ── Gebäudestruktur (Gebäude → Etagen → Räume) ─────────────────────── */ - -.structure-columns { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: var(--space-md); - align-items: start; -} - -@media (min-width: 992px) { - .structure-columns { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } -} - -.structure-column-title { - font-size: 1rem; - font-weight: 700; - margin: 0; - display: flex; - align-items: center; -} - -.structure-icon-button { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.25rem 0.4rem; - line-height: 1; -} - -.structure-back-link { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.35rem 0.45rem; - margin-top: 0.1rem; -} - -.structure-list { - max-height: 70vh; - overflow-y: auto; -} - -.structure-entry { - display: flex; - align-items: center; - gap: var(--space-sm); - padding: 0; -} - -.structure-entry-main { - flex: 1 1 auto; - min-width: 0; - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-sm); - padding: 0.55rem 0.75rem; - border: 0; - background: transparent; - color: inherit; - text-align: left; - font: inherit; -} - -button.structure-entry-main { - cursor: pointer; -} - -button.structure-entry-main:hover { - background: var(--surface-hover); -} - -button.structure-entry-main:focus-visible { - outline: 2px solid var(--accent-blue); - outline-offset: -2px; -} - -.structure-entry.is-selected { - background: var(--surface-selected); - box-shadow: inset 3px 0 0 var(--color-signal); -} - -.structure-entry.is-selected .structure-entry-label { - font-weight: 600; -} - -.structure-entry.is-selected button.structure-entry-main:hover { - background: transparent; -} - -.structure-entry-text { - min-width: 0; - display: flex; - flex-direction: column; -} - -.structure-entry-label { - overflow-wrap: anywhere; -} - -.structure-entry-hint { - font-size: 0.78rem; - color: var(--color-ink-soft); -} - -.structure-entry-meta { - flex: 0 0 auto; - font-size: 0.78rem; - color: var(--color-ink-soft); - white-space: nowrap; -} - -.structure-entry-actions { - flex: 0 0 auto; - display: inline-flex; - gap: 4px; - padding-right: 0.75rem; -} - -.structure-room-number { - font-weight: 600; - font-variant-numeric: tabular-nums; -} - -.structure-group-label { - font-size: 0.66rem; - letter-spacing: 0.08em; - text-transform: uppercase; - font-weight: 600; - color: var(--color-accent); - background: var(--panel-bg-subtle); - padding: 0.35rem 0.75rem; -} - -.structure-empty { - color: var(--color-ink-soft); - font-size: 0.9rem; -} diff --git a/src/app/projects/[projectId]/page.tsx b/src/app/projects/[projectId]/page.tsx index 0719078..73f9460 100644 --- a/src/app/projects/[projectId]/page.tsx +++ b/src/app/projects/[projectId]/page.tsx @@ -8,15 +8,17 @@ import { copyDistributionBoard, copyProjectDeviceToGlobal, createDistributionBoard, + createFloor, createProjectDevice, createRoom, + deleteFloor, deleteProjectDevice, + deleteRoom, deleteDistributionBoard, disconnectProjectDeviceRows, exportProjectTransfer, getProject, getProjectDeviceSyncPreview, - listBuildings, listCircuitLists, listDistributionBoards, listFloors, @@ -26,11 +28,12 @@ import { importProjectTransfer, synchronizeProjectDeviceRows, updateDistributionBoard, + updateFloor, updateProjectDevice, updateProjectSettings, + updateRoom, } from "../../../frontend/utils/api"; import type { - BuildingDto, CircuitListDto, CreateRoomInput, CreateProjectDeviceInput, @@ -64,7 +67,7 @@ import { import { FormModal } from "../../../frontend/components/form-modal"; import { ProjectDeviceModal } from "../../../frontend/components/project-device-modal"; import { RevitCsvModal } from "../../../frontend/components/revit-csv-modal"; -import { FloorSelectOptions } from "../../../frontend/components/floor-select-options"; +import { RoomModal } from "../../../frontend/components/room-modal"; const projectDeviceSyncFieldLabels: Record = { name: "Technischer Name", @@ -86,7 +89,6 @@ export default function ProjectDetailPage() { const [project, setProject] = useState(null); const [boards, setBoards] = useState([]); const [circuitLists, setCircuitLists] = useState([]); - const [buildings, setBuildings] = useState([]); const [floors, setFloors] = useState([]); const [rooms, setRooms] = useState([]); const [projectDevices, setProjectDevices] = useState([]); @@ -114,9 +116,14 @@ export default function ProjectDetailPage() { remark: null, }) ); + const [floorName, setFloorName] = useState(""); + const [editingFloor, setEditingFloor] = useState(null); + const [editingRoom, setEditingRoom] = useState(null); const [isProjectSettingsOpen, setIsProjectSettingsOpen] = useState(false); const [isRevitCsvOpen, setIsRevitCsvOpen] = useState(false); - const [structureModal, setStructureModal] = useState<"board" | null>(null); + const [structureModal, setStructureModal] = useState< + "board" | "floor" | "room" | null + >(null); const [isProjectDeviceModalOpen, setIsProjectDeviceModalOpen] = useState(false); const [editingProjectDevice, setEditingProjectDevice] = @@ -140,7 +147,6 @@ export default function ProjectDetailPage() { getProject(projectId), listDistributionBoards(projectId), listCircuitLists(projectId), - listBuildings(projectId), listFloors(projectId), listRooms(projectId), listProjectDevices(projectId), @@ -150,7 +156,6 @@ export default function ProjectDetailPage() { currentProject, distributionBoards, loadedCircuitLists, - loadedBuildings, loadedFloors, loadedRooms, loadedProjectDevices, @@ -159,7 +164,6 @@ export default function ProjectDetailPage() { setProject(currentProject); setBoards(distributionBoards); setCircuitLists(loadedCircuitLists); - setBuildings(loadedBuildings); setFloors(loadedFloors); setRooms(loadedRooms); setProjectDevices(loadedProjectDevices); @@ -172,7 +176,6 @@ export default function ProjectDetailPage() { }, [projectId]); const boardCount = useMemo(() => boards.length, [boards.length]); - const buildingCount = useMemo(() => buildings.length, [buildings.length]); const floorCount = useMemo(() => floors.length, [floors.length]); const roomCount = useMemo(() => rooms.length, [rooms.length]); const projectDeviceCount = useMemo(() => projectDevices.length, [projectDevices.length]); @@ -257,6 +260,74 @@ export default function ProjectDetailPage() { } } + async function handleSaveFloor(event: FormEvent) { + event.preventDefault(); + if (!projectId || !project || !floorName.trim()) { + return; + } + setIsSaving(true); + setError(null); + try { + const result = editingFloor + ? await updateFloor( + projectId, + editingFloor.id, + { name: floorName.trim() }, + project.currentRevision + ) + : await createFloor( + projectId, + { name: floorName.trim() }, + project.currentRevision + ); + setFloors((current) => + editingFloor + ? current.map((floor) => + floor.id === result.floor.id ? result.floor : floor + ) + : [...current, result.floor] + ); + applyProjectRevision(result.history.currentRevision); + setFloorName(""); + setEditingFloor(null); + setStructureModal(null); + } catch (err) { + setError(err instanceof Error ? err.message : "Etage konnte nicht gespeichert werden."); + } finally { + setIsSaving(false); + } + } + + async function handleSaveRoom(input: CreateRoomInput) { + if (!projectId || !project) return; + setIsSaving(true); + setError(null); + try { + const result = editingRoom + ? await updateRoom( + projectId, + editingRoom.id, + input, + project.currentRevision + ) + : await createRoom(projectId, input, project.currentRevision); + setRooms((current) => + editingRoom + ? current.map((room) => + room.id === result.room.id ? result.room : room + ) + : [...current, result.room] + ); + applyProjectRevision(result.history.currentRevision); + setEditingRoom(null); + setStructureModal(null); + } catch (err) { + setError(err instanceof Error ? err.message : "Raum konnte nicht gespeichert werden."); + } finally { + setIsSaving(false); + } + } + async function handleSaveProjectSettings(input: ProjectSettingsInput) { if (!projectId || !project) { return; @@ -444,6 +515,65 @@ export default function ProjectDetailPage() { return result; } + function openCreateFloor() { + setEditingFloor(null); + setFloorName(""); + setStructureModal("floor"); + } + + function openEditFloor(floor: FloorDto) { + setEditingFloor(floor); + setFloorName(floor.name); + setStructureModal("floor"); + } + + function openCreateRoom() { + setEditingRoom(null); + setStructureModal("room"); + } + + function openEditRoom(room: RoomDto) { + setEditingRoom(room); + setStructureModal("room"); + } + + async function handleDeleteFloor() { + if (!projectId || !project || !editingFloor) return; + if (!window.confirm(`Etage „${editingFloor.name}“ wirklich löschen?`)) return; + setIsSaving(true); + setError(null); + try { + const result = await deleteFloor(projectId, editingFloor.id, project.currentRevision); + setFloors((current) => current.filter((floor) => floor.id !== editingFloor.id)); + applyProjectRevision(result.history.currentRevision); + setEditingFloor(null); + setFloorName(""); + setStructureModal(null); + } catch (err) { + setError(err instanceof Error ? err.message : "Etage konnte nicht gelöscht werden."); + } finally { + setIsSaving(false); + } + } + + async function handleDeleteRoom() { + if (!projectId || !project || !editingRoom) return; + if (!window.confirm(`Raum „${editingRoom.roomNumber} ${editingRoom.roomName}“ wirklich löschen?`)) return; + setIsSaving(true); + setError(null); + try { + const result = await deleteRoom(projectId, editingRoom.id, project.currentRevision); + setRooms((current) => current.filter((room) => room.id !== editingRoom.id)); + applyProjectRevision(result.history.currentRevision); + setEditingRoom(null); + setStructureModal(null); + } catch (err) { + setError(err instanceof Error ? err.message : "Raum konnte nicht gelöscht werden."); + } finally { + setIsSaving(false); + } + } + async function handleCopyBoard() { if ( !projectId || @@ -772,18 +902,14 @@ export default function ProjectDetailPage() {
Verteilungen
{boardCount}
- -
Gebäude
-
{buildingCount}
- - +
Etagen
{floorCount}
- - +
+
Räume
{roomCount}
- +
Projektgeräte
{projectDeviceCount}
@@ -899,6 +1025,88 @@ export default function ProjectDetailPage() { + +
+
+
+
+ Etagen + {floorCount} +
+ +
+
    + {floors.map((floor) => ( +
  • + {floor.name} + +
  • + ))} + {!floors.length ? ( +
  • Noch keine Etagen vorhanden.
  • + ) : null} +
+
+
+ +
+
+
+
+ Räume + {roomCount} +
+ +
+
+ + + + + + + + + + + {rooms.map((room) => ( + + + + + + + ))} + {!rooms.length ? ( + + + + ) : null} + +
RaumnummerRaumnameEtageAktionen
{room.roomNumber}{room.roomName}{room.floorId ? floorById.get(room.floorId)?.name ?? "-" : "-"} + +
+ Noch keine Räume vorhanden. +
+
+
+
@@ -1158,7 +1366,11 @@ export default function ProjectDetailPage() { value={boardFloorId} > - + {floors.map((floor) => ( + + ))}
@@ -1224,7 +1436,11 @@ export default function ProjectDetailPage() { value={editingBoardFloorId} > - + {floors.map((floor) => ( + + ))}
@@ -1356,6 +1572,57 @@ export default function ProjectDetailPage() { ) : null} + {structureModal === "floor" ? ( + { + setStructureModal(null); + setEditingFloor(null); + }} + onSubmit={handleSaveFloor} + submitDisabled={!floorName.trim()} + submitLabel={editingFloor ? "Änderungen speichern" : "Etage hinzufügen"} + title={editingFloor ? "Etage bearbeiten" : "Etage hinzufügen"} + > + + setFloorName(event.target.value)} + placeholder="z. B. EG" + required + value={floorName} + /> + {editingFloor ? ( +
+

+ Etagen mit zugeordneten Räumen oder Verteilungen können nicht gelöscht werden. +

+ +
+ ) : null} +
+ ) : null} + + {structureModal === "room" ? ( + { + setStructureModal(null); + setEditingRoom(null); + }} + onDelete={editingRoom ? () => void handleDeleteRoom() : undefined} + onSave={handleSaveRoom} + /> + ) : null} + {isProjectDeviceModalOpen ? ( void; - title: string; -} - -function StructureColumn({ - addLabel, - children, - count, - headingId, - isAddDisabled, - onAdd, - title, -}: StructureColumnProps) { - return ( -
-
-

- {title} - {count} -

- -
-
    {children}
-
- ); -} - -interface StructureEntryProps { - actions?: ReactNode; - hint?: string; - isSelected?: boolean; - label: ReactNode; - meta?: string; - onSelect?: () => void; -} - -function StructureEntry({ - actions, - hint, - isSelected = false, - label, - meta, - onSelect, -}: StructureEntryProps) { - const content = ( - <> - - {label} - {hint ? {hint} : null} - - {meta ? {meta} : null} - - ); - return ( -
  • - {onSelect ? ( - - ) : ( -
    {content}
    - )} - {actions ?
    {actions}
    : null} -
  • - ); -} - -interface EntryActionsProps { - deleteLabel: string; - editLabel: string; - isDisabled: boolean; - onDelete: () => void; - onEdit: () => void; -} - -function EntryActions({ - deleteLabel, - editLabel, - isDisabled, - onDelete, - onEdit, -}: EntryActionsProps) { - return ( - <> - - - - ); -} - -export default function BuildingStructurePage() { - const params = useParams<{ projectId: string }>(); - const projectId = params.projectId; - const [project, setProject] = useState(null); - const [buildings, setBuildings] = useState([]); - const [floors, setFloors] = useState([]); - const [rooms, setRooms] = useState([]); - const [buildingSelection, setBuildingSelection] = - useState(ALL_SELECTION); - const [floorSelection, setFloorSelection] = - useState(ALL_SELECTION); - const [dialog, setDialog] = useState(null); - const [dialogName, setDialogName] = useState(""); - const [dialogBuildingId, setDialogBuildingId] = useState(""); - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - - const reloadStructure = useCallback(async () => { - const [currentProject, loadedBuildings, loadedFloors, loadedRooms] = - await Promise.all([ - getProject(projectId), - listBuildings(projectId), - listFloors(projectId), - listRooms(projectId), - ]); - setProject(currentProject); - setBuildings(loadedBuildings); - setFloors(loadedFloors); - setRooms(loadedRooms); - }, [projectId]); - - useEffect(() => { - if (!projectId) { - return; - } - setIsLoading(true); - reloadStructure() - .then(() => setError(null)) - .catch((err: unknown) => - setError( - err instanceof Error - ? err.message - : "Gebäudestruktur konnte nicht geladen werden." - ) - ) - .finally(() => setIsLoading(false)); - }, [projectId, reloadStructure]); - - const effectiveBuildingSelection = normalizeBuildingSelection( - buildingSelection, - floors, - buildings - ); - const effectiveFloorSelection = normalizeFloorSelection( - floorSelection, - rooms, - floors, - buildings, - effectiveBuildingSelection - ); - - const visibleFloors = useMemo( - () => filterFloorsByBuilding(floors, buildings, effectiveBuildingSelection), - [buildings, effectiveBuildingSelection, floors] - ); - const visibleFloorGroups = useMemo( - () => - effectiveBuildingSelection === ALL_SELECTION && buildings.length - ? groupFloorsByBuilding(visibleFloors, buildings) - : [{ buildingId: null, label: "", floors: visibleFloors }], - [buildings, effectiveBuildingSelection, visibleFloors] - ); - const visibleRooms = useMemo( - () => - filterRooms( - rooms, - floors, - buildings, - effectiveBuildingSelection, - effectiveFloorSelection - ).sort(compareRooms), - [buildings, effectiveBuildingSelection, effectiveFloorSelection, floors, rooms] - ); - const roomsInBuildingSelection = useMemo( - () => - filterRooms(rooms, floors, buildings, effectiveBuildingSelection, ALL_SELECTION) - .length, - [buildings, effectiveBuildingSelection, floors, rooms] - ); - const floorCountByBuilding = useMemo( - () => countFloorsByBuilding(floors, buildings), - [buildings, floors] - ); - const roomCountByFloor = useMemo( - () => countRoomsByFloor(rooms, floors), - [floors, rooms] - ); - const showUnassignedBuilding = hasFloorsWithoutBuilding(floors, buildings); - const showUnassignedFloor = showsRoomsWithoutFloorEntry( - rooms, - floors, - effectiveBuildingSelection - ); - const isBusy = isSaving || !project; - - function selectBuilding(selection: StructureSelection) { - setBuildingSelection(selection); - setFloorSelection(ALL_SELECTION); - } - - async function runCommand( - execute: (expectedRevision: number) => Promise, - fallbackMessage: string - ): Promise { - if (!project) { - return false; - } - setIsSaving(true); - setError(null); - try { - const result = await execute(project.currentRevision); - setProject((current) => - current - ? { ...current, currentRevision: result.history.currentRevision } - : current - ); - await reloadStructure(); - return true; - } catch (err) { - setError(err instanceof Error ? err.message : fallbackMessage); - // Refresh the revision and lists so a stale state does not block the - // next attempt; the command error stays visible. - await reloadStructure().catch(() => undefined); - return false; - } finally { - setIsSaving(false); - } - } - - function openBuildingDialog(building: BuildingDto | null) { - setDialogName(building?.name ?? ""); - setDialog({ kind: "building", building }); - } - - function openFloorDialog(floor: FloorDto | null) { - setDialogName(floor?.name ?? ""); - setDialogBuildingId( - floor - ? floor.buildingId ?? "" - : defaultBuildingIdForNewFloor(effectiveBuildingSelection) - ); - setDialog({ kind: "floor", floor }); - } - - function openRoomDialog(room: RoomDto | null) { - setDialog({ - kind: "room", - room, - defaultFloorId: defaultFloorIdForNewRoom(effectiveFloorSelection), - }); - } - - async function handleSaveBuilding(event: FormEvent) { - event.preventDefault(); - if (dialog?.kind !== "building" || !dialogName.trim()) { - return; - } - const editingBuilding = dialog.building; - const name = dialogName.trim(); - // An unchanged dialog is not a command; the backend rejects no-op updates. - if (editingBuilding && editingBuilding.name === name) { - setDialog(null); - return; - } - const saved = await runCommand( - (expectedRevision) => - editingBuilding - ? updateBuilding(projectId, editingBuilding.id, { name }, expectedRevision) - : createBuilding(projectId, { name }, expectedRevision), - "Gebäude konnte nicht gespeichert werden." - ); - if (saved) { - setDialog(null); - } - } - - async function handleSaveFloor(event: FormEvent) { - event.preventDefault(); - if (dialog?.kind !== "floor" || !dialogName.trim()) { - return; - } - const editingFloor = dialog.floor; - const input = { - name: dialogName.trim(), - buildingId: dialogBuildingId || null, - }; - if ( - editingFloor && - editingFloor.name === input.name && - editingFloor.buildingId === input.buildingId - ) { - setDialog(null); - return; - } - const saved = await runCommand( - (expectedRevision) => - editingFloor - ? updateFloor(projectId, editingFloor.id, input, expectedRevision) - : createFloor(projectId, input, expectedRevision), - "Etage konnte nicht gespeichert werden." - ); - if (saved) { - setDialog(null); - } - } - - async function handleSaveRoom(input: CreateRoomInput) { - if (dialog?.kind !== "room") { - return; - } - const editingRoom = dialog.room; - if ( - editingRoom && - editingRoom.roomNumber === input.roomNumber.trim() && - editingRoom.roomName === input.roomName.trim() && - editingRoom.floorId === (input.floorId || null) - ) { - setDialog(null); - return; - } - const saved = await runCommand( - (expectedRevision) => - editingRoom - ? updateRoom(projectId, editingRoom.id, input, expectedRevision) - : createRoom(projectId, input, expectedRevision), - "Raum konnte nicht gespeichert werden." - ); - if (saved) { - setDialog(null); - } - } - - async function handleDeleteBuilding(building: BuildingDto) { - if ((floorCountByBuilding.get(building.id) ?? 0) > 0) { - setError(BUILDING_HAS_FLOORS_MESSAGE); - return; - } - if (!window.confirm(`Gebäude „${building.name}“ wirklich löschen?`)) { - return; - } - await runCommand( - (expectedRevision) => - deleteBuilding(projectId, building.id, expectedRevision), - "Gebäude konnte nicht gelöscht werden." - ); - } - - async function handleDeleteFloor(floor: FloorDto) { - if (!window.confirm(`Etage „${floor.name}“ wirklich löschen?`)) { - return; - } - await runCommand( - (expectedRevision) => deleteFloor(projectId, floor.id, expectedRevision), - "Etage konnte nicht gelöscht werden." - ); - } - - async function handleDeleteRoom(room: RoomDto) { - if ( - !window.confirm( - `Raum „${room.roomNumber} ${room.roomName}“ wirklich löschen?` - ) - ) { - return; - } - await runCommand( - (expectedRevision) => deleteRoom(projectId, room.id, expectedRevision), - "Raum konnte nicht gelöscht werden." - ); - } - - return ( -
    -
    -
    - -
    -
    - - {error ? ( -
    - {error} -
    - ) : null} - {isLoading ? ( -

    - Gebäudestruktur wird geladen … -

    - ) : null} - -
    - openBuildingDialog(null)} - title="Gebäude" - > - selectBuilding(ALL_SELECTION)} - /> - {buildings.map((building) => ( - void handleDeleteBuilding(building)} - onEdit={() => openBuildingDialog(building)} - /> - } - isSelected={effectiveBuildingSelection === building.id} - key={building.id} - label={building.name} - meta={formatCount( - floorCountByBuilding.get(building.id) ?? 0, - "Etage", - "Etagen" - )} - onSelect={() => selectBuilding(building.id)} - /> - ))} - {showUnassignedBuilding ? ( - {UNASSIGNED_BUILDING_LABEL}} - meta={formatCount( - floorCountByBuilding.get(null) ?? 0, - "Etage", - "Etagen" - )} - onSelect={() => selectBuilding(UNASSIGNED_SELECTION)} - /> - ) : null} - {!buildings.length && !isLoading ? ( -
  • - Noch keine Gebäude vorhanden. -
  • - ) : null} -
    - - openFloorDialog(null)} - title="Etagen" - > - setFloorSelection(ALL_SELECTION)} - /> - {visibleFloorGroups.map((group) => ( - - {group.floors.map((floor) => ( - void handleDeleteFloor(floor)} - onEdit={() => openFloorDialog(floor)} - /> - } - isSelected={effectiveFloorSelection === floor.id} - key={floor.id} - label={floor.name} - meta={formatCount( - roomCountByFloor.get(floor.id) ?? 0, - "Raum", - "Räume" - )} - onSelect={() => setFloorSelection(floor.id)} - /> - ))} - - ))} - {showUnassignedFloor ? ( - {UNASSIGNED_FLOOR_LABEL}} - meta={formatCount(roomCountByFloor.get(null) ?? 0, "Raum", "Räume")} - onSelect={() => setFloorSelection(UNASSIGNED_SELECTION)} - /> - ) : null} - {!visibleFloors.length && !isLoading ? ( -
  • - {floors.length - ? "Keine Etagen in dieser Auswahl." - : "Noch keine Etagen vorhanden."} -
  • - ) : null} -
    - - openRoomDialog(null)} - title="Räume" - > - {visibleRooms.map((room) => ( - void handleDeleteRoom(room)} - onEdit={() => openRoomDialog(room)} - /> - } - hint={ - effectiveFloorSelection === ALL_SELECTION - ? describeRoomLocation(room, floors, buildings) - : undefined - } - key={room.id} - label={ - <> - {room.roomNumber}{" "} - {room.roomName} - - } - /> - ))} - {!visibleRooms.length && !isLoading ? ( -
  • - {rooms.length - ? "Keine Räume in dieser Auswahl." - : "Noch keine Räume vorhanden."} -
  • - ) : null} -
    -
    - - {dialog?.kind === "building" ? ( - setDialog(null)} - onSubmit={handleSaveBuilding} - submitDisabled={!dialogName.trim()} - submitLabel={dialog.building ? "Änderungen speichern" : "Gebäude hinzufügen"} - title={dialog.building ? "Gebäude bearbeiten" : "Gebäude hinzufügen"} - > - - setDialogName(event.target.value)} - placeholder="z. B. Haus A" - required - value={dialogName} - /> - - ) : null} - - {dialog?.kind === "floor" ? ( - setDialog(null)} - onSubmit={handleSaveFloor} - submitDisabled={!dialogName.trim()} - submitLabel={dialog.floor ? "Änderungen speichern" : "Etage hinzufügen"} - title={dialog.floor ? "Etage bearbeiten" : "Etage hinzufügen"} - > -
    -
    - - setDialogName(event.target.value)} - placeholder="z. B. EG" - required - value={dialogName} - /> -
    -
    - - -
    -
    -
    - ) : null} - - {dialog?.kind === "room" ? ( - setDialog(null)} - onSave={handleSaveRoom} - /> - ) : null} -
    - ); -} - -function FloorGroupEntries({ - children, - label, -}: { - children: ReactNode; - label: string; -}) { - return ( - <> - {label ? ( -
  • - {label} -
  • - ) : null} - {children} - - ); -} diff --git a/src/db/migrations/0009_buildings.sql b/src/db/migrations/0009_buildings.sql deleted file mode 100644 index 6f65174..0000000 --- a/src/db/migrations/0009_buildings.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE `buildings` ( - `id` text PRIMARY KEY NOT NULL, - `project_id` text NOT NULL, - `name` text NOT NULL, - `sort_order` integer DEFAULT 0 NOT NULL, - FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -ALTER TABLE `floors` ADD `building_id` text REFERENCES buildings(id) ON DELETE set null; \ No newline at end of file diff --git a/src/db/migrations/meta/0009_snapshot.json b/src/db/migrations/meta/0009_snapshot.json deleted file mode 100644 index 1b7d566..0000000 --- a/src/db/migrations/meta/0009_snapshot.json +++ /dev/null @@ -1,2626 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "2cd9adef-2f08-4635-b3d9-ecf71f624c7f", - "prevId": "fb609719-ea17-4833-a27b-f75da0d3e860", - "tables": { - "buildings": { - "name": "buildings", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "buildings_project_id_projects_id_fk": { - "name": "buildings_project_id_projects_id_fk", - "tableFrom": "buildings", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "manual_quantity": { - "name": "manual_quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuit_device_rows_circuit_id_idx": { - "name": "circuit_device_rows_circuit_id_idx", - "columns": [ - "circuit_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_list_equipment_identifiers": { - "name": "circuit_list_equipment_identifiers", - "columns": { - "owner_type": { - "name": "owner_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "owner_id": { - "name": "owner_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_list_equipment_identifiers_owner_unique": { - "name": "circuit_list_equipment_identifiers_owner_unique", - "columns": [ - "owner_type", - "owner_id" - ], - "isUnique": true - }, - "circuit_list_equipment_identifiers_list_identifier_unique": { - "name": "circuit_list_equipment_identifiers_list_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_list_equipment_identifiers", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_protection_devices": { - "name": "circuit_protection_devices", - "columns": { - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "rated_current_a": { - "name": "rated_current_a", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "fuse_utilization_category": { - "name": "fuse_utilization_category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trip_characteristic": { - "name": "trip_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_type": { - "name": "rcd_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rated_residual_current_ma": { - "name": "rated_residual_current_ma", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_protection_devices_circuit_id_circuits_id_fk": { - "name": "circuit_protection_devices_circuit_id_circuits_id_fk", - "tableFrom": "circuit_protection_devices", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_number": { - "name": "group_number", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - }, - "circuit_sections_list_category_group_unique": { - "name": "circuit_sections_list_category_group_unique", - "columns": [ - "circuit_list_id", - "category", - "group_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_section_id_idx": { - "name": "circuits_section_id_idx", - "columns": [ - "section_id" - ], - "isUnique": false - }, - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_board_component_protection_devices": { - "name": "distribution_board_component_protection_devices", - "columns": { - "component_id": { - "name": "component_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "rated_current_a": { - "name": "rated_current_a", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "fuse_utilization_category": { - "name": "fuse_utilization_category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trip_characteristic": { - "name": "trip_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_type": { - "name": "rcd_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rated_residual_current_ma": { - "name": "rated_residual_current_ma", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk": { - "name": "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk", - "tableFrom": "distribution_board_component_protection_devices", - "tableTo": "distribution_board_components", - "columnsFrom": [ - "component_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_board_components": { - "name": "distribution_board_components", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "placement": { - "name": "placement", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "distribution_board_components_list_identifier_unique": { - "name": "distribution_board_components_list_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - }, - "distribution_board_components_section_role_unique": { - "name": "distribution_board_components_section_role_unique", - "columns": [ - "section_id", - "role" - ], - "isUnique": true - } - }, - "foreignKeys": { - "distribution_board_components_circuit_list_id_circuit_lists_id_fk": { - "name": "distribution_board_components_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "distribution_board_components", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "distribution_board_components_section_id_circuit_sections_id_fk": { - "name": "distribution_board_components_section_id_circuit_sections_id_fk", - "tableFrom": "distribution_board_components", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "supply_type": { - "name": "supply_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "board_number": { - "name": "board_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "location": { - "name": "location", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "fed_from": { - "name": "fed_from", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "distribution_boards_floor_id_floors_id_fk": { - "name": "distribution_boards_floor_id_floors_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "document_branding_assets": { - "name": "document_branding_assets", - "columns": { - "key": { - "name": "key", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "mime_type": { - "name": "mime_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "file_name": { - "name": "file_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "data": { - "name": "data", - "type": "blob", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "document_branding_settings": { - "name": "document_branding_settings", - "columns": { - "key": { - "name": "key", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_csv_configurations": { - "name": "external_csv_configurations", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "configuration_version": { - "name": "configuration_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "configuration": { - "name": "configuration", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "external_csv_configurations_project_id_unique": { - "name": "external_csv_configurations_project_id_unique", - "columns": [ - "project_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "external_csv_configurations_project_id_projects_id_fk": { - "name": "external_csv_configurations_project_id_projects_id_fk", - "tableFrom": "external_csv_configurations", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_import_batches": { - "name": "external_import_batches", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_id": { - "name": "source_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "import_kind": { - "name": "import_kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "imported_at_iso": { - "name": "imported_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "file_name": { - "name": "file_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sha256": { - "name": "sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "applied_project_revision": { - "name": "applied_project_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "configuration_version": { - "name": "configuration_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "configuration_snapshot": { - "name": "configuration_snapshot", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "original_bytes": { - "name": "original_bytes", - "type": "blob", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "document": { - "name": "document", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "external_import_batches_project_revision_idx": { - "name": "external_import_batches_project_revision_idx", - "columns": [ - "project_id", - "applied_project_revision" - ], - "isUnique": false - }, - "external_import_batches_source_imported_idx": { - "name": "external_import_batches_source_imported_idx", - "columns": [ - "source_id", - "imported_at_iso" - ], - "isUnique": false - } - }, - "foreignKeys": { - "external_import_batches_project_id_projects_id_fk": { - "name": "external_import_batches_project_id_projects_id_fk", - "tableFrom": "external_import_batches", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_import_batches_source_id_external_model_sources_id_fk": { - "name": "external_import_batches_source_id_external_model_sources_id_fk", - "tableFrom": "external_import_batches", - "tableTo": "external_model_sources", - "columnsFrom": [ - "source_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_model_objects": { - "name": "external_model_objects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_id": { - "name": "source_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "ifc_guid": { - "name": "ifc_guid", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "last_seen_import_batch_id": { - "name": "last_seen_import_batch_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "last_accepted_import_batch_id": { - "name": "last_accepted_import_batch_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "accepted_source_values": { - "name": "accepted_source_values", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "planning_values": { - "name": "planning_values", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "external_room_mapping_id": { - "name": "external_room_mapping_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "presence_status": { - "name": "presence_status", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'present'" - } - }, - "indexes": { - "external_model_objects_source_ifc_guid_unique": { - "name": "external_model_objects_source_ifc_guid_unique", - "columns": [ - "source_id", - "ifc_guid" - ], - "isUnique": true - }, - "external_model_objects_project_presence_idx": { - "name": "external_model_objects_project_presence_idx", - "columns": [ - "project_id", - "presence_status" - ], - "isUnique": false - }, - "external_model_objects_distribution_board_idx": { - "name": "external_model_objects_distribution_board_idx", - "columns": [ - "distribution_board_id" - ], - "isUnique": false - }, - "external_model_objects_circuit_device_row_idx": { - "name": "external_model_objects_circuit_device_row_idx", - "columns": [ - "circuit_device_row_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "external_model_objects_project_id_projects_id_fk": { - "name": "external_model_objects_project_id_projects_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_model_objects_source_id_external_model_sources_id_fk": { - "name": "external_model_objects_source_id_external_model_sources_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "external_model_sources", - "columnsFrom": [ - "source_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk": { - "name": "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "external_import_batches", - "columnsFrom": [ - "last_seen_import_batch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk": { - "name": "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "external_import_batches", - "columnsFrom": [ - "last_accepted_import_batch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk": { - "name": "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "external_room_mappings", - "columnsFrom": [ - "external_room_mapping_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "external_model_objects_distribution_board_id_distribution_boards_id_fk": { - "name": "external_model_objects_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "external_model_objects_linked_project_device_id_project_devices_id_fk": { - "name": "external_model_objects_linked_project_device_id_project_devices_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "circuit_device_rows", - "columnsFrom": [ - "circuit_device_row_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_model_sources": { - "name": "external_model_sources", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_type": { - "name": "source_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'revit_csv'" - } - }, - "indexes": { - "external_model_sources_project_type_unique": { - "name": "external_model_sources_project_type_unique", - "columns": [ - "project_id", - "source_type" - ], - "isUnique": true - } - }, - "foreignKeys": { - "external_model_sources_project_id_projects_id_fk": { - "name": "external_model_sources_project_id_projects_id_fk", - "tableFrom": "external_model_sources", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_room_mappings": { - "name": "external_room_mappings", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_id": { - "name": "source_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "normalized_source_room_key": { - "name": "normalized_source_room_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_floor_name": { - "name": "source_floor_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source_room_number": { - "name": "source_room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_room_name": { - "name": "source_room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "default_distribution_board_id": { - "name": "default_distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "external_room_mappings_source_key_unique": { - "name": "external_room_mappings_source_key_unique", - "columns": [ - "source_id", - "normalized_source_room_key" - ], - "isUnique": true - }, - "external_room_mappings_project_room_idx": { - "name": "external_room_mappings_project_room_idx", - "columns": [ - "project_id", - "room_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "external_room_mappings_project_id_projects_id_fk": { - "name": "external_room_mappings_project_id_projects_id_fk", - "tableFrom": "external_room_mappings", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_room_mappings_source_id_external_model_sources_id_fk": { - "name": "external_room_mappings_source_id_external_model_sources_id_fk", - "tableFrom": "external_room_mappings", - "tableTo": "external_model_sources", - "columnsFrom": [ - "source_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_room_mappings_room_id_rooms_id_fk": { - "name": "external_room_mappings_room_id_rooms_id_fk", - "tableFrom": "external_room_mappings", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk": { - "name": "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "external_room_mappings", - "tableTo": "distribution_boards", - "columnsFrom": [ - "default_distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "building_id": { - "name": "building_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "floors_building_id_buildings_id_fk": { - "name": "floors_building_id_buildings_id_fk", - "tableFrom": "floors", - "tableTo": "buildings", - "columnsFrom": [ - "building_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "tableTo": "project_revisions", - "columnsFrom": [ - "project_revision_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "project_change_sets", - "columnsFrom": [ - "change_set_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_snapshots": { - "name": "project_snapshots", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_revision": { - "name": "source_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "schema_version": { - "name": "schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "kind": { - "name": "kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'named'" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_sha256": { - "name": "payload_sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_by_actor_id": { - "name": "created_by_actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_snapshots_project_created_idx": { - "name": "project_snapshots_project_created_idx", - "columns": [ - "project_id", - "created_at_iso" - ], - "isUnique": false - }, - "project_snapshots_project_kind_revision_idx": { - "name": "project_snapshots_project_kind_revision_idx", - "columns": [ - "project_id", - "kind", - "source_revision" - ], - "isUnique": false - }, - "project_snapshots_project_name_unique": { - "name": "project_snapshots_project_name_unique", - "columns": [ - "project_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_snapshots_project_id_projects_id_fk": { - "name": "project_snapshots_project_id_projects_id_fk", - "tableFrom": "project_snapshots", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "internal_project_number": { - "name": "internal_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "external_project_number": { - "name": "external_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "building_owner": { - "name": "building_owner", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_public_building": { - "name": "is_public_building", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "enabled_distribution_board_supply_types": { - "name": "enabled_distribution_board_supply_types", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" - }, - "site_address": { - "name": "site_address", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "work_stage": { - "name": "work_stage", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_lead": { - "name": "project_lead", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_editor": { - "name": "project_editor", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 485b8f4..8322eb3 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -64,13 +64,6 @@ "when": 1790411560953, "tag": "0008_project_cover_and_office_profile", "breakpoints": true - }, - { - "idx": 9, - "version": "6", - "when": 1790419185882, - "tag": "0009_buildings", - "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/repositories/building.repository.ts b/src/db/repositories/building.repository.ts deleted file mode 100644 index 011739f..0000000 --- a/src/db/repositories/building.repository.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { and, asc, eq } from "drizzle-orm"; -import type { AppDatabase } from "../database-context.js"; -import { buildings } from "../schema/buildings.js"; - -export class BuildingRepository { - constructor(private readonly database: AppDatabase) {} - - async listByProject(projectId: string) { - return this.database - .select() - .from(buildings) - .where(eq(buildings.projectId, projectId)) - .orderBy(asc(buildings.sortOrder), asc(buildings.name)); - } - - async findById(projectId: string, buildingId: string) { - return ( - this.database - .select() - .from(buildings) - .where( - and(eq(buildings.projectId, projectId), eq(buildings.id, buildingId)) - ) - .get() ?? null - ); - } -} diff --git a/src/db/repositories/project-location-structure-project-command.repository.ts b/src/db/repositories/project-location-structure-project-command.repository.ts index 9ec3f48..7cbadde 100644 --- a/src/db/repositories/project-location-structure-project-command.repository.ts +++ b/src/db/repositories/project-location-structure-project-command.repository.ts @@ -1,34 +1,23 @@ import { and, eq } from "drizzle-orm"; import { - assertProjectBuildingDeleteProjectCommand, - assertProjectBuildingInsertProjectCommand, - assertProjectBuildingUpdateProjectCommand, assertProjectFloorDeleteProjectCommand, assertProjectFloorInsertProjectCommand, assertProjectFloorUpdateProjectCommand, assertProjectRoomDeleteProjectCommand, assertProjectRoomInsertProjectCommand, assertProjectRoomUpdateProjectCommand, - createProjectBuildingDeleteProjectCommand, - createProjectBuildingInsertProjectCommand, - createProjectBuildingUpdateProjectCommand, createProjectFloorDeleteProjectCommand, createProjectFloorInsertProjectCommand, createProjectFloorUpdateProjectCommand, createProjectRoomDeleteProjectCommand, createProjectRoomInsertProjectCommand, createProjectRoomUpdateProjectCommand, - projectBuildingDeleteCommandType, - projectBuildingInsertCommandType, - projectBuildingUpdateCommandType, projectFloorDeleteCommandType, projectFloorInsertCommandType, projectFloorUpdateCommandType, projectRoomDeleteCommandType, projectRoomInsertCommandType, projectRoomUpdateCommandType, - upgradeProjectFloorCommand, - type ProjectBuildingSnapshot, type ProjectFloorSnapshot, type ProjectLocationStructureProjectCommand, type ProjectRoomSnapshot, @@ -38,7 +27,6 @@ import type { ProjectLocationStructureProjectCommandStore, } from "../../domain/ports/project-location-structure-project-command.store.js"; import type { AppDatabase } from "../database-context.js"; -import { buildings } from "../schema/buildings.js"; import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; import { floors } from "../schema/floors.js"; import { distributionBoards } from "../schema/distribution-boards.js"; @@ -52,14 +40,11 @@ export class ProjectLocationStructureProjectCommandRepository constructor(private readonly database: AppDatabase) {} execute(input: ExecuteProjectLocationStructureCommandInput) { - // Forward and inverse are persisted with one schema version, so a - // version 1 floor command from older history is upgraded first. - const command = toCurrentCommandVersion(input.command); return executeProjectCommandTransaction( this.database, - { ...input, command }, + input, (tx) => - this.applyCommand(tx, input.projectId, command) + this.applyCommand(tx, input.projectId, input.command) ); } @@ -69,43 +54,23 @@ export class ProjectLocationStructureProjectCommandRepository command: ProjectLocationStructureProjectCommand ): ProjectLocationStructureProjectCommand { switch (command.type) { - case projectBuildingInsertCommandType: - assertProjectBuildingInsertProjectCommand(command); - return this.insertBuilding(database, projectId, command.payload.building); - case projectBuildingDeleteCommandType: - assertProjectBuildingDeleteProjectCommand(command); - return this.deleteBuilding(database, projectId, command.payload.building); - case projectBuildingUpdateCommandType: - assertProjectBuildingUpdateProjectCommand(command); - return this.updateBuilding( - database, - projectId, - command.payload.expected, - command.payload.target - ); - case projectFloorInsertCommandType: { + case projectFloorInsertCommandType: assertProjectFloorInsertProjectCommand(command); - const current = upgradeProjectFloorCommand(command); return this.insertFloor( database, projectId, - current.payload.floor + command.payload.floor ); - } - case projectFloorDeleteCommandType: { + case projectFloorDeleteCommandType: assertProjectFloorDeleteProjectCommand(command); - const current = upgradeProjectFloorCommand(command); return this.deleteFloor( database, projectId, - current.payload.floor + command.payload.floor ); - } - case projectFloorUpdateCommandType: { + case projectFloorUpdateCommandType: assertProjectFloorUpdateProjectCommand(command); - const current = upgradeProjectFloorCommand(command); - return this.updateFloor(database, projectId, current.payload.expected, current.payload.target); - } + return this.updateFloor(database, projectId, command.payload.expected, command.payload.target); case projectRoomInsertCommandType: assertProjectRoomInsertProjectCommand(command); return this.insertRoom( @@ -135,7 +100,6 @@ export class ProjectLocationStructureProjectCommandRepository if (floor.projectId !== projectId) { throw new Error("Project-floor snapshot does not belong to project."); } - this.assertBuildingInProject(database, projectId, floor.buildingId); const existing = database .select({ id: floors.id }) .from(floors) @@ -203,104 +167,6 @@ export class ProjectLocationStructureProjectCommandRepository return createProjectFloorInsertProjectCommand(expected); } - private insertBuilding( - database: AppDatabase, - projectId: string, - building: ProjectBuildingSnapshot - ) { - this.assertProjectExists(database, projectId); - if (building.projectId !== projectId) { - throw new Error("Project-building snapshot does not belong to project."); - } - const existing = database - .select({ id: buildings.id }) - .from(buildings) - .where(eq(buildings.id, building.id)) - .get(); - if (existing) { - throw new Error("Project-building id already exists."); - } - database.insert(buildings).values(building).run(); - return createProjectBuildingDeleteProjectCommand(building); - } - - private deleteBuilding( - database: AppDatabase, - projectId: string, - expected: ProjectBuildingSnapshot - ) { - const persisted = database - .select() - .from(buildings) - .where(and(eq(buildings.id, expected.id), eq(buildings.projectId, projectId))) - .get(); - if (!persisted || !sameRecord(expected, persisted)) { - throw new Error("Project building changed before deletion."); - } - const referencedFloor = database - .select({ id: floors.id }) - .from(floors) - .where(eq(floors.buildingId, expected.id)) - .limit(1) - .get(); - if (referencedFloor) { - throw new Error( - "Das Gebäude kann nicht gelöscht werden, solange Etagen zugeordnet sind." - ); - } - const deleted = database - .delete(buildings) - .where(and(eq(buildings.id, expected.id), eq(buildings.projectId, projectId))) - .run(); - if (deleted.changes !== 1) { - throw new Error("Project building could not be deleted."); - } - return createProjectBuildingInsertProjectCommand(expected); - } - - private updateBuilding( - database: AppDatabase, - projectId: string, - expected: ProjectBuildingSnapshot, - target: ProjectBuildingSnapshot - ) { - const persisted = database - .select() - .from(buildings) - .where(and(eq(buildings.id, expected.id), eq(buildings.projectId, projectId))) - .get(); - if (!persisted || !sameRecord(expected, persisted)) { - throw new Error("Project building changed before update."); - } - const updated = database - .update(buildings) - .set({ name: target.name }) - .where(and(eq(buildings.id, expected.id), eq(buildings.projectId, projectId))) - .run(); - if (updated.changes !== 1) { - throw new Error("Das Gebäude konnte nicht aktualisiert werden."); - } - return createProjectBuildingUpdateProjectCommand(target, expected); - } - - private assertBuildingInProject( - database: AppDatabase, - projectId: string, - buildingId: string | null - ) { - if (buildingId === null) { - return; - } - const building = database - .select({ id: buildings.id }) - .from(buildings) - .where(and(eq(buildings.id, buildingId), eq(buildings.projectId, projectId))) - .get(); - if (!building) { - throw new Error("Project floor references a foreign building."); - } - } - private insertRoom( database: AppDatabase, projectId: string, @@ -351,10 +217,9 @@ export class ProjectLocationStructureProjectCommandRepository if (!persisted || !sameRecord(expected, persisted)) { throw new Error("Project floor changed before update."); } - this.assertBuildingInProject(database, projectId, target.buildingId); const updated = database .update(floors) - .set({ name: target.name, buildingId: target.buildingId }) + .set({ name: target.name }) .where(and(eq(floors.id, expected.id), eq(floors.projectId, projectId))) .run(); if (updated.changes !== 1) { @@ -461,24 +326,6 @@ export class ProjectLocationStructureProjectCommandRepository } } -function toCurrentCommandVersion( - command: ProjectLocationStructureProjectCommand -): ProjectLocationStructureProjectCommand { - switch (command.type) { - case projectFloorInsertCommandType: - assertProjectFloorInsertProjectCommand(command); - return upgradeProjectFloorCommand(command); - case projectFloorDeleteCommandType: - assertProjectFloorDeleteProjectCommand(command); - return upgradeProjectFloorCommand(command); - case projectFloorUpdateCommandType: - assertProjectFloorUpdateProjectCommand(command); - return upgradeProjectFloorCommand(command); - default: - return command; - } -} - function sameRecord( expected: object, actual: object diff --git a/src/db/repositories/project-state-restore-command.repository.ts b/src/db/repositories/project-state-restore-command.repository.ts index 496f2cb..cf6f9bc 100644 --- a/src/db/repositories/project-state-restore-command.repository.ts +++ b/src/db/repositories/project-state-restore-command.repository.ts @@ -21,7 +21,6 @@ import { circuits } from "../schema/circuits.js"; import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js"; import { distributionBoardComponents } from "../schema/distribution-board-components.js"; import { distributionBoards } from "../schema/distribution-boards.js"; -import { buildings } from "../schema/buildings.js"; import { floors } from "../schema/floors.js"; import { projectDevices } from "../schema/project-devices.js"; import { projectChangeSets } from "../schema/project-change-sets.js"; @@ -165,7 +164,6 @@ function canonicalProjectState(state: ProjectStateSnapshot) { })) ), projectDevices: byId(state.projectDevices), - buildings: byId(state.buildings), floors: byId(state.floors), rooms: byId(state.rooms), externalCsvConfiguration: state.externalCsvConfiguration, @@ -221,10 +219,6 @@ function replaceProjectState( .delete(floors) .where(eq(floors.projectId, state.project.id)) .run(); - database - .delete(buildings) - .where(eq(buildings.projectId, state.project.id)) - .run(); database .update(projects) @@ -247,7 +241,6 @@ function replaceProjectState( .where(eq(projects.id, state.project.id)) .run(); - insertMany(database, buildings, state.buildings); insertMany(database, floors, state.floors); if (state.externalCsvConfiguration !== null) { database diff --git a/src/db/repositories/project-state-snapshot.persistence.ts b/src/db/repositories/project-state-snapshot.persistence.ts index a11cd58..84131e7 100644 --- a/src/db/repositories/project-state-snapshot.persistence.ts +++ b/src/db/repositories/project-state-snapshot.persistence.ts @@ -15,7 +15,6 @@ import { circuits } from "../schema/circuits.js"; import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js"; import { distributionBoardComponents } from "../schema/distribution-board-components.js"; import { distributionBoards } from "../schema/distribution-boards.js"; -import { buildings } from "../schema/buildings.js"; import { floors } from "../schema/floors.js"; import { projectDevices } from "../schema/project-devices.js"; import { projects } from "../schema/projects.js"; @@ -220,12 +219,6 @@ export function readProjectStateSnapshot( .where(eq(projectDevices.projectId, projectId)) .orderBy(asc(projectDevices.displayName), asc(projectDevices.id)) .all(); - const buildingRows = database - .select() - .from(buildings) - .where(eq(buildings.projectId, projectId)) - .orderBy(asc(buildings.sortOrder), asc(buildings.id)) - .all(); const floorRows = database .select() .from(floors) @@ -280,7 +273,6 @@ export function readProjectStateSnapshot( deviceRows: deviceRowsByCircuit.get(circuit.id) ?? [], })), projectDevices: projectDeviceRows, - buildings: buildingRows, floors: floorRows, rooms: roomRows, }); diff --git a/src/db/repositories/project-transfer.repository.ts b/src/db/repositories/project-transfer.repository.ts index 11f23f7..07a5067 100644 --- a/src/db/repositories/project-transfer.repository.ts +++ b/src/db/repositories/project-transfer.repository.ts @@ -16,7 +16,6 @@ import { circuits } from "../schema/circuits.js"; import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js"; import { distributionBoardComponents } from "../schema/distribution-board-components.js"; import { distributionBoards } from "../schema/distribution-boards.js"; -import { buildings } from "../schema/buildings.js"; import { floors } from "../schema/floors.js"; import { projectDevices } from "../schema/project-devices.js"; import { projects } from "../schema/projects.js"; @@ -128,7 +127,6 @@ function insertProjectState( .values(state.externalCsvConfiguration) .run(); } - insertMany(database, buildings, state.buildings); insertMany(database, floors, state.floors); insertMany(database, rooms, state.rooms); insertMany(database, projectDevices, state.projectDevices); diff --git a/src/db/schema/buildings.ts b/src/db/schema/buildings.ts deleted file mode 100644 index 7d2fb78..0000000 --- a/src/db/schema/buildings.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; -import { projects } from "./projects.js"; - -export const buildings = sqliteTable("buildings", { - id: text("id").primaryKey(), - projectId: text("project_id") - .notNull() - .references(() => projects.id, { onDelete: "cascade" }), - name: text("name").notNull(), - sortOrder: integer("sort_order").notNull().default(0), -}); diff --git a/src/db/schema/floors.ts b/src/db/schema/floors.ts index bd50534..5c68753 100644 --- a/src/db/schema/floors.ts +++ b/src/db/schema/floors.ts @@ -1,5 +1,4 @@ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; -import { buildings } from "./buildings.js"; import { projects } from "./projects.js"; export const floors = sqliteTable("floors", { @@ -9,9 +8,4 @@ export const floors = sqliteTable("floors", { .references(() => projects.id, { onDelete: "cascade" }), name: text("name").notNull(), sortOrder: integer("sort_order").notNull().default(0), - // Optional parent building. Floors created before buildings existed stay - // without one and are shown as "Ohne Gebäude". - buildingId: text("building_id").references(() => buildings.id, { - onDelete: "set null", - }), }); diff --git a/src/domain/models/project-location-structure-project-command.model.ts b/src/domain/models/project-location-structure-project-command.model.ts index 21dfa89..616add9 100644 --- a/src/domain/models/project-location-structure-project-command.model.ts +++ b/src/domain/models/project-location-structure-project-command.model.ts @@ -1,42 +1,21 @@ import crypto from "node:crypto"; import type { SerializedProjectCommand } from "./project-command.model.js"; -export const projectBuildingInsertCommandType = "project-building.insert" as const; -export const projectBuildingDeleteCommandType = "project-building.delete" as const; -export const projectBuildingUpdateCommandType = "project-building.update" as const; export const projectFloorInsertCommandType = "project-floor.insert" as const; export const projectFloorDeleteCommandType = "project-floor.delete" as const; export const projectFloorUpdateCommandType = "project-floor.update" as const; export const projectRoomInsertCommandType = "project-room.insert" as const; export const projectRoomDeleteCommandType = "project-room.delete" as const; export const projectRoomUpdateCommandType = "project-room.update" as const; -/** Room commands, building commands and legacy floor commands. */ export const projectLocationStructureCommandSchemaVersion = 1 as const; -/** - * Floor commands carry `buildingId` since version 2. Version 1 floor - * commands stay executable from persisted history: they were recorded before - * buildings existed, so they are upgraded with `buildingId = null`. - */ -export const projectFloorCommandSchemaVersion = 2 as const; -const legacyProjectFloorCommandSchemaVersion = 1 as const; - -export interface ProjectBuildingSnapshot { - id: string; - projectId: string; - name: string; - sortOrder: number; -} export interface ProjectFloorSnapshot { id: string; projectId: string; name: string; sortOrder: number; - buildingId: string | null; } -export type LegacyProjectFloorSnapshot = Omit; - export interface ProjectRoomSnapshot { id: string; projectId: string; @@ -45,93 +24,39 @@ export interface ProjectRoomSnapshot { roomName: string; } -interface ProjectBuildingStructureCommandPayload { - building: ProjectBuildingSnapshot; -} - interface ProjectFloorStructureCommandPayload { floor: ProjectFloorSnapshot; } -interface LegacyProjectFloorStructureCommandPayload { - floor: LegacyProjectFloorSnapshot; -} - interface ProjectRoomStructureCommandPayload { room: ProjectRoomSnapshot; } -interface ProjectBuildingUpdateCommandPayload { - expected: ProjectBuildingSnapshot; - target: ProjectBuildingSnapshot; -} - interface ProjectFloorUpdateCommandPayload { expected: ProjectFloorSnapshot; target: ProjectFloorSnapshot; } -interface LegacyProjectFloorUpdateCommandPayload { - expected: LegacyProjectFloorSnapshot; - target: LegacyProjectFloorSnapshot; -} - interface ProjectRoomUpdateCommandPayload { expected: ProjectRoomSnapshot; target: ProjectRoomSnapshot; } -export interface ProjectBuildingInsertProjectCommand - extends SerializedProjectCommand { - schemaVersion: typeof projectLocationStructureCommandSchemaVersion; - type: typeof projectBuildingInsertCommandType; -} - -export interface ProjectBuildingDeleteProjectCommand - extends SerializedProjectCommand { - schemaVersion: typeof projectLocationStructureCommandSchemaVersion; - type: typeof projectBuildingDeleteCommandType; -} - -export interface ProjectBuildingUpdateProjectCommand - extends SerializedProjectCommand { - schemaVersion: typeof projectLocationStructureCommandSchemaVersion; - type: typeof projectBuildingUpdateCommandType; -} - export interface ProjectFloorInsertProjectCommand extends SerializedProjectCommand { - schemaVersion: typeof projectFloorCommandSchemaVersion; + schemaVersion: typeof projectLocationStructureCommandSchemaVersion; type: typeof projectFloorInsertCommandType; } export interface ProjectFloorDeleteProjectCommand extends SerializedProjectCommand { - schemaVersion: typeof projectFloorCommandSchemaVersion; + schemaVersion: typeof projectLocationStructureCommandSchemaVersion; type: typeof projectFloorDeleteCommandType; } export interface ProjectFloorUpdateProjectCommand extends SerializedProjectCommand { - schemaVersion: typeof projectFloorCommandSchemaVersion; - type: typeof projectFloorUpdateCommandType; -} - -export interface LegacyProjectFloorInsertProjectCommand - extends SerializedProjectCommand { - schemaVersion: typeof legacyProjectFloorCommandSchemaVersion; - type: typeof projectFloorInsertCommandType; -} - -export interface LegacyProjectFloorDeleteProjectCommand - extends SerializedProjectCommand { - schemaVersion: typeof legacyProjectFloorCommandSchemaVersion; - type: typeof projectFloorDeleteCommandType; -} - -export interface LegacyProjectFloorUpdateProjectCommand - extends SerializedProjectCommand { - schemaVersion: typeof legacyProjectFloorCommandSchemaVersion; + schemaVersion: typeof projectLocationStructureCommandSchemaVersion; type: typeof projectFloorUpdateCommandType; } @@ -154,46 +79,23 @@ export interface ProjectRoomUpdateProjectCommand } export type ProjectLocationStructureProjectCommand = - | ProjectBuildingInsertProjectCommand - | ProjectBuildingDeleteProjectCommand - | ProjectBuildingUpdateProjectCommand | ProjectFloorInsertProjectCommand | ProjectFloorDeleteProjectCommand | ProjectFloorUpdateProjectCommand - | LegacyProjectFloorInsertProjectCommand - | LegacyProjectFloorDeleteProjectCommand - | LegacyProjectFloorUpdateProjectCommand | ProjectRoomInsertProjectCommand | ProjectRoomDeleteProjectCommand | ProjectRoomUpdateProjectCommand; -export function createProjectBuildingSnapshot( - projectId: string, - name: string, - sortOrder: number -): ProjectBuildingSnapshot { - const building: ProjectBuildingSnapshot = { - id: crypto.randomUUID(), - projectId, - name: name.trim(), - sortOrder, - }; - assertProjectBuildingSnapshot(building); - return building; -} - export function createProjectFloorSnapshot( projectId: string, name: string, - sortOrder: number, - buildingId: string | null = null + sortOrder: number ): ProjectFloorSnapshot { const floor: ProjectFloorSnapshot = { id: crypto.randomUUID(), projectId, name: name.trim(), sortOrder, - buildingId: buildingId?.trim() || null, }; assertProjectFloorSnapshot(floor); return floor; @@ -218,46 +120,12 @@ export function createProjectRoomSnapshot( return room; } -export function createProjectBuildingInsertProjectCommand( - building: ProjectBuildingSnapshot -): ProjectBuildingInsertProjectCommand { - assertProjectBuildingSnapshot(building); - return { - schemaVersion: projectLocationStructureCommandSchemaVersion, - type: projectBuildingInsertCommandType, - payload: { building }, - }; -} - -export function createProjectBuildingDeleteProjectCommand( - building: ProjectBuildingSnapshot -): ProjectBuildingDeleteProjectCommand { - assertProjectBuildingSnapshot(building); - return { - schemaVersion: projectLocationStructureCommandSchemaVersion, - type: projectBuildingDeleteCommandType, - payload: { building }, - }; -} - -export function createProjectBuildingUpdateProjectCommand( - expected: ProjectBuildingSnapshot, - target: ProjectBuildingSnapshot -): ProjectBuildingUpdateProjectCommand { - assertMatchingBuildingUpdate(expected, target); - return { - schemaVersion: projectLocationStructureCommandSchemaVersion, - type: projectBuildingUpdateCommandType, - payload: { expected, target }, - }; -} - export function createProjectFloorInsertProjectCommand( floor: ProjectFloorSnapshot ): ProjectFloorInsertProjectCommand { assertProjectFloorSnapshot(floor); return { - schemaVersion: projectFloorCommandSchemaVersion, + schemaVersion: projectLocationStructureCommandSchemaVersion, type: projectFloorInsertCommandType, payload: { floor }, }; @@ -268,7 +136,7 @@ export function createProjectFloorDeleteProjectCommand( ): ProjectFloorDeleteProjectCommand { assertProjectFloorSnapshot(floor); return { - schemaVersion: projectFloorCommandSchemaVersion, + schemaVersion: projectLocationStructureCommandSchemaVersion, type: projectFloorDeleteCommandType, payload: { floor }, }; @@ -302,7 +170,7 @@ export function createProjectFloorUpdateProjectCommand( ): ProjectFloorUpdateProjectCommand { assertMatchingFloorUpdate(expected, target); return { - schemaVersion: projectFloorCommandSchemaVersion, + schemaVersion: projectLocationStructureCommandSchemaVersion, type: projectFloorUpdateCommandType, payload: { expected, target }, }; @@ -320,43 +188,9 @@ export function createProjectRoomUpdateProjectCommand( }; } -export function assertProjectBuildingInsertProjectCommand( - command: SerializedProjectCommand -): asserts command is ProjectBuildingInsertProjectCommand { - assertProjectBuildingStructureProjectCommand( - command, - projectBuildingInsertCommandType - ); -} - -export function assertProjectBuildingDeleteProjectCommand( - command: SerializedProjectCommand -): asserts command is ProjectBuildingDeleteProjectCommand { - assertProjectBuildingStructureProjectCommand( - command, - projectBuildingDeleteCommandType - ); -} - -export function assertProjectBuildingUpdateProjectCommand( - command: SerializedProjectCommand -): asserts command is ProjectBuildingUpdateProjectCommand { - if ( - command.schemaVersion !== projectLocationStructureCommandSchemaVersion || - command.type !== projectBuildingUpdateCommandType || - !isPlainObject(command.payload) || - Object.keys(command.payload).length !== 2 - ) { - throw new Error("Unsupported project-building update command."); - } - assertMatchingBuildingUpdate(command.payload.expected, command.payload.target); -} - export function assertProjectFloorInsertProjectCommand( command: SerializedProjectCommand -): asserts command is - | ProjectFloorInsertProjectCommand - | LegacyProjectFloorInsertProjectCommand { +): asserts command is ProjectFloorInsertProjectCommand { assertProjectFloorStructureProjectCommand( command, projectFloorInsertCommandType @@ -365,9 +199,7 @@ export function assertProjectFloorInsertProjectCommand( export function assertProjectFloorDeleteProjectCommand( command: SerializedProjectCommand -): asserts command is - | ProjectFloorDeleteProjectCommand - | LegacyProjectFloorDeleteProjectCommand { +): asserts command is ProjectFloorDeleteProjectCommand { assertProjectFloorStructureProjectCommand( command, projectFloorDeleteCommandType @@ -394,26 +226,15 @@ export function assertProjectRoomDeleteProjectCommand( export function assertProjectFloorUpdateProjectCommand( command: SerializedProjectCommand -): asserts command is - | ProjectFloorUpdateProjectCommand - | LegacyProjectFloorUpdateProjectCommand { +): asserts command is ProjectFloorUpdateProjectCommand { if ( + command.schemaVersion !== projectLocationStructureCommandSchemaVersion || command.type !== projectFloorUpdateCommandType || !isPlainObject(command.payload) || Object.keys(command.payload).length !== 2 ) { throw new Error("Unsupported project-floor update command."); } - if (command.schemaVersion === legacyProjectFloorCommandSchemaVersion) { - assertMatchingFloorUpdate( - upgradeLegacyFloorSnapshot(command.payload.expected), - upgradeLegacyFloorSnapshot(command.payload.target) - ); - return; - } - if (command.schemaVersion !== projectFloorCommandSchemaVersion) { - throw new Error("Unsupported project-floor update command."); - } assertMatchingFloorUpdate(command.payload.expected, command.payload.target); } @@ -431,68 +252,6 @@ export function assertProjectRoomUpdateProjectCommand( assertMatchingRoomUpdate(command.payload.expected, command.payload.target); } -/** - * Upgrades a (validated) floor command of any supported version to the - * current version. Version 1 commands predate buildings; their floors had no - * building, so `buildingId` becomes null. - */ -export function upgradeProjectFloorCommand( - command: - | ProjectFloorInsertProjectCommand - | LegacyProjectFloorInsertProjectCommand -): ProjectFloorInsertProjectCommand; -export function upgradeProjectFloorCommand( - command: - | ProjectFloorDeleteProjectCommand - | LegacyProjectFloorDeleteProjectCommand -): ProjectFloorDeleteProjectCommand; -export function upgradeProjectFloorCommand( - command: - | ProjectFloorUpdateProjectCommand - | LegacyProjectFloorUpdateProjectCommand -): ProjectFloorUpdateProjectCommand; -export function upgradeProjectFloorCommand( - command: - | ProjectFloorInsertProjectCommand - | ProjectFloorDeleteProjectCommand - | ProjectFloorUpdateProjectCommand - | LegacyProjectFloorInsertProjectCommand - | LegacyProjectFloorDeleteProjectCommand - | LegacyProjectFloorUpdateProjectCommand -) { - if (command.schemaVersion === projectFloorCommandSchemaVersion) { - return command; - } - switch (command.type) { - case projectFloorInsertCommandType: - return createProjectFloorInsertProjectCommand( - upgradeLegacyFloorSnapshot(command.payload.floor) - ); - case projectFloorDeleteCommandType: - return createProjectFloorDeleteProjectCommand( - upgradeLegacyFloorSnapshot(command.payload.floor) - ); - case projectFloorUpdateCommandType: - return createProjectFloorUpdateProjectCommand( - upgradeLegacyFloorSnapshot(command.payload.expected), - upgradeLegacyFloorSnapshot(command.payload.target) - ); - } -} - -function assertMatchingBuildingUpdate(expected: unknown, target: unknown) { - assertProjectBuildingSnapshot(expected); - assertProjectBuildingSnapshot(target); - if ( - expected.id !== target.id || - expected.projectId !== target.projectId || - expected.sortOrder !== target.sortOrder || - expected.name === target.name - ) { - throw new Error("Project-building update must change only the name."); - } -} - function assertMatchingFloorUpdate(expected: unknown, target: unknown) { assertProjectFloorSnapshot(expected); assertProjectFloorSnapshot(target); @@ -500,11 +259,9 @@ function assertMatchingFloorUpdate(expected: unknown, target: unknown) { expected.id !== target.id || expected.projectId !== target.projectId || expected.sortOrder !== target.sortOrder || - (expected.name === target.name && expected.buildingId === target.buildingId) + expected.name === target.name ) { - throw new Error( - "Project-floor update must change only the name or the building." - ); + throw new Error("Project-floor update must change only the name."); } } @@ -520,49 +277,22 @@ function assertMatchingRoomUpdate(expected: unknown, target: unknown) { } } -export function assertProjectBuildingSnapshot( - building: unknown -): asserts building is ProjectBuildingSnapshot { - if (!isPlainObject(building) || Object.keys(building).length !== 4) { - throw new Error("Project-building snapshot is invalid."); - } - assertNormalizedNonEmptyString(building.id, "building.id"); - assertNormalizedNonEmptyString(building.projectId, "building.projectId"); - assertNormalizedNonEmptyString(building.name, "building.name"); - assertSortOrder(building.sortOrder, "building.sortOrder"); -} - export function assertProjectFloorSnapshot( floor: unknown ): asserts floor is ProjectFloorSnapshot { - if (!isPlainObject(floor) || Object.keys(floor).length !== 5) { - throw new Error("Project-floor snapshot is invalid."); - } - assertLegacyFloorFields(floor); - if (floor.buildingId !== null) { - assertNormalizedNonEmptyString(floor.buildingId, "floor.buildingId"); - } -} - -function assertLegacyProjectFloorSnapshot( - floor: unknown -): asserts floor is LegacyProjectFloorSnapshot { if (!isPlainObject(floor) || Object.keys(floor).length !== 4) { throw new Error("Project-floor snapshot is invalid."); } - assertLegacyFloorFields(floor); -} - -function assertLegacyFloorFields(floor: Record) { assertNormalizedNonEmptyString(floor.id, "floor.id"); assertNormalizedNonEmptyString(floor.projectId, "floor.projectId"); assertNormalizedNonEmptyString(floor.name, "floor.name"); - assertSortOrder(floor.sortOrder, "floor.sortOrder"); -} - -function upgradeLegacyFloorSnapshot(floor: unknown): ProjectFloorSnapshot { - assertLegacyProjectFloorSnapshot(floor); - return { ...floor, buildingId: null }; + if ( + typeof floor.sortOrder !== "number" || + !Number.isSafeInteger(floor.sortOrder) || + floor.sortOrder < 0 + ) { + throw new Error("floor.sortOrder must be a non-negative integer."); + } } export function assertProjectRoomSnapshot( @@ -580,23 +310,6 @@ export function assertProjectRoomSnapshot( assertNormalizedNonEmptyString(room.roomName, "room.roomName"); } -function assertProjectBuildingStructureProjectCommand( - command: SerializedProjectCommand, - expectedType: - | typeof projectBuildingInsertCommandType - | typeof projectBuildingDeleteCommandType -) { - if ( - command.schemaVersion !== projectLocationStructureCommandSchemaVersion || - command.type !== expectedType || - !isPlainObject(command.payload) || - Object.keys(command.payload).length !== 1 - ) { - throw new Error("Unsupported project-building structure command."); - } - assertProjectBuildingSnapshot(command.payload.building); -} - function assertProjectFloorStructureProjectCommand( command: SerializedProjectCommand, expectedType: @@ -604,19 +317,13 @@ function assertProjectFloorStructureProjectCommand( | typeof projectFloorDeleteCommandType ) { if ( + command.schemaVersion !== projectLocationStructureCommandSchemaVersion || command.type !== expectedType || !isPlainObject(command.payload) || Object.keys(command.payload).length !== 1 ) { throw new Error("Unsupported project-floor structure command."); } - if (command.schemaVersion === legacyProjectFloorCommandSchemaVersion) { - assertLegacyProjectFloorSnapshot(command.payload.floor); - return; - } - if (command.schemaVersion !== projectFloorCommandSchemaVersion) { - throw new Error("Unsupported project-floor structure command."); - } assertProjectFloorSnapshot(command.payload.floor); } @@ -637,16 +344,6 @@ function assertProjectRoomStructureProjectCommand( assertProjectRoomSnapshot(command.payload.room); } -function assertSortOrder(value: unknown, field: string) { - if ( - typeof value !== "number" || - !Number.isSafeInteger(value) || - value < 0 - ) { - throw new Error(`${field} must be a non-negative integer.`); - } -} - function assertNormalizedNonEmptyString( value: unknown, field: string diff --git a/src/domain/models/project-state-snapshot.model.ts b/src/domain/models/project-state-snapshot.model.ts index 07db42d..5564e1f 100644 --- a/src/domain/models/project-state-snapshot.model.ts +++ b/src/domain/models/project-state-snapshot.model.ts @@ -35,8 +35,7 @@ import { type ExternalModelStateSnapshot, } from "../../external-model/domain/external-model-contracts.js"; -export const projectStateSnapshotSchemaVersion = 7 as const; -const coverProjectStateSnapshotSchemaVersion = 6 as const; +export const projectStateSnapshotSchemaVersion = 6 as const; const boardInfoProjectStateSnapshotSchemaVersion = 5 as const; const previousProjectStateSnapshotSchemaVersion = 4 as const; const externalModelProjectStateSnapshotSchemaVersion = 3 as const; @@ -238,7 +237,7 @@ const projectDeviceSchema = z }) .strict(); -const buildingSchema = z +const floorSchema = z .object({ id: idSchema, projectId: idSchema, @@ -247,19 +246,6 @@ const buildingSchema = z }) .strict(); -const legacyFloorSchema = z - .object({ - id: idSchema, - projectId: idSchema, - name: z.string().trim().min(1), - sortOrder: finiteNumberSchema, - }) - .strict(); - -const floorSchema = legacyFloorSchema - .extend({ buildingId: idSchema.nullable() }) - .strict(); - const roomSchema = z .object({ id: idSchema, @@ -504,24 +490,11 @@ export function parseExternalModelStateSnapshot( return externalModelStateSchema.parse(value); } -// Versions 1-6 have floors without a building and no building list. -const previousProjectStateSnapshotContents = { +const projectStateSnapshotContents = { circuitLists: z.array(circuitListSchema), circuitSections: z.array(circuitSectionSchema), circuits: z.array(circuitSchema), projectDevices: z.array(projectDeviceSchema), - floors: z.array(legacyFloorSchema), - rooms: z.array(roomSchema), - distributionBoardComponents: z.array(distributionBoardComponentSchema), - circuitProtectionDevices: z.array(circuitProtectionDeviceSchema), - distributionBoardComponentProtectionDevices: z.array( - componentProtectionDeviceSchema - ), -}; - -const projectStateSnapshotContents = { - ...previousProjectStateSnapshotContents, - buildings: z.array(buildingSchema), floors: z.array(floorSchema), rooms: z.array(roomSchema), distributionBoardComponents: z.array(distributionBoardComponentSchema), @@ -542,17 +515,6 @@ export const projectStateSnapshotSchema = z }) .strict(); -const coverProjectStateSnapshotSchema = z - .object({ - schemaVersion: z.literal(coverProjectStateSnapshotSchemaVersion), - project: projectSchema, - distributionBoards: z.array(distributionBoardSchema), - externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(), - externalModel: externalModelStateSchema, - ...previousProjectStateSnapshotContents, - }) - .strict(); - const boardInfoProjectStateSnapshotSchema = z .object({ schemaVersion: z.literal(boardInfoProjectStateSnapshotSchemaVersion), @@ -560,7 +522,7 @@ const boardInfoProjectStateSnapshotSchema = z distributionBoards: z.array(legacyDistributionBoardSchema), externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(), externalModel: externalModelStateSchema, - ...previousProjectStateSnapshotContents, + ...projectStateSnapshotContents, }) .strict(); @@ -571,7 +533,7 @@ const previousProjectStateSnapshotSchema = z distributionBoards: z.array(legacyDistributionBoardSchema), externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(), externalModel: externalModelStateSchema, - ...previousProjectStateSnapshotContents, + ...projectStateSnapshotContents, }) .strict(); @@ -581,7 +543,7 @@ const externalModelProjectStateSnapshotSchema = z project: publicBuildingProjectSchema, distributionBoards: z.array(legacyDistributionBoardSchema), externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(), - ...previousProjectStateSnapshotContents, + ...projectStateSnapshotContents, }) .strict(); @@ -590,7 +552,7 @@ const legacyProjectStateSnapshotSchema = z schemaVersion: z.literal(legacyProjectStateSnapshotSchemaVersion), project: publicBuildingProjectSchema, distributionBoards: z.array(legacyDistributionBoardSchema), - ...previousProjectStateSnapshotContents, + ...projectStateSnapshotContents, }) .strict(); @@ -599,7 +561,7 @@ const baselineProjectStateSnapshotSchema = z schemaVersion: z.literal(baselineProjectStateSnapshotSchemaVersion), project: previousProjectSchema, distributionBoards: z.array(legacyDistributionBoardSchema), - ...previousProjectStateSnapshotContents, + ...projectStateSnapshotContents, }) .strict(); @@ -633,28 +595,6 @@ export function parseProjectStateSnapshot( } function upgradePreviousProjectStateSnapshot(value: unknown): unknown { - const cover = upgradeToCoverProjectStateSnapshot(value); - if ( - cover === null || - typeof cover !== "object" || - Array.isArray(cover) || - (cover as { schemaVersion?: unknown }).schemaVersion !== - coverProjectStateSnapshotSchemaVersion - ) { - return cover; - } - // Every supported version before 7 lacks buildings; its floors had no - // building. - const previous = coverProjectStateSnapshotSchema.parse(cover); - return { - ...previous, - schemaVersion: projectStateSnapshotSchemaVersion, - buildings: [], - floors: previous.floors.map((floor) => ({ ...floor, buildingId: null })), - }; -} - -function upgradeToCoverProjectStateSnapshot(value: unknown): unknown { if ( value === null || typeof value !== "object" || @@ -671,7 +611,7 @@ function upgradeToCoverProjectStateSnapshot(value: unknown): unknown { const previous = boardInfoProjectStateSnapshotSchema.parse(value); return { ...previous, - schemaVersion: coverProjectStateSnapshotSchemaVersion, + schemaVersion: projectStateSnapshotSchemaVersion, project: { ...previous.project, ...emptyProjectCoverValues }, distributionBoards: withEmptyDistributionBoardInfo( previous.distributionBoards @@ -682,7 +622,7 @@ function upgradeToCoverProjectStateSnapshot(value: unknown): unknown { const previous = previousProjectStateSnapshotSchema.parse(value); return { ...previous, - schemaVersion: coverProjectStateSnapshotSchemaVersion, + schemaVersion: projectStateSnapshotSchemaVersion, project: { ...previous.project, ...emptyProjectCoverValues }, distributionBoards: withEmptyDistributionBoardInfo( previous.distributionBoards @@ -693,7 +633,7 @@ function upgradeToCoverProjectStateSnapshot(value: unknown): unknown { const previous = externalModelProjectStateSnapshotSchema.parse(value); return { ...previous, - schemaVersion: coverProjectStateSnapshotSchemaVersion, + schemaVersion: projectStateSnapshotSchemaVersion, project: { ...previous.project, ...emptyProjectCoverValues }, distributionBoards: withEmptyDistributionBoardInfo( previous.distributionBoards @@ -705,7 +645,7 @@ function upgradeToCoverProjectStateSnapshot(value: unknown): unknown { const legacy = legacyProjectStateSnapshotSchema.parse(value); return { ...legacy, - schemaVersion: coverProjectStateSnapshotSchemaVersion, + schemaVersion: projectStateSnapshotSchemaVersion, project: { ...legacy.project, ...emptyProjectCoverValues }, distributionBoards: withEmptyDistributionBoardInfo( legacy.distributionBoards @@ -718,7 +658,7 @@ function upgradeToCoverProjectStateSnapshot(value: unknown): unknown { const baseline = baselineProjectStateSnapshotSchema.parse(value); return { ...baseline, - schemaVersion: coverProjectStateSnapshotSchemaVersion, + schemaVersion: projectStateSnapshotSchemaVersion, project: { ...baseline.project, isPublicBuilding: false, @@ -825,7 +765,6 @@ function assertProjectStateSnapshotRelations( snapshot.projectDevices, "project device" ); - const buildingIds = uniqueIds(snapshot.buildings, "building"); const floorIds = uniqueIds(snapshot.floors, "floor"); const roomIds = uniqueIds(snapshot.rooms, "room"); const circuitIds = uniqueIds(snapshot.circuits, "circuit"); @@ -904,14 +843,8 @@ function assertProjectStateSnapshotRelations( ); } } - for (const building of snapshot.buildings) { - assertProjectOwnership(building.projectId, projectId, "building"); - } for (const floor of snapshot.floors) { assertProjectOwnership(floor.projectId, projectId, "floor"); - if (floor.buildingId !== null) { - assertReference(buildingIds, floor.buildingId, "floor building"); - } } for (const room of snapshot.rooms) { assertProjectOwnership(room.projectId, projectId, "room"); diff --git a/src/domain/models/project-transfer.model.ts b/src/domain/models/project-transfer.model.ts index 24c0a9c..3f28f51 100644 --- a/src/domain/models/project-transfer.model.ts +++ b/src/domain/models/project-transfer.model.ts @@ -84,7 +84,6 @@ export function remapProjectState( createId ); const deviceIds = idMap(source.projectDevices, createId); - const buildingIds = idMap(source.buildings, createId); const floorIds = idMap(source.floors, createId); const roomIds = idMap(source.rooms, createId); const rowIds = new Map( @@ -243,19 +242,10 @@ export function remapProjectState( id: requiredId(deviceIds, device.id), projectId: targetProjectId, })), - buildings: source.buildings.map((building) => ({ - ...building, - id: requiredId(buildingIds, building.id), - projectId: targetProjectId, - })), floors: source.floors.map((floor) => ({ ...floor, id: requiredId(floorIds, floor.id), projectId: targetProjectId, - buildingId: - floor.buildingId === null - ? null - : requiredId(buildingIds, floor.buildingId), })), rooms: source.rooms.map((room) => ({ ...room, diff --git a/src/domain/services/project-command.service.ts b/src/domain/services/project-command.service.ts index 91ee754..bee31a3 100644 --- a/src/domain/services/project-command.service.ts +++ b/src/domain/services/project-command.service.ts @@ -70,18 +70,12 @@ import { type SerializedProjectCommand, } from "../models/project-command.model.js"; import { - assertProjectBuildingDeleteProjectCommand, - assertProjectBuildingInsertProjectCommand, - assertProjectBuildingUpdateProjectCommand, assertProjectFloorDeleteProjectCommand, assertProjectFloorInsertProjectCommand, assertProjectFloorUpdateProjectCommand, assertProjectRoomDeleteProjectCommand, assertProjectRoomInsertProjectCommand, assertProjectRoomUpdateProjectCommand, - projectBuildingDeleteCommandType, - projectBuildingInsertCommandType, - projectBuildingUpdateCommandType, projectFloorDeleteCommandType, projectFloorInsertCommandType, projectFloorUpdateCommandType, @@ -498,18 +492,6 @@ export class ProjectCommandService implements ProjectCommandExecutor { command: input.command, }).revision; } - case projectBuildingInsertCommandType: { - assertProjectBuildingInsertProjectCommand(input.command); - return this.projectLocationStructureStore.execute({ ...input, command: input.command }).revision; - } - case projectBuildingDeleteCommandType: { - assertProjectBuildingDeleteProjectCommand(input.command); - return this.projectLocationStructureStore.execute({ ...input, command: input.command }).revision; - } - case projectBuildingUpdateCommandType: { - assertProjectBuildingUpdateProjectCommand(input.command); - return this.projectLocationStructureStore.execute({ ...input, command: input.command }).revision; - } case projectFloorInsertCommandType: { assertProjectFloorInsertProjectCommand(input.command); return this.projectLocationStructureStore.execute({ diff --git a/src/frontend/components/AppShell.tsx b/src/frontend/components/AppShell.tsx index 77b7c78..bbfa0ae 100644 --- a/src/frontend/components/AppShell.tsx +++ b/src/frontend/components/AppShell.tsx @@ -4,14 +4,12 @@ import { useEffect, useState } from "react"; import { usePathname } from "next/navigation"; import { Sidebar } from "./Sidebar"; -// Every page shows the (collapsible) sidebar except the print view, which is -// a stand-alone document opened in its own tab and printed as PDF. -const PRINT_VIEW_PATTERN = /^\/projects\/[^/]+\/print(\/|$)/; +const PROJECT_AREA_PATTERN = /^\/projects(\/[^/]+)?$/; const SIDEBAR_COLLAPSED_STORAGE_KEY = "leistungsbilanz:sidebar-collapsed"; export function AppShell({ children }: { children: React.ReactNode }) { const pathname = usePathname(); - const showSidebar = !PRINT_VIEW_PATTERN.test(pathname ?? ""); + const showSidebar = PROJECT_AREA_PATTERN.test(pathname ?? ""); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); useEffect(() => { diff --git a/src/frontend/components/Sidebar.tsx b/src/frontend/components/Sidebar.tsx index a94378e..b3cbb4f 100644 --- a/src/frontend/components/Sidebar.tsx +++ b/src/frontend/components/Sidebar.tsx @@ -7,21 +7,14 @@ import { listProjects } from "../utils/api"; import type { ProjectDto } from "../types"; import { ThemeToggle } from "./ThemeToggle"; -// Anchors on the project overview page (/projects/[id]). const PROJECT_SECTIONS = [ { id: "verlauf", label: "Verlauf", icon: "↺" }, { id: "verteilungen", label: "Verteilungen", icon: "⌁" }, + { id: "etagen", label: "Etagen", icon: "▥" }, + { id: "raeume", label: "Räume", icon: "⌂" }, { id: "projektgeraete", label: "Projektgeräte", icon: "⚙" }, ] as const; -// Separate project sub-routes (/projects/[id]/). -const PROJECT_ROUTES = [ - { segment: "struktur", label: "Gebäudestruktur", icon: "▥" }, -] as const; - -// Matches the project overview and all of its sub-routes (group 2 = rest). -const PROJECT_PATH_PATTERN = /^\/projects\/([^/]+)(\/.*)?$/; - interface SidebarProps { isCollapsed: boolean; onToggleCollapsed: () => void; @@ -29,14 +22,8 @@ interface SidebarProps { export function Sidebar({ isCollapsed, onToggleCollapsed }: SidebarProps) { const pathname = usePathname(); - const projectMatch = pathname?.match(PROJECT_PATH_PATTERN); + const projectMatch = pathname?.match(/^\/projects\/([^/]+)$/); const activeProjectId = projectMatch ? projectMatch[1] : null; - const projectSubPath = projectMatch?.[2] ?? ""; - // Section anchors only exist (and are observed) on the overview itself. - const overviewProjectId = - activeProjectId && (projectSubPath === "" || projectSubPath === "/") - ? activeProjectId - : null; const [projects, setProjects] = useState([]); const [activeSection, setActiveSection] = useState(null); @@ -55,7 +42,7 @@ export function Sidebar({ isCollapsed, onToggleCollapsed }: SidebarProps) { }, []); useEffect(() => { - if (!overviewProjectId) { + if (!activeProjectId) { setActiveSection(null); return; } @@ -82,7 +69,7 @@ export function Sidebar({ isCollapsed, onToggleCollapsed }: SidebarProps) { elements.forEach((element) => observer.observe(element)); return () => observer.disconnect(); - }, [overviewProjectId]); + }, [activeProjectId]); return (
    diff --git a/src/frontend/types.ts b/src/frontend/types.ts index 5c14f5e..49bb77f 100644 --- a/src/frontend/types.ts +++ b/src/frontend/types.ts @@ -226,24 +226,11 @@ export interface CircuitListDto { name: string; } -export interface BuildingDto { - id: string; - projectId: string; - name: string; - sortOrder: number; -} - -export interface ProjectBuildingCommandResultDto - extends ProjectCommandResultDto { - building: BuildingDto; -} - export interface FloorDto { id: string; projectId: string; name: string; sortOrder: number; - buildingId: string | null; } export interface ProjectFloorCommandResultDto @@ -301,14 +288,8 @@ export interface ProjectDeviceCommandResultDto projectDevice: ProjectDeviceDto; } -export interface CreateBuildingInput { - name: string; -} - export interface CreateFloorInput { name: string; - /** Omitted = keep the current assignment on update; null = no building. */ - buildingId?: string | null; } export interface CreateRoomInput { diff --git a/src/frontend/utils/api.ts b/src/frontend/utils/api.ts index e1f4608..d55a915 100644 --- a/src/frontend/utils/api.ts +++ b/src/frontend/utils/api.ts @@ -1,7 +1,5 @@ import type { - BuildingDto, CircuitListDto, - CreateBuildingInput, CreateFloorInput, CreateProjectDeviceInput, CreateRoomInput, @@ -19,7 +17,6 @@ import type { ProjectCommandResultDto, ProjectDeviceCommandResultDto, ProjectDeviceDto, - ProjectBuildingCommandResultDto, ProjectFloorCommandResultDto, ProjectHistoryStateDto, ProjectRevisionPageDto, @@ -884,61 +881,6 @@ export function renumberCircuitSectionCommand( ); } -export function listBuildings(projectId: string) { - return request(`/api/projects/${projectId}/buildings`); -} - -export function createBuilding( - projectId: string, - input: CreateBuildingInput, - expectedRevision: number -) { - return request( - `/api/projects/${projectId}/buildings`, - { - method: "POST", - body: JSON.stringify({ name: input.name, expectedRevision }), - } - ); -} - -export function updateBuilding( - projectId: string, - buildingId: string, - input: CreateBuildingInput, - expectedRevision: number -) { - return request( - `/api/projects/${projectId}/buildings/${buildingId}`, - { - method: "PUT", - body: JSON.stringify({ name: input.name, expectedRevision }), - } - ); -} - -export function deleteBuilding( - projectId: string, - buildingId: string, - expectedRevision: number -) { - return request( - `/api/projects/${projectId}/buildings/${buildingId}`, - { - method: "DELETE", - body: JSON.stringify({ expectedRevision }), - } - ); -} - -// buildingId is only sent when the caller passes it (string or null); -// an omitted buildingId keeps the current assignment on update. -function toFloorRequestBody(input: CreateFloorInput, expectedRevision: number) { - return input.buildingId === undefined - ? { name: input.name, expectedRevision } - : { name: input.name, buildingId: input.buildingId, expectedRevision }; -} - export function listFloors(projectId: string) { return request(`/api/projects/${projectId}/floors`); } @@ -952,7 +894,7 @@ export function createFloor( `/api/projects/${projectId}/floors`, { method: "POST", - body: JSON.stringify(toFloorRequestBody(input, expectedRevision)), + body: JSON.stringify({ ...input, expectedRevision }), } ); } @@ -965,7 +907,7 @@ export function updateFloor( ) { return request(`/api/projects/${projectId}/floors/${floorId}`, { method: "PUT", - body: JSON.stringify(toFloorRequestBody(input, expectedRevision)), + body: JSON.stringify({ ...input, expectedRevision }), }); } diff --git a/src/frontend/utils/building-structure.ts b/src/frontend/utils/building-structure.ts deleted file mode 100644 index 0c9d6ab..0000000 --- a/src/frontend/utils/building-structure.ts +++ /dev/null @@ -1,284 +0,0 @@ -// Pure selection, filtering and grouping rules for the building structure -// view (Gebäude → Etagen → Räume) and grouped floor selects. -import type { BuildingDto, FloorDto, RoomDto } from "../types"; - -/** Selection key for "Alle" in the building and floor columns. */ -export const ALL_SELECTION = "__all__"; -/** Selection key for "Ohne Gebäude" / "Ohne Etage". */ -export const UNASSIGNED_SELECTION = "__unassigned__"; - -/** ALL_SELECTION, UNASSIGNED_SELECTION or a building/floor id. */ -export type StructureSelection = string; - -export const UNASSIGNED_BUILDING_LABEL = "Ohne Gebäude"; -export const UNASSIGNED_FLOOR_LABEL = "Ohne Etage"; - -type BuildingLike = Pick; -type FloorLike = Pick & { - buildingId?: string | null; -}; -type RoomLike = Pick; - -export interface FloorGroup { - /** null for floors without (known) building. */ - buildingId: string | null; - label: string; - floors: TFloor[]; -} - -/** - * The building a floor belongs to, or null when it has none or references a - * building that is not (or no longer) loaded. - */ -export function resolveFloorBuildingId( - floor: FloorLike, - buildingIds: ReadonlySet -): string | null { - const buildingId = floor.buildingId ?? null; - return buildingId !== null && buildingIds.has(buildingId) - ? buildingId - : null; -} - -function toIdSet(entries: ReadonlyArray<{ id: string }>) { - return new Set(entries.map((entry) => entry.id)); -} - -export function hasFloorsWithoutBuilding( - floors: ReadonlyArray, - buildings: ReadonlyArray -): boolean { - const buildingIds = toIdSet(buildings); - return floors.some( - (floor) => resolveFloorBuildingId(floor, buildingIds) === null - ); -} - -export function hasRoomsWithoutFloor( - rooms: ReadonlyArray, - floors: ReadonlyArray -): boolean { - const floorIds = toIdSet(floors); - return rooms.some( - (room) => room.floorId === null || !floorIds.has(room.floorId) - ); -} - -/** - * Groups floors by building in building order; floors without a known - * building come last under "Ohne Gebäude". Empty groups are omitted and the - * floor order inside a group is preserved. - */ -export function groupFloorsByBuilding( - floors: ReadonlyArray, - buildings: ReadonlyArray -): FloorGroup[] { - const buildingIds = toIdSet(buildings); - const floorsByBuilding = new Map(); - for (const floor of floors) { - const buildingId = resolveFloorBuildingId(floor, buildingIds); - const group = floorsByBuilding.get(buildingId) ?? []; - group.push(floor); - floorsByBuilding.set(buildingId, group); - } - const groups: FloorGroup[] = []; - for (const building of buildings) { - const groupFloors = floorsByBuilding.get(building.id); - if (groupFloors?.length) { - groups.push({ - buildingId: building.id, - label: building.name, - floors: groupFloors, - }); - } - } - const unassigned = floorsByBuilding.get(null); - if (unassigned?.length) { - groups.push({ - buildingId: null, - label: UNASSIGNED_BUILDING_LABEL, - floors: unassigned, - }); - } - return groups; -} - -/** Floors shown in the floor column for a building selection. */ -export function filterFloorsByBuilding( - floors: ReadonlyArray, - buildings: ReadonlyArray, - buildingSelection: StructureSelection -): TFloor[] { - if (buildingSelection === ALL_SELECTION) { - return [...floors]; - } - const buildingIds = toIdSet(buildings); - const targetBuildingId = - buildingSelection === UNASSIGNED_SELECTION ? null : buildingSelection; - return floors.filter( - (floor) => resolveFloorBuildingId(floor, buildingIds) === targetBuildingId - ); -} - -/** - * "Ohne Etage" is offered only when rooms without floor exist and the - * building selection can contain them ("Alle" or "Ohne Gebäude"), because a - * room without floor has no building either. - */ -export function showsRoomsWithoutFloorEntry( - rooms: ReadonlyArray, - floors: ReadonlyArray, - buildingSelection: StructureSelection -): boolean { - return ( - (buildingSelection === ALL_SELECTION || - buildingSelection === UNASSIGNED_SELECTION) && - hasRoomsWithoutFloor(rooms, floors) - ); -} - -/** Rooms shown in the room column for the current selections. */ -export function filterRooms( - rooms: ReadonlyArray, - floors: ReadonlyArray, - buildings: ReadonlyArray, - buildingSelection: StructureSelection, - floorSelection: StructureSelection -): TRoom[] { - const floorIds = toIdSet(floors); - const isWithoutFloor = (room: RoomLike) => - room.floorId === null || !floorIds.has(room.floorId); - - if (floorSelection === UNASSIGNED_SELECTION) { - return showsRoomsWithoutFloorEntry(rooms, floors, buildingSelection) - ? rooms.filter(isWithoutFloor) - : []; - } - if (floorSelection !== ALL_SELECTION) { - return rooms.filter((room) => room.floorId === floorSelection); - } - if (buildingSelection === ALL_SELECTION) { - return [...rooms]; - } - const visibleFloorIds = toIdSet( - filterFloorsByBuilding(floors, buildings, buildingSelection) - ); - return rooms.filter( - (room) => - (room.floorId !== null && visibleFloorIds.has(room.floorId)) || - (buildingSelection === UNASSIGNED_SELECTION && isWithoutFloor(room)) - ); -} - -/** Falls back to "Alle" when a selected building vanished. */ -export function normalizeBuildingSelection( - selection: StructureSelection, - floors: ReadonlyArray, - buildings: ReadonlyArray -): StructureSelection { - if (selection === ALL_SELECTION) { - return selection; - } - if (selection === UNASSIGNED_SELECTION) { - return hasFloorsWithoutBuilding(floors, buildings) - ? selection - : ALL_SELECTION; - } - return buildings.some((building) => building.id === selection) - ? selection - : ALL_SELECTION; -} - -/** Falls back to "Alle" when a selected floor is no longer visible. */ -export function normalizeFloorSelection( - selection: StructureSelection, - rooms: ReadonlyArray, - floors: ReadonlyArray, - buildings: ReadonlyArray, - buildingSelection: StructureSelection -): StructureSelection { - if (selection === ALL_SELECTION) { - return selection; - } - if (selection === UNASSIGNED_SELECTION) { - return showsRoomsWithoutFloorEntry(rooms, floors, buildingSelection) - ? selection - : ALL_SELECTION; - } - return filterFloorsByBuilding(floors, buildings, buildingSelection).some( - (floor) => floor.id === selection - ) - ? selection - : ALL_SELECTION; -} - -/** Building preselected in the floor dialog ("" = Ohne Gebäude). */ -export function defaultBuildingIdForNewFloor( - buildingSelection: StructureSelection -): string { - return buildingSelection === ALL_SELECTION || - buildingSelection === UNASSIGNED_SELECTION - ? "" - : buildingSelection; -} - -/** Floor preselected in the room dialog ("" = Ohne Etage). */ -export function defaultFloorIdForNewRoom( - floorSelection: StructureSelection -): string { - return floorSelection === ALL_SELECTION || - floorSelection === UNASSIGNED_SELECTION - ? "" - : floorSelection; -} - -export function countFloorsByBuilding( - floors: ReadonlyArray, - buildings: ReadonlyArray -): Map { - const buildingIds = toIdSet(buildings); - const counts = new Map(); - for (const floor of floors) { - const buildingId = resolveFloorBuildingId(floor, buildingIds); - counts.set(buildingId, (counts.get(buildingId) ?? 0) + 1); - } - return counts; -} - -/** Room counts per floor id; rooms without known floor count under null. */ -export function countRoomsByFloor( - rooms: ReadonlyArray, - floors: ReadonlyArray -): Map { - const floorIds = toIdSet(floors); - const counts = new Map(); - for (const room of rooms) { - const floorId = - room.floorId !== null && floorIds.has(room.floorId) - ? room.floorId - : null; - counts.set(floorId, (counts.get(floorId) ?? 0) + 1); - } - return counts; -} - -/** "EG · Haus A", "EG" (floor without building) or "Ohne Etage". */ -export function describeRoomLocation( - room: RoomLike, - floors: ReadonlyArray, - buildings: ReadonlyArray -): string { - const floor = - room.floorId === null - ? undefined - : floors.find((entry) => entry.id === room.floorId); - if (!floor) { - return UNASSIGNED_FLOOR_LABEL; - } - const buildingId = resolveFloorBuildingId(floor, toIdSet(buildings)); - const building = - buildingId === null - ? undefined - : buildings.find((entry) => entry.id === buildingId); - return building ? `${floor.name} · ${building.name}` : floor.name; -} diff --git a/src/frontend/utils/project-version-history.ts b/src/frontend/utils/project-version-history.ts index a2880ec..5685b29 100644 --- a/src/frontend/utils/project-version-history.ts +++ b/src/frontend/utils/project-version-history.ts @@ -46,9 +46,6 @@ const commandTypeLabels: Record = { "circuit.move-group": "Stromkreis in andere Gruppe verschoben", "circuit-group.delete-subtree": "Stromkreisgruppe vollständig entfernt", "circuit-group.restore-subtree": "Stromkreisgruppe vollständig wiederhergestellt", - "project-building.insert": "Gebäude angelegt", - "project-building.update": "Gebäude bearbeitet", - "project-building.delete": "Gebäude entfernt", "project-floor.insert": "Geschoss angelegt", "project-floor.update": "Geschoss bearbeitet", "project-floor.delete": "Geschoss entfernt", diff --git a/src/server/composition/application-repositories.ts b/src/server/composition/application-repositories.ts index 4f9b5ee..944c843 100644 --- a/src/server/composition/application-repositories.ts +++ b/src/server/composition/application-repositories.ts @@ -6,7 +6,6 @@ import { CircuitRepository } from "../../db/repositories/circuit.repository.js"; import { CircuitProtectionDeviceRepository } from "../../db/repositories/circuit-protection-device.repository.js"; import { DistributionBoardComponentRepository } from "../../db/repositories/distribution-board-component.repository.js"; import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js"; -import { BuildingRepository } from "../../db/repositories/building.repository.js"; import { FloorRepository } from "../../db/repositories/floor.repository.js"; import { GlobalDeviceRepository } from "../../db/repositories/global-device.repository.js"; import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js"; @@ -27,7 +26,6 @@ export const distributionBoardComponentRepository = new DistributionBoardComponentRepository(db); export const distributionBoardRepository = new DistributionBoardRepository(db); -export const buildingRepository = new BuildingRepository(db); export const floorRepository = new FloorRepository(db); export const globalDeviceRepository = new GlobalDeviceRepository(db); export const projectDeviceRepository = new ProjectDeviceRepository(db); diff --git a/src/server/controllers/building.controller.ts b/src/server/controllers/building.controller.ts deleted file mode 100644 index 3a49120..0000000 --- a/src/server/controllers/building.controller.ts +++ /dev/null @@ -1,111 +0,0 @@ -import type { Request, Response } from "express"; -import { - createProjectBuildingDeleteProjectCommand, - createProjectBuildingInsertProjectCommand, - createProjectBuildingSnapshot, - createProjectBuildingUpdateProjectCommand, -} from "../../domain/models/project-location-structure-project-command.model.js"; -import { - createBuildingSchema, - deleteProjectLocationSchema, - updateBuildingSchema, -} from "../../shared/validation/project-structure.schemas.js"; -import { projectCommandService } from "../composition/project-command-stores.js"; -import { buildingRepository } from "../composition/application-repositories.js"; -import { respondWithProjectCommandError } from "./project-command.controller.js"; - -export async function listBuildingsByProject(req: Request, res: Response) { - const { projectId } = req.params; - if (typeof projectId !== "string") { - return res.status(400).json({ error: "Invalid projectId" }); - } - - const result = await buildingRepository.listByProject(projectId); - return res.json(result); -} - -export async function createBuilding(req: Request, res: Response) { - const { projectId } = req.params; - if (typeof projectId !== "string") { - return res.status(400).json({ error: "Invalid projectId" }); - } - - const parsed = createBuildingSchema.safeParse(req.body); - if (!parsed.success) { - return res.status(400).json({ error: parsed.error.flatten() }); - } - - const existingBuildings = await buildingRepository.listByProject(projectId); - const nextSortOrder = existingBuildings.length - ? Math.max(...existingBuildings.map((building) => building.sortOrder)) + 1 - : 0; - const building = createProjectBuildingSnapshot( - projectId, - parsed.data.name, - nextSortOrder - ); - try { - const result = projectCommandService.executeUser({ - projectId, - expectedRevision: parsed.data.expectedRevision, - description: "Gebäude anlegen", - command: createProjectBuildingInsertProjectCommand(building), - }); - return res.status(201).json({ ...result, building }); - } catch (error) { - return respondWithProjectCommandError(error, res); - } -} - -export async function updateBuilding(req: Request, res: Response) { - const { projectId, buildingId } = req.params; - if (typeof projectId !== "string" || typeof buildingId !== "string") { - return res.status(400).json({ error: "Invalid parameters" }); - } - const parsed = updateBuildingSchema.safeParse(req.body); - if (!parsed.success) { - return res.status(400).json({ error: parsed.error.flatten() }); - } - const current = await buildingRepository.findById(projectId, buildingId); - if (!current) { - return res.status(404).json({ error: "Building not found" }); - } - const target = { ...current, name: parsed.data.name }; - try { - const result = projectCommandService.executeUser({ - projectId, - expectedRevision: parsed.data.expectedRevision, - description: "Gebäude bearbeiten", - command: createProjectBuildingUpdateProjectCommand(current, target), - }); - return res.json({ ...result, building: target }); - } catch (error) { - return respondWithProjectCommandError(error, res); - } -} - -export async function deleteBuilding(req: Request, res: Response) { - const { projectId, buildingId } = req.params; - if (typeof projectId !== "string" || typeof buildingId !== "string") { - return res.status(400).json({ error: "Invalid parameters" }); - } - const parsed = deleteProjectLocationSchema.safeParse(req.body); - if (!parsed.success) { - return res.status(400).json({ error: parsed.error.flatten() }); - } - const current = await buildingRepository.findById(projectId, buildingId); - if (!current) { - return res.status(404).json({ error: "Building not found" }); - } - try { - const result = projectCommandService.executeUser({ - projectId, - expectedRevision: parsed.data.expectedRevision, - description: "Gebäude löschen", - command: createProjectBuildingDeleteProjectCommand(current), - }); - return res.json(result); - } catch (error) { - return respondWithProjectCommandError(error, res); - } -} diff --git a/src/server/controllers/floor.controller.ts b/src/server/controllers/floor.controller.ts index b000db3..085aa10 100644 --- a/src/server/controllers/floor.controller.ts +++ b/src/server/controllers/floor.controller.ts @@ -42,8 +42,7 @@ export async function createFloor(req: Request, res: Response) { const floor = createProjectFloorSnapshot( projectId, parsed.data.name, - nextSortOrder, - parsed.data.buildingId ?? null + nextSortOrder ); try { const result = projectCommandService.executeUser({ @@ -71,14 +70,7 @@ export async function updateFloor(req: Request, res: Response) { if (!current) { return res.status(404).json({ error: "Floor not found" }); } - const target = { - ...current, - name: parsed.data.name, - buildingId: - parsed.data.buildingId === undefined - ? current.buildingId - : parsed.data.buildingId, - }; + const target = { ...current, name: parsed.data.name }; try { const result = projectCommandService.executeUser({ projectId, diff --git a/src/server/routes/project.routes.ts b/src/server/routes/project.routes.ts index 0b61255..8801784 100644 --- a/src/server/routes/project.routes.ts +++ b/src/server/routes/project.routes.ts @@ -13,12 +13,6 @@ import { updateDistributionBoard, } from "../controllers/distribution-board.controller.js"; import { listCircuitListsByProject } from "../controllers/circuit-list.controller.js"; -import { - createBuilding, - deleteBuilding, - listBuildingsByProject, - updateBuilding, -} from "../controllers/building.controller.js"; import { createFloor, deleteFloor, listFloorsByProject, updateFloor } from "../controllers/floor.controller.js"; import { createRoom, deleteRoom, listRoomsByProject, updateRoom } from "../controllers/room.controller.js"; import { getCircuitTree } from "../controllers/circuit-tree.controller.js"; @@ -106,10 +100,6 @@ projectRouter.get( "/:projectId/circuit-sections/:sectionId/next-identifier", getNextCircuitIdentifier ); -projectRouter.get("/:projectId/buildings", listBuildingsByProject); -projectRouter.post("/:projectId/buildings", createBuilding); -projectRouter.put("/:projectId/buildings/:buildingId", updateBuilding); -projectRouter.delete("/:projectId/buildings/:buildingId", deleteBuilding); projectRouter.get("/:projectId/floors", listFloorsByProject); projectRouter.post("/:projectId/floors", createFloor); projectRouter.put("/:projectId/floors/:floorId", updateFloor); diff --git a/src/shared/validation/project-structure.schemas.ts b/src/shared/validation/project-structure.schemas.ts index 5ab13bf..143be0a 100644 --- a/src/shared/validation/project-structure.schemas.ts +++ b/src/shared/validation/project-structure.schemas.ts @@ -102,22 +102,10 @@ export const deleteDistributionBoardSchema = z }) .strict(); -export const createBuildingSchema = z - .object({ - expectedRevision: expectedProjectRevisionSchema, - name: z.string().trim().min(1).max(200), - }) - .strict(); - -export const updateBuildingSchema = createBuildingSchema; - -// A missing buildingId keeps the current value (older clients send only the -// name); null removes the floor from its building. export const createFloorSchema = z .object({ expectedRevision: expectedProjectRevisionSchema, name: z.string().trim().min(1).max(200), - buildingId: z.string().trim().min(1).nullable().optional(), }) .strict(); diff --git a/tests/building-structure-frontend-api.test.ts b/tests/building-structure-frontend-api.test.ts deleted file mode 100644 index 9e23150..0000000 --- a/tests/building-structure-frontend-api.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; -import { - createBuilding, - createFloor, - deleteBuilding, - listBuildings, - updateBuilding, - updateFloor, -} from "../src/frontend/utils/api.js"; - -describe("building structure frontend API", () => { - it("sends building commands and optional floor building assignments", 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: init?.body === undefined ? undefined : JSON.parse(String(init.body)), - }); - return new Response(JSON.stringify([]), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - }; - try { - await listBuildings("project-1"); - await createBuilding("project-1", { name: "Haus A" }, 3); - await updateBuilding("project-1", "building-1", { name: "Haus B" }, 4); - await deleteBuilding("project-1", "building-1", 5); - await createFloor( - "project-1", - { name: "EG", buildingId: "building-1" }, - 6 - ); - await createFloor("project-1", { name: "Dach", buildingId: null }, 7); - await createFloor("project-1", { name: "UG", buildingId: undefined }, 8); - await updateFloor( - "project-1", - "floor-1", - { name: "1. OG", buildingId: null }, - 9 - ); - await updateFloor("project-1", "floor-1", { name: "2. OG" }, 10); - } finally { - globalThis.fetch = originalFetch; - } - - assert.deepEqual(requests, [ - { - url: "/api/projects/project-1/buildings", - method: "GET", - body: undefined, - }, - { - url: "/api/projects/project-1/buildings", - method: "POST", - body: { name: "Haus A", expectedRevision: 3 }, - }, - { - url: "/api/projects/project-1/buildings/building-1", - method: "PUT", - body: { name: "Haus B", expectedRevision: 4 }, - }, - { - url: "/api/projects/project-1/buildings/building-1", - method: "DELETE", - body: { expectedRevision: 5 }, - }, - { - url: "/api/projects/project-1/floors", - method: "POST", - body: { name: "EG", buildingId: "building-1", expectedRevision: 6 }, - }, - { - url: "/api/projects/project-1/floors", - method: "POST", - body: { name: "Dach", buildingId: null, expectedRevision: 7 }, - }, - { - url: "/api/projects/project-1/floors", - method: "POST", - body: { name: "UG", expectedRevision: 8 }, - }, - { - url: "/api/projects/project-1/floors/floor-1", - method: "PUT", - body: { name: "1. OG", buildingId: null, expectedRevision: 9 }, - }, - { - url: "/api/projects/project-1/floors/floor-1", - method: "PUT", - body: { name: "2. OG", expectedRevision: 10 }, - }, - ]); - const updateWithoutBuilding = requests[8].body as Record; - assert.equal("buildingId" in updateWithoutBuilding, false); - }); -}); diff --git a/tests/building-structure-view.test.ts b/tests/building-structure-view.test.ts deleted file mode 100644 index 59271af..0000000 --- a/tests/building-structure-view.test.ts +++ /dev/null @@ -1,284 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; -import type { - BuildingDto, - FloorDto, - RoomDto, -} from "../src/frontend/types.js"; -import { - ALL_SELECTION, - UNASSIGNED_SELECTION, - countFloorsByBuilding, - countRoomsByFloor, - defaultBuildingIdForNewFloor, - defaultFloorIdForNewRoom, - describeRoomLocation, - filterFloorsByBuilding, - filterRooms, - groupFloorsByBuilding, - hasFloorsWithoutBuilding, - hasRoomsWithoutFloor, - normalizeBuildingSelection, - normalizeFloorSelection, - showsRoomsWithoutFloorEntry, -} from "../src/frontend/utils/building-structure.js"; - -function building(id: string, name: string, sortOrder: number): BuildingDto { - return { id, projectId: "project-1", name, sortOrder }; -} - -function floor( - id: string, - name: string, - buildingId: string | null, - sortOrder = 0 -): FloorDto { - return { id, projectId: "project-1", name, sortOrder, buildingId }; -} - -function room(id: string, floorId: string | null, roomNumber = id): RoomDto { - return { - id, - projectId: "project-1", - floorId, - roomNumber, - roomName: `Raum ${roomNumber}`, - }; -} - -const buildings = [building("b-a", "Haus A", 0), building("b-b", "Haus B", 1)]; -const floors = [ - floor("f-free", "Außenanlage", null, 0), - floor("f-a-eg", "EG", "b-a", 1), - floor("f-b-eg", "EG", "b-b", 2), - floor("f-a-og", "1. OG", "b-a", 3), - floor("f-orphan", "Keller", "b-deleted", 4), -]; -const rooms = [ - room("r-a-eg", "f-a-eg"), - room("r-a-og", "f-a-og"), - room("r-b-eg", "f-b-eg"), - room("r-free", "f-free"), - room("r-orphan-floor", "f-orphan"), - room("r-none", null), - room("r-missing-floor", "f-deleted"), -]; - -const ids = (entries: ReadonlyArray<{ id: string }>) => - entries.map((entry) => entry.id); - -describe("building structure view", () => { - it("groups floors in building order and puts unassigned floors last", () => { - const groups = groupFloorsByBuilding(floors, buildings); - assert.deepEqual( - groups.map((group) => ({ - buildingId: group.buildingId, - label: group.label, - floors: ids(group.floors), - })), - [ - { buildingId: "b-a", label: "Haus A", floors: ["f-a-eg", "f-a-og"] }, - { buildingId: "b-b", label: "Haus B", floors: ["f-b-eg"] }, - { - buildingId: null, - label: "Ohne Gebäude", - floors: ["f-free", "f-orphan"], - }, - ] - ); - }); - - it("omits empty groups", () => { - const groups = groupFloorsByBuilding( - [floor("f-1", "EG", "b-b")], - buildings - ); - assert.deepEqual( - groups.map((group) => group.buildingId), - ["b-b"] - ); - assert.deepEqual(groupFloorsByBuilding([], buildings), []); - }); - - it("filters floors by building selection", () => { - assert.deepEqual( - ids(filterFloorsByBuilding(floors, buildings, ALL_SELECTION)), - ids(floors) - ); - assert.deepEqual( - ids(filterFloorsByBuilding(floors, buildings, "b-a")), - ["f-a-eg", "f-a-og"] - ); - assert.deepEqual( - ids(filterFloorsByBuilding(floors, buildings, UNASSIGNED_SELECTION)), - ["f-free", "f-orphan"] - ); - }); - - it("detects floors without building and rooms without floor", () => { - assert.equal(hasFloorsWithoutBuilding(floors, buildings), true); - assert.equal( - hasFloorsWithoutBuilding([floor("f-1", "EG", "b-a")], buildings), - false - ); - assert.equal(hasRoomsWithoutFloor(rooms, floors), true); - assert.equal(hasRoomsWithoutFloor([room("r-1", "f-a-eg")], floors), false); - }); - - it("filters rooms for building and floor selections", () => { - assert.deepEqual( - ids(filterRooms(rooms, floors, buildings, ALL_SELECTION, ALL_SELECTION)), - ids(rooms) - ); - assert.deepEqual( - ids(filterRooms(rooms, floors, buildings, "b-a", ALL_SELECTION)), - ["r-a-eg", "r-a-og"] - ); - assert.deepEqual( - ids(filterRooms(rooms, floors, buildings, "b-a", "f-a-og")), - ["r-a-og"] - ); - assert.deepEqual( - ids(filterRooms(rooms, floors, buildings, ALL_SELECTION, "f-b-eg")), - ["r-b-eg"] - ); - assert.deepEqual( - ids( - filterRooms( - rooms, - floors, - buildings, - UNASSIGNED_SELECTION, - ALL_SELECTION - ) - ), - ["r-free", "r-orphan-floor", "r-none", "r-missing-floor"] - ); - assert.deepEqual( - ids( - filterRooms( - rooms, - floors, - buildings, - ALL_SELECTION, - UNASSIGNED_SELECTION - ) - ), - ["r-none", "r-missing-floor"] - ); - assert.deepEqual( - ids(filterRooms(rooms, floors, buildings, "b-a", UNASSIGNED_SELECTION)), - [] - ); - }); - - it("offers 'Ohne Etage' only where rooms without floor can belong", () => { - assert.equal(showsRoomsWithoutFloorEntry(rooms, floors, ALL_SELECTION), true); - assert.equal( - showsRoomsWithoutFloorEntry(rooms, floors, UNASSIGNED_SELECTION), - true - ); - assert.equal(showsRoomsWithoutFloorEntry(rooms, floors, "b-a"), false); - assert.equal( - showsRoomsWithoutFloorEntry([room("r-1", "f-a-eg")], floors, ALL_SELECTION), - false - ); - }); - - it("falls back to 'Alle' for vanished or hidden selections", () => { - assert.equal(normalizeBuildingSelection("b-a", floors, buildings), "b-a"); - assert.equal( - normalizeBuildingSelection("b-deleted", floors, buildings), - ALL_SELECTION - ); - assert.equal( - normalizeBuildingSelection(UNASSIGNED_SELECTION, floors, buildings), - UNASSIGNED_SELECTION - ); - assert.equal( - normalizeBuildingSelection( - UNASSIGNED_SELECTION, - [floor("f-1", "EG", "b-a")], - buildings - ), - ALL_SELECTION - ); - - assert.equal( - normalizeFloorSelection("f-a-og", rooms, floors, buildings, "b-a"), - "f-a-og" - ); - assert.equal( - normalizeFloorSelection("f-b-eg", rooms, floors, buildings, "b-a"), - ALL_SELECTION - ); - assert.equal( - normalizeFloorSelection("f-deleted", rooms, floors, buildings, ALL_SELECTION), - ALL_SELECTION - ); - assert.equal( - normalizeFloorSelection( - UNASSIGNED_SELECTION, - rooms, - floors, - buildings, - ALL_SELECTION - ), - UNASSIGNED_SELECTION - ); - assert.equal( - normalizeFloorSelection(UNASSIGNED_SELECTION, rooms, floors, buildings, "b-a"), - ALL_SELECTION - ); - }); - - it("preselects the selected building and floor for new entries", () => { - assert.equal(defaultBuildingIdForNewFloor("b-a"), "b-a"); - assert.equal(defaultBuildingIdForNewFloor(ALL_SELECTION), ""); - assert.equal(defaultBuildingIdForNewFloor(UNASSIGNED_SELECTION), ""); - assert.equal(defaultFloorIdForNewRoom("f-a-eg"), "f-a-eg"); - assert.equal(defaultFloorIdForNewRoom(ALL_SELECTION), ""); - assert.equal(defaultFloorIdForNewRoom(UNASSIGNED_SELECTION), ""); - }); - - it("counts floors per building and rooms per floor", () => { - assert.deepEqual( - [...countFloorsByBuilding(floors, buildings)], - [ - [null, 2], - ["b-a", 2], - ["b-b", 1], - ] - ); - assert.deepEqual( - [...countRoomsByFloor(rooms, floors)], - [ - ["f-a-eg", 1], - ["f-a-og", 1], - ["f-b-eg", 1], - ["f-free", 1], - ["f-orphan", 1], - [null, 2], - ] - ); - }); - - it("describes a room location with floor and building", () => { - assert.equal( - describeRoomLocation(room("r", "f-a-og"), floors, buildings), - "1. OG · Haus A" - ); - assert.equal( - describeRoomLocation(room("r", "f-free"), floors, buildings), - "Außenanlage" - ); - assert.equal( - describeRoomLocation(room("r", "f-orphan"), floors, buildings), - "Keller" - ); - assert.equal( - describeRoomLocation(room("r", null), floors, buildings), - "Ohne Etage" - ); - }); -}); diff --git a/tests/project-building-structure.test.ts b/tests/project-building-structure.test.ts deleted file mode 100644 index 2c9ccdf..0000000 --- a/tests/project-building-structure.test.ts +++ /dev/null @@ -1,305 +0,0 @@ -import path from "node:path"; -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; -import { eq } from "drizzle-orm"; -import { migrate } from "drizzle-orm/better-sqlite3/migrator"; -import { - createDatabaseContext, - type DatabaseContext, -} from "../src/db/database-context.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 { buildings } from "../src/db/schema/buildings.js"; -import { floors } from "../src/db/schema/floors.js"; -import { projects } from "../src/db/schema/projects.js"; -import { - assertProjectFloorInsertProjectCommand, - createProjectBuildingDeleteProjectCommand, - createProjectBuildingInsertProjectCommand, - createProjectBuildingSnapshot, - createProjectBuildingUpdateProjectCommand, - createProjectFloorInsertProjectCommand, - createProjectFloorSnapshot, - createProjectFloorUpdateProjectCommand, - projectFloorCommandSchemaVersion, - type ProjectLocationStructureProjectCommand, -} from "../src/domain/models/project-location-structure-project-command.model.js"; -import { - createBuildingSchema, - createFloorSchema, - updateFloorSchema, -} from "../src/shared/validation/project-structure.schemas.js"; - -function createTestDatabase(): DatabaseContext { - const context = createDatabaseContext(":memory:"); - migrate(context.db, { - migrationsFolder: path.resolve("src", "db", "migrations"), - }); - context.db - .insert(projects) - .values([ - { id: "project-1", name: "Test project" }, - { id: "project-2", name: "Foreign project" }, - ]) - .run(); - return context; -} - -/** Executes a user command and returns a runner for its undo and redo. */ -function createRunner(context: DatabaseContext) { - const repository = new ProjectLocationStructureProjectCommandRepository( - context.db - ); - const history = new ProjectHistoryRepository(context.db); - let revision = 0; - const run = ( - command: ProjectLocationStructureProjectCommand, - source: "user" | "undo" | "redo" = "user" - ) => { - const step = - source === "user" ? null : history.getNextCommand("project-1", source); - if (source !== "user") { - assert.ok(step); - } - const result = repository.execute({ - projectId: "project-1", - expectedRevision: revision, - source, - historyTargetChangeSetId: step?.changeSetId, - command, - }); - revision += 1; - return result; - }; - return { repository, run }; -} - -function getBuilding(context: DatabaseContext, id: string) { - return context.db.select().from(buildings).where(eq(buildings.id, id)).get(); -} - -function getFloor(context: DatabaseContext, id: string) { - return context.db.select().from(floors).where(eq(floors.id, id)).get(); -} - -describe("project building structure", () => { - it("creates, renames and deletes buildings with persisted undo and redo", () => { - const context = createTestDatabase(); - try { - const { run } = createRunner(context); - const building = createProjectBuildingSnapshot("project-1", " Haus A ", 0); - assert.equal(building.name, "Haus A"); - - const insert = createProjectBuildingInsertProjectCommand(building); - const inserted = run(insert); - assert.deepEqual(getBuilding(context, building.id), building); - - const renamed = { ...building, name: "Haus B" }; - const update = createProjectBuildingUpdateProjectCommand(building, renamed); - const updated = run(update); - assert.equal(getBuilding(context, building.id)?.name, "Haus B"); - - run(updated.inverse, "undo"); - assert.deepEqual(getBuilding(context, building.id), building); - run(inserted.inverse, "undo"); - assert.equal(getBuilding(context, building.id), undefined); - run(insert, "redo"); - run(update, "redo"); - assert.deepEqual(getBuilding(context, building.id), renamed); - - const deleted = run(createProjectBuildingDeleteProjectCommand(renamed)); - assert.equal(getBuilding(context, building.id), undefined); - run(deleted.inverse, "undo"); - assert.deepEqual(getBuilding(context, building.id), renamed); - } finally { - context.close(); - } - }); - - it("assigns floors to buildings and blocks deleting a building in use", () => { - const context = createTestDatabase(); - try { - const { run } = createRunner(context); - const houseA = createProjectBuildingSnapshot("project-1", "Haus A", 0); - const houseB = createProjectBuildingSnapshot("project-1", "Haus B", 1); - run(createProjectBuildingInsertProjectCommand(houseA)); - run(createProjectBuildingInsertProjectCommand(houseB)); - - const floor = createProjectFloorSnapshot("project-1", "EG", 0, houseA.id); - const floorInsert = createProjectFloorInsertProjectCommand(floor); - assert.equal(floorInsert.schemaVersion, projectFloorCommandSchemaVersion); - run(floorInsert); - assert.deepEqual(getFloor(context, floor.id), floor); - - const moved = { ...floor, buildingId: houseB.id }; - const moveCommand = createProjectFloorUpdateProjectCommand(floor, moved); - const movedResult = run(moveCommand); - assert.equal(getFloor(context, floor.id)?.buildingId, houseB.id); - - assert.throws( - () => run(createProjectBuildingDeleteProjectCommand(houseB)), - /solange Etagen zugeordnet/ - ); - assert.ok(getBuilding(context, houseB.id)); - - run(movedResult.inverse, "undo"); - assert.equal(getFloor(context, floor.id)?.buildingId, houseA.id); - run(moveCommand, "redo"); - assert.equal(getFloor(context, floor.id)?.buildingId, houseB.id); - - const unassigned = { ...moved, buildingId: null }; - run(createProjectFloorUpdateProjectCommand(moved, unassigned)); - assert.equal(getFloor(context, floor.id)?.buildingId, null); - run(createProjectBuildingDeleteProjectCommand(houseB)); - assert.equal(getBuilding(context, houseB.id), undefined); - } finally { - context.close(); - } - }); - - it("rejects floors that reference a missing or foreign building", () => { - const context = createTestDatabase(); - try { - const { run } = createRunner(context); - context.db - .insert(buildings) - .values({ id: "foreign", projectId: "project-2", name: "Fremd", sortOrder: 0 }) - .run(); - assert.throws( - () => - run( - createProjectFloorInsertProjectCommand( - createProjectFloorSnapshot("project-1", "EG", 0, "foreign") - ) - ), - /foreign building/ - ); - assert.throws( - () => - run( - createProjectFloorInsertProjectCommand( - createProjectFloorSnapshot("project-1", "EG", 0, "missing") - ) - ), - /foreign building/ - ); - assert.equal(context.db.select().from(floors).all().length, 0); - - const floor = createProjectFloorSnapshot("project-1", "EG", 0); - run(createProjectFloorInsertProjectCommand(floor)); - assert.throws( - () => - run( - createProjectFloorUpdateProjectCommand(floor, { - ...floor, - buildingId: "foreign", - }) - ), - /foreign building/ - ); - assert.throws( - () => createProjectFloorUpdateProjectCommand(floor, { ...floor }), - /only the name or the building/ - ); - assert.throws( - () => - createProjectFloorUpdateProjectCommand(floor, { - ...floor, - sortOrder: 5, - name: "Erdgeschoss", - }), - /only the name or the building/ - ); - } finally { - context.close(); - } - }); - - it("executes version 1 floor commands recorded before buildings existed", () => { - const context = createTestDatabase(); - try { - const { run } = createRunner(context); - const legacyFloor = { - id: "floor-legacy", - projectId: "project-1", - name: "EG", - sortOrder: 0, - }; - const legacyInsert = { - schemaVersion: 1 as const, - type: "project-floor.insert" as const, - payload: { floor: legacyFloor }, - }; - assertProjectFloorInsertProjectCommand(legacyInsert); - const inserted = run(legacyInsert); - assert.deepEqual(getFloor(context, legacyFloor.id), { - ...legacyFloor, - buildingId: null, - }); - // The inverse is always written in the current version. - assert.equal(inserted.inverse.schemaVersion, projectFloorCommandSchemaVersion); - - const legacyUpdate = { - schemaVersion: 1 as const, - type: "project-floor.update" as const, - payload: { - expected: legacyFloor, - target: { ...legacyFloor, name: "Erdgeschoss" }, - }, - }; - const updated = run(legacyUpdate); - assert.equal(getFloor(context, legacyFloor.id)?.name, "Erdgeschoss"); - run(updated.inverse, "undo"); - assert.equal(getFloor(context, legacyFloor.id)?.name, "EG"); - - // A version 1 snapshot must not carry buildingId, a version 2 one must. - assert.throws(() => - assertProjectFloorInsertProjectCommand({ - schemaVersion: 1, - type: "project-floor.insert", - payload: { floor: { ...legacyFloor, buildingId: null } }, - }) - ); - assert.throws(() => - assertProjectFloorInsertProjectCommand({ - schemaVersion: 2, - type: "project-floor.insert", - payload: { floor: legacyFloor }, - }) - ); - } finally { - context.close(); - } - }); - - it("validates building and floor requests", () => { - assert.equal( - createBuildingSchema.safeParse({ expectedRevision: 0, name: "Haus A" }).success, - true - ); - assert.equal( - createBuildingSchema.safeParse({ expectedRevision: 0, name: " " }).success, - false - ); - assert.equal( - createBuildingSchema.safeParse({ - expectedRevision: 0, - name: "Haus A", - sortOrder: 1, - }).success, - false - ); - for (const buildingId of [undefined, null, "building-1"]) { - assert.equal( - createFloorSchema.safeParse({ expectedRevision: 0, name: "EG", buildingId }) - .success, - true - ); - } - assert.equal( - updateFloorSchema.safeParse({ expectedRevision: 0, name: "EG", buildingId: "" }) - .success, - false - ); - }); -}); diff --git a/tests/project-command.service.test.ts b/tests/project-command.service.test.ts index 5630cb8..861edcd 100644 --- a/tests/project-command.service.test.ts +++ b/tests/project-command.service.test.ts @@ -41,7 +41,6 @@ import { projectChangeSets } from "../src/db/schema/project-change-sets.js"; import { projectDevices } from "../src/db/schema/project-devices.js"; import { projectRevisions } from "../src/db/schema/project-revisions.js"; import { projects } from "../src/db/schema/projects.js"; -import { buildings } from "../src/db/schema/buildings.js"; import { floors } from "../src/db/schema/floors.js"; import { rooms } from "../src/db/schema/rooms.js"; import { externalCsvConfigurations } from "../src/db/schema/external-csv-configurations.js"; @@ -79,8 +78,6 @@ import { createDistributionBoardComponentUpdateProjectCommand, } from "../src/domain/models/distribution-board-component-structure-project-command.model.js"; import { - createProjectBuildingInsertProjectCommand, - createProjectBuildingSnapshot, createProjectFloorInsertProjectCommand, createProjectFloorSnapshot, createProjectRoomInsertProjectCommand, @@ -1613,43 +1610,6 @@ describe("project command service", () => { } }); - it("dispatches building commands and floor assignments with persisted inverses", () => { - const context = createTestDatabase(); - try { - const service = createService(context); - const building = createProjectBuildingSnapshot("project-1", "Haus A", 0); - service.executeUser({ - projectId: "project-1", - expectedRevision: 0, - command: createProjectBuildingInsertProjectCommand(building), - }); - const floor = createProjectFloorSnapshot("project-1", "EG", 0, building.id); - const created = service.executeUser({ - projectId: "project-1", - expectedRevision: 1, - command: createProjectFloorInsertProjectCommand(floor), - }); - assert.equal(created.history.currentRevision, 2); - assert.equal( - context.db.select().from(floors).where(eq(floors.id, floor.id)).get() - ?.buildingId, - building.id - ); - - createService(context).undo({ projectId: "project-1", expectedRevision: 2 }); - createService(context).undo({ projectId: "project-1", expectedRevision: 3 }); - assert.equal(context.db.select().from(buildings).all().length, 0); - createService(context).redo({ projectId: "project-1", expectedRevision: 4 }); - createService(context).redo({ projectId: "project-1", expectedRevision: 5 }); - assert.deepEqual( - context.db.select().from(floors).where(eq(floors.id, floor.id)).get(), - floor - ); - } finally { - context.close(); - } - }); - it("dispatches floor and room setup with their persisted inverses", () => { const context = createTestDatabase(); try { diff --git a/tests/project-snapshot.repository.test.ts b/tests/project-snapshot.repository.test.ts index 0ed798d..449864a 100644 --- a/tests/project-snapshot.repository.test.ts +++ b/tests/project-snapshot.repository.test.ts @@ -20,7 +20,6 @@ import { circuits } from "../src/db/schema/circuits.js"; import { distributionBoardComponentProtectionDevices } from "../src/db/schema/distribution-board-component-protection-devices.js"; import { distributionBoardComponents } from "../src/db/schema/distribution-board-components.js"; import { distributionBoards } from "../src/db/schema/distribution-boards.js"; -import { buildings } from "../src/db/schema/buildings.js"; import { floors } from "../src/db/schema/floors.js"; import { projectDevices } from "../src/db/schema/project-devices.js"; import { projectSnapshots } from "../src/db/schema/project-snapshots.js"; @@ -60,15 +59,6 @@ function createTestDatabase(): DatabaseContext { .where(eq(circuitSections.circuitListId, list.id)) .get(); assert.ok(section); - context.db - .insert(buildings) - .values({ - id: "building-1", - projectId: "project-1", - name: "Haus A", - sortOrder: 0, - }) - .run(); context.db .insert(floors) .values({ @@ -76,7 +66,6 @@ function createTestDatabase(): DatabaseContext { projectId: "project-1", name: "EG", sortOrder: 10, - buildingId: "building-1", }) .run(); context.db @@ -259,9 +248,7 @@ describe("project snapshot repository", () => { "device-1" ); assert.equal(state.projectDevices.length, 1); - assert.equal(state.buildings.length, 1); assert.equal(state.floors.length, 1); - assert.equal(state.floors[0].buildingId, "building-1"); assert.equal(state.rooms.length, 1); assert.deepEqual(repository.listByProject("project-1"), [ created, @@ -413,14 +400,8 @@ describe("project snapshot repository", () => { schemaVersion: number; project: Record; distributionBoards: Array>; - buildings?: unknown; - floors: Array>; }; payload.schemaVersion = 5; - delete payload.buildings; - for (const floor of payload.floors) { - delete floor.buildingId; - } for (const field of ["siteAddress", "workStage", "projectLead", "projectEditor"]) { delete payload.project[field]; } @@ -453,9 +434,6 @@ describe("project snapshot repository", () => { (board) => board.boardNumber === null && board.fedFrom === null ) ); - // Version 5 predates buildings: the upgraded state has none. - assert.deepEqual(targetState.buildings, []); - assert.ok(targetState.floors.every((floor) => floor.buildingId === null)); } finally { context.close(); } @@ -519,11 +497,6 @@ describe("portable project transfer", () => { copied.state.distributionBoards[0].floorId, copied.state.floors[0].id ); - assert.notEqual(copied.state.buildings[0].id, "building-1"); - assert.equal( - copied.state.floors[0].buildingId, - copied.state.buildings[0].id - ); assert.equal( copied.state.distributionBoards[0].supplyType, "SV" diff --git a/tests/project-state-snapshot.test.ts b/tests/project-state-snapshot.test.ts index c75555f..04d4901 100644 --- a/tests/project-state-snapshot.test.ts +++ b/tests/project-state-snapshot.test.ts @@ -8,22 +8,10 @@ import { } from "../src/domain/models/project-state-snapshot.model.js"; import { createNamedProjectSnapshotSchema } from "../src/shared/validation/project-snapshot.schemas.js"; -/** Removes the fields introduced by snapshot schema version 7. */ -function withoutVersionSevenFields< - T extends { floors: object[] }, ->(snapshot: T): T { - delete (snapshot as Record).buildings; - for (const floor of snapshot.floors) { - delete (floor as Record).buildingId; - } - return snapshot; -} - -/** Removes the fields introduced by snapshot schema versions 6 and 7. */ +/** Removes the fields introduced by snapshot schema version 6. */ function withoutVersionSixFields< - T extends { project: object; distributionBoards: object[]; floors: object[] }, + T extends { project: object; distributionBoards: object[] }, >(snapshot: T): T { - withoutVersionSevenFields(snapshot); const project = snapshot.project as Record; for (const field of ["siteAddress", "workStage", "projectLead", "projectEditor"]) { delete project[field]; @@ -78,19 +66,7 @@ function minimalSnapshot() { distributionBoardComponentProtectionDevices: [], circuits: [], projectDevices: [], - buildings: [] as Array<{ - id: string; - projectId: string; - name: string; - sortOrder: number; - }>, - floors: [] as Array<{ - id: string; - projectId: string; - name: string; - sortOrder: number; - buildingId: string | null; - }>, + floors: [], rooms: [], }; } @@ -330,10 +306,7 @@ describe("project state snapshot model", () => { delete (outdated.project as Record).description; assert.throws(() => parseProjectStateSnapshot(outdated)); assert.throws(() => - parseProjectStateSnapshot({ - ...minimalSnapshot(), - schemaVersion: projectStateSnapshotSchemaVersion + 1, - }) + parseProjectStateSnapshot({ ...minimalSnapshot(), schemaVersion: 7 }) ); }); @@ -382,99 +355,12 @@ describe("project state snapshot model", () => { projectId: "project-2", name: "EG", sortOrder: 10, - buildingId: null, }); assert.throws( () => parseProjectStateSnapshot(wrongOwner), /floor belongs to a different project/ ); }); - - it("keeps buildings and validates floor building references", () => { - const snapshot = minimalSnapshot(); - snapshot.buildings.push({ - id: "building-1", - projectId: "project-1", - name: "Haus A", - sortOrder: 0, - }); - snapshot.floors.push( - { - id: "floor-1", - projectId: "project-1", - name: "EG", - sortOrder: 0, - buildingId: "building-1", - }, - { - id: "floor-2", - projectId: "project-1", - name: "Keller", - sortOrder: 1, - buildingId: null, - } - ); - const parsed = deserializeProjectStateSnapshot( - serializeProjectStateSnapshot(parseProjectStateSnapshot(snapshot)) - ); - assert.deepEqual(parsed.buildings, snapshot.buildings); - assert.deepEqual( - parsed.floors.map((floor) => floor.buildingId), - ["building-1", null] - ); - - const unknownBuilding = structuredClone(snapshot); - unknownBuilding.floors[0]!.buildingId = "building-x"; - assert.throws( - () => parseProjectStateSnapshot(unknownBuilding), - /floor building/ - ); - - const foreignBuilding = structuredClone(snapshot); - foreignBuilding.buildings[0]!.projectId = "project-2"; - assert.throws( - () => parseProjectStateSnapshot(foreignBuilding), - /building belongs to a different project/ - ); - - const floorWithoutBuildingField = structuredClone(snapshot); - delete (floorWithoutBuildingField.floors[1] as Record) - .buildingId; - assert.throws(() => parseProjectStateSnapshot(floorWithoutBuildingField)); - }); - - it("upgrades snapshot version 6 without buildings", () => { - const snapshot = minimalSnapshot(); - snapshot.floors.push({ - id: "floor-1", - projectId: "project-1", - name: "EG", - sortOrder: 0, - buildingId: null, - }); - const previous = withoutVersionSevenFields(structuredClone(snapshot)); - previous.schemaVersion = 6 as typeof previous.schemaVersion; - - const upgraded = parseProjectStateSnapshot(previous); - - assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion); - assert.deepEqual(upgraded.buildings, []); - assert.deepEqual(upgraded.floors, [ - { - id: "floor-1", - projectId: "project-1", - name: "EG", - sortOrder: 0, - buildingId: null, - }, - ]); - // Version 6 cover values survive the upgrade. - assert.equal(upgraded.project.workStage, "LPH 5"); - - const mislabeled = structuredClone(snapshot); - mislabeled.schemaVersion = 6 as typeof mislabeled.schemaVersion; - assert.throws(() => parseProjectStateSnapshot(mislabeled)); - }); }); describe("named project snapshot request", () => {