- Redux-State für vonDatum und bisDatum wird bei handleClose geleert - verhindert unerwünschtes Vorfiltern bei erneutem Öffnen des Modals
363 lines
9.9 KiB
TypeScript
363 lines
9.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";
|
|
import DateRangePicker from "@/components/common/DateRangePicker";
|
|
import {
|
|
setVonDatum,
|
|
setBisDatum,
|
|
} from "@/redux/slices/kabelueberwachungChartSlice";
|
|
|
|
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
|
|
);
|
|
|
|
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) => {
|
|
const chartRef = useRef<any>(null);
|
|
const [chartData, setChartData] = useState<any>({
|
|
datasets: [],
|
|
});
|
|
const vonDatum = useSelector(
|
|
(state: RootState) => state.kabelueberwachungChartSlice.vonDatum
|
|
);
|
|
const bisDatum = useSelector(
|
|
(state: RootState) => state.kabelueberwachungChartSlice.bisDatum
|
|
);
|
|
|
|
const [filteredData, setFilteredData] = useState<ReduxDataEntry[]>([]);
|
|
|
|
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 [];
|
|
}
|
|
}) as ReduxDataEntry[];
|
|
|
|
const isFullScreen = useSelector(
|
|
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
|
|
);
|
|
const dispatch = useDispatch();
|
|
|
|
const toggleFullScreen = () => {
|
|
dispatch(setFullScreen(!isFullScreen));
|
|
setTimeout(() => {
|
|
chartRef.current?.resize();
|
|
}, 50);
|
|
};
|
|
|
|
const handleClose = () => {
|
|
dispatch(setFullScreen(false));
|
|
dispatch(setVonDatum(""));
|
|
dispatch(setBisDatum(""));
|
|
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();
|
|
}, []);
|
|
|
|
const handleFetchData = () => {
|
|
let sortedData = [...reduxData].reverse();
|
|
|
|
if (vonDatum && bisDatum) {
|
|
const vonDate = new Date(vonDatum);
|
|
const bisDate = new Date(bisDatum);
|
|
sortedData = sortedData.filter((entry) => {
|
|
const entryDate = new Date(entry.t);
|
|
return entryDate >= vonDate && entryDate <= bisDate;
|
|
});
|
|
}
|
|
|
|
setFilteredData(sortedData);
|
|
|
|
setChartData({
|
|
datasets: [
|
|
{
|
|
label: "Minimum",
|
|
data: sortedData.map((p) => ({ x: new Date(p.t), y: p.i })),
|
|
borderColor: "lightgrey",
|
|
backgroundColor: "rgba(211,211,211,0.3)",
|
|
borderWidth: 2,
|
|
pointRadius: 0,
|
|
tension: 0.1,
|
|
},
|
|
{
|
|
label: "Maximum",
|
|
data: sortedData.map((p) => ({ x: new Date(p.t), y: p.a })),
|
|
borderColor: "lightgrey",
|
|
backgroundColor: "rgba(211,211,211,0.3)",
|
|
borderWidth: 2,
|
|
pointRadius: 0,
|
|
tension: 0.1,
|
|
},
|
|
{
|
|
label: zeitraum === "DIA0" ? "Messwert" : "Durchschnitt",
|
|
data: sortedData.map((p) => ({
|
|
x: new Date(p.t),
|
|
y: zeitraum === "DIA0" ? p.m : p.g,
|
|
})),
|
|
borderColor: "rgba(59,130,246,1)",
|
|
backgroundColor: "rgba(59,130,246,0.3)",
|
|
borderWidth: 2,
|
|
pointRadius: 0,
|
|
tension: 0.1,
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
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]);
|
|
// beim start soll der Chart einmal aufgerufen wird, also einmal der Button "Daten laden" geklickt werden
|
|
useEffect(() => {
|
|
if (isOpen && selectedKey) {
|
|
handleFetchData();
|
|
}
|
|
}, [isOpen, selectedKey]);
|
|
|
|
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>
|
|
|
|
<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>
|
|
|
|
<div className="flex items-center justify-start gap-4 mb-4 flex-wrap">
|
|
<DateRangePicker />
|
|
<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>
|
|
<button
|
|
onClick={handleFetchData}
|
|
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm"
|
|
>
|
|
Daten laden
|
|
</button>
|
|
</div>
|
|
|
|
<div className="h-[85%]">
|
|
<Line ref={chartRef} data={chartData} options={chartOptions} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|