diff --git a/scripts/db-verify-circuit-schema.js b/scripts/db-verify-circuit-schema.js
index e08483b..f371e8f 100644
--- a/scripts/db-verify-circuit-schema.js
+++ b/scripts/db-verify-circuit-schema.js
@@ -98,6 +98,7 @@ const requiredProjectColumns = [
"work_stage",
"project_lead",
"project_editor",
+ "max_sockets_per_circuit",
];
const projectColumns = new Set(
db.prepare("PRAGMA table_info(projects)").all().map((column) => column.name)
diff --git a/src/app/projects/[projectId]/revit/page.tsx b/src/app/projects/[projectId]/revit/page.tsx
index 8c2735c..fb34bd6 100644
--- a/src/app/projects/[projectId]/revit/page.tsx
+++ b/src/app/projects/[projectId]/revit/page.tsx
@@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
import { IconLink } from "../../../../frontend/components/icon-link";
import { RevitInitialImportWizard } from "../../../../frontend/components/revit-initial-import-wizard";
import { RevitFollowUpImportWizard } from "../../../../frontend/components/revit-follow-up-import-wizard";
+import { RevitDeviceTypeCard } from "../../../../frontend/components/revit-device-type-card";
import { RevitObjectTable } from "../../../../frontend/components/revit-object-table";
import { RevitXlsxSettingsModal } from "../../../../frontend/components/revit-xlsx-settings-modal";
import type { ExternalModelConfiguration } from "../../../../external-model/configuration/external-model-configuration";
@@ -46,6 +47,7 @@ export default function RevitPage() {
Projekt · Revit
{project?.name ?? "Revit-Anbindung"}
Linear-XLSX austauschen und Revit-Objekte verwalten
{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} · Fehlend: {importStatus.missingObjectCount}
> :
Noch kein Linear-XLSX-Import vorhanden.
}
+ {snapshot ? : null}
{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}
{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/0018_project_socket_limit.sql b/src/db/migrations/0018_project_socket_limit.sql
new file mode 100644
index 0000000..59b9cd0
--- /dev/null
+++ b/src/db/migrations/0018_project_socket_limit.sql
@@ -0,0 +1 @@
+ALTER TABLE `projects` ADD `max_sockets_per_circuit` integer;
\ No newline at end of file
diff --git a/src/db/migrations/meta/0018_snapshot.json b/src/db/migrations/meta/0018_snapshot.json
new file mode 100644
index 0000000..73c3333
--- /dev/null
+++ b/src/db/migrations/meta/0018_snapshot.json
@@ -0,0 +1,3217 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "631fc8e7-1e06-4499-849d-c1b22b91bdf5",
+ "prevId": "0090a9c1-0646-480d-91ec-07f831a6daca",
+ "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": false,
+ "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
+ },
+ "max_sockets_per_circuit": {
+ "name": "max_sockets_per_circuit",
+ "type": "integer",
+ "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 5d4e2aa..64cbf0a 100644
--- a/src/db/migrations/meta/_journal.json
+++ b/src/db/migrations/meta/_journal.json
@@ -127,6 +127,13 @@
"when": 1793359753805,
"tag": "0017_circuit_protection_none",
"breakpoints": true
+ },
+ {
+ "idx": 18,
+ "version": "6",
+ "when": 1793391088159,
+ "tag": "0018_project_socket_limit",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/src/db/repositories/project-settings-project-command.repository.ts b/src/db/repositories/project-settings-project-command.repository.ts
index 905577a..9b22c1e 100644
--- a/src/db/repositories/project-settings-project-command.repository.ts
+++ b/src/db/repositories/project-settings-project-command.repository.ts
@@ -4,6 +4,7 @@ import {
baselineProjectSettingsUpdateCommandSchemaVersion,
createProjectSettingsUpdateProjectCommand,
withoutCoverProjectSettingsUpdateCommandSchemaVersion,
+ withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion,
type ProjectSettingsValues,
} from "../../domain/models/project-settings-project-command.model.js";
import { projectCoverFields } from "../../shared/constants/project-cover.js";
@@ -53,17 +54,24 @@ export class ProjectSettingsProjectCommandRepository
// the current project state and are therefore also absent from the
// inverse of the same version.
const command = input.command;
+ const currentSocketLimit = {
+ maxSocketsPerCircuit: current.maxSocketsPerCircuit,
+ };
const target: ProjectSettingsValues =
command.schemaVersion === baselineProjectSettingsUpdateCommandSchemaVersion
? {
...command.payload,
isPublicBuilding: current.isPublicBuilding,
...currentCoverValues,
+ ...currentSocketLimit,
}
: command.schemaVersion ===
withoutCoverProjectSettingsUpdateCommandSchemaVersion
- ? { ...command.payload, ...currentCoverValues }
- : command.payload;
+ ? { ...command.payload, ...currentCoverValues, ...currentSocketLimit }
+ : command.schemaVersion ===
+ withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion
+ ? { ...command.payload, ...currentSocketLimit }
+ : command.payload;
if (projectSettingsEqual(current, target)) {
throw new Error("Project settings did not change.");
}
@@ -80,6 +88,7 @@ export class ProjectSettingsProjectCommandRepository
enabledDistributionBoardSupplyTypes:
current.enabledDistributionBoardSupplyTypes,
...currentCoverValues,
+ ...currentSocketLimit,
};
const inverse =
command.schemaVersion === baselineProjectSettingsUpdateCommandSchemaVersion
@@ -87,7 +96,10 @@ export class ProjectSettingsProjectCommandRepository
: command.schemaVersion ===
withoutCoverProjectSettingsUpdateCommandSchemaVersion
? createWithoutCoverProjectSettingsUpdateCommand(inverseValues)
- : createProjectSettingsUpdateProjectCommand(inverseValues);
+ : command.schemaVersion ===
+ withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion
+ ? createWithoutSocketLimitProjectSettingsUpdateCommand(inverseValues)
+ : createProjectSettingsUpdateProjectCommand(inverseValues);
assertDisabledSupplyTypesAreUnused(tx, input.projectId, target);
const updated = tx
.update(projects)
@@ -103,6 +115,17 @@ export class ProjectSettingsProjectCommandRepository
}
}
+function createWithoutSocketLimitProjectSettingsUpdateCommand(
+ values: ProjectSettingsValues
+) {
+ const { maxSocketsPerCircuit: _omitted, ...payload } = values;
+ return {
+ schemaVersion: withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion,
+ type: "project.update-settings" as const,
+ payload,
+ };
+}
+
function createWithoutCoverProjectSettingsUpdateCommand(
values: ProjectSettingsValues
) {
@@ -158,6 +181,7 @@ function projectSettingsEqual(
current.singlePhaseVoltageV === target.singlePhaseVoltageV &&
current.threePhaseVoltageV === target.threePhaseVoltageV &&
projectCoverFields.every((field) => current[field] === target[field]) &&
+ current.maxSocketsPerCircuit === target.maxSocketsPerCircuit &&
sameSupplyTypes(
current.enabledDistributionBoardSupplyTypes,
target.enabledDistributionBoardSupplyTypes
diff --git a/src/db/repositories/project-state-restore-command.repository.ts b/src/db/repositories/project-state-restore-command.repository.ts
index 2386bf6..dac047c 100644
--- a/src/db/repositories/project-state-restore-command.repository.ts
+++ b/src/db/repositories/project-state-restore-command.repository.ts
@@ -325,6 +325,7 @@ function replaceProjectState(
workStage: state.project.workStage,
projectLead: state.project.projectLead,
projectEditor: state.project.projectEditor,
+ maxSocketsPerCircuit: state.project.maxSocketsPerCircuit,
})
.where(eq(projects.id, state.project.id))
.run();
diff --git a/src/db/repositories/project-state-snapshot.persistence.ts b/src/db/repositories/project-state-snapshot.persistence.ts
index 431241b..a472d12 100644
--- a/src/db/repositories/project-state-snapshot.persistence.ts
+++ b/src/db/repositories/project-state-snapshot.persistence.ts
@@ -51,6 +51,7 @@ export function readProjectStateSnapshot(
workStage: projects.workStage,
projectLead: projects.projectLead,
projectEditor: projects.projectEditor,
+ maxSocketsPerCircuit: projects.maxSocketsPerCircuit,
currentRevision: projects.currentRevision,
})
.from(projects)
@@ -264,6 +265,7 @@ export function readProjectStateSnapshot(
workStage: project.workStage,
projectLead: project.projectLead,
projectEditor: project.projectEditor,
+ maxSocketsPerCircuit: project.maxSocketsPerCircuit,
},
distributionBoards: boardRows,
externalModelConfiguration,
diff --git a/src/db/repositories/project.repository.ts b/src/db/repositories/project.repository.ts
index 9fa0f5c..343293a 100644
--- a/src/db/repositories/project.repository.ts
+++ b/src/db/repositories/project.repository.ts
@@ -35,6 +35,7 @@ export class ProjectRepository {
workStage: null,
projectLead: null,
projectEditor: null,
+ maxSocketsPerCircuit: null,
currentRevision: 0,
};
await db.insert(projects).values(project);
diff --git a/src/db/schema/projects.ts b/src/db/schema/projects.ts
index 5687fa8..fa0eee9 100644
--- a/src/db/schema/projects.ts
+++ b/src/db/schema/projects.ts
@@ -25,5 +25,7 @@ export const projects = sqliteTable("projects", {
workStage: text("work_stage"),
projectLead: text("project_lead"),
projectEditor: text("project_editor"),
+ // Client-specific highest effective socket count per circuit; null = none.
+ maxSocketsPerCircuit: integer("max_sockets_per_circuit"),
currentRevision: integer("current_revision").notNull().default(0),
});
diff --git a/src/domain/models/project-settings-project-command.model.ts b/src/domain/models/project-settings-project-command.model.ts
index fa1b29b..3563421 100644
--- a/src/domain/models/project-settings-project-command.model.ts
+++ b/src/domain/models/project-settings-project-command.model.ts
@@ -8,13 +8,17 @@ import {
projectCoverFields,
type ProjectCoverField,
} from "../../shared/constants/project-cover.js";
+import { isValidMaxSocketsPerCircuit } from "../../shared/constants/socket-limit.js";
export const projectSettingsUpdateCommandType =
"project.update-settings" as const;
-// Version 3 adds the print cover fields. Versions 2 (without cover fields)
-// and 1 (additionally without the public-building flag) stay executable;
-// their missing values keep the current project values.
-export const projectSettingsUpdateCommandSchemaVersion = 3 as const;
+// Version 4 adds the per-project socket limit, version 3 the print cover
+// fields. Versions 3 (without socket limit), 2 (additionally without cover
+// fields) and 1 (additionally without the public-building flag) stay
+// executable; their missing values keep the current project values.
+export const projectSettingsUpdateCommandSchemaVersion = 4 as const;
+export const withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion =
+ 3 as const;
export const withoutCoverProjectSettingsUpdateCommandSchemaVersion = 2 as const;
export const baselineProjectSettingsUpdateCommandSchemaVersion = 1 as const;
@@ -29,10 +33,17 @@ export interface ProjectSettingsValues
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
+ // Highest effective socket count per circuit; null means no limit.
+ maxSocketsPerCircuit: number | null;
}
-export type WithoutCoverProjectSettingsValues = Omit<
+export type WithoutSocketLimitProjectSettingsValues = Omit<
ProjectSettingsValues,
+ "maxSocketsPerCircuit"
+>;
+
+export type WithoutCoverProjectSettingsValues = Omit<
+ WithoutSocketLimitProjectSettingsValues,
ProjectCoverField
>;
@@ -47,6 +58,12 @@ export interface CurrentProjectSettingsUpdateProjectCommand
type: typeof projectSettingsUpdateCommandType;
}
+export interface WithoutSocketLimitProjectSettingsUpdateProjectCommand
+ extends SerializedProjectCommand {
+ schemaVersion: typeof withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion;
+ type: typeof projectSettingsUpdateCommandType;
+}
+
export interface WithoutCoverProjectSettingsUpdateProjectCommand
extends SerializedProjectCommand {
schemaVersion: typeof withoutCoverProjectSettingsUpdateCommandSchemaVersion;
@@ -61,6 +78,7 @@ export interface BaselineProjectSettingsUpdateProjectCommand
export type ProjectSettingsUpdateProjectCommand =
| CurrentProjectSettingsUpdateProjectCommand
+ | WithoutSocketLimitProjectSettingsUpdateProjectCommand
| WithoutCoverProjectSettingsUpdateProjectCommand
| BaselineProjectSettingsUpdateProjectCommand;
@@ -82,6 +100,8 @@ export function assertProjectSettingsUpdateProjectCommand(
if (
command.type !== projectSettingsUpdateCommandType ||
(command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion &&
+ command.schemaVersion !==
+ withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion &&
command.schemaVersion !==
withoutCoverProjectSettingsUpdateCommandSchemaVersion &&
command.schemaVersion !==
@@ -123,16 +143,23 @@ export function assertProjectSettingsUpdateProjectCommand(
: command.schemaVersion ===
withoutCoverProjectSettingsUpdateCommandSchemaVersion
? 9
- : 9 + projectCoverFields.length;
+ : command.schemaVersion ===
+ withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion
+ ? 9 + projectCoverFields.length
+ : 10 + projectCoverFields.length;
if (
Object.keys(payload).length !== expectedKeyCount ||
(command.schemaVersion !==
baselineProjectSettingsUpdateCommandSchemaVersion &&
typeof payload.isPublicBuilding !== "boolean") ||
- (command.schemaVersion === projectSettingsUpdateCommandSchemaVersion &&
+ ((command.schemaVersion === projectSettingsUpdateCommandSchemaVersion ||
+ command.schemaVersion ===
+ withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion) &&
!projectCoverFields.every((field) =>
isNullableTrimmedString(payload[field], projectCoverFieldMaxLengths[field])
- ))
+ )) ||
+ (command.schemaVersion === projectSettingsUpdateCommandSchemaVersion &&
+ !isValidMaxSocketsPerCircuit(payload.maxSocketsPerCircuit))
) {
throw new Error(
"Project settings update command contains invalid values."
diff --git a/src/domain/models/project-state-snapshot.model.ts b/src/domain/models/project-state-snapshot.model.ts
index 745a09e..8d1e10b 100644
--- a/src/domain/models/project-state-snapshot.model.ts
+++ b/src/domain/models/project-state-snapshot.model.ts
@@ -8,6 +8,7 @@ import {
projectCoverFieldMaxLengths,
type ProjectCoverField,
} from "../../shared/constants/project-cover.js";
+import { maxSocketsPerCircuitUpperBound } from "../../shared/constants/socket-limit.js";
import {
circuitGroupCategories,
} from "../../shared/constants/circuit-group.js";
@@ -43,7 +44,8 @@ import {
type ExternalModelStateSnapshot,
} from "../../external-model/domain/external-model-contracts.js";
-export const projectStateSnapshotSchemaVersion = 10 as const;
+export const projectStateSnapshotSchemaVersion = 11 as const;
+const withoutSocketLimitProjectStateSnapshotSchemaVersion = 10 as const;
const legacyCsvProjectStateSnapshotSchemaVersion = 9 as const;
const roomAreaProjectStateSnapshotSchemaVersion = 8 as const;
const buildingProjectStateSnapshotSchemaVersion = 7 as const;
@@ -87,7 +89,7 @@ const projectCoverValueSchema = (field: ProjectCoverField) =>
z.string().trim().min(1).max(projectCoverFieldMaxLengths[field]).nullable();
// Schema version 6 adds the print cover fields.
-const projectSchema = publicBuildingProjectSchema
+const coverProjectSchema = publicBuildingProjectSchema
.extend({
siteAddress: projectCoverValueSchema("siteAddress"),
workStage: projectCoverValueSchema("workStage"),
@@ -96,6 +98,18 @@ const projectSchema = publicBuildingProjectSchema
})
.strict();
+// Schema version 11 adds the per-project socket limit.
+const projectSchema = coverProjectSchema
+ .extend({
+ maxSocketsPerCircuit: z
+ .number()
+ .int()
+ .min(1)
+ .max(maxSocketsPerCircuitUpperBound)
+ .nullable(),
+ })
+ .strict();
+
const emptyProjectCoverValues = {
siteAddress: null,
workStage: null,
@@ -737,10 +751,21 @@ export const projectStateSnapshotSchema = z
})
.strict();
+const withoutSocketLimitProjectStateSnapshotSchema = z
+ .object({
+ schemaVersion: z.literal(withoutSocketLimitProjectStateSnapshotSchemaVersion),
+ project: coverProjectSchema,
+ distributionBoards: z.array(distributionBoardSchema),
+ externalModelConfiguration: externalModelConfigurationSnapshotSchema.nullable(),
+ externalModel: externalModelStateSchema,
+ ...projectStateSnapshotContents,
+ })
+ .strict();
+
const legacyCsvProjectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(legacyCsvProjectStateSnapshotSchemaVersion),
- project: projectSchema,
+ project: coverProjectSchema,
distributionBoards: z.array(distributionBoardSchema),
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
externalModel: legacyExternalModelStateSchema,
@@ -751,7 +776,7 @@ const legacyCsvProjectStateSnapshotSchema = z
const roomAreaProjectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(roomAreaProjectStateSnapshotSchemaVersion),
- project: projectSchema,
+ project: coverProjectSchema,
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
externalModel: legacyExternalModelStateSchema,
@@ -762,7 +787,7 @@ const roomAreaProjectStateSnapshotSchema = z
const buildingProjectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(buildingProjectStateSnapshotSchemaVersion),
- project: projectSchema,
+ project: coverProjectSchema,
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
externalModel: legacyExternalModelStateSchema,
@@ -773,7 +798,7 @@ const buildingProjectStateSnapshotSchema = z
const coverProjectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(coverProjectStateSnapshotSchemaVersion),
- project: projectSchema,
+ project: coverProjectSchema,
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
externalModel: legacyExternalModelStateSchema,
@@ -861,6 +886,28 @@ export function parseProjectStateSnapshot(
}
function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
+ const withoutSocketLimit = upgradeToWithoutSocketLimitProjectStateSnapshot(value);
+ if (
+ withoutSocketLimit !== null &&
+ typeof withoutSocketLimit === "object" &&
+ !Array.isArray(withoutSocketLimit) &&
+ (withoutSocketLimit as { schemaVersion?: unknown }).schemaVersion ===
+ withoutSocketLimitProjectStateSnapshotSchemaVersion
+ ) {
+ const previous =
+ withoutSocketLimitProjectStateSnapshotSchema.parse(withoutSocketLimit);
+ return {
+ ...previous,
+ schemaVersion: projectStateSnapshotSchemaVersion,
+ project: { ...previous.project, maxSocketsPerCircuit: null },
+ };
+ }
+ return withoutSocketLimit;
+}
+
+function upgradeToWithoutSocketLimitProjectStateSnapshot(
+ value: unknown
+): unknown {
const legacyCsv = upgradeToLegacyCsvProjectStateSnapshot(value);
if (
legacyCsv !== null &&
@@ -885,7 +932,7 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
} = previous;
return {
...remaining,
- schemaVersion: projectStateSnapshotSchemaVersion,
+ schemaVersion: withoutSocketLimitProjectStateSnapshotSchemaVersion,
externalModelConfiguration: null,
externalModel: emptyExternalModelState(),
};
diff --git a/src/external-model/configuration/external-model-configuration.ts b/src/external-model/configuration/external-model-configuration.ts
index b3dc20e..0fff421 100644
--- a/src/external-model/configuration/external-model-configuration.ts
+++ b/src/external-model/configuration/external-model-configuration.ts
@@ -5,7 +5,12 @@ import {
type ExternalXlsxTransportConfiguration,
} from "../xlsx/external-xlsx-contracts.js";
-export const externalModelConfigurationSchemaVersion = 1 as const;
+// Version 2 adds `deviceTypes[].isSocket`. Version 1 stays readable wherever
+// it is stored (configuration row, import-batch copies, history commands,
+// snapshots); socket classification then derives from the family rules
+// (`resolveSocketDeviceTypeIds`).
+export const externalModelConfigurationSchemaVersion = 2 as const;
+export const legacyExternalModelConfigurationSchemaVersion = 1 as const;
export const externalQuantityRules = ["one", "socketCount"] as const;
export type ExternalQuantityRule = (typeof externalQuantityRules)[number];
@@ -21,6 +26,8 @@ export interface ExternalDeviceTypeConfiguration {
name: string;
category: CircuitGroupCategory;
defaults: ExternalDeviceTypeDefaults;
+ // Required from schema version 2, absent in version 1.
+ isSocket?: boolean;
}
export interface ExternalFamilyTypeRule {
@@ -32,7 +39,9 @@ export interface ExternalFamilyTypeRule {
export interface ExternalModelConfiguration
extends ExternalXlsxTransportConfiguration {
- schemaVersion: typeof externalModelConfigurationSchemaVersion;
+ schemaVersion:
+ | typeof externalModelConfigurationSchemaVersion
+ | typeof legacyExternalModelConfigurationSchemaVersion;
deviceTypes: ExternalDeviceTypeConfiguration[];
familyTypeRules: ExternalFamilyTypeRule[];
}
@@ -49,7 +58,11 @@ export function createDefaultExternalModelConfiguration(): ExternalModelConfigur
export function assertExternalModelConfiguration(
value: unknown
): asserts value is ExternalModelConfiguration {
- if (!isRecord(value) || value.schemaVersion !== externalModelConfigurationSchemaVersion) {
+ if (
+ !isRecord(value) ||
+ (value.schemaVersion !== externalModelConfigurationSchemaVersion &&
+ value.schemaVersion !== legacyExternalModelConfigurationSchemaVersion)
+ ) {
throw new Error("Nicht unterstützte Revit-XLSX-Konfiguration.");
}
assertExactKeys(value, [
@@ -72,7 +85,11 @@ export function assertExternalModelConfiguration(
const deviceTypeIds = new Set();
const deviceTypeNames = new Set();
for (const [index, deviceType] of value.deviceTypes.entries()) {
- assertDeviceType(deviceType, index);
+ assertDeviceType(
+ deviceType,
+ index,
+ value.schemaVersion === externalModelConfigurationSchemaVersion
+ );
assertUnique(deviceTypeIds, deviceType.id, "Geräteart-ID");
assertUnique(deviceTypeNames, deviceType.name, "Geräteartname");
}
@@ -153,9 +170,22 @@ function assertColumns(
}
}
-function assertDeviceType(value: unknown, index: number): asserts value is ExternalDeviceTypeConfiguration {
+function assertDeviceType(
+ value: unknown,
+ index: number,
+ hasSocketFlag: boolean
+): asserts value is ExternalDeviceTypeConfiguration {
if (!isRecord(value)) throw new Error(`Geräteart ${index + 1} ist ungültig.`);
- assertExactKeys(value, ["id", "name", "category", "defaults"], `Geräteart ${index + 1}`);
+ assertExactKeys(
+ value,
+ hasSocketFlag
+ ? ["id", "name", "category", "defaults", "isSocket"]
+ : ["id", "name", "category", "defaults"],
+ `Geräteart ${index + 1}`
+ );
+ if (hasSocketFlag && typeof value.isSocket !== "boolean") {
+ throw new Error(`Geräteart ${index + 1}.isSocket muss ja oder nein sein.`);
+ }
assertUuid(value.id, `Geräteart ${index + 1}.id`);
assertNonEmptyString(value.name, `Geräteart ${index + 1}.name`);
if (!(["lighting", "single_phase", "three_phase"] as unknown[]).includes(value.category)) {
diff --git a/src/frontend/components/project-settings-modal.tsx b/src/frontend/components/project-settings-modal.tsx
index 21c40ad..0eb8b02 100644
--- a/src/frontend/components/project-settings-modal.tsx
+++ b/src/frontend/components/project-settings-modal.tsx
@@ -13,6 +13,7 @@ import {
projectCoverFields,
type ProjectCoverField,
} from "../../shared/constants/project-cover";
+import { maxSocketsPerCircuitUpperBound } from "../../shared/constants/socket-limit";
import { FormModal } from "./form-modal";
export interface ProjectSettingsInput
@@ -26,6 +27,7 @@ export interface ProjectSettingsInput
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
+ maxSocketsPerCircuit: number | null;
}
const coverFieldPlaceholders: Record = {
@@ -93,6 +95,11 @@ export function ProjectSettingsModal({
projectLead: project.projectLead ?? "",
projectEditor: project.projectEditor ?? "",
});
+ const [maxSocketsPerCircuit, setMaxSocketsPerCircuit] = useState(
+ project.maxSocketsPerCircuit === null
+ ? ""
+ : String(project.maxSocketsPerCircuit)
+ );
const [transfer, setTransfer] = useState(null);
const [transferFilename, setTransferFilename] = useState("");
const [importMode, setImportMode] = useState<"replace" | "duplicate">(
@@ -117,6 +124,7 @@ export function ProjectSettingsModal({
workStage: toNullableString(coverValues.workStage),
projectLead: toNullableString(coverValues.projectLead),
projectEditor: toNullableString(coverValues.projectEditor),
+ maxSocketsPerCircuit: toNullableSocketLimit(maxSocketsPerCircuit),
});
}
@@ -124,7 +132,8 @@ export function ProjectSettingsModal({
name.trim().length > 0 &&
Number(singlePhaseVoltageV) > 0 &&
Number(threePhaseVoltageV) > 0 &&
- enabledDistributionBoardSupplyTypes.length > 0;
+ enabledDistributionBoardSupplyTypes.length > 0 &&
+ isSocketLimitInputValid(maxSocketsPerCircuit);
function toggleSupplyType(supplyType: DistributionBoardSupplyType) {
if (
@@ -335,6 +344,30 @@ export function ProjectSettingsModal({
value={threePhaseVoltageV}
/>
+
+
+
+ setMaxSocketsPerCircuit(event.target.value)
+ }
+ placeholder="keine Grenze"
+ step="1"
+ type="number"
+ value={maxSocketsPerCircuit}
+ />
+
+ Vorgabe des Auftraggebers. Gilt bei der Zuordnung von
+ Revit-Steckdosen; leer bedeutet keine Grenze.
+
+
Deckblatt
@@ -487,3 +520,19 @@ export function ProjectSettingsModal({
function toNullableString(value: string) {
return value.trim() || null;
}
+
+function isSocketLimitInputValid(value: string) {
+ const trimmed = value.trim();
+ if (trimmed === "") return true;
+ const parsed = Number(trimmed);
+ return (
+ Number.isInteger(parsed) &&
+ parsed >= 1 &&
+ parsed <= maxSocketsPerCircuitUpperBound
+ );
+}
+
+function toNullableSocketLimit(value: string) {
+ const trimmed = value.trim();
+ return trimmed === "" ? null : Number(trimmed);
+}
diff --git a/src/frontend/components/revit-device-type-card.tsx b/src/frontend/components/revit-device-type-card.tsx
new file mode 100644
index 0000000..4114c17
--- /dev/null
+++ b/src/frontend/components/revit-device-type-card.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import type { ExternalModelConfiguration } from "../../external-model/configuration/external-model-configuration";
+import { circuitGroupCategoryLabels } from "../../shared/constants/circuit-group";
+
+/**
+ * Device types of the Revit configuration with their socket flag. Only
+ * device types marked as socket count towards the project's socket limit
+ * per circuit.
+ */
+export function RevitDeviceTypeCard({
+ configuration,
+ isSaving,
+ onSave,
+}: {
+ configuration: ExternalModelConfiguration;
+ isSaving: boolean;
+ onSave: (configuration: ExternalModelConfiguration) => Promise
;
+}) {
+ const ruleCounts = new Map();
+ for (const rule of configuration.familyTypeRules) {
+ ruleCounts.set(rule.deviceTypeId, (ruleCounts.get(rule.deviceTypeId) ?? 0) + 1);
+ }
+
+ function toggleSocket(deviceTypeId: string, isSocket: boolean) {
+ void onSave({
+ ...configuration,
+ deviceTypes: configuration.deviceTypes.map((deviceType) =>
+ deviceType.id === deviceTypeId ? { ...deviceType, isSocket } : deviceType
+ ),
+ });
+ }
+
+ return (
+
+
+
+ Gerätearten ({configuration.deviceTypes.length})
+
+
+ {configuration.deviceTypes.length === 0 ? (
+
+ Gerätearten entstehen bei der Familienzuordnung im Import.
+
+ ) : (
+ <>
+
+ Als „Steckdose“ markierte Gerätearten zählen für den
+ Höchstwert je Stromkreis aus den Projekteinstellungen.
+
+
+ >
+ )}
+
+
+
+ );
+}
diff --git a/src/frontend/components/revit-follow-up-import-wizard.tsx b/src/frontend/components/revit-follow-up-import-wizard.tsx
index 8b561c5..b639362 100644
--- a/src/frontend/components/revit-follow-up-import-wizard.tsx
+++ b/src/frontend/components/revit-follow-up-import-wizard.tsx
@@ -40,7 +40,7 @@ export function RevitFollowUpImportWizard({ projectId, expectedRevision, initial
if (!file) return;
const next = structuredClone(configuration);
const targetTypeId = deviceTypeId ?? createClientId();
- if (!deviceTypeId) next.deviceTypes.push({ id: targetTypeId, name: familyAndType, category, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: null, costGroup: null } });
+ if (!deviceTypeId) next.deviceTypes.push({ id: targetTypeId, name: familyAndType, category, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: null, costGroup: null }, ...(next.schemaVersion === 2 ? { isSocket: quantityRule === "socketCount" } : {}) });
next.familyTypeRules.push({ id: createClientId(), exactFamilyAndType: familyAndType, deviceTypeId: targetTypeId, quantityRule });
setConfiguration(next); await loadPlan(file, next);
}
diff --git a/src/frontend/components/revit-initial-import-wizard.tsx b/src/frontend/components/revit-initial-import-wizard.tsx
index 8a4fc4d..b23ca2d 100644
--- a/src/frontend/components/revit-initial-import-wizard.tsx
+++ b/src/frontend/components/revit-initial-import-wizard.tsx
@@ -44,7 +44,7 @@ export function RevitInitialImportWizard({ projectId, expectedRevision, initialC
if (!file) return;
const deviceTypeId = createClientId();
const next = structuredClone(configuration);
- next.deviceTypes.push({ id: deviceTypeId, name: familyAndType, category, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: null, costGroup: null } });
+ next.deviceTypes.push({ id: deviceTypeId, name: familyAndType, category, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: null, costGroup: null }, ...(next.schemaVersion === 2 ? { isSocket: quantityRule === "socketCount" } : {}) });
next.familyTypeRules.push({ id: createClientId(), exactFamilyAndType: familyAndType, deviceTypeId, quantityRule });
setConfiguration(next);
await loadPlan(file, next);
diff --git a/src/frontend/types.ts b/src/frontend/types.ts
index 09459ba..8cd677f 100644
--- a/src/frontend/types.ts
+++ b/src/frontend/types.ts
@@ -194,6 +194,7 @@ export interface ProjectDto {
workStage: string | null;
projectLead: string | null;
projectEditor: string | null;
+ maxSocketsPerCircuit: number | null;
currentRevision: number;
}
diff --git a/src/frontend/utils/api.ts b/src/frontend/utils/api.ts
index e3cfab0..8727ff4 100644
--- a/src/frontend/utils/api.ts
+++ b/src/frontend/utils/api.ts
@@ -446,7 +446,7 @@ export function updateProjectSettings(
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
enabledDistributionBoardSupplyTypes: ProjectDto["enabledDistributionBoardSupplyTypes"];
- } & Partial>
+ } & Partial>
) {
return request(
`/api/projects/${projectId}`,
diff --git a/src/frontend/utils/revit-xlsx-settings.ts b/src/frontend/utils/revit-xlsx-settings.ts
index 6c05c14..ea42704 100644
--- a/src/frontend/utils/revit-xlsx-settings.ts
+++ b/src/frontend/utils/revit-xlsx-settings.ts
@@ -2,7 +2,7 @@ import type { ExternalModelConfiguration } from "../../external-model/configurat
export function createDefaultRevitXlsxConfiguration(): ExternalModelConfiguration {
return {
- schemaVersion: 1,
+ schemaVersion: 2,
layout: { headerRow: 1, firstDataRow: 3, firstDataColumn: 1 },
circuitMode: "combined",
columns: {
diff --git a/src/server/controllers/external-model-configuration.controller.ts b/src/server/controllers/external-model-configuration.controller.ts
index 0a4166a..ecb6bf6 100644
--- a/src/server/controllers/external-model-configuration.controller.ts
+++ b/src/server/controllers/external-model-configuration.controller.ts
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import type { Request, Response } from "express";
import { createExternalModelConfigurationUpdateProjectCommand } from "../../domain/models/external-model-configuration-project-command.model.js";
import { assertExternalModelConfiguration } from "../../external-model/configuration/external-model-configuration.js";
+import { withSocketDeviceTypeFlags } from "../../shared/revit/socket-device-types.js";
import { parseExternalXlsx } from "../../external-model/xlsx/external-xlsx-transport.js";
import { externalModelConfigurationRepository } from "../composition/application-repositories.js";
import { projectCommandService } from "../composition/project-command-stores.js";
@@ -11,7 +12,16 @@ export function getExternalModelConfiguration(req: Request, res: Response) {
const projectId = String(req.params.projectId ?? "");
const result = externalModelConfigurationRepository.getByProject(projectId);
if (!result.projectExists) return res.status(404).json({ error: "Projekt nicht gefunden." });
- return res.json({ configuration: result.configuration });
+ // Clients always edit version 2; a stored version 1 is upgraded here so a
+ // later save writes the explicit socket flags.
+ return res.json({
+ configuration: result.configuration === null
+ ? null
+ : {
+ ...result.configuration,
+ configuration: withSocketDeviceTypeFlags(result.configuration.configuration),
+ },
+ });
}
export function updateExternalModelConfiguration(req: Request, res: Response) {
diff --git a/src/server/controllers/project.controller.ts b/src/server/controllers/project.controller.ts
index e3cd43e..3bcdde8 100644
--- a/src/server/controllers/project.controller.ts
+++ b/src/server/controllers/project.controller.ts
@@ -70,6 +70,10 @@ export async function updateProjectSettings(req: Request, res: Response) {
command: createProjectSettingsUpdateProjectCommand({
...settings,
...coverValues,
+ maxSocketsPerCircuit:
+ settings.maxSocketsPerCircuit === undefined
+ ? current.maxSocketsPerCircuit
+ : settings.maxSocketsPerCircuit,
}),
});
const project = await projectRepository.findById(projectId);
diff --git a/src/shared/constants/socket-limit.ts b/src/shared/constants/socket-limit.ts
new file mode 100644
index 0000000..eaa5faa
--- /dev/null
+++ b/src/shared/constants/socket-limit.ts
@@ -0,0 +1,19 @@
+/**
+ * Client-specific highest number of effective sockets per circuit. It is an
+ * ordinary project setting (`project.update-settings`, logical snapshots and
+ * project transfer); `null` means no limit. Only Revit objects whose device
+ * type is marked as socket count towards it.
+ */
+export const maxSocketsPerCircuitUpperBound = 1000;
+
+export function isValidMaxSocketsPerCircuit(
+ value: unknown
+): value is number | null {
+ return (
+ value === null ||
+ (typeof value === "number" &&
+ Number.isInteger(value) &&
+ value >= 1 &&
+ value <= maxSocketsPerCircuitUpperBound)
+ );
+}
diff --git a/src/shared/revit/socket-device-types.ts b/src/shared/revit/socket-device-types.ts
new file mode 100644
index 0000000..c23f7c4
--- /dev/null
+++ b/src/shared/revit/socket-device-types.ts
@@ -0,0 +1,61 @@
+/**
+ * Socket classification of Revit device types, shared by API and frontend.
+ *
+ * Configuration schema version 2 stores an explicit `isSocket` flag per
+ * device type. Version 1 has no flag; there a device type counts as socket
+ * when one of its family rules takes the quantity from the socket-count
+ * column. This file has no imports because Turbopack resolves frontend
+ * imports without the `.js` suffix the API build needs.
+ */
+
+export const socketFlagConfigurationSchemaVersion = 2 as const;
+
+export interface SocketClassificationConfiguration {
+ schemaVersion: number;
+ deviceTypes: ReadonlyArray<{ id: string; isSocket?: boolean }>;
+ familyTypeRules: ReadonlyArray<{ deviceTypeId: string; quantityRule: string }>;
+}
+
+export function deriveSocketDeviceTypeIds(
+ familyTypeRules: SocketClassificationConfiguration["familyTypeRules"]
+): Set {
+ return new Set(
+ familyTypeRules
+ .filter((rule) => rule.quantityRule === "socketCount")
+ .map((rule) => rule.deviceTypeId)
+ );
+}
+
+export function resolveSocketDeviceTypeIds(
+ configuration: SocketClassificationConfiguration | null
+): Set {
+ if (configuration === null) return new Set();
+ if (configuration.schemaVersion >= socketFlagConfigurationSchemaVersion) {
+ return new Set(
+ configuration.deviceTypes
+ .filter((deviceType) => deviceType.isSocket === true)
+ .map((deviceType) => deviceType.id)
+ );
+ }
+ return deriveSocketDeviceTypeIds(configuration.familyTypeRules);
+}
+
+/** Returns a version-2 configuration; version 1 derives the socket flags. */
+export function withSocketDeviceTypeFlags<
+ T extends SocketClassificationConfiguration & {
+ deviceTypes: Array<{ id: string; isSocket?: boolean }>;
+ },
+>(configuration: T): T {
+ if (configuration.schemaVersion >= socketFlagConfigurationSchemaVersion) {
+ return configuration;
+ }
+ const socketIds = deriveSocketDeviceTypeIds(configuration.familyTypeRules);
+ return {
+ ...configuration,
+ schemaVersion: socketFlagConfigurationSchemaVersion,
+ deviceTypes: configuration.deviceTypes.map((deviceType) => ({
+ ...deviceType,
+ isSocket: socketIds.has(deviceType.id),
+ })),
+ };
+}
diff --git a/src/shared/validation/project-structure.schemas.ts b/src/shared/validation/project-structure.schemas.ts
index 65ee156..6c70b54 100644
--- a/src/shared/validation/project-structure.schemas.ts
+++ b/src/shared/validation/project-structure.schemas.ts
@@ -8,6 +8,7 @@ import {
projectCoverFieldMaxLengths,
type ProjectCoverField,
} from "../constants/project-cover.js";
+import { maxSocketsPerCircuitUpperBound } from "../constants/socket-limit.js";
import { expectedProjectRevisionSchema } from "./project-command.schemas.js";
// Empty text is stored as null so "not entered" has one representation.
@@ -53,6 +54,14 @@ export const updateProjectSettingsSchema = z
workStage: optionalCoverValueSchema("workStage"),
projectLead: optionalCoverValueSchema("projectLead"),
projectEditor: optionalCoverValueSchema("projectEditor"),
+ // Socket limit per circuit; null = no limit, omitted = unchanged.
+ maxSocketsPerCircuit: z
+ .number()
+ .int()
+ .min(1)
+ .max(maxSocketsPerCircuitUpperBound)
+ .nullable()
+ .optional(),
})
.strict();
diff --git a/tests/external-follow-up-import-project-command.repository.test.ts b/tests/external-follow-up-import-project-command.repository.test.ts
index 8b7f6cc..a01deb2 100644
--- a/tests/external-follow-up-import-project-command.repository.test.ts
+++ b/tests/external-follow-up-import-project-command.repository.test.ts
@@ -29,7 +29,7 @@ import { createRevitXlsxFixture, externalXlsxTestConfiguration, loadXlsxWorkbook
const deviceTypeA = "10000000-0000-4000-8000-000000000001";
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
-const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: 0.9, costGroup: null } }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 1, cosPhi: null, costGroup: null } }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
+const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: 0.9, costGroup: null }, isSocket: true }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 1, cosPhi: null, costGroup: null }, isSocket: false }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
const catalogs = { floors: [], rooms: [], distributionBoards: [], projectDevices: [] };
describe("external follow-up import project command", () => {
diff --git a/tests/external-model-configuration.test.ts b/tests/external-model-configuration.test.ts
index 7c1066a..1430691 100644
--- a/tests/external-model-configuration.test.ts
+++ b/tests/external-model-configuration.test.ts
@@ -14,6 +14,10 @@ import {
assertExternalModelConfiguration,
createDefaultExternalModelConfiguration,
} from "../src/external-model/configuration/external-model-configuration.js";
+import {
+ resolveSocketDeviceTypeIds,
+ withSocketDeviceTypeFlags,
+} from "../src/shared/revit/socket-device-types.js";
const deviceTypeId = "10000000-0000-4000-8000-000000000001";
const ruleId = "20000000-0000-4000-8000-000000000001";
@@ -25,6 +29,7 @@ function configuration() {
name: "Steckdose",
category: "single_phase",
defaults: { powerPerUnitW: 200, simultaneityFactor: null, cosPhi: 0.95, costGroup: "440" },
+ isSocket: true,
});
value.familyTypeRules.push({
id: ruleId,
@@ -61,6 +66,27 @@ describe("external model configuration", () => {
assert.throws(() => assertExternalModelConfiguration(unknownType), /unbekannte Ger.*teart/);
});
+ it("requires the socket flag in version 2 and keeps version 1 readable", () => {
+ const missingFlag = configuration();
+ delete missingFlag.deviceTypes[0]!.isSocket;
+ assert.throws(() => assertExternalModelConfiguration(missingFlag), /unbekannte oder fehlende Felder/);
+
+ const legacy = { ...configuration(), schemaVersion: 1 as const };
+ assert.throws(() => assertExternalModelConfiguration(legacy), /unbekannte oder fehlende Felder/);
+ delete legacy.deviceTypes[0]!.isSocket;
+ assert.doesNotThrow(() => assertExternalModelConfiguration(legacy));
+
+ assert.deepEqual([...resolveSocketDeviceTypeIds(legacy)], [deviceTypeId]);
+ const upgraded = withSocketDeviceTypeFlags(legacy);
+ assert.equal(upgraded.schemaVersion, 2);
+ assert.equal(upgraded.deviceTypes[0]!.isSocket, true);
+ assert.doesNotThrow(() => assertExternalModelConfiguration(upgraded));
+
+ const notSocket = configuration();
+ notSocket.deviceTypes[0]!.isSocket = false;
+ assert.equal(resolveSocketDeviceTypeIds(notSocket).size, 0);
+ });
+
it("persists changes without an independent configuration revision", () => {
const context = createTestDatabase();
try {
diff --git a/tests/external-xlsx-follow-up-import.test.ts b/tests/external-xlsx-follow-up-import.test.ts
index eb88fa0..d23bc3d 100644
--- a/tests/external-xlsx-follow-up-import.test.ts
+++ b/tests/external-xlsx-follow-up-import.test.ts
@@ -12,7 +12,7 @@ const deviceTypeA = "10000000-0000-4000-8000-000000000001";
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
const newWorksharingId = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE-00000003";
const catalogs = { floors: [], rooms: [], distributionBoards: [], projectDevices: [] };
-const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: 0.9, costGroup: null } }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 1, cosPhi: null, costGroup: null } }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
+const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: 0.9, costGroup: null }, isSocket: true }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 1, cosPhi: null, costGroup: null }, isSocket: false }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
async function initialState() {
const bytes = await createRevitXlsxFixture();
diff --git a/tests/external-xlsx-initial-import.test.ts b/tests/external-xlsx-initial-import.test.ts
index df17e23..450a7f2 100644
--- a/tests/external-xlsx-initial-import.test.ts
+++ b/tests/external-xlsx-initial-import.test.ts
@@ -23,8 +23,8 @@ function configured() {
...createDefaultExternalModelConfiguration(),
...externalXlsxTestConfiguration,
deviceTypes: [
- { id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: null, cosPhi: 0.9, costGroup: "440" } },
- { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 0.8, cosPhi: null, costGroup: null } },
+ { id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: null, cosPhi: 0.9, costGroup: "440" }, isSocket: true },
+ { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 0.8, cosPhi: null, costGroup: null }, isSocket: false },
],
familyTypeRules: [
{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const },
diff --git a/tests/project-command.service.test.ts b/tests/project-command.service.test.ts
index 67106e7..bdc5119 100644
--- a/tests/project-command.service.test.ts
+++ b/tests/project-command.service.test.ts
@@ -1081,6 +1081,7 @@ describe("project command service", () => {
workStage: null,
projectLead: null,
projectEditor: null,
+ maxSocketsPerCircuit: null,
}),
});
assert.equal(updated.history.currentRevision, 1);
diff --git a/tests/project-settings-project-command.repository.test.ts b/tests/project-settings-project-command.repository.test.ts
index 52dd16d..fbf3815 100644
--- a/tests/project-settings-project-command.repository.test.ts
+++ b/tests/project-settings-project-command.repository.test.ts
@@ -56,6 +56,7 @@ function getSettings(context: DatabaseContext) {
workStage: projects.workStage,
projectLead: projects.projectLead,
projectEditor: projects.projectEditor,
+ maxSocketsPerCircuit: projects.maxSocketsPerCircuit,
currentRevision: projects.currentRevision,
})
.from(projects)
@@ -91,6 +92,7 @@ function settings(
workStage: null,
projectLead: null,
projectEditor: null,
+ maxSocketsPerCircuit: null,
...overrides,
};
}
@@ -231,6 +233,7 @@ describe("project settings project-command repository", () => {
workStage: null,
projectLead: null,
projectEditor: null,
+ maxSocketsPerCircuit: null,
},
},
]);
@@ -307,6 +310,7 @@ describe("project settings project-command repository", () => {
workStage: null,
projectLead: null,
projectEditor: null,
+ maxSocketsPerCircuit: null,
});
const undoStep = history.getNextCommand("project-1", "undo");
@@ -339,6 +343,7 @@ describe("project settings project-command repository", () => {
workStage: null,
projectLead: null,
projectEditor: null,
+ maxSocketsPerCircuit: null,
currentRevision: 2,
});
assert.deepEqual(getDerivedVoltages(context), {
@@ -382,6 +387,7 @@ describe("project settings project-command repository", () => {
workStage: null,
projectLead: null,
projectEditor: null,
+ maxSocketsPerCircuit: null,
currentRevision: 3,
});
assert.deepEqual(getDerivedVoltages(context), {
@@ -495,7 +501,7 @@ describe("project settings project-command repository", () => {
projectLead: "P. Leitung",
projectEditor: "B. Arbeiter",
});
- assert.equal(forward.inverse.schemaVersion, 3);
+ assert.equal(forward.inverse.schemaVersion, 4);
const undoStep = new ProjectHistoryRepository(
context.db
@@ -543,6 +549,7 @@ describe("project settings project-command repository", () => {
workStage: _workStage,
projectLead: _projectLead,
projectEditor: _projectEditor,
+ maxSocketsPerCircuit: _maxSocketsPerCircuit,
...versionTwoPayload
} = settings({ name: "Stand aus Version 2" });
const forward = repository.executeUpdate({
@@ -570,6 +577,83 @@ describe("project settings project-command repository", () => {
}
});
+ it("persists the socket limit with undo and keeps it for version 3 commands", () => {
+ const context = createTestDatabase();
+ try {
+ const repository = new ProjectSettingsProjectCommandRepository(
+ context.db
+ );
+ const readLimit = () =>
+ context.db
+ .select({ maxSocketsPerCircuit: projects.maxSocketsPerCircuit })
+ .from(projects)
+ .where(eq(projects.id, "project-1"))
+ .get()?.maxSocketsPerCircuit;
+ const forward = repository.executeUpdate({
+ projectId: "project-1",
+ expectedRevision: 0,
+ source: "user",
+ command: createProjectSettingsUpdateProjectCommand(
+ settings({ maxSocketsPerCircuit: 8 })
+ ),
+ });
+ assert.equal(readLimit(), 8);
+ assert.equal(forward.inverse.schemaVersion, 4);
+ assert.equal(
+ (forward.inverse.payload as { maxSocketsPerCircuit: unknown })
+ .maxSocketsPerCircuit,
+ null
+ );
+
+ const {
+ maxSocketsPerCircuit: _maxSocketsPerCircuit,
+ ...versionThreePayload
+ } = settings({ name: "Stand aus Version 3" });
+ const versionThree = repository.executeUpdate({
+ projectId: "project-1",
+ expectedRevision: 1,
+ source: "user",
+ command: {
+ schemaVersion: 3,
+ type: "project.update-settings",
+ payload: versionThreePayload,
+ },
+ });
+ assert.equal(readLimit(), 8);
+ assert.equal(versionThree.inverse.schemaVersion, 3);
+ assert.equal(
+ "maxSocketsPerCircuit" in versionThree.inverse.payload,
+ false
+ );
+
+ const history = new ProjectHistoryRepository(context.db);
+ for (const expectedRevision of [2, 3]) {
+ const undoStep = history.getNextCommand("project-1", "undo");
+ assert.ok(undoStep);
+ repository.executeUpdate({
+ projectId: "project-1",
+ expectedRevision,
+ source: "undo",
+ historyTargetChangeSetId: undoStep.changeSetId,
+ command: undoStep.command as never,
+ });
+ }
+ assert.equal(readLimit(), null);
+
+ for (const invalid of [0, 2.5, 1001]) {
+ assert.throws(
+ () =>
+ createProjectSettingsUpdateProjectCommand(
+ settings({ maxSocketsPerCircuit: invalid })
+ ),
+ /invalid values/
+ );
+ }
+ } finally {
+ context.close();
+ }
+ });
+
it("persists the project supply-type selection and rejects disabling an in-use type", () => {
const context = createTestDatabase();
try {
diff --git a/tests/project-snapshot.repository.test.ts b/tests/project-snapshot.repository.test.ts
index b85485c..6336894 100644
--- a/tests/project-snapshot.repository.test.ts
+++ b/tests/project-snapshot.repository.test.ts
@@ -474,7 +474,7 @@ describe("project snapshot repository", () => {
for (const room of payload.rooms) {
delete room.areaM2;
}
- for (const field of ["siteAddress", "workStage", "projectLead", "projectEditor"]) {
+ for (const field of ["siteAddress", "workStage", "projectLead", "projectEditor", "maxSocketsPerCircuit"]) {
delete payload.project[field];
}
for (const board of payload.distributionBoards) {
diff --git a/tests/project-state-snapshot.test.ts b/tests/project-state-snapshot.test.ts
index e7ea64a..1123cfc 100644
--- a/tests/project-state-snapshot.test.ts
+++ b/tests/project-state-snapshot.test.ts
@@ -13,10 +13,19 @@ import { createDefaultExternalCsvConfiguration } from "../src/external-model/csv
import { externalCsvTestColumns } from "./fixtures/revit-csv-fixtures.js";
import { zodIssues } from "./support/zod-assertions.js";
-/** Removes the fields introduced by snapshot schema version 9. */
+/** Removes the field introduced by snapshot schema version 11. */
+function withoutVersionElevenFields(
+ snapshot: T
+): T {
+ delete (snapshot.project as Record).maxSocketsPerCircuit;
+ return snapshot;
+}
+
+/** Removes the fields introduced by snapshot schema versions 9 to 11. */
function withoutVersionNineFields<
- T extends { distributionBoards: object[] },
+ T extends { project: object; distributionBoards: object[] },
>(snapshot: T): T {
+ withoutVersionElevenFields(snapshot);
const record = snapshot as Record;
record.externalCsvConfiguration = null;
delete record.externalModelConfiguration;
@@ -29,7 +38,7 @@ function withoutVersionNineFields<
/** Removes the fields introduced by snapshot schema version 7. */
function withoutVersionSevenFields<
- T extends { floors: object[]; distributionBoards: object[] },
+ T extends { project: object; floors: object[]; distributionBoards: object[] },
>(snapshot: T): T {
withoutVersionNineFields(snapshot);
delete (snapshot as Record).buildings;
@@ -82,6 +91,7 @@ function minimalSnapshot() {
workStage: "LPH 5" as string | null,
projectLead: "P. Leitung" as string | null,
projectEditor: null as string | null,
+ maxSocketsPerCircuit: null as number | null,
},
distributionBoards: [],
externalModelConfiguration: null,
@@ -351,6 +361,24 @@ describe("project state snapshot model", () => {
});
});
+ it("keeps the socket limit in version 11 and upgrades version 10 without a limit", () => {
+ const current = structuredClone(minimalSnapshot());
+ current.project.maxSocketsPerCircuit = 8;
+ assert.equal(parseProjectStateSnapshot(current).project.maxSocketsPerCircuit, 8);
+
+ const previous = withoutVersionElevenFields(structuredClone(minimalSnapshot()));
+ previous.schemaVersion = 10 as typeof previous.schemaVersion;
+ const upgraded = parseProjectStateSnapshot(previous);
+ assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion);
+ assert.equal(upgraded.project.maxSocketsPerCircuit, null);
+
+ const invalid = structuredClone(minimalSnapshot());
+ invalid.project.maxSocketsPerCircuit = 0;
+ assert.throws(() => parseProjectStateSnapshot(invalid));
+ const missing = withoutVersionElevenFields(structuredClone(minimalSnapshot()));
+ assert.throws(() => parseProjectStateSnapshot(missing));
+ });
+
it("upgrades snapshot version 3 with an empty external model", () => {
const previous = withoutVersionSixFields(structuredClone(minimalSnapshot()));
previous.schemaVersion = 3 as typeof previous.schemaVersion;
diff --git a/tests/revit-xlsx-end-to-end.test.ts b/tests/revit-xlsx-end-to-end.test.ts
index ec6f5f5..c15475f 100644
--- a/tests/revit-xlsx-end-to-end.test.ts
+++ b/tests/revit-xlsx-end-to-end.test.ts
@@ -48,7 +48,7 @@ const circuitId = "circuit-e2e";
const rowId = "row-e2e";
const deviceTypeA = "10000000-0000-4000-8000-000000000001";
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
-const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" } }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 0.8, cosPhi: null, costGroup: null } }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
+const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" }, isSocket: true }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 0.8, cosPhi: null, costGroup: null }, isSocket: false }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
describe("Linear XLSX end-to-end", () => {
it("survives return, follow-up, snapshot restore, duplicate and restart undo/redo", async () => {
diff --git a/tests/revit-xlsx-scale.test.ts b/tests/revit-xlsx-scale.test.ts
index 386916f..8eb3d35 100644
--- a/tests/revit-xlsx-scale.test.ts
+++ b/tests/revit-xlsx-scale.test.ts
@@ -29,7 +29,7 @@ const deviceTypeId = "10000000-0000-4000-8000-000000000001";
const configuration = {
...createDefaultExternalModelConfiguration(),
...externalXlsxTestConfiguration,
- deviceTypes: [{ id: deviceTypeId, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" } }],
+ deviceTypes: [{ id: deviceTypeId, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" }, isSocket: true }],
familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId, quantityRule: "socketCount" as const }],
};