'use client'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Users, Heart, GraduationCap, Clock, CheckCircle2, XCircle, AlertCircle, Loader2, ChevronRight, Calendar, BookOpen, Shield, MessageSquare, Thermometer, TrendingUp, Timer, Plus, FileText, BarChart3, ArrowUpRight, ArrowDownRight, Minus, Megaphone, CalendarDays, Download, Upload, BookMarked, ClipboardCheck, Phone, Mail, Bell, CalendarClock, StickyNote, Activity, } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Progress } from '@/components/ui/progress'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { useAppStore } from '@/lib/store'; import { t } from '@/lib/i18n'; import { apiPost } from '@/lib/api'; import { toast } from 'sonner'; import { format, parseISO, differenceInCalendarDays } from 'date-fns'; import { de as deLocale, enUS as enLocale } from 'date-fns/locale'; import { RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, } from 'recharts'; /* ── Types ─────────────────────────────────────────────────────────── */ interface ChildData { studentId: string; firstName: string; lastName: string; relationship: string | null; userId: string | null; classGroups: Array<{ id: string; name: string; gradeLevel: number }>; illnessReports: Array<{ id: string; reason: string; description: string | null; startDate: string; endDate: string | null; parentApprovalStatus: string; status: string; reporterType: string; documentUrl: string | null; createdAt: string; }>; upcomingExams: Array<{ id: string; title: string; date: string; startTime: string | null; endTime: string | null; subject: { id: string; name: string } | null; classGroup: { id: string; name: string } | null; daysUntil: number; }>; recentGrades: Array<{ id: string; computedValue: number; period: string; subject: { id: string; name: string }; classGroup: { id: string; name: string }; computedAt: string; }>; attendanceSummary: { total: number; present: number; absent: number; excused: number; late: number; }; counselingAppointments: Array<{ id: string; requestType: string; status: string; scheduledAt: string | null; description: string | null; counselor: { firstName: string; lastName: string }; }>; disciplinaryCases: Array<{ id: string; caseType: string; description: string; status: string; createdAt: string; resolution: string | null; }>; homeworkDue: Array<{ id: string; title: string; dueDate: string; subject: { id: string; name: string } | null; }>; } interface PendingApproval { id: string; reason: string; description: string | null; startDate: string; endDate: string | null; parentApprovalStatus: string; status: string; reporterType: string; student: { id: string; firstName: string; lastName: string }; } interface Announcement { id: string; title: string; content: string; priority: string; announcementType: string; isPinned: boolean; createdAt: string; author: { firstName: string; lastName: string } | null; } interface CommRoom { id: string; status: string; roomType: string; student: { id: string; firstName: string; lastName: string }; teacher: { id: string; firstName: string; lastName: string }; lastMessage: { id: string; content: string; createdAt: string; senderId: string } | null; updatedAt: string; } interface SchoolEventShort { id: string; title: string; startDate: string; eventType: string; location: string | null; } interface SubjectGrade { subject: { id: string; name: string; code: string | null }; grades: Array<{ id: string; computedValue: number; period: string; computedAt: string; classGroup: { id: string; name: string }; schoolYear: { id: string; name: string }; }>; average: number; trend: 'up' | 'down' | 'stable'; } interface ChildProgress { subjectGrades: SubjectGrade[]; classAverages: Record; competencyBySubject: Array<{ subject: { id: string; name: string }; competencies: Array<{ id: string; code: string; title: string; level: number; assessedAt: string }>; averageLevel: number; }>; assessments: Array<{ id: string; score: number; maxScore: number; grade: string | null; feedback: string | null; assessedAt: string; assessment: { id: string; title: string; type: string; date: string; subject: { id: string; name: string } }; }>; } interface ScheduleData { timetableSlots: Array<{ id: string; dayOfWeek: number; period: number; startTime: string; endTime: string; isBreak: boolean; subject: { id: string; name: string; code: string | null } | null; teacher: { id: string; firstName: string; lastName: string } | null; classGroup: { id: string; name: string }; }>; upcomingExams: Array<{ id: string; title: string; date: string; startTime: string | null; endTime: string | null; subject: { id: string; name: string } | null; classGroup: { id: string; name: string } | null; daysUntil: number; }>; schoolEvents: Array<{ id: string; title: string; description: string | null; startDate: string; endDate: string | null; eventType: string; location: string | null; organizer: { id: string; firstName: string; lastName: string }; }>; counselingAppointments: Array<{ id: string; requestType: string; status: string; scheduledAt: string | null; description: string | null; counselor: { firstName: string; lastName: string }; }>; homeworkDue: Array<{ id: string; title: string; description: string | null; dueDate: string; homeworkType: string; subject: { id: string; name: string } | null; classGroup: { id: string; name: string }; }>; } /* ── Helpers ───────────────────────────────────────────────────────── */ const fmtDate = (d: string, loc: 'de' | 'en') => format(parseISO(d), 'd. MMM yyyy', { locale: loc === 'de' ? deLocale : enLocale }); const fmtShort = (d: string, loc: 'de' | 'en') => format(parseISO(d), 'd. MMM', { locale: loc === 'de' ? deLocale : enLocale }); const dayNames = (loc: 'de' | 'en') => [ loc === 'de' ? 'Mo' : 'Mon', loc === 'de' ? 'Di' : 'Tue', loc === 'de' ? 'Mi' : 'Wed', loc === 'de' ? 'Do' : 'Thu', loc === 'de' ? 'Fr' : 'Fri', ]; const subjectColors = [ '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#ec4899', '#14b8a6', '#f97316', '#6366f1', '#84cc16', ]; /* ── Animated Counter ──────────────────────────────────────────────── */ function AnimatedCounter({ value, duration = 1200 }: { 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) { start = value; clearInterval(timer); } setDisplay(Math.round(start * 10) / 10); }, 16); return () => clearInterval(timer); }, [value, duration]); return <>{display}; } /* ── Circular Progress ─────────────────────────────────────────────── */ function CircularProgress({ value, size = 120, strokeWidth = 10 }: { value: number; size?: number; strokeWidth?: number }) { const radius = (size - strokeWidth) / 2; const circumference = 2 * Math.PI * radius; const offset = circumference - (value / 100) * circumference; const color = value >= 90 ? '#10b981' : value >= 75 ? '#f59e0b' : '#ef4444'; return (
{value}%
); } /* ── Main Component ────────────────────────────────────────────────── */ export default function ParentPortalView() { const currentUser = useAppStore((s) => s.currentUser); const locale = useAppStore((s) => s.locale); const loc = (locale === 'en' ? 'en' : 'de') as 'de' | 'en'; const [children, setChildren] = useState([]); const [pendingApprovals, setPendingApprovals] = useState([]); const [announcements, setAnnouncements] = useState([]); const [commRooms, setCommRooms] = useState([]); const [schoolEvents, setSchoolEvents] = useState([]); const [selectedChild, setSelectedChild] = useState(null); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState('overview'); // Sub-data const [progressData, setProgressData] = useState(null); const [scheduleData, setScheduleData] = useState(null); const [loadingProgress, setLoadingProgress] = useState(false); const [loadingSchedule, setLoadingSchedule] = useState(false); // Dialogs const [showIllnessDialog, setShowIllnessDialog] = useState(false); const [showConversationDialog, setShowConversationDialog] = useState(false); const [showMeetingDialog, setShowMeetingDialog] = useState(false); const [illnessForm, setIllnessForm] = useState({ reason: 'illness', description: '', startDate: format(new Date(), 'yyyy-MM-dd'), endDate: '', }); const [conversationForm, setConversationForm] = useState({ message: '' }); const [meetingForm, setMeetingForm] = useState({ topic: '', preferredDate: '', notes: '' }); const [submitting, setSubmitting] = useState(false); /* ── Load main data ────────────────────────────────────────────────── */ const loadData = useCallback(async () => { if (!currentUser?.schoolId) return; setLoading(true); try { const res = await fetch(`/api/parent-portal?schoolId=${currentUser.schoolId}`); if (res.ok) { const data = await res.json(); setChildren(data.children ?? []); setPendingApprovals(data.pendingApprovals ?? []); setAnnouncements(data.announcements ?? []); setCommRooms(data.communicationRooms ?? []); setSchoolEvents(data.schoolEvents ?? []); if (data.children?.length > 0 && !selectedChild) { setSelectedChild(data.children[0]); } } } catch { toast.error('Failed to load parent portal data'); } finally { setLoading(false); } }, [currentUser?.schoolId]); useEffect(() => { loadData(); }, [loadData]); /* ── Load sub-data when child changes ──────────────────────────────── */ useEffect(() => { if (!selectedChild || !currentUser?.schoolId) return; setLoadingProgress(true); setLoadingSchedule(true); fetch(`/api/parent-portal/child-progress?studentId=${selectedChild.studentId}&schoolId=${currentUser.schoolId}`) .then((r) => r.json()) .then((d) => setProgressData(d)) .catch(() => {}) .finally(() => setLoadingProgress(false)); fetch(`/api/parent-portal/child-schedule?studentId=${selectedChild.studentId}&schoolId=${currentUser.schoolId}`) .then((r) => r.json()) .then((d) => setScheduleData(d)) .catch(() => {}) .finally(() => setLoadingSchedule(false)); }, [selectedChild?.studentId, currentUser?.schoolId]); /* ── Handlers ──────────────────────────────────────────────────────── */ const handleApproveIllness = async (reportId: string) => { try { const res = await fetch('/api/illness-reports/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reportId, action: 'approve' }), }); if (res.ok) { toast.success(t('parent_portal.illness_approved')); loadData(); } else toast.error('Failed to approve'); } catch { toast.error('Failed to approve'); } }; const handleRejectIllness = async (reportId: string) => { try { const res = await fetch('/api/illness-reports/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reportId, action: 'reject' }), }); if (res.ok) { toast.success(t('parent_portal.illness_rejected')); loadData(); } else toast.error('Failed to reject'); } catch { toast.error('Failed to reject'); } }; const handleReportIllness = async () => { if (!currentUser?.schoolId || !selectedChild) return; setSubmitting(true); try { await apiPost('/api/illness-reports', { schoolId: currentUser.schoolId, studentId: selectedChild.studentId, reportedBy: currentUser.id, reporterType: 'parent', reason: illnessForm.reason, description: illnessForm.description, startDate: illnessForm.startDate, endDate: illnessForm.endDate || null, }); toast.success(t('parent_portal.report_illness')); setShowIllnessDialog(false); setIllnessForm({ reason: 'illness', description: '', startDate: format(new Date(), 'yyyy-MM-dd'), endDate: '' }); loadData(); } catch { toast.error('Failed to report illness'); } finally { setSubmitting(false); } }; const handleRequestConversation = async () => { if (!currentUser?.schoolId || !selectedChild) return; setSubmitting(true); try { await apiPost('/api/counseling', { schoolId: currentUser.schoolId, studentId: selectedChild.userId, requestType: 'guidance', description: conversationForm.message, }); toast.success(t('parent_portal.conversation_requested')); setShowConversationDialog(false); setConversationForm({ message: '' }); loadData(); } catch { toast.error('Failed to request conversation'); } finally { setSubmitting(false); } }; const handleRequestMeeting = async () => { if (!currentUser?.schoolId || !selectedChild) return; setSubmitting(true); try { await apiPost('/api/counseling', { schoolId: currentUser.schoolId, studentId: selectedChild.userId, requestType: 'parent_teacher', description: `${meetingForm.topic}${meetingForm.preferredDate ? ` | ${loc === 'de' ? 'Gewuenschter Termin' : 'Preferred date'}: ${meetingForm.preferredDate}` : ''}${meetingForm.notes ? ` | ${meetingForm.notes}` : ''}`, }); toast.success(t('parent_portal.request_meeting')); setShowMeetingDialog(false); setMeetingForm({ topic: '', preferredDate: '', notes: '' }); loadData(); } catch { toast.error('Failed to request meeting'); } finally { setSubmitting(false); } }; const getAttendanceRate = (summary: { total: number; present: number; excused: number }) => { if (summary.total === 0) return 100; return Math.round(((summary.present + summary.excused) / summary.total) * 100); }; /* ── Chart data ────────────────────────────────────────────────────── */ const radarData = useMemo(() => { if (!progressData?.competencyBySubject) return []; return progressData.competencyBySubject.map((cs) => ({ subject: cs.subject.name, level: cs.averageLevel, fullMark: 5, })); }, [progressData]); const gradeHistoryData = useMemo(() => { if (!progressData?.subjectGrades) return []; const periods = new Set(); for (const sg of progressData.subjectGrades) { for (const g of sg.grades) periods.add(g.period); } const sortedPeriods = Array.from(periods).sort(); return sortedPeriods.map((period) => { const entry: Record = { period }; for (const sg of progressData.subjectGrades) { const pg = sg.grades.find((g) => g.period === period); if (pg) entry[sg.subject.name] = pg.computedValue; } return entry; }); }, [progressData]); const gradeComparisonData = useMemo(() => { if (!progressData?.subjectGrades) return []; return progressData.subjectGrades.map((sg) => ({ subject: sg.subject.name, student: sg.average, classAvg: progressData.classAverages[sg.subject.id] ?? 0, })); }, [progressData]); /* ── Loading State ─────────────────────────────────────────────────── */ if (loading) { return (
); } /* ── Empty State ───────────────────────────────────────────────────── */ if (children.length === 0) { return (

{t('parent_portal.title')}

{t('parent_portal.subtitle')}

{t('parent_portal.no_children')}

{t('parent_portal.no_children_desc')}

); } /* ── Render ────────────────────────────────────────────────────────── */ return (
{/* ── Gradient Header Banner ──────────────────────────────────── */}

{t('parent_portal.title')}

{t('parent_portal.subtitle')}

{selectedChild && ( <> )}
{/* ── Pending Approvals ───────────────────────────────────────── */} {pendingApprovals.length > 0 && ( {t('parent_portal.pending_approvals')} ({pendingApprovals.length})
{pendingApprovals.map((pa) => (

{pa.student.firstName} {pa.student.lastName}

{pa.reason} - {fmtShort(pa.startDate, loc)}

))}
)} {/* ── Child Selector ──────────────────────────────────────────── */} {children.map((child) => { const isActive = selectedChild?.studentId === child.studentId; return ( setSelectedChild(child)} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} className={`flex items-center gap-2 px-4 py-2.5 rounded-xl border-2 transition-all whitespace-nowrap min-h-[44px] ${isActive ? 'bg-gradient-to-r from-emerald-500 to-teal-500 text-white border-emerald-500 shadow-lg shadow-emerald-300/40 dark:shadow-emerald-900/40' : 'bg-white dark:bg-gray-900/40 border-emerald-100 dark:border-emerald-900/30 text-gray-700 dark:text-gray-300 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 hover:border-emerald-300/60 dark:hover:border-emerald-700/60'}`} > {child.firstName[0]}{child.lastName[0]} {child.firstName} {child.classGroups.length > 0 && ( {child.classGroups[0].name} )} ); })} {/* ── Child Data ──────────────────────────────────────────────── */} {selectedChild && ( {t('parent_portal.child_dashboard')} {t('parent_portal.academic_progress')} {t('parent_portal.attendance_illness')} {t('parent_portal.communication')} {t('parent_portal.schedule')} {t('parent_portal.reports_docs')} {/* ═══════════════════════════════════════════════════════════ OVERVIEW TAB ═══════════════════════════════════════════════════════════ */}
{/* Quick Stats Cards */}

{t('parent_portal.attendance_rate')}

%

{t('parent_portal.recent_grades')}

{t('parent_portal.upcoming_exams')}

{t('parent_portal.counseling_appointments')}

{/* Two Column: Upcoming Deadlines + Recent Activity */}
{/* Upcoming Deadlines */} {t('parent_portal.upcoming_deadlines')} {selectedChild.upcomingExams.length === 0 && selectedChild.homeworkDue.length === 0 ? (

{t('parent_portal.no_exams')}

) : (
{selectedChild.upcomingExams.slice(0, 4).map((exam) => (

{exam.title}

{exam.subject?.name ?? ''}

{exam.daysUntil === 0 ? (loc === 'de' ? 'Heute' : 'Today') : `${exam.daysUntil}d`}
))} {selectedChild.homeworkDue.slice(0, 3).map((hw) => (

{hw.title}

{hw.subject?.name ?? ''}

{t('parent_portal.due')}: {fmtShort(hw.dueDate, loc)}
))}
)}
{/* Recent Activity */} {t('parent_portal.recent_activity')} {selectedChild.recentGrades.length === 0 && selectedChild.illnessReports.length === 0 ? (

{loc === 'de' ? 'Keine Aktivitaeten' : 'No activities'}

) : (
{selectedChild.recentGrades.slice(0, 4).map((g) => (

{g.subject.name}

{g.period}

{g.computedValue.toFixed(1)}
))} {selectedChild.illnessReports.slice(0, 2).map((ir) => (

{ir.reason}

{fmtShort(ir.startDate, loc)}

{ir.parentApprovalStatus === 'approved' ? t('parent_portal.approved') : t('status.pending')}
))}
)}
{/* Quick Actions */}

{t('parent_portal.quick_actions')}

{/* ═══════════════════════════════════════════════════════════ ACADEMIC PROGRESS TAB ═══════════════════════════════════════════════════════════ */} {loadingProgress ? (
) : (
{/* Subject Grades with Trends */} {t('parent_portal.subject_grades')} {!progressData?.subjectGrades?.length ? (

{t('parent_portal.no_grades')}

) : (
{progressData.subjectGrades.map((sg, i) => (
{sg.subject.name.substring(0, 2).toUpperCase()}

{sg.subject.name}

{sg.grades.length} {loc === 'de' ? 'Bewertungen' : 'grades'}

{sg.average.toFixed(1)}

{progressData.classAverages[sg.subject.id] != null && (

{t('parent_portal.class')}: {progressData.classAverages[sg.subject.id].toFixed(1)}

)}
{sg.trend === 'up' ? : sg.trend === 'down' ? : }
))}
)}
{/* Charts Row */}
{/* Competency Radar */} {t('parent_portal.competency_radar')} {radarData.length < 3 ? (

{t('parent_portal.no_grades')}

) : ( )}
{/* Grade Comparison */} {t('parent_portal.comparison')} {gradeComparisonData.length === 0 ? (

{t('parent_portal.no_grades')}

) : ( )}
{/* Grade History Chart */} {gradeHistoryData.length > 0 && ( {t('parent_portal.grade_history')} {progressData?.subjectGrades?.slice(0, 5).map((sg, i) => ( ))} )} {/* Assessment Results */} {t('parent_portal.assessment_results')} {!progressData?.assessments?.length ? (

{t('parent_portal.no_assessments')}

) : (
{progressData.assessments.map((a) => (

{a.assessment.title}

{a.assessment.subject?.name ?? ''} - {fmtShort(a.assessedAt, loc)}

{a.score}/{a.maxScore}

{a.grade &&

{a.grade}

}
))}
)}
)}
{/* ═══════════════════════════════════════════════════════════ ATTENDANCE & ILLNESS TAB ═══════════════════════════════════════════════════════════ */}
{/* Attendance Rate Circular */} {t('parent_portal.attendance_rate_circular')}

{selectedChild.attendanceSummary.present}

{t('parent_portal.days_present')}

{selectedChild.attendanceSummary.absent}

{t('parent_portal.days_absent')}

{selectedChild.attendanceSummary.excused}

{t('parent_portal.days_excused')}

{selectedChild.attendanceSummary.late}

{t('parent_portal.days_late')}

{/* Illness Reports */}
{t('parent_portal.illness_history')}
{selectedChild.illnessReports.length === 0 ? (

{t('parent_portal.no_illness_reports')}

) : (
{selectedChild.illnessReports.map((report) => (

{report.reason}

{fmtShort(report.startDate, loc)} {report.endDate ? ` - ${fmtShort(report.endDate, loc)}` : ''}

{report.description &&

{report.description}

}
{report.parentApprovalStatus === 'approved' ? t('parent_portal.approved') : report.parentApprovalStatus === 'rejected' ? t('parent_portal.rejected') : t('status.pending')} {report.documentUrl && ( {t('parent_portal.upload_certificate')} )}
))}
)}
{/* Upload Certificate */}

{t('parent_portal.upload_certificate')}

{loc === 'de' ? 'Laden Sie ein aerztliches Attest hoch' : 'Upload a medical certificate'}

{/* ═══════════════════════════════════════════════════════════ COMMUNICATION TAB ═══════════════════════════════════════════════════════════ */}
{/* Messages from Teachers */} {t('parent_portal.messages_teachers')} {commRooms.length === 0 ? (

{t('parent_portal.no_messages')}

) : (
{commRooms.map((room) => ( {room.teacher.firstName[0]}{room.teacher.lastName[0]}

{room.teacher.firstName} {room.teacher.lastName}

{room.lastMessage?.content ?? (loc === 'de' ? 'Kein Inhalt' : 'No content')}

{room.lastMessage ? fmtShort(room.lastMessage.createdAt, loc) : ''}

{room.status}
))}
)}
{/* School Announcements */} {t('parent_portal.school_announcements')} {announcements.length === 0 ? (

{t('parent_portal.no_announcements')}

) : (
{announcements.map((ann) => (

{ann.title}

{ann.isPinned && {t('parent_portal.pinned')}} {ann.priority === 'urgent' && {t('parent_portal.urgent')}}

{ann.content}

{fmtShort(ann.createdAt, loc)}{ann.author ? ` - ${ann.author.firstName} ${ann.author.lastName}` : ''}

))}
)}
{/* Conversation Rooms */}
{t('parent_portal.conversation_rooms')}
{commRooms.length === 0 ? (

{t('parent_portal.no_messages')}

) : (
{commRooms.map((room) => (

{room.teacher.firstName} {room.teacher.lastName}

{loc === 'de' ? 'Fuer' : 'For'} {room.student.firstName} {room.student.lastName}

{room.status}
))}
)}
{/* School Events */} {t('parent_portal.school_events')} {schoolEvents.length === 0 ? (

{t('parent_portal.no_events')}

) : (
{schoolEvents.map((ev) => (

{ev.title}

{fmtShort(ev.startDate, loc)}{ev.location ? ` - ${ev.location}` : ''}

{ev.eventType}
))}
)}
{/* ═══════════════════════════════════════════════════════════ SCHEDULE TAB ═══════════════════════════════════════════════════════════ */} {loadingSchedule ? (
) : (
{/* Weekly Timetable */} {t('parent_portal.weekly_timetable')} {!scheduleData?.timetableSlots?.length ? (

{t('parent_portal.no_schedule')}

) : (
{dayNames(loc).map((d) => ( ))} {Array.from({ length: 8 }, (_, p) => p + 1).map((period) => { const slotsForPeriod = scheduleData.timetableSlots.filter((s) => s.period === period && !s.isBreak); const breakSlot = scheduleData.timetableSlots.find((s) => s.period === period && s.isBreak); if (breakSlot) { return ( ); } return ( {Array.from({ length: 5 }, (_, d) => d).map((day) => { const slot = slotsForPeriod.find((s) => s.dayOfWeek === day); if (!slot) return ); })} ); })}
{t('parent_portal.period')}{d}
{t('parent_portal.break')} ({breakSlot.startTime} - {breakSlot.endTime})
{period}. {t('parent_portal.period')} ; return (

{slot.subject?.name ?? '-'}

{slot.startTime}-{slot.endTime}

{slot.teacher &&

{slot.teacher.firstName[0]}. {slot.teacher.lastName}

}
)}
{/* Exam Dates */} {t('parent_portal.exam_dates')} {!scheduleData?.upcomingExams?.length ? (

{t('parent_portal.no_exams')}

) : (
{scheduleData.upcomingExams.map((exam) => (

{exam.title}

{exam.subject?.name ?? ''} - {fmtShort(exam.date, loc)}

{exam.daysUntil === 0 ? (loc === 'de' ? 'Heute' : 'Today') : `${exam.daysUntil}d`}
))}
)}
{/* Counseling Appointments */} {t('parent_portal.counseling_appointments_short')} {!scheduleData?.counselingAppointments?.length ? (

{t('parent_portal.no_counseling')}

) : (
{scheduleData.counselingAppointments.map((apt) => (

{apt.requestType}

{apt.counselor.firstName} {apt.counselor.lastName}{apt.scheduledAt ? ` - ${fmtShort(apt.scheduledAt, loc)}` : ''}

{apt.status}
))}
)}
{/* Homework Due */} {t('parent_portal.homework_due')} {!scheduleData?.homeworkDue?.length ? (

{t('parent_portal.no_homework')}

) : (
{scheduleData.homeworkDue.map((hw) => (

{hw.title}

{hw.subject?.name ?? ''} - {hw.classGroup.name}

{t('parent_portal.due')}: {fmtShort(hw.dueDate, loc)}
))}
)}
)}
{/* ═══════════════════════════════════════════════════════════ REPORTS & DOCUMENTS TAB ═══════════════════════════════════════════════════════════ */}
{/* Report Cards */} {t('parent_portal.report_cards')} {selectedChild.recentGrades.length === 0 ? (

{t('parent_portal.no_grades')}

) : (
{selectedChild.recentGrades.map((g) => (
{g.computedValue.toFixed(1)}

{g.subject.name}

{g.period} - {g.classGroup.name}

))}
)}
{/* Assessment Results */} {t('parent_portal.assessment_results')} {!progressData?.assessments?.length ? (

{t('parent_portal.no_assessments')}

) : (
{progressData.assessments.map((a) => (

{a.assessment.title}

{a.assessment.subject?.name ?? ''} - {fmtShort(a.assessedAt, loc)}

{a.feedback &&

{t('parent_portal.feedback')}: {a.feedback}

}

{a.score}/{a.maxScore}

{a.grade &&

{a.grade}

}
))}
)}
{/* Portfolio Highlights */} {t('parent_portal.portfolio_highlights')}

{t('parent_portal.no_portfolio')}

{loc === 'de' ? 'Portfolio-Eintraege werden hier angezeigt, sobald sie verfuegbar sind.' : 'Portfolio entries will appear here when available.'}

)} {/* ═══════════════════════════════════════════════════════════════ DIALOGS ═══════════════════════════════════════════════════════════════ */} {/* Illness Report Dialog */} {t('parent_portal.report_illness')} {selectedChild ? `${t('parent_portal.report_illness')} - ${selectedChild.firstName} ${selectedChild.lastName}` : ''}
setIllnessForm((d) => ({ ...d, startDate: e.target.value }))} />
setIllnessForm((d) => ({ ...d, endDate: e.target.value }))} />