diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 6c1bff3..ed3812e 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -209,12 +209,16 @@ unverknüpft und vollständig unverändert sind. Gerät, Linkänderungen, Revisi und Historienstapel teilen dieselbe Transaktion. Create, Import aus der globalen Gerätebibliothek und Delete laufen über dieselbe Command-Grenze; ihre Antworten liefern Gerät und aktualisierten Historienstand an die Projektseite zurück. -`project.update-settings` versioniert die beiden Projekt-Standardspannungen als -eine atomare Änderung. Der Store leitet das inverse Kommando aus dem -gespeicherten Projekt ab und schreibt Werte, Revision und Historienstapel -gemeinsam. `PUT /api/projects/:projectId` verlangt deshalb -`expectedRevision`, liefert Projekt plus aktualisierten Historienstand und -besitzt keinen separaten direkten Settings-Schreibweg mehr. +`project.update-settings` versioniert Projektname, interne und externe +Projektnummer, Bauherr, Beschreibung sowie beide Standardspannungen als eine +atomare Änderung. Die Projektseite bearbeitet diese Angaben in einem +beschrifteten Einstellungsmodal statt in einer permanenten Formularkarte. Der +Store leitet das inverse Kommando aus dem gespeicherten Projekt ab und schreibt +Werte, Revision und Historienstapel gemeinsam. `PUT /api/projects/:projectId` +verlangt deshalb `expectedRevision`, liefert Projekt plus aktualisierten +Historienstand und besitzt keinen separaten direkten Settings-Schreibweg mehr. +Kommando- und Snapshot-Versionen vor dieser Erweiterung bleiben les- und +ausführbar; fehlende Metadaten werden dabei als `null` behandelt. `distribution-board.insert` versioniert die Anlage einer Verteilung als einen vollständigen Block aus Verteilung, Stromkreisliste und vier Standardbereichen. Alle UUIDs entstehen vor dem Command und bleiben über Undo/Redo stabil. diff --git a/scripts/db-verify-circuit-schema.js b/scripts/db-verify-circuit-schema.js index fa38fca..2159898 100644 --- a/scripts/db-verify-circuit-schema.js +++ b/scripts/db-verify-circuit-schema.js @@ -48,12 +48,25 @@ const circuitColumns = new Set( db.prepare("PRAGMA table_info(circuits)").all().map((column) => column.name) ); const missingCircuitColumns = requiredCircuitColumns.filter((name) => !circuitColumns.has(name)); +const requiredProjectColumns = [ + "internal_project_number", + "external_project_number", + "building_owner", + "description", +]; +const projectColumns = new Set( + db.prepare("PRAGMA table_info(projects)").all().map((column) => column.name) +); +const missingProjectColumns = requiredProjectColumns.filter( + (name) => !projectColumns.has(name) +); console.log("Database:", dbPath); console.log("Required tables:", requiredTables.join(", ")); console.log("Existing tables:", [...existing].join(", ") || "(none)"); console.log("Required project-device columns:", requiredProjectDeviceColumns.join(", ")); console.log("Required circuit columns:", requiredCircuitColumns.join(", ")); +console.log("Required project columns:", requiredProjectColumns.join(", ")); if (missing.length > 0) { console.error("Missing tables:", missing.join(", ")); @@ -78,4 +91,9 @@ if (missingCircuitColumns.length > 0) { process.exit(1); } +if (missingProjectColumns.length > 0) { + console.error("Missing project columns:", missingProjectColumns.join(", ")); + process.exit(1); +} + console.log("Circuit-first schema verification passed."); diff --git a/src/app/projects/[projectId]/page.tsx b/src/app/projects/[projectId]/page.tsx index e9d7660..f36c0b6 100644 --- a/src/app/projects/[projectId]/page.tsx +++ b/src/app/projects/[projectId]/page.tsx @@ -40,6 +40,10 @@ import { type ProjectDeviceSyncField, } from "../../../shared/constants/project-device-sync-fields"; import { ProjectVersionHistory } from "../../../frontend/components/project-version-history"; +import { + ProjectSettingsModal, + type ProjectSettingsInput, +} from "../../../frontend/components/project-settings-modal"; const projectDeviceSyncFieldLabels: Record = { name: "Technischer Name", @@ -92,8 +96,7 @@ export default function ProjectDetailPage() { const [roomNumber, setRoomNumber] = useState(""); const [roomName, setRoomName] = useState(""); const [roomFloorId, setRoomFloorId] = useState(""); - const [singlePhaseVoltageV, setSinglePhaseVoltageV] = useState("230"); - const [threePhaseVoltageV, setThreePhaseVoltageV] = useState("400"); + const [isProjectSettingsOpen, setIsProjectSettingsOpen] = useState(false); const [projectDeviceForm, setProjectDeviceForm] = useState>({ name: "", displayName: "", @@ -142,10 +145,6 @@ export default function ProjectDetailPage() { ]) => { const currentProject = projects.find((item) => item.id === projectId) ?? null; setProject(currentProject); - if (currentProject) { - setSinglePhaseVoltageV(String(currentProject.singlePhaseVoltageV)); - setThreePhaseVoltageV(String(currentProject.threePhaseVoltageV)); - } setBoards(distributionBoards); setCircuitLists(loadedCircuitLists); setFloors(loadedFloors); @@ -259,7 +258,7 @@ export default function ProjectDetailPage() { } } - async function handleSaveProjectSettings() { + async function handleSaveProjectSettings(input: ProjectSettingsInput) { if (!projectId || !project) { return; } @@ -269,12 +268,10 @@ export default function ProjectDetailPage() { const result = await updateProjectSettings( projectId, project.currentRevision, - { - singlePhaseVoltageV: Number(singlePhaseVoltageV), - threePhaseVoltageV: Number(threePhaseVoltageV), - } + input ); setProject(result.project); + setIsProjectSettingsOpen(false); } catch (err) { setError(err instanceof Error ? err.message : "Projekteigenschaften konnten nicht gespeichert werden."); } finally { @@ -532,66 +529,34 @@ export default function ProjectDetailPage() { return (
-
+

{project?.name ?? "Projekt"}

-

Verteilerübersicht und Einstieg in die Stromkreislisten

+

+ {project?.internalProjectNumber + ? `Projektnummer ${project.internalProjectNumber} · ` + : ""} + Verteilerübersicht und Einstieg in die Stromkreislisten +

+
+
+ + + Zur Projektübersicht +
- - Zur Projektseite -
{error ?
{error}
: null}
-
-
-
Projekteigenschaften
-
-
-
- - setSinglePhaseVoltageV(event.target.value)} - /> -
-
- - setThreePhaseVoltageV(event.target.value)} - /> -
-
- -
-
-
-
-
- {project ? (
+ {project && isProjectSettingsOpen ? ( + setIsProjectSettingsOpen(false)} + onSave={handleSaveProjectSettings} + project={project} + /> + ) : null}
); } diff --git a/src/db/migrations/0016_dashing_darkstar.sql b/src/db/migrations/0016_dashing_darkstar.sql new file mode 100644 index 0000000..2228b1b --- /dev/null +++ b/src/db/migrations/0016_dashing_darkstar.sql @@ -0,0 +1,4 @@ +ALTER TABLE `projects` ADD `internal_project_number` text;--> statement-breakpoint +ALTER TABLE `projects` ADD `external_project_number` text;--> statement-breakpoint +ALTER TABLE `projects` ADD `building_owner` text;--> statement-breakpoint +ALTER TABLE `projects` ADD `description` text; \ No newline at end of file diff --git a/src/db/migrations/meta/0016_snapshot.json b/src/db/migrations/meta/0016_snapshot.json new file mode 100644 index 0000000..1e43d03 --- /dev/null +++ b/src/db/migrations/meta/0016_snapshot.json @@ -0,0 +1,1850 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "8502c7d2-e925-42c3-8044-3e965c170f97", + "prevId": "3248cb0b-5512-43d3-8193-e7218cb8ae66", + "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", + "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_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_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", + "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 + }, + "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", + "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": {} + }, + "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", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "consumers_distribution_board_id_distribution_boards_id_fk": { + "name": "consumers_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "consumers", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "consumers_circuit_list_id_circuit_lists_id_fk": { + "name": "consumers_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "consumers", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "consumers_project_device_id_project_devices_id_fk": { + "name": "consumers_project_device_id_project_devices_id_fk", + "tableFrom": "consumers", + "tableTo": "project_devices", + "columnsFrom": [ + "project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "consumers_room_id_rooms_id_fk": { + "name": "consumers_room_id_rooms_id_fk", + "tableFrom": "consumers", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "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 + } + }, + "indexes": {}, + "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" + } + }, + "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", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "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": {} + }, + "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", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "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", + "tableTo": "circuit_device_rows", + "columnsFrom": [ + "circuit_device_row_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "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", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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 + }, + "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 + }, + "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", + "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": {} + } + }, + "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 152562d..580b1ce 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -113,6 +113,13 @@ "when": 1785014383032, "tag": "0015_modern_madame_masque", "breakpoints": true + }, + { + "idx": 16, + "version": "6", + "when": 1785254079435, + "tag": "0016_dashing_darkstar", + "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 f767b8f..10485ff 100644 --- a/src/db/repositories/project-settings-project-command.repository.ts +++ b/src/db/repositories/project-settings-project-command.repository.ts @@ -2,6 +2,8 @@ import { eq } from "drizzle-orm"; import { assertProjectSettingsUpdateProjectCommand, createProjectSettingsUpdateProjectCommand, + legacyProjectSettingsUpdateCommandSchemaVersion, + type ProjectSettingsValues, } from "../../domain/models/project-settings-project-command.model.js"; import type { ExecuteProjectSettingsUpdateCommandInput, @@ -37,22 +39,45 @@ export class ProjectSettingsProjectCommandRepository if (!current) { throw new Error("Project not found."); } - if ( - current.singlePhaseVoltageV === - input.command.payload.singlePhaseVoltageV && - current.threePhaseVoltageV === - input.command.payload.threePhaseVoltageV - ) { + const target: ProjectSettingsValues = + input.command.schemaVersion === + legacyProjectSettingsUpdateCommandSchemaVersion + ? { + name: current.name, + internalProjectNumber: current.internalProjectNumber, + externalProjectNumber: current.externalProjectNumber, + buildingOwner: current.buildingOwner, + description: current.description, + ...input.command.payload, + } + : input.command.payload; + if (projectSettingsEqual(current, target)) { throw new Error("Project settings did not change."); } - const inverse = createProjectSettingsUpdateProjectCommand({ - singlePhaseVoltageV: current.singlePhaseVoltageV, - threePhaseVoltageV: current.threePhaseVoltageV, - }); + const inverse = + input.command.schemaVersion === + legacyProjectSettingsUpdateCommandSchemaVersion + ? { + schemaVersion: legacyProjectSettingsUpdateCommandSchemaVersion, + type: input.command.type, + payload: { + singlePhaseVoltageV: current.singlePhaseVoltageV, + threePhaseVoltageV: current.threePhaseVoltageV, + }, + } + : createProjectSettingsUpdateProjectCommand({ + name: current.name, + internalProjectNumber: current.internalProjectNumber, + externalProjectNumber: current.externalProjectNumber, + buildingOwner: current.buildingOwner, + description: current.description, + singlePhaseVoltageV: current.singlePhaseVoltageV, + threePhaseVoltageV: current.threePhaseVoltageV, + }); const updated = tx .update(projects) - .set(input.command.payload) + .set(target) .where(eq(projects.id, input.projectId)) .run(); if (updated.changes !== 1) { @@ -62,3 +87,18 @@ export class ProjectSettingsProjectCommandRepository return inverse; } } + +function projectSettingsEqual( + current: ProjectSettingsValues, + target: ProjectSettingsValues +) { + return ( + current.name === target.name && + current.internalProjectNumber === target.internalProjectNumber && + current.externalProjectNumber === target.externalProjectNumber && + current.buildingOwner === target.buildingOwner && + current.description === target.description && + current.singlePhaseVoltageV === target.singlePhaseVoltageV && + current.threePhaseVoltageV === target.threePhaseVoltageV + ); +} diff --git a/src/db/repositories/project-snapshot.repository.ts b/src/db/repositories/project-snapshot.repository.ts index 5274fee..8fee255 100644 --- a/src/db/repositories/project-snapshot.repository.ts +++ b/src/db/repositories/project-snapshot.repository.ts @@ -5,6 +5,7 @@ import { ProjectSnapshotNameConflictError } from "../../domain/errors/project-sn import { createProjectStateRestoreCommand } from "../../domain/models/project-state-restore-command.model.js"; import { deserializeProjectStateSnapshot, + legacyProjectStateSnapshotSchemaVersion, projectStateSnapshotSchemaVersion, } from "../../domain/models/project-state-snapshot.model.js"; import type { @@ -138,7 +139,10 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore { if (actualChecksum !== stored.payloadSha256) { throw new Error("Project snapshot checksum verification failed."); } - if (stored.schemaVersion !== projectStateSnapshotSchemaVersion) { + if ( + stored.schemaVersion !== legacyProjectStateSnapshotSchemaVersion && + stored.schemaVersion !== projectStateSnapshotSchemaVersion + ) { throw new Error("Project snapshot schema version is not supported."); } const targetState = deserializeProjectStateSnapshot(stored.payloadJson); diff --git a/src/db/repositories/project-state-restore-command.repository.ts b/src/db/repositories/project-state-restore-command.repository.ts index 8a67fac..28c6168 100644 --- a/src/db/repositories/project-state-restore-command.repository.ts +++ b/src/db/repositories/project-state-restore-command.repository.ts @@ -5,6 +5,7 @@ import { createProjectStateRestoreCommand, } from "../../domain/models/project-state-restore-command.model.js"; import { + parseProjectStateSnapshot, serializeProjectStateSnapshot, type ProjectStateSnapshot, } from "../../domain/models/project-state-snapshot.model.js"; @@ -38,7 +39,10 @@ export class ProjectStateRestoreCommandRepository execute(input: ExecuteProjectStateRestoreCommandInput) { assertProjectStateRestoreCommand(input.command); - if (input.command.payload.targetState.project.id !== input.projectId) { + const targetState = parseProjectStateSnapshot( + input.command.payload.targetState + ); + if (targetState.project.id !== input.projectId) { throw new Error("Restore state belongs to a different project."); } @@ -68,15 +72,16 @@ export class ProjectStateRestoreCommandRepository ); } - const targetPayloadJson = serializeProjectStateSnapshot( + const targetState = parseProjectStateSnapshot( input.command.payload.targetState ); + const targetPayloadJson = serializeProjectStateSnapshot(targetState); const inverse = createProjectStateRestoreCommand( hashProjectStatePayload(targetPayloadJson), current.state ); - replaceProjectState(tx, input.command.payload.targetState); + replaceProjectState(tx, targetState); return inverse; } @@ -111,6 +116,10 @@ function replaceProjectState( .update(projects) .set({ name: state.project.name, + internalProjectNumber: state.project.internalProjectNumber, + externalProjectNumber: state.project.externalProjectNumber, + buildingOwner: state.project.buildingOwner, + description: state.project.description, singlePhaseVoltageV: state.project.singlePhaseVoltageV, threePhaseVoltageV: state.project.threePhaseVoltageV, }) diff --git a/src/db/repositories/project-state-snapshot.persistence.ts b/src/db/repositories/project-state-snapshot.persistence.ts index 3183c4f..d399dad 100644 --- a/src/db/repositories/project-state-snapshot.persistence.ts +++ b/src/db/repositories/project-state-snapshot.persistence.ts @@ -32,6 +32,10 @@ export function readProjectStateSnapshot( .select({ id: projects.id, name: projects.name, + internalProjectNumber: projects.internalProjectNumber, + externalProjectNumber: projects.externalProjectNumber, + buildingOwner: projects.buildingOwner, + description: projects.description, singlePhaseVoltageV: projects.singlePhaseVoltageV, threePhaseVoltageV: projects.threePhaseVoltageV, currentRevision: projects.currentRevision, @@ -172,6 +176,10 @@ export function readProjectStateSnapshot( project: { id: project.id, name: project.name, + internalProjectNumber: project.internalProjectNumber, + externalProjectNumber: project.externalProjectNumber, + buildingOwner: project.buildingOwner, + description: project.description, singlePhaseVoltageV: project.singlePhaseVoltageV, threePhaseVoltageV: project.threePhaseVoltageV, }, diff --git a/src/db/repositories/project.repository.ts b/src/db/repositories/project.repository.ts index 0ef03a2..d729422 100644 --- a/src/db/repositories/project.repository.ts +++ b/src/db/repositories/project.repository.ts @@ -20,6 +20,10 @@ export class ProjectRepository { const project = { id, name: input.name, + internalProjectNumber: input.internalProjectNumber ?? null, + externalProjectNumber: input.externalProjectNumber ?? null, + buildingOwner: input.buildingOwner ?? null, + description: input.description ?? null, singlePhaseVoltageV: input.singlePhaseVoltageV ?? 230, threePhaseVoltageV: input.threePhaseVoltageV ?? 400, currentRevision: 0, diff --git a/src/db/schema/projects.ts b/src/db/schema/projects.ts index b0d27b1..05d6324 100644 --- a/src/db/schema/projects.ts +++ b/src/db/schema/projects.ts @@ -3,6 +3,10 @@ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; export const projects = sqliteTable("projects", { id: text("id").primaryKey(), name: text("name").notNull(), + internalProjectNumber: text("internal_project_number"), + externalProjectNumber: text("external_project_number"), + buildingOwner: text("building_owner"), + description: text("description"), singlePhaseVoltageV: integer("single_phase_voltage_v").notNull().default(230), threePhaseVoltageV: integer("three_phase_voltage_v").notNull().default(400), 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 bd9110a..df943ef 100644 --- a/src/domain/models/project-settings-project-command.model.ts +++ b/src/domain/models/project-settings-project-command.model.ts @@ -2,23 +2,42 @@ import type { SerializedProjectCommand } from "./project-command.model.js"; export const projectSettingsUpdateCommandType = "project.update-settings" as const; -export const projectSettingsUpdateCommandSchemaVersion = 1 as const; +export const legacyProjectSettingsUpdateCommandSchemaVersion = 1 as const; +export const projectSettingsUpdateCommandSchemaVersion = 2 as const; -export interface ProjectSettingsValues { +export interface LegacyProjectSettingsValues { singlePhaseVoltageV: number; threePhaseVoltageV: number; } -export interface ProjectSettingsUpdateProjectCommand +export interface ProjectSettingsValues extends LegacyProjectSettingsValues { + name: string; + internalProjectNumber: string | null; + externalProjectNumber: string | null; + buildingOwner: string | null; + description: string | null; +} + +export interface LegacyProjectSettingsUpdateProjectCommand + extends SerializedProjectCommand { + schemaVersion: typeof legacyProjectSettingsUpdateCommandSchemaVersion; + type: typeof projectSettingsUpdateCommandType; +} + +export interface CurrentProjectSettingsUpdateProjectCommand extends SerializedProjectCommand { schemaVersion: typeof projectSettingsUpdateCommandSchemaVersion; type: typeof projectSettingsUpdateCommandType; } +export type ProjectSettingsUpdateProjectCommand = + | LegacyProjectSettingsUpdateProjectCommand + | CurrentProjectSettingsUpdateProjectCommand; + export function createProjectSettingsUpdateProjectCommand( values: ProjectSettingsValues -): ProjectSettingsUpdateProjectCommand { - const command: ProjectSettingsUpdateProjectCommand = { +): CurrentProjectSettingsUpdateProjectCommand { + const command: CurrentProjectSettingsUpdateProjectCommand = { schemaVersion: projectSettingsUpdateCommandSchemaVersion, type: projectSettingsUpdateCommandType, payload: { ...values }, @@ -31,16 +50,45 @@ export function assertProjectSettingsUpdateProjectCommand( command: SerializedProjectCommand ): asserts command is ProjectSettingsUpdateProjectCommand { if ( - command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion || - command.type !== projectSettingsUpdateCommandType + command.type !== projectSettingsUpdateCommandType || + (command.schemaVersion !== + legacyProjectSettingsUpdateCommandSchemaVersion && + command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion) ) { throw new Error("Unsupported project settings update command."); } + if (!isPlainObject(command.payload)) { + throw new Error( + "Project settings update command contains invalid values." + ); + } + if (command.schemaVersion === legacyProjectSettingsUpdateCommandSchemaVersion) { + assertLegacyValues(command.payload); + return; + } if ( - !isPlainObject(command.payload) || + !isTrimmedString(command.payload.name, 1, 200) || + !isNullableTrimmedString(command.payload.internalProjectNumber, 100) || + !isNullableTrimmedString(command.payload.externalProjectNumber, 100) || + !isNullableTrimmedString(command.payload.buildingOwner, 200) || + !isNullableTrimmedString(command.payload.description, 2000) || !isPositiveFiniteNumber(command.payload.singlePhaseVoltageV) || !isPositiveFiniteNumber(command.payload.threePhaseVoltageV) || - Object.keys(command.payload).length !== 2 + Object.keys(command.payload).length !== 7 + ) { + throw new Error( + "Project settings update command contains invalid values." + ); + } +} + +function assertLegacyValues( + payload: Record +): asserts payload is Record & LegacyProjectSettingsValues { + if ( + !isPositiveFiniteNumber(payload.singlePhaseVoltageV) || + !isPositiveFiniteNumber(payload.threePhaseVoltageV) || + Object.keys(payload).length !== 2 ) { throw new Error( "Project settings update command contains invalid voltages." @@ -59,3 +107,26 @@ function isPositiveFiniteNumber(value: unknown): value is number { function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } + +function isTrimmedString( + value: unknown, + minimumLength: number, + maximumLength: number +): value is string { + return ( + typeof value === "string" && + value === value.trim() && + value.length >= minimumLength && + value.length <= maximumLength + ); +} + +function isNullableTrimmedString( + value: unknown, + maximumLength: number +): value is string | null { + return ( + value === null || + (isTrimmedString(value, 1, maximumLength)) + ); +} diff --git a/src/domain/models/project-state-snapshot.model.ts b/src/domain/models/project-state-snapshot.model.ts index 7b44ea2..cd74685 100644 --- a/src/domain/models/project-state-snapshot.model.ts +++ b/src/domain/models/project-state-snapshot.model.ts @@ -1,12 +1,13 @@ import { z } from "zod"; -export const projectStateSnapshotSchemaVersion = 1 as const; +export const legacyProjectStateSnapshotSchemaVersion = 1 as const; +export const projectStateSnapshotSchemaVersion = 2 as const; const idSchema = z.string().trim().min(1); const nullableStringSchema = z.string().nullable(); const finiteNumberSchema = z.number().finite(); -const projectSchema = z +const legacyProjectSchema = z .object({ id: idSchema, name: z.string().trim().min(1), @@ -15,6 +16,13 @@ const projectSchema = z }) .strict(); +const projectSchema = legacyProjectSchema.extend({ + internalProjectNumber: z.string().trim().min(1).max(100).nullable(), + externalProjectNumber: z.string().trim().min(1).max(100).nullable(), + buildingOwner: z.string().trim().min(1).max(200).nullable(), + description: z.string().trim().min(1).max(2000).nullable(), +}); + const distributionBoardSchema = z .object({ id: idSchema, @@ -132,17 +140,29 @@ const roomSchema = z }) .strict(); +const projectStateSnapshotContents = { + distributionBoards: z.array(distributionBoardSchema), + circuitLists: z.array(circuitListSchema), + circuitSections: z.array(circuitSectionSchema), + circuits: z.array(circuitSchema), + projectDevices: z.array(projectDeviceSchema), + floors: z.array(floorSchema), + rooms: z.array(roomSchema), +}; + +const legacyProjectStateSnapshotSchema = z + .object({ + schemaVersion: z.literal(legacyProjectStateSnapshotSchemaVersion), + project: legacyProjectSchema, + ...projectStateSnapshotContents, + }) + .strict(); + export const projectStateSnapshotSchema = z .object({ schemaVersion: z.literal(projectStateSnapshotSchemaVersion), project: projectSchema, - distributionBoards: z.array(distributionBoardSchema), - circuitLists: z.array(circuitListSchema), - circuitSections: z.array(circuitSectionSchema), - circuits: z.array(circuitSchema), - projectDevices: z.array(projectDeviceSchema), - floors: z.array(floorSchema), - rooms: z.array(roomSchema), + ...projectStateSnapshotContents, }) .strict(); @@ -153,11 +173,37 @@ export type ProjectStateSnapshot = z.infer< export function parseProjectStateSnapshot( value: unknown ): ProjectStateSnapshot { - const snapshot = projectStateSnapshotSchema.parse(value); + const version = isPlainObject(value) ? value.schemaVersion : undefined; + const snapshot = + version === legacyProjectStateSnapshotSchemaVersion + ? upgradeLegacyProjectStateSnapshot( + legacyProjectStateSnapshotSchema.parse(value) + ) + : projectStateSnapshotSchema.parse(value); assertProjectStateSnapshotRelations(snapshot); return snapshot; } +function upgradeLegacyProjectStateSnapshot( + snapshot: z.infer +): ProjectStateSnapshot { + return { + ...snapshot, + schemaVersion: projectStateSnapshotSchemaVersion, + project: { + ...snapshot.project, + internalProjectNumber: null, + externalProjectNumber: null, + buildingOwner: null, + description: null, + }, + }; +} + +function isPlainObject(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + export function serializeProjectStateSnapshot( snapshot: ProjectStateSnapshot ): string { diff --git a/src/frontend/components/project-settings-modal.tsx b/src/frontend/components/project-settings-modal.tsx new file mode 100644 index 0000000..e19d130 --- /dev/null +++ b/src/frontend/components/project-settings-modal.tsx @@ -0,0 +1,225 @@ +"use client"; + +import { FormEvent, useState } from "react"; +import type { ProjectDto } from "../types"; + +export interface ProjectSettingsInput { + name: string; + internalProjectNumber: string | null; + externalProjectNumber: string | null; + buildingOwner: string | null; + description: string | null; + singlePhaseVoltageV: number; + threePhaseVoltageV: number; +} + +interface ProjectSettingsModalProps { + isSaving: boolean; + onClose: () => void; + onSave: (input: ProjectSettingsInput) => Promise; + project: ProjectDto; +} + +export function ProjectSettingsModal({ + isSaving, + onClose, + onSave, + project, +}: ProjectSettingsModalProps) { + const [name, setName] = useState(project.name); + const [internalProjectNumber, setInternalProjectNumber] = useState( + project.internalProjectNumber ?? "" + ); + const [externalProjectNumber, setExternalProjectNumber] = useState( + project.externalProjectNumber ?? "" + ); + const [buildingOwner, setBuildingOwner] = useState( + project.buildingOwner ?? "" + ); + const [description, setDescription] = useState( + project.description ?? "" + ); + const [singlePhaseVoltageV, setSinglePhaseVoltageV] = useState( + String(project.singlePhaseVoltageV) + ); + const [threePhaseVoltageV, setThreePhaseVoltageV] = useState( + String(project.threePhaseVoltageV) + ); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + await onSave({ + name: name.trim(), + internalProjectNumber: toNullableString(internalProjectNumber), + externalProjectNumber: toNullableString(externalProjectNumber), + buildingOwner: toNullableString(buildingOwner), + description: toNullableString(description), + singlePhaseVoltageV: Number(singlePhaseVoltageV), + threePhaseVoltageV: Number(threePhaseVoltageV), + }); + } + + const isValid = + name.trim().length > 0 && + Number(singlePhaseVoltageV) > 0 && + Number(threePhaseVoltageV) > 0; + + return ( + <> +
+
+
+
+
+

+ Projekteinstellungen +

+

+ Stammdaten und elektrische Standardwerte des Projekts +

+
+
+
+
+
+ + setName(event.target.value)} + required + value={name} + /> +
+
+ + + setInternalProjectNumber(event.target.value) + } + value={internalProjectNumber} + /> +
+
+ + + setExternalProjectNumber(event.target.value) + } + value={externalProjectNumber} + /> +
+
+ + setBuildingOwner(event.target.value)} + value={buildingOwner} + /> +
+
+ +