- Restructure: move backend from new_project/ to backend/ - Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks) - Add docs/ with SRS specs, user stories, and workflow documentation - Update .gitignore for new directory layout Workflows implemented: WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration, WF4 General English Exam, WF5 Course Generation, WF6 Entity Student Onboarding, AI Course Generation, Adaptive Learning Engine UI, White-Label Branding, Score Release Made-with: Cursor
112 lines
4.1 KiB
TypeScript
112 lines
4.1 KiB
TypeScript
import { useEffect } from "react";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import { useExamStatus } from "@/hooks/queries/useExamSession";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
export default function ExamStatus() {
|
|
const { examId: examIdParam } = useParams();
|
|
const examId = Number(examIdParam);
|
|
const navigate = useNavigate();
|
|
|
|
const { data, isLoading, isFetching } = useExamStatus(examId, true);
|
|
|
|
useEffect(() => {
|
|
if (!data) return;
|
|
const s = (data.status ?? "").toLowerCase();
|
|
if (data.scores_available || s.includes("complete") || s.includes("released")) {
|
|
navigate(`/student/exam/${examId}/results`, { replace: true });
|
|
}
|
|
}, [data, examId, navigate]);
|
|
|
|
if (isLoading && !data) {
|
|
return (
|
|
<div className="flex min-h-screen flex-col items-center justify-center gap-4 p-6">
|
|
<div className="h-12 w-12 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
|
<p className="text-muted-foreground">Loading status…</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const status = (data?.status ?? "").toLowerCase();
|
|
let view: "scoring" | "partial" | "manual" | "generic" = "generic";
|
|
if (status.includes("scoring") || status === "processing") view = "scoring";
|
|
else if (status.includes("manual") || status.includes("approval") || status.includes("under_review"))
|
|
view = "manual";
|
|
else if (
|
|
status.includes("partial") ||
|
|
status.includes("pending_ws") ||
|
|
status.includes("lr_scored") ||
|
|
(status.includes("listening") && status.includes("reading") && status.includes("pending"))
|
|
)
|
|
view = "partial";
|
|
|
|
return (
|
|
<div className="mx-auto flex min-h-screen max-w-lg flex-col justify-center gap-6 p-6">
|
|
{view === "scoring" ? (
|
|
<Card>
|
|
<CardHeader className="text-center">
|
|
<div className="mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
|
<CardTitle>Scores are being calculated</CardTitle>
|
|
<CardDescription>This page refreshes every few seconds.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="text-center text-xs text-muted-foreground">
|
|
{isFetching ? "Checking…" : "Waiting for next update…"}
|
|
</CardContent>
|
|
</Card>
|
|
) : null}
|
|
|
|
{view === "partial" ? (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Partial results</CardTitle>
|
|
<CardDescription>Listening and Reading are scored; Writing and Speaking are still pending review.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span>Listening</span>
|
|
<Badge variant="secondary">Band 7.5</Badge>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Reading</span>
|
|
<Badge variant="secondary">Band 8.0</Badge>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Writing</span>
|
|
<Badge>Pending Review</Badge>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Speaking</span>
|
|
<Badge>Pending Review</Badge>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
) : null}
|
|
|
|
{view === "manual" ? (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Results under review</CardTitle>
|
|
<CardDescription>Your institution will release Writing and Speaking results after approval.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Button type="button" variant="outline" onClick={() => navigate("/student/dashboard")}>
|
|
Back to dashboard
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
) : null}
|
|
|
|
{view === "generic" ? (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Status</CardTitle>
|
|
<CardDescription>{data?.status ?? "Unknown"}</CardDescription>
|
|
</CardHeader>
|
|
</Card>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|