Compare commits
13 Commits
f4bfef9071
...
43a3bb0b69
| Author | SHA1 | Date | |
|---|---|---|---|
| 43a3bb0b69 | |||
| facf788ce8 | |||
| 1eed19ef6b | |||
| 1e3c721cfa | |||
| 5fb81bab04 | |||
| 734a2bcfc4 | |||
| 5bee3cb103 | |||
| 86f1d42e60 | |||
| 0fd564a930 | |||
| 13693f64b1 | |||
| bd8dd09f79 | |||
| 54874f9fcb | |||
| 2964205a27 |
@@ -33,17 +33,12 @@ It must support circuits, device rows, project devices, drag-and-drop restructur
|
||||
- General application repositories also require an explicit `AppDatabase`;
|
||||
`src/server/composition/application-repositories.ts` owns their runtime
|
||||
instances, and controllers never import the global SQLite client.
|
||||
- The upgrade-only legacy migration service follows the same dependency rule;
|
||||
its concrete repositories are instantiated only in
|
||||
`scripts/db-migrate-legacy-consumers.ts`.
|
||||
- General Circuit, CircuitDeviceRow, CircuitList and DistributionBoard
|
||||
repositories expose only active reads. Runtime writes belong in typed command
|
||||
repositories; direct integration fixtures belong under `tests/support`.
|
||||
|
||||
The supported runtime model is Circuit-First. The former Consumer UI and API are
|
||||
removed. Retained `consumers` rows, migration mappings and reports are upgrade-only
|
||||
data accessed by `npm run db:migrate:legacy-consumers`; do not build application
|
||||
features on them.
|
||||
The supported runtime model is Circuit-First. The former Consumer UI, API,
|
||||
tables and upgrade tooling are removed; do not reintroduce them.
|
||||
|
||||
See `docs/current-architecture.md` for the complete module and request flow.
|
||||
|
||||
@@ -250,14 +245,14 @@ Immutable revision metadata is available through the paginated
|
||||
stored command payloads.
|
||||
Named logical snapshots can be created and listed through project-scoped API
|
||||
endpoints. Their schema-versioned payload contains the complete supported
|
||||
project runtime state and a SHA-256, excludes global/upgrade-only data and does
|
||||
project runtime state and a SHA-256, excludes global data outside the project and does
|
||||
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.
|
||||
Snapshot schema version 7 also contains circuit-group identity,
|
||||
Baseline snapshot schema version 1 contains circuit-group identity,
|
||||
distribution-board components and the separate circuit/component protection
|
||||
records. Versions 1 through 6 remain readable. Portable duplicate imports
|
||||
records. Pre-baseline snapshots are unsupported. Portable duplicate imports
|
||||
remap component ids and protection-owner references together with the existing
|
||||
project graph.
|
||||
The central revision boundary creates an automatic logical snapshot after each
|
||||
@@ -294,13 +289,18 @@ change in one revision and Undo/Redo restores them together; the project PUT
|
||||
route requires `expectedRevision`. The system catalog is `AV`, `SV`, `EV`,
|
||||
`USV`, `MSR`, `SiBe`; at least one must be enabled and a type used by a board
|
||||
cannot be disabled.
|
||||
Distribution-board setup uses `distribution-board.insert` schema version 3
|
||||
Distribution-board setup uses `distribution-board.insert` schema version 1
|
||||
with a complete stable snapshot of the board, circuit list, three default
|
||||
groups, main switch `-Q0` and surge protective device `-FA`. Its
|
||||
inverse removes only the same unchanged and still-empty structure; the POST
|
||||
route requires `expectedRevision` and returns the updated history state.
|
||||
Stored schema-version 1 and 2 setup commands remain executable with their four
|
||||
legacy sections and without invented components.
|
||||
Complete populated distribution-board copying and deletion use
|
||||
`distribution-board.insert-subtree` and `distribution-board.delete-subtree`.
|
||||
Their exact snapshots include the board, circuit list, groups, circuits,
|
||||
device rows, components and all one-to-one protection data. Copy remaps all
|
||||
owning ids but preserves project-device and room links. Both operations are
|
||||
atomic persistent commands with restart-safe Undo/Redo; deletion requires an
|
||||
explicit UI warning.
|
||||
Mutable group-protection and auxiliary distribution-board components use
|
||||
`distribution-board-component.insert` and
|
||||
`distribution-board-component.delete`; edits and footer reordering use
|
||||
@@ -335,15 +335,15 @@ UUID in foreign-key-safe order.
|
||||
The circuit-tree read model exposes fixed `headerComponents`, group-owned
|
||||
`components`, `footerComponents`, optional group category/number and optional
|
||||
one-to-one protection DTOs for circuits and group components. These are
|
||||
additive to the retained flat circuit protection fields during the transition.
|
||||
the only supported circuit-protection source; no flat compatibility fields remain.
|
||||
Distribution-board floor assignment and a project-enabled supply type use
|
||||
`distribution-board.update`; both values are snapshot/export fields and one
|
||||
persistent undo step.
|
||||
Floor and room setup use `project-floor.insert` and `project-room.insert` with
|
||||
complete stable snapshots. Their inverses remove only unchanged records and
|
||||
Floor and room setup uses persistent insert, update and delete commands with
|
||||
complete stable snapshots. Their inverses remove or restore only unchanged records and
|
||||
reject floors with assigned rooms/distribution boards or rooms referenced by
|
||||
device rows or retained upgrade data. Both POST routes require `expectedRevision` and return
|
||||
the updated history state.
|
||||
device rows. All write routes require `expectedRevision` and return the updated
|
||||
history state.
|
||||
|
||||
Required operations:
|
||||
|
||||
@@ -382,7 +382,10 @@ Users must be able to override sizing suggestions.
|
||||
## Persistence and Migration Rules
|
||||
|
||||
- SQLite is the currently supported database.
|
||||
- Never edit an already applied migration.
|
||||
- Migration `0000` is the clean release baseline for empty databases.
|
||||
- Pre-baseline development databases, snapshots and stored commands are not
|
||||
supported.
|
||||
- After the first release, never edit an already applied migration.
|
||||
- Back up an existing database before applying a new migration.
|
||||
- Inspect generated SQL; it must contain only the intended schema change.
|
||||
- Keep database backups separate from logical project snapshots.
|
||||
|
||||
@@ -108,11 +108,9 @@ npm run db:generate
|
||||
```
|
||||
|
||||
`db:backup` verwendet die SQLite-Online-Backup-API und prüft das Ergebnis auf
|
||||
Integrität und Fremdschlüsselverletzungen. Vor jeder Migration einer bestehenden
|
||||
Datenbank ist ein Backup erforderlich.
|
||||
|
||||
`db:migrate:legacy-consumers` und `db:backfill:sections` sind ausschließlich
|
||||
Upgrade-Werkzeuge für ältere Datenbanken. Neue Installationen benötigen sie nicht.
|
||||
Integrität und Fremdschlüsselverletzungen. Die Migration `0000` ist die
|
||||
Release-Baseline und richtet ausschließlich eine neue, leere Datenbank ein.
|
||||
Datenbanken aus Entwicklungsständen vor dieser Baseline werden nicht unterstützt.
|
||||
|
||||
## Dokumentation
|
||||
|
||||
|
||||
Binary file not shown.
@@ -20,9 +20,6 @@ ausdrücklich getrennt und dürfen nicht als bereits implementiert verstanden we
|
||||
|
||||
- [Entwicklungs- und Contributor-Workflow](development-workflow.md)
|
||||
- [Deployment und Betrieb](deployment.md)
|
||||
- [Lokale Circuit-First-Datenbankmigration](local-db-circuit-first-migration.md)
|
||||
- [Retained Legacy-Datenmigration](circuit-list-editor-migration.md) –
|
||||
ausschließlich für Upgrades alter Datenbanken
|
||||
|
||||
## Zukunftsarchitektur
|
||||
|
||||
|
||||
@@ -74,8 +74,8 @@ Protected-board structure uses
|
||||
persisted inverse of a delete. The dispatcher also supports the documented
|
||||
project-device, project settings, distribution-board and project-location
|
||||
commands. The generic editor commands listed here use schema version `1`;
|
||||
older stored formats can remain supported by their owning command boundary.
|
||||
Unsupported types are rejected. Insert commands contain the complete entity or circuit block
|
||||
the clean pre-release baseline does not accept older development formats.
|
||||
Unsupported types or schema versions are rejected. Insert commands contain the complete entity or circuit block
|
||||
with stable ids; delete commands include the expected parent identity. Circuit
|
||||
snapshots contain zero, one or multiple complete device rows. Move commands
|
||||
contain each row's expected and target circuit plus its exact expected and
|
||||
@@ -225,14 +225,27 @@ returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`.
|
||||
`{ "distributionBoard": { ... }, "revision": { ... }, "history": { ... } }`
|
||||
- persistent Undo removes only the unchanged and still-empty generated
|
||||
structure; Redo restores the same ids
|
||||
- stored command schema versions 1 and 2 remain executable with their four
|
||||
legacy sections and without generated components
|
||||
- stale revisions return `409 PROJECT_REVISION_CONFLICT`
|
||||
- `PUT /projects/:projectId/distribution-boards/:distributionBoardId`
|
||||
- body:
|
||||
`{ "floorId": null, "supplyType": "AV", "expectedRevision": 13 }`
|
||||
- executes `distribution-board.update`; floor and supply type are restored
|
||||
together by persistent Undo/Redo
|
||||
- `POST /projects/:projectId/distribution-boards/:distributionBoardId/copy`
|
||||
- body: `{ "name": "UV-02 Kopie", "expectedRevision": 14 }`
|
||||
- duplicates the complete distribution-board subtree with new owning UUIDs
|
||||
while preserving project-device and room links
|
||||
- response includes the new `distributionBoard`, its `circuitList`, the
|
||||
revision and current history state
|
||||
- `DELETE /projects/:projectId/distribution-boards/:distributionBoardId`
|
||||
- body: `{ "expectedRevision": 15 }`
|
||||
- removes the complete, exactly captured distribution-board subtree in one
|
||||
transaction
|
||||
- response includes `distributionBoardId`, the revision and current history
|
||||
state
|
||||
- Copy and deletion use `distribution-board.insert-subtree` and
|
||||
`distribution-board.delete-subtree`. Persistent Undo/Redo restores or
|
||||
removes the same complete subtree, including protection devices.
|
||||
|
||||
### Project Floors and Rooms
|
||||
|
||||
@@ -240,16 +253,27 @@ returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`.
|
||||
- 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", "expectedRevision": 14 }`
|
||||
- executes an exact persistent floor update
|
||||
- `DELETE /projects/:projectId/floors/:floorId`
|
||||
- body: `{ "expectedRevision": 15 }`
|
||||
- rejects floors with assigned rooms or distribution boards
|
||||
- `POST /projects/:projectId/rooms`
|
||||
- body:
|
||||
`{ "floorId": "floor_1", "roomNumber": "001", "roomName": "Technik", "expectedRevision": 14 }`
|
||||
- executes `project-room.insert` with a stable room id; `floorId` is optional
|
||||
and must belong to the project when present
|
||||
- response: `{ "room": { ... }, "revision": { ... }, "history": { ... } }`
|
||||
- Persistent Undo removes only unchanged, unreferenced records. A floor with
|
||||
assigned rooms or distribution boards and a room referenced by device rows
|
||||
or retained upgrade data are rejected instead of silently clearing foreign
|
||||
keys.
|
||||
- `PUT /projects/:projectId/rooms/:roomId`
|
||||
- body:
|
||||
`{ "floorId": null, "roomNumber": "101", "roomName": "Büro", "expectedRevision": 16 }`
|
||||
- updates number, name and optional floor as one persistent command
|
||||
- `DELETE /projects/:projectId/rooms/:roomId`
|
||||
- body: `{ "expectedRevision": 17 }`
|
||||
- rejects rooms referenced by device rows
|
||||
- Persistent Undo/Redo restores exact previous and target records without
|
||||
silently clearing foreign keys.
|
||||
- Stale revisions return `409 PROJECT_REVISION_CONFLICT`.
|
||||
|
||||
### Tree Endpoint
|
||||
@@ -343,12 +367,6 @@ available only as the corresponding versioned commands through
|
||||
structure POST, move, reorder, renumber, identifier-restore, Circuit DELETE or
|
||||
CircuitDeviceRow DELETE routes.
|
||||
|
||||
## Removed Legacy Endpoints
|
||||
|
||||
The former `/consumers` read/write endpoints were removed after every retained
|
||||
consumer had a verified Circuit-First migration mapping. Database upgrade tooling
|
||||
reads retained legacy rows directly; application features must use the Circuit-First
|
||||
endpoints above.
|
||||
|
||||
## Linked Project Device Review
|
||||
|
||||
|
||||
@@ -49,11 +49,6 @@ The pure grid modules have no React state and are covered by focused unit tests.
|
||||
- Represents fixed header components, optional group protection and manually
|
||||
named auxiliary footer devices with their own unique BMK.
|
||||
- Group protection may own a separate one-to-one protection configuration.
|
||||
- Retained legacy migration source
|
||||
- Old `consumers` rows, mappings and reports are available only to explicit
|
||||
database upgrade tooling.
|
||||
- They are not application-domain entities and have no UI or API.
|
||||
|
||||
## Why A Circuit Is Not One Row
|
||||
|
||||
A circuit can contain zero, one, or many device rows. Treating a circuit as a single row breaks:
|
||||
@@ -116,8 +111,8 @@ Primary circuit-first editor route:
|
||||
|
||||
- `/projects/:projectId/circuit-lists/:circuitListId/tree-edit`
|
||||
|
||||
The sibling `/tree` route is a read-only structure preview. Old
|
||||
`/projects/:projectId/circuit-lists` bookmarks redirect to the project page.
|
||||
Old `/projects/:projectId/circuit-lists` bookmarks redirect to the project
|
||||
page. A future print view will provide a dedicated read-only presentation.
|
||||
|
||||
## Future Persistence Direction
|
||||
|
||||
|
||||
@@ -23,4 +23,3 @@
|
||||
- Protection and cable values are planner-owned manual selections. The later
|
||||
sizing/warning engine does not yet validate them against load, length or
|
||||
voltage-drop rules.
|
||||
- Legacy consumer UI and server endpoints are removed; retained source rows and migration mappings remain available only for upgrade traceability.
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
# Circuit List Editor Migration
|
||||
|
||||
## Status
|
||||
|
||||
Upgrade-only. New installations already use the Circuit-First schema and do not
|
||||
run this workflow.
|
||||
|
||||
## Goal
|
||||
|
||||
Migrate legacy row-first consumers into the circuit-first model without deleting legacy data.
|
||||
|
||||
## Legacy Mapping
|
||||
|
||||
Legacy `Consumer` rows map into:
|
||||
|
||||
- `Circuit` for shared circuit identity and circuit-level technical fields
|
||||
- `CircuitDeviceRow` for per-device load rows
|
||||
|
||||
Multiple legacy consumers can map into one circuit when they share normalized circuit identity.
|
||||
|
||||
For one circuit list, all new circuits, device rows, trace mappings and the migration report are committed in one SQLite transaction. A failed run therefore leaves none of those prepared migration writes behind.
|
||||
|
||||
## Grouping Strategy (`circuitNumber`)
|
||||
|
||||
Migration groups legacy rows by normalized `circuitNumber`:
|
||||
|
||||
- valid/normalizable values become one target circuit per normalized value
|
||||
- duplicates are grouped under that circuit (multiple `CircuitDeviceRow`s)
|
||||
- missing/invalid values trigger generated identifiers and may fall back to `unassigned` section
|
||||
|
||||
## Default Section Backfill
|
||||
|
||||
Before migration, default sections are created/backfilled per circuit list.
|
||||
This guarantees a valid target section space, including `unassigned` when no section can be inferred.
|
||||
|
||||
## Migration Commands
|
||||
|
||||
Run in this order for local database workflows:
|
||||
|
||||
1. Backup:
|
||||
- `npm run db:backup`
|
||||
2. Migrate schema:
|
||||
- `npm run db:migrate`
|
||||
3. Verify circuit schema:
|
||||
- `npm run db:verify:circuit-schema`
|
||||
4. Backfill missing sections:
|
||||
- `npm run db:backfill:sections`
|
||||
5. Migrate legacy consumers:
|
||||
- `npm run db:migrate:legacy-consumers`
|
||||
|
||||
The migration command finishes with a cutover verification across the complete
|
||||
database. It exits with an error while any legacy consumer lacks a migration
|
||||
mapping, including consumers that cannot be migrated because they have no circuit
|
||||
list assignment. The legacy multi-list UI was removed only after this check passed;
|
||||
old `/projects/:projectId/circuit-lists` bookmarks redirect to the project page.
|
||||
|
||||
## Validation Checks
|
||||
|
||||
After migration, verify:
|
||||
|
||||
- tree endpoint returns sections/circuits/rows
|
||||
- grouped duplicate circuit numbers are reported
|
||||
- generated identifiers are reported where expected
|
||||
- migrated rows include `legacyConsumerId` traceability
|
||||
- no duplicate BMKs exist inside one circuit list
|
||||
|
||||
## If Tree Endpoint Returns Empty Sections
|
||||
|
||||
Likely causes:
|
||||
|
||||
- circuit-first tables missing (migration not run)
|
||||
- sections not backfilled yet
|
||||
- migrated dataset genuinely empty for selected list
|
||||
|
||||
Actions:
|
||||
|
||||
1. Run schema migration and verification commands.
|
||||
2. Run section backfill command.
|
||||
3. Run legacy-consumer migration command.
|
||||
4. Retry tree endpoint.
|
||||
|
||||
## Legacy Data Retention
|
||||
|
||||
Do not delete legacy consumers yet.
|
||||
|
||||
- legacy read/write endpoints have been removed from the application server
|
||||
- migration trace tables reference old/new mapping
|
||||
- retained rows allow upgrade verification and audit of the completed mapping
|
||||
- a future automatic cleanup migration must migrate and verify old databases
|
||||
before dropping these source and trace tables
|
||||
@@ -9,10 +9,8 @@ Der unterstützte Editor ist Circuit-First:
|
||||
Ein Stromkreis ist nicht dasselbe wie eine Gerätezeile. BMK, Schutz- und Kabeldaten
|
||||
gehören zum Stromkreis; Last-, Raum- und Kategoriedaten gehören zur Gerätezeile.
|
||||
|
||||
Die frühere Consumer-Oberfläche und ihre API sind entfernt. Die Tabelle
|
||||
`consumers` sowie Mappings und Reports bleiben ausschließlich erhalten, damit
|
||||
ältere Datenbanken über den expliziten Upgrade-Befehl migriert und geprüft werden
|
||||
können.
|
||||
Die frühere Consumer-Oberfläche, ihre API, Tabellen und Upgrade-Werkzeuge sind
|
||||
entfernt. Neue Funktionen bauen ausschließlich auf dem Circuit-First-Modell auf.
|
||||
|
||||
## Laufzeit
|
||||
|
||||
@@ -130,8 +128,8 @@ Datenbank-Backups gespeichert. `POST /api/projects/:projectId/snapshots`
|
||||
erzeugt bei passender erwarteter Revision transaktional einen vollständigen,
|
||||
schema-versionierten Projektzustand mit SHA-256-Prüfwert. Enthalten sind
|
||||
Projekteinstellungen, Verteiler, Stromkreislisten, Bereiche, Stromkreise und
|
||||
Gerätezeilen sowie Projektgeräte, Geschosse und Räume. Globale Geräte,
|
||||
Legacy-Consumer und Migrationsberichte sind nicht Teil des Projekt-Snapshots.
|
||||
Gerätezeilen sowie Projektgeräte, Geschosse und Räume. Globale Geräte sind nicht
|
||||
Teil des Projekt-Snapshots.
|
||||
Create/List verändern weder Projektrevision noch Undo-/Redo-Stapel.
|
||||
`kind` unterscheidet benannte und automatische Stände. Die zentrale
|
||||
Revisionspersistenz erzeugt nach jeweils 25 weiteren Projektänderungen
|
||||
@@ -144,9 +142,7 @@ Prüfsumme, erwartete Revision und den unmittelbar zuvor gelesenen
|
||||
Projektzustand. Der Restore ersetzt alle unterstützten Projektdaten in einer
|
||||
Transaktion und schreibt dabei eine neue Revision mit Quelle `restore` sowie
|
||||
ein vollständiges inverses Kommando. Undo und Redo können deshalb auch einen
|
||||
Restore nach einem Neustart exakt zurücknehmen oder wiederholen. Kompatible
|
||||
Upgrade-only-Verknüpfungen und Migrationsnachweise bleiben erhalten, obwohl sie
|
||||
nicht Bestandteil des logischen Snapshots sind.
|
||||
Restore nach einem Neustart exakt zurücknehmen oder wiederholen.
|
||||
Die Projektseite bindet diese APIs in einem einklappbaren Bereich
|
||||
„Versionen und Sicherungspunkte“ ein. Dort können Benutzer Sicherungspunkte
|
||||
benennen, jeden aufgeführten benannten oder automatischen Stand nach
|
||||
@@ -246,8 +242,7 @@ Store leitet das inverse Kommando aus dem gespeicherten Projekt ab und schreibt
|
||||
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-Versionen vor dieser Erweiterung bleiben les- und
|
||||
ausführbar; fehlende Metadaten werden dabei als `null` behandelt.
|
||||
Kommando- und Snapshot-Payloads müssen dem aktuellen Baseline-Schema entsprechen.
|
||||
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,
|
||||
@@ -263,16 +258,16 @@ läuft über `project.restore-state` und ist dadurch eine atomare, dauerhaft
|
||||
rückgängig machbare Projektrevision. Der Modus `duplicate` ordnet Projekt-,
|
||||
Struktur-, Gruppen-, Verteilerkomponenten-, Raum-, Stromkreis- und
|
||||
Gerätezeilen-UUIDs sowie Schutzgeräte-Referenzen vollständig neu zu und legt
|
||||
die Kopie mit Revision `0` in einer Transaktion an. Upgrade-only-
|
||||
Consumer-Verweise werden nicht in die Kopie übernommen; fachliche Verknüpfungen
|
||||
innerhalb des unterstützten Laufzeitmodells bleiben erhalten. Die
|
||||
die Kopie mit Revision `0` in einer Transaktion an. Fachliche Verknüpfungen
|
||||
innerhalb des unterstützten Laufzeitmodells bleiben
|
||||
erhalten. Die
|
||||
Projektübersicht verwendet dafür den separaten Collection-Endpunkt
|
||||
`POST /api/projects/import`, der ausschließlich eine neue Kopie anlegt und
|
||||
deshalb weder eine bestehende Projekt-ID noch `expectedRevision` annimmt. Das
|
||||
Ersetzen eines Projekts bleibt auf dessen Einstellungsmodal und den
|
||||
projektgebundenen Endpunkt beschränkt.
|
||||
`distribution-board.insert` versioniert die Anlage einer Verteilung mit
|
||||
Command-Schema 3 als vollständigen Block aus Verteilung, Stromkreisliste, den
|
||||
Command-Schema 1 als vollständigen Block aus Verteilung, Stromkreisliste, den
|
||||
drei Gruppen `Beleuchtung 1`, `1-phasig 1`, `3-phasig 1`, Hauptschalter `-Q0`
|
||||
und Überspannungsableiter `-FA`.
|
||||
Alle UUIDs entstehen vor dem Command und bleiben über Undo/Redo stabil.
|
||||
@@ -283,8 +278,16 @@ Controller-Schreibweg ist entfernt. Verteilungen besitzen eine optionale
|
||||
Etagenreferenz sowie eine Netzart aus dem Projektkatalog. Anlage und
|
||||
nachträgliche Bearbeitung prüfen die Projektzugehörigkeit der Etage und die
|
||||
Freigabe der Netzart in den Projekteinstellungen.
|
||||
Gespeicherte Anlage-Commands der Schemas 1 und 2 bleiben mit ihren vier
|
||||
Legacy-Abschnitten und ohne nachträglich erfundene Komponenten ausführbar.
|
||||
Bereits befüllte Verteilungen werden über
|
||||
`distribution-board.insert-subtree` und `distribution-board.delete-subtree`
|
||||
als vollständiger Unterbaum kopiert beziehungsweise gelöscht. Der Snapshot
|
||||
umfasst Verteilung, Stromkreisliste, Gruppen, Stromkreise, Gerätezeilen,
|
||||
Verteilerkomponenten und alle zugehörigen Schutzgeräte. Beim Kopieren werden
|
||||
sämtliche besitzenden UUIDs neu vergeben, während fachliche Verknüpfungen zu
|
||||
Projektgeräten und Räumen erhalten bleiben. Beide Aktionen sind atomare
|
||||
Projektrevisionen und bleiben nach einem Neustart über Undo/Redo umkehrbar. Die
|
||||
Projektseite bietet sie im Einstellungsmodal der jeweiligen Verteilung an;
|
||||
Löschen verlangt dort eine ausdrückliche Bestätigung.
|
||||
`distribution-board-component.insert` und
|
||||
`distribution-board-component.delete` sowie
|
||||
`distribution-board-component.update` versionieren Anlage, Entfernung,
|
||||
@@ -346,9 +349,9 @@ Teilwiederherstellungen.
|
||||
Der Circuit-Tree liefert zusätzlich `headerComponents`,
|
||||
gruppenbezogene `components` und `footerComponents`. Abschnitte enthalten
|
||||
optionale Gruppenkategorie und -nummer; Stromkreise und Gruppenkomponenten
|
||||
enthalten ihre optionale getrennte Schutzgerätekonfiguration. Die bisherigen
|
||||
flachen Stromkreis-Schutzfelder bleiben während der Übergangsphase additiv
|
||||
erhalten. Separate aktive Read-Repositories lesen Komponenten- und beide
|
||||
enthalten ihre getrennte Schutzgerätekonfiguration. Die
|
||||
1:1-Schutzgerätetabellen sind die einzige unterstützte Quelle dieser Daten.
|
||||
Separate aktive Read-Repositories lesen Komponenten- und beide
|
||||
Schutzgerätetabellen; Schreibzugriffe bleiben ausschließlich in Commands.
|
||||
`src/frontend/utils/circuit-structure-projection.ts` projiziert diesen Tree
|
||||
rein und deterministisch in die drei Zonen Kopf, vollständige Gruppen und Fuß.
|
||||
@@ -378,12 +381,12 @@ Projektzugehörigkeit und schreibt Schutzgerät, Revision und Historienübergang
|
||||
atomar. Benutzer können Schutzdaten anlegen oder ändern, aber nicht entfernen;
|
||||
Undo einer erstmaligen Anlage darf den zuvor fehlenden Datensatz exakt
|
||||
wiederherstellen.
|
||||
Der vollständige `CircuitSnapshot` kann rückwärtskompatibel einen
|
||||
`protectionDevice`-Datensatz enthalten. Neue gruppierte Stromkreise und durch
|
||||
Geräteverschiebung erzeugte Zielstromkreise verwenden die vereinbarten
|
||||
Der vollständige `CircuitSnapshot` enthält den zugehörigen
|
||||
`protectionDevice`-Datensatz, sofern einer vorhanden ist. Neue Stromkreise und
|
||||
durch Geräteverschiebung erzeugte Zielstromkreise verwenden die vereinbarten
|
||||
Kategorie-Standardwerte und schreiben Schutzgerät, Stromkreis sowie
|
||||
Gerätezeilen atomar. Delete/Undo erfasst denselben Datensatz vollständig.
|
||||
Im Editor sind die bisherigen Schutzspalten deshalb nur noch eine
|
||||
Im Editor sind die Schutzspalten deshalb eine
|
||||
schreibgeschützte Projektion der 1:1-Daten; Änderungen erfolgen über ein
|
||||
geräteabhängiges Schutzgeräte-Modal und `circuit-protection.update`.
|
||||
Gruppen lassen sich im Editor schrittweise nur gegenüber einer benachbarten
|
||||
@@ -411,34 +414,26 @@ wieder her.
|
||||
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
||||
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
||||
ist für bestehende sowie neu angelegte Verteilungen standardmäßig `1`.
|
||||
Snapshot-Schema 6 und der portable Projekttransfer enthalten diesen Wert;
|
||||
Schema 5 und älter werden mit dem neutralen Faktor `1` hochgestuft.
|
||||
Snapshot-Schema 4 enthält bereits Etage und Netzart; Schema 1/2 sowie
|
||||
gespeicherte Version-1-Strukturcommands werden ohne erfundene Zuordnung
|
||||
hochgestuft. Snapshot-Schema 3 wird mit allen sechs Netzarten als
|
||||
Projektauswahl hochgestuft.
|
||||
Snapshot-Schema 7 ergänzt Gruppenkategorie und -nummer,
|
||||
Verteilerkomponenten sowie die getrennten 1:1-Schutzgerätedaten. Capture,
|
||||
Das aktuelle Baseline-Snapshot-Schema enthält den Gleichzeitigkeitsfaktor,
|
||||
Gruppenkategorie und -nummer, Verteilerkomponenten sowie die getrennten
|
||||
1:1-Schutzgerätedaten. Capture,
|
||||
benannte und automatische Snapshots, Wiederherstellung, Undo/Redo und beide
|
||||
JSON-Importmodi verwenden denselben vollständigen Zustand. Schema 6 und älter
|
||||
bleiben lesbar: Die drei bekannten Abschnittsschlüssel werden deterministisch
|
||||
Gruppe 1 zugeordnet, neue Komponenten- und Schutzgerätesammlungen bleiben leer
|
||||
und bestehende flache Schutzangaben unverändert erhalten.
|
||||
`project-floor.insert` und `project-room.insert` versionieren die Anlage von
|
||||
Geschossen und Räumen mit stabilen UUIDs. Die vollständigen Datensätze bilden
|
||||
jeweils die persistierte Inverse für Undo/Redo. Ein Geschoss wird durch Undo nur
|
||||
JSON-Importmodi verwenden denselben vollständigen Zustand. Vorherige
|
||||
Entwicklungsschemas werden nicht mehr eingelesen.
|
||||
Persistente Insert-, Update- und Delete-Commands versionieren Anlage,
|
||||
Bearbeitung und Löschung von Geschossen und Räumen mit stabilen UUIDs und
|
||||
exakten Vorher-/Nachher-Snapshots. Ein Geschoss wird nur
|
||||
entfernt, solange ihm weder ein Raum noch eine Verteilung zugeordnet wurde. Ein
|
||||
Raum wird nur entfernt,
|
||||
solange weder eine CircuitDeviceRow noch ein aufbewahrter Upgrade-Datensatz auf
|
||||
ihn verweist. Beide POST-Endpunkte verlangen `expectedRevision`, liefern den
|
||||
aktualisierten Historienstand und besitzen keinen direkten Create-Schreibweg
|
||||
mehr.
|
||||
Raum wird nur entfernt, solange keine CircuitDeviceRow auf ihn verweist. Alle
|
||||
Schreibendpunkte verlangen `expectedRevision` und liefern den aktualisierten
|
||||
Historienstand.
|
||||
|
||||
## Projektgeräte
|
||||
|
||||
`ProjectDevice` verwendet ausschließlich die kanonischen Circuit-First-Felder:
|
||||
`phaseType`, `powerPerUnit`, `simultaneityFactor`, `cosPhi`, `remark` sowie
|
||||
optionale technische und kategorisierende Felder.
|
||||
Die Projektgerätekategorie `lighting`, `single_phase` oder `three_phase` ist die
|
||||
fachliche Klassifikation und Zielgruppe. `phaseType` ist keine separate
|
||||
Benutzereingabe, sondern wird daraus für Spannung, Verknüpfung und spätere
|
||||
Dimensionierung abgeleitet.
|
||||
|
||||
Beim Einfügen entsteht eine verknüpfte `CircuitDeviceRow`. Der Anzeigename wird
|
||||
kopiert, aber nicht still synchronisiert. Spätere Änderungen am Projektgerät
|
||||
@@ -452,32 +447,30 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät.
|
||||
|
||||
- SQLite ist die aktuell unterstützte Datenbank.
|
||||
- Fremdschlüssel werden für jeden Datenbankkontext aktiviert.
|
||||
- `npm run db:migrate` wendet Drizzle-Migrationen an.
|
||||
- `npm run db:migrate` richtet mit der einzelnen Migration `0000` das komplette
|
||||
aktuelle Schema einer leeren Datenbank ein.
|
||||
- `npm run db:verify:circuit-schema` prüft erforderliche und entfernte Spalten.
|
||||
- `npm run db:backup` erzeugt ein konsistentes und verifiziertes Online-Backup.
|
||||
- `npm run typecheck:scripts` prüft alle TypeScript-Wartungs- und
|
||||
Upgrade-Skripte mit ihren Anwendungspfaden, ohne Code zu erzeugen.
|
||||
- Angewendete Migrationen werden niemals nachträglich verändert.
|
||||
- `db:migrate:legacy-consumers` ist Upgrade-Werkzeug, kein Anwendungspfad.
|
||||
Der fachliche Migrationsdienst kennt nur schmale Reader-/Store-Ports unter
|
||||
`src/domain/ports`; konkrete SQLite-Repositories werden ausschließlich im
|
||||
CLI-Skript zusammengesetzt.
|
||||
- `npm run typecheck:scripts` prüft die TypeScript-Wartungsskripte, ohne Code zu
|
||||
erzeugen.
|
||||
- Die Baseline ersetzt alle vorigen Entwicklungsmigrationen. Datenbanken,
|
||||
Snapshots und persistierte Commands aus Vor-Baseline-Ständen werden bewusst
|
||||
nicht unterstützt. Ab der ersten veröffentlichten Version sind angewendete
|
||||
Migrationen unveränderlich und Änderungen erfolgen additiv.
|
||||
- Allgemeine Circuit-, Gerätezeilen-, CircuitList- und DistributionBoard-
|
||||
Repositories stellen im Anwendungspfad nur noch benötigte Leseabfragen bereit.
|
||||
Fachliche Schreibvorgänge liegen in den typisierten Command-Repositories;
|
||||
Upgrade-Schreibvorgänge bleiben in expliziten Migrationsadaptern.
|
||||
Fachliche Schreibvorgänge liegen in den typisierten Command-Repositories.
|
||||
- Die vollständige Verteilungs-Testfixture liegt unter
|
||||
`tests/support/distribution-board-fixture.ts` und ist kein exportierter
|
||||
Produktions-Schreibweg.
|
||||
- Migration `0024` bildet die additive relationale Grundlage für geschützte
|
||||
Stromkreisgruppen und Verteilerkomponenten. Die drei bestehenden
|
||||
Stromkreisabschnitte erhalten Kategorie und Gruppennummer 1. Separate
|
||||
1:1-Tabellen halten künftig Stromkreis- und Komponenten-Schutzgeräte; die
|
||||
bisherigen flachen Schutzfelder bleiben in dieser Übergangsphase unverändert.
|
||||
- Die Baseline bildet die relationale Grundlage für geschützte Stromkreisgruppen
|
||||
und Verteilerkomponenten. Separate
|
||||
1:1-Tabellen halten Stromkreis- und Komponenten-Schutzgeräte. Die früheren
|
||||
flachen Stromkreis-Schutzfelder sind aus der Baseline entfernt.
|
||||
Ein triggergeführtes Register erzwingt bereits eine normalisierte,
|
||||
stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und
|
||||
Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden
|
||||
Snapshot-Schema 7. Persistente Insert/Delete/Update-Commands für
|
||||
Snapshot-Schema 1 der Baseline. Persistente Insert/Delete/Update-Commands für
|
||||
veränderliche Verteilerkomponenten, Gruppen einschließlich befüllter
|
||||
Unterbäume sowie vollständige Gruppensortierung sind integriert. Der Editor
|
||||
zeigt die geschützte Struktur an und bearbeitet veränderliche Gruppen- und
|
||||
|
||||
@@ -96,18 +96,19 @@ erneut direkte Schreibmethoden erhalten.
|
||||
|
||||
## Migrationen
|
||||
|
||||
1. Bestehende lokale Datenbank sichern: `npm run db:backup`.
|
||||
2. Schema unter `src/db/schema/` ändern.
|
||||
3. `npm run db:generate` ausführen.
|
||||
4. Das generierte SQL vollständig prüfen; es darf nur die beabsichtigte Änderung
|
||||
1. Schema unter `src/db/schema/` ändern.
|
||||
2. `npm run db:generate` ausführen.
|
||||
3. Das generierte SQL vollständig prüfen; es darf nur die beabsichtigte Änderung
|
||||
enthalten.
|
||||
5. Upgrade-/Erhaltungstest ergänzen, wenn Spalten oder Beziehungen geändert
|
||||
4. Upgrade-/Erhaltungstest ergänzen, wenn Spalten oder Beziehungen geändert
|
||||
werden.
|
||||
6. `npm test`, `npm run db:migrate` und
|
||||
5. `npm test`, `npm run db:migrate` und
|
||||
`npm run db:verify:circuit-schema` ausführen.
|
||||
|
||||
Bereits angewendete SQL-Migrationen dürfen nicht geändert oder neu sortiert
|
||||
werden. Korrekturen erfolgen immer über eine neue Migration.
|
||||
`0000` ist die saubere Baseline für leere Neuinstallationen. Vor-Baseline-
|
||||
Entwicklungsdatenbanken werden nicht aktualisiert. Nach Veröffentlichung der
|
||||
ersten Version dürfen angewendete SQL-Migrationen nicht geändert oder neu
|
||||
sortiert werden; Korrekturen erfolgen dann immer über eine neue Migration.
|
||||
|
||||
## Pull-Request-Handoff
|
||||
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
# Local DB Migration: Circuit-First Schema
|
||||
|
||||
This project uses SQLite at `data/leistungsbilanz.db` and Drizzle migrations in `src/db/migrations`.
|
||||
|
||||
This document is an upgrade runbook for databases created before the Circuit-First
|
||||
cutover. A clean installation only needs the normal `db:migrate` and schema
|
||||
verification steps documented in the main README.
|
||||
|
||||
## Safe command order
|
||||
|
||||
1. Backup local DB (required before schema/data migration)
|
||||
|
||||
```bash
|
||||
npm run db:backup
|
||||
```
|
||||
|
||||
The command uses SQLite's online backup API, so committed WAL changes are included
|
||||
even while the application is running. It opens the resulting standalone database
|
||||
and requires both `PRAGMA integrity_check` and `PRAGMA foreign_key_check` to pass.
|
||||
Database backups are operational recovery files and remain separate from the future
|
||||
user-visible project version history.
|
||||
|
||||
2. Apply pending schema migrations (includes `0008_circuit_first_model`, the additive
|
||||
`0009_project_device_circuit_fields` transition and the post-cutover
|
||||
`0011_project_device_canonical_fields` cleanup)
|
||||
|
||||
```bash
|
||||
npm run db:migrate
|
||||
```
|
||||
|
||||
3. Verify circuit-first tables exist
|
||||
|
||||
```bash
|
||||
npm run db:verify:circuit-schema
|
||||
```
|
||||
|
||||
4. Backfill default sections for existing `circuit_lists`
|
||||
|
||||
```bash
|
||||
npm run db:backfill:sections
|
||||
```
|
||||
|
||||
5. Run legacy consumer -> circuit/device-row migration explicitly
|
||||
|
||||
```bash
|
||||
npm run db:migrate:legacy-consumers
|
||||
```
|
||||
|
||||
## Verification SQL
|
||||
|
||||
Run these against `data/leistungsbilanz.db`:
|
||||
|
||||
```sql
|
||||
SELECT name
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
AND name IN (
|
||||
'circuit_sections',
|
||||
'circuits',
|
||||
'circuit_device_rows',
|
||||
'legacy_consumer_circuit_migrations',
|
||||
'legacy_consumer_migration_reports'
|
||||
)
|
||||
ORDER BY name;
|
||||
```
|
||||
|
||||
The verification command also checks the circuit-first project-device columns added by migration `0009`.
|
||||
|
||||
```sql
|
||||
SELECT circuit_list_id, key, prefix, sort_order
|
||||
FROM circuit_sections
|
||||
ORDER BY circuit_list_id, sort_order;
|
||||
```
|
||||
|
||||
```sql
|
||||
SELECT circuit_list_id, COUNT(*) AS circuits
|
||||
FROM circuits
|
||||
GROUP BY circuit_list_id;
|
||||
```
|
||||
|
||||
```sql
|
||||
SELECT c.circuit_list_id, COUNT(r.id) AS device_rows
|
||||
FROM circuits c
|
||||
LEFT JOIN circuit_device_rows r ON r.circuit_id = c.id
|
||||
GROUP BY c.circuit_list_id;
|
||||
```
|
||||
|
||||
## API verification
|
||||
|
||||
After the steps above:
|
||||
|
||||
`GET /api/projects/:projectId/circuit-lists/:circuitListId/tree`
|
||||
|
||||
Expected response shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"circuitListId": "string",
|
||||
"sections": [
|
||||
{
|
||||
"id": "string",
|
||||
"key": "lighting|single_phase|three_phase|unassigned|...",
|
||||
"displayName": "string",
|
||||
"prefix": "-1F|-2F|-3F|-UF|...",
|
||||
"sortOrder": 10,
|
||||
"circuits": [
|
||||
{
|
||||
"id": "string",
|
||||
"equipmentIdentifier": "-2F1",
|
||||
"displayName": "string",
|
||||
"sortOrder": 10,
|
||||
"isReserve": false,
|
||||
"circuitTotalPower": 1.23,
|
||||
"deviceRows": [
|
||||
{
|
||||
"id": "string",
|
||||
"displayName": "string",
|
||||
"quantity": 1,
|
||||
"powerPerUnit": 0.3,
|
||||
"simultaneityFactor": 1,
|
||||
"rowTotalPower": 0.3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If migrations were not applied, endpoint may return an empty fallback with a warning.
|
||||
|
||||
## Visual verification
|
||||
|
||||
Manual device rows and multi-device circuits can be created directly in the
|
||||
circuit-list editor. The former development scripts for direct database writes
|
||||
were removed because they bypassed project revisions and persistent undo/redo.
|
||||
@@ -84,7 +84,8 @@ Implemented named-snapshot foundation:
|
||||
stores the complete project state in one SQLite transaction
|
||||
- the versioned payload contains project settings, distribution boards,
|
||||
circuit lists, sections, circuits with device rows, project devices, floors
|
||||
and rooms while excluding global and upgrade-only data
|
||||
and rooms while excluding the global device catalog and other
|
||||
non-project-owned data
|
||||
- snapshot creation/listing does not create a project revision or affect
|
||||
undo/redo eligibility
|
||||
- public Create/List endpoints expose metadata but not the logical payload
|
||||
@@ -93,8 +94,6 @@ Implemented named-snapshot foundation:
|
||||
- restore records a new `restore` revision containing both the selected target
|
||||
state and the complete pre-restore inverse; undo/redo therefore remains
|
||||
available after application restarts
|
||||
- compatible upgrade-only Consumer links, mappings and reports are preserved
|
||||
separately and are never imported into the logical snapshot payload
|
||||
- arbitrary restore payloads are rejected by the generic user-command
|
||||
endpoint; only server-stored snapshots can initiate a forward restore
|
||||
- the project page provides an initially collapsed snapshot/timeline panel,
|
||||
|
||||
@@ -6,9 +6,10 @@ This is a conceptual model for Codex. It is not a final database schema, but the
|
||||
|
||||
A distribution board contains a circuit list.
|
||||
|
||||
A circuit list contains sections.
|
||||
A circuit list contains circuit groups. `CircuitSection` is the persisted name
|
||||
of that group aggregate.
|
||||
|
||||
A section contains circuits.
|
||||
A circuit group contains circuits of one category.
|
||||
|
||||
A circuit contains zero, one or multiple device rows.
|
||||
|
||||
@@ -60,29 +61,28 @@ User-facing fields:
|
||||
- `section`
|
||||
- `displayName` optional
|
||||
- `circuitTotalPower`
|
||||
- `protectionType`
|
||||
- `protectionRatedCurrent`
|
||||
- `protectionCharacteristic`
|
||||
- `protectionDevice`
|
||||
- `cableType`
|
||||
- `cableCrossSection`
|
||||
- `cableLength`
|
||||
- `rcdAssignment` optional
|
||||
- `terminalDesignation` optional
|
||||
- `voltage` optional
|
||||
- `voltage`, derived from project settings and phase category
|
||||
- `controlRequirement` optional, for example DALI or KNX
|
||||
- `status` optional
|
||||
- `isReserve` optional
|
||||
- `remark` optional
|
||||
|
||||
`protectionDevice` is the circuit-owned one-to-one protection configuration.
|
||||
It contains the device type, rated current and only the type-dependent values
|
||||
such as trip characteristic or residual current. Newly created circuits
|
||||
receive the category default defined in the protection catalog.
|
||||
|
||||
Circuit-level fields only:
|
||||
|
||||
- protection type
|
||||
- protection rated current
|
||||
- protection characteristic
|
||||
- protection device and all of its technical values
|
||||
- cable type
|
||||
- cable cross-section
|
||||
- cable length
|
||||
- RCD assignment
|
||||
- terminal designation
|
||||
- circuit total power
|
||||
|
||||
|
||||
@@ -342,9 +342,8 @@ Implemented foundation:
|
||||
- circuit-device-row transaction tests cover both successful commits and forced SQLite rollbacks
|
||||
- project-device row synchronization has real multi-row SQLite commit and rollback coverage
|
||||
- BMK swaps and section reorder have real SQLite commit and rollback coverage
|
||||
- the legacy consumer migration service depends only on narrow domain ports;
|
||||
all concrete repositories are composed at the script entry point
|
||||
- legacy circuit, row, mapping and report writes have real late-failure rollback coverage
|
||||
- the former Consumer migration service, mappings and reports were removed with
|
||||
the pre-release database cutover
|
||||
- circuit and device-row persistence value mapping is separated from the general repositories
|
||||
- obsolete direct Circuit, CircuitDeviceRow, CircuitList, DistributionBoard and
|
||||
Project repository writes are removed; integration fixtures live under
|
||||
@@ -362,8 +361,9 @@ Implemented foundation:
|
||||
direct global SQLite access
|
||||
- TypeScript maintenance and upgrade scripts have a dedicated no-emit
|
||||
typecheck, including all referenced application modules
|
||||
- the legacy consumer UI and application read/write endpoints are removed after verified data cutover
|
||||
- retained legacy rows are accessible only through explicit database upgrade tooling
|
||||
- the former Consumer UI, API, tables and upgrade tooling are removed
|
||||
- migration `0000` is the clean baseline for new installations; pre-baseline
|
||||
development databases are intentionally unsupported
|
||||
- project devices no longer persist duplicate legacy power, phase, cosPhi or remark fields
|
||||
|
||||
## Phase 12: Project Revisions and Persistent Undo / Redo
|
||||
@@ -559,11 +559,11 @@ PostgreSQL should be introduced when shared multi-user operation, background job
|
||||
|
||||
Goal:
|
||||
|
||||
After the legacy consumer path has been removed, provide one accurate entry point for developers, contributors and LLM-assisted work.
|
||||
With the former Consumer path removed, provide one accurate entry point for developers, contributors and LLM-assisted work.
|
||||
|
||||
Timing:
|
||||
|
||||
- perform the final rewrite after legacy cutover so the documentation describes only the supported architecture
|
||||
- keep the post-cleanup documentation limited to the supported architecture
|
||||
- update documentation incrementally during earlier phases, but do not present transitional paths as the long-term design
|
||||
|
||||
README target structure:
|
||||
@@ -618,7 +618,8 @@ Implemented handoff:
|
||||
|
||||
- README is a concise setup and project entry point without prototype status lists
|
||||
- `docs/README.md` separates current, operational, future, specification and archived material
|
||||
- current runtime, module boundaries and upgrade-only legacy data are documented explicitly
|
||||
- current runtime and module boundaries are documented without compatibility
|
||||
paths for unsupported pre-release data formats
|
||||
- contributor workflow covers clean databases, safe demo data, migrations, tests and commits
|
||||
- development Compose is clearly separated from the currently unsupported production deployment
|
||||
- `AGENTS.md` points LLM-assisted work to supported code paths and deferred architecture
|
||||
|
||||
@@ -14,11 +14,11 @@ requirements and intended sequencing, not proof of implementation.
|
||||
component model in phased work packages.
|
||||
- [x] Phase A: central protection catalog, validation, defaults and pure grouped
|
||||
BMK rules.
|
||||
- [x] Phase B1: additive relational schema, group backfill, component/protection
|
||||
tables and shared BMK uniqueness.
|
||||
- [x] Phase B2: snapshot/transfer schema upgrade and deterministic compatibility
|
||||
mapping.
|
||||
- [x] Phase C1: versioned new-board command with three groups and fixed header
|
||||
- [x] Phase B1: relational group, component and protection tables with shared
|
||||
BMK uniqueness.
|
||||
- [x] Phase B2: complete snapshot/transfer schema for the supported project
|
||||
state.
|
||||
- [x] Phase C1: new-board command with three groups and fixed header
|
||||
components.
|
||||
- [x] Phase C2a1: persistent insert/delete commands for mutable group
|
||||
protection and auxiliary components.
|
||||
@@ -52,22 +52,22 @@ requirements and intended sequencing, not proof of implementation.
|
||||
|
||||
## First Release Baseline
|
||||
|
||||
Complete this immediately before the first supported release, once the schema
|
||||
and initial feature set are frozen:
|
||||
The pre-release cleanup established the following compatibility boundary:
|
||||
|
||||
- Decide explicitly whether pre-release databases, portable JSON exports,
|
||||
logical snapshots and persisted command histories remain supported.
|
||||
- Create one clean baseline migration that builds the complete release schema
|
||||
on an empty database.
|
||||
- Replace the active development migration chain and Drizzle metadata only if
|
||||
no released installation depends on it.
|
||||
- Remove upgrade-only database, snapshot and command compatibility code only
|
||||
for formats that are explicitly declared unsupported.
|
||||
- Preserve the pre-release migration history in Git history or a release tag.
|
||||
- Verify installation, migrations, application startup and core workflows
|
||||
against a completely empty database.
|
||||
- Mark the resulting compatibility boundary with the first release tag.
|
||||
- Treat every migration published after that release as immutable.
|
||||
- [x] Pre-release databases, portable JSON exports, logical snapshots and
|
||||
persisted command histories are explicitly unsupported.
|
||||
- [x] Migration `0000` builds the complete supported schema on an empty
|
||||
database.
|
||||
- [x] The development migration chain and Drizzle metadata were replaced
|
||||
before any supported release depended on them.
|
||||
- [x] Upgrade-only database, snapshot and command compatibility paths were
|
||||
removed.
|
||||
- [x] Pre-cleanup development history remains available through Git history.
|
||||
- [x] Clean installation, migration, startup and core workflows were verified
|
||||
against an empty database.
|
||||
- [ ] Mark this compatibility boundary with the first supported release tag.
|
||||
- After that release, every published migration is immutable and later schema
|
||||
changes must use new additive migrations.
|
||||
|
||||
## Project History
|
||||
|
||||
@@ -109,9 +109,9 @@ and initial feature set are frozen:
|
||||
device rows with a valid phase are three-phase.
|
||||
- [x] Changing project voltage settings updates all project devices and circuits
|
||||
atomically in the same persistent Undo/Redo step.
|
||||
- [x] Database migration `0019` and snapshot schema version `5` normalize older
|
||||
stored values. Version-four and older imports remain supported and are
|
||||
normalized while being upgraded.
|
||||
- [x] Baseline migration `0000` and project-state schema version `1` store only
|
||||
the canonical derived values. Pre-release imports are intentionally
|
||||
unsupported.
|
||||
|
||||
## Circuit List Power Summary
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
|
||||
This document records the agreed and implemented behavior for
|
||||
distribution-board components, protected circuit groups and circuit
|
||||
protection devices. Delivery phases A through E are complete. Phase F retains
|
||||
the final GUI checklist; the current runtime paths are documented in
|
||||
`docs/current-architecture.md`.
|
||||
protection devices. Delivery phases A through F are complete. The current
|
||||
runtime paths are documented in `docs/current-architecture.md`.
|
||||
|
||||
Full rule-based protection and cable sizing is deliberately deferred. This
|
||||
phase establishes the structure and manually selected technical values that a
|
||||
@@ -34,7 +33,7 @@ later calculation and warning engine will evaluate.
|
||||
- channel-level relationships between actors and controlled circuits
|
||||
- PostgreSQL or multi-user operation
|
||||
|
||||
## Existing Model Audit
|
||||
## Implemented Model Boundary
|
||||
|
||||
### Keep
|
||||
|
||||
@@ -44,33 +43,28 @@ later calculation and warning engine will evaluate.
|
||||
- Circuit-level load, cable and control fields remain circuit-owned.
|
||||
- Existing stable UUIDs, project revisions, snapshots and persistent Undo/Redo
|
||||
remain authoritative.
|
||||
- `CircuitSection` remains the migration starting point for a protected circuit
|
||||
- `CircuitSection` is the persisted representation of a protected circuit
|
||||
group.
|
||||
- Grid projection remains separate from persisted domain structure.
|
||||
|
||||
### Change
|
||||
|
||||
- A `CircuitSection` gains an explicit category and group number and represents
|
||||
- A `CircuitSection` has an explicit category and group number and represents
|
||||
one protected circuit group.
|
||||
- Circuit protection changes from loosely related flat strings to an explicit
|
||||
- Circuit protection uses an explicit
|
||||
one-to-one protection-device model.
|
||||
- Distribution-board components become independent BMK-bearing list entries.
|
||||
- Equipment-identifier uniqueness covers circuits and all distribution-board
|
||||
components in the complete circuit list.
|
||||
- Moving a circuit to another group of the same category atomically assigns the
|
||||
next identifier of the target group.
|
||||
- New distribution boards create three default groups rather than treating the
|
||||
optional unassigned compatibility section as a normal fourth group.
|
||||
- New distribution boards create exactly three default groups.
|
||||
|
||||
### Remove Eventually
|
||||
### Remaining Model Cleanup
|
||||
|
||||
- hard-coded assumptions that each category has exactly one section
|
||||
- numbering that only understands `prefix + integer`
|
||||
- free-text protection-device combinations that cannot be validated
|
||||
- `rcdAssignment` as a substitute for an explicit group relationship
|
||||
|
||||
Removal happens only after migrated state, snapshots, commands and the editor
|
||||
use the replacement model.
|
||||
- The older optional `rcdAssignment` circuit field still exists separately
|
||||
from the explicit group relationship. Removing it requires its own reviewed
|
||||
domain and UI change; it is not a compatibility path for pre-release data.
|
||||
|
||||
## Target Domain Structure
|
||||
|
||||
@@ -464,7 +458,7 @@ Every command must:
|
||||
- commit the domain mutation, revision and history transition atomically
|
||||
- remain undoable and redoable after application restart
|
||||
|
||||
## Snapshot and Transfer Compatibility
|
||||
## Snapshot and Transfer Boundary
|
||||
|
||||
The supported logical project snapshot and portable JSON transfer must include:
|
||||
|
||||
@@ -473,28 +467,15 @@ The supported logical project snapshot and portable JSON transfer must include:
|
||||
- circuit protection-device state
|
||||
- all new component relationships and sort positions
|
||||
|
||||
Introducing the model requires a snapshot-schema version increase and explicit
|
||||
upgraders for supported older payloads.
|
||||
The clean pre-release baseline uses project-state schema version `1`. It
|
||||
contains the complete supported structure above and is shared by logical
|
||||
snapshots, restore, Undo/Redo and portable JSON transfer. Payloads from earlier
|
||||
development schemas are intentionally unsupported and rejected before any
|
||||
write.
|
||||
|
||||
Database migration rules:
|
||||
|
||||
- preserve all existing stable UUIDs
|
||||
- preserve existing circuit equipment identifiers during schema migration
|
||||
- map the existing lighting, single-phase and three-phase sections to group 1
|
||||
of the respective category
|
||||
- preserve an existing unassigned section only when required by existing data;
|
||||
it is not created for a new distribution board
|
||||
- preserve existing flat circuit protection values when creating the new
|
||||
one-to-one protection state
|
||||
- do not silently replace an existing protection selection with a new default
|
||||
- allow legacy identifiers to remain until an explicit, collision-safe
|
||||
conversion or renumber action is approved
|
||||
|
||||
New defaults apply to newly created circuits and boards, not as an implicit
|
||||
rewrite of existing planning data.
|
||||
|
||||
Historical persisted commands and snapshots that remain in the supported
|
||||
compatibility window must still deserialize and execute after the migration.
|
||||
Migration `0000` creates the complete relational model on an empty database.
|
||||
New defaults apply when boards, groups or circuits are created; they never
|
||||
silently overwrite a planner's later selection.
|
||||
|
||||
## Later Sizing and Warning Engine
|
||||
|
||||
@@ -544,7 +525,7 @@ Acceptance:
|
||||
- invalid rating/type/field combinations are rejected
|
||||
- defaults produce the exact agreed values
|
||||
|
||||
### B. Persistence and Compatibility Model
|
||||
### B. Persistence and Snapshot Model
|
||||
|
||||
Status: Complete.
|
||||
|
||||
@@ -552,44 +533,31 @@ Status: Complete.
|
||||
- add distribution-board component persistence
|
||||
- add one-to-one circuit protection persistence
|
||||
- implement shared BMK uniqueness
|
||||
- generate and inspect one additive migration
|
||||
- add snapshot/transfer schema upgrade
|
||||
- preserve existing identifiers and protection values
|
||||
- create the complete relational baseline schema
|
||||
- include the model in snapshots and portable transfers
|
||||
- preserve identifiers and protection values within supported project state
|
||||
|
||||
Implemented in B1:
|
||||
Implemented persistence:
|
||||
|
||||
- additive migration `0024` adds group identity to circuit sections and maps
|
||||
the three established sections to group 1 without changing UUIDs
|
||||
- baseline migration `0000` creates group identity, components and protection
|
||||
state directly for clean installations
|
||||
- separate component and circuit/component protection tables preserve the
|
||||
one-to-one ownership boundaries
|
||||
- a trigger-maintained circuit-list registry rejects normalized BMK collisions
|
||||
across circuits and distribution-board components
|
||||
- the existing flat circuit protection fields remain untouched until their
|
||||
deterministic snapshot/runtime transition is implemented
|
||||
- clean and populated pre-migration databases are covered by migration tests
|
||||
- newly created legacy-compatible board structures receive the same initial
|
||||
group identity while their persisted command format remains unchanged
|
||||
|
||||
Implemented in B2:
|
||||
|
||||
- snapshot schema version 7 includes group identity, distribution-board
|
||||
- snapshot schema version `1` includes group identity, distribution-board
|
||||
components and both one-to-one protection collections
|
||||
- capture, named and automatic snapshots, restore, Undo/Redo and portable JSON
|
||||
transfers preserve the complete new relational state
|
||||
- duplicate imports remap component UUIDs and both protection-owner references
|
||||
together with the existing project structure
|
||||
- versions 1 through 6 remain readable; version 6 maps the three established
|
||||
section keys to group 1 and leaves new collections empty
|
||||
- older flat circuit protection values remain present and are not guessed into
|
||||
strict new protection configurations
|
||||
- current snapshots reject invalid ownership, duplicate group numbers,
|
||||
- snapshots reject invalid ownership, duplicate group numbers,
|
||||
cross-entity BMKs and invalid protection configurations before persistence
|
||||
|
||||
Acceptance:
|
||||
|
||||
- a current database migrates without data loss
|
||||
- an empty database creates the complete schema
|
||||
- old supported snapshots and transfers upgrade deterministically
|
||||
- unsupported pre-release snapshots and transfers are rejected before writing
|
||||
- BMK collisions across circuits and components are rejected
|
||||
|
||||
### C. Persistent Commands and Board Defaults
|
||||
@@ -603,14 +571,13 @@ Status: Complete.
|
||||
|
||||
Implemented in C1:
|
||||
|
||||
- `distribution-board.insert` schema version 3 carries stable UUIDs for three
|
||||
- `distribution-board.insert` schema version `1` carries stable UUIDs for three
|
||||
initial groups, main switch `-Q0` and surge protective device `-FA`
|
||||
- its inverse verifies and removes the unchanged complete structure; Redo
|
||||
restores the same UUIDs
|
||||
- a late revision/history failure rolls back board, list, groups and components
|
||||
together
|
||||
- stored schema versions 1 and 2 retain four legacy sections and receive no
|
||||
invented components
|
||||
- unsupported command schema versions are rejected
|
||||
|
||||
Implemented in C2a1:
|
||||
|
||||
@@ -819,13 +786,13 @@ checklist are complete.
|
||||
Acceptance:
|
||||
|
||||
- documentation distinguishes implemented behavior from later sizing
|
||||
- clean install and migrated database both work
|
||||
- clean installation and baseline migration work
|
||||
- snapshots, JSON transfer and Undo/Redo cover all new state
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- Mixed legacy and grouped BMKs:
|
||||
preserve old identifiers and provide an explicit conversion path.
|
||||
- Incorrect implicit BMK changes:
|
||||
preserve identifiers during sorting and require explicit renumbering.
|
||||
- Cross-entity BMK collisions:
|
||||
enforce one transaction-level uniqueness boundary for the complete list.
|
||||
- Partial destructive writes:
|
||||
@@ -836,8 +803,9 @@ Acceptance:
|
||||
use one shared typed catalog for UI, API and domain validation.
|
||||
- Silent planning changes:
|
||||
apply defaults only on creation and keep later recommendations explicit.
|
||||
- Snapshot incompatibility:
|
||||
version payloads and test every supported upgrade path.
|
||||
- Future snapshot evolution:
|
||||
introduce a new explicit schema version and upgrader only after defining its
|
||||
supported compatibility window.
|
||||
|
||||
## Resolved Implementation Decisions
|
||||
|
||||
@@ -848,6 +816,6 @@ Acceptance:
|
||||
- structural component rows remain outside editable-cell filtering and
|
||||
spreadsheet navigation; circuit filtering continues to preserve complete
|
||||
circuit blocks
|
||||
- explicit UI for converting retained legacy identifiers to grouped numbering
|
||||
- whether fixed header components become editable beyond name and BMK in this
|
||||
phase
|
||||
- explicit group renumbering updates grouped identifiers collision-safely
|
||||
- fixed header components remain read-only; mutable group and auxiliary
|
||||
components use their dedicated modals
|
||||
|
||||
+3
-5
@@ -15,14 +15,12 @@
|
||||
"build:web": "next build",
|
||||
"typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json",
|
||||
"start": "node dist/server/index.js",
|
||||
"test": "tsx --test tests/protection-device.test.ts tests/distribution-board-component.test.ts tests/distribution-board-component-schema-migration.test.ts tests/circuit-group-numbering.test.ts tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.service.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-overview-import.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
||||
"test:watch": "tsx --watch --test tests/protection-device.test.ts tests/distribution-board-component.test.ts tests/distribution-board-component-schema-migration.test.ts tests/circuit-group-numbering.test.ts tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.service.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-overview-import.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
||||
"test": "node scripts/run-tests.mjs",
|
||||
"test:watch": "node scripts/run-tests.mjs --watch",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:backup": "tsx scripts/db-backup.ts",
|
||||
"db:verify:circuit-schema": "node scripts/db-verify-circuit-schema.js",
|
||||
"db:backfill:sections": "tsx scripts/db-backfill-sections.ts",
|
||||
"db:migrate:legacy-consumers": "tsx scripts/db-migrate-legacy-consumers.ts"
|
||||
"db:verify:circuit-schema": "node scripts/db-verify-circuit-schema.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { db } from "../src/db/client.js";
|
||||
import { CircuitListRepository } from "../src/db/repositories/circuit-list.repository.js";
|
||||
import { CircuitSectionRepository } from "../src/db/repositories/circuit-section.repository.js";
|
||||
import { ProjectRepository } from "../src/db/repositories/project.repository.js";
|
||||
|
||||
const projectRepository = new ProjectRepository(db);
|
||||
const circuitListRepository = new CircuitListRepository(db);
|
||||
const circuitSectionRepository = new CircuitSectionRepository(db);
|
||||
|
||||
async function run() {
|
||||
const projects = await projectRepository.list();
|
||||
let totalLists = 0;
|
||||
for (const project of projects) {
|
||||
const lists = await circuitListRepository.listByProject(project.id);
|
||||
for (const list of lists) {
|
||||
await circuitSectionRepository.createDefaults(list.id);
|
||||
totalLists += 1;
|
||||
}
|
||||
}
|
||||
console.log(`Section backfill done for ${totalLists} circuit list(s).`);
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error("Section backfill failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
import { db } from "../src/db/client.js";
|
||||
import { CircuitRepository } from "../src/db/repositories/circuit.repository.js";
|
||||
import { CircuitListRepository } from "../src/db/repositories/circuit-list.repository.js";
|
||||
import { CircuitSectionRepository } from "../src/db/repositories/circuit-section.repository.js";
|
||||
import { LegacyConsumerMigrationRepository } from "../src/db/repositories/legacy-consumer-migration.repository.js";
|
||||
import { ProjectRepository } from "../src/db/repositories/project.repository.js";
|
||||
import { RoomRepository } from "../src/db/repositories/room.repository.js";
|
||||
import { LegacyConsumerMigrationService } from "../src/domain/services/legacy-consumer-migration.service.js";
|
||||
|
||||
const projectRepository = new ProjectRepository(db);
|
||||
const circuitListRepository = new CircuitListRepository(db);
|
||||
const migrationRepository = new LegacyConsumerMigrationRepository(db);
|
||||
const migrationService = new LegacyConsumerMigrationService({
|
||||
circuitListReader: circuitListRepository,
|
||||
sectionStore: new CircuitSectionRepository(db),
|
||||
circuitReader: new CircuitRepository(db),
|
||||
roomReader: new RoomRepository(db),
|
||||
migrationStore: migrationRepository,
|
||||
});
|
||||
|
||||
async function run() {
|
||||
const projects = await projectRepository.list();
|
||||
const reports = [];
|
||||
|
||||
for (const project of projects) {
|
||||
const lists = await circuitListRepository.listByProject(project.id);
|
||||
for (const list of lists) {
|
||||
const report = await migrationService.migrateCircuitList(project.id, list.id);
|
||||
reports.push({
|
||||
projectId: project.id,
|
||||
circuitListId: list.id,
|
||||
legacyConsumerCount: report.legacyConsumerCount,
|
||||
createdCircuitCount: report.createdCircuitCount,
|
||||
createdDeviceRowCount: report.createdDeviceRowCount,
|
||||
generatedIdentifiers: report.generatedIdentifiers.length,
|
||||
unassignedRows: report.unassignedRows.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const unmigratedConsumers =
|
||||
await migrationRepository.listUnmigratedConsumers();
|
||||
if (unmigratedConsumers.length > 0) {
|
||||
const withoutCircuitList = unmigratedConsumers.filter(
|
||||
(consumer) => !consumer.circuitListId
|
||||
).length;
|
||||
throw new Error(
|
||||
`Cutover verification failed: ${unmigratedConsumers.length} legacy consumer(s) remain unmigrated` +
|
||||
` (${withoutCircuitList} without a circuit list).`
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Legacy consumer migration summary:");
|
||||
console.table(reports);
|
||||
console.log("Cutover verification passed: all legacy consumers are mapped.");
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error("Legacy consumer migration failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -12,8 +12,6 @@ const requiredTables = [
|
||||
"circuit_protection_devices",
|
||||
"distribution_board_components",
|
||||
"distribution_board_component_protection_devices",
|
||||
"legacy_consumer_circuit_migrations",
|
||||
"legacy_consumer_migration_reports",
|
||||
];
|
||||
|
||||
const tablePlaceholders = requiredTables.map(() => "?").join(", ");
|
||||
@@ -57,6 +55,14 @@ const circuitColumns = new Set(
|
||||
db.prepare("PRAGMA table_info(circuits)").all().map((column) => column.name)
|
||||
);
|
||||
const missingCircuitColumns = requiredCircuitColumns.filter((name) => !circuitColumns.has(name));
|
||||
const removedCircuitColumns = [
|
||||
"protection_type",
|
||||
"protection_rated_current",
|
||||
"protection_characteristic",
|
||||
];
|
||||
const remainingRemovedCircuitColumns = removedCircuitColumns.filter((name) =>
|
||||
circuitColumns.has(name)
|
||||
);
|
||||
const requiredCircuitSectionColumns = ["category", "group_number"];
|
||||
const circuitSectionColumns = new Set(
|
||||
db
|
||||
@@ -164,6 +170,14 @@ if (missingCircuitColumns.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (remainingRemovedCircuitColumns.length > 0) {
|
||||
console.error(
|
||||
"Transitional circuit protection columns still present:",
|
||||
remainingRemovedCircuitColumns.join(", ")
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (missingCircuitSectionColumns.length > 0) {
|
||||
console.error(
|
||||
"Missing circuit-section columns:",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { readdirSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
|
||||
const testFiles = readdirSync(path.resolve("tests"), {
|
||||
withFileTypes: true,
|
||||
})
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".test.ts"))
|
||||
.map((entry) => path.join("tests", entry.name))
|
||||
.sort();
|
||||
|
||||
if (testFiles.length === 0) {
|
||||
throw new Error("No TypeScript test files found.");
|
||||
}
|
||||
|
||||
const watch = process.argv.includes("--watch");
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
"tsx",
|
||||
"--test",
|
||||
"--test-concurrency=1",
|
||||
...(watch ? ["--watch"] : []),
|
||||
...testFiles,
|
||||
],
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
|
||||
process.exit(result.status ?? 1);
|
||||
+14
-29
@@ -305,35 +305,6 @@ a.kpi:hover {
|
||||
min-width: 8rem;
|
||||
}
|
||||
|
||||
.circuit-tree-table .section-row td {
|
||||
background: #e9eef8;
|
||||
border-top: 2px solid #c6d3ea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.circuit-tree-table .summary-row td {
|
||||
background: #f5f8fd;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.circuit-tree-table .device-row td {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.circuit-tree-table .reserve-row td {
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.circuit-tree-table .placeholder-row td {
|
||||
background: #f7f7f7;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.circuit-tree-table .indented-cell {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.tree-editor-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1052,6 +1023,20 @@ a.kpi:hover {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.tree-grid .cell-protection-trigger {
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition:
|
||||
background-color 0.12s ease,
|
||||
box-shadow 0.12s ease;
|
||||
}
|
||||
|
||||
.tree-grid .cell-protection-trigger:hover {
|
||||
background: #eaf6f0;
|
||||
box-shadow: inset 0 0 0 1px var(--color-signal);
|
||||
}
|
||||
|
||||
.tree-grid .device-drag-handle {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
@@ -15,12 +15,6 @@ export default function CircuitTreeEditPage() {
|
||||
<p className="text-secondary mb-0">Stromkreise und zugeordnete Geräte bearbeiten.</p>
|
||||
</div>
|
||||
<div className="d-flex gap-2">
|
||||
<Link
|
||||
href={`/projects/${params.projectId}/circuit-lists/${params.circuitListId}/tree`}
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
>
|
||||
Strukturvorschau
|
||||
</Link>
|
||||
<Link href={`/projects/${params.projectId}`} className="btn btn-outline-secondary btn-sm">
|
||||
Zum Projekt
|
||||
</Link>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { CircuitTreePreview } from "../../../../../../frontend/components/circuit-tree-preview";
|
||||
|
||||
export default function CircuitTreePreviewPage() {
|
||||
const params = useParams<{ projectId: string; circuitListId: string }>();
|
||||
const projectId = params.projectId;
|
||||
const circuitListId = params.circuitListId;
|
||||
|
||||
return (
|
||||
<main className="container py-4">
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h1 className="h4 mb-1">Vorschau der Stromkreisstruktur</h1>
|
||||
<p className="text-secondary mb-0">
|
||||
Schreibgeschützte Vorschau der Bereiche, Stromkreise und Gerätezeilen.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href={`/projects/${projectId}/circuit-lists/${circuitListId}/tree-edit`}
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
>
|
||||
Editor öffnen
|
||||
</Link>
|
||||
<Link
|
||||
href={`/projects/${projectId}`}
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
>
|
||||
Zum Projekt
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<CircuitTreePreview projectId={projectId} circuitListId={circuitListId} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -5,12 +5,16 @@ import { useParams } from "next/navigation";
|
||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
copyGlobalDeviceToProject,
|
||||
copyDistributionBoard,
|
||||
copyProjectDeviceToGlobal,
|
||||
createDistributionBoard,
|
||||
createFloor,
|
||||
createProjectDevice,
|
||||
createRoom,
|
||||
deleteFloor,
|
||||
deleteProjectDevice,
|
||||
deleteRoom,
|
||||
deleteDistributionBoard,
|
||||
disconnectProjectDeviceRows,
|
||||
exportProjectTransfer,
|
||||
getProjectDeviceSyncPreview,
|
||||
@@ -24,8 +28,10 @@ import {
|
||||
importProjectTransfer,
|
||||
synchronizeProjectDeviceRows,
|
||||
updateDistributionBoard,
|
||||
updateFloor,
|
||||
updateProjectDevice,
|
||||
updateProjectSettings,
|
||||
updateRoom,
|
||||
} from "../../../frontend/utils/api";
|
||||
import type {
|
||||
CircuitListDto,
|
||||
@@ -91,10 +97,13 @@ export default function ProjectDetailPage() {
|
||||
editingBoardSimultaneityFactor,
|
||||
setEditingBoardSimultaneityFactor,
|
||||
] = useState("1");
|
||||
const [editingBoardCopyName, setEditingBoardCopyName] = useState("");
|
||||
const [floorName, setFloorName] = useState("");
|
||||
const [editingFloor, setEditingFloor] = useState<FloorDto | null>(null);
|
||||
const [roomNumber, setRoomNumber] = useState("");
|
||||
const [roomName, setRoomName] = useState("");
|
||||
const [roomFloorId, setRoomFloorId] = useState("");
|
||||
const [editingRoom, setEditingRoom] = useState<RoomDto | null>(null);
|
||||
const [isProjectSettingsOpen, setIsProjectSettingsOpen] = useState(false);
|
||||
const [structureModal, setStructureModal] = useState<
|
||||
"board" | "floor" | "room" | null
|
||||
@@ -236,7 +245,7 @@ export default function ProjectDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateFloor(event: FormEvent<HTMLFormElement>) {
|
||||
async function handleSaveFloor(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!projectId || !project || !floorName.trim()) {
|
||||
return;
|
||||
@@ -244,23 +253,37 @@ export default function ProjectDetailPage() {
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await createFloor(
|
||||
const result = editingFloor
|
||||
? await updateFloor(
|
||||
projectId,
|
||||
editingFloor.id,
|
||||
{ name: floorName.trim() },
|
||||
project.currentRevision
|
||||
)
|
||||
: await createFloor(
|
||||
projectId,
|
||||
{ name: floorName.trim() },
|
||||
project.currentRevision
|
||||
);
|
||||
setFloors((current) => [...current, result.floor]);
|
||||
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 erstellt werden.");
|
||||
setError(err instanceof Error ? err.message : "Etage konnte nicht gespeichert werden.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateRoom(event: FormEvent<HTMLFormElement>) {
|
||||
async function handleSaveRoom(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (
|
||||
!projectId ||
|
||||
@@ -273,23 +296,34 @@ export default function ProjectDetailPage() {
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await createRoom(
|
||||
projectId,
|
||||
{
|
||||
const input = {
|
||||
floorId: roomFloorId || undefined,
|
||||
roomNumber: roomNumber.trim(),
|
||||
roomName: roomName.trim(),
|
||||
},
|
||||
};
|
||||
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]
|
||||
);
|
||||
setRooms((current) => [...current, result.room]);
|
||||
applyProjectRevision(result.history.currentRevision);
|
||||
setRoomNumber("");
|
||||
setRoomName("");
|
||||
setRoomFloorId("");
|
||||
setEditingRoom(null);
|
||||
setStructureModal(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Raum konnte nicht erstellt werden.");
|
||||
setError(err instanceof Error ? err.message : "Raum konnte nicht gespeichert werden.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -405,6 +439,7 @@ export default function ProjectDetailPage() {
|
||||
setEditingBoardSimultaneityFactor(
|
||||
String(board.simultaneityFactor)
|
||||
);
|
||||
setEditingBoardCopyName(`${board.name} Kopie`);
|
||||
}
|
||||
|
||||
function openBoardCreator() {
|
||||
@@ -463,6 +498,156 @@ export default function ProjectDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateFloor() {
|
||||
setEditingFloor(null);
|
||||
setFloorName("");
|
||||
setStructureModal("floor");
|
||||
}
|
||||
|
||||
function openEditFloor(floor: FloorDto) {
|
||||
setEditingFloor(floor);
|
||||
setFloorName(floor.name);
|
||||
setStructureModal("floor");
|
||||
}
|
||||
|
||||
function openCreateRoom() {
|
||||
setEditingRoom(null);
|
||||
setRoomNumber("");
|
||||
setRoomName("");
|
||||
setRoomFloorId("");
|
||||
setStructureModal("room");
|
||||
}
|
||||
|
||||
function openEditRoom(room: RoomDto) {
|
||||
setEditingRoom(room);
|
||||
setRoomNumber(room.roomNumber);
|
||||
setRoomName(room.roomName);
|
||||
setRoomFloorId(room.floorId ?? "");
|
||||
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);
|
||||
setRoomNumber("");
|
||||
setRoomName("");
|
||||
setRoomFloorId("");
|
||||
setStructureModal(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Raum konnte nicht gelöscht werden.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCopyBoard() {
|
||||
if (
|
||||
!projectId ||
|
||||
!project ||
|
||||
!editingBoard ||
|
||||
!editingBoardCopyName.trim()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await copyDistributionBoard(
|
||||
projectId,
|
||||
editingBoard.id,
|
||||
editingBoardCopyName.trim(),
|
||||
project.currentRevision
|
||||
);
|
||||
setBoards((current) => [
|
||||
...current,
|
||||
result.distributionBoard,
|
||||
]);
|
||||
setCircuitLists((current) => [
|
||||
...current,
|
||||
result.circuitList,
|
||||
]);
|
||||
applyProjectRevision(result.history.currentRevision);
|
||||
setEditingBoard(null);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "Verteilung konnte nicht kopiert werden."
|
||||
);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteBoard() {
|
||||
if (!projectId || !project || !editingBoard) {
|
||||
return;
|
||||
}
|
||||
const confirmed = window.confirm(
|
||||
`Verteilung „${editingBoard.name}“ wirklich löschen?\n\n` +
|
||||
"Dabei werden die Stromkreisliste, alle Gruppen, Stromkreise, Gerätezeilen, Schutzgeräte und weiteren Verteilergeräte entfernt. " +
|
||||
"Der Vorgang kann anschließend über die projektweite Historie rückgängig gemacht werden."
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await deleteDistributionBoard(
|
||||
projectId,
|
||||
editingBoard.id,
|
||||
project.currentRevision
|
||||
);
|
||||
setBoards((current) =>
|
||||
current.filter((board) => board.id !== result.distributionBoardId)
|
||||
);
|
||||
setCircuitLists((current) =>
|
||||
current.filter(
|
||||
(circuitList) =>
|
||||
circuitList.distributionBoardId !==
|
||||
result.distributionBoardId
|
||||
)
|
||||
);
|
||||
applyProjectRevision(result.history.currentRevision);
|
||||
setEditingBoard(null);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "Verteilung konnte nicht gelöscht werden."
|
||||
);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExportProject() {
|
||||
if (!project) {
|
||||
return;
|
||||
@@ -821,7 +1006,7 @@ export default function ProjectDetailPage() {
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
onClick={() => setStructureModal("floor")}
|
||||
onClick={openCreateFloor}
|
||||
type="button"
|
||||
>
|
||||
Hinzufügen
|
||||
@@ -829,8 +1014,11 @@ export default function ProjectDetailPage() {
|
||||
</div>
|
||||
<ul className="list-group list-group-flush">
|
||||
{floors.map((floor) => (
|
||||
<li className="list-group-item" key={floor.id}>
|
||||
{floor.name}
|
||||
<li className="list-group-item d-flex justify-content-between align-items-center gap-2" key={floor.id}>
|
||||
<span>{floor.name}</span>
|
||||
<button className="btn btn-sm btn-outline-secondary" onClick={() => openEditFloor(floor)} type="button">
|
||||
Bearbeiten
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{!floors.length ? (
|
||||
@@ -849,7 +1037,7 @@ export default function ProjectDetailPage() {
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
onClick={() => setStructureModal("room")}
|
||||
onClick={openCreateRoom}
|
||||
type="button"
|
||||
>
|
||||
Hinzufügen
|
||||
@@ -862,6 +1050,7 @@ export default function ProjectDetailPage() {
|
||||
<th>Raumnummer</th>
|
||||
<th>Raumname</th>
|
||||
<th>Etage</th>
|
||||
<th className="text-end">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -870,11 +1059,16 @@ export default function ProjectDetailPage() {
|
||||
<td>{room.roomNumber}</td>
|
||||
<td>{room.roomName}</td>
|
||||
<td>{room.floorId ? floorById.get(room.floorId)?.name ?? "-" : "-"}</td>
|
||||
<td className="text-end">
|
||||
<button className="btn btn-sm btn-outline-secondary" onClick={() => openEditRoom(room)} type="button">
|
||||
Bearbeiten
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!rooms.length ? (
|
||||
<tr>
|
||||
<td colSpan={3} className="text-center text-secondary py-4">
|
||||
<td colSpan={4} className="text-center text-secondary py-4">
|
||||
Noch keine Räume vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1111,7 +1305,7 @@ export default function ProjectDetailPage() {
|
||||
|
||||
{structureModal === "board" ? (
|
||||
<FormModal
|
||||
description="Eine Stromkreisliste mit den vier Standardbereichen wird automatisch angelegt."
|
||||
description="Eine Stromkreisliste mit den drei Standardgruppen wird automatisch angelegt."
|
||||
isSaving={isSaving}
|
||||
onClose={() => setStructureModal(null)}
|
||||
onSubmit={handleCreateBoard}
|
||||
@@ -1271,17 +1465,89 @@ export default function ProjectDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-4" />
|
||||
<section aria-labelledby="copy-distribution-board-heading">
|
||||
<h3
|
||||
className="h6"
|
||||
id="copy-distribution-board-heading"
|
||||
>
|
||||
Verteilung kopieren
|
||||
</h3>
|
||||
<p className="text-secondary small">
|
||||
Erstellt eine vollständige Kopie des zuletzt gespeicherten
|
||||
Stands einschließlich Gruppen, Stromkreisen, Gerätezeilen und
|
||||
Schutzgeräten. Noch nicht gespeicherte Änderungen oben werden
|
||||
nicht übernommen.
|
||||
</p>
|
||||
<div className="row g-2 align-items-end">
|
||||
<div className="col-12 col-md">
|
||||
<label
|
||||
className="form-label"
|
||||
htmlFor="copy-distribution-board-name"
|
||||
>
|
||||
Bezeichnung der Kopie
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="copy-distribution-board-name"
|
||||
maxLength={200}
|
||||
onChange={(event) =>
|
||||
setEditingBoardCopyName(event.target.value)
|
||||
}
|
||||
value={editingBoardCopyName}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-auto">
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
disabled={isSaving || !editingBoardCopyName.trim()}
|
||||
onClick={() => void handleCopyBoard()}
|
||||
type="button"
|
||||
>
|
||||
Verteilung kopieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<hr className="my-4" />
|
||||
<section
|
||||
aria-labelledby="delete-distribution-board-heading"
|
||||
className="border border-danger-subtle rounded p-3"
|
||||
>
|
||||
<h3
|
||||
className="h6 text-danger"
|
||||
id="delete-distribution-board-heading"
|
||||
>
|
||||
Gefahrenbereich
|
||||
</h3>
|
||||
<p className="text-secondary small mb-3">
|
||||
Entfernt die vollständige Verteilung. Die Aktion wird erst
|
||||
nach einer ausdrücklichen Warnung ausgeführt und kann über die
|
||||
projektweite Historie rückgängig gemacht werden.
|
||||
</p>
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
disabled={isSaving}
|
||||
onClick={() => void handleDeleteBoard()}
|
||||
type="button"
|
||||
>
|
||||
Verteilung löschen
|
||||
</button>
|
||||
</section>
|
||||
</FormModal>
|
||||
) : null}
|
||||
|
||||
{structureModal === "floor" ? (
|
||||
<FormModal
|
||||
isSaving={isSaving}
|
||||
onClose={() => setStructureModal(null)}
|
||||
onSubmit={handleCreateFloor}
|
||||
onClose={() => {
|
||||
setStructureModal(null);
|
||||
setEditingFloor(null);
|
||||
}}
|
||||
onSubmit={handleSaveFloor}
|
||||
submitDisabled={!floorName.trim()}
|
||||
submitLabel="Etage hinzufügen"
|
||||
title="Etage hinzufügen"
|
||||
submitLabel={editingFloor ? "Änderungen speichern" : "Etage hinzufügen"}
|
||||
title={editingFloor ? "Etage bearbeiten" : "Etage hinzufügen"}
|
||||
>
|
||||
<label className="form-label" htmlFor="floor-name">
|
||||
Bezeichnung
|
||||
@@ -1295,17 +1561,30 @@ export default function ProjectDetailPage() {
|
||||
required
|
||||
value={floorName}
|
||||
/>
|
||||
{editingFloor ? (
|
||||
<div className="border-top mt-4 pt-3">
|
||||
<p className="text-secondary small mb-2">
|
||||
Etagen mit zugeordneten Räumen oder Verteilungen können nicht gelöscht werden.
|
||||
</p>
|
||||
<button className="btn btn-outline-danger" disabled={isSaving} onClick={() => void handleDeleteFloor()} type="button">
|
||||
Etage löschen
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</FormModal>
|
||||
) : null}
|
||||
|
||||
{structureModal === "room" ? (
|
||||
<FormModal
|
||||
isSaving={isSaving}
|
||||
onClose={() => setStructureModal(null)}
|
||||
onSubmit={handleCreateRoom}
|
||||
onClose={() => {
|
||||
setStructureModal(null);
|
||||
setEditingRoom(null);
|
||||
}}
|
||||
onSubmit={handleSaveRoom}
|
||||
submitDisabled={!roomNumber.trim() || !roomName.trim()}
|
||||
submitLabel="Raum anlegen"
|
||||
title="Raum hinzufügen"
|
||||
submitLabel={editingRoom ? "Änderungen speichern" : "Raum anlegen"}
|
||||
title={editingRoom ? "Raum bearbeiten" : "Raum hinzufügen"}
|
||||
>
|
||||
<div className="row g-3">
|
||||
<div className="col-12 col-md-4">
|
||||
@@ -1352,6 +1631,16 @@ export default function ProjectDetailPage() {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{editingRoom ? (
|
||||
<div className="border-top mt-4 pt-3">
|
||||
<p className="text-secondary small mb-2">
|
||||
In Stromkreisen verwendete Räume können nicht gelöscht werden.
|
||||
</p>
|
||||
<button className="btn btn-outline-danger" disabled={isSaving} onClick={() => void handleDeleteRoom()} type="button">
|
||||
Raum löschen
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</FormModal>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
CREATE TABLE `consumers` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`distribution_board_id` text,
|
||||
`name` text NOT NULL,
|
||||
`category` text,
|
||||
`quantity` integer NOT NULL,
|
||||
`installed_power_per_unit_kw` real NOT NULL,
|
||||
`demand_factor` real NOT NULL,
|
||||
`voltage_v` real,
|
||||
`phase_count` integer,
|
||||
`power_factor` real,
|
||||
`note` text,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`distribution_board_id`) REFERENCES `distribution_boards`(`id`) ON UPDATE no action ON DELETE set null
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `distribution_boards` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `projects` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,300 @@
|
||||
CREATE TABLE `circuit_device_rows` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`circuit_id` text NOT NULL,
|
||||
`linked_project_device_id` text,
|
||||
`sort_order` integer DEFAULT 0 NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`display_name` text NOT NULL,
|
||||
`phase_type` text,
|
||||
`connection_kind` text,
|
||||
`cost_group` text,
|
||||
`category` text,
|
||||
`level` text,
|
||||
`room_id` text,
|
||||
`room_number_snapshot` text,
|
||||
`room_name_snapshot` text,
|
||||
`quantity` integer NOT NULL,
|
||||
`power_per_unit` real NOT NULL,
|
||||
`simultaneity_factor` real NOT NULL,
|
||||
`cos_phi` real,
|
||||
`remark` text,
|
||||
`overridden_fields` text,
|
||||
FOREIGN KEY (`circuit_id`) REFERENCES `circuits`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`linked_project_device_id`) REFERENCES `project_devices`(`id`) ON UPDATE no action ON DELETE set null,
|
||||
FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON UPDATE no action ON DELETE set null
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `circuit_list_equipment_identifiers` (
|
||||
`owner_type` text NOT NULL,
|
||||
`owner_id` text NOT NULL,
|
||||
`circuit_list_id` text NOT NULL,
|
||||
`equipment_identifier` text NOT NULL,
|
||||
FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_owner_unique` ON `circuit_list_equipment_identifiers` (`owner_type`,`owner_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_list_identifier_unique` ON `circuit_list_equipment_identifiers` (`circuit_list_id`,`equipment_identifier`);--> statement-breakpoint
|
||||
CREATE TABLE `circuit_lists` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`distribution_board_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`distribution_board_id`) REFERENCES `distribution_boards`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuit_lists_distribution_board_id_unique` ON `circuit_lists` (`distribution_board_id`);--> statement-breakpoint
|
||||
CREATE TABLE `circuit_protection_devices` (
|
||||
`circuit_id` text PRIMARY KEY NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`rated_current_a` real NOT NULL,
|
||||
`fuse_utilization_category` text,
|
||||
`trip_characteristic` text,
|
||||
`rcd_type` text,
|
||||
`rated_residual_current_ma` real,
|
||||
FOREIGN KEY (`circuit_id`) REFERENCES `circuits`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `circuit_sections` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`circuit_list_id` text NOT NULL,
|
||||
`key` text NOT NULL,
|
||||
`display_name` text NOT NULL,
|
||||
`prefix` text NOT NULL,
|
||||
`sort_order` integer DEFAULT 0 NOT NULL,
|
||||
`category` text,
|
||||
`group_number` integer,
|
||||
FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuit_sections_list_key_unique` ON `circuit_sections` (`circuit_list_id`,`key`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuit_sections_list_prefix_unique` ON `circuit_sections` (`circuit_list_id`,`prefix`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuit_sections_list_category_group_unique` ON `circuit_sections` (`circuit_list_id`,`category`,`group_number`);--> statement-breakpoint
|
||||
CREATE TABLE `circuits` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`circuit_list_id` text NOT NULL,
|
||||
`section_id` text NOT NULL,
|
||||
`equipment_identifier` text NOT NULL,
|
||||
`display_name` text,
|
||||
`sort_order` integer DEFAULT 0 NOT NULL,
|
||||
`cable_type` text,
|
||||
`cable_cross_section` text,
|
||||
`cable_length` real,
|
||||
`rcd_assignment` text,
|
||||
`terminal_designation` text,
|
||||
`voltage` real,
|
||||
`control_requirement` text,
|
||||
`status` text,
|
||||
`is_reserve` integer DEFAULT 0 NOT NULL,
|
||||
`remark` text,
|
||||
FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`section_id`) REFERENCES `circuit_sections`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuits_list_equipment_identifier_unique` ON `circuits` (`circuit_list_id`,`equipment_identifier`);--> statement-breakpoint
|
||||
CREATE TABLE `distribution_board_component_protection_devices` (
|
||||
`component_id` text PRIMARY KEY NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`rated_current_a` real NOT NULL,
|
||||
`fuse_utilization_category` text,
|
||||
`trip_characteristic` text,
|
||||
`rcd_type` text,
|
||||
`rated_residual_current_ma` real,
|
||||
FOREIGN KEY (`component_id`) REFERENCES `distribution_board_components`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `distribution_board_components` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`circuit_list_id` text NOT NULL,
|
||||
`section_id` text,
|
||||
`equipment_identifier` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`role` text NOT NULL,
|
||||
`placement` text NOT NULL,
|
||||
`sort_order` integer DEFAULT 0 NOT NULL,
|
||||
FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`section_id`) REFERENCES `circuit_sections`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `distribution_board_components_list_identifier_unique` ON `distribution_board_components` (`circuit_list_id`,`equipment_identifier`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `distribution_board_components_section_role_unique` ON `distribution_board_components` (`section_id`,`role`);--> statement-breakpoint
|
||||
CREATE TABLE `distribution_boards` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`floor_id` text,
|
||||
`supply_type` text,
|
||||
`simultaneity_factor` real DEFAULT 1 NOT NULL,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`floor_id`) REFERENCES `floors`(`id`) ON UPDATE no action ON DELETE set null
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `floors` (
|
||||
`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
|
||||
CREATE TABLE `global_devices` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`display_name` text NOT NULL,
|
||||
`category` text,
|
||||
`quantity` integer NOT NULL,
|
||||
`installed_power_per_unit_kw` real NOT NULL,
|
||||
`demand_factor` real NOT NULL,
|
||||
`voltage_v` real,
|
||||
`phase_count` integer,
|
||||
`power_factor` real,
|
||||
`note` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `project_change_sets` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_revision_id` text NOT NULL,
|
||||
`command_type` text NOT NULL,
|
||||
`payload_schema_version` integer NOT NULL,
|
||||
`forward_payload_json` text NOT NULL,
|
||||
`inverse_payload_json` text NOT NULL,
|
||||
FOREIGN KEY (`project_revision_id`) REFERENCES `project_revisions`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `project_change_sets_revision_unique` ON `project_change_sets` (`project_revision_id`);--> statement-breakpoint
|
||||
CREATE TABLE `project_devices` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`display_name` text NOT NULL,
|
||||
`phase_type` text DEFAULT 'single_phase' NOT NULL,
|
||||
`connection_kind` text,
|
||||
`cost_group` text,
|
||||
`category` text,
|
||||
`quantity` integer NOT NULL,
|
||||
`power_per_unit` real DEFAULT 0 NOT NULL,
|
||||
`simultaneity_factor` real DEFAULT 1 NOT NULL,
|
||||
`cos_phi` real,
|
||||
`remark` text,
|
||||
`voltage_v` real,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `project_history_stack_entries` (
|
||||
`change_set_id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`stack` text NOT NULL,
|
||||
`position` integer NOT NULL,
|
||||
FOREIGN KEY (`change_set_id`) REFERENCES `project_change_sets`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
CONSTRAINT "project_history_stack_entries_stack_check" CHECK("project_history_stack_entries"."stack" in ('undo', 'redo'))
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `project_history_stack_entries_position_unique` ON `project_history_stack_entries` (`project_id`,`stack`,`position`);--> statement-breakpoint
|
||||
CREATE TABLE `project_revisions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`revision_number` integer NOT NULL,
|
||||
`created_at_iso` text NOT NULL,
|
||||
`actor_id` text,
|
||||
`source` text NOT NULL,
|
||||
`description` text,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `project_revisions_project_number_unique` ON `project_revisions` (`project_id`,`revision_number`);--> statement-breakpoint
|
||||
CREATE TABLE `project_snapshots` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`source_revision` integer NOT NULL,
|
||||
`schema_version` integer NOT NULL,
|
||||
`kind` text DEFAULT 'named' NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`description` text,
|
||||
`payload_json` text NOT NULL,
|
||||
`payload_sha256` text NOT NULL,
|
||||
`created_at_iso` text NOT NULL,
|
||||
`created_by_actor_id` text,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `project_snapshots_project_created_idx` ON `project_snapshots` (`project_id`,`created_at_iso`);--> statement-breakpoint
|
||||
CREATE INDEX `project_snapshots_project_kind_revision_idx` ON `project_snapshots` (`project_id`,`kind`,`source_revision`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `project_snapshots_project_name_unique` ON `project_snapshots` (`project_id`,`name`);--> statement-breakpoint
|
||||
CREATE TABLE `projects` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`internal_project_number` text,
|
||||
`external_project_number` text,
|
||||
`building_owner` text,
|
||||
`description` text,
|
||||
`single_phase_voltage_v` integer DEFAULT 230 NOT NULL,
|
||||
`three_phase_voltage_v` integer DEFAULT 400 NOT NULL,
|
||||
`enabled_distribution_board_supply_types` text DEFAULT '["AV","SV","EV","USV","MSR","SiBe"]' NOT NULL,
|
||||
`current_revision` integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `rooms` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`floor_id` text,
|
||||
`room_number` text NOT NULL,
|
||||
`room_name` text NOT NULL,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`floor_id`) REFERENCES `floors`(`id`) ON UPDATE no action ON DELETE set null
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_normalized_unique`
|
||||
ON `circuit_list_equipment_identifiers` (`circuit_list_id`, lower(trim(`equipment_identifier`)));
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `distribution_board_components_fixed_role_unique`
|
||||
ON `distribution_board_components` (`circuit_list_id`, `role`)
|
||||
WHERE `role` IN ('main_switch', 'surge_protective_device');
|
||||
--> statement-breakpoint
|
||||
CREATE TRIGGER `circuits_equipment_identifier_insert`
|
||||
AFTER INSERT ON `circuits`
|
||||
BEGIN
|
||||
INSERT INTO `circuit_list_equipment_identifiers`
|
||||
(`owner_type`, `owner_id`, `circuit_list_id`, `equipment_identifier`)
|
||||
VALUES ('circuit', NEW.`id`, NEW.`circuit_list_id`, NEW.`equipment_identifier`);
|
||||
END;
|
||||
--> statement-breakpoint
|
||||
CREATE TRIGGER `circuits_equipment_identifier_update`
|
||||
AFTER UPDATE OF `circuit_list_id`, `equipment_identifier` ON `circuits`
|
||||
BEGIN
|
||||
UPDATE `circuit_list_equipment_identifiers`
|
||||
SET `circuit_list_id` = NEW.`circuit_list_id`,
|
||||
`equipment_identifier` = NEW.`equipment_identifier`
|
||||
WHERE `owner_type` = 'circuit' AND `owner_id` = OLD.`id`;
|
||||
END;
|
||||
--> statement-breakpoint
|
||||
CREATE TRIGGER `circuits_equipment_identifier_delete`
|
||||
AFTER DELETE ON `circuits`
|
||||
BEGIN
|
||||
DELETE FROM `circuit_list_equipment_identifiers`
|
||||
WHERE `owner_type` = 'circuit' AND `owner_id` = OLD.`id`;
|
||||
END;
|
||||
--> statement-breakpoint
|
||||
CREATE TRIGGER `distribution_board_components_equipment_identifier_insert`
|
||||
AFTER INSERT ON `distribution_board_components`
|
||||
BEGIN
|
||||
INSERT INTO `circuit_list_equipment_identifiers`
|
||||
(`owner_type`, `owner_id`, `circuit_list_id`, `equipment_identifier`)
|
||||
VALUES ('distribution_board_component', NEW.`id`, NEW.`circuit_list_id`, NEW.`equipment_identifier`);
|
||||
END;
|
||||
--> statement-breakpoint
|
||||
CREATE TRIGGER `distribution_board_components_equipment_identifier_update`
|
||||
AFTER UPDATE OF `circuit_list_id`, `equipment_identifier` ON `distribution_board_components`
|
||||
BEGIN
|
||||
UPDATE `circuit_list_equipment_identifiers`
|
||||
SET `circuit_list_id` = NEW.`circuit_list_id`,
|
||||
`equipment_identifier` = NEW.`equipment_identifier`
|
||||
WHERE `owner_type` = 'distribution_board_component' AND `owner_id` = OLD.`id`;
|
||||
END;
|
||||
--> statement-breakpoint
|
||||
CREATE TRIGGER `distribution_board_components_equipment_identifier_delete`
|
||||
AFTER DELETE ON `distribution_board_components`
|
||||
BEGIN
|
||||
DELETE FROM `circuit_list_equipment_identifiers`
|
||||
WHERE `owner_type` = 'distribution_board_component' AND `owner_id` = OLD.`id`;
|
||||
END;
|
||||
@@ -1,12 +0,0 @@
|
||||
CREATE TABLE `global_devices` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`category` text,
|
||||
`quantity` integer NOT NULL,
|
||||
`installed_power_per_unit_kw` real NOT NULL,
|
||||
`demand_factor` real NOT NULL,
|
||||
`voltage_v` real,
|
||||
`phase_count` integer,
|
||||
`power_factor` real,
|
||||
`note` text
|
||||
);
|
||||
@@ -1,3 +0,0 @@
|
||||
ALTER TABLE `projects` ADD COLUMN `single_phase_voltage_v` integer NOT NULL DEFAULT 230;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `projects` ADD COLUMN `three_phase_voltage_v` integer NOT NULL DEFAULT 400;
|
||||
@@ -1,19 +0,0 @@
|
||||
CREATE TABLE `floors` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`sort_order` integer NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `rooms` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`floor_id` text,
|
||||
`room_number` text NOT NULL,
|
||||
`room_name` text NOT NULL,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`floor_id`) REFERENCES `floors`(`id`) ON UPDATE no action ON DELETE set null
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `room_id` text REFERENCES `rooms`(`id`) ON UPDATE no action ON DELETE set null;
|
||||
@@ -1,44 +0,0 @@
|
||||
CREATE TABLE `circuit_lists` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`distribution_board_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`distribution_board_id`) REFERENCES `distribution_boards`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuit_lists_distribution_board_id_unique` ON `circuit_lists` (`distribution_board_id`);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `circuit_lists` (`id`, `project_id`, `distribution_board_id`, `name`)
|
||||
SELECT `id`, `project_id`, `id`, `name` || ' Stromkreisliste'
|
||||
FROM `distribution_boards`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `circuit_list_id` text REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE set null;
|
||||
--> statement-breakpoint
|
||||
UPDATE `consumers` SET `circuit_list_id` = `distribution_board_id` WHERE `distribution_board_id` IS NOT NULL;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `circuit_number` text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `description` text;
|
||||
--> statement-breakpoint
|
||||
UPDATE `consumers` SET `description` = `name` WHERE `description` IS NULL;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `device_type` text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `phase_type` text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `trade_or_cost_group` text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `group_name` text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `protection_type` text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `protection_rated_current` real;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `protection_characteristic` text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `cable_type` text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `cable_cross_section` text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `comment` text;
|
||||
@@ -1,14 +0,0 @@
|
||||
CREATE TABLE `project_devices` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`category` text,
|
||||
`quantity` integer NOT NULL,
|
||||
`installed_power_per_unit_kw` real NOT NULL,
|
||||
`demand_factor` real NOT NULL,
|
||||
`voltage_v` real,
|
||||
`phase_count` integer,
|
||||
`power_factor` real,
|
||||
`note` text,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
@@ -1,7 +0,0 @@
|
||||
ALTER TABLE `global_devices` ADD COLUMN `display_name` text;
|
||||
--> statement-breakpoint
|
||||
UPDATE `global_devices` SET `display_name` = `name` WHERE `display_name` IS NULL;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `project_devices` ADD COLUMN `display_name` text;
|
||||
--> statement-breakpoint
|
||||
UPDATE `project_devices` SET `display_name` = `name` WHERE `display_name` IS NULL;
|
||||
@@ -1,3 +0,0 @@
|
||||
ALTER TABLE `consumers` ADD COLUMN `project_device_id` text REFERENCES `project_devices`(`id`) ON UPDATE no action ON DELETE set null;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `consumers` ADD COLUMN `is_linked_to_device` integer NOT NULL DEFAULT 0;
|
||||
@@ -1,99 +0,0 @@
|
||||
CREATE TABLE `circuit_sections` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`circuit_list_id` text NOT NULL,
|
||||
`key` text NOT NULL,
|
||||
`display_name` text NOT NULL,
|
||||
`prefix` text NOT NULL,
|
||||
`sort_order` integer NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuit_sections_list_key_unique` ON `circuit_sections` (`circuit_list_id`,`key`);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuit_sections_list_prefix_unique` ON `circuit_sections` (`circuit_list_id`,`prefix`);
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE TABLE `circuits` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`circuit_list_id` text NOT NULL,
|
||||
`section_id` text NOT NULL,
|
||||
`equipment_identifier` text NOT NULL,
|
||||
`display_name` text,
|
||||
`sort_order` integer NOT NULL DEFAULT 0,
|
||||
`protection_type` text,
|
||||
`protection_rated_current` real,
|
||||
`protection_characteristic` text,
|
||||
`cable_type` text,
|
||||
`cable_cross_section` text,
|
||||
`cable_length` real,
|
||||
`rcd_assignment` text,
|
||||
`terminal_designation` text,
|
||||
`voltage` real,
|
||||
`status` text,
|
||||
`is_reserve` integer NOT NULL DEFAULT 0,
|
||||
`remark` text,
|
||||
FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`section_id`) REFERENCES `circuit_sections`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuits_list_equipment_identifier_unique` ON `circuits` (`circuit_list_id`,`equipment_identifier`);
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE TABLE `circuit_device_rows` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`circuit_id` text NOT NULL,
|
||||
`linked_project_device_id` text,
|
||||
`legacy_consumer_id` text,
|
||||
`sort_order` integer NOT NULL DEFAULT 0,
|
||||
`name` text NOT NULL,
|
||||
`display_name` text NOT NULL,
|
||||
`phase_type` text,
|
||||
`connection_kind` text,
|
||||
`cost_group` text,
|
||||
`category` text,
|
||||
`level` text,
|
||||
`room_id` text,
|
||||
`room_number_snapshot` text,
|
||||
`room_name_snapshot` text,
|
||||
`quantity` integer NOT NULL,
|
||||
`power_per_unit` real NOT NULL,
|
||||
`simultaneity_factor` real NOT NULL,
|
||||
`cos_phi` real,
|
||||
`remark` text,
|
||||
`overridden_fields` text,
|
||||
FOREIGN KEY (`circuit_id`) REFERENCES `circuits`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`linked_project_device_id`) REFERENCES `project_devices`(`id`) ON UPDATE no action ON DELETE set null,
|
||||
FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON UPDATE no action ON DELETE set null
|
||||
);
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE TABLE `legacy_consumer_circuit_migrations` (
|
||||
`consumer_id` text PRIMARY KEY NOT NULL,
|
||||
`circuit_id` text NOT NULL,
|
||||
`circuit_device_row_id` text NOT NULL,
|
||||
`circuit_list_id` text NOT NULL,
|
||||
`created_at_iso` text NOT NULL,
|
||||
FOREIGN KEY (`circuit_id`) REFERENCES `circuits`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`circuit_device_row_id`) REFERENCES `circuit_device_rows`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE TABLE `legacy_consumer_migration_reports` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`circuit_list_id` text NOT NULL,
|
||||
`legacy_consumer_count` integer NOT NULL,
|
||||
`created_circuit_count` integer NOT NULL,
|
||||
`created_device_row_count` integer NOT NULL,
|
||||
`duplicate_grouped_count` integer NOT NULL,
|
||||
`generated_identifier_count` integer NOT NULL,
|
||||
`unassigned_row_count` integer NOT NULL,
|
||||
`warnings_json` text NOT NULL,
|
||||
`generated_identifiers_json` text NOT NULL,
|
||||
`duplicate_groups_json` text NOT NULL,
|
||||
`created_at_iso` text NOT NULL,
|
||||
FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `legacy_consumer_migration_reports_list_unique` ON `legacy_consumer_migration_reports` (`circuit_list_id`);
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
ALTER TABLE `project_devices` ADD COLUMN `phase_type` text NOT NULL DEFAULT 'single_phase';
|
||||
--> statement-breakpoint
|
||||
UPDATE `project_devices`
|
||||
SET `phase_type` = CASE WHEN `phase_count` = 3 THEN 'three_phase' ELSE 'single_phase' END;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `project_devices` ADD COLUMN `connection_kind` text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `project_devices` ADD COLUMN `cost_group` text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `project_devices` ADD COLUMN `power_per_unit` real NOT NULL DEFAULT 0;
|
||||
--> statement-breakpoint
|
||||
UPDATE `project_devices` SET `power_per_unit` = `installed_power_per_unit_kw`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `project_devices` ADD COLUMN `simultaneity_factor` real NOT NULL DEFAULT 1;
|
||||
--> statement-breakpoint
|
||||
UPDATE `project_devices` SET `simultaneity_factor` = `demand_factor`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `project_devices` ADD COLUMN `cos_phi` real;
|
||||
--> statement-breakpoint
|
||||
UPDATE `project_devices` SET `cos_phi` = `power_factor`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `project_devices` ADD COLUMN `remark` text;
|
||||
--> statement-breakpoint
|
||||
UPDATE `project_devices` SET `remark` = `note`;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE `circuits` ADD COLUMN `control_requirement` text;
|
||||
@@ -1,9 +0,0 @@
|
||||
ALTER TABLE `project_devices` DROP COLUMN `installed_power_per_unit_kw`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `project_devices` DROP COLUMN `demand_factor`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `project_devices` DROP COLUMN `phase_count`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `project_devices` DROP COLUMN `power_factor`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `project_devices` DROP COLUMN `note`;
|
||||
@@ -1,24 +0,0 @@
|
||||
CREATE TABLE `project_change_sets` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_revision_id` text NOT NULL,
|
||||
`command_type` text NOT NULL,
|
||||
`payload_schema_version` integer NOT NULL,
|
||||
`forward_payload_json` text NOT NULL,
|
||||
`inverse_payload_json` text NOT NULL,
|
||||
FOREIGN KEY (`project_revision_id`) REFERENCES `project_revisions`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `project_change_sets_revision_unique` ON `project_change_sets` (`project_revision_id`);--> statement-breakpoint
|
||||
CREATE TABLE `project_revisions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`revision_number` integer NOT NULL,
|
||||
`created_at_iso` text NOT NULL,
|
||||
`actor_id` text,
|
||||
`source` text NOT NULL,
|
||||
`description` text,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `project_revisions_project_number_unique` ON `project_revisions` (`project_id`,`revision_number`);--> statement-breakpoint
|
||||
ALTER TABLE `projects` ADD `current_revision` integer DEFAULT 0 NOT NULL;
|
||||
@@ -1,11 +0,0 @@
|
||||
CREATE TABLE `project_history_stack_entries` (
|
||||
`change_set_id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`stack` text NOT NULL,
|
||||
`position` integer NOT NULL,
|
||||
FOREIGN KEY (`change_set_id`) REFERENCES `project_change_sets`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
CONSTRAINT "project_history_stack_entries_stack_check" CHECK("project_history_stack_entries"."stack" in ('undo', 'redo'))
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `project_history_stack_entries_position_unique` ON `project_history_stack_entries` (`project_id`,`stack`,`position`);
|
||||
@@ -1,16 +0,0 @@
|
||||
CREATE TABLE `project_snapshots` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`source_revision` integer NOT NULL,
|
||||
`schema_version` integer NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`description` text,
|
||||
`payload_json` text NOT NULL,
|
||||
`payload_sha256` text NOT NULL,
|
||||
`created_at_iso` text NOT NULL,
|
||||
`created_by_actor_id` text,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `project_snapshots_project_created_idx` ON `project_snapshots` (`project_id`,`created_at_iso`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `project_snapshots_project_name_unique` ON `project_snapshots` (`project_id`,`name`);
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE `project_snapshots` ADD `kind` text DEFAULT 'named' NOT NULL;--> statement-breakpoint
|
||||
CREATE INDEX `project_snapshots_project_kind_revision_idx` ON `project_snapshots` (`project_id`,`kind`,`source_revision`);
|
||||
@@ -1,4 +0,0 @@
|
||||
ALTER TABLE `projects` ADD `internal_project_number` text;--> statement-breakpoint
|
||||
ALTER TABLE `projects` ADD `external_project_number` text;--> statement-breakpoint
|
||||
ALTER TABLE `projects` ADD `building_owner` text;--> statement-breakpoint
|
||||
ALTER TABLE `projects` ADD `description` text;
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE `distribution_boards` ADD `floor_id` text REFERENCES floors(id);--> statement-breakpoint
|
||||
ALTER TABLE `distribution_boards` ADD `supply_type` text;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE `projects` ADD `enabled_distribution_board_supply_types` text DEFAULT '["AV","SV","EV","USV","MSR","SiBe"]' NOT NULL;
|
||||
@@ -1,48 +0,0 @@
|
||||
UPDATE `global_devices`
|
||||
SET `voltage_v` = NULL;
|
||||
--> statement-breakpoint
|
||||
UPDATE `project_devices` AS `pd`
|
||||
SET `voltage_v` = CASE
|
||||
WHEN `pd`.`phase_type` = 'three_phase' THEN `p`.`three_phase_voltage_v`
|
||||
ELSE `p`.`single_phase_voltage_v`
|
||||
END
|
||||
FROM `projects` AS `p`
|
||||
WHERE `p`.`id` = `pd`.`project_id`;
|
||||
--> statement-breakpoint
|
||||
WITH `derived_circuit_voltages` AS (
|
||||
SELECT
|
||||
`c`.`id` AS `circuit_id`,
|
||||
CASE
|
||||
WHEN `cs`.`key` = 'three_phase' THEN `p`.`three_phase_voltage_v`
|
||||
WHEN `cs`.`key` IN ('lighting', 'single_phase') THEN `p`.`single_phase_voltage_v`
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM `circuit_device_rows` AS `three_phase_row`
|
||||
WHERE `three_phase_row`.`circuit_id` = `c`.`id`
|
||||
AND `three_phase_row`.`phase_type` = 'three_phase'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM `circuit_device_rows` AS `other_row`
|
||||
WHERE `other_row`.`circuit_id` = `c`.`id`
|
||||
AND `other_row`.`phase_type` = 'single_phase'
|
||||
) THEN `p`.`three_phase_voltage_v`
|
||||
ELSE `p`.`single_phase_voltage_v`
|
||||
END AS `derived_voltage`
|
||||
FROM `circuits` AS `c`
|
||||
INNER JOIN `circuit_lists` AS `cl`
|
||||
ON `cl`.`id` = `c`.`circuit_list_id`
|
||||
INNER JOIN `projects` AS `p`
|
||||
ON `p`.`id` = `cl`.`project_id`
|
||||
INNER JOIN `circuit_sections` AS `cs`
|
||||
ON `cs`.`id` = `c`.`section_id`
|
||||
)
|
||||
UPDATE `circuits`
|
||||
SET `voltage` = (
|
||||
SELECT `derived_voltage`
|
||||
FROM `derived_circuit_voltages`
|
||||
WHERE `circuit_id` = `circuits`.`id`
|
||||
)
|
||||
WHERE `id` IN (
|
||||
SELECT `circuit_id`
|
||||
FROM `derived_circuit_voltages`
|
||||
);
|
||||
@@ -1,85 +0,0 @@
|
||||
UPDATE `project_devices`
|
||||
SET `phase_type` = CASE
|
||||
WHEN LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', ''))
|
||||
IN ('1ph', '1phase', '1phasig', 'einphasig', 'singlephase')
|
||||
THEN 'single_phase'
|
||||
WHEN LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', ''))
|
||||
IN ('3ph', '3phase', '3phasig', 'dreiphasig', 'threephase')
|
||||
THEN 'three_phase'
|
||||
ELSE `phase_type`
|
||||
END
|
||||
WHERE LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', ''))
|
||||
IN (
|
||||
'1ph',
|
||||
'1phase',
|
||||
'1phasig',
|
||||
'einphasig',
|
||||
'singlephase',
|
||||
'3ph',
|
||||
'3phase',
|
||||
'3phasig',
|
||||
'dreiphasig',
|
||||
'threephase'
|
||||
);
|
||||
--> statement-breakpoint
|
||||
UPDATE `circuit_device_rows`
|
||||
SET `phase_type` = CASE
|
||||
WHEN LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', ''))
|
||||
IN ('1ph', '1phase', '1phasig', 'einphasig', 'singlephase')
|
||||
THEN 'single_phase'
|
||||
WHEN LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', ''))
|
||||
IN ('3ph', '3phase', '3phasig', 'dreiphasig', 'threephase')
|
||||
THEN 'three_phase'
|
||||
ELSE `phase_type`
|
||||
END
|
||||
WHERE LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', ''))
|
||||
IN (
|
||||
'1ph',
|
||||
'1phase',
|
||||
'1phasig',
|
||||
'einphasig',
|
||||
'singlephase',
|
||||
'3ph',
|
||||
'3phase',
|
||||
'3phasig',
|
||||
'dreiphasig',
|
||||
'threephase'
|
||||
);
|
||||
--> statement-breakpoint
|
||||
WITH `derived_circuit_voltages` AS (
|
||||
SELECT
|
||||
`c`.`id` AS `circuit_id`,
|
||||
CASE
|
||||
WHEN `cs`.`key` = 'three_phase' THEN `p`.`three_phase_voltage_v`
|
||||
WHEN `cs`.`key` IN ('lighting', 'single_phase') THEN `p`.`single_phase_voltage_v`
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM `circuit_device_rows` AS `three_phase_row`
|
||||
WHERE `three_phase_row`.`circuit_id` = `c`.`id`
|
||||
AND `three_phase_row`.`phase_type` = 'three_phase'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM `circuit_device_rows` AS `single_phase_row`
|
||||
WHERE `single_phase_row`.`circuit_id` = `c`.`id`
|
||||
AND `single_phase_row`.`phase_type` = 'single_phase'
|
||||
) THEN `p`.`three_phase_voltage_v`
|
||||
ELSE `p`.`single_phase_voltage_v`
|
||||
END AS `derived_voltage`
|
||||
FROM `circuits` AS `c`
|
||||
INNER JOIN `circuit_lists` AS `cl`
|
||||
ON `cl`.`id` = `c`.`circuit_list_id`
|
||||
INNER JOIN `projects` AS `p`
|
||||
ON `p`.`id` = `cl`.`project_id`
|
||||
INNER JOIN `circuit_sections` AS `cs`
|
||||
ON `cs`.`id` = `c`.`section_id`
|
||||
)
|
||||
UPDATE `circuits`
|
||||
SET `voltage` = (
|
||||
SELECT `derived_voltage`
|
||||
FROM `derived_circuit_voltages`
|
||||
WHERE `circuit_id` = `circuits`.`id`
|
||||
)
|
||||
WHERE `id` IN (
|
||||
SELECT `circuit_id`
|
||||
FROM `derived_circuit_voltages`
|
||||
);
|
||||
@@ -1,75 +0,0 @@
|
||||
UPDATE `circuit_device_rows`
|
||||
SET `phase_type` = COALESCE(
|
||||
(
|
||||
SELECT `pd`.`phase_type`
|
||||
FROM `project_devices` AS `pd`
|
||||
WHERE `pd`.`id` = `circuit_device_rows`.`linked_project_device_id`
|
||||
AND `pd`.`phase_type` IN ('single_phase', 'three_phase')
|
||||
),
|
||||
(
|
||||
SELECT CASE
|
||||
WHEN `cs`.`key` = 'three_phase' THEN 'three_phase'
|
||||
WHEN `cs`.`key` IN ('lighting', 'single_phase') THEN 'single_phase'
|
||||
ELSE NULL
|
||||
END
|
||||
FROM `circuits` AS `c`
|
||||
INNER JOIN `circuit_sections` AS `cs`
|
||||
ON `cs`.`id` = `c`.`section_id`
|
||||
WHERE `c`.`id` = `circuit_device_rows`.`circuit_id`
|
||||
)
|
||||
)
|
||||
WHERE `phase_type` IS NULL
|
||||
AND (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM `project_devices` AS `pd`
|
||||
WHERE `pd`.`id` = `circuit_device_rows`.`linked_project_device_id`
|
||||
AND `pd`.`phase_type` IN ('single_phase', 'three_phase')
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM `circuits` AS `c`
|
||||
INNER JOIN `circuit_sections` AS `cs`
|
||||
ON `cs`.`id` = `c`.`section_id`
|
||||
WHERE `c`.`id` = `circuit_device_rows`.`circuit_id`
|
||||
AND `cs`.`key` IN ('lighting', 'single_phase', 'three_phase')
|
||||
)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
WITH `derived_circuit_voltages` AS (
|
||||
SELECT
|
||||
`c`.`id` AS `circuit_id`,
|
||||
CASE
|
||||
WHEN `cs`.`key` = 'three_phase' THEN `p`.`three_phase_voltage_v`
|
||||
WHEN `cs`.`key` IN ('lighting', 'single_phase') THEN `p`.`single_phase_voltage_v`
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM `circuit_device_rows` AS `three_phase_row`
|
||||
WHERE `three_phase_row`.`circuit_id` = `c`.`id`
|
||||
AND `three_phase_row`.`phase_type` = 'three_phase'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM `circuit_device_rows` AS `single_phase_row`
|
||||
WHERE `single_phase_row`.`circuit_id` = `c`.`id`
|
||||
AND `single_phase_row`.`phase_type` = 'single_phase'
|
||||
) THEN `p`.`three_phase_voltage_v`
|
||||
ELSE `p`.`single_phase_voltage_v`
|
||||
END AS `derived_voltage`
|
||||
FROM `circuits` AS `c`
|
||||
INNER JOIN `circuit_lists` AS `cl`
|
||||
ON `cl`.`id` = `c`.`circuit_list_id`
|
||||
INNER JOIN `projects` AS `p`
|
||||
ON `p`.`id` = `cl`.`project_id`
|
||||
INNER JOIN `circuit_sections` AS `cs`
|
||||
ON `cs`.`id` = `c`.`section_id`
|
||||
)
|
||||
UPDATE `circuits`
|
||||
SET `voltage` = (
|
||||
SELECT `derived_voltage`
|
||||
FROM `derived_circuit_voltages`
|
||||
WHERE `circuit_id` = `circuits`.`id`
|
||||
)
|
||||
WHERE `id` IN (
|
||||
SELECT `circuit_id`
|
||||
FROM `derived_circuit_voltages`
|
||||
);
|
||||
@@ -1,7 +0,0 @@
|
||||
UPDATE `circuit_device_rows`
|
||||
SET `phase_type` = 'single_phase'
|
||||
WHERE `phase_type` IS NULL;
|
||||
--> statement-breakpoint
|
||||
UPDATE `global_devices`
|
||||
SET `phase_count` = 1
|
||||
WHERE `phase_count` IS NULL OR `phase_count` NOT IN (1, 3);
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE `distribution_boards` ADD `simultaneity_factor` real DEFAULT 1 NOT NULL;
|
||||
@@ -1,150 +0,0 @@
|
||||
CREATE TABLE `circuit_list_equipment_identifiers` (
|
||||
`owner_type` text NOT NULL,
|
||||
`owner_id` text NOT NULL,
|
||||
`circuit_list_id` text NOT NULL,
|
||||
`equipment_identifier` text NOT NULL,
|
||||
FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_owner_unique` ON `circuit_list_equipment_identifiers` (`owner_type`,`owner_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_list_identifier_unique` ON `circuit_list_equipment_identifiers` (`circuit_list_id`,`equipment_identifier`);--> statement-breakpoint
|
||||
CREATE TABLE `circuit_protection_devices` (
|
||||
`circuit_id` text PRIMARY KEY NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`rated_current_a` real NOT NULL,
|
||||
`fuse_utilization_category` text,
|
||||
`trip_characteristic` text,
|
||||
`rcd_type` text,
|
||||
`rated_residual_current_ma` real,
|
||||
FOREIGN KEY (`circuit_id`) REFERENCES `circuits`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `distribution_board_component_protection_devices` (
|
||||
`component_id` text PRIMARY KEY NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`rated_current_a` real NOT NULL,
|
||||
`fuse_utilization_category` text,
|
||||
`trip_characteristic` text,
|
||||
`rcd_type` text,
|
||||
`rated_residual_current_ma` real,
|
||||
FOREIGN KEY (`component_id`) REFERENCES `distribution_board_components`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `distribution_board_components` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`circuit_list_id` text NOT NULL,
|
||||
`section_id` text,
|
||||
`equipment_identifier` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`role` text NOT NULL,
|
||||
`placement` text NOT NULL,
|
||||
`sort_order` integer DEFAULT 0 NOT NULL,
|
||||
FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`section_id`) REFERENCES `circuit_sections`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `distribution_board_components_list_identifier_unique` ON `distribution_board_components` (`circuit_list_id`,`equipment_identifier`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `distribution_board_components_section_role_unique` ON `distribution_board_components` (`section_id`,`role`);--> statement-breakpoint
|
||||
ALTER TABLE `circuit_sections` ADD `category` text;--> statement-breakpoint
|
||||
ALTER TABLE `circuit_sections` ADD `group_number` integer;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuit_sections_list_category_group_unique` ON `circuit_sections` (`circuit_list_id`,`category`,`group_number`);--> statement-breakpoint
|
||||
UPDATE `circuit_sections`
|
||||
SET
|
||||
`category` = CASE `key`
|
||||
WHEN 'lighting' THEN 'lighting'
|
||||
WHEN 'single_phase' THEN 'single_phase'
|
||||
WHEN 'three_phase' THEN 'three_phase'
|
||||
END,
|
||||
`group_number` = 1
|
||||
WHERE `key` IN ('lighting', 'single_phase', 'three_phase');--> statement-breakpoint
|
||||
INSERT INTO `circuit_list_equipment_identifiers` (
|
||||
`owner_type`,
|
||||
`owner_id`,
|
||||
`circuit_list_id`,
|
||||
`equipment_identifier`
|
||||
)
|
||||
SELECT
|
||||
'circuit',
|
||||
`id`,
|
||||
`circuit_list_id`,
|
||||
`equipment_identifier`
|
||||
FROM `circuits`;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_normalized_unique`
|
||||
ON `circuit_list_equipment_identifiers` (
|
||||
`circuit_list_id`,
|
||||
lower(trim(`equipment_identifier`))
|
||||
);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `distribution_board_components_fixed_role_unique`
|
||||
ON `distribution_board_components` (`circuit_list_id`, `role`)
|
||||
WHERE `role` IN ('main_switch', 'surge_protective_device');--> statement-breakpoint
|
||||
CREATE TRIGGER `circuits_equipment_identifier_insert`
|
||||
AFTER INSERT ON `circuits`
|
||||
BEGIN
|
||||
INSERT INTO `circuit_list_equipment_identifiers` (
|
||||
`owner_type`,
|
||||
`owner_id`,
|
||||
`circuit_list_id`,
|
||||
`equipment_identifier`
|
||||
)
|
||||
VALUES (
|
||||
'circuit',
|
||||
NEW.`id`,
|
||||
NEW.`circuit_list_id`,
|
||||
NEW.`equipment_identifier`
|
||||
);
|
||||
END;--> statement-breakpoint
|
||||
CREATE TRIGGER `circuits_equipment_identifier_update`
|
||||
AFTER UPDATE OF `circuit_list_id`, `equipment_identifier` ON `circuits`
|
||||
BEGIN
|
||||
UPDATE `circuit_list_equipment_identifiers`
|
||||
SET
|
||||
`circuit_list_id` = NEW.`circuit_list_id`,
|
||||
`equipment_identifier` = NEW.`equipment_identifier`
|
||||
WHERE
|
||||
`owner_type` = 'circuit'
|
||||
AND `owner_id` = OLD.`id`;
|
||||
END;--> statement-breakpoint
|
||||
CREATE TRIGGER `circuits_equipment_identifier_delete`
|
||||
AFTER DELETE ON `circuits`
|
||||
BEGIN
|
||||
DELETE FROM `circuit_list_equipment_identifiers`
|
||||
WHERE
|
||||
`owner_type` = 'circuit'
|
||||
AND `owner_id` = OLD.`id`;
|
||||
END;--> statement-breakpoint
|
||||
CREATE TRIGGER `distribution_board_components_equipment_identifier_insert`
|
||||
AFTER INSERT ON `distribution_board_components`
|
||||
BEGIN
|
||||
INSERT INTO `circuit_list_equipment_identifiers` (
|
||||
`owner_type`,
|
||||
`owner_id`,
|
||||
`circuit_list_id`,
|
||||
`equipment_identifier`
|
||||
)
|
||||
VALUES (
|
||||
'distribution_board_component',
|
||||
NEW.`id`,
|
||||
NEW.`circuit_list_id`,
|
||||
NEW.`equipment_identifier`
|
||||
);
|
||||
END;--> statement-breakpoint
|
||||
CREATE TRIGGER `distribution_board_components_equipment_identifier_update`
|
||||
AFTER UPDATE OF `circuit_list_id`, `equipment_identifier`
|
||||
ON `distribution_board_components`
|
||||
BEGIN
|
||||
UPDATE `circuit_list_equipment_identifiers`
|
||||
SET
|
||||
`circuit_list_id` = NEW.`circuit_list_id`,
|
||||
`equipment_identifier` = NEW.`equipment_identifier`
|
||||
WHERE
|
||||
`owner_type` = 'distribution_board_component'
|
||||
AND `owner_id` = OLD.`id`;
|
||||
END;--> statement-breakpoint
|
||||
CREATE TRIGGER `distribution_board_components_equipment_identifier_delete`
|
||||
AFTER DELETE ON `distribution_board_components`
|
||||
BEGIN
|
||||
DELETE FROM `circuit_list_equipment_identifiers`
|
||||
WHERE
|
||||
`owner_type` = 'distribution_board_component'
|
||||
AND `owner_id` = OLD.`id`;
|
||||
END;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,176 +5,8 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1777565414148,
|
||||
"tag": "0000_bizarre_colossus",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1777577000000,
|
||||
"tag": "0001_global_devices",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1777580000000,
|
||||
"tag": "0002_project_voltage_defaults",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "6",
|
||||
"when": 1777589000000,
|
||||
"tag": "0003_project_floors_rooms",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "6",
|
||||
"when": 1777594000000,
|
||||
"tag": "0004_circuit_lists_and_entry_fields",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "6",
|
||||
"when": 1777597000000,
|
||||
"tag": "0005_project_devices",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "6",
|
||||
"when": 1777652000000,
|
||||
"tag": "0006_device_display_name",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "6",
|
||||
"when": 1777680000000,
|
||||
"tag": "0007_consumer_device_link",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "6",
|
||||
"when": 1777800000000,
|
||||
"tag": "0008_circuit_first_model",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "6",
|
||||
"when": 1784743200000,
|
||||
"tag": "0009_project_device_circuit_fields",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "6",
|
||||
"when": 1784746800000,
|
||||
"tag": "0010_circuit_control_requirement",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 11,
|
||||
"version": "6",
|
||||
"when": 1784832541202,
|
||||
"tag": "0011_project_device_canonical_fields",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "6",
|
||||
"when": 1784833957929,
|
||||
"tag": "0012_project_revision_foundation",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 13,
|
||||
"version": "6",
|
||||
"when": 1784835703230,
|
||||
"tag": "0013_project_history_stacks",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "6",
|
||||
"when": 1785009799979,
|
||||
"tag": "0014_project_snapshots",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 15,
|
||||
"version": "6",
|
||||
"when": 1785014383032,
|
||||
"tag": "0015_modern_madame_masque",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 16,
|
||||
"version": "6",
|
||||
"when": 1785254079435,
|
||||
"tag": "0016_dashing_darkstar",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 17,
|
||||
"version": "6",
|
||||
"when": 1785307189539,
|
||||
"tag": "0017_vengeful_romulus",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 18,
|
||||
"version": "6",
|
||||
"when": 1785308458789,
|
||||
"tag": "0018_fancy_argent",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 19,
|
||||
"version": "6",
|
||||
"when": 1785310907349,
|
||||
"tag": "0019_normalize_project_voltages",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 20,
|
||||
"version": "6",
|
||||
"when": 1785314248007,
|
||||
"tag": "0020_normalize_phase_types",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 21,
|
||||
"version": "6",
|
||||
"when": 1785314558555,
|
||||
"tag": "0021_fill_missing_phase_types",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 22,
|
||||
"version": "6",
|
||||
"when": 1785314626476,
|
||||
"tag": "0022_default_remaining_phase_types",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 23,
|
||||
"version": "6",
|
||||
"when": 1785343645615,
|
||||
"tag": "0023_cheerful_night_thrasher",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 24,
|
||||
"version": "6",
|
||||
"when": 1785431250141,
|
||||
"tag": "0024_damp_mister_sinister",
|
||||
"when": 1785499119431,
|
||||
"tag": "0000_whole_surge",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -382,9 +382,6 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||
equipmentIdentifier: snapshot.equipmentIdentifier,
|
||||
displayName: snapshot.displayName,
|
||||
sortOrder: snapshot.sortOrder,
|
||||
protectionType: snapshot.protectionType,
|
||||
protectionRatedCurrent: snapshot.protectionRatedCurrent,
|
||||
protectionCharacteristic: snapshot.protectionCharacteristic,
|
||||
cableType: snapshot.cableType,
|
||||
cableCrossSection: snapshot.cableCrossSection,
|
||||
cableLength: snapshot.cableLength,
|
||||
@@ -423,11 +420,6 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||
snapshot.equipmentIdentifier ||
|
||||
current.displayName !== snapshot.displayName ||
|
||||
current.sortOrder !== snapshot.sortOrder ||
|
||||
current.protectionType !== snapshot.protectionType ||
|
||||
current.protectionRatedCurrent !==
|
||||
snapshot.protectionRatedCurrent ||
|
||||
current.protectionCharacteristic !==
|
||||
snapshot.protectionCharacteristic ||
|
||||
current.cableType !== snapshot.cableType ||
|
||||
current.cableCrossSection !== snapshot.cableCrossSection ||
|
||||
current.cableLength !== snapshot.cableLength ||
|
||||
|
||||
@@ -77,11 +77,6 @@ export class CircuitDeviceRowStructureProjectCommandRepository
|
||||
source: ExecuteCircuitDeviceRowStructureCommandInput["source"],
|
||||
row: CircuitDeviceRowSnapshot
|
||||
) {
|
||||
if (source === "user" && row.legacyConsumerId !== null) {
|
||||
throw new Error(
|
||||
"User commands cannot assign legacy consumer ids."
|
||||
);
|
||||
}
|
||||
if (source === "user" && !isElectricalPhaseType(row.phaseType)) {
|
||||
throw new Error("Device-row phase type is invalid.");
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ export function toCircuitDeviceRowSnapshot(
|
||||
id: row.id,
|
||||
circuitId: row.circuitId,
|
||||
linkedProjectDeviceId: row.linkedProjectDeviceId,
|
||||
legacyConsumerId: row.legacyConsumerId,
|
||||
sortOrder: row.sortOrder,
|
||||
name: row.name,
|
||||
displayName: row.displayName,
|
||||
|
||||
@@ -44,7 +44,6 @@ export interface CircuitDeviceRowPatchInput {
|
||||
export interface CircuitDeviceRowCreateInput extends CircuitDeviceRowUpdateInput {
|
||||
circuitId: string;
|
||||
linkedProjectDeviceId?: string;
|
||||
legacyConsumerId?: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
@@ -110,7 +109,6 @@ export function toCircuitDeviceRowCreateValues(
|
||||
return {
|
||||
id,
|
||||
circuitId: input.circuitId,
|
||||
legacyConsumerId: input.legacyConsumerId ?? null,
|
||||
sortOrder: input.sortOrder,
|
||||
...toCircuitDeviceRowUpdateValues(input),
|
||||
};
|
||||
|
||||
@@ -25,7 +25,6 @@ export class CircuitDeviceRowRepository {
|
||||
id: circuitDeviceRows.id,
|
||||
circuitId: circuitDeviceRows.circuitId,
|
||||
linkedProjectDeviceId: circuitDeviceRows.linkedProjectDeviceId,
|
||||
legacyConsumerId: circuitDeviceRows.legacyConsumerId,
|
||||
sortOrder: circuitDeviceRows.sortOrder,
|
||||
name: circuitDeviceRows.name,
|
||||
displayName: circuitDeviceRows.displayName,
|
||||
|
||||
@@ -133,11 +133,6 @@ export class CircuitStructureProjectCommandRepository
|
||||
throw new Error("Circuit device-row id already exists.");
|
||||
}
|
||||
for (const row of snapshot.deviceRows) {
|
||||
if (source === "user" && row.legacyConsumerId !== null) {
|
||||
throw new Error(
|
||||
"User commands cannot assign legacy consumer ids."
|
||||
);
|
||||
}
|
||||
if (source === "user" && !isElectricalPhaseType(row.phaseType)) {
|
||||
throw new Error("Device-row phase type is invalid.");
|
||||
}
|
||||
@@ -158,9 +153,6 @@ export class CircuitStructureProjectCommandRepository
|
||||
equipmentIdentifier: snapshot.equipmentIdentifier,
|
||||
displayName: snapshot.displayName,
|
||||
sortOrder: snapshot.sortOrder,
|
||||
protectionType: snapshot.protectionType,
|
||||
protectionRatedCurrent: snapshot.protectionRatedCurrent,
|
||||
protectionCharacteristic: snapshot.protectionCharacteristic,
|
||||
cableType: snapshot.cableType,
|
||||
cableCrossSection: snapshot.cableCrossSection,
|
||||
cableLength: snapshot.cableLength,
|
||||
@@ -238,9 +230,6 @@ export class CircuitStructureProjectCommandRepository
|
||||
equipmentIdentifier: circuit.equipmentIdentifier,
|
||||
displayName: circuit.displayName,
|
||||
sortOrder: circuit.sortOrder,
|
||||
protectionType: circuit.protectionType,
|
||||
protectionRatedCurrent: circuit.protectionRatedCurrent,
|
||||
protectionCharacteristic: circuit.protectionCharacteristic,
|
||||
cableType: circuit.cableType,
|
||||
cableCrossSection: circuit.cableCrossSection,
|
||||
cableLength: circuit.cableLength,
|
||||
|
||||
@@ -4,9 +4,6 @@ export interface CircuitCreatePersistenceInput {
|
||||
equipmentIdentifier: string;
|
||||
displayName?: string;
|
||||
sortOrder: number;
|
||||
protectionType?: string;
|
||||
protectionRatedCurrent?: number;
|
||||
protectionCharacteristic?: string;
|
||||
cableType?: string;
|
||||
cableCrossSection?: string;
|
||||
cableLength?: number;
|
||||
@@ -24,9 +21,6 @@ export interface CircuitPatchPersistenceInput {
|
||||
equipmentIdentifier?: string;
|
||||
displayName?: string | null;
|
||||
sortOrder?: number;
|
||||
protectionType?: string | null;
|
||||
protectionRatedCurrent?: number | null;
|
||||
protectionCharacteristic?: string | null;
|
||||
cableType?: string | null;
|
||||
cableCrossSection?: string | null;
|
||||
cableLength?: number | null;
|
||||
@@ -47,9 +41,6 @@ export function toCircuitCreateValues(id: string, input: CircuitCreatePersistenc
|
||||
equipmentIdentifier: input.equipmentIdentifier,
|
||||
displayName: input.displayName ?? null,
|
||||
sortOrder: input.sortOrder,
|
||||
protectionType: input.protectionType ?? null,
|
||||
protectionRatedCurrent: input.protectionRatedCurrent ?? null,
|
||||
protectionCharacteristic: input.protectionCharacteristic ?? null,
|
||||
cableType: input.cableType ?? null,
|
||||
cableCrossSection: input.cableCrossSection ?? null,
|
||||
cableLength: input.cableLength ?? null,
|
||||
@@ -74,13 +65,6 @@ export function toCircuitPatchValues(input: CircuitPatchPersistenceInput) {
|
||||
}
|
||||
if (has("displayName")) values.displayName = input.displayName ?? null;
|
||||
if (input.sortOrder !== undefined) values.sortOrder = input.sortOrder;
|
||||
if (has("protectionType")) values.protectionType = input.protectionType ?? null;
|
||||
if (has("protectionRatedCurrent")) {
|
||||
values.protectionRatedCurrent = input.protectionRatedCurrent ?? null;
|
||||
}
|
||||
if (has("protectionCharacteristic")) {
|
||||
values.protectionCharacteristic = input.protectionCharacteristic ?? null;
|
||||
}
|
||||
if (has("cableType")) values.cableType = input.cableType ?? null;
|
||||
if (has("cableCrossSection")) {
|
||||
values.cableCrossSection = input.cableCrossSection ?? null;
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import {
|
||||
assertDistributionBoardUpdateProjectCommand,
|
||||
createDistributionBoardUpdateProjectCommand,
|
||||
legacyDistributionBoardUpdateCommandSchemaVersion,
|
||||
type DistributionBoardUpdateField,
|
||||
type DistributionBoardUpdatePatch,
|
||||
type DistributionBoardUpdateProjectCommand,
|
||||
@@ -332,19 +331,10 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||
getDistributionBoardFieldValue(current, change.field),
|
||||
])
|
||||
) as DistributionBoardUpdatePatch;
|
||||
const currentInverse = createDistributionBoardUpdateProjectCommand(
|
||||
const inverse = createDistributionBoardUpdateProjectCommand(
|
||||
current.id,
|
||||
inversePatch
|
||||
);
|
||||
const inverse =
|
||||
input.command.schemaVersion ===
|
||||
legacyDistributionBoardUpdateCommandSchemaVersion
|
||||
? ({
|
||||
...currentInverse,
|
||||
schemaVersion:
|
||||
legacyDistributionBoardUpdateCommandSchemaVersion,
|
||||
} as DistributionBoardUpdateProjectCommand)
|
||||
: currentInverse;
|
||||
const updated = database
|
||||
.update(distributionBoards)
|
||||
.set(patch)
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
import { and, asc, eq, inArray } from "drizzle-orm";
|
||||
import {
|
||||
assertDistributionBoardDeleteSubtreeProjectCommand,
|
||||
assertDistributionBoardInsertSubtreeProjectCommand,
|
||||
createDistributionBoardDeleteSubtreeProjectCommand,
|
||||
createDistributionBoardInsertSubtreeProjectCommand,
|
||||
distributionBoardDeleteSubtreeCommandType,
|
||||
distributionBoardInsertSubtreeCommandType,
|
||||
type DistributionBoardSubtreeProjectCommand,
|
||||
} from "../../domain/models/distribution-board-subtree-project-command.model.js";
|
||||
import {
|
||||
assertDistributionBoardSubtreeSnapshot,
|
||||
type DistributionBoardSubtreeSnapshot,
|
||||
} from "../../domain/models/distribution-board-subtree-snapshot.model.js";
|
||||
import type {
|
||||
DistributionBoardSubtreeProjectCommandStore,
|
||||
ExecuteDistributionBoardSubtreeCommandInput,
|
||||
} from "../../domain/ports/distribution-board-subtree-project-command.store.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||
import { circuitLists } from "../schema/circuit-lists.js";
|
||||
import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js";
|
||||
import { circuitSections } from "../schema/circuit-sections.js";
|
||||
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 { floors } from "../schema/floors.js";
|
||||
import { projectDevices } from "../schema/project-devices.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { rooms } from "../schema/rooms.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
|
||||
export class DistributionBoardSubtreeProjectCommandRepository
|
||||
implements DistributionBoardSubtreeProjectCommandStore
|
||||
{
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
|
||||
capture(projectId: string, distributionBoardId: string) {
|
||||
return this.captureFrom(
|
||||
this.database,
|
||||
projectId,
|
||||
distributionBoardId
|
||||
);
|
||||
}
|
||||
|
||||
execute(input: ExecuteDistributionBoardSubtreeCommandInput) {
|
||||
if (input.command.type === distributionBoardInsertSubtreeCommandType) {
|
||||
assertDistributionBoardInsertSubtreeProjectCommand(input.command);
|
||||
} else {
|
||||
assertDistributionBoardDeleteSubtreeProjectCommand(input.command);
|
||||
}
|
||||
return executeProjectCommandTransaction(
|
||||
this.database,
|
||||
input,
|
||||
(tx) => this.applyCommand(tx, input.projectId, input.command)
|
||||
);
|
||||
}
|
||||
|
||||
private applyCommand(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
command: DistributionBoardSubtreeProjectCommand
|
||||
) {
|
||||
if (command.type === distributionBoardInsertSubtreeCommandType) {
|
||||
this.insert(database, projectId, command.payload.snapshot);
|
||||
return createDistributionBoardDeleteSubtreeProjectCommand(
|
||||
command.payload.snapshot
|
||||
);
|
||||
}
|
||||
if (command.type === distributionBoardDeleteSubtreeCommandType) {
|
||||
const current = this.captureFrom(
|
||||
database,
|
||||
projectId,
|
||||
command.payload.snapshot.distributionBoard.id
|
||||
);
|
||||
if (!sameSnapshot(current, command.payload.snapshot)) {
|
||||
throw new Error(
|
||||
"Distribution-board subtree changed before deletion."
|
||||
);
|
||||
}
|
||||
const deleted = database
|
||||
.delete(distributionBoards)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
distributionBoards.id,
|
||||
current.distributionBoard.id
|
||||
),
|
||||
eq(distributionBoards.projectId, projectId)
|
||||
)
|
||||
)
|
||||
.run();
|
||||
if (deleted.changes !== 1) {
|
||||
throw new Error(
|
||||
"Distribution-board subtree could not be deleted."
|
||||
);
|
||||
}
|
||||
return createDistributionBoardInsertSubtreeProjectCommand(
|
||||
command.payload.snapshot
|
||||
);
|
||||
}
|
||||
throw new Error("Unsupported distribution-board subtree command.");
|
||||
}
|
||||
|
||||
private captureFrom(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
distributionBoardId: string
|
||||
): DistributionBoardSubtreeSnapshot {
|
||||
const distributionBoard = database
|
||||
.select()
|
||||
.from(distributionBoards)
|
||||
.where(
|
||||
and(
|
||||
eq(distributionBoards.id, distributionBoardId),
|
||||
eq(distributionBoards.projectId, projectId)
|
||||
)
|
||||
)
|
||||
.get();
|
||||
if (!distributionBoard) {
|
||||
throw new Error("Distribution board not found.");
|
||||
}
|
||||
const circuitList = database
|
||||
.select()
|
||||
.from(circuitLists)
|
||||
.where(
|
||||
and(
|
||||
eq(circuitLists.distributionBoardId, distributionBoard.id),
|
||||
eq(circuitLists.projectId, projectId)
|
||||
)
|
||||
)
|
||||
.get();
|
||||
if (!circuitList) {
|
||||
throw new Error("Distribution-board circuit list not found.");
|
||||
}
|
||||
const sections = database
|
||||
.select()
|
||||
.from(circuitSections)
|
||||
.where(eq(circuitSections.circuitListId, circuitList.id))
|
||||
.orderBy(asc(circuitSections.sortOrder), asc(circuitSections.id))
|
||||
.all();
|
||||
const components = database
|
||||
.select()
|
||||
.from(distributionBoardComponents)
|
||||
.where(eq(distributionBoardComponents.circuitListId, circuitList.id))
|
||||
.orderBy(
|
||||
asc(distributionBoardComponents.sortOrder),
|
||||
asc(distributionBoardComponents.id)
|
||||
)
|
||||
.all()
|
||||
.map((component) => ({
|
||||
component,
|
||||
protectionDevice:
|
||||
database
|
||||
.select()
|
||||
.from(distributionBoardComponentProtectionDevices)
|
||||
.where(
|
||||
eq(
|
||||
distributionBoardComponentProtectionDevices.componentId,
|
||||
component.id
|
||||
)
|
||||
)
|
||||
.get() ?? null,
|
||||
}));
|
||||
const circuitSnapshots = database
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.circuitListId, circuitList.id))
|
||||
.orderBy(asc(circuits.sortOrder), asc(circuits.id))
|
||||
.all()
|
||||
.map((circuit) => {
|
||||
const { isReserve, ...values } = circuit;
|
||||
return {
|
||||
...values,
|
||||
isReserve: Boolean(isReserve),
|
||||
deviceRows: database
|
||||
.select()
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.circuitId, circuit.id))
|
||||
.orderBy(
|
||||
asc(circuitDeviceRows.sortOrder),
|
||||
asc(circuitDeviceRows.id)
|
||||
)
|
||||
.all(),
|
||||
protectionDevice:
|
||||
database
|
||||
.select()
|
||||
.from(circuitProtectionDevices)
|
||||
.where(eq(circuitProtectionDevices.circuitId, circuit.id))
|
||||
.get() ?? null,
|
||||
};
|
||||
});
|
||||
const snapshot: DistributionBoardSubtreeSnapshot = {
|
||||
distributionBoard,
|
||||
circuitList,
|
||||
sections,
|
||||
components,
|
||||
circuits: circuitSnapshots,
|
||||
};
|
||||
assertDistributionBoardSubtreeSnapshot(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private insert(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
snapshot: DistributionBoardSubtreeSnapshot
|
||||
) {
|
||||
assertDistributionBoardSubtreeSnapshot(snapshot);
|
||||
if (
|
||||
snapshot.distributionBoard.projectId !== projectId ||
|
||||
snapshot.circuitList.projectId !== projectId
|
||||
) {
|
||||
throw new Error(
|
||||
"Distribution-board subtree belongs to another project."
|
||||
);
|
||||
}
|
||||
this.assertProjectReferences(database, projectId, snapshot);
|
||||
this.assertIdsAvailable(database, snapshot);
|
||||
|
||||
database
|
||||
.insert(distributionBoards)
|
||||
.values(snapshot.distributionBoard)
|
||||
.run();
|
||||
database.insert(circuitLists).values(snapshot.circuitList).run();
|
||||
if (snapshot.sections.length > 0) {
|
||||
database.insert(circuitSections).values(snapshot.sections).run();
|
||||
}
|
||||
for (const entry of snapshot.components) {
|
||||
database
|
||||
.insert(distributionBoardComponents)
|
||||
.values(entry.component)
|
||||
.run();
|
||||
if (entry.protectionDevice !== null) {
|
||||
database
|
||||
.insert(distributionBoardComponentProtectionDevices)
|
||||
.values(entry.protectionDevice)
|
||||
.run();
|
||||
}
|
||||
}
|
||||
for (const circuit of snapshot.circuits) {
|
||||
const { deviceRows, protectionDevice, isReserve, ...values } =
|
||||
circuit;
|
||||
database
|
||||
.insert(circuits)
|
||||
.values({ ...values, isReserve: isReserve ? 1 : 0 })
|
||||
.run();
|
||||
if (protectionDevice !== null && protectionDevice !== undefined) {
|
||||
database
|
||||
.insert(circuitProtectionDevices)
|
||||
.values(protectionDevice)
|
||||
.run();
|
||||
}
|
||||
if (deviceRows.length > 0) {
|
||||
database.insert(circuitDeviceRows).values(deviceRows).run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private assertProjectReferences(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
snapshot: DistributionBoardSubtreeSnapshot
|
||||
) {
|
||||
const project = database
|
||||
.select({
|
||||
enabledSupplyTypes:
|
||||
projects.enabledDistributionBoardSupplyTypes,
|
||||
})
|
||||
.from(projects)
|
||||
.where(eq(projects.id, projectId))
|
||||
.get();
|
||||
if (!project) {
|
||||
throw new Error("Project not found.");
|
||||
}
|
||||
if (
|
||||
snapshot.distributionBoard.supplyType !== null &&
|
||||
!project.enabledSupplyTypes.includes(
|
||||
snapshot.distributionBoard.supplyType
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
"Distribution-board subtree uses a disabled supply type."
|
||||
);
|
||||
}
|
||||
if (snapshot.distributionBoard.floorId !== null) {
|
||||
const floor = database
|
||||
.select({ projectId: floors.projectId })
|
||||
.from(floors)
|
||||
.where(eq(floors.id, snapshot.distributionBoard.floorId))
|
||||
.get();
|
||||
if (!floor || floor.projectId !== projectId) {
|
||||
throw new Error(
|
||||
"Distribution-board floor does not belong to project."
|
||||
);
|
||||
}
|
||||
}
|
||||
const linkedProjectDeviceIds = uniqueStrings(
|
||||
snapshot.circuits.flatMap((circuit) =>
|
||||
circuit.deviceRows.flatMap((row) =>
|
||||
row.linkedProjectDeviceId === null
|
||||
? []
|
||||
: [row.linkedProjectDeviceId]
|
||||
)
|
||||
)
|
||||
);
|
||||
if (linkedProjectDeviceIds.length > 0) {
|
||||
const references = database
|
||||
.select({ id: projectDevices.id, projectId: projectDevices.projectId })
|
||||
.from(projectDevices)
|
||||
.where(inArray(projectDevices.id, linkedProjectDeviceIds))
|
||||
.all();
|
||||
if (
|
||||
references.length !== linkedProjectDeviceIds.length ||
|
||||
references.some((reference) => reference.projectId !== projectId)
|
||||
) {
|
||||
throw new Error(
|
||||
"Distribution-board project-device reference is invalid."
|
||||
);
|
||||
}
|
||||
}
|
||||
const roomIds = uniqueStrings(
|
||||
snapshot.circuits.flatMap((circuit) =>
|
||||
circuit.deviceRows.flatMap((row) =>
|
||||
row.roomId === null ? [] : [row.roomId]
|
||||
)
|
||||
)
|
||||
);
|
||||
if (roomIds.length > 0) {
|
||||
const references = database
|
||||
.select({ id: rooms.id, projectId: rooms.projectId })
|
||||
.from(rooms)
|
||||
.where(inArray(rooms.id, roomIds))
|
||||
.all();
|
||||
if (
|
||||
references.length !== roomIds.length ||
|
||||
references.some((reference) => reference.projectId !== projectId)
|
||||
) {
|
||||
throw new Error("Distribution-board room reference is invalid.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private assertIdsAvailable(
|
||||
database: AppDatabase,
|
||||
snapshot: DistributionBoardSubtreeSnapshot
|
||||
) {
|
||||
const sectionIds = snapshot.sections.map((section) => section.id);
|
||||
const componentIds = snapshot.components.map(
|
||||
({ component }) => component.id
|
||||
);
|
||||
const circuitIds = snapshot.circuits.map((circuit) => circuit.id);
|
||||
const deviceRowIds = snapshot.circuits.flatMap((circuit) =>
|
||||
circuit.deviceRows.map((row) => row.id)
|
||||
);
|
||||
const conflicts = [
|
||||
database
|
||||
.select({ id: distributionBoards.id })
|
||||
.from(distributionBoards)
|
||||
.where(eq(distributionBoards.id, snapshot.distributionBoard.id))
|
||||
.get(),
|
||||
database
|
||||
.select({ id: circuitLists.id })
|
||||
.from(circuitLists)
|
||||
.where(eq(circuitLists.id, snapshot.circuitList.id))
|
||||
.get(),
|
||||
sectionIds.length === 0
|
||||
? undefined
|
||||
: database
|
||||
.select({ id: circuitSections.id })
|
||||
.from(circuitSections)
|
||||
.where(inArray(circuitSections.id, sectionIds))
|
||||
.limit(1)
|
||||
.get(),
|
||||
componentIds.length === 0
|
||||
? undefined
|
||||
: database
|
||||
.select({ id: distributionBoardComponents.id })
|
||||
.from(distributionBoardComponents)
|
||||
.where(inArray(distributionBoardComponents.id, componentIds))
|
||||
.limit(1)
|
||||
.get(),
|
||||
circuitIds.length === 0
|
||||
? undefined
|
||||
: database
|
||||
.select({ id: circuits.id })
|
||||
.from(circuits)
|
||||
.where(inArray(circuits.id, circuitIds))
|
||||
.limit(1)
|
||||
.get(),
|
||||
deviceRowIds.length === 0
|
||||
? undefined
|
||||
: database
|
||||
.select({ id: circuitDeviceRows.id })
|
||||
.from(circuitDeviceRows)
|
||||
.where(inArray(circuitDeviceRows.id, deviceRowIds))
|
||||
.limit(1)
|
||||
.get(),
|
||||
];
|
||||
if (conflicts.some(Boolean)) {
|
||||
throw new Error("Distribution-board subtree id already exists.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function uniqueStrings(values: string[]) {
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
function sameSnapshot(
|
||||
left: DistributionBoardSubtreeSnapshot,
|
||||
right: DistributionBoardSubtreeSnapshot
|
||||
) {
|
||||
return canonicalJson(left) === canonicalJson(right);
|
||||
}
|
||||
|
||||
function canonicalJson(value: unknown): string {
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map(canonicalJson).join(",")}]`;
|
||||
}
|
||||
if (value !== null && typeof value === "object") {
|
||||
return `{${Object.entries(value)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(
|
||||
([key, child]) =>
|
||||
`${JSON.stringify(key)}:${canonicalJson(child)}`
|
||||
)
|
||||
.join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value) ?? "null";
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import { and, asc, eq } from "drizzle-orm";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { floors } from "../schema/floors.js";
|
||||
|
||||
@@ -13,4 +13,12 @@ export class FloorRepository {
|
||||
.where(eq(floors.projectId, projectId))
|
||||
.orderBy(asc(floors.sortOrder), asc(floors.name));
|
||||
}
|
||||
|
||||
async findById(projectId: string, floorId: string) {
|
||||
return this.database
|
||||
.select()
|
||||
.from(floors)
|
||||
.where(and(eq(floors.projectId, projectId), eq(floors.id, floorId)))
|
||||
.get() ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
import { eq, inArray, isNull } from "drizzle-orm";
|
||||
import type {
|
||||
LegacyMigrationCircuitInput,
|
||||
LegacyMigrationReportInput,
|
||||
} from "../../domain/ports/legacy-consumer-migration.store.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import { consumers } from "../schema/consumers.js";
|
||||
import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js";
|
||||
import { legacyConsumerMigrationReports } from "../schema/legacy-consumer-migration-report.js";
|
||||
import { toCircuitDeviceRowCreateValues } from "./circuit-device-row.persistence.js";
|
||||
import { toCircuitCreateValues } from "./circuit.persistence.js";
|
||||
|
||||
export class LegacyConsumerMigrationRepository {
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
|
||||
async listSourceConsumersByCircuitList(circuitListId: string) {
|
||||
return this.database
|
||||
.select()
|
||||
.from(consumers)
|
||||
.where(eq(consumers.circuitListId, circuitListId));
|
||||
}
|
||||
|
||||
async listUnmigratedConsumers() {
|
||||
return this.database
|
||||
.select({
|
||||
id: consumers.id,
|
||||
projectId: consumers.projectId,
|
||||
circuitListId: consumers.circuitListId,
|
||||
})
|
||||
.from(consumers)
|
||||
.leftJoin(
|
||||
legacyConsumerCircuitMigrations,
|
||||
eq(legacyConsumerCircuitMigrations.consumerId, consumers.id)
|
||||
)
|
||||
.where(isNull(legacyConsumerCircuitMigrations.consumerId));
|
||||
}
|
||||
|
||||
async listMigratedConsumerIds(circuitListId: string) {
|
||||
const rows = await this.database
|
||||
.select({ consumerId: legacyConsumerCircuitMigrations.consumerId })
|
||||
.from(legacyConsumerCircuitMigrations)
|
||||
.where(eq(legacyConsumerCircuitMigrations.circuitListId, circuitListId));
|
||||
return rows.map((row) => row.consumerId);
|
||||
}
|
||||
|
||||
persistCircuitListMigration(input: {
|
||||
circuitListId: string;
|
||||
circuits: LegacyMigrationCircuitInput[];
|
||||
report: LegacyMigrationReportInput;
|
||||
}) {
|
||||
if (input.circuits.some((entry) => entry.circuit.circuitListId !== input.circuitListId)) {
|
||||
throw new Error("All migrated circuits must belong to the target circuit list.");
|
||||
}
|
||||
|
||||
const consumerIds = input.circuits.flatMap((entry) =>
|
||||
entry.deviceRows.map((row) => row.legacyConsumerId)
|
||||
);
|
||||
if (new Set(consumerIds).size !== consumerIds.length) {
|
||||
throw new Error("A legacy consumer may only be migrated once per operation.");
|
||||
}
|
||||
|
||||
const preparedCircuits = input.circuits.map((entry) => {
|
||||
const circuitId = crypto.randomUUID();
|
||||
return {
|
||||
circuitId,
|
||||
circuitValues: toCircuitCreateValues(circuitId, entry.circuit),
|
||||
rows: entry.deviceRows.map((row) => {
|
||||
const rowId = crypto.randomUUID();
|
||||
return {
|
||||
consumerId: row.legacyConsumerId,
|
||||
rowId,
|
||||
rowValues: toCircuitDeviceRowCreateValues(rowId, {
|
||||
...row,
|
||||
circuitId,
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
const createdAtIso = new Date().toISOString();
|
||||
|
||||
this.database.transaction((tx) => {
|
||||
if (consumerIds.length > 0) {
|
||||
const existingMappings = tx
|
||||
.select({ consumerId: legacyConsumerCircuitMigrations.consumerId })
|
||||
.from(legacyConsumerCircuitMigrations)
|
||||
.where(inArray(legacyConsumerCircuitMigrations.consumerId, consumerIds))
|
||||
.all();
|
||||
if (existingMappings.length > 0) {
|
||||
throw new Error("A legacy consumer was migrated before this operation completed.");
|
||||
}
|
||||
}
|
||||
|
||||
for (const preparedCircuit of preparedCircuits) {
|
||||
tx.insert(circuits).values(preparedCircuit.circuitValues).run();
|
||||
for (const row of preparedCircuit.rows) {
|
||||
tx.insert(circuitDeviceRows).values(row.rowValues).run();
|
||||
tx
|
||||
.insert(legacyConsumerCircuitMigrations)
|
||||
.values({
|
||||
consumerId: row.consumerId,
|
||||
circuitId: preparedCircuit.circuitId,
|
||||
circuitDeviceRowId: row.rowId,
|
||||
circuitListId: input.circuitListId,
|
||||
createdAtIso,
|
||||
})
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
const reportValues = {
|
||||
...input.report,
|
||||
createdAtIso,
|
||||
};
|
||||
const existingReport = tx
|
||||
.select({ id: legacyConsumerMigrationReports.id })
|
||||
.from(legacyConsumerMigrationReports)
|
||||
.where(eq(legacyConsumerMigrationReports.circuitListId, input.circuitListId))
|
||||
.limit(1)
|
||||
.all();
|
||||
if (existingReport.length > 0) {
|
||||
tx
|
||||
.update(legacyConsumerMigrationReports)
|
||||
.set(reportValues)
|
||||
.where(eq(legacyConsumerMigrationReports.id, existingReport[0].id))
|
||||
.run();
|
||||
} else {
|
||||
tx
|
||||
.insert(legacyConsumerMigrationReports)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
circuitListId: input.circuitListId,
|
||||
...reportValues,
|
||||
})
|
||||
.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,6 @@ const circuitDeviceRowSnapshotFields = [
|
||||
"id",
|
||||
"circuitId",
|
||||
"linkedProjectDeviceId",
|
||||
"legacyConsumerId",
|
||||
"sortOrder",
|
||||
"name",
|
||||
"displayName",
|
||||
|
||||
@@ -2,16 +2,22 @@ import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
assertProjectFloorDeleteProjectCommand,
|
||||
assertProjectFloorInsertProjectCommand,
|
||||
assertProjectFloorUpdateProjectCommand,
|
||||
assertProjectRoomDeleteProjectCommand,
|
||||
assertProjectRoomInsertProjectCommand,
|
||||
assertProjectRoomUpdateProjectCommand,
|
||||
createProjectFloorDeleteProjectCommand,
|
||||
createProjectFloorInsertProjectCommand,
|
||||
createProjectFloorUpdateProjectCommand,
|
||||
createProjectRoomDeleteProjectCommand,
|
||||
createProjectRoomInsertProjectCommand,
|
||||
createProjectRoomUpdateProjectCommand,
|
||||
projectFloorDeleteCommandType,
|
||||
projectFloorInsertCommandType,
|
||||
projectFloorUpdateCommandType,
|
||||
projectRoomDeleteCommandType,
|
||||
projectRoomInsertCommandType,
|
||||
projectRoomUpdateCommandType,
|
||||
type ProjectFloorSnapshot,
|
||||
type ProjectLocationStructureProjectCommand,
|
||||
type ProjectRoomSnapshot,
|
||||
@@ -22,7 +28,6 @@ import type {
|
||||
} from "../../domain/ports/project-location-structure-project-command.store.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||
import { consumers } from "../schema/consumers.js";
|
||||
import { floors } from "../schema/floors.js";
|
||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
@@ -63,6 +68,9 @@ export class ProjectLocationStructureProjectCommandRepository
|
||||
projectId,
|
||||
command.payload.floor
|
||||
);
|
||||
case projectFloorUpdateCommandType:
|
||||
assertProjectFloorUpdateProjectCommand(command);
|
||||
return this.updateFloor(database, projectId, command.payload.expected, command.payload.target);
|
||||
case projectRoomInsertCommandType:
|
||||
assertProjectRoomInsertProjectCommand(command);
|
||||
return this.insertRoom(
|
||||
@@ -77,6 +85,9 @@ export class ProjectLocationStructureProjectCommandRepository
|
||||
projectId,
|
||||
command.payload.room
|
||||
);
|
||||
case projectRoomUpdateCommandType:
|
||||
assertProjectRoomUpdateProjectCommand(command);
|
||||
return this.updateRoom(database, projectId, command.payload.expected, command.payload.target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +138,7 @@ export class ProjectLocationStructureProjectCommandRepository
|
||||
.get();
|
||||
if (referencedRoom) {
|
||||
throw new Error(
|
||||
"A project floor with assigned rooms cannot be removed by history."
|
||||
"Die Etage kann nicht gelöscht werden, solange Räume zugeordnet sind."
|
||||
);
|
||||
}
|
||||
const referencedDistributionBoard = database
|
||||
@@ -138,7 +149,7 @@ export class ProjectLocationStructureProjectCommandRepository
|
||||
.get();
|
||||
if (referencedDistributionBoard) {
|
||||
throw new Error(
|
||||
"A project floor assigned to a distribution board cannot be removed by history."
|
||||
"Die Etage kann nicht gelöscht werden, solange eine Verteilung zugeordnet ist."
|
||||
);
|
||||
}
|
||||
const deleted = database
|
||||
@@ -192,6 +203,70 @@ export class ProjectLocationStructureProjectCommandRepository
|
||||
return createProjectRoomDeleteProjectCommand(room);
|
||||
}
|
||||
|
||||
private updateFloor(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
expected: ProjectFloorSnapshot,
|
||||
target: ProjectFloorSnapshot
|
||||
) {
|
||||
const persisted = database
|
||||
.select()
|
||||
.from(floors)
|
||||
.where(and(eq(floors.id, expected.id), eq(floors.projectId, projectId)))
|
||||
.get();
|
||||
if (!persisted || !sameRecord(expected, persisted)) {
|
||||
throw new Error("Project floor changed before update.");
|
||||
}
|
||||
const updated = database
|
||||
.update(floors)
|
||||
.set({ name: target.name })
|
||||
.where(and(eq(floors.id, expected.id), eq(floors.projectId, projectId)))
|
||||
.run();
|
||||
if (updated.changes !== 1) {
|
||||
throw new Error("Die Etage konnte nicht aktualisiert werden.");
|
||||
}
|
||||
return createProjectFloorUpdateProjectCommand(target, expected);
|
||||
}
|
||||
|
||||
private updateRoom(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
expected: ProjectRoomSnapshot,
|
||||
target: ProjectRoomSnapshot
|
||||
) {
|
||||
const persisted = database
|
||||
.select()
|
||||
.from(rooms)
|
||||
.where(and(eq(rooms.id, expected.id), eq(rooms.projectId, projectId)))
|
||||
.get();
|
||||
if (!persisted || !sameRecord(expected, persisted)) {
|
||||
throw new Error("Project room changed before update.");
|
||||
}
|
||||
if (target.floorId) {
|
||||
const targetFloor = database
|
||||
.select({ id: floors.id })
|
||||
.from(floors)
|
||||
.where(and(eq(floors.id, target.floorId), eq(floors.projectId, projectId)))
|
||||
.get();
|
||||
if (!targetFloor) {
|
||||
throw new Error("Project room references a foreign floor.");
|
||||
}
|
||||
}
|
||||
const updated = database
|
||||
.update(rooms)
|
||||
.set({
|
||||
floorId: target.floorId,
|
||||
roomNumber: target.roomNumber,
|
||||
roomName: target.roomName,
|
||||
})
|
||||
.where(and(eq(rooms.id, expected.id), eq(rooms.projectId, projectId)))
|
||||
.run();
|
||||
if (updated.changes !== 1) {
|
||||
throw new Error("Der Raum konnte nicht aktualisiert werden.");
|
||||
}
|
||||
return createProjectRoomUpdateProjectCommand(target, expected);
|
||||
}
|
||||
|
||||
private deleteRoom(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
@@ -216,15 +291,9 @@ export class ProjectLocationStructureProjectCommandRepository
|
||||
.where(eq(circuitDeviceRows.roomId, expected.id))
|
||||
.limit(1)
|
||||
.get();
|
||||
const referencedLegacyConsumer = database
|
||||
.select({ id: consumers.id })
|
||||
.from(consumers)
|
||||
.where(eq(consumers.roomId, expected.id))
|
||||
.limit(1)
|
||||
.get();
|
||||
if (referencedDeviceRow || referencedLegacyConsumer) {
|
||||
if (referencedDeviceRow) {
|
||||
throw new Error(
|
||||
"A referenced project room cannot be removed by history."
|
||||
"Der Raum kann nicht gelöscht werden, solange er in einem Stromkreis verwendet wird."
|
||||
);
|
||||
}
|
||||
const deleted = database
|
||||
|
||||
@@ -2,9 +2,6 @@ import { and, eq, isNotNull } from "drizzle-orm";
|
||||
import {
|
||||
assertProjectSettingsUpdateProjectCommand,
|
||||
createProjectSettingsUpdateProjectCommand,
|
||||
legacyProjectSettingsUpdateCommandSchemaVersion,
|
||||
normalizeProjectSettingsValues,
|
||||
previousProjectSettingsUpdateCommandSchemaVersion,
|
||||
type ProjectSettingsValues,
|
||||
} from "../../domain/models/project-settings-project-command.model.js";
|
||||
import type {
|
||||
@@ -43,39 +40,12 @@ export class ProjectSettingsProjectCommandRepository
|
||||
if (!current) {
|
||||
throw new Error("Project not found.");
|
||||
}
|
||||
const target = normalizeProjectSettingsValues(input.command, current);
|
||||
const target = input.command.payload;
|
||||
if (projectSettingsEqual(current, target)) {
|
||||
throw new Error("Project settings did not change.");
|
||||
}
|
||||
|
||||
const inverse =
|
||||
input.command.schemaVersion ===
|
||||
legacyProjectSettingsUpdateCommandSchemaVersion
|
||||
? {
|
||||
schemaVersion: legacyProjectSettingsUpdateCommandSchemaVersion,
|
||||
type: input.command.type,
|
||||
payload: {
|
||||
singlePhaseVoltageV: current.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: current.threePhaseVoltageV,
|
||||
},
|
||||
}
|
||||
: input.command.schemaVersion ===
|
||||
previousProjectSettingsUpdateCommandSchemaVersion
|
||||
? {
|
||||
schemaVersion:
|
||||
previousProjectSettingsUpdateCommandSchemaVersion,
|
||||
type: input.command.type,
|
||||
payload: {
|
||||
name: current.name,
|
||||
internalProjectNumber: current.internalProjectNumber,
|
||||
externalProjectNumber: current.externalProjectNumber,
|
||||
buildingOwner: current.buildingOwner,
|
||||
description: current.description,
|
||||
singlePhaseVoltageV: current.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: current.threePhaseVoltageV,
|
||||
},
|
||||
}
|
||||
: createProjectSettingsUpdateProjectCommand({
|
||||
const inverse = createProjectSettingsUpdateProjectCommand({
|
||||
name: current.name,
|
||||
internalProjectNumber: current.internalProjectNumber,
|
||||
externalProjectNumber: current.externalProjectNumber,
|
||||
|
||||
@@ -4,14 +4,8 @@ import { ProjectRevisionConflictError } from "../../domain/errors/project-revisi
|
||||
import { ProjectSnapshotNameConflictError } from "../../domain/errors/project-snapshot-name-conflict.error.js";
|
||||
import { createProjectStateRestoreCommand } from "../../domain/models/project-state-restore-command.model.js";
|
||||
import {
|
||||
distributionBoardProjectStateSnapshotSchemaVersion,
|
||||
deserializeProjectStateSnapshot,
|
||||
legacyProjectStateSnapshotSchemaVersion,
|
||||
previousProjectStateSnapshotSchemaVersion,
|
||||
projectStateSnapshotSchemaVersion,
|
||||
simultaneityFactorProjectStateSnapshotSchemaVersion,
|
||||
supplyTypesProjectStateSnapshotSchemaVersion,
|
||||
voltageProjectStateSnapshotSchemaVersion,
|
||||
} from "../../domain/models/project-state-snapshot.model.js";
|
||||
import type {
|
||||
CreateNamedProjectSnapshotInput,
|
||||
@@ -144,19 +138,7 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
|
||||
if (actualChecksum !== stored.payloadSha256) {
|
||||
throw new Error("Project snapshot checksum verification failed.");
|
||||
}
|
||||
if (
|
||||
stored.schemaVersion !== legacyProjectStateSnapshotSchemaVersion &&
|
||||
stored.schemaVersion !== previousProjectStateSnapshotSchemaVersion &&
|
||||
stored.schemaVersion !==
|
||||
distributionBoardProjectStateSnapshotSchemaVersion &&
|
||||
stored.schemaVersion !==
|
||||
supplyTypesProjectStateSnapshotSchemaVersion &&
|
||||
stored.schemaVersion !==
|
||||
voltageProjectStateSnapshotSchemaVersion &&
|
||||
stored.schemaVersion !==
|
||||
simultaneityFactorProjectStateSnapshotSchemaVersion &&
|
||||
stored.schemaVersion !== projectStateSnapshotSchemaVersion
|
||||
) {
|
||||
if (stored.schemaVersion !== projectStateSnapshotSchemaVersion) {
|
||||
throw new Error("Project snapshot schema version is not supported.");
|
||||
}
|
||||
const targetState = deserializeProjectStateSnapshot(stored.payloadJson);
|
||||
|
||||
@@ -18,13 +18,10 @@ import { circuitLists } from "../schema/circuit-lists.js";
|
||||
import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js";
|
||||
import { circuitSections } from "../schema/circuit-sections.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import { consumers } from "../schema/consumers.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 { floors } from "../schema/floors.js";
|
||||
import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js";
|
||||
import { legacyConsumerMigrationReports } from "../schema/legacy-consumer-migration-report.js";
|
||||
import { projectDevices } from "../schema/project-devices.js";
|
||||
import { projectChangeSets } from "../schema/project-change-sets.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
@@ -186,10 +183,6 @@ function replaceProjectState(
|
||||
database: AppDatabase,
|
||||
state: ProjectStateSnapshot
|
||||
) {
|
||||
const upgradeOnlyData = captureUpgradeOnlyData(
|
||||
database,
|
||||
state.project.id
|
||||
);
|
||||
database
|
||||
.delete(distributionBoards)
|
||||
.where(eq(distributionBoards.projectId, state.project.id))
|
||||
@@ -257,136 +250,6 @@ function replaceProjectState(
|
||||
circuitDeviceRows,
|
||||
state.circuits.flatMap((circuit) => circuit.deviceRows)
|
||||
);
|
||||
restoreCompatibleUpgradeOnlyData(database, state, upgradeOnlyData);
|
||||
}
|
||||
|
||||
function captureUpgradeOnlyData(
|
||||
database: AppDatabase,
|
||||
projectId: string
|
||||
) {
|
||||
const consumerLinks = database
|
||||
.select({
|
||||
id: consumers.id,
|
||||
distributionBoardId: consumers.distributionBoardId,
|
||||
circuitListId: consumers.circuitListId,
|
||||
projectDeviceId: consumers.projectDeviceId,
|
||||
roomId: consumers.roomId,
|
||||
})
|
||||
.from(consumers)
|
||||
.where(eq(consumers.projectId, projectId))
|
||||
.all();
|
||||
const migrationMappings = database
|
||||
.select({
|
||||
consumerId: legacyConsumerCircuitMigrations.consumerId,
|
||||
circuitId: legacyConsumerCircuitMigrations.circuitId,
|
||||
circuitDeviceRowId:
|
||||
legacyConsumerCircuitMigrations.circuitDeviceRowId,
|
||||
circuitListId: legacyConsumerCircuitMigrations.circuitListId,
|
||||
createdAtIso: legacyConsumerCircuitMigrations.createdAtIso,
|
||||
})
|
||||
.from(legacyConsumerCircuitMigrations)
|
||||
.innerJoin(
|
||||
circuitLists,
|
||||
eq(
|
||||
circuitLists.id,
|
||||
legacyConsumerCircuitMigrations.circuitListId
|
||||
)
|
||||
)
|
||||
.where(eq(circuitLists.projectId, projectId))
|
||||
.all();
|
||||
const migrationReports = database
|
||||
.select({
|
||||
id: legacyConsumerMigrationReports.id,
|
||||
circuitListId: legacyConsumerMigrationReports.circuitListId,
|
||||
legacyConsumerCount:
|
||||
legacyConsumerMigrationReports.legacyConsumerCount,
|
||||
createdCircuitCount:
|
||||
legacyConsumerMigrationReports.createdCircuitCount,
|
||||
createdDeviceRowCount:
|
||||
legacyConsumerMigrationReports.createdDeviceRowCount,
|
||||
duplicateGroupedCount:
|
||||
legacyConsumerMigrationReports.duplicateGroupedCount,
|
||||
generatedIdentifierCount:
|
||||
legacyConsumerMigrationReports.generatedIdentifierCount,
|
||||
unassignedRowCount:
|
||||
legacyConsumerMigrationReports.unassignedRowCount,
|
||||
warningsJson: legacyConsumerMigrationReports.warningsJson,
|
||||
generatedIdentifiersJson:
|
||||
legacyConsumerMigrationReports.generatedIdentifiersJson,
|
||||
duplicateGroupsJson:
|
||||
legacyConsumerMigrationReports.duplicateGroupsJson,
|
||||
createdAtIso: legacyConsumerMigrationReports.createdAtIso,
|
||||
})
|
||||
.from(legacyConsumerMigrationReports)
|
||||
.innerJoin(
|
||||
circuitLists,
|
||||
eq(circuitLists.id, legacyConsumerMigrationReports.circuitListId)
|
||||
)
|
||||
.where(eq(circuitLists.projectId, projectId))
|
||||
.all();
|
||||
return { consumerLinks, migrationMappings, migrationReports };
|
||||
}
|
||||
|
||||
function restoreCompatibleUpgradeOnlyData(
|
||||
database: AppDatabase,
|
||||
state: ProjectStateSnapshot,
|
||||
data: ReturnType<typeof captureUpgradeOnlyData>
|
||||
) {
|
||||
const boardIds = new Set(
|
||||
state.distributionBoards.map((entry) => entry.id)
|
||||
);
|
||||
const listIds = new Set(state.circuitLists.map((entry) => entry.id));
|
||||
const deviceIds = new Set(
|
||||
state.projectDevices.map((entry) => entry.id)
|
||||
);
|
||||
const roomIds = new Set(state.rooms.map((entry) => entry.id));
|
||||
const circuitIds = new Set(state.circuits.map((entry) => entry.id));
|
||||
const rowIds = new Set(
|
||||
state.circuits.flatMap((circuit) =>
|
||||
circuit.deviceRows.map((entry) => entry.id)
|
||||
)
|
||||
);
|
||||
|
||||
for (const link of data.consumerLinks) {
|
||||
database
|
||||
.update(consumers)
|
||||
.set({
|
||||
distributionBoardId:
|
||||
link.distributionBoardId &&
|
||||
boardIds.has(link.distributionBoardId)
|
||||
? link.distributionBoardId
|
||||
: null,
|
||||
circuitListId:
|
||||
link.circuitListId && listIds.has(link.circuitListId)
|
||||
? link.circuitListId
|
||||
: null,
|
||||
projectDeviceId:
|
||||
link.projectDeviceId && deviceIds.has(link.projectDeviceId)
|
||||
? link.projectDeviceId
|
||||
: null,
|
||||
roomId:
|
||||
link.roomId && roomIds.has(link.roomId) ? link.roomId : null,
|
||||
})
|
||||
.where(eq(consumers.id, link.id))
|
||||
.run();
|
||||
}
|
||||
insertMany(
|
||||
database,
|
||||
legacyConsumerCircuitMigrations,
|
||||
data.migrationMappings.filter(
|
||||
(entry) =>
|
||||
listIds.has(entry.circuitListId) &&
|
||||
circuitIds.has(entry.circuitId) &&
|
||||
rowIds.has(entry.circuitDeviceRowId)
|
||||
)
|
||||
);
|
||||
insertMany(
|
||||
database,
|
||||
legacyConsumerMigrationReports,
|
||||
data.migrationReports.filter((entry) =>
|
||||
listIds.has(entry.circuitListId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function insertMany<TTable extends Parameters<AppDatabase["insert"]>[0]>(
|
||||
|
||||
@@ -145,9 +145,6 @@ export function readProjectStateSnapshot(
|
||||
equipmentIdentifier: circuits.equipmentIdentifier,
|
||||
displayName: circuits.displayName,
|
||||
sortOrder: circuits.sortOrder,
|
||||
protectionType: circuits.protectionType,
|
||||
protectionRatedCurrent: circuits.protectionRatedCurrent,
|
||||
protectionCharacteristic: circuits.protectionCharacteristic,
|
||||
cableType: circuits.cableType,
|
||||
cableCrossSection: circuits.cableCrossSection,
|
||||
cableLength: circuits.cableLength,
|
||||
@@ -173,7 +170,6 @@ export function readProjectStateSnapshot(
|
||||
id: circuitDeviceRows.id,
|
||||
circuitId: circuitDeviceRows.circuitId,
|
||||
linkedProjectDeviceId: circuitDeviceRows.linkedProjectDeviceId,
|
||||
legacyConsumerId: circuitDeviceRows.legacyConsumerId,
|
||||
sortOrder: circuitDeviceRows.sortOrder,
|
||||
name: circuitDeviceRows.name,
|
||||
displayName: circuitDeviceRows.displayName,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import { and, asc, eq } from "drizzle-orm";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { rooms } from "../schema/rooms.js";
|
||||
|
||||
@@ -13,4 +13,12 @@ export class RoomRepository {
|
||||
.where(eq(rooms.projectId, projectId))
|
||||
.orderBy(asc(rooms.roomNumber), asc(rooms.roomName));
|
||||
}
|
||||
|
||||
async findById(projectId: string, roomId: string) {
|
||||
return this.database
|
||||
.select()
|
||||
.from(rooms)
|
||||
.where(and(eq(rooms.projectId, projectId), eq(rooms.id, roomId)))
|
||||
.get() ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ export const circuitDeviceRows = sqliteTable("circuit_device_rows", {
|
||||
linkedProjectDeviceId: text("linked_project_device_id").references(() => projectDevices.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
legacyConsumerId: text("legacy_consumer_id"),
|
||||
sortOrder: integer("sort_order").notNull().default(0),
|
||||
name: text("name").notNull(),
|
||||
displayName: text("display_name").notNull(),
|
||||
|
||||
@@ -15,9 +15,6 @@ export const circuits = sqliteTable(
|
||||
equipmentIdentifier: text("equipment_identifier").notNull(),
|
||||
displayName: text("display_name"),
|
||||
sortOrder: integer("sort_order").notNull().default(0),
|
||||
protectionType: text("protection_type"),
|
||||
protectionRatedCurrent: real("protection_rated_current"),
|
||||
protectionCharacteristic: text("protection_characteristic"),
|
||||
cableType: text("cable_type"),
|
||||
cableCrossSection: text("cable_cross_section"),
|
||||
cableLength: real("cable_length"),
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
import { circuitLists } from "./circuit-lists.js";
|
||||
import { distributionBoards } from "./distribution-boards.js";
|
||||
import { projectDevices } from "./project-devices.js";
|
||||
import { projects } from "./projects.js";
|
||||
import { rooms } from "./rooms.js";
|
||||
|
||||
export const consumers = sqliteTable("consumers", {
|
||||
id: text("id").primaryKey(),
|
||||
projectId: text("project_id")
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: "cascade" }),
|
||||
distributionBoardId: text("distribution_board_id").references(() => distributionBoards.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
circuitListId: text("circuit_list_id").references(() => circuitLists.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
projectDeviceId: text("project_device_id").references(() => projectDevices.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
isLinkedToDevice: integer("is_linked_to_device").notNull().default(0),
|
||||
roomId: text("room_id").references(() => rooms.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
circuitNumber: text("circuit_number"),
|
||||
description: text("description"),
|
||||
name: text("name").notNull(),
|
||||
category: text("category"),
|
||||
deviceType: text("device_type"),
|
||||
phaseType: text("phase_type"),
|
||||
tradeOrCostGroup: text("trade_or_cost_group"),
|
||||
group: text("group_name"),
|
||||
protectionType: text("protection_type"),
|
||||
protectionRatedCurrent: real("protection_rated_current"),
|
||||
protectionCharacteristic: text("protection_characteristic"),
|
||||
cableType: text("cable_type"),
|
||||
cableCrossSection: text("cable_cross_section"),
|
||||
comment: text("comment"),
|
||||
quantity: integer("quantity").notNull(),
|
||||
installedPowerPerUnitKw: real("installed_power_per_unit_kw").notNull(),
|
||||
demandFactor: real("demand_factor").notNull(),
|
||||
voltageV: real("voltage_v"),
|
||||
phaseCount: integer("phase_count"),
|
||||
powerFactor: real("power_factor"),
|
||||
note: text("note"),
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
import { circuitDeviceRows } from "./circuit-device-rows.js";
|
||||
import { circuitLists } from "./circuit-lists.js";
|
||||
import { circuits } from "./circuits.js";
|
||||
|
||||
export const legacyConsumerCircuitMigrations = sqliteTable("legacy_consumer_circuit_migrations", {
|
||||
consumerId: text("consumer_id").primaryKey(),
|
||||
circuitId: text("circuit_id")
|
||||
.notNull()
|
||||
.references(() => circuits.id, { onDelete: "cascade" }),
|
||||
circuitDeviceRowId: text("circuit_device_row_id")
|
||||
.notNull()
|
||||
.references(() => circuitDeviceRows.id, { onDelete: "cascade" }),
|
||||
circuitListId: text("circuit_list_id")
|
||||
.notNull()
|
||||
.references(() => circuitLists.id, { onDelete: "cascade" }),
|
||||
createdAtIso: text("created_at_iso").notNull(),
|
||||
});
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
|
||||
import { circuitLists } from "./circuit-lists.js";
|
||||
|
||||
export const legacyConsumerMigrationReports = sqliteTable(
|
||||
"legacy_consumer_migration_reports",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
circuitListId: text("circuit_list_id")
|
||||
.notNull()
|
||||
.references(() => circuitLists.id, { onDelete: "cascade" }),
|
||||
legacyConsumerCount: integer("legacy_consumer_count").notNull(),
|
||||
createdCircuitCount: integer("created_circuit_count").notNull(),
|
||||
createdDeviceRowCount: integer("created_device_row_count").notNull(),
|
||||
duplicateGroupedCount: integer("duplicate_grouped_count").notNull(),
|
||||
generatedIdentifierCount: integer("generated_identifier_count").notNull(),
|
||||
unassignedRowCount: integer("unassigned_row_count").notNull(),
|
||||
warningsJson: text("warnings_json").notNull(),
|
||||
generatedIdentifiersJson: text("generated_identifiers_json").notNull(),
|
||||
duplicateGroupsJson: text("duplicate_groups_json").notNull(),
|
||||
createdAtIso: text("created_at_iso").notNull(),
|
||||
},
|
||||
(table) => [unique("legacy_consumer_migration_reports_list_unique").on(table.circuitListId)]
|
||||
);
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface CircuitDeviceRowSnapshot {
|
||||
id: string;
|
||||
circuitId: string;
|
||||
linkedProjectDeviceId: string | null;
|
||||
legacyConsumerId: string | null;
|
||||
sortOrder: number;
|
||||
name: string;
|
||||
displayName: string;
|
||||
@@ -101,7 +100,6 @@ export function assertCircuitDeviceRowInsertProjectCommand(
|
||||
assertNonEmptyString(row.id, "row.id");
|
||||
assertNonEmptyString(row.circuitId, "row.circuitId");
|
||||
assertNullableString(row.linkedProjectDeviceId, "row.linkedProjectDeviceId");
|
||||
assertNullableString(row.legacyConsumerId, "row.legacyConsumerId");
|
||||
assertFiniteNumber(row.sortOrder, "row.sortOrder");
|
||||
assertNonEmptyString(row.name, "row.name");
|
||||
assertNonEmptyString(row.displayName, "row.displayName");
|
||||
|
||||
@@ -2,7 +2,6 @@ export interface CircuitDeviceRow {
|
||||
id: string;
|
||||
circuitId: string;
|
||||
linkedProjectDeviceId?: string;
|
||||
legacyConsumerId?: string;
|
||||
sortOrder: number;
|
||||
name: string;
|
||||
displayName: string;
|
||||
|
||||
@@ -8,9 +8,6 @@ export interface CircuitUpdateValues {
|
||||
equipmentIdentifier: string;
|
||||
displayName: string | null;
|
||||
sortOrder: number;
|
||||
protectionType: string | null;
|
||||
protectionRatedCurrent: number | null;
|
||||
protectionCharacteristic: string | null;
|
||||
cableType: string | null;
|
||||
cableCrossSection: string | null;
|
||||
cableLength: number | null;
|
||||
@@ -118,7 +115,6 @@ function assertCircuitUpdateFieldValue(
|
||||
return;
|
||||
}
|
||||
if (
|
||||
field === "protectionRatedCurrent" ||
|
||||
field === "cableLength" ||
|
||||
field === "voltage"
|
||||
) {
|
||||
@@ -147,9 +143,6 @@ function isCircuitUpdateField(value: string): value is CircuitUpdateField {
|
||||
"equipmentIdentifier",
|
||||
"displayName",
|
||||
"sortOrder",
|
||||
"protectionType",
|
||||
"protectionRatedCurrent",
|
||||
"protectionCharacteristic",
|
||||
"cableType",
|
||||
"cableCrossSection",
|
||||
"cableLength",
|
||||
|
||||
@@ -21,9 +21,6 @@ export interface CircuitSnapshot {
|
||||
equipmentIdentifier: string;
|
||||
displayName: string | null;
|
||||
sortOrder: number;
|
||||
protectionType: string | null;
|
||||
protectionRatedCurrent: number | null;
|
||||
protectionCharacteristic: string | null;
|
||||
cableType: string | null;
|
||||
cableCrossSection: string | null;
|
||||
cableLength: number | null;
|
||||
@@ -115,8 +112,6 @@ export function assertCircuitInsertProjectCommand(
|
||||
assertNullableString(circuit.displayName, "circuit.displayName");
|
||||
assertFiniteNumber(circuit.sortOrder, "circuit.sortOrder");
|
||||
for (const field of [
|
||||
"protectionType",
|
||||
"protectionCharacteristic",
|
||||
"cableType",
|
||||
"cableCrossSection",
|
||||
"rcdAssignment",
|
||||
@@ -127,10 +122,6 @@ export function assertCircuitInsertProjectCommand(
|
||||
] as const) {
|
||||
assertNullableString(circuit[field], `circuit.${field}`);
|
||||
}
|
||||
assertNullableNonNegativeNumber(
|
||||
circuit.protectionRatedCurrent,
|
||||
"circuit.protectionRatedCurrent"
|
||||
);
|
||||
assertNullableNonNegativeNumber(
|
||||
circuit.cableLength,
|
||||
"circuit.cableLength"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export interface CircuitTreeDeviceRow {
|
||||
id: string;
|
||||
linkedProjectDeviceId?: string;
|
||||
legacyConsumerId?: string;
|
||||
sortOrder: number;
|
||||
name: string;
|
||||
displayName: string;
|
||||
@@ -29,9 +28,6 @@ export interface CircuitTreeCircuit {
|
||||
equipmentIdentifier: string;
|
||||
displayName?: string;
|
||||
sortOrder: number;
|
||||
protectionType?: string;
|
||||
protectionRatedCurrent?: number;
|
||||
protectionCharacteristic?: string;
|
||||
cableType?: string;
|
||||
cableCrossSection?: string;
|
||||
cableLength?: number;
|
||||
@@ -73,17 +69,6 @@ export interface CircuitTreeResponse {
|
||||
footerComponents: CircuitTreeComponent[];
|
||||
}
|
||||
|
||||
export interface LegacyMigrationReport {
|
||||
circuitListId: string;
|
||||
legacyConsumerCount: number;
|
||||
createdCircuitCount: number;
|
||||
createdDeviceRowCount: number;
|
||||
groupedDuplicateCircuitNumbers: Array<{ normalizedCircuitNumber: string; count: number }>;
|
||||
generatedIdentifiers: string[];
|
||||
unassignedRows: Array<{ consumerId: string; reason: string }>;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js";
|
||||
import type {
|
||||
DistributionBoardComponentPlacement,
|
||||
|
||||
@@ -5,9 +5,6 @@ export interface Circuit {
|
||||
equipmentIdentifier: string;
|
||||
displayName?: string;
|
||||
sortOrder: number;
|
||||
protectionType?: string;
|
||||
protectionRatedCurrent?: number;
|
||||
protectionCharacteristic?: string;
|
||||
cableType?: string;
|
||||
cableCrossSection?: string;
|
||||
cableLength?: number;
|
||||
|
||||
@@ -6,8 +6,7 @@ import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||
|
||||
export const distributionBoardUpdateCommandType =
|
||||
"distribution-board.update" as const;
|
||||
export const legacyDistributionBoardUpdateCommandSchemaVersion = 1 as const;
|
||||
export const distributionBoardUpdateCommandSchemaVersion = 2 as const;
|
||||
export const distributionBoardUpdateCommandSchemaVersion = 1 as const;
|
||||
|
||||
export interface DistributionBoardUpdateValues {
|
||||
floorId: string | null;
|
||||
@@ -33,27 +32,17 @@ export interface DistributionBoardUpdateCommandPayload {
|
||||
changes: DistributionBoardUpdateFieldChange[];
|
||||
}
|
||||
|
||||
interface CurrentDistributionBoardUpdateProjectCommand
|
||||
export interface DistributionBoardUpdateProjectCommand
|
||||
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
|
||||
schemaVersion: typeof distributionBoardUpdateCommandSchemaVersion;
|
||||
type: typeof distributionBoardUpdateCommandType;
|
||||
}
|
||||
|
||||
interface LegacyDistributionBoardUpdateProjectCommand
|
||||
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
|
||||
schemaVersion: typeof legacyDistributionBoardUpdateCommandSchemaVersion;
|
||||
type: typeof distributionBoardUpdateCommandType;
|
||||
}
|
||||
|
||||
export type DistributionBoardUpdateProjectCommand =
|
||||
| CurrentDistributionBoardUpdateProjectCommand
|
||||
| LegacyDistributionBoardUpdateProjectCommand;
|
||||
|
||||
export function createDistributionBoardUpdateProjectCommand(
|
||||
distributionBoardId: string,
|
||||
patch: DistributionBoardUpdatePatch
|
||||
): CurrentDistributionBoardUpdateProjectCommand {
|
||||
const command: CurrentDistributionBoardUpdateProjectCommand = {
|
||||
): DistributionBoardUpdateProjectCommand {
|
||||
const command: DistributionBoardUpdateProjectCommand = {
|
||||
schemaVersion: distributionBoardUpdateCommandSchemaVersion,
|
||||
type: distributionBoardUpdateCommandType,
|
||||
payload: {
|
||||
@@ -72,9 +61,7 @@ export function assertDistributionBoardUpdateProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is DistributionBoardUpdateProjectCommand {
|
||||
if (
|
||||
(command.schemaVersion !==
|
||||
legacyDistributionBoardUpdateCommandSchemaVersion &&
|
||||
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion) ||
|
||||
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion ||
|
||||
command.type !== distributionBoardUpdateCommandType ||
|
||||
!isPlainObject(command.payload)
|
||||
) {
|
||||
@@ -97,9 +84,7 @@ export function assertDistributionBoardUpdateProjectCommand(
|
||||
!isPlainObject(change) ||
|
||||
(change.field !== "floorId" &&
|
||||
change.field !== "supplyType" &&
|
||||
(command.schemaVersion ===
|
||||
legacyDistributionBoardUpdateCommandSchemaVersion ||
|
||||
change.field !== "simultaneityFactor")) ||
|
||||
change.field !== "simultaneityFactor") ||
|
||||
seen.has(change.field)
|
||||
) {
|
||||
throw new Error(
|
||||
|
||||
@@ -18,36 +18,7 @@ export const distributionBoardInsertCommandType =
|
||||
"distribution-board.insert" as const;
|
||||
export const distributionBoardDeleteCommandType =
|
||||
"distribution-board.delete" as const;
|
||||
export const legacyDistributionBoardStructureCommandSchemaVersion = 1 as const;
|
||||
export const previousDistributionBoardStructureCommandSchemaVersion = 2 as const;
|
||||
export const distributionBoardStructureCommandSchemaVersion = 3 as const;
|
||||
|
||||
const legacyDefaultCircuitSectionDefinitions = [
|
||||
{
|
||||
key: "lighting",
|
||||
displayName: "Lighting",
|
||||
prefix: "-1F",
|
||||
sortOrder: 10,
|
||||
},
|
||||
{
|
||||
key: "single_phase",
|
||||
displayName: "Single-phase circuits",
|
||||
prefix: "-2F",
|
||||
sortOrder: 20,
|
||||
},
|
||||
{
|
||||
key: "three_phase",
|
||||
displayName: "Three-phase circuits",
|
||||
prefix: "-3F",
|
||||
sortOrder: 30,
|
||||
},
|
||||
{
|
||||
key: "unassigned",
|
||||
displayName: "Unassigned",
|
||||
prefix: "-UF",
|
||||
sortOrder: 90,
|
||||
},
|
||||
] as const;
|
||||
export const distributionBoardStructureCommandSchemaVersion = 1 as const;
|
||||
|
||||
export const defaultCircuitSectionDefinitions = [
|
||||
{
|
||||
@@ -76,31 +47,6 @@ export const defaultCircuitSectionDefinitions = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface LegacyDistributionBoardStructureSnapshot {
|
||||
distributionBoard: {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
};
|
||||
circuitList: DistributionBoardStructureSnapshot["circuitList"];
|
||||
sections: LegacyCircuitSectionSnapshot[];
|
||||
}
|
||||
|
||||
interface PreviousDistributionBoardStructureSnapshot {
|
||||
distributionBoard: DistributionBoardStructureSnapshot["distributionBoard"];
|
||||
circuitList: DistributionBoardStructureSnapshot["circuitList"];
|
||||
sections: LegacyCircuitSectionSnapshot[];
|
||||
}
|
||||
|
||||
interface LegacyCircuitSectionSnapshot {
|
||||
id: string;
|
||||
circuitListId: string;
|
||||
key: string;
|
||||
displayName: string;
|
||||
prefix: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface DistributionBoardStructureSnapshot {
|
||||
distributionBoard: {
|
||||
id: string;
|
||||
@@ -140,22 +86,15 @@ export interface DistributionBoardStructureSnapshot {
|
||||
export interface NormalizedDistributionBoardStructureSnapshot {
|
||||
distributionBoard: DistributionBoardStructureSnapshot["distributionBoard"];
|
||||
circuitList: DistributionBoardStructureSnapshot["circuitList"];
|
||||
sections: Array<
|
||||
LegacyCircuitSectionSnapshot & {
|
||||
category: CircuitGroupCategory | null;
|
||||
groupNumber: number | null;
|
||||
}
|
||||
>;
|
||||
sections: DistributionBoardStructureSnapshot["sections"];
|
||||
components: DistributionBoardStructureSnapshot["components"];
|
||||
}
|
||||
|
||||
interface DistributionBoardStructureCommandPayload<
|
||||
TStructure = DistributionBoardStructureSnapshot,
|
||||
> {
|
||||
structure: TStructure;
|
||||
interface DistributionBoardStructureCommandPayload {
|
||||
structure: DistributionBoardStructureSnapshot;
|
||||
}
|
||||
|
||||
interface CurrentDistributionBoardStructureProjectCommand
|
||||
interface DistributionBoardStructureProjectCommandBase
|
||||
extends SerializedProjectCommand<DistributionBoardStructureCommandPayload> {
|
||||
schemaVersion: typeof distributionBoardStructureCommandSchemaVersion;
|
||||
type:
|
||||
@@ -163,41 +102,19 @@ interface CurrentDistributionBoardStructureProjectCommand
|
||||
| typeof distributionBoardDeleteCommandType;
|
||||
}
|
||||
|
||||
interface LegacyDistributionBoardStructureProjectCommand
|
||||
extends SerializedProjectCommand<
|
||||
DistributionBoardStructureCommandPayload<LegacyDistributionBoardStructureSnapshot>
|
||||
> {
|
||||
schemaVersion: typeof legacyDistributionBoardStructureCommandSchemaVersion;
|
||||
type:
|
||||
| typeof distributionBoardInsertCommandType
|
||||
| typeof distributionBoardDeleteCommandType;
|
||||
}
|
||||
|
||||
interface PreviousDistributionBoardStructureProjectCommand
|
||||
extends SerializedProjectCommand<
|
||||
DistributionBoardStructureCommandPayload<PreviousDistributionBoardStructureSnapshot>
|
||||
> {
|
||||
schemaVersion: typeof previousDistributionBoardStructureCommandSchemaVersion;
|
||||
type:
|
||||
| typeof distributionBoardInsertCommandType
|
||||
| typeof distributionBoardDeleteCommandType;
|
||||
}
|
||||
|
||||
export interface DistributionBoardInsertProjectCommand
|
||||
extends CurrentDistributionBoardStructureProjectCommand {
|
||||
extends DistributionBoardStructureProjectCommandBase {
|
||||
type: typeof distributionBoardInsertCommandType;
|
||||
}
|
||||
|
||||
export interface DistributionBoardDeleteProjectCommand
|
||||
extends CurrentDistributionBoardStructureProjectCommand {
|
||||
extends DistributionBoardStructureProjectCommandBase {
|
||||
type: typeof distributionBoardDeleteCommandType;
|
||||
}
|
||||
|
||||
export type DistributionBoardStructureProjectCommand =
|
||||
| DistributionBoardInsertProjectCommand
|
||||
| DistributionBoardDeleteProjectCommand
|
||||
| PreviousDistributionBoardStructureProjectCommand
|
||||
| LegacyDistributionBoardStructureProjectCommand;
|
||||
| DistributionBoardDeleteProjectCommand;
|
||||
|
||||
export function createDistributionBoardStructureSnapshot(
|
||||
projectId: string,
|
||||
@@ -296,35 +213,6 @@ export function assertDistributionBoardDeleteProjectCommand(
|
||||
export function normalizeDistributionBoardStructureProjectCommand(
|
||||
command: DistributionBoardStructureProjectCommand
|
||||
): NormalizedDistributionBoardStructureSnapshot {
|
||||
if (
|
||||
command.schemaVersion ===
|
||||
legacyDistributionBoardStructureCommandSchemaVersion
|
||||
) {
|
||||
return {
|
||||
distributionBoard: {
|
||||
...command.payload.structure.distributionBoard,
|
||||
floorId: null,
|
||||
supplyType: null,
|
||||
},
|
||||
circuitList: command.payload.structure.circuitList,
|
||||
sections: command.payload.structure.sections.map(
|
||||
normalizeLegacySection
|
||||
),
|
||||
components: [],
|
||||
};
|
||||
}
|
||||
if (
|
||||
command.schemaVersion ===
|
||||
previousDistributionBoardStructureCommandSchemaVersion
|
||||
) {
|
||||
return {
|
||||
...command.payload.structure,
|
||||
sections: command.payload.structure.sections.map(
|
||||
normalizeLegacySection
|
||||
),
|
||||
components: [],
|
||||
};
|
||||
}
|
||||
return command.payload.structure;
|
||||
}
|
||||
|
||||
@@ -340,26 +228,10 @@ export function invertDistributionBoardStructureProjectCommand(
|
||||
} as DistributionBoardStructureProjectCommand;
|
||||
}
|
||||
|
||||
function normalizeLegacySection(
|
||||
section: LegacyCircuitSectionSnapshot
|
||||
): NormalizedDistributionBoardStructureSnapshot["sections"][number] {
|
||||
const category =
|
||||
section.key === "lighting" ||
|
||||
section.key === "single_phase" ||
|
||||
section.key === "three_phase"
|
||||
? section.key
|
||||
: null;
|
||||
return {
|
||||
...section,
|
||||
category,
|
||||
groupNumber: category === null ? null : 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function assertDistributionBoardStructureSnapshot(
|
||||
structure: unknown
|
||||
): asserts structure is DistributionBoardStructureSnapshot {
|
||||
assertDistributionBoardStructureSnapshotVersion(structure, "current");
|
||||
assertDistributionBoardStructureSnapshotVersion(structure);
|
||||
}
|
||||
|
||||
function assertDistributionBoardStructureProjectCommand(
|
||||
@@ -375,63 +247,36 @@ function assertDistributionBoardStructureProjectCommand(
|
||||
) {
|
||||
throw new Error("Unsupported distribution-board structure command.");
|
||||
}
|
||||
if (
|
||||
command.schemaVersion ===
|
||||
legacyDistributionBoardStructureCommandSchemaVersion
|
||||
) {
|
||||
assertDistributionBoardStructureSnapshotVersion(
|
||||
command.payload.structure,
|
||||
"legacy"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
command.schemaVersion ===
|
||||
previousDistributionBoardStructureCommandSchemaVersion
|
||||
) {
|
||||
assertDistributionBoardStructureSnapshotVersion(
|
||||
command.payload.structure,
|
||||
"previous"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
command.schemaVersion !==
|
||||
distributionBoardStructureCommandSchemaVersion
|
||||
) {
|
||||
throw new Error("Unsupported distribution-board structure command.");
|
||||
}
|
||||
assertDistributionBoardStructureSnapshotVersion(
|
||||
command.payload.structure,
|
||||
"current"
|
||||
);
|
||||
assertDistributionBoardStructureSnapshotVersion(command.payload.structure);
|
||||
}
|
||||
|
||||
function assertDistributionBoardStructureSnapshotVersion(
|
||||
structure: unknown,
|
||||
version: "legacy" | "previous" | "current"
|
||||
structure: unknown
|
||||
) {
|
||||
const current = version === "current";
|
||||
if (
|
||||
!isPlainObject(structure) ||
|
||||
Object.keys(structure).length !== (current ? 4 : 3)
|
||||
Object.keys(structure).length !== 4
|
||||
) {
|
||||
throw new Error("Distribution-board structure is invalid.");
|
||||
}
|
||||
const { distributionBoard, circuitList, sections, components } =
|
||||
structure;
|
||||
const expectedSections = current
|
||||
? defaultCircuitSectionDefinitions
|
||||
: legacyDefaultCircuitSectionDefinitions;
|
||||
const expectedSections = defaultCircuitSectionDefinitions;
|
||||
if (
|
||||
!isPlainObject(distributionBoard) ||
|
||||
Object.keys(distributionBoard).length !==
|
||||
(version === "legacy" ? 3 : 5) ||
|
||||
Object.keys(distributionBoard).length !== 5 ||
|
||||
!isPlainObject(circuitList) ||
|
||||
Object.keys(circuitList).length !== 4 ||
|
||||
!Array.isArray(sections) ||
|
||||
sections.length !== expectedSections.length ||
|
||||
(current && (!Array.isArray(components) || components.length !== 2))
|
||||
!Array.isArray(components) ||
|
||||
components.length !== 2
|
||||
) {
|
||||
throw new Error("Distribution-board structure is incomplete.");
|
||||
}
|
||||
@@ -441,10 +286,8 @@ function assertDistributionBoardStructureSnapshotVersion(
|
||||
`distributionBoard.${field}`
|
||||
);
|
||||
}
|
||||
if (version !== "legacy") {
|
||||
assertNullableId(distributionBoard.floorId, "distributionBoard.floorId");
|
||||
assertNullableSupplyType(distributionBoard.supplyType);
|
||||
}
|
||||
for (const [field, value] of Object.entries(circuitList)) {
|
||||
assertNonEmptyString(value, `circuitList.${field}`);
|
||||
}
|
||||
@@ -462,7 +305,7 @@ function assertDistributionBoardStructureSnapshotVersion(
|
||||
const expected = expectedSections[index];
|
||||
if (
|
||||
!isPlainObject(section) ||
|
||||
Object.keys(section).length !== (current ? 8 : 6)
|
||||
Object.keys(section).length !== 8
|
||||
) {
|
||||
throw new Error("Default circuit section is invalid.");
|
||||
}
|
||||
@@ -477,10 +320,8 @@ function assertDistributionBoardStructureSnapshotVersion(
|
||||
section.displayName !== expected.displayName ||
|
||||
section.prefix !== expected.prefix ||
|
||||
section.sortOrder !== expected.sortOrder ||
|
||||
(current &&
|
||||
(!("category" in expected) ||
|
||||
section.category !== expected.category ||
|
||||
section.groupNumber !== expected.groupNumber))
|
||||
section.groupNumber !== expected.groupNumber
|
||||
) {
|
||||
throw new Error(
|
||||
"Distribution-board structure has invalid default sections."
|
||||
@@ -488,10 +329,8 @@ function assertDistributionBoardStructureSnapshotVersion(
|
||||
}
|
||||
}
|
||||
|
||||
if (current) {
|
||||
assertDefaultComponents(components, circuitList.id);
|
||||
}
|
||||
}
|
||||
|
||||
function assertDefaultComponents(
|
||||
components: unknown,
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||
import {
|
||||
assertDistributionBoardSubtreeSnapshot,
|
||||
type DistributionBoardSubtreeSnapshot,
|
||||
} from "./distribution-board-subtree-snapshot.model.js";
|
||||
|
||||
export const distributionBoardInsertSubtreeCommandType =
|
||||
"distribution-board.insert-subtree" as const;
|
||||
export const distributionBoardDeleteSubtreeCommandType =
|
||||
"distribution-board.delete-subtree" as const;
|
||||
export const distributionBoardSubtreeCommandSchemaVersion = 1 as const;
|
||||
|
||||
interface Payload {
|
||||
snapshot: DistributionBoardSubtreeSnapshot;
|
||||
}
|
||||
|
||||
export interface DistributionBoardInsertSubtreeProjectCommand
|
||||
extends SerializedProjectCommand<Payload> {
|
||||
schemaVersion: typeof distributionBoardSubtreeCommandSchemaVersion;
|
||||
type: typeof distributionBoardInsertSubtreeCommandType;
|
||||
}
|
||||
|
||||
export interface DistributionBoardDeleteSubtreeProjectCommand
|
||||
extends SerializedProjectCommand<Payload> {
|
||||
schemaVersion: typeof distributionBoardSubtreeCommandSchemaVersion;
|
||||
type: typeof distributionBoardDeleteSubtreeCommandType;
|
||||
}
|
||||
|
||||
export type DistributionBoardSubtreeProjectCommand =
|
||||
| DistributionBoardInsertSubtreeProjectCommand
|
||||
| DistributionBoardDeleteSubtreeProjectCommand;
|
||||
|
||||
export function createDistributionBoardInsertSubtreeProjectCommand(
|
||||
snapshot: DistributionBoardSubtreeSnapshot
|
||||
): DistributionBoardInsertSubtreeProjectCommand {
|
||||
const command: DistributionBoardInsertSubtreeProjectCommand = {
|
||||
schemaVersion: distributionBoardSubtreeCommandSchemaVersion,
|
||||
type: distributionBoardInsertSubtreeCommandType,
|
||||
payload: { snapshot },
|
||||
};
|
||||
assertDistributionBoardInsertSubtreeProjectCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function createDistributionBoardDeleteSubtreeProjectCommand(
|
||||
snapshot: DistributionBoardSubtreeSnapshot
|
||||
): DistributionBoardDeleteSubtreeProjectCommand {
|
||||
const command: DistributionBoardDeleteSubtreeProjectCommand = {
|
||||
schemaVersion: distributionBoardSubtreeCommandSchemaVersion,
|
||||
type: distributionBoardDeleteSubtreeCommandType,
|
||||
payload: { snapshot },
|
||||
};
|
||||
assertDistributionBoardDeleteSubtreeProjectCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function assertDistributionBoardInsertSubtreeProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is DistributionBoardInsertSubtreeProjectCommand {
|
||||
assertCommand(command, distributionBoardInsertSubtreeCommandType);
|
||||
}
|
||||
|
||||
export function assertDistributionBoardDeleteSubtreeProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is DistributionBoardDeleteSubtreeProjectCommand {
|
||||
assertCommand(command, distributionBoardDeleteSubtreeCommandType);
|
||||
}
|
||||
|
||||
function assertCommand(
|
||||
command: SerializedProjectCommand<unknown>,
|
||||
type: DistributionBoardSubtreeProjectCommand["type"]
|
||||
) {
|
||||
if (
|
||||
command.schemaVersion !== distributionBoardSubtreeCommandSchemaVersion ||
|
||||
command.type !== type ||
|
||||
!isPlainObject(command.payload) ||
|
||||
Object.keys(command.payload).length !== 1
|
||||
) {
|
||||
throw new Error("Unsupported distribution-board subtree command.");
|
||||
}
|
||||
assertDistributionBoardSubtreeSnapshot(command.payload.snapshot);
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
import crypto from "node:crypto";
|
||||
import {
|
||||
circuitGroupCategories,
|
||||
type CircuitGroupCategory,
|
||||
} from "../../shared/constants/circuit-group.js";
|
||||
import {
|
||||
distributionBoardComponentPlacements,
|
||||
distributionBoardComponentRoles,
|
||||
type DistributionBoardComponentPlacement,
|
||||
type DistributionBoardComponentRole,
|
||||
} from "../../shared/constants/distribution-board-component.js";
|
||||
import {
|
||||
distributionBoardSupplyTypes,
|
||||
type DistributionBoardSupplyType,
|
||||
} from "../../shared/constants/distribution-board.js";
|
||||
import type {
|
||||
BreakerTripCharacteristic,
|
||||
FuseUtilizationCategory,
|
||||
ProtectionDeviceType,
|
||||
RcdType,
|
||||
} from "../../shared/constants/protection-device.js";
|
||||
import { protectionDeviceConfigurationSchema } from "../../shared/validation/protection-device.schemas.js";
|
||||
import {
|
||||
assertCircuitInsertProjectCommand,
|
||||
circuitInsertCommandType,
|
||||
circuitStructureCommandSchemaVersion,
|
||||
type CircuitSnapshot,
|
||||
} from "./circuit-structure-project-command.model.js";
|
||||
|
||||
export interface DistributionBoardSubtreeSectionSnapshot {
|
||||
id: string;
|
||||
circuitListId: string;
|
||||
key: string;
|
||||
displayName: string;
|
||||
prefix: string;
|
||||
sortOrder: number;
|
||||
category: CircuitGroupCategory | null;
|
||||
groupNumber: number | null;
|
||||
}
|
||||
|
||||
export interface DistributionBoardSubtreeComponentProtectionSnapshot {
|
||||
componentId: string;
|
||||
type: ProtectionDeviceType;
|
||||
ratedCurrentA: number;
|
||||
fuseUtilizationCategory: FuseUtilizationCategory | null;
|
||||
tripCharacteristic: BreakerTripCharacteristic | null;
|
||||
rcdType: RcdType | null;
|
||||
ratedResidualCurrentMa: number | null;
|
||||
}
|
||||
|
||||
export interface DistributionBoardSubtreeComponentSnapshot {
|
||||
component: {
|
||||
id: string;
|
||||
circuitListId: string;
|
||||
sectionId: string | null;
|
||||
equipmentIdentifier: string;
|
||||
name: string;
|
||||
role: DistributionBoardComponentRole;
|
||||
placement: DistributionBoardComponentPlacement;
|
||||
sortOrder: number;
|
||||
};
|
||||
protectionDevice:
|
||||
| DistributionBoardSubtreeComponentProtectionSnapshot
|
||||
| null;
|
||||
}
|
||||
|
||||
export interface DistributionBoardSubtreeSnapshot {
|
||||
distributionBoard: {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
floorId: string | null;
|
||||
supplyType: DistributionBoardSupplyType | null;
|
||||
simultaneityFactor: number;
|
||||
};
|
||||
circuitList: {
|
||||
id: string;
|
||||
projectId: string;
|
||||
distributionBoardId: string;
|
||||
name: string;
|
||||
};
|
||||
sections: DistributionBoardSubtreeSectionSnapshot[];
|
||||
components: DistributionBoardSubtreeComponentSnapshot[];
|
||||
circuits: CircuitSnapshot[];
|
||||
}
|
||||
|
||||
export function cloneDistributionBoardSubtree(
|
||||
source: DistributionBoardSubtreeSnapshot,
|
||||
name: string,
|
||||
createId: () => string = () => crypto.randomUUID()
|
||||
): DistributionBoardSubtreeSnapshot {
|
||||
assertDistributionBoardSubtreeSnapshot(source);
|
||||
const normalizedName = name.trim();
|
||||
assertNonEmptyString(normalizedName, "name");
|
||||
|
||||
const distributionBoardId = createId();
|
||||
const circuitListId = distributionBoardId;
|
||||
const sectionIds = new Map(
|
||||
source.sections.map((section) => [section.id, createId()])
|
||||
);
|
||||
const circuitIds = new Map(
|
||||
source.circuits.map((circuit) => [circuit.id, createId()])
|
||||
);
|
||||
const componentIds = new Map(
|
||||
source.components.map(({ component }) => [component.id, createId()])
|
||||
);
|
||||
|
||||
const clone: DistributionBoardSubtreeSnapshot = {
|
||||
distributionBoard: {
|
||||
...source.distributionBoard,
|
||||
id: distributionBoardId,
|
||||
name: normalizedName,
|
||||
},
|
||||
circuitList: {
|
||||
...source.circuitList,
|
||||
id: circuitListId,
|
||||
distributionBoardId,
|
||||
name: `${normalizedName} Stromkreisliste`,
|
||||
},
|
||||
sections: source.sections.map((section) => ({
|
||||
...section,
|
||||
id: requiredMappedId(sectionIds, section.id),
|
||||
circuitListId,
|
||||
})),
|
||||
components: source.components.map(
|
||||
({ component, protectionDevice }) => {
|
||||
const componentId = requiredMappedId(
|
||||
componentIds,
|
||||
component.id
|
||||
);
|
||||
return {
|
||||
component: {
|
||||
...component,
|
||||
id: componentId,
|
||||
circuitListId,
|
||||
sectionId:
|
||||
component.sectionId === null
|
||||
? null
|
||||
: requiredMappedId(sectionIds, component.sectionId),
|
||||
},
|
||||
protectionDevice:
|
||||
protectionDevice === null
|
||||
? null
|
||||
: { ...protectionDevice, componentId },
|
||||
};
|
||||
}
|
||||
),
|
||||
circuits: source.circuits.map((circuit) => {
|
||||
const circuitId = requiredMappedId(circuitIds, circuit.id);
|
||||
return {
|
||||
...circuit,
|
||||
id: circuitId,
|
||||
circuitListId,
|
||||
sectionId: requiredMappedId(sectionIds, circuit.sectionId),
|
||||
deviceRows: circuit.deviceRows.map((row) => ({
|
||||
...row,
|
||||
id: createId(),
|
||||
circuitId,
|
||||
})),
|
||||
protectionDevice:
|
||||
circuit.protectionDevice === null ||
|
||||
circuit.protectionDevice === undefined
|
||||
? null
|
||||
: { ...circuit.protectionDevice, circuitId },
|
||||
};
|
||||
}),
|
||||
};
|
||||
assertDistributionBoardSubtreeSnapshot(clone);
|
||||
return clone;
|
||||
}
|
||||
|
||||
export function assertDistributionBoardSubtreeSnapshot(
|
||||
value: unknown
|
||||
): asserts value is DistributionBoardSubtreeSnapshot {
|
||||
if (
|
||||
!isPlainObject(value) ||
|
||||
Object.keys(value).length !== 5 ||
|
||||
!isPlainObject(value.distributionBoard) ||
|
||||
!isPlainObject(value.circuitList) ||
|
||||
!Array.isArray(value.sections) ||
|
||||
!Array.isArray(value.components) ||
|
||||
!Array.isArray(value.circuits)
|
||||
) {
|
||||
throw new Error("Distribution-board subtree snapshot is invalid.");
|
||||
}
|
||||
const board = value.distributionBoard;
|
||||
const list = value.circuitList;
|
||||
for (const field of ["id", "projectId", "name"] as const) {
|
||||
assertNonEmptyString(board[field], `distributionBoard.${field}`);
|
||||
}
|
||||
assertNullableString(board.floorId, "distributionBoard.floorId");
|
||||
if (
|
||||
board.supplyType !== null &&
|
||||
!distributionBoardSupplyTypes.includes(
|
||||
board.supplyType as DistributionBoardSupplyType
|
||||
)
|
||||
) {
|
||||
throw new Error("Distribution-board supply type is invalid.");
|
||||
}
|
||||
assertFiniteNumber(
|
||||
board.simultaneityFactor,
|
||||
"distributionBoard.simultaneityFactor"
|
||||
);
|
||||
if (board.simultaneityFactor < 0 || board.simultaneityFactor > 1) {
|
||||
throw new Error("Distribution-board simultaneity factor is invalid.");
|
||||
}
|
||||
for (const field of [
|
||||
"id",
|
||||
"projectId",
|
||||
"distributionBoardId",
|
||||
"name",
|
||||
] as const) {
|
||||
assertNonEmptyString(list[field], `circuitList.${field}`);
|
||||
}
|
||||
const circuitListId = list.id;
|
||||
assertNonEmptyString(circuitListId, "circuitList.id");
|
||||
if (
|
||||
list.projectId !== board.projectId ||
|
||||
list.distributionBoardId !== board.id
|
||||
) {
|
||||
throw new Error("Distribution-board circuit list ownership is invalid.");
|
||||
}
|
||||
|
||||
const sectionIds = new Set<string>();
|
||||
const sectionKeys = new Set<string>();
|
||||
const sectionPrefixes = new Set<string>();
|
||||
const categoryGroups = new Set<string>();
|
||||
for (const section of value.sections) {
|
||||
assertSection(section, circuitListId);
|
||||
registerUnique(sectionIds, section.id, "circuit-section id");
|
||||
registerUnique(sectionKeys, section.key, "circuit-section key");
|
||||
registerUnique(sectionPrefixes, section.prefix, "circuit-section prefix");
|
||||
if (section.category !== null && section.groupNumber !== null) {
|
||||
registerUnique(
|
||||
categoryGroups,
|
||||
`${section.category}:${section.groupNumber}`,
|
||||
"circuit-group number"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const equipmentIdentifiers = new Set<string>();
|
||||
const componentIds = new Set<string>();
|
||||
const fixedRoles = new Set<string>();
|
||||
const groupRoles = new Set<string>();
|
||||
for (const entry of value.components) {
|
||||
assertComponent(entry, circuitListId, sectionIds);
|
||||
const component = entry.component;
|
||||
registerUnique(componentIds, component.id, "component id");
|
||||
registerEquipmentIdentifier(
|
||||
equipmentIdentifiers,
|
||||
component.equipmentIdentifier
|
||||
);
|
||||
if (
|
||||
component.role === "main_switch" ||
|
||||
component.role === "surge_protective_device"
|
||||
) {
|
||||
registerUnique(fixedRoles, component.role, "fixed component role");
|
||||
}
|
||||
if (
|
||||
component.role === "group_upstream_protection" ||
|
||||
component.role === "group_residual_current_protection"
|
||||
) {
|
||||
registerUnique(
|
||||
groupRoles,
|
||||
`${component.sectionId}:${component.role}`,
|
||||
"group component role"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const circuitIds = new Set<string>();
|
||||
const deviceRowIds = new Set<string>();
|
||||
for (const circuit of value.circuits) {
|
||||
assertCircuitInsertProjectCommand({
|
||||
schemaVersion: circuitStructureCommandSchemaVersion,
|
||||
type: circuitInsertCommandType,
|
||||
payload: { circuit },
|
||||
});
|
||||
if (
|
||||
circuit.circuitListId !== circuitListId ||
|
||||
!sectionIds.has(circuit.sectionId) ||
|
||||
!("protectionDevice" in circuit)
|
||||
) {
|
||||
throw new Error("Distribution-board circuit ownership is invalid.");
|
||||
}
|
||||
registerUnique(circuitIds, circuit.id, "circuit id");
|
||||
registerEquipmentIdentifier(
|
||||
equipmentIdentifiers,
|
||||
circuit.equipmentIdentifier
|
||||
);
|
||||
for (const row of circuit.deviceRows) {
|
||||
registerUnique(deviceRowIds, row.id, "device-row id");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertSection(
|
||||
value: unknown,
|
||||
circuitListId: string
|
||||
): asserts value is DistributionBoardSubtreeSectionSnapshot {
|
||||
if (!isPlainObject(value) || Object.keys(value).length !== 8) {
|
||||
throw new Error("Distribution-board circuit section is invalid.");
|
||||
}
|
||||
for (const field of ["id", "circuitListId", "key", "displayName", "prefix"] as const) {
|
||||
assertNonEmptyString(value[field], `section.${field}`);
|
||||
}
|
||||
assertFiniteNumber(value.sortOrder, "section.sortOrder");
|
||||
if (value.circuitListId !== circuitListId) {
|
||||
throw new Error("Distribution-board section belongs to another list.");
|
||||
}
|
||||
if (
|
||||
value.category === null
|
||||
? value.groupNumber !== null
|
||||
: !circuitGroupCategories.includes(
|
||||
value.category as CircuitGroupCategory
|
||||
) ||
|
||||
!Number.isInteger(value.groupNumber) ||
|
||||
(value.groupNumber as number) <= 0
|
||||
) {
|
||||
throw new Error("Distribution-board circuit group is invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
function assertComponent(
|
||||
value: unknown,
|
||||
circuitListId: string,
|
||||
sectionIds: ReadonlySet<string>
|
||||
): asserts value is DistributionBoardSubtreeComponentSnapshot {
|
||||
if (
|
||||
!isPlainObject(value) ||
|
||||
Object.keys(value).length !== 2 ||
|
||||
!isPlainObject(value.component)
|
||||
) {
|
||||
throw new Error("Distribution-board component snapshot is invalid.");
|
||||
}
|
||||
const component = value.component;
|
||||
for (const field of ["id", "circuitListId", "equipmentIdentifier", "name"] as const) {
|
||||
assertNonEmptyString(component[field], `component.${field}`);
|
||||
}
|
||||
if (
|
||||
component.circuitListId !== circuitListId ||
|
||||
!distributionBoardComponentRoles.includes(
|
||||
component.role as DistributionBoardComponentRole
|
||||
) ||
|
||||
!distributionBoardComponentPlacements.includes(
|
||||
component.placement as DistributionBoardComponentPlacement
|
||||
)
|
||||
) {
|
||||
throw new Error("Distribution-board component values are invalid.");
|
||||
}
|
||||
assertFiniteNumber(component.sortOrder, "component.sortOrder");
|
||||
assertNullableString(component.sectionId, "component.sectionId");
|
||||
const fixed =
|
||||
component.role === "main_switch" ||
|
||||
component.role === "surge_protective_device";
|
||||
const grouped =
|
||||
component.role === "group_upstream_protection" ||
|
||||
component.role === "group_residual_current_protection";
|
||||
if (
|
||||
(fixed &&
|
||||
(component.placement !== "header" || component.sectionId !== null)) ||
|
||||
(grouped &&
|
||||
(component.placement !== "group" ||
|
||||
typeof component.sectionId !== "string" ||
|
||||
!sectionIds.has(component.sectionId))) ||
|
||||
(component.role === "auxiliary" &&
|
||||
(component.placement !== "footer" || component.sectionId !== null))
|
||||
) {
|
||||
throw new Error("Distribution-board component placement is invalid.");
|
||||
}
|
||||
if (value.protectionDevice === null) {
|
||||
if (grouped) {
|
||||
throw new Error("Group protection component lacks protection data.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!grouped || !isPlainObject(value.protectionDevice)) {
|
||||
throw new Error("Component protection data is invalid.");
|
||||
}
|
||||
const protection = value.protectionDevice;
|
||||
if (
|
||||
protection.componentId !== component.id ||
|
||||
!protectionDeviceConfigurationSchema.safeParse({
|
||||
type: protection.type,
|
||||
ratedCurrentA: protection.ratedCurrentA,
|
||||
...(protection.fuseUtilizationCategory === null
|
||||
? {}
|
||||
: { fuseUtilizationCategory: protection.fuseUtilizationCategory }),
|
||||
...(protection.tripCharacteristic === null
|
||||
? {}
|
||||
: { tripCharacteristic: protection.tripCharacteristic }),
|
||||
...(protection.rcdType === null ? {} : { rcdType: protection.rcdType }),
|
||||
...(protection.ratedResidualCurrentMa === null
|
||||
? {}
|
||||
: { ratedResidualCurrentMa: protection.ratedResidualCurrentMa }),
|
||||
}).success
|
||||
) {
|
||||
throw new Error("Component protection configuration is invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
function requiredMappedId(
|
||||
ids: ReadonlyMap<string, string>,
|
||||
sourceId: string
|
||||
) {
|
||||
const id = ids.get(sourceId);
|
||||
if (!id) {
|
||||
throw new Error("Distribution-board copy reference is invalid.");
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function registerEquipmentIdentifier(
|
||||
identifiers: Set<string>,
|
||||
value: string
|
||||
) {
|
||||
registerUnique(
|
||||
identifiers,
|
||||
value.trim().toLocaleLowerCase("de"),
|
||||
"equipment identifier"
|
||||
);
|
||||
}
|
||||
|
||||
function registerUnique(
|
||||
values: Set<string>,
|
||||
value: string,
|
||||
label: string
|
||||
) {
|
||||
if (values.has(value)) {
|
||||
throw new Error(`Distribution-board subtree contains duplicate ${label}.`);
|
||||
}
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
function assertNonEmptyString(
|
||||
value: unknown,
|
||||
field: string
|
||||
): asserts value is string {
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
throw new Error(`${field} must be a non-empty string.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertNullableString(
|
||||
value: unknown,
|
||||
field: string
|
||||
): asserts value is string | null {
|
||||
if (value !== null) {
|
||||
assertNonEmptyString(value, field);
|
||||
}
|
||||
}
|
||||
|
||||
function assertFiniteNumber(
|
||||
value: unknown,
|
||||
field: string
|
||||
): asserts value is number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
throw new Error(`${field} must be finite.`);
|
||||
}
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user