import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import { X, Droplets, Thermometer, TrendingUp, TrendingDown, Minus } from 'lucide-react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, ReferenceLine, } from 'recharts'; import { api, type AMSHistoryResponse } from '../api/client'; import { parseUTCDate, applyTimeFormat, type TimeFormat } from '../utils/date'; import { useTranslation } from 'react-i18next'; import { useTheme } from '../contexts/ThemeContext'; interface AMSHistoryModalProps { isOpen: boolean; onClose: () => void; printerId: number; printerName: string; amsId: number; amsLabel: string; initialMode?: 'humidity' | 'temperature'; thresholds?: { humidityGood: number; humidityFair: number; tempGood: number; tempFair: number; }; } 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 }, ]; export function AMSHistoryModal({ isOpen, onClose, printerId, printerName, amsId, amsLabel, initialMode = 'humidity', thresholds, }: AMSHistoryModalProps) { const { t } = useTranslation(); const { mode: themeMode } = useTheme(); const [timeRange, setTimeRange] = useState('24h'); const [mode, setMode] = useState<'humidity' | 'temperature'>(initialMode); const isDark = themeMode === 'dark'; const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: api.getSettings, }); const timeFormat: TimeFormat = settings?.time_format || 'system'; // Close on Escape key 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: ['ams-history', printerId, amsId, hours], queryFn: () => api.getAMSHistory(printerId, amsId, hours), enabled: isOpen, refetchInterval: 60000, // Refresh every minute }); if (!isOpen) return null; // Format data for chart const rawPoints = data?.data.map(point => { const date = parseUTCDate(point.recorded_at) || new Date(); return { time: date.getTime(), humidity: point.humidity, temperature: point.temperature, }; }) || []; // Pad edges so the line extends across the full time window 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 }); } } // Get thresholds const humidityGood = thresholds?.humidityGood || 40; const humidityFair = thresholds?.humidityFair || 60; const tempGood = thresholds?.tempGood || 30; const tempFair = thresholds?.tempFair || 35; // Current values (last data point) const lastPoint = chartData[chartData.length - 1]; const currentHumidity = lastPoint?.humidity; const currentTemp = lastPoint?.temperature; // Trend calculation (compare first and last 20% of data) 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 humidityTrend = getTrend(chartData.map(d => d.humidity)); const tempTrend = getTrend(chartData.map(d => d.temperature)); const TrendIcon = ({ trend }: { trend: string }) => { if (trend === 'up') return ; if (trend === 'down') return ; return ; }; // Get status color for current value const getHumidityColor = (value: number | undefined | null) => { if (value == null) return '#9ca3af'; if (value <= humidityGood) return '#22a352'; if (value <= humidityFair) return '#d4a017'; return '#c62828'; }; const getTempColor = (value: number | undefined | null) => { if (value == null) return '#9ca3af'; if (value <= tempGood) return '#22a352'; if (value <= tempFair) return '#d4a017'; return '#c62828'; }; // Theme-aware styles (using isDark since dark: prefix doesn't work in portals) const modalBg = isDark ? '#2d2d2d' : '#ffffff'; const cardBg = isDark ? '#1d1d1d' : '#f3f4f6'; const borderColor = isDark ? '#3d3d3d' : '#e5e7eb'; const textPrimary = isDark ? '#ffffff' : '#111827'; const textSecondary = isDark ? '#9ca3af' : '#4b5563'; return (
e.stopPropagation()} > {/* Header */}

{amsLabel} {t('common.history', 'History')}

{printerName}

{/* Content */}
{/* Time Range & Mode Selector */}
{TIME_RANGES.map(range => ( ))}
{/* Stats Cards */}
{mode === 'humidity' ? ( <>

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

{currentHumidity != null ? `${currentHumidity}%` : '—'}

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

{data?.avg_humidity != null ? `${data.avg_humidity}%` : '—'}

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

{data?.min_humidity != null ? `${data.min_humidity}%` : '—'}

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

{data?.max_humidity != null ? `${data.max_humidity}%` : '—'}

) : ( <>

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

{currentTemp != null ? `${currentTemp}°C` : '—'}

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

{data?.avg_temperature != null ? `${data.avg_temperature}°C` : '—'}

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

{data?.min_temperature != null ? `${data.min_temperature}°C` : '—'}

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

{data?.max_temperature != null ? `${data.max_temperature}°C` : '—'}

)}
{/* Chart */}
{isLoading ? (
{t('common.loading', 'Loading...')}
) : error ? (
{t('common.error', 'Error loading data')}
) : chartData.length === 0 ? (
{t('common.noData', 'No data available for this time range')}
) : ( { const date = new Date(ts); if (hours > 24) { return date.toLocaleDateString([], { day: 'numeric', month: 'short' }); } return date.toLocaleTimeString([], applyTimeFormat({ hour: '2-digit', minute: '2-digit' }, timeFormat)); }} stroke={isDark ? '#9ca3af' : '#6b7280'} tick={{ fontSize: 12 }} /> mode === 'humidity' ? `${value}%` : `${value}°C`} /> new Date(ts).toLocaleString(undefined, applyTimeFormat({ year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }, timeFormat))} formatter={(value) => [ mode === 'humidity' ? `${value ?? 0}%` : `${value ?? 0}°C`, mode === 'humidity' ? 'Humidity' : 'Temperature' ]} /> {/* Threshold lines */} {mode === 'humidity' ? ( <> ) : ( <> )} )}
{/* Info */}
{t('amsHistory.recordingInfo', 'Data is recorded every 5 minutes while the printer is connected')}
); }