- Sprint lifecycle: ACTIVE→OPEN, COMPLETED→CLOSED, +ARCHIVED (FAILED behouden) - TaskStatus: +EXCLUDED (overgeslagen door agent-loop via bestaande TO_DO filter) - Cookie-gebaseerde actieve sprint per product (lib/active-sprint.ts) - Route splitsen: /products/[id]/sprint/[sprintId] + /sprint redirect-page - NavBar: gestapelde product/sprint dropdowns + BUILDING-badge derivatie - Backlog selectie-modus + nieuwe-sprint-dialog (createSprintWithPbisAction) - Migratie 20260507210000_sprint_lifecycle: ALTER TYPE RENAME (geen data-rewrite) - Version bump 1.0.0 → 1.2.0 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
65 lines
1.9 KiB
TypeScript
65 lines
1.9 KiB
TypeScript
import { prisma } from '@/lib/prisma'
|
|
import { productAccessFilter } from '@/lib/product-access'
|
|
|
|
export interface VelocitySprint {
|
|
sprintId: string
|
|
sprintCode: string
|
|
sprintGoal: string
|
|
productId: string
|
|
productName: string
|
|
doneCount: number
|
|
completedAt: string
|
|
}
|
|
|
|
export interface VelocityData {
|
|
sprints: VelocitySprint[]
|
|
productNames: { id: string; name: string }[]
|
|
}
|
|
|
|
export async function getVelocity(userId: string, sprintsBack = 5): Promise<VelocityData> {
|
|
const sprints = await prisma.sprint.findMany({
|
|
where: {
|
|
status: 'CLOSED',
|
|
product: productAccessFilter(userId),
|
|
},
|
|
orderBy: { completed_at: 'desc' },
|
|
take: sprintsBack,
|
|
select: {
|
|
id: true,
|
|
code: true,
|
|
sprint_goal: true,
|
|
completed_at: true,
|
|
product: { select: { id: true, name: true } },
|
|
tasks: { select: { status: true } },
|
|
},
|
|
})
|
|
|
|
// Reverse to chronological order (oldest first, for x-axis)
|
|
// Type-guard so the narrowed array carries `completed_at: Date` (not Date | null).
|
|
// A `.filter(s => s.completed_at != null)` alone does NOT narrow the element type.
|
|
type SprintWithCompletedAt = (typeof sprints)[number] & { completed_at: Date }
|
|
const chronological = [...sprints]
|
|
.filter((s): s is SprintWithCompletedAt => s.completed_at != null)
|
|
.reverse()
|
|
|
|
const result: VelocitySprint[] = chronological.map(sprint => ({
|
|
sprintId: sprint.id,
|
|
sprintCode: sprint.code,
|
|
sprintGoal: sprint.sprint_goal,
|
|
productId: sprint.product.id,
|
|
productName: sprint.product.name,
|
|
doneCount: sprint.tasks.filter(t => t.status === 'DONE').length,
|
|
completedAt: sprint.completed_at.toISOString(),
|
|
}))
|
|
|
|
const seen = new Set<string>()
|
|
const productNames: { id: string; name: string }[] = []
|
|
for (const s of result) {
|
|
if (!seen.has(s.productId)) {
|
|
seen.add(s.productId)
|
|
productNames.push({ id: s.productId, name: s.productName })
|
|
}
|
|
}
|
|
|
|
return { sprints: result, productNames }
|
|
}
|