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>
65 lines
2.6 KiB
TypeScript
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>
|
|
);
|
|
}
|