This commit is contained in:
Bastien COIGNOUX
2026-04-24 11:50:39 +02:00
parent 745c8ae133
commit ca4c64bbb0
28 changed files with 2269 additions and 116 deletions

View File

@ -3,9 +3,57 @@ import {
exportConfigJson,
mergeImportedConfig,
type DashboardConfig,
type LaneLabelsConfig,
type Milestone,
type StatusBucketConfig,
} from '../lib/dashboardConfig'
function parseBucketLines(raw: string): string[] {
return raw
.split(/[\n,;]+/)
.map((s) => s.trim())
.filter(Boolean)
}
const BUCKET_FIELD_DEFS: {
key: keyof StatusBucketConfig
title: string
hint: string
}[] = [
{ key: 'todo', title: 'À faire', hint: 'Ex. À faire, Open, Backlog' },
{ key: 'in_progress', title: 'En cours', hint: 'Ex. In Progress, Code Review, Recette' },
{ key: 'blocked', title: 'Bloqué', hint: 'Ex. Bloqué, Blocked' },
{ key: 'done', title: 'Terminé', hint: 'Ex. Done, Terminé, Closed, Livré' },
{ key: 'cancel', title: 'Annulé', hint: 'Ex. Annulé, Cancelled, Won\'t fix' },
]
const LANE_LABEL_FIELD_DEFS: {
key: keyof LaneLabelsConfig
title: string
hint: string
}[] = [
{
key: 'analyse',
title: 'Piste Analyse',
hint: 'Étiquettes Jira comptées comme Analyse (ex. analyse). Une par ligne ou virgules.',
},
{
key: 'design',
title: 'Piste Design',
hint: 'Ex. design, maquette (si vous utilisez ces étiquettes en Jira).',
},
{
key: 'integration',
title: 'Piste Intégration',
hint: 'Ex. integration — ou tout libellé détiquette utilisé côté dev / recette.',
},
{
key: 'blocked',
title: 'Marquage bloqué (étiquettes)',
hint: 'Ex. blocked : les tickets portant lune de ces étiquettes sont signalés comme bloqués par étiquette (liste et tooltips).',
},
]
type Props = {
open: boolean
config: DashboardConfig
@ -19,6 +67,7 @@ function newMilestone(): Milestone {
title: '',
date: new Date().toISOString().slice(0, 10),
linkedStoryKeys: [],
critical: false,
}
}
@ -96,6 +145,66 @@ export function DashboardSettingsModal({ open, config, onClose, onSave }: Props)
</div>
<div className="space-y-5 px-5 py-4">
<div className="rounded-xl border border-amber-500/25 bg-amber-500/5 p-3">
<p className="text-xs font-semibold uppercase tracking-wide text-amber-200/90">
Capacité & reporting
</p>
<div className="mt-3 grid gap-3 sm:grid-cols-3">
<div>
<label className="text-[10px] uppercase text-slate-500">Effectif actif</label>
<input
type="number"
min={0.25}
step={0.25}
className="mt-1 w-full rounded-lg border border-white/10 bg-black/30 px-2 py-1.5 text-sm outline-none"
value={draft.teamCapacity}
onChange={(e) =>
setDraft((d) => ({
...d,
teamCapacity: Math.max(0.25, Number(e.target.value) || 0.25),
}))
}
/>
</div>
<div>
<label className="text-[10px] uppercase text-slate-500">Baseline vélocité</label>
<input
type="number"
min={0.25}
step={0.25}
className="mt-1 w-full rounded-lg border border-white/10 bg-black/30 px-2 py-1.5 text-sm outline-none"
value={draft.baselineCapacity}
onChange={(e) =>
setDraft((d) => ({
...d,
baselineCapacity: Math.max(0.25, Number(e.target.value) || 0.25),
}))
}
/>
</div>
<div>
<label className="text-[10px] uppercase text-slate-500">WIP / pers.</label>
<input
type="number"
min={1}
step={1}
className="mt-1 w-full rounded-lg border border-white/10 bg-black/30 px-2 py-1.5 text-sm outline-none"
value={draft.wipSlotsPerDev}
onChange={(e) =>
setDraft((d) => ({
...d,
wipSlotsPerDev: Math.max(1, Math.floor(Number(e.target.value) || 1)),
}))
}
/>
</div>
</div>
<p className="mt-2 text-[10px] text-slate-500">
La date datterrissage utilise : vélocité × (effectif / baseline). WIP = créneaux
nominaux pour la jauge « Ressources ».
</p>
</div>
<div>
<label className="block text-xs font-medium uppercase tracking-wide text-slate-500">
Mon accountId Jira (recommandé pour « Ma vue »)
@ -120,6 +229,75 @@ export function DashboardSettingsModal({ open, config, onClose, onSave }: Props)
/>
</div>
<div className="rounded-xl border border-violet-500/25 bg-violet-500/5 p-3">
<p className="text-xs font-semibold uppercase tracking-wide text-violet-200/90">
Cartographie des statuts Jira
</p>
<p className="mt-1 text-[10px] leading-relaxed text-slate-500">
Libellés exacts des statuts dans Jira (un par ligne ou séparés par des virgules). La
comparaison ignore casse et accents. Si un statut nest dans aucune liste, la catégorie
Jira (nouveau, en cours, terminé) sert de secours. Les listes vides au prochain
chargement reprennent les valeurs par défaut.
</p>
<div className="mt-3 space-y-3">
{BUCKET_FIELD_DEFS.map(({ key, title, hint }) => (
<div key={key}>
<label className="text-[10px] font-medium uppercase tracking-wide text-slate-400">
{title}
</label>
<p className="text-[10px] text-slate-600">{hint}</p>
<textarea
rows={3}
spellCheck={false}
className="mt-1 w-full resize-y rounded-lg border border-white/10 bg-black/30 px-2 py-1.5 font-mono text-[11px] leading-relaxed text-slate-200 outline-none ring-cyan-500/20 focus:ring-1"
value={draft.statusBuckets[key].join('\n')}
onChange={(e) =>
setDraft((d) => ({
...d,
statusBuckets: { ...d.statusBuckets, [key]: parseBucketLines(e.target.value) },
}))
}
/>
</div>
))}
</div>
</div>
<div className="rounded-xl border border-cyan-500/25 bg-cyan-500/5 p-3">
<p className="text-xs font-semibold uppercase tracking-wide text-cyan-200/90">
Étiquettes Jira (pistes & bloqué)
</p>
<p className="mt-1 text-[10px] leading-relaxed text-slate-500">
Les sous-tâches sont classées en Analyse / Design / Intégration si elles portent au moins
une des étiquettes listées (ordre de priorité : Analyse, puis Design, puis Intégration).
Sinon, lancienne détection par mots dans le résumé et le statut sapplique. Les listes
vides au prochain chargement reprennent les valeurs par défaut (analyse, design,
integration, blocked).
</p>
<div className="mt-3 space-y-3">
{LANE_LABEL_FIELD_DEFS.map(({ key, title, hint }) => (
<div key={key}>
<label className="text-[10px] font-medium uppercase tracking-wide text-slate-400">
{title}
</label>
<p className="text-[10px] text-slate-600">{hint}</p>
<textarea
rows={2}
spellCheck={false}
className="mt-1 w-full resize-y rounded-lg border border-white/10 bg-black/30 px-2 py-1.5 font-mono text-[11px] leading-relaxed text-slate-200 outline-none ring-cyan-500/20 focus:ring-1"
value={draft.laneLabels[key].join('\n')}
onChange={(e) =>
setDraft((d) => ({
...d,
laneLabels: { ...d.laneLabels, [key]: parseBucketLines(e.target.value) },
}))
}
/>
</div>
))}
</div>
</div>
<div>
<div className="mb-2 flex items-center justify-between">
<span className="text-xs font-medium uppercase tracking-wide text-slate-500">
@ -145,7 +323,16 @@ export function DashboardSettingsModal({ open, config, onClose, onSave }: Props)
onChange={(e) => updateMilestone(m.id, { title: e.target.value })}
placeholder="ex. Fin design"
/>
<div className="flex flex-wrap gap-2">
<label className="mt-2 flex cursor-pointer items-center gap-2 text-[11px] text-amber-100/90">
<input
type="checkbox"
checked={Boolean(m.critical)}
onChange={(e) => updateMilestone(m.id, { critical: e.target.checked })}
className="rounded border-amber-400/50"
/>
Jalon critique (alerte dimpact si retard)
</label>
<div className="mt-2 flex flex-wrap gap-2">
<input
type="date"
className="rounded border border-white/10 bg-transparent px-2 py-1 text-xs"