Files
OpenFin/frontend/src/pages/CalendarPage.tsx
OpenSquared d256b65d30 Initial commit — GeoOptions Intelligence Cockpit v2.0
Stack: FastAPI + React/TypeScript + SQLite + GPT-4o
Features: Radar géopolitique, Marchés, Régime Macro, Journal de Bord MTM,
Rapport IA, Super Contexte (base de raisonnement évolutive), Boucle feedback IA.
Deploy: Docker + docker-compose + nginx pour openfin.open-squared.tech

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:29:59 +02:00

200 lines
9.1 KiB
TypeScript

import { useCalendar, useGeoNews } from '../hooks/useApi'
import clsx from 'clsx'
import { Calendar, Clock, Globe, AlertTriangle } from 'lucide-react'
import type { EconomicEvent, AssetClass } from '../types'
import { format, parseISO, isAfter, isBefore, addDays } from 'date-fns'
import { fr } from 'date-fns/locale'
const ASSET_EMOJIS: Record<string, string> = {
energy: '⛽', metals: '🥇', agriculture: '🌾', equities: '📈',
indices: '📊', forex: '💱', rates: '🏦',
}
const COUNTRY_FLAGS: Record<string, string> = {
US: '🇺🇸', EU: '🇪🇺', CN: '🇨🇳', JP: '🇯🇵', GB: '🇬🇧',
DE: '🇩🇪', FR: '🇫🇷', Global: '🌍',
}
const IMPORTANCE_CONFIG: Record<string, { color: string; label: string; dots: number }> = {
high: { color: 'text-red-400 border-red-700/40', label: 'Majeur', dots: 3 },
medium: { color: 'text-yellow-400 border-yellow-700/40', label: 'Modéré', dots: 2 },
low: { color: 'text-slate-400 border-slate-700/40', label: 'Mineur', dots: 1 },
}
function EventCard({ ev }: { ev: EconomicEvent }) {
const cfg = IMPORTANCE_CONFIG[ev.importance]
const isPast = ev.actual !== undefined && ev.actual !== null
const today = new Date()
const evDate = parseISO(ev.date)
const isToday = format(evDate, 'yyyy-MM-dd') === format(today, 'yyyy-MM-dd')
const isSoon = !isToday && isAfter(evDate, today) && isBefore(evDate, addDays(today, 3))
return (
<div className={clsx('card hover:border-slate-600/50 transition-colors', {
'border-yellow-500/40 bg-yellow-900/5': isToday,
'border-blue-500/30': isSoon && !isToday,
'opacity-60': isPast,
})}>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-base">{COUNTRY_FLAGS[ev.country] ?? '🌍'}</span>
<span className={clsx('text-xs font-bold uppercase tracking-wider', cfg.color.split(' ')[0])}>
{'●'.repeat(cfg.dots)}
</span>
{isToday && <span className="badge badge-yellow text-xs">AUJOURD'HUI</span>}
{isSoon && !isToday && <span className="badge badge-blue text-xs">BIENTÔT</span>}
</div>
<div className="text-sm text-white font-semibold">{ev.title}</div>
<div className="text-xs text-slate-500 mt-0.5">
{format(evDate, "EEEE d MMM yyyy", { locale: fr })} · {ev.country}
</div>
</div>
<div className="text-right shrink-0">
<div className={clsx('text-xs font-semibold', cfg.color.split(' ')[0])}>{cfg.label}</div>
{ev.previous && <div className="text-xs text-slate-600 mt-0.5">Préc: {ev.previous}</div>}
{ev.forecast && <div className="text-xs text-slate-500">Prév: {ev.forecast}</div>}
{ev.actual && <div className="text-xs text-emerald-400 font-bold">Réel: {ev.actual}</div>}
</div>
</div>
{ev.asset_impact && ev.asset_impact.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{ev.asset_impact.map(cls => (
<span key={cls} className="text-xs bg-dark-700 text-slate-400 px-1.5 py-0.5 rounded border border-slate-700/40">
{ASSET_EMOJIS[cls] ?? ''} {cls}
</span>
))}
</div>
)}
</div>
)
}
export default function CalendarPage() {
const { data: calendar, isLoading } = useCalendar()
const { data: news } = useGeoNews()
const today = new Date()
const upcoming = calendar?.filter(ev => isAfter(parseISO(ev.date), today)) ?? []
const past = calendar?.filter(ev => isBefore(parseISO(ev.date), today)) ?? []
const highImpactNews = news?.filter(n => n.impact_score > 0.4).slice(0, 5) ?? []
return (
<div className="p-6 space-y-5">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<Calendar className="w-5 h-5 text-blue-400" /> Calendrier Économique & Géopolitique
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Événements macro, catalyseurs géopolitiques, dates clés
</p>
</div>
{/* Legend */}
<div className="flex items-center gap-4 text-xs text-slate-500">
<div className="flex items-center gap-1"><span className="text-red-400">●●●</span> Majeur (forte volatilité attendue)</div>
<div className="flex items-center gap-1"><span className="text-yellow-400">●●</span> Modéré</div>
<div className="flex items-center gap-1"><span className="text-slate-400">●</span> Mineur</div>
</div>
<div className="grid grid-cols-3 gap-5">
{/* Economic calendar */}
<div className="col-span-2 space-y-3">
<div className="section-title flex items-center gap-1">
<Clock className="w-3 h-3" /> Événements à venir ({upcoming.length})
</div>
{isLoading ? (
[1,2,3].map(i => <div key={i} className="card animate-pulse h-20 bg-dark-700"></div>)
) : upcoming.length > 0 ? (
upcoming.map((ev, i) => <EventCard key={i} ev={ev} />)
) : (
<div className="card text-center py-8 text-slate-500 text-sm">
Démarrer le backend pour charger le calendrier
</div>
)}
{past.length > 0 && (
<>
<div className="section-title mt-6 flex items-center gap-1 opacity-60">
Événements passés ({past.length})
</div>
{past.map((ev, i) => <EventCard key={i} ev={ev} />)}
</>
)}
</div>
{/* Right: geo alerts + timeline */}
<div className="col-span-1 space-y-4">
<div className="card">
<div className="section-title flex items-center gap-1">
<AlertTriangle className="w-3 h-3 text-orange-400" /> Alertes géopolitiques
</div>
{highImpactNews.length > 0 ? (
<div className="space-y-2">
{highImpactNews.map((n, i) => (
<div key={i} className="card-sm">
<div className="flex items-start justify-between gap-1">
<div className="text-xs text-white line-clamp-2">{n.title}</div>
<span className="text-xs text-orange-400 font-bold shrink-0 ml-1">
{Math.round(n.impact_score * 100)}
</span>
</div>
<div className="text-xs text-slate-600 mt-1">{n.source}</div>
</div>
))}
</div>
) : (
<div className="text-xs text-slate-600 text-center py-4">Charger les actualités géopolitiques</div>
)}
</div>
{/* Trade opportunity windows */}
<div className="card">
<div className="section-title">Fenêtres d'opportunité</div>
<div className="space-y-2 text-xs">
{[
{ window: 'Pré-FOMC (-3j)', strategy: 'Straddle sur SPY', rationale: 'IV monte avant décision' },
{ window: 'Pré-NFP (-2j)', strategy: 'Straddle sur indices', rationale: 'Directional uncertainty' },
{ window: 'Post-OPEC', strategy: 'Bull Call Spread USO', rationale: 'Cut → oil spike probable' },
{ window: 'Pré-USDA Crop', strategy: 'Long Call WEAT', rationale: 'Supply news catalyst' },
{ window: 'Élections US approche', strategy: 'Long VIX Call', rationale: 'Vol expansion garantie' },
].map((op, i) => (
<div key={i} className="card-sm">
<div className="font-semibold text-blue-400">{op.window}</div>
<div className="text-white mt-0.5">{op.strategy}</div>
<div className="text-slate-500">{op.rationale}</div>
</div>
))}
</div>
</div>
{/* Geo-event impact guide */}
<div className="card">
<div className="section-title flex items-center gap-1">
<Globe className="w-3 h-3" /> Guide d'impact
</div>
<div className="space-y-1.5 text-xs">
{[
{ event: 'Conflit Moyen-Orient', impact: 'Oil +10-20%', cls: 'energy' },
{ event: 'Sanctions Russie', impact: 'Gaz +15-40%', cls: 'energy' },
{ event: 'Tarifs US-Chine', impact: 'Soja -8%', cls: 'agriculture' },
{ event: 'Crise sanitaire', impact: 'Or +7-12%', cls: 'metals' },
{ event: 'Hausses Fed', impact: 'USD +2-4%', cls: 'forex' },
{ event: 'Guerre Ukraine', impact: 'Blé +15-50%', cls: 'agriculture' },
].map((g, i) => (
<div key={i} className="flex justify-between items-center py-1 border-b border-slate-700/20 last:border-0">
<span className="text-slate-400">{g.event}</span>
<span className={clsx('font-bold', g.impact.includes('+') ? 'positive' : 'negative')}>
{g.impact}
</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
)
}