Files
yellow-bank-soal/frontend/src/pages/admin/overview/DataOverview.tsx
2026-06-20 01:43:39 +07:00

180 lines
6.7 KiB
TypeScript

import { Link } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { api } from '@/lib/api'
import { hasWebsiteScope, scopedQueryKey } from '@/lib/queryKeys'
import { useAppStore } from '@/store/useAppStore'
import type { Question } from '@/types'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { Network, Sparkles } from 'lucide-react'
type OverviewSnapshot = {
id: number
tryout_id: string
title: string
question_count: number
created_at: string
basis_items: Question[]
}
type OverviewWebsite = {
id: number
name: string
domain: string
snapshots: OverviewSnapshot[]
}
type HierarchyOverview = {
summary: {
websites: number
snapshots: number
source_questions: number
basis_items: number
ai_runs: number
variants: number
snapshots_without_basis: number
basis_without_variants: number
orphan_variants: number
}
websites: OverviewWebsite[]
}
function SummaryCard({ label, value }: { label: string; value: number }) {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">{label}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
</CardContent>
</Card>
)
}
export default function DataOverview() {
const { websiteId } = useAppStore()
const { data, isLoading, isError } = useQuery({
queryKey: scopedQueryKey(websiteId, 'data-overview'),
queryFn: async () => {
const res = await api.get<HierarchyOverview>('/admin/overview/hierarchy')
return res.data
},
enabled: hasWebsiteScope(websiteId),
})
if (!hasWebsiteScope(websiteId)) {
return <div className="text-muted-foreground">Select a website to load the data overview.</div>
}
if (isLoading) return <Skeleton className="h-[520px] w-full" />
if (isError || !data) {
return (
<Alert variant="destructive">
<AlertTitle>Failed to load data overview</AlertTitle>
<AlertDescription>Check the selected website and backend API availability.</AlertDescription>
</Alert>
)
}
return (
<div className="space-y-6">
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight">Data Overview</h1>
<p className="text-muted-foreground mt-1">Snapshot, basis question, AI run, and variant hierarchy.</p>
</div>
<Button variant="outline" asChild>
<Link to="/admin/import">Open Import</Link>
</Button>
</div>
<div className="grid gap-4 md:grid-cols-4">
<SummaryCard label="Snapshots" value={data.summary.snapshots} />
<SummaryCard label="Source Questions" value={data.summary.source_questions} />
<SummaryCard label="Basis Items" value={data.summary.basis_items} />
<SummaryCard label="AI Variants" value={data.summary.variants} />
</div>
{(data.summary.snapshots_without_basis > 0 ||
data.summary.basis_without_variants > 0 ||
data.summary.orphan_variants > 0) && (
<Alert>
<Network className="h-4 w-4" />
<AlertTitle>Hierarchy gaps detected</AlertTitle>
<AlertDescription>
{data.summary.snapshots_without_basis} snapshots without promoted basis items,{' '}
{data.summary.basis_without_variants} basis items without variants, and{' '}
{data.summary.orphan_variants} variants without a visible basis.
</AlertDescription>
</Alert>
)}
{data.websites.map((website) => (
<div key={website.id} className="space-y-4">
<div>
<h2 className="text-xl font-semibold">{website.name}</h2>
<p className="text-sm text-muted-foreground">{website.domain}</p>
</div>
{website.snapshots.length === 0 ? (
<div className="rounded-md border border-dashed p-8 text-center text-muted-foreground">
No imported snapshots found for this website.
</div>
) : (
<div className="rounded-md border">
<table className="w-full text-sm">
<thead className="bg-muted text-muted-foreground">
<tr>
<th className="p-3 text-left font-medium">Snapshot</th>
<th className="p-3 text-left font-medium">Tryout</th>
<th className="p-3 text-right font-medium">Questions</th>
<th className="p-3 text-right font-medium">Basis</th>
<th className="p-3 text-right font-medium">Actions</th>
</tr>
</thead>
<tbody className="divide-y">
{website.snapshots.map((snapshot) => (
<tr key={snapshot.id}>
<td className="p-3">
<div className="font-medium">{snapshot.title}</div>
<div className="text-xs text-muted-foreground">Snapshot #{snapshot.id}</div>
</td>
<td className="p-3">{snapshot.tryout_id}</td>
<td className="p-3 text-right">{snapshot.question_count}</td>
<td className="p-3 text-right">
<Badge variant={snapshot.basis_items.length ? 'default' : 'secondary'}>
{snapshot.basis_items.length}
</Badge>
</td>
<td className="p-3 text-right">
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm" asChild>
<Link to={`/admin/tryouts/${snapshot.tryout_id}/questions`}>Questions</Link>
</Button>
{snapshot.basis_items[0] && (
<Button variant="outline" size="sm" asChild>
<Link to={`/admin/tryouts/${snapshot.tryout_id}/questions/${snapshot.basis_items[0].id}/ai-workspace`}>
<Sparkles className="mr-2 h-4 w-4" />
AI
</Link>
</Button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
))}
</div>
)
}