diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index ac87c28..29e44c4 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -150,7 +150,11 @@ synchronizes linked circuit rows implicitly. - response: `{ "project": { ... }, "revision": { ... }, "history": { ... } }` - persistent Undo/Redo restores metadata, voltage values and the enabled - distribution-board supply types together + distribution-board supply types together; project-device and circuit + voltages are recalculated from the restored project settings in the same + transaction + - project-device and circuit voltage are derived values and are not accepted + as editable frontend fields - at least one of `AV`, `SV`, `EV`, `USV`, `MSR`, `SiBe` must be enabled; a supply type currently used by a distribution board cannot be disabled - unchanged values are rejected without creating a revision; a stale diff --git a/docs/project-history-and-external-model-architecture.md b/docs/project-history-and-external-model-architecture.md index 8cfc985..dffecfc 100644 --- a/docs/project-history-and-external-model-architecture.md +++ b/docs/project-history-and-external-model-architecture.md @@ -265,7 +265,9 @@ Completed foundation: snapshots in the same transaction - `project.update-settings` persists both project voltage defaults, its exact inverse, revision and history transition atomically; the project page and - existing PUT route require the current expected revision + existing PUT route require the current expected revision; the same + transaction derives every project-device and circuit voltage again, so + Undo/Redo cannot leave mixed voltage states - `distribution-board.insert` persists the complete generated board, circuit list and default-section structure with stable ids; its delete inverse refuses changed or populated structures and the project-page POST tracks the diff --git a/docs/spec/08-current-product-backlog.md b/docs/spec/08-current-product-backlog.md index 9c54f76..e9a3e07 100644 --- a/docs/spec/08-current-product-backlog.md +++ b/docs/spec/08-current-product-backlog.md @@ -35,21 +35,27 @@ requirements and intended sequencing, not proof of implementation. - Persist changes through project commands so they remain undoable after a restart. -## Device Voltage Defaults +## Device Voltage Derivation - Derive voltage from the selected phase type and the project settings: - single-phase uses the project's single-phase voltage; - three-phase uses the project's three-phase voltage. -- Apply this default consistently when creating global devices, project - devices and manual circuit-list entries. -- Define explicit override behavior before implementation so a user-entered - exceptional voltage is not silently overwritten when phase type or project - settings change. +- Voltage is not editable on global devices, project devices or circuits. + Global devices store only their phase; copying one into a project derives + the target project's voltage. +- Project-device voltage follows its phase. Circuit voltage follows the + section phase; an unassigned circuit is three-phase only when all assigned + device rows with a valid phase are three-phase. +- Changing project voltage settings updates all project devices and circuits + atomically in the same persistent Undo/Redo step. +- Database migration `0019` and snapshot schema version `5` normalize older + stored values. Version-four and older imports remain supported and are + normalized while being upgraded. ## Recommended Sequence 1. Distribution-board floor and supply fields. -2. Device voltage default and override semantics. +2. Device voltage derivation without overrides. 3. Snapshot-to-revision descriptions and history presentation. The Revit/CSV/IFCGUID round-trip remains a separate jointly planned phase and diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx index befe3b9..39f7aa9 100644 --- a/src/app/projects/page.tsx +++ b/src/app/projects/page.tsx @@ -24,7 +24,6 @@ const emptyGlobalDevice: CreateGlobalDeviceInput = { quantity: 1, installedPowerPerUnitKw: 0.1, demandFactor: 1, - voltageV: 230, phaseCount: 1, powerFactor: 1, note: "", @@ -49,7 +48,6 @@ export default function ProjectsPage() { quantity: "1", installedPowerPerUnitKw: "0.1", demandFactor: "1", - voltageV: "230", phaseCount: "1", powerFactor: "1", note: "", @@ -153,7 +151,6 @@ export default function ProjectsPage() { quantity: Number(globalDeviceForm.quantity), installedPowerPerUnitKw: Number(globalDeviceForm.installedPowerPerUnitKw), demandFactor: Number(globalDeviceForm.demandFactor), - voltageV: toOptionalNumber(globalDeviceForm.voltageV), phaseCount: globalDeviceForm.phaseCount === "3" ? 3 : 1, powerFactor: toOptionalNumber(globalDeviceForm.powerFactor), note: globalDeviceForm.note.trim() || undefined, @@ -170,7 +167,6 @@ export default function ProjectsPage() { quantity: String(emptyGlobalDevice.quantity), installedPowerPerUnitKw: String(emptyGlobalDevice.installedPowerPerUnitKw), demandFactor: String(emptyGlobalDevice.demandFactor), - voltageV: String(emptyGlobalDevice.voltageV ?? ""), phaseCount: String(emptyGlobalDevice.phaseCount ?? 1), powerFactor: String(emptyGlobalDevice.powerFactor ?? ""), note: emptyGlobalDevice.note ?? "", @@ -207,7 +203,6 @@ export default function ProjectsPage() { quantity: device.quantity, installedPowerPerUnitKw: device.installedPowerPerUnitKw, demandFactor: device.demandFactor, - voltageV: device.voltageV ?? undefined, phaseCount: device.phaseCount ?? undefined, powerFactor: device.powerFactor ?? undefined, note: device.note ?? undefined, diff --git a/src/db/migrations/0019_normalize_project_voltages.sql b/src/db/migrations/0019_normalize_project_voltages.sql new file mode 100644 index 0000000..6d74e31 --- /dev/null +++ b/src/db/migrations/0019_normalize_project_voltages.sql @@ -0,0 +1,48 @@ +UPDATE `global_devices` +SET `voltage_v` = NULL; +--> statement-breakpoint +UPDATE `project_devices` AS `pd` +SET `voltage_v` = CASE + WHEN `pd`.`phase_type` = 'three_phase' THEN `p`.`three_phase_voltage_v` + ELSE `p`.`single_phase_voltage_v` +END +FROM `projects` AS `p` +WHERE `p`.`id` = `pd`.`project_id`; +--> statement-breakpoint +WITH `derived_circuit_voltages` AS ( + SELECT + `c`.`id` AS `circuit_id`, + CASE + WHEN `cs`.`key` = 'three_phase' THEN `p`.`three_phase_voltage_v` + WHEN `cs`.`key` IN ('lighting', 'single_phase') THEN `p`.`single_phase_voltage_v` + WHEN EXISTS ( + SELECT 1 + FROM `circuit_device_rows` AS `three_phase_row` + WHERE `three_phase_row`.`circuit_id` = `c`.`id` + AND `three_phase_row`.`phase_type` = 'three_phase' + ) AND NOT EXISTS ( + SELECT 1 + FROM `circuit_device_rows` AS `other_row` + WHERE `other_row`.`circuit_id` = `c`.`id` + AND `other_row`.`phase_type` = 'single_phase' + ) THEN `p`.`three_phase_voltage_v` + ELSE `p`.`single_phase_voltage_v` + END AS `derived_voltage` + FROM `circuits` AS `c` + INNER JOIN `circuit_lists` AS `cl` + ON `cl`.`id` = `c`.`circuit_list_id` + INNER JOIN `projects` AS `p` + ON `p`.`id` = `cl`.`project_id` + INNER JOIN `circuit_sections` AS `cs` + ON `cs`.`id` = `c`.`section_id` +) +UPDATE `circuits` +SET `voltage` = ( + SELECT `derived_voltage` + FROM `derived_circuit_voltages` + WHERE `circuit_id` = `circuits`.`id` +) +WHERE `id` IN ( + SELECT `circuit_id` + FROM `derived_circuit_voltages` +); diff --git a/src/db/migrations/meta/0019_snapshot.json b/src/db/migrations/meta/0019_snapshot.json new file mode 100644 index 0000000..74be78e --- /dev/null +++ b/src/db/migrations/meta/0019_snapshot.json @@ -0,0 +1,1885 @@ +{ + "id": "38fade39-c2c3-494e-ae8d-ee4677b2cbc7", + "prevId": "6297060c-1a3f-4f7c-867f-7c3e96c34645", + "version": "6", + "dialect": "sqlite", + "tables": { + "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 + }, + "legacy_consumer_id": { + "name": "legacy_consumer_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 + }, + "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": {}, + "foreignKeys": { + "circuit_device_rows_circuit_id_circuits_id_fk": { + "name": "circuit_device_rows_circuit_id_circuits_id_fk", + "tableFrom": "circuit_device_rows", + "columnsFrom": [ + "circuit_id" + ], + "tableTo": "circuits", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "linked_project_device_id" + ], + "tableTo": "project_devices", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "circuit_device_rows_room_id_rooms_id_fk": { + "name": "circuit_device_rows_room_id_rooms_id_fk", + "tableFrom": "circuit_device_rows", + "columnsFrom": [ + "room_id" + ], + "tableTo": "rooms", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "circuit_lists_distribution_board_id_distribution_boards_id_fk": { + "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "circuit_lists", + "columnsFrom": [ + "distribution_board_id" + ], + "tableTo": "distribution_boards", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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 + } + }, + "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 + } + }, + "foreignKeys": { + "circuit_sections_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_sections", + "columnsFrom": [ + "circuit_list_id" + ], + "tableTo": "circuit_lists", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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 + }, + "protection_type": { + "name": "protection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protection_rated_current": { + "name": "protection_rated_current", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protection_characteristic": { + "name": "protection_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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_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", + "columnsFrom": [ + "circuit_list_id" + ], + "tableTo": "circuit_lists", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "circuits_section_id_circuit_sections_id_fk": { + "name": "circuits_section_id_circuit_sections_id_fk", + "tableFrom": "circuits", + "columnsFrom": [ + "section_id" + ], + "tableTo": "circuit_sections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "consumers": { + "name": "consumers", + "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": false, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_device_id": { + "name": "project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_linked_to_device": { + "name": "is_linked_to_device", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "circuit_number": { + "name": "circuit_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trade_or_cost_group": { + "name": "trade_or_cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protection_type": { + "name": "protection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protection_rated_current": { + "name": "protection_rated_current", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protection_characteristic": { + "name": "protection_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "comment": { + "name": "comment", + "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": { + "consumers_project_id_projects_id_fk": { + "name": "consumers_project_id_projects_id_fk", + "tableFrom": "consumers", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "consumers_distribution_board_id_distribution_boards_id_fk": { + "name": "consumers_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "consumers", + "columnsFrom": [ + "distribution_board_id" + ], + "tableTo": "distribution_boards", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "consumers_circuit_list_id_circuit_lists_id_fk": { + "name": "consumers_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "consumers", + "columnsFrom": [ + "circuit_list_id" + ], + "tableTo": "circuit_lists", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "consumers_project_device_id_project_devices_id_fk": { + "name": "consumers_project_device_id_project_devices_id_fk", + "tableFrom": "consumers", + "columnsFrom": [ + "project_device_id" + ], + "tableTo": "project_devices", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "consumers_room_id_rooms_id_fk": { + "name": "consumers_room_id_rooms_id_fk", + "tableFrom": "consumers", + "columnsFrom": [ + "room_id" + ], + "tableTo": "rooms", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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 + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_boards_project_id_projects_id_fk": { + "name": "distribution_boards_project_id_projects_id_fk", + "tableFrom": "distribution_boards", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "distribution_boards_floor_id_floors_id_fk": { + "name": "distribution_boards_floor_id_floors_id_fk", + "tableFrom": "distribution_boards", + "columnsFrom": [ + "floor_id" + ], + "tableTo": "floors", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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 + } + }, + "indexes": {}, + "foreignKeys": { + "floors_project_id_projects_id_fk": { + "name": "floors_project_id_projects_id_fk", + "tableFrom": "floors", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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": {} + }, + "legacy_consumer_circuit_migrations": { + "name": "legacy_consumer_circuit_migrations", + "columns": { + "consumer_id": { + "name": "consumer_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "circuit_device_row_id": { + "name": "circuit_device_row_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { + "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", + "tableFrom": "legacy_consumer_circuit_migrations", + "columnsFrom": [ + "circuit_id" + ], + "tableTo": "circuits", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { + "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", + "tableFrom": "legacy_consumer_circuit_migrations", + "columnsFrom": [ + "circuit_device_row_id" + ], + "tableTo": "circuit_device_rows", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { + "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "legacy_consumer_circuit_migrations", + "columnsFrom": [ + "circuit_list_id" + ], + "tableTo": "circuit_lists", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "legacy_consumer_migration_reports": { + "name": "legacy_consumer_migration_reports", + "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 + }, + "legacy_consumer_count": { + "name": "legacy_consumer_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_circuit_count": { + "name": "created_circuit_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_device_row_count": { + "name": "created_device_row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duplicate_grouped_count": { + "name": "duplicate_grouped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generated_identifier_count": { + "name": "generated_identifier_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unassigned_row_count": { + "name": "unassigned_row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "warnings_json": { + "name": "warnings_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generated_identifiers_json": { + "name": "generated_identifiers_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duplicate_groups_json": { + "name": "duplicate_groups_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "legacy_consumer_migration_reports_list_unique": { + "name": "legacy_consumer_migration_reports_list_unique", + "columns": [ + "circuit_list_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { + "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "legacy_consumer_migration_reports", + "columnsFrom": [ + "circuit_list_id" + ], + "tableTo": "circuit_lists", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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", + "columnsFrom": [ + "project_revision_id" + ], + "tableTo": "project_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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", + "columnsFrom": [ + "change_set_id" + ], + "tableTo": "project_change_sets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "project_history_stack_entries_project_id_projects_id_fk": { + "name": "project_history_stack_entries_project_id_projects_id_fk", + "tableFrom": "project_history_stack_entries", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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 + }, + "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\"]'" + }, + "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 + } + }, + "indexes": {}, + "foreignKeys": { + "rooms_project_id_projects_id_fk": { + "name": "rooms_project_id_projects_id_fk", + "tableFrom": "rooms", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "rooms_floor_id_floors_id_fk": { + "name": "rooms_floor_id_floors_id_fk", + "tableFrom": "rooms", + "columnsFrom": [ + "floor_id" + ], + "tableTo": "floors", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "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 a3e4e22..72ab885 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -134,6 +134,13 @@ "when": 1785308458789, "tag": "0018_fancy_argent", "breakpoints": true + }, + { + "idx": 19, + "version": "6", + "when": 1785310907349, + "tag": "0019_normalize_project_voltages", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/repositories/circuit-device-row-move-project-command.repository.ts b/src/db/repositories/circuit-device-row-move-project-command.repository.ts index ae470fc..66b8273 100644 --- a/src/db/repositories/circuit-device-row-move-project-command.repository.ts +++ b/src/db/repositories/circuit-device-row-move-project-command.repository.ts @@ -20,11 +20,14 @@ import { circuitLists } from "../schema/circuit-lists.js"; import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; +import { resolveCircuitVoltage } from "./project-voltage.persistence.js"; interface PersistedMoveRow { id: string; circuitId: string; sortOrder: number; + phaseType: string | null; } export class CircuitDeviceRowMoveProjectCommandRepository @@ -85,6 +88,7 @@ export class CircuitDeviceRowMoveProjectCommandRepository ); this.applyMoves(database, command.payload.moves); this.updateReserveStates(database, circuitIds); + this.updateVoltages(database, projectId, circuitIds); return inverse; } @@ -94,7 +98,16 @@ export class CircuitDeviceRowMoveProjectCommandRepository const { targetCircuit, targetCircuitAction, moves } = command.payload; const rowsById = this.loadExpectedRows(database, moves); - this.assertCircuitLocation(database, projectId, targetCircuit); + const appliedTargetCircuit = { + ...targetCircuit, + voltage: resolveCircuitVoltage( + database, + projectId, + targetCircuit.sectionId, + moves.map((move) => rowsById.get(move.rowId)?.phaseType) + ), + }; + this.assertCircuitLocation(database, projectId, appliedTargetCircuit); if (targetCircuitAction === "create") { const sourceCircuitIds = [ @@ -106,13 +119,13 @@ export class CircuitDeviceRowMoveProjectCommandRepository sourceCircuitIds, targetCircuit.circuitListId ); - this.assertTargetCircuitAvailable(database, targetCircuit); - this.insertTargetCircuit(database, targetCircuit); + this.assertTargetCircuitAvailable(database, appliedTargetCircuit); + this.insertTargetCircuit(database, appliedTargetCircuit); const inverse = createCircuitDeviceRowMoveWithNewCircuitProjectCommand( "delete", - targetCircuit, + appliedTargetCircuit, this.reverseMoves(moves, rowsById) ); this.applyMoves(database, moves); @@ -120,12 +133,16 @@ export class CircuitDeviceRowMoveProjectCommandRepository ...sourceCircuitIds, targetCircuit.id, ]); + this.updateVoltages(database, projectId, [ + ...sourceCircuitIds, + targetCircuit.id, + ]); return inverse; } this.assertTargetCircuitUnchanged( database, - targetCircuit, + appliedTargetCircuit, moves.map((move) => move.rowId) ); const destinationCircuitIds = [ @@ -140,7 +157,7 @@ export class CircuitDeviceRowMoveProjectCommandRepository const inverse = createCircuitDeviceRowMoveWithNewCircuitProjectCommand( "create", - targetCircuit, + appliedTargetCircuit, this.reverseMoves(moves, rowsById) ); this.applyMoves(database, moves); @@ -148,6 +165,11 @@ export class CircuitDeviceRowMoveProjectCommandRepository targetCircuit.id, ...destinationCircuitIds, ]); + this.updateVoltages( + database, + projectId, + destinationCircuitIds + ); const deleted = database .delete(circuits) .where( @@ -169,6 +191,16 @@ export class CircuitDeviceRowMoveProjectCommandRepository return inverse; } + private updateVoltages( + database: AppDatabase, + projectId: string, + circuitIds: string[] + ) { + for (const circuitId of new Set(circuitIds)) { + updateDerivedCircuitVoltage(database, projectId, circuitId); + } + } + private loadExpectedRows( database: AppDatabase, moves: CircuitDeviceRowMoveAssignment[] @@ -179,6 +211,7 @@ export class CircuitDeviceRowMoveProjectCommandRepository id: circuitDeviceRows.id, circuitId: circuitDeviceRows.circuitId, sortOrder: circuitDeviceRows.sortOrder, + phaseType: circuitDeviceRows.phaseType, }) .from(circuitDeviceRows) .where(inArray(circuitDeviceRows.id, rowIds)) diff --git a/src/db/repositories/circuit-device-row-project-command.repository.ts b/src/db/repositories/circuit-device-row-project-command.repository.ts index dd79c86..b9a5c43 100644 --- a/src/db/repositories/circuit-device-row-project-command.repository.ts +++ b/src/db/repositories/circuit-device-row-project-command.repository.ts @@ -22,6 +22,7 @@ import { type CircuitDeviceRowPatchInput, } from "./circuit-device-row.persistence.js"; import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js"; +import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect; @@ -117,6 +118,13 @@ export class CircuitDeviceRowProjectCommandRepository if (update.changes !== 1) { throw new Error("Circuit device row changed before command execution."); } + if (patch.phaseType !== undefined) { + updateDerivedCircuitVoltage( + tx, + input.projectId, + current.circuitId + ); + } return { forward: appliedForward, diff --git a/src/db/repositories/circuit-device-row-structure-project-command.repository.ts b/src/db/repositories/circuit-device-row-structure-project-command.repository.ts index b53a16a..f164db7 100644 --- a/src/db/repositories/circuit-device-row-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-device-row-structure-project-command.repository.ts @@ -22,6 +22,7 @@ import { toCircuitDeviceRowSnapshot, } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; export class CircuitDeviceRowStructureProjectCommandRepository implements CircuitDeviceRowStructureProjectCommandStore @@ -101,6 +102,7 @@ export class CircuitDeviceRowStructureProjectCommandRepository if (circuitUpdate.changes !== 1) { throw new Error("Circuit changed before device-row insertion."); } + updateDerivedCircuitVoltage(database, projectId, row.circuitId); return createCircuitDeviceRowDeleteProjectCommand( row.id, @@ -153,6 +155,11 @@ export class CircuitDeviceRowStructureProjectCommandRepository if (circuitUpdate.changes !== 1) { throw new Error("Circuit changed before device-row deletion."); } + updateDerivedCircuitVoltage( + database, + projectId, + expectedCircuitId + ); return createCircuitDeviceRowInsertProjectCommand( toCircuitDeviceRowSnapshot(row) diff --git a/src/db/repositories/circuit-project-command.repository.ts b/src/db/repositories/circuit-project-command.repository.ts index 45702fc..6f04ffb 100644 --- a/src/db/repositories/circuit-project-command.repository.ts +++ b/src/db/repositories/circuit-project-command.repository.ts @@ -18,7 +18,9 @@ import { toCircuitPatchValues, type CircuitPatchPersistenceInput, } from "./circuit.persistence.js"; -import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js"; +import { resolveCircuitVoltage } from "./project-voltage.persistence.js"; +import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; type CircuitRow = typeof circuits.$inferSelect; @@ -30,7 +32,7 @@ export class CircuitProjectCommandRepository executeUpdate(input: ExecuteCircuitUpdateCommandInput) { assertCircuitUpdateProjectCommand(input.command); - return executeProjectCommandTransaction( + return executeProjectCommandTransactionWithAppliedForward( this.database, input, (tx) => this.applyCommand(tx, input) @@ -71,14 +73,44 @@ export class CircuitProjectCommandRepository ]) ) as CircuitPatchPersistenceInput; this.assertSectionInCircuitList(tx, current, patch.sectionId); + if (input.source === "user") { + if ( + input.command.payload.changes.every( + (change) => change.field === "voltage" + ) + ) { + throw new Error("Circuit voltage is derived and cannot be edited."); + } + const devicePhaseTypes = tx + .select({ phaseType: circuitDeviceRows.phaseType }) + .from(circuitDeviceRows) + .where(eq(circuitDeviceRows.circuitId, current.id)) + .all() + .map((row) => row.phaseType); + const derivedVoltage = resolveCircuitVoltage( + tx, + input.projectId, + patch.sectionId ?? current.sectionId, + devicePhaseTypes + ); + if (derivedVoltage === current.voltage) { + delete patch.voltage; + } else { + patch.voltage = derivedVoltage; + } + } this.assertUniqueEquipmentIdentifier( tx, current, patch.equipmentIdentifier ); + const appliedForward = createCircuitUpdateProjectCommand( + current.id, + patch as CircuitUpdatePatch + ); const inversePatch = Object.fromEntries( - input.command.payload.changes.map((change) => [ + appliedForward.payload.changes.map((change) => [ change.field, getCircuitFieldValue(current, change.field), ]) @@ -97,7 +129,7 @@ export class CircuitProjectCommandRepository throw new Error("Circuit changed before command execution."); } - return inverse; + return { forward: appliedForward, inverse }; } private assertSectionInCircuitList( diff --git a/src/db/repositories/circuit-structure-project-command.repository.ts b/src/db/repositories/circuit-structure-project-command.repository.ts index 0bc973b..013ef0e 100644 --- a/src/db/repositories/circuit-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-structure-project-command.repository.ts @@ -23,6 +23,7 @@ import { toCircuitDeviceRowSnapshot, } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { resolveCircuitVoltage } from "./project-voltage.persistence.js"; export class CircuitStructureProjectCommandRepository implements CircuitStructureProjectCommandStore @@ -77,6 +78,19 @@ export class CircuitStructureProjectCommandRepository snapshot: CircuitSnapshot ) { this.assertCircuitLocation(database, projectId, snapshot); + if (source === "user") { + const derivedVoltage = resolveCircuitVoltage( + database, + projectId, + snapshot.sectionId, + snapshot.deviceRows.map((row) => row.phaseType) + ); + if (snapshot.voltage !== derivedVoltage) { + throw new Error( + "Circuit voltage must match the project phase voltage." + ); + } + } const existingCircuit = database .select({ id: circuits.id }) diff --git a/src/db/repositories/global-device.repository.ts b/src/db/repositories/global-device.repository.ts index bd18e39..92e52ee 100644 --- a/src/db/repositories/global-device.repository.ts +++ b/src/db/repositories/global-device.repository.ts @@ -26,7 +26,7 @@ export class GlobalDeviceRepository { quantity: input.quantity, installedPowerPerUnitKw: input.installedPowerPerUnitKw, demandFactor: input.demandFactor, - voltageV: input.voltageV ?? null, + voltageV: null, phaseCount: input.phaseCount ?? null, powerFactor: input.powerFactor ?? null, note: input.note ?? null, @@ -45,7 +45,7 @@ export class GlobalDeviceRepository { quantity: input.quantity, installedPowerPerUnitKw: input.installedPowerPerUnitKw, demandFactor: input.demandFactor, - voltageV: input.voltageV ?? null, + voltageV: null, phaseCount: input.phaseCount ?? null, powerFactor: input.powerFactor ?? null, note: input.note ?? null, diff --git a/src/db/repositories/project-device-project-command.repository.ts b/src/db/repositories/project-device-project-command.repository.ts index dca7afc..f6bd3ef 100644 --- a/src/db/repositories/project-device-project-command.repository.ts +++ b/src/db/repositories/project-device-project-command.repository.ts @@ -12,7 +12,8 @@ import type { } from "../../domain/ports/project-device-project-command.store.js"; import type { AppDatabase } from "../database-context.js"; import { projectDevices } from "../schema/project-devices.js"; -import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js"; +import { resolveProjectDeviceVoltage } from "./project-voltage.persistence.js"; type ProjectDeviceRow = typeof projectDevices.$inferSelect; @@ -24,7 +25,7 @@ export class ProjectDeviceProjectCommandRepository executeUpdate(input: ExecuteProjectDeviceUpdateCommandInput) { assertProjectDeviceUpdateProjectCommand(input.command); - return executeProjectCommandTransaction( + return executeProjectCommandTransactionWithAppliedForward( this.database, input, (tx) => this.applyCommand(tx, input) @@ -60,8 +61,40 @@ export class ProjectDeviceProjectCommandRepository change.value, ]) ) as ProjectDeviceUpdatePatch; + if (input.source === "user") { + if ( + input.command.payload.changes.every( + (change) => change.field === "voltageV" + ) + ) { + throw new Error( + "Project-device voltage is derived and cannot be edited." + ); + } + const targetPhaseType = patch.phaseType ?? current.phaseType; + if ( + targetPhaseType !== "single_phase" && + targetPhaseType !== "three_phase" + ) { + throw new Error("Project-device phase type is invalid."); + } + const derivedVoltage = resolveProjectDeviceVoltage( + tx, + input.projectId, + targetPhaseType + ); + if (derivedVoltage === current.voltageV) { + delete patch.voltageV; + } else { + patch.voltageV = derivedVoltage; + } + } + const appliedForward = createProjectDeviceUpdateProjectCommand( + current.id, + patch + ); const inversePatch = Object.fromEntries( - input.command.payload.changes.map((change) => [ + appliedForward.payload.changes.map((change) => [ change.field, getProjectDeviceFieldValue(current, change.field), ]) @@ -87,7 +120,7 @@ export class ProjectDeviceProjectCommandRepository ); } - return inverse; + return { forward: appliedForward, inverse }; } } diff --git a/src/db/repositories/project-device-row-sync-project-command.repository.ts b/src/db/repositories/project-device-row-sync-project-command.repository.ts index d4265a0..da17458 100644 --- a/src/db/repositories/project-device-row-sync-project-command.repository.ts +++ b/src/db/repositories/project-device-row-sync-project-command.repository.ts @@ -16,6 +16,7 @@ import { circuitLists } from "../schema/circuit-lists.js"; import { circuits } from "../schema/circuits.js"; import { projectDevices } from "../schema/project-devices.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect; @@ -129,6 +130,25 @@ export class ProjectDeviceRowSyncProjectCommandRepository ); } } + const affectedCircuitIds = new Set( + input.command.payload.rows + .filter( + (assignment) => + assignment.expected.phaseType !== + assignment.target.phaseType + ) + .map( + (assignment) => + persistedById.get(assignment.rowId)!.circuitId + ) + ); + for (const circuitId of affectedCircuitIds) { + updateDerivedCircuitVoltage( + tx, + input.projectId, + circuitId + ); + } return inverse; } diff --git a/src/db/repositories/project-device-structure-project-command.repository.ts b/src/db/repositories/project-device-structure-project-command.repository.ts index 1561b18..fcbdb2c 100644 --- a/src/db/repositories/project-device-structure-project-command.repository.ts +++ b/src/db/repositories/project-device-structure-project-command.repository.ts @@ -28,6 +28,7 @@ import { projectDevices } from "../schema/project-devices.js"; import { projects } from "../schema/projects.js"; import { toCircuitDeviceRowSnapshot } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { resolveProjectDeviceVoltage } from "./project-voltage.persistence.js"; const circuitDeviceRowSnapshotFields = [ "id", @@ -125,6 +126,19 @@ export class ProjectDeviceStructureProjectCommandRepository if (!project) { throw new Error("Project does not exist."); } + if ( + source === "user" && + snapshot.voltageV !== + resolveProjectDeviceVoltage( + database, + projectId, + snapshot.phaseType + ) + ) { + throw new Error( + "Project-device voltage must match the project phase voltage." + ); + } const existing = database .select({ id: projectDevices.id }) .from(projectDevices) diff --git a/src/db/repositories/project-settings-project-command.repository.ts b/src/db/repositories/project-settings-project-command.repository.ts index 8e0a44a..a5a3621 100644 --- a/src/db/repositories/project-settings-project-command.repository.ts +++ b/src/db/repositories/project-settings-project-command.repository.ts @@ -15,6 +15,7 @@ import type { AppDatabase } from "../database-context.js"; import { projects } from "../schema/projects.js"; import { distributionBoards } from "../schema/distribution-boards.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { updateAllDerivedProjectVoltages } from "./project-voltage.persistence.js"; export class ProjectSettingsProjectCommandRepository implements ProjectSettingsProjectCommandStore @@ -94,6 +95,7 @@ export class ProjectSettingsProjectCommandRepository if (updated.changes !== 1) { throw new Error("Project changed before settings update."); } + updateAllDerivedProjectVoltages(tx, input.projectId); return inverse; } diff --git a/src/db/repositories/project-snapshot.repository.ts b/src/db/repositories/project-snapshot.repository.ts index 21d5ecf..17b989d 100644 --- a/src/db/repositories/project-snapshot.repository.ts +++ b/src/db/repositories/project-snapshot.repository.ts @@ -9,6 +9,7 @@ import { legacyProjectStateSnapshotSchemaVersion, previousProjectStateSnapshotSchemaVersion, projectStateSnapshotSchemaVersion, + supplyTypesProjectStateSnapshotSchemaVersion, } from "../../domain/models/project-state-snapshot.model.js"; import type { CreateNamedProjectSnapshotInput, @@ -146,6 +147,8 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore { stored.schemaVersion !== previousProjectStateSnapshotSchemaVersion && stored.schemaVersion !== distributionBoardProjectStateSnapshotSchemaVersion && + stored.schemaVersion !== + supplyTypesProjectStateSnapshotSchemaVersion && stored.schemaVersion !== projectStateSnapshotSchemaVersion ) { throw new Error("Project snapshot schema version is not supported."); diff --git a/src/db/repositories/project-voltage.persistence.ts b/src/db/repositories/project-voltage.persistence.ts new file mode 100644 index 0000000..555ca0b --- /dev/null +++ b/src/db/repositories/project-voltage.persistence.ts @@ -0,0 +1,154 @@ +import { and, eq } from "drizzle-orm"; +import { + resolveCircuitPhaseType, + resolveProjectVoltage, + type ElectricalPhaseType, + type ProjectVoltageSettings, +} from "../../domain/services/project-voltage.service.js"; +import type { AppDatabase } from "../database-context.js"; +import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; +import { circuitLists } from "../schema/circuit-lists.js"; +import { circuitSections } from "../schema/circuit-sections.js"; +import { circuits } from "../schema/circuits.js"; +import { projectDevices } from "../schema/project-devices.js"; +import { projects } from "../schema/projects.js"; + +export function readProjectVoltageSettings( + database: AppDatabase, + projectId: string +): ProjectVoltageSettings { + const project = database + .select({ + singlePhaseVoltageV: projects.singlePhaseVoltageV, + threePhaseVoltageV: projects.threePhaseVoltageV, + }) + .from(projects) + .where(eq(projects.id, projectId)) + .get(); + if (!project) { + throw new Error("Project not found."); + } + return project; +} + +export function resolveProjectDeviceVoltage( + database: AppDatabase, + projectId: string, + phaseType: ElectricalPhaseType +) { + return resolveProjectVoltage( + phaseType, + readProjectVoltageSettings(database, projectId) + ); +} + +export function resolveCircuitVoltage( + database: AppDatabase, + projectId: string, + sectionId: string, + devicePhaseTypes: ReadonlyArray = [] +) { + const section = database + .select({ key: circuitSections.key }) + .from(circuitSections) + .innerJoin( + circuitLists, + eq(circuitLists.id, circuitSections.circuitListId) + ) + .where( + and( + eq(circuitSections.id, sectionId), + eq(circuitLists.projectId, projectId) + ) + ) + .get(); + if (!section) { + throw new Error("Circuit section does not belong to project."); + } + return resolveProjectVoltage( + resolveCircuitPhaseType(section.key, devicePhaseTypes), + readProjectVoltageSettings(database, projectId) + ); +} + +export function updateDerivedCircuitVoltage( + database: AppDatabase, + projectId: string, + circuitId: string +) { + const circuit = database + .select({ + id: circuits.id, + sectionId: circuits.sectionId, + }) + .from(circuits) + .innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId)) + .where( + and( + eq(circuits.id, circuitId), + eq(circuitLists.projectId, projectId) + ) + ) + .get(); + if (!circuit) { + throw new Error("Circuit does not belong to project."); + } + const devicePhaseTypes = database + .select({ phaseType: circuitDeviceRows.phaseType }) + .from(circuitDeviceRows) + .where(eq(circuitDeviceRows.circuitId, circuitId)) + .all() + .map((row) => row.phaseType); + const voltage = resolveCircuitVoltage( + database, + projectId, + circuit.sectionId, + devicePhaseTypes + ); + database + .update(circuits) + .set({ voltage }) + .where(eq(circuits.id, circuitId)) + .run(); + return voltage; +} + +export function updateAllDerivedProjectVoltages( + database: AppDatabase, + projectId: string +) { + const settings = readProjectVoltageSettings(database, projectId); + const devices = database + .select({ + id: projectDevices.id, + phaseType: projectDevices.phaseType, + }) + .from(projectDevices) + .where(eq(projectDevices.projectId, projectId)) + .all(); + for (const device of devices) { + if ( + device.phaseType !== "single_phase" && + device.phaseType !== "three_phase" + ) { + throw new Error("Persisted project-device phase type is invalid."); + } + database + .update(projectDevices) + .set({ + voltageV: resolveProjectVoltage(device.phaseType, settings), + }) + .where(eq(projectDevices.id, device.id)) + .run(); + } + + const projectCircuits = database + .select({ id: circuits.id }) + .from(circuits) + .innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId)) + .where(eq(circuitLists.projectId, projectId)) + .all(); + for (const circuit of projectCircuits) { + updateDerivedCircuitVoltage(database, projectId, circuit.id); + } +} diff --git a/src/domain/models/circuit-tree.model.ts b/src/domain/models/circuit-tree.model.ts index 311d3b0..d476c51 100644 --- a/src/domain/models/circuit-tree.model.ts +++ b/src/domain/models/circuit-tree.model.ts @@ -58,6 +58,8 @@ export interface CircuitTreeSectionBlock { export interface CircuitTreeResponse { circuitListId: string; currentRevision: number; + singlePhaseVoltageV: number; + threePhaseVoltageV: number; sections: CircuitTreeSectionBlock[]; } diff --git a/src/domain/models/project-state-snapshot.model.ts b/src/domain/models/project-state-snapshot.model.ts index c1a7c91..8279388 100644 --- a/src/domain/models/project-state-snapshot.model.ts +++ b/src/domain/models/project-state-snapshot.model.ts @@ -3,11 +3,16 @@ import { defaultDistributionBoardSupplyTypes, distributionBoardSupplyTypes, } from "../../shared/constants/distribution-board.js"; +import { + resolveCircuitPhaseType, + resolveProjectVoltage, +} from "../services/project-voltage.service.js"; export const legacyProjectStateSnapshotSchemaVersion = 1 as const; export const previousProjectStateSnapshotSchemaVersion = 2 as const; export const distributionBoardProjectStateSnapshotSchemaVersion = 3 as const; -export const projectStateSnapshotSchemaVersion = 4 as const; +export const supplyTypesProjectStateSnapshotSchemaVersion = 4 as const; +export const projectStateSnapshotSchemaVersion = 5 as const; const idSchema = z.string().trim().min(1); const nullableStringSchema = z.string().nullable(); @@ -198,15 +203,20 @@ const distributionBoardProjectStateSnapshotSchema = z }) .strict(); -export const projectStateSnapshotSchema = z +const supplyTypesProjectStateSnapshotSchema = z .object({ - schemaVersion: z.literal(projectStateSnapshotSchemaVersion), + schemaVersion: z.literal(supplyTypesProjectStateSnapshotSchemaVersion), project: currentProjectSchema, distributionBoards: z.array(distributionBoardSchema), ...commonProjectStateSnapshotContents, }) .strict(); +export const projectStateSnapshotSchema = + supplyTypesProjectStateSnapshotSchema.extend({ + schemaVersion: z.literal(projectStateSnapshotSchemaVersion), + }); + export type ProjectStateSnapshot = z.infer< typeof projectStateSnapshotSchema >; @@ -217,23 +227,33 @@ export function parseProjectStateSnapshot( const version = isPlainObject(value) ? value.schemaVersion : undefined; const snapshot = version === legacyProjectStateSnapshotSchemaVersion - ? upgradeDistributionBoardProjectStateSnapshot( - upgradePreviousProjectStateSnapshot( - upgradeLegacyProjectStateSnapshot( - legacyProjectStateSnapshotSchema.parse(value) + ? upgradeSupplyTypesProjectStateSnapshot( + upgradeDistributionBoardProjectStateSnapshot( + upgradePreviousProjectStateSnapshot( + upgradeLegacyProjectStateSnapshot( + legacyProjectStateSnapshotSchema.parse(value) + ) ) ) ) : version === previousProjectStateSnapshotSchemaVersion - ? upgradeDistributionBoardProjectStateSnapshot( - upgradePreviousProjectStateSnapshot( - previousProjectStateSnapshotSchema.parse(value) + ? upgradeSupplyTypesProjectStateSnapshot( + upgradeDistributionBoardProjectStateSnapshot( + upgradePreviousProjectStateSnapshot( + previousProjectStateSnapshotSchema.parse(value) + ) ) ) : version === distributionBoardProjectStateSnapshotSchemaVersion - ? upgradeDistributionBoardProjectStateSnapshot( - distributionBoardProjectStateSnapshotSchema.parse(value) + ? upgradeSupplyTypesProjectStateSnapshot( + upgradeDistributionBoardProjectStateSnapshot( + distributionBoardProjectStateSnapshotSchema.parse(value) + ) ) + : version === supplyTypesProjectStateSnapshotSchemaVersion + ? upgradeSupplyTypesProjectStateSnapshot( + supplyTypesProjectStateSnapshotSchema.parse(value) + ) : projectStateSnapshotSchema.parse(value); assertProjectStateSnapshotRelations(snapshot); return snapshot; @@ -271,10 +291,10 @@ function upgradePreviousProjectStateSnapshot( function upgradeDistributionBoardProjectStateSnapshot( snapshot: z.infer -): ProjectStateSnapshot { +): z.infer { return { ...snapshot, - schemaVersion: projectStateSnapshotSchemaVersion, + schemaVersion: supplyTypesProjectStateSnapshotSchemaVersion, project: { ...snapshot.project, enabledDistributionBoardSupplyTypes: [ @@ -284,6 +304,37 @@ function upgradeDistributionBoardProjectStateSnapshot( }; } +function upgradeSupplyTypesProjectStateSnapshot( + snapshot: z.infer +): ProjectStateSnapshot { + const settings = snapshot.project; + const sectionById = new Map( + snapshot.circuitSections.map((section) => [section.id, section]) + ); + return { + ...snapshot, + schemaVersion: projectStateSnapshotSchemaVersion, + projectDevices: snapshot.projectDevices.map((device) => ({ + ...device, + voltageV: resolveProjectVoltage(device.phaseType, settings), + })), + circuits: snapshot.circuits.map((circuit) => { + const section = sectionById.get(circuit.sectionId); + if (!section) { + return circuit; + } + const phaseType = resolveCircuitPhaseType( + section.key, + circuit.deviceRows.map((row) => row.phaseType) + ); + return { + ...circuit, + voltage: resolveProjectVoltage(phaseType, settings), + }; + }), + }; +} + function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } @@ -364,6 +415,14 @@ function assertProjectStateSnapshotRelations( } for (const device of snapshot.projectDevices) { assertProjectOwnership(device.projectId, projectId, "project device"); + if ( + device.voltageV !== + resolveProjectVoltage(device.phaseType, snapshot.project) + ) { + throw new Error( + "Snapshot project device voltage must match project settings." + ); + } } for (const floor of snapshot.floors) { assertProjectOwnership(floor.projectId, projectId, "floor"); @@ -394,6 +453,18 @@ function assertProjectStateSnapshotRelations( "Snapshot circuit reserve state must match its device rows." ); } + const expectedVoltage = resolveProjectVoltage( + resolveCircuitPhaseType( + section.key, + circuit.deviceRows.map((row) => row.phaseType) + ), + snapshot.project + ); + if (circuit.voltage !== expectedVoltage) { + throw new Error( + "Snapshot circuit voltage must match project settings." + ); + } for (const row of circuit.deviceRows) { if (row.circuitId !== circuit.id) { throw new Error( diff --git a/src/domain/services/project-voltage.service.ts b/src/domain/services/project-voltage.service.ts new file mode 100644 index 0000000..cbc78cf --- /dev/null +++ b/src/domain/services/project-voltage.service.ts @@ -0,0 +1,35 @@ +export type ElectricalPhaseType = "single_phase" | "three_phase"; + +export interface ProjectVoltageSettings { + singlePhaseVoltageV: number; + threePhaseVoltageV: number; +} + +export function resolveProjectVoltage( + phaseType: ElectricalPhaseType, + settings: ProjectVoltageSettings +) { + return phaseType === "three_phase" + ? settings.threePhaseVoltageV + : settings.singlePhaseVoltageV; +} + +export function resolveCircuitPhaseType( + sectionKey: string, + devicePhaseTypes: ReadonlyArray = [] +): ElectricalPhaseType { + if (sectionKey === "three_phase") { + return "three_phase"; + } + if (sectionKey === "lighting" || sectionKey === "single_phase") { + return "single_phase"; + } + const assignedPhaseTypes = devicePhaseTypes.filter( + (phaseType): phaseType is ElectricalPhaseType => + phaseType === "single_phase" || phaseType === "three_phase" + ); + return assignedPhaseTypes.length > 0 && + assignedPhaseTypes.every((phaseType) => phaseType === "three_phase") + ? "three_phase" + : "single_phase"; +} diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index b5f5880..1836291 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -5,6 +5,10 @@ import { inferProjectDeviceSectionKey, isProjectDevicePlacementValid, } from "../../domain/services/project-device-placement.service"; +import { + resolveCircuitPhaseType, + resolveProjectVoltage, +} from "../../domain/services/project-voltage.service"; import { getInsertionSortOrder, resolveGridInsertionIntent, @@ -819,6 +823,19 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str values: CreateCircuitInputDto, deviceRowValues: CreateCircuitDeviceRowInputDto[] = [] ) { + const section = data?.sections.find( + (entry) => entry.id === values.sectionId + ); + if (!section || !data) { + throw new Error("Bereich wurde nicht gefunden."); + } + const voltage = resolveProjectVoltage( + resolveCircuitPhaseType( + section.key, + deviceRowValues.map((row) => row.phaseType) + ), + data + ); const circuitId = crypto.randomUUID(); const deviceRows = deviceRowValues.map((row, index) => createDeviceRowSnapshot(circuitId, row, (index + 1) * 10) @@ -826,7 +843,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str return buildCircuitInsertSnapshot({ id: circuitId, circuitListId, - values, + values: { ...values, voltage }, deviceRows, }); } diff --git a/src/frontend/components/project-device-modal.tsx b/src/frontend/components/project-device-modal.tsx index 17035b4..907fffe 100644 --- a/src/frontend/components/project-device-modal.tsx +++ b/src/frontend/components/project-device-modal.tsx @@ -47,7 +47,6 @@ export function ProjectDeviceModal({ simultaneityFactor: Number(values.simultaneityFactor), cosPhi: optionalNumber(values.cosPhi), remark: optionalString(values.remark), - voltageV: optionalNumber(values.voltageV), }); } @@ -182,13 +181,6 @@ export function ProjectDeviceModal({ step="0.01" value={values.cosPhi} /> - update("voltageV", value)} - value={values.voltageV} - />