Voegt een verplicht code-veld toe aan Sprint, sequentieel per product (consistent met PBI-N, ST-NNN, T-N). - **Schema** — `Sprint.code String @db.VarChar(30)` + `@@unique([product_id, code])` - **Migratie** — voegt kolom toe als nullable, backfillt bestaande sprints via `ROW_NUMBER() OVER (PARTITION BY product_id ORDER BY created_at)` als `SP-N`, en zet daarna NOT NULL + UNIQUE. - **Generator** — `generateNextSprintCode(productId)` in lib/code-server.ts volgt het patroon van story/pbi/task; createSprintAction gebruikt `createWithCodeRetry` voor race-bescherming. - **Seed** — sprint-counter per product (`SP-1`, `SP-2`, ...). Zichtbaar in: - Sprint-header (`Product › Sprint actief · SP-3`) - JobCard + JobDetailPane voor SPRINT_IMPLEMENTATION jobs - Insights: VelocityChart x-axis (compacter dan goal-truncated), AlignmentTrend tooltip, SprintInfoStrip - actions/jobs-page.ts: `sprintCode` is weer een echte code i.p.v. null Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
72 lines
2 KiB
TypeScript
72 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; sprintCode: string; 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">
|
|
<span className="font-mono text-xs text-muted-foreground mr-2">{d.sprintCode}</span>
|
|
{d.sprintGoal}
|
|
</p>
|
|
<p className="text-muted-foreground">
|
|
{aligned} / {d.total} aligned ({d.alignedRatio}%)
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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: p.sprintCode,
|
|
}))
|
|
|
|
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>
|
|
)
|
|
}
|