init
This commit is contained in:
@ -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 l’une 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 d’atterrissage 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 n’est 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, l’ancienne détection par mots dans le résumé et le statut s’applique. 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 d’impact 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"
|
||||
|
||||
Reference in New Issue
Block a user