'use client'; import React, { useEffect, useState } from 'react'; import { motion } from 'framer-motion'; import { RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, ResponsiveContainer, Tooltip, } from 'recharts'; import { ArrowLeft, User, Calendar, School, Hash, FileText, ClipboardCheck, Calculator, Flower2, BookOpen, GraduationCap, TrendingUp, Award, Printer, ChevronRight, Sparkles, MessageSquare, Grid3X3, Trophy, Flag, Zap, Rocket, Target, PenLine, Pencil, Home, ClipboardList, Star, BarChart3, LucideIcon, Download, FileSpreadsheet, FileDown, Heart, Users as UsersIcon, Eye, Plus, Trash2, Clock, CheckCircle2, XCircle, SlidersHorizontal, Lightbulb, Phone, PhoneCall, MapPin, ShieldAlert, UserCheck, AlertCircle, Globe, Bus, Car, Bike, Footprints, Train, Truck, Droplets, Pill, Stethoscope, EyeOff, Shield, Activity, HeartPulse, Syringe, QrCode, CalendarCheck, Leaf, Camera, Upload, } 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 { Skeleton } from '@/components/ui/skeleton'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Tooltip as ShadTooltip, TooltipContent as ShadTooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; import { useAppStore, type CurrentUser } from '@/lib/store'; import { t } from '@/lib/i18n'; import StudentAvatar from '@/components/student-avatar'; import { fetchStudentDetail, getReportPdfUrl, type StudentDetailData, fetchParentLinks, type ParentStudentLinkData, apiGet, apiPost, apiPut, apiDelete, fetchBadgeProgress, type BadgeProgressData, fetchStudentBadges, type StudentBadgeData, type BadgeData } from '@/lib/api'; import { generateQRCodeSync, downloadQRCode, type QRCodeData } from '@/lib/qrcode'; import { toast } from 'sonner'; import TeacherNotesSection from './teacher-notes-section'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Slider } from '@/components/ui/slider'; import { Label } from '@/components/ui/label'; import { Progress } from '@/components/ui/progress'; // ─── Self-Assessment & Learning Goal Types ──────────────────────────────── interface PeerAssessmentData { id: string; schoolId: string; assessorId: string; assessedId: string; competencyId: string | null; classGroupId: string | null; assessmentType: string; level: number | null; comment: string | null; rubricId: string | null; isAnonymous: boolean; isDemo: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; assessor: { id: string; firstName: string; lastName: string }; assessed: { id: string; firstName: string; lastName: string }; competency: { id: string; code: string; title: string } | null; classGroup: { id: string; name: string } | null; rubric: { id: string; title: string } | null; } interface EmergencyContactData { id: string; schoolId: string; studentId: string; name: string; relationship: string; phone: string; phoneAlt: string | null; email: string | null; address: string | null; isPrimary: boolean; priority: number; notes: string | null; isDemo: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; student: { id: string; firstName: string; lastName: string }; } interface SelfAssessmentData { id: string; schoolId: string; studentId: string; competencyId: string; classGroupId: string | null; selfLevel: number; confidence: number | null; reflection: string | null; evidence: string | null; goalId: string | null; isDemo: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; student: { id: string; firstName: string; lastName: string }; competency: { id: string; code: string; title: string }; classGroup: { id: string; name: string } | null; goal: { id: string; title: string; status: string } | null; } interface LearningGoalData { id: string; schoolId: string; studentId: string; competencyId: string | null; title: string; description: string | null; targetLevel: number | null; currentLevel: number | null; deadline: string | null; status: string; progress: number; isDemo: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; student: { id: string; firstName: string; lastName: string }; competency: { id: string; code: string; title: string } | null; selfAssessments: { id: string; selfLevel: number; confidence: number | null; createdAt: string }[]; } // ─── Journey Timeline Time Filter State ───────────────────────────────── type JourneyTimeRange = '30' | '90' | 'all'; function useJourneyTimeRange() { const [range, setRange] = React.useState('all'); return [range, setRange] as const; } function JourneyTimeFilter({ range, setRange }: { range: JourneyTimeRange; setRange: (v: JourneyTimeRange) => void }) { const options: { value: JourneyTimeRange; labelKey: string }[] = [ { value: '30', labelKey: 'student.journey_last_30' }, { value: '90', labelKey: 'student.journey_last_90' }, { value: 'all', labelKey: 'student.journey_all' }, ]; return (
{options.map((opt) => ( ))}
); } // ─── Journey Timeline Types ───────────────────────────────────────────── interface MilestoneItem { key: string; date: string; type: 'achieved' | 'started' | 'above_avg' | 'grade'; title: string; desc: string; icon: React.ElementType; color: string; bg: string; } function JourneyTimeline({ progressEntries, assessmentResults, computedGrades, reports, timeRange, }: { progressEntries: StudentDetailData['progressEntries']; assessmentResults: StudentDetailData['assessmentResults']; computedGrades: StudentDetailData['computedGrades']; reports: StudentDetailData['reports']; timeRange: JourneyTimeRange; }) { // Build milestones from data const milestones = React.useMemo(() => { const items: MilestoneItem[] = []; // Progress entries with mastery ≥ 3.5 → competency achieved const seenCategories = new Set(); for (const e of progressEntries) { const cat = e.competency.category?.name ?? ''; // First entry in a new category → started new area if (cat && !seenCategories.has(cat)) { seenCategories.add(cat); items.push({ key: `started-${e.id}`, date: e.date, type: 'started', title: e.competency.category.name, desc: t('student.journey_started'), icon: Rocket, color: 'bg-teal-500', bg: 'bg-teal-100 dark:bg-teal-900/30 text-teal-600 dark:text-teal-300', }); } // Mastery ≥ 3.5 → competency achieved if (e.masteryLevelValue >= 3.5) { items.push({ key: `achieved-${e.id}`, date: e.date, type: 'achieved', title: e.competency.title, desc: t('student.journey_achieved'), icon: Target, color: 'bg-emerald-500', bg: 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-300', }); } } // Assessment results above average → above average performance if (assessmentResults.length > 0) { const scoresWithMax = assessmentResults.filter((r) => r.score !== null && r.assessment.maxScore); if (scoresWithMax.length > 0) { const avgRatio = scoresWithMax.reduce((s, r) => s + (r.score! / r.assessment.maxScore!), 0) / scoresWithMax.length; for (const r of scoresWithMax) { if (r.score !== null && r.assessment.maxScore && (r.score / r.assessment.maxScore) > avgRatio) { items.push({ key: `above-${r.id}`, date: r.assessment.date, type: 'above_avg', title: r.assessment.title, desc: t('student.journey_above_avg'), icon: Sparkles, color: 'bg-amber-500', bg: 'bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-300', }); } } } } // Grade computation milestones for (const g of computedGrades) { items.push({ key: `grade-${g.id}`, date: g.period, type: 'grade', title: `${g.subject.name} · ${g.period}`, desc: `${t('student.journey_grade')} — ${(g.overriddenValue ?? g.computedValue).toFixed(1)}`, icon: Calculator, color: 'bg-violet-500', bg: 'bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-300', }); } // Sort by date descending items.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); // Filter by time range if (timeRange !== 'all') { const days = parseInt(timeRange); const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - days); return items.filter((item) => { const d = new Date(item.date); return d >= cutoff || isNaN(d.getTime()); }); } return items.slice(0, 20); }, [progressEntries, assessmentResults, computedGrades, timeRange]); if (milestones.length === 0) { return (

{t('student_detail.no_data')}

); } return (
{milestones.map((m, i) => (

{m.title}

{new Date(m.date).toLocaleDateString()}

{m.desc}

))}
); } const PETAL_COLORS = [ '#10b981', '#14b8a6', '#06b6d4', '#0ea5e9', '#8b5cf6', '#f59e0b', '#ef4444', '#ec4899', '#6366f1', '#84cc16', ]; const masteryBadge = (level: number) => { if (level <= 0) return 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-300'; if (level <= 1) return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'; if (level <= 2) return 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'; if (level <= 3) return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300'; return 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-300'; }; // Icon map for badge icons const iconMap: Record = { CalendarCheck, Award, TrendingUp, Star, BookOpen, Pencil, ClipboardCheck, UsersIcon, Target, Leaf, Trophy, Zap, Rocket, Heart, GraduationCap, CheckCircle2, Shield, Activity, Flower2, Flag, Lightbulb, }; const gradeColor = (value: number) => { if (value <= 2) return 'text-emerald-600 dark:text-emerald-400'; if (value <= 4) return 'text-amber-600 dark:text-amber-400'; return 'text-rose-600 dark:text-rose-400'; }; const assessmentTypeIcon: Record = { TEST: FileText, ORAL: Pencil, PROJECT: Target, HOMEWORK: Home, OTHER: ClipboardList, }; function calcAge(dob: string | null): number | null { if (!dob) return null; const birth = new Date(dob); const now = new Date(); let age = now.getFullYear() - birth.getFullYear(); const m = now.getMonth() - birth.getMonth(); if (m < 0 || (m === 0 && now.getDate() < birth.getDate())) age--; return age >= 0 ? age : null; } // ─── CSV Export Helper ─────────────────────────────────────────────────── function exportStudentCsv(data: StudentDetailData) { const { student, progressEntries, assessmentResults, computedGrades, flowers, stats } = data; const studentName = `${student.firstName} ${student.lastName}`; const primaryClass = student.enrollments[0]?.classGroup.name ?? ''; const schoolName = student.school?.name ?? ''; const rows: string[] = []; // Header section rows.push('CompetenceTrack - Student Export'); rows.push(`Student,${studentName}`); rows.push(`Class,${primaryClass}`); rows.push(`School,${schoolName}`); rows.push(`Date of Birth,${student.dateOfBirth ?? ''}`); rows.push(`External ID,${student.externalId ?? ''}`); rows.push(''); rows.push('Quick Stats'); rows.push(`Total Progress Entries,${stats.totalProgressEntries}`); rows.push(`Average Mastery,${stats.averageMastery.toFixed(2)}`); rows.push(`Latest Grade,${stats.latestGrade ? stats.latestGrade.value.toFixed(1) : ''}`); rows.push(`Total Reports,${stats.totalReports}`); rows.push(`Total Assessments,${stats.totalAssessments}`); rows.push(''); // Competence flower summary rows.push('Competence Flower Summary'); rows.push('Subject,Category,Average Mastery,Assessed Competencies,Total Competencies'); for (const flower of flowers) { for (const cat of flower.categories) { rows.push(`"${flower.subjectName}","${cat.categoryName}",${cat.averageMasteryLevel.toFixed(2)},${cat.assessedCompetencyCount},${cat.competencyCount}`); } } rows.push(''); // Progress entries rows.push('Progress Entries'); rows.push('Date,Competency Code,Competency Title,Category,Mastery Level,Class,Teacher,Note'); for (const e of progressEntries) { const note = (e.note ?? '').replace(/"/g, '""'); rows.push(`${e.date},"${e.competency.code}","${e.competency.title}","${e.competency.category?.name ?? ''}",${e.masteryLevelValue},"${e.classGroup.name}","${e.teacher.firstName} ${e.teacher.lastName}","${note}"`); } rows.push(''); // Assessment results rows.push('Assessment Results'); rows.push('Date,Assessment Title,Subject,Type,Score,Max Score,Mastery Level,Note'); for (const r of assessmentResults) { const note = (r.note ?? '').replace(/"/g, '""'); rows.push(`${r.assessment.date},"${r.assessment.title}","${r.assessment.subject.name}","${r.assessment.type}",${r.score ?? ''},${r.assessment.maxScore ?? ''},${r.masteryLevelValue ?? ''},"${note}"`); } rows.push(''); // Computed grades rows.push('Computed Grades'); rows.push('Subject,Period,Computed Value,Overridden Value,Finalized,Class,School Year'); for (const g of computedGrades) { rows.push(`"${g.subject.name}","${g.period}",${g.computedValue.toFixed(2)},${g.overriddenValue !== null ? g.overriddenValue.toFixed(2) : ''},${g.isFinalized ? 'Yes' : 'No'},"${g.classGroup.name}","${g.schoolYear.label}"`); } rows.push(''); const csvContent = rows.join('\n'); const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${studentName.replace(/\s+/g, '_')}_export.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } export default function StudentDetailView() { const currentStudentId = useAppStore((s) => s.currentStudentId); const navigateBack = useAppStore((s) => s.navigateBack); const setCurrentView = useAppStore((s) => s.setCurrentView); const currentUser = useAppStore((s) => s.currentUser); const isStudentSelf = currentUser?.role === 'STUDENT'; const isParent = currentUser?.role === 'PARENT'; const [parentLinks, setParentLinks] = useState([]); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [selectedFlowerSubjectId, setSelectedFlowerSubjectId] = useState(''); const [timeRange, setTimeRange] = useState('all'); // Self-Assessment & Learning Goals state const [selfAssessments, setSelfAssessments] = useState([]); const [learningGoals, setLearningGoals] = useState([]); const [saDialogOpen, setSaDialogOpen] = useState(false); const [saEditId, setSaEditId] = useState(null); const [saForm, setSaForm] = useState({ competencyId: '', selfLevel: 3, confidence: 3, reflection: '', evidence: '', goalId: '' }); const [lgDialogOpen, setLgDialogOpen] = useState(false); const [lgEditId, setLgEditId] = useState(null); const [lgForm, setLgForm] = useState({ title: '', description: '', competencyId: '', targetLevel: 4, currentLevel: 1, deadline: '', status: 'active', progress: 0 }); const [goalCelebration, setGoalCelebration] = useState(null); // Peer Assessment & Emergency Contacts state const [peerAssessments, setPeerAssessments] = useState([]); const [emergencyContacts, setEmergencyContacts] = useState([]); const [paDialogOpen, setPaDialogOpen] = useState(false); const [paForm, setPaForm] = useState({ assessmentType: 'competency', competencyId: '', level: 3, comment: '', isAnonymous: false }); const [ecDialogOpen, setEcDialogOpen] = useState(false); const [ecEditId, setEcEditId] = useState(null); const [ecForm, setEcForm] = useState({ name: '', relationship: 'mother', phone: '', phoneAlt: '', email: '', address: '', isPrimary: false, priority: 1, notes: '' }); // Transportation state interface TransportData { id: string; schoolId: string; studentId: string; transportType: string; routeNumber: string | null; stopName: string | null; pickupTime: string | null; dropoffTime: string | null; driverName: string | null; driverPhone: string | null; distanceKm: number | null; notes: string | null; isDemo: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; student: { id: string; firstName: string; lastName: string }; } const [transports, setTransports] = useState([]); const [transportDialogOpen, setTransportDialogOpen] = useState(false); const [transportEditId, setTransportEditId] = useState(null); const [transportForm, setTransportForm] = useState({ transportType: 'bus', routeNumber: '', stopName: '', pickupTime: '', dropoffTime: '', driverName: '', driverPhone: '', distanceKm: '', notes: '', }); // Health Records state interface HealthRecordData { id: string; schoolId: string; studentId: string; bloodType: string | null; allergies: string | null; medications: string | null; conditions: string | null; doctorName: string | null; doctorPhone: string | null; insuranceNumber: string | null; insuranceProvider: string | null; lastCheckup: string | null; notes: string | null; isConfidential: boolean; isDemo: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; student: { id: string; firstName: string; lastName: string }; } const [healthRecord, setHealthRecord] = useState(null); const [healthDialogOpen, setHealthDialogOpen] = useState(false); const [healthEditId, setHealthEditId] = useState(null); const [healthForm, setHealthForm] = useState({ bloodType: '', allergies: '' as string, medications: '' as string, conditions: '' as string, doctorName: '', doctorPhone: '', insuranceNumber: '', insuranceProvider: '', lastCheckup: '', notes: '', isConfidential: true, }); // QR Code & Badge state const [qrDialogOpen, setQrDialogOpen] = useState(false); const [qrCodeDataUrl, setQrCodeDataUrl] = useState(''); const [badgeProgress, setBadgeProgress] = useState([]); const [earnedBadges, setEarnedBadges] = useState([]); const [badgeDetailDialogOpen, setBadgeDetailDialogOpen] = useState(false); const [selectedBadgeDetail, setSelectedBadgeDetail] = useState(null); // Avatar upload dialog const [avatarUploadOpen, setAvatarUploadOpen] = useState(false); const [avatarUrlInput, setAvatarUrlInput] = useState(''); const [avatarUploading, setAvatarUploading] = useState(false); // Load parent links for parent users useEffect(() => { if (isParent && currentUser?.id) { fetchParentLinks(currentUser.id).then(setParentLinks).catch(() => {}); } }, [isParent, currentUser?.id]); useEffect(() => { if (!currentStudentId) { setLoading(false); return; } const studentId = currentStudentId; // capture for closure let cancelled = false; async function load() { setLoading(true); try { const d = await fetchStudentDetail(studentId); if (cancelled) return; setData(d); if (d.flowers.length > 0) { setSelectedFlowerSubjectId(d.flowers[0].subjectId); } else { setSelectedFlowerSubjectId(''); } } catch (err) { if (!cancelled) { toast.error(err instanceof Error ? err.message : t('error.generic')); } } finally { if (!cancelled) setLoading(false); } } load(); return () => { cancelled = true; }; }, [currentStudentId]); // Load self-assessments and learning goals useEffect(() => { if (!currentStudentId || !currentUser?.schoolId) return; const schoolId = currentUser.schoolId; async function loadSA() { try { const data = await apiGet(`/api/self-assessments?schoolId=${schoolId}&studentId=${currentStudentId}`); setSelfAssessments(data); } catch { /* ignore */ } } async function loadLG() { try { const data = await apiGet(`/api/learning-goals?schoolId=${schoolId}&studentId=${currentStudentId}`); setLearningGoals(data); } catch { /* ignore */ } } loadSA(); loadLG(); }, [currentStudentId, currentUser?.schoolId]); // Load peer assessments and emergency contacts useEffect(() => { if (!currentStudentId || !currentUser?.schoolId) return; const schoolId = currentUser.schoolId; async function loadPA() { try { const data = await apiGet(`/api/peer-assessments?schoolId=${schoolId}&assessedId=${currentStudentId}`); setPeerAssessments(data); } catch { /* ignore */ } } async function loadEC() { try { const data = await apiGet(`/api/emergency-contacts?schoolId=${schoolId}&studentId=${currentStudentId}`); setEmergencyContacts(data); } catch { /* ignore */ } } loadPA(); loadEC(); }, [currentStudentId, currentUser?.schoolId]); // Load transportation and health records useEffect(() => { if (!currentStudentId || !currentUser?.schoolId) return; const schoolId = currentUser.schoolId; async function loadTransport() { try { const data = await apiGet(`/api/student-transport?schoolId=${schoolId}&studentId=${currentStudentId}`); setTransports(data); } catch { /* ignore */ } } async function loadHealth() { try { const data = await apiGet(`/api/health-records?schoolId=${schoolId}&studentId=${currentStudentId}`); setHealthRecord(data.length > 0 ? data[0] : null); } catch { /* ignore */ } } loadTransport(); loadHealth(); }, [currentStudentId, currentUser?.schoolId]); // Load badge progress and earned badges useEffect(() => { if (!currentStudentId || !currentUser?.schoolId) return; const schoolId = currentUser.schoolId; async function loadBadges() { try { const progress = await fetchBadgeProgress(schoolId, currentStudentId); setBadgeProgress(progress); } catch { /* ignore */ } } async function loadEarnedBadges() { try { const data = await fetchStudentBadges(schoolId, currentStudentId); setEarnedBadges(data); } catch { /* ignore */ } } loadBadges(); loadEarnedBadges(); }, [currentStudentId, currentUser?.schoolId]); if (loading) { return (
); } if (!data || !currentStudentId) { return (

{t('student_detail.no_data')}

); } const { student, progressEntries, assessmentResults, computedGrades, reports, flowers, stats } = data; const studentName = `${student.firstName} ${student.lastName}`; const age = calcAge(student.dateOfBirth); const initials = `${student.firstName[0] ?? ''}${student.lastName[0] ?? ''}`.toUpperCase(); const primaryClass = student.enrollments[0]?.classGroup; const currentFlower = flowers.find((f) => f.subjectId === selectedFlowerSubjectId) ?? null; const chartData = currentFlower?.categories.map((cat, i) => ({ category: cat.categoryName, value: cat.averageMasteryLevel > 0 ? cat.averageMasteryLevel : 0.2, fill: PETAL_COLORS[i % PETAL_COLORS.length], })) ?? []; return (
{/* Back button + Export actions */}
{/* Header card with student info & quick stats */} {/* Decorative background pattern */}
{/* Avatar + name */}
{/* Decorative halo */}
{/* Mastery badge */}
{/* Upload button */}

{studentName}

{primaryClass && ( {t('student_detail.class')}: {primaryClass.name} )} {student.school && ( {student.school.name} )}
{student.dateOfBirth && ( {new Date(student.dateOfBirth).toLocaleDateString()} {age !== null && ( ({age} {t('student_detail.years')}) )} )} {student.externalId && ( {student.externalId} )}
{/* Quick stats */}

{stats.totalProgressEntries}

{t('student_detail.total_entries')}

{stats.averageMastery.toFixed(1)}

{t('student_detail.avg_mastery')}

{stats.latestGrade ? stats.latestGrade.value.toFixed(1) : '—'}

{t('student_detail.latest_grade')}

{stats.totalReports}

{t('student_detail.reports')}

{/* Class enrollment badges */} {student.enrollments.length > 0 && (
{t('student_detail.class')}: {student.enrollments.map((e, i) => ( {e.classGroup.name} · {e.schoolYear.label} ))}
)} {/* Quick Stats Bar + Badge Collection */}
{stats.totalProgressEntries} {t('student_detail.total_entries')}
{stats.averageMastery.toFixed(1)} {t('student_detail.avg_mastery')}
{student.externalId || student.id.slice(0, 8)}
{badgeProgress.filter(b => b.earned).length}/{badgeProgress.length} {t('badges.title')}
{/* Badge Collection Section */} {badgeProgress.length > 0 && (
{t('badges.collection')}
{t('badges.earned_count', { earned: badgeProgress.filter(b => b.earned).length, total: badgeProgress.length })}
{badgeProgress.map((bp) => { const BadgeIcon = (iconMap as Record)[bp.icon] || Award; return (
{ setSelectedBadgeDetail(bp); setBadgeDetailDialogOpen(true); }} > {bp.earned &&
} {!bp.earned && bp.progress > 0 && (
)}
{bp.name} {bp.earned ? ` — ${t('badges.earned')}` : ` — ${bp.progress}% ${t('badges.progress')}`} ); })}
{/* Recent badge awards */} {earnedBadges.length > 0 && (

{t('badges.recent')}

{earnedBadges.slice(0, 5).map((eb) => { const BadgeIcon = (iconMap as Record)[eb.badge.icon] || Award; return (
{eb.badge.name} {new Date(eb.awardedAt).toLocaleDateString()}
); })}
)} )} {/* Badge Detail Dialog */} {selectedBadgeDetail && (() => { const BadgeIcon = (iconMap as Record)[selectedBadgeDetail.icon] || Award; return ; })()} {selectedBadgeDetail?.name} {selectedBadgeDetail?.earned ? t('badges.earned') : t('badges.unearned')} {selectedBadgeDetail && (
{(() => { const BadgeIcon = (iconMap as Record)[selectedBadgeDetail.icon] || Award; return ; })()}
{!selectedBadgeDetail.earned && (
{t('badges.progress')} {selectedBadgeDetail.current}/{selectedBadgeDetail.target}
)}
{t('badges.category')}: {t(`badges.${selectedBadgeDetail.category}`)}
)}
{/* QR Code Dialog */} {t('qr.student')} {studentName}
{qrCodeDataUrl ? ( {t('qr.title')} ) : (
)}

{studentName}

{student.externalId || student.id.slice(0, 8)}

{/* Competence Flower (mini radar) */}
{t('student_detail.competence_flower')}
{flowers.length > 0 && ( )}
{flowers.length === 0 ? (

{t('student_detail.no_flower')}

{t('student_detail.no_flower_hint')}

) : !currentFlower || chartData.length === 0 ? (

{t('student_detail.flower_not_assigned')}

) : (
[`${value.toFixed(2)} / 4`, t('student_detail.value')]} />

{t('student_detail.value_hint')}

{t('student_detail.sparse_chart_hint')}

{/* Category breakdown */}
{currentFlower.categories.map((cat, i) => (
{cat.categoryName}
0 ? masteryBadge(cat.averageMasteryLevel) : 'bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400'} text-xs cursor-default`}> {cat.averageMasteryLevel > 0 ? cat.averageMasteryLevel.toFixed(2) : t('student_detail.no_assessment')} {cat.averageMasteryLevel > 0 ? `${cat.averageMasteryLevel.toFixed(2)} / 4` : t('student_detail.no_assessment')}

{cat.assessedCompetencyCount} / {cat.competencyCount} {t('student_detail.competency').toLowerCase()}

))}
)} {/* Progress timeline */}
{t('student_detail.progress_timeline')} {progressEntries.length}
{progressEntries.length === 0 ? (

{t('student_detail.no_progress_entries')}

) : (
{progressEntries.map((entry, i) => (
{entry.masteryLevelValue} {entry.competency.code} — {entry.competency.title}
{entry.competency.category && (

{entry.competency.category.name} · {entry.classGroup.name}

)} {entry.note && (

"{entry.note}"

)}
{new Date(entry.date).toLocaleDateString()} — {entry.teacher.firstName} {entry.teacher.lastName}
))}
)} {/* Two-column: Assessment results + Computed grades */}
{/* Assessment results */}
{t('student_detail.assessment_results')} {assessmentResults.length}
{assessmentResults.length === 0 ? (

{t('student_detail.no_assessments')}

) : (
{t('student_detail.assessment')} {t('student_detail.date')} {t('student_detail.score')} {assessmentResults.map((r, idx) => (
{(() => { const Icon = assessmentTypeIcon[r.assessment.type] ?? ClipboardList; return ; })()}

{r.assessment.title}

{r.assessment.subject.name}

{new Date(r.assessment.date).toLocaleDateString()} {r.score !== null && r.assessment.maxScore ? ( {r.score} / {r.assessment.maxScore} ) : r.masteryLevelValue !== null ? ( {r.masteryLevelValue} ) : ( — )}
))}
)}
{/* Computed grades */}
{t('student_detail.computed_grades')} {computedGrades.length}
{computedGrades.length === 0 ? (

{t('student_detail.no_grades')}

) : (
{t('student_detail.subject')} {t('student_detail.period')} {t('student_detail.value')} {computedGrades.map((g, idx) => { const value = g.overriddenValue ?? g.computedValue; return ( {g.subject.name} {g.period} {value.toFixed(1)} {g.overriddenValue !== null && ( )} ); })}
)}
{/* Reports list */}
{t('student_detail.reports')} {reports.length}
{reports.length === 0 ? (

{t('student_detail.no_reports')}

) : (
{reports.map((r, i) => (

{r.period}

{r.status} {r.includesGrades && ( {t('student_detail.computed_grades')} )}

{r.classGroup.name} · {r.schoolYear.label}

{t('student_detail.generated_at')}: {new Date(r.generatedAt).toLocaleDateString()} — {r.generatedByUser.firstName} {r.generatedByUser.lastName}

))}
)}
{/* Student journey timeline + competency mastery grid */}
{/* Student journey timeline — enhanced with milestones & time filter */}
{t('student.journey')}
{/* Competency mastery grid */}
{t('polish.competency_grid')}
{currentFlower && currentFlower.categories.length > 0 ? (
{currentFlower.categories.map((cat, i) => { const catColor = PETAL_COLORS[i % PETAL_COLORS.length]; // Generate deterministic pseudo mastery cells per category let h = 0; for (let j = 0; j < cat.categoryId.length; j++) h = (h * 31 + cat.categoryId.charCodeAt(j)) | 0; h = Math.abs(h); const cells = Array.from({ length: cat.competencyCount }, (_, idx) => { if (idx < cat.assessedCompetencyCount) { const lvl = 1 + ((h >> (idx * 2)) & 3); return lvl; } return 0; // unassessed }); return (
{cat.categoryName}
{cat.assessedCompetencyCount}/{cat.competencyCount}
{cells.map((lvl, idx) => (
))}
); })} {/* Legend */}
{[ { lvl: 0, label: t('polish.never'), color: 'bg-gray-300 dark:bg-gray-600' }, { lvl: 1, label: t('polish.level_1'), color: 'bg-red-400' }, { lvl: 2, label: t('polish.level_2'), color: 'bg-amber-400' }, { lvl: 3, label: t('polish.level_3'), color: 'bg-emerald-400' }, { lvl: 4, label: t('polish.level_4'), color: 'bg-teal-400' }, ].map((l) => ( {l.label} ))}
) : (

{t('student_detail.no_flower')}

)}
{/* Recent achievements + Teacher notes */}
{/* Recent achievements */}
{t('polish.recent_achievements')}
{(() => { const achievements = progressEntries .filter((e) => e.masteryLevelValue >= 3.5) .slice(0, 6); if (achievements.length === 0) { return (

{t('polish.no_results')}

); } const achievementBadges: { icon: LucideIcon; label: string; color: string }[] = [ { icon: Trophy, label: t('polish.level_4'), color: 'bg-teal-100 dark:bg-teal-900/30 text-teal-700 dark:text-teal-300' }, { icon: Star, label: t('polish.strengths'), color: 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300' }, { icon: Sparkles, label: t('polish.level_3'), color: 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300' }, { icon: Target, label: t('label.mastery'), color: 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300' }, { icon: BookOpen, label: t('nav.competencies'), color: 'bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300' }, ]; return (
{achievements.map((e, i) => (
{(() => { const BadgeIcon = achievementBadges[i % achievementBadges.length].icon; return ; })()}

{e.competency.title}

{e.competency.category.name} · {new Date(e.date).toLocaleDateString()}

{e.masteryLevelValue.toFixed(1)}
))}
); })()}
{/* Teacher notes — replaced with the dedicated TeacherNotesSection */} {/* My Progress section (for student self-view) */} {isStudentSelf && (
{t('student.my_competencies')}

{t('student.progress_overview')}

{/* Mastery Level Visualization */}
{stats.averageMastery > 0 && (
{t('student_detail.avg_mastery')} {stats.averageMastery.toFixed(1)} / 4.0
)} {stats.totalProgressEntries > 0 && (
{t('student_detail.total_entries')} {stats.totalProgressEntries}
)} {stats.latestGrade && (
{t('student_detail.latest_grade')} {stats.latestGrade.value.toFixed(1)}
)}
)} {/* Parent Info section (for parent users) */} {isParent && parentLinks.length > 0 && (
{t('parent.my_children')}
{parentLinks.map((link) => (
{link.student.firstName[0]}{link.student.lastName[0]}
{link.student.firstName} {link.student.lastName}
{link.relationship && ( {t(`parent.${link.relationship}`)} )}
))}
)}
{/* ─── Self-Assessment Section ─────────────────────────────────── */}
{t('self_assessment.title')} {selfAssessments.length}
{selfAssessments.length === 0 ? (

{t('self_assessment.no_assessments')}

) : ( <> {/* Comparison radar chart */} {(() => { const comparisonData = selfAssessments.map((sa) => { const teacherEntry = data?.progressEntries.find( (pe) => pe.competencyId === sa.competencyId ); return { competency: sa.competency.code.length > 12 ? sa.competency.code.slice(0, 12) : sa.competency.code, self: sa.selfLevel, teacher: teacherEntry ? teacherEntry.masteryLevelValue : 0, }; }).filter((d) => d.teacher > 0); if (comparisonData.length < 3) return null; return (

{t('self_assessment.comparison_chart')}

{t('self_assessment.self')} {t('self_assessment.teacher')}
); })()} {/* Self-assessment list */}
{selfAssessments.map((sa, i) => { const teacherEntry = data?.progressEntries.find((pe) => pe.competencyId === sa.competencyId); const gap = teacherEntry ? sa.selfLevel - teacherEntry.masteryLevelValue : null; return (
{sa.competency.code} — {sa.competency.title} {t('self_assessment.level')}: {sa.selfLevel}/6 {sa.confidence && ( {t('self_assessment.confidence')}: {sa.confidence}/5 )} {gap !== null && ( {t('self_assessment.gap')}: {gap > 0 ? '+' : ''}{gap.toFixed(1)} )}
{sa.reflection && (

“{sa.reflection}”

)} {sa.evidence && (

{sa.evidence}

)}

{new Date(sa.createdAt).toLocaleDateString()}

); })}
)}
{/* ─── Learning Goals Section ─────────────────────────────────── */}
{t('learning_goal.title')} {learningGoals.filter((g) => g.status === 'active').length}
{learningGoals.length === 0 ? (

{t('learning_goal.no_goals')}

) : (
{learningGoals.map((goal, i) => { const daysLeft = goal.deadline ? Math.ceil((new Date(goal.deadline).getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : null; const isOverdue = daysLeft !== null && daysLeft < 0; const statusColor = goal.status === 'completed' ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300' : goal.status === 'abandoned' ? 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-300' : isOverdue ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300' : 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300'; const statusLabel = goal.status === 'completed' ? t('learning_goal.completed') : goal.status === 'abandoned' ? t('learning_goal.abandoned') : t('learning_goal.active'); return (
{goal.title} {goal.status === 'completed' ? : goal.status === 'abandoned' ? : } {statusLabel} {goal.competency && ( {goal.competency.code} )}
{goal.description && (

{goal.description}

)} {/* Progress bar */}
{t('learning_goal.progress')} {goal.progress}%
{/* Level indicators */}
{goal.currentLevel !== null && ( {t('learning_goal.current_level')}: {goal.currentLevel} )} {goal.targetLevel !== null && ( {t('learning_goal.target_level')}: {goal.targetLevel} )} {daysLeft !== null && ( {isOverdue ? t('learning_goal.overdue') : `${daysLeft} ${t('learning_goal.days_left')}`} )}
{goal.status === 'active' && ( )}
{/* Celebration overlay */} {goalCelebration === goal.id && ( {t('learning_goal.celebration')} )}
); })}
)}
{/* ─── Peer Assessment Section ─────────────────────────────────── */}
{t('peer.title')} {peerAssessments.length}
{peerAssessments.length === 0 ? (

{t('peer.no_assessments')}

) : ( <> {/* 3-column radar comparison: Peer / Teacher / Self */} {(() => { const peerByCompetency = peerAssessments.filter((pa) => pa.competencyId && pa.level); const selfByCompetency = selfAssessments.filter((sa) => sa.selfLevel); const teacherByCompetency = data?.progressEntries ?? []; const compIds = new Set(); peerByCompetency.forEach((pa) => { if (pa.competencyId) compIds.add(pa.competencyId); }); selfByCompetency.forEach((sa) => compIds.add(sa.competencyId)); teacherByCompetency.forEach((pe) => compIds.add(pe.competencyId)); const comparisonData = Array.from(compIds).slice(0, 8).map((cId) => { const peerLevels = peerByCompetency.filter((pa) => pa.competencyId === cId).map((pa) => pa.level!); const selfEntry = selfByCompetency.find((sa) => sa.competencyId === cId); const teacherEntry = teacherByCompetency.find((pe) => pe.competencyId === cId); const code = peerLevels.length > 0 ? (peerByCompetency.find((pa) => pa.competencyId === cId)?.competency?.code ?? cId.slice(0, 8)) : selfEntry?.competency?.code ?? teacherEntry?.competency?.code ?? cId.slice(0, 8); return { competency: code.length > 12 ? code.slice(0, 12) : code, peer: peerLevels.length > 0 ? peerLevels.reduce((a, b) => a + b, 0) / peerLevels.length : 0, self: selfEntry ? selfEntry.selfLevel : 0, teacher: teacherEntry ? teacherEntry.masteryLevelValue : 0, }; }).filter((d) => d.peer > 0 || d.self > 0 || d.teacher > 0); if (comparisonData.length < 3) return null; return (

{t('peer.comparison')}

{t('peer.title')} {t('peer.teacher')} {t('peer.self')}
); })()} {/* Average peer rating per competency */} {(() => { const groupedByComp = peerAssessments .filter((pa) => pa.competencyId && pa.level) .reduce((acc, pa) => { const key = pa.competencyId!; if (!acc[key]) acc[key] = { code: pa.competency?.code ?? '', title: pa.competency?.title ?? '', levels: [] }; acc[key].levels.push(pa.level!); return acc; }, {} as Record); const averages = Object.entries(groupedByComp); if (averages.length === 0) return null; return (

{t('peer.average')}

{averages.map(([cId, data]) => { const avg = data.levels.reduce((a, b) => a + b, 0) / data.levels.length; return (
{avg.toFixed(1)} {data.code || data.title}
); })}
); })()} {/* Peer assessment list */}
{peerAssessments.map((pa, i) => { const typeLabel = t(`peer.type_${pa.assessmentType}`) || pa.assessmentType; return (
{typeLabel} {pa.level && ( {pa.level}/6 )} {pa.isAnonymous && ( {t('peer.anonymous')} )} {pa.competency && ( {pa.competency.code} — {pa.competency.title} )}
{!pa.isAnonymous && (

{t('peer.from')}: {pa.assessor.firstName} {pa.assessor.lastName}

)} {pa.comment && (

“{pa.comment}”

)}

{new Date(pa.createdAt).toLocaleDateString()}

); })}
)}
{/* ─── Emergency Contacts Section ─────────────────────────────────── */}
{t('emergency.title')} {emergencyContacts.length}
{emergencyContacts.length === 0 ? (

{t('emergency.no_contacts')}

) : (
{emergencyContacts.map((ec, i) => { const relLabel = t(`emergency.${ec.relationship}`) || ec.relationship; return (
{ec.name} {relLabel} {ec.isPrimary && ( {t('emergency.primary')} )} {t('emergency.priority')}: {ec.priority}
{ec.phone} {ec.phoneAlt && ( {ec.phoneAlt} )} {ec.email && ( {ec.email} )}
{ec.address && (

{ec.address}

)} {ec.notes && (

{ec.notes}

)}
); })}
)}
{/* ─── Transportation Section ─────────────────────────────────── */}
{t('transport.title')} {transports.length}
{transports.length === 0 ? (

{t('transport.no_transport')}

) : (
{transports.map((tr, i) => { const typeIcon: Record = { bus: , tram: , walk: , car: , bike: , other: , }; const typeColor: Record = { bus: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300', tram: 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-300', walk: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300', car: 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300', bike: 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-300', other: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-300', }; return (
{typeIcon[tr.transportType] || typeIcon.other}{t(`transport.${tr.transportType}`)} {tr.routeNumber && {t('transport.route')}: {tr.routeNumber}} {tr.distanceKm != null && {t('transport.distance_km', { count: tr.distanceKm })}}
{tr.stopName && {tr.stopName}} {tr.pickupTime && {t('transport.pickup')}: {tr.pickupTime}} {tr.dropoffTime && {t('transport.dropoff')}: {tr.dropoffTime}}
{(tr.driverName || tr.driverPhone) && (
{tr.driverName && {tr.driverName}} {tr.driverPhone && ( {tr.driverPhone} )}
)} {tr.notes &&

{tr.notes}

}
); })}
)}
{/* ─── Health Records Section ─────────────────────────────────── */}
{t('health.title')} {healthRecord?.isConfidential && ( {t('health.confidential')} )} {!healthRecord && 0}
{!healthRecord ? (

{t('health.no_record')}

) : (
{/* Blood Type */} {healthRecord.bloodType && (
{t('health.blood_type')}
{healthRecord.bloodType}
)} {/* Last Checkup */} {healthRecord.lastCheckup && (
{t('health.last_checkup')}
{new Date(healthRecord.lastCheckup).toLocaleDateString()}
)}
{/* Allergies */} {healthRecord.allergies && (() => { const allergies: string[] = JSON.parse(healthRecord.allergies); if (allergies.length === 0) return null; return (
{t('health.allergies')}
{allergies.map((a, i) => ( {a} ))}
); })()} {/* Medications */} {healthRecord.medications && (() => { const medications: Array<{ name: string; dosage: string; frequency: string }> = JSON.parse(healthRecord.medications); if (medications.length === 0) return null; return (
{t('health.medications')}
{medications.map((m, i) => (
{m.name} {m.dosage && {t('health.dosage')}: {m.dosage}} {m.frequency && {t('health.frequency')}: {m.frequency}}
))}
); })()} {/* Conditions */} {healthRecord.conditions && (() => { const conditions: string[] = JSON.parse(healthRecord.conditions); if (conditions.length === 0) return null; return (
{t('health.conditions')}
{conditions.map((c, i) => ( {c} ))}
); })()} {/* Doctor & Insurance */}
{(healthRecord.doctorName || healthRecord.doctorPhone) && (
{t('health.doctor')}
{healthRecord.doctorName &&

{healthRecord.doctorName}

} {healthRecord.doctorPhone && ( {healthRecord.doctorPhone} )}
)} {(healthRecord.insuranceNumber || healthRecord.insuranceProvider) && (
{t('health.insurance')}
{healthRecord.insuranceProvider &&

{healthRecord.insuranceProvider}

} {healthRecord.insuranceNumber &&

{healthRecord.insuranceNumber}

}
)}
{healthRecord.notes && (

{healthRecord.notes}

)}
)}
{/* ─── Transportation Dialog ──────────────────────────────────── */} {transportEditId ? t('transport.edit') : t('transport.add')} {t('transport.title')}
setTransportForm((f) => ({ ...f, routeNumber: e.target.value }))} className="mt-1 rounded-lg" placeholder="42" />
setTransportForm((f) => ({ ...f, stopName: e.target.value }))} className="mt-1 rounded-lg" placeholder={t('transport.stop_name')} />
setTransportForm((f) => ({ ...f, pickupTime: e.target.value }))} className="mt-1 rounded-lg" />
setTransportForm((f) => ({ ...f, dropoffTime: e.target.value }))} className="mt-1 rounded-lg" />
setTransportForm((f) => ({ ...f, driverName: e.target.value }))} className="mt-1 rounded-lg" placeholder={t('transport.driver_name')} />
setTransportForm((f) => ({ ...f, driverPhone: e.target.value }))} className="mt-1 rounded-lg" placeholder="+49 123 456789" />
setTransportForm((f) => ({ ...f, distanceKm: e.target.value }))} className="mt-1 rounded-lg" placeholder="2.5" />