import { useState } from 'react'; import { Pencil, Trash2, Plus } from 'lucide-react'; import { toast } from 'sonner'; import { clsx } from 'clsx'; import { trpc } from '../../lib/trpc'; import { Card } from '../ui/Card'; import { Button } from '../ui/Button'; import { Modal } from '../ui/Modal'; import { formatDuration, formatDurationCompact } from '../../lib/format-duration'; import { TIME_CATEGORIES, type TimeCategory } from '../../lib/time-categories'; const CATEGORY_LABEL: Record = Object.fromEntries( TIME_CATEGORIES.map((c) => [c.value, c.label]), ); function pad(n: number): string { return String(n).padStart(2, '0'); } function toLocalInput(d: Date): string { return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; } function fromLocalInput(s: string): Date { return new Date(s); } export function TimelineSection() { const listQuery = trpc.time.listToday.useQuery(undefined, { refetchInterval: 30_000 }); const todayQuery = trpc.dayEntry.today.useQuery(); const [manualOpen, setManualOpen] = useState(false); const [editing, setEditing] = useState(null); if (listQuery.isPending) return null; // Aufsteigend sortieren: Vormittag oben, Abend unten. const entries = [...(listQuery.data?.entries ?? [])].sort( (a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime(), ); const totalSeconds = listQuery.data?.totalSeconds ?? 0; const todoTitles = new Map((todayQuery.data?.todos ?? []).map((t) => [t.id, t.title])); return (

Zeit heute

Gesamt: {formatDurationCompact(totalSeconds)}

{entries.length === 0 ? (

Noch keine Zeit erfasst. Starte einen Timer oder trag einen Block nach.

) : ( {entries.map((e) => ( setEditing(e.id)} onCancelEdit={() => setEditing(null)} onSaved={() => setEditing(null)} /> ))} )} {manualOpen && ( setManualOpen(false)} todos={todayQuery.data?.todos ?? []} /> )}
); } type TimeEntryRow = { id: string; startedAt: string | Date; endedAt: string | Date | null; durationSeconds: number | null; todoId: string | null; todoTitle?: string | null; category: string; description: string | null; }; function TimelineRow({ entry, todoTitle, todos, isEditing, onEdit, onCancelEdit, onSaved, }: { entry: TimeEntryRow; todoTitle?: string; todos: Array<{ id: string; title: string }>; isEditing: boolean; onEdit: () => void; onCancelEdit: () => void; onSaved: () => void; }) { const utils = trpc.useUtils(); const deleteMutation = trpc.time.delete.useMutation({ onSuccess: async () => { await utils.time.listToday.invalidate(); await utils.time.now.invalidate(); toast('Eintrag gelöscht.'); }, }); const updateMutation = trpc.time.update.useMutation({ onSuccess: async () => { await utils.time.listToday.invalidate(); onSaved(); toast('Aktualisiert.'); }, }); const start = new Date(entry.startedAt); const end = entry.endedAt ? new Date(entry.endedAt) : null; const duration = end ? (entry.durationSeconds ?? Math.round((end.getTime() - start.getTime()) / 1000)) : Math.round((Date.now() - start.getTime()) / 1000); const label = todoTitle || entry.description || CATEGORY_LABEL[entry.category] || 'Eintrag'; const isRunning = !end; const [editStart, setEditStart] = useState(toLocalInput(start)); const [editEnd, setEditEnd] = useState(end ? toLocalInput(end) : ''); const [editDescription, setEditDescription] = useState(entry.description ?? ''); const [editCategory, setEditCategory] = useState(entry.category as TimeCategory); const [editTodoId, setEditTodoId] = useState(entry.todoId ?? ''); if (isEditing) { return (
); } return (
{start.toDateString() !== new Date().toDateString() && ( {pad(start.getDate())}.{pad(start.getMonth() + 1)}. )} {pad(start.getHours())}:{pad(start.getMinutes())} {end && ( <> {' – '} {pad(end.getHours())}:{pad(end.getMinutes())} )}

{label}

{CATEGORY_LABEL[entry.category]} {entry.description && !todoTitle && entry.description !== label && ` · ${entry.description}`}

{formatDuration(duration)}
{!isRunning && (
)}
); } export function ManualDialog({ onClose, todos, defaultTodoId, }: { onClose: () => void; todos: Array<{ id: string; title: string }>; defaultTodoId?: string; }) { const now = new Date(); const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); const [startedAt, setStartedAt] = useState(toLocalInput(oneHourAgo)); const [endedAt, setEndedAt] = useState(toLocalInput(now)); const [todoId, setTodoId] = useState(defaultTodoId ?? ''); const [category, setCategory] = useState('focus'); const [description, setDescription] = useState(''); const utils = trpc.useUtils(); const mutation = trpc.time.upsertManual.useMutation({ onSuccess: async () => { await utils.time.listToday.invalidate(); onClose(); toast('Block nachgetragen.'); }, }); function submit(e: React.FormEvent) { e.preventDefault(); mutation.mutate({ startedAt: fromLocalInput(startedAt).toISOString(), endedAt: fromLocalInput(endedAt).toISOString(), todoId: todoId || undefined, category, description: description.trim() || undefined, }); } return (
); }