import { useMemo, useState } from 'react'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell, Legend, } from 'recharts'; import type { Archive } from '../api/client'; import { parseUTCDate } from '../utils/date'; interface FilamentTrendsProps { archives: Archive[]; currency?: string; } type TimeRange = '7d' | '30d' | '90d' | '365d' | 'all'; const COLORS = ['#00ae42', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316']; function getDateRange(range: TimeRange): Date { const now = new Date(); switch (range) { case '7d': return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); case '30d': return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); case '90d': return new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); case '365d': return new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000); case 'all': return new Date(0); } } export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps) { const [timeRange, setTimeRange] = useState('30d'); // Filter archives by time range const filteredArchives = useMemo(() => { const startDate = getDateRange(timeRange); return archives.filter(a => (parseUTCDate(a.completed_at || a.created_at) || new Date(0)) >= startDate); }, [archives, timeRange]); // Calculate daily usage data const dailyData = useMemo(() => { const dataMap = new Map(); filteredArchives.forEach(archive => { const date = parseUTCDate(archive.completed_at || archive.created_at) || new Date(); // Use local date string for grouping const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; const existing = dataMap.get(key) || { date: key, filament: 0, cost: 0, prints: 0 }; const qty = archive.quantity || 1; existing.filament += (archive.filament_used_grams || 0) * qty; existing.cost += archive.cost || 0; existing.prints += qty; dataMap.set(key, existing); }); return Array.from(dataMap.values()) .sort((a, b) => a.date.localeCompare(b.date)) .map(d => ({ ...d, dateLabel: new Date(d.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), })); }, [filteredArchives]); // Calculate weekly aggregated data for longer time ranges const weeklyData = useMemo(() => { if (timeRange === '7d' || timeRange === '30d') return dailyData; const dataMap = new Map(); filteredArchives.forEach(archive => { const date = parseUTCDate(archive.completed_at || archive.created_at) || new Date(); // Get week start (Sunday) const weekStart = new Date(date); weekStart.setDate(date.getDate() - date.getDay()); const key = `${weekStart.getFullYear()}-${String(weekStart.getMonth() + 1).padStart(2, '0')}-${String(weekStart.getDate()).padStart(2, '0')}`; const existing = dataMap.get(key) || { week: key, filament: 0, cost: 0, prints: 0 }; const qty = archive.quantity || 1; existing.filament += (archive.filament_used_grams || 0) * qty; existing.cost += archive.cost || 0; existing.prints += qty; dataMap.set(key, existing); }); return Array.from(dataMap.values()) .sort((a, b) => a.week.localeCompare(b.week)) .map(d => ({ date: d.week, dateLabel: `Week of ${new Date(d.week).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`, ...d, })); }, [filteredArchives, dailyData, timeRange]); // Usage by filament type const filamentTypeData = useMemo(() => { const dataMap = new Map(); filteredArchives.forEach(archive => { const type = archive.filament_type || 'Unknown'; const qty = archive.quantity || 1; // Handle multiple types (e.g., "PLA, PETG") const types = type.split(', '); types.forEach(t => { const grams = ((archive.filament_used_grams || 0) * qty) / types.length; dataMap.set(t, (dataMap.get(t) || 0) + grams); }); }); return Array.from(dataMap.entries()) .map(([name, value]) => ({ name, value: Math.round(value) })) .sort((a, b) => b.value - a.value); }, [filteredArchives]); // Monthly comparison data const monthlyComparison = useMemo(() => { const now = new Date(); const months: { month: string; filament: number; cost: number; prints: number }[] = []; for (let i = 5; i >= 0; i--) { const monthDate = new Date(now.getFullYear(), now.getMonth() - i, 1); const monthEnd = new Date(now.getFullYear(), now.getMonth() - i + 1, 0); const monthStr = monthDate.toLocaleDateString('en-US', { month: 'short', year: '2-digit' }); const monthArchives = archives.filter(a => { const d = parseUTCDate(a.completed_at || a.created_at) || new Date(0); return d >= monthDate && d <= monthEnd; }); months.push({ month: monthStr, filament: Math.round(monthArchives.reduce((sum, a) => sum + (a.filament_used_grams || 0), 0)), cost: monthArchives.reduce((sum, a) => sum + (a.cost || 0), 0), prints: monthArchives.reduce((sum, a) => sum + (a.quantity || 1), 0), }); } return months; }, [archives]); const chartData = timeRange === '7d' || timeRange === '30d' ? dailyData : weeklyData; const totalFilament = filteredArchives.reduce((sum, a) => sum + (a.filament_used_grams || 0), 0); const totalCost = filteredArchives.reduce((sum, a) => sum + (a.cost || 0), 0); const totalPrints = filteredArchives.reduce((sum, a) => sum + (a.quantity || 1), 0); return (
{/* Time Range Selector */}

Filament Usage Trends

{(['7d', '30d', '90d', '365d', 'all'] as TimeRange[]).map((range) => ( ))}
{/* Summary Cards */}

Period Filament

{(totalFilament / 1000).toFixed(2)}kg

{totalFilament.toFixed(0)}g total

Period Cost

{currency}{totalCost.toFixed(2)}

{totalPrints} prints

Avg per Print

{totalPrints > 0 ? (totalFilament / totalPrints).toFixed(0) : 0}g

{currency}{totalPrints > 0 ? (totalCost / totalPrints).toFixed(2) : '0.00'} avg

{/* Usage Over Time Chart */} {chartData.length > 0 ? (

Usage Over Time

`${value}g`} /> [`${Number(value ?? 0).toFixed(0)}g`, 'Filament']} />
) : (
No data for selected time range
)} {/* Bottom Charts */}
{/* Filament Type Distribution */}

By Filament Type

{filamentTypeData.length > 0 ? (
{filamentTypeData.map((_, index) => ( ))} [`${value ?? 0}g`, 'Usage']} />
{filamentTypeData.map((entry, index) => { const total = filamentTypeData.reduce((sum, e) => sum + e.value, 0); const percent = total > 0 ? ((entry.value / total) * 100).toFixed(0) : 0; return (
{entry.name} {percent}%
); })}
) : (
No filament data
)}
{/* Monthly Comparison */}

Monthly Comparison

`${v}g`} /> [ name === 'filament' ? `${value ?? 0}g` : name === 'cost' ? `${currency}${Number(value ?? 0).toFixed(2)}` : value ?? 0, name === 'filament' ? 'Filament' : name === 'cost' ? 'Cost' : 'Prints' ]} />
); }