Add buildings above floors and a building structure page

Projects with several buildings could not be structured: floors and rooms
hung directly off the project. Floors can now belong to a building
(migration 0009: table buildings, nullable floors.building_id). Rooms and
distribution boards keep their floor; their building follows from it.
Existing floors stay without a building.

Buildings use the location-structure command store with
project-building.insert/.update/.delete, inverses and persistent Undo/Redo;
deleting a building with assigned floors is rejected. Floor commands carry
buildingId since command version 2. Version 1 floor commands from existing
history predate buildings and are upgraded with buildingId = null before
they are executed and persisted, so forward and inverse keep one version.

Project snapshot schema 7 stores buildings and floor.buildingId; versions
1-6 are upgraded without buildings. Restore and portable transfer include
buildings, and the transfer remaps building ids.

Buildings, floors and rooms move from the project overview to their own
page /projects/:id/struktur with three columns (Gebäude -> Etagen -> Räume)
that filter each other, and a sidebar entry. Floor selects in the board and
room dialogs are grouped by building. Unchanged dialogs close without a
command. Details in docs/building-structure.md; see issue #6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Grovy311 2026-09-26 12:52:34 +02:00
parent 7692699ee2
commit cd89c4a499
42 changed files with 5883 additions and 373 deletions

View file

@ -0,0 +1,79 @@
# 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.