From e48042bde3257c4b22babed9b243c09c344c3ed1 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 30 Oct 2026 21:18:08 +0100 Subject: [PATCH] Add per-project socket limit and Revit socket device-type flag - Project setting maxSocketsPerCircuit (migration 0018, project.update-settings schema 4, snapshot schema 11); older command and snapshot versions keep or upgrade the value to null. - Revit configuration schema 2 marks device types as socket; version 1 stays readable and derives the flag from socketCount family rules. - Settings dialog field and device-type card on the Revit page. Co-Authored-By: Claude Opus 5.5 --- scripts/db-verify-circuit-schema.js | 1 + src/app/projects/[projectId]/revit/page.tsx | 2 + .../migrations/0018_project_socket_limit.sql | 1 + src/db/migrations/meta/0018_snapshot.json | 3217 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + ...ect-settings-project-command.repository.ts | 30 +- ...roject-state-restore-command.repository.ts | 1 + .../project-state-snapshot.persistence.ts | 2 + src/db/repositories/project.repository.ts | 1 + src/db/schema/projects.ts | 2 + .../project-settings-project-command.model.ts | 43 +- .../models/project-state-snapshot.model.ts | 61 +- .../external-model-configuration.ts | 42 +- .../components/project-settings-modal.tsx | 51 +- .../components/revit-device-type-card.tsx | 86 + .../revit-follow-up-import-wizard.tsx | 2 +- .../revit-initial-import-wizard.tsx | 2 +- src/frontend/types.ts | 1 + src/frontend/utils/api.ts | 2 +- src/frontend/utils/revit-xlsx-settings.ts | 2 +- ...external-model-configuration.controller.ts | 12 +- src/server/controllers/project.controller.ts | 4 + src/shared/constants/socket-limit.ts | 19 + src/shared/revit/socket-device-types.ts | 61 + .../validation/project-structure.schemas.ts | 9 + ...-import-project-command.repository.test.ts | 2 +- tests/external-model-configuration.test.ts | 26 + tests/external-xlsx-follow-up-import.test.ts | 2 +- tests/external-xlsx-initial-import.test.ts | 4 +- tests/project-command.service.test.ts | 1 + ...ettings-project-command.repository.test.ts | 86 +- tests/project-snapshot.repository.test.ts | 2 +- tests/project-state-snapshot.test.ts | 34 +- tests/revit-xlsx-end-to-end.test.ts | 2 +- tests/revit-xlsx-scale.test.ts | 2 +- 35 files changed, 3781 insertions(+), 41 deletions(-) create mode 100644 src/db/migrations/0018_project_socket_limit.sql create mode 100644 src/db/migrations/meta/0018_snapshot.json create mode 100644 src/frontend/components/revit-device-type-card.tsx create mode 100644 src/shared/constants/socket-limit.ts create mode 100644 src/shared/revit/socket-device-types.ts 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. +

+ + + + + + + + + + + {configuration.deviceTypes.map((deviceType) => ( + + + + + + + ))} + +
GeräteartKategorieFamilienSteckdose
{deviceType.name}{circuitGroupCategoryLabels[deviceType.category]}{ruleCounts.get(deviceType.id) ?? 0} + toggleSocket(deviceType.id, event.target.checked)} + type="checkbox" + /> +
+ + )} +
+
+
+ ); +} 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 }], };