← back to Omega Watches 2
src/app/(dashboard)/terminal/page.tsx
214 lines
'use client';
import { useEffect, useState, useMemo } from 'react';
import { api } from '@/lib/api-client';
import { formatCurrency, formatDate } from '@/lib/utils';
import dynamic from 'next/dynamic';
const StockChart = dynamic(() => import('@/components/charts/StockChart'), { ssr: false });
const WatchSelector = dynamic(() => import('@/components/panels/WatchSelector'), { ssr: false });
const WilliamPanel = dynamic(() => import('@/components/panels/WilliamPanel'), { ssr: false });
interface WatchRef {
id: string;
brand: string;
collection: string;
model_name: string;
reference_number: string;
calibre: string;
case_material: string;
case_diameter_mm: string;
dial_color: string;
year_introduced: number;
notes?: string;
event_count: string;
msrp_count: string;
latest_msrp: string | null;
avg_market_price: string | null;
latest_sale_price: string | null;
latest_sale_date: string | null;
}
export default function TerminalPage() {
const [references, setReferences] = useState<WatchRef[]>([]);
const [selected, setSelected] = useState<WatchRef | null>(null);
const [compareList, setCompareList] = useState<WatchRef[]>([]);
const [detail, setDetail] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [chartLoading, setChartLoading] = useState(false);
useEffect(() => {
loadReferences();
}, []);
async function loadReferences() {
try {
const token = localStorage.getItem('omega_token');
const res = await fetch('/api/references', {
headers: { Authorization: `Bearer ${token}` },
});
const json = await res.json();
if (json.success) {
setReferences(json.data);
}
} catch {
// Handle silently
} finally {
setLoading(false);
}
}
async function handleSelect(ref: WatchRef) {
setSelected(ref);
setChartLoading(true);
try {
const [msrp, ts] = await Promise.all([
api.getMSRP(ref.reference_number),
api.getTimeseries(ref.reference_number, { include_asks: 'true' }),
]);
setDetail({ msrp, timeseries: ts });
} catch {
setDetail(null);
} finally {
setChartLoading(false);
}
}
function handleToggleCompare(ref: WatchRef) {
setCompareList(prev =>
prev.some(c => c.id === ref.id)
? prev.filter(c => c.id !== ref.id)
: [...prev, ref]
);
}
// Build chart data from detail
const chartData = useMemo(() => {
if (!detail) return [];
const points: any[] = [];
(detail.timeseries?.msrp_history || []).forEach((m: any) => {
points.push({ date: m.date, msrp: parseFloat(m.value), source: 'MSRP' });
});
(detail.timeseries?.market_events || []).forEach((e: any) => {
points.push({
date: e.date,
price: parseFloat(e.value),
event_type: e.event_type,
source: e.source,
});
});
points.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
const merged: any[] = [];
let lastMsrp: number | undefined;
for (const p of points) {
if (p.msrp) lastMsrp = p.msrp;
merged.push({ ...p, msrp: p.msrp || lastMsrp });
}
return merged;
}, [detail]);
// Stats
const stats = useMemo(() => {
if (!selected || !detail) return null;
const msrp = selected.latest_msrp ? parseFloat(selected.latest_msrp) : null;
const avg = selected.avg_market_price ? parseFloat(selected.avg_market_price) : null;
const lastSale = selected.latest_sale_price ? parseFloat(selected.latest_sale_price) : null;
const premium = msrp && avg ? ((avg - msrp) / msrp) * 100 : null;
const events = parseInt(selected.event_count) || 0;
const high = chartData.length > 0 ? Math.max(...chartData.filter(d => d.price).map(d => d.price!)) : null;
const low = chartData.length > 0 ? Math.min(...chartData.filter(d => d.price).map(d => d.price!)) : null;
return { msrp, avg, lastSale, premium, events, high, low };
}, [selected, detail, chartData]);
if (loading) {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-gray-400">Loading terminal...</div>
</div>
);
}
return (
<div className="flex h-[calc(100vh-2rem)] gap-3">
{/* Left — Watch Selector */}
<div className="w-72 flex-shrink-0">
<WatchSelector
references={references}
selected={selected}
compareList={compareList}
onSelect={handleSelect}
onToggleCompare={handleToggleCompare}
/>
</div>
{/* Center — Chart + Stats */}
<div className="flex min-w-0 flex-1 flex-col gap-3">
{/* Stats bar */}
{stats && selected && (
<div className="grid grid-cols-7 gap-2">
{[
{ label: 'MSRP', value: stats.msrp ? formatCurrency(stats.msrp) : '—', color: 'text-red-400' },
{ label: 'Avg Market', value: stats.avg ? formatCurrency(stats.avg) : '—', color: 'text-omega-400' },
{ label: 'Last Sale', value: stats.lastSale ? formatCurrency(stats.lastSale) : '—', color: 'text-green-400' },
{ label: 'Premium', value: stats.premium !== null ? `${stats.premium >= 0 ? '+' : ''}${stats.premium.toFixed(1)}%` : '—', color: stats.premium && stats.premium >= 0 ? 'text-green-400' : 'text-red-400' },
{ label: 'High', value: stats.high ? formatCurrency(stats.high) : '—', color: 'text-gray-200' },
{ label: 'Low', value: stats.low ? formatCurrency(stats.low) : '—', color: 'text-gray-200' },
{ label: 'Events', value: `${stats.events}`, color: 'text-blue-400' },
].map((s, i) => (
<div key={i} className="rounded-lg border border-navy-700 bg-navy-800/40 px-3 py-2 text-center">
<div className="text-[10px] uppercase text-gray-500">{s.label}</div>
<div className={`mt-0.5 text-sm font-bold ${s.color}`}>{s.value}</div>
</div>
))}
</div>
)}
{/* Chart */}
<div className="flex-1">
{chartLoading ? (
<div className="flex h-full items-center justify-center rounded-xl border border-navy-700 bg-navy-800/30">
<div className="text-gray-400">Loading chart data...</div>
</div>
) : (
<StockChart
data={chartData}
title={selected ? selected.model_name : undefined}
reference={selected?.reference_number}
/>
)}
</div>
{/* Bottom stats — recent events */}
{detail?.timeseries?.market_events?.length > 0 && (
<div className="rounded-xl border border-navy-700 bg-navy-800/30 p-3">
<h4 className="mb-2 text-xs font-semibold uppercase text-gray-400">Recent Transactions</h4>
<div className="flex gap-2 overflow-x-auto">
{detail.timeseries.market_events.slice(-8).reverse().map((ev: any, i: number) => (
<div key={i} className="flex-shrink-0 rounded-lg border border-navy-700 bg-navy-900 px-3 py-2 text-center">
<div className="text-[10px] text-gray-500">{ev.source}</div>
<div className="mt-0.5 font-mono text-sm text-gray-200">
{ev.value ? formatCurrency(parseFloat(ev.value)) : '—'}
</div>
<div className="text-[10px] text-gray-500">{ev.date ? formatDate(ev.date) : ''}</div>
<span className={`mt-0.5 inline-block rounded px-1.5 py-0.5 text-[9px] font-medium ${
ev.event_type === 'auction_realized' ? 'bg-green-900/30 text-green-400' :
ev.event_type === 'dealer_ask' ? 'bg-amber-900/30 text-amber-400' :
'bg-blue-900/30 text-blue-400'
}`}>
{ev.event_type?.replace(/_/g, ' ')}
</span>
</div>
))}
</div>
</div>
)}
</div>
{/* Right — William Panel */}
<div className="w-72 flex-shrink-0">
<WilliamPanel />
</div>
</div>
);
}