feat(FE): Trade Intelligence — UN Comtrade, IMF, World Bank, Eurostat + AI predictions
This commit is contained in:
parent
3d5ca76b54
commit
110322fdc5
1 changed files with 264 additions and 0 deletions
264
src/app/dashboard/trade-intelligence/page.tsx
Normal file
264
src/app/dashboard/trade-intelligence/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue