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
+15
View File
@@ -14,6 +14,8 @@ import { ServiceError } from "@/server/services/context";
import { tenantTimeZone } from "@/server/services/reports/build-content";
import { getReportDetail } from "@/server/services/reports/queries";
import { readCtx } from "@/server/services/reports/read-ctx";
import { SendToCustomer } from "@/components/reports/send-to-customer";
import { defaultReportRecipient, listCustomerMailings } from "@/server/services/reports/send-to-customer";
/** /reports/[id] — structured view, PDF, versions, approve/reject/new version (reject as popup ?reject=1). */
export default async function ReportDetailPage({ params, searchParams }: { params: Promise<{ id: string }>; searchParams: Promise<{ reject?: string }> }) {
@@ -29,6 +31,14 @@ export default async function ReportDetailPage({ params, searchParams }: { param
const { report, content, versions, workOrder, permissions } = detail;
const dt = (d: Date | null) => (d ? format.dateTime(d, { dateStyle: "medium", timeStyle: "short", timeZone }) : null);
const base = `/reports/${id}`;
// L11: customer mail section — approved report with PDF, backoffice right (the action re-checks against the DB)
const customerMail =
report.status === "approved" && report.pdfDocumentId && ctx.permissions.has("report:approve")
? await Promise.all([defaultReportRecipient(ctx, report), listCustomerMailings(ctx, id)]).then(([defaultTo, rows]) => ({
defaultTo,
mailings: rows.map((m) => ({ id: m.id, to: m.to, status: m.status, version: m.version, when: dt(m.sentAt ?? m.createdAt) ?? "" })),
}))
: null;
const meta: Array<[string, string | null]> = [
[t("field.workOrder"), `${workOrder.number}`],
@@ -87,6 +97,11 @@ export default async function ReportDetailPage({ params, searchParams }: { param
<div className="mb-4 max-w-3xl">
<LotseReportPanel reportId={id} />
</div>
{customerMail && (
<div className="mb-4 max-w-3xl">
<SendToCustomer reportId={id} defaultTo={customerMail.defaultTo} mailings={customerMail.mailings} />
</div>
)}
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_280px]">
<ReportView content={content} reportId={id} timeZone={timeZone} />
+15
View File
@@ -0,0 +1,15 @@
import { requireApiContext } from "@/server/api/context";
import { json, readJsonObject, withApi } from "@/server/api/respond";
import { sendReportToCustomer } from "@/server/services/reports/send-to-customer";
/**
* POST /api/v1/reports/:id/send — e-mail the approved report PDF to the customer (L11).
* Body (optional): { to?: string, message?: string }. 202 queued, 200 sent/duplicate/failed.
*/
export const POST = withApi(async (req: Request, { params }: { params: Promise<{ id: string }> }) => {
const ctx = await requireApiContext("reports", "report:approve");
const { id } = await params;
const body = await readJsonObject(req, { allowEmpty: true });
const res = await sendReportToCustomer(ctx, { reportId: id, to: body.to, message: body.message });
return json(res, { status: res.status === "queued" ? 202 : 200 });
});