L5 Berichte & Unterschrift: Backoffice- und Mobil-Oberflächen

/reports mit Filtern (zur Prüfung zuerst), /reports/[id] mit Aktionen, Versionen und PDF-Link;
mobile Komponenten für Bericht, Prüfung und Unterschrift inkl. Signature-Pad; Texte de/en.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 12:22:40 +02:00
co-authored by Claude Opus 5
parent 8f53df6208
commit 94092ade3f
20 changed files with 1805 additions and 3 deletions
@@ -0,0 +1,72 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { getFormatter, getTranslations } from "next-intl/server";
import { ArrowLeft } from "lucide-react";
import { REPORT_EDITABLE, type ReportStatus } from "@/lib/reports/content";
import { can, ServiceError } from "@/server/services/context";
import { getMobileReportState } from "@/server/services/reports/queries";
import { readCtx } from "@/server/services/reports/read-ctx";
import { ReportStatusBadge } from "../status-badge";
import { StepIndicator } from "../step-indicator";
import { SignFlow } from "./sign-flow";
/** /m/orders/[id]/sign — signature or documented reason, then submit the completion report. */
export async function SignScreen({ workOrderId }: { workOrderId: string }) {
const t = await getTranslations("reports");
const format = await getFormatter();
const ctx = await readCtx();
let state: Awaited<ReturnType<typeof getMobileReportState>>;
try {
state = await getMobileReportState(ctx, workOrderId, "completion");
} catch (err) {
if (err instanceof ServiceError && err.code === "not_found") notFound();
throw err;
}
const { workOrder, report, content, timeZone } = state;
const base = `/m/orders/${workOrderId}`;
const steps = [t("mobile.stepReview"), t("mobile.stepEdit"), t("mobile.stepSign"), t("mobile.stepSubmit")];
return (
<main className="mx-auto w-full max-w-2xl flex-1 space-y-4 p-4">
<Link href={`${base}/report?type=completion`} className="inline-flex min-h-11 items-center gap-1.5 text-[13px] font-semibold text-muted-foreground hover:text-foreground">
<ArrowLeft className="size-4" aria-hidden />
{t("mobile.review")}
</Link>
<header>
<p className="text-xs text-muted-foreground">
{workOrder.number} · {workOrder.title}
</p>
<h1 className="text-[22px]">{t("sign.title")}</h1>
</header>
{!report || !content ? (
<p className="shadow-card rounded-xl border bg-card p-4 text-[14px]">
{t("sign.noReport")}{" "}
<Link href={`${base}/report?type=completion`} className="font-semibold underline">
{t("mobile.createCompletion")}
</Link>
</p>
) : (
<>
<StepIndicator steps={steps} current={3} label={t("mobile.stepOf", { current: 3, total: steps.length })} />
<div className="flex flex-wrap items-center gap-2">
<ReportStatusBadge status={report.status as ReportStatus} label={t(`status.${report.status}`)} />
<span className="text-[13px] text-muted-foreground">
{content.reportNumber} · {t("field.version")} {report.version}
</span>
</div>
<SignFlow
reportId={report.id}
reportNumber={content.reportNumber}
orderNumber={workOrder.number}
dateLabel={format.dateTime(new Date(), { dateStyle: "medium", timeZone })}
canNotRequired={!workOrder.signatureRequired || can(ctx, "report:approve")}
existing={content.signature ? { outcome: content.signature.outcome, signerName: content.signature.signerName, reason: content.signature.reason } : null}
editable={REPORT_EDITABLE.includes(report.status as ReportStatus)}
doneHref={`${base}/report?type=completion`}
/>
</>
)}
</main>
);
}