Checkpoint React frontend migration

This commit is contained in:
Dwindi Ramadhana
2026-06-20 01:43:39 +07:00
parent ab86c254d1
commit b8e201b45f
173 changed files with 34116 additions and 782 deletions

View File

@@ -0,0 +1,274 @@
import { useQuery } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import { api } from '@/lib/api'
import { hasWebsiteScope, scopedQueryKey } from '@/lib/queryKeys'
import { useAppStore } from '@/store/useAppStore'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { Badge } from '@/components/ui/badge'
import {
FileText,
Users,
CheckCircle2,
Target,
AlertCircle,
Activity,
Bot
} from 'lucide-react'
// Adjust type for dashboard stats
interface DashboardStats {
metrics: {
tryouts: number
items: number
sessions: number
completed_sessions: number
completion_rate: number
calibration_percentage: number
}
recent_sessions: Array<{
id: number
wp_user_id: string
tryout_id: string
end_time: string
NM: number | null
NN: number | null
}>
recent_ai_runs: Array<{
id: number
requested_count: number
basis_item_id: number
created_at: string
status: string
pending_review_count?: number
}>
}
export default function Dashboard() {
const { websiteId } = useAppStore()
const { data, isLoading, isError } = useQuery({
queryKey: scopedQueryKey(websiteId, 'dashboard-stats'),
queryFn: async () => {
const res = await api.get<DashboardStats>('/admin/dashboard/stats')
return res.data
},
enabled: hasWebsiteScope(websiteId),
})
if (!hasWebsiteScope(websiteId)) {
return (
<div className="p-4 border rounded-md bg-muted/30 text-muted-foreground">
Select a website to load dashboard statistics.
</div>
)
}
const hasPendingAIReview =
data?.recent_ai_runs.some((run) => run.status === 'pending_review' || (run.pending_review_count || 0) > 0) ?? false
return (
<div className="space-y-6">
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-bold tracking-tight">Good Morning, Admin</h1>
<p className="text-muted-foreground">Here is your system overview for today.</p>
</div>
<Card className="bg-primary/5 border-primary/20">
<CardHeader>
<CardTitle>Getting Started & Workflow</CardTitle>
<CardDescription>Follow these steps to generate and calibrate IRT questions.</CardDescription>
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-4">
<div className="flex flex-col gap-2 p-4 bg-background rounded-lg border">
<div className="flex items-center gap-2 font-semibold">
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-sm">1</div>
Create Tryout
</div>
<p className="text-sm text-muted-foreground">Import Tryout snapshots from Sejoli to initialize the question tree.</p>
</div>
<div className="flex flex-col gap-2 p-4 bg-background rounded-lg border">
<div className="flex items-center gap-2 font-semibold">
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-sm">2</div>
Generate Variants
</div>
<p className="text-sm text-muted-foreground">Use AI to generate parallel question variants (Mudah/Sedang/Sulit).</p>
</div>
<div className="flex flex-col gap-2 p-4 bg-background rounded-lg border">
<div className="flex items-center gap-2 font-semibold">
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-sm">3</div>
Gather Data
</div>
<p className="text-sm text-muted-foreground">Students complete tryouts to gather participant answer data.</p>
</div>
<div className="flex flex-col gap-2 p-4 bg-background rounded-lg border">
<div className="flex items-center gap-2 font-semibold">
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-sm">4</div>
Calibrate & Normalize
</div>
<p className="text-sm text-muted-foreground">System calibrates IRT parameters (p-value, IRT b) and calculates NN score.</p>
</div>
</CardContent>
</Card>
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[1, 2, 3, 4].map((i) => (
<Card key={i}>
<CardHeader className="pb-2">
<Skeleton className="h-4 w-1/2 mb-2" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-1/3" />
</CardContent>
</Card>
))}
</div>
) : isError || !data ? (
<div className="p-4 border border-destructive/50 bg-destructive/10 text-destructive rounded-md">
Failed to load dashboard statistics.
</div>
) : (
<>
{/* System Overview KPIs */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Active Exams</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{data.metrics.tryouts}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total Questions</CardTitle>
<Target className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{data.metrics.items}</div>
<p className="text-xs text-muted-foreground mt-1">
{data.metrics.calibration_percentage}% Calibrated
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Student Attempts</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{data.metrics.sessions}</div>
<p className="text-xs text-muted-foreground mt-1">
{data.metrics.completed_sessions} completed
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Completion Rate</CardTitle>
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{data.metrics.completion_rate}%</div>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Attention Needed */}
<Card className="border-destructive/50">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-5 w-5" />
Attention Needed
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{data.metrics.calibration_percentage < 100 && (
<div className="flex items-start gap-3 p-3 bg-muted/50 rounded-lg">
<Target className="h-5 w-5 text-amber-500 shrink-0 mt-0.5" />
<div>
<p className="font-medium">Questions need calibration</p>
<p className="text-sm text-muted-foreground">
Some questions have enough data but haven't been calibrated yet.
</p>
</div>
</div>
)}
{hasPendingAIReview && (
<div className="flex items-start gap-3 p-3 bg-muted/50 rounded-lg">
<Bot className="h-5 w-5 text-blue-500 shrink-0 mt-0.5" />
<div>
<p className="font-medium">AI generated questions pending review</p>
<p className="text-sm text-muted-foreground">
You have new AI-generated questions waiting for your approval.
</p>
<Link to="/admin/ai-generation" className="text-sm text-primary hover:underline mt-1 block">
Review now
</Link>
</div>
</div>
)}
{data.metrics.calibration_percentage === 100 && !hasPendingAIReview && (
<p className="text-muted-foreground text-sm">You're all caught up! No urgent tasks.</p>
)}
</CardContent>
</Card>
{/* Recent Activity */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Recent Activity
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{data.recent_sessions.length === 0 && data.recent_ai_runs.length === 0 ? (
<p className="text-muted-foreground text-sm">No recent activity.</p>
) : (
<div className="space-y-4">
{data.recent_sessions.map((session) => (
<div key={`session-${session.id}`} className="flex items-start justify-between border-b pb-2 last:border-0 last:pb-0">
<div>
<p className="text-sm font-medium">Student {session.wp_user_id}</p>
<p className="text-xs text-muted-foreground">Completed Tryout: {session.tryout_id}</p>
</div>
<div className="text-right">
<Badge variant="secondary">Score: {session.NN ?? session.NM ?? 0}</Badge>
<p className="text-[10px] text-muted-foreground mt-1">
{new Date(session.end_time).toLocaleTimeString()}
</p>
</div>
</div>
))}
{data.recent_ai_runs.map((run) => (
<div key={`run-${run.id}`} className="flex items-start justify-between border-b pb-2 last:border-0 last:pb-0">
<div>
<p className="text-sm font-medium">AI Generation Run #{run.id}</p>
<p className="text-xs text-muted-foreground">Target: {run.requested_count} questions</p>
</div>
<div className="text-right">
<Badge variant={run.status === 'completed' ? 'default' : 'outline'}>{run.status}</Badge>
<p className="text-[10px] text-muted-foreground mt-1">
{new Date(run.created_at).toLocaleTimeString()}
</p>
</div>
</div>
))}
</div>
)}
</div>
</CardContent>
</Card>
</div>
</>
)}
</div>
)
}