L5 Berichte & Unterschrift: Berichtsinhalt, Services, Unterschrift und PDF

ReportContent-Vertrag, Content-Builder mit Tagesfilter, Services für Tages-/Abschlussbericht,
Bearbeiten, Absenden, Freigabe, Zurückweisen, neue Version, Unterschrift und PDF-Erzeugung
(playwright-core, Worker-Processor, Dockerfile-Stage worker). Stubs für L2-Transition/Blocker
und Dokumenten-Store.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 12:22:40 +02:00
co-authored by Claude Opus 5
parent bf4456718e
commit 8a6fdd8f7a
26 changed files with 2129 additions and 3 deletions
+48
View File
@@ -0,0 +1,48 @@
import { ZodError } from "zod";
import { moduleGuard } from "@/server/action-guard";
import { ModuleDisabledError } from "@/server/modules";
import { ForbiddenError, type Permission } from "@/server/rbac";
import { ctxFromGuard, ServiceError, type ServiceCtx } from "@/server/services/context";
/**
* Thin adapter for /api/v1 report route handlers: module gate + DB-authoritative permissions → ServiceCtx,
* uniform JSON error mapping. (No shared requireApiContext exists yet; replace at merge if the architect adds one.)
*/
const guard = moduleGuard("reports");
const STATUS: Record<ServiceError["code"], number> = { not_found: 404, forbidden: 403, invalid: 400, conflict: 409, blocked: 422 };
export function apiError(err: unknown): Response {
if (err instanceof ServiceError) return Response.json({ error: err.code, details: err.details ?? null }, { status: STATUS[err.code] });
if (err instanceof ZodError) return Response.json({ error: "invalid", details: err.issues.map((i) => ({ path: i.path.join("."), code: i.code })) }, { status: 400 });
if (err instanceof ForbiddenError || err instanceof ModuleDisabledError) return Response.json({ error: "forbidden" }, { status: 403 });
if (err instanceof Error && /Nicht angemeldet|nicht aktiv|nicht mehr gueltig|Passwortwechsel/.test(err.message)) {
return Response.json({ error: "unauthorized" }, { status: 401 });
}
console.error("[api/reports]", err);
return Response.json({ error: "internal" }, { status: 500 });
}
export async function withReportsApi(permissions: Permission[], handler: (ctx: ServiceCtx) => Promise<Response>): Promise<Response> {
try {
const g = await guard(...permissions);
return await handler(ctxFromGuard(g));
} catch (err) {
return apiError(err);
}
}
export async function readJson(req: Request): Promise<Record<string, unknown>> {
const text = await req.text();
if (!text.trim()) return {};
try {
const v = JSON.parse(text);
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
} catch {
throw new ServiceError("invalid", "body must be JSON");
}
}
export function reportDto(r: { id: string; type: string; status: string; version: number; workOrderId: string; lineageId: string; pdfDocumentId?: string | null }) {
return { id: r.id, type: r.type, status: r.status, version: r.version, workOrderId: r.workOrderId, lineageId: r.lineageId, hasPdf: Boolean(r.pdfDocumentId) };
}