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>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user