'use client'; import React, { useEffect, useMemo, useState } from 'react'; import { motion } from 'framer-motion'; import { Grid3X3, RefreshCw, Download, Layers, Users, CheckCircle2, XCircle, BarChart3, Calendar, BookOpen, ListChecks, Sprout, Leaf, TreePine, Trees, } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { useAppStore } from '@/lib/store'; import { t } from '@/lib/i18n'; import { fetchMasteryMatrix, fetchClasses, fetchSubjects, fetchLearningProgress, type MasteryMatrixData, type ClassGroup, type Subject, type LearningProgressEntry, } from '@/lib/api'; import { toast } from 'sonner'; function masteryCellColor(level: number | null): string { if (level === null) return 'bg-gray-100 dark:bg-gray-800/60 text-gray-400'; if (level <= 1) return 'bg-red-200 dark:bg-red-900/40 text-red-800 dark:text-red-200'; if (level <= 2) return 'bg-amber-200 dark:bg-amber-900/40 text-amber-800 dark:text-amber-200'; if (level <= 3) return 'bg-emerald-200 dark:bg-emerald-900/40 text-emerald-800 dark:text-emerald-200'; return 'bg-teal-200 dark:bg-teal-900/40 text-teal-800 dark:text-teal-200'; } function avgColor(avg: number): string { if (avg <= 0) return 'text-gray-400'; if (avg < 2) return 'text-rose-600 dark:text-rose-400'; if (avg < 3) return 'text-amber-600 dark:text-amber-400'; if (avg < 3.5) return 'text-emerald-600 dark:text-emerald-400'; return 'text-teal-600 dark:text-teal-400'; } function escapeCsv(field: string | number | null | undefined): string { if (field === null || field === undefined) return ''; const str = String(field); if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) { return `"${str.replace(/"/g, '""')}"`; } return str; } export default function MasteryMatrixView() { const currentUser = useAppStore((s) => s.currentUser); const schoolYearId = useAppStore((s) => s.schoolYearId); const storeClassId = useAppStore((s) => s.currentClassId); const [classes, setClasses] = useState([]); const [subjects, setSubjects] = useState([]); const [selectedClassId, setSelectedClassId] = useState(storeClassId ?? ''); const [selectedSubjectId, setSelectedSubjectId] = useState(''); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [detail, setDetail] = useState<{ studentName: string; competencyCode: string; competencyTitle: string; entries: LearningProgressEntry[] } | null>(null); const [detailLoading, setDetailLoading] = useState(false); // Load classes + subjects for filters useEffect(() => { async function load() { try { const [cls, subs] = await Promise.all([ fetchClasses(currentUser?.schoolId ?? undefined, schoolYearId ?? undefined).catch(() => []), fetchSubjects(currentUser?.schoolId ?? undefined).catch(() => []), ]); setClasses(cls); setSubjects(subs); if (!selectedClassId && cls.length > 0) { setSelectedClassId(cls[0].id); } } catch { // ignore } } load(); }, [currentUser?.schoolId, schoolYearId]); const loadMatrix = React.useCallback(async () => { if (!selectedClassId || !selectedSubjectId) { setData(null); return; } setLoading(true); setError(null); try { const d = await fetchMasteryMatrix({ classGroupId: selectedClassId, subjectId: selectedSubjectId, }); setData(d); } catch (err) { const msg = err instanceof Error ? err.message : t('error.generic'); setError(msg); toast.error(msg); } finally { setLoading(false); } }, [selectedClassId, selectedSubjectId]); useEffect(() => { loadMatrix(); }, [loadMatrix]); // Build matrix lookup map: `${studentId}::${competencyId}` -> cell const matrixMap = useMemo(() => { const m = new Map(); if (data) { for (const cell of data.matrix) { m.set(`${cell.studentId}::${cell.competencyId}`, { level: cell.latestMasteryLevel, count: cell.entryCount, lastDate: cell.lastEntryDate, }); } } return m; }, [data]); // Row averages (per student) and column averages (per competency) const rowAverages = useMemo(() => { if (!data) return new Map(); const m = new Map(); for (const student of data.students) { m.set(student.id, { total: 0, count: 0 }); } for (const cell of data.matrix) { if (cell.latestMasteryLevel !== null) { const cur = m.get(cell.studentId); if (cur) { cur.total += cell.latestMasteryLevel; cur.count += 1; } } } const result = new Map(); for (const [sid, v] of m) { result.set(sid, v.count > 0 ? Math.round((v.total / v.count) * 100) / 100 : 0); } return result; }, [data]); const colAverages = useMemo(() => { if (!data) return new Map(); const m = new Map(); for (const comp of data.competencies) { m.set(comp.id, { total: 0, count: 0 }); } for (const cell of data.matrix) { if (cell.latestMasteryLevel !== null) { const cur = m.get(cell.competencyId); if (cur) { cur.total += cell.latestMasteryLevel; cur.count += 1; } } } const result = new Map(); for (const [cid, v] of m) { result.set(cid, v.count > 0 ? Math.round((v.total / v.count) * 100) / 100 : 0); } return result; }, [data]); const overallAvg = useMemo(() => { if (!data || data.matrix.length === 0) return 0; const assessed = data.matrix.filter((c) => c.latestMasteryLevel !== null); if (assessed.length === 0) return 0; return Math.round((assessed.reduce((s, c) => s + (c.latestMasteryLevel ?? 0), 0) / assessed.length) * 100) / 100; }, [data]); // Total entries across all cells const totalEntries = useMemo(() => { if (!data) return 0; return data.matrix.reduce((s, c) => s + c.entryCount, 0); }, [data]); // Coverage %: how many cells have at least one entry const coveragePct = useMemo(() => { if (!data || data.students.length === 0 || data.competencies.length === 0) return 0; const totalCells = data.students.length * data.competencies.length; if (totalCells === 0) return 0; const assessed = data.matrix.filter((c) => c.latestMasteryLevel !== null).length; return Math.round((assessed / totalCells) * 100); }, [data]); // Mastery distribution across the whole matrix (count per level) const masteryDist = useMemo(() => { if (!data) return [0, 0, 0, 0]; const counts = [0, 0, 0, 0]; for (const cell of data.matrix) { if (cell.latestMasteryLevel !== null && cell.latestMasteryLevel >= 1 && cell.latestMasteryLevel <= 4) { counts[cell.latestMasteryLevel - 1] += 1; } } return counts; }, [data]); const handleCellClick = async (studentId: string, competencyId: string) => { if (!data) return; const student = data.students.find((s) => s.id === studentId); const comp = data.competencies.find((c) => c.id === competencyId); if (!student || !comp) return; setDetailLoading(true); setDetail({ studentName: `${student.firstName} ${student.lastName}`, competencyCode: comp.code, competencyTitle: comp.title, entries: [], }); try { const entries = await fetchLearningProgress({ studentId, competencyId, }); setDetail({ studentName: `${student.firstName} ${student.lastName}`, competencyCode: comp.code, competencyTitle: comp.title, entries, }); } catch (err) { const msg = err instanceof Error ? err.message : t('error.generic'); toast.error(msg); setDetail(null); } finally { setDetailLoading(false); } }; const handleExportCsv = () => { if (!data) return; const header = [ t('matrix.student'), ...data.competencies.map((c) => c.code), t('matrix.row_avg'), ]; let csv = header.map(escapeCsv).join(',') + '\n'; for (const s of data.students) { const row: (string | number)[] = [`${s.firstName} ${s.lastName}`]; for (const c of data.competencies) { const cell = matrixMap.get(`${s.id}::${c.id}`); row.push(cell?.level ?? ''); } row.push(rowAverages.get(s.id) ?? 0); csv += row.map(escapeCsv).join(',') + '\n'; } // Column averages row const avgRow: (string | number)[] = [t('matrix.col_avg')]; for (const c of data.competencies) { avgRow.push(colAverages.get(c.id) ?? 0); } avgRow.push(overallAvg); csv += avgRow.map(escapeCsv).join(',') + '\n'; const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `mastery_matrix_${selectedClassId}_${selectedSubjectId}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast.success(t('matrix.export_success')); }; const hasFilters = selectedClassId && selectedSubjectId; return (
{/* Header */} {/* Decorative gradient banner */}

{t('matrix.title')}

{t('matrix.subtitle')}

{/* Filters */}
{/* Summary banner */} {data && (
{/* Class average */}

{t('matrix.avg')}

{overallAvg > 0 ? overallAvg.toFixed(2) : '—'}

{/* Total entries */}

{t('matrix.entries')}

{totalEntries}

{/* Coverage */}

{t('polish.coverage')}

{coveragePct}%

{/* Mastery distribution mini bar chart */}

{t('polish.mastery_distribution')}

{masteryDist.map((cnt, i) => { const max = Math.max(...masteryDist, 1); const h = (cnt / max) * 100; const colors = ['bg-red-400 dark:bg-red-500', 'bg-amber-400 dark:bg-amber-500', 'bg-emerald-400 dark:bg-emerald-500', 'bg-teal-400 dark:bg-teal-500']; return (
); })}
)} {!hasFilters ? (

{t('polish.empty_title_matrix')}

{t('polish.empty_subtitle_matrix')}

{/* How it works — 3-step guide */}
{t('polish.matrix_how_it_works_title')} · {t('polish.matrix_how_it_works_subtitle')}
{/* Step 1 */}
1

{t('polish.matrix_step_1_title')}

{t('polish.matrix_step_1_desc')}

{/* Step 2 */}
2

{t('polish.matrix_step_2_title')}

{t('polish.matrix_step_2_desc')}

{/* Step 3 */}
3

{t('polish.matrix_step_3_title')}

{t('polish.matrix_step_3_desc')}

) : loading ? ( ) : error ? (

{error}

) : !data ? null : data.competencies.length === 0 ? (

{t('polish.empty_title_no_data')}

{t('matrix.no_template_hint')}

) : data.students.length === 0 ? (

{t('polish.empty_title_no_data')}

{t('matrix.no_students')}

) : ( {t('matrix.title')}
{t('matrix.student')} {data.competencies.map((c) => (
{c.code}
))} {t('matrix.row_avg')}
{data.students.map((s, sIdx) => { const rowAvg = rowAverages.get(s.id) ?? 0; return ( {s.firstName} {s.lastName} {data.competencies.map((c) => { const cell = matrixMap.get(`${s.id}::${c.id}`); const level = cell?.level ?? null; const lastDate = cell?.lastDate; const dateStr = lastDate ? new Date(lastDate).toLocaleDateString() : null; const count = cell?.count ?? 0; return ( ); })} {rowAvg > 0 ? rowAvg.toFixed(2) : '—'} ); })} {/* Column averages row */} {t('matrix.col_avg')} {data.competencies.map((c) => { const avg = colAverages.get(c.id) ?? 0; return ( {avg > 0 ? avg.toFixed(1) : '—'} ); })} {overallAvg > 0 ? overallAvg.toFixed(2) : '—'}
{/* Enhanced Legend */}
{t('matrix.legend')}: {t('matrix.legend_unassessed')} {t('matrix.legend_level_1')} {t('matrix.legend_level_2')} {t('matrix.legend_level_3')} {t('matrix.legend_level_4')} {t('polish.matrix_click_hint')}
)} {/* Detail dialog */} !open && setDetail(null)}> {detail?.competencyCode} — {detail?.studentName} {detail?.competencyTitle} · {t('matrix.entries_for')} {detail?.studentName}
{detailLoading ? ( ) : detail && detail.entries.length > 0 ? (
{/* Vertical timeline line */}
{detail.entries.map((e, i) => (
{e.masteryLevelValue <= 1 ? : e.masteryLevelValue <= 2 ? : e.masteryLevelValue <= 3 ? : } {t('analytics.level')} {e.masteryLevelValue} {new Date(e.date).toLocaleDateString()} — {e.teacher.firstName} {e.teacher.lastName}
{e.note && (

"{e.note}"

)}
))}
) : (

{t('matrix.no_entries_for')}

)}
); } function masteryCellColorBadge(level: number): string { if (level <= 1) return 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-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'; }