"use client"; import { useState, useEffect, useCallback, useMemo, Fragment } from "react"; import PropTypes from "prop-types"; import Card from "@/shared/components/Card"; import Badge from "@/shared/components/Badge"; const fmt = (n) => new Intl.NumberFormat().format(n || 0); const fmtCost = (n) => `$${(n || 0).toFixed(2)}`; function fmtTime(iso) { if (!iso) return "Never"; const diffMins = Math.floor((Date.now() - new Date(iso)) / 60000); if (diffMins < 1) return "Just now"; if (diffMins < 60) return `${diffMins}m ago`; if (diffMins < 1440) return `${Math.floor(diffMins / 60)}h ago`; return new Date(iso).toLocaleDateString(); } function SortIcon({ field, currentSort, currentOrder }) { if (currentSort !== field) return ; return {currentOrder === "asc" ? "↑" : "↓"}; } SortIcon.propTypes = { field: PropTypes.string.isRequired, currentSort: PropTypes.string.isRequired, currentOrder: PropTypes.string.isRequired, }; /** * Render 3 token or cost cells based on viewMode */ function ValueCells({ item, viewMode, isSummary = false }) { if (viewMode === "tokens") { return ( <> {isSummary && item.promptTokens === undefined ? "—" : fmt(item.promptTokens)} {isSummary && item.completionTokens === undefined ? "—" : fmt(item.completionTokens)} {fmt(item.totalTokens)} ); } return ( <> {isSummary && item.inputCost === undefined ? "—" : fmtCost(item.inputCost)} {isSummary && item.outputCost === undefined ? "—" : fmtCost(item.outputCost)} {fmtCost(item.totalCost || item.cost)} ); } ValueCells.propTypes = { item: PropTypes.object.isRequired, viewMode: PropTypes.string.isRequired, isSummary: PropTypes.bool, }; /** * Reusable sortable usage table with expandable group rows. * * @param {object} props * @param {string} props.title - Table title * @param {Array} props.columns - Column definitions [{field, label}] * @param {Array} props.groupedData - Grouped data from groupDataByKey * @param {string} props.tableType - Table type key for sort URL params * @param {string} props.sortBy - Current sort field * @param {string} props.sortOrder - Current sort order * @param {function} props.onToggleSort - Sort toggle handler * @param {string} props.viewMode - "tokens" or "costs" * @param {string} props.storageKey - localStorage key for expanded state * @param {function} props.renderGroupLabel - Render group summary first cell content * @param {function} props.renderDetailCells - Render detail row custom cells (before value cells) * @param {function} props.renderSummaryCells - Render summary row cells after group label (placeholder cols) * @param {string} props.emptyMessage - Empty state message */ export default function UsageTable({ title, columns, groupedData, tableType, sortBy, sortOrder, onToggleSort, viewMode, storageKey, renderDetailCells, renderSummaryCells, emptyMessage, }) { const [expanded, setExpanded] = useState(new Set()); // Load expanded state from localStorage useEffect(() => { try { const saved = localStorage.getItem(storageKey); if (saved) setExpanded(new Set(JSON.parse(saved))); } catch (e) { console.error(`Failed to load ${storageKey}:`, e); } }, [storageKey]); // Save expanded state to localStorage useEffect(() => { try { localStorage.setItem(storageKey, JSON.stringify([...expanded])); } catch (e) { console.error(`Failed to save ${storageKey}:`, e); } }, [expanded, storageKey]); const toggleGroup = useCallback((groupKey) => { setExpanded((prev) => { const next = new Set(prev); next.has(groupKey) ? next.delete(groupKey) : next.add(groupKey); return next; }); }, []); const valueColumns = useMemo(() => { if (viewMode === "tokens") { return [ { field: "promptTokens", label: "Input Tokens" }, { field: "completionTokens", label: "Output Tokens" }, { field: "totalTokens", label: "Total Tokens" }, ]; } return [ { field: "promptTokens", label: "Input Cost" }, { field: "completionTokens", label: "Output Cost" }, { field: "cost", label: "Total Cost" }, ]; }, [viewMode]); const totalColSpan = columns.length + valueColumns.length; return (

{title}

{columns.map((col) => ( ))} {valueColumns.map((col) => ( ))} {groupedData.map((group) => ( {/* Group summary row */} toggleGroup(group.groupKey)} > {renderSummaryCells(group)} {/* Detail rows */} {expanded.has(group.groupKey) && group.items.map((item) => ( {renderDetailCells(item)} ))} ))} {groupedData.length === 0 && ( )}
onToggleSort(tableType, col.field)} > {col.label}{" "} onToggleSort(tableType, col.field)} > {col.label}{" "}
chevron_right 0 ? "text-primary" : ""}`}> {group.groupKey}
{emptyMessage}
); } UsageTable.propTypes = { title: PropTypes.string.isRequired, columns: PropTypes.arrayOf(PropTypes.shape({ field: PropTypes.string.isRequired, label: PropTypes.string.isRequired, align: PropTypes.string, })).isRequired, groupedData: PropTypes.array.isRequired, tableType: PropTypes.string.isRequired, sortBy: PropTypes.string.isRequired, sortOrder: PropTypes.string.isRequired, onToggleSort: PropTypes.func.isRequired, viewMode: PropTypes.string.isRequired, storageKey: PropTypes.string.isRequired, renderDetailCells: PropTypes.func.isRequired, renderSummaryCells: PropTypes.func.isRequired, emptyMessage: PropTypes.string.isRequired, }; // Re-export utilities for use in UsageStats orchestrator export { fmt, fmtCost, fmtTime };