sprint
This commit is contained in:
149
src/components/SprintView.tsx
Normal file
149
src/components/SprintView.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import type { StoryGroup } from '../types/jira'
|
||||
import { useStatusBuckets } from '../context/StatusBucketContext'
|
||||
import { collectSprintOptions, filterGroupsBySprint } from '../lib/sprintExtract'
|
||||
import { subtaskDoneRatioPercent } from '../lib/storyMetrics'
|
||||
import { StoryCard } from './StoryCard'
|
||||
|
||||
type Props = {
|
||||
groups: StoryGroup[]
|
||||
sprintFieldId: string | null
|
||||
onOpenSettings: () => void
|
||||
}
|
||||
|
||||
export function SprintView({ groups, sprintFieldId, onOpenSettings }: Props) {
|
||||
const cfg = useStatusBuckets()
|
||||
const options = useMemo(
|
||||
() => collectSprintOptions(groups, sprintFieldId),
|
||||
[groups, sprintFieldId],
|
||||
)
|
||||
const [focusId, setFocusId] = useState<number | 'all'>('all')
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!sprintFieldId || focusId === 'all') return groups
|
||||
return filterGroupsBySprint(groups, focusId, sprintFieldId)
|
||||
}, [groups, focusId, sprintFieldId])
|
||||
|
||||
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])
|
||||
|
||||
if (!sprintFieldId) {
|
||||
return (
|
||||
<section className="mb-10 rounded-2xl border border-amber-500/25 bg-amber-500/5 px-5 py-8 text-center backdrop-blur-xl sm:px-8">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-[0.18em] text-amber-200/90">
|
||||
Vue Sprint — configuration requise
|
||||
</h2>
|
||||
<p className="mx-auto mt-3 max-w-xl text-sm leading-relaxed text-amber-100/90">
|
||||
Indiquez l’identifiant du champ personnalisé Sprint Jira (souvent{' '}
|
||||
<code className="rounded bg-black/30 px-1 font-mono text-xs">customfield_10020</code>) dans
|
||||
les réglages ou via la variable{' '}
|
||||
<code className="rounded bg-black/30 px-1 font-mono text-xs">VITE_JIRA_SPRINT_FIELD</code> dans
|
||||
<code className="rounded bg-black/30 px-1 font-mono text-xs"> .env</code>, puis actualisez
|
||||
les données. L’ID se trouve dans Jira : Administration → Issues → Champs personnalisés →
|
||||
Sprint.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenSettings}
|
||||
className="mt-5 rounded-lg border border-cyan-500/50 bg-cyan-500/15 px-4 py-2 text-sm font-medium text-cyan-100"
|
||||
>
|
||||
Ouvrir les réglages
|
||||
</button>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
if (options.length === 0) {
|
||||
return (
|
||||
<section className="mb-10 rounded-2xl border border-white/[0.08] bg-slate-950/40 px-5 py-8 backdrop-blur-xl sm:px-8">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-[0.18em] text-slate-400">
|
||||
Vue Sprint
|
||||
</h2>
|
||||
<p className="mt-3 text-sm text-slate-400">
|
||||
Aucun sprint détecté sur les tickets chargés (story ou sous-tâches). Vérifiez que le
|
||||
champ configuré est bien le champ Sprint Scrum, et que vos tickets sont affectés à un
|
||||
sprint dans Jira.
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="mb-10 space-y-6">
|
||||
<div className="rounded-2xl border border-white/[0.08] bg-slate-950/40 px-4 py-4 backdrop-blur-xl sm:px-6">
|
||||
<div className="flex flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold uppercase tracking-[0.18em] text-slate-400">
|
||||
Vue Sprint
|
||||
</h2>
|
||||
<p className="mt-1 max-w-2xl text-xs text-slate-500">
|
||||
Choisissez un sprint pour filtrer les stories (une story reste visible si la story ou
|
||||
une sous-tâche est dans le sprint). Les pastilles sprint sur chaque carte proviennent
|
||||
des données Jira.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex min-w-[200px] flex-1 flex-col gap-1 sm:max-w-md">
|
||||
<label className="text-[10px] font-medium uppercase tracking-wide text-slate-500">
|
||||
Sprint ciblé
|
||||
</label>
|
||||
<select
|
||||
value={focusId === 'all' ? 'all' : String(focusId)}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value
|
||||
setFocusId(v === 'all' ? 'all' : Number(v))
|
||||
}}
|
||||
className="rounded-lg border border-white/10 bg-black/40 px-3 py-2 text-sm text-slate-200 outline-none ring-cyan-500/20 focus:ring-2"
|
||||
>
|
||||
<option value="all">Tous les sprints ({groups.length} stories)</option>
|
||||
{options.map((s) => (
|
||||
<option key={s.id} value={String(s.id)}>
|
||||
{s.name}
|
||||
{s.state ? ` (${s.state})` : ''} — {s.storyCount} stories
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-4 border-t border-white/[0.06] pt-4 text-sm text-slate-400">
|
||||
<span>
|
||||
Stories :{' '}
|
||||
<span className="font-mono font-semibold text-slate-200">{stats.stories}</span>
|
||||
</span>
|
||||
<span>
|
||||
Sous-tâches :{' '}
|
||||
<span className="font-mono font-semibold text-slate-200">{stats.subtasks}</span>
|
||||
</span>
|
||||
<span title="Moyenne des % sous-tâches terminées sur les stories qui ont des sous-tâches">
|
||||
Avancement moy. :{' '}
|
||||
<span className="font-mono font-semibold text-emerald-300">{stats.avgPct}%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<p className="rounded-2xl border border-white/10 bg-slate-950/35 px-6 py-8 text-center text-sm text-slate-400">
|
||||
Aucune story dans ce sprint pour le périmètre actuel (filtre « Ma vue » inclus).
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-5 lg:grid-cols-2">
|
||||
{filtered.map((g) => (
|
||||
<StoryCard key={g.story.key} group={g} sprintFieldId={sprintFieldId} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user