Integrate print view and document branding

This commit is contained in:
Julian Appel 2026-09-27 10:32:07 +02:00
commit 4643c8d11a
59 changed files with 8852 additions and 75 deletions

View file

@ -0,0 +1,110 @@
"use client";
import {
distributionBoardInfoFieldLabels,
distributionBoardInfoFieldMaxLengths,
distributionBoardInfoFields,
type DistributionBoardInfoField,
} from "../../shared/constants/distribution-board";
import type { DistributionBoardInfoDto } from "../types";
export type DistributionBoardInfoDraft = Record<DistributionBoardInfoField, string>;
const placeholders: Record<DistributionBoardInfoField, string> = {
boardNumber: "z. B. UV 1.1",
location: "z. B. Raum 003 Technik",
fedFrom: "z. B. HV 0.1 oder HAK / Zähleranlage",
remark: "",
};
export function toDistributionBoardInfoDraft(
info: DistributionBoardInfoDto
): DistributionBoardInfoDraft {
return {
boardNumber: info.boardNumber ?? "",
location: info.location ?? "",
fedFrom: info.fedFrom ?? "",
remark: info.remark ?? "",
};
}
export function fromDistributionBoardInfoDraft(
draft: DistributionBoardInfoDraft
): DistributionBoardInfoDto {
const value = (field: DistributionBoardInfoField) =>
draft[field].trim() ? draft[field].trim() : null;
return {
boardNumber: value("boardNumber"),
location: value("location"),
fedFrom: value("fedFrom"),
remark: value("remark"),
};
}
/**
* Descriptive board data used by the print view title block. The values are
* local planning values; a later Revit import may prefill them only through
* an explicit, previewed decision.
*/
export function DistributionBoardInfoCard({
draft,
onChange,
}: {
draft: DistributionBoardInfoDraft;
onChange: (draft: DistributionBoardInfoDraft) => void;
}) {
return (
<section
aria-labelledby="distribution-board-info-heading"
className="card"
>
<div className="card-header" id="distribution-board-info-heading">
Infokarte
</div>
<div className="card-body row g-3">
{distributionBoardInfoFields.map((field) => (
<div
className={field === "remark" ? "col-12" : "col-12 col-md-6"}
key={field}
>
<label
className="form-label"
htmlFor={`distribution-board-info-${field}`}
>
{distributionBoardInfoFieldLabels[field]}
</label>
{field === "remark" ? (
<textarea
className="form-control"
id={`distribution-board-info-${field}`}
maxLength={distributionBoardInfoFieldMaxLengths[field]}
onChange={(event) =>
onChange({ ...draft, [field]: event.target.value })
}
rows={2}
value={draft[field]}
/>
) : (
<input
className="form-control"
id={`distribution-board-info-${field}`}
maxLength={distributionBoardInfoFieldMaxLengths[field]}
onChange={(event) =>
onChange({ ...draft, [field]: event.target.value })
}
placeholder={placeholders[field]}
value={draft[field]}
/>
)}
{field === "fedFrom" ? (
<div className="form-text">
Vorerst Freitext. Die logische Verknüpfung mit dem
speisenden Verteiler folgt.
</div>
) : null}
</div>
))}
</div>
</section>
);
}

View file

@ -7,9 +7,16 @@ import {
distributionBoardSupplyTypes,
type DistributionBoardSupplyType,
} from "../../shared/constants/distribution-board";
import {
projectCoverFieldLabels,
projectCoverFieldMaxLengths,
projectCoverFields,
type ProjectCoverField,
} from "../../shared/constants/project-cover";
import { FormModal } from "./form-modal";
export interface ProjectSettingsInput {
export interface ProjectSettingsInput
extends Record<ProjectCoverField, string | null> {
name: string;
internalProjectNumber: string | null;
externalProjectNumber: string | null;
@ -21,6 +28,13 @@ export interface ProjectSettingsInput {
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
}
const coverFieldPlaceholders: Record<ProjectCoverField, string> = {
siteAddress: "Straße Nr.\nPLZ Ort",
workStage: "z. B. LPH 5 – Ausführungsplanung",
projectLead: "",
projectEditor: "",
};
interface ProjectSettingsModalProps {
isSaving: boolean;
onClose: () => void;
@ -71,6 +85,14 @@ export function ProjectSettingsModal({
] = useState<DistributionBoardSupplyType[]>(
project.enabledDistributionBoardSupplyTypes
);
const [coverValues, setCoverValues] = useState<
Record<ProjectCoverField, string>
>({
siteAddress: project.siteAddress ?? "",
workStage: project.workStage ?? "",
projectLead: project.projectLead ?? "",
projectEditor: project.projectEditor ?? "",
});
const [transfer, setTransfer] = useState<unknown>(null);
const [transferFilename, setTransferFilename] = useState("");
const [importMode, setImportMode] = useState<"replace" | "duplicate">(
@ -91,6 +113,10 @@ export function ProjectSettingsModal({
singlePhaseVoltageV: Number(singlePhaseVoltageV),
threePhaseVoltageV: Number(threePhaseVoltageV),
enabledDistributionBoardSupplyTypes,
siteAddress: toNullableString(coverValues.siteAddress),
workStage: toNullableString(coverValues.workStage),
projectLead: toNullableString(coverValues.projectLead),
projectEditor: toNullableString(coverValues.projectEditor),
});
}
@ -309,6 +335,57 @@ export function ProjectSettingsModal({
value={threePhaseVoltageV}
/>
</div>
<div className="col-12">
<hr className="my-2" />
<h3 className="h6">Deckblatt</h3>
<p className="text-secondary small">
Angaben für das Deckblatt der Seitenansicht. Büroname und
Büroanschrift werden projektübergreifend in der Seitenansicht
gepflegt.
</p>
<div className="row g-3">
{projectCoverFields.map((field) => (
<div
className={field === "siteAddress" ? "col-12" : "col-12 col-md-6"}
key={field}
>
<label className="form-label" htmlFor={`project-cover-${field}`}>
{projectCoverFieldLabels[field]}
</label>
{field === "siteAddress" ? (
<textarea
className="form-control"
id={`project-cover-${field}`}
maxLength={projectCoverFieldMaxLengths[field]}
onChange={(event) =>
setCoverValues((current) => ({
...current,
[field]: event.target.value,
}))
}
placeholder={coverFieldPlaceholders[field]}
rows={2}
value={coverValues[field]}
/>
) : (
<input
className="form-control"
id={`project-cover-${field}`}
maxLength={projectCoverFieldMaxLengths[field]}
onChange={(event) =>
setCoverValues((current) => ({
...current,
[field]: event.target.value,
}))
}
placeholder={coverFieldPlaceholders[field]}
value={coverValues[field]}
/>
)}
</div>
))}
</div>
</div>
<div className="col-12">
<hr className="my-2" />
<h3 className="h6">Projekt importieren oder exportieren</h3>