Checkpoint React frontend migration
This commit is contained in:
274
frontend/src/pages/admin/Dashboard.tsx
Normal file
274
frontend/src/pages/admin/Dashboard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user