import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, Pressable, RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native'; import { router, useLocalSearchParams } from 'expo-router'; import { createStaffTimeEvent, fetchStaffTimeSpans, StaffTimeSpan, submitStaffTime, } from '@/src/lib/api'; import { useAuth } from '@/src/providers/auth-provider'; const PRIMARY = '#69c350'; function formatDateTime(value: string | null): string { if (!value) return '-'; const d = new Date(value); return d.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit', }); } function formatDuration(minutes: number): string { const h = Math.floor(minutes / 60); const m = minutes % 60; return `${h}h ${String(m).padStart(2, '0')}m`; } function getStateLabel(state: string): string { if (state === 'approved') return 'Genehmigt'; if (state === 'submitted') return 'Eingereicht'; if (state === 'rejected') return 'Abgelehnt'; if (state === 'factual') return 'Faktisch'; return 'Entwurf'; } function getTypeLabel(type: string): string { if (type === 'vacation') return 'Urlaub'; if (type === 'sick') return 'Krankheit'; if (type === 'holiday') return 'Feiertag'; if (type === 'other') return 'Sonstiges'; return 'Arbeitszeit'; } export default function TimeTrackingScreen() { const { token, user } = useAuth(); const params = useLocalSearchParams<{ action?: string | string[] }>(); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [actionLoading, setActionLoading] = useState(false); const [error, setError] = useState(null); const currentUserId = useMemo(() => (user?.id ? String(user.id) : null), [user]); const handledActionRef = useRef(null); const active = useMemo(() => entries.find((entry) => !entry.stopped_at) || null, [entries]); const incomingAction = useMemo(() => { const raw = Array.isArray(params.action) ? params.action[0] : params.action; return String(raw || '').toLowerCase(); }, [params.action]); const load = useCallback( async (showSpinner = true) => { if (!token || !currentUserId) return; if (showSpinner) setLoading(true); setError(null); try { const spans = await fetchStaffTimeSpans(token, currentUserId); setEntries(spans); } catch (err) { setError(err instanceof Error ? err.message : 'Zeiten konnten nicht geladen werden.'); } finally { setLoading(false); setRefreshing(false); } }, [currentUserId, token] ); useEffect(() => { if (!currentUserId || !token) return; void load(true); }, [currentUserId, load, token]); async function onRefresh() { setRefreshing(true); await load(false); } const onStart = useCallback(async () => { if (!token || !currentUserId) return; setActionLoading(true); setError(null); try { await createStaffTimeEvent(token, { eventtype: 'work_start', eventtime: new Date().toISOString(), user_id: currentUserId, description: 'Arbeitszeit gestartet', }); await load(false); } catch (err) { setError(err instanceof Error ? err.message : 'Starten fehlgeschlagen.'); } finally { setActionLoading(false); } }, [currentUserId, token, load]); const onStop = useCallback(async () => { if (!token || !currentUserId || !active) return; setActionLoading(true); setError(null); try { await createStaffTimeEvent(token, { eventtype: 'work_end', eventtime: new Date().toISOString(), user_id: currentUserId, }); await load(false); } catch (err) { setError(err instanceof Error ? err.message : 'Stoppen fehlgeschlagen.'); } finally { setActionLoading(false); } }, [active, currentUserId, token, load]); async function onSubmit(entry: StaffTimeSpan) { if (!token || !entry.eventIds?.length) return; setActionLoading(true); setError(null); try { await submitStaffTime(token, entry.eventIds); await load(false); } catch (err) { setError(err instanceof Error ? err.message : 'Einreichen fehlgeschlagen.'); } finally { setActionLoading(false); } } const onSubmitAll = useCallback(async () => { if (!token) return; const submitCandidates = entries.filter( (entry) => (entry.state === 'draft' || entry.state === 'factual') && !!entry.stopped_at && !!entry.eventIds?.length ); if (submitCandidates.length === 0) return; setActionLoading(true); setError(null); try { for (const entry of submitCandidates) { await submitStaffTime(token, entry.eventIds); } await load(false); } catch (err) { setError(err instanceof Error ? err.message : 'Einreichen fehlgeschlagen.'); } finally { setActionLoading(false); } }, [entries, load, token]); useEffect(() => { if (!token || !currentUserId) return; if (!incomingAction) return; if (handledActionRef.current === incomingAction) return; if (incomingAction === 'start' && !active) { handledActionRef.current = incomingAction; void onStart().finally(() => router.replace('/(tabs)/time')); return; } if (incomingAction === 'stop' && active) { handledActionRef.current = incomingAction; void onStop().finally(() => router.replace('/(tabs)/time')); return; } if (incomingAction === 'submit') { handledActionRef.current = incomingAction; void onSubmitAll().finally(() => router.replace('/(tabs)/time')); return; } handledActionRef.current = incomingAction; void router.replace('/(tabs)/time'); }, [active, currentUserId, incomingAction, onStart, onStop, onSubmitAll, token]); return ( }> Aktive Zeit {active ? `Läuft seit ${formatDateTime(active.started_at)}` : 'Keine aktive Zeit'} {active ? ( Stop ) : ( Start )} {error ? {error} : null} {loading ? ( Zeiten werden geladen... ) : null} {!loading && entries.length === 0 ? Keine Zeiteinträge vorhanden. : null} {!loading && entries.map((entry) => { const canSubmit = (entry.state === 'draft' || entry.state === 'factual') && !!entry.stopped_at; return ( {getTypeLabel(entry.type)} {getStateLabel(entry.state)} Start: {formatDateTime(entry.started_at)} Ende: {entry.stopped_at ? formatDateTime(entry.stopped_at) : 'läuft...'} Dauer: {formatDuration(entry.duration_minutes)} {entry.description ? {entry.description} : null} {canSubmit ? ( onSubmit(entry)} disabled={actionLoading}> Einreichen ) : null} ); })} ); } const styles = StyleSheet.create({ container: { padding: 16, gap: 12, backgroundColor: '#f9fafb', }, statusCard: { backgroundColor: '#ffffff', borderRadius: 12, padding: 12, borderWidth: 1, borderColor: '#e5e7eb', gap: 8, }, statusLabel: { color: '#6b7280', fontSize: 12, textTransform: 'uppercase', }, statusValue: { color: '#111827', fontSize: 16, fontWeight: '600', }, statusActions: { flexDirection: 'row', justifyContent: 'flex-end', }, startButton: { minHeight: 40, borderRadius: 10, backgroundColor: PRIMARY, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center', }, startButtonText: { color: '#ffffff', fontWeight: '700', }, stopButton: { minHeight: 40, borderRadius: 10, backgroundColor: '#dc2626', paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center', }, stopButtonText: { color: '#ffffff', fontWeight: '700', }, error: { color: '#dc2626', fontSize: 13, }, loadingBox: { padding: 16, alignItems: 'center', gap: 8, }, loadingText: { color: '#6b7280', }, empty: { color: '#6b7280', textAlign: 'center', paddingVertical: 16, }, entryCard: { backgroundColor: '#ffffff', borderRadius: 12, padding: 12, borderWidth: 1, borderColor: '#e5e7eb', gap: 6, }, entryHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 8, }, entryType: { color: '#111827', fontWeight: '600', }, entryState: { color: '#3d7a30', fontWeight: '600', backgroundColor: '#eff9ea', borderRadius: 999, paddingHorizontal: 8, paddingVertical: 3, fontSize: 12, overflow: 'hidden', }, entryTime: { color: '#4b5563', fontSize: 13, }, entryDescription: { color: '#374151', fontSize: 14, }, entryActions: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 4, }, actionButton: { minHeight: 36, borderRadius: 8, backgroundColor: '#0ea5e9', paddingHorizontal: 10, alignItems: 'center', justifyContent: 'center', }, actionButtonText: { color: '#ffffff', fontWeight: '600', fontSize: 12, }, buttonDisabled: { opacity: 0.6, }, });