'use client'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { FileText, Plus, Search, Filter, Download, Eye, Edit3, Trash2, Send, Archive, CheckCircle2, Clock, AlertCircle, ChevronDown, ChevronRight, Printer, Star, BookOpen, Users, Calendar, X, Save, Copy, BarChart3, Award, MessageSquare, UserCheck, Shield, GraduationCap, Loader2, MoreHorizontal, LayoutTemplate, } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Checkbox } from '@/components/ui/checkbox'; import { Separator } from '@/components/ui/separator'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Progress } from '@/components/ui/progress'; import { useAppStore } from '@/lib/store'; import { t } from '@/lib/i18n'; import { apiGet, apiPost, apiPut, apiDelete } from '@/lib/api'; import { toast } from 'sonner'; import { RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, ResponsiveContainer, Tooltip as RechartsTooltip, } from 'recharts'; // ─── Types ────────────────────────────────────────────────────────── interface StudentInfo { id: string; firstName: string; lastName: string; avatarUrl?: string; dateOfBirth?: string; } interface ClassGroupInfo { id: string; name: string; gradeLevel: number; schoolType?: string; responsibleTeacher?: { id: string; firstName: string; lastName: string }; } interface SchoolYearInfo { id: string; label: string; } interface UserInfo { id: string; firstName: string; lastName: string; } interface CompetencyCategoryInfo { id: string; name: string; color?: string; } interface ReportSectionInfo { id: string; competencyCategoryId?: string | null; generatedText: string; order: number; competencyCategory?: CompetencyCategoryInfo | null; } interface ComputedGradeInfo { id: string; computedValue: number; overriddenValue?: number | null; isFinalized: boolean; subject: { id: string; name: string }; } interface TemplateInfo { id: string; name: string; layout: string; gradingScale?: string; } interface ReportCard { id: string; studentId: string; classGroupId: string; schoolYearId: string; period: string; generatedByUserId: string; generatedAt: string; status: string; pdfFilePath?: string | null; includesGrades: boolean; teacherComments?: string | null; attendanceSummary?: string | null; overallAssessment?: string | null; templateId?: string | null; reviewedByUserId?: string | null; reviewedAt?: string | null; publishedAt?: string | null; student: StudentInfo; classGroup: ClassGroupInfo; schoolYear: SchoolYearInfo; generatedByUser: UserInfo; sections: ReportSectionInfo[]; template?: TemplateInfo | null; computedGrades?: ComputedGradeInfo[]; } interface TemplateItem { id: string; name: string; description?: string; sections: string; gradingScale: string; layout: string; isDefault: boolean; } // ─── German Grade Helpers ─────────────────────────────────────────── const GRADE_LABELS: Record = { 1: 'Sehr gut', 2: 'Gut', 3: 'Befriedigend', 4: 'Ausreichend', 5: 'Mangelhaft', 6: 'Ungenügend', }; function getGradeColor(grade: number): string { if (grade <= 1) return 'text-emerald-600 bg-emerald-50 dark:bg-emerald-950/50 dark:text-emerald-400'; if (grade <= 2) return 'text-green-600 bg-green-50 dark:bg-green-950/50 dark:text-green-400'; if (grade <= 3) return 'text-yellow-600 bg-yellow-50 dark:bg-yellow-950/50 dark:text-yellow-400'; if (grade <= 4) return 'text-amber-600 bg-amber-50 dark:bg-amber-950/50 dark:text-amber-400'; if (grade <= 5) return 'text-orange-600 bg-orange-50 dark:bg-orange-950/50 dark:text-orange-400'; return 'text-red-600 bg-red-50 dark:bg-red-950/50 dark:text-red-400'; } function getGradeBorder(grade: number): string { if (grade <= 2) return 'border-emerald-300 dark:border-emerald-700'; if (grade <= 3) return 'border-yellow-300 dark:border-yellow-700'; if (grade <= 4) return 'border-amber-300 dark:border-amber-700'; return 'border-red-300 dark:border-red-700'; } function getStatusBadge(status: string) { const shimmerBase = 'relative overflow-hidden'; const shimmerAfter = 'after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/20 after:to-transparent after:animate-[shimmer_2s_infinite]'; switch (status) { case 'DRAFT': return {t('report_card.status_draft')}; case 'REVIEW': return {t('report_card.status_review')}; case 'PUBLISHED': return {t('report_card.status_published')}; case 'ARCHIVED': return {t('report_card.status_archived')}; case 'FINAL': return {t('report_card.status_final')}; default: return {status}; } } // ─── Animated Counter ─────────────────────────────────────────────── function AnimatedCounter({ value, duration = 1000 }: { value: number; duration?: number }) { const [count, setCount] = useState(0); useEffect(() => { let start = 0; const step = Math.max(1, Math.ceil(value / (duration / 30))); const timer = setInterval(() => { start += step; if (start >= value) { setCount(value); clearInterval(timer); } else { setCount(start); } }, 30); return () => clearInterval(timer); }, [value, duration]); return {count}; } // ─── Main Component ───────────────────────────────────────────────── export default function ReportCardView() { const { currentUser, currentClassId, schoolYearId } = useAppStore(); const role = currentUser?.role ?? 'TEACHER'; const [reportCards, setReportCards] = useState([]); const [isLoading, setIsLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [filterClass, setFilterClass] = useState(currentClassId ?? 'all'); const [filterStatus, setFilterStatus] = useState('all'); const [filterPeriod, setFilterPeriod] = useState('all'); const [selectedReport, setSelectedReport] = useState(null); const [viewMode, setViewMode] = useState<'list' | 'editor' | 'preview'>('list'); const [classes, setClasses] = useState<{ id: string; name: string; gradeLevel: number }[]>([]); const [selectedIds, setSelectedIds] = useState>(new Set()); const [templates, setTemplates] = useState([]); // Dialog states const [showCreateDialog, setShowCreateDialog] = useState(false); const [showBatchDialog, setShowBatchDialog] = useState(false); const [showTemplateDialog, setShowTemplateDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); // Editor state const [editTeacherComments, setEditTeacherComments] = useState(''); const [editOverallAssessment, setEditOverallAssessment] = useState(''); const [editSections, setEditSections] = useState([]); const [isSaving, setIsSaving] = useState(false); // Create form state const [createForm, setCreateForm] = useState({ studentId: '', classGroupId: currentClassId ?? '', schoolYearId: schoolYearId ?? '', period: 'Semester 1', includesGrades: true, templateId: '', teacherComments: '', overallAssessment: '', }); const [students, setStudents] = useState<{ id: string; firstName: string; lastName: string }[]>([]); const [schoolYears, setSchoolYears] = useState<{ id: string; label: string }[]>([]); // Batch form state const [batchForm, setBatchForm] = useState({ classGroupId: currentClassId ?? '', schoolYearId: schoolYearId ?? '', period: 'Semester 1', templateId: '', includesGrades: true, }); // Template form state const [templateForm, setTemplateForm] = useState({ name: '', description: '', layout: 'default' as 'default' | 'detailed' | 'compact', isDefault: false, }); // ─── Data Fetching ────────────────────────────────────────────── const fetchReportCards = useCallback(async () => { setIsLoading(true); try { const params = new URLSearchParams(); if (filterClass && filterClass !== 'all') params.set('classGroupId', filterClass); if (filterStatus && filterStatus !== 'all') params.set('status', filterStatus); if (filterPeriod && filterPeriod !== 'all') params.set('period', filterPeriod); if (schoolYearId) params.set('schoolYearId', schoolYearId); const data = await apiGet(`/api/report-cards?${params.toString()}`); setReportCards(data); } catch (error) { console.error('Failed to fetch report cards:', error); toast.error(t('report_card.load_error')); } finally { setIsLoading(false); } }, [filterClass, filterStatus, filterPeriod, schoolYearId]); const fetchClasses = useCallback(async () => { try { const data = await apiGet<{ id: string; name: string; gradeLevel: number }[]>('/api/classes'); setClasses(data); } catch { // ignore } }, []); const fetchStudents = useCallback(async (classGroupId: string) => { try { const data = await apiGet<{ id: string; firstName: string; lastName: string }[]>( `/api/classes/${classGroupId}/students` ); setStudents(data); } catch { setStudents([]); } }, []); const fetchSchoolYears = useCallback(async () => { try { const data = await apiGet<{ id: string; label: string }[]>('/api/school-years'); setSchoolYears(data); if (!schoolYearId && data.length > 0) { setCreateForm((f) => ({ ...f, schoolYearId: data[0].id })); setBatchForm((f) => ({ ...f, schoolYearId: data[0].id })); } } catch { // ignore } }, [schoolYearId]); const fetchTemplates = useCallback(async () => { if (!currentUser?.schoolId) return; try { const data = await apiGet(`/api/report-cards/templates?schoolId=${currentUser.schoolId}`); setTemplates(data); } catch { // ignore } }, [currentUser?.schoolId]); useEffect(() => { fetchReportCards(); fetchClasses(); fetchSchoolYears(); fetchTemplates(); }, [fetchReportCards, fetchClasses, fetchSchoolYears, fetchTemplates]); useEffect(() => { if (createForm.classGroupId) { fetchStudents(createForm.classGroupId); } }, [createForm.classGroupId, fetchStudents]); // ─── Filtered Data ────────────────────────────────────────────── const filteredCards = useMemo(() => { let result = reportCards; if (searchQuery) { const q = searchQuery.toLowerCase(); result = result.filter( (r) => r.student.firstName.toLowerCase().includes(q) || r.student.lastName.toLowerCase().includes(q) || r.classGroup.name.toLowerCase().includes(q) || r.period.toLowerCase().includes(q) ); } return result; }, [reportCards, searchQuery]); // ─── Stats ────────────────────────────────────────────────────── const stats = useMemo(() => { const total = reportCards.length; const drafts = reportCards.filter((r) => r.status === 'DRAFT').length; const inReview = reportCards.filter((r) => r.status === 'REVIEW').length; const published = reportCards.filter((r) => r.status === 'PUBLISHED' || r.status === 'FINAL').length; return { total, drafts, inReview, published }; }, [reportCards]); // ─── Actions ──────────────────────────────────────────────────── const handleCreateReport = async () => { try { const report = await apiPost('/api/report-cards', createForm); setReportCards((prev) => [report, ...prev]); setShowCreateDialog(false); toast.success(t('report_card.created')); } catch (error) { toast.error(t('report_card.create_error')); } }; const handleBatchGenerate = async () => { try { const result = await apiPost<{ generated: number; skipped: number }>('/api/report-cards/generate', batchForm); toast.success(t('report_card.batch_generated').replace('{count}', String(result.generated))); setShowBatchDialog(false); fetchReportCards(); } catch (error) { toast.error(t('report_card.batch_error')); } }; const handleSaveReport = async () => { if (!selectedReport) return; setIsSaving(true); try { const updated = await apiPut(`/api/report-cards/${selectedReport.id}`, { teacherComments: editTeacherComments, overallAssessment: editOverallAssessment, sections: editSections.map((s, i) => ({ competencyCategoryId: s.competencyCategoryId, generatedText: s.generatedText, order: i, })), }); setReportCards((prev) => prev.map((r) => (r.id === updated.id ? updated : r))); setSelectedReport(updated); toast.success(t('report_card.saved')); } catch (error) { toast.error(t('report_card.save_error')); } finally { setIsSaving(false); } }; const handleStatusChange = async (reportId: string, newStatus: string) => { try { const updated = await apiPut(`/api/report-cards/${reportId}`, { status: newStatus }); setReportCards((prev) => prev.map((r) => (r.id === updated.id ? updated : r))); if (selectedReport?.id === reportId) setSelectedReport(updated); toast.success(t('report_card.status_updated')); } catch (error) { toast.error(t('report_card.status_error')); } }; const handleDelete = async () => { if (!deleteTarget) return; try { await apiDelete(`/api/report-cards/${deleteTarget}`); setReportCards((prev) => prev.filter((r) => r.id !== deleteTarget)); if (selectedReport?.id === deleteTarget) { setSelectedReport(null); setViewMode('list'); } setShowDeleteDialog(false); setDeleteTarget(null); toast.success(t('report_card.deleted')); } catch (error) { toast.error(t('report_card.delete_error')); } }; const handleBulkAction = async (action: 'publish' | 'archive' | 'delete') => { if (selectedIds.size === 0) return; try { for (const id of selectedIds) { if (action === 'publish') { await apiPut(`/api/report-cards/${id}`, { status: 'PUBLISHED' }); } else if (action === 'archive') { await apiPut(`/api/report-cards/${id}`, { status: 'ARCHIVED' }); } else if (action === 'delete') { await apiDelete(`/api/report-cards/${id}`); } } setSelectedIds(new Set()); toast.success(t('report_card.bulk_action_success')); fetchReportCards(); } catch (error) { toast.error(t('report_card.bulk_action_error')); } }; const handleExportPDF = async (reportIds: string[]) => { try { const data = await apiPost<{ reports: ReportCard[]; school: { name: string; motto?: string; logoUrl?: string; address?: string; phone?: string; primaryColor?: string }; exportDate: string; }>('/api/report-cards/export', { reportIds }); // Store export data and open print dialog const printWindow = window.open('', '_blank'); if (printWindow) { printWindow.document.write(generatePrintHTML(data.reports, data.school)); printWindow.document.close(); setTimeout(() => printWindow.print(), 500); } toast.success(t('report_card.export_success')); } catch (error) { toast.error(t('report_card.export_error')); } }; const handlePrintCurrent = () => { window.print(); }; const handleCreateTemplate = async () => { try { const template = await apiPost('/api/report-cards/templates', { ...templateForm, sections: JSON.stringify([ { type: 'header', label: t('report_card.section_header') }, { type: 'grades', label: t('report_card.section_grades') }, { type: 'competencies', label: t('report_card.section_competencies') }, { type: 'comments', label: t('report_card.section_comments') }, { type: 'attendance', label: t('report_card.section_attendance') }, { type: 'signature', label: t('report_card.section_signature') }, ]), gradingScale: JSON.stringify({ min: 1, max: 6, labels: { 1: 'Sehr gut', 2: 'Gut', 3: 'Befriedigend', 4: 'Ausreichend', 5: 'Mangelhaft', 6: 'Ungenügend' }, }), }); setTemplates((prev) => [...prev, template]); setShowTemplateDialog(false); toast.success(t('report_card.template_created')); } catch (error) { toast.error(t('report_card.template_error')); } }; // ─── Editor Setup ─────────────────────────────────────────────── const openEditor = (report: ReportCard) => { setSelectedReport(report); setEditTeacherComments(report.teacherComments ?? ''); setEditOverallAssessment(report.overallAssessment ?? ''); setEditSections(report.sections ?? []); setViewMode('editor'); }; const openPreview = (report: ReportCard) => { setSelectedReport(report); setViewMode('preview'); }; // ─── Radar Chart Data ────────────────────────────────────────── const radarData = useMemo(() => { if (!selectedReport?.computedGrades) return []; const grades = selectedReport.computedGrades; return grades.map((g) => ({ subject: g.subject.name, grade: 7 - (g.overriddenValue ?? g.computedValue), // Invert for radar (higher = better) fullMark: 6, })); }, [selectedReport]); // ─── Attendance Parsed ───────────────────────────────────────── const attendanceParsed = useMemo(() => { if (!selectedReport?.attendanceSummary) return null; try { return JSON.parse(selectedReport.attendanceSummary); } catch { return null; } }, [selectedReport?.attendanceSummary]); // ─── Permission Checks ───────────────────────────────────────── const isAdmin = role === 'SUPER_ADMIN' || role === 'SCHOOL_ADMIN' || role === 'VICE_PRINCIPAL'; const isTeacher = role === 'TEACHER'; const isStudent = role === 'STUDENT'; const isParent = role === 'PARENT'; const canCreate = isAdmin || isTeacher; const canEdit = isAdmin || isTeacher; const canPublish = isAdmin; const canDelete = isAdmin; const canBatch = isAdmin; // ─── Periods ──────────────────────────────────────────────────── const periods = ['Semester 1', 'Semester 2', 'Halbjahr 1', 'Halbjahr 2', 'Ganzjahr', '1. Halbjahr', '2. Halbjahr']; // ─── Render ───────────────────────────────────────────────────── return (
{/* Gradient Header Banner */}

{t('report_card.title')}

{t('report_card.subtitle')}

{t('report_card.total_reports')}
{/* Header Stats */}

{t('report_card.total_reports')}

{t('report_card.drafts')}

{t('report_card.in_review')}

{t('report_card.published')}

{/* List View */} {viewMode === 'list' && ( {/* Toolbar */}
setSearchQuery(e.target.value)} className="pl-9" />
{canCreate && ( )} {canBatch && ( )} {isAdmin && ( )}
{/* Bulk Actions */} {selectedIds.size > 0 && (
{selectedIds.size} {t('report_card.selected')} {canPublish && ( )} {canDelete && ( )}
)}
{/* Report Cards List */} {isLoading ? (
) : filteredCards.length === 0 ? (

{t('report_card.no_reports')}

{canCreate && ( )}
) : (
{filteredCards.map((report, idx) => (
{(isAdmin || isTeacher) && ( { const next = new Set(selectedIds); if (checked) next.add(report.id); else next.delete(report.id); setSelectedIds(next); }} className="mt-1" /> )}

{report.student.firstName} {report.student.lastName}

{getStatusBadge(report.status)} {report.classGroup.name} {report.period}
{new Date(report.generatedAt).toLocaleDateString('de-DE')} {report.generatedByUser.firstName} {report.generatedByUser.lastName} {report.template && ( {report.template.name} )} {report.includesGrades && ( {t('report_card.with_grades')} )}
{report.sections.length > 0 && (
{report.sections.slice(0, 3).map((s) => ( {s.competencyCategory?.name ?? s.generatedText.slice(0, 30)} ))} {report.sections.length > 3 && ( +{report.sections.length - 3} {t('report_card.more')} )}
)}
{canEdit && ( )} {canEdit && report.status === 'DRAFT' && ( handleStatusChange(report.id, 'REVIEW')}> {t('report_card.submit_review')} )} {canPublish && report.status === 'REVIEW' && ( handleStatusChange(report.id, 'PUBLISHED')}> {t('report_card.publish')} )} {canPublish && report.status === 'PUBLISHED' && ( handleStatusChange(report.id, 'FINAL')}> {t('report_card.finalize')} )} handleExportPDF([report.id])}> {t('report_card.export_pdf')} handlePrintCurrent()}> {t('action.print')} {report.status !== 'ARCHIVED' && ( handleStatusChange(report.id, 'ARCHIVED')}> {t('report_card.archive')} )} {canDelete && ( <> { setDeleteTarget(report.id); setShowDeleteDialog(true); }} > {t('action.delete')} )}
))}
)}
)} {/* Editor View */} {viewMode === 'editor' && selectedReport && (

{t('report_card.edit_title')} - {selectedReport.student.firstName} {selectedReport.student.lastName}

{getStatusBadge(selectedReport.status)}
{/* Left: Main Editor */}
{/* Student Info */} {t('report_card.student_info')}

{selectedReport.student.firstName} {selectedReport.student.lastName}

{selectedReport.classGroup.name}

{selectedReport.schoolYear.label}

{selectedReport.period}

{/* Subject Grades Table */} {selectedReport.computedGrades && selectedReport.computedGrades.length > 0 && ( {t('report_card.subject_grades')}
{selectedReport.computedGrades.map((g) => { const grade = g.overriddenValue ?? g.computedValue; const rounded = Math.round(grade * 10) / 10; const gradeLabel = GRADE_LABELS[Math.round(grade)] ?? ''; return ( ); })}
{t('report_card.subject')} {t('report_card.grade')} {t('report_card.label')} {t('report_card.status_col')}
{g.subject.name} {rounded} {gradeLabel} {g.isFinalized ? ( ) : ( )}
)} {/* Competency Sections */} {t('report_card.competency_sections')} {editSections.map((section, idx) => (
{section.competencyCategory?.color && (
)}