feat: AlignmentTrend component + trendlijn integratie in PlanQualityCard
LineChart met % ALIGNED per voltooide sprint, custom tooltip met absolute counts. PlanQualityCard uitgebreid met AlignmentTrend onder de donut+tabellen. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9b8361229d
commit
0906ee28f6
2 changed files with 185 additions and 0 deletions
73
app/(app)/insights/components/alignment-trend.tsx
Normal file
73
app/(app)/insights/components/alignment-trend.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
'use client'
|
||||
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts'
|
||||
import type { TrendPoint } from '@/lib/insights/verify-stats'
|
||||
|
||||
interface Props {
|
||||
trend: TrendPoint[]
|
||||
}
|
||||
|
||||
interface TooltipPayload {
|
||||
payload?: { total: number; alignedRatio: number; sprintGoal: string }
|
||||
}
|
||||
|
||||
function CustomTooltip({ active, payload }: { active?: boolean; payload?: TooltipPayload[] }) {
|
||||
if (!active || !payload?.length) return null
|
||||
const d = payload[0].payload
|
||||
if (!d) return null
|
||||
const aligned = Math.round((d.alignedRatio / 100) * d.total)
|
||||
return (
|
||||
<div className="rounded border border-border bg-surface-container px-3 py-2 text-sm shadow">
|
||||
<p className="font-medium text-foreground">{d.sprintGoal}</p>
|
||||
<p className="text-muted-foreground">
|
||||
{aligned} / {d.total} aligned ({d.alignedRatio}%)
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function sprintLabel(goal: string): string {
|
||||
return goal.length > 20 ? goal.slice(0, 18) + '…' : goal
|
||||
}
|
||||
|
||||
export function AlignmentTrend({ trend }: Props) {
|
||||
if (trend.length === 0) {
|
||||
return (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Geen voltooide sprints met verify-data gevonden.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
const data = trend.map(p => ({
|
||||
...p,
|
||||
label: sprintLabel(p.sprintGoal),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
% Aligned per sprint (laatste {trend.length})
|
||||
</p>
|
||||
<ResponsiveContainer width="100%" height={180}>
|
||||
<LineChart data={data}>
|
||||
<XAxis dataKey="label" tick={{ fontSize: 11 }} />
|
||||
<YAxis domain={[0, 100]} tickFormatter={v => `${v}%`} tick={{ fontSize: 11 }} />
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Line
|
||||
dataKey="alignedRatio"
|
||||
stroke="var(--status-done)"
|
||||
dot={{ fill: 'var(--status-done)' }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
112
app/(app)/insights/components/plan-quality.tsx
Normal file
112
app/(app)/insights/components/plan-quality.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
||||
import type { VerifyResultStats, VerifyResultKey, TopJob, TrendPoint } from '@/lib/insights/verify-stats'
|
||||
import { AlignmentTrend } from './alignment-trend'
|
||||
|
||||
interface Props {
|
||||
stats: VerifyResultStats
|
||||
trend: TrendPoint[]
|
||||
nowMs: number
|
||||
}
|
||||
|
||||
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, trend, nowMs }: Props) {
|
||||
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="space-y-6">
|
||||
<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>
|
||||
|
||||
<AlignmentTrend trend={trend} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue