leistungsbilanz-ts/src/server/controllers/project.controller.ts
Grovy311 7692699ee2 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>
2026-09-26 10:39:18 +02:00

83 lines
2.9 KiB
TypeScript

import type { Request, Response } from "express";
import {
createProjectSchema,
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";
export async function listProjects(_req: Request, res: Response) {
const result = await projectRepository.list();
res.json(result);
}
export async function createProject(req: Request, res: Response) {
const parsed = createProjectSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.flatten() });
}
const project = await projectRepository.create(parsed.data);
return res.status(201).json(project);
}
export async function getProject(req: Request, res: Response) {
const { projectId } = req.params;
if (typeof projectId !== "string") {
return res.status(400).json({ error: "Invalid projectId" });
}
const row = await projectRepository.findById(projectId);
if (!row) {
return res.status(404).json({ error: "Project not found" });
}
return res.json(row);
}
export async function updateProjectSettings(req: Request, res: Response) {
const { projectId } = req.params;
if (typeof projectId !== "string") {
return res.status(400).json({ error: "Invalid projectId" });
}
const parsed = updateProjectSettingsSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.flatten() });
}
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,
...coverValues,
}),
});
const project = await projectRepository.findById(projectId);
if (!project) {
throw new Error("Updated project could not be loaded.");
}
return res.json({ ...result, project });
} catch (error) {
return respondWithProjectCommandError(error, res);
}
}