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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user