'use client' import { useState } from 'react' import { useFetch } from '@/hooks/use-fetch' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { PageHeader } from '../primitives' import { formatMinutes } from '@/lib/format' import { cn } from '@/lib/utils' import { toast } from 'sonner' import { ChevronLeft, ChevronRight, Lock, Clock, TrendingUp, CheckCircle2, Calendar } from 'lucide-react' interface DayData { date: string minutes: number locked: boolean entries: { id: string; description: string; minutes: number; billable: boolean; locked: boolean }[] } interface GridRow { project: { id: string; name: string } days: DayData[] totalMinutes: number } interface TimesheetData { week: { start: string; end: string; label: string } grid: GridRow[] dayTotals: { date: string; label: string; dayNum: number; minutes: number }[] stats: { grandTotalMinutes: number; billableMinutes: number; lockedCount: number; totalEntries: number; allLocked: boolean; projectCount: number } } const WEEKDAY_LABELS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'] const WEEKEND = [5, 6] // Saturday, Sunday export function TimesheetView() { const now = new Date() const [weekParam, setWeekParam] = useState(`${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`) const { data, loading, refetch } = useFetch(`/api/aos/timesheet?week=${weekParam}`) const [locking, setLocking] = useState(false) const navigateWeek = (delta: number) => { const d = new Date(weekParam + 'T00:00:00Z') d.setDate(d.getDate() + delta * 7) setWeekParam(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`) } const lockWeek = async () => { setLocking(true) const t = toast.loading('Woche wird gesperrt (PM-Freigabe)…') try { const r = await fetch('/api/aos/timesheet', { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ week: weekParam }), }) const res = await r.json() if (!r.ok) throw new Error(res.error || 'lock failed') toast.success(`${res.locked} Einträge gesperrt (Audit protokolliert)`, { id: t }) refetch() } catch (err) { toast.error(err instanceof Error ? err.message : 'Sperren fehlgeschlagen', { id: t }) } finally { setLocking(false) } } if (loading || !data) { return (
) } const { grid, dayTotals, stats, week } = data return (
0 ? ( ) : stats.allLocked ? ( Gesperrt ) : undefined } /> {/* Week navigation */}
{week.label}
{/* KPIs */}

Gesamt

{formatMinutes(stats.grandTotalMinutes)}

Abrechenbar

{formatMinutes(stats.billableMinutes)}

Projekte

{stats.projectCount}

Gesperrt

{stats.lockedCount}/{stats.totalEntries}

{/* Timesheet grid */}
{dayTotals.map((d, i) => ( ))} {grid.length === 0 && ( )} {grid.map((row) => ( {row.days.map((day, i) => ( ))} ))} {dayTotals.map((d, i) => ( ))}
Projekt
{d.label} {d.dayNum}
Σ
Keine Zeiteinträge in dieser Woche. Erfasse Zeit unter „Projekte".

{row.project.name}

{day.minutes > 0 ? (
{Math.floor(day.minutes / 60)}:{String(day.minutes % 60).padStart(2, '0')} {day.locked && } {day.entries.length > 1 && {day.entries.length}× Einträge}
) : ( — )}
{Math.floor(row.totalMinutes / 60)}:{String(row.totalMinutes % 60).padStart(2, '0')}
Tagessumme {d.minutes > 0 ? `${Math.floor(d.minutes / 60)}:${String(d.minutes % 60).padStart(2, '0')}` : '—'} {Math.floor(stats.grandTotalMinutes / 60)}:{String(stats.grandTotalMinutes % 60).padStart(2, '0')}
{/* Entry details */} Einträge dieser Woche ({stats.totalEntries}) {grid.flatMap((row) => row.days.flatMap((day) => day.entries.map((e) => ({ ...e, projectName: row.project.name, date: day.date })))).map((e) => (
{e.locked ? : }

{e.description}

{e.projectName} · {new Date(e.date).toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: 'short' })}

{e.billable && billable} {formatMinutes(e.minutes)}
))} {stats.totalEntries === 0 &&

Keine Einträge.

}
{/* Lock info */}

Wochen-Sperre (PM-Freigabe)

Nach der Freigabe durch den Projektmanager werden alle Zeiteinträge der Woche gesperrt (locked=true). Gesperrte Einträge können nicht mehr geändert werden — GoBD-konform. Die Sperrung wird im Audit-Log protokolliert.

) }