fix(ceo-web): correct imports in MarketPage (session-provider + apiFetch pattern)
This commit is contained in:
parent
5b734aa14b
commit
1be08612f8
1 changed files with 105 additions and 153 deletions
|
|
@ -1,167 +1,119 @@
|
|||
'use client';
|
||||
import { useSession } from '../../../lib/session';
|
||||
import { apiFetch } from '../../../lib/api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiFetch, type FinancialSignal } from '../../../lib/api';
|
||||
import { useSession } from '../../../components/session-provider';
|
||||
|
||||
interface Signal {
|
||||
id: string;
|
||||
category: string;
|
||||
region: string;
|
||||
source: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
rawValue?: string;
|
||||
changePercent?: string;
|
||||
unit?: string;
|
||||
severity: string;
|
||||
publishedAt: string;
|
||||
}
|
||||
|
||||
const SEV_CLS: Record<string, string> = {
|
||||
critical: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300',
|
||||
warning: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300',
|
||||
info: 'bg-sky-100 text-sky-800 dark:bg-sky-900/30 dark:text-sky-300',
|
||||
const SEVERITY_COLOR: Record<string, string> = {
|
||||
critical: 'bg-red-500/10 text-red-600 border-red-500/30',
|
||||
high: 'bg-orange-500/10 text-orange-600 border-orange-500/30',
|
||||
medium: 'bg-yellow-500/10 text-yellow-700 border-yellow-500/30',
|
||||
low: 'bg-green-500/10 text-green-700 border-green-500/30',
|
||||
info: 'bg-blue-500/10 text-blue-600 border-blue-500/30',
|
||||
};
|
||||
|
||||
const CAT: Record<string, string> = {
|
||||
macro: 'Macro', fx: 'FX / Valute', rates: 'Dobânzi',
|
||||
commodity: 'Mărfuri', news: 'Știri Economice',
|
||||
const CATEGORY_LABEL: Record<string, string> = {
|
||||
macro: 'Macro',
|
||||
fx: 'FX',
|
||||
rates: 'Rate',
|
||||
commodity: 'Commodity',
|
||||
news: 'Știri',
|
||||
};
|
||||
|
||||
const REGION: Record<string, string> = {
|
||||
ro: 'RO', de: 'DE', eu: 'EU', us: 'US', global: '—',
|
||||
};
|
||||
export default function MarketPage() {
|
||||
const { activeTenant } = useSession();
|
||||
const tenantId = activeTenant?.tenantId ?? null;
|
||||
|
||||
function fmt(v?: string, u?: string) {
|
||||
if (!v) return null;
|
||||
const n = parseFloat(v);
|
||||
return isNaN(n) ? null : `${n.toLocaleString('ro-RO', { maximumFractionDigits: 2 })}${u ? ' ' + u : ''}`;
|
||||
}
|
||||
|
||||
function Chip({ v }: { v?: string }) {
|
||||
if (!v) return null;
|
||||
const n = parseFloat(v);
|
||||
if (isNaN(n)) return null;
|
||||
return (
|
||||
<span className={n >= 0 ? 'text-emerald-600 font-medium' : 'text-red-600 font-medium'}>
|
||||
{n >= 0 ? '+' : ''}{n.toFixed(2)}%
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MarketIntelligencePage() {
|
||||
const { session } = useSession();
|
||||
const { data: signals = [], isLoading } = useQuery<Signal[]>({
|
||||
queryKey: ['financial-signals'],
|
||||
enabled: !!session,
|
||||
staleTime: 5 * 60_000,
|
||||
queryFn: () =>
|
||||
apiFetch('/v1/financial-intelligence/signals?limit=100', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session!.access_token}`,
|
||||
'x-tenant-id': session!.tenant_id,
|
||||
},
|
||||
}),
|
||||
const { data: signals = [], isLoading, error } = useQuery({
|
||||
queryKey: ['financial-signals', tenantId],
|
||||
queryFn: () => apiFetch<FinancialSignal[]>('/v1/financial-intelligence/signals?limit=100', { tenantId }),
|
||||
enabled: true,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const cats = [...new Set(signals.map((s) => s.category))].sort();
|
||||
const byCategory = (cat: string) => signals.filter((s) => s.category === cat);
|
||||
const byCategory = signals.reduce<Record<string, FinancialSignal[]>>((acc, s) => {
|
||||
const cat = s.category ?? 'other';
|
||||
if (!acc[cat]) acc[cat] = [];
|
||||
acc[cat].push(s);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const critical = signals.filter((s) => s.severity === 'critical').length;
|
||||
const warning = signals.filter((s) => s.severity === 'warning').length;
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<div className="flex items-start justify-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Market Intelligence</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Semnale economice live — World Bank · ECB · Eurostat · FRED
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2 text-sm">
|
||||
{critical > 0 && (
|
||||
<span className="px-3 py-1 rounded-full bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300 font-medium">
|
||||
{critical} critice
|
||||
</span>
|
||||
)}
|
||||
{warning > 0 && (
|
||||
<span className="px-3 py-1 rounded-full bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300 font-medium">
|
||||
{warning} atenționări
|
||||
</span>
|
||||
)}
|
||||
<span className="px-3 py-1 rounded-full bg-muted text-muted-foreground">
|
||||
{signals.length} total
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex items-center gap-2 text-muted-foreground py-8">
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
Se încarcă semnalele...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && signals.length === 0 && (
|
||||
<div className="rounded-xl border border-dashed p-12 text-center space-y-3">
|
||||
<p className="text-lg font-medium">Fără semnale economice</p>
|
||||
<p className="text-sm text-muted-foreground max-w-md mx-auto">
|
||||
Importă workflow-ul n8n “08 Economic Data API” în n8n
|
||||
(rulează la fiecare 6h și populează această pagină automat).
|
||||
</p>
|
||||
<div className="text-xs bg-muted rounded-lg p-4 text-left inline-block mt-2">
|
||||
<p className="font-mono">Fișier: /root/ceo-os-economic-data-n8n.json</p>
|
||||
<p className="text-muted-foreground mt-1">POST → /v1/financial-intelligence/ingest</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cats.map((cat) => (
|
||||
<section key={cat}>
|
||||
<h2 className="text-xs font-semibold uppercase tracking-widest text-muted-foreground mb-3">
|
||||
{CAT[cat] ?? cat}
|
||||
</h2>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{byCategory(cat).map((s) => (
|
||||
<article key={s.id}
|
||||
className="rounded-xl border bg-card p-4 flex flex-col gap-2 hover:shadow-sm transition-shadow">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="flex-1 font-medium text-sm leading-snug">{s.title}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full shrink-0 font-medium ${SEV_CLS[s.severity] ?? SEV_CLS.info}`}>
|
||||
{s.severity}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{(s.rawValue || s.changePercent) && (
|
||||
<div className="flex items-center gap-3">
|
||||
{s.rawValue && (
|
||||
<span className="text-2xl font-bold tabular-nums leading-none">
|
||||
{fmt(s.rawValue, s.unit)}
|
||||
</span>
|
||||
)}
|
||||
<Chip v={s.changePercent} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground line-clamp-2 flex-1">{s.summary}</p>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground pt-1 border-t">
|
||||
<span>{s.source}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{s.region !== 'global' && (
|
||||
<span className="bg-muted px-1.5 py-0.5 rounded text-xs">
|
||||
{REGION[s.region] ?? s.region}
|
||||
</span>
|
||||
)}
|
||||
<span>{new Date(s.publishedAt).toLocaleDateString('ro-RO')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
if (isLoading) return (
|
||||
<div className="p-6 space-y-4 animate-pulse">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className="h-24 rounded-xl bg-muted" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-lg border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||
Nu pot încărca semnalele de piață.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (signals.length === 0) return (
|
||||
<div className="p-6 space-y-4 max-w-2xl">
|
||||
<h1 className="text-2xl font-semibold">Market Intelligence</h1>
|
||||
<div className="rounded-xl border bg-card p-6 space-y-3">
|
||||
<p className="font-medium">Nicio dată disponibilă</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Activează workflow-ul <strong>08 Economic Data API</strong> din n8n pentru a incepe
|
||||
colectarea automată a datelor macro, FX și știri la fiecare 6h.
|
||||
</p>
|
||||
<ol className="text-sm text-muted-foreground list-decimal list-inside space-y-1">
|
||||
<li>Deschide n8n → căutați "08 Economic Data API"</li>
|
||||
<li>Toggle ON → datele apar în ~1 minut</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6 max-w-5xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Market Intelligence</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{signals.length} semnale · actualizat automat la 6h
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Object.entries(byCategory).map(([cat, items]) => (
|
||||
<div key={cat} className="space-y-3">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{CATEGORY_LABEL[cat] ?? cat}
|
||||
</h2>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{items.map((s) => (
|
||||
<div key={s.id} className="rounded-xl border bg-card p-4 space-y-2">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-sm font-medium leading-snug">{s.title}</p>
|
||||
<span className={`shrink-0 text-xs border rounded-full px-2 py-0.5 font-medium capitalize ${SEVERITY_COLOR[s.severity] ?? 'bg-muted text-muted-foreground border-border'}`}>
|
||||
{s.severity}
|
||||
</span>
|
||||
</div>
|
||||
{s.summary && (
|
||||
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-2">{s.summary}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground/70">
|
||||
{s.rawValue != null && (
|
||||
<span className="font-mono">
|
||||
{Number(s.rawValue).toLocaleString('ro-RO', { maximumFractionDigits: 2 })}
|
||||
{s.unit ? ` ${s.unit}` : ''}
|
||||
{s.changePercent != null ? ` (${Number(s.changePercent) >= 0 ? '+' : ''}${Number(s.changePercent).toFixed(2)}%)` : ''}
|
||||
</span>
|
||||
)}
|
||||
<span>{s.region}</span>
|
||||
<span>{new Date(s.publishedAt).toLocaleDateString('ro-RO')}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue