180 lines
6.7 KiB
TypeScript
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>
|
|
)
|
|
}
|