feat: event category selector in detail panel

- Always-visible 'Catégorie IA' block in EventDetail (view + edit mode)
- Dropdown lists all EventCategories; selecting one instantly saves sub_type
  and updates category type via PUT (no need to enter full edit mode)
- Shows matched category description + default impacts preview (ticker chips
  with direction arrows and sensitivity %)
- Edit mode: category dropdown also auto-sets the category type field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-25 21:32:02 +02:00
parent f71e3be3ff
commit 984e6860ad

View File

@@ -308,6 +308,8 @@ function EventDetail({
const [editing, setEditing] = useState(false)
const [editData, setEditData] = useState<Partial<MarketEvent>>({})
const [saving, setSaving] = useState(false)
const [allCats, setAllCats] = useState<EventCategory[]>([])
const [catSaving, setCatSaving] = useState(false)
const loadDetail = useCallback(async () => {
const r = await fetch(`/api/market-events/${event.id}`)
@@ -318,6 +320,34 @@ function EventDetail({
}, [event.id])
useEffect(() => { loadDetail() }, [loadDetail])
useEffect(() => {
fetch('/api/impact/categories')
.then(r => r.json())
.then(d => setAllCats(Array.isArray(d) ? d : []))
.catch(() => {})
}, [])
// Quick-save sub_type without entering full edit mode
const assignCategory = async (catName: string) => {
const cat = allCats.find(c => c.name === catName)
setCatSaving(true)
const payload = {
...detail,
sub_type: catName,
category: cat?.type ?? detail.category,
source_refs: detail.source_refs ?? [],
}
const r = await fetch(`/api/market-events/${event.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
setCatSaving(false)
if (r.ok) {
setDetail(d => ({ ...d, sub_type: catName, category: cat?.type ?? d.category }))
onUpdated({ ...detail, sub_type: catName, category: cat?.type ?? detail.category })
}
}
const evaluate = async (force = false) => {
setEval(true)
@@ -377,7 +407,6 @@ function EventDetail({
)}
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
<CatBadge cat={detail.category} />
{detail.sub_type && <span className="text-xs text-slate-500">{detail.sub_type}</span>}
<span className={clsx('text-xs font-semibold', LEVEL_COLORS[detail.level])}>{detail.level}</span>
<span className="text-xs text-slate-500">{detail.start_date}</span>
{detail.end_date && <span className="text-xs text-slate-600"> {detail.end_date}</span>}
@@ -419,6 +448,77 @@ function EventDetail({
)}
</div>
{/* ── Catégorie IA ─────────────────────────────────────────────── */}
{(() => {
const currentSub = editing ? (editData.sub_type ?? detail.sub_type ?? '') : (detail.sub_type ?? '')
const matchedCat = allCats.find(c => c.name === currentSub)
const nDefaults = matchedCat?.default_impacts?.length ?? 0
return (
<div className="bg-slate-800/40 rounded-lg p-3 border border-slate-700/30">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-slate-500 uppercase tracking-wide">Catégorie IA</span>
{matchedCat && (
<span className="text-xs text-slate-600">
{nDefaults} impact{nDefaults !== 1 ? 's' : ''} par défaut
</span>
)}
</div>
{editing ? (
<select
value={editData.sub_type ?? detail.sub_type ?? ''}
onChange={e => {
const cat = allCats.find(c => c.name === e.target.value)
setEditData(d => ({
...d,
sub_type: e.target.value,
...(cat ? { category: cat.type } : {}),
}))
}}
className="w-full bg-dark-900 border border-slate-700 rounded px-2 py-1.5 text-slate-200 text-sm"
>
<option value=""> aucune catégorie </option>
{allCats.map(c => (
<option key={c.name} value={c.name}>{c.name} ({c.type})</option>
))}
</select>
) : (
<div className="flex items-center gap-2">
<select
value={currentSub}
onChange={e => assignCategory(e.target.value)}
disabled={catSaving}
className="flex-1 bg-dark-900 border border-slate-700 rounded px-2 py-1.5 text-slate-200 text-sm"
>
<option value=""> aucune catégorie </option>
{allCats.map(c => (
<option key={c.name} value={c.name}>{c.name} ({c.type})</option>
))}
</select>
{catSaving && <Loader2 size={13} className="animate-spin text-slate-500 shrink-0" />}
</div>
)}
{matchedCat?.description && (
<div className="text-xs text-slate-600 mt-1.5 italic">{matchedCat.description}</div>
)}
{matchedCat && nDefaults > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{matchedCat.default_impacts.slice(0, 8).map((di: DefaultImpact) => (
<span key={di.instrument_id}
className={clsx('text-xs px-1.5 py-0.5 rounded border font-mono',
di.typical_direction === 'bullish' ? 'text-emerald-400 border-emerald-800/40 bg-emerald-900/20' :
di.typical_direction === 'bearish' ? 'text-red-400 border-red-800/40 bg-red-900/20' :
'text-slate-400 border-slate-700 bg-slate-800/40')}>
{di.typical_direction === 'bullish' ? '↑' : di.typical_direction === 'bearish' ? '↓' : ''} {di.instrument_id}
<span className="text-slate-600 ml-1">{(di.sensitivity * 100).toFixed(0)}%</span>
</span>
))}
</div>
)}
</div>
)
})()}
{/* Description */}
{editing ? (
<textarea
@@ -432,9 +532,9 @@ function EventDetail({
) : null}
{editing ? (
<div className="grid grid-cols-3 gap-2">
<div className="grid grid-cols-2 gap-2">
<div>
<div className="text-xs text-slate-500 mb-1">Catégorie</div>
<div className="text-xs text-slate-500 mb-1">Type</div>
<select defaultValue={detail.category}
onChange={e => setEditData(d => ({ ...d, category: e.target.value }))}
className="w-full bg-dark-900 border border-slate-700 rounded px-2 py-1 text-slate-200 text-sm">