forked from jappel/leistungsbilanz-ts
Add print cover fields and office profile
- Project settings gain siteAddress, workStage, projectLead and projectEditor (project.update-settings schema version 3, migration 0009); versions 2 and 1 stay executable and keep the current cover values. Snapshot schema 6 carries the fields; older versions upgrade with null. - Global office profile (office name/address) next to the document logo: table document_branding_settings, PUT /api/document-branding/office, editable from the print view toolbar. - Cover page reads all fields; empty values show the "[ergänzen]" marker. The Planstände editor column shows the project editor. - Shared logo constants moved to a dependency-free module so the Next.js bundler can import them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4779e4a1a7
commit
7692699ee2
35 changed files with 3479 additions and 113 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/* Print view (Seitenansicht) in the provisional kbp corporate design.
|
||||
Colors are derived from the kbp logo and presentation material and will be
|
||||
replaced by the official CD specification; change them only here.
|
||||
/* Print view (Seitenansicht) in the kbp corporate design. Colors are
|
||||
derived from the kbp logo and presentation material; change them only
|
||||
here.
|
||||
All selectors are scoped to .print-view. The @page rules (format, margins,
|
||||
page numbers) are injected by the print page because they need project
|
||||
data. */
|
||||
|
|
@ -113,6 +113,9 @@
|
|||
color: var(--pd-accent);
|
||||
font-weight: bold;
|
||||
}
|
||||
.pd-multiline {
|
||||
white-space: pre-line;
|
||||
}
|
||||
.pd-logo {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type {
|
|||
CircuitTreeCircuitDto,
|
||||
CircuitTreeComponentDto,
|
||||
CircuitTreeDeviceRowDto,
|
||||
OfficeProfileDto,
|
||||
ProjectDto,
|
||||
ProjectSnapshotMetadataDto,
|
||||
} from "../types";
|
||||
|
|
@ -18,17 +19,23 @@ import {
|
|||
type PrintBoardHierarchyEntry,
|
||||
} from "./print-document-model";
|
||||
|
||||
// Provisional office data until the project/cover fields are added.
|
||||
export const printOfficeName = "kbp köhler beraten + planen GmbH";
|
||||
|
||||
export interface PrintDocumentProps {
|
||||
project: ProjectDto;
|
||||
office: OfficeProfileDto;
|
||||
entries: PrintBoardHierarchyEntry[];
|
||||
namedSnapshots: ProjectSnapshotMetadataDto[];
|
||||
logoSrc: string | null;
|
||||
printedAt: Date;
|
||||
}
|
||||
|
||||
/** Shows a value or the orange "[ergänzen]" marker for missing data. */
|
||||
function Value({ value, multiline = false }: { value: string | null; multiline?: boolean }) {
|
||||
if (!value) {
|
||||
return <Placeholder />;
|
||||
}
|
||||
return multiline ? <span className="pd-multiline">{value}</span> : <>{value}</>;
|
||||
}
|
||||
|
||||
const dateFormat = new Intl.DateTimeFormat("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
|
|
@ -129,6 +136,7 @@ function boardLabel(entry: PrintBoardHierarchyEntry) {
|
|||
|
||||
function CoverSheet({
|
||||
project,
|
||||
office,
|
||||
entries,
|
||||
namedSnapshots,
|
||||
logoSrc,
|
||||
|
|
@ -149,11 +157,11 @@ function CoverSheet({
|
|||
<caption>Projekt</caption>
|
||||
<tbody>
|
||||
<tr><th>Bauvorhaben</th><td>{project.name}</td></tr>
|
||||
<tr><th>Anschrift</th><td><Placeholder /></td></tr>
|
||||
<tr><th>Bauherr</th><td>{project.buildingOwner ?? <Placeholder />}</td></tr>
|
||||
<tr><th>Projektnummer intern</th><td>{project.internalProjectNumber ?? <Placeholder />}</td></tr>
|
||||
<tr><th>Projektnummer extern</th><td>{project.externalProjectNumber ?? <Placeholder />}</td></tr>
|
||||
<tr><th>Leistungsphase</th><td><Placeholder /></td></tr>
|
||||
<tr><th>Anschrift</th><td><Value multiline value={project.siteAddress} /></td></tr>
|
||||
<tr><th>Bauherr</th><td><Value value={project.buildingOwner} /></td></tr>
|
||||
<tr><th>Projektnummer intern</th><td><Value value={project.internalProjectNumber} /></td></tr>
|
||||
<tr><th>Projektnummer extern</th><td><Value value={project.externalProjectNumber} /></td></tr>
|
||||
<tr><th>Leistungsphase</th><td><Value value={project.workStage} /></td></tr>
|
||||
<tr><th>Gewerk</th><td>KG 440 – Starkstromanlagen</td></tr>
|
||||
<tr><th>Beschreibung</th><td>{project.description ?? "–"}</td></tr>
|
||||
</tbody>
|
||||
|
|
@ -163,10 +171,10 @@ function CoverSheet({
|
|||
<table className="pd-kv">
|
||||
<caption>Planverfasser</caption>
|
||||
<tbody>
|
||||
<tr><th>Büro</th><td>{printOfficeName}</td></tr>
|
||||
<tr><th>Anschrift</th><td><Placeholder /></td></tr>
|
||||
<tr><th>Projektleitung</th><td><Placeholder /></td></tr>
|
||||
<tr><th>Bearbeitung</th><td><Placeholder /></td></tr>
|
||||
<tr><th>Büro</th><td><Value value={office.officeName} /></td></tr>
|
||||
<tr><th>Anschrift</th><td><Value multiline value={office.officeAddress} /></td></tr>
|
||||
<tr><th>Projektleitung</th><td><Value value={project.projectLead} /></td></tr>
|
||||
<tr><th>Bearbeitung</th><td><Value value={project.projectEditor} /></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table className="pd-kv">
|
||||
|
|
@ -192,7 +200,9 @@ function CoverSheet({
|
|||
<td>{String.fromCharCode(97 + (index % 26))}</td>
|
||||
<td>{dateFormat.format(new Date(snapshot.createdAtIso))}</td>
|
||||
<td>{snapshot.name}</td>
|
||||
<td><Placeholder /></td>
|
||||
{/* Snapshots do not record an editor yet; the project's
|
||||
current editor is shown for every entry. */}
|
||||
<td><Value value={project.projectEditor} /></td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
|
|
@ -582,6 +592,7 @@ function SectionRows({
|
|||
|
||||
export function PrintDocument({
|
||||
project,
|
||||
office,
|
||||
entries,
|
||||
namedSnapshots,
|
||||
logoSrc,
|
||||
|
|
@ -594,6 +605,7 @@ export function PrintDocument({
|
|||
entries={entries}
|
||||
logoSrc={logoSrc}
|
||||
namedSnapshots={namedSnapshots}
|
||||
office={office}
|
||||
project={project}
|
||||
stand={stand}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -93,9 +93,18 @@ export interface ProjectDto {
|
|||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
|
||||
siteAddress: string | null;
|
||||
workStage: string | null;
|
||||
projectLead: string | null;
|
||||
projectEditor: string | null;
|
||||
currentRevision: number;
|
||||
}
|
||||
|
||||
export interface OfficeProfileDto {
|
||||
officeName: string | null;
|
||||
officeAddress: string | null;
|
||||
}
|
||||
|
||||
export interface ProjectHistoryStateDto {
|
||||
projectId: string;
|
||||
currentRevision: number;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type {
|
|||
DistributionBoardInfoDto,
|
||||
DocumentLogoMetadataDto,
|
||||
FloorDto,
|
||||
OfficeProfileDto,
|
||||
GlobalDeviceDto,
|
||||
ProjectCommandDto,
|
||||
ProjectCommandResultDto,
|
||||
|
|
@ -258,7 +259,7 @@ export function updateProjectSettings(
|
|||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
enabledDistributionBoardSupplyTypes: ProjectDto["enabledDistributionBoardSupplyTypes"];
|
||||
}
|
||||
} & Partial<Pick<ProjectDto, "siteAddress" | "workStage" | "projectLead" | "projectEditor">>
|
||||
) {
|
||||
return request<ProjectSettingsCommandResultDto>(
|
||||
`/api/projects/${projectId}`,
|
||||
|
|
@ -346,9 +347,17 @@ export function updateDistributionBoard(
|
|||
export const documentLogoUrl = "/api/document-branding/logo";
|
||||
|
||||
export function getDocumentBranding() {
|
||||
return request<{ logo: DocumentLogoMetadataDto | null }>(
|
||||
"/api/document-branding"
|
||||
);
|
||||
return request<{
|
||||
logo: DocumentLogoMetadataDto | null;
|
||||
office: OfficeProfileDto;
|
||||
}>("/api/document-branding");
|
||||
}
|
||||
|
||||
export function updateOfficeProfile(office: OfficeProfileDto) {
|
||||
return request<{ office: OfficeProfileDto }>("/api/document-branding/office", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(office),
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadDocumentLogo(file: File) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue