L5 Berichte & Unterschrift: Server Actions und API v1
Actions mit moduleGuard("reports"); POST daily-report/completion-report, POST approve,
GET pdf sowie Dateiauslieferung für im Bericht referenzierte Dokumente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
"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/reports/_stubs/documents";
|
||||
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<ReportActionState> {
|
||||
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<typeof captureSignature>[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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user