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
9
src/shared/constants/document-branding.ts
Normal file
9
src/shared/constants/document-branding.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export const documentLogoMimeTypes = [
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/svg+xml",
|
||||
] as const;
|
||||
|
||||
export type DocumentLogoMimeType = (typeof documentLogoMimeTypes)[number];
|
||||
|
||||
export const maxDocumentLogoBytes = 2 * 1024 * 1024;
|
||||
45
src/shared/constants/project-cover.ts
Normal file
45
src/shared/constants/project-cover.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Project data shown on the print view cover page. They are ordinary project
|
||||
* settings (persisted through `project.update-settings`, part of logical
|
||||
* snapshots and project transfer).
|
||||
*/
|
||||
export const projectCoverFields = [
|
||||
"siteAddress",
|
||||
"workStage",
|
||||
"projectLead",
|
||||
"projectEditor",
|
||||
] as const;
|
||||
|
||||
export type ProjectCoverField = (typeof projectCoverFields)[number];
|
||||
|
||||
export const projectCoverFieldLabels: Record<ProjectCoverField, string> = {
|
||||
siteAddress: "Anschrift Bauvorhaben",
|
||||
workStage: "Leistungsphase",
|
||||
projectLead: "Projektleitung",
|
||||
projectEditor: "Bearbeitung",
|
||||
};
|
||||
|
||||
export const projectCoverFieldMaxLengths: Record<ProjectCoverField, number> = {
|
||||
siteAddress: 500,
|
||||
workStage: 100,
|
||||
projectLead: 200,
|
||||
projectEditor: 200,
|
||||
};
|
||||
|
||||
/**
|
||||
* Office-wide data for the cover page ("Planverfasser"). Global branding like
|
||||
* the document logo, outside project revisions and snapshots.
|
||||
*/
|
||||
export const officeProfileFields = ["officeName", "officeAddress"] as const;
|
||||
|
||||
export type OfficeProfileField = (typeof officeProfileFields)[number];
|
||||
|
||||
export const officeProfileFieldLabels: Record<OfficeProfileField, string> = {
|
||||
officeName: "Büro",
|
||||
officeAddress: "Anschrift Büro",
|
||||
};
|
||||
|
||||
export const officeProfileFieldMaxLengths: Record<OfficeProfileField, number> = {
|
||||
officeName: 200,
|
||||
officeAddress: 500,
|
||||
};
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import { z } from "zod";
|
||||
import {
|
||||
documentLogoMimeTypes,
|
||||
maxDocumentLogoBytes,
|
||||
} from "../constants/document-branding.js";
|
||||
import { officeProfileFieldMaxLengths } from "../constants/project-cover.js";
|
||||
|
||||
export const documentLogoMimeTypes = [
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/svg+xml",
|
||||
] as const;
|
||||
|
||||
export type DocumentLogoMimeType = (typeof documentLogoMimeTypes)[number];
|
||||
|
||||
export const maxDocumentLogoBytes = 2 * 1024 * 1024;
|
||||
export {
|
||||
documentLogoMimeTypes,
|
||||
maxDocumentLogoBytes,
|
||||
type DocumentLogoMimeType,
|
||||
} from "../constants/document-branding.js";
|
||||
|
||||
export const uploadDocumentLogoSchema = z
|
||||
.object({
|
||||
|
|
@ -23,3 +24,19 @@ export const uploadDocumentLogoSchema = z
|
|||
.strict();
|
||||
|
||||
export type UploadDocumentLogoInput = z.infer<typeof uploadDocumentLogoSchema>;
|
||||
|
||||
// Empty text is stored as "not set" (null).
|
||||
const officeValueSchema = (maximumLength: number) =>
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.max(maximumLength)
|
||||
.nullable()
|
||||
.transform((value) => (value ? value : null));
|
||||
|
||||
export const updateOfficeProfileSchema = z
|
||||
.object({
|
||||
officeName: officeValueSchema(officeProfileFieldMaxLengths.officeName),
|
||||
officeAddress: officeValueSchema(officeProfileFieldMaxLengths.officeAddress),
|
||||
})
|
||||
.strict();
|
||||
|
|
|
|||
|
|
@ -4,8 +4,22 @@ import {
|
|||
distributionBoardSupplyTypes,
|
||||
type DistributionBoardInfoField,
|
||||
} from "../constants/distribution-board.js";
|
||||
import {
|
||||
projectCoverFieldMaxLengths,
|
||||
type ProjectCoverField,
|
||||
} from "../constants/project-cover.js";
|
||||
import { expectedProjectRevisionSchema } from "./project-command.schemas.js";
|
||||
|
||||
// Empty text is stored as null so "not entered" has one representation.
|
||||
const optionalCoverValueSchema = (field: ProjectCoverField) =>
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.max(projectCoverFieldMaxLengths[field])
|
||||
.nullable()
|
||||
.transform((value) => (value ? value : null))
|
||||
.optional();
|
||||
|
||||
export const createProjectSchema = z.object({
|
||||
name: z.string().trim().min(1).max(200),
|
||||
internalProjectNumber: z.string().trim().max(100).optional(),
|
||||
|
|
@ -34,6 +48,11 @@ export const updateProjectSettingsSchema = z
|
|||
.refine((values) => new Set(values).size === values.length, {
|
||||
message: "Netzarten dürfen nicht mehrfach ausgewählt werden.",
|
||||
}),
|
||||
// Print cover fields; omitted fields keep their current value.
|
||||
siteAddress: optionalCoverValueSchema("siteAddress"),
|
||||
workStage: optionalCoverValueSchema("workStage"),
|
||||
projectLead: optionalCoverValueSchema("projectLead"),
|
||||
projectEditor: optionalCoverValueSchema("projectEditor"),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue