Scrum4Me/app/(app)/insights/components/sprint-info-strip.tsx
Janpeter Visser a268df3680
feat(PBI-59): Sprint.code (SP-N sequentieel per product) (#153)
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>
2026-05-07 20:10:16 +02:00

47 lines
1.4 KiB
TypeScript

'use client'
interface SprintInfo {
sprintId: string
sprintCode: string
productName: string
sprintGoal: string
taskCount: number
daysLeft: number
}
interface Props {
sprints: SprintInfo[]
}
function daysLeftColor(daysLeft: number): string {
if (daysLeft >= 3) return 'text-[color:var(--status-done)]'
if (daysLeft >= 1) return 'text-[color:var(--priority-medium)]'
return 'text-[color:var(--priority-critical)]'
}
function truncate(text: string, max: number): string {
return text.length > max ? text.slice(0, max) + '…' : text
}
export function SprintInfoStrip({ sprints }: Props) {
if (sprints.length === 0) return null
return (
<div className="flex flex-wrap gap-2">
{sprints.map(s => (
<div
key={s.sprintId}
className="flex items-center gap-3 rounded-lg border border-border bg-surface-container px-3 py-2 text-sm"
>
<span className="font-medium text-foreground">{s.productName}</span>
<span className="font-mono text-xs text-muted-foreground">{s.sprintCode}</span>
<span className="text-muted-foreground">{truncate(s.sprintGoal, 60)}</span>
<span className={`font-mono tabular-nums ${daysLeftColor(s.daysLeft)}`}>
{s.daysLeft > 0 ? `${s.daysLeft}d over` : `${Math.abs(s.daysLeft)}d over tijd`}
</span>
<span className="text-muted-foreground">{s.taskCount} tasks</span>
</div>
))}
</div>
)
}