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
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { ChangeEvent, useEffect, useMemo, useState } from "react";
|
||||
import { ChangeEvent, FormEvent, useEffect, useMemo, useState } from "react";
|
||||
import "../../../../frontend/print/print-document.css";
|
||||
import {
|
||||
deleteDocumentLogo,
|
||||
|
|
@ -14,13 +14,20 @@ import {
|
|||
listDistributionBoards,
|
||||
listFloors,
|
||||
listProjectSnapshots,
|
||||
updateOfficeProfile,
|
||||
uploadDocumentLogo,
|
||||
} from "../../../../frontend/utils/api";
|
||||
import type {
|
||||
DocumentLogoMetadataDto,
|
||||
OfficeProfileDto,
|
||||
ProjectDto,
|
||||
ProjectSnapshotMetadataDto,
|
||||
} from "../../../../frontend/types";
|
||||
import { FormModal } from "../../../../frontend/components/form-modal";
|
||||
import {
|
||||
officeProfileFieldLabels,
|
||||
officeProfileFieldMaxLengths,
|
||||
} from "../../../../shared/constants/project-cover";
|
||||
import { PrintDocument } from "../../../../frontend/print/print-document";
|
||||
import {
|
||||
buildPreliminaryBoardHierarchy,
|
||||
|
|
@ -30,7 +37,7 @@ import {
|
|||
import {
|
||||
documentLogoMimeTypes,
|
||||
maxDocumentLogoBytes,
|
||||
} from "../../../../shared/validation/document-branding.schemas";
|
||||
} from "../../../../shared/constants/document-branding";
|
||||
|
||||
type PlanFormat = "a4" | "a3";
|
||||
|
||||
|
|
@ -61,6 +68,14 @@ export default function ProjectPrintPage() {
|
|||
const projectId = params.projectId;
|
||||
const [data, setData] = useState<PrintData | null>(null);
|
||||
const [logo, setLogo] = useState<DocumentLogoMetadataDto | null>(null);
|
||||
const [office, setOffice] = useState<OfficeProfileDto>({
|
||||
officeName: null,
|
||||
officeAddress: null,
|
||||
});
|
||||
const [officeDraft, setOfficeDraft] = useState<{
|
||||
officeName: string;
|
||||
officeAddress: string;
|
||||
} | null>(null);
|
||||
const [format, setFormat] = useState<PlanFormat>("a4");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
|
@ -116,6 +131,7 @@ export default function ProjectPrintPage() {
|
|||
.sort((left, right) => left.createdAtIso.localeCompare(right.createdAtIso)),
|
||||
});
|
||||
setLogo(branding.logo);
|
||||
setOffice(branding.office);
|
||||
document.title = `Leistungsbilanz – ${project.name}`;
|
||||
}
|
||||
load().catch((err: unknown) => {
|
||||
|
|
@ -172,6 +188,28 @@ export default function ProjectPrintPage() {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleOfficeSave(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!officeDraft) {
|
||||
return;
|
||||
}
|
||||
setIsBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await updateOfficeProfile({
|
||||
officeName: officeDraft.officeName.trim() || null,
|
||||
officeAddress: officeDraft.officeAddress.trim() || null,
|
||||
});
|
||||
setOffice(result.office);
|
||||
setOfficeDraft(null);
|
||||
setMessage("Büroangaben gespeichert. Sie gelten für alle Projekte.");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Die Büroangaben konnten nicht gespeichert werden.");
|
||||
} finally {
|
||||
setIsBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogoDelete() {
|
||||
if (!window.confirm("Hinterlegtes Logo für alle Projekte entfernen?")) {
|
||||
return;
|
||||
|
|
@ -213,6 +251,19 @@ export default function ProjectPrintPage() {
|
|||
<option value="a3">A3 quer</option>
|
||||
</select>
|
||||
</label>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
disabled={isBusy}
|
||||
onClick={() =>
|
||||
setOfficeDraft({
|
||||
officeName: office.officeName ?? "",
|
||||
officeAddress: office.officeAddress ?? "",
|
||||
})
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
Büroangaben
|
||||
</button>
|
||||
<label className={`btn btn-sm btn-outline-primary mb-0${isBusy ? " disabled" : ""}`}>
|
||||
{logo ? "Logo ersetzen" : "Logo hinterlegen"}
|
||||
<input
|
||||
|
|
@ -245,11 +296,56 @@ export default function ProjectPrintPage() {
|
|||
Zum Projekt
|
||||
</Link>
|
||||
</div>
|
||||
{officeDraft ? (
|
||||
<FormModal
|
||||
description="Planverfasser auf dem Deckblatt. Gilt für alle Projekte."
|
||||
isSaving={isBusy}
|
||||
onClose={() => setOfficeDraft(null)}
|
||||
onSubmit={handleOfficeSave}
|
||||
submitLabel="Büroangaben speichern"
|
||||
title="Büroangaben"
|
||||
>
|
||||
<div className="row g-3">
|
||||
<div className="col-12">
|
||||
<label className="form-label" htmlFor="office-name">
|
||||
{officeProfileFieldLabels.officeName}
|
||||
</label>
|
||||
<input
|
||||
autoFocus
|
||||
className="form-control"
|
||||
id="office-name"
|
||||
maxLength={officeProfileFieldMaxLengths.officeName}
|
||||
onChange={(event) =>
|
||||
setOfficeDraft({ ...officeDraft, officeName: event.target.value })
|
||||
}
|
||||
value={officeDraft.officeName}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label className="form-label" htmlFor="office-address">
|
||||
{officeProfileFieldLabels.officeAddress}
|
||||
</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
id="office-address"
|
||||
maxLength={officeProfileFieldMaxLengths.officeAddress}
|
||||
onChange={(event) =>
|
||||
setOfficeDraft({ ...officeDraft, officeAddress: event.target.value })
|
||||
}
|
||||
placeholder={"Straße Nr.\nPLZ Ort"}
|
||||
rows={3}
|
||||
value={officeDraft.officeAddress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FormModal>
|
||||
) : null}
|
||||
{data ? (
|
||||
<PrintDocument
|
||||
entries={entries}
|
||||
logoSrc={logoSrc}
|
||||
namedSnapshots={data.namedSnapshots}
|
||||
office={office}
|
||||
printedAt={printedAt}
|
||||
project={data.project}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue