feat(FE): Trade Intelligence — UN Comtrade, IMF, World Bank, Eurostat + AI predictions

This commit is contained in:
admin-valentin 2026-08-26 19:12:35 +00:00
parent 3d5ca76b54
commit 110322fdc5

View file

@ -0,0 +1,264 @@
'use client';
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { apiFetch } from '../../../lib/api';
import { useSession } from '../../../components/session-provider';
const SOURCES = [
{ id: 'comtrade', name: 'UN Comtrade', flag: '🌐', desc: 'Fluxuri comerciale bilaterale, 200+ țări' },
{ id: 'imf', name: 'IMF WEO', flag: '💵', desc: 'GDP, inflație, balanță comercială' },
{ id: 'worldbank',name: 'World Bank', flag: '🏦', desc: 'Indicatori de dezvoltare, export/import' },
{ id: 'eurostat', name: 'Eurostat', flag: '🇪🇺', desc: 'Comex intra+extra EU, CN8' },
];
const HS_EXAMPLES = [
{ code: '847150', label: 'Laptopuri' },
{ code: '271000', label: 'Ulei mineral' },
{ code: '870322', label: 'Autoturisme 1500cc' },
{ code: '300490', label: 'Medicamente' },
{ code: '720271', label: 'Ferosilicon' },
{ code: '100190', label: 'Grâu dur' },
];
const IMF_INDICATORS = [
{ code: 'NGDP_RPCH', label: 'GDP real growth %' },
{ code: 'PCPI', label: 'Inflație CPI' },
{ code: 'BCA_NGDPD', label: 'Balanță cont curent % GDP' },
{ code: 'LUR', label: 'Rata șomajului' },
];
interface ApiResult { source: string; rows: number; data: Record<string, unknown>[]; error?: string; }
export default function TradeIntelligencePage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const [activeSource, setActiveSource] = useState('comtrade');
const [result, setResult] = useState<ApiResult | null>(null);
const [prediction, setPrediction] = useState('');
// Comtrade params
const [reporter, setReporter] = useState('276'); // Germany
const [partner, setPartner] = useState('0'); // World
const [period, setPeriod] = useState('2024');
const [hs, setHs] = useState('847150');
const [flow, setFlow] = useState('M');
// IMF params
const [countries, setCountries] = useState('DEU,ROU,FRA');
const [indicators, setIndicators] = useState('NGDP_RPCH,PCPI,BCA_NGDPD');
// WB params
const [wbCountries, setWbCountries] = useState('DE;RO;FR');
const [wbIndicators, setWbIndicators] = useState('NY.GDP.MKTP.CD,TM.VAL.MRCH.CD.WT');
// Eurostat params
const [product, setProduct] = useState('84715000');
const [esReporter, setEsReporter] = useState('DE');
// Predict
const [predCommodity, setPredCommodity] = useState('');
const fetchMut = useMutation({
mutationFn: () => {
if (activeSource === 'comtrade')
return apiFetch<ApiResult>(`/v1/trade/comtrade?reporter=${reporter}&partner=${partner}&period=${period}&hs=${hs}&flow=${flow}`, { tenantId });
if (activeSource === 'imf')
return apiFetch<ApiResult>(`/v1/trade/imf?countries=${countries}&indicators=${indicators}`, { tenantId });
if (activeSource === 'worldbank')
return apiFetch<ApiResult>(`/v1/trade/worldbank?countries=${wbCountries}&indicators=${wbIndicators}`, { tenantId });
return apiFetch<ApiResult>(`/v1/trade/eurostat?product=${product}&reporter=${esReporter}&period=${period}`, { tenantId });
},
onSuccess: setResult,
});
const predictMut = useMutation({
mutationFn: () => apiFetch<{ prediction: string }>('/v1/trade/predict', {
tenantId, method: 'POST',
body: { commodity: predCommodity, reporter, partner, horizonMonths: 12 },
}),
onSuccess: d => setPrediction(d.prediction),
});
return (
<div className="max-w-5xl space-y-6 p-6">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Trade Intelligence</h1>
<p className="text-sm text-ink-faint mt-1">
UN Comtrade · IMF · World Bank · Eurostat · Predicții AI import/export
</p>
</div>
{/* Source selector */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{SOURCES.map(s => (
<button key={s.id} onClick={() => setActiveSource(s.id)}
className={`card p-3 text-left transition-all ${activeSource === s.id ? 'border-primary bg-primary/5' : 'hover:border-primary/30'}`}>
<span className="text-xl">{s.flag}</span>
<p className="text-xs font-semibold text-ink mt-1">{s.name}</p>
<p className="text-[10px] text-ink-faint">{s.desc}</p>
</button>
))}
</div>
{/* Params */}
<div className="card p-5 space-y-4">
{activeSource === 'comtrade' && (
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div>
<label className="text-xs text-ink-faint">Reporter (cod numeric)</label>
<input value={reporter} onChange={e => setReporter(e.target.value)}
placeholder="276 = Germania" className="mt-1 w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
<div>
<label className="text-xs text-ink-faint">Partener (0 = Mondial)</label>
<input value={partner} onChange={e => setPartner(e.target.value)}
className="mt-1 w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
<div>
<label className="text-xs text-ink-faint">Perioada (YYYY sau YYYYMM)</label>
<input value={period} onChange={e => setPeriod(e.target.value)}
className="mt-1 w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
<div>
<label className="text-xs text-ink-faint">Cod HS6</label>
<input value={hs} onChange={e => setHs(e.target.value)}
className="mt-1 w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
<div className="flex flex-wrap gap-1 mt-1">
{HS_EXAMPLES.map(h => (
<button key={h.code} onClick={() => setHs(h.code)}
className="rounded px-1.5 py-0.5 text-[10px] bg-muted text-ink-faint hover:text-primary">{h.label}</button>
))}
</div>
</div>
<div>
<label className="text-xs text-ink-faint">Flux</label>
<select value={flow} onChange={e => setFlow(e.target.value)}
className="mt-1 w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring">
<option value="M">Import</option>
<option value="X">Export</option>
</select>
</div>
</div>
)}
{activeSource === 'imf' && (
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-ink-faint">Țări (ISO3, virgulă)</label>
<input value={countries} onChange={e => setCountries(e.target.value)}
placeholder="DEU,ROU,FRA" className="mt-1 w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
<div>
<label className="text-xs text-ink-faint">Indicatori IMF</label>
<input value={indicators} onChange={e => setIndicators(e.target.value)}
className="mt-1 w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
<div className="flex flex-wrap gap-1 mt-1">
{IMF_INDICATORS.map(i => (
<button key={i.code} onClick={() => setIndicators(ind => ind.includes(i.code) ? ind : ind + ',' + i.code)}
className="rounded px-1.5 py-0.5 text-[10px] bg-muted text-ink-faint hover:text-primary">{i.label}</button>
))}
</div>
</div>
</div>
)}
{activeSource === 'worldbank' && (
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-ink-faint">Țări (ISO2, punct-virgulă)</label>
<input value={wbCountries} onChange={e => setWbCountries(e.target.value)}
placeholder="DE;RO;FR" className="mt-1 w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
<div>
<label className="text-xs text-ink-faint">Indicatori World Bank</label>
<input value={wbIndicators} onChange={e => setWbIndicators(e.target.value)}
className="mt-1 w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
</div>
)}
{activeSource === 'eurostat' && (
<div className="grid grid-cols-3 gap-3">
<div>
<label className="text-xs text-ink-faint">Cod CN8 (produs)</label>
<input value={product} onChange={e => setProduct(e.target.value)}
placeholder="84715000" className="mt-1 w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
<div>
<label className="text-xs text-ink-faint">Reporter (ISO2)</label>
<input value={esReporter} onChange={e => setEsReporter(e.target.value)}
placeholder="DE" className="mt-1 w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
<div>
<label className="text-xs text-ink-faint">Perioada</label>
<input value={period} onChange={e => setPeriod(e.target.value)}
placeholder="2024" className="mt-1 w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
</div>
)}
<button onClick={() => fetchMut.mutate()} disabled={fetchMut.isPending}
className="w-full rounded-xl bg-primary py-2.5 text-sm font-semibold text-white disabled:opacity-50">
{fetchMut.isPending ? 'Se încarcă…' : `Fetch ${SOURCES.find(s => s.id === activeSource)?.name}`}
</button>
</div>
{/* Results */}
{result && (
<div className="card p-5 space-y-3">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-ink">
{result.source} {result.rows} rânduri
</p>
<button onClick={() => setResult(null)} className="text-xs text-ink-faint hover:text-ink"></button>
</div>
{result.error ? (
<p className="text-sm text-signal-danger font-mono">{result.error}</p>
) : result.data.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-border/50">
{Object.keys(result.data[0]).slice(0, 8).map(k => (
<th key={k} className="px-2 py-1.5 text-left font-medium text-ink-faint">{k}</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-border/30">
{result.data.slice(0, 30).map((row, i) => (
<tr key={i} className="hover:bg-muted/20">
{Object.values(row).slice(0, 8).map((v, j) => (
<td key={j} className="px-2 py-1.5 text-ink max-w-[150px] truncate">{String(v ?? '')}</td>
))}
</tr>
))}
</tbody>
</table>
{result.data.length > 30 && <p className="text-[10px] text-ink-faint text-center pt-2">+{result.data.length - 30} rânduri</p>}
</div>
) : <p className="text-sm text-ink-faint">Nicio dată returnată. Verifică parametrii sau setează UN_COMTRADE_KEY în ceo-api.</p>}
</div>
)}
{/* AI Prediction */}
<div className="card p-5 space-y-3 border-indigo-200 bg-indigo-50">
<p className="text-sm font-semibold text-indigo-800"> Predicție AI Import/Export</p>
<div className="flex gap-2">
<input value={predCommodity} onChange={e => setPredCommodity(e.target.value)}
placeholder="Descrie produsul… ex: Laptopuri (HS 8471), Ulei de floarea soarelui, Turbine eoliene"
className="flex-1 rounded-lg border bg-white px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-indigo-400" />
<button onClick={() => predictMut.mutate()} disabled={!predCommodity.trim() || predictMut.isPending}
className="rounded-lg bg-indigo-600 px-4 py-2 text-sm text-white disabled:opacity-50">
{predictMut.isPending ? '…' : 'Predicție 12 luni'}
</button>
</div>
{prediction && (
<div className="rounded-xl bg-white p-4 border border-indigo-100">
<p className="text-xs text-ink whitespace-pre-line leading-relaxed">{prediction}</p>
<button onClick={() => setPrediction('')} className="mt-2 text-[10px] text-indigo-400 hover:text-indigo-700"> Șterge</button>
</div>
)}
<p className="text-[10px] text-indigo-600">
Variabilă env necesară pentru volume mari: <code>UN_COMTRADE_KEY</code> în ceo-api (gratuit la comtradeplus.un.org)
</p>
</div>
</div>
);
}