import { useEffect, useState, useCallback } from 'react' import { api } from '../api/client' import { Spinner } from '../components/Spinner' import { Layout } from '../components/Layout' interface UserOut { id: string; first_name: string; last_name: string; email: string; role: string; company_id: string } interface CalendarEntry { user_id: string user_name: string absence_id: string type_name: string type_color: string start_date: string end_date: string status: string working_days: number } interface PublicHoliday { id: string date: string name: string } const WEEKDAYS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'] const MONTHS = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'] function isoDate(d: Date): string { return d.toISOString().slice(0, 10) } function getDaysInMonth(year: number, month: number): Date[] { const days: Date[] = [] const d = new Date(year, month, 1) while (d.getMonth() === month) { days.push(new Date(d)) d.setDate(d.getDate() + 1) } return days } // Mon=0..Sun=6 function dayOfWeek(d: Date): number { return (d.getDay() + 6) % 7 } export function CalendarPage() { const today = new Date() const [user, setUser] = useState(null) const [year, setYear] = useState(today.getFullYear()) const [month, setMonth] = useState(today.getMonth()) const [entries, setEntries] = useState([]) const [holidays, setHolidays] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [tooltip, setTooltip] = useState(null) const load = useCallback(async () => { setLoading(true) try { const [me, cal, hol] = await Promise.all([ api.get('/auth/me'), api.get(`/absences/calendar?year=${year}&month=${month + 1}`), api.get(`/public-holidays/?year=${year}&country=DE`), ]) setUser(me) setEntries(cal.filter(e => e.status === 'approved' || e.status === 'pending')) setHolidays(hol) } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Fehler') } finally { setLoading(false) } }, [year, month]) useEffect(() => { load() }, [load]) const prevMonth = () => { if (month === 0) { setMonth(11); setYear(y => y - 1) } else setMonth(m => m - 1) } const nextMonth = () => { if (month === 11) { setMonth(0); setYear(y => y + 1) } else setMonth(m => m + 1) } const days = getDaysInMonth(year, month) const firstDow = dayOfWeek(days[0]) // 0=Mon // Absent people per day const absentOn = (dateStr: string): CalendarEntry[] => entries.filter(e => e.start_date <= dateStr && e.end_date >= dateStr) const isHoliday = (dateStr: string): string | null => holidays.find(h => h.date === dateStr)?.name ?? null if (loading) return
if (!user) return null return (
{/* Header */}

{MONTHS[month]} {year}

{error &&
{error}
} {/* Calendar grid */}
{/* Weekday headers */}
{WEEKDAYS.map(d => (
{d}
))}
{/* Days */}
{/* Leading empty cells */} {Array.from({ length: firstDow }).map((_, i) => (
))} {days.map(day => { const ds = isoDate(day) const dow = dayOfWeek(day) const isToday = ds === isoDate(today) const isWeekend = dow >= 5 const holiday = isHoliday(ds) const absent = absentOn(ds) return (
{day.getDate()} {holiday && ( {holiday.length > 8 ? holiday.slice(0, 8) + '…' : holiday} )}
{/* Absence chips */}
{absent.slice(0, 3).map(e => (
setTooltip(`${e.user_name}: ${e.type_name}`)} onMouseLeave={() => setTooltip(null)} > {e.user_name.split(' ')[0]} {e.status === 'pending' && ' ?'}
))} {absent.length > 3 && (
+{absent.length - 3} weitere
)}
) })}
{/* Legend */}
Feiertag
Wochenende
Vorname ? = ausstehend
{/* Summary: who's absent this month */} {entries.length > 0 && (

Abwesenheiten im {MONTHS[month]}

{Array.from(new Set(entries.map(e => e.user_id))).map(uid => { const userEntries = entries.filter(e => e.user_id === uid) const name = userEntries[0].user_name return (
{name.split(' ').map(n => n[0]).join('').slice(0, 2)}

{name}

{userEntries.map(e => ( {e.type_name} · {new Date(e.start_date).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })} {e.start_date !== e.end_date && ` – ${new Date(e.end_date).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })}`} {e.status === 'pending' && ' (ausstehend)'} ))}
) })}
)}
{/* Tooltip overlay (hidden, just accessibility) */} {tooltip && (
{tooltip}
)} ) }