'use client'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Map, Plus, Pencil, Trash2, Search, Loader2, Link2, Unlink, AlertTriangle, CheckCircle2, Shield, BookOpen, Grid3X3, ChevronDown, ChevronRight, X, BarChart3, Eye, } 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 { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Progress } from '@/components/ui/progress'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Separator } from '@/components/ui/separator'; import { useAppStore } from '@/lib/store'; import { t } from '@/lib/i18n'; import { fetchCurriculumStandards, createCurriculumStandard, updateCurriculumStandard, deleteCurriculumStandard, linkCurriculumStandard, unlinkCurriculumStandard, fetchSubjects, fetchCompetencyTemplates, fetchClassCompetencyAssignments, type CurriculumStandard, type CurriculumStandardLink, type Subject, type CompetencyTemplate, } from '@/lib/api'; import { toast } from 'sonner'; /* ── Coverage level colors ── */ const COVERAGE_COLORS: Record = { full: 'bg-emerald-500', partial: 'bg-amber-500', related: 'bg-teal-500', }; const COVERAGE_BG: Record = { full: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300', partial: 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300', related: 'bg-teal-100 text-teal-800 dark:bg-teal-900/40 dark:text-teal-300', }; const COVERAGE_LABELS: Record = { full: 'curriculum.coverage_full', partial: 'curriculum.coverage_partial', related: 'curriculum.coverage_related', }; /* ── Main Component ──────────────────────────────────────────────── */ export default function CurriculumCoverageView() { const currentUser = useAppStore((s) => s.currentUser); const schoolId = currentUser?.schoolId; const [standards, setStandards] = useState([]); const [subjects, setSubjects] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [subjectFilter, setSubjectFilter] = useState('all'); const [gradeFilter, setGradeFilter] = useState('all'); // Dialogs const [formOpen, setFormOpen] = useState(false); const [editingStandard, setEditingStandard] = useState(null); const [saving, setSaving] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const [deletingStandard, setDeletingStandard] = useState(null); const [linkOpen, setLinkOpen] = useState(false); const [linkingStandard, setLinkingStandard] = useState(null); const [detailOpen, setDetailOpen] = useState(false); const [detailStandard, setDetailStandard] = useState(null); // Link form const [availableCompetencies, setAvailableCompetencies] = useState>([]); const [selectedCompetencyId, setSelectedCompetencyId] = useState(''); const [selectedCoverageLevel, setSelectedCoverageLevel] = useState<'full' | 'partial' | 'related'>('full'); const [linkNotes, setLinkNotes] = useState(''); const [competencySearch, setCompetencySearch] = useState(''); // Form state const [formCode, setFormCode] = useState(''); const [formTitle, setFormTitle] = useState(''); const [formDescription, setFormDescription] = useState(''); const [formSubjectId, setFormSubjectId] = useState(''); const [formGradeLevel, setFormGradeLevel] = useState(null); const [formCategory, setFormCategory] = useState(''); const [formSource, setFormSource] = useState(''); const loadData = useCallback(async () => { if (!schoolId) return; try { setLoading(true); const [standardsData, subjectsData] = await Promise.all([ fetchCurriculumStandards(schoolId), fetchSubjects(schoolId), ]); setStandards(standardsData); setSubjects(subjectsData); } catch { toast.error(t('rubrics.error')); } finally { setLoading(false); } }, [schoolId]); useEffect(() => { loadData(); }, [loadData]); // Filtered standards const filtered = useMemo(() => { let result = standards; if (searchQuery) { const q = searchQuery.toLowerCase(); result = result.filter( (s) => s.title.toLowerCase().includes(q) || s.code.toLowerCase().includes(q) || (s.description ?? '').toLowerCase().includes(q) ); } if (subjectFilter && subjectFilter !== 'all') { result = result.filter((s) => s.subjectId === subjectFilter); } if (gradeFilter && gradeFilter !== 'all') { result = result.filter((s) => s.gradeLevel === parseInt(gradeFilter)); } return result; }, [standards, searchQuery, subjectFilter, gradeFilter]); // Stats const stats = useMemo(() => { const total = standards.length; const linked = standards.filter((s) => s.competencyLinks.length > 0).length; const unlinked = total - linked; const coveragePercent = total > 0 ? Math.round((linked / total) * 100) : 0; return { total, linked, unlinked, coveragePercent }; }, [standards]); // Gap analysis: standards without any competency links const gaps = useMemo(() => { return standards.filter((s) => s.competencyLinks.length === 0); }, [standards]); // Heatmap data: group by subject and grade level const heatmapData = useMemo(() => { const map = new Map(); for (const s of standards) { const key = `${s.subject?.name ?? 'No Subject'}-${s.gradeLevel ?? 'N/A'}`; const existing = map.get(key) || { subject: s.subject?.name ?? 'No Subject', gradeLevel: s.gradeLevel ? String(s.gradeLevel) : 'N/A', total: 0, linked: 0, coverage: 0, }; existing.total++; if (s.competencyLinks.length > 0) existing.linked++; map.set(key, existing); } return Array.from(map.values()).map((d) => ({ ...d, coverage: d.total > 0 ? Math.round((d.linked / d.total) * 100) : 0, })); }, [standards]); /* ── Form helpers ─────────────────────────────────────────────── */ const openCreateForm = () => { setEditingStandard(null); setFormCode(''); setFormTitle(''); setFormDescription(''); setFormSubjectId(''); setFormGradeLevel(null); setFormCategory(''); setFormSource(''); setFormOpen(true); }; const openEditForm = (s: CurriculumStandard) => { setEditingStandard(s); setFormCode(s.code); setFormTitle(s.title); setFormDescription(s.description ?? ''); setFormSubjectId(s.subjectId ?? ''); setFormGradeLevel(s.gradeLevel); setFormCategory(s.category ?? ''); setFormSource(s.source ?? ''); setFormOpen(true); }; const handleSave = async () => { if (!formTitle.trim() || !formCode.trim()) { toast.error(t('rubrics.title_required')); return; } try { setSaving(true); if (editingStandard) { await updateCurriculumStandard(editingStandard.id, { code: formCode, title: formTitle, description: formDescription || null, subjectId: formSubjectId || null, gradeLevel: formGradeLevel, category: formCategory || null, source: formSource || null, }); } else { if (!schoolId) return; await createCurriculumStandard({ schoolId, code: formCode, title: formTitle, description: formDescription || null, subjectId: formSubjectId || null, gradeLevel: formGradeLevel, category: formCategory || null, source: formSource || null, }); } toast.success(t('rubrics.saved')); setFormOpen(false); loadData(); } catch (err) { toast.error(err instanceof Error ? err.message : t('rubrics.error')); } finally { setSaving(false); } }; const handleDelete = async () => { if (!deletingStandard) return; try { await deleteCurriculumStandard(deletingStandard.id); toast.success(t('rubrics.deleted')); setDeleteOpen(false); setDeletingStandard(null); loadData(); } catch (err) { toast.error(err instanceof Error ? err.message : t('rubrics.error')); } }; const openLinkDialog = async (standard: CurriculumStandard) => { setLinkingStandard(standard); setSelectedCompetencyId(''); setSelectedCoverageLevel('full'); setLinkNotes(''); setCompetencySearch(''); setLinkOpen(true); // Load available competencies try { if (!schoolId) return; const templates = await fetchCompetencyTemplates({ schoolId }); const comps: Array<{ id: string; code: string; title: string; categoryName: string; categoryColor: string | null }> = []; for (const tmpl of templates) { for (const cat of tmpl.categories) { for (const comp of cat.competencies) { comps.push({ id: comp.id, code: comp.code, title: comp.title, categoryName: cat.name, categoryColor: cat.color, }); } } } setAvailableCompetencies(comps); } catch { // silent } }; const handleLink = async () => { if (!linkingStandard || !selectedCompetencyId) return; try { await linkCurriculumStandard(linkingStandard.id, { competencyId: selectedCompetencyId, coverageLevel: selectedCoverageLevel, notes: linkNotes || null, }); toast.success(t('curriculum.link_competency')); setLinkOpen(false); loadData(); } catch (err) { toast.error(err instanceof Error ? err.message : t('rubrics.error')); } }; const handleUnlink = async (standardId: string, competencyId: string) => { try { await unlinkCurriculumStandard(standardId, competencyId); toast.success(t('curriculum.unlink_competency')); loadData(); } catch (err) { toast.error(err instanceof Error ? err.message : t('rubrics.error')); } }; const openDetail = (s: CurriculumStandard) => { setDetailStandard(s); setDetailOpen(true); }; /* ── Render ───────────────────────────────────────────────────── */ if (loading) { return (
); } return (
{/* Header */}

{t('curriculum.title')}

{t('curriculum.subtitle')}

{/* Stats Cards */}
{t('curriculum.standards_count')}
{stats.total}
{t('curriculum.linked_count')}
{stats.linked}
{t('curriculum.unlinked_count')}
{stats.unlinked}
{t('curriculum.coverage_percent')}
{stats.coveragePercent}%
{/* Filters */}
setSearchQuery(e.target.value)} className="pl-9" />
{/* Tabs */} {t('curriculum.standards')} {t('curriculum.heatmap')} {t('curriculum.gap_analysis')} {/* Standards Tab */} {filtered.length === 0 ? (

{t('curriculum.empty_title')}

{t('curriculum.empty_desc')}

) : (
{filtered.map((standard, idx) => ( 0 ? '#10b981' : '#f59e0b', }} onClick={() => openDetail(standard)} >
{standard.code} {standard.title}
{standard.description && (

{standard.description}

)}
{standard.subject && ( {standard.subject.name} )} {standard.gradeLevel && ( Kl. {standard.gradeLevel} )} {standard.category && ( {standard.category} )} {standard.source && ( {standard.source} )}
0 ? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300' : 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300' } > {standard.competencyLinks.length > 0 ? `${standard.competencyLinks.length} ${t('curriculum.linked_competencies')}` : t('curriculum.no_links')}
))}
)}
{/* Heatmap Tab */} {t('curriculum.coverage_visualization')} {heatmapData.length === 0 ? (
{t('curriculum.empty_title')}
) : (
{/* Header row */}
{t('curriculum.subject')} / {t('curriculum.grade_level')}
{t('curriculum.coverage')}
{t('curriculum.coverage_percent')}
{heatmapData.map((row, idx) => (
{row.subject} — {row.gradeLevel}
= 80 ? 'bg-emerald-500' : row.coverage >= 50 ? 'bg-amber-500' : row.coverage > 0 ? 'bg-rose-500' : 'bg-gray-300 dark:bg-gray-700' }`} style={{ width: `${Math.max(row.coverage, 2)}%` }} />
{row.linked}/{row.total}
{row.coverage}%
))}
)} {/* Legend */}
{'> 80%'}
50-80%
{'< 50%'}
{/* Gap Analysis Tab */} {t('curriculum.gap_title')} {gaps.length === 0 ? (

{t('curriculum.no_gaps')}

) : (
{gaps.map((standard) => (
{standard.code} {standard.title}
{standard.subject && ( {standard.subject.name} )} {standard.gradeLevel && ( Kl. {standard.gradeLevel} )}
))}
)}
{/* ── Create/Edit Dialog ─────────────────────────────────────── */} { if (!open && saving) return; setFormOpen(open); }}> {editingStandard ? t('curriculum.edit_standard') : t('curriculum.create_standard')}
setFormCode(e.target.value)} placeholder="M.1.1" />
setFormTitle(e.target.value)} placeholder={t('curriculum.standard_title')} />