Files
craftvia/src/components/field/switch-session-sheet.tsx
T
msolarczekandClaude Opus 5 fe3eba6e44 L12 Zeiterfassung: Mobile Uhr-Leiste, Meine Zeiten, Freigaben und Backoffice-Liste
Laufende-Uhr-Leiste auf allen /m-Seiten, Start/Pause direkt auf den Auftragskarten,
Auto-Wechsel-Dialog, Auftragsdetail mit Pause / Für heute beenden / Abschließen-Link
und Segmentwechseln, /m/time (Tagesübersicht, Nachtragen, Korrektur vorschlagen),
/m/approvals für Teamleiter, /work-orders/time-approvals mit Sammelfreigabe,
Zeiten-Tab mit Badges und Inline-Freigabe, Dashboard-Kachel, Texte de/en.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:29:38 +02:00

107 lines
4.2 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { ArrowLeftRight, LoaderCircle } from "lucide-react";
import type { SyncOpResult } from "@/lib/sync/envelope";
import { isSuccess, newClientId, submitOp } from "@/lib/field/client-ops";
import { currentPosition } from "@/lib/field/image";
import { otherSessionOf, type OtherSession } from "@/lib/field/time-rules";
import { btnPrimary, btnSecondary } from "./ui";
type StartKind = "travel" | "work" | "resume";
async function startPayload(workOrderId: string, mode: "travel" | "work", switchFromOther: boolean) {
const pos = await currentPosition(3000);
return {
workOrderId,
mode,
clientId: newClientId(),
at: new Date().toISOString(),
offline: typeof navigator !== "undefined" ? !navigator.onLine : false,
deviceInfo: typeof navigator !== "undefined" ? navigator.userAgent.slice(0, 200) : undefined,
...(switchFromOther ? { switchFromOther: true } : {}),
...(pos ?? {}),
};
}
/** Sends session.start / session.resume (optionally with the auto switch flag). */
export async function submitStart(workOrderId: string, kind: StartKind, switchFromOther = false): Promise<SyncOpResult> {
if (kind === "resume") {
return submitOp({ opType: "session.resume", payload: { workOrderId, at: new Date().toISOString(), ...(switchFromOther ? { switchFromOther: true } : {}) } });
}
return submitOp({ opType: "session.start", payload: await startPayload(workOrderId, kind, switchFromOther) });
}
/**
* L12 auto switch: start/resume work; when another order is still on the clock the server answers
* `other_session_running` and the bottom sheet asks „A-00041 läuft noch. Pausieren und A-00042 starten?".
*/
export function useSessionStart(workOrderId: string, number: string, onDone?: (result: SyncOpResult) => void) {
const router = useRouter();
const [busy, setBusy] = useState<StartKind | null>(null);
const [pending, setPending] = useState<{ kind: StartKind; other: OtherSession } | null>(null);
const [result, setResult] = useState<SyncOpResult | null>(null);
async function start(kind: StartKind, switchFromOther = false) {
setBusy(kind);
const r = await submitStart(workOrderId, kind, switchFromOther);
setBusy(null);
const other = otherSessionOf(r);
if (other && !switchFromOther) {
setPending({ kind, other });
return r;
}
setPending(null);
setResult(r);
onDone?.(r);
if (isSuccess(r)) router.refresh();
return r;
}
const sheet = pending ? (
<SwitchSessionSheet
from={pending.other.number}
to={number}
busy={busy !== null}
onConfirm={() => start(pending.kind, true)}
onCancel={() => setPending(null)}
/>
) : null;
return { start, busy, result, sheet };
}
export function SwitchSessionSheet({ from, to, busy, onConfirm, onCancel }: { from: string; to: string; busy: boolean; onConfirm: () => void; onCancel: () => void }) {
const t = useTranslations("field.switch");
return (
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/40" onClick={onCancel}>
<div
role="dialog"
aria-modal="true"
aria-labelledby="switch-session-title"
className="w-full max-w-xl space-y-4 rounded-t-2xl bg-card p-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] shadow-card"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-start gap-3">
<ArrowLeftRight className="mt-1 size-6 shrink-0 text-primary" aria-hidden />
<div>
<h2 id="switch-session-title" className="text-[19px]">
{t("title")}
</h2>
<p className="mt-1 text-[16px]">{from ? t("text", { from, to }) : t("textUnknown", { to })}</p>
</div>
</div>
<button type="button" className={btnPrimary} disabled={busy} onClick={onConfirm} autoFocus>
{busy ? <LoaderCircle className="size-5 animate-spin" aria-hidden /> : <ArrowLeftRight className="size-5" aria-hidden />}
{t("confirm")}
</button>
<button type="button" className={btnSecondary} disabled={busy} onClick={onCancel}>
{t("cancel")}
</button>
</div>
</div>
);
}