import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import { X, Flame, Square, Box, TrendingUp, TrendingDown, Minus } from 'lucide-react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, } from 'recharts'; import { api, type HeaterSensorKind, type PrinterSensorHistoryResponse } from '../api/client'; import { parseUTCDate, applyTimeFormat, type TimeFormat } from '../utils/date'; import { useTranslation } from 'react-i18next'; interface HeaterHistoryModalProps { isOpen: boolean; onClose: () => void; printerId: number; printerName: string; initialKind?: HeaterSensorKind; availableKinds?: HeaterSensorKind[]; } type TimeRange = '6h' | '24h' | '48h' | '7d'; const TIME_RANGES: { value: TimeRange; label: string; hours: number }[] = [ { value: '6h', label: '6h', hours: 6 }, { value: '24h', label: '24h', hours: 24 }, { value: '48h', label: '48h', hours: 48 }, { value: '7d', label: '7d', hours: 168 }, ]; const KIND_COLORS: Record = { nozzle: '#fb923c', nozzle_2: '#f97316', bed: '#60a5fa', chamber: '#34d399', }; const KIND_TARGET_COLORS: Record = { nozzle: '#fed7aa', nozzle_2: '#fdba74', bed: '#bfdbfe', chamber: '#a7f3d0', }; export function HeaterHistoryModal({ isOpen, onClose, printerId, printerName, initialKind = 'nozzle', availableKinds = ['nozzle', 'bed', 'chamber'], }: HeaterHistoryModalProps) { const { t } = useTranslation(); const [timeRange, setTimeRange] = useState('24h'); const [kind, setKind] = useState(initialKind); useEffect(() => { setKind(initialKind); }, [initialKind]); const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: api.getSettings, }); const timeFormat: TimeFormat = settings?.time_format || 'system'; useEffect(() => { if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isOpen, onClose]); const hours = TIME_RANGES.find(r => r.value === timeRange)?.hours || 24; const { data, isLoading, error } = useQuery({ queryKey: ['printer-sensor-history', printerId, hours, availableKinds.join(',')], queryFn: () => api.getPrinterSensorHistory(printerId, hours, availableKinds), enabled: isOpen, refetchInterval: 60000, }); if (!isOpen) return null; const series = data?.series.find(s => s.sensor_kind === kind); const rawPoints = (series?.data || []).map(p => { const date = parseUTCDate(p.recorded_at) || new Date(); return { time: date.getTime(), value: p.value, target: p.target, }; }); const domainStart = Date.now() - hours * 60 * 60 * 1000; const domainEnd = Date.now(); const chartData = [...rawPoints]; if (chartData.length > 0) { const first = chartData[0]; if (first.time > domainStart) { chartData.unshift({ ...first, time: domainStart }); } const last = chartData[chartData.length - 1]; if (last.time < domainEnd) { chartData.push({ ...last, time: domainEnd }); } } const lastPoint = chartData[chartData.length - 1]; const currentValue = lastPoint?.value; const currentTarget = lastPoint?.target; const getTrend = (values: (number | null)[]) => { const filtered = values.filter((v): v is number => v != null); if (filtered.length < 4) return 'stable'; const firstQuarter = filtered.slice(0, Math.floor(filtered.length / 4)); const lastQuarter = filtered.slice(-Math.floor(filtered.length / 4)); const firstAvg = firstQuarter.reduce((a, b) => a + b, 0) / firstQuarter.length; const lastAvg = lastQuarter.reduce((a, b) => a + b, 0) / lastQuarter.length; const diff = lastAvg - firstAvg; if (Math.abs(diff) < 2) return 'stable'; return diff > 0 ? 'up' : 'down'; }; const trend = getTrend(chartData.map(d => d.value)); const TrendIcon = ({ trend }: { trend: string }) => { if (trend === 'up') return ; if (trend === 'down') return ; return ; }; const modalBg = 'var(--bg-secondary)'; const cardBg = 'var(--bg-primary)'; const borderColor = 'var(--border-color)'; const textPrimary = 'var(--text-primary)'; const textSecondary = 'var(--text-secondary)'; const axisColor = 'var(--text-muted)'; const kindLabel = (k: HeaterSensorKind) => { switch (k) { case 'nozzle': return t('printers.heaterHistory.nozzle', 'Nozzle'); case 'nozzle_2': return t('printers.heaterHistory.nozzle2', 'Nozzle 2'); case 'bed': return t('printers.heaterHistory.bed', 'Bed'); case 'chamber': return t('printers.heaterHistory.chamber', 'Chamber'); } }; const KindIcon = ({ k }: { k: HeaterSensorKind }) => { if (k === 'nozzle' || k === 'nozzle_2') return ; if (k === 'bed') return ; return ; }; return (
e.stopPropagation()} >

{t('printers.heaterHistory.title', 'Heater History')}

{printerName}

{availableKinds.map(k => ( ))}
{TIME_RANGES.map(range => ( ))}

{t('common.current', 'Current')}

{currentValue != null ? `${Math.round(currentValue)}°C` : '—'}

{currentTarget != null && currentTarget > 0 && (

{t('common.target', 'Target')}: {Math.round(currentTarget)}°C

)}

{t('common.average', 'Average')}

{series?.avg_value != null ? `${series.avg_value}°C` : '—'}

{t('common.min', 'Min')}

{series?.min_value != null ? `${Math.round(series.min_value)}°C` : '—'}

{t('common.max', 'Max')}

{series?.max_value != null ? `${Math.round(series.max_value)}°C` : '—'}

{isLoading ? (
{t('common.loading', 'Loading...')}
) : error ? (
{t('printers.heaterHistory.error', 'Failed to load history')}
) : chartData.length === 0 ? (
{t('printers.heaterHistory.empty', 'No data recorded yet')}
) : ( new Date(ts).toLocaleTimeString( [], applyTimeFormat({ hour: '2-digit', minute: '2-digit' }, timeFormat), ) } stroke={axisColor} fontSize={11} /> `${Math.round(v)}°`} /> new Date(ts as number).toLocaleString( undefined, applyTimeFormat( { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }, timeFormat, ), ) } formatter={(value) => (value != null ? `${Math.round(Number(value))}°C` : '—')} /> )}
); }