diff --git a/docs/spec/revit-excel-integration-requirements.md b/docs/spec/revit-excel-integration-requirements.md index 29a4aaf..ce80c46 100644 --- a/docs/spec/revit-excel-integration-requirements.md +++ b/docs/spec/revit-excel-integration-requirements.md @@ -306,6 +306,16 @@ aktuellen Row-Werte als Planungswerte und entsprechende Overrides. ## 11. Editor-Zuordnung und Löschen +Umsetzungsstand R7: Der Stromkreislisteneditor lädt die kompakte Projektliste +gemeinsam mit dem Baum, gruppiert unzugeordnete Objekte des aktuellen +Verteilers im Client und bietet Gruppen sowie einzelne Worksharing-IDs als +Drag-Quelle an. Drop auf Row, Circuit und freien Gruppenbereich verwendet die +drei persistenten Zuordnungscommands. Row-Badges zeigen die Linkzahl; Lösen +kopiert die aktuellen Row-Planungswerte als Overrides. Row-, Circuit- und +Gruppenlöschungen speichern die vollständigen Linkübergänge im Forward- und +Inverse-Command. Raum- und Verteilerlöschungen prüfen externe Referenzen +explizit. + Der rechte, überlagernde Editorbereich zeigt unzugeordnete konfliktfreie Objekte des aktuellen Verteilers. Gruppierungsschlüssel sind interner Raum, Auswahlkenner, Geräteart, Kategorie, identische Planungswerte und kompatibler @@ -338,6 +348,14 @@ Pfade. ## 12. Rückgabe-Excel +Umsetzungsstand R8: Die Revit-Objekttabelle bietet alle drei Rückgabeumfänge +an und lädt eine aus dem letzten vollständigen Import erzeugte XLSX herunter. +Die Serverprojektion verwendet für verknüpfte Objekte die aktuellen Row- und +Circuit-Werte, für freie Objekte die Planungswerte und schreibt Zahlen als +Excel-Zahlen. Hinweise zu nicht berechenbaren Werten und fehlenden optionalen +Spalten werden nach dem Download angezeigt. Der append-only Exportnachweis +liegt außerhalb von Projektrevision, Undo/Redo, Snapshots und Transfer. + Der Export ist eine Leseoperation ohne Projektrevision. Ein append-only Nachweis speichert Projektrevision, zugrunde liegenden letzten Import, Zeitpunkt, Dateiname, SHA-256, Konfigurationskopie beziehungsweise ihren Hash diff --git a/docs/spec/revit-excel-phase-14-plan.md b/docs/spec/revit-excel-phase-14-plan.md index 9b43c4f..61d8f2d 100644 --- a/docs/spec/revit-excel-phase-14-plan.md +++ b/docs/spec/revit-excel-phase-14-plan.md @@ -15,8 +15,8 @@ Der alte CSV-Plan 14.0–14.6 ist abgelöst und wird nicht fortgeschrieben. | R4 | abgeschlossen | Revit-Seite, Navigation und revisionierte XLSX-Einstellungen mit Musterdateiprüfung sind umgesetzt; das alte Projektseitenmodal ist entfernt. | | R5 | abgeschlossen | Zustandslose XLSX-Vorschau, sechsstufiger Erstimportwizard und atomarer Importcommand mit parserfreiem Undo/Redo. | | R6 | abgeschlossen | Projektweite kompakte Objektliste, clientseitige Filter/Sortierung, Virtualisierung und revisionierte Sammeländerungen. | -| R7 | offen | Editorbereich, Gruppierung, Drag-and-drop, Lösen und integrierte Löschübergänge. | -| R8 | offen | Rückgabe-Excel und append-only Exportnachweis. | +| R7 | abgeschlossen | Gruppierter Editorbereich, drei Drag-and-drop-Ziele, Row-Indikator, ausdrückliches Lösen und atomare Löschübergänge mit exaktem Undo/Redo. | +| R8 | abgeschlossen | Rückgabe-Excel für alle, abweichende oder ausgewählte Objekte und append-only Exportnachweis sind umgesetzt. | | R9 | offen | Vollständiger Folgeimport nach Worksharing-ID. | | R10 | offen | Ende-zu-Ende-Härtung, große Datenmengen und Abschlussdokumentation. | @@ -173,6 +173,13 @@ Sammelaktionen gesperrt und folgen weiterhin ausschließlich ihrer Row. R8 baut auf der in R2 durch Linear bestätigten Transportstrategie auf. +Umsetzungsstand: Die Revit-Objekttabelle erzeugt die Rückgabe revisionssicher +aus dem letzten vollständigen Import. Die gemeinsame Projektion berechnet +Stromkreis, Bezeichnung, Leistung, Spannung und Strom; der XLSX-Adapter erhält +Struktur und Formate und meldet fehlende optionale Spalten. Jeder erfolgreiche +Download wird außerhalb von Projektcommands, Snapshots und Transfers mit +Revision, Importbatch, Konfigurationskopie und Prüfsummen protokolliert. + ## R9 – Folgeimport - exaktes, case-sensitives Matching nach Worksharing-ID; diff --git a/src/app/globals.css b/src/app/globals.css index 8b55c75..67985f4 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1128,6 +1128,17 @@ a.kpi:hover { border-style: dashed; } +.project-device-item > summary { + display: flex; + flex-direction: column; + gap: 0.1rem; + cursor: grab; +} + +.project-device-item > summary::marker { + color: var(--text-faint); +} + .sidebar-actions { display: flex; flex-direction: column; diff --git a/src/app/projects/[projectId]/revit/page.tsx b/src/app/projects/[projectId]/revit/page.tsx index fc8b85e..46c3d5d 100644 --- a/src/app/projects/[projectId]/revit/page.tsx +++ b/src/app/projects/[projectId]/revit/page.tsx @@ -45,7 +45,7 @@ export default function RevitPage() { {error ?
{error}
: null}

XLSX-Konfiguration

{snapshot ? "Die Linear-Spalten und das Tabellenlayout sind konfiguriert." : "Noch keine XLSX-Konfiguration gespeichert."}

{snapshot ? "Konfiguriert" : "Offen"}

Importstatus

{importStatus?.imported ? <>

{importStatus.objectCount} Revit-Objekte{importStatus.fileName ? ` aus ${importStatus.fileName}` : ""}

Quelle: {importStatus.sourceName}

:

Noch kein Linear-XLSX-Import vorhanden.

}
{importStatus?.imported ? setProject((current) => current ? { ...current, currentRevision: revision } : current)} /> : null} - {settingsOpen ? setSettingsOpen(false)} onInspectSample={async (file, configuration) => { const result = await detectExternalModelWorkbook(projectId, file, configuration); return `Muster erkannt: ${result.sheetName}, ${result.columnCount} Spalten, ${result.objectCount} Objekte.`; }} onSave={save} /> : null} + {settingsOpen ? setSettingsOpen(false)} onInspectSample={async (file, configuration) => { const result = await detectExternalModelWorkbook(projectId, file, configuration); return `Muster erkannt: ${result.sheetName}, ${result.columnCount} Spalten, ${result.objectCount} Objekte.`; }} onSave={save} /> : null} {importOpen && project && snapshot ? setImportOpen(false)} onImported={({ history }) => { setProject({ ...project, currentRevision: history.currentRevision }); setImportOpen(false); void Promise.all([getExternalInitialImportStatus(projectId), getExternalModelConfiguration(projectId)]).then(([status, configuration]) => { setImportStatus(status); setSnapshot(configuration.configuration); }); }} /> : null} ; } diff --git a/src/db/migrations/0015_wide_night_thrasher.sql b/src/db/migrations/0015_wide_night_thrasher.sql new file mode 100644 index 0000000..fcd376b --- /dev/null +++ b/src/db/migrations/0015_wide_night_thrasher.sql @@ -0,0 +1,16 @@ +CREATE TABLE `external_export_batches` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `project_revision` integer NOT NULL, + `import_batch_id` text NOT NULL, + `exported_at_iso` text NOT NULL, + `file_name` text NOT NULL, + `sha256` text NOT NULL, + `configuration_snapshot` text NOT NULL, + `configuration_sha256` text NOT NULL, + `scope` text NOT NULL, + `object_count` integer NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `external_export_batches_project_exported_idx` ON `external_export_batches` (`project_id`,`exported_at_iso`); \ No newline at end of file diff --git a/src/db/migrations/meta/0015_snapshot.json b/src/db/migrations/meta/0015_snapshot.json new file mode 100644 index 0000000..9c00cc3 --- /dev/null +++ b/src/db/migrations/meta/0015_snapshot.json @@ -0,0 +1,3210 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "eab4abfa-dc46-4330-9ddd-b4676343d610", + "prevId": "b443af16-e0aa-459a-9443-62a7f8c6b6dd", + "tables": { + "buildings": { + "name": "buildings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "buildings_project_id_projects_id_fk": { + "name": "buildings_project_id_projects_id_fk", + "tableFrom": "buildings", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_device_rows": { + "name": "circuit_device_rows", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "linked_project_device_id": { + "name": "linked_project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number_snapshot": { + "name": "room_number_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_name_snapshot": { + "name": "room_name_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "manual_quantity": { + "name": "manual_quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overridden_fields": { + "name": "overridden_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuit_device_rows_circuit_id_idx": { + "name": "circuit_device_rows_circuit_id_idx", + "columns": [ + "circuit_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "circuit_device_rows_circuit_id_circuits_id_fk": { + "name": "circuit_device_rows_circuit_id_circuits_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { + "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "project_devices", + "columnsFrom": [ + "linked_project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "circuit_device_rows_room_id_rooms_id_fk": { + "name": "circuit_device_rows_room_id_rooms_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_list_equipment_identifiers": { + "name": "circuit_list_equipment_identifiers", + "columns": { + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_list_equipment_identifiers_owner_unique": { + "name": "circuit_list_equipment_identifiers_owner_unique", + "columns": [ + "owner_type", + "owner_id" + ], + "isUnique": true + }, + "circuit_list_equipment_identifiers_list_identifier_unique": { + "name": "circuit_list_equipment_identifiers_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_list_equipment_identifiers", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_lists": { + "name": "circuit_lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_lists_distribution_board_id_unique": { + "name": "circuit_lists_distribution_board_id_unique", + "columns": [ + "distribution_board_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_lists_project_id_projects_id_fk": { + "name": "circuit_lists_project_id_projects_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_lists_distribution_board_id_distribution_boards_id_fk": { + "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_protection_devices": { + "name": "circuit_protection_devices", + "columns": { + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "circuit_protection_devices_circuit_id_circuits_id_fk": { + "name": "circuit_protection_devices_circuit_id_circuits_id_fk", + "tableFrom": "circuit_protection_devices", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_sections": { + "name": "circuit_sections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_number": { + "name": "group_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuit_sections_list_key_unique": { + "name": "circuit_sections_list_key_unique", + "columns": [ + "circuit_list_id", + "key" + ], + "isUnique": true + }, + "circuit_sections_list_prefix_unique": { + "name": "circuit_sections_list_prefix_unique", + "columns": [ + "circuit_list_id", + "prefix" + ], + "isUnique": true + }, + "circuit_sections_list_category_group_unique": { + "name": "circuit_sections_list_category_group_unique", + "columns": [ + "circuit_list_id", + "category", + "group_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_sections_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_sections", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuits": { + "name": "circuits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cable_type": { + "name": "cable_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_cross_section": { + "name": "cable_cross_section", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_length": { + "name": "cable_length", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_assignment": { + "name": "rcd_assignment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_designation": { + "name": "terminal_designation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage": { + "name": "voltage", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "control_requirement": { + "name": "control_requirement", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_reserve": { + "name": "is_reserve", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuits_section_id_idx": { + "name": "circuits_section_id_idx", + "columns": [ + "section_id" + ], + "isUnique": false + }, + "circuits_list_equipment_identifier_unique": { + "name": "circuits_list_equipment_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuits_circuit_list_id_circuit_lists_id_fk": { + "name": "circuits_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuits_section_id_circuit_sections_id_fk": { + "name": "circuits_section_id_circuit_sections_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_component_protection_devices": { + "name": "distribution_board_component_protection_devices", + "columns": { + "component_id": { + "name": "component_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk": { + "name": "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk", + "tableFrom": "distribution_board_component_protection_devices", + "tableTo": "distribution_board_components", + "columnsFrom": [ + "component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_components": { + "name": "distribution_board_components", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "placement": { + "name": "placement", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "distribution_board_components_list_identifier_unique": { + "name": "distribution_board_components_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + }, + "distribution_board_components_section_role_unique": { + "name": "distribution_board_components_section_role_unique", + "columns": [ + "section_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": { + "distribution_board_components_circuit_list_id_circuit_lists_id_fk": { + "name": "distribution_board_components_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_board_components_section_id_circuit_sections_id_fk": { + "name": "distribution_board_components_section_id_circuit_sections_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_boards": { + "name": "distribution_boards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supply_type": { + "name": "supply_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "board_number": { + "name": "board_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fed_from": { + "name": "fed_from", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feeding_distribution_board_id": { + "name": "feeding_distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feeding_circuit_id": { + "name": "feeding_circuit_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "distribution_boards_feeding_board_idx": { + "name": "distribution_boards_feeding_board_idx", + "columns": [ + "feeding_distribution_board_id" + ], + "isUnique": false + }, + "distribution_boards_feeding_circuit_idx": { + "name": "distribution_boards_feeding_circuit_idx", + "columns": [ + "feeding_circuit_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "distribution_boards_project_id_projects_id_fk": { + "name": "distribution_boards_project_id_projects_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_boards_floor_id_floors_id_fk": { + "name": "distribution_boards_floor_id_floors_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "distribution_boards_feeding_distribution_board_id_distribution_boards_id_fk": { + "name": "distribution_boards_feeding_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "distribution_boards", + "columnsFrom": [ + "feeding_distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "distribution_boards_feeding_circuit_id_circuits_id_fk": { + "name": "distribution_boards_feeding_circuit_id_circuits_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "circuits", + "columnsFrom": [ + "feeding_circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "document_branding_assets": { + "name": "document_branding_assets", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "document_branding_settings": { + "name": "document_branding_settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "estimate_balances": { + "name": "estimate_balances", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "work_stage": { + "name": "work_stage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date_iso": { + "name": "date_iso", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reserve_av_percent": { + "name": "reserve_av_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reserve_sv_percent": { + "name": "reserve_sv_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "estimate_balances_project_id_idx": { + "name": "estimate_balances_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "estimate_balances_project_id_projects_id_fk": { + "name": "estimate_balances_project_id_projects_id_fk", + "tableFrom": "estimate_balances", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "estimate_groups": { + "name": "estimate_groups", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "balance_id": { + "name": "balance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "editor": { + "name": "editor", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "estimate_groups_balance_id_idx": { + "name": "estimate_groups_balance_id_idx", + "columns": [ + "balance_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "estimate_groups_balance_id_estimate_balances_id_fk": { + "name": "estimate_groups_balance_id_estimate_balances_id_fk", + "tableFrom": "estimate_groups", + "tableTo": "estimate_balances", + "columnsFrom": [ + "balance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "estimate_rows": { + "name": "estimate_rows", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "supply": { + "name": "supply", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "quantity": { + "name": "quantity", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'piece'" + }, + "connection": { + "name": "connection", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "power_per_unit_w": { + "name": "power_per_unit_w", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "safety_supply": { + "name": "safety_supply", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "estimate_rows_group_id_idx": { + "name": "estimate_rows_group_id_idx", + "columns": [ + "group_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "estimate_rows_group_id_estimate_groups_id_fk": { + "name": "estimate_rows_group_id_estimate_groups_id_fk", + "tableFrom": "estimate_rows", + "tableTo": "estimate_groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_export_batches": { + "name": "external_export_batches", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_revision": { + "name": "project_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "import_batch_id": { + "name": "import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exported_at_iso": { + "name": "exported_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_snapshot": { + "name": "configuration_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_sha256": { + "name": "configuration_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_count": { + "name": "object_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_export_batches_project_exported_idx": { + "name": "external_export_batches_project_exported_idx", + "columns": [ + "project_id", + "exported_at_iso" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_export_batches_project_id_projects_id_fk": { + "name": "external_export_batches_project_id_projects_id_fk", + "tableFrom": "external_export_batches", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_import_batches": { + "name": "external_import_batches", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "import_kind": { + "name": "import_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imported_at_iso": { + "name": "imported_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_count": { + "name": "object_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_project_revision": { + "name": "applied_project_revision", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "configuration_snapshot": { + "name": "configuration_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "original_bytes": { + "name": "original_bytes", + "type": "blob", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "document": { + "name": "document", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "external_import_batches_project_revision_idx": { + "name": "external_import_batches_project_revision_idx", + "columns": [ + "project_id", + "applied_project_revision" + ], + "isUnique": false + }, + "external_import_batches_source_imported_idx": { + "name": "external_import_batches_source_imported_idx", + "columns": [ + "source_id", + "imported_at_iso" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_import_batches_project_id_projects_id_fk": { + "name": "external_import_batches_project_id_projects_id_fk", + "tableFrom": "external_import_batches", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_import_batches_source_id_external_model_sources_id_fk": { + "name": "external_import_batches_source_id_external_model_sources_id_fk", + "tableFrom": "external_import_batches", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_model_configurations": { + "name": "external_model_configurations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_model_configurations_project_id_unique": { + "name": "external_model_configurations_project_id_unique", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_model_configurations_project_id_projects_id_fk": { + "name": "external_model_configurations_project_id_projects_id_fk", + "tableFrom": "external_model_configurations", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_model_objects": { + "name": "external_model_objects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worksharing_id": { + "name": "worksharing_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ifc_guid": { + "name": "ifc_guid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "device_type_id": { + "name": "device_type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_import_batch_id": { + "name": "last_seen_import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_accepted_import_batch_id": { + "name": "last_accepted_import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_source_values": { + "name": "accepted_source_values", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "planning_values": { + "name": "planning_values", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "overridden_fields": { + "name": "overridden_fields", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_room_mapping_id": { + "name": "external_room_mapping_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_project_device_id": { + "name": "linked_project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "circuit_device_row_id": { + "name": "circuit_device_row_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "presence_status": { + "name": "presence_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'present'" + } + }, + "indexes": { + "external_model_objects_source_worksharing_id_unique": { + "name": "external_model_objects_source_worksharing_id_unique", + "columns": [ + "source_id", + "worksharing_id" + ], + "isUnique": true + }, + "external_model_objects_project_presence_idx": { + "name": "external_model_objects_project_presence_idx", + "columns": [ + "project_id", + "presence_status" + ], + "isUnique": false + }, + "external_model_objects_project_device_type_idx": { + "name": "external_model_objects_project_device_type_idx", + "columns": [ + "project_id", + "device_type_id" + ], + "isUnique": false + }, + "external_model_objects_external_room_mapping_idx": { + "name": "external_model_objects_external_room_mapping_idx", + "columns": [ + "external_room_mapping_id" + ], + "isUnique": false + }, + "external_model_objects_distribution_board_idx": { + "name": "external_model_objects_distribution_board_idx", + "columns": [ + "distribution_board_id" + ], + "isUnique": false + }, + "external_model_objects_linked_project_device_idx": { + "name": "external_model_objects_linked_project_device_idx", + "columns": [ + "linked_project_device_id" + ], + "isUnique": false + }, + "external_model_objects_circuit_device_row_idx": { + "name": "external_model_objects_circuit_device_row_idx", + "columns": [ + "circuit_device_row_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_model_objects_project_id_projects_id_fk": { + "name": "external_model_objects_project_id_projects_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_model_objects_source_id_external_model_sources_id_fk": { + "name": "external_model_objects_source_id_external_model_sources_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk": { + "name": "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_import_batches", + "columnsFrom": [ + "last_seen_import_batch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk": { + "name": "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_import_batches", + "columnsFrom": [ + "last_accepted_import_batch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk": { + "name": "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_room_mappings", + "columnsFrom": [ + "external_room_mapping_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_distribution_board_id_distribution_boards_id_fk": { + "name": "external_model_objects_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_linked_project_device_id_project_devices_id_fk": { + "name": "external_model_objects_linked_project_device_id_project_devices_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "project_devices", + "columnsFrom": [ + "linked_project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk": { + "name": "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "circuit_device_rows", + "columnsFrom": [ + "circuit_device_row_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_model_sources": { + "name": "external_model_sources", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'revit_xlsx'" + } + }, + "indexes": { + "external_model_sources_project_unique": { + "name": "external_model_sources_project_unique", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_model_sources_project_id_projects_id_fk": { + "name": "external_model_sources_project_id_projects_id_fk", + "tableFrom": "external_model_sources", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_room_mappings": { + "name": "external_room_mappings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "normalized_source_room_key": { + "name": "normalized_source_room_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_floor_name": { + "name": "source_floor_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_room_number": { + "name": "source_room_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_room_name": { + "name": "source_room_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_distribution_board_id": { + "name": "default_distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "external_room_mappings_source_key_unique": { + "name": "external_room_mappings_source_key_unique", + "columns": [ + "source_id", + "normalized_source_room_key" + ], + "isUnique": true + }, + "external_room_mappings_project_room_idx": { + "name": "external_room_mappings_project_room_idx", + "columns": [ + "project_id", + "room_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_room_mappings_project_id_projects_id_fk": { + "name": "external_room_mappings_project_id_projects_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_room_mappings_source_id_external_model_sources_id_fk": { + "name": "external_room_mappings_source_id_external_model_sources_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_room_mappings_room_id_rooms_id_fk": { + "name": "external_room_mappings_room_id_rooms_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk": { + "name": "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "distribution_boards", + "columnsFrom": [ + "default_distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "floors": { + "name": "floors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "building_id": { + "name": "building_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "floors_project_id_projects_id_fk": { + "name": "floors_project_id_projects_id_fk", + "tableFrom": "floors", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "floors_building_id_buildings_id_fk": { + "name": "floors_building_id_buildings_id_fk", + "tableFrom": "floors", + "tableTo": "buildings", + "columnsFrom": [ + "building_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "global_devices": { + "name": "global_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "installed_power_per_unit_kw": { + "name": "installed_power_per_unit_kw", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "demand_factor": { + "name": "demand_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase_count": { + "name": "phase_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "power_factor": { + "name": "power_factor", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_change_sets": { + "name": "project_change_sets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_revision_id": { + "name": "project_revision_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_schema_version": { + "name": "payload_schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forward_payload_json": { + "name": "forward_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inverse_payload_json": { + "name": "inverse_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_change_sets_revision_unique": { + "name": "project_change_sets_revision_unique", + "columns": [ + "project_revision_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_change_sets_project_revision_id_project_revisions_id_fk": { + "name": "project_change_sets_project_revision_id_project_revisions_id_fk", + "tableFrom": "project_change_sets", + "tableTo": "project_revisions", + "columnsFrom": [ + "project_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_devices": { + "name": "project_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'single_phase'" + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_devices_project_id_projects_id_fk": { + "name": "project_devices_project_id_projects_id_fk", + "tableFrom": "project_devices", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_history_stack_entries": { + "name": "project_history_stack_entries", + "columns": { + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stack": { + "name": "stack", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_history_stack_entries_position_unique": { + "name": "project_history_stack_entries_position_unique", + "columns": [ + "project_id", + "stack", + "position" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { + "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "project_change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_history_stack_entries_project_id_projects_id_fk": { + "name": "project_history_stack_entries_project_id_projects_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "project_history_stack_entries_stack_check": { + "name": "project_history_stack_entries_stack_check", + "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" + } + } + }, + "project_revisions": { + "name": "project_revisions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_revisions_project_number_unique": { + "name": "project_revisions_project_number_unique", + "columns": [ + "project_id", + "revision_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_revisions_project_id_projects_id_fk": { + "name": "project_revisions_project_id_projects_id_fk", + "tableFrom": "project_revisions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_snapshots": { + "name": "project_snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_revision": { + "name": "source_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'named'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_sha256": { + "name": "payload_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_snapshots_project_created_idx": { + "name": "project_snapshots_project_created_idx", + "columns": [ + "project_id", + "created_at_iso" + ], + "isUnique": false + }, + "project_snapshots_project_kind_revision_idx": { + "name": "project_snapshots_project_kind_revision_idx", + "columns": [ + "project_id", + "kind", + "source_revision" + ], + "isUnique": false + }, + "project_snapshots_project_name_unique": { + "name": "project_snapshots_project_name_unique", + "columns": [ + "project_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_snapshots_project_id_projects_id_fk": { + "name": "project_snapshots_project_id_projects_id_fk", + "tableFrom": "project_snapshots", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_project_number": { + "name": "internal_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_number": { + "name": "external_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "building_owner": { + "name": "building_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_public_building": { + "name": "is_public_building", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "single_phase_voltage_v": { + "name": "single_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 230 + }, + "three_phase_voltage_v": { + "name": "three_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 400 + }, + "enabled_distribution_board_supply_types": { + "name": "enabled_distribution_board_supply_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" + }, + "site_address": { + "name": "site_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "work_stage": { + "name": "work_stage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_lead": { + "name": "project_lead", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_editor": { + "name": "project_editor", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "current_revision": { + "name": "current_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rooms": { + "name": "rooms", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number": { + "name": "room_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_name": { + "name": "room_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "area_m2": { + "name": "area_m2", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "rooms_project_id_projects_id_fk": { + "name": "rooms_project_id_projects_id_fk", + "tableFrom": "rooms", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rooms_floor_id_floors_id_fk": { + "name": "rooms_floor_id_floors_id_fk", + "tableFrom": "rooms", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "must_change_password": { + "name": "must_change_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 0f6c303..0ab138e 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -106,6 +106,13 @@ "when": 1790748107566, "tag": "0014_linear_xlsx_external_model", "breakpoints": true + }, + { + "idx": 15, + "version": "6", + "when": 1790773066696, + "tag": "0015_wide_night_thrasher", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/repositories/circuit-device-row-structure-project-command.repository.ts b/src/db/repositories/circuit-device-row-structure-project-command.repository.ts index 6919d20..f69dc78 100644 --- a/src/db/repositories/circuit-device-row-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-device-row-structure-project-command.repository.ts @@ -23,8 +23,15 @@ import { toCircuitDeviceRowInsertValues, toCircuitDeviceRowSnapshot, } from "./circuit-device-row-structure.persistence.js"; -import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { executeProjectCommandTransactionWithAppliedForward, type AppliedProjectCommand } from "./project-command-transaction.persistence.js"; import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; +import { + applyExternalObjectLinks, + captureExternalObjectDetachTransitions, + invertExternalObjectLinkTransitions, + loadExpectedExternalObjectTransitions, + type ExternalObjectLinkTransition, +} from "./external-object-assignment.persistence.js"; export class CircuitDeviceRowStructureProjectCommandRepository implements CircuitDeviceRowStructureProjectCommandStore @@ -32,7 +39,11 @@ export class CircuitDeviceRowStructureProjectCommandRepository constructor(private readonly database: AppDatabase) {} execute(input: ExecuteCircuitDeviceRowStructureCommandInput) { - return executeProjectCommandTransaction( + return executeProjectCommandTransactionWithAppliedForward< + CircuitDeviceRowStructureProjectCommand, + CircuitDeviceRowStructureProjectCommand, + CircuitDeviceRowStructureProjectCommand + >( this.database, input, (tx) => @@ -50,14 +61,15 @@ export class CircuitDeviceRowStructureProjectCommandRepository projectId: string, source: ExecuteCircuitDeviceRowStructureCommandInput["source"], command: CircuitDeviceRowStructureProjectCommand - ): CircuitDeviceRowStructureProjectCommand { + ): AppliedProjectCommand { if (command.type === circuitDeviceRowInsertCommandType) { assertCircuitDeviceRowInsertProjectCommand(command); return this.insert( database, projectId, source, - command.payload.row + command.payload.row, + command.payload.externalObjects ?? [] ); } if (command.type === circuitDeviceRowDeleteCommandType) { @@ -66,7 +78,8 @@ export class CircuitDeviceRowStructureProjectCommandRepository database, projectId, command.payload.rowId, - command.payload.expectedCircuitId + command.payload.expectedCircuitId, + command.payload.externalObjects ); } throw new Error("Unsupported circuit device-row structure command."); @@ -76,7 +89,8 @@ export class CircuitDeviceRowStructureProjectCommandRepository database: AppDatabase, projectId: string, source: ExecuteCircuitDeviceRowStructureCommandInput["source"], - row: CircuitDeviceRowSnapshot + row: CircuitDeviceRowSnapshot, + externalObjects: ExternalObjectLinkTransition[] ) { if (source === "user" && !isElectricalPhaseType(row.phaseType)) { throw new Error("Device-row phase type is invalid."); @@ -107,17 +121,23 @@ export class CircuitDeviceRowStructureProjectCommandRepository } updateDerivedCircuitVoltage(database, projectId, row.circuitId); - return createCircuitDeviceRowDeleteProjectCommand( - row.id, - row.circuitId - ); + if (externalObjects.length > 0) { + loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Wiederherstellen geändert."); + applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Wiederherstellen geändert."); + } + + return { + forward: createCircuitDeviceRowInsertProjectCommand(row, externalObjects), + inverse: createCircuitDeviceRowDeleteProjectCommand(row.id, row.circuitId, invertExternalObjectLinkTransitions(externalObjects)), + }; } private delete( database: AppDatabase, projectId: string, rowId: string, - expectedCircuitId: string + expectedCircuitId: string, + suppliedExternalObjects?: ExternalObjectLinkTransition[] ) { const row = database .select() @@ -130,6 +150,10 @@ export class CircuitDeviceRowStructureProjectCommandRepository ); } this.assertCircuitInProject(database, projectId, row.circuitId); + const externalObjects = suppliedExternalObjects ?? + captureExternalObjectDetachTransitions(database, projectId, [rowId]); + loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Löschen geändert."); + applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Löschen geändert."); const result = database .delete(circuitDeviceRows) @@ -164,9 +188,10 @@ export class CircuitDeviceRowStructureProjectCommandRepository expectedCircuitId ); - return createCircuitDeviceRowInsertProjectCommand( - toCircuitDeviceRowSnapshot(row) - ); + return { + forward: createCircuitDeviceRowDeleteProjectCommand(rowId, expectedCircuitId, externalObjects), + inverse: createCircuitDeviceRowInsertProjectCommand(toCircuitDeviceRowSnapshot(row), invertExternalObjectLinkTransitions(externalObjects)), + }; } private assertCircuitInProject( diff --git a/src/db/repositories/circuit-group-subtree-project-command.repository.ts b/src/db/repositories/circuit-group-subtree-project-command.repository.ts index 9bf27a0..b4baeb6 100644 --- a/src/db/repositories/circuit-group-subtree-project-command.repository.ts +++ b/src/db/repositories/circuit-group-subtree-project-command.repository.ts @@ -25,8 +25,14 @@ 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 { toCircuitDeviceRowInsertValues } from "./circuit-device-row-structure.persistence.js"; -import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { executeProjectCommandTransactionWithAppliedForward, type AppliedProjectCommand } from "./project-command-transaction.persistence.js"; import { assertCircuitsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js"; +import { + applyExternalObjectLinks, + captureExternalObjectDetachTransitions, + invertExternalObjectLinkTransitions, + loadExpectedExternalObjectTransitions, +} from "./external-object-assignment.persistence.js"; export class CircuitGroupSubtreeProjectCommandRepository implements CircuitGroupSubtreeProjectCommandStore @@ -43,7 +49,11 @@ export class CircuitGroupSubtreeProjectCommandRepository } else { assertCircuitGroupRestoreSubtreeProjectCommand(input.command); } - return executeProjectCommandTransaction( + return executeProjectCommandTransactionWithAppliedForward< + CircuitGroupSubtreeProjectCommand, + CircuitGroupSubtreeProjectCommand, + CircuitGroupSubtreeProjectCommand + >( this.database, input, (tx) => this.applyCommand(tx, input.projectId, input.command) @@ -54,7 +64,7 @@ export class CircuitGroupSubtreeProjectCommandRepository database: AppDatabase, projectId: string, command: CircuitGroupSubtreeProjectCommand - ) { + ): AppliedProjectCommand { if (command.type === circuitGroupDeleteSubtreeCommandType) { const current = this.captureFrom( database, @@ -71,6 +81,14 @@ export class CircuitGroupSubtreeProjectCommandRepository projectId, current.circuits.map((entry) => entry.circuit.id) ); + const externalObjects = command.payload.externalObjects ?? + captureExternalObjectDetachTransitions( + database, + projectId, + current.circuits.flatMap((entry) => entry.circuit.deviceRows.map((row) => row.id)) + ); + loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Löschen geändert."); + applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Löschen geändert."); const deleted = database .delete(circuitSections) .where(eq(circuitSections.id, current.group.id)) @@ -78,15 +96,20 @@ export class CircuitGroupSubtreeProjectCommandRepository if (deleted.changes !== 1) { throw new Error("Circuit-group subtree could not be deleted."); } - return createCircuitGroupRestoreSubtreeProjectCommand( - command.payload.snapshot - ); + return { + forward: createCircuitGroupDeleteSubtreeProjectCommand(command.payload.snapshot, externalObjects), + inverse: createCircuitGroupRestoreSubtreeProjectCommand(command.payload.snapshot, invertExternalObjectLinkTransitions(externalObjects)), + }; } if (command.type === circuitGroupRestoreSubtreeCommandType) { this.restore(database, projectId, command.payload.snapshot); - return createCircuitGroupDeleteSubtreeProjectCommand( - command.payload.snapshot - ); + const externalObjects = command.payload.externalObjects ?? []; + loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Wiederherstellen geändert."); + applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Wiederherstellen geändert."); + return { + forward: createCircuitGroupRestoreSubtreeProjectCommand(command.payload.snapshot, externalObjects), + inverse: createCircuitGroupDeleteSubtreeProjectCommand(command.payload.snapshot, invertExternalObjectLinkTransitions(externalObjects)), + }; } throw new Error("Unsupported circuit-group subtree command."); } diff --git a/src/db/repositories/circuit-structure-project-command.repository.ts b/src/db/repositories/circuit-structure-project-command.repository.ts index 4841616..f4fc420 100644 --- a/src/db/repositories/circuit-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-structure-project-command.repository.ts @@ -25,10 +25,17 @@ import { toCircuitDeviceRowInsertValues, toCircuitDeviceRowSnapshot, } from "./circuit-device-row-structure.persistence.js"; -import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { executeProjectCommandTransactionWithAppliedForward, type AppliedProjectCommand } from "./project-command-transaction.persistence.js"; import { assertCircuitsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js"; import { resolveCircuitVoltage } from "./project-voltage.persistence.js"; import { assertEquipmentIdentifierAvailable } from "./equipment-identifier-uniqueness.persistence.js"; +import { + applyExternalObjectLinks, + captureExternalObjectDetachTransitions, + invertExternalObjectLinkTransitions, + loadExpectedExternalObjectTransitions, + type ExternalObjectLinkTransition, +} from "./external-object-assignment.persistence.js"; export class CircuitStructureProjectCommandRepository implements CircuitStructureProjectCommandStore @@ -36,11 +43,15 @@ export class CircuitStructureProjectCommandRepository constructor(private readonly database: AppDatabase) {} execute(input: ExecuteCircuitStructureCommandInput) { - return executeProjectCommandTransaction( + return executeProjectCommandTransactionWithAppliedForward< + CircuitStructureProjectCommand, + CircuitStructureProjectCommand, + CircuitStructureProjectCommand + >( this.database, input, (tx) => - this.applyInTransaction( + this.applyWithForward( tx, input.projectId, input.source, @@ -59,13 +70,23 @@ export class CircuitStructureProjectCommandRepository source: ExecuteCircuitStructureCommandInput["source"], command: CircuitStructureProjectCommand ): CircuitStructureProjectCommand { + return this.applyWithForward(database, projectId, source, command).inverse; + } + + private applyWithForward( + database: AppDatabase, + projectId: string, + source: ExecuteCircuitStructureCommandInput["source"], + command: CircuitStructureProjectCommand + ): AppliedProjectCommand { if (command.type === circuitInsertCommandType) { assertCircuitInsertProjectCommand(command); return this.insert( database, projectId, source, - command.payload.circuit + command.payload.circuit, + command.payload.externalObjects ?? [] ); } if (command.type === circuitDeleteCommandType) { @@ -74,7 +95,8 @@ export class CircuitStructureProjectCommandRepository database, projectId, command.payload.circuitId, - command.payload.expectedCircuitListId + command.payload.expectedCircuitListId, + command.payload.externalObjects ); } throw new Error("Unsupported circuit structure command."); @@ -84,7 +106,8 @@ export class CircuitStructureProjectCommandRepository database: AppDatabase, projectId: string, source: ExecuteCircuitStructureCommandInput["source"], - snapshot: CircuitSnapshot + snapshot: CircuitSnapshot, + externalObjects: ExternalObjectLinkTransition[] ) { this.assertCircuitLocation(database, projectId, snapshot); if (source === "user") { @@ -172,17 +195,23 @@ export class CircuitStructureProjectCommandRepository .run(); } - return createCircuitDeleteProjectCommand( - snapshot.id, - snapshot.circuitListId - ); + if (externalObjects.length > 0) { + loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Wiederherstellen geändert."); + applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Wiederherstellen geändert."); + } + + return { + forward: createCircuitInsertProjectCommand(snapshot, externalObjects), + inverse: createCircuitDeleteProjectCommand(snapshot.id, snapshot.circuitListId, invertExternalObjectLinkTransitions(externalObjects)), + }; } private delete( database: AppDatabase, projectId: string, circuitId: string, - expectedCircuitListId: string + expectedCircuitListId: string, + suppliedExternalObjects?: ExternalObjectLinkTransition[] ) { const circuit = database .select() @@ -220,7 +249,12 @@ export class CircuitStructureProjectCommandRepository .from(circuitProtectionDevices) .where(eq(circuitProtectionDevices.circuitId, circuit.id)) .get(); - const inverse = createCircuitInsertProjectCommand({ + const externalObjects = suppliedExternalObjects ?? captureExternalObjectDetachTransitions( + database, projectId, rows.map((row) => row.id) + ); + loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Löschen geändert."); + applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Löschen geändert."); + const circuitSnapshot: CircuitSnapshot = { id: circuit.id, circuitListId: circuit.circuitListId, sectionId: circuit.sectionId, @@ -239,7 +273,7 @@ export class CircuitStructureProjectCommandRepository remark: circuit.remark, deviceRows: rows.map(toCircuitDeviceRowSnapshot), protectionDevice: protectionDevice ?? null, - }); + }; assertCircuitsAreNotReferencedAsFeeders(database, projectId, [circuit.id]); @@ -255,7 +289,10 @@ export class CircuitStructureProjectCommandRepository if (result.changes !== 1) { throw new Error("Circuit could not be deleted."); } - return inverse; + return { + forward: createCircuitDeleteProjectCommand(circuitId, expectedCircuitListId, externalObjects), + inverse: createCircuitInsertProjectCommand(circuitSnapshot, invertExternalObjectLinkTransitions(externalObjects)), + }; } private assertCircuitLocation( diff --git a/src/db/repositories/distribution-board-structure-project-command.repository.ts b/src/db/repositories/distribution-board-structure-project-command.repository.ts index 115705b..f7003c5 100644 --- a/src/db/repositories/distribution-board-structure-project-command.repository.ts +++ b/src/db/repositories/distribution-board-structure-project-command.repository.ts @@ -46,6 +46,7 @@ import { floors } from "../schema/floors.js"; import { projects } from "../schema/projects.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; import { assertBoardsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js"; +import { assertDistributionBoardHasNoExternalObjects } from "./external-object-deletion-guard.persistence.js"; import { CircuitStructureProjectCommandRepository } from "./circuit-structure-project-command.repository.js"; export class DistributionBoardStructureProjectCommandRepository @@ -290,6 +291,7 @@ export class DistributionBoardStructureProjectCommandRepository } assertBoardsAreNotReferencedAsFeeders(database, projectId, [board.id]); + assertDistributionBoardHasNoExternalObjects(database, projectId, board.id); const deleted = database .delete(distributionBoards) diff --git a/src/db/repositories/distribution-board-subtree-project-command.repository.ts b/src/db/repositories/distribution-board-subtree-project-command.repository.ts index 969c287..b32fee5 100644 --- a/src/db/repositories/distribution-board-subtree-project-command.repository.ts +++ b/src/db/repositories/distribution-board-subtree-project-command.repository.ts @@ -33,6 +33,7 @@ import { rooms } from "../schema/rooms.js"; import { toCircuitDeviceRowInsertValues } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; import { assertBoardsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js"; +import { assertDistributionBoardHasNoExternalObjects } from "./external-object-deletion-guard.persistence.js"; export class DistributionBoardSubtreeProjectCommandRepository implements DistributionBoardSubtreeProjectCommandStore @@ -86,6 +87,7 @@ export class DistributionBoardSubtreeProjectCommandRepository assertBoardsAreNotReferencedAsFeeders(database, projectId, [ current.distributionBoard.id, ]); + assertDistributionBoardHasNoExternalObjects(database, projectId, current.distributionBoard.id); const deleted = database .delete(distributionBoards) .where( diff --git a/src/db/repositories/external-export-batch.repository.ts b/src/db/repositories/external-export-batch.repository.ts new file mode 100644 index 0000000..c8f7a7d --- /dev/null +++ b/src/db/repositories/external-export-batch.repository.ts @@ -0,0 +1,18 @@ +import { desc, eq } from "drizzle-orm"; +import type { AppDatabase } from "../database-context.js"; +import { externalExportBatches } from "../schema/external-export-batches.js"; + +export class ExternalExportBatchRepository { + constructor(private readonly database: AppDatabase) {} + + append(batch: typeof externalExportBatches.$inferInsert) { + this.database.insert(externalExportBatches).values(batch).run(); + return batch; + } + + listByProject(projectId: string) { + return this.database.select().from(externalExportBatches) + .where(eq(externalExportBatches.projectId, projectId)) + .orderBy(desc(externalExportBatches.exportedAtIso), desc(externalExportBatches.id)).all(); + } +} diff --git a/src/db/repositories/external-object-assignment.persistence.ts b/src/db/repositories/external-object-assignment.persistence.ts index 1111730..505481d 100644 --- a/src/db/repositories/external-object-assignment.persistence.ts +++ b/src/db/repositories/external-object-assignment.persistence.ts @@ -11,6 +11,27 @@ export interface ExternalObjectLinkTransition { target: ExternalModelObjectSnapshot; } +export function captureExternalObjectDetachTransitions( + database: AppDatabase, + projectId: string, + rowIds: string[] +): ExternalObjectLinkTransition[] { + if (rowIds.length === 0) return []; + return database.select().from(externalModelObjects) + .where(and( + eq(externalModelObjects.projectId, projectId), + inArray(externalModelObjects.circuitDeviceRowId, rowIds) + )).all() + .map(toExternalObjectSnapshot) + .map((expected) => ({ expected, target: { ...expected, circuitDeviceRowId: null } })); +} + +export function invertExternalObjectLinkTransitions( + transitions: ExternalObjectLinkTransition[] +): ExternalObjectLinkTransition[] { + return transitions.map(({ expected, target }) => ({ expected: target, target: expected })); +} + export function loadExpectedExternalObjectTransitions( database: AppDatabase, projectId: string, @@ -86,7 +107,13 @@ export function applyExternalObjectLinks( ) { for (const { expected, target } of transitions) { const updated = database.update(externalModelObjects) - .set({ circuitDeviceRowId: target.circuitDeviceRowId }) + .set({ + circuitDeviceRowId: target.circuitDeviceRowId, + distributionBoardId: target.distributionBoardId, + linkedProjectDeviceId: target.linkedProjectDeviceId, + planningValues: target.planningValues, + overriddenFields: target.overriddenFields, + }) .where(and( eq(externalModelObjects.id, expected.id), expected.circuitDeviceRowId === null diff --git a/src/db/repositories/external-object-deletion-guard.persistence.ts b/src/db/repositories/external-object-deletion-guard.persistence.ts new file mode 100644 index 0000000..45fdac7 --- /dev/null +++ b/src/db/repositories/external-object-deletion-guard.persistence.ts @@ -0,0 +1,37 @@ +import { and, eq } from "drizzle-orm"; +import type { AppDatabase } from "../database-context.js"; +import { externalModelObjects } from "../schema/external-model-objects.js"; +import { externalRoomMappings } from "../schema/external-room-mappings.js"; + +export function assertDistributionBoardHasNoExternalObjects( + database: AppDatabase, + projectId: string, + distributionBoardId: string +) { + const reference = database.select({ id: externalModelObjects.id }) + .from(externalModelObjects) + .where(and( + eq(externalModelObjects.projectId, projectId), + eq(externalModelObjects.distributionBoardId, distributionBoardId) + )).limit(1).get(); + if (reference) { + throw new Error("Der Verteiler kann nicht gelöscht werden, solange Revit-Objekte auf ihn verweisen."); + } +} + +export function assertRoomHasNoExternalObjects( + database: AppDatabase, + projectId: string, + roomId: string +) { + const reference = database.select({ id: externalModelObjects.id }) + .from(externalModelObjects) + .innerJoin(externalRoomMappings, eq(externalRoomMappings.id, externalModelObjects.externalRoomMappingId)) + .where(and( + eq(externalModelObjects.projectId, projectId), + eq(externalRoomMappings.roomId, roomId) + )).limit(1).get(); + if (reference) { + throw new Error("Der Raum kann nicht gelöscht werden, solange Revit-Objekte auf ihn verweisen."); + } +} diff --git a/src/db/repositories/project-location-structure-project-command.repository.ts b/src/db/repositories/project-location-structure-project-command.repository.ts index 51edb3b..40a1743 100644 --- a/src/db/repositories/project-location-structure-project-command.repository.ts +++ b/src/db/repositories/project-location-structure-project-command.repository.ts @@ -46,6 +46,7 @@ import { distributionBoards } from "../schema/distribution-boards.js"; import { projects } from "../schema/projects.js"; import { rooms } from "../schema/rooms.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { assertRoomHasNoExternalObjects } from "./external-object-deletion-guard.persistence.js"; export class ProjectLocationStructureProjectCommandRepository implements ProjectLocationStructureProjectCommandStore @@ -439,6 +440,7 @@ export class ProjectLocationStructureProjectCommandRepository "Der Raum kann nicht gelöscht werden, solange er in einem Stromkreis verwendet wird." ); } + assertRoomHasNoExternalObjects(database, projectId, expected.id); const deleted = database .delete(rooms) .where( diff --git a/src/db/schema/external-export-batches.ts b/src/db/schema/external-export-batches.ts new file mode 100644 index 0000000..37e94d8 --- /dev/null +++ b/src/db/schema/external-export-batches.ts @@ -0,0 +1,24 @@ +import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import type { ExternalModelConfiguration } from "../../external-model/configuration/external-model-configuration.js"; +import { projects } from "./projects.js"; + +export const externalExportScopes = ["all", "deviating", "selected"] as const; +export type ExternalExportScope = (typeof externalExportScopes)[number]; + +export const externalExportBatches = sqliteTable( + "external_export_batches", + { + id: text("id").primaryKey(), + projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }), + projectRevision: integer("project_revision").notNull(), + importBatchId: text("import_batch_id").notNull(), + exportedAtIso: text("exported_at_iso").notNull(), + fileName: text("file_name").notNull(), + sha256: text("sha256").notNull(), + configurationSnapshot: text("configuration_snapshot", { mode: "json" }).$type().notNull(), + configurationSha256: text("configuration_sha256").notNull(), + scope: text("scope", { enum: externalExportScopes }).notNull(), + objectCount: integer("object_count").notNull(), + }, + (table) => [index("external_export_batches_project_exported_idx").on(table.projectId, table.exportedAtIso)] +); diff --git a/src/domain/models/circuit-device-row-structure-project-command.model.ts b/src/domain/models/circuit-device-row-structure-project-command.model.ts index 2464a27..ebba5ac 100644 --- a/src/domain/models/circuit-device-row-structure-project-command.model.ts +++ b/src/domain/models/circuit-device-row-structure-project-command.model.ts @@ -1,4 +1,11 @@ import type { SerializedProjectCommand } from "./project-command.model.js"; +import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js"; +import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js"; + +export interface ExternalObjectLinkSnapshotTransition { + expected: ExternalModelObjectSnapshot; + target: ExternalModelObjectSnapshot; +} export const circuitDeviceRowInsertCommandType = "circuit-device-row.insert" as const; @@ -32,11 +39,13 @@ export interface CircuitDeviceRowSnapshot { export interface CircuitDeviceRowInsertCommandPayload { row: CircuitDeviceRowSnapshot; + externalObjects?: ExternalObjectLinkSnapshotTransition[]; } export interface CircuitDeviceRowDeleteCommandPayload { rowId: string; expectedCircuitId: string; + externalObjects?: ExternalObjectLinkSnapshotTransition[]; } export interface CircuitDeviceRowInsertProjectCommand @@ -56,7 +65,8 @@ export type CircuitDeviceRowStructureProjectCommand = | CircuitDeviceRowDeleteProjectCommand; export function createCircuitDeviceRowInsertProjectCommand( - row: CircuitDeviceRowSnapshot + row: CircuitDeviceRowSnapshot, + externalObjects?: ExternalObjectLinkSnapshotTransition[] ): CircuitDeviceRowInsertProjectCommand { const normalizedRow = { ...row, @@ -65,7 +75,7 @@ export function createCircuitDeviceRowInsertProjectCommand( const command: CircuitDeviceRowInsertProjectCommand = { schemaVersion: circuitDeviceRowStructureCommandSchemaVersion, type: circuitDeviceRowInsertCommandType, - payload: { row: normalizedRow }, + payload: { row: normalizedRow, ...(externalObjects?.length ? { externalObjects } : {}) }, }; assertCircuitDeviceRowInsertProjectCommand(command); return command; @@ -73,12 +83,13 @@ export function createCircuitDeviceRowInsertProjectCommand( export function createCircuitDeviceRowDeleteProjectCommand( rowId: string, - expectedCircuitId: string + expectedCircuitId: string, + externalObjects?: ExternalObjectLinkSnapshotTransition[] ): CircuitDeviceRowDeleteProjectCommand { const command: CircuitDeviceRowDeleteProjectCommand = { schemaVersion: circuitDeviceRowStructureCommandSchemaVersion, type: circuitDeviceRowDeleteCommandType, - payload: { rowId, expectedCircuitId }, + payload: { rowId, expectedCircuitId, ...(externalObjects?.length ? { externalObjects } : {}) }, }; assertCircuitDeviceRowDeleteProjectCommand(command); return command; @@ -143,6 +154,7 @@ export function assertCircuitDeviceRowInsertProjectCommand( throw new Error("row.cosPhi must be positive or null."); } } + assertExternalObjectLinkTransitions(command.payload.externalObjects); } export function assertCircuitDeviceRowDeleteProjectCommand( @@ -163,6 +175,41 @@ export function assertCircuitDeviceRowDeleteProjectCommand( command.payload.expectedCircuitId, "expectedCircuitId" ); + assertExternalObjectLinkTransitions(command.payload.externalObjects); +} + +export function assertExternalObjectLinkTransitions(value: unknown) { + if (value === undefined) return; + if (!Array.isArray(value)) throw new Error("externalObjects must be an array."); + const ids = new Set(); + for (const transition of value) { + if (!isPlainObject(transition) || !isPlainObject(transition.expected) || !isPlainObject(transition.target)) { + throw new Error("Invalid external object link transition."); + } + const expectedId = transition.expected.id; + const targetId = transition.target.id; + if (typeof expectedId !== "string" || expectedId !== targetId || ids.has(expectedId)) { + throw new Error("External object link transition has mismatched or duplicate ids."); + } + ids.add(expectedId); + const expectedRowId = transition.expected.circuitDeviceRowId; + const targetRowId = transition.target.circuitDeviceRowId; + if ((expectedRowId !== null && typeof expectedRowId !== "string") || (targetRowId !== null && typeof targetRowId !== "string")) { + throw new Error("Invalid external object row link."); + } + const parsed = parseExternalModelStateSnapshot({ source: null, importBatches: [], roomMappings: [], objects: [transition.expected, transition.target] }).objects; + const expectedRest = { ...parsed[0], circuitDeviceRowId: null }; + const targetRest = { ...parsed[1], circuitDeviceRowId: null }; + if (canonicalJson(expectedRest) !== canonicalJson(targetRest) || expectedRowId === targetRowId) { + throw new Error("External object transition may only change the row link."); + } + } +} + +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + if (isPlainObject(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`; + return JSON.stringify(value) ?? "null"; } function assertNonEmptyString(value: unknown, field: string) { diff --git a/src/domain/models/circuit-group-subtree-project-command.model.ts b/src/domain/models/circuit-group-subtree-project-command.model.ts index a3e53b3..4470cd2 100644 --- a/src/domain/models/circuit-group-subtree-project-command.model.ts +++ b/src/domain/models/circuit-group-subtree-project-command.model.ts @@ -3,6 +3,7 @@ import { type CircuitGroupSubtreeSnapshot, } from "./circuit-group-subtree-snapshot.model.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; +import { assertExternalObjectLinkTransitions, type ExternalObjectLinkSnapshotTransition } from "./circuit-device-row-structure-project-command.model.js"; export const circuitGroupDeleteSubtreeCommandType = "circuit-group.delete-subtree" as const; @@ -12,6 +13,7 @@ export const circuitGroupSubtreeCommandSchemaVersion = 1 as const; interface Payload { snapshot: CircuitGroupSubtreeSnapshot; + externalObjects?: ExternalObjectLinkSnapshotTransition[]; } export interface CircuitGroupDeleteSubtreeProjectCommand @@ -31,24 +33,26 @@ export type CircuitGroupSubtreeProjectCommand = | CircuitGroupRestoreSubtreeProjectCommand; export function createCircuitGroupDeleteSubtreeProjectCommand( - snapshot: CircuitGroupSubtreeSnapshot + snapshot: CircuitGroupSubtreeSnapshot, + externalObjects?: ExternalObjectLinkSnapshotTransition[] ): CircuitGroupDeleteSubtreeProjectCommand { const command: CircuitGroupDeleteSubtreeProjectCommand = { schemaVersion: circuitGroupSubtreeCommandSchemaVersion, type: circuitGroupDeleteSubtreeCommandType, - payload: { snapshot }, + payload: { snapshot, ...(externalObjects?.length ? { externalObjects } : {}) }, }; assertCircuitGroupDeleteSubtreeProjectCommand(command); return command; } export function createCircuitGroupRestoreSubtreeProjectCommand( - snapshot: CircuitGroupSubtreeSnapshot + snapshot: CircuitGroupSubtreeSnapshot, + externalObjects?: ExternalObjectLinkSnapshotTransition[] ): CircuitGroupRestoreSubtreeProjectCommand { const command: CircuitGroupRestoreSubtreeProjectCommand = { schemaVersion: circuitGroupSubtreeCommandSchemaVersion, type: circuitGroupRestoreSubtreeCommandType, - payload: { snapshot }, + payload: { snapshot, ...(externalObjects?.length ? { externalObjects } : {}) }, }; assertCircuitGroupRestoreSubtreeProjectCommand(command); return command; @@ -76,11 +80,12 @@ function assertCommand( command.schemaVersion !== circuitGroupSubtreeCommandSchemaVersion || command.type !== type || !isPlainObject(command.payload) || - Object.keys(command.payload).length !== 1 + Object.keys(command.payload).some((key) => key !== "snapshot" && key !== "externalObjects") ) { throw new Error("Unsupported circuit-group subtree command."); } assertCircuitGroupSubtreeSnapshot(command.payload.snapshot); + assertExternalObjectLinkTransitions(command.payload.externalObjects); } function isPlainObject(value: unknown): value is Record { diff --git a/src/domain/models/circuit-structure-project-command.model.ts b/src/domain/models/circuit-structure-project-command.model.ts index 5260c4b..292c776 100644 --- a/src/domain/models/circuit-structure-project-command.model.ts +++ b/src/domain/models/circuit-structure-project-command.model.ts @@ -3,6 +3,8 @@ import { circuitDeviceRowInsertCommandType, circuitDeviceRowStructureCommandSchemaVersion, type CircuitDeviceRowSnapshot, + type ExternalObjectLinkSnapshotTransition, + assertExternalObjectLinkTransitions, } from "./circuit-device-row-structure-project-command.model.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; import { @@ -37,11 +39,13 @@ export interface CircuitSnapshot { export interface CircuitInsertCommandPayload { circuit: CircuitSnapshot; + externalObjects?: ExternalObjectLinkSnapshotTransition[]; } export interface CircuitDeleteCommandPayload { circuitId: string; expectedCircuitListId: string; + externalObjects?: ExternalObjectLinkSnapshotTransition[]; } export interface CircuitInsertProjectCommand @@ -61,12 +65,13 @@ export type CircuitStructureProjectCommand = | CircuitDeleteProjectCommand; export function createCircuitInsertProjectCommand( - circuit: CircuitSnapshot + circuit: CircuitSnapshot, + externalObjects?: ExternalObjectLinkSnapshotTransition[] ): CircuitInsertProjectCommand { const command: CircuitInsertProjectCommand = { schemaVersion: circuitStructureCommandSchemaVersion, type: circuitInsertCommandType, - payload: { circuit }, + payload: { circuit, ...(externalObjects?.length ? { externalObjects } : {}) }, }; assertCircuitInsertProjectCommand(command); return command; @@ -74,12 +79,13 @@ export function createCircuitInsertProjectCommand( export function createCircuitDeleteProjectCommand( circuitId: string, - expectedCircuitListId: string + expectedCircuitListId: string, + externalObjects?: ExternalObjectLinkSnapshotTransition[] ): CircuitDeleteProjectCommand { const command: CircuitDeleteProjectCommand = { schemaVersion: circuitStructureCommandSchemaVersion, type: circuitDeleteCommandType, - payload: { circuitId, expectedCircuitListId }, + payload: { circuitId, expectedCircuitListId, ...(externalObjects?.length ? { externalObjects } : {}) }, }; assertCircuitDeleteProjectCommand(command); return command; @@ -168,6 +174,7 @@ export function assertCircuitInsertProjectCommand( } rowIds.add(row.id); } + assertExternalObjectLinkTransitions(command.payload.externalObjects); } export function assertCircuitDeleteProjectCommand( @@ -187,6 +194,7 @@ export function assertCircuitDeleteProjectCommand( command.payload.expectedCircuitListId, "expectedCircuitListId" ); + assertExternalObjectLinkTransitions(command.payload.externalObjects); } function assertNonEmptyString(value: unknown, field: string) { diff --git a/src/domain/models/circuit-tree.model.ts b/src/domain/models/circuit-tree.model.ts index 00a0c2d..fd1bd2c 100644 --- a/src/domain/models/circuit-tree.model.ts +++ b/src/domain/models/circuit-tree.model.ts @@ -20,6 +20,7 @@ export interface CircuitTreeDeviceRow { remark?: string; overriddenFields?: string; rowTotalPower: number; + externalObjectCount?: number; } export interface CircuitTreeCircuit { diff --git a/src/domain/models/external-object-new-circuit-project-command.model.ts b/src/domain/models/external-object-new-circuit-project-command.model.ts index b34a449..e7a3e23 100644 --- a/src/domain/models/external-object-new-circuit-project-command.model.ts +++ b/src/domain/models/external-object-new-circuit-project-command.model.ts @@ -146,8 +146,8 @@ function assertOnlyAssignmentChanged( expected: ExternalModelObjectSnapshot, target: ExternalModelObjectSnapshot ) { - const expectedRest = { ...expected, circuitDeviceRowId: null }; - const targetRest = { ...target, circuitDeviceRowId: null }; + const expectedRest = { ...expected, circuitDeviceRowId: null, distributionBoardId: null }; + const targetRest = { ...target, circuitDeviceRowId: null, distributionBoardId: null }; if (canonicalJson(expectedRest) !== canonicalJson(targetRest)) { throw new Error("New-circuit assignment may only change the external object row link."); } diff --git a/src/domain/models/external-object-new-row-project-command.model.ts b/src/domain/models/external-object-new-row-project-command.model.ts index d0416dd..6bae039 100644 --- a/src/domain/models/external-object-new-row-project-command.model.ts +++ b/src/domain/models/external-object-new-row-project-command.model.ts @@ -141,8 +141,8 @@ function assertOnlyAssignmentChanged( expected: ExternalModelObjectSnapshot, target: ExternalModelObjectSnapshot ) { - const expectedRest = { ...expected, circuitDeviceRowId: null }; - const targetRest = { ...target, circuitDeviceRowId: null }; + const expectedRest = { ...expected, circuitDeviceRowId: null, distributionBoardId: null }; + const targetRest = { ...target, circuitDeviceRowId: null, distributionBoardId: null }; if (canonicalJson(expectedRest) !== canonicalJson(targetRest)) { throw new Error("New-row assignment may only change the external object row link."); } diff --git a/src/domain/models/external-object-row-assignment-project-command.model.ts b/src/domain/models/external-object-row-assignment-project-command.model.ts index a45fe31..cb4ae97 100644 --- a/src/domain/models/external-object-row-assignment-project-command.model.ts +++ b/src/domain/models/external-object-row-assignment-project-command.model.ts @@ -104,10 +104,10 @@ export function assertExternalObjectRowAssignmentProjectCommand( if (expected.circuitDeviceRowId === target.circuitDeviceRowId) { throw new Error("External object row assignment must change its row link."); } - assertOnlyFieldChanged( + assertOnlyFieldsChanged( expected as unknown as Record, target as unknown as Record, - "circuitDeviceRowId", + new Set(["circuitDeviceRowId", "distributionBoardId", "linkedProjectDeviceId", "planningValues", "overriddenFields"]), "external object" ); if (expected.circuitDeviceRowId) affectedRowIds.add(expected.circuitDeviceRowId); @@ -161,11 +161,22 @@ function assertOnlyFieldChanged( target: Record, allowedField: string, label: string +) { + assertOnlyFieldsChanged(expected, target, new Set([allowedField]), label); +} + +function assertOnlyFieldsChanged( + expected: Record, + target: Record, + allowedFields: ReadonlySet, + label: string ) { const expectedRest = { ...expected }; const targetRest = { ...target }; - delete expectedRest[allowedField]; - delete targetRest[allowedField]; + for (const field of allowedFields) { + delete expectedRest[field]; + delete targetRest[field]; + } if (canonicalJson(expectedRest) !== canonicalJson(targetRest)) { throw new Error(`Row assignment may only change the ${label} assignment state.`); } diff --git a/src/external-model/application/external-xlsx-return-projection.ts b/src/external-model/application/external-xlsx-return-projection.ts new file mode 100644 index 0000000..a80125c --- /dev/null +++ b/src/external-model/application/external-xlsx-return-projection.ts @@ -0,0 +1,135 @@ +import type { ExternalModelObjectSnapshot } from "../domain/external-model-contracts.js"; +import type { ExternalXlsxCircuitMode, ExternalXlsxReturnValues } from "../xlsx/external-xlsx-contracts.js"; +import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js"; + +export interface ExternalReturnRowContext { + rowId: string; + displayName: string; + powerPerUnitKw: number; + cosPhi: number | null; + phaseType: string | null; + category: CircuitGroupCategory | null; + circuitIdentifier: string; + circuitVoltageV: number | null; + distributionBoardName: string; +} + +export interface ExternalReturnProjection { + objectId: string; + worksharingId: string; + values: ExternalXlsxReturnValues; + differsFromImported: boolean; + warnings: string[]; +} + +export function getRevitBoardName(board: { name: string }) { + return board.name; +} + +export function projectExternalXlsxReturn(input: { + object: ExternalModelObjectSnapshot; + row: ExternalReturnRowContext | null; + circuitMode: ExternalXlsxCircuitMode; + singlePhaseVoltageV: number; + threePhaseVoltageV: number; +}): ExternalReturnProjection { + const { object, row } = input; + const planning = object.planningValues; + const warnings: string[] = []; + const category = row?.category ?? planning.category; + const phaseType = row?.phaseType ?? (category === "three_phase" ? "three_phase" : category ? "single_phase" : null); + const voltageV = row?.circuitVoltageV ?? defaultVoltage(category, input.singlePhaseVoltageV, input.threePhaseVoltageV); + const powerPerUnitW = row ? row.powerPerUnitKw * 1000 : planning.powerPerUnitW; + const electricalPowerW = powerPerUnitW === null + ? null + : roundNumber(planning.effectiveQuantity * powerPerUnitW, 6); + const cosPhi = row?.cosPhi ?? planning.cosPhi; + const currentA = calculateCurrent({ electricalPowerW, voltageV, cosPhi, phaseType }); + if (electricalPowerW === null) warnings.push("Elektrische Leistung ist nicht berechenbar."); + if (voltageV === null) warnings.push("Spannung ist nicht bestimmbar."); + if (currentA === null) warnings.push("Stromstärke ist nicht berechenbar."); + + const values: ExternalXlsxReturnValues = { + ...(input.circuitMode === "combined" + ? { circuitCombined: row ? `${getRevitBoardName({ name: row.distributionBoardName })}${row.circuitIdentifier}` : "" } + : { + boardName: row ? getRevitBoardName({ name: row.distributionBoardName }) : "", + circuitIdentifier: row?.circuitIdentifier ?? "", + }), + designation: row?.displayName ?? planning.displayName ?? "", + electricalPowerW, + voltageV, + currentA, + }; + const source = object.acceptedSourceValues; + const sourceValues: ExternalXlsxReturnValues = input.circuitMode === "combined" + ? { + circuitCombined: source.circuitCombined, + designation: source.designation, + electricalPowerW: source.electricalPowerW, + voltageV: source.voltageV, + currentA: source.currentA, + } + : { + boardName: source.boardName, + circuitIdentifier: source.circuitIdentifier, + designation: source.designation, + electricalPowerW: source.electricalPowerW, + voltageV: source.voltageV, + currentA: source.currentA, + }; + + return { + objectId: object.id, + worksharingId: object.worksharingId, + values, + differsFromImported: Object.keys(values).some((key) => + !sameScalar(values[key as keyof ExternalXlsxReturnValues], sourceValues[key as keyof ExternalXlsxReturnValues]) + ), + warnings, + }; +} + +function defaultVoltage(category: CircuitGroupCategory | null, singlePhase: number, threePhase: number) { + if (category === "three_phase") return threePhase; + if (category === "lighting" || category === "single_phase") return singlePhase; + return null; +} + +function calculateCurrent(input: { electricalPowerW: number | null; voltageV: number | null; cosPhi: number | null; phaseType: string | null }) { + if ( + input.electricalPowerW === null || + input.voltageV === null || input.voltageV <= 0 || + input.cosPhi === null || input.cosPhi <= 0 || + (input.phaseType !== "single_phase" && input.phaseType !== "three_phase") + ) return null; + const divisor = input.phaseType === "three_phase" + ? Math.sqrt(3) * input.voltageV * input.cosPhi + : input.voltageV * input.cosPhi; + return roundNumber(input.electricalPowerW / divisor, 2); +} + +function sameScalar(left: string | number | boolean | null | undefined, right: string | number | boolean | null | undefined) { + const normalizedLeft = normalizeScalar(left); + const normalizedRight = normalizeScalar(right); + if (typeof normalizedLeft === "number" && typeof normalizedRight === "number") { + return Math.abs(normalizedLeft - normalizedRight) < 1e-9; + } + return normalizedLeft === normalizedRight; +} + +function normalizeScalar(value: string | number | boolean | null | undefined): string | number | boolean { + if (value === null || value === undefined || value === "") return ""; + if (typeof value === "string") { + const trimmed = value.trim(); + if (!trimmed) return ""; + const numeric = Number(trimmed.replace(",", ".")); + return Number.isFinite(numeric) ? numeric : trimmed; + } + return value; +} + +function roundNumber(value: number, decimals: number) { + const factor = 10 ** decimals; + return Math.round((value + Number.EPSILON) * factor) / factor; +} diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index f75b64e..5418922 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -120,6 +120,11 @@ import { deleteCircuitGroupCommand, deleteCircuitGroupSubtreeCommand, deleteDistributionBoardComponentCommand, + assignExternalProjectObjectsToRow, + assignExternalProjectObjectsToNewRow, + assignExternalProjectObjectsToNewCircuit, + detachExternalProjectObjectsFromRow, + getExternalProjectObjects, getCircuitTree, getNextCircuitIdentifier, getProjectHistory, @@ -154,6 +159,7 @@ import type { ProjectCommandResultDto, ProjectDeviceDto, ProjectHistoryStateDto, + ExternalProjectObjectsDto, } from "../types"; import type { CircuitDeviceRowSnapshot, @@ -330,6 +336,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const [cableSizingEditorCircuit, setCableSizingEditorCircuit] = useState(null); const [projectDevices, setProjectDevices] = useState([]); + const [externalObjects, setExternalObjects] = useState(null); + const [isRevitDrawerOpen, setIsRevitDrawerOpen] = useState(false); + const [draggingExternalObjectIds, setDraggingExternalObjectIds] = useState([]); const [isProjectDeviceDrawerOpen, setIsProjectDeviceDrawerOpen] = useState(false); const [projectDeviceSearch, setProjectDeviceSearch] = useState(""); @@ -381,6 +390,26 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str () => orderedColumns.filter((column) => visibleColumnKeys.includes(column.key)), [orderedColumns, visibleColumnKeys] ); + const externalObjectGroups = useMemo(() => { + if (!data || !externalObjects) return []; + const groups = new Map(); + for (const object of externalObjects.objects) { + if (object.presenceStatus !== "present" || object.circuitDeviceRowId !== null || object.distributionBoardId !== data.distributionBoardId) continue; + const key = JSON.stringify([ + object.roomId, object.selectionMarker, object.deviceTypeId, + object.planningValues.category, object.linkedProjectDeviceId, + object.planningValues, + ]); + const entries = groups.get(key) ?? []; + entries.push(object); + groups.set(key, entries); + } + return [...groups.values()].map((objects) => ({ + key: objects.map((object) => object.id).sort().join("|"), + objects, + quantity: objects.reduce((sum, object) => sum + object.planningValues.effectiveQuantity, 0), + })); + }, [data, externalObjects]); async function loadTree(options?: { showLoading?: boolean }) { const showLoading = options?.showLoading ?? false; @@ -389,13 +418,17 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str } setError(null); try { - const { tree, history } = await loadCircuitEditorSnapshot( - () => getCircuitTree(projectId, circuitListId), - () => getProjectHistory(projectId) - ); + const [{ tree, history }, external] = await Promise.all([ + loadCircuitEditorSnapshot( + () => getCircuitTree(projectId, circuitListId), + () => getProjectHistory(projectId) + ), + getExternalProjectObjects(projectId), + ]); projectRevisionRef.current = history.currentRevision; setHistoryState(history); setData(tree); + setExternalObjects(external); } catch (err) { setError(normalizeUiError(err)); } finally { @@ -1321,12 +1354,20 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const summary = subtree ? summarizeCircuitGroupSubtree(subtree) : null; + const externalObjectCount = section.circuits.reduce( + (total, circuit) => total + circuit.deviceRows.reduce( + (rowTotal, row) => rowTotal + (row.externalObjectCount ?? 0), 0 + ), 0 + ); + const externalWarning = externalObjectCount > 0 + ? `\n\n${externalObjectCount} Revit-Objekte werden wieder unzugeordnet.` + : ""; const warning = isEmpty ? `Leere Stromkreisgruppe „${section.displayName}“ entfernen?` : `Stromkreisgruppe „${section.displayName}“ vollständig entfernen?\n\n` + `Dabei werden ${summary!.circuitCount} Stromkreis(e), ${summary!.deviceRowCount} Gerätezeile(n) und ${summary!.protectionComponentCount} Gruppenschutzgerät(e) gelöscht.\n\n` + "Die Änderung kann über die projektweite Versionshistorie rückgängig gemacht werden."; - if (!confirm(warning)) { + if (!confirm(warning + externalWarning)) { return; } await runCommand({ @@ -2878,11 +2919,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str setError("Die Gerätezeile ist ungültig."); return; } + const externalWarning = (rowSnapshot.externalObjectCount ?? 0) > 0 + ? `\n\n${rowSnapshot.externalObjectCount} Revit-Objekte werden wieder unzugeordnet.` + : ""; if (sourceCircuit.deviceRows.length === 1) { const keepAsReserve = confirm( `„${rowSnapshot.displayName}“ ist das letzte Gerät in ${sourceCircuit.equipmentIdentifier}.\n\n` + "OK: Gerät löschen und den leeren Stromkreis als Reserve behalten.\n" + - "Abbrechen: anschließend entscheiden, ob stattdessen der gesamte Stromkreis gelöscht werden soll." + "Abbrechen: anschließend entscheiden, ob stattdessen der gesamte Stromkreis gelöscht werden soll." + + externalWarning ); if (!keepAsReserve) { const deleteCircuit = confirm( @@ -2894,7 +2939,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str } return; } - } else if (!confirm(`Gerätezeile „${rowSnapshot.displayName}“ löschen?`)) { + } else if (!confirm(`Gerätezeile „${rowSnapshot.displayName}“ löschen?${externalWarning}`)) { return; } await runCommand({ @@ -2917,11 +2962,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str setError("Der Stromkreis ist ungültig."); return; } + const externalObjectCount = circuit.deviceRows.reduce( + (total, row) => total + (row.externalObjectCount ?? 0), 0 + ); + const externalWarning = externalObjectCount > 0 + ? `\n\n${externalObjectCount} Revit-Objekte werden wieder unzugeordnet.` + : ""; if ( askForConfirmation && !confirm( `Stromkreis ${circuit.equipmentIdentifier} und ${circuit.deviceRows.length} zugeordnete Gerätezeile(n) löschen?\n\n` + - "Bestehende Betriebsmittelkennzeichen werden nicht neu nummeriert." + "Bestehende Betriebsmittelkennzeichen werden nicht neu nummeriert." + + externalWarning ) ) { return; @@ -2958,6 +3010,56 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str await handleDeleteCircuit(intent.circuitId); } + async function handleAssignExternalObjects(rowId: string, objectIds: string[]) { + if (!confirm(`${objectIds.length} Revit-Objekt(e) dieser Gerätezeile zuordnen?\n\nAbweichende Planungswerte werden ausdrücklich zugunsten der Gerätezeile bestätigt.`)) return; + await runCommand({ + label: "Revit-Objekte zuordnen", + redo: async () => { + const result = await assignExternalProjectObjectsToRow( + projectId, + getExpectedProjectRevision(), + objectIds, + rowId, + objectIds + ); + applyProjectCommandResult(result); + setDraggingExternalObjectIds([]); + return null; + }, + }); + } + + async function handleAssignExternalObjectsToNewRow(circuitId: string, objectIds: string[]) { + if (!confirm(`${objectIds.length} Revit-Objekt(e) als neue Gerätezeile anlegen?`)) return; + await runCommand({ label: "Revit-Objekte als Gerätezeile zuordnen", redo: async () => { + const result = await assignExternalProjectObjectsToNewRow(projectId, getExpectedProjectRevision(), objectIds, circuitId); + applyProjectCommandResult(result); setDraggingExternalObjectIds([]); return null; + }}); + } + + async function handleAssignExternalObjectsToNewCircuit(sectionId: string, objectIds: string[]) { + if (!confirm(`${objectIds.length} Revit-Objekt(e) als neuen Stromkreis anlegen?`)) return; + await runCommand({ label: "Revit-Objekte als Stromkreis zuordnen", redo: async () => { + const result = await assignExternalProjectObjectsToNewCircuit(projectId, getExpectedProjectRevision(), objectIds, sectionId); + applyProjectCommandResult(result); setDraggingExternalObjectIds([]); return null; + }}); + } + + async function handleDetachExternalObjects(rowId: string) { + const objectIds = externalObjects?.objects + .filter((object) => object.circuitDeviceRowId === rowId) + .map((object) => object.id) ?? []; + if (objectIds.length === 0 || !confirm(`${objectIds.length} Revit-Objekt(e) lösen?\n\nDie aktuellen Werte der Gerätezeile werden als Planungswerte und Overrides übernommen.`)) return; + await runCommand({ + label: "Revit-Objekte lösen", + redo: async () => { + const result = await detachExternalProjectObjectsFromRow(projectId, getExpectedProjectRevision(), objectIds); + applyProjectCommandResult(result); + return null; + }, + }); + } + // Explicit renumber action through project history. Sorting and device rows // remain unchanged; the server handles collision-safe identifier swaps. async function handleRenumberSection(sectionId: string) { @@ -3381,6 +3483,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str } showLabel /> + sum + group.objects.length, 0)})`} + onClick={() => setIsRevitDrawerOpen((current) => !current)} + showLabel + /> Wird gespeichert … : null}
+ {isRevitDrawerOpen ? ( + + ) : null} {isProjectDeviceDrawerOpen ? (