import { useState, useMemo, type ReactNode } from "react"; interface Stock { id: string; ticker: string; notes: string | null; lastDecision: string | null; lastJobId: string | null; createdAt: string; updatedAt: string; } interface StockTableProps { stocks: Stock[]; onNotesSave: (ticker: string, notes: string) => Promise; saveError: string | null; } type SortField = "ticker" | "createdAt" | "updatedAt"; type SortDirection = "asc" | "desc"; function SortHeader({ field, children, sortField, sortDirection, onSort }: { field: SortField; children: ReactNode; sortField: SortField; sortDirection: SortDirection; onSort: (field: SortField) => void; }) { return ( onSort(field)} > {children} {sortField === field && {sortDirection === "asc" ? "↑" : "↓"}} ); } export default function StockTable({ stocks, onNotesSave, saveError }: StockTableProps) { const [search, setSearch] = useState(""); const [sortField, setSortField] = useState("ticker"); const [sortDirection, setSortDirection] = useState("asc"); const [editingTicker, setEditingTicker] = useState(null); const [editingNotes, setEditingNotes] = useState(""); const [savingNotes, setSavingNotes] = useState(null); const [page, setPage] = useState(0); const pageSize = 20; const filtered = useMemo(() => { const q = search.toLowerCase(); const result = stocks.filter((s) => s.ticker.toLowerCase().includes(q)); result.sort((a, b) => { const aVal = a[sortField] ?? ""; const bVal = b[sortField] ?? ""; const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0; return sortDirection === "asc" ? cmp : -cmp; }); return result; }, [stocks, search, sortField, sortDirection]); const totalPages = Math.ceil(filtered.length / pageSize); const paged = filtered.slice(page * pageSize, (page + 1) * pageSize); const handleSort = (field: SortField) => { setPage(0); if (sortField === field) { setSortDirection((d) => (d === "asc" ? "desc" : "asc")); } else { setSortField(field); setSortDirection("asc"); } }; const startEditing = (stock: Stock) => { setEditingTicker(stock.ticker); setEditingNotes(stock.notes ?? ""); }; const saveNotes = async () => { if (!editingTicker) return; setSavingNotes(editingTicker); try { await onNotesSave(editingTicker, editingNotes); setEditingTicker(null); } catch (e) { console.error("Failed to save notes:", e); } finally { setSavingNotes(null); } }; if (stocks.length === 0) { return (

No stocks tracked yet. Visit the stocks page to add some.

); } return (
{saveError && (
{saveError}
)}
{ setSearch(e.target.value); setPage(0); }} className="border border-gray-300 rounded-lg px-4 py-2.5 w-64 focus:ring-2 focus:ring-blue-500" /> {filtered.length} stock{filtered.length !== 1 ? "s" : ""}
TickerCreatedUpdated {paged.map((stock) => ( ))}
Notes Last Decision Last Job
{stock.ticker} {editingTicker === stock.ticker ? (
setEditingNotes(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") saveNotes(); if (e.key === "Escape") setEditingTicker(null); }} className="flex-1 border border-blue-300 rounded px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500" autoFocus />
) : ( startEditing(stock)} > {stock.notes || Click to add notes...} )}
{stock.lastDecision ? ( {stock.lastDecision.toUpperCase()} ) : "-"} {stock.lastJobId ? ( {stock.lastJobId.slice(0, 12)}... ) : "-"} {new Date(stock.createdAt).toLocaleDateString()} {new Date(stock.updatedAt).toLocaleDateString()}
{filtered.length === 0 && (

No stocks found matching "{search}"

)} {totalPages > 1 && (
Showing {page * pageSize + 1}-{Math.min((page + 1) * pageSize, filtered.length)} of {filtered.length}
)}
); }