Add versioned project settings modal
This commit is contained in:
@@ -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
|
und Historienstapel teilen dieselbe Transaktion. Create, Import aus der globalen
|
||||||
Gerätebibliothek und Delete laufen über dieselbe Command-Grenze; ihre Antworten
|
Gerätebibliothek und Delete laufen über dieselbe Command-Grenze; ihre Antworten
|
||||||
liefern Gerät und aktualisierten Historienstand an die Projektseite zurück.
|
liefern Gerät und aktualisierten Historienstand an die Projektseite zurück.
|
||||||
`project.update-settings` versioniert die beiden Projekt-Standardspannungen als
|
`project.update-settings` versioniert Projektname, interne und externe
|
||||||
eine atomare Änderung. Der Store leitet das inverse Kommando aus dem
|
Projektnummer, Bauherr, Beschreibung sowie beide Standardspannungen als eine
|
||||||
gespeicherten Projekt ab und schreibt Werte, Revision und Historienstapel
|
atomare Änderung. Die Projektseite bearbeitet diese Angaben in einem
|
||||||
gemeinsam. `PUT /api/projects/:projectId` verlangt deshalb
|
beschrifteten Einstellungsmodal statt in einer permanenten Formularkarte. Der
|
||||||
`expectedRevision`, liefert Projekt plus aktualisierten Historienstand und
|
Store leitet das inverse Kommando aus dem gespeicherten Projekt ab und schreibt
|
||||||
besitzt keinen separaten direkten Settings-Schreibweg mehr.
|
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
|
`distribution-board.insert` versioniert die Anlage einer Verteilung als einen
|
||||||
vollständigen Block aus Verteilung, Stromkreisliste und vier Standardbereichen.
|
vollständigen Block aus Verteilung, Stromkreisliste und vier Standardbereichen.
|
||||||
Alle UUIDs entstehen vor dem Command und bleiben über Undo/Redo stabil.
|
Alle UUIDs entstehen vor dem Command und bleiben über Undo/Redo stabil.
|
||||||
|
|||||||
@@ -48,12 +48,25 @@ const circuitColumns = new Set(
|
|||||||
db.prepare("PRAGMA table_info(circuits)").all().map((column) => column.name)
|
db.prepare("PRAGMA table_info(circuits)").all().map((column) => column.name)
|
||||||
);
|
);
|
||||||
const missingCircuitColumns = requiredCircuitColumns.filter((name) => !circuitColumns.has(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("Database:", dbPath);
|
||||||
console.log("Required tables:", requiredTables.join(", "));
|
console.log("Required tables:", requiredTables.join(", "));
|
||||||
console.log("Existing tables:", [...existing].join(", ") || "(none)");
|
console.log("Existing tables:", [...existing].join(", ") || "(none)");
|
||||||
console.log("Required project-device columns:", requiredProjectDeviceColumns.join(", "));
|
console.log("Required project-device columns:", requiredProjectDeviceColumns.join(", "));
|
||||||
console.log("Required circuit columns:", requiredCircuitColumns.join(", "));
|
console.log("Required circuit columns:", requiredCircuitColumns.join(", "));
|
||||||
|
console.log("Required project columns:", requiredProjectColumns.join(", "));
|
||||||
|
|
||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
console.error("Missing tables:", missing.join(", "));
|
console.error("Missing tables:", missing.join(", "));
|
||||||
@@ -78,4 +91,9 @@ if (missingCircuitColumns.length > 0) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (missingProjectColumns.length > 0) {
|
||||||
|
console.error("Missing project columns:", missingProjectColumns.join(", "));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Circuit-first schema verification passed.");
|
console.log("Circuit-first schema verification passed.");
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ import {
|
|||||||
type ProjectDeviceSyncField,
|
type ProjectDeviceSyncField,
|
||||||
} from "../../../shared/constants/project-device-sync-fields";
|
} from "../../../shared/constants/project-device-sync-fields";
|
||||||
import { ProjectVersionHistory } from "../../../frontend/components/project-version-history";
|
import { ProjectVersionHistory } from "../../../frontend/components/project-version-history";
|
||||||
|
import {
|
||||||
|
ProjectSettingsModal,
|
||||||
|
type ProjectSettingsInput,
|
||||||
|
} from "../../../frontend/components/project-settings-modal";
|
||||||
|
|
||||||
const projectDeviceSyncFieldLabels: Record<ProjectDeviceSyncField, string> = {
|
const projectDeviceSyncFieldLabels: Record<ProjectDeviceSyncField, string> = {
|
||||||
name: "Technischer Name",
|
name: "Technischer Name",
|
||||||
@@ -92,8 +96,7 @@ export default function ProjectDetailPage() {
|
|||||||
const [roomNumber, setRoomNumber] = useState("");
|
const [roomNumber, setRoomNumber] = useState("");
|
||||||
const [roomName, setRoomName] = useState("");
|
const [roomName, setRoomName] = useState("");
|
||||||
const [roomFloorId, setRoomFloorId] = useState("");
|
const [roomFloorId, setRoomFloorId] = useState("");
|
||||||
const [singlePhaseVoltageV, setSinglePhaseVoltageV] = useState("230");
|
const [isProjectSettingsOpen, setIsProjectSettingsOpen] = useState(false);
|
||||||
const [threePhaseVoltageV, setThreePhaseVoltageV] = useState("400");
|
|
||||||
const [projectDeviceForm, setProjectDeviceForm] = useState<Record<string, string>>({
|
const [projectDeviceForm, setProjectDeviceForm] = useState<Record<string, string>>({
|
||||||
name: "",
|
name: "",
|
||||||
displayName: "",
|
displayName: "",
|
||||||
@@ -142,10 +145,6 @@ export default function ProjectDetailPage() {
|
|||||||
]) => {
|
]) => {
|
||||||
const currentProject = projects.find((item) => item.id === projectId) ?? null;
|
const currentProject = projects.find((item) => item.id === projectId) ?? null;
|
||||||
setProject(currentProject);
|
setProject(currentProject);
|
||||||
if (currentProject) {
|
|
||||||
setSinglePhaseVoltageV(String(currentProject.singlePhaseVoltageV));
|
|
||||||
setThreePhaseVoltageV(String(currentProject.threePhaseVoltageV));
|
|
||||||
}
|
|
||||||
setBoards(distributionBoards);
|
setBoards(distributionBoards);
|
||||||
setCircuitLists(loadedCircuitLists);
|
setCircuitLists(loadedCircuitLists);
|
||||||
setFloors(loadedFloors);
|
setFloors(loadedFloors);
|
||||||
@@ -259,7 +258,7 @@ export default function ProjectDetailPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveProjectSettings() {
|
async function handleSaveProjectSettings(input: ProjectSettingsInput) {
|
||||||
if (!projectId || !project) {
|
if (!projectId || !project) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -269,12 +268,10 @@ export default function ProjectDetailPage() {
|
|||||||
const result = await updateProjectSettings(
|
const result = await updateProjectSettings(
|
||||||
projectId,
|
projectId,
|
||||||
project.currentRevision,
|
project.currentRevision,
|
||||||
{
|
input
|
||||||
singlePhaseVoltageV: Number(singlePhaseVoltageV),
|
|
||||||
threePhaseVoltageV: Number(threePhaseVoltageV),
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
setProject(result.project);
|
setProject(result.project);
|
||||||
|
setIsProjectSettingsOpen(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Projekteigenschaften konnten nicht gespeichert werden.");
|
setError(err instanceof Error ? err.message : "Projekteigenschaften konnten nicht gespeichert werden.");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -532,66 +529,34 @@ export default function ProjectDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="container py-4">
|
<main className="container py-4">
|
||||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
<div className="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="h3 mb-1">{project?.name ?? "Projekt"}</h1>
|
<h1 className="h3 mb-1">{project?.name ?? "Projekt"}</h1>
|
||||||
<p className="text-secondary mb-0">Verteilerübersicht und Einstieg in die Stromkreislisten</p>
|
<p className="text-secondary mb-0">
|
||||||
|
{project?.internalProjectNumber
|
||||||
|
? `Projektnummer ${project.internalProjectNumber} · `
|
||||||
|
: ""}
|
||||||
|
Verteilerübersicht und Einstieg in die Stromkreislisten
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="d-flex gap-2">
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
disabled={!project}
|
||||||
|
onClick={() => setIsProjectSettingsOpen(true)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Projekteinstellungen
|
||||||
|
</button>
|
||||||
<Link className="btn btn-outline-secondary" href="/projects">
|
<Link className="btn btn-outline-secondary" href="/projects">
|
||||||
Zur Projektseite
|
Zur Projektübersicht
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error ? <div className="alert alert-warning">{error}</div> : null}
|
{error ? <div className="alert alert-warning">{error}</div> : null}
|
||||||
|
|
||||||
<div className="row g-4">
|
<div className="row g-4">
|
||||||
<section className="col-12">
|
|
||||||
<div className="card shadow-sm">
|
|
||||||
<div className="card-header">Projekteigenschaften</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<div className="row g-3 align-items-end">
|
|
||||||
<div className="col-12 col-md-4">
|
|
||||||
<label className="form-label">Standardspannung 1-phasig [V]</label>
|
|
||||||
<input
|
|
||||||
className="form-control"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
value={singlePhaseVoltageV}
|
|
||||||
onChange={(event) => setSinglePhaseVoltageV(event.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-4">
|
|
||||||
<label className="form-label">Standardspannung 3-phasig [V]</label>
|
|
||||||
<input
|
|
||||||
className="form-control"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
value={threePhaseVoltageV}
|
|
||||||
onChange={(event) => setThreePhaseVoltageV(event.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-4">
|
|
||||||
<button
|
|
||||||
className="btn btn-primary w-100"
|
|
||||||
type="button"
|
|
||||||
onClick={handleSaveProjectSettings}
|
|
||||||
disabled={
|
|
||||||
isSaving ||
|
|
||||||
!project ||
|
|
||||||
(Number(singlePhaseVoltageV) ===
|
|
||||||
project.singlePhaseVoltageV &&
|
|
||||||
Number(threePhaseVoltageV) ===
|
|
||||||
project.threePhaseVoltageV)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Projekteigenschaften speichern
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{project ? (
|
{project ? (
|
||||||
<section className="col-12">
|
<section className="col-12">
|
||||||
<ProjectVersionHistory
|
<ProjectVersionHistory
|
||||||
@@ -1156,6 +1121,14 @@ export default function ProjectDetailPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{project && isProjectSettingsOpen ? (
|
||||||
|
<ProjectSettingsModal
|
||||||
|
isSaving={isSaving}
|
||||||
|
onClose={() => setIsProjectSettingsOpen(false)}
|
||||||
|
onSave={handleSaveProjectSettings}
|
||||||
|
project={project}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -113,6 +113,13 @@
|
|||||||
"when": 1785014383032,
|
"when": 1785014383032,
|
||||||
"tag": "0015_modern_madame_masque",
|
"tag": "0015_modern_madame_masque",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1785254079435,
|
||||||
|
"tag": "0016_dashing_darkstar",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,8 @@ import { eq } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
assertProjectSettingsUpdateProjectCommand,
|
assertProjectSettingsUpdateProjectCommand,
|
||||||
createProjectSettingsUpdateProjectCommand,
|
createProjectSettingsUpdateProjectCommand,
|
||||||
|
legacyProjectSettingsUpdateCommandSchemaVersion,
|
||||||
|
type ProjectSettingsValues,
|
||||||
} from "../../domain/models/project-settings-project-command.model.js";
|
} from "../../domain/models/project-settings-project-command.model.js";
|
||||||
import type {
|
import type {
|
||||||
ExecuteProjectSettingsUpdateCommandInput,
|
ExecuteProjectSettingsUpdateCommandInput,
|
||||||
@@ -37,22 +39,45 @@ export class ProjectSettingsProjectCommandRepository
|
|||||||
if (!current) {
|
if (!current) {
|
||||||
throw new Error("Project not found.");
|
throw new Error("Project not found.");
|
||||||
}
|
}
|
||||||
if (
|
const target: ProjectSettingsValues =
|
||||||
current.singlePhaseVoltageV ===
|
input.command.schemaVersion ===
|
||||||
input.command.payload.singlePhaseVoltageV &&
|
legacyProjectSettingsUpdateCommandSchemaVersion
|
||||||
current.threePhaseVoltageV ===
|
? {
|
||||||
input.command.payload.threePhaseVoltageV
|
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.");
|
throw new Error("Project settings did not change.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const inverse = createProjectSettingsUpdateProjectCommand({
|
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,
|
singlePhaseVoltageV: current.singlePhaseVoltageV,
|
||||||
threePhaseVoltageV: current.threePhaseVoltageV,
|
threePhaseVoltageV: current.threePhaseVoltageV,
|
||||||
});
|
});
|
||||||
const updated = tx
|
const updated = tx
|
||||||
.update(projects)
|
.update(projects)
|
||||||
.set(input.command.payload)
|
.set(target)
|
||||||
.where(eq(projects.id, input.projectId))
|
.where(eq(projects.id, input.projectId))
|
||||||
.run();
|
.run();
|
||||||
if (updated.changes !== 1) {
|
if (updated.changes !== 1) {
|
||||||
@@ -62,3 +87,18 @@ export class ProjectSettingsProjectCommandRepository
|
|||||||
return inverse;
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ProjectSnapshotNameConflictError } from "../../domain/errors/project-sn
|
|||||||
import { createProjectStateRestoreCommand } from "../../domain/models/project-state-restore-command.model.js";
|
import { createProjectStateRestoreCommand } from "../../domain/models/project-state-restore-command.model.js";
|
||||||
import {
|
import {
|
||||||
deserializeProjectStateSnapshot,
|
deserializeProjectStateSnapshot,
|
||||||
|
legacyProjectStateSnapshotSchemaVersion,
|
||||||
projectStateSnapshotSchemaVersion,
|
projectStateSnapshotSchemaVersion,
|
||||||
} from "../../domain/models/project-state-snapshot.model.js";
|
} from "../../domain/models/project-state-snapshot.model.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -138,7 +139,10 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
|
|||||||
if (actualChecksum !== stored.payloadSha256) {
|
if (actualChecksum !== stored.payloadSha256) {
|
||||||
throw new Error("Project snapshot checksum verification failed.");
|
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.");
|
throw new Error("Project snapshot schema version is not supported.");
|
||||||
}
|
}
|
||||||
const targetState = deserializeProjectStateSnapshot(stored.payloadJson);
|
const targetState = deserializeProjectStateSnapshot(stored.payloadJson);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
createProjectStateRestoreCommand,
|
createProjectStateRestoreCommand,
|
||||||
} from "../../domain/models/project-state-restore-command.model.js";
|
} from "../../domain/models/project-state-restore-command.model.js";
|
||||||
import {
|
import {
|
||||||
|
parseProjectStateSnapshot,
|
||||||
serializeProjectStateSnapshot,
|
serializeProjectStateSnapshot,
|
||||||
type ProjectStateSnapshot,
|
type ProjectStateSnapshot,
|
||||||
} from "../../domain/models/project-state-snapshot.model.js";
|
} from "../../domain/models/project-state-snapshot.model.js";
|
||||||
@@ -38,7 +39,10 @@ export class ProjectStateRestoreCommandRepository
|
|||||||
|
|
||||||
execute(input: ExecuteProjectStateRestoreCommandInput) {
|
execute(input: ExecuteProjectStateRestoreCommandInput) {
|
||||||
assertProjectStateRestoreCommand(input.command);
|
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.");
|
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
|
input.command.payload.targetState
|
||||||
);
|
);
|
||||||
|
const targetPayloadJson = serializeProjectStateSnapshot(targetState);
|
||||||
const inverse = createProjectStateRestoreCommand(
|
const inverse = createProjectStateRestoreCommand(
|
||||||
hashProjectStatePayload(targetPayloadJson),
|
hashProjectStatePayload(targetPayloadJson),
|
||||||
current.state
|
current.state
|
||||||
);
|
);
|
||||||
|
|
||||||
replaceProjectState(tx, input.command.payload.targetState);
|
replaceProjectState(tx, targetState);
|
||||||
|
|
||||||
return inverse;
|
return inverse;
|
||||||
}
|
}
|
||||||
@@ -111,6 +116,10 @@ function replaceProjectState(
|
|||||||
.update(projects)
|
.update(projects)
|
||||||
.set({
|
.set({
|
||||||
name: state.project.name,
|
name: state.project.name,
|
||||||
|
internalProjectNumber: state.project.internalProjectNumber,
|
||||||
|
externalProjectNumber: state.project.externalProjectNumber,
|
||||||
|
buildingOwner: state.project.buildingOwner,
|
||||||
|
description: state.project.description,
|
||||||
singlePhaseVoltageV: state.project.singlePhaseVoltageV,
|
singlePhaseVoltageV: state.project.singlePhaseVoltageV,
|
||||||
threePhaseVoltageV: state.project.threePhaseVoltageV,
|
threePhaseVoltageV: state.project.threePhaseVoltageV,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ export function readProjectStateSnapshot(
|
|||||||
.select({
|
.select({
|
||||||
id: projects.id,
|
id: projects.id,
|
||||||
name: projects.name,
|
name: projects.name,
|
||||||
|
internalProjectNumber: projects.internalProjectNumber,
|
||||||
|
externalProjectNumber: projects.externalProjectNumber,
|
||||||
|
buildingOwner: projects.buildingOwner,
|
||||||
|
description: projects.description,
|
||||||
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
||||||
threePhaseVoltageV: projects.threePhaseVoltageV,
|
threePhaseVoltageV: projects.threePhaseVoltageV,
|
||||||
currentRevision: projects.currentRevision,
|
currentRevision: projects.currentRevision,
|
||||||
@@ -172,6 +176,10 @@ export function readProjectStateSnapshot(
|
|||||||
project: {
|
project: {
|
||||||
id: project.id,
|
id: project.id,
|
||||||
name: project.name,
|
name: project.name,
|
||||||
|
internalProjectNumber: project.internalProjectNumber,
|
||||||
|
externalProjectNumber: project.externalProjectNumber,
|
||||||
|
buildingOwner: project.buildingOwner,
|
||||||
|
description: project.description,
|
||||||
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||||
threePhaseVoltageV: project.threePhaseVoltageV,
|
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ export class ProjectRepository {
|
|||||||
const project = {
|
const project = {
|
||||||
id,
|
id,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
|
internalProjectNumber: input.internalProjectNumber ?? null,
|
||||||
|
externalProjectNumber: input.externalProjectNumber ?? null,
|
||||||
|
buildingOwner: input.buildingOwner ?? null,
|
||||||
|
description: input.description ?? null,
|
||||||
singlePhaseVoltageV: input.singlePhaseVoltageV ?? 230,
|
singlePhaseVoltageV: input.singlePhaseVoltageV ?? 230,
|
||||||
threePhaseVoltageV: input.threePhaseVoltageV ?? 400,
|
threePhaseVoltageV: input.threePhaseVoltageV ?? 400,
|
||||||
currentRevision: 0,
|
currentRevision: 0,
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|||||||
export const projects = sqliteTable("projects", {
|
export const projects = sqliteTable("projects", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
name: text("name").notNull(),
|
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),
|
singlePhaseVoltageV: integer("single_phase_voltage_v").notNull().default(230),
|
||||||
threePhaseVoltageV: integer("three_phase_voltage_v").notNull().default(400),
|
threePhaseVoltageV: integer("three_phase_voltage_v").notNull().default(400),
|
||||||
currentRevision: integer("current_revision").notNull().default(0),
|
currentRevision: integer("current_revision").notNull().default(0),
|
||||||
|
|||||||
@@ -2,23 +2,42 @@ import type { SerializedProjectCommand } from "./project-command.model.js";
|
|||||||
|
|
||||||
export const projectSettingsUpdateCommandType =
|
export const projectSettingsUpdateCommandType =
|
||||||
"project.update-settings" as const;
|
"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;
|
singlePhaseVoltageV: number;
|
||||||
threePhaseVoltageV: 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<LegacyProjectSettingsValues> {
|
||||||
|
schemaVersion: typeof legacyProjectSettingsUpdateCommandSchemaVersion;
|
||||||
|
type: typeof projectSettingsUpdateCommandType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurrentProjectSettingsUpdateProjectCommand
|
||||||
extends SerializedProjectCommand<ProjectSettingsValues> {
|
extends SerializedProjectCommand<ProjectSettingsValues> {
|
||||||
schemaVersion: typeof projectSettingsUpdateCommandSchemaVersion;
|
schemaVersion: typeof projectSettingsUpdateCommandSchemaVersion;
|
||||||
type: typeof projectSettingsUpdateCommandType;
|
type: typeof projectSettingsUpdateCommandType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProjectSettingsUpdateProjectCommand =
|
||||||
|
| LegacyProjectSettingsUpdateProjectCommand
|
||||||
|
| CurrentProjectSettingsUpdateProjectCommand;
|
||||||
|
|
||||||
export function createProjectSettingsUpdateProjectCommand(
|
export function createProjectSettingsUpdateProjectCommand(
|
||||||
values: ProjectSettingsValues
|
values: ProjectSettingsValues
|
||||||
): ProjectSettingsUpdateProjectCommand {
|
): CurrentProjectSettingsUpdateProjectCommand {
|
||||||
const command: ProjectSettingsUpdateProjectCommand = {
|
const command: CurrentProjectSettingsUpdateProjectCommand = {
|
||||||
schemaVersion: projectSettingsUpdateCommandSchemaVersion,
|
schemaVersion: projectSettingsUpdateCommandSchemaVersion,
|
||||||
type: projectSettingsUpdateCommandType,
|
type: projectSettingsUpdateCommandType,
|
||||||
payload: { ...values },
|
payload: { ...values },
|
||||||
@@ -31,16 +50,45 @@ export function assertProjectSettingsUpdateProjectCommand(
|
|||||||
command: SerializedProjectCommand<unknown>
|
command: SerializedProjectCommand<unknown>
|
||||||
): asserts command is ProjectSettingsUpdateProjectCommand {
|
): asserts command is ProjectSettingsUpdateProjectCommand {
|
||||||
if (
|
if (
|
||||||
command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion ||
|
command.type !== projectSettingsUpdateCommandType ||
|
||||||
command.type !== projectSettingsUpdateCommandType
|
(command.schemaVersion !==
|
||||||
|
legacyProjectSettingsUpdateCommandSchemaVersion &&
|
||||||
|
command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion)
|
||||||
) {
|
) {
|
||||||
throw new Error("Unsupported project settings update command.");
|
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 (
|
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.singlePhaseVoltageV) ||
|
||||||
!isPositiveFiniteNumber(command.payload.threePhaseVoltageV) ||
|
!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<string, unknown>
|
||||||
|
): asserts payload is Record<string, unknown> & LegacyProjectSettingsValues {
|
||||||
|
if (
|
||||||
|
!isPositiveFiniteNumber(payload.singlePhaseVoltageV) ||
|
||||||
|
!isPositiveFiniteNumber(payload.threePhaseVoltageV) ||
|
||||||
|
Object.keys(payload).length !== 2
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Project settings update command contains invalid voltages."
|
"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<string, unknown> {
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
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))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { z } from "zod";
|
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 idSchema = z.string().trim().min(1);
|
||||||
const nullableStringSchema = z.string().nullable();
|
const nullableStringSchema = z.string().nullable();
|
||||||
const finiteNumberSchema = z.number().finite();
|
const finiteNumberSchema = z.number().finite();
|
||||||
|
|
||||||
const projectSchema = z
|
const legacyProjectSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: idSchema,
|
id: idSchema,
|
||||||
name: z.string().trim().min(1),
|
name: z.string().trim().min(1),
|
||||||
@@ -15,6 +16,13 @@ const projectSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.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
|
const distributionBoardSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: idSchema,
|
id: idSchema,
|
||||||
@@ -132,10 +140,7 @@ const roomSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export const projectStateSnapshotSchema = z
|
const projectStateSnapshotContents = {
|
||||||
.object({
|
|
||||||
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
|
|
||||||
project: projectSchema,
|
|
||||||
distributionBoards: z.array(distributionBoardSchema),
|
distributionBoards: z.array(distributionBoardSchema),
|
||||||
circuitLists: z.array(circuitListSchema),
|
circuitLists: z.array(circuitListSchema),
|
||||||
circuitSections: z.array(circuitSectionSchema),
|
circuitSections: z.array(circuitSectionSchema),
|
||||||
@@ -143,6 +148,21 @@ export const projectStateSnapshotSchema = z
|
|||||||
projectDevices: z.array(projectDeviceSchema),
|
projectDevices: z.array(projectDeviceSchema),
|
||||||
floors: z.array(floorSchema),
|
floors: z.array(floorSchema),
|
||||||
rooms: z.array(roomSchema),
|
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,
|
||||||
|
...projectStateSnapshotContents,
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
@@ -153,11 +173,37 @@ export type ProjectStateSnapshot = z.infer<
|
|||||||
export function parseProjectStateSnapshot(
|
export function parseProjectStateSnapshot(
|
||||||
value: unknown
|
value: unknown
|
||||||
): ProjectStateSnapshot {
|
): 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);
|
assertProjectStateSnapshotRelations(snapshot);
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function upgradeLegacyProjectStateSnapshot(
|
||||||
|
snapshot: z.infer<typeof legacyProjectStateSnapshotSchema>
|
||||||
|
): ProjectStateSnapshot {
|
||||||
|
return {
|
||||||
|
...snapshot,
|
||||||
|
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||||
|
project: {
|
||||||
|
...snapshot.project,
|
||||||
|
internalProjectNumber: null,
|
||||||
|
externalProjectNumber: null,
|
||||||
|
buildingOwner: null,
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
export function serializeProjectStateSnapshot(
|
export function serializeProjectStateSnapshot(
|
||||||
snapshot: ProjectStateSnapshot
|
snapshot: ProjectStateSnapshot
|
||||||
): string {
|
): string {
|
||||||
|
|||||||
@@ -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<void>;
|
||||||
|
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<HTMLFormElement>) {
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
aria-labelledby="project-settings-title"
|
||||||
|
aria-modal="true"
|
||||||
|
className="modal fade show d-block"
|
||||||
|
role="dialog"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<div className="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<form className="modal-content" onSubmit={handleSubmit}>
|
||||||
|
<div className="modal-header">
|
||||||
|
<div>
|
||||||
|
<h2 className="modal-title fs-5" id="project-settings-title">
|
||||||
|
Projekteinstellungen
|
||||||
|
</h2>
|
||||||
|
<p className="text-secondary small mb-0">
|
||||||
|
Stammdaten und elektrische Standardwerte des Projekts
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
aria-label="Schließen"
|
||||||
|
className="btn-close"
|
||||||
|
disabled={isSaving}
|
||||||
|
onClick={onClose}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="row g-3">
|
||||||
|
<div className="col-12">
|
||||||
|
<label className="form-label" htmlFor="project-name">
|
||||||
|
Projektname
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
className="form-control"
|
||||||
|
id="project-name"
|
||||||
|
maxLength={200}
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
required
|
||||||
|
value={name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<label className="form-label" htmlFor="internal-project-number">
|
||||||
|
Projektnummer intern
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
id="internal-project-number"
|
||||||
|
maxLength={100}
|
||||||
|
onChange={(event) =>
|
||||||
|
setInternalProjectNumber(event.target.value)
|
||||||
|
}
|
||||||
|
value={internalProjectNumber}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<label className="form-label" htmlFor="external-project-number">
|
||||||
|
Projektnummer extern
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
id="external-project-number"
|
||||||
|
maxLength={100}
|
||||||
|
onChange={(event) =>
|
||||||
|
setExternalProjectNumber(event.target.value)
|
||||||
|
}
|
||||||
|
value={externalProjectNumber}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12">
|
||||||
|
<label className="form-label" htmlFor="building-owner">
|
||||||
|
Bauherr
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
id="building-owner"
|
||||||
|
maxLength={200}
|
||||||
|
onChange={(event) => setBuildingOwner(event.target.value)}
|
||||||
|
value={buildingOwner}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12">
|
||||||
|
<label className="form-label" htmlFor="project-description">
|
||||||
|
Beschreibung
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className="form-control"
|
||||||
|
id="project-description"
|
||||||
|
maxLength={2000}
|
||||||
|
onChange={(event) => setDescription(event.target.value)}
|
||||||
|
rows={4}
|
||||||
|
value={description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<label className="form-label" htmlFor="single-phase-voltage">
|
||||||
|
Standardspannung 1-phasig [V]
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
id="single-phase-voltage"
|
||||||
|
min="1"
|
||||||
|
onChange={(event) =>
|
||||||
|
setSinglePhaseVoltageV(event.target.value)
|
||||||
|
}
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
value={singlePhaseVoltageV}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<label className="form-label" htmlFor="three-phase-voltage">
|
||||||
|
Standardspannung 3-phasig [V]
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
id="three-phase-voltage"
|
||||||
|
min="1"
|
||||||
|
onChange={(event) =>
|
||||||
|
setThreePhaseVoltageV(event.target.value)
|
||||||
|
}
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
value={threePhaseVoltageV}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
disabled={isSaving}
|
||||||
|
onClick={onClose}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={isSaving || !isValid}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{isSaving ? "Wird gespeichert …" : "Einstellungen speichern"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="modal-backdrop fade show" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNullableString(value: string) {
|
||||||
|
return value.trim() || null;
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
export interface ProjectDto {
|
export interface ProjectDto {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
internalProjectNumber: string | null;
|
||||||
|
externalProjectNumber: string | null;
|
||||||
|
buildingOwner: string | null;
|
||||||
|
description: string | null;
|
||||||
singlePhaseVoltageV: number;
|
singlePhaseVoltageV: number;
|
||||||
threePhaseVoltageV: number;
|
threePhaseVoltageV: number;
|
||||||
currentRevision: number;
|
currentRevision: number;
|
||||||
|
|||||||
@@ -210,7 +210,15 @@ export function createProject(name: string) {
|
|||||||
export function updateProjectSettings(
|
export function updateProjectSettings(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
expectedRevision: number,
|
expectedRevision: number,
|
||||||
input: { singlePhaseVoltageV: number; threePhaseVoltageV: number }
|
input: {
|
||||||
|
name: string;
|
||||||
|
internalProjectNumber: string | null;
|
||||||
|
externalProjectNumber: string | null;
|
||||||
|
buildingOwner: string | null;
|
||||||
|
description: string | null;
|
||||||
|
singlePhaseVoltageV: number;
|
||||||
|
threePhaseVoltageV: number;
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
return request<ProjectSettingsCommandResultDto>(
|
return request<ProjectSettingsCommandResultDto>(
|
||||||
`/api/projects/${projectId}`,
|
`/api/projects/${projectId}`,
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { z } from "zod";
|
|||||||
import { expectedProjectRevisionSchema } from "./project-command.schemas.js";
|
import { expectedProjectRevisionSchema } from "./project-command.schemas.js";
|
||||||
|
|
||||||
export const createProjectSchema = z.object({
|
export const createProjectSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().trim().min(1).max(200),
|
||||||
|
internalProjectNumber: z.string().trim().max(100).optional(),
|
||||||
|
externalProjectNumber: z.string().trim().max(100).optional(),
|
||||||
|
buildingOwner: z.string().trim().max(200).optional(),
|
||||||
|
description: z.string().trim().max(2000).optional(),
|
||||||
singlePhaseVoltageV: z.number().positive().optional(),
|
singlePhaseVoltageV: z.number().positive().optional(),
|
||||||
threePhaseVoltageV: z.number().positive().optional(),
|
threePhaseVoltageV: z.number().positive().optional(),
|
||||||
});
|
});
|
||||||
@@ -10,6 +14,11 @@ export const createProjectSchema = z.object({
|
|||||||
export const updateProjectSettingsSchema = z
|
export const updateProjectSettingsSchema = z
|
||||||
.object({
|
.object({
|
||||||
expectedRevision: expectedProjectRevisionSchema,
|
expectedRevision: expectedProjectRevisionSchema,
|
||||||
|
name: z.string().trim().min(1).max(200),
|
||||||
|
internalProjectNumber: z.string().trim().max(100).nullable(),
|
||||||
|
externalProjectNumber: z.string().trim().max(100).nullable(),
|
||||||
|
buildingOwner: z.string().trim().max(200).nullable(),
|
||||||
|
description: z.string().trim().max(2000).nullable(),
|
||||||
singlePhaseVoltageV: z.number().positive(),
|
singlePhaseVoltageV: z.number().positive(),
|
||||||
threePhaseVoltageV: z.number().positive(),
|
threePhaseVoltageV: z.number().positive(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -951,6 +951,11 @@ describe("project command service", () => {
|
|||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
expectedRevision: 0,
|
expectedRevision: 0,
|
||||||
command: createProjectSettingsUpdateProjectCommand({
|
command: createProjectSettingsUpdateProjectCommand({
|
||||||
|
name: "Test project",
|
||||||
|
internalProjectNumber: null,
|
||||||
|
externalProjectNumber: null,
|
||||||
|
buildingOwner: null,
|
||||||
|
description: null,
|
||||||
singlePhaseVoltageV: 240,
|
singlePhaseVoltageV: 240,
|
||||||
threePhaseVoltageV: 415,
|
threePhaseVoltageV: 415,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
.values({
|
.values({
|
||||||
id: "project-1",
|
id: "project-1",
|
||||||
name: "Testprojekt",
|
name: "Testprojekt",
|
||||||
|
internalProjectNumber: "INT-1",
|
||||||
singlePhaseVoltageV: 230,
|
singlePhaseVoltageV: 230,
|
||||||
threePhaseVoltageV: 400,
|
threePhaseVoltageV: 400,
|
||||||
})
|
})
|
||||||
@@ -35,6 +36,11 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
function getSettings(context: DatabaseContext) {
|
function getSettings(context: DatabaseContext) {
|
||||||
return context.db
|
return context.db
|
||||||
.select({
|
.select({
|
||||||
|
name: projects.name,
|
||||||
|
internalProjectNumber: projects.internalProjectNumber,
|
||||||
|
externalProjectNumber: projects.externalProjectNumber,
|
||||||
|
buildingOwner: projects.buildingOwner,
|
||||||
|
description: projects.description,
|
||||||
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
||||||
threePhaseVoltageV: projects.threePhaseVoltageV,
|
threePhaseVoltageV: projects.threePhaseVoltageV,
|
||||||
currentRevision: projects.currentRevision,
|
currentRevision: projects.currentRevision,
|
||||||
@@ -44,15 +50,32 @@ function getSettings(context: DatabaseContext) {
|
|||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function settings(
|
||||||
|
overrides: Partial<Parameters<
|
||||||
|
typeof createProjectSettingsUpdateProjectCommand
|
||||||
|
>[0]> = {}
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
name: "Testprojekt",
|
||||||
|
internalProjectNumber: "INT-1",
|
||||||
|
externalProjectNumber: null,
|
||||||
|
buildingOwner: null,
|
||||||
|
description: null,
|
||||||
|
singlePhaseVoltageV: 230,
|
||||||
|
threePhaseVoltageV: 400,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe("project settings project-command repository", () => {
|
describe("project settings project-command repository", () => {
|
||||||
it("validates settings commands and sends the expected revision from the frontend", async () => {
|
it("validates settings commands and sends the expected revision from the frontend", async () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() =>
|
() =>
|
||||||
createProjectSettingsUpdateProjectCommand({
|
createProjectSettingsUpdateProjectCommand({
|
||||||
|
...settings(),
|
||||||
singlePhaseVoltageV: 0,
|
singlePhaseVoltageV: 0,
|
||||||
threePhaseVoltageV: 400,
|
|
||||||
}),
|
}),
|
||||||
/invalid voltages/
|
/invalid values/
|
||||||
);
|
);
|
||||||
const requests: Array<{ url: string; body: unknown }> = [];
|
const requests: Array<{ url: string; body: unknown }> = [];
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
@@ -68,6 +91,7 @@ describe("project settings project-command repository", () => {
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await updateProjectSettings("project-1", 7, {
|
await updateProjectSettings("project-1", 7, {
|
||||||
|
...settings(),
|
||||||
singlePhaseVoltageV: 240,
|
singlePhaseVoltageV: 240,
|
||||||
threePhaseVoltageV: 415,
|
threePhaseVoltageV: 415,
|
||||||
});
|
});
|
||||||
@@ -79,6 +103,11 @@ describe("project settings project-command repository", () => {
|
|||||||
url: "/api/projects/project-1",
|
url: "/api/projects/project-1",
|
||||||
body: {
|
body: {
|
||||||
expectedRevision: 7,
|
expectedRevision: 7,
|
||||||
|
name: "Testprojekt",
|
||||||
|
internalProjectNumber: "INT-1",
|
||||||
|
externalProjectNumber: null,
|
||||||
|
buildingOwner: null,
|
||||||
|
description: null,
|
||||||
singlePhaseVoltageV: 240,
|
singlePhaseVoltageV: 240,
|
||||||
threePhaseVoltageV: 415,
|
threePhaseVoltageV: 415,
|
||||||
},
|
},
|
||||||
@@ -94,6 +123,9 @@ describe("project settings project-command repository", () => {
|
|||||||
);
|
);
|
||||||
const history = new ProjectHistoryRepository(context.db);
|
const history = new ProjectHistoryRepository(context.db);
|
||||||
const command = createProjectSettingsUpdateProjectCommand({
|
const command = createProjectSettingsUpdateProjectCommand({
|
||||||
|
...settings(),
|
||||||
|
name: "Neuer Projektname",
|
||||||
|
buildingOwner: "Beispiel Bau GmbH",
|
||||||
singlePhaseVoltageV: 240,
|
singlePhaseVoltageV: 240,
|
||||||
threePhaseVoltageV: 415,
|
threePhaseVoltageV: 415,
|
||||||
});
|
});
|
||||||
@@ -104,11 +136,21 @@ describe("project settings project-command repository", () => {
|
|||||||
command,
|
command,
|
||||||
});
|
});
|
||||||
assert.deepEqual(getSettings(context), {
|
assert.deepEqual(getSettings(context), {
|
||||||
|
name: "Neuer Projektname",
|
||||||
|
internalProjectNumber: "INT-1",
|
||||||
|
externalProjectNumber: null,
|
||||||
|
buildingOwner: "Beispiel Bau GmbH",
|
||||||
|
description: null,
|
||||||
singlePhaseVoltageV: 240,
|
singlePhaseVoltageV: 240,
|
||||||
threePhaseVoltageV: 415,
|
threePhaseVoltageV: 415,
|
||||||
currentRevision: 1,
|
currentRevision: 1,
|
||||||
});
|
});
|
||||||
assert.deepEqual(forward.inverse.payload, {
|
assert.deepEqual(forward.inverse.payload, {
|
||||||
|
name: "Testprojekt",
|
||||||
|
internalProjectNumber: "INT-1",
|
||||||
|
externalProjectNumber: null,
|
||||||
|
buildingOwner: null,
|
||||||
|
description: null,
|
||||||
singlePhaseVoltageV: 230,
|
singlePhaseVoltageV: 230,
|
||||||
threePhaseVoltageV: 400,
|
threePhaseVoltageV: 400,
|
||||||
});
|
});
|
||||||
@@ -123,6 +165,11 @@ describe("project settings project-command repository", () => {
|
|||||||
command: forward.inverse,
|
command: forward.inverse,
|
||||||
});
|
});
|
||||||
assert.deepEqual(getSettings(context), {
|
assert.deepEqual(getSettings(context), {
|
||||||
|
name: "Testprojekt",
|
||||||
|
internalProjectNumber: "INT-1",
|
||||||
|
externalProjectNumber: null,
|
||||||
|
buildingOwner: null,
|
||||||
|
description: null,
|
||||||
singlePhaseVoltageV: 230,
|
singlePhaseVoltageV: 230,
|
||||||
threePhaseVoltageV: 400,
|
threePhaseVoltageV: 400,
|
||||||
currentRevision: 2,
|
currentRevision: 2,
|
||||||
@@ -138,6 +185,11 @@ describe("project settings project-command repository", () => {
|
|||||||
command,
|
command,
|
||||||
});
|
});
|
||||||
assert.deepEqual(getSettings(context), {
|
assert.deepEqual(getSettings(context), {
|
||||||
|
name: "Neuer Projektname",
|
||||||
|
internalProjectNumber: "INT-1",
|
||||||
|
externalProjectNumber: null,
|
||||||
|
buildingOwner: "Beispiel Bau GmbH",
|
||||||
|
description: null,
|
||||||
singlePhaseVoltageV: 240,
|
singlePhaseVoltageV: 240,
|
||||||
threePhaseVoltageV: 415,
|
threePhaseVoltageV: 415,
|
||||||
currentRevision: 3,
|
currentRevision: 3,
|
||||||
@@ -147,6 +199,37 @@ describe("project settings project-command repository", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("applies persisted version-one voltage commands without clearing metadata", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const repository = new ProjectSettingsProjectCommandRepository(
|
||||||
|
context.db
|
||||||
|
);
|
||||||
|
repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
type: "project.update-settings",
|
||||||
|
payload: {
|
||||||
|
singlePhaseVoltageV: 240,
|
||||||
|
threePhaseVoltageV: 415,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.deepEqual(getSettings(context), {
|
||||||
|
...settings({
|
||||||
|
singlePhaseVoltageV: 240,
|
||||||
|
threePhaseVoltageV: 415,
|
||||||
|
}),
|
||||||
|
currentRevision: 1,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects no-op, invalid project and stale revision without partial writes", () => {
|
it("rejects no-op, invalid project and stale revision without partial writes", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
@@ -160,8 +243,7 @@ describe("project settings project-command repository", () => {
|
|||||||
expectedRevision: 0,
|
expectedRevision: 0,
|
||||||
source: "user",
|
source: "user",
|
||||||
command: createProjectSettingsUpdateProjectCommand({
|
command: createProjectSettingsUpdateProjectCommand({
|
||||||
singlePhaseVoltageV: 230,
|
...settings(),
|
||||||
threePhaseVoltageV: 400,
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
/did not change/
|
/did not change/
|
||||||
@@ -173,6 +255,7 @@ describe("project settings project-command repository", () => {
|
|||||||
expectedRevision: 0,
|
expectedRevision: 0,
|
||||||
source: "user",
|
source: "user",
|
||||||
command: createProjectSettingsUpdateProjectCommand({
|
command: createProjectSettingsUpdateProjectCommand({
|
||||||
|
...settings(),
|
||||||
singlePhaseVoltageV: 240,
|
singlePhaseVoltageV: 240,
|
||||||
threePhaseVoltageV: 415,
|
threePhaseVoltageV: 415,
|
||||||
}),
|
}),
|
||||||
@@ -186,6 +269,7 @@ describe("project settings project-command repository", () => {
|
|||||||
expectedRevision: 1,
|
expectedRevision: 1,
|
||||||
source: "user",
|
source: "user",
|
||||||
command: createProjectSettingsUpdateProjectCommand({
|
command: createProjectSettingsUpdateProjectCommand({
|
||||||
|
...settings(),
|
||||||
singlePhaseVoltageV: 240,
|
singlePhaseVoltageV: 240,
|
||||||
threePhaseVoltageV: 415,
|
threePhaseVoltageV: 415,
|
||||||
}),
|
}),
|
||||||
@@ -193,6 +277,11 @@ describe("project settings project-command repository", () => {
|
|||||||
ProjectRevisionConflictError
|
ProjectRevisionConflictError
|
||||||
);
|
);
|
||||||
assert.deepEqual(getSettings(context), {
|
assert.deepEqual(getSettings(context), {
|
||||||
|
name: "Testprojekt",
|
||||||
|
internalProjectNumber: "INT-1",
|
||||||
|
externalProjectNumber: null,
|
||||||
|
buildingOwner: null,
|
||||||
|
description: null,
|
||||||
singlePhaseVoltageV: 230,
|
singlePhaseVoltageV: 230,
|
||||||
threePhaseVoltageV: 400,
|
threePhaseVoltageV: 400,
|
||||||
currentRevision: 0,
|
currentRevision: 0,
|
||||||
@@ -226,6 +315,7 @@ describe("project settings project-command repository", () => {
|
|||||||
expectedRevision: 0,
|
expectedRevision: 0,
|
||||||
source: "user",
|
source: "user",
|
||||||
command: createProjectSettingsUpdateProjectCommand({
|
command: createProjectSettingsUpdateProjectCommand({
|
||||||
|
...settings(),
|
||||||
singlePhaseVoltageV: 240,
|
singlePhaseVoltageV: 240,
|
||||||
threePhaseVoltageV: 415,
|
threePhaseVoltageV: 415,
|
||||||
}),
|
}),
|
||||||
@@ -233,6 +323,11 @@ describe("project settings project-command repository", () => {
|
|||||||
/forced project settings history failure/
|
/forced project settings history failure/
|
||||||
);
|
);
|
||||||
assert.deepEqual(getSettings(context), {
|
assert.deepEqual(getSettings(context), {
|
||||||
|
name: "Testprojekt",
|
||||||
|
internalProjectNumber: "INT-1",
|
||||||
|
externalProjectNumber: null,
|
||||||
|
buildingOwner: null,
|
||||||
|
description: null,
|
||||||
singlePhaseVoltageV: 230,
|
singlePhaseVoltageV: 230,
|
||||||
threePhaseVoltageV: 400,
|
threePhaseVoltageV: 400,
|
||||||
currentRevision: 0,
|
currentRevision: 0,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { projectSnapshots } from "../src/db/schema/project-snapshots.js";
|
|||||||
import { projects } from "../src/db/schema/projects.js";
|
import { projects } from "../src/db/schema/projects.js";
|
||||||
import { rooms } from "../src/db/schema/rooms.js";
|
import { rooms } from "../src/db/schema/rooms.js";
|
||||||
import { deserializeProjectStateSnapshot } from "../src/domain/models/project-state-snapshot.model.js";
|
import { deserializeProjectStateSnapshot } from "../src/domain/models/project-state-snapshot.model.js";
|
||||||
|
import { projectStateSnapshotSchemaVersion } from "../src/domain/models/project-state-snapshot.model.js";
|
||||||
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
||||||
import { ProjectSnapshotNameConflictError } from "../src/domain/errors/project-snapshot-name-conflict.error.js";
|
import { ProjectSnapshotNameConflictError } from "../src/domain/errors/project-snapshot-name-conflict.error.js";
|
||||||
|
|
||||||
@@ -133,7 +134,7 @@ describe("project snapshot repository", () => {
|
|||||||
});
|
});
|
||||||
assert.ok(created);
|
assert.ok(created);
|
||||||
assert.equal(created.sourceRevision, 0);
|
assert.equal(created.sourceRevision, 0);
|
||||||
assert.equal(created.schemaVersion, 1);
|
assert.equal(created.schemaVersion, projectStateSnapshotSchemaVersion);
|
||||||
assert.equal(created.name, "Vor Ausschreibung");
|
assert.equal(created.name, "Vor Ausschreibung");
|
||||||
assert.equal(created.description, "Sicherer Stand");
|
assert.equal(created.description, "Sicherer Stand");
|
||||||
assert.equal(created.createdByActorId, "planner-1");
|
assert.equal(created.createdByActorId, "planner-1");
|
||||||
@@ -262,6 +263,42 @@ describe("project snapshot repository", () => {
|
|||||||
prepared.command.payload.targetState.project.id,
|
prepared.command.payload.targetState.project.id,
|
||||||
"project-1"
|
"project-1"
|
||||||
);
|
);
|
||||||
|
const currentPayload = JSON.parse(
|
||||||
|
context.db
|
||||||
|
.select({ payloadJson: projectSnapshots.payloadJson })
|
||||||
|
.from(projectSnapshots)
|
||||||
|
.where(eq(projectSnapshots.id, snapshot.id))
|
||||||
|
.get()!.payloadJson
|
||||||
|
) as Record<string, unknown> & {
|
||||||
|
project: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
currentPayload.schemaVersion = 1;
|
||||||
|
delete currentPayload.project.internalProjectNumber;
|
||||||
|
delete currentPayload.project.externalProjectNumber;
|
||||||
|
delete currentPayload.project.buildingOwner;
|
||||||
|
delete currentPayload.project.description;
|
||||||
|
const legacyPayloadJson = JSON.stringify(currentPayload);
|
||||||
|
context.db
|
||||||
|
.update(projectSnapshots)
|
||||||
|
.set({
|
||||||
|
schemaVersion: 1,
|
||||||
|
payloadJson: legacyPayloadJson,
|
||||||
|
payloadSha256: crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(legacyPayloadJson)
|
||||||
|
.digest("hex"),
|
||||||
|
})
|
||||||
|
.where(eq(projectSnapshots.id, snapshot.id))
|
||||||
|
.run();
|
||||||
|
const legacyPrepared = repository.prepareRestore(
|
||||||
|
"project-1",
|
||||||
|
snapshot.id
|
||||||
|
);
|
||||||
|
assert.ok(legacyPrepared);
|
||||||
|
assert.equal(
|
||||||
|
legacyPrepared.command.payload.targetState.project.buildingOwner,
|
||||||
|
null
|
||||||
|
);
|
||||||
context.db
|
context.db
|
||||||
.update(projectSnapshots)
|
.update(projectSnapshots)
|
||||||
.set({ payloadJson: `${JSON.stringify({})}\n` })
|
.set({ payloadJson: `${JSON.stringify({})}\n` })
|
||||||
|
|||||||
@@ -3,16 +3,21 @@ import { describe, it } from "node:test";
|
|||||||
import {
|
import {
|
||||||
deserializeProjectStateSnapshot,
|
deserializeProjectStateSnapshot,
|
||||||
parseProjectStateSnapshot,
|
parseProjectStateSnapshot,
|
||||||
|
projectStateSnapshotSchemaVersion,
|
||||||
serializeProjectStateSnapshot,
|
serializeProjectStateSnapshot,
|
||||||
} from "../src/domain/models/project-state-snapshot.model.js";
|
} from "../src/domain/models/project-state-snapshot.model.js";
|
||||||
import { createNamedProjectSnapshotSchema } from "../src/shared/validation/project-snapshot.schemas.js";
|
import { createNamedProjectSnapshotSchema } from "../src/shared/validation/project-snapshot.schemas.js";
|
||||||
|
|
||||||
function minimalSnapshot() {
|
function minimalSnapshot() {
|
||||||
return {
|
return {
|
||||||
schemaVersion: 1 as const,
|
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||||
project: {
|
project: {
|
||||||
id: "project-1",
|
id: "project-1",
|
||||||
name: "Projekt",
|
name: "Projekt",
|
||||||
|
internalProjectNumber: "INT-1",
|
||||||
|
externalProjectNumber: null,
|
||||||
|
buildingOwner: null,
|
||||||
|
description: null,
|
||||||
singlePhaseVoltageV: 230,
|
singlePhaseVoltageV: 230,
|
||||||
threePhaseVoltageV: 400,
|
threePhaseVoltageV: 400,
|
||||||
},
|
},
|
||||||
@@ -37,6 +42,28 @@ describe("project state snapshot model", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("upgrades version-one snapshots with empty project metadata", () => {
|
||||||
|
const legacy = {
|
||||||
|
...minimalSnapshot(),
|
||||||
|
schemaVersion: 1 as const,
|
||||||
|
project: {
|
||||||
|
id: "project-1",
|
||||||
|
name: "Altprojekt",
|
||||||
|
singlePhaseVoltageV: 230,
|
||||||
|
threePhaseVoltageV: 400,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const snapshot = parseProjectStateSnapshot(legacy);
|
||||||
|
assert.equal(snapshot.schemaVersion, projectStateSnapshotSchemaVersion);
|
||||||
|
assert.deepEqual(snapshot.project, {
|
||||||
|
...legacy.project,
|
||||||
|
internalProjectNumber: null,
|
||||||
|
externalProjectNumber: null,
|
||||||
|
buildingOwner: null,
|
||||||
|
description: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects duplicate ids and cross-project ownership", () => {
|
it("rejects duplicate ids and cross-project ownership", () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
Reference in New Issue
Block a user