Scrum4Me/app/(app)/insights/components/alignment-trend.tsx
Janpeter Visser 0454eede74
feat(insights): port unique files from closed bundle-PRs (#41)
Re-introduce the 3 unique files from closed PRs #37 and #40 that
overlap-merged with already-landed sub-PRs (#34, #35, #36, #38, #39):

- app/(app)/insights/page.tsx — Server Component dat alle helpers
  parallel aanroept en de 5 sectie-Cards rendert (Sprint Health,
  Plan-quality, Agent throughput, Velocity, Backlog health)
- app/(app)/insights/components/sprint-info-strip.tsx — chips per
  active sprint met productname + goal + dagen-over + taakcount
- app/(app)/insights/components/alignment-trend.tsx — Recharts
  LineChart die % ALIGNED jobs per sprint over laatste 5 sprints toont
- lib/insights/verify-stats.ts — TrendPoint type + getAlignmentTrend
  helper (uitgebreid van PR #38)

Plus dependency: recharts (was in package.json van #37/#40 die we
sloten).

Tests: 290/290 groen, tsc clean, lint clean.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:44:53 +02:00

73 lines
2 KiB
TypeScript

'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>
)
}