import { useEffect, useState } from "react"; import { Link, useNavigate } from "react-router"; import { ProtectedRoute } from "~/components/ProtectedRoute"; import { StatCard } from "~/components/StatCard"; import { SubjectCard } from "~/components/SubjectCard"; import { LoadingSpinner } from "~/components/LoadingSpinner"; import { Button } from "~/components/Button"; import { api } from "~/api/client"; import type { Subject, Grade, ReportPeriod, User, TeacherGrade } from "~/types/api"; import { calculateReportCard, calculateOverallGPA, calculateTargetProgress, getTargetStatusColor } from "~/utils/gradeCalculations"; import { formatGradeBySystem, germanGradeToPercentage, type GradeSystem } from "~/utils/gradeSystems"; export default function Dashboard() { const navigate = useNavigate(); const [user, setUser] = useState(null); const [subjects, setSubjects] = useState([]); const [grades, setGrades] = useState([]); const [periods, setPeriods] = useState([]); const [selectedPeriod, setSelectedPeriod] = useState(null); const [teacherGrades, setTeacherGrades] = useState([]); const [editingTeacherGrade, setEditingTeacherGrade] = useState<{ subjectId: string; value: string; maxValue: string; notes: string } | null>(null); const [loading, setLoading] = useState(true); const [exporting, setExporting] = useState(false); const [deletingTeacherGradeId, setDeletingTeacherGradeId] = useState(null); useEffect(() => { loadData(); }, []); const loadData = async () => { try { const [userData, subjectsData, gradesData, periodsData, teacherGradesData] = await Promise.all([ api.getMe(), api.getSubjects(), api.getGrades(), api.getPeriods(), api.getTeacherGrades(), ]); setUser(userData); setSubjects(subjectsData); setGrades(gradesData); setPeriods(periodsData); setTeacherGrades(teacherGradesData); if (periodsData.length > 0) { // Find the period that contains today's date const today = new Date(); const currentPeriod = periodsData.find(period => { const start = new Date(period.start_date); const end = new Date(period.end_date); return today >= start && today <= end; }); // Use current period if found, otherwise use the most recent period (first in list) setSelectedPeriod(currentPeriod || periodsData[0]); } } catch (error) { console.error("Failed to load data:", error); } finally { setLoading(false); } }; const handleLogout = () => { api.logout(); navigate("/login"); }; const handleTeacherGradeClick = (subjectId: string) => { const subject = subjects.find(s => s._id === subjectId); if (!subject || !selectedPeriod) return; const existing = teacherGrades.find( tg => tg.subject_id === subjectId && tg.period_id === selectedPeriod._id ); const config = getGradeInputConfig(subject.grade_system as GradeSystem); setEditingTeacherGrade({ subjectId, value: existing ? existing.grade.toString() : "", maxValue: existing ? existing.max_grade.toString() : config.maxGradeValue, notes: existing?.notes || "", }); }; const handleSaveTeacherGrade = async () => { if (!editingTeacherGrade || !selectedPeriod) return; try { const subject = subjects.find(s => s._id === editingTeacherGrade.subjectId); if (!subject) return; const config = getGradeInputConfig(subject.grade_system as GradeSystem); const finalMaxGrade = config.showMaxGradeInput ? parseFloat(editingTeacherGrade.maxValue) : parseFloat(config.maxGradeValue); const data = { subject_id: editingTeacherGrade.subjectId, period_id: selectedPeriod._id, grade: parseFloat(editingTeacherGrade.value), max_grade: finalMaxGrade, notes: editingTeacherGrade.notes || undefined, }; const result = await api.createTeacherGrade(data); setTeacherGrades([...teacherGrades.filter( tg => !(tg.subject_id === result.subject_id && tg.period_id === result.period_id) ), result]); setEditingTeacherGrade(null); } catch (error) { alert("Failed to save teacher grade"); } }; const handleDeleteTeacherGrade = async (subjectId: string) => { if (!selectedPeriod) return; const existing = teacherGrades.find( tg => tg.subject_id === subjectId && tg.period_id === selectedPeriod._id ); if (existing && confirm("Remove teacher's grade?")) { try { setDeletingTeacherGradeId(subjectId); await api.deleteTeacherGrade(existing._id); setTeacherGrades(teacherGrades.filter(tg => tg._id !== existing._id)); } catch (error) { alert("Failed to delete teacher grade"); } finally { setDeletingTeacherGradeId(null); } } }; const getGradeInputConfig = (gradeSystem: GradeSystem) => { switch (gradeSystem) { case "german": return { label: "German Grade", placeholder: "1.0", min: "1", max: "6", step: "0.1", maxGradeValue: "6", showMaxGradeInput: false, }; case "us-letter": return { label: "Percentage", placeholder: "85", min: "0", max: "100", step: "0.01", maxGradeValue: "100", showMaxGradeInput: false, }; default: return { label: "Grade", placeholder: "85", min: "0", max: undefined, step: "0.01", maxGradeValue: "100", showMaxGradeInput: true, }; } }; const handleExportCSV = async () => { try { setExporting(true); await api.exportGradesCSV(selectedPeriod?._id); } catch (error) { console.error("Failed to export grades:", error); alert("Failed to export grades. Please try again."); } finally { setExporting(false); } }; if (loading) { return (
); } const reportCardData = selectedPeriod ? calculateReportCard( subjects, grades, new Date(selectedPeriod.start_date), new Date(selectedPeriod.end_date) ) : []; const calculatedGPA = calculateOverallGPA(reportCardData); // Calculate GPA with teacher grade overrides const getSubjectFinalGrade = (subjectId: string, calculatedAverage: number) => { if (!selectedPeriod) return calculatedAverage; const teacherGrade = teacherGrades.find( tg => tg.subject_id === subjectId && tg.period_id === selectedPeriod._id ); if (!teacherGrade) return calculatedAverage; const subject = subjects.find(s => s._id === subjectId); if (!subject) return calculatedAverage; // Convert teacher grade to percentage const gradeSystem = subject.grade_system as GradeSystem || "percentage"; if (gradeSystem === "german" && teacherGrade.max_grade === 6) { return germanGradeToPercentage(teacherGrade.grade); } return (teacherGrade.grade / teacherGrade.max_grade) * 100; }; const overallGPA = selectedPeriod && reportCardData.length > 0 ? reportCardData.reduce((sum, data) => { const finalGrade = getSubjectFinalGrade(data.subject._id, data.overallAverage); return sum + finalGrade; }, 0) / reportCardData.length : calculatedGPA; // Use the first subject's grading system as the primary system const primaryGradeSystem = (subjects[0]?.grade_system as GradeSystem) || "percentage"; return (
{/* Header */}

Grademaxxing

Welcome back, {user?.username}!

{/* Period Selector */} {periods.length > 0 && (
)} {/* Stats Overview */}
0 ? selectedPeriod && teacherGrades.some(tg => tg.period_id === selectedPeriod._id) && calculatedGPA !== overallGPA ? `${formatGradeBySystem(overallGPA, primaryGradeSystem, 1)} (${formatGradeBySystem(calculatedGPA, primaryGradeSystem, 1)})` : formatGradeBySystem(overallGPA, primaryGradeSystem, 1) : "-" } subtitle={selectedPeriod?.name || "All time"} color="#3b82f6" /> d.grades.length > 0).length} with grades`} color="#10b981" /> sum + d.grades.length, 0) : grades.length } in period`} color="#f59e0b" />
{/* Improvement Tips */} {selectedPeriod && reportCardData.length > 0 && ( (() => { const tipsForSubjects = reportCardData .filter(data => data.categoryAverages.length > 1 && data.categoryAverages.every(c => c.average > 0)) .map(data => { const sortedCategories = [...data.categoryAverages].sort((a, b) => a.average - b.average); const weakest = sortedCategories[0]; const strongest = sortedCategories[sortedCategories.length - 1]; const difference = strongest.average - weakest.average; if (difference > 5) { return { subject: data.subject, weakest, strongest, difference }; } return null; }) .filter(tip => tip !== null); if (tipsForSubjects.length > 0) { return (

💡 Improvement Tips

{tipsForSubjects.map((tip, idx) => (

{tip!.subject.name}: Your {tip!.weakest.categoryName} ({formatGradeBySystem( tip!.weakest.average, primaryGradeSystem, 1 )}) needs work compared to {tip!.strongest.categoryName} ({formatGradeBySystem( tip!.strongest.average, primaryGradeSystem, 1 )}). Focus on {tip!.weakest.categoryName.toLowerCase()} to improve!

))}
); } return null; })() )} {/* Report Card */} {selectedPeriod && reportCardData.length > 0 ? (

Report Card - {selectedPeriod.name}

{reportCardData[0]?.categoryAverages.map((cat) => ( ))} {reportCardData.map((data) => { const teacherGrade = selectedPeriod ? teacherGrades.find( tg => tg.subject_id === data.subject._id && tg.period_id === selectedPeriod._id ) : null; const isEditing = editingTeacherGrade?.subjectId === data.subject._id; // Calculate target progress for this subject const targetProgress = calculateTargetProgress(data.overallAverage, data.subject); return ( {data.categoryAverages.map((cat) => ( ))} ); })}
Subject {cat.categoryName} ({cat.weight}%) Final Grade Target Diff Teacher's Grade
{data.subject.name} {cat.average > 0 ? formatGradeBySystem( cat.average, primaryGradeSystem, 1 ) : "-"} {teacherGrade ? ( {formatGradeBySystem( primaryGradeSystem === "german" && teacherGrade.max_grade === 6 ? germanGradeToPercentage(teacherGrade.grade) : (teacherGrade.grade / teacherGrade.max_grade) * 100, primaryGradeSystem, 1 )} {data.overallAverage > 0 && ( {" "}({formatGradeBySystem( data.overallAverage, primaryGradeSystem, 1 )}) )} ) : ( {data.overallAverage > 0 ? formatGradeBySystem( data.overallAverage, primaryGradeSystem, 1 ) : "-"} )} {targetProgress.status !== "no-target" ? primaryGradeSystem === "german" && data.subject.target_grade_max === 6 ? data.subject.target_grade?.toFixed(1) : formatGradeBySystem( targetProgress.targetPercentage, primaryGradeSystem, 1 ) : "-"} {targetProgress.status !== "no-target" && data.overallAverage > 0 ? (targetProgress.difference > 0 ? "+" : "") + targetProgress.difference.toFixed(1) + "%" : "-"} {isEditing ? (
setEditingTeacherGrade({ ...editingTeacherGrade, value: e.target.value })} className="w-20 px-2 py-1 border rounded text-sm" placeholder={getGradeInputConfig(primaryGradeSystem).placeholder} min={getGradeInputConfig(primaryGradeSystem).min} max={getGradeInputConfig(primaryGradeSystem).max} step={getGradeInputConfig(primaryGradeSystem).step} />
) : teacherGrade ? (
{formatGradeBySystem( primaryGradeSystem === "german" && teacherGrade.max_grade === 6 ? germanGradeToPercentage(teacherGrade.grade) : (teacherGrade.grade / teacherGrade.max_grade) * 100, primaryGradeSystem, 1 )}
) : ( )}
) : (

{periods.length === 0 ? ( <> No report periods defined.{" "} Create your first period ) : ( "No grades recorded for this period yet." )}

)} {/* Subjects Grid */}

Your Subjects

{subjects.length > 0 ? (
{subjects.map((subject) => { const subjectGrades = grades.filter((g) => g.subject_id === subject._id); const avgData = reportCardData.find((d) => d.subject._id === subject._id); const teacherGrade = selectedPeriod ? teacherGrades.find( tg => tg.subject_id === subject._id && tg.period_id === selectedPeriod._id ) : null; const calculatedAverage = avgData?.overallAverage; const finalAverage = calculatedAverage !== undefined ? getSubjectFinalGrade(subject._id, calculatedAverage) : undefined; return ( ); })}
) : (

No subjects yet. Create your first subject!

)}
); }