* chore: voeg recharts toe aan dependencies Vereist door PlanQualityCard component. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: getVerifyResultStats helper (lib/insights/verify-stats.ts) Aggregeert verify_result counts en top-5 EMPTY/DIVERGENT jobs over de laatste N dagen. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: PlanQualityCard — verify_result donut + top-5 EMPTY/DIVERGENT tabellen PieChart (donut) met ALIGNED/PARTIAL/EMPTY/DIVERGENT verdeling, MD3-kleuren. Twee tabellen rechts met Next.js Link deeplinks naar TaskDetailDialog. Empty-state met link naar Plan-verify gating story. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
|
import type { VerifyResultStats, VerifyResultKey, TopJob } from '@/lib/insights/verify-stats'
|
|
|
|
interface Props {
|
|
stats: VerifyResultStats
|
|
}
|
|
|
|
const VERIFY_COLORS: Record<VerifyResultKey, string> = {
|
|
ALIGNED: 'var(--status-done)',
|
|
PARTIAL: 'var(--priority-medium)',
|
|
EMPTY: 'var(--priority-critical)',
|
|
DIVERGENT: 'var(--priority-high)',
|
|
}
|
|
|
|
const VERIFY_LABELS: Record<VerifyResultKey, string> = {
|
|
ALIGNED: 'Aligned',
|
|
PARTIAL: 'Partial',
|
|
EMPTY: 'Empty',
|
|
DIVERGENT: 'Divergent',
|
|
}
|
|
|
|
function daysAgo(date: Date, nowMs: number): string {
|
|
const diff = Math.floor((nowMs - new Date(date).getTime()) / 86_400_000)
|
|
return diff === 0 ? 'vandaag' : `${diff}d geleden`
|
|
}
|
|
|
|
function TopTable({ title, jobs, nowMs }: { title: string; jobs: TopJob[]; nowMs: number }) {
|
|
if (jobs.length === 0) return null
|
|
return (
|
|
<div className="space-y-1">
|
|
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">{title}</p>
|
|
<table className="w-full text-sm">
|
|
<tbody>
|
|
{jobs.map(j => (
|
|
<tr key={j.jobId} className="border-b border-border last:border-0">
|
|
<td className="py-1 pr-2">
|
|
<Link
|
|
href={`/products/${j.productId}/solo?task=${j.taskId}`}
|
|
className="text-primary underline line-clamp-1"
|
|
>
|
|
{j.taskTitle}
|
|
</Link>
|
|
</td>
|
|
<td className="py-1 pr-2 text-muted-foreground whitespace-nowrap">{j.productName}</td>
|
|
<td className="py-1 text-muted-foreground whitespace-nowrap">{daysAgo(j.finishedAt, nowMs)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function PlanQualityCard({ stats, nowMs }: Props & { nowMs: number }) {
|
|
if (stats.counts.length === 0) {
|
|
return (
|
|
<div className="rounded-lg border border-border p-4 space-y-2">
|
|
<p className="text-sm text-muted-foreground">
|
|
Plan-verify gating moet eerst geactiveerd worden.{' '}
|
|
<Link
|
|
href="/products/cmohrysyj0000rd17clnjy4tc/backlog?story=cmomp6n670007bortkxslr3na"
|
|
className="underline text-primary"
|
|
>
|
|
Bekijk de Plan-verify gating story
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const labeled = stats.counts.map(c => ({
|
|
...c,
|
|
name: VERIFY_LABELS[c.result],
|
|
}))
|
|
|
|
return (
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<ResponsiveContainer width="100%" height={240}>
|
|
<PieChart>
|
|
<Pie
|
|
data={labeled}
|
|
dataKey="count"
|
|
nameKey="name"
|
|
innerRadius={50}
|
|
outerRadius={80}
|
|
>
|
|
{labeled.map(entry => (
|
|
<Cell key={entry.result} fill={VERIFY_COLORS[entry.result]} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip />
|
|
<Legend />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
|
|
<div className="space-y-4 self-start">
|
|
<TopTable title="Top 5 Empty (verify_only kandidaten)" jobs={stats.topEmpty} nowMs={nowMs} />
|
|
<TopTable title="Top 5 Divergent (te vage plans)" jobs={stats.topDivergent} nowMs={nowMs} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|