Add versioned project settings modal

This commit is contained in:
2026-07-28 18:16:22 +02:00
parent e1fc81a083
commit d77cfbeb49
22 changed files with 2563 additions and 111 deletions
+36 -63
View File
@@ -40,6 +40,10 @@ import {
type ProjectDeviceSyncField,
} from "../../../shared/constants/project-device-sync-fields";
import { ProjectVersionHistory } from "../../../frontend/components/project-version-history";
import {
ProjectSettingsModal,
type ProjectSettingsInput,
} from "../../../frontend/components/project-settings-modal";
const projectDeviceSyncFieldLabels: Record<ProjectDeviceSyncField, string> = {
name: "Technischer Name",
@@ -92,8 +96,7 @@ export default function ProjectDetailPage() {
const [roomNumber, setRoomNumber] = useState("");
const [roomName, setRoomName] = useState("");
const [roomFloorId, setRoomFloorId] = useState("");
const [singlePhaseVoltageV, setSinglePhaseVoltageV] = useState("230");
const [threePhaseVoltageV, setThreePhaseVoltageV] = useState("400");
const [isProjectSettingsOpen, setIsProjectSettingsOpen] = useState(false);
const [projectDeviceForm, setProjectDeviceForm] = useState<Record<string, string>>({
name: "",
displayName: "",
@@ -142,10 +145,6 @@ export default function ProjectDetailPage() {
]) => {
const currentProject = projects.find((item) => item.id === projectId) ?? null;
setProject(currentProject);
if (currentProject) {
setSinglePhaseVoltageV(String(currentProject.singlePhaseVoltageV));
setThreePhaseVoltageV(String(currentProject.threePhaseVoltageV));
}
setBoards(distributionBoards);
setCircuitLists(loadedCircuitLists);
setFloors(loadedFloors);
@@ -259,7 +258,7 @@ export default function ProjectDetailPage() {
}
}
async function handleSaveProjectSettings() {
async function handleSaveProjectSettings(input: ProjectSettingsInput) {
if (!projectId || !project) {
return;
}
@@ -269,12 +268,10 @@ export default function ProjectDetailPage() {
const result = await updateProjectSettings(
projectId,
project.currentRevision,
{
singlePhaseVoltageV: Number(singlePhaseVoltageV),
threePhaseVoltageV: Number(threePhaseVoltageV),
}
input
);
setProject(result.project);
setIsProjectSettingsOpen(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Projekteigenschaften konnten nicht gespeichert werden.");
} finally {
@@ -532,66 +529,34 @@ export default function ProjectDetailPage() {
return (
<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>
<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 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">
Zur Projektübersicht
</Link>
</div>
<Link className="btn btn-outline-secondary" href="/projects">
Zur Projektseite
</Link>
</div>
{error ? <div className="alert alert-warning">{error}</div> : null}
<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 ? (
<section className="col-12">
<ProjectVersionHistory
@@ -1156,6 +1121,14 @@ export default function ProjectDetailPage() {
) : null}
</section>
{project && isProjectSettingsOpen ? (
<ProjectSettingsModal
isSaving={isSaving}
onClose={() => setIsProjectSettingsOpen(false)}
onSave={handleSaveProjectSettings}
project={project}
/>
) : null}
</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
+7
View File
@@ -113,6 +113,13 @@
"when": 1785014383032,
"tag": "0015_modern_madame_masque",
"breakpoints": true
},
{
"idx": 16,
"version": "6",
"when": 1785254079435,
"tag": "0016_dashing_darkstar",
"breakpoints": true
}
]
}
@@ -2,6 +2,8 @@ import { eq } from "drizzle-orm";
import {
assertProjectSettingsUpdateProjectCommand,
createProjectSettingsUpdateProjectCommand,
legacyProjectSettingsUpdateCommandSchemaVersion,
type ProjectSettingsValues,
} from "../../domain/models/project-settings-project-command.model.js";
import type {
ExecuteProjectSettingsUpdateCommandInput,
@@ -37,22 +39,45 @@ export class ProjectSettingsProjectCommandRepository
if (!current) {
throw new Error("Project not found.");
}
if (
current.singlePhaseVoltageV ===
input.command.payload.singlePhaseVoltageV &&
current.threePhaseVoltageV ===
input.command.payload.threePhaseVoltageV
) {
const target: ProjectSettingsValues =
input.command.schemaVersion ===
legacyProjectSettingsUpdateCommandSchemaVersion
? {
name: current.name,
internalProjectNumber: current.internalProjectNumber,
externalProjectNumber: current.externalProjectNumber,
buildingOwner: current.buildingOwner,
description: current.description,
...input.command.payload,
}
: input.command.payload;
if (projectSettingsEqual(current, target)) {
throw new Error("Project settings did not change.");
}
const inverse = createProjectSettingsUpdateProjectCommand({
singlePhaseVoltageV: current.singlePhaseVoltageV,
threePhaseVoltageV: current.threePhaseVoltageV,
});
const inverse =
input.command.schemaVersion ===
legacyProjectSettingsUpdateCommandSchemaVersion
? {
schemaVersion: legacyProjectSettingsUpdateCommandSchemaVersion,
type: input.command.type,
payload: {
singlePhaseVoltageV: current.singlePhaseVoltageV,
threePhaseVoltageV: current.threePhaseVoltageV,
},
}
: createProjectSettingsUpdateProjectCommand({
name: current.name,
internalProjectNumber: current.internalProjectNumber,
externalProjectNumber: current.externalProjectNumber,
buildingOwner: current.buildingOwner,
description: current.description,
singlePhaseVoltageV: current.singlePhaseVoltageV,
threePhaseVoltageV: current.threePhaseVoltageV,
});
const updated = tx
.update(projects)
.set(input.command.payload)
.set(target)
.where(eq(projects.id, input.projectId))
.run();
if (updated.changes !== 1) {
@@ -62,3 +87,18 @@ export class ProjectSettingsProjectCommandRepository
return inverse;
}
}
function projectSettingsEqual(
current: ProjectSettingsValues,
target: ProjectSettingsValues
) {
return (
current.name === target.name &&
current.internalProjectNumber === target.internalProjectNumber &&
current.externalProjectNumber === target.externalProjectNumber &&
current.buildingOwner === target.buildingOwner &&
current.description === target.description &&
current.singlePhaseVoltageV === target.singlePhaseVoltageV &&
current.threePhaseVoltageV === target.threePhaseVoltageV
);
}
@@ -5,6 +5,7 @@ import { ProjectSnapshotNameConflictError } from "../../domain/errors/project-sn
import { createProjectStateRestoreCommand } from "../../domain/models/project-state-restore-command.model.js";
import {
deserializeProjectStateSnapshot,
legacyProjectStateSnapshotSchemaVersion,
projectStateSnapshotSchemaVersion,
} from "../../domain/models/project-state-snapshot.model.js";
import type {
@@ -138,7 +139,10 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
if (actualChecksum !== stored.payloadSha256) {
throw new Error("Project snapshot checksum verification failed.");
}
if (stored.schemaVersion !== projectStateSnapshotSchemaVersion) {
if (
stored.schemaVersion !== legacyProjectStateSnapshotSchemaVersion &&
stored.schemaVersion !== projectStateSnapshotSchemaVersion
) {
throw new Error("Project snapshot schema version is not supported.");
}
const targetState = deserializeProjectStateSnapshot(stored.payloadJson);
@@ -5,6 +5,7 @@ import {
createProjectStateRestoreCommand,
} from "../../domain/models/project-state-restore-command.model.js";
import {
parseProjectStateSnapshot,
serializeProjectStateSnapshot,
type ProjectStateSnapshot,
} from "../../domain/models/project-state-snapshot.model.js";
@@ -38,7 +39,10 @@ export class ProjectStateRestoreCommandRepository
execute(input: ExecuteProjectStateRestoreCommandInput) {
assertProjectStateRestoreCommand(input.command);
if (input.command.payload.targetState.project.id !== input.projectId) {
const targetState = parseProjectStateSnapshot(
input.command.payload.targetState
);
if (targetState.project.id !== input.projectId) {
throw new Error("Restore state belongs to a different project.");
}
@@ -68,15 +72,16 @@ export class ProjectStateRestoreCommandRepository
);
}
const targetPayloadJson = serializeProjectStateSnapshot(
const targetState = parseProjectStateSnapshot(
input.command.payload.targetState
);
const targetPayloadJson = serializeProjectStateSnapshot(targetState);
const inverse = createProjectStateRestoreCommand(
hashProjectStatePayload(targetPayloadJson),
current.state
);
replaceProjectState(tx, input.command.payload.targetState);
replaceProjectState(tx, targetState);
return inverse;
}
@@ -111,6 +116,10 @@ function replaceProjectState(
.update(projects)
.set({
name: state.project.name,
internalProjectNumber: state.project.internalProjectNumber,
externalProjectNumber: state.project.externalProjectNumber,
buildingOwner: state.project.buildingOwner,
description: state.project.description,
singlePhaseVoltageV: state.project.singlePhaseVoltageV,
threePhaseVoltageV: state.project.threePhaseVoltageV,
})
@@ -32,6 +32,10 @@ export function readProjectStateSnapshot(
.select({
id: projects.id,
name: projects.name,
internalProjectNumber: projects.internalProjectNumber,
externalProjectNumber: projects.externalProjectNumber,
buildingOwner: projects.buildingOwner,
description: projects.description,
singlePhaseVoltageV: projects.singlePhaseVoltageV,
threePhaseVoltageV: projects.threePhaseVoltageV,
currentRevision: projects.currentRevision,
@@ -172,6 +176,10 @@ export function readProjectStateSnapshot(
project: {
id: project.id,
name: project.name,
internalProjectNumber: project.internalProjectNumber,
externalProjectNumber: project.externalProjectNumber,
buildingOwner: project.buildingOwner,
description: project.description,
singlePhaseVoltageV: project.singlePhaseVoltageV,
threePhaseVoltageV: project.threePhaseVoltageV,
},
@@ -20,6 +20,10 @@ export class ProjectRepository {
const project = {
id,
name: input.name,
internalProjectNumber: input.internalProjectNumber ?? null,
externalProjectNumber: input.externalProjectNumber ?? null,
buildingOwner: input.buildingOwner ?? null,
description: input.description ?? null,
singlePhaseVoltageV: input.singlePhaseVoltageV ?? 230,
threePhaseVoltageV: input.threePhaseVoltageV ?? 400,
currentRevision: 0,
+4
View File
@@ -3,6 +3,10 @@ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const projects = sqliteTable("projects", {
id: text("id").primaryKey(),
name: text("name").notNull(),
internalProjectNumber: text("internal_project_number"),
externalProjectNumber: text("external_project_number"),
buildingOwner: text("building_owner"),
description: text("description"),
singlePhaseVoltageV: integer("single_phase_voltage_v").notNull().default(230),
threePhaseVoltageV: integer("three_phase_voltage_v").notNull().default(400),
currentRevision: integer("current_revision").notNull().default(0),
@@ -2,23 +2,42 @@ import type { SerializedProjectCommand } from "./project-command.model.js";
export const projectSettingsUpdateCommandType =
"project.update-settings" as const;
export const projectSettingsUpdateCommandSchemaVersion = 1 as const;
export const legacyProjectSettingsUpdateCommandSchemaVersion = 1 as const;
export const projectSettingsUpdateCommandSchemaVersion = 2 as const;
export interface ProjectSettingsValues {
export interface LegacyProjectSettingsValues {
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
}
export interface ProjectSettingsUpdateProjectCommand
export interface ProjectSettingsValues extends LegacyProjectSettingsValues {
name: string;
internalProjectNumber: string | null;
externalProjectNumber: string | null;
buildingOwner: string | null;
description: string | null;
}
export interface LegacyProjectSettingsUpdateProjectCommand
extends SerializedProjectCommand<LegacyProjectSettingsValues> {
schemaVersion: typeof legacyProjectSettingsUpdateCommandSchemaVersion;
type: typeof projectSettingsUpdateCommandType;
}
export interface CurrentProjectSettingsUpdateProjectCommand
extends SerializedProjectCommand<ProjectSettingsValues> {
schemaVersion: typeof projectSettingsUpdateCommandSchemaVersion;
type: typeof projectSettingsUpdateCommandType;
}
export type ProjectSettingsUpdateProjectCommand =
| LegacyProjectSettingsUpdateProjectCommand
| CurrentProjectSettingsUpdateProjectCommand;
export function createProjectSettingsUpdateProjectCommand(
values: ProjectSettingsValues
): ProjectSettingsUpdateProjectCommand {
const command: ProjectSettingsUpdateProjectCommand = {
): CurrentProjectSettingsUpdateProjectCommand {
const command: CurrentProjectSettingsUpdateProjectCommand = {
schemaVersion: projectSettingsUpdateCommandSchemaVersion,
type: projectSettingsUpdateCommandType,
payload: { ...values },
@@ -31,16 +50,45 @@ export function assertProjectSettingsUpdateProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is ProjectSettingsUpdateProjectCommand {
if (
command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion ||
command.type !== projectSettingsUpdateCommandType
command.type !== projectSettingsUpdateCommandType ||
(command.schemaVersion !==
legacyProjectSettingsUpdateCommandSchemaVersion &&
command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion)
) {
throw new Error("Unsupported project settings update command.");
}
if (!isPlainObject(command.payload)) {
throw new Error(
"Project settings update command contains invalid values."
);
}
if (command.schemaVersion === legacyProjectSettingsUpdateCommandSchemaVersion) {
assertLegacyValues(command.payload);
return;
}
if (
!isPlainObject(command.payload) ||
!isTrimmedString(command.payload.name, 1, 200) ||
!isNullableTrimmedString(command.payload.internalProjectNumber, 100) ||
!isNullableTrimmedString(command.payload.externalProjectNumber, 100) ||
!isNullableTrimmedString(command.payload.buildingOwner, 200) ||
!isNullableTrimmedString(command.payload.description, 2000) ||
!isPositiveFiniteNumber(command.payload.singlePhaseVoltageV) ||
!isPositiveFiniteNumber(command.payload.threePhaseVoltageV) ||
Object.keys(command.payload).length !== 2
Object.keys(command.payload).length !== 7
) {
throw new Error(
"Project settings update command contains invalid values."
);
}
}
function assertLegacyValues(
payload: Record<string, unknown>
): asserts payload is Record<string, unknown> & LegacyProjectSettingsValues {
if (
!isPositiveFiniteNumber(payload.singlePhaseVoltageV) ||
!isPositiveFiniteNumber(payload.threePhaseVoltageV) ||
Object.keys(payload).length !== 2
) {
throw new Error(
"Project settings update command contains invalid voltages."
@@ -59,3 +107,26 @@ function isPositiveFiniteNumber(value: unknown): value is number {
function isPlainObject(value: unknown): value is Record<string, unknown> {
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";
export const projectStateSnapshotSchemaVersion = 1 as const;
export const legacyProjectStateSnapshotSchemaVersion = 1 as const;
export const projectStateSnapshotSchemaVersion = 2 as const;
const idSchema = z.string().trim().min(1);
const nullableStringSchema = z.string().nullable();
const finiteNumberSchema = z.number().finite();
const projectSchema = z
const legacyProjectSchema = z
.object({
id: idSchema,
name: z.string().trim().min(1),
@@ -15,6 +16,13 @@ const projectSchema = z
})
.strict();
const projectSchema = legacyProjectSchema.extend({
internalProjectNumber: z.string().trim().min(1).max(100).nullable(),
externalProjectNumber: z.string().trim().min(1).max(100).nullable(),
buildingOwner: z.string().trim().min(1).max(200).nullable(),
description: z.string().trim().min(1).max(2000).nullable(),
});
const distributionBoardSchema = z
.object({
id: idSchema,
@@ -132,17 +140,29 @@ const roomSchema = z
})
.strict();
const projectStateSnapshotContents = {
distributionBoards: z.array(distributionBoardSchema),
circuitLists: z.array(circuitListSchema),
circuitSections: z.array(circuitSectionSchema),
circuits: z.array(circuitSchema),
projectDevices: z.array(projectDeviceSchema),
floors: z.array(floorSchema),
rooms: z.array(roomSchema),
};
const legacyProjectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(legacyProjectStateSnapshotSchemaVersion),
project: legacyProjectSchema,
...projectStateSnapshotContents,
})
.strict();
export const projectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
project: projectSchema,
distributionBoards: z.array(distributionBoardSchema),
circuitLists: z.array(circuitListSchema),
circuitSections: z.array(circuitSectionSchema),
circuits: z.array(circuitSchema),
projectDevices: z.array(projectDeviceSchema),
floors: z.array(floorSchema),
rooms: z.array(roomSchema),
...projectStateSnapshotContents,
})
.strict();
@@ -153,11 +173,37 @@ export type ProjectStateSnapshot = z.infer<
export function parseProjectStateSnapshot(
value: unknown
): ProjectStateSnapshot {
const snapshot = projectStateSnapshotSchema.parse(value);
const version = isPlainObject(value) ? value.schemaVersion : undefined;
const snapshot =
version === legacyProjectStateSnapshotSchemaVersion
? upgradeLegacyProjectStateSnapshot(
legacyProjectStateSnapshotSchema.parse(value)
)
: projectStateSnapshotSchema.parse(value);
assertProjectStateSnapshotRelations(snapshot);
return snapshot;
}
function upgradeLegacyProjectStateSnapshot(
snapshot: z.infer<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(
snapshot: ProjectStateSnapshot
): 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;
}
+4
View File
@@ -1,6 +1,10 @@
export interface ProjectDto {
id: string;
name: string;
internalProjectNumber: string | null;
externalProjectNumber: string | null;
buildingOwner: string | null;
description: string | null;
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
currentRevision: number;
+9 -1
View File
@@ -210,7 +210,15 @@ export function createProject(name: string) {
export function updateProjectSettings(
projectId: string,
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>(
`/api/projects/${projectId}`,
@@ -2,7 +2,11 @@ import { z } from "zod";
import { expectedProjectRevisionSchema } from "./project-command.schemas.js";
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(),
threePhaseVoltageV: z.number().positive().optional(),
});
@@ -10,6 +14,11 @@ export const createProjectSchema = z.object({
export const updateProjectSettingsSchema = z
.object({
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(),
threePhaseVoltageV: z.number().positive(),
})