115 lines
4.4 KiB
TypeScript
115 lines
4.4 KiB
TypeScript
import type { Subject } from "~/types/api";
|
|
import { Link } from "react-router";
|
|
import { formatGradeBySystem, type GradeSystem } from "~/utils/gradeSystems";
|
|
import { calculateTargetProgress, getTargetStatusBgColor } from "~/utils/gradeCalculations";
|
|
|
|
interface SubjectCardProps {
|
|
subject: Subject;
|
|
averageGrade?: number;
|
|
calculatedAverage?: number;
|
|
gradeSystem?: string;
|
|
onDelete?: () => void;
|
|
}
|
|
|
|
export function SubjectCard({ subject, averageGrade, calculatedAverage, gradeSystem, onDelete }: SubjectCardProps) {
|
|
// Use provided gradeSystem, fallback to subject's own system
|
|
const displaySystem = (gradeSystem || subject.grade_system || "percentage") as GradeSystem;
|
|
|
|
// Calculate target progress if grade is available
|
|
const targetProgress = averageGrade !== undefined ? calculateTargetProgress(averageGrade, subject) : null;
|
|
const hasTaget = targetProgress && targetProgress.status !== "no-target";
|
|
|
|
const isOverridden = calculatedAverage !== undefined && averageGrade !== undefined && Math.abs(calculatedAverage - averageGrade) > 0.01;
|
|
|
|
return (
|
|
<div
|
|
className={`p-6 rounded-lg border-2 hover:shadow-lg transition-shadow ${hasTaget ? getTargetStatusBgColor(targetProgress.status) : ""}`}
|
|
style={{ borderColor: subject.color || "#3b82f6" }}
|
|
>
|
|
<div className="flex justify-between items-start mb-3">
|
|
<Link to={`/subjects/${subject._id}`} className="flex-1">
|
|
<h3 className="text-xl font-semibold text-gray-900 hover:text-blue-600">
|
|
{subject.name}
|
|
</h3>
|
|
{subject.teacher && (
|
|
<p className="text-sm text-gray-600 mt-1">{subject.teacher}</p>
|
|
)}
|
|
</Link>
|
|
{onDelete && (
|
|
<button
|
|
onClick={onDelete}
|
|
className="text-red-500 hover:text-red-700 ml-2"
|
|
aria-label="Delete subject"
|
|
>
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{averageGrade !== undefined && (
|
|
<div className="mb-3">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="text-3xl font-bold" style={{ color: subject.color || "#3b82f6" }}>
|
|
{formatGradeBySystem(averageGrade, displaySystem, 1)}
|
|
{isOverridden && (
|
|
<span className="text-gray-500 text-lg font-normal ml-2">
|
|
({formatGradeBySystem(calculatedAverage!, displaySystem, 1)})
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-gray-600">Current Average</p>
|
|
</div>
|
|
{hasTaget && (
|
|
<div className="text-right">
|
|
<div className="text-sm text-gray-600">Target: {formatGradeBySystem(targetProgress.targetPercentage, displaySystem, 1)}</div>
|
|
<div className="text-xs font-semibold mt-1">
|
|
{targetProgress.status === "above" && (
|
|
<span className="text-green-700">✓ Above Target</span>
|
|
)}
|
|
{targetProgress.status === "near" && (
|
|
<span className="text-yellow-700">→ Near Target</span>
|
|
)}
|
|
{targetProgress.status === "below" && (
|
|
<span className="text-red-700">↓ Below Target</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<p className="text-sm font-medium text-gray-700">Grading Categories:</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{subject.grading_categories.map((category, index) => (
|
|
<span
|
|
key={index}
|
|
className="px-2 py-1 text-xs font-medium rounded-full"
|
|
style={{
|
|
backgroundColor: category.color || "#e5e7eb",
|
|
color: "#1f2937",
|
|
}}
|
|
>
|
|
{category.name} ({category.weight}%)
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|