'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, } 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 } from '@/lib/store'; import { t } from '@/lib/i18n'; import { fetchStudentDetail, getReportPdfUrl, type StudentDetailData } from '@/lib/api'; import { toast } from 'sonner'; import TeacherNotesSection from './teacher-notes-section'; // ─── 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'; }; 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 [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [selectedFlowerSubjectId, setSelectedFlowerSubjectId] = useState(''); const [timeRange, setTimeRange] = useState('all'); 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]); 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 */}
{initials}
{/* Mastery badge */}

{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} ))}
)} {/* 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 */}
); }