import type { JiraIssue, StoryGroup } from '../types/jira' /** Snapshot sprint tel que renvoyé par le champ Sprint Jira (souvent `customfield_10020`). */ export type JiraSprintSnapshot = { id: number name: string state?: string boardId?: number startDate?: string endDate?: string goal?: string } function coerceSprintObject(o: Record): JiraSprintSnapshot | null { const id = Number(o.id) const name = typeof o.name === 'string' ? o.name.trim() : '' if (!Number.isFinite(id) || !name) return null return { id, name, state: typeof o.state === 'string' ? o.state : undefined, boardId: typeof o.boardId === 'number' ? o.boardId : undefined, startDate: typeof o.startDate === 'string' ? o.startDate : undefined, endDate: typeof o.endDate === 'string' ? o.endDate : undefined, goal: typeof o.goal === 'string' ? o.goal : undefined, } } /** Parse la valeur brute du champ Sprint (tableau d’objets ou de chaînes JSON historiques). */ export function parseSprintFieldRaw(raw: unknown): JiraSprintSnapshot[] { if (raw == null) return [] if (!Array.isArray(raw)) return [] const out: JiraSprintSnapshot[] = [] for (const item of raw) { if (typeof item === 'string') { try { const o = JSON.parse(item) as Record const s = coerceSprintObject(o) if (s) out.push(s) } catch { /* ignore */ } } else if (typeof item === 'object' && item !== null) { const s = coerceSprintObject(item as Record) if (s) out.push(s) } } return out } export function getSprintsOnIssue(issue: JiraIssue, fieldId: string | null): JiraSprintSnapshot[] { if (!fieldId) return [] const raw = (issue.fields as Record)[fieldId] return parseSprintFieldRaw(raw) } /** Sprints distincts sur la story et ses sous-tâches (par id). */ export function mergeSprintsForGroup(group: StoryGroup, fieldId: string | null): JiraSprintSnapshot[] { if (!fieldId) return [] const map = new Map() for (const issue of [group.story, ...group.subtasks]) { for (const sp of getSprintsOnIssue(issue, fieldId)) { map.set(sp.id, sp) } } return [...map.values()].sort((a, b) => { const ae = a.endDate ?? '' const be = b.endDate ?? '' if (ae && be) return be.localeCompare(ae) return a.name.localeCompare(b.name, 'fr') }) } /** La story ou une sous-tâche est dans le sprint `sprintId`. */ export function groupInSprint(group: StoryGroup, sprintId: number, fieldId: string | null): boolean { if (!fieldId) return false return mergeSprintsForGroup(group, fieldId).some((s) => s.id === sprintId) } export type SprintOption = JiraSprintSnapshot & { storyCount: number } /** Sprints distincts sur tout le périmètre, avec nombre de stories touchées. */ export function collectSprintOptions(groups: StoryGroup[], fieldId: string | null): SprintOption[] { if (!fieldId) return [] const acc = new Map }>() for (const g of groups) { const seen = new Set() for (const issue of [g.story, ...g.subtasks]) { for (const sp of getSprintsOnIssue(issue, fieldId)) { seen.add(sp.id) const cur = acc.get(sp.id) if (cur) { cur.keys.add(g.story.key) } else { acc.set(sp.id, { sprint: sp, keys: new Set([g.story.key]) }) } } } } return [...acc.values()] .map(({ sprint, keys }) => ({ ...sprint, storyCount: keys.size })) .sort((a, b) => { const ae = a.endDate ?? '' const be = b.endDate ?? '' if (ae && be) return be.localeCompare(ae) return b.name.localeCompare(a.name, 'fr') }) } export function filterGroupsBySprint( groups: StoryGroup[], sprintId: number | null, fieldId: string | null, ): StoryGroup[] { if (!fieldId || sprintId == null) return groups return groups.filter((g) => groupInSprint(g, sprintId, fieldId)) }