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

@ -1,7 +1,9 @@
import type { Request, Response } from "express";
import type { DocumentBrandingAsset } from "../../db/schema/document-branding-assets.js";
import { officeProfileFields } from "../../shared/constants/project-cover.js";
import {
maxDocumentLogoBytes,
updateOfficeProfileSchema,
uploadDocumentLogoSchema,
type DocumentLogoMimeType,
} from "../../shared/validation/document-branding.schemas.js";
@ -11,7 +13,21 @@ const logoKey = "logo";
export function getDocumentBrandingHandler(_req: Request, res: Response) {
const logo = documentBrandingRepository.get(logoKey);
return res.json({ logo: logo ? toLogoMetadata(logo) : null });
return res.json({
logo: logo ? toLogoMetadata(logo) : null,
office: documentBrandingRepository.getSettings(officeProfileFields),
});
}
export function updateOfficeProfileHandler(req: Request, res: Response) {
const parsed = updateOfficeProfileSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.flatten() });
}
documentBrandingRepository.setSettings(parsed.data);
return res.json({
office: documentBrandingRepository.getSettings(officeProfileFields),
});
}
export function getDocumentLogoHandler(_req: Request, res: Response) {

View file

@ -4,6 +4,10 @@ import {
updateProjectSettingsSchema,
} from "../../shared/validation/project-structure.schemas.js";
import { createProjectSettingsUpdateProjectCommand } from "../../domain/models/project-settings-project-command.model.js";
import {
projectCoverFields,
type ProjectCoverField,
} from "../../shared/constants/project-cover.js";
import { projectCommandService } from "../composition/project-command-stores.js";
import { projectRepository } from "../composition/application-repositories.js";
import { respondWithProjectCommandError } from "./project-command.controller.js";
@ -46,12 +50,27 @@ export async function updateProjectSettings(req: Request, res: Response) {
}
const { expectedRevision, ...settings } = parsed.data;
const current = await projectRepository.findById(projectId);
if (!current) {
return res.status(404).json({ error: "Project not found" });
}
// Cover fields are optional in the request and keep their current value
// when omitted. `expectedRevision` rejects a project changed meanwhile.
const coverValues = Object.fromEntries(
projectCoverFields.map((field) => [
field,
settings[field] === undefined ? current[field] : settings[field],
])
) as Record<ProjectCoverField, string | null>;
try {
const result = projectCommandService.executeUser({
projectId,
expectedRevision,
description: "Projekteinstellungen bearbeiten",
command: createProjectSettingsUpdateProjectCommand(settings),
command: createProjectSettingsUpdateProjectCommand({
...settings,
...coverValues,
}),
});
const project = await projectRepository.findById(projectId);
if (!project) {

View file

@ -4,6 +4,7 @@ import * as documentBrandingController from "../controllers/document-branding.co
export const documentBrandingRouter = express.Router();
documentBrandingRouter.get("/", documentBrandingController.getDocumentBrandingHandler);
documentBrandingRouter.put("/office", documentBrandingController.updateOfficeProfileHandler);
documentBrandingRouter.get("/logo", documentBrandingController.getDocumentLogoHandler);
documentBrandingRouter.put("/logo", documentBrandingController.uploadDocumentLogoHandler);
documentBrandingRouter.delete("/logo", documentBrandingController.deleteDocumentLogoHandler);