L11 Kundenversand: Bericht-PDF an Kunden senden (Service, Action, API, Berichtsseite)

- sendReportToCustomer: report:approve, Scope, nur approved mit PDF, Empfänger
  Ansprechpartner -> Kunde, Dedupe je Adresse+Version, Audit export
- Server Action + POST /api/v1/reports/{id}/send inkl. OpenAPI-Eintrag
- /reports/[id]: Abschnitt „An Kunden senden" mit bisherigen Versänden, Texte de/en

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 18:48:57 +02:00
co-authored by Claude Opus 5
parent e0106b8f8e
commit 29c80a0e73
9 changed files with 413 additions and 0 deletions
+27
View File
@@ -1015,6 +1015,33 @@ const paths: Record<string, Record<string, Schema>> = {
responses: { "200": binaryResponse("Datei"), ...errors("not_found") },
}),
},
"/reports/{id}/send": {
post: op({
tag: "Berichte",
operationId: "sendReportToCustomer",
summary: "Freigegebenes Berichts-PDF per E-Mail an den Kunden senden",
description:
"Nur Status `approved` mit PDF (sonst 422 `blocked`, details.reason `report_not_approved` | `pdf_missing`). Empfänger: `to` oder Ansprechpartner des Auftrags → Kunde; keiner → 422 `invalid` (details.reason `recipient_missing`). Gleiche Adresse + Version → `duplicate` (keine zweite Mail). Das PDF wird als Dokument-Referenz eingestellt und beim Zustellen mandantengebunden geladen und per SHA-256 geprüft. Audit `export`.",
module: "reports",
permissions: ["report:approve"],
parameters: [idParam("des Berichts")],
requestBody: jsonBody(obj({ to: str({ format: "email", maxLength: 254 }), message: str({ maxLength: 2000 }) }), false),
responses: {
"200": jsonResponse(
"Versendet, bereits versendet oder Zustellung fehlgeschlagen",
obj(
{ status: str({ enum: ["sent", "duplicate", "failed"] }), reportId: str(), to: str({ format: "email" }), version: int(), mailLogId: str() },
["status", "reportId", "to", "version"],
),
),
"202": jsonResponse(
"In die Mail-Queue eingestellt",
obj({ status: str({ const: "queued" }), reportId: str(), to: str({ format: "email" }), version: int(), mailLogId: str() }, ["status", "reportId", "to", "version"]),
),
...errors("not_found", "unprocessable"),
},
}),
},
"/sync": {
post: op({
tag: "Einsatz",