'use client'; import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { UserCheck, Plus, Search, Filter, Star, Phone, Mail, Calendar, Clock, AlertTriangle, CheckCircle2, XCircle, Edit3, Trash2, ChevronLeft, ChevronRight, Printer, Bell, BarChart3, TrendingUp, Users, FileText, Settings, Eye, Sparkles, ArrowRight, RefreshCw, UserPlus, CalendarDays, Shield, BookOpen, MapPin, Stethoscope, GraduationCap, Heart, Briefcase, X, Save, Info, } from 'lucide-react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Progress } from '@/components/ui/progress'; import { Separator } from '@/components/ui/separator'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useAppStore } from '@/lib/store'; import { t } from '@/lib/i18n'; import { apiGet } from '@/lib/api'; import { toast } from 'sonner'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, PieChart, Pie, Cell, LineChart, Line, Legend, } from 'recharts'; // ── Types ────────────────────────────────────────────────────────── interface SubstituteTeacherData { id: string; schoolId: string; userId: string | null; firstName: string; lastName: string; email: string | null; phone: string | null; qualifications: string | null; subjects: string | null; gradeLevels: string | null; availability: string | null; maxDaysPerWeek: number; rating: number; totalAssignments: number; notes: string | null; isActive: boolean; createdAt: string; updatedAt: string; user?: { id: string; firstName: string; lastName: string; email: string } | null; assignments?: { id: string; date: string; status: string }[]; } interface TeacherAbsenceData { id: string; schoolId: string; teacherId: string; type: string; startDate: string; endDate: string; reason: string | null; status: string; notes: string | null; createdAt: string; updatedAt: string; teacher: { id: string; firstName: string; lastName: string; email: string }; assignments?: SubstitutionAssignmentData[]; } interface SubstitutionAssignmentData { id: string; schoolId: string; absenceId: string; substituteId: string; classGroupId: string | null; subjectId: string | null; date: string; period: number | null; room: string | null; status: string; notes: string | null; createdAt: string; updatedAt: string; absence?: TeacherAbsenceData; substitute?: { id: string; firstName: string; lastName: string; rating: number }; classGroup?: { id: string; name: string } | null; subject?: { id: string; name: string } | null; } interface TeacherOption { id: string; firstName: string; lastName: string; email: string; } // ── Helpers ──────────────────────────────────────────────────────── function parseJSON(val: string | null | undefined, fallback: T): T { if (!val) return fallback; try { return JSON.parse(val) as T; } catch { return fallback; } } function formatDate(dateStr: string): string { return new Date(dateStr).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); } function getDayOfWeek(date: Date): string { const days = ['sub.sunday', 'sub.monday', 'sub.tuesday', 'sub.wednesday', 'sub.thursday', 'sub.friday', 'sub.saturday']; return days[date.getDay()]; } function getAbsenceTypeColor(type: string): string { switch (type) { case 'SICK': return 'bg-red-100 text-red-800 dark:bg-red-950/50 dark:text-red-300'; case 'PERSONAL': return 'bg-amber-100 text-amber-800 dark:bg-amber-950/50 dark:text-amber-300'; case 'TRAINING': return 'bg-blue-100 text-blue-800 dark:bg-blue-950/50 dark:text-blue-300'; default: return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'; } } function getAssignmentStatusColor(status: string): string { switch (status) { case 'pending': return 'bg-amber-100 text-amber-800 dark:bg-amber-950/50 dark:text-amber-300'; case 'confirmed': return 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/50 dark:text-emerald-300'; case 'completed': return 'bg-teal-100 text-teal-800 dark:bg-teal-950/50 dark:text-teal-300'; case 'cancelled': return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'; default: return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'; } } function getAbsenceTypeIcon(type: string) { switch (type) { case 'SICK': return Stethoscope; case 'PERSONAL': return Heart; case 'TRAINING': return GraduationCap; default: return Briefcase; } } function renderStars(rating: number) { return (
{[1, 2, 3, 4, 5].map((star) => ( ))} {rating.toFixed(1)}
); } // ── Animated Counter ─────────────────────────────────────────────── function AnimatedCounter({ value, duration = 1000 }: { value: number; duration?: number }) { const [display, setDisplay] = useState(0); useEffect(() => { let start = 0; const step = value / (duration / 16); const timer = setInterval(() => { start += step; if (start >= value) { setDisplay(value); clearInterval(timer); } else setDisplay(Math.floor(start)); }, 16); return () => clearInterval(timer); }, [value, duration]); return {display}; } // ── Main Component ───────────────────────────────────────────────── export default function SubstituteTeacherView() { const currentUser = useAppStore((s) => s.currentUser); const schoolId = currentUser?.schoolId; const role = currentUser?.role || 'TEACHER'; const isAdmin = role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL' || role === 'SUPER_ADMIN'; const isTeacher = role === 'TEACHER'; const isStudent = role === 'STUDENT'; const isParent = role === 'PARENT'; const isReadOnly = isStudent || isParent; // ── Data State ────────────────────────────────────────────────── const [substitutes, setSubstitutes] = useState([]); const [absences, setAbsences] = useState([]); const [assignments, setAssignments] = useState([]); const [teachers, setTeachers] = useState([]); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState('pool'); // ── Filter State ──────────────────────────────────────────────── const [searchQuery, setSearchQuery] = useState(''); const [filterActive, setFilterActive] = useState(true); const [absenceFilter, setAbsenceFilter] = useState('all'); const [weekOffset, setWeekOffset] = useState(0); // ── Dialog State ──────────────────────────────────────────────── const [showSubstituteDialog, setShowSubstituteDialog] = useState(false); const [editingSubstitute, setEditingSubstitute] = useState(null); const [showAbsenceDialog, setShowAbsenceDialog] = useState(false); const [editingAbsence, setEditingAbsence] = useState(null); const [showAssignmentDialog, setShowAssignmentDialog] = useState(false); const [editingAssignment, setEditingAssignment] = useState(null); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [deleteTarget, setDeleteTarget] = useState<{ type: string; id: string } | null>(null); const [showRateDialog, setShowRateDialog] = useState(false); const [ratingSubstitute, setRatingSubstitute] = useState(null); const [newRating, setNewRating] = useState(0); // ── Form State ────────────────────────────────────────────────── const [subForm, setSubForm] = useState({ firstName: '', lastName: '', email: '', phone: '', qualifications: [] as string[], subjects: [] as string[], gradeLevels: [] as string[], maxDaysPerWeek: 5, notes: '', }); const [absenceForm, setAbsenceForm] = useState({ teacherId: '', type: 'SICK', startDate: '', endDate: '', reason: '', notes: '', }); const [assignmentForm, setAssignmentForm] = useState({ absenceId: '', substituteId: '', classGroupId: '', subjectId: '', date: '', period: 1, room: '', notes: '', }); const [qualInput, setQualInput] = useState(''); const [gradeInput, setGradeInput] = useState(''); // ── Fetch Data ────────────────────────────────────────────────── const fetchSubstitutes = useCallback(async () => { if (!schoolId) return; try { const data = await apiGet(`/api/substitutes?schoolId=${schoolId}`); setSubstitutes(data); } catch (err) { console.error('Error fetching substitutes:', err); toast.error(t('sub.error_load')); } }, [schoolId]); const fetchAbsences = useCallback(async () => { if (!schoolId) return; try { const data = await apiGet(`/api/substitutes/absences?schoolId=${schoolId}`); setAbsences(data); } catch (err) { console.error('Error fetching absences:', err); toast.error(t('sub.error_load')); } }, [schoolId]); const fetchAssignments = useCallback(async () => { if (!schoolId) return; try { const data = await apiGet(`/api/substitutes/assignments?schoolId=${schoolId}`); setAssignments(data); } catch (err) { console.error('Error fetching assignments:', err); toast.error(t('sub.error_load')); } }, [schoolId]); const fetchTeachers = useCallback(async () => { if (!schoolId) return; try { const data = await apiGet(`/api/users?schoolId=${schoolId}&role=TEACHER`); setTeachers(Array.isArray(data) ? data : []); } catch { // ignore — teachers list is optional } }, [schoolId]); useEffect(() => { if (schoolId) { setLoading(true); Promise.all([fetchSubstitutes(), fetchAbsences(), fetchAssignments(), fetchTeachers()]) .finally(() => setLoading(false)); } }, [schoolId, fetchSubstitutes, fetchAbsences, fetchAssignments, fetchTeachers]); // ── Computed Data ─────────────────────────────────────────────── const filteredSubstitutes = useMemo(() => { let result = substitutes; if (filterActive) result = result.filter((s) => s.isActive); if (searchQuery) { const q = searchQuery.toLowerCase(); result = result.filter( (s) => s.firstName.toLowerCase().includes(q) || s.lastName.toLowerCase().includes(q) || (s.email && s.email.toLowerCase().includes(q)) ); } return result; }, [substitutes, filterActive, searchQuery]); const filteredAbsences = useMemo(() => { let result = absences; if (absenceFilter !== 'all') result = result.filter((a) => a.status === absenceFilter); if (isTeacher) result = result.filter((a) => a.teacherId === currentUser?.id); return result; }, [absences, absenceFilter, isTeacher, currentUser?.id]); const weekDates = useMemo(() => { const now = new Date(); const monday = new Date(now); monday.setDate(now.getDate() - now.getDay() + 1 + weekOffset * 7); const dates: Date[] = []; for (let i = 0; i < 6; i++) { const d = new Date(monday); d.setDate(monday.getDate() + i); dates.push(d); } return dates; }, [weekOffset]); const weekAssignments = useMemo(() => { const weekStart = weekDates[0]; const weekEnd = weekDates[5]; return assignments.filter((a) => { const d = new Date(a.date); return d >= weekStart && d <= weekEnd && a.status !== 'cancelled'; }); }, [assignments, weekDates]); // ── Statistics ────────────────────────────────────────────────── const stats = useMemo(() => { const totalAbsences = absences.length; const coveredAbsences = absences.filter((a) => a.status === 'covered').length; const activeSubstitutes = substitutes.filter((s) => s.isActive).length; const totalAssignments = assignments.length; const pendingAssignments = assignments.filter((a) => a.status === 'pending').length; const confirmedAssignments = assignments.filter((a) => a.status === 'confirmed').length; const completedAssignments = assignments.filter((a) => a.status === 'completed').length; const avgRating = substitutes.length > 0 ? substitutes.reduce((sum, s) => sum + s.rating, 0) / substitutes.length : 0; const coverageRate = totalAbsences > 0 ? Math.round((coveredAbsences / totalAbsences) * 100) : 100; // Absences per month (last 6 months) const monthLabels: string[] = []; const monthCounts: Record = {}; for (let i = 5; i >= 0; i--) { const d = new Date(); d.setMonth(d.getMonth() - i); const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; monthLabels.push(key); monthCounts[key] = 0; } absences.forEach((a) => { const d = new Date(a.startDate); const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; if (monthCounts[key] !== undefined) monthCounts[key]++; }); const absencesPerMonth = monthLabels.map((m) => ({ month: m, count: monthCounts[m], })); // Assignments by status const assignmentsByStatus = [ { name: t('sub.status_pending'), value: pendingAssignments, color: '#f59e0b' }, { name: t('sub.status_confirmed'), value: confirmedAssignments, color: '#10b981' }, { name: t('sub.status_completed'), value: completedAssignments, color: '#14b8a6' }, ].filter((s) => s.value > 0); // Top absent teachers const teacherAbsenceCount: Record = {}; absences.forEach((a) => { const key = a.teacherId; if (!teacherAbsenceCount[key]) { teacherAbsenceCount[key] = { name: `${a.teacher.firstName} ${a.teacher.lastName}`, count: 0 }; } teacherAbsenceCount[key].count++; }); const topAbsentTeachers = Object.values(teacherAbsenceCount) .sort((a, b) => b.count - a.count) .slice(0, 5); // Substitute utilization const subUtilization = substitutes .filter((s) => s.isActive) .map((s) => ({ name: `${s.firstName} ${s.lastName}`, assignments: s.totalAssignments, maxDays: s.maxDaysPerWeek, })) .sort((a, b) => b.assignments - a.assignments) .slice(0, 10); // Cost tracking (mock data based on assignments) const dailyRate = 150; const totalCost = assignments.filter((a) => a.status === 'completed').length * dailyRate; return { totalAbsences, coveredAbsences, activeSubstitutes, totalAssignments, pendingAssignments, confirmedAssignments, completedAssignments, avgRating, coverageRate, absencesPerMonth, assignmentsByStatus, topAbsentTeachers, subUtilization, totalCost, dailyRate, }; }, [absences, substitutes, assignments]); // ── CRUD Handlers ─────────────────────────────────────────────── const handleSaveSubstitute = async () => { if (!schoolId || !subForm.firstName || !subForm.lastName) { toast.error(t('sub.error_save')); return; } try { const payload = { schoolId, firstName: subForm.firstName, lastName: subForm.lastName, email: subForm.email || null, phone: subForm.phone || null, qualifications: subForm.qualifications.length > 0 ? subForm.qualifications : null, subjects: null, gradeLevels: subForm.gradeLevels.length > 0 ? subForm.gradeLevels : null, availability: null, maxDaysPerWeek: subForm.maxDaysPerWeek, notes: subForm.notes || null, }; if (editingSubstitute) { const res = await fetch(`/api/substitutes/${editingSubstitute.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(); } else { const res = await fetch('/api/substitutes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(); } toast.success(t('sub.save_success')); setShowSubstituteDialog(false); setEditingSubstitute(null); resetSubForm(); fetchSubstitutes(); } catch { toast.error(t('sub.error_save')); } }; const handleSaveAbsence = async () => { if (!schoolId || !absenceForm.teacherId || !absenceForm.startDate || !absenceForm.endDate) { toast.error(t('sub.error_save')); return; } try { const payload = { schoolId, teacherId: absenceForm.teacherId, type: absenceForm.type, startDate: absenceForm.startDate, endDate: absenceForm.endDate, reason: absenceForm.reason || null, notes: absenceForm.notes || null, }; if (editingAbsence) { const res = await fetch(`/api/substitutes/absences/${editingAbsence.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(); } else { const res = await fetch('/api/substitutes/absences', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(); } toast.success(t('sub.save_success')); setShowAbsenceDialog(false); setEditingAbsence(null); resetAbsenceForm(); fetchAbsences(); } catch { toast.error(t('sub.error_save')); } }; const handleSaveAssignment = async () => { if (!schoolId || !assignmentForm.absenceId || !assignmentForm.substituteId || !assignmentForm.date) { toast.error(t('sub.error_save')); return; } try { const payload = { schoolId, absenceId: assignmentForm.absenceId, substituteId: assignmentForm.substituteId, classGroupId: assignmentForm.classGroupId || null, subjectId: assignmentForm.subjectId || null, date: assignmentForm.date, period: assignmentForm.period || null, room: assignmentForm.room || null, notes: assignmentForm.notes || null, }; if (editingAssignment) { const res = await fetch(`/api/substitutes/assignments/${editingAssignment.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(); } else { const res = await fetch('/api/substitutes/assignments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(); } toast.success(t('sub.save_success')); setShowAssignmentDialog(false); setEditingAssignment(null); resetAssignmentForm(); fetchAssignments(); fetchAbsences(); } catch { toast.error(t('sub.error_save')); } }; const handleDelete = async () => { if (!deleteTarget) return; try { let url = ''; if (deleteTarget.type === 'substitute') url = `/api/substitutes/${deleteTarget.id}`; else if (deleteTarget.type === 'absence') url = `/api/substitutes/absences/${deleteTarget.id}`; else if (deleteTarget.type === 'assignment') url = `/api/substitutes/assignments/${deleteTarget.id}`; const res = await fetch(url, { method: 'DELETE' }); if (!res.ok) throw new Error(); toast.success(t('sub.delete_success')); setShowDeleteDialog(false); setDeleteTarget(null); fetchSubstitutes(); fetchAbsences(); fetchAssignments(); } catch { toast.error(t('sub.error_delete')); } }; const handleUpdateAssignmentStatus = async (id: string, status: string) => { try { const res = await fetch(`/api/substitutes/assignments/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }), }); if (!res.ok) throw new Error(); toast.success(t('sub.save_success')); fetchAssignments(); fetchAbsences(); } catch { toast.error(t('sub.error_save')); } }; const handleRateSubstitute = async () => { if (!ratingSubstitute || newRating < 1 || newRating > 5) return; try { const res = await fetch(`/api/substitutes/${ratingSubstitute.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rating: newRating }), }); if (!res.ok) throw new Error(); toast.success(t('sub.save_success')); setShowRateDialog(false); setRatingSubstitute(null); setNewRating(0); fetchSubstitutes(); } catch { toast.error(t('sub.error_save')); } }; // ── Form Helpers ──────────────────────────────────────────────── const resetSubForm = () => { setSubForm({ firstName: '', lastName: '', email: '', phone: '', qualifications: [], subjects: [], gradeLevels: [], maxDaysPerWeek: 5, notes: '' }); }; const resetAbsenceForm = () => { setAbsenceForm({ teacherId: '', type: 'SICK', startDate: '', endDate: '', reason: '', notes: '' }); }; const resetAssignmentForm = () => { setAssignmentForm({ absenceId: '', substituteId: '', classGroupId: '', subjectId: '', date: '', period: 1, room: '', notes: '' }); }; const openEditSubstitute = (sub: SubstituteTeacherData) => { setEditingSubstitute(sub); setSubForm({ firstName: sub.firstName, lastName: sub.lastName, email: sub.email || '', phone: sub.phone || '', qualifications: parseJSON(sub.qualifications, []), subjects: parseJSON(sub.subjects, []), gradeLevels: parseJSON(sub.gradeLevels, []), maxDaysPerWeek: sub.maxDaysPerWeek, notes: sub.notes || '', }); setShowSubstituteDialog(true); }; const openEditAbsence = (abs: TeacherAbsenceData) => { setEditingAbsence(abs); setAbsenceForm({ teacherId: abs.teacherId, type: abs.type, startDate: abs.startDate.slice(0, 10), endDate: abs.endDate.slice(0, 10), reason: abs.reason || '', notes: abs.notes || '', }); setShowAbsenceDialog(true); }; const openCreateAssignment = (absenceId?: string) => { setEditingAssignment(null); resetAssignmentForm(); if (absenceId) setAssignmentForm((prev) => ({ ...prev, absenceId })); setShowAssignmentDialog(true); }; const addQualification = () => { if (qualInput.trim()) { setSubForm((prev) => ({ ...prev, qualifications: [...prev.qualifications, qualInput.trim()] })); setQualInput(''); } }; const removeQualification = (idx: number) => { setSubForm((prev) => ({ ...prev, qualifications: prev.qualifications.filter((_, i) => i !== idx) })); }; const addGradeLevel = () => { if (gradeInput.trim()) { setSubForm((prev) => ({ ...prev, gradeLevels: [...prev.gradeLevels, gradeInput.trim()] })); setGradeInput(''); } }; const removeGradeLevel = (idx: number) => { setSubForm((prev) => ({ ...prev, gradeLevels: prev.gradeLevels.filter((_, i) => i !== idx) })); }; // ── Auto-assign logic ─────────────────────────────────────────── const handleAutoAssign = async (absenceId: string) => { const absence = absences.find((a) => a.id === absenceId); if (!absence) return; const activeSubs = substitutes.filter((s) => s.isActive); if (activeSubs.length === 0) { toast.error(t('sub.no_substitutes')); return; } // Simple scoring: prefer substitutes with higher rating and fewer assignments const scored = activeSubs.map((s) => ({ sub: s, score: s.rating * 10 - s.totalAssignments * 0.5, })).sort((a, b) => b.score - a.score); const best = scored[0].sub; const startDate = new Date(absence.startDate); const endDate = new Date(absence.endDate); try { for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { if (d.getDay() === 0 || d.getDay() === 6) continue; // Skip weekends await fetch('/api/substitutes/assignments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ schoolId, absenceId, substituteId: best.id, date: d.toISOString().slice(0, 10), status: 'pending', }), }); } toast.success(t('sub.save_success')); fetchAssignments(); fetchAbsences(); } catch { toast.error(t('sub.error_save')); } }; // ── Render ────────────────────────────────────────────────────── if (loading) { return (
Loading...
); } return (
{/* ── Header Banner ──────────────────────────────────────── */}

{t('sub.title')}

{t('sub.subtitle')}

{/* Stats cards in header */}
{[ { label: t('sub.total_substitutes'), value: stats.activeSubstitutes, icon: Users }, { label: t('sub.total_absences'), value: stats.totalAbsences, icon: CalendarDays }, { label: t('sub.coverage_rate'), value: `${stats.coverageRate}%`, icon: Shield }, { label: t('sub.avg_rating'), value: stats.avgRating.toFixed(1), icon: Star }, ].map((s, i) => (
{s.label}

{typeof s.value === 'number' ? : s.value}

))}
{/* ── Tabs ───────────────────────────────────────────────── */} {t('sub.pool')} {t('sub.absences')} {t('sub.schedule')} {t('sub.statistics')} {/* ── Substitute Pool Tab ──────────────────────────────── */} {/* Search & Filter Bar */}
setSearchQuery(e.target.value)} className="pl-10" />
{!isReadOnly && ( )}
{/* Substitute Cards */} {filteredSubstitutes.length === 0 ? (

{t('sub.no_substitutes')}

) : (
{filteredSubstitutes.map((sub, idx) => (
{sub.firstName[0]}{sub.lastName[0]}
{sub.firstName} {sub.lastName}
{renderStars(sub.rating)}
{sub.isActive ? t('sub.active') : t('sub.inactive')}
{/* Contact Info */}
{sub.email && (
{sub.email}
)} {sub.phone && (
{sub.phone}
)}
{/* Qualifications */} {parseJSON(sub.qualifications, []).length > 0 && (

{t('sub.qualifications')}

{parseJSON(sub.qualifications, []).map((q, i) => ( {q} ))}
)} {/* Grade Levels */} {parseJSON(sub.gradeLevels, []).length > 0 && (

{t('sub.grade_levels')}

{parseJSON(sub.gradeLevels, []).map((g, i) => ( {g} ))}
)} {/* Stats Row */}
{sub.totalAssignments} {t('sub.lessons')} {sub.maxDaysPerWeek}/w
{!isReadOnly && (
)}
))}
)}
{/* ── Absences Tab ─────────────────────────────────────── */} {/* Filter & Actions */}
{['all', 'reported', 'covered', 'cancelled'].map((f) => ( ))}
{!isReadOnly && ( )}
{/* Absence List */} {filteredAbsences.length === 0 ? (

{t('sub.no_absences')}

) : (
{filteredAbsences.map((abs, idx) => { const TypeIcon = getAbsenceTypeIcon(abs.type); const assignmentCount = abs.assignments?.length || 0; const coveredCount = abs.assignments?.filter((a) => a.status === 'confirmed' || a.status === 'completed').length || 0; return (
{/* Teacher & Type */}

{abs.teacher.firstName} {abs.teacher.lastName}

{formatDate(abs.startDate)} — {formatDate(abs.endDate)}

{/* Type Badge */} {t(`sub.type_${abs.type.toLowerCase()}`)} {/* Coverage Progress */}
0 ? (coveredCount / assignmentCount) * 100 : 0} className="h-2 flex-1" /> {coveredCount}/{assignmentCount}
{/* Status Badge */} {t(`sub.status_${abs.status}`)} {/* Actions */} {!isReadOnly && (
{abs.status === 'reported' && ( )} {abs.status === 'reported' && isAdmin && ( )}
)}
{abs.reason && (

{abs.reason}

)}
); })}
)}
{/* ── Schedule Tab ─────────────────────────────────────── */} {/* Week Navigation */}
{formatDate(weekDates[0].toISOString())} — {formatDate(weekDates[5].toISOString())}
{!isReadOnly && isAdmin && ( )}
{/* Weekly Grid */}
{/* Header */}
{weekDates.map((date, i) => (

{t(getDayOfWeek(date))}

{date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })}

))}
{/* Body */}
{weekDates.map((date, i) => { const dayStr = date.toISOString().slice(0, 10); const dayAssignments = weekAssignments.filter( (a) => new Date(a.date).toISOString().slice(0, 10) === dayStr ); return (
{dayAssignments.length === 0 ? (

—

) : ( dayAssignments.map((assignment) => ( { if (!isReadOnly) { setEditingAssignment(assignment); setAssignmentForm({ absenceId: assignment.absenceId, substituteId: assignment.substituteId, classGroupId: assignment.classGroupId || '', subjectId: assignment.subjectId || '', date: new Date(assignment.date).toISOString().slice(0, 10), period: assignment.period || 1, room: assignment.room || '', notes: assignment.notes || '', }); setShowAssignmentDialog(true); } }} >
{assignment.period ? `${assignment.period}. ` : ''} {assignment.subject?.name || ''}
{assignment.classGroup?.name || '—'}
{assignment.substitute?.firstName} {assignment.substitute?.lastName}
{assignment.room && (
{t('sub.room')}: {assignment.room}
)}
)) )}
); })}
{/* Assignment List */}
{t('sub.schedule')} {!isReadOnly && ( )}
{assignments.length === 0 ? (

{t('sub.no_assignments')}

) : (
{assignments .filter((a) => a.status !== 'cancelled') .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) .map((assignment) => (
{new Date(assignment.date).toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit' })} {assignment.period && ( {assignment.period}. {t('sub.period')} )} {t(`sub.status_${assignment.status}`)}
{assignment.substitute?.firstName} {assignment.substitute?.lastName} {assignment.subject?.name || '—'} {assignment.classGroup?.name || '—'} {assignment.room && ( {assignment.room} )}
{assignment.absence && (

{t('sub.teacher')}: {assignment.absence.teacher?.firstName} {assignment.absence.teacher?.lastName}

)}
{!isReadOnly && (
{assignment.status === 'pending' && ( )} {assignment.status === 'confirmed' && ( )}
)}
))}
)}
{/* ── Statistics Tab ───────────────────────────────────── */} {/* Stat Cards */}
{[ { label: t('sub.total_absences'), value: stats.totalAbsences, icon: CalendarDays, color: 'from-red-500 to-orange-500' }, { label: t('sub.coverage_rate'), value: `${stats.coverageRate}%`, icon: Shield, color: 'from-emerald-500 to-teal-500' }, { label: t('sub.total_assignments_count'), value: stats.totalAssignments, icon: FileText, color: 'from-blue-500 to-indigo-500' }, { label: t('sub.total_cost'), value: `€${stats.totalCost}`, icon: BarChart3, color: 'from-amber-500 to-orange-500' }, ].map((s, i) => (

{typeof s.value === 'number' ? : s.value}

{s.label}

))}
{/* Charts Row */}
{/* Monthly Absence Trends */} {t('sub.monthly_trends')} {t('sub.absences_per_month')} {/* Assignments by Status */} {t('sub.assignments_per_status')} {stats.assignmentsByStatus.length > 0 ? ( `${name} (${(percent * 100).toFixed(0)}%)`} > {stats.assignmentsByStatus.map((entry, i) => ( ))} ) : (

{t('sub.no_assignments')}

)}
{/* Second Charts Row */}
{/* Top Absent Teachers */} {t('sub.top_absent_teachers')} {stats.topAbsentTeachers.length > 0 ? ( ) : (

{t('sub.no_absences')}

)}
{/* Substitute Utilization */} {t('sub.substitute_utilization')} {stats.subUtilization.length > 0 ? ( ) : (

{t('sub.no_substitutes')}

)}
{/* Cost Tracking */} {isAdmin && ( {t('sub.cost_tracking')}

{t('sub.daily_rate')}

€{stats.dailyRate}

{t('sub.per_day')}

{t('sub.total_cost')}

€{stats.totalCost}

{stats.completedAssignments} {t('sub.lessons')}

{t('sub.coverage_rate')}

{stats.coverageRate}%

)}
{/* ── Substitute Dialog ─────────────────────────────────────── */} {editingSubstitute ? t('sub.edit_substitute') : t('sub.add_substitute')} {editingSubstitute ? t('sub.edit_substitute') : t('sub.add_substitute')}
setSubForm((p) => ({ ...p, firstName: e.target.value }))} />
setSubForm((p) => ({ ...p, lastName: e.target.value }))} />
setSubForm((p) => ({ ...p, email: e.target.value }))} />
setSubForm((p) => ({ ...p, phone: e.target.value }))} />
setQualInput(e.target.value)} placeholder={t('sub.add_substitute')} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addQualification(); } }} />
{subForm.qualifications.map((q, i) => ( {q} removeQualification(i)} /> ))}
setGradeInput(e.target.value)} placeholder="5-6" onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addGradeLevel(); } }} />
{subForm.gradeLevels.map((g, i) => ( {g} removeGradeLevel(i)} /> ))}
setSubForm((p) => ({ ...p, maxDaysPerWeek: parseInt(e.target.value) || 5 }))} />