Files
craftvia/src/components/field/local-pending-times.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

65 lines
2.6 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useLocale, useTranslations } from "next-intl";
import { CloudOff } from "lucide-react";
import { fmtDateTime } from "@/lib/field/format";
import { listOutbox, subscribeOffline } from "@/lib/offline/outbox";
import { isPending } from "@/lib/offline/outbox-core";
import type { OutboxEntry } from "@/lib/offline/types";
import { card } from "./ui";
/**
* L12 optimistic view: manual time entries and „Für heute beenden" stored on this device but not
* yet transmitted (offline outbox) — shown with a pending badge until the server confirms them.
*/
export function LocalPendingTimes() {
const t = useTranslations("field");
const locale = useLocale();
const [ops, setOps] = useState<OutboxEntry[]>([]);
useEffect(() => {
let cancelled = false;
const load = () =>
void listOutbox()
.then((l) => {
if (!cancelled) setOps(l.ops.filter((o) => isPending(o) && (o.opType === "time.add_manual" || o.opType === "time.propose_correction" || o.opType === "session.stop_day")));
})
.catch(() => undefined);
load();
const unsubscribe = subscribeOffline(load);
return () => {
cancelled = true;
unsubscribe();
};
}, []);
if (!ops.length) return null;
return (
<section className={card} aria-label={t("myTime.localTitle")}>
<h2 className="flex items-center gap-2 text-[17px]">
<CloudOff className="size-5 text-muted-foreground" aria-hidden />
{t("myTime.localTitle")}
</h2>
<ul className="mt-2 divide-y">
{ops.map((o) => {
const p = o.payload as { type?: string; startedAt?: string; endedAt?: string; durationMinutes?: number; at?: string };
const label =
o.opType === "session.stop_day" ? t("action.stop_day") : o.opType === "time.propose_correction" ? t("time.propose") : `${t("myTime.add")} · ${t(`time.type.${p.type ?? "work"}`)}`;
const when = p.startedAt ?? p.at ?? o.clientCreatedAt;
return (
<li key={o.clientOpId} className="flex flex-wrap items-baseline justify-between gap-2 py-2.5 text-[15px]">
<span className="font-semibold">{label}</span>
<span className="text-[14px]">
{fmtDateTime(when, locale)}
{p.durationMinutes ? ` · ${p.durationMinutes} min` : ""}
</span>
<span className="inline-flex min-h-7 items-center rounded-lg bg-[color-mix(in_oklch,var(--warn)_14%,transparent)] px-2 text-[13px] font-semibold">{t("myTime.localPending")}</span>
</li>
);
})}
</ul>
</section>
);
}