import type { Prisma, PrismaClient, SprintStatus, StoryStatus, } from '@prisma/client' export type EligibilityReason = 'DONE' | 'IN_OTHER_SPRINT' export type CrossSprintBlock = { storyId: string sprintId: string sprintName: string } export type EligibilityPartition = { eligible: string[] notEligible: { storyId: string; reason: EligibilityReason }[] crossSprint: CrossSprintBlock[] } type StoryEligibilityInput = { sprint_id: string | null status: StoryStatus // De bijbehorende sprint, indien sprint_id !== null. Alleen sprints met // status='OPEN' blokkeren eligibility — een story uit een CLOSED/ARCHIVED/ // FAILED sprint is weer beschikbaar voor planning (consistent met // getBlockingSprintMap, dat alleen OPEN sprints als blocking telt). sprint?: { status: SprintStatus } | null } export function isEligibleForSprint(story: StoryEligibilityInput): boolean { if (story.status === 'DONE') return false if (story.sprint_id === null) return true // Story heeft een sprint_id — sprint-status MOET bekend zijn om eligibility // te bepalen. Ontbrekende sprint-data is conservatief: niet eligible. if (!story.sprint) return false return story.sprint.status !== 'OPEN' } type PrismaLike = Pick | Prisma.TransactionClient export async function partitionByEligibility( prisma: PrismaLike, storyIds: string[], excludeSprintId?: string, ): Promise { if (storyIds.length === 0) { return { eligible: [], notEligible: [], crossSprint: [] } } const stories = await prisma.story.findMany({ where: { id: { in: storyIds } }, select: { id: true, sprint_id: true, status: true, sprint: { select: { id: true, code: true, status: true } }, }, }) const eligible: string[] = [] const notEligible: { storyId: string; reason: EligibilityReason }[] = [] const crossSprint: CrossSprintBlock[] = [] for (const story of stories) { // Een story blokkeert alleen als hij in een ANDERE sprint zit DIE NOG OPEN // is. Stories die in een CLOSED/ARCHIVED/FAILED sprint zaten, zijn weer // vrij voor planning — consistent met getBlockingSprintMap. const inOtherActiveSprint = story.sprint_id !== null && story.sprint_id !== excludeSprintId && story.sprint?.status === 'OPEN' if (inOtherActiveSprint && story.sprint) { crossSprint.push({ storyId: story.id, sprintId: story.sprint.id, sprintName: story.sprint.code, }) notEligible.push({ storyId: story.id, reason: 'IN_OTHER_SPRINT' }) continue } if (story.status === 'DONE') { notEligible.push({ storyId: story.id, reason: 'DONE' }) continue } eligible.push(story.id) } return { eligible, notEligible, crossSprint } } export async function getBlockingSprintMap( prisma: PrismaLike, productId: string, storyIds: string[], excludeSprintId?: string, ): Promise> { const out = new Map() if (storyIds.length === 0) return out const stories = await prisma.story.findMany({ where: { id: { in: storyIds }, product_id: productId, sprint_id: { not: null }, sprint: { status: 'OPEN' }, }, select: { id: true, sprint_id: true, sprint: { select: { id: true, code: true, status: true } }, }, }) for (const story of stories) { if (!story.sprint) continue if (excludeSprintId !== undefined && story.sprint.id === excludeSprintId) continue out.set(story.id, { sprintId: story.sprint.id, sprintName: story.sprint.code, }) } return out }