"use server"; import { revalidatePath } from "next/cache"; import type { ReportActionState } from "@/lib/reports/action-state"; import { moduleGuard } from "@/server/action-guard"; import { ctxFromGuard, ServiceError } from "@/server/services/context"; import { requireVisibleReport } from "@/server/services/reports/common"; import { captureSignature } from "@/server/services/reports/signature"; // TODO(merge L4): signature PNG could go through POST /api/v1/uploads once available import { storeFile } from "@/server/services/documents/store"; import { errorState, okState, str } from "./_state"; const guard = moduleGuard("reports"); const MAX_PNG_BYTES = 2 * 1024 * 1024; /** * Capture signature or documented exception. Form fields: reportId, outcome, signerName, signerRole, reason, * confirmationText, signaturePng (data:image/png;base64,… from the signature pad; only for outcome=signed). */ export async function captureSignatureAction(_prev: ReportActionState, fd: FormData): Promise { try { const ctx = ctxFromGuard(await guard("report:write")); const reportId = str(fd, "reportId") ?? ""; const outcome = str(fd, "outcome") ?? ""; const report = await requireVisibleReport(ctx, reportId); let imageDocumentId: string | null = null; const png = str(fd, "signaturePng") ?? ""; if (outcome === "signed") { const m = /^data:image\/png;base64,([A-Za-z0-9+/=]+)$/.exec(png); if (!m) throw new ServiceError("invalid", "signature image required", { field: "image" }); const bytes = Buffer.from(m[1], "base64"); if (bytes.byteLength > MAX_PNG_BYTES) throw new ServiceError("invalid", "signature image too large", { field: "image" }); if (!str(fd, "signerName")?.trim()) throw new ServiceError("invalid", "signer name required", { field: "signerName" }); const wo = await ctx.db.workOrder.findFirstOrThrow({ where: { id: report.workOrderId }, select: { id: true, customerId: true, siteId: true } }); const doc = await storeFile(ctx, { bytes, fileName: `unterschrift-${report.id}.png`, declaredMime: "image/png", category: "signature", visibility: "customer_report", links: { customerId: wo.customerId, siteId: wo.siteId, workOrderId: wo.id }, }); imageDocumentId = doc.id; } try { await captureSignature(ctx, { reportId, outcome: outcome as Parameters[1]["outcome"], signerName: str(fd, "signerName"), signerRole: str(fd, "signerRole"), reason: str(fd, "reason"), confirmationText: str(fd, "confirmationText") ?? "", imageDocumentId, }); } catch (err) { if (imageDocumentId) await ctx.db.document.update({ where: { id: imageDocumentId }, data: { deletedAt: new Date() } }); throw err; } revalidatePath(`/m/orders/${report.workOrderId}/sign`); revalidatePath(`/m/orders/${report.workOrderId}/report`); revalidatePath(`/reports/${report.id}`); return okState(report.id); } catch (err) { return errorState(err); } }