Files
CPLv4.0/components/main/system/DetailModal.tsx
Ismail Ali 49f9c3737a feat: DetailModal um Min/Max/Durchschnitt ergänzt
- Chart zeigt jetzt zusätzlich zu Messwert auch Minimal-, Maximal- und Durchschnittswerte an
- Datenstruktur an Redux angepasst (i, a, g)
- Darstellung entspricht jetzt LoopMeasurementChart
2025-07-10 19:12:06 +02:00

338 lines
8.9 KiB
TypeScript

"use client";
// /components/main/system/DetailModal.tsx
import React, { useEffect, useRef, useState } from "react";
import { Line } from "react-chartjs-2";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "@/redux/store";
import { Listbox } from "@headlessui/react";
import { setFullScreen } from "@/redux/slices/kabelueberwachungChartSlice";
type ReduxDataEntry = {
t: string;
i: number;
a?: number; // Maximalwert
g?: number; // Durchschnittswert
};
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
);
const initialChartData = {
datasets: [
{
label: "Minimum",
data: [],
borderColor: "lightgrey",
backgroundColor: "rgba(211,211,211,0.3)",
borderWidth: 2,
pointRadius: 0,
tension: 0.1,
},
{
label: "Maximum",
data: [],
borderColor: "lightgrey",
backgroundColor: "rgba(211,211,211,0.3)",
borderWidth: 2,
pointRadius: 0,
tension: 0.1,
},
{
label: "Durchschnitt",
data: [],
borderColor: "rgba(59,130,246,1)",
backgroundColor: "rgba(59,130,246,0.3)",
borderWidth: 2,
pointRadius: 0,
tension: 0.1,
},
],
};
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) => {
const chartRef = useRef<any>(null);
const reduxData: ReduxDataEntry[] = useSelector((state: RootState) => {
switch (selectedKey) {
case "+5V":
return state.systemspannung5Vplus[zeitraum] as ReduxDataEntry[];
case "+15V":
return state.systemspannung15Vplus[zeitraum] as ReduxDataEntry[];
case "-15V":
return state.systemspannung15Vminus[zeitraum] as ReduxDataEntry[];
case "-98V":
return state.systemspannung98Vminus[zeitraum] as ReduxDataEntry[];
case "ADC Temp":
return state.temperaturAdWandler[zeitraum] as ReduxDataEntry[];
case "CPU Temp":
return state.temperaturProzessor[zeitraum] as ReduxDataEntry[];
default:
return [] as ReduxDataEntry[];
}
});
const isFullScreen = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
);
const dispatch = useDispatch();
// **Vollbildmodus umschalten**
const toggleFullScreen = () => {
dispatch(setFullScreen(!isFullScreen));
setTimeout(() => {
chartRef.current?.resize();
}, 50);
};
const handleClose = () => {
dispatch({
type: "kabelueberwachungChartSlice/setFullScreen",
payload: false,
}); // Beim Schließen zurücksetzen
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) {
const chart = chartRef.current;
const sortedData = [...reduxData].reverse();
chart.data.datasets[0].data = sortedData.map((p) => ({
x: new Date(p.t),
y: p.i,
}));
chart.data.datasets[1].data = sortedData.map((p) => ({
x: new Date(p.t),
y: p.a,
}));
chart.data.datasets[2].data = sortedData.map((p) => ({
x: new Date(p.t),
y: p.g,
}));
}
}, [reduxData]);
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]);
if (!isOpen || !selectedKey) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50">
<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%]"
}`}
>
<div className="relative">
<h2 className="text-xl font-semibold">
Detailansicht: {selectedKey}
</h2>
{/* Action-Buttons */}
<div className="absolute top-0 right-0 flex gap-3">
{/* Fullscreen-Button */}
<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>
{/* Schließen-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>
<div className="flex items-center space-x-2 mb-4">
<label className="font-medium">Zeitraum:</label>
<Listbox value={zeitraum} onChange={setZeitraum}>
<div className="relative w-48">
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
<span>
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündlich",
DIA2: "Täglich",
}[zeitraum]
}
</span>
<svg
className="w-5 h-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
{["DIA0", "DIA1", "DIA2"].map((option) => (
<Listbox.Option
key={option}
value={option}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
>
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündlich",
DIA2: "Täglich",
}[option]
}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
<div className="h-[85%]">
<Line ref={chartRef} data={initialChartData} options={chartOptions} />
</div>
</div>
</div>
);
};