'use client'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Ruler, Plus, Pencil, Trash2, Copy, Search, Filter, Loader2, Eye, Printer, Lock, Globe, ChevronDown, ChevronUp, X, GripVertical, BarChart3, GraduationCap, FileDown, LayoutTemplate, CheckCircle2, Calculator, } 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 { Switch } from '@/components/ui/switch'; 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 { fetchRubrics, createRubric, updateRubric, deleteRubric, duplicateRubric, fetchSubjects, fetchRubricTemplates, fetchRubricAnalytics, gradeWithRubric, type Rubric, type RubricCriterion, type RubricLevel, type RubricCriterionInput, type RubricLevelInput, type Subject, type RubricTemplate, type RubricAnalytics, type RubricGradeResult, } from '@/lib/api'; import { toast } from 'sonner'; /* ── Helpers ─────────────────────────────────────────────────────── */ interface CriterionDraft { id?: string; name: string; description: string; weight: number; maxPoints: number; order: number; levels: LevelDraft[]; } interface LevelDraft { id?: string; label: string; description: string; points: number; order: number; } function emptyCriterion(order: number): CriterionDraft { return { name: '', description: '', weight: 1.0, maxPoints: 25, order, levels: [ { label: '', description: '', points: 25, order: 0 }, { label: '', description: '', points: 19, order: 1 }, { label: '', description: '', points: 12, order: 2 }, { label: '', description: '', points: 6, order: 3 }, ], }; } function rubricToDraft(r: Rubric): { title: string; description: string; type: 'ANALYTIC' | 'HOLISTIC'; subjectId: string; maxPoints: number; isPublic: boolean; criteria: CriterionDraft[]; } { return { title: r.title, description: r.description ?? '', type: r.type as 'ANALYTIC' | 'HOLISTIC', subjectId: r.subjectId ?? '', maxPoints: r.maxPoints, isPublic: r.isPublic, criteria: r.criteria.map((c, ci) => ({ id: c.id, name: c.name, description: c.description ?? '', weight: c.weight, maxPoints: c.maxPoints, order: ci, levels: c.levels.map((l, li) => ({ id: l.id, label: l.label, description: l.description, points: l.points, order: li, })), })), }; } const LEVEL_COLORS = ['bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300', 'bg-teal-100 text-teal-800 dark:bg-teal-900/40 dark:text-teal-300', 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300', 'bg-rose-100 text-rose-800 dark:bg-rose-900/40 dark:text-rose-300']; const LEVEL_HEADER_COLORS = ['bg-emerald-500', 'bg-teal-500', 'bg-amber-500', 'bg-rose-500']; /* ── Main Component ──────────────────────────────────────────────── */ export default function RubricLibraryView() { const currentUser = useAppStore((s) => s.currentUser); const schoolId = currentUser?.schoolId; const [rubrics, setRubrics] = useState([]); const [subjects, setSubjects] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [subjectFilter, setSubjectFilter] = useState('all'); const [showMyOnly, setShowMyOnly] = useState(false); // Dialogs const [formOpen, setFormOpen] = useState(false); const [editingRubric, setEditingRubric] = useState(null); const [detailOpen, setDetailOpen] = useState(false); const [detailRubric, setDetailRubric] = useState(null); const [deleteOpen, setDeleteOpen] = useState(false); const [deletingRubric, setDeletingRubric] = useState(null); const [saving, setSaving] = useState(false); const [formTab, setFormTab] = useState('edit'); // Template dialog const [templateOpen, setTemplateOpen] = useState(false); const [templates, setTemplates] = useState([]); const [templatesLoading, setTemplatesLoading] = useState(false); // Analytics dialog const [analyticsOpen, setAnalyticsOpen] = useState(false); const [analyticsData, setAnalyticsData] = useState(null); const [analyticsLoading, setAnalyticsLoading] = useState(false); // Grading dialog const [gradeOpen, setGradeOpen] = useState(false); const [gradingRubric, setGradingRubric] = useState(null); const [gradeScores, setGradeScores] = useState>({}); const [gradeResult, setGradeResult] = useState(null); const [gradingCalc, setGradingCalc] = useState(false); // Form state const [formTitle, setFormTitle] = useState(''); const [formDescription, setFormDescription] = useState(''); const [formType, setFormType] = useState<'ANALYTIC' | 'HOLISTIC'>('ANALYTIC'); const [formSubjectId, setFormSubjectId] = useState(''); const [formMaxPoints, setFormMaxPoints] = useState(100); const [formIsPublic, setFormIsPublic] = useState(false); const [formCriteria, setFormCriteria] = useState([emptyCriterion(0)]); const loadRubrics = useCallback(async () => { if (!schoolId) return; try { setLoading(true); const data = await fetchRubrics(schoolId); setRubrics(data); } catch { toast.error(t('rubrics.error')); } finally { setLoading(false); } }, [schoolId]); const loadSubjects = useCallback(async () => { if (!schoolId) return; try { const data = await fetchSubjects(schoolId); setSubjects(data); } catch { // silent } }, [schoolId]); useEffect(() => { loadRubrics(); loadSubjects(); }, [loadRubrics, loadSubjects]); // Filtered rubrics const filtered = useMemo(() => { let result = rubrics; if (searchQuery) { const q = searchQuery.toLowerCase(); result = result.filter( (r) => r.title.toLowerCase().includes(q) || (r.description ?? '').toLowerCase().includes(q) ); } if (subjectFilter && subjectFilter !== 'all') { result = result.filter((r) => r.subjectId === subjectFilter); } if (showMyOnly && currentUser) { result = result.filter((r) => r.teacherId === currentUser.id); } return result; }, [rubrics, searchQuery, subjectFilter, showMyOnly, currentUser]); /* ── Form helpers ─────────────────────────────────────────────── */ const openCreateForm = () => { setEditingRubric(null); setFormTitle(''); setFormDescription(''); setFormType('ANALYTIC'); setFormSubjectId(''); setFormMaxPoints(100); setFormIsPublic(false); setFormCriteria([emptyCriterion(0)]); setFormTab('edit'); setFormOpen(true); }; const openEditForm = (r: Rubric) => { const draft = rubricToDraft(r); setEditingRubric(r); setFormTitle(draft.title); setFormDescription(draft.description); setFormType(draft.type); setFormSubjectId(draft.subjectId); setFormMaxPoints(draft.maxPoints); setFormIsPublic(draft.isPublic); setFormCriteria(draft.criteria); setFormTab('edit'); setFormOpen(true); }; const openTemplateDialog = async () => { setTemplateOpen(true); setTemplatesLoading(true); try { const data = await fetchRubricTemplates(); setTemplates(data); } catch { toast.error(t('rubrics.error')); } finally { setTemplatesLoading(false); } }; const applyTemplate = (template: RubricTemplate) => { setEditingRubric(null); setFormTitle(template.title); setFormDescription(template.description); setFormType(template.type); setFormMaxPoints(template.maxPoints); setFormIsPublic(false); // Find matching subject const matchingSubject = subjects.find((s) => s.name === template.subject); setFormSubjectId(matchingSubject?.id ?? ''); setFormCriteria( template.criteria.map((c, ci) => ({ name: c.name, description: c.description, weight: c.weight, maxPoints: c.maxPoints, order: ci, levels: c.levels.map((l, li) => ({ label: l.label, description: l.description, points: l.points, order: li, })), })) ); setFormTab('edit'); setTemplateOpen(false); setFormOpen(true); }; const addCriterion = () => { setFormCriteria((prev) => [...prev, emptyCriterion(prev.length)]); }; const removeCriterion = (idx: number) => { setFormCriteria((prev) => prev.filter((_, i) => i !== idx).map((c, i) => ({ ...c, order: i }))); }; const updateCriterion = (idx: number, field: keyof CriterionDraft, value: unknown) => { setFormCriteria((prev) => prev.map((c, i) => (i === idx ? { ...c, [field]: value } : c)) ); }; const addLevel = (cIdx: number) => { setFormCriteria((prev) => prev.map((c, i) => i === cIdx ? { ...c, levels: [...c.levels, { label: '', description: '', points: 0, order: c.levels.length }] } : c ) ); }; const removeLevel = (cIdx: number, lIdx: number) => { setFormCriteria((prev) => prev.map((c, i) => i === cIdx ? { ...c, levels: c.levels.filter((_, li) => li !== lIdx).map((l, li) => ({ ...l, order: li })) } : c ) ); }; const updateLevel = (cIdx: number, lIdx: number, field: keyof LevelDraft, value: unknown) => { setFormCriteria((prev) => prev.map((c, i) => i === cIdx ? { ...c, levels: c.levels.map((l, li) => (li === lIdx ? { ...l, [field]: value } : l)), } : c ) ); }; const handleSave = async () => { if (!formTitle.trim()) { toast.error(t('rubrics.title_required')); return; } if (formCriteria.length === 0) { toast.error(t('rubrics.criteria_required')); return; } for (const c of formCriteria) { if (c.levels.length === 0) { toast.error(t('rubrics.levels_required')); return; } } const criteriaPayload: RubricCriterionInput[] = formCriteria.map((c, ci) => ({ id: c.id, name: c.name, description: c.description || null, weight: c.weight, maxPoints: c.maxPoints, order: ci, levels: c.levels.map((l, li) => ({ id: l.id, label: l.label, description: l.description, points: l.points, order: li, })), })); try { setSaving(true); if (editingRubric) { await updateRubric(editingRubric.id, { title: formTitle, description: formDescription || null, type: formType, subjectId: formSubjectId || null, maxPoints: formMaxPoints, isPublic: formIsPublic, criteria: criteriaPayload, }); } else { if (!schoolId) return; await createRubric({ schoolId, title: formTitle, description: formDescription || null, type: formType, subjectId: formSubjectId || null, maxPoints: formMaxPoints, isPublic: formIsPublic, criteria: criteriaPayload, }); } toast.success(t('rubrics.saved')); setFormOpen(false); loadRubrics(); } catch (err) { toast.error(err instanceof Error ? err.message : t('rubrics.error')); } finally { setSaving(false); } }; const handleDelete = async () => { if (!deletingRubric) return; try { await deleteRubric(deletingRubric.id); toast.success(t('rubrics.deleted')); setDeleteOpen(false); setDeletingRubric(null); loadRubrics(); } catch (err) { toast.error(err instanceof Error ? err.message : t('rubrics.error')); } }; const handleDuplicate = async (r: Rubric) => { try { await duplicateRubric(r.id); toast.success(t('rubrics.duplicated')); loadRubrics(); } catch (err) { toast.error(err instanceof Error ? err.message : t('rubrics.error')); } }; const openDetail = (r: Rubric) => { setDetailRubric(r); setDetailOpen(true); }; const handlePrint = () => { window.print(); }; /* ── Analytics ──────────────────────────────────────────────────── */ const openAnalytics = async (r: Rubric) => { setAnalyticsOpen(true); setAnalyticsLoading(true); setAnalyticsData(null); try { const data = await fetchRubricAnalytics(r.id); setAnalyticsData(data); } catch { toast.error(t('rubrics.error')); } finally { setAnalyticsLoading(false); } }; /* ── Grading ────────────────────────────────────────────────────── */ const openGrading = (r: Rubric) => { setGradingRubric(r); setGradeScores({}); setGradeResult(null); setGradeOpen(true); }; const selectLevel = (criterionId: string, levelId: string, points: number) => { setGradeScores((prev) => ({ ...prev, [criterionId]: { levelId, points } })); setGradeResult(null); // Reset result when scores change }; const autoCalculateGrade = () => { if (!gradingRubric) return; const scores = Object.entries(gradeScores).map(([criterionId, data]) => ({ criterionId, levelId: data.levelId, points: data.points, })); if (scores.length < gradingRubric.criteria.length) { toast.error(t('rubrics.select_level')); return; } setGradingCalc(true); gradeWithRubric(gradingRubric.id, { studentId: 'preview', scores, }) .then((result) => { setGradeResult(result); }) .catch(() => { toast.error(t('rubrics.error')); }) .finally(() => { setGradingCalc(false); }); }; /* ── Render ───────────────────────────────────────────────────── */ if (loading) { return (
); } return (
{/* Header */}

{t('rubrics.title')}

{t('rubrics.subtitle')}

{/* Filters */}
setSearchQuery(e.target.value)} className="pl-9" />
{/* Grid */} {filtered.length === 0 ? (

{t('rubrics.empty_title')}

{t('rubrics.empty_desc')}

) : (
{filtered.map((rubric, idx) => ( openDetail(rubric)} >
{rubric.title} {rubric.description && (

{rubric.description}

)}
{rubric.type === 'ANALYTIC' ? t('rubrics.type_analytic') : t('rubrics.type_holistic')}
{rubric.subject && ( {rubric.subject.name} )} {t('rubrics.criteria_count', { count: rubric.criteria.length })} {rubric.maxPoints} {t('rubrics.points')} {rubric.isPublic ? ( ) : ( )} {rubric.isPublic ? t('rubrics.public') : t('rubrics.private')}
{t('rubrics.by_teacher')}{' '} {rubric.teacher ? `${rubric.teacher.firstName} ${rubric.teacher.lastName}` : ''}
))}
)} {/* ── Template Dialog ─────────────────────────────────────────── */} {t('rubrics.templates_title')} {t('rubrics.templates_desc')} {templatesLoading ? (
) : (
{templates.map((template) => ( applyTemplate(template)} >
{template.title}

{template.description}

{template.subject}
{template.criteria.length} {t('rubrics.criteria')} {template.maxPoints} {t('rubrics.points')}
{template.criteria.map((c) => ( {c.name} ))}
))}
)}
{/* ── Create/Edit Dialog ─────────────────────────────────────── */} { if (!open && saving) return; setFormOpen(open); }}> {editingRubric ? t('rubrics.edit') : t('rubrics.create')} {t('rubrics.subtitle')} {t('rubrics.edit_form')} {t('rubrics.preview')} {/* Basic info */}
setFormTitle(e.target.value)} placeholder={t('rubrics.field.title')} />
setFormMaxPoints(parseInt(e.target.value) || 100)} />