Add versioned project settings modal

This commit is contained in:
Julian Appel 2026-07-28 18:16:22 +02:00
parent e1fc81a083
commit d77cfbeb49
22 changed files with 2563 additions and 111 deletions

View file

@ -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;
}

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;

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}`,