- Aufträge: createWorkOrder/transitionWorkOrder/getCompletionBlockers aus L2 - Dokumente: storeFile aus L1; neu services/documents/read.ts (readStoredBytes, readDocumentBytes mit Prüfsummenprüfung); L2-Upload nutzt zentrale Ablage - Dubletten aus L1 (findDuplicateCustomers(ctx)); Objekt-Kandidaten als imports/site-candidates.ts; Dateityp-Erkennung mobil als field/mime.ts - Objekt-Historie mobil als Adapter auf L1 getSiteHistory, PDF über /api/v1/reports/:id/pdf - L4-Upload-Idempotenz: fester Upload-Lineage nach storeFile, Race → Soft-Delete + Replay - PDF-Worker-Kontext: document:write zum Ablegen des Berichts-PDF - Import: doppeltes Work-Order-Audit entfernt; bestätigte Aufträge starten planned - Dateilinks in Auftragsdetail auf /files/[documentId] - proxy: /api/v1 ohne Session → 401 JSON statt Login-Redirect - ARCHITEKTUR §2: Objekt-Historie für Feldrollen (freigegebene Einsätze aller Teams) Gate: tsc, lint, build, 42/42 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
69 lines
3.0 KiB
TypeScript
69 lines
3.0 KiB
TypeScript
"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<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);
|
|
}
|
|
}
|