From 311d47211e82dbba6cca2cbdb7aea9128f0bf578 Mon Sep 17 00:00:00 2001 From: ISA Date: Mon, 21 Jul 2025 08:57:38 +0200 Subject: [PATCH] Nach Betriebsferien einmal sichern --- .env.development | 2 +- .env.production | 2 +- CHANGELOG.md | 20 +++ components/common/DateRangePicker.tsx | 9 +- .../main/analogInputs/AnalogInputsChart.tsx | 157 ++++++++++++++++-- docs/TODO.md | 1 + package-lock.json | 4 +- package.json | 2 +- redux/slices/dateRangePickerSlice.ts | 32 ++++ redux/store.ts | 2 + redux/thunks/getAnalogInputsHistoryThunk.ts | 7 +- services/fetchAnalogInputsHistoryService.ts | 13 +- src/components/AnalogInputsChart.tsx | 55 ++++++ 13 files changed, 272 insertions(+), 34 deletions(-) create mode 100644 redux/slices/dateRangePickerSlice.ts create mode 100644 src/components/AnalogInputsChart.tsx diff --git a/.env.development b/.env.development index 4222847..330c95f 100644 --- a/.env.development +++ b/.env.development @@ -6,6 +6,6 @@ NEXT_PUBLIC_USE_MOCK_BACKEND_LOOP_START=false NEXT_PUBLIC_EXPORT_STATIC=false NEXT_PUBLIC_USE_CGI=false # App-Versionsnummer -NEXT_PUBLIC_APP_VERSION=1.6.602 +NEXT_PUBLIC_APP_VERSION=1.6.604 NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter) diff --git a/.env.production b/.env.production index 6164a5d..329de0d 100644 --- a/.env.production +++ b/.env.production @@ -5,5 +5,5 @@ NEXT_PUBLIC_CPL_API_PATH=/CPL NEXT_PUBLIC_EXPORT_STATIC=true NEXT_PUBLIC_USE_CGI=true # App-Versionsnummer -NEXT_PUBLIC_APP_VERSION=1.6.602 +NEXT_PUBLIC_APP_VERSION=1.6.604 NEXT_PUBLIC_CPL_MODE=production \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e78ace5..661db78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## [1.6.604] – 2025-07-21 + +- feat(analogInputs): automatisches Laden der Chart-Daten bei Tabellenklick via Redux + +- analogInputsHistorySlice um `autoLoad` erweitert, um automatisches Laden zu triggern +- handleSelect in AnalogInputsTable dispatcht jetzt `setAutoLoad(true)` +- AnalogInputsChart lauscht auf `autoLoad` + `selectedId` und lädt Daten automatisch +- `autoLoad` wird nach dem Laden wieder auf false zurückgesetzt + +--- +## [1.6.603] – 2025-07-21 + +- feat(analogInputs): automatisches Laden der Chart-Daten bei Tabellenklick via Redux + +- analogInputsHistorySlice um `autoLoad` erweitert, um automatisches Laden zu triggern +- handleSelect in AnalogInputsTable dispatcht jetzt `setAutoLoad(true)` +- AnalogInputsChart lauscht auf `autoLoad` + `selectedId` und lädt Daten automatisch +- `autoLoad` wird nach dem Laden wieder auf false zurückgesetzt + +--- ## [1.6.602] – 2025-07-15 - feat(analogInputs): automatisches Laden der Chart-Daten bei Tabellenklick via Redux diff --git a/components/common/DateRangePicker.tsx b/components/common/DateRangePicker.tsx index b233561..669c94e 100644 --- a/components/common/DateRangePicker.tsx +++ b/components/common/DateRangePicker.tsx @@ -3,20 +3,17 @@ import React, { useEffect } from "react"; import DatePicker from "react-datepicker"; import { useSelector, useDispatch } from "react-redux"; import { RootState } from "@/redux/store"; -import { - setVonDatum, - setBisDatum, -} from "@/redux/slices/kabelueberwachungChartSlice"; +import { setVonDatum, setBisDatum } from "@/redux/slices/dateRangePickerSlice"; import "react-datepicker/dist/react-datepicker.css"; const DateRangePicker: React.FC = () => { const dispatch = useDispatch(); const reduxVonDatum = useSelector( - (state: RootState) => state.kabelueberwachungChartSlice.vonDatum + (state: RootState) => state.dateRangePicker.vonDatum ); const reduxBisDatum = useSelector( - (state: RootState) => state.kabelueberwachungChartSlice.bisDatum + (state: RootState) => state.dateRangePicker.bisDatum ); const today = new Date(); diff --git a/components/main/analogInputs/AnalogInputsChart.tsx b/components/main/analogInputs/AnalogInputsChart.tsx index 5e24b14..cae52c2 100644 --- a/components/main/analogInputs/AnalogInputsChart.tsx +++ b/components/main/analogInputs/AnalogInputsChart.tsx @@ -45,12 +45,91 @@ type AnalogInputHistoryPoint = { }; export default function AnalogInputsChart() { + useEffect(() => { + const loadZoomPlugin = async () => { + const zoomPlugin = (await import("chartjs-plugin-zoom")).default; + if (!ChartJS.registry.plugins.get("zoom")) { + ChartJS.register(zoomPlugin); + } + }; + loadZoomPlugin(); + }, []); + const dispatch = useDispatch(); const chartRef = useRef(null); const { zeitraum, vonDatum, bisDatum, data, autoLoad, selectedId } = useSelector((state: RootState) => state.analogInputsHistory); + const selectedAnalogInput = useSelector( + (state: RootState) => state.selectedAnalogInput + ); + // ✅ Button-Klick → Fetch auslösen + const handleFetchData = () => { + if (!selectedAnalogInput?.id) return; + // Sicherstellen, dass die neuesten Werte aus dem DateRangePicker verwendet werden + const latestVonDatum = vonDatum || new Date().toISOString().slice(0, 10); + const latestBisDatum = bisDatum || new Date().toISOString().slice(0, 10); + + dispatch( + getAnalogInputsHistoryThunk({ + eingang: selectedAnalogInput.id, + zeitraum, + vonDatum: latestVonDatum, + bisDatum: latestBisDatum, + }) + ); + + if (chartRef.current) { + const chart = chartRef.current; + + // Aktualisiere die X-Achse basierend auf den neuesten Werten + chart.options.scales.x.min = new Date(latestVonDatum).getTime(); + chart.options.scales.x.max = new Date(latestBisDatum).getTime(); + + // Aktualisiere die Daten des Diagramms + const chartKey = selectedAnalogInput?.id ? String(selectedAnalogInput.id + 99) : null; + const inputData = chartKey ? data[chartKey] ?? [] : []; + const filteredData = inputData.filter((point) => { + const date = new Date(point.t); + const from = new Date(latestVonDatum); + const to = new Date(latestBisDatum); + return (!from || date >= from) && (!to || date <= to); + }); + + chart.data.datasets = [ + { + label: selectedAnalogInput?.label + ? `Messwerteingang ${selectedAnalogInput.label}` + : "Messwerte", + data: filteredData.map((point) => ({ x: point.t, y: point.m })), + fill: false, + borderColor: getColor("littwin-blue"), + backgroundColor: "rgba(59,130,246,0.3)", + borderWidth: 2, + pointRadius: 0, + tension: 0.1, + }, + ]; + + // Erzwinge ein vollständiges Redraw des Diagramms + chart.update("none"); + } + }; + + // ✅ Filtere Daten aus Redux + const chartKey = selectedAnalogInput?.id + ? String(selectedAnalogInput.id + 99) + : null; + const inputData = chartKey ? data[chartKey] ?? [] : []; + // ✅ Zeitbereich anwenden (nur Anzeige gefiltert) + const filteredData = inputData.filter((point) => { + const date = new Date(point.t); + const from = vonDatum ? new Date(vonDatum) : null; + const to = bisDatum ? new Date(bisDatum) : null; + + return (!from || date >= from) && (!to || date <= to); + }); useEffect(() => { const today = new Date(); const vor30Tagen = new Date(today); @@ -73,15 +152,29 @@ export default function AnalogInputsChart() { }; const dataKey = selectedId ? String(selectedId + 99) : null; + let filteredPoints: AnalogInputHistoryPoint[] = []; + if (dataKey && data[dataKey]) { + const fromDate = vonDatum ? new Date(vonDatum) : null; + const toDate = bisDatum ? new Date(bisDatum) : null; + + filteredPoints = data[dataKey].filter((p: AnalogInputHistoryPoint) => { + const pointDate = new Date(p.t); + return ( + (!fromDate || pointDate >= fromDate) && (!toDate || pointDate <= toDate) + ); + }); + } const chartData = { datasets: - dataKey && data[dataKey] + filteredPoints.length > 0 ? [ { - label: `Messwerteingang ${selectedId}`, - data: data[dataKey].map((p: AnalogInputHistoryPoint) => ({ - x: new Date(p.t), - y: p.m, + label: selectedAnalogInput?.label + ? `Messwerteingang ${selectedAnalogInput.label}` + : "Messwerte", + data: filteredData.map((point) => ({ + x: point.t, + y: point.m, })), fill: false, borderColor: getColor("littwin-blue"), @@ -94,36 +187,63 @@ export default function AnalogInputsChart() { : [], }; - const chartOptions: ChartOptions<"line"> = { + const chartOptions = { responsive: true, plugins: { - legend: { position: "top" }, - title: { display: true, text: "Verlauf" }, + legend: { position: "top" as const }, tooltip: { - mode: "index", + mode: "index" as const, intersect: false, callbacks: { - label: (ctx) => `Messwert: ${ctx.parsed.y}`, - title: (items) => - `Zeitpunkt: ${new Date(items[0].parsed.x).toLocaleString("de-DE")}`, + label: function (context: any) { + return `Messwert: ${context.parsed.y}`; + }, + title: function (tooltipItems: any[]) { + const date = tooltipItems[0].parsed.x; + return `Zeitpunkt: ${new Date(date).toLocaleString("de-DE")}`; + }, }, }, + title: { + display: true, + text: selectedAnalogInput?.label + ? `Verlauf: ${selectedAnalogInput.label}` + : "Messwert-Verlauf", + }, + zoom: { + pan: { enabled: true, mode: "x" as const }, + zoom: { wheel: { enabled: true }, pinch: { enabled: true }, mode: "x" as const }, + }, }, scales: { x: { - type: "time", + type: "time" as const, time: { - unit: "day", + unit: "day" as const, tooltipFormat: "dd.MM.yyyy HH:mm", - displayFormats: { day: "dd.MM.yyyy" }, + displayFormats: { + day: "dd.MM.yyyy", + }, }, adapters: { date: { locale: de } }, title: { display: true, text: "Zeit" }, + // ✅ Hier definieren wir den sichtbaren Bereich dynamisch + min: vonDatum ? new Date(vonDatum).getTime() : undefined, + max: bisDatum ? new Date(bisDatum).getTime() : undefined, + }, + y: { + title: { + display: true, + text: `Messwert ${selectedAnalogInput?.unit || ""}`, + }, }, - y: { title: { display: true, text: "Messwert" } }, }, }; - + // ✅ DateRangePicker Event → Redux-Datum setzen (aber KEIN Fetch!) + const handleDateChange = (from: string, to: string) => { + dispatch(setVonDatum(from)); + dispatch(setBisDatum(to)); + }; useEffect(() => { if (autoLoad && selectedId) { dispatch( @@ -137,6 +257,7 @@ export default function AnalogInputsChart() { dispatch(setAutoLoad(false)); // ✅ zurücksetzen, sonst endlose Schleife } }, [autoLoad, selectedId, dispatch, zeitraum, vonDatum, bisDatum]); + return (
@@ -173,7 +294,7 @@ export default function AnalogInputsChart() { +
+ +
+ ); +}; + +export default AnalogInputsChart;