import { useQuery } from '@tanstack/react-query';
import { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
Package,
Clock,
CheckCircle,
XCircle,
DollarSign,
Target,
Zap,
AlertTriangle,
TrendingDown,
FileSpreadsheet,
FileText,
Loader2,
Eye,
RotateCcw,
Calculator,
Calendar,
ChevronDown,
} from 'lucide-react';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
import { Button } from '../components/Button';
import { useToast } from '../contexts/ToastContext';
import { useAuth } from '../contexts/AuthContext';
import { api, type Archive } from '../api/client';
import { PrintCalendar } from '../components/PrintCalendar';
import { FilamentTrends } from '../components/FilamentTrends';
import { Dashboard, type DashboardWidget } from '../components/Dashboard';
import { getCurrencySymbol } from '../utils/currency';
import { formatWeight } from '../utils/weight';
import { parseUTCDate, formatDuration } from '../utils/date';
import { MetricToggle, type Metric } from '../components/MetricToggle';
// Timeframe types and helpers
type TimeframePreset = 'today' | 'this-week' | 'this-month' | 'last-7' | 'last-30' | 'last-90' | 'this-year' | 'all-time' | 'custom';
interface TimeframeState {
preset: TimeframePreset;
dateFrom: string | undefined; // YYYY-MM-DD
dateTo: string | undefined; // YYYY-MM-DD
}
function computeDateRange(preset: TimeframePreset): { dateFrom?: string; dateTo?: string } {
const now = new Date();
const y = now.getUTCFullYear(), m = now.getUTCMonth(), d = now.getUTCDate();
const fmt = (dt: Date) => dt.toISOString().split('T')[0];
const todayStr = fmt(now);
switch (preset) {
case 'today':
return { dateFrom: todayStr, dateTo: todayStr };
case 'this-week': {
const day = now.getUTCDay();
const start = new Date(Date.UTC(y, m, d - (day === 0 ? 6 : day - 1)));
return { dateFrom: fmt(start), dateTo: todayStr };
}
case 'this-month':
return { dateFrom: fmt(new Date(Date.UTC(y, m, 1))), dateTo: todayStr };
case 'last-7':
return { dateFrom: fmt(new Date(Date.UTC(y, m, d - 6))), dateTo: todayStr };
case 'last-30':
return { dateFrom: fmt(new Date(Date.UTC(y, m, d - 29))), dateTo: todayStr };
case 'last-90':
return { dateFrom: fmt(new Date(Date.UTC(y, m, d - 89))), dateTo: todayStr };
case 'this-year':
return { dateFrom: fmt(new Date(Date.UTC(y, 0, 1))), dateTo: todayStr };
case 'all-time':
return { dateFrom: undefined, dateTo: undefined };
case 'custom':
return {};
}
}
const TIMEFRAME_PRESETS: TimeframePreset[] = [
'today', 'this-week', 'this-month',
'last-7', 'last-30', 'last-90',
'this-year', 'all-time',
];
// Constants
const DAY_LABELS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const HOUR_LABELS = [
'12am', '1am', '2am', '3am', '4am', '5am',
'6am', '7am', '8am', '9am', '10am', '11am',
'12pm', '1pm', '2pm', '3pm', '4pm', '5pm',
'6pm', '7pm', '8pm', '9pm', '10pm', '11pm',
];
const DURATION_BUCKETS = [
{ key: '<30m', max: 1800 },
{ key: '30m-1h', max: 3600 },
{ key: '1-2h', max: 7200 },
{ key: '2-4h', max: 14400 },
{ key: '4-8h', max: 28800 },
{ key: '8-12h', max: 43200 },
{ key: '12-24h', max: 86400 },
{ key: '24h+', max: Infinity },
];
const RECHARTS_TOOLTIP_STYLE = {
backgroundColor: '#2d2d2d',
border: '1px solid #3d3d3d',
borderRadius: '8px',
};
// Widget Components
function QuickStatsWidget({
stats,
currency,
}: {
stats: {
total_prints: number;
successful_prints: number;
failed_prints: number;
total_print_time_hours: number;
total_filament_grams: number;
total_cost: number;
total_energy_kwh: number;
total_energy_cost: number;
} | undefined;
currency: string;
}) {
const { t } = useTranslation();
return (
{t('stats.totalPrints')}
{stats?.total_prints || 0}
{t('stats.printTime')}
{stats?.total_print_time_hours.toFixed(1) || 0}h
{t('stats.filamentUsed')}
{formatWeight(stats?.total_filament_grams || 0)}
{t('stats.filamentCost')}
{currency} {stats?.total_cost.toFixed(2) || '0.00'}
{t('stats.energyUsed')}
{stats?.total_energy_kwh.toFixed(3) || '0.000'} kWh
{t('stats.energyCost')}
{currency} {stats?.total_energy_cost.toFixed(2) || '0.00'}
);
}
function SuccessRateWidget({
stats,
printerMap,
size = 1,
}: {
stats: {
total_prints: number;
successful_prints: number;
failed_prints: number;
prints_by_printer: Record;
} | undefined;
printerMap: Map;
size?: 1 | 2 | 4;
}) {
const { t } = useTranslation();
const completedAndFailed = (stats?.successful_prints || 0) + (stats?.failed_prints || 0);
const successRate = completedAndFailed
? Math.round((stats!.successful_prints / completedAndFailed) * 100)
: 0;
// Scale gauge size based on widget size
const gaugeSize = size === 1 ? 112 : size === 2 ? 128 : 144;
const radius = gaugeSize / 2 - 8;
const circumference = radius * 2 * Math.PI;
return (
= 2 ? 'text-2xl' : 'text-xl'}`}>{successRate}%
{t('stats.successful')}
{stats?.successful_prints || 0}
{t('stats.failed')}
{stats?.failed_prints || 0}
{/* Show per-printer breakdown when expanded */}
{size >= 2 && stats?.prints_by_printer && Object.keys(stats.prints_by_printer).length > 0 && (
{t('stats.printsByPrinter')}
{Object.entries(stats.prints_by_printer).map(([printerId, count]) => (
{printerMap.get(printerId) || `${t('common.printer')} ${printerId}`}
{count}
))}
)}
);
}
function TimeAccuracyWidget({
stats,
printerMap,
size = 1,
}: {
stats: {
average_time_accuracy: number | null;
time_accuracy_by_printer: Record | null;
} | undefined;
printerMap: Map;
size?: 1 | 2 | 4;
}) {
const { t } = useTranslation();
const accuracy = stats?.average_time_accuracy;
if (accuracy === null || accuracy === undefined) {
return (
{t('stats.noTimeAccuracyData')}
);
}
// Normalize accuracy for display (100% = perfect, clamp between 50-150 for gauge)
const displayValue = Math.min(150, Math.max(50, accuracy));
const normalizedForGauge = ((displayValue - 50) / 100) * 100; // 50-150 -> 0-100
// Color based on accuracy
const getColor = (acc: number) => {
if (acc >= 95 && acc <= 105) return '#00ae42'; // Green - within 5%
if (acc > 105) return '#3b82f6'; // Blue - faster than expected
return '#f97316'; // Orange - slower than expected
};
const color = getColor(accuracy);
const deviation = accuracy - 100;
// Scale gauge size based on widget size
const gaugeSize = size === 1 ? 112 : size === 2 ? 128 : 144;
const radius = gaugeSize / 2 - 8;
const circumference = radius * 2 * Math.PI;
// Show more printers when expanded
const maxPrinters = size === 1 ? 3 : size === 2 ? 6 : 999;
const printerEntries = stats?.time_accuracy_by_printer
? Object.entries(stats.time_accuracy_by_printer).slice(0, maxPrinters)
: [];
return (
= 2 ? 'text-2xl' : 'text-xl'}`}>{accuracy.toFixed(0)}%
= 0 ? 'text-blue-400' : 'text-orange-400'}`}>
{deviation >= 0 ? '+' : ''}{deviation.toFixed(0)}%
{t('stats.perfectEstimate')}
{printerEntries.length > 0 && (
{printerEntries.map(([printerId, acc]) => (
{printerMap.get(printerId) || `${t('common.printer')} ${printerId}`}
= 95 && acc <= 105 ? 'text-status-ok' :
acc > 105 ? 'text-blue-400' : 'text-status-warning'
}`}>
{acc.toFixed(0)}%
))}
)}
);
}
function PrintActivityWidget({
printDates,
size = 2,
}: {
printDates: string[];
size?: 1 | 2 | 4;
}) {
// Show more months when widget is larger - cell size auto-calculated
const months = size === 1 ? 3 : size === 2 ? 6 : 12;
return ;
}
function PrinterStatsWidget({
stats,
archives,
printerMap,
}: {
stats: { prints_by_printer: Record } | undefined;
archives: Archive[];
printerMap: Map;
}) {
const { t } = useTranslation();
const [printerMetric, setPrinterMetric] = useState('weight');
const [habitsMetric, setHabitsMetric] = useState('weight');
// Per-printer data
const printerData = useMemo(() => {
const map = new Map();
if (stats?.prints_by_printer) {
Object.entries(stats.prints_by_printer).forEach(([id, count]) => {
const entry = map.get(id) || { prints: 0, weight: 0, time: 0 };
entry.prints = count;
map.set(id, entry);
});
}
archives.forEach(a => {
if (!a.printer_id) return;
const id = String(a.printer_id);
const entry = map.get(id) || { prints: 0, weight: 0, time: 0 };
entry.weight += a.filament_used_grams || 0;
entry.time += a.actual_time_seconds || a.print_time_seconds || 0;
if (!stats?.prints_by_printer) entry.prints++;
map.set(id, entry);
});
return Array.from(map.entries())
.map(([id, v]) => ({
name: printerMap.get(id) || `${t('common.printer')} ${id}`,
value: printerMetric === 'prints' ? v.prints :
printerMetric === 'weight' ? Math.round(v.weight) :
Math.round((v.time / 3600) * 10) / 10,
}))
.sort((a, b) => b.value - a.value);
}, [stats, archives, printerMap, printerMetric, t]);
// Hourly distribution (time of day)
const hourlyData = useMemo(() => {
const hours = Array.from({ length: 24 }, (_, i) => ({
hour: i,
label: HOUR_LABELS[i],
total: 0,
failures: 0,
}));
archives.forEach(a => {
if (!a.started_at) return;
const date = parseUTCDate(a.started_at);
if (!date) return;
const h = date.getHours();
hours[h].total++;
if (a.status === 'failed') {
hours[h].failures++;
}
});
return hours;
}, [archives]);
// Duration distribution
const durationData = useMemo(() => {
const counts = DURATION_BUCKETS.map(b => ({ name: b.key, count: 0 }));
archives.forEach(a => {
const seconds = a.actual_time_seconds || a.print_time_seconds;
if (!seconds || seconds <= 0) return;
for (let i = 0; i < DURATION_BUCKETS.length; i++) {
if (seconds <= DURATION_BUCKETS[i].max) {
counts[i].count++;
break;
}
}
});
return counts;
}, [archives]);
// Habits (avg per day-of-week)
const habitsData = useMemo(() => {
const dayValues = [0, 0, 0, 0, 0, 0, 0];
const weeksSet = new Set();
archives.forEach(a => {
const date = parseUTCDate(a.created_at) || new Date(a.created_at);
let day = date.getDay() - 1;
if (day < 0) day = 6;
if (habitsMetric === 'prints') dayValues[day]++;
else if (habitsMetric === 'weight') dayValues[day] += a.filament_used_grams || 0;
else dayValues[day] += (a.actual_time_seconds || a.print_time_seconds || 0) / 3600;
const weekStart = new Date(date);
weekStart.setDate(date.getDate() - ((date.getDay() + 6) % 7));
weeksSet.add(weekStart.toISOString().split('T')[0]);
});
const numWeeks = Math.max(weeksSet.size, 1);
return DAY_LABELS.map((name, i) => ({
name,
avg: Math.round((dayValues[i] / numWeeks) * 10) / 10,
}));
}, [archives, habitsMetric]);
const pUnit = printerMetric === 'weight' ? 'g' : printerMetric === 'time' ? 'h' : '';
const pLabel = printerMetric === 'weight' ? t('stats.filamentByWeight') : printerMetric === 'time' ? t('stats.hours') : t('common.prints');
const pColor = printerMetric === 'weight' ? '#00ae42' : printerMetric === 'time' ? '#3b82f6' : '#f59e0b';
const hUnit = habitsMetric === 'weight' ? 'g' : habitsMetric === 'time' ? 'h' : '';
const hLabel = habitsMetric === 'weight' ? t('stats.avgWeight') : habitsMetric === 'time' ? t('stats.avgTime') : t('stats.avgPrints');
const hColor = habitsMetric === 'weight' ? '#00ae42' : habitsMetric === 'time' ? '#3b82f6' : '#f59e0b';
return (
{/* By Printer */}
{t('stats.printsByPrinter')}
{printerData.length > 0 ? (
[
printerMetric === 'weight' ? formatWeight(Number(v ?? 0)) : `${v ?? 0}${pUnit}`,
pLabel,
]}
/>
) : (
{t('stats.noPrinterData')}
)}
{/* Print Duration */}
{t('stats.printDuration')}
{archives.length > 0 ? (
) : (
{t('stats.noArchiveData')}
)}
{/* Print Habits */}
{t('stats.printHabits')}
{archives.length > 0 ? (
[`${v ?? 0}${hUnit}`, hLabel]} />
) : (
{t('stats.noArchiveData')}
)}
{/* Print Time of Day */}
{t('stats.printTimeOfDay')}
{archives.length > 0 ? (
) : (
{t('stats.noArchiveData')}
)}
);
}
function FilamentTrendsWidget({
archives,
currency,
}: {
archives: Parameters[0]['archives'];
currency: string;
}) {
const { t } = useTranslation();
if (!archives || archives.length === 0) {
return {t('stats.noPrintData')}
;
}
return ;
}
function FailureAnalysisWidget({ size = 1, dateFrom, dateTo }: {
size?: 1 | 2 | 4;
dateFrom?: string;
dateTo?: string;
}) {
const { t } = useTranslation();
const hasDateRange = !!(dateFrom || dateTo);
const { data: analysis, isLoading } = useQuery({
queryKey: ['failureAnalysis', dateFrom, dateTo],
queryFn: () => api.getFailureAnalysis({
...(hasDateRange ? { dateFrom, dateTo } : { days: 30 }),
}),
});
if (isLoading) {
return (
);
}
if (!analysis || analysis.total_prints === 0) {
return {hasDateRange ? t('stats.noPrintDataInRange') : t('stats.noPrintDataLast30Days')}
;
}
// Show more reasons when expanded
const maxReasons = size === 1 ? 5 : size === 2 ? 8 : 999;
const allReasons = Object.entries(analysis.failures_by_reason).sort(([, a], [, b]) => b - a);
const topReasons = allReasons.slice(0, maxReasons);
const hasMore = allReasons.length > maxReasons;
return (
= 2 ? 'flex gap-8' : 'space-y-4'}`}>
{/* Summary */}
= 2 ? 'flex-shrink-0' : ''}>
20 ? 'text-status-error' : analysis.failure_rate > 10 ? 'text-status-warning' : 'text-status-ok'}`} />
= 2 ? 'text-3xl' : 'text-2xl'}`}>{analysis.failure_rate.toFixed(1)}%
{t('stats.failedPrintsCount', { failed: analysis.failed_prints, total: analysis.total_prints })}
{/* Trend indicator */}
{analysis.trend && analysis.trend.length >= 2 && (
= 2 ? 'mt-4' : 'mt-2 pt-2 border-t border-bambu-dark-tertiary'}`}>
{t('stats.lastWeekRate', { rate: analysis.trend[analysis.trend.length - 1].failure_rate.toFixed(1) })}
)}
{/* Failure Reasons */}
{topReasons.length > 0 && (
= 2 ? 'border-l border-bambu-dark-tertiary pl-8' : 'pt-2'}`}>
{size >= 2 ? t('stats.failureReasons') : t('stats.topFailureReasons')}
{topReasons.map(([reason, count]) => (
{reason || t('common.unknown')}
{count}
))}
{hasMore && (
{t('common.more', { count: allReasons.length - maxReasons })}
)}
)}
);
}
function RecordsWidget({ archives, currency }: { archives: Archive[]; currency: string }) {
const { t } = useTranslation();
const records = useMemo(() => {
const result: Array<{
icon: typeof Clock;
iconColor: string;
label: string;
value: string;
detail: string | null;
}> = [];
if (archives.length === 0) return result;
// Longest print
let longestArchive: Archive | null = null;
let longestTime = 0;
archives.forEach(a => {
if (a.actual_time_seconds && a.actual_time_seconds > longestTime) {
longestTime = a.actual_time_seconds;
longestArchive = a;
}
});
if (longestArchive && longestTime > 0) {
result.push({
icon: Clock,
iconColor: 'text-blue-400',
label: t('stats.longestPrint'),
value: formatDuration(longestTime),
detail: (longestArchive as Archive).print_name || null,
});
}
// Heaviest print
let heaviestArchive: Archive | null = null;
let heaviestWeight = 0;
archives.forEach(a => {
if (a.filament_used_grams && a.filament_used_grams > heaviestWeight) {
heaviestWeight = a.filament_used_grams;
heaviestArchive = a;
}
});
if (heaviestArchive && heaviestWeight > 0) {
result.push({
icon: Package,
iconColor: 'text-orange-400',
label: t('stats.heaviestPrint'),
value: formatWeight(heaviestWeight),
detail: (heaviestArchive as Archive).print_name || null,
});
}
// Most expensive print
let costliestArchive: Archive | null = null;
let highestCost = 0;
archives.forEach(a => {
if (a.cost && a.cost > highestCost) {
highestCost = a.cost;
costliestArchive = a;
}
});
if (costliestArchive && highestCost > 0) {
result.push({
icon: DollarSign,
iconColor: 'text-green-400',
label: t('stats.mostExpensivePrint'),
value: `${currency}${highestCost.toFixed(2)}`,
detail: (costliestArchive as Archive).print_name || null,
});
}
// Busiest day
const dayCounts = new Map();
archives.forEach(a => {
const date = parseUTCDate(a.created_at) || new Date(a.created_at);
const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
dayCounts.set(key, (dayCounts.get(key) || 0) + 1);
});
let busiestDay = '';
let busiestCount = 0;
dayCounts.forEach((count, day) => {
if (count > busiestCount) {
busiestCount = count;
busiestDay = day;
}
});
if (busiestCount > 1) {
result.push({
icon: Calendar,
iconColor: 'text-purple-400',
label: t('stats.busiestDay'),
value: `${busiestCount} ${t('common.prints')}`,
detail: new Date(busiestDay).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }),
});
}
// Success streak
const sorted = [...archives]
.filter(a => a.status === 'completed' || a.status === 'failed')
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
let streak = 0;
for (const a of sorted) {
if (a.status === 'completed') streak++;
else break;
}
if (streak > 0) {
result.push({
icon: Zap,
iconColor: 'text-yellow-400',
label: t('stats.successStreak'),
value: `${streak}`,
detail: streak === 1 ? t('stats.streakPrint') : t('stats.streakPrints', { count: streak }),
});
}
return result;
}, [archives, currency, t]);
if (records.length === 0) {
return {t('stats.noArchiveData')}
;
}
return (
{records.map((record, i) => (
{record.label}
{record.value}
{record.detail && (
{record.detail}
)}
))}
);
}
export function StatsPage() {
const { t } = useTranslation();
const { showToast } = useToast();
const { hasPermission } = useAuth();
const [isExporting, setIsExporting] = useState(false);
const [showExportMenu, setShowExportMenu] = useState(false);
const [dashboardKey, setDashboardKey] = useState(0);
const [hiddenCount, setHiddenCount] = useState(0);
const [isRecalculating, setIsRecalculating] = useState(false);
const [timeframe, setTimeframe] = useState({
preset: 'all-time',
dateFrom: undefined,
dateTo: undefined,
});
const [showTimeframePicker, setShowTimeframePicker] = useState(false);
const effectiveDateRange = useMemo(() => {
if (timeframe.preset === 'custom') {
return { dateFrom: timeframe.dateFrom, dateTo: timeframe.dateTo };
}
return computeDateRange(timeframe.preset);
}, [timeframe]);
// Read hidden count from localStorage
useEffect(() => {
const updateHiddenCount = () => {
try {
const saved = localStorage.getItem('bambusy-dashboard-layout-v2');
if (saved) {
const layout = JSON.parse(saved);
setHiddenCount(layout.hidden?.length || 0);
}
} catch {
setHiddenCount(0);
}
};
updateHiddenCount();
// Listen for storage changes
window.addEventListener('storage', updateHiddenCount);
// Also poll for changes (since storage event doesn't fire for same-tab changes)
const interval = setInterval(updateHiddenCount, 2000);
return () => {
window.removeEventListener('storage', updateHiddenCount);
clearInterval(interval);
};
}, [dashboardKey]);
const { data: stats, isLoading, refetch: refetchStats } = useQuery({
queryKey: ['archiveStats', effectiveDateRange.dateFrom, effectiveDateRange.dateTo],
queryFn: () => api.getArchiveStats({
dateFrom: effectiveDateRange.dateFrom,
dateTo: effectiveDateRange.dateTo,
}),
});
const { data: printers } = useQuery({
queryKey: ['printers'],
queryFn: api.getPrinters,
});
const { data: archives, refetch: refetchArchives } = useQuery({
queryKey: ['archives', effectiveDateRange.dateFrom, effectiveDateRange.dateTo],
queryFn: () => api.getArchives(undefined, undefined, 10000, 0, effectiveDateRange.dateFrom, effectiveDateRange.dateTo),
});
const { data: settings } = useQuery({
queryKey: ['settings'],
queryFn: api.getSettings,
});
const handleExport = async (format: 'csv' | 'xlsx') => {
setShowExportMenu(false);
setIsExporting(true);
try {
const { blob, filename } = await api.exportStats({ format, days: 90 });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
showToast(t('stats.exportDownloaded'));
} catch {
showToast(t('stats.exportFailed'), 'error');
} finally {
setIsExporting(false);
}
};
const handleRecalculateCosts = async () => {
setIsRecalculating(true);
try {
const result = await api.recalculateCosts();
await Promise.all([refetchStats(), refetchArchives()]);
showToast(t('stats.recalculatedCosts', { count: result.updated }));
} catch {
showToast(t('stats.recalculateFailed'), 'error');
} finally {
setIsRecalculating(false);
}
};
const currency = getCurrencySymbol(settings?.currency || 'USD');
const printerMap = new Map(printers?.map((p) => [String(p.id), p.name]) || []);
const printDates = useMemo(() => archives?.map((a) => a.created_at) || [], [archives]);
if (isLoading) {
return (
{t('stats.loadingStats')}
);
}
// Define dashboard widgets
// Sizes: 1 = quarter (1/4), 2 = half (1/2), 4 = full width
// Widgets can use render functions to receive the current size for responsive content
const widgets: DashboardWidget[] = [
{
id: 'quick-stats',
title: t('stats.quickStats'),
component: ,
defaultSize: 2,
},
{
id: 'success-rate',
title: t('stats.successRate'),
component: (size) => ,
defaultSize: 1,
},
{
id: 'time-accuracy',
title: t('stats.timeAccuracy'),
component: (size) => ,
defaultSize: 1,
},
{
id: 'failure-analysis',
title: t('stats.failureAnalysis'),
component: (size) => ,
defaultSize: 1,
},
{
id: 'print-activity',
title: t('stats.printActivity'),
component: (size) => ,
defaultSize: 2,
},
{
id: 'records',
title: t('stats.records'),
component: ,
defaultSize: 1,
},
{
id: 'printer-stats',
title: t('stats.printerStats'),
component: ,
defaultSize: 4,
},
{
id: 'filament-trends',
title: t('stats.filamentTrends'),
component: ,
defaultSize: 4,
},
];
return (
{t('stats.title')}
{t('stats.subtitle')}
{/* Hidden widgets button - toggles panel in Dashboard */}
{hiddenCount > 0 && (
)}
{/* Reset Layout */}
{/* Recalculate Costs */}
{/* Export dropdown */}
{showExportMenu && (
)}
{/* Timeframe Selector */}
{showTimeframePicker && (
<>
setShowTimeframePicker(false)}
/>
{TIMEFRAME_PRESETS.map((preset) => (
))}
{timeframe.preset === 'custom' && (
)}
>
)}
);
}