Files
jira/src/lib/executiveKpis.ts
Bastien COIGNOUX 7cd2d6dc40 init
2026-04-24 07:41:55 +02:00

97 lines
3.4 KiB
TypeScript

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')
}