Files
CPLv4.0/components/main/system/DetailModal.tsx

491 lines
14 KiB
TypeScript

"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,
} 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 = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: "top" as const },
title: {
display: true,
text: "Verlauf",
},
tooltip: {
mode: "index" as const,
intersect: false,
callbacks: {
label: function (ctx: any) {
return `Messwert: ${ctx.parsed.y}`;
},
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<ReduxDataEntry> = Object.freeze([]);
const chartRef = useRef<any>(null);
const [chartData, setChartData] = useState<any>({
datasets: [],
});
const [isLoading, setIsLoading] = useState(false);
const [shouldUpdateChart, setShouldUpdateChart] = useState(false);
const [forceUpdate, setForceUpdate] = useState(0); // Für periodische UI-Updates
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 "-98V":
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 "-98V":
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)) {
const interval = setInterval(() => {
setForceUpdate((prev) => prev + 1); // Force re-render für cursor-wait Update
}, 2000);
return () => clearInterval(interval);
}
}, [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) {
chartRef.current.options.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 prevCallback = chartInstance.options.animation?.onComplete;
chartInstance.options.animation = {
...chartInstance.options.animation,
onComplete: () => {
setIsLoading(false);
if (typeof prevCallback === "function") prevCallback();
},
};
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 = [];
// 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 = {
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]);
if (!isOpen || !selectedKey) return null;
// Prüfen ob Chart Daten haben (für cursor-wait)
const hasChartData = chartData.datasets && chartData.datasets.length > 0;
return (
<div
className={`fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50 ${
!hasChartData ? "cursor-wait" : ""
}`}
>
<div
className={`bg-white p-6 rounded-xl overflow-auto shadow-2xl transition-all duration-300 ${
isFullScreen ? "w-[95vw] h-[90vh]" : "w-[50%] h-[60%]"
} ${!hasChartData ? "cursor-wait" : ""}`}
>
<div className="relative">
<h2 className="text-xl font-semibold">
Detailansicht: {selectedKey}
</h2>
<div className="absolute top-0 right-0 flex gap-3">
<button
onClick={toggleFullScreen}
className="text-2xl text-gray-600 hover:text-gray-800"
>
<i
className={
isFullScreen
? "bi bi-fullscreen-exit"
: "bi bi-arrows-fullscreen"
}
></i>
</button>
<button
onClick={handleClose}
className="text-2xl text-gray-600 hover:text-gray-800"
>
<i className="bi bi-x-circle-fill"></i>
</button>
</div>
</div>
<SystemChartActionBar
zeitraum={zeitraum}
setZeitraum={setZeitraum}
onFetchData={handleFetchData}
isLoading={isLoading}
/>
<div className="h-[85%] bg-white dark:bg-gray-800 rounded shadow border border-gray-200 dark:border-gray-700 p-2">
<Line ref={chartRef} data={chartData} options={chartOptions} />
</div>
</div>
</div>
);
};