"use client"; // /components/main/system/DetailModal.tsx import React, { useEffect, useRef, useState, useCallback } from "react"; import { Line } from "react-chartjs-2"; import { useSelector } from "react-redux"; import { RootState, useAppDispatch } from "@/redux/store"; import { setFullScreen } from "@/redux/slices/kabelueberwachungChartSlice"; import { resetDateRange } from "@/redux/slices/dateRangePickerSlice"; // Import Thunks import SystemChartActionBar from "@/components/main/system/SystemChartActionBar"; import { getSystemspannung5VplusThunk } from "@/redux/thunks/getSystemspannung5VplusThunk"; import { getSystemspannung15VplusThunk } from "@/redux/thunks/getSystemspannung15VplusThunk"; import { getSystemspannung15VminusThunk } from "@/redux/thunks/getSystemspannung15VminusThunk"; import { getSystemspannung98VminusThunk } from "@/redux/thunks/getSystemspannung98VminusThunk"; import { getTemperaturAdWandlerThunk } from "@/redux/thunks/getTemperaturAdWandlerThunk"; import { getTemperaturProzessorThunk } from "@/redux/thunks/getTemperaturProzessorThunk"; import { Chart as ChartJS, LineElement, PointElement, CategoryScale, LinearScale, Title, Tooltip, Legend, Filler, TimeScale, type ChartDataset, type ChartOptions, type ChartData, type Chart, } from "chart.js"; import "chartjs-adapter-date-fns"; import { de } from "date-fns/locale"; ChartJS.register( LineElement, PointElement, CategoryScale, LinearScale, Title, Tooltip, Legend, Filler, TimeScale ); // Tailwind-basierte Farbdefinitionen für Chart.js const chartColors = { gray: { line: "#6B7280", // tailwind gray-500 background: "rgba(107, 114, 128, 0.2)", // tailwind gray-500 mit opacity }, littwinBlue: { line: "#00AEEF", // littwin-blue background: "rgba(0, 174, 239, 0.2)", // littwin-blue mit opacity }, }; type ReduxDataEntry = { //Alle DIA0 t,m,i,a , DIA1 und DIA2 t,i,a,g t: string; // Zeitstempel i: number; // Minimum a: number; // Maximum g?: number; // Durchschnitt (optional, falls vorhanden) m?: number; // aktueller Messwert (optional, falls vorhanden) }; const chartOptions: ChartOptions<"line"> = { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: "top" as const }, title: { display: true, text: "Verlauf", }, tooltip: { mode: "index" as const, intersect: false, callbacks: { // eslint-disable-next-line @typescript-eslint/no-explicit-any label: function (ctx: any) { return `Messwert: ${ctx.parsed.y}`; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any title: function (items: any[]) { const date = items[0].parsed.x; return `Zeitpunkt: ${new Date(date).toLocaleString("de-DE")}`; }, }, }, zoom: { pan: { enabled: true, mode: "x" as const }, zoom: { wheel: { enabled: true }, pinch: { enabled: true }, mode: "x" as const, }, }, }, scales: { x: { type: "time" as const, time: { unit: "day" as const, tooltipFormat: "dd.MM.yyyy HH:mm", displayFormats: { day: "dd.MM.yyyy", }, }, adapters: { date: { locale: de }, }, title: { display: true, text: "Zeit", }, }, y: { title: { display: true, text: "Messwert", }, }, }, }; type Props = { isOpen: boolean; selectedKey: string | null; onClose: () => void; zeitraum: "DIA0" | "DIA1" | "DIA2"; setZeitraum: (typ: "DIA0" | "DIA1" | "DIA2") => void; }; export const DetailModal = ({ isOpen, selectedKey, onClose, zeitraum, setZeitraum, }: Props) => { // Stable empty reference to avoid React-Redux dev warning about selector returning new [] each call const EMPTY_REDUX_DATA: ReadonlyArray = Object.freeze([]); const chartRef = useRef | null>(null); const [chartData, setChartData] = useState>({ datasets: [], }); const [isLoading, setIsLoading] = useState(false); const [shouldUpdateChart, setShouldUpdateChart] = useState(false); // const [forceUpdate, setForceUpdate] = useState(0); // Für periodische UI-Updates (derzeit nicht benötigt) const reduxData = useSelector((state: RootState) => { switch (selectedKey) { case "+5V": return state.systemspannung5Vplus[zeitraum]; case "+15V": return state.systemspannung15Vplus[zeitraum]; case "-15V": return state.systemspannung15Vminus[zeitraum]; case "-96V": return state.systemspannung98Vminus[zeitraum]; case "ADC Temp": return state.temperaturAdWandler[zeitraum]; case "CPU Temp": return state.temperaturProzessor[zeitraum]; default: return EMPTY_REDUX_DATA; } }) as ReduxDataEntry[]; const isFullScreen = useSelector( (state: RootState) => state.kabelueberwachungChartSlice.isFullScreen ); const dispatch = useAppDispatch(); // API-Request beim Klick auf "Daten laden" - memoized für useEffect dependency const handleFetchData = useCallback(() => { setIsLoading(true); // Clear previous chart data setChartData({ datasets: [] }); // Flag setzen, dass Chart nach Datenempfang aktualisiert werden soll setShouldUpdateChart(true); switch (selectedKey) { case "+5V": dispatch(getSystemspannung5VplusThunk(zeitraum)); break; case "+15V": dispatch(getSystemspannung15VplusThunk(zeitraum)); break; case "-15V": dispatch(getSystemspannung15VminusThunk(zeitraum)); break; case "-96V": dispatch(getSystemspannung98VminusThunk(zeitraum)); break; case "ADC Temp": dispatch(getTemperaturAdWandlerThunk(zeitraum)); break; case "CPU Temp": dispatch(getTemperaturProzessorThunk(zeitraum)); break; default: break; } }, [selectedKey, zeitraum, dispatch]); // Reset Zeitraum auf DIA0 und Datumswerte wenn Modal geöffnet wird useEffect(() => { if (isOpen) { setZeitraum("DIA0"); // Reset DateRangePicker to its defaults (it sets 30 days → today on mount) dispatch(resetDateRange()); // Chart-Daten zurücksetzen beim Öffnen setChartData({ datasets: [] }); } }, [isOpen, setZeitraum, dispatch]); // Periodische UI-Updates alle 2 Sekunden während Wartezeit useEffect(() => { if (isOpen && (!chartData.datasets || chartData.datasets.length === 0)) { // Optional: periodische Re-Renders wurden deaktiviert, da nicht mehr notwendig // (kann wieder aktiviert werden falls Cursor-Animation erwünscht ist) } }, [isOpen, chartData.datasets]); // Automatisches "Daten laden" alle 4 Sekunden, maximal 2 Versuche useEffect(() => { if (isOpen && (!chartData.datasets || chartData.datasets.length === 0)) { let attempts = 0; const interval = setInterval(() => { if (attempts < 2) { console.log("Auto-clicking 'Daten laden' button..."); handleFetchData(); attempts++; } else { clearInterval(interval); } }, 4000); return () => clearInterval(interval); } }, [isOpen, chartData.datasets, handleFetchData]); const toggleFullScreen = () => { dispatch(setFullScreen(!isFullScreen)); setTimeout(() => { chartRef.current?.resize(); }, 50); }; const handleClose = () => { dispatch(setFullScreen(false)); dispatch(resetDateRange()); onClose(); }; useEffect(() => { const loadZoomPlugin = async () => { if (typeof window !== "undefined") { const zoomPlugin = (await import("chartjs-plugin-zoom")).default; if (!ChartJS.registry.plugins.get("zoom")) { ChartJS.register(zoomPlugin); } } }; loadZoomPlugin(); }, []); useEffect(() => { if (chartRef.current && selectedKey) { const opts = chartRef.current.options as ChartOptions<"line"> & { plugins?: { title?: { text?: string } }; }; if (opts.plugins?.title) { opts.plugins.title.text = `Verlauf ${selectedKey}`; } chartRef.current.update("none"); } }, [selectedKey]); useEffect(() => { if (chartRef.current) { chartRef.current.resetZoom(); } }, [zeitraum]); // Chart.js animation complete callback to set isLoading false useEffect(() => { if (chartRef.current && isLoading) { const chartInstance = chartRef.current; // Save previous callback to restore later const animation: any = chartInstance.options.animation || {}; // eslint-disable-line @typescript-eslint/no-explicit-any const prevCallback = animation.onComplete; animation.onComplete = () => { setIsLoading(false); if (typeof prevCallback === "function") prevCallback(); }; chartInstance.options.animation = animation; chartInstance.update(); } }, [chartData, isLoading]); // DateRange from global DateRangePicker slice const pickerVonDatum = useSelector( (state: RootState) => state.dateRangePicker.vonDatum ); const pickerBisDatum = useSelector( (state: RootState) => state.dateRangePicker.bisDatum ); // Update chart data when Redux data changes (only after button click) useEffect(() => { if (shouldUpdateChart && reduxData && reduxData.length > 0) { // Filter data by selected date range (inclusive end date) let filtered = reduxData; try { if (pickerVonDatum && pickerBisDatum) { const start = new Date(`${pickerVonDatum}T00:00:00`); const end = new Date(`${pickerBisDatum}T23:59:59`); const s = start.getTime(); const e = end.getTime(); filtered = reduxData.filter((entry) => { const t = new Date(entry.t).getTime(); return t >= s && t <= e; }); } } catch (err) { console.warn("Zeitfilter konnte nicht angewendet werden:", err); } console.log("Redux data for chart (filtered):", filtered.length); if (!filtered.length) { setChartData({ datasets: [] }); setShouldUpdateChart(false); return; } // Create datasets array for multiple lines const datasets: ChartDataset<"line">[] = []; // Check which data fields are available and create datasets accordingly const hasMinimum = filtered.some( (entry) => entry.i !== undefined && entry.i !== null && entry.i !== 0 ); const hasMaximum = filtered.some( (entry) => entry.a !== undefined && entry.a !== null ); const hasAverage = filtered.some( (entry) => entry.g !== undefined && entry.g !== null ); const hasCurrent = filtered.some( (entry) => entry.m !== undefined && entry.m !== null ); // Zuerst Hintergrund-Linien (Minimum/Maximum) - grau if (hasMinimum) { datasets.push({ label: "Messwert Minimum", data: filtered.map((entry) => ({ x: new Date(entry.t).getTime(), y: entry.i || 0, })), borderColor: "gray", borderWidth: 1, pointRadius: 0, tension: 0.1, order: 1, }); } if (hasMaximum) { datasets.push({ label: "Messwert Maximum", data: filtered.map((entry) => ({ x: new Date(entry.t).getTime(), y: entry.a || 0, })), borderColor: "gray", borderWidth: 1, pointRadius: 0, tension: 0.1, order: 3, }); } // Dann Vordergrund-Linien (Durchschnitt/Messwert) - littwin-blue if (hasAverage) { datasets.push({ label: "Durchschnitt", data: filtered.map((entry) => ({ x: new Date(entry.t).getTime(), y: entry.g || 0, })), borderColor: chartColors.littwinBlue.line, backgroundColor: chartColors.littwinBlue.background, tension: 0.1, fill: false, order: 2, }); } if (hasCurrent) { datasets.push({ label: "Messwert", data: filtered.map((entry) => ({ x: new Date(entry.t).getTime(), y: entry.m || 0, })), borderColor: chartColors.littwinBlue.line, backgroundColor: chartColors.littwinBlue.background, tension: 0.1, fill: false, order: 2, }); } const newChartData: ChartData<"line"> = { labels: [], datasets: datasets, }; console.log("Chart datasets:", datasets.length, "lines"); setChartData(newChartData); setShouldUpdateChart(false); // Reset flag } else if (shouldUpdateChart && (!reduxData || reduxData.length === 0)) { console.log("No Redux data available"); setChartData({ datasets: [] }); setShouldUpdateChart(false); // Reset flag } }, [ reduxData, selectedKey, shouldUpdateChart, pickerVonDatum, pickerBisDatum, ]); if (!isOpen || !selectedKey) return null; // Prüfen ob Chart Daten haben (für cursor-wait) const hasChartData = chartData.datasets && chartData.datasets.length > 0; return (
{/* Header */}

Detailansicht: {selectedKey}

{/* Body */}
{/* Optional Footer (currently empty, reserved for future) */}
); };