← back to Omega Watches 2

src/app/(dashboard)/models/page.tsx

347 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 PriceTimeSeries = dynamic(() => import("@/components/charts/PriceTimeSeries"), { ssr: false });
const ConditionBreakdown = dynamic(() => import("@/components/charts/ConditionBreakdown"), { 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;
}

interface Summary {
  total_references: string;
  total_events: string;
  total_snapshots: string;
  collection_count: string;
}

export default function ModelDashboardPage() {
  const [references, setReferences] = useState<WatchRef[]>([]);
  const [summary, setSummary] = useState<Summary | null>(null);
  const [error, setError] = useState("");
  const [search, setSearch] = useState("");
  const [collection, setCollection] = useState("");
  const [selected, setSelected] = useState<WatchRef | null>(null);
  const [detail, setDetail] = useState<any>(null);

  useEffect(() => {
    load();
  }, [collection]);

  async function load() {
    try {
      const params: any = {};
      if (collection) params.collection = collection;
      const token = localStorage.getItem("omega_token");
      const qs = new URLSearchParams(params).toString();
      const res = await fetch(`/api/references${qs ? `?${qs}` : ""}`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      const json = await res.json();
      if (json.success) {
        setReferences(json.data);
        setSummary(json.summary);
      }
    } catch (e: any) {
      setError(e.message);
    }
  }

  async function loadDetail(ref: WatchRef) {
    setSelected(ref);
    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);
    }
  }

  // Build chart data from detail
  const chartData = useMemo(() => {
    if (!detail) return [];
    const points: any[] = [];
    // Add MSRP points
    (detail.timeseries?.msrp_history || []).forEach((m: any) => {
      points.push({ date: m.date, msrp: parseFloat(m.value), source: "MSRP" });
    });
    // Add market event points
    (detail.timeseries?.market_events || []).forEach((e: any) => {
      points.push({
        date: e.date,
        price: parseFloat(e.value),
        event_type: e.event_type,
        source: e.source,
      });
    });
    // Sort by date and merge same-date MSRP+price
    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]);

  // Build condition breakdown from events
  const conditionData = useMemo(() => {
    if (!detail?.timeseries?.market_events) return [];
    const counts: Record<string, { count: number; total: number }> = {};
    for (const e of detail.timeseries.market_events) {
      const cond = e.condition || "unknown";
      if (!counts[cond]) counts[cond] = { count: 0, total: 0 };
      counts[cond].count++;
      counts[cond].total += parseFloat(e.value) || 0;
    }
    return Object.entries(counts).map(([condition, { count, total }]) => ({
      condition,
      count,
      avg_price: Math.round(total / count),
    }));
  }, [detail]);

  const collections = [...new Set(references.map((r) => r.collection))];
  const filtered = search
    ? references.filter(
        (r) =>
          r.reference_number.toLowerCase().includes(search.toLowerCase()) ||
          r.model_name.toLowerCase().includes(search.toLowerCase()) ||
          r.collection.toLowerCase().includes(search.toLowerCase())
      )
    : references;

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <h1 className="text-xl font-bold text-gray-100">Watch References</h1>
        <div className="flex gap-2">
          <select
            className="input w-40"
            value={collection}
            onChange={(e) => setCollection(e.target.value)}
          >
            <option value="">All Collections</option>
            {collections.map((c) => (
              <option key={c} value={c}>{c}</option>
            ))}
          </select>
          <input
            type="text"
            placeholder="Search references..."
            className="input w-64"
            value={search}
            onChange={(e) => setSearch(e.target.value)}
          />
        </div>
      </div>

      {error && <div className="rounded bg-red-900/30 p-3 text-red-300">{error}</div>}

      {summary && (
        <div className="grid gap-4 md:grid-cols-4">
          <div className="card border-navy-700 text-center">
            <div className="text-3xl font-bold text-omega-400">{summary.total_references}</div>
            <div className="text-sm text-gray-500">Watch References</div>
          </div>
          <div className="card border-navy-700 text-center">
            <div className="text-3xl font-bold text-blue-400">{summary.total_events}</div>
            <div className="text-sm text-gray-500">Market Events</div>
          </div>
          <div className="card border-navy-700 text-center">
            <div className="text-3xl font-bold text-green-400">{summary.total_snapshots}</div>
            <div className="text-sm text-gray-500">MSRP Snapshots</div>
          </div>
          <div className="card border-navy-700 text-center">
            <div className="text-3xl font-bold text-purple-400">{summary.collection_count}</div>
            <div className="text-sm text-gray-500">Collections</div>
          </div>
        </div>
      )}

      <div className="card border-navy-700">
        <h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400">
          Reference Catalog ({filtered.length})
        </h2>
        <div className="overflow-x-auto">
          <table className="w-full text-sm">
            <thead>
              <tr className="border-b border-navy-700 text-left text-xs uppercase text-gray-500">
                <th className="pb-2 pr-3">Reference</th>
                <th className="pb-2 pr-3">Collection</th>
                <th className="pb-2 pr-3">Model</th>
                <th className="pb-2 pr-3">Calibre</th>
                <th className="pb-2 pr-3">Size</th>
                <th className="pb-2 pr-3">Dial</th>
                <th className="pb-2 pr-3 text-right">MSRP</th>
                <th className="pb-2 pr-3 text-right">Avg Market</th>
                <th className="pb-2 pr-3 text-right">Events</th>
                <th className="pb-2 text-right">Year</th>
              </tr>
            </thead>
            <tbody>
              {filtered.map((ref) => {
                const msrp = ref.latest_msrp ? parseFloat(ref.latest_msrp) : null;
                const avg = ref.avg_market_price ? parseFloat(ref.avg_market_price) : null;
                const premium = msrp && avg ? ((avg - msrp) / msrp) * 100 : null;

                return (
                  <tr
                    key={ref.id}
                    className={`cursor-pointer border-b border-navy-800 transition-colors hover:bg-navy-900 ${
                      selected?.id === ref.id ? "bg-navy-900" : ""
                    }`}
                    onClick={() => loadDetail(ref)}
                  >
                    <td className="py-2.5 pr-3 font-mono text-xs text-omega-400">
                      {ref.reference_number}
                    </td>
                    <td className="py-2.5 pr-3">
                      <span className="badge badge-info">{ref.collection}</span>
                    </td>
                    <td className="py-2.5 pr-3 font-medium text-gray-200">{ref.model_name}</td>
                    <td className="py-2.5 pr-3 text-gray-400">{ref.calibre}</td>
                    <td className="py-2.5 pr-3 text-gray-400">{ref.case_diameter_mm}mm</td>
                    <td className="py-2.5 pr-3 text-gray-400">{ref.dial_color}</td>
                    <td className="py-2.5 pr-3 text-right text-gray-200">
                      {msrp ? formatCurrency(msrp) : "—"}
                    </td>
                    <td className="py-2.5 pr-3 text-right">
                      {avg ? (
                        <span className={premium && premium > 0 ? "text-green-400" : premium && premium < -5 ? "text-red-400" : "text-gray-200"}>
                          {formatCurrency(avg)}
                          {premium !== null && (
                            <span className="ml-1 text-xs">
                              ({premium > 0 ? "+" : ""}{premium.toFixed(0)}%)
                            </span>
                          )}
                        </span>
                      ) : "—"}
                    </td>
                    <td className="py-2.5 pr-3 text-right text-gray-400">{ref.event_count}</td>
                    <td className="py-2.5 text-right text-gray-500">{ref.year_introduced}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </div>

      {/* Detail panel with charts */}
      {selected && detail && (
        <>
          {/* Price Chart */}
          {chartData.length > 0 && (
            <PriceTimeSeries
              data={chartData}
              title={`${selected.model_name} — Price History`}
              showMSRP={true}
            />
          )}

          <div className="grid gap-4 md:grid-cols-2">
            {/* Condition breakdown donut */}
            {conditionData.length > 0 && (
              <ConditionBreakdown
                data={conditionData}
                title="Condition Distribution"
              />
            )}

            {/* Market Events list */}
            <div className="card border-navy-700">
              <h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400">
                Recent Market Events ({detail.timeseries?.market_events?.length || 0})
              </h3>
              {detail.timeseries?.market_events?.length > 0 ? (
                <div className="space-y-2 max-h-80 overflow-y-auto">
                  {detail.timeseries.market_events.map((ev: any, i: number) => (
                    <div key={i} className="flex items-center justify-between rounded bg-navy-900 p-2">
                      <div>
                        <span className={`badge text-xs ${
                          ev.event_type === "auction_realized" ? "badge-success" :
                          ev.event_type === "marketplace_sold" ? "badge-info" :
                          ev.event_type === "dealer_ask" ? "badge-warning" : "badge-info"
                        }`}>
                          {ev.event_type.replace(/_/g, " ")}
                        </span>
                        <span className="ml-2 text-xs text-gray-500">{ev.source}</span>
                      </div>
                      <div className="text-right">
                        <div className="font-mono text-gray-200">
                          {ev.value ? formatCurrency(parseFloat(ev.value)) : "—"}
                        </div>
                        <div className="text-xs text-gray-500">
                          {ev.date ? formatDate(ev.date) : ""}
                        </div>
                      </div>
                    </div>
                  ))}
                </div>
              ) : (
                <div className="text-center text-sm text-gray-600">No market events recorded</div>
              )}
            </div>
          </div>

          {/* MSRP detail */}
          <div className="card border-navy-700">
            <h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400">
              MSRP Snapshots — {selected.reference_number}
            </h3>
            {detail.msrp?.history?.length > 0 ? (
              <div className="grid gap-2 md:grid-cols-5">
                {detail.msrp.history.map((h: any, i: number) => {
                  const prev = i > 0 ? parseFloat(detail.msrp.history[i - 1].msrp_amount) : null;
                  const curr = parseFloat(h.msrp_amount);
                  const change = prev ? curr - prev : 0;
                  return (
                    <div key={i} className="rounded bg-navy-900 p-3 text-center">
                      <div className="text-xs text-gray-500">{formatDate(h.captured_date)}</div>
                      <div className="mt-1 text-lg font-bold text-gray-200">{formatCurrency(curr)}</div>
                      {change !== 0 && (
                        <div className={`text-xs ${change > 0 ? "text-green-400" : "text-red-400"}`}>
                          {change > 0 ? "+" : ""}{formatCurrency(change)}
                        </div>
                      )}
                    </div>
                  );
                })}
              </div>
            ) : (
              <div className="text-center text-sm text-gray-600">
                {selected.year_introduced < 2000 ? "Vintage/discontinued — no current MSRP" : "No MSRP snapshots yet"}
              </div>
            )}
          </div>
        </>
      )}
    </div>
  );
}