← back to Trademarks Copyright

src/components/ItemsTable.tsx

319 lines

"use client";

import { useEffect, useMemo, useState } from "react";
import {
  useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel,
  flexRender, createColumnHelper, type SortingState, type RowSelectionState,
} from "@tanstack/react-table";
import Link from "next/link";

type Row = {
  id: number;
  kind: "trademark" | "copyright";
  name: string;
  original_owner: string | null;
  nice_class: string | null;
  goods_services: string | null;
  status: string;
  expired_date: string;
  years_expired: number;
  follow_up_date: string;
  distinctiveness_score: number | null;
  recognition_score: number | null;
  domain_available: boolean | null;
  competing_active_marks: number | null;
  monetization_breadth: number | null;
  composite_score: number | null;
};

const col = createColumnHelper<Row>();

function scoreCell(n: number | null) {
  if (n === null) return <span className="text-ink/30">—</span>;
  const hue = Math.round((n / 100) * 120); // red→green
  return (
    <span
      className="inline-block rounded px-1.5 py-0.5 text-xs font-semibold text-white"
      style={{ backgroundColor: `hsl(${hue} 55% 40%)` }}
      title="Composite score"
    >
      {n.toFixed(1)}
    </span>
  );
}

export default function ItemsTable() {
  const [rows, setRows] = useState<Row[]>([]);
  const [loading, setLoading] = useState(true);
  const [q, setQ] = useState("");
  const [kind, setKind] = useState("");
  const [status, setStatus] = useState("");
  const [sorting, setSorting] = useState<SortingState>([{ id: "composite_score", desc: true }]);
  const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
  const [swotOpen, setSwotOpen] = useState(false);
  const [swotItemId, setSwotItemId] = useState<number | null>(null);

  useEffect(() => {
    const url = new URL("/api/items", window.location.origin);
    if (q) url.searchParams.set("q", q);
    if (kind) url.searchParams.set("kind", kind);
    if (status) url.searchParams.set("status", status);
    setLoading(true);
    fetch(url.toString())
      .then((r) => r.json())
      .then((j) => setRows(j.items || []))
      .finally(() => setLoading(false));
  }, [q, kind, status]);

  const columns = useMemo(
    () => [
      col.display({
        id: "select",
        header: ({ table }) => (
          <input
            type="checkbox"
            checked={table.getIsAllRowsSelected()}
            ref={(el) => { if (el) el.indeterminate = table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected(); }}
            onChange={table.getToggleAllRowsSelectedHandler()}
          />
        ),
        cell: ({ row }) => (
          <input
            type="checkbox"
            checked={row.getIsSelected()}
            onChange={row.getToggleSelectedHandler()}
          />
        ),
      }),
      col.accessor("composite_score", {
        header: "Score", cell: (c) => scoreCell(c.getValue() as number | null),
      }),
      col.accessor("name", {
        header: "Name",
        cell: (c) => (
          <Link href={`/item/${c.row.original.id}`} className="font-medium text-ink hover:text-brass">
            {c.getValue() as string}
          </Link>
        ),
      }),
      col.accessor("kind", { header: "Type", cell: (c) => <span className="tag">{c.getValue()}</span> }),
      col.accessor("status", { header: "Status", cell: (c) => <span className="tag">{c.getValue()}</span> }),
      col.accessor("original_owner", { header: "Original Owner" }),
      col.accessor("nice_class", { header: "Class", cell: (c) => c.getValue() ?? "—" }),
      col.accessor("expired_date", {
        header: "Date Expired",
        cell: (c) => (c.getValue() as string)?.slice(0, 10),
      }),
      col.accessor("years_expired", {
        header: "Yrs Exp.",
        cell: (c) => <span>{c.getValue() as number}</span>,
      }),
      col.accessor("follow_up_date", {
        header: "Follow-up (+6mo)",
        cell: (c) => {
          const d = (c.getValue() as string)?.slice(0, 10);
          return <span className="text-ink/80" title="Revisit date — 6 months from today">{d}</span>;
        },
      }),
      col.accessor("distinctiveness_score", { header: "Distinct." }),
      col.accessor("recognition_score", { header: "Recog." }),
      col.accessor("competing_active_marks", { header: "Competing" }),
      col.accessor("domain_available", {
        header: ".com",
        cell: (c) => {
          const v = c.getValue() as boolean | null;
          return v === true ? <span className="text-green-700">●</span> :
                 v === false ? <span className="text-red-600">○</span> :
                 <span className="text-ink/30">?</span>;
        },
      }),
      col.accessor("monetization_breadth", { header: "Monet." }),
    ],
    []
  );

  const table = useReactTable({
    data: rows,
    columns,
    state: { sorting, rowSelection },
    onSortingChange: setSorting,
    onRowSelectionChange: setRowSelection,
    getRowId: (r) => String(r.id),
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    enableRowSelection: true,
    enableMultiRowSelection: true,
  });

  const selectedIds = useMemo(
    () => Object.entries(rowSelection).filter(([, v]) => v).map(([k]) => Number(k)),
    [rowSelection]
  );

  function sendToCanvas() {
    if (!selectedIds.length) return;
    sessionStorage.setItem("canvasIds", JSON.stringify(selectedIds));
    window.location.href = "/canvas";
  }

  function runSwotFor(id: number) {
    setSwotItemId(id);
    setSwotOpen(true);
  }

  return (
    <div className="space-y-4">
      <div className="flex flex-wrap items-center gap-3">
        <input
          placeholder="Search name, owner, goods…"
          value={q}
          onChange={(e) => setQ(e.target.value)}
          className="w-80"
        />
        <select value={kind} onChange={(e) => setKind(e.target.value)}>
          <option value="">All types</option>
          <option value="trademark">Trademark</option>
          <option value="copyright">Copyright</option>
        </select>
        <select value={status} onChange={(e) => setStatus(e.target.value)}>
          <option value="">All statuses</option>
          <option value="abandoned">Abandoned</option>
          <option value="cancelled">Cancelled</option>
          <option value="expired">Expired</option>
          <option value="public_domain">Public Domain</option>
        </select>

        <div className="ml-auto flex items-center gap-2 text-sm">
          <span className="text-ink/60">{selectedIds.length} selected</span>
          <button
            className="btn-ghost"
            disabled={selectedIds.length !== 1}
            onClick={() => selectedIds.length === 1 && runSwotFor(selectedIds[0])}
            title="Run SWOT on the single selected row"
          >
            Run SWOT
          </button>
          <button
            className="btn"
            disabled={selectedIds.length === 0}
            onClick={sendToCanvas}
          >
            Send {selectedIds.length || ""} → Canvas
          </button>
        </div>
      </div>

      <div className="card overflow-x-auto">
        <table className="min-w-full text-sm">
          <thead className="border-b border-ink/10 bg-ink/5 text-left">
            {table.getHeaderGroups().map((hg) => (
              <tr key={hg.id}>
                {hg.headers.map((h) => (
                  <th
                    key={h.id}
                    className="select-none px-3 py-2 font-semibold text-ink/80"
                    onClick={h.column.getCanSort() ? h.column.getToggleSortingHandler() : undefined}
                    style={{ cursor: h.column.getCanSort() ? "pointer" : "default" }}
                  >
                    {flexRender(h.column.columnDef.header, h.getContext())}
                    {{ asc: " ▲", desc: " ▼" }[h.column.getIsSorted() as string] ?? ""}
                  </th>
                ))}
              </tr>
            ))}
          </thead>
          <tbody>
            {loading && (
              <tr><td colSpan={columns.length} className="p-6 text-center text-ink/50">Loading…</td></tr>
            )}
            {!loading && table.getRowModel().rows.length === 0 && (
              <tr><td colSpan={columns.length} className="p-6 text-center text-ink/50">No matches.</td></tr>
            )}
            {table.getRowModel().rows.map((row) => (
              <tr key={row.id} className="border-b border-ink/5 hover:bg-brass/5">
                {row.getVisibleCells().map((cell) => (
                  <td key={cell.id} className="px-3 py-2 align-top">
                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
                  </td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {swotOpen && swotItemId !== null && (
        <SwotModal itemId={swotItemId} onClose={() => setSwotOpen(false)} />
      )}
    </div>
  );
}

function SwotModal({ itemId, onClose }: { itemId: number; onClose: () => void }) {
  const [loading, setLoading] = useState(true);
  const [force, setForce] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [quadrants, setQuadrants] = useState<{ quadrant: string; content: string }[]>([]);
  const [itemName, setItemName] = useState<string>("");

  useEffect(() => {
    fetch(`/api/items/${itemId}`).then((r) => r.json()).then((j) => setItemName(j.item?.name ?? ""));
  }, [itemId]);

  useEffect(() => {
    setLoading(true);
    setError(null);
    fetch("/api/swot", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ itemId, force }),
    })
      .then(async (r) => {
        const j = await r.json();
        if (!r.ok) throw new Error(j.error || `HTTP ${r.status}`);
        return j;
      })
      .then((j) => setQuadrants(j.quadrants || []))
      .catch((e) => setError(String(e.message || e)))
      .finally(() => setLoading(false));
  }, [itemId, force]);

  const byQ = (q: string) => quadrants.find((x) => x.quadrant === q)?.content ?? "";
  const colorOf: Record<string, string> = {
    strength: "bg-green-50 border-green-300",
    weakness: "bg-red-50 border-red-300",
    opportunity: "bg-blue-50 border-blue-300",
    threat: "bg-amber-50 border-amber-300",
  };

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
      <div className="card w-full max-w-5xl max-h-[90vh] overflow-auto bg-parchment p-5" onClick={(e) => e.stopPropagation()}>
        <div className="mb-3 flex items-center justify-between">
          <div>
            <h2 className="text-xl font-semibold">SWOT — {itemName}</h2>
            <p className="text-xs text-ink/60">4 Claude sub-agents, one per quadrant. Cached after first run.</p>
          </div>
          <div className="flex gap-2">
            <button className="btn-ghost" onClick={() => setForce(true)} disabled={loading}>Re-run</button>
            <button className="btn-ghost" onClick={onClose}>Close</button>
          </div>
        </div>
        {loading && <div className="p-8 text-center text-ink/50">Running 4 agents in parallel…</div>}
        {error && <div className="p-4 text-red-700">{error}</div>}
        {!loading && !error && (
          <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
            {(["strength", "weakness", "opportunity", "threat"] as const).map((q) => (
              <div key={q} className={`card border-2 p-3 ${colorOf[q]}`}>
                <div className="mb-1 text-xs font-bold uppercase tracking-wide">{q}</div>
                <pre className="whitespace-pre-wrap font-sans text-sm">{byQ(q) || "—"}</pre>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}