|
|
@@ -36,16 +36,17 @@ export function FilamentTrends({ archives, currency = '$', dateFrom, dateTo }: F
|
|
|
|
|
|
// Calculate daily usage data
|
|
|
const dailyData = useMemo(() => {
|
|
|
- const dataMap = new Map<string, { date: string; filament: number; cost: number; prints: number }>();
|
|
|
+ const dataMap = new Map<string, { date: string; filament: number; cost: number; energy: number; prints: number }>();
|
|
|
|
|
|
archives.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 existing = dataMap.get(key) || { date: key, filament: 0, cost: 0, energy: 0, prints: 0 };
|
|
|
existing.filament += archive.filament_used_grams || 0;
|
|
|
existing.cost += archive.cost || 0;
|
|
|
+ existing.energy += archive.energy_kwh || 0;
|
|
|
existing.prints += archive.quantity || 1;
|
|
|
dataMap.set(key, existing);
|
|
|
});
|
|
|
@@ -75,7 +76,7 @@ export function FilamentTrends({ archives, currency = '$', dateFrom, dateTo }: F
|
|
|
const hourlyData = useMemo(() => {
|
|
|
if (spanDays > 7) return [];
|
|
|
|
|
|
- const dataMap = new Map<string, { date: string; filament: number; cost: number; prints: number }>();
|
|
|
+ const dataMap = new Map<string, { date: string; filament: number; cost: number; energy: number; prints: number }>();
|
|
|
const multiDay = spanDays > 1;
|
|
|
|
|
|
archives.forEach(archive => {
|
|
|
@@ -83,9 +84,10 @@ export function FilamentTrends({ archives, currency = '$', dateFrom, dateTo }: F
|
|
|
const h = date.getHours();
|
|
|
const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}T${String(h).padStart(2, '0')}`;
|
|
|
|
|
|
- const existing = dataMap.get(key) || { date: key, filament: 0, cost: 0, prints: 0 };
|
|
|
+ const existing = dataMap.get(key) || { date: key, filament: 0, cost: 0, energy: 0, prints: 0 };
|
|
|
existing.filament += archive.filament_used_grams || 0;
|
|
|
existing.cost += archive.cost || 0;
|
|
|
+ existing.energy += archive.energy_kwh || 0;
|
|
|
existing.prints += archive.quantity || 1;
|
|
|
dataMap.set(key, existing);
|
|
|
});
|
|
|
@@ -107,7 +109,7 @@ export function FilamentTrends({ archives, currency = '$', dateFrom, dateTo }: F
|
|
|
const weeklyData = useMemo(() => {
|
|
|
if (dailyData.length <= 60) return dailyData;
|
|
|
|
|
|
- const dataMap = new Map<string, { week: string; filament: number; cost: number; prints: number }>();
|
|
|
+ const dataMap = new Map<string, { week: string; filament: number; cost: number; energy: number; prints: number }>();
|
|
|
|
|
|
dailyData.forEach(day => {
|
|
|
const date = new Date(day.date);
|
|
|
@@ -115,9 +117,10 @@ export function FilamentTrends({ archives, currency = '$', dateFrom, dateTo }: F
|
|
|
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 existing = dataMap.get(key) || { week: key, filament: 0, cost: 0, energy: 0, prints: 0 };
|
|
|
existing.filament += day.filament;
|
|
|
existing.cost += day.cost;
|
|
|
+ existing.energy += day.energy;
|
|
|
existing.prints += day.prints;
|
|
|
dataMap.set(key, existing);
|
|
|
});
|
|
|
@@ -237,6 +240,8 @@ export function FilamentTrends({ archives, currency = '$', dateFrom, dateTo }: F
|
|
|
const chartData = spanDays <= 7 && hourlyData.length > 0 ? hourlyData : weeklyData;
|
|
|
const totalFilament = archives.reduce((sum, a) => sum + (a.filament_used_grams || 0), 0);
|
|
|
const totalCost = archives.reduce((sum, a) => sum + (a.cost || 0), 0);
|
|
|
+ const totalEnergy = archives.reduce((sum, a) => sum + (a.energy_kwh || 0), 0);
|
|
|
+ const totalEnergyCost = archives.reduce((sum, a) => sum + (a.energy_cost || 0), 0);
|
|
|
const totalPrints = archives.reduce((sum, a) => sum + (a.quantity || 1), 0);
|
|
|
const printerCount = new Set(archives.map(a => a.printer_id).filter(Boolean)).size;
|
|
|
|
|
|
@@ -323,6 +328,57 @@ export function FilamentTrends({ archives, currency = '$', dateFrom, dateTo }: F
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
+ {/* Energy Over Time Chart (#1432) — only when smart-plug per-print data exists */}
|
|
|
+ {totalEnergy > 0 && chartData.length > 0 && (
|
|
|
+ <div className="bg-bambu-dark rounded-lg p-4">
|
|
|
+ <div className="flex items-center justify-between mb-4">
|
|
|
+ <h4 className="text-sm font-medium text-bambu-gray">{t('stats.energyOverTime')}</h4>
|
|
|
+ <span className="text-xs text-bambu-gray">
|
|
|
+ {totalEnergy.toFixed(3)} kWh · {currency}{totalEnergyCost.toFixed(2)}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <ResponsiveContainer width="100%" height={250}>
|
|
|
+ <AreaChart data={chartData}>
|
|
|
+ <defs>
|
|
|
+ <linearGradient id="colorEnergy" x1="0" y1="0" x2="0" y2="1">
|
|
|
+ <stop offset="5%" stopColor="#f59e0b" stopOpacity={0.3}/>
|
|
|
+ <stop offset="95%" stopColor="#f59e0b" stopOpacity={0}/>
|
|
|
+ </linearGradient>
|
|
|
+ </defs>
|
|
|
+ <CartesianGrid strokeDasharray="3 3" stroke="#3d3d3d" />
|
|
|
+ <XAxis
|
|
|
+ dataKey="dateLabel"
|
|
|
+ stroke="#9ca3af"
|
|
|
+ tick={{ fontSize: 12 }}
|
|
|
+ interval="preserveStartEnd"
|
|
|
+ />
|
|
|
+ <YAxis
|
|
|
+ stroke="#9ca3af"
|
|
|
+ tick={{ fontSize: 12 }}
|
|
|
+ tickFormatter={(value) => `${value}kWh`}
|
|
|
+ />
|
|
|
+ <Tooltip
|
|
|
+ contentStyle={{
|
|
|
+ backgroundColor: '#2d2d2d',
|
|
|
+ border: '1px solid #3d3d3d',
|
|
|
+ borderRadius: '8px',
|
|
|
+ }}
|
|
|
+ labelStyle={{ color: '#fff' }}
|
|
|
+ formatter={(value) => [`${Number(value ?? 0).toFixed(3)} kWh`, t('stats.energyUsed')]}
|
|
|
+ />
|
|
|
+ <Area
|
|
|
+ type="monotone"
|
|
|
+ dataKey="energy"
|
|
|
+ stroke="#f59e0b"
|
|
|
+ strokeWidth={2}
|
|
|
+ fillOpacity={1}
|
|
|
+ fill="url(#colorEnergy)"
|
|
|
+ />
|
|
|
+ </AreaChart>
|
|
|
+ </ResponsiveContainer>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
{/* Bottom Charts */}
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
|
|
{/* Filament Type Distribution */}
|