'use client'; import React, { useState, useMemo, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { BarChart3, Plus, Edit, Trash2, Filter, Download, Upload, TrendingUp, Loader2, AlertCircle, CheckCircle2, Smartphone, Monitor, Table as TableIcon, } 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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { toast } from 'sonner'; import { DataTable } from './data-table'; import { FormBuilder } from './form-builder'; import { GradingEngine } from '@/lib/grading-engine'; import { useApiGet, useApiMutation } from '@/lib/hooks/useApi'; import { cn } from '@/lib/utils'; interface Grade { id: string; studentId: string; studentName: string; subject: string; assessment: string; score: number; maxScore: number; weight?: number; comment?: string; gradedAt?: string; status: 'pending' | 'graded' | 'released'; } interface UnifiedGradingPanelProps { classId?: string; studentId?: string; mode?: 'teacher' | 'student' | 'admin' | 'parent'; variant?: 'desktop' | 'tablet' | 'list'; onGradeSubmit?: (grades: Grade[]) => Promise; className?: string; } /** * Unified Grading Panel * Consolidates: grading-view.tsx, tablet-grading-view.tsx, grade-analytics-view.tsx * Single component supports all viewing modes and devices */ export function UnifiedGradingPanel({ classId, studentId, mode = 'teacher', variant = 'desktop', onGradeSubmit, className, }: UnifiedGradingPanelProps) { const [grades, setGrades] = useState([]); const [filteredGrades, setFilteredGrades] = useState([]); const [selectedGrade, setSelectedGrade] = useState(null); const [isEditingGrade, setIsEditingGrade] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [filterSubject, setFilterSubject] = useState('all'); const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'graded' | 'released'>('all'); const [viewType, setViewType] = useState<'grid' | 'list' | 'analytics'>( variant === 'list' ? 'list' : 'grid' ); // Fetch grades from API const queryParams = new URLSearchParams(); if (classId) queryParams.append('classId', classId); if (studentId) queryParams.append('studentId', studentId); const { data: backendGrades, isLoading, mutate } = useApiGet( `/api/v1/grades${queryParams.toString() ? '?' + queryParams.toString() : ''}`, { revalidateOnFocus: true } ); // Update local grades when backend data changes React.useEffect(() => { if (backendGrades) { setGrades(backendGrades); applyFilters(backendGrades); } }, [backendGrades]); // Apply filters const applyFilters = useCallback((gradesToFilter: Grade[]) => { let filtered = gradesToFilter; // Search filter if (searchTerm) { const searchLower = searchTerm.toLowerCase(); filtered = filtered.filter( (g) => g.studentName.toLowerCase().includes(searchLower) || g.subject.toLowerCase().includes(searchLower) || g.assessment.toLowerCase().includes(searchLower) ); } // Subject filter if (filterSubject !== 'all') { filtered = filtered.filter((g) => g.subject === filterSubject); } // Status filter if (filterStatus !== 'all') { filtered = filtered.filter((g) => g.status === filterStatus); } setFilteredGrades(filtered); }, [searchTerm, filterSubject, filterStatus]); // Update filters when any filter changes React.useEffect(() => { applyFilters(grades); }, [searchTerm, filterSubject, filterStatus, applyFilters]); // Calculate statistics const stats = useMemo(() => { const completed = grades.filter((g) => g.status !== 'pending').length; const average = grades.length > 0 ? (grades.reduce((sum, g) => sum + (g.score / g.maxScore) * 100, 0) / grades.length).toFixed(1) : 0; return { total: grades.length, completed, pending: grades.length - completed, average, }; }, [grades]); // Handle grade submission const handleSubmitGrades = useCallback(async () => { try { const gradesToSubmit = grades.filter((g) => g.status === 'graded'); if (gradesToSubmit.length === 0) { toast.error('No grades to submit'); return; } if (onGradeSubmit) { await onGradeSubmit(gradesToSubmit); } else { // Use API await fetch('/api/v1/grades/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(gradesToSubmit), }); } setGrades((prev) => prev.map((g) => (g.status === 'graded' ? { ...g, status: 'released' } : g)) ); toast.success(`${gradesToSubmit.length} grades submitted successfully`); mutate(); } catch (error) { toast.error('Failed to submit grades'); console.error(error); } }, [grades, onGradeSubmit, mutate]); // Handle grade update const handleUpdateGrade = useCallback((updatedGrade: Grade) => { setGrades((prev) => prev.map((g) => (g.id === updatedGrade.id ? updatedGrade : g)) ); setSelectedGrade(null); setIsEditingGrade(false); toast.success('Grade updated'); }, []); // Handle grade deletion const handleDeleteGrade = useCallback(async (gradeId: string) => { try { await fetch(`/api/v1/grades/${gradeId}`, { method: 'DELETE' }); setGrades((prev) => prev.filter((g) => g.id !== gradeId)); setSelectedGrade(null); toast.success('Grade deleted'); mutate(); } catch (error) { toast.error('Failed to delete grade'); console.error(error); } }, [mutate]); // Subject list for filtering const subjects = useMemo(() => { const subjectSet = new Set(grades.map((g) => g.subject)); return Array.from(subjectSet).sort(); }, [grades]); // Render desktop grid view const renderDesktopView = () => (
{/* Statistics */}
Total Grades
{stats.total}
Completed
{stats.completed}
Pending
{stats.pending}
Average
{stats.average}%
{/* Grades table */} Grades
{filteredGrades.map((grade) => ( ))}
Student Subject Assessment Score Percentage Status Actions
{grade.studentName} {grade.subject} {grade.assessment} {grade.score}/{grade.maxScore} {((grade.score / grade.maxScore) * 100).toFixed(1)}% {grade.status}
); // Render tablet view (card grid) const renderTabletView = () => (
{filteredGrades.map((grade) => ( { setSelectedGrade(grade); setIsEditingGrade(true); }} >

{grade.studentName}

{grade.subject}

{grade.score}/{grade.maxScore} {grade.status}
))}
); // Render list view const renderListView = () => (
{filteredGrades.map((grade) => ( { setSelectedGrade(grade); setIsEditingGrade(true); }} >

{grade.studentName}

{grade.subject} - {grade.assessment}

{grade.score}/{grade.maxScore}
{grade.status}
))}
); if (isLoading) { return (
); } return (
{/* Header with filters */}

Grading Panel

{/* Filters */}
setSearchTerm(e.target.value)} /> {/* View type switcher */}
{/* Views */} {viewType === 'grid' && renderDesktopView()} {viewType === 'list' && renderListView()} {/* Edit grade dialog */} {isEditingGrade && selectedGrade && ( setIsEditingGrade(false)} onUpdate={handleUpdateGrade} onDelete={handleDeleteGrade} /> )}
); } // Grade Edit Dialog function GradeEditDialog({ grade, onClose, onUpdate, onDelete, }: { grade: Grade; onClose: () => void; onUpdate: (grade: Grade) => void; onDelete: (id: string) => void; }) { const [formData, setFormData] = useState(grade); return ( Edit Grade: {grade.studentName} {grade.subject} - {grade.assessment}
setFormData({ ...formData, score: parseFloat(e.target.value) })} />
setFormData({ ...formData, maxScore: parseFloat(e.target.value) })} />
setFormData({ ...formData, comment: e.target.value })} placeholder="Add a comment..." />
); } export default UnifiedGradingPanel;