import { useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Pressable, RefreshControl, ScrollView, StyleSheet, Text, View, } from 'react-native'; import { router } from 'expo-router'; import { EventCreateModal } from '@/components/event-create-modal'; import { CalendarEvent, fetchEvents, fetchProjects, Project } from '@/src/lib/api'; import { useAuth } from '@/src/providers/auth-provider'; const PRIMARY = '#69c350'; const WEEKDAYS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; function startOfDay(date: Date): Date { return new Date(date.getFullYear(), date.getMonth(), date.getDate()); } function dateKey(date: Date): string { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } function buildCalendarDays(month: Date): Date[] { const first = new Date(month.getFullYear(), month.getMonth(), 1); const mondayOffset = (first.getDay() + 6) % 7; const start = new Date(first); start.setDate(first.getDate() - mondayOffset); return Array.from({ length: 42 }, (_, index) => { const date = new Date(start); date.setDate(start.getDate() + index); return date; }); } function formatTime(value: string): string { const date = new Date(value); return Number.isNaN(date.getTime()) ? '-' : date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); } export default function CalendarScreen() { const { token } = useAuth(); const today = useMemo(() => startOfDay(new Date()), []); const [month, setMonth] = useState(() => new Date(today.getFullYear(), today.getMonth(), 1)); const [selectedDate, setSelectedDate] = useState(today); const [events, setEvents] = useState([]); const [projects, setProjects] = useState([]); const [createOpen, setCreateOpen] = useState(false); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); const load = useCallback(async (showSpinner = true) => { if (!token) return; if (showSpinner) setLoading(true); setError(null); try { const [eventRows, projectRows] = await Promise.all([fetchEvents(token), fetchProjects(token)]); setEvents(eventRows); setProjects(projectRows); } catch (err) { setError(err instanceof Error ? err.message : 'Der Kalender konnte nicht geladen werden.'); } finally { setLoading(false); setRefreshing(false); } }, [token]); useEffect(() => { void load(true); }, [load]); const eventsByDay = useMemo(() => { const map = new Map(); events.forEach((event) => { const start = new Date(event.startDate); if (Number.isNaN(start.getTime())) return; const key = dateKey(start); const rows = map.get(key) || []; rows.push(event); map.set(key, rows); }); map.forEach((rows) => rows.sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime())); return map; }, [events]); const days = useMemo(() => buildCalendarDays(month), [month]); const selectedEvents = eventsByDay.get(dateKey(selectedDate)) || []; function changeMonth(delta: number) { const next = new Date(month.getFullYear(), month.getMonth() + delta, 1); setMonth(next); setSelectedDate(next); } function selectDay(date: Date) { setSelectedDate(date); if (date.getMonth() !== month.getMonth() || date.getFullYear() !== month.getFullYear()) { setMonth(new Date(date.getFullYear(), date.getMonth(), 1)); } } async function onRefresh() { setRefreshing(true); await load(false); } return ( }> changeMonth(-1)}> { setMonth(new Date(today.getFullYear(), today.getMonth(), 1)); setSelectedDate(today); }}> {month.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' })} Heute changeMonth(1)}> {error ? {error} : null} {loading ? : null} {WEEKDAYS.map((weekday) => {weekday})} {days.map((date) => { const key = dateKey(date); const dayEvents = eventsByDay.get(key) || []; const outside = date.getMonth() !== month.getMonth(); const selected = key === dateKey(selectedDate); const isToday = key === dateKey(today); return ( selectDay(date)}> {date.getDate()} {dayEvents.slice(0, 3).map((event) => ( ))} ); })} {selectedDate.toLocaleDateString('de-DE', { weekday: 'long', day: '2-digit', month: 'long' })} {selectedEvents.length} {selectedEvents.length === 1 ? 'Termin' : 'Termine'} setCreateOpen(true)}>+ Termin {selectedEvents.length === 0 ? Keine Termine an diesem Tag. : null} {selectedEvents.map((event) => ( router.push(`/more/event/${event.id}` as any)}> {formatTime(event.startDate)} {event.name} {typeof event.project === 'object' && event.project?.name ? {event.project.name} : null} ))} setCreateOpen(false)} onCreated={() => load(false)} /> ); } const styles = StyleSheet.create({ screen: { flex: 1, backgroundColor: '#f9fafb' }, container: { padding: 12, gap: 12, paddingBottom: 28 }, monthHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, navButton: { width: 42, height: 42, borderRadius: 21, backgroundColor: '#ffffff', alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: '#e5e7eb' }, navButtonText: { color: '#111827', fontSize: 29, lineHeight: 31 }, monthTitle: { color: '#111827', fontSize: 18, fontWeight: '700', textAlign: 'center' }, todayHint: { color: '#3d7a30', fontSize: 11, fontWeight: '600', textAlign: 'center', marginTop: 2 }, calendarCard: { backgroundColor: '#ffffff', borderRadius: 12, borderWidth: 1, borderColor: '#e5e7eb', overflow: 'hidden' }, weekRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#e5e7eb', paddingVertical: 7 }, weekday: { width: '14.2857%', textAlign: 'center', color: '#6b7280', fontSize: 12, fontWeight: '700' }, daysGrid: { flexDirection: 'row', flexWrap: 'wrap' }, dayCell: { width: '14.2857%', height: 52, borderRightWidth: 1, borderBottomWidth: 1, borderColor: '#f0f1f2', alignItems: 'center', paddingTop: 6 }, dayCellSelected: { backgroundColor: '#eff9ea' }, dayNumber: { color: '#111827', fontSize: 13, fontWeight: '600' }, dayNumberOutside: { color: '#b6bbc3' }, dayNumberToday: { color: '#3d7a30', fontWeight: '900' }, eventDots: { flexDirection: 'row', gap: 2, marginTop: 5 }, eventDot: { width: 5, height: 5, borderRadius: 3 }, agendaCard: { backgroundColor: '#ffffff', borderRadius: 12, borderWidth: 1, borderColor: '#e5e7eb', padding: 12, gap: 8 }, agendaHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }, agendaTitle: { color: '#111827', fontSize: 16, fontWeight: '700' }, agendaCount: { color: '#6b7280', fontSize: 12, marginTop: 2 }, addButton: { backgroundColor: PRIMARY, borderRadius: 8, minHeight: 36, paddingHorizontal: 11, alignItems: 'center', justifyContent: 'center' }, addButtonText: { color: '#ffffff', fontSize: 13, fontWeight: '700' }, eventRow: { minHeight: 62, flexDirection: 'row', alignItems: 'stretch', borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 9, overflow: 'hidden' }, colorBar: { width: 5 }, eventMain: { flex: 1, padding: 9 }, eventTime: { color: '#3d7a30', fontSize: 12, fontWeight: '700' }, eventName: { color: '#111827', fontSize: 14, fontWeight: '700' }, eventProject: { color: '#6b7280', fontSize: 12, marginTop: 2 }, arrow: { alignSelf: 'center', color: '#9ca3af', fontSize: 24, paddingRight: 9 }, empty: { color: '#6b7280', fontSize: 13, paddingVertical: 8 }, error: { color: '#dc2626', fontSize: 13 }, loader: { paddingVertical: 12 }, });