'use client'; import React, { useEffect, useMemo, useState, useCallback } from 'react'; import { motion } from 'framer-motion'; import { Target, RefreshCw, CheckCircle2, AlertCircle, XCircle, BookOpen, Users, Layers, TrendingUp, Clock, Search, ChevronRight, Sparkles, GraduationCap, AlertTriangle, PartyPopper, Sprout, Leaf, TreePine, Trees, } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; 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 { useAppStore } from '@/lib/store'; import { t } from '@/lib/i18n'; import { toast } from 'sonner'; import { fetchClasses, fetchSubjects, fetchCurriculumCoverage, type ClassGroup, type Subject, type CurriculumCoverage, type CurriculumCoverageCompetency, } from '@/lib/api'; const STATUS_META: Record< 'covered' | 'partial' | 'untouched', { icon: React.ElementType; bg: string; text: string; border: string } > = { covered: { icon: CheckCircle2, bg: 'bg-emerald-50 dark:bg-emerald-900/20', text: 'text-emerald-700 dark:text-emerald-300', border: 'border-emerald-200 dark:border-emerald-800', }, partial: { icon: AlertTriangle, bg: 'bg-amber-50 dark:bg-amber-900/20', text: 'text-amber-700 dark:text-amber-300', border: 'border-amber-200 dark:border-amber-800', }, untouched: { icon: XCircle, bg: 'bg-rose-50 dark:bg-rose-900/20', text: 'text-rose-700 dark:text-rose-300', border: 'border-rose-200 dark:border-rose-800', }, }; function formatDate(iso: string | null, locale: string): string { if (!iso) return ''; try { const d = new Date(iso); return d.toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US', { day: '2-digit', month: 'short', year: 'numeric', }); } catch { return iso; } } function MasteryIcon({ level }: { level: number | null }) { if (level === null || level === undefined) return ยท; if (level <= 1) return ; if (level === 2) return ; if (level === 3) return ; return ; } export default function CurriculumCoverageView() { const currentUser = useAppStore((s) => s.currentUser); const schoolYearId = useAppStore((s) => s.schoolYearId); const storeClassId = useAppStore((s) => s.currentClassId); const setCurrentView = useAppStore((s) => s.setCurrentView); const setCurrentClass = useAppStore((s) => s.setCurrentClass); const locale = currentUser?.locale ?? 'de'; 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); // Filter/sort state for matrix const [statusFilter, setStatusFilter] = useState<'all' | 'covered' | 'partial' | 'untouched'>('all'); const [sortBy, setSortBy] = useState<'category' | 'status' | 'recent'>('category'); const [searchQuery, setSearchQuery] = useState(''); const [detailComp, setDetailComp] = useState(null); // Load classes + subjects 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 loadCoverage = useCallback(async () => { if (!selectedClassId) { setData(null); return; } setLoading(true); setError(null); try { const d = await fetchCurriculumCoverage({ classGroupId: selectedClassId, subjectId: selectedSubjectId || undefined, }); setData(d); } catch (err) { const msg = err instanceof Error ? err.message : t('coverage.error'); setError(msg); } finally { setLoading(false); } }, [selectedClassId, selectedSubjectId]); useEffect(() => { loadCoverage(); }, [loadCoverage]); // Sync selected class back to store for cross-view consistency useEffect(() => { if (selectedClassId && selectedClassId !== storeClassId) { setCurrentClass(selectedClassId); } }, [selectedClassId, storeClassId, setCurrentClass]); // Filtered + sorted competency list const filteredCompetencies = useMemo(() => { if (!data) return []; let list = data.byCompetency.slice(); if (statusFilter !== 'all') { list = list.filter((c) => c.status === statusFilter); } if (searchQuery.trim()) { const q = searchQuery.toLowerCase().trim(); list = list.filter( (c) => c.code.toLowerCase().includes(q) || c.title.toLowerCase().includes(q) ); } if (sortBy === 'category') { list.sort((a, b) => a.categoryName.localeCompare(b.categoryName) || a.code.localeCompare(b.code)); } else if (sortBy === 'status') { const order = { untouched: 0, partial: 1, covered: 2 }; list.sort((a, b) => order[a.status] - order[b.status] || a.categoryName.localeCompare(b.categoryName)); } else if (sortBy === 'recent') { list.sort((a, b) => { const aT = a.lastAssessedDate ? new Date(a.lastAssessedDate).getTime() : 0; const bT = b.lastAssessedDate ? new Date(b.lastAssessedDate).getTime() : 0; return bT - aT; }); } return list; }, [data, statusFilter, sortBy, searchQuery]); return (
{/* Header */}

{t('coverage.title')}

{t('coverage.subtitle')}

{/* Filter row */}
{/* Main content */} {!selectedClassId ? ( ) : loading ? ( ) : error ? ( ) : !data ? null : !data.hasAssignments ? ( setCurrentView('competencies')} /> ) : (
{/* KPI tiles */} {/* Section 1: by category */} {t('coverage.section.by_category')} {t('coverage.section.by_category_desc')} {data.byCategory.length === 0 ? (

{t('coverage.no_data')}

) : ( data.byCategory.map((cat) => ( )) )}
{/* Section 2: matrix */}
{t('coverage.section.matrix')} {t('coverage.section.matrix_desc')}
{/* Filters */}
{(['all', 'covered', 'partial', 'untouched'] as const).map((f) => ( ))}
setSearchQuery(e.target.value)} placeholder={t('coverage.search_placeholder')} className="pl-8 rounded-xl" />
{filteredCompetencies.length === 0 ? (

{t('coverage.no_data')}

) : (
{filteredCompetencies.map((comp, i) => ( setDetailComp(comp)} /> ))}
)}
{/* Section 3 + 4: action needed + recent (2-col on desktop) */}
{/* Action needed */} {t('coverage.section.action_needed')} {t('coverage.section.action_needed_desc')} {data.notAssessedList.length === 0 ? (

{t('coverage.empty_action')}

{t('coverage.empty_action_desc')}

) : (
    {data.notAssessedList.slice(0, 10).map((c) => { const meta = STATUS_META[c.status]; return (
  • {c.code} {c.title}
    {c.categoryName} ยท {c.studentsAssessed}/{c.studentsCount} {t('coverage.assessed_count').toLowerCase()}
  • ); })}
)}
{/* Recent */} {t('coverage.section.recent')} {t('coverage.section.recent_desc')} {data.recentAssessed.length === 0 ? (
๐Ÿ“ญ

{t('coverage.empty_recent')}

{t('coverage.empty_recent_desc')}

) : (
    {data.recentAssessed.map((r, idx) => (
    {r.code} {r.title}
    {r.categoryName} ยท {formatDate(r.date, locale)}
    ))}
)}
)} {/* Detail dialog */} !v && setDetailComp(null)}> {detailComp && ( <> {(() => { const StatusIcon = STATUS_META[detailComp.status].icon; return ; })()} {detailComp.code} {detailComp.title}
{t('coverage.assessed_count')} {detailComp.studentsAssessed} / {data?.totals.studentsCount ?? 0}
{t('coverage.progress_count')} {detailComp.progressCount}
{t('coverage.assessments_count')} {detailComp.assessmentCount}
{t('coverage.last_assessed')} {detailComp.lastAssessedDate ? formatDate(detailComp.lastAssessedDate, locale) : t('coverage.never_assessed')}
{t('label.category')} {detailComp.categoryName}
)}
); } /* โ”€โ”€ Sub-components โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ function KpiTiles({ data }: { data: CurriculumCoverage }) { const tiles = [ { label: t('coverage.kpi.total'), value: data.totals.competencies, icon: Layers, gradient: 'from-emerald-500 to-teal-600', bg: 'bg-emerald-50 dark:bg-emerald-900/20', }, { label: t('coverage.kpi.assessed'), value: `${data.totals.assessed} (${data.totals.coveragePercent}%)`, icon: CheckCircle2, gradient: 'from-teal-500 to-emerald-600', bg: 'bg-teal-50 dark:bg-teal-900/20', ring: true, coveragePercent: data.totals.coveragePercent, }, { label: t('coverage.kpi.not_assessed'), value: `${data.totals.notAssessed} (${100 - data.totals.coveragePercent}%)`, icon: AlertCircle, gradient: 'from-rose-500 to-amber-500', bg: 'bg-rose-50 dark:bg-rose-900/20', }, { label: t('coverage.kpi.students'), value: data.totals.studentsCount, icon: Users, gradient: 'from-amber-500 to-violet-500', bg: 'bg-amber-50 dark:bg-amber-900/20', }, ]; return (
{tiles.map((tile, i) => (

{tile.label}

{tile.value}

{tile.ring && ( {tile.coveragePercent}% )}
))}
); } function CategoryBar({ cat, }: { cat: CurriculumCoverage['byCategory'][number]; }) { return (
{cat.categoryName}
{cat.assessed}/{cat.total} {cat.coveragePercent}%
); } function CompetencyCard({ comp, studentsCount, locale, onClick, }: { comp: CurriculumCoverageCompetency; studentsCount: number; locale: string; onClick: () => void; }) { const meta = STATUS_META[comp.status]; return ( ); } function EmptyClassState() { return (

{t('coverage.select_class')}

{t('coverage.guide.subtitle')}

{t('coverage.guide.title')}

{[ { num: 1, icon: Users, title: t('coverage.guide.step1_title'), desc: t('coverage.guide.step1_desc'), gradient: 'from-emerald-500 to-teal-600' }, { num: 2, icon: BookOpen, title: t('coverage.guide.step2_title'), desc: t('coverage.guide.step2_desc'), gradient: 'from-teal-500 to-emerald-600' }, { num: 3, icon: Target, title: t('coverage.guide.step3_title'), desc: t('coverage.guide.step3_desc'), gradient: 'from-amber-500 to-violet-500' }, ].map((step) => (
{step.num}
{step.title}

{step.desc}

))}
); } function NoAssignmentsState({ onGoToGrid }: { onGoToGrid: () => void }) { return (

{t('coverage.no_assignments')}

{t('coverage.no_assignments_desc')}

); } function LoadingState() { return (
{[1, 2, 3, 4].map((i) => ( ))}
{[1, 2, 3, 4].map((i) => ( ))}
{[1, 2, 3, 4, 5, 6].map((i) => ( ))}
); } function ErrorState({ message, onRetry }: { message: string; onRetry: () => void }) { return (

{t('coverage.error')}

{message}

); }