L8 Notdienst: Backoffice-Nachbearbeitung /work-orders/emergency-review

Liste und Detail mit fünf Prüfschritten (Kunde, Objekt, Auftrag, Bericht,
Abrechnung), Navigationseintrag, Dashboard-Kachel verlinkt auf die Prüfung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 13:12:33 +02:00
co-authored by Claude Opus 5
parent ef61754a0a
commit dece70c129
10 changed files with 532 additions and 1 deletions
+1
View File
@@ -66,6 +66,7 @@ const ENTITY_LABEL: Record<string, string> = {
import: "Auftragsimport",
report: "Bericht",
emergency: "Notdienst",
emergency_customer_search: "Notdienst-Kundensuche", emergency_site_lookup: "Notdienst-Objektauswahl",
signature: "Unterschrift",
import_job: "Auftragsimport",
material_usage: "Material",
+57
View File
@@ -0,0 +1,57 @@
"use client";
import { useActionState } from "react";
import { useTranslations } from "next-intl";
import { AlertCircle, CheckCircle2 } from "lucide-react";
import type { ActionState } from "@/server/api/action-state";
import { buttonCls } from "@/components/work-orders/action-form";
import { cn } from "@/lib/utils";
type Action = (prev: ActionState, fd: FormData) => Promise<ActionState>;
/** Form bound to an emergency review server action; errors are translated codes (emergency.review.errors.*). */
export function ReviewForm({
action,
children,
submitLabel,
successText,
variant = "default",
className,
}: {
action: Action;
children?: React.ReactNode;
submitLabel: string;
successText?: string;
variant?: "primary" | "default" | "outline" | "danger";
className?: string;
}) {
const [state, formAction, pending] = useActionState(action, { status: "idle" } as ActionState);
const t = useTranslations("emergency.review");
let errorText = "";
if (state.status === "error") {
const reasonKey = state.reason ? `errors.${state.reason}` : "";
errorText = reasonKey && t.has(reasonKey) ? t(reasonKey) : t.has(`errors.${state.code}`) ? t(`errors.${state.code}`) : t("errors.generic");
}
return (
<form action={formAction} className={cn("space-y-3", className)}>
{children}
{state.status === "error" && (
<p role="alert" className="flex items-center gap-2 rounded-lg border border-[var(--risk)] bg-card px-3 py-2 text-sm font-semibold text-[var(--risk)]">
<AlertCircle className="size-4 shrink-0" aria-hidden />
{errorText}
</p>
)}
{state.status === "ok" && successText && (
<p role="status" className="flex items-center gap-2 text-sm text-[var(--ok)]">
<CheckCircle2 className="size-4" aria-hidden />
{successText}
</p>
)}
<button type="submit" disabled={pending} className={buttonCls(variant)}>
{submitLabel}
</button>
</form>
);
}
@@ -0,0 +1,29 @@
import { CheckCircle2, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
/** "x von 5 Prüfschritten erledigt" — bar plus text (never colour alone). */
export function ReviewProgressBar({ count, total, label, className }: { count: number; total: number; label: string; className?: string }) {
const pct = total ? Math.round((count / total) * 100) : 0;
return (
<div className={cn("flex-1", className)}>
<p className="mb-1 text-xs font-semibold">{label}</p>
<div className="h-2 overflow-hidden rounded-full bg-muted" role="progressbar" aria-valuemin={0} aria-valuemax={total} aria-valuenow={count} aria-label={label}>
<div className="h-full rounded-full bg-[var(--ok)]" style={{ width: `${pct}%` }} />
</div>
</div>
);
}
export function StepState({ done, doneLabel, openLabel }: { done: boolean; doneLabel: string; openLabel: string }) {
return done ? (
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-[var(--ok)]">
<CheckCircle2 className="size-4" aria-hidden />
{doneLabel}
</span>
) : (
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-muted-foreground">
<Circle className="size-4" aria-hidden />
{openLabel}
</span>
);
}