import type { StoryGroup } from '../types/jira' import type { Milestone } from './dashboardConfig' 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, cfg: StatusBucketConfig): number { if (g.subtasks.length === 0) return 100 return subtaskDoneRatioPercent(g.subtasks, cfg) } function endOfDay(isoDate: string): Date { const d = new Date(isoDate + 'T12:00:00') d.setHours(23, 59, 59, 999) return d } /** 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) 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 (0–100). */ 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 n’est 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 }