fix(ceo-web): correct imports in MarketPage (session-provider + apiFetch pattern)

This commit is contained in:
admin-valentin 2026-08-01 20:25:40 +00:00
parent 5b734aa14b
commit 1be08612f8

View file

@ -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 &ldquo;08 Economic Data API&rdquo; î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>
);
}