import { useEffect, useState, useContext, useMemo } from 'react' import { Card, Spin, Segmented, Flex } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import { Column } from '@ant-design/charts' import PropTypes from 'prop-types' import { getModelByName } from '../../../database/ObjectModels' import { ApiServerContext } from '../context/ApiServerContext' import { AuthContext } from '../context/AuthContext' import dayjs from 'dayjs' import { useThemeContext } from '../context/ThemeContext' const HistoryDisplay = ({ objectType, startDate, endDate, styles }) => { const { getModelHistory, connected } = useContext(ApiServerContext) const { token } = useContext(AuthContext) const [historyData, setHistoryData] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [timeRange, setTimeRange] = useState('4hrs') const { isDarkMode, getColors } = useThemeContext() // Calculate dates based on selected time range or provided props const { defaultStartDate, defaultEndDate } = useMemo(() => { const now = new Date() // If startDate/endDate props are provided, use them (ignore time range) if (startDate || endDate) { return { defaultStartDate: startDate || new Date(now.getTime() - 24 * 60 * 60 * 1000), defaultEndDate: endDate || now } } // Otherwise, calculate based on selected time range const hoursMap = { '8hrs': 8, '12hrs': 12, '24hrs': 24, '4hrs': 4, '1hrs': 1 } const hours = hoursMap[timeRange] || 1 return { defaultStartDate: new Date(now.getTime() - hours * 60 * 60 * 1000), defaultEndDate: now } }, [startDate, endDate, timeRange]) useEffect(() => { if (!objectType || !getModelHistory || !token || !connected) { return } const fetchHistory = async () => { try { setLoading(true) setError(null) const fetchedHistory = await getModelHistory( objectType, defaultStartDate, defaultEndDate ) if (fetchedHistory) { setHistoryData(fetchedHistory) } else { setError('Failed to fetch history') } } catch (err) { console.error('Error fetching history:', err) setError('Failed to fetch history') } finally { setLoading(false) } } fetchHistory() }, [ objectType, getModelHistory, token, connected, defaultStartDate, defaultEndDate ]) if (!objectType) { return null } const model = getModelByName(objectType) if (!model) { return null } const modelStats = model.stats || [] if (modelStats.length === 0) { return null } if (error || !Array.isArray(historyData)) { return null } /** * Extracts the stat key from history data based on stat definition name * e.g., "states.standby.count" -> "standby", "standbyPrinters" -> "standby" * The history data keys are the rollup names (like "standby", "printing") */ const extractStatKey = (statName) => { // Handle dot notation paths like "states.standby.count" if (statName.includes('.')) { const parts = statName.split('.') // Extract the middle part (e.g., "standby" from "states.standby.count") // Skip first part (usually "states") and last part (usually "count") if (parts.length >= 2) { return parts[parts.length - 2] // Get second-to-last part } return parts[parts.length - 1] // Fallback to last part } // Remove common suffixes return statName .replace(/Printers?$/i, '') .replace(/Jobs?$/i, '') .toLowerCase() } /** * Gets a nested value from an object using dot notation */ const getNestedValue = (obj, path) => { if (!obj || !path) return undefined const keys = path.split('.') let value = obj for (const key of keys) { if (value === null || value === undefined) return undefined value = value[key] } return value } /** * Extracts numeric value from a rollup object * Handles both old format (number) and new format ({count: 0}, {sum: 100}, {avg: 5.5}) */ const extractValue = (value, preferredOperation) => { if (typeof value === 'number') { return value } if (value && typeof value === 'object') { // If a preferred operation is specified and exists, use it if (preferredOperation && value[preferredOperation] !== undefined) { return value[preferredOperation] } // Handle new format: {count: 0}, {sum: 100}, {avg: 5.5} const operationKeys = ['count', 'sum', 'avg'] for (const key of operationKeys) { if (value[key] !== undefined) { return value[key] } } // Fallback: return first numeric value found const firstValue = Object.values(value)[0] return typeof firstValue === 'number' ? firstValue : 0 } return 0 } /** * Gets the stat value from history data point */ const getStatValueFromPoint = (point, statDef) => { // Handle special combined stats (support both 'combine' and 'sum' properties) const combineList = statDef.combine || statDef.sum if (combineList) { return combineList.reduce((sum, statName) => { const tempStatDef = { name: statName } const value = getStatValueFromPoint(point, tempStatDef) return sum + (value || 0) }, 0) } const statName = statDef.name // Determine operation from stat name let operation if (statName.endsWith('.count')) operation = 'count' else if (statName.endsWith('.sum')) operation = 'sum' else if (statName.endsWith('.avg')) operation = 'avg' let value // Extract the key that matches the history data structure // History data has keys like "standby", "printing" (the rollup names) const statKey = extractStatKey(statName) value = point[statKey] // If not found with extracted key, try direct access with full path if (value === undefined && statName.includes('.')) { value = getNestedValue(point, statName) } // If still not found, try with original stat name if (value === undefined) { value = point[statName] } return extractValue(value, operation) } // Transform data for the chart using model stats configuration // Each data point has: { date: '2024-01-01', standby: {count: 0}, printing: {count: 0}, ... } // We need to transform to: [{ date: '2024-01-01', category: 'Ready', value: 0 }, ...] const chartData = historyData.flatMap((point) => { return modelStats.map((statDef) => { const statValue = getStatValueFromPoint(point, statDef) const label = statDef.label || statDef.name return { date: point.date, dateFormatted: dayjs(point.date).format('DD/MM HH:mm'), category: label, value: statValue || 0 } }) }) // Sort by date chartData.sort((a, b) => { return new Date(a.date) - new Date(b.date) }) const themeColors = getColors() // Create color mapping from model stats const colors = { success: themeColors.colorSuccess, info: themeColors.colorInfo, error: themeColors.colorError, warning: themeColors.colorWarning, default: '#8c8c8c' } // Build color range array based on model stats order const colorRange = modelStats.map((stat) => { if (stat.color) { return colors[stat.color] || stat.color } return colors.default }) const config = { data: chartData, xField: 'dateFormatted', yField: 'value', theme: { type: isDarkMode ? 'dark' : 'light', fontFamily: '"DM Sans", -apple-system, BlinkMacSystemFont, sans-serif' }, stack: true, colorField: 'category', columnWidthRatio: 1, scale: { color: { range: colorRange } }, smooth: true, interaction: { tooltip: { marker: false } }, legend: { position: 'top' }, animation: { appear: { animation: 'wave-in', duration: 1000 } } } return ( }> {!startDate && !endDate && ( )} ) } HistoryDisplay.propTypes = { objectType: PropTypes.string.isRequired, startDate: PropTypes.oneOfType([ PropTypes.string, PropTypes.instanceOf(Date) ]), styles: PropTypes.object, endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]) } export default HistoryDisplay