Files
jira/src/lib/sprintExtract.ts
Bastien COIGNOUX 19af51160a sprint
2026-04-24 15:23:23 +02:00

118 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, unknown>): 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 dobjets 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<string, unknown>
const s = coerceSprintObject(o)
if (s) out.push(s)
} catch {
/* ignore */
}
} else if (typeof item === 'object' && item !== null) {
const s = coerceSprintObject(item as Record<string, unknown>)
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<string, unknown>)[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<number, JiraSprintSnapshot>()
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<number, { sprint: JiraSprintSnapshot; keys: Set<string> }>()
for (const g of groups) {
const seen = new Set<number>()
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))
}