import { useState } from 'react' import { Link } from 'react-router-dom' import { useInstitutionalReports, useInstitutionalStats, useRefreshInstitutionalReports } from '../hooks/useApi' import { useQueryClient } from '@tanstack/react-query' import { RefreshCw, TrendingUp, TrendingDown, Minus, AlertTriangle, Star, CheckCircle, Clock, Users, ExternalLink } from 'lucide-react' import clsx from 'clsx' import { format } from 'date-fns' interface InstitutionalReport { id: number report_type: string report_date: string fetch_date: string title: string source: string importance: number category: string key_points: string[] trading_implications: string signal_energy: string signal_metals: string signal_indices: string signal_forex: string ai_summary: string absorbed_score: number | null injected_in_cycle_id: string | null } const SIGNAL_CONFIG: Record = { bullish: { label: 'Bullish', color: 'text-emerald-400 bg-emerald-900/30 border-emerald-700/40', icon: }, bearish: { label: 'Bearish', color: 'text-red-400 bg-red-900/30 border-red-700/40', icon: }, neutral: { label: 'Neutral', color: 'text-slate-400 bg-slate-800/50 border-slate-700/40', icon: }, } const ASSET_CLASSES = ['energy', 'metals', 'indices', 'forex'] as const function SignalBadge({ signal, label }: { signal: string; label: string }) { const cfg = SIGNAL_CONFIG[signal] ?? SIGNAL_CONFIG.neutral return ( {cfg.icon} {label}: {cfg.label} ) } function ImportanceStars({ n }: { n: number }) { return ( {[1, 2, 3].map(i => ( ))} ) } function AbsorptionBadge({ score }: { score: number | null }) { if (score === null || score === undefined) { return ( Not absorbed ) } const pct = Math.round(score * 100) const isAbsorbed = pct >= 30 return ( {isAbsorbed ? `Absorbed ${pct}%` : `Low ${pct}%`} ) } const TYPE_COLORS: Record = { cot: 'bg-purple-900/30 text-purple-300 border-purple-700/40', eia: 'bg-orange-900/30 text-orange-300 border-orange-700/40', earnings: 'bg-blue-900/30 text-blue-300 border-blue-700/40', vx_curve: 'bg-rose-900/30 text-rose-300 border-rose-700/40', central_bank: 'bg-cyan-900/30 text-cyan-300 border-cyan-700/40', sentiment: 'bg-yellow-900/30 text-yellow-300 border-yellow-700/40', } const TYPE_LABELS: Record = { cot: 'COT', eia: 'EIA', earnings: 'Earnings', vx_curve: 'VX Curve', central_bank: 'Central Banks', sentiment: 'Sentiment', } function ReportCard({ report }: { report: InstitutionalReport }) { const [expanded, setExpanded] = useState(false) const typeColor = TYPE_COLORS[report.report_type] ?? 'bg-slate-800 text-slate-400 border-slate-700/40' return (
{/* Header */}
{TYPE_LABELS[report.report_type] ?? report.report_type} {report.report_date}
{report.title}
{report.source}
{/* Signal badges row */}
{/* Key points preview (always visible) */} {report.key_points?.length > 0 && (
    {(expanded ? report.key_points : report.key_points.slice(0, 3)).map((kp, i) => { const isExtreme = kp.includes('EXTREME') return (
  • {isExtreme ? : } {kp}
  • ) })} {!expanded && report.key_points.length > 3 && (
  • +{report.key_points.length - 3} more…
  • )}
)} {/* Expanded: trading implications + AI summary */} {expanded && (
{report.trading_implications && (
Trading Implications
{report.trading_implications.split(' | ').map((impl, i) => (

{impl}

))}
)} {report.ai_summary && (
AI Summary

{report.ai_summary}

)} {report.injected_in_cycle_id && (
Injected in cycle: {report.injected_in_cycle_id.slice(0, 16)}
)}
)}
) } export default function InstitutionalReports() { const qc = useQueryClient() const [filterType, setFilterType] = useState('') const [filterCategory, setFilterCategory] = useState('') const [filterImportance, setFilterImportance] = useState(0) const [filterDays, setFilterDays] = useState(90) const params = { ...(filterType ? { report_type: filterType } : {}), ...(filterCategory ? { category: filterCategory } : {}), ...(filterImportance ? { importance: filterImportance } : {}), days: filterDays, } const { data: reports = [], isLoading, isFetching } = useInstitutionalReports(params) const { data: stats } = useInstitutionalStats() const refresh = useRefreshInstitutionalReports() const handleRefresh = (type?: string) => { refresh.mutate(type, { onSuccess: () => { qc.invalidateQueries({ queryKey: ['institutional-reports'] }) qc.invalidateQueries({ queryKey: ['institutional-stats'] }) }, }) } return (
{/* Header */}

Institutional Reports

COT · EIA · Earnings · VX Curve · Central Banks · Sentiment — scheduled daily/weekly

{([ { type: 'cot', label: 'COT', cls: 'bg-purple-900/40 text-purple-300 border-purple-700/40 hover:bg-purple-800/50' }, { type: 'eia', label: 'EIA', cls: 'bg-orange-900/40 text-orange-300 border-orange-700/40 hover:bg-orange-800/50' }, { type: 'earnings', label: 'Earn.', cls: 'bg-blue-900/40 text-blue-300 border-blue-700/40 hover:bg-blue-800/50' }, { type: 'vx_curve', label: 'VX', cls: 'bg-rose-900/40 text-rose-300 border-rose-700/40 hover:bg-rose-800/50' }, { type: 'central_bank', label: 'CB RSS', cls: 'bg-cyan-900/40 text-cyan-300 border-cyan-700/40 hover:bg-cyan-800/50' }, { type: 'sentiment', label: 'Senti.', cls: 'bg-yellow-900/40 text-yellow-300 border-yellow-700/40 hover:bg-yellow-800/50' }, ] as const).map(({ type, label, cls }) => ( ))}
{/* Stats bar */} {stats && (
{[ { label: 'COT', value: stats.by_type?.cot ?? 0, cls: 'text-purple-400' }, { label: 'EIA', value: stats.by_type?.eia ?? 0, cls: 'text-orange-400' }, { label: 'Earnings', value: stats.by_type?.earnings ?? 0, cls: 'text-blue-400' }, { label: 'VX Curve', value: stats.by_type?.vx_curve ?? 0, cls: 'text-rose-400' }, { label: 'Central Bank', value: stats.by_type?.central_bank ?? 0, cls: 'text-cyan-400' }, { label: 'Sentiment', value: stats.by_type?.sentiment ?? 0, cls: 'text-yellow-400' }, ].map(({ label, value, cls }) => (
{value}
{label}
))}
)} {/* Latest dates */} {stats && (
{stats.latest_cot && COT: {stats.latest_cot}} {stats.latest_eia && EIA: {stats.latest_eia}} {stats.by_type?.earnings ? Earnings: {Object.keys(stats.by_type).includes('earnings') ? 'tracked' : ''} : null} Total: {stats.total} · Absorbed: {stats.absorbed_count}
)} {/* Specialist Desks link */}
Ces rapports sont analysés automatiquement (fetch + scoring IA). Pour gérer le calendrier des rapports par desk — ajouter un rapport custom (ex. broyage cacao), planifier les prochaines dates, lier à un desk specialist — Specialist Desks
{/* Filters */}
Type:
{(['', 'cot', 'eia', 'earnings', 'vx_curve', 'central_bank', 'sentiment'] as const).map(t => ( ))}
Category:
{([ { v: '', label: 'All' }, { v: 'energy', label: 'Energy' }, { v: 'metals', label: 'Metals' }, { v: 'agri', label: 'Agri' }, { v: 'equities', label: 'Equities' }, { v: 'forex', label: 'Forex' }, { v: 'crypto', label: 'Crypto' }, { v: 'bonds', label: 'Bonds' }, { v: 'volatility',label: 'Volatility'}, { v: 'macro', label: 'Macro' }, { v: 'sentiment', label: 'Sentiment'}, { v: 'multi', label: 'Multi' }, ]).map(({ v, label }) => ( ))}
Min importance:
{[0, 1, 2, 3].map(n => ( ))}
Period:
{/* Report list */} {isLoading || isFetching ? (
Loading reports…
) : reports.length === 0 ? (
📋
No institutional reports yet

Use the refresh buttons above to fetch reports. COT and EIA require API keys. Earnings, VX, Central Bank RSS, and Sentiment fetch automatically.

) : (
{reports.length} report{reports.length > 1 ? 's' : ''}
{(reports as InstitutionalReport[]).map(r => ( ))}
)}
) }