import { useEffect, useState } from "react"; import { useParams, Link, useNavigate } from "react-router"; import { ProtectedRoute } from "~/components/ProtectedRoute"; import { GradeTable } from "~/components/GradeTable"; import { GradeCalculator } from "~/components/GradeCalculator"; import { LoadingSpinner } from "~/components/LoadingSpinner"; import { Button } from "~/components/Button"; import { Input } from "~/components/Input"; import { api } from "~/api/client"; import type { Subject, Grade, GradeCreate, GradeUpdate } from "~/types/api"; import { calculateSubjectAverage, calculateTargetProgress, getTargetStatusColor } from "~/utils/gradeCalculations"; import { formatGradeBySystem, getGermanGradeDescription, germanGradeToPercentage, type GradeSystem } from "~/utils/gradeSystems"; export default function SubjectDetail() { const { subjectId } = useParams<{ subjectId: string }>(); const navigate = useNavigate(); const [subject, setSubject] = useState(null); const [grades, setGrades] = useState([]); const [loading, setLoading] = useState(true); const [showAddForm, setShowAddForm] = useState(false); const [editingGrade, setEditingGrade] = useState(null); const [deletingGradeId, setDeletingGradeId] = useState(null); // Form state const [gradeName, setGradeName] = useState(""); const [gradeValue, setGradeValue] = useState(""); const [maxGrade, setMaxGrade] = useState("100"); const [category, setCategory] = useState(""); const [weight, setWeight] = useState("1"); const [date, setDate] = useState(new Date().toISOString().split("T")[0]); const [notes, setNotes] = useState(""); const [error, setError] = useState(""); // Get input parameters based on grading system const getGradeInputConfig = () => { const system = (subject?.grade_system || "percentage") as GradeSystem; switch (system) { case "german": return { label: "German Grade", placeholder: "1.0", min: "1", max: "6", step: "0.1", maxGradeValue: "6", showMaxGradeInput: false, helpText: "1 = sehr gut, 6 = ungenügend" }; case "us-letter": return { label: "Percentage", placeholder: "85", min: "0", max: "100", step: "0.01", maxGradeValue: "100", showMaxGradeInput: false, helpText: "Enter as percentage (0-100)" }; default: // percentage return { label: "Grade", placeholder: "85", min: "0", max: undefined, step: "0.01", maxGradeValue: "100", showMaxGradeInput: true, helpText: undefined }; } }; useEffect(() => { loadData(); }, [subjectId]); const loadData = async () => { if (!subjectId) return; try { const [subjectData, gradesData] = await Promise.all([ api.getSubject(subjectId), api.getGrades(subjectId), ]); setSubject(subjectData); setGrades(gradesData); if (subjectData.grading_categories.length > 0) { setCategory(subjectData.grading_categories[0].name); } } catch (error) { console.error("Failed to load subject:", error); navigate("/subjects"); } finally { setLoading(false); } }; const handleSubmitGrade = async (e: React.FormEvent) => { e.preventDefault(); setError(""); if (!subject) return; const config = getGradeInputConfig(); const finalMaxGrade = config.showMaxGradeInput ? parseFloat(maxGrade) : parseFloat(config.maxGradeValue); try { if (editingGrade) { // Update existing grade const updateData: GradeUpdate = { category_name: category, grade: parseFloat(gradeValue), max_grade: finalMaxGrade, weight_in_category: parseFloat(weight), name: gradeName.trim() || undefined, date: new Date(date).toISOString(), notes: notes.trim() || undefined, }; const updated = await api.updateGrade(editingGrade._id, updateData); setGrades(grades.map((g) => (g._id === editingGrade._id ? updated : g))); } else { // Create new grade const gradeData: GradeCreate = { subject_id: subject._id, category_name: category, grade: parseFloat(gradeValue), max_grade: finalMaxGrade, weight_in_category: parseFloat(weight), name: gradeName.trim() || undefined, date: new Date(date).toISOString(), notes: notes.trim() || undefined, }; const newGrade = await api.createGrade(gradeData); setGrades([newGrade, ...grades]); } // Reset form setGradeName(""); setGradeValue(""); setMaxGrade("100"); setWeight("1"); setDate(new Date().toISOString().split("T")[0]); setNotes(""); setShowAddForm(false); setEditingGrade(null); } catch (err) { setError(err instanceof Error ? err.message : editingGrade ? "Failed to update grade" : "Failed to add grade"); } }; const gradeInputConfig = subject ? getGradeInputConfig() : null; const handleEditGrade = (grade: Grade) => { setEditingGrade(grade); setGradeName(grade.name || ""); setGradeValue(grade.grade.toString()); setMaxGrade(grade.max_grade.toString()); setCategory(grade.category_name); setWeight(grade.weight_in_category.toString()); setDate(new Date(grade.date).toISOString().split("T")[0]); setNotes(grade.notes || ""); setShowAddForm(true); }; const handleCancelEdit = () => { setEditingGrade(null); setGradeName(""); setGradeValue(""); setMaxGrade("100"); setWeight("1"); setDate(new Date().toISOString().split("T")[0]); setNotes(""); setShowAddForm(false); }; const handleDeleteGrade = async (gradeId: string) => { if (!confirm("Delete this grade?")) return; try { setDeletingGradeId(gradeId); await api.deleteGrade(gradeId); setGrades(grades.filter((g) => g._id !== gradeId)); } catch (error) { alert("Failed to delete grade"); } finally { setDeletingGradeId(null); } }; if (loading) { return (
); } if (!subject) { return null; } const avgData = calculateSubjectAverage(subject, grades); const targetProgress = calculateTargetProgress(avgData.overallAverage, subject); return (
← Back to Subjects

{subject.name}

{subject.teacher && (

{subject.teacher}

)}
{avgData.overallAverage > 0 ? formatGradeBySystem( avgData.overallAverage, (subject.grade_system as GradeSystem) || "percentage", 1 ) : "-"}

Overall Average

{targetProgress.status !== "no-target" && avgData.overallAverage > 0 && (
Target: {formatGradeBySystem( targetProgress.targetPercentage, (subject.grade_system as GradeSystem) || "percentage", 1 )}
{targetProgress.difference > 0 ? "+" : ""}{targetProgress.difference.toFixed(1)}% {targetProgress.status === "above" ? "above target" : targetProgress.status === "near" ? "near target" : "below target"}
)}
{/* Improvement Tip */} {avgData.categoryAverages.length > 1 && avgData.categoryAverages.every(c => c.average > 0) && ( (() => { const sortedCategories = [...avgData.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) { // Only show if there's a significant difference return (

� Improvement Tip

Your {weakest.categoryName} grades ({formatGradeBySystem( weakest.average, (subject.grade_system as GradeSystem) || "percentage", 1 )}) are lower than your {strongest.categoryName} ({formatGradeBySystem( strongest.average, (subject.grade_system as GradeSystem) || "percentage", 1 )}). Focus more on improving your {weakest.categoryName.toLowerCase()} performance to boost your overall grade!

); } return null; })() )} {/* Grade Calculator */}
{/* Category Averages */}
{avgData.categoryAverages.map((cat) => (

{cat.categoryName}

{cat.average > 0 ? formatGradeBySystem( cat.average, (subject.grade_system as GradeSystem) || "percentage", 1 ) : "-"}

Weight: {cat.weight}%

))}
{/* Add Grade Button/Form */}
{!showAddForm ? (
) : (

{editingGrade ? "Edit Grade" : "Add New Grade"}

setGradeName(e.target.value)} placeholder="e.g., Midterm Exam" />
setGradeValue(e.target.value)} placeholder={gradeInputConfig?.placeholder || "85"} required min={gradeInputConfig?.min || "0"} max={gradeInputConfig?.max} step={gradeInputConfig?.step || "0.01"} /> {gradeInputConfig?.helpText && (

{gradeInputConfig.helpText}

)}
{gradeInputConfig?.showMaxGradeInput && ( setMaxGrade(e.target.value)} placeholder="100" required min="0" step="0.01" /> )}
{/* Grade Preview */} {gradeValue && gradeInputConfig && (
Grade Preview
{subject?.grade_system === "german" ? ( <>
{parseFloat(gradeValue).toFixed(1)}
{getGermanGradeDescription(parseFloat(gradeValue))}
Percentage equivalent: {germanGradeToPercentage(parseFloat(gradeValue)).toFixed(1)}%
) : ( <>
{formatGradeBySystem( gradeInputConfig.showMaxGradeInput && maxGrade && parseFloat(maxGrade) > 0 ? (parseFloat(gradeValue) / parseFloat(maxGrade)) * 100 : parseFloat(gradeValue), (subject?.grade_system as GradeSystem) || "percentage" )}
{gradeInputConfig.showMaxGradeInput && maxGrade && parseFloat(maxGrade) > 0 && (
Percentage: {((parseFloat(gradeValue) / parseFloat(maxGrade)) * 100).toFixed(1)}%
)} )}
)}
setWeight(e.target.value)} placeholder="1" required min="0" step="0.1" /> setDate(e.target.value)} required />
setNotes(e.target.value)} placeholder="Additional notes..." /> {error && (
{error}
)}
)}
{/* Grades Table */}

All Grades

); }