import { useEffect, useState } from 'react'; import { clsx } from 'clsx'; import { trpc } from '../../lib/trpc'; import { Card } from '../ui/Card'; import { formatDuration, formatDurationCompact } from '../../lib/format-duration'; import { TIME_CATEGORIES } from '../../lib/time-categories'; const CATEGORY_LABEL: Record = Object.fromEntries( TIME_CATEGORIES.map((c) => [c.value, c.label]), ); // Tönungen pro Kategorie (für den horizontalen Balken). const CATEGORY_TINT: Record = { focus: 'bg-[var(--color-ink)]', meeting: 'bg-[var(--color-ampel-amber-fg)]', admin: 'bg-[var(--color-ink-muted)]', learning: 'bg-[var(--color-ampel-green-fg)]', break: 'bg-[var(--color-ink-faint)]', other: 'bg-[var(--color-border-strong)]', }; function initials(name: string) { return name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((p) => p[0]?.toUpperCase() ?? '') .join(''); } /** Anwesenheit (Lock-in → Feierabend bzw. jetzt) — reine Anzeige, kein Tracking. */ function presenceSecondsOf(it: { checkedInAt: string | null; checkedOutAt: string | null; }): number | null { if (!it.checkedInAt) return null; const endMs = it.checkedOutAt ? new Date(it.checkedOutAt).getTime() : Date.now(); return Math.max(0, Math.round((endMs - new Date(it.checkedInAt).getTime()) / 1000)); } export function TeamTimeSection() { const team = trpc.time.teamToday.useQuery(undefined, { refetchInterval: 30_000 }); const users = trpc.user.list.useQuery(); // 1-Sekunden-Tick für lebendige aktive Timer. const [tick, setTick] = useState(0); useEffect(() => { const id = window.setInterval(() => setTick((t) => t + 1), 1000); return () => window.clearInterval(id); }, []); void tick; if (team.isPending || users.isPending) return null; const userMap = new Map((users.data ?? []).map((u) => [u.id, u.name])); const items = (team.data ?? []) .map((b) => ({ ...b, name: userMap.get(b.userId) ?? '—' })) .sort((a, b) => b.totalSeconds - a.totalSeconds); return (

Zeit im Team heute

{items.length} {items.length === 1 ? 'Person trackt' : 'Personen tracken'}

{items.length === 0 ? (

Heute hat noch niemand Zeit erfasst.

) : (
{items.map((it) => (
{initials(it.name)}

{it.name}

Gesamt:{' '} {formatDurationCompact(it.totalSeconds)} {it.trackedSeconds > it.totalSeconds + 60 && ( {' · getrackt '} {formatDurationCompact(it.trackedSeconds)} )} {presenceSecondsOf(it) !== null && ( {' · Anwesend: '} {formatDurationCompact(presenceSecondsOf(it)!)} )}

{it.active ? : }
))}
)}
); } function ActiveLine({ active, }: { active: { startedAt: string; category: string; todoTitle: string | null; description: string | null; }; }) { const since = new Date(active.startedAt).getTime(); const elapsed = Math.max(0, Math.round((Date.now() - since) / 1000)); const label = active.todoTitle || active.description || CATEGORY_LABEL[active.category] || 'läuft'; return (
{label} {formatDuration(elapsed)}
); } function IdleLine() { return

Aktuell kein Timer.

; } function Breakdown({ byCategory, total }: { byCategory: Record; total: number }) { if (total === 0) return null; const ordered = TIME_CATEGORIES.map((c) => ({ key: c.value, label: c.label, seconds: byCategory[c.value] ?? 0, })).filter((x) => x.seconds > 0); return (
{ordered.map((o) => (
))}
{ordered.map((o) => ( {o.label} {formatDurationCompact(o.seconds)} ))}
); }