gantt
This commit is contained in:
@ -1,13 +1,20 @@
|
||||
import { useEffect, useId, useRef, useState, type ChangeEvent } from 'react'
|
||||
import {
|
||||
exportConfigJson,
|
||||
exportSynologyBackupJson,
|
||||
GANTT_SPRINT_METRIC_OPTIONS,
|
||||
mergeImportedConfig,
|
||||
normalizeFunctionalGapsForSave,
|
||||
sanitizeExcludedSprintIds,
|
||||
type DashboardConfig,
|
||||
type FunctionalGapBadge,
|
||||
type GanttSprintRowMetric,
|
||||
type LaneLabelsConfig,
|
||||
type Milestone,
|
||||
type MilestoneKind,
|
||||
type StatusBucketConfig,
|
||||
} from '../lib/dashboardConfig'
|
||||
import type { JiraSprintSnapshot } from '../lib/sprintExtract'
|
||||
import { MILESTONE_KIND_OPTIONS } from '../lib/milestoneKinds'
|
||||
|
||||
function parseBucketLines(raw: string): string[] {
|
||||
@ -61,6 +68,8 @@ type Props = {
|
||||
config: DashboardConfig
|
||||
onClose: () => void
|
||||
onSave: (next: DashboardConfig) => void
|
||||
/** Sprints board (API) pour masquage sélectif ; optionnel si pas encore chargés. */
|
||||
boardSprints?: JiraSprintSnapshot[]
|
||||
}
|
||||
|
||||
function newMilestone(): Milestone {
|
||||
@ -75,7 +84,26 @@ function newMilestone(): Milestone {
|
||||
}
|
||||
}
|
||||
|
||||
export function DashboardSettingsModal({ open, config, onClose, onSave }: Props) {
|
||||
function newGapBadge(): FunctionalGapBadge {
|
||||
return {
|
||||
id:
|
||||
typeof crypto !== 'undefined' && crypto.randomUUID
|
||||
? `gap-${crypto.randomUUID().slice(0, 10)}`
|
||||
: `gap-${Date.now()}`,
|
||||
label: '',
|
||||
terms: [''],
|
||||
criticalFlow: false,
|
||||
}
|
||||
}
|
||||
|
||||
function parseGapTerms(raw: string): string[] {
|
||||
return raw
|
||||
.split(/[,;]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export function DashboardSettingsModal({ open, config, onClose, onSave, boardSprints }: Props) {
|
||||
const dialogRef = useRef<HTMLDialogElement>(null)
|
||||
const fileRef = useRef<HTMLInputElement>(null)
|
||||
const titleId = useId()
|
||||
@ -119,7 +147,7 @@ export function DashboardSettingsModal({ open, config, onClose, onSave }: Props)
|
||||
const parsed = JSON.parse(String(reader.result)) as unknown
|
||||
const merged = mergeImportedConfig(draft, parsed)
|
||||
if (merged) setDraft(merged)
|
||||
else alert('Fichier JSON invalide (version 1 attendue).')
|
||||
else alert('Fichier JSON invalide (configuration v1 ou bundle Synology v1).')
|
||||
} catch {
|
||||
alert('Impossible de lire ce fichier JSON.')
|
||||
}
|
||||
@ -322,6 +350,159 @@ export function DashboardSettingsModal({ open, config, onClose, onSave }: Props)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-indigo-500/25 bg-indigo-500/5 p-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-indigo-200/90">
|
||||
Gantt & vue Sprint
|
||||
</p>
|
||||
<label className="mt-2 block text-[10px] font-semibold uppercase tracking-wide text-slate-500">
|
||||
Infos sous les barres (Gantt)
|
||||
</label>
|
||||
<select
|
||||
className="mt-1 w-full rounded-lg border border-white/10 bg-black/30 px-2 py-2 text-sm text-slate-200 outline-none"
|
||||
value={draft.ganttSprintRowMetric}
|
||||
onChange={(e) =>
|
||||
setDraft((d) => ({
|
||||
...d,
|
||||
ganttSprintRowMetric: e.target.value as GanttSprintRowMetric,
|
||||
}))
|
||||
}
|
||||
>
|
||||
{GANTT_SPRINT_METRIC_OPTIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-3 text-[10px] font-semibold uppercase tracking-wide text-indigo-200/80">
|
||||
Sprints à masquer
|
||||
</p>
|
||||
<p className="mt-1 text-[10px] leading-relaxed text-slate-500">
|
||||
Sprints cochés : retirés du Gantt et du menu de la vue Sprint. Rechargez les données si la
|
||||
liste est vide.
|
||||
</p>
|
||||
{boardSprints && boardSprints.length > 0 ? (
|
||||
<ul className="mt-2 max-h-44 space-y-2 overflow-y-auto pr-1">
|
||||
{boardSprints.map((sp) => (
|
||||
<li key={sp.id} className="flex items-start gap-2 text-xs text-slate-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`ex-sp-${sp.id}`}
|
||||
checked={draft.excludedSprintIds.includes(sp.id)}
|
||||
onChange={(e) =>
|
||||
setDraft((d) => {
|
||||
const next = new Set(d.excludedSprintIds)
|
||||
if (e.target.checked) next.add(sp.id)
|
||||
else next.delete(sp.id)
|
||||
return { ...d, excludedSprintIds: [...next] }
|
||||
})
|
||||
}
|
||||
className="mt-0.5 rounded border-indigo-400/50"
|
||||
/>
|
||||
<label htmlFor={`ex-sp-${sp.id}`} className="cursor-pointer leading-snug">
|
||||
<span className="font-medium">{sp.name}</span>
|
||||
<span className="ml-2 font-mono text-[10px] text-slate-500">#{sp.id}</span>
|
||||
{sp.state ? (
|
||||
<span className="ml-2 text-[10px] uppercase text-slate-500">{sp.state}</span>
|
||||
) : null}
|
||||
</label>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="mt-2 text-[11px] text-slate-500">
|
||||
Aucun sprint en mémoire : actualisez le cockpit puis rouvrez les réglages.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-rose-500/20 bg-rose-500/[0.06] p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-xs font-semibold uppercase tracking-wide text-rose-100/90">
|
||||
Badges « gaps » (PO)
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setDraft((d) => ({ ...d, functionalGaps: [...d.functionalGaps, newGapBadge()] }))
|
||||
}
|
||||
className="text-xs font-medium text-rose-200/90 hover:text-rose-100"
|
||||
>
|
||||
+ Ajouter
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] leading-relaxed text-slate-500">
|
||||
Termes cherchés dans clés, résumés et étiquettes (insensible casse / accents). Cochez
|
||||
« flux critique » pour renforcer le feu rouge macro (Panier, Checkout…).
|
||||
</p>
|
||||
<ul className="mt-3 max-h-48 space-y-3 overflow-y-auto pr-1">
|
||||
{draft.functionalGaps.map((g) => (
|
||||
<li
|
||||
key={g.id}
|
||||
className="rounded-lg border border-white/10 bg-black/25 p-2 text-xs text-slate-200"
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<input
|
||||
className="min-w-[100px] flex-1 rounded border border-white/10 bg-transparent px-2 py-1"
|
||||
value={g.label}
|
||||
onChange={(e) =>
|
||||
setDraft((d) => ({
|
||||
...d,
|
||||
functionalGaps: d.functionalGaps.map((x) =>
|
||||
x.id === g.id ? { ...x, label: e.target.value } : x,
|
||||
),
|
||||
}))
|
||||
}
|
||||
placeholder="Libellé (ex. Panier)"
|
||||
/>
|
||||
<label className="flex cursor-pointer items-center gap-1.5 text-[11px] text-rose-100/90">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(g.criticalFlow)}
|
||||
onChange={(e) =>
|
||||
setDraft((d) => ({
|
||||
...d,
|
||||
functionalGaps: d.functionalGaps.map((x) =>
|
||||
x.id === g.id ? { ...x, criticalFlow: e.target.checked } : x,
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="rounded border-rose-400/50"
|
||||
/>
|
||||
Flux critique
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setDraft((d) => ({
|
||||
...d,
|
||||
functionalGaps: d.functionalGaps.filter((x) => x.id !== g.id),
|
||||
}))
|
||||
}
|
||||
className="text-[11px] text-rose-400 hover:text-rose-300"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
rows={2}
|
||||
spellCheck={false}
|
||||
className="mt-2 w-full resize-y rounded border border-white/10 bg-black/30 px-2 py-1 font-mono text-[11px] outline-none"
|
||||
value={g.terms.join(', ')}
|
||||
onChange={(e) =>
|
||||
setDraft((d) => ({
|
||||
...d,
|
||||
functionalGaps: d.functionalGaps.map((x) =>
|
||||
x.id === g.id ? { ...x, terms: parseGapTerms(e.target.value) } : x,
|
||||
),
|
||||
}))
|
||||
}
|
||||
placeholder="panier, cart, basket…"
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
||||
@ -428,6 +609,14 @@ export function DashboardSettingsModal({ open, config, onClose, onSave }: Props)
|
||||
>
|
||||
Exporter configuration (JSON)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => exportSynologyBackupJson(draft)}
|
||||
className="rounded-lg border border-emerald-400/35 bg-emerald-950/40 px-3 py-2 text-xs font-semibold text-emerald-50"
|
||||
title="Enveloppe bundleVersion + exportedAt pour sauvegarde NAS / Docker Synology."
|
||||
>
|
||||
Bundle Synology (JSON)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileRef.current?.click()}
|
||||
@ -456,7 +645,11 @@ export function DashboardSettingsModal({ open, config, onClose, onSave }: Props)
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSave(draft)
|
||||
onSave({
|
||||
...draft,
|
||||
functionalGaps: normalizeFunctionalGapsForSave(draft.functionalGaps),
|
||||
excludedSprintIds: sanitizeExcludedSprintIds(draft.excludedSprintIds),
|
||||
})
|
||||
onClose()
|
||||
}}
|
||||
className="rounded-lg bg-cyan-500 px-4 py-2 text-sm font-semibold text-slate-950 hover:bg-cyan-400"
|
||||
|
||||
Reference in New Issue
Block a user