Add public building project setting

This commit is contained in:
2026-07-31 17:41:30 +02:00
parent 43a3bb0b69
commit 44b6fde940
29 changed files with 2057 additions and 54 deletions
+10 -6
View File
@@ -250,9 +250,11 @@ not change the project revision or undo/redo stacks. Restoring a server-stored
snapshot verifies its checksum and the current-state hash, replaces supported
project data atomically and records a new `restore` revision with a complete
inverse command. Restore can therefore be undone and redone after a restart.
Baseline snapshot schema version 1 contains circuit-group identity,
distribution-board components and the separate circuit/component protection
records. Pre-baseline snapshots are unsupported. Portable duplicate imports
Current snapshot schema version 2 additionally stores whether the project is a
public building. Supported baseline version 1 snapshots are upgraded with
`isPublicBuilding = false`; pre-baseline snapshots remain unsupported. Both
versions contain circuit-group identity, distribution-board components and the
separate circuit/component protection records. Portable duplicate imports
remap component ids and protection-owner references together with the existing
project graph.
The central revision boundary creates an automatic logical snapshot after each
@@ -284,9 +286,11 @@ global-to-project copy API/UI paths use these persistent commands and track the
returned project revision. ProjectDevice synchronization/disconnect API and UI
paths do the same; their undo action uses the project-wide history endpoint.
Project settings use the persistent `project.update-settings` command. Project
metadata, both voltage defaults and the enabled distribution-board supply types
change in one revision and Undo/Redo restores them together; the project PUT
route requires `expectedRevision`. The system catalog is `AV`, `SV`, `EV`,
metadata, the public-building classification, both voltage defaults and the
enabled distribution-board supply types change in one revision and Undo/Redo
restores them together; the project PUT route requires `expectedRevision`.
The public-building flag is the future sizing input for requiring halogen-free
cables and lines. The system catalog is `AV`, `SV`, `EV`,
`USV`, `MSR`, `SiBe`; at least one must be enabled and a type used by a board
cannot be disabled.
Distribution-board setup uses `distribution-board.insert` schema version 1
+4 -3
View File
@@ -108,9 +108,10 @@ npm run db:generate
```
`db:backup` verwendet die SQLite-Online-Backup-API und prüft das Ergebnis auf
Integrität und Fremdschlüsselverletzungen. Die Migration `0000` ist die
Release-Baseline und richtet ausschließlich eine neue, leere Datenbank ein.
Datenbanken aus Entwicklungsständen vor dieser Baseline werden nicht unterstützt.
Integrität und Fremdschlüsselverletzungen. Migration `0000` ist die
Release-Baseline für eine leere Datenbank; nachfolgende nummerierte Migrationen
entwickeln diese Baseline additiv weiter. Datenbanken aus Entwicklungsständen
vor der Baseline werden nicht unterstützt.
## Dokumentation
+8 -6
View File
@@ -73,9 +73,11 @@ Protected-board structure uses
`circuit-protection.update`. A subtree restore is normally emitted only as the
persisted inverse of a delete. The dispatcher also supports the documented
project-device, project settings, distribution-board and project-location
commands. The generic editor commands listed here use schema version `1`;
the clean pre-release baseline does not accept older development formats.
Unsupported types or schema versions are rejected. Insert commands contain the complete entity or circuit block
commands. The generic editor commands listed here use schema version `1`.
`project.update-settings` uses version `2` and still accepts supported baseline
version `1` commands without changing the newer building classification.
Pre-baseline formats remain unsupported. Unsupported types or schema versions
are rejected. Insert commands contain the complete entity or circuit block
with stable ids; delete commands include the expected parent identity. Circuit
snapshots contain zero, one or multiple complete device rows. Move commands
contain each row's expected and target circuit plus its exact expected and
@@ -170,12 +172,12 @@ synchronizes linked circuit rows implicitly.
- `PUT /projects/:projectId`
- request:
`{ "expectedRevision": 12, "singlePhaseVoltageV": 230, "threePhaseVoltageV": 400, "enabledDistributionBoardSupplyTypes": ["AV", "MSR", "SiBe"], ...projectMetadata }`
`{ "expectedRevision": 12, "isPublicBuilding": true, "singlePhaseVoltageV": 230, "threePhaseVoltageV": 400, "enabledDistributionBoardSupplyTypes": ["AV", "MSR", "SiBe"], ...projectMetadata }`
- executes `project.update-settings` as one atomic revision
- response:
`{ "project": { ... }, "revision": { ... }, "history": { ... } }`
- persistent Undo/Redo restores metadata, voltage values and the enabled
distribution-board supply types together; project-device and circuit
- persistent Undo/Redo restores metadata, the public-building flag, voltage
values and enabled distribution-board supply types together; project-device and circuit
voltages are recalculated from the restored project settings in the same
transaction
- project-device and circuit voltage are derived values and are not accepted
+12 -9
View File
@@ -232,8 +232,10 @@ 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 Projektname, interne und externe
Projektnummer, Bauherr, Beschreibung, beide Standardspannungen sowie die im
Projekt freigeschalteten Verteiler-Netzarten als eine atomare Änderung. Der
Projektnummer, Bauherr, Beschreibung, die Kennzeichnung als öffentliches
Gebäude, beide Standardspannungen sowie die im Projekt freigeschalteten
Verteiler-Netzarten als eine atomare Änderung. Die Gebäudekennzeichnung ist
der spätere Eingang für die Auswahl halogenfreier Kabel und Leitungen. Der
Systemkatalog besteht aus `AV`, `SV`, `EV`, `USV`, `MSR` und `SiBe`; mindestens
eine Netzart muss aktiv bleiben und eine bereits von einer Verteilung
verwendete Netzart kann nicht deaktiviert werden. Die Projektseite bearbeitet diese Angaben in einem
@@ -414,12 +416,13 @@ wieder her.
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
ist für bestehende sowie neu angelegte Verteilungen standardmäßig `1`.
Das aktuelle Baseline-Snapshot-Schema enthält den Gleichzeitigkeitsfaktor,
Snapshot-Schema 2 enthält zusätzlich die Gebäudekennzeichnung sowie den Gleichzeitigkeitsfaktor,
Gruppenkategorie und -nummer, Verteilerkomponenten sowie die getrennten
1:1-Schutzgerätedaten. Capture,
benannte und automatische Snapshots, Wiederherstellung, Undo/Redo und beide
JSON-Importmodi verwenden denselben vollständigen Zustand. Vorherige
Entwicklungsschemas werden nicht mehr eingelesen.
JSON-Importmodi verwenden denselben vollständigen Zustand. Die unterstützte
Baseline-Version 1 wird mit `isPublicBuilding = false` hochgestuft; Formate vor
dieser Baseline werden nicht eingelesen.
Persistente Insert-, Update- und Delete-Commands versionieren Anlage,
Bearbeitung und Löschung von Geschossen und Räumen mit stabilen UUIDs und
exakten Vorher-/Nachher-Snapshots. Ein Geschoss wird nur
@@ -447,8 +450,8 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät.
- SQLite ist die aktuell unterstützte Datenbank.
- Fremdschlüssel werden für jeden Datenbankkontext aktiviert.
- `npm run db:migrate` richtet mit der einzelnen Migration `0000` das komplette
aktuelle Schema einer leeren Datenbank ein.
- `npm run db:migrate` richtet mit Baseline-Migration `0000` und den folgenden
additiven Migrationen das aktuelle Schema ein.
- `npm run db:verify:circuit-schema` prüft erforderliche und entfernte Spalten.
- `npm run db:backup` erzeugt ein konsistentes und verifiziertes Online-Backup.
- `npm run typecheck:scripts` prüft die TypeScript-Wartungsskripte, ohne Code zu
@@ -469,8 +472,8 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät.
flachen Stromkreis-Schutzfelder sind aus der Baseline entfernt.
Ein triggergeführtes Register erzwingt bereits eine normalisierte,
stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und
Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden
Snapshot-Schema 1 der Baseline. Persistente Insert/Delete/Update-Commands für
Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden aktuell
Snapshot-Schema 2. Persistente Insert/Delete/Update-Commands für
veränderliche Verteilerkomponenten, Gruppen einschließlich befüllter
Unterbäume sowie vollständige Gruppensortierung sind integriert. Der Editor
zeigt die geschützte Struktur an und bearbeitet veränderliche Gruppen- und
@@ -262,8 +262,9 @@ Completed foundation:
- `project-device.insert` and `project-device.delete` preserve stable device
ids; delete undo restores links only from complete unchanged disconnected-row
snapshots in the same transaction
- `project.update-settings` persists both project voltage defaults, its exact
inverse, revision and history transition atomically; the project page and
- `project.update-settings` persists the public-building classification and
both project voltage defaults with their exact inverse, revision and history
transition atomically; the project page and
existing PUT route require the current expected revision; the same
transaction derives every project-device and circuit voltage again, so
Undo/Redo cannot leave mixed voltage states
+1 -1
View File
@@ -52,7 +52,7 @@ Wichtige fachliche Grenzen:
Für einen späteren Revit-Austausch besonders relevante Daten sind:
- Projekt: Name, interne/externe Projektnummer, Bauherr, Beschreibung,
Projektspannungen.
Kennzeichnung als öffentliches Gebäude und Projektspannungen.
- Geschoss und Raum: interne UUID, Name beziehungsweise Raumnummer und
Raumname.
- Verteilung: interne UUID, Name, Etage, Netzart und Gleichzeitigkeitsfaktor.
@@ -14,12 +14,18 @@ Future suggestions or checks may include:
- cable type
- number of cores
- cable cross-section
- halogen-free cable or line requirement for public buildings
- minimum cable cross-section based on current
- maximum permissible cable length based on voltage drop
- warning messages if cable length, cross-section or protection device do not match
Users must remain able to manually override suggestions.
The project-owned `isPublicBuilding` setting is the authoritative input for
the later halogen-free selection rule. The setting is persisted now; this
document does not yet define or implement the complete cable catalog or sizing
algorithm.
## Initial Design Rule Examples
These are common planning defaults, not a replacement for full norm-compliant calculation.
+3 -3
View File
@@ -465,9 +465,9 @@ Implemented foundation:
- ProjectDevice synchronization and disconnect endpoints also require
`expectedRevision`; their UI undo uses project-wide persistent history and
the obsolete direct restore/reconnect write paths are removed
- project voltage settings use `project.update-settings`; both values, inverse,
revision and history transition commit atomically and the former direct
settings write is removed
- project settings use `project.update-settings`; metadata, public-building
classification, voltage values, inverse, revision and history transition
commit atomically and the former direct settings write is removed
- distribution-board setup uses `distribution-board.insert` with a complete
stable board/list/default-section snapshot; Undo removes only an unchanged
empty setup and the former direct controller write is removed
+11 -4
View File
@@ -54,7 +54,7 @@ requirements and intended sequencing, not proof of implementation.
The pre-release cleanup established the following compatibility boundary:
- [x] Pre-release databases, portable JSON exports, logical snapshots and
- [x] Pre-baseline databases, portable JSON exports, logical snapshots and
persisted command histories are explicitly unsupported.
- [x] Migration `0000` builds the complete supported schema on an empty
database.
@@ -109,9 +109,16 @@ The pre-release cleanup established the following compatibility boundary:
device rows with a valid phase are three-phase.
- [x] Changing project voltage settings updates all project devices and circuits
atomically in the same persistent Undo/Redo step.
- [x] Baseline migration `0000` and project-state schema version `1` store only
the canonical derived values. Pre-release imports are intentionally
unsupported.
- [x] Baseline migration `0000` stores only canonical derived values.
Pre-baseline imports are intentionally unsupported.
## Building Classification
- [x] Store whether a project represents a public building.
- [x] Edit the value in the project settings and preserve it through persistent
Undo/Redo, snapshots and portable project transfers.
- [x] Provide the value as an explicit future sizing input for selecting
halogen-free cables and lines; automatic sizing remains deferred.
## Circuit List Power Summary
@@ -467,11 +467,11 @@ The supported logical project snapshot and portable JSON transfer must include:
- circuit protection-device state
- all new component relationships and sort positions
The clean pre-release baseline uses project-state schema version `1`. It
contains the complete supported structure above and is shared by logical
snapshots, restore, Undo/Redo and portable JSON transfer. Payloads from earlier
development schemas are intentionally unsupported and rejected before any
write.
Current project-state schema version `2` contains the complete supported
structure above and the project building classification. It is shared by
logical snapshots, restore, Undo/Redo and portable JSON transfer. Supported
baseline version `1` is upgraded with the non-public default; payloads from
before that baseline are rejected before any write.
Migration `0000` creates the complete relational model on an empty database.
New defaults apply when boards, groups or circuits are created; they never
@@ -545,7 +545,7 @@ Implemented persistence:
one-to-one ownership boundaries
- a trigger-maintained circuit-list registry rejects normalized BMK collisions
across circuits and distribution-board components
- snapshot schema version `1` includes group identity, distribution-board
- snapshot schema version `2` includes group identity, distribution-board
components and both one-to-one protection collections
- capture, named and automatic snapshots, restore, Undo/Redo and portable JSON
transfers preserve the complete new relational state
+1
View File
@@ -78,6 +78,7 @@ const requiredProjectColumns = [
"external_project_number",
"building_owner",
"description",
"is_public_building",
"enabled_distribution_board_supply_types",
];
const projectColumns = new Set(
@@ -0,0 +1 @@
ALTER TABLE `projects` ADD `is_public_building` integer DEFAULT false NOT NULL;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -8,6 +8,13 @@
"when": 1785499119431,
"tag": "0000_whole_surge",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1785511894897,
"tag": "0001_add_public_building_setting",
"breakpoints": true
}
]
}
@@ -40,22 +40,33 @@ export class ProjectSettingsProjectCommandRepository
if (!current) {
throw new Error("Project not found.");
}
const target = input.command.payload;
const target: ProjectSettingsValues =
input.command.schemaVersion === 1
? {
...input.command.payload,
isPublicBuilding: current.isPublicBuilding,
}
: input.command.payload;
if (projectSettingsEqual(current, target)) {
throw new Error("Project settings did not change.");
}
const inverse = createProjectSettingsUpdateProjectCommand({
const inverseValues: ProjectSettingsValues = {
name: current.name,
internalProjectNumber: current.internalProjectNumber,
externalProjectNumber: current.externalProjectNumber,
buildingOwner: current.buildingOwner,
description: current.description,
isPublicBuilding: current.isPublicBuilding,
singlePhaseVoltageV: current.singlePhaseVoltageV,
threePhaseVoltageV: current.threePhaseVoltageV,
enabledDistributionBoardSupplyTypes:
current.enabledDistributionBoardSupplyTypes,
});
};
const inverse =
input.command.schemaVersion === 1
? createBaselineProjectSettingsUpdateCommand(inverseValues)
: createProjectSettingsUpdateProjectCommand(inverseValues);
assertDisabledSupplyTypesAreUnused(tx, input.projectId, target);
const updated = tx
.update(projects)
@@ -71,6 +82,26 @@ export class ProjectSettingsProjectCommandRepository
}
}
function createBaselineProjectSettingsUpdateCommand(
values: ProjectSettingsValues
) {
return {
schemaVersion: 1 as const,
type: "project.update-settings" as const,
payload: {
name: values.name,
internalProjectNumber: values.internalProjectNumber,
externalProjectNumber: values.externalProjectNumber,
buildingOwner: values.buildingOwner,
description: values.description,
singlePhaseVoltageV: values.singlePhaseVoltageV,
threePhaseVoltageV: values.threePhaseVoltageV,
enabledDistributionBoardSupplyTypes:
values.enabledDistributionBoardSupplyTypes,
},
};
}
function projectSettingsEqual(
current: ProjectSettingsValues,
target: ProjectSettingsValues
@@ -81,6 +112,7 @@ function projectSettingsEqual(
current.externalProjectNumber === target.externalProjectNumber &&
current.buildingOwner === target.buildingOwner &&
current.description === target.description &&
current.isPublicBuilding === target.isPublicBuilding &&
current.singlePhaseVoltageV === target.singlePhaseVoltageV &&
current.threePhaseVoltageV === target.threePhaseVoltageV
&& sameSupplyTypes(
@@ -208,6 +208,7 @@ function replaceProjectState(
externalProjectNumber: state.project.externalProjectNumber,
buildingOwner: state.project.buildingOwner,
description: state.project.description,
isPublicBuilding: state.project.isPublicBuilding,
singlePhaseVoltageV: state.project.singlePhaseVoltageV,
threePhaseVoltageV: state.project.threePhaseVoltageV,
enabledDistributionBoardSupplyTypes:
@@ -39,6 +39,7 @@ export function readProjectStateSnapshot(
externalProjectNumber: projects.externalProjectNumber,
buildingOwner: projects.buildingOwner,
description: projects.description,
isPublicBuilding: projects.isPublicBuilding,
singlePhaseVoltageV: projects.singlePhaseVoltageV,
threePhaseVoltageV: projects.threePhaseVoltageV,
enabledDistributionBoardSupplyTypes:
@@ -233,6 +234,7 @@ export function readProjectStateSnapshot(
externalProjectNumber: project.externalProjectNumber,
buildingOwner: project.buildingOwner,
description: project.description,
isPublicBuilding: project.isPublicBuilding,
singlePhaseVoltageV: project.singlePhaseVoltageV,
threePhaseVoltageV: project.threePhaseVoltageV,
enabledDistributionBoardSupplyTypes:
@@ -25,6 +25,7 @@ export class ProjectRepository {
externalProjectNumber: input.externalProjectNumber ?? null,
buildingOwner: input.buildingOwner ?? null,
description: input.description ?? null,
isPublicBuilding: input.isPublicBuilding ?? false,
singlePhaseVoltageV: input.singlePhaseVoltageV ?? 230,
threePhaseVoltageV: input.threePhaseVoltageV ?? 400,
enabledDistributionBoardSupplyTypes: [
+3
View File
@@ -9,6 +9,9 @@ export const projects = sqliteTable("projects", {
externalProjectNumber: text("external_project_number"),
buildingOwner: text("building_owner"),
description: text("description"),
isPublicBuilding: integer("is_public_building", { mode: "boolean" })
.notNull()
.default(false),
singlePhaseVoltageV: integer("single_phase_voltage_v").notNull().default(230),
threePhaseVoltageV: integer("three_phase_voltage_v").notNull().default(400),
enabledDistributionBoardSupplyTypes: text(
@@ -6,7 +6,8 @@ import {
export const projectSettingsUpdateCommandType =
"project.update-settings" as const;
export const projectSettingsUpdateCommandSchemaVersion = 1 as const;
export const projectSettingsUpdateCommandSchemaVersion = 2 as const;
const previousProjectSettingsUpdateCommandSchemaVersion = 1 as const;
export interface ProjectSettingsValues {
name: string;
@@ -14,21 +15,37 @@ export interface ProjectSettingsValues {
externalProjectNumber: string | null;
buildingOwner: string | null;
description: string | null;
isPublicBuilding: boolean;
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
}
export interface ProjectSettingsUpdateProjectCommand
type PreviousProjectSettingsValues = Omit<
ProjectSettingsValues,
"isPublicBuilding"
>;
export interface CurrentProjectSettingsUpdateProjectCommand
extends SerializedProjectCommand<ProjectSettingsValues> {
schemaVersion: typeof projectSettingsUpdateCommandSchemaVersion;
type: typeof projectSettingsUpdateCommandType;
}
interface PreviousProjectSettingsUpdateProjectCommand
extends SerializedProjectCommand<PreviousProjectSettingsValues> {
schemaVersion: typeof previousProjectSettingsUpdateCommandSchemaVersion;
type: typeof projectSettingsUpdateCommandType;
}
export type ProjectSettingsUpdateProjectCommand =
| CurrentProjectSettingsUpdateProjectCommand
| PreviousProjectSettingsUpdateProjectCommand;
export function createProjectSettingsUpdateProjectCommand(
values: ProjectSettingsValues
): ProjectSettingsUpdateProjectCommand {
const command: ProjectSettingsUpdateProjectCommand = {
): CurrentProjectSettingsUpdateProjectCommand {
const command: CurrentProjectSettingsUpdateProjectCommand = {
schemaVersion: projectSettingsUpdateCommandSchemaVersion,
type: projectSettingsUpdateCommandType,
payload: { ...values },
@@ -42,7 +59,8 @@ export function assertProjectSettingsUpdateProjectCommand(
): asserts command is ProjectSettingsUpdateProjectCommand {
if (
command.type !== projectSettingsUpdateCommandType ||
command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion
(command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion &&
command.schemaVersion !== previousProjectSettingsUpdateCommandSchemaVersion)
) {
throw new Error("Unsupported project settings update command.");
}
@@ -58,8 +76,7 @@ export function assertProjectSettingsUpdateProjectCommand(
!isNullableTrimmedString(command.payload.buildingOwner, 200) ||
!isNullableTrimmedString(command.payload.description, 2000) ||
!isPositiveFiniteNumber(command.payload.singlePhaseVoltageV) ||
!isPositiveFiniteNumber(command.payload.threePhaseVoltageV) ||
Object.keys(command.payload).length !== 8
!isPositiveFiniteNumber(command.payload.threePhaseVoltageV)
) {
throw new Error(
"Project settings update command contains invalid values."
@@ -74,6 +91,18 @@ export function assertProjectSettingsUpdateProjectCommand(
"Project settings update command contains invalid supply types."
);
}
const isPreviousSchema =
command.schemaVersion === previousProjectSettingsUpdateCommandSchemaVersion;
if (
(isPreviousSchema && Object.keys(command.payload).length !== 8) ||
(!isPreviousSchema &&
(typeof command.payload.isPublicBuilding !== "boolean" ||
Object.keys(command.payload).length !== 9))
) {
throw new Error(
"Project settings update command contains invalid values."
);
}
}
function isValidSupplyTypes(
@@ -19,13 +19,14 @@ import {
resolveProjectVoltage,
} from "../services/project-voltage.service.js";
export const projectStateSnapshotSchemaVersion = 1 as const;
export const projectStateSnapshotSchemaVersion = 2 as const;
const previousProjectStateSnapshotSchemaVersion = 1 as const;
const idSchema = z.string().trim().min(1);
const nullableStringSchema = z.string().nullable();
const finiteNumberSchema = z.number().finite();
const projectSchema = z
const previousProjectSchema = z
.object({
id: idSchema,
name: z.string().trim().min(1),
@@ -42,6 +43,10 @@ const projectSchema = z
})
.strict();
const projectSchema = previousProjectSchema
.extend({ isPublicBuilding: z.boolean() })
.strict();
const distributionBoardSchema = z
.object({
id: idSchema,
@@ -267,6 +272,15 @@ export const projectStateSnapshotSchema = z
})
.strict();
const previousProjectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(previousProjectStateSnapshotSchemaVersion),
project: previousProjectSchema,
distributionBoards: z.array(distributionBoardSchema),
...projectStateSnapshotContents,
})
.strict();
export type ProjectStateSnapshot = z.infer<
typeof projectStateSnapshotSchema
>;
@@ -274,12 +288,35 @@ export type ProjectStateSnapshot = z.infer<
export function parseProjectStateSnapshot(
value: unknown
): ProjectStateSnapshot {
const parsedSnapshot = projectStateSnapshotSchema.parse(value);
const parsedSnapshot = projectStateSnapshotSchema.parse(
upgradePreviousProjectStateSnapshot(value)
);
const snapshot = normalizeSnapshotPhaseTypes(parsedSnapshot);
assertProjectStateSnapshotRelations(snapshot);
return snapshot;
}
function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
if (
value === null ||
typeof value !== "object" ||
Array.isArray(value) ||
(value as { schemaVersion?: unknown }).schemaVersion !==
previousProjectStateSnapshotSchemaVersion
) {
return value;
}
const previous = previousProjectStateSnapshotSchema.parse(value);
return {
...previous,
schemaVersion: projectStateSnapshotSchemaVersion,
project: {
...previous.project,
isPublicBuilding: false,
},
};
}
function normalizeSnapshotPhaseTypes(
snapshot: ProjectStateSnapshot
): ProjectStateSnapshot {
@@ -14,6 +14,7 @@ export interface ProjectSettingsInput {
externalProjectNumber: string | null;
buildingOwner: string | null;
description: string | null;
isPublicBuilding: boolean;
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
@@ -54,6 +55,9 @@ export function ProjectSettingsModal({
const [description, setDescription] = useState(
project.description ?? ""
);
const [isPublicBuilding, setIsPublicBuilding] = useState(
project.isPublicBuilding
);
const [singlePhaseVoltageV, setSinglePhaseVoltageV] = useState(
String(project.singlePhaseVoltageV)
);
@@ -82,6 +86,7 @@ export function ProjectSettingsModal({
externalProjectNumber: toNullableString(externalProjectNumber),
buildingOwner: toNullableString(buildingOwner),
description: toNullableString(description),
isPublicBuilding,
singlePhaseVoltageV: Number(singlePhaseVoltageV),
threePhaseVoltageV: Number(threePhaseVoltageV),
enabledDistributionBoardSupplyTypes,
@@ -269,6 +274,27 @@ export function ProjectSettingsModal({
value={description}
/>
</div>
<div className="col-12">
<label className="form-check">
<input
checked={isPublicBuilding}
className="form-check-input"
id="public-building"
onChange={(event) =>
setIsPublicBuilding(event.target.checked)
}
type="checkbox"
/>
<span className="form-check-label">
Öffentliches Gebäude
</span>
</label>
<div className="form-text ms-4">
Wird bei der späteren Leitungsauslegung berücksichtigt,
insbesondere bei der Auswahl halogenfreier Kabel und
Leitungen.
</div>
</div>
<div className="col-12 col-md-6">
<label className="form-label" htmlFor="single-phase-voltage">
Standardspannung 1-phasig [V]
+1
View File
@@ -18,6 +18,7 @@ export interface ProjectDto {
externalProjectNumber: string | null;
buildingOwner: string | null;
description: string | null;
isPublicBuilding: boolean;
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
+1
View File
@@ -245,6 +245,7 @@ export function updateProjectSettings(
externalProjectNumber: string | null;
buildingOwner: string | null;
description: string | null;
isPublicBuilding: boolean;
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
enabledDistributionBoardSupplyTypes: ProjectDto["enabledDistributionBoardSupplyTypes"];
@@ -8,6 +8,7 @@ export const createProjectSchema = z.object({
externalProjectNumber: z.string().trim().max(100).optional(),
buildingOwner: z.string().trim().max(200).optional(),
description: z.string().trim().max(2000).optional(),
isPublicBuilding: z.boolean().optional(),
singlePhaseVoltageV: z.number().positive().optional(),
threePhaseVoltageV: z.number().positive().optional(),
});
@@ -20,6 +21,7 @@ export const updateProjectSettingsSchema = z
externalProjectNumber: z.string().trim().max(100).nullable(),
buildingOwner: z.string().trim().max(200).nullable(),
description: z.string().trim().max(2000).nullable(),
isPublicBuilding: z.boolean(),
singlePhaseVoltageV: z.number().positive(),
threePhaseVoltageV: z.number().positive(),
enabledDistributionBoardSupplyTypes: z
+5
View File
@@ -1057,6 +1057,7 @@ describe("project command service", () => {
externalProjectNumber: null,
buildingOwner: null,
description: null,
isPublicBuilding: true,
singlePhaseVoltageV: 240,
threePhaseVoltageV: 415,
enabledDistributionBoardSupplyTypes: [
@@ -1073,6 +1074,7 @@ describe("project command service", () => {
assert.deepEqual(
context.db
.select({
isPublicBuilding: projects.isPublicBuilding,
singlePhaseVoltageV: projects.singlePhaseVoltageV,
threePhaseVoltageV: projects.threePhaseVoltageV,
})
@@ -1080,6 +1082,7 @@ describe("project command service", () => {
.where(eq(projects.id, "project-1"))
.get(),
{
isPublicBuilding: true,
singlePhaseVoltageV: 240,
threePhaseVoltageV: 415,
}
@@ -1093,6 +1096,7 @@ describe("project command service", () => {
assert.deepEqual(
context.db
.select({
isPublicBuilding: projects.isPublicBuilding,
singlePhaseVoltageV: projects.singlePhaseVoltageV,
threePhaseVoltageV: projects.threePhaseVoltageV,
})
@@ -1100,6 +1104,7 @@ describe("project command service", () => {
.where(eq(projects.id, "project-1"))
.get(),
{
isPublicBuilding: false,
singlePhaseVoltageV: 230,
threePhaseVoltageV: 400,
}
@@ -47,6 +47,7 @@ function getSettings(context: DatabaseContext) {
externalProjectNumber: projects.externalProjectNumber,
buildingOwner: projects.buildingOwner,
description: projects.description,
isPublicBuilding: projects.isPublicBuilding,
singlePhaseVoltageV: projects.singlePhaseVoltageV,
threePhaseVoltageV: projects.threePhaseVoltageV,
enabledDistributionBoardSupplyTypes:
@@ -69,6 +70,7 @@ function settings(
externalProjectNumber: null,
buildingOwner: null,
description: null,
isPublicBuilding: false,
singlePhaseVoltageV: 230,
threePhaseVoltageV: 400,
enabledDistributionBoardSupplyTypes: [
@@ -204,6 +206,7 @@ describe("project settings project-command repository", () => {
externalProjectNumber: null,
buildingOwner: null,
description: null,
isPublicBuilding: false,
singlePhaseVoltageV: 240,
threePhaseVoltageV: 415,
enabledDistributionBoardSupplyTypes: [
@@ -231,6 +234,7 @@ describe("project settings project-command repository", () => {
...settings(),
name: "Neuer Projektname",
buildingOwner: "Beispiel Bau GmbH",
isPublicBuilding: true,
singlePhaseVoltageV: 240,
threePhaseVoltageV: 415,
enabledDistributionBoardSupplyTypes: [
@@ -252,6 +256,7 @@ describe("project settings project-command repository", () => {
...settings({
name: "Neuer Projektname",
buildingOwner: "Beispiel Bau GmbH",
isPublicBuilding: true,
singlePhaseVoltageV: 240,
threePhaseVoltageV: 415,
}),
@@ -273,6 +278,7 @@ describe("project settings project-command repository", () => {
externalProjectNumber: null,
buildingOwner: null,
description: null,
isPublicBuilding: false,
singlePhaseVoltageV: 230,
threePhaseVoltageV: 400,
enabledDistributionBoardSupplyTypes: [
@@ -300,6 +306,7 @@ describe("project settings project-command repository", () => {
externalProjectNumber: null,
buildingOwner: null,
description: null,
isPublicBuilding: false,
singlePhaseVoltageV: 230,
threePhaseVoltageV: 400,
enabledDistributionBoardSupplyTypes: [
@@ -338,6 +345,7 @@ describe("project settings project-command repository", () => {
externalProjectNumber: null,
buildingOwner: "Beispiel Bau GmbH",
description: null,
isPublicBuilding: true,
singlePhaseVoltageV: 240,
threePhaseVoltageV: 415,
enabledDistributionBoardSupplyTypes: [
@@ -365,6 +373,66 @@ describe("project settings project-command repository", () => {
}
});
it("preserves the building classification when applying a baseline command", () => {
const context = createTestDatabase();
try {
context.db
.update(projects)
.set({ isPublicBuilding: true })
.where(eq(projects.id, "project-1"))
.run();
const repository = new ProjectSettingsProjectCommandRepository(
context.db
);
const forward = repository.executeUpdate({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: {
schemaVersion: 1,
type: "project.update-settings",
payload: {
name: "Stand aus Baseline-Command",
internalProjectNumber: "INT-1",
externalProjectNumber: null,
buildingOwner: null,
description: null,
singlePhaseVoltageV: 230,
threePhaseVoltageV: 400,
enabledDistributionBoardSupplyTypes: [
"AV",
"SV",
"EV",
"USV",
"MSR",
"SiBe",
],
},
},
});
assert.equal(getSettings(context)?.name, "Stand aus Baseline-Command");
assert.equal(getSettings(context)?.isPublicBuilding, true);
assert.equal(forward.inverse.schemaVersion, 1);
const history = new ProjectHistoryRepository(context.db);
const undoStep = history.getNextCommand("project-1", "undo");
assert.ok(undoStep);
repository.executeUpdate({
projectId: "project-1",
expectedRevision: 1,
source: "undo",
historyTargetChangeSetId: undoStep.changeSetId,
command: forward.inverse,
});
assert.equal(getSettings(context)?.name, "Testprojekt");
assert.equal(getSettings(context)?.isPublicBuilding, true);
} finally {
context.close();
}
});
it("persists the project supply-type selection and rejects disabling an in-use type", () => {
const context = createTestDatabase();
try {
+13 -1
View File
@@ -18,6 +18,7 @@ function minimalSnapshot() {
externalProjectNumber: null,
buildingOwner: null,
description: null,
isPublicBuilding: true,
singlePhaseVoltageV: 230,
threePhaseVoltageV: 400,
enabledDistributionBoardSupplyTypes: [
@@ -142,7 +143,18 @@ describe("project state snapshot model", () => {
);
});
it("rejects pre-baseline snapshot shapes", () => {
it("upgrades the supported baseline snapshot with a safe building default", () => {
const baseline = structuredClone(minimalSnapshot());
baseline.schemaVersion = 1 as typeof baseline.schemaVersion;
delete (baseline.project as Record<string, unknown>).isPublicBuilding;
const upgraded = parseProjectStateSnapshot(baseline);
assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion);
assert.equal(upgraded.project.isPublicBuilding, false);
});
it("rejects malformed baseline and unknown snapshot shapes", () => {
const outdated = structuredClone(minimalSnapshot());
delete (outdated.project as Record<string, unknown>).description;
assert.throws(() => parseProjectStateSnapshot(outdated));
+2
View File
@@ -247,6 +247,7 @@ describe("project settings presentation", () => {
externalProjectNumber: null,
buildingOwner: null,
description: null,
isPublicBuilding: true,
singlePhaseVoltageV: 230,
threePhaseVoltageV: 400,
enabledDistributionBoardSupplyTypes: [
@@ -262,6 +263,7 @@ describe("project settings presentation", () => {
assert.match(markup, /MSR/);
assert.match(markup, /SiBe/);
assert.match(markup, /in Verwendung/);
assert.match(markup, /Öffentliches Gebäude/);
});
});