import type { JiraIssue, StoryGroup } from '../types/jira' import { statusToPhase } from './statusPhase' function norm(s: string): string { return s .trim() .toLowerCase() .normalize('NFD') .replace(/\p{M}/gu, '') } export function isBlockingStatus(statusName: string): boolean { const n = norm(statusName) return ( n.includes('bloque') || n.includes('blocked') || n.includes('recette ko') || n.includes('recetteko') || n.includes('recette nok') ) } function allSubtasks(groups: StoryGroup[]): JiraIssue[] { return groups.flatMap((g) => g.subtasks) } export function maquetteRelatedSubtasks(groups: StoryGroup[]): JiraIssue[] { return allSubtasks(groups).filter(isMaquetteRelated) } export function goldenCarbonSubtasks(groups: StoryGroup[]): JiraIssue[] { return groups.flatMap((g) => g.subtasks.filter((st) => isGoldenCarbonRelated(st, g.story)), ) } /** Progression globale : sous-tâches terminées / sous-tâches totales. */ export function globalProgressPercent(groups: StoryGroup[]): number { const subs = allSubtasks(groups) if (subs.length === 0) return 0 const done = subs.filter((s) => statusToPhase(s.fields.status.name) === 'done').length return Math.round((done / subs.length) * 100) } /** Sous-tâches considérées comme « maquette » (libellé à ajuster selon votre vocabulaire Jira). */ export function isMaquetteRelated(st: JiraIssue): boolean { const t = `${st.fields.summary} ${st.key}`.toLowerCase() return /maquette|mockup|figma|wireframe|zoning|ui\s*design/i.test(t) } /** % de maquettes validées parmi les sous-tâches identifiées comme maquettes. */ export function designHealthPercent(groups: StoryGroup[]): number { const candidates = maquetteRelatedSubtasks(groups) if (candidates.length === 0) return 0 const ok = candidates.filter((s) => statusToPhase(s.fields.status.name) === 'done').length return Math.round((ok / candidates.length) * 100) } function textWithComponents(st: JiraIssue, story: JiraIssue): string { const comps = [...(st.fields.components ?? []), ...(story.fields.components ?? [])] .map((c) => c.name) .join(' ') return `${st.fields.summary} ${comps}`.toLowerCase() } /** Intégration « Golden Carbon » : filtre par mot-clé ou composant. */ function isGoldenCarbonRelated(st: JiraIssue, story: JiraIssue): boolean { return /golden\s*carbon|goldencarbon/i.test(textWithComponents(st, story)) } export function goldenCarbonHealthPercent(groups: StoryGroup[]): number { const candidates = goldenCarbonSubtasks(groups) if (candidates.length === 0) return 0 const ok = candidates.filter((s) => statusToPhase(s.fields.status.name) === 'done').length return Math.round((ok / candidates.length) * 100) } export function blockingTicketsCount(groups: StoryGroup[]): number { const issues: JiraIssue[] = [ ...groups.map((g) => g.story), ...allSubtasks(groups), ] return issues.filter((i) => isBlockingStatus(i.fields.status.name)).length } export function blockingIssuesInGroup(group: StoryGroup): JiraIssue[] { return [group.story, ...group.subtasks].filter((i) => isBlockingStatus(i.fields.status.name), ) } export function blockingSummaryForTooltip(group: StoryGroup): string { const list = blockingIssuesInGroup(group) if (list.length === 0) return 'Aucun ticket bloquant sur cette story.' return list.map((i) => `${i.key} — ${i.fields.status.name}: ${i.fields.summary}`).join('\n') }