sprint
This commit is contained in:
117
src/lib/sprintExtract.ts
Normal file
117
src/lib/sprintExtract.ts
Normal file
@ -0,0 +1,117 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user