From 0906ee28f6de5643cef5957a46d27e6556f56572 Mon Sep 17 00:00:00 2001 From: janpeter visser Date: Fri, 1 May 2026 16:10:02 +0200 Subject: [PATCH] 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 --- .../insights/components/alignment-trend.tsx | 73 ++++++++++++ .../insights/components/plan-quality.tsx | 112 ++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 app/(app)/insights/components/alignment-trend.tsx create mode 100644 app/(app)/insights/components/plan-quality.tsx diff --git a/app/(app)/insights/components/alignment-trend.tsx b/app/(app)/insights/components/alignment-trend.tsx new file mode 100644 index 0000000..45375d1 --- /dev/null +++ b/app/(app)/insights/components/alignment-trend.tsx @@ -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 ( +
+

{d.sprintGoal}

+

+ {aligned} / {d.total} aligned ({d.alignedRatio}%) +

+
+ ) +} + +function sprintLabel(goal: string): string { + return goal.length > 20 ? goal.slice(0, 18) + '…' : goal +} + +export function AlignmentTrend({ trend }: Props) { + if (trend.length === 0) { + return ( +

+ Geen voltooide sprints met verify-data gevonden. +

+ ) + } + + const data = trend.map(p => ({ + ...p, + label: sprintLabel(p.sprintGoal), + })) + + return ( +
+

+ % Aligned per sprint (laatste {trend.length}) +

+ + + + `${v}%`} tick={{ fontSize: 11 }} /> + } /> + + + +
+ ) +} diff --git a/app/(app)/insights/components/plan-quality.tsx b/app/(app)/insights/components/plan-quality.tsx new file mode 100644 index 0000000..ec92d5c --- /dev/null +++ b/app/(app)/insights/components/plan-quality.tsx @@ -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 = { + ALIGNED: 'var(--status-done)', + PARTIAL: 'var(--priority-medium)', + EMPTY: 'var(--priority-critical)', + DIVERGENT: 'var(--priority-high)', +} + +const VERIFY_LABELS: Record = { + 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 ( +
+

{title}

+ + + {jobs.map(j => ( + + + + + + ))} + +
+ + {j.taskTitle} + + {j.productName}{daysAgo(j.finishedAt, nowMs)}
+
+ ) +} + +export function PlanQualityCard({ stats, trend, nowMs }: Props) { + if (stats.counts.length === 0) { + return ( +
+

+ Plan-verify gating moet eerst geactiveerd worden.{' '} + + Bekijk de Plan-verify gating story + +

+
+ ) + } + + const labeled = stats.counts.map(c => ({ + ...c, + name: VERIFY_LABELS[c.result], + })) + + return ( +
+
+ + + + {labeled.map(entry => ( + + ))} + + + + + + +
+ + +
+
+ + +
+ ) +}