'use client' import React, { useState, useEffect, useCallback } from 'react' import { HourEntry, HoursSummary, HOUR_CATEGORIES, apiFetch, formatDate, currentMonth, monthLabel, prevMonth, nextMonth, } from './types' import { Skeleton, Modal, Badge, HoursBar, FormLabel, FormInput, FormTextarea, FormSelect, PrimaryButton, SecondaryButton, ErrorState, EmptyState, IconClock, IconPlus, } from './ui-primitives' export function ZeiterfassungTab({ assignmentId, monthlyBudget, addToast, }: { assignmentId: string monthlyBudget: number addToast: (msg: string, type?: 'success' | 'error') => void }) { const [hours, setHours] = useState([]) const [summary, setSummary] = useState(null) const [month, setMonth] = useState(currentMonth()) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [showModal, setShowModal] = useState(false) const [saving, setSaving] = useState(false) const [formDate, setFormDate] = useState(new Date().toISOString().slice(0, 10)) const [formHours, setFormHours] = useState('1') const [formCategory, setFormCategory] = useState(HOUR_CATEGORIES[0]) const [formDesc, setFormDesc] = useState('') const [formBillable, setFormBillable] = useState(true) const fetchData = useCallback(async () => { setLoading(true); setError('') try { const [hoursData, summaryData] = await Promise.all([ apiFetch(`/api/sdk/v1/dsb/assignments/${assignmentId}/hours?month=${month}`), apiFetch(`/api/sdk/v1/dsb/assignments/${assignmentId}/hours/summary?month=${month}`), ]) setHours(hoursData); setSummary(summaryData) } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Fehler beim Laden der Zeiterfassung') } finally { setLoading(false) } }, [assignmentId, month]) useEffect(() => { fetchData() }, [fetchData]) const handleLogHours = async (e: React.FormEvent) => { e.preventDefault(); setSaving(true) try { await apiFetch(`/api/sdk/v1/dsb/assignments/${assignmentId}/hours`, { method: 'POST', body: JSON.stringify({ date: formDate, hours: parseFloat(formHours), category: formCategory, description: formDesc, billable: formBillable, }), }) addToast('Stunden erfasst'); setShowModal(false) setFormDate(new Date().toISOString().slice(0, 10)) setFormHours('1'); setFormCategory(HOUR_CATEGORIES[0]) setFormDesc(''); setFormBillable(true); fetchData() } catch (e: unknown) { addToast(e instanceof Error ? e.message : 'Fehler', 'error') } finally { setSaving(false) } } const maxCatHours = summary ? Math.max(...Object.values(summary.by_category), 1) : 1 return (
{/* Toolbar */}
{monthLabel(month)}
setShowModal(true)} className="flex items-center gap-1.5"> Stunden erfassen
{loading ? (
) : error ? ( ) : (
{/* Summary cards */} {summary && (

Gesamt-Stunden

{summary.total_hours}h

Abrechnungsfaehig

{summary.billable_hours}h

{summary.total_hours > 0 ? `${Math.round((summary.billable_hours / summary.total_hours) * 100)}% der Gesamtstunden` : 'Keine Stunden erfasst'}

Budget verbleibend

{Math.max(monthlyBudget - summary.total_hours, 0)}h

von {monthlyBudget}h Monatsbudget

)} {/* Hours by category */} {summary && Object.keys(summary.by_category).length > 0 && (

Stunden nach Kategorie

{Object.entries(summary.by_category).sort(([, a], [, b]) => b - a).map(([cat, h]) => (
{cat}
{h}h
))}
)} {/* Hours table */} {hours.length === 0 ? ( } title="Keine Stunden erfasst" description={`Fuer ${monthLabel(month)} wurden noch keine Stunden erfasst.`} /> ) : (
{hours.map((entry) => ( ))}
Datum Stunden Kategorie Beschreibung Abrechenbar
{formatDate(entry.date)} {entry.hours}h {entry.description || '-'} {entry.billable ? Ja : Nein}
)}
)} {/* Log hours modal */} setShowModal(false)} title="Stunden erfassen">
Datum *
Stunden *
Kategorie ({ value: c, label: c }))} />
Beschreibung
setFormBillable(e.target.checked)} className="rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
setShowModal(false)}>Abbrechen {saving ? 'Erfasse...' : 'Stunden erfassen'}
) }