118 lines
3.9 KiB
TypeScript
118 lines
3.9 KiB
TypeScript
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 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<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))
|
||
}
|