import { KeyValueRow } from "../../../components";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
DataTableStackView,
EmptyState,
PaneStatusBody, QueryBar, Tabs,
usePaneFooter,
usePaneHeaderTabs,
usePaneMenuItems,
type DataTableCell,
type DataTableKeyEvent,
type DataTableRootKeyContext,
type PaneFooterSegment
} from "../../../components";
import { useShortcut } from "../../../react/input";
import { usePaneInstance } from "../../../state/app/context";
import { colors } from "../../../theme/colors";
import type { PaneProps } from "../../../types/plugin";
import { Box, ScrollBox, Text, TextAttributes, type InputRenderable } from "../../../ui";
import { formatCompact } from "../../../utils/format";
import { isPlainKey } from "../../../utils/keyboard";
import { formatRelativeAge } from "../../../utils/relative-time";
import { isPlainArrowUp, stopSearchFocusNavigation } from "../../../utils/search-focus-navigation";
import { cycleSortPreference } from "../../../utils/sort-values";
import { usePluginPaneState } from "../../runtime";
import { useAutoRefresh } from "../shared/auto-refresh";
import { loadTreasuryAuctions } from "./cache";
import {
AUCTION_FILTERS,
AUCTION_SORT_COLUMN_IDS,
DEFAULT_AUCTION_SORT,
auctionHistoryDays,
auctionSize,
buildAuctionColumns,
formatAuctionRate,
indirectPct,
isPendingAuction,
nextAuctionSort,
nextFilter,
rateLabel,
rateValue,
visibleAuctions,
type AuctionColumn,
type AuctionColumnId,
type AuctionFilter,
type AuctionSortPreference,
} from "./model";
import {
TREASURY_AUCTIONS_PANE_ID,
type LoadStatus,
type TreasuryAuction,
} from "./types";
function formatAuctionDate(value: string, withYear = false): string {
const timestamp = Date.parse(`${value}T00:00:00Z`);
if (!Number.isFinite(timestamp)) return "—";
return new Date(timestamp).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: withYear ? "numeric" : undefined,
timeZone: "UTC",
});
}
function formatRate(value: number | null): string {
return value == null ? "—" : `${value.toFixed(3)}%`;
}
function formatRatio(value: number | null): string {
return value == null ? "—" : value.toFixed(2);
}
function formatPct(value: number | null): string {
return value == null ? "—" : `${value.toFixed(1)}%`;
}
function formatMoney(value: number | null): string {
return value == null ? "—" : `$${formatCompact(value)}`;
}
function secTypeColor(secType: string, selected: boolean): string {
if (selected) return colors.selectedText;
switch (secType.toLowerCase()) {
case "bill":
case "cmb":
return colors.positive;
case "note":
case "frn":
return colors.textBright;
case "bond":
case "tips":
return colors.warning;
default:
return colors.text;
}
}
// Stable table adapters so memoized rows survive pane re-renders.
const auctionKey = (auction: TreasuryAuction) => auction.id;
const renderAuctionRow = (
auction: TreasuryAuction,
column: AuctionColumn,
_index: number,
rowState: { selected: boolean },
) => renderAuctionCell(auction, column, rowState);
function renderAuctionCell(
auction: TreasuryAuction,
column: AuctionColumn,
rowState: { selected: boolean },
): DataTableCell {
const selected = rowState.selected ? colors.selectedText : undefined;
const dimmed = selected ?? colors.textDim;
switch (column.id) {
case "date":
// Announced auctions have no results yet; every metric cell reads "—",
// so the date carries the distinction instead of a second placeholder.
return {
text: formatAuctionDate(auction.auctionDate),
color: rowState.selected ? colors.selectedText : isPendingAuction(auction) ? colors.textBright : colors.textDim,
};
case "type":
return {
text: auction.secType,
color: secTypeColor(auction.secType, rowState.selected),
attributes: TextAttributes.BOLD,
};
case "term":
return { text: auction.securityTerm, color: selected ?? colors.text };
case "rate":
return { text: formatAuctionRate(auction, rateValue(auction), "—"), color: selected ?? colors.textBright };
case "btc":
return { text: formatRatio(auction.bidToCoverRatio), color: selected ?? colors.text };
case "indirect":
return { text: formatPct(indirectPct(auction)), color: selected ?? colors.text };
case "size":
return { text: formatMoney(auctionSize(auction)), color: dimmed };
}
}
function TreasuryAuctionDetail({ auction, width }: { auction: TreasuryAuction; width: number }) {
const share = (value: number | null, total: number | null): string => (
value == null || !total ? formatMoney(value) : `${formatMoney(value)} (${((value / total) * 100).toFixed(1)}%)`
);
return (
{formatAuctionDate(auction.auctionDate, true)}
{isPendingAuction(auction) && results pending}
{auction.avgMedYield != null && (
)}
);
}
export function TreasuryAuctionsPane({ focused, width, height }: PaneProps) {
const historyDays = auctionHistoryDays(usePaneInstance()?.settings);
const [auctions, setAuctions] = useState([]);
const [status, setStatus] = useState("idle");
const [error, setError] = useState(null);
const [stale, setStale] = useState(false);
const [fetchedAt, setFetchedAt] = useState(null);
const [filter, setFilter] = usePluginPaneState("activeTab", "all");
const [selectedId, setSelectedId] = usePluginPaneState("selectedId", null);
const [detailOpen, setDetailOpen] = usePluginPaneState("detailOpen", false);
const [sortPreference, setSortPreference] = useState(DEFAULT_AUCTION_SORT);
const [searchQuery, setSearchQuery] = useState("");
const [searchFocused, setSearchFocused] = useState(false);
const [searchFocusToken, setSearchFocusToken] = useState(0);
const searchInputRef = useRef(null);
const fetchGenRef = useRef(0);
const focusSearch = useCallback(() => {
setSearchFocused(true);
setSearchFocusToken((current) => current + 1);
}, []);
const blurSearch = useCallback(() => setSearchFocused(false), []);
const load = useCallback((force = false) => {
fetchGenRef.current += 1;
const generation = fetchGenRef.current;
setStatus((current) => (current === "loaded" ? "loaded" : "loading"));
setError(null);
loadTreasuryAuctions(force, undefined, historyDays)
.then((result) => {
if (fetchGenRef.current !== generation) return;
setAuctions(result.auctions);
setFetchedAt(result.fetchedAt);
setStale(result.stale);
setError(result.refreshError ?? null);
setStatus("loaded");
})
.catch((loadError: unknown) => {
if (fetchGenRef.current !== generation) return;
setError(loadError instanceof Error ? loadError.message : String(loadError));
setStatus("error");
});
}, [historyDays]);
useEffect(() => { load(false); }, [load]);
// The cache decides whether a tick becomes a request; only [r] forces it.
const refresh = useCallback(() => load(false), [load]);
useAutoRefresh(stale ? null : fetchedAt, refresh);
const rows = useMemo(
() => visibleAuctions(auctions, { filter, query: searchQuery, sort: sortPreference }),
[auctions, filter, searchQuery, sortPreference],
);
const selected = rows.find((auction) => auction.id === selectedId) ?? null;
useEffect(() => {
// Before the first answer there is nothing to match a restored selection
// against, so leave it alone rather than clear it.
if (auctions.length === 0 && (status === "idle" || status === "loading")) return;
if (rows.length === 0) {
if (selectedId !== null) setSelectedId(null);
setDetailOpen(false);
return;
}
if (!selectedId || !rows.some((auction) => auction.id === selectedId)) {
setSelectedId(rows[0]!.id);
}
}, [auctions.length, rows, selectedId, status]);
const selectFilter = useCallback((next: AuctionFilter) => {
setFilter(next);
setDetailOpen(false);
}, []);
const cycleFilter = useCallback(() => {
setFilter((current) => nextFilter(current));
setDetailOpen(false);
}, []);
const cycleSort = useCallback((step: 1 | -1) => {
setSortPreference((current) => {
const next = cycleSortPreference(AUCTION_SORT_COLUMN_IDS, current, step);
return { columnId: next.columnId ?? DEFAULT_AUCTION_SORT.columnId, direction: next.direction };
});
}, []);
// Refresh answers in every state, including the failed load that mounts no table.
useShortcut((event) => {
if (!isPlainKey(event, "r")) return;
event.preventDefault();
load(true);
}, { enabled: focused && !searchFocused });
const handlePaneKey = useCallback((event: DataTableKeyEvent): boolean => {
if (isPlainKey(event, "f")) {
stopSearchFocusNavigation(event);
cycleFilter();
return true;
}
if (isPlainKey(event, "]") || isPlainKey(event, "[")) {
stopSearchFocusNavigation(event);
cycleSort(event.name === "]" ? 1 : -1);
return true;
}
return false;
}, [cycleFilter, cycleSort]);
const handleRootKeyDown = useCallback((
event: DataTableKeyEvent,
context: DataTableRootKeyContext,
) => {
if (context.selectedIndex <= 0 && isPlainArrowUp(event)) {
stopSearchFocusNavigation(event);
focusSearch();
return true;
}
if (isPlainKey(event, "/")) {
stopSearchFocusNavigation(event);
focusSearch();
return true;
}
return handlePaneKey(event);
}, [focusSearch, handlePaneKey]);
const columns = useMemo(() => buildAuctionColumns(), []);
usePaneFooter(TREASURY_AUCTIONS_PANE_ID, () => {
const info: PaneFooterSegment[] = [];
if (status === "loading") info.push({ id: "loading", parts: [{ text: "loading", tone: "muted" }] });
if (error) info.push({ id: "error", parts: [{ text: error, tone: "warning" }] });
if (stale) info.push({ id: "stale", parts: [{ text: "stale cache", tone: "warning" }] });
if (fetchedAt) {
info.push({ id: "updated", parts: [{ text: formatRelativeAge(fetchedAt), tone: "muted" }] });
}
return {
info,
hints: detailOpen || auctions.length === 0
? []
: [
{ id: "search", key: "/", label: "search", onPress: focusSearch },
{ id: "filter", key: "f", label: "ilter", onPress: cycleFilter },
],
};
}, [
auctions.length,
cycleFilter,
detailOpen,
error,
fetchedAt,
focusSearch,
stale,
status,
]);
// The pane menu names the sort keys; "Sort by…" there picks a column directly.
usePaneMenuItems("treasury-auctions:sort-keys", () => detailOpen || auctions.length === 0 ? null : [
{ id: "sort-next", label: "Next Sort Column", accelerator: "]", onSelect: () => cycleSort(1) },
{ id: "sort-previous", label: "Previous Sort Column", accelerator: "[", onSelect: () => cycleSort(-1) },
], [auctions.length, cycleSort, detailOpen]);
const filterTabs = useMemo(
() => AUCTION_FILTERS.map((entry) => ({ label: entry.label, value: entry.value })),
[],
);
const tabsInHeader = usePaneHeaderTabs({
tabs: filterTabs,
activeValue: filter,
onSelect: (value) => selectFilter(value as AuctionFilter),
focused: focused && !detailOpen && !searchFocused,
});
const tabRows = tabsInHeader ? 0 : 1;
const tabs = tabsInHeader ? null : (
selectFilter(value as AuctionFilter)}
compact
variant="bare"
focused={focused && !detailOpen && !searchFocused}
/>
);
if (status === "loading" && auctions.length === 0) {
return (
{tabs}
);
}
if (error && auctions.length === 0) {
return (
{tabs}
);
}
return (
{tabs}
focused={focused && !searchFocused}
detailOpen={detailOpen && !!selected}
onBack={() => setDetailOpen(false)}
detailContent={selected ? : null}
detailTitle={selected ? `${selected.secType} ${selected.securityTerm}` : undefined}
rootBefore={(
{ if (active) focusSearch(); else blurSearch(); },
focusToken: searchFocusToken,
inputRef: searchInputRef,
onNavigateDown: blurSearch,
}}
/>
)}
onRootKeyDown={handleRootKeyDown}
onDetailKeyDown={handlePaneKey}
selection={{
kind: "id",
selectedId,
getId: (auction) => auction.id,
onChange: (id) => setSelectedId(id),
}}
onActivate={() => {
blurSearch();
setDetailOpen(true);
}}
rootWidth={width}
rootHeight={Math.max(1, height - tabRows)}
columns={columns}
items={rows}
sortColumnId={sortPreference.columnId}
sortDirection={sortPreference.direction}
onHeaderClick={(columnId) => setSortPreference((current) => (
nextAuctionSort(current, columnId as AuctionColumnId)
))}
getItemKey={auctionKey}
renderCell={renderAuctionRow}
emptyStateTitle={searchQuery.trim() ? "No matching auctions." : "No recent auctions."}
emptyStateHint={searchQuery.trim() ? "Clear search." : undefined}
/>
);
}