import { useState, useEffect } from 'react'; import { RankingTable } from './RankingTable'; import { UserDetailCard } from './UserDetailCard'; interface Period { index: number; startSnapshotId: number | null; startAt: string | null; endAt: string | null; isCurrent: boolean; } interface PeriodSummary { period: { index: number; startAt: string | null; endAt: string | null; isCurrent: boolean; }; totals: { totalCost: number; userCount: number; }; ranking: Array<{ id: string; name: string; cost: number; share: number; isMe: boolean; rawStart: any; rawEnd: any; periodTokens: number; periodRequests: number; }>; } interface HistoricalPeriodsProps { periods: Period[]; apiKey: string; userId: string; } interface PeriodOption { period: Period; totalCost: number | null; // null means still loading } export function HistoricalPeriods({ periods, apiKey, userId }: HistoricalPeriodsProps) { const [selectedPeriod, setSelectedPeriod] = useState(null); const [summary, setSummary] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); const [periodOptions, setPeriodOptions] = useState([]); const [isSelectOpen, setIsSelectOpen] = useState(false); // Fetch total costs for all periods useEffect(() => { const fetchAllPeriodCosts = async () => { if (periods.length === 0) return; // Initialize with loading state (totalCost: null) const initialOptions: PeriodOption[] = periods.map(period => ({ period, totalCost: null })); setPeriodOptions(initialOptions); if (!selectedPeriod && initialOptions.length > 0) { setSelectedPeriod(initialOptions[0].period); } // Fetch costs individually to update progressively const updatedOptions = [...initialOptions]; for (let i = 0; i < periods.length; i++) { const period = periods[i]; try { const response = await fetch(`/api/periods/${period.index}/summary`, { headers: { 'X-API-Key': apiKey }, }); if (response.ok) { const data = await response.json(); updatedOptions[i] = { period, totalCost: data.totals?.totalCost || 0 }; } else { updatedOptions[i] = { period, totalCost: 0 }; } } catch { updatedOptions[i] = { period, totalCost: 0 }; } // Update state after each fetch for progressive loading setPeriodOptions([...updatedOptions]); } }; fetchAllPeriodCosts(); }, [periods, apiKey]); useEffect(() => { if (periods.length > 0 && !selectedPeriod) { setSelectedPeriod(periods[0] || null); // Select the most recent historical period } }, [periods]); useEffect(() => { if (selectedPeriod) { fetchSummary(selectedPeriod.index); } }, [selectedPeriod]); const fetchSummary = async (periodIndex: number) => { setIsLoading(true); setError(''); try { const response = await fetch(`/api/periods/${periodIndex}/summary`, { headers: { 'X-API-Key': apiKey, }, }); if (!response.ok) { throw new Error(`Failed to fetch period summary: ${response.status}`); } const data = await response.json(); setSummary(data); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load period data'); setSummary(null); } finally { setIsLoading(false); } }; const formatDate = (dateString: string | null, isFirstPeriod: boolean = false) => { if (!dateString) return isFirstPeriod ? 'Beginning' : 'Unknown'; const date = new Date(dateString); return date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, }); }; const formatDateRange = (startAt: string | null, endAt: string | null, isFirstPeriod: boolean = false) => { return `${formatDate(startAt, isFirstPeriod)} → ${formatDate(endAt)}`; }; const formatCurrency = (amount: number) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(amount); }; if (periods.length === 0) { return (

No Historical Periods

Historical periods will appear here after you create billing snapshots using the bun begin-period command.

); } const myUser = summary?.ranking.find(u => u.isMe); return (
{/* Period Selector */}
{isSelectOpen && (
{periodOptions.map((option) => ( ))}
)}
{/* Click overlay to close dropdown */} {isSelectOpen && (
setIsSelectOpen(false)} /> )}
{selectedPeriod && ( <> {/* Period Summary */} {isLoading ? (
) : error ? (

Failed to Load Period Data

{error}

) : summary && ( <>

Period #{selectedPeriod.index}: {formatDateRange(summary.period.startAt, summary.period.endAt, selectedPeriod.index === 0)}

{formatCurrency(summary.totals.totalCost)}
Total Cost
{summary.totals.userCount}
Active Users
{myUser ? formatCurrency(myUser.cost) : '$0.00'}
Your Cost ({myUser ? (myUser.share * 100).toFixed(2) : '0.00'}%)
{/* Ranking Table */} {/* User Detail Card */} )} )}
); }