Scrum4Me/app/(app)/insights/components/backlog-health.tsx
janpeter visser 2f7c62f2ea fix(insights): integrate all 5 sections in /insights + add missing components
The /insights page was rendering only Sprint Health (2 charts).
PRs #47/#48/#49 delivered helpers + tests for Velocity, Backlog health
and Agent throughput, but Velocity and Backlog never produced their
UI components, and none of the four new sections were wired into
page.tsx. Result: user sees 2 charts where 5 sections were promised.

This PR fills the gaps:

- New `app/(app)/insights/components/velocity-chart.tsx` — Recharts
  grouped BarChart with optional ReferenceLine for the average. Empty
  state when <2 completed sprints.
- New `app/(app)/insights/components/backlog-health.tsx` — counters
  (stories sans AC / tasks sans plan / stuck>7d) + stuck-tasks table
  with severity-coded days-stuck cell.
- `app/(app)/insights/page.tsx` rewritten as 5 sections:
  Sprint Health, Plan-quality (donut + alignment-trend), Agent
  throughput, Velocity, Backlog health. Helpers run in one
  Promise.all so the page renders in a single tick.

Tests: 314/314 green, tsc clean, lint 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:46:25 +02:00

90 lines
2.9 KiB
TypeScript

'use client'
import Link from 'next/link'
import { CheckCircle2, AlertTriangle } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { BacklogHealth, StuckTask } from '@/lib/insights/backlog-health'
interface Props {
data: BacklogHealth
}
function Counter({ count, label }: { count: number; label: string }) {
const ok = count === 0
return (
<div className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm">
{ok ? (
<CheckCircle2 className="size-4 text-status-done shrink-0" />
) : (
<AlertTriangle className="size-4 text-priority-medium shrink-0" />
)}
<span className={cn('font-semibold', ok && 'text-status-done')}>{count}</span>
<span className="text-muted-foreground">{label}</span>
</div>
)
}
function StuckRow({ task }: { task: StuckTask }) {
const severity =
task.daysStuck >= 14
? 'bg-priority-critical/10 text-priority-critical'
: task.daysStuck >= 7
? 'bg-priority-high/10 text-priority-high'
: ''
return (
<tr className="border-b border-border last:border-0">
<td className="py-1 pr-2">
<Link
href={`/products/${task.productId}/solo?task=${task.taskId}`}
className="text-primary underline line-clamp-1"
>
{task.title}
</Link>
</td>
<td className="py-1 pr-2 text-muted-foreground whitespace-nowrap">
{task.productName}
</td>
<td className={cn('py-1 px-2 text-right whitespace-nowrap rounded font-medium', severity)}>
{task.daysStuck}d
</td>
<td className="py-1 pl-2 text-muted-foreground whitespace-nowrap">
{task.sprintGoal ?? '—'}
</td>
</tr>
)
}
export function BacklogHealthCard({ data }: Props) {
const { storiesWithoutAc, tasksWithoutPlan, stuckTasks } = data
const allClear =
storiesWithoutAc === 0 && tasksWithoutPlan === 0 && stuckTasks.length === 0
return (
<div className="rounded-lg border border-border p-4 space-y-3">
<h2 className="text-sm font-medium">Backlog health</h2>
<div className="flex flex-wrap gap-2">
<Counter count={storiesWithoutAc} label="stories zonder AC" />
<Counter count={tasksWithoutPlan} label="tasks zonder plan" />
<Counter count={stuckTasks.length} label="stuck > 7d" />
</div>
{allClear ? (
<p className="text-sm text-status-done">Geen stuck tasks 🎉</p>
) : stuckTasks.length > 0 ? (
<div>
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1">
Stuck tasks (top {stuckTasks.length})
</p>
<table className="w-full text-sm">
<tbody>
{stuckTasks.map(t => (
<StuckRow key={t.taskId} task={t} />
))}
</tbody>
</table>
</div>
) : null}
</div>
)
}