import { useEffect, useMemo, useState } from 'react' import type { StoryGroup } from '../types/jira' import type { FunctionalGapBadge } from '../lib/dashboardConfig' import { fetchAllIssueKeysInSprint } from '../api/jiraClient' import { useStatusBuckets } from '../context/StatusBucketContext' import type { JiraSprintSnapshot } from '../lib/sprintExtract' import { collectSprintOptions, filterGroupsBySprint, filterGroupsBySprintIssueKeys, sprintOptionsFromBoardAndGroups, sprintOptionsFromBoardOnly, } from '../lib/sprintExtract' import { subtaskDoneRatioPercent } from '../lib/storyMetrics' import { StoryCard } from './StoryCard' type Props = { groups: StoryGroup[] sprintFieldId: string | null /** Sprints actifs/futurs depuis l’API Agile du board (sans élargir le périmètre JQL). */ boardSprintsFromApi?: JiraSprintSnapshot[] onOpenSettings: () => void gapBadges?: FunctionalGapBadge[] } export function SprintView({ groups, sprintFieldId, boardSprintsFromApi = [], onOpenSettings, gapBadges, }: Props) { const cfg = useStatusBuckets() const [focusId, setFocusId] = useState('all') const [sprintIssueKeys, setSprintIssueKeys] = useState>(new Set()) const [sprintIssuesLoading, setSprintIssuesLoading] = useState(false) const options = useMemo(() => { if (boardSprintsFromApi.length > 0) { if (sprintFieldId) { const fromBoard = sprintOptionsFromBoardAndGroups( boardSprintsFromApi, groups, sprintFieldId, ) if (fromBoard.length > 0) return fromBoard } else { return sprintOptionsFromBoardOnly(boardSprintsFromApi) } } if (sprintFieldId) return collectSprintOptions(groups, sprintFieldId) return [] }, [boardSprintsFromApi, groups, sprintFieldId]) useEffect(() => { if (sprintFieldId || focusId === 'all') { setSprintIssueKeys(new Set()) setSprintIssuesLoading(false) return } const sprintId = focusId as number const ac = new AbortController() setSprintIssuesLoading(true) setSprintIssueKeys(new Set()) void fetchAllIssueKeysInSprint(sprintId, ac.signal) .then((set) => { if (!ac.signal.aborted) setSprintIssueKeys(set) }) .catch(() => { if (!ac.signal.aborted) setSprintIssueKeys(new Set()) }) .finally(() => { if (!ac.signal.aborted) setSprintIssuesLoading(false) }) return () => ac.abort() }, [focusId, sprintFieldId]) const filtered = useMemo(() => { if (focusId === 'all') return groups if (sprintFieldId) return filterGroupsBySprint(groups, focusId, sprintFieldId) if (sprintIssuesLoading) return [] return filterGroupsBySprintIssueKeys(groups, sprintIssueKeys) }, [groups, focusId, sprintFieldId, sprintIssueKeys, sprintIssuesLoading]) const stats = useMemo(() => { let subs = 0 let sumPct = 0 let nWithSubs = 0 for (const g of filtered) { subs += g.subtasks.length if (g.subtasks.length > 0) { sumPct += subtaskDoneRatioPercent(g.subtasks, cfg) nWithSubs += 1 } } const avgPct = nWithSubs > 0 ? Math.round(sumPct / nWithSubs) : 0 return { stories: filtered.length, subtasks: subs, avgPct } }, [filtered, cfg]) /** Aucune liste possible : ni board Agile, ni champ Sprint sur les tickets. */ const mustConfigureSprintField = options.length === 0 && !sprintFieldId && boardSprintsFromApi.length === 0 if (mustConfigureSprintField) { return (

Vue Sprint — configuration requise

Aucun sprint actif/futur n’a été reçu du board Agile, et le champ Sprint n’est pas renseigné. Indiquez l’identifiant du champ personnalisé Sprint (souvent{' '} customfield_10020) dans les réglages ou via{' '} VITE_JIRA_SPRINT_FIELD, ou vérifiez le board (VITE_JIRA_BOARD_ID ) puis actualisez.

) } if (options.length === 0) { return (

Vue Sprint

Aucun sprint actif ou futur sur le board, et aucun sprint détecté sur les tickets chargés.

) } const filterModeAgile = !sprintFieldId && boardSprintsFromApi.length > 0 return (
{filterModeAgile && (
Filtre sprint via l’API Agile Jira (issues du sprint). Pour afficher les pastilles sprint sur chaque carte, renseignez aussi le champ Sprint (ID{' '} customfield_…) dans les réglages.
)}

Vue Sprint

{sprintFieldId ? ( <> Sprints actifs/futurs du board Jira Agile ; le nombre de stories compte le périmètre chargé (épopée) avec le champ Sprint sur les tickets. ) : ( <> Sprints actifs/futurs du board (API Agile). Le filtre par sprint interroge les issues du sprint côté Jira ; le périmètre affiché reste celui de l’épopée Golden Carbon. )}

Stories :{' '} {stats.stories} Sous-tâches :{' '} {stats.subtasks} Avancement moy. :{' '} {stats.avgPct}%
{sprintIssuesLoading && focusId !== 'all' && !sprintFieldId ? (

Chargement des tickets du sprint…

) : filtered.length === 0 ? (

Aucune story du périmètre dans ce sprint (filtre « Ma vue » inclus), ou erreur lors du chargement des issues du sprint.

) : (
{filtered.map((g) => ( ))}
)}
) }