diff --git a/.env.example b/.env.example index 87944c5..f712bb4 100644 --- a/.env.example +++ b/.env.example @@ -22,6 +22,9 @@ JIRA_API_KEY= # Champ Story Points dans Jira (Administration → Issues → champs personnalisés → ID) # VITE_JIRA_STORY_POINTS_FIELD=customfield_10028 +# Champ Sprint (Scrum) pour la vue Sprint — ID souvent proche de 10020 selon les instances +# VITE_JIRA_SPRINT_FIELD=customfield_10020 + # Clé de l’épopée : utilisée dans le JQL (parentEpic + exclusion) et pour le groupement UI # VITE_JIRA_EPIC_KEY=DCC-5514 diff --git a/src/App.tsx b/src/App.tsx index d3468a7..42b0b82 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,8 +35,11 @@ import { StatusBucketProvider } from './context/StatusBucketContext' import { LaneLabelsProvider } from './context/LaneLabelsContext' import { PipelineOverview } from './components/PipelineOverview' import { LaneTicketsListView } from './components/LaneTicketsListView' +import { ProjectTimelineView } from './components/ProjectTimelineView' +import { SprintView } from './components/SprintView' +import { resolveSprintFieldId } from './lib/jiraSprintField' -type ViewMode = 'list' | 'board' +type ViewMode = 'list' | 'board' | 'project' | 'sprint' export default function App() { const dashboardRef = useRef(null) @@ -54,6 +57,11 @@ export default function App() { const myViewActive = Boolean(dashboardCfg.myViewActive) + const sprintFieldResolved = useMemo( + () => resolveSprintFieldId({ sprintFieldId: dashboardCfg.sprintFieldId }), + [dashboardCfg.sprintFieldId], + ) + const displayGroups = useMemo(() => { if (!myViewActive) return groups return groups.filter((g) => @@ -137,7 +145,10 @@ export default function App() { setLoading(true) setError(null) try { - const issues = await fetchAllIssuesByJql(MIGRATION_JQL, signal) + const sprintField = resolveSprintFieldId({ sprintFieldId: dashboardCfg.sprintFieldId }) + const issues = await fetchAllIssuesByJql(MIGRATION_JQL, signal, { + additionalFields: sprintField ? [sprintField] : [], + }) const grouped = groupSubtasksUnderStories(issues) setGroups(grouped) setUpdatedAt(new Date()) @@ -169,7 +180,7 @@ export default function App() { } finally { if (!signal?.aborted) setLoading(false) } - }, []) + }, [dashboardCfg.sprintFieldId]) useEffect(() => { const ac = new AbortController() @@ -243,6 +254,30 @@ export default function App() { > Board + +
{!loading && groups.length > 0 && ( @@ -293,64 +328,86 @@ export default function App() { {!loading && !error && groups.length > 0 && (
- setSettingsOpen(true)} - impactMessages={impactMessages} - /> + {view === 'project' ? ( + setSettingsOpen(true)} + /> + ) : view === 'sprint' ? ( + setSettingsOpen(true)} + /> + ) : ( + <> + setSettingsOpen(true)} + impactMessages={impactMessages} + /> - + - + - + - + -
-

- Reporting -

-
- - -
-
+
+

+ Reporting +

+
+ + +
+
-
-
-

- {view === 'list' ? 'Stories (liste)' : 'Stories par composant'} -

- {myViewActive && ( - - Filtre actif : {displayGroups.length} / {groups.length} stories - - )} -
- {displayGroups.length === 0 ? ( -

- Aucune story ne correspond à « Ma vue ». Vérifiez vos assignations ou configurez - votre accountId / e-mail dans les réglages. -

- ) : view === 'list' ? ( -
- {displayGroups.map((g) => ( - - ))} -
- ) : ( - - )} -
+
+
+

+ {view === 'list' ? 'Stories (liste)' : 'Stories par composant'} +

+ {myViewActive && ( + + Filtre actif : {displayGroups.length} / {groups.length} stories + + )} +
+ {displayGroups.length === 0 ? ( +

+ Aucune story ne correspond à « Ma vue ». Vérifiez vos assignations ou configurez + votre accountId / e-mail dans les réglages. +

+ ) : view === 'list' ? ( +
+ {displayGroups.map((g) => ( + + ))} +
+ ) : ( + + )} +
+ + )}
)} diff --git a/src/api/jiraClient.ts b/src/api/jiraClient.ts index c091dba..e2a6866 100644 --- a/src/api/jiraClient.ts +++ b/src/api/jiraClient.ts @@ -63,6 +63,7 @@ export async function fetchJqlApproximateCount( export async function fetchAllIssuesByJql( jql: string, signal?: AbortSignal, + options?: { additionalFields?: string[] }, ): Promise { const base = clientBaseUrl() if (!base) { @@ -74,7 +75,7 @@ export async function fetchAllIssuesByJql( const maxResults = pageSize() const storyPointsField = import.meta.env.VITE_JIRA_STORY_POINTS_FIELD?.trim() || 'customfield_10028' - const fields = [ + const baseFields = [ 'summary', 'status', 'issuetype', @@ -86,7 +87,9 @@ export async function fetchAllIssuesByJql( 'timetracking', 'labels', storyPointsField, - ] as const + ] + const extra = (options?.additionalFields ?? []).map((f) => f.trim()).filter(Boolean) + const fields = [...new Set([...baseFields, ...extra])] const collected: JiraIssue[] = [] let nextPageToken: string | undefined @@ -96,7 +99,7 @@ export async function fetchAllIssuesByJql( for (let page = 0; page < MAX_PAGES; page += 1) { const body: Record = { jql, - fields: [...fields], + fields, maxResults, ...(nextPageToken ? { nextPageToken } : {}), } diff --git a/src/components/BoardView.tsx b/src/components/BoardView.tsx index 56c0cae..c3b61be 100644 --- a/src/components/BoardView.tsx +++ b/src/components/BoardView.tsx @@ -4,9 +4,10 @@ import { StoryCard } from './StoryCard' type Props = { groups: StoryGroup[] + sprintFieldId?: string | null } -export function BoardView({ groups }: Props) { +export function BoardView({ groups, sprintFieldId = null }: Props) { const columns = groupStoriesByComponent(groups) return ( @@ -24,7 +25,7 @@ export function BoardView({ groups }: Props) {
{col.map((g) => ( - + ))}
diff --git a/src/components/DashboardSettingsModal.tsx b/src/components/DashboardSettingsModal.tsx index b9f678a..8966b88 100644 --- a/src/components/DashboardSettingsModal.tsx +++ b/src/components/DashboardSettingsModal.tsx @@ -5,8 +5,10 @@ import { type DashboardConfig, type LaneLabelsConfig, type Milestone, + type MilestoneKind, type StatusBucketConfig, } from '../lib/dashboardConfig' +import { MILESTONE_KIND_OPTIONS } from '../lib/milestoneKinds' function parseBucketLines(raw: string): string[] { return raw @@ -68,6 +70,8 @@ function newMilestone(): Milestone { date: new Date().toISOString().slice(0, 10), linkedStoryKeys: [], critical: false, + kind: 'generic', + expectedActions: undefined, } } @@ -199,6 +203,26 @@ export function DashboardSettingsModal({ open, config, onClose, onSave }: Props) /> +
+ + + setDraft((d) => ({ + ...d, + sprintFieldId: e.target.value.trim() || undefined, + })) + } + placeholder="ex. customfield_10020 (vide = utiliser VITE_JIRA_SPRINT_FIELD ou désactiver)" + /> +

+ Laissez vide pour n’utiliser que la variable d’environnement, ou saisissez l’ID exact du + champ Sprint de votre projet Scrum. +

+

La date d’atterrissage utilise : vélocité × (effectif / baseline). WIP = créneaux nominaux pour la jauge « Ressources ». @@ -323,6 +347,37 @@ export function DashboardSettingsModal({ open, config, onClose, onSave }: Props) onChange={(e) => updateMilestone(m.id, { title: e.target.value })} placeholder="ex. Fin design" /> +

+ + +
+
+ +