This commit is contained in:
Bastien COIGNOUX
2026-04-24 11:50:39 +02:00
parent 745c8ae133
commit ca4c64bbb0
28 changed files with 2269 additions and 116 deletions

View File

@ -1,11 +1,14 @@
import type { StoryGroup } from '../types/jira'
import type { Milestone } from './dashboardConfig'
import { storyProgressPercent } from './storyMetrics'
import { getRemainingEstimateUnits } from './jiraFieldExtractors'
import type { StatusBucketConfig } from './statusBuckets'
import { resolveWorkBucketFromIssue } from './statusBuckets'
import { subtaskDoneRatioPercent } from './storyMetrics'
/** Pour les jalons : story sans sous-tâche = considérée comme « livrée » côté sous-tâches. */
function storyCompletionForMilestone(g: StoryGroup): number {
function storyCompletionForMilestone(g: StoryGroup, cfg: StatusBucketConfig): number {
if (g.subtasks.length === 0) return 100
return storyProgressPercent(g.subtasks)
return subtaskDoneRatioPercent(g.subtasks, cfg)
}
function endOfDay(isoDate: string): Date {
@ -14,19 +17,61 @@ function endOfDay(isoDate: string): Date {
return d
}
/** Jalon en retard : date dépassée et au moins une story concernée nest pas à 100 %. */
export function isMilestoneLate(m: Milestone, groups: StoryGroup[]): boolean {
if (groups.length === 0) return false
const deadline = endOfDay(m.date)
if (new Date() <= deadline) return false
/** Stories du périmètre du jalon (clés liées si renseignées, sinon tout le chargement). */
export function milestoneLinkedGroups(m: Milestone, groups: StoryGroup[]): StoryGroup[] {
const keys =
m.linkedStoryKeys && m.linkedStoryKeys.length > 0
? m.linkedStoryKeys
: groups.map((g) => g.story.key)
for (const key of keys) {
const g = groups.find((x) => x.story.key === key)
if (!g) continue
if (storyCompletionForMilestone(g) < 100) return true
const set = new Set(keys)
return groups.filter((g) => set.has(g.story.key))
}
/** Moyenne des % sous-tâches terminées sur les stories du périmètre (0100). */
export function milestoneAverageCompletionPercent(
m: Milestone,
groups: StoryGroup[],
cfg: StatusBucketConfig,
): number {
const linked = milestoneLinkedGroups(m, groups)
if (linked.length === 0) return 100
const sum = linked.reduce((acc, g) => acc + storyCompletionForMilestone(g, cfg), 0)
return Math.round(sum / linked.length)
}
/** Somme du reste à faire (unités Jira) sur sous-tâches non terminées / non annulées du périmètre. */
export function milestoneOpenRemainingUnits(
m: Milestone,
groups: StoryGroup[],
cfg: StatusBucketConfig,
): number {
let u = 0
for (const g of milestoneLinkedGroups(m, groups)) {
for (const s of g.subtasks) {
const b = resolveWorkBucketFromIssue(s, cfg)
if (b !== 'done' && b !== 'cancel') u += getRemainingEstimateUnits(s)
}
}
return u
}
/** Jours calendaires jusquà la fin du jour du jalon (négatif = retard). */
export function milestoneCalendarDaysUntil(m: Milestone): number {
const end = endOfDay(m.date)
return Math.ceil((end.getTime() - Date.now()) / 86400000)
}
/** Jalon en retard : date dépassée et au moins une story concernée nest pas à 100 %. */
export function isMilestoneLate(
m: Milestone,
groups: StoryGroup[],
cfg: StatusBucketConfig,
): boolean {
if (groups.length === 0) return false
const deadline = endOfDay(m.date)
if (new Date() <= deadline) return false
for (const g of milestoneLinkedGroups(m, groups)) {
if (storyCompletionForMilestone(g, cfg) < 100) return true
}
return false
}