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:
Grovy311 2026-09-25 20:42:44 +02:00
parent 4779e4a1a7
commit 7692699ee2
35 changed files with 3479 additions and 113 deletions

View file

@ -3,13 +3,23 @@ import {
distributionBoardSupplyTypes,
type DistributionBoardSupplyType,
} from "../../shared/constants/distribution-board.js";
import {
projectCoverFieldMaxLengths,
projectCoverFields,
type ProjectCoverField,
} from "../../shared/constants/project-cover.js";
export const projectSettingsUpdateCommandType =
"project.update-settings" as const;
export const projectSettingsUpdateCommandSchemaVersion = 2 as const;
const previousProjectSettingsUpdateCommandSchemaVersion = 1 as const;
// Version 3 adds the print cover fields. Versions 2 (without cover fields)
// and 1 (additionally without the public-building flag) stay executable;
// their missing values keep the current project values.
export const projectSettingsUpdateCommandSchemaVersion = 3 as const;
export const withoutCoverProjectSettingsUpdateCommandSchemaVersion = 2 as const;
export const baselineProjectSettingsUpdateCommandSchemaVersion = 1 as const;
export interface ProjectSettingsValues {
export interface ProjectSettingsValues
extends Record<ProjectCoverField, string | null> {
name: string;
internalProjectNumber: string | null;
externalProjectNumber: string | null;
@ -21,8 +31,13 @@ export interface ProjectSettingsValues {
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
}
type PreviousProjectSettingsValues = Omit<
export type WithoutCoverProjectSettingsValues = Omit<
ProjectSettingsValues,
ProjectCoverField
>;
export type BaselineProjectSettingsValues = Omit<
WithoutCoverProjectSettingsValues,
"isPublicBuilding"
>;
@ -32,15 +47,22 @@ export interface CurrentProjectSettingsUpdateProjectCommand
type: typeof projectSettingsUpdateCommandType;
}
interface PreviousProjectSettingsUpdateProjectCommand
extends SerializedProjectCommand<PreviousProjectSettingsValues> {
schemaVersion: typeof previousProjectSettingsUpdateCommandSchemaVersion;
export interface WithoutCoverProjectSettingsUpdateProjectCommand
extends SerializedProjectCommand<WithoutCoverProjectSettingsValues> {
schemaVersion: typeof withoutCoverProjectSettingsUpdateCommandSchemaVersion;
type: typeof projectSettingsUpdateCommandType;
}
export interface BaselineProjectSettingsUpdateProjectCommand
extends SerializedProjectCommand<BaselineProjectSettingsValues> {
schemaVersion: typeof baselineProjectSettingsUpdateCommandSchemaVersion;
type: typeof projectSettingsUpdateCommandType;
}
export type ProjectSettingsUpdateProjectCommand =
| CurrentProjectSettingsUpdateProjectCommand
| PreviousProjectSettingsUpdateProjectCommand;
| WithoutCoverProjectSettingsUpdateProjectCommand
| BaselineProjectSettingsUpdateProjectCommand;
export function createProjectSettingsUpdateProjectCommand(
values: ProjectSettingsValues
@ -60,7 +82,10 @@ export function assertProjectSettingsUpdateProjectCommand(
if (
command.type !== projectSettingsUpdateCommandType ||
(command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion &&
command.schemaVersion !== previousProjectSettingsUpdateCommandSchemaVersion)
command.schemaVersion !==
withoutCoverProjectSettingsUpdateCommandSchemaVersion &&
command.schemaVersion !==
baselineProjectSettingsUpdateCommandSchemaVersion)
) {
throw new Error("Unsupported project settings update command.");
}
@ -91,13 +116,23 @@ export function assertProjectSettingsUpdateProjectCommand(
"Project settings update command contains invalid supply types."
);
}
const isPreviousSchema =
command.schemaVersion === previousProjectSettingsUpdateCommandSchemaVersion;
const payload = command.payload;
const expectedKeyCount =
command.schemaVersion === baselineProjectSettingsUpdateCommandSchemaVersion
? 8
: command.schemaVersion ===
withoutCoverProjectSettingsUpdateCommandSchemaVersion
? 9
: 9 + projectCoverFields.length;
if (
(isPreviousSchema && Object.keys(command.payload).length !== 8) ||
(!isPreviousSchema &&
(typeof command.payload.isPublicBuilding !== "boolean" ||
Object.keys(command.payload).length !== 9))
Object.keys(payload).length !== expectedKeyCount ||
(command.schemaVersion !==
baselineProjectSettingsUpdateCommandSchemaVersion &&
typeof payload.isPublicBuilding !== "boolean") ||
(command.schemaVersion === projectSettingsUpdateCommandSchemaVersion &&
!projectCoverFields.every((field) =>
isNullableTrimmedString(payload[field], projectCoverFieldMaxLengths[field])
))
) {
throw new Error(
"Project settings update command contains invalid values."

View file

@ -4,6 +4,10 @@ import {
distributionBoardSupplyTypes,
type DistributionBoardInfoField,
} from "../../shared/constants/distribution-board.js";
import {
projectCoverFieldMaxLengths,
type ProjectCoverField,
} from "../../shared/constants/project-cover.js";
import {
circuitGroupCategories,
} from "../../shared/constants/circuit-group.js";
@ -63,10 +67,30 @@ const previousProjectSchema = z
})
.strict();
const projectSchema = previousProjectSchema
const publicBuildingProjectSchema = previousProjectSchema
.extend({ isPublicBuilding: z.boolean() })
.strict();
const projectCoverValueSchema = (field: ProjectCoverField) =>
z.string().trim().min(1).max(projectCoverFieldMaxLengths[field]).nullable();
// Schema version 6 adds the print cover fields.
const projectSchema = publicBuildingProjectSchema
.extend({
siteAddress: projectCoverValueSchema("siteAddress"),
workStage: projectCoverValueSchema("workStage"),
projectLead: projectCoverValueSchema("projectLead"),
projectEditor: projectCoverValueSchema("projectEditor"),
})
.strict();
const emptyProjectCoverValues = {
siteAddress: null,
workStage: null,
projectLead: null,
projectEditor: null,
};
const legacyDistributionBoardSchema = z
.object({
id: idSchema,
@ -494,7 +518,7 @@ export const projectStateSnapshotSchema = z
const boardInfoProjectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(boardInfoProjectStateSnapshotSchemaVersion),
project: projectSchema,
project: publicBuildingProjectSchema,
distributionBoards: z.array(legacyDistributionBoardSchema),
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
externalModel: externalModelStateSchema,
@ -505,7 +529,7 @@ const boardInfoProjectStateSnapshotSchema = z
const previousProjectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(previousProjectStateSnapshotSchemaVersion),
project: projectSchema,
project: publicBuildingProjectSchema,
distributionBoards: z.array(legacyDistributionBoardSchema),
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
externalModel: externalModelStateSchema,
@ -516,7 +540,7 @@ const previousProjectStateSnapshotSchema = z
const externalModelProjectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(externalModelProjectStateSnapshotSchemaVersion),
project: projectSchema,
project: publicBuildingProjectSchema,
distributionBoards: z.array(legacyDistributionBoardSchema),
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
...projectStateSnapshotContents,
@ -526,7 +550,7 @@ const externalModelProjectStateSnapshotSchema = z
const legacyProjectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(legacyProjectStateSnapshotSchemaVersion),
project: projectSchema,
project: publicBuildingProjectSchema,
distributionBoards: z.array(legacyDistributionBoardSchema),
...projectStateSnapshotContents,
})
@ -581,12 +605,14 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
}
const schemaVersion = (value as { schemaVersion: number }).schemaVersion;
// Every supported version before 6 lacks the distribution-board info
// fields; they are upgraded as empty (null) values.
// fields and the project cover fields; they are upgraded as empty (null)
// values.
if (schemaVersion === boardInfoProjectStateSnapshotSchemaVersion) {
const previous = boardInfoProjectStateSnapshotSchema.parse(value);
return {
...previous,
schemaVersion: projectStateSnapshotSchemaVersion,
project: { ...previous.project, ...emptyProjectCoverValues },
distributionBoards: withEmptyDistributionBoardInfo(
previous.distributionBoards
),
@ -597,6 +623,7 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
return {
...previous,
schemaVersion: projectStateSnapshotSchemaVersion,
project: { ...previous.project, ...emptyProjectCoverValues },
distributionBoards: withEmptyDistributionBoardInfo(
previous.distributionBoards
),
@ -607,6 +634,7 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
return {
...previous,
schemaVersion: projectStateSnapshotSchemaVersion,
project: { ...previous.project, ...emptyProjectCoverValues },
distributionBoards: withEmptyDistributionBoardInfo(
previous.distributionBoards
),
@ -618,6 +646,7 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
return {
...legacy,
schemaVersion: projectStateSnapshotSchemaVersion,
project: { ...legacy.project, ...emptyProjectCoverValues },
distributionBoards: withEmptyDistributionBoardInfo(
legacy.distributionBoards
),
@ -630,7 +659,11 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
return {
...baseline,
schemaVersion: projectStateSnapshotSchemaVersion,
project: { ...baseline.project, isPublicBuilding: false },
project: {
...baseline.project,
isPublicBuilding: false,
...emptyProjectCoverValues,
},
distributionBoards: withEmptyDistributionBoardInfo(
baseline.distributionBoards
),