From 99294f26da3dc44fe7b1345ffbf15ac431240657 Mon Sep 17 00:00:00 2001 From: ISA Date: Fri, 11 Jul 2025 14:01:57 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20AnalogInputsChart=20mit=20DateRangePick?= =?UTF-8?q?er=20und=20vollst=C3=A4ndiger=20Redux-Integration=20erweitert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - analogInputsHistorySlice angepasst: zeitraum, vonDatum, bisDatum und data hinzugefügt - Typdefinitionen im Slice und Thunk korrigiert - getAnalogInputsHistoryThunk erweitert, um vonDatum und bisDatum zu akzeptieren - DateRangePicker korrekt in AnalogInputsChart.tsx integriert - Fehler bei Selector-Zugriffen und Dispatch behoben --- .env.development | 2 +- .env.production | 2 +- CHANGELOG.md | 11 + .../main/analogInputs/AnalogInputsChart.tsx | 248 ++++++++---------- package-lock.json | 4 +- package.json | 2 +- pages/_app.tsx | 9 +- redux/slices/analogInputsChartSlice.ts | 40 --- redux/slices/analogInputsHistorySlice.ts | 49 ++-- redux/thunks/getAnalogInputsChartDataThunk.ts | 1 - redux/thunks/getAnalogInputsHistoryThunk.ts | 42 +-- 11 files changed, 187 insertions(+), 223 deletions(-) delete mode 100644 redux/slices/analogInputsChartSlice.ts diff --git a/.env.development b/.env.development index 6c53f3e..40745cf 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.597 +NEXT_PUBLIC_APP_VERSION=1.6.598 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 b7ffe69..9d6b1f5 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.597 +NEXT_PUBLIC_APP_VERSION=1.6.598 NEXT_PUBLIC_CPL_MODE=production \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f780ff..46429c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [1.6.598] – 2025-07-11 + +- feat: AnalogInputsChart mit DateRangePicker und vollständiger Redux-Integration erweitert + +- analogInputsHistorySlice angepasst: zeitraum, vonDatum, bisDatum und data hinzugefügt +- Typdefinitionen im Slice und Thunk korrigiert +- getAnalogInputsHistoryThunk erweitert, um vonDatum und bisDatum zu akzeptieren +- DateRangePicker korrekt in AnalogInputsChart.tsx integriert +- Fehler bei Selector-Zugriffen und Dispatch behoben + +--- ## [1.6.597] – 2025-07-11 - feat(api): Zeitraum und Eingang als Pflichtparameter für AnalogInputs-API eingeführt diff --git a/components/main/analogInputs/AnalogInputsChart.tsx b/components/main/analogInputs/AnalogInputsChart.tsx index 06e95ad..7fe1981 100644 --- a/components/main/analogInputs/AnalogInputsChart.tsx +++ b/components/main/analogInputs/AnalogInputsChart.tsx @@ -1,12 +1,8 @@ "use client"; -type AnalogInput = { - id: number; - label: string; - unit: string; -}; -import React, { useEffect } from "react"; +import React, { useEffect, useRef } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { RootState, AppDispatch } from "@/redux/store"; import { Line } from "react-chartjs-2"; -import { getColor } from "@/utils/colors"; import { Chart as ChartJS, LineElement, @@ -18,19 +14,19 @@ import { Filler, TimeScale, } from "chart.js"; +import type { ChartOptions } from "chart.js"; import "chartjs-adapter-date-fns"; import { de } from "date-fns/locale"; -import { useSelector, useDispatch } from "react-redux"; -import type { RootState, AppDispatch } from "@/redux/store"; import { getAnalogInputsHistoryThunk } from "@/redux/thunks/getAnalogInputsHistoryThunk"; -import DateRangePicker from "@/components/common/DateRangePicker"; -import { Listbox } from "@headlessui/react"; import { setVonDatum, setBisDatum, -} from "@/redux/slices/analogInputsChartSlice"; + setZeitraum, +} from "@/redux/slices/analogInputsHistorySlice"; +import DateRangePicker from "@/components/common/DateRangePicker"; +import { Listbox } from "@headlessui/react"; +import { getColor } from "@/utils/colors"; -// Basis-Registrierung (ohne Zoom-Plugin) ChartJS.register( LineElement, PointElement, @@ -42,160 +38,142 @@ ChartJS.register( TimeScale ); +type AnalogInputHistoryPoint = { + t: string; + m: number; +}; + export default function AnalogInputsChart({ selectedId, }: { selectedId: number | null; }) { - const selectedInput = useSelector( - (state: RootState) => state.selectedAnalogInput - ) as unknown as AnalogInput | null; - const dispatch = useDispatch(); + const chartRef = useRef(null); - type AnalogInputHistoryPoint = { t: string | number | Date; m: number }; - - const { data } = useSelector( + const { zeitraum, vonDatum, bisDatum, data, isLoading } = useSelector( (state: RootState) => state.analogInputsHistory - ) as { - data: { [key: string]: AnalogInputHistoryPoint[] }; - }; - const zeitraum = useSelector( - (state: RootState) => state.analogInputsHistory.zeitraum ); - const handleFetchData = () => { - if (!selectedId || !zeitraum) return; - dispatch(getAnalogInputsHistoryThunk({ eingang: selectedId, zeitraum })); + useEffect(() => { + const today = new Date(); + const vor30Tagen = new Date(today); + vor30Tagen.setDate(today.getDate() - 30); + + if (!vonDatum) dispatch(setVonDatum(vor30Tagen.toISOString().slice(0, 10))); + if (!bisDatum) dispatch(setBisDatum(today.toISOString().slice(0, 10))); + }, [dispatch, vonDatum, bisDatum]); + + const handleFetchChartData = () => { + if (!selectedId) return; + dispatch( + getAnalogInputsHistoryThunk({ + eingang: selectedId, + zeitraum, + vonDatum, + bisDatum, + }) + ); }; - useEffect(() => { - if (selectedId && zeitraum) { - dispatch(getAnalogInputsHistoryThunk({ eingang: selectedId, zeitraum })); - } - }, [dispatch, selectedId, zeitraum]); - - // ✅ Zoom-Plugin dynamisch importieren und registrieren - 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(); - }, []); - - if (!selectedId) { - return ( -
Bitte einen Messwerteingang auswählen
- ); - } - - const key = String(selectedId + 99); - const inputData = data[key]; - - if (!inputData) { - return ( -
- Keine Verlaufsdaten für Messwerteingang {selectedId} gefunden. -
- ); - } - + const dataKey = selectedId ? String(selectedId + 99) : null; const chartData = { - datasets: [ - { - label: `Messkurve ${selectedInput?.label ?? "Eingang"} [${ - selectedInput?.unit ?? "" - }]`, - data: inputData.map((point: AnalogInputHistoryPoint) => ({ - x: point.t, - y: point.m, - })), - fill: false, - borderColor: getColor("littwin-blue"), - backgroundColor: "rgba(59,130,246,0.5)", - borderWidth: 2, - pointRadius: 0, - pointHoverRadius: 10, - tension: 0.1, - }, - ], + datasets: + dataKey && data[dataKey] + ? [ + { + label: `Messwerteingang ${selectedId}`, + data: data[dataKey].map((p: AnalogInputHistoryPoint) => ({ + x: new Date(p.t), + y: p.m, + })), + fill: false, + borderColor: getColor("littwin-blue"), + backgroundColor: "rgba(59,130,246,0.3)", + borderWidth: 2, + pointRadius: 0, + tension: 0.1, + }, + ] + : [], }; - const chartOptions = { + const chartOptions: ChartOptions<"line"> = { responsive: true, plugins: { - legend: { position: "top" as const }, + legend: { position: "top" }, + title: { display: true, text: "Verlauf" }, tooltip: { - mode: "index" as const, + mode: "index", intersect: false, callbacks: { - label: function (context: import("chart.js").TooltipItem<"line">) { - const y = context.parsed.y; - return `Messwert: ${y}`; - }, - title: function ( - tooltipItems: import("chart.js").TooltipItem<"line">[] - ) { - const date = tooltipItems[0].parsed.x; - return `Zeitpunkt: ${new Date(date).toLocaleString("de-DE")}`; - }, - }, - }, - - title: { - display: true, - text: `Verlauf der letzten 30 Tage`, - }, - zoom: { - pan: { - enabled: true, - mode: "x" as const, - }, - zoom: { - wheel: { enabled: true }, - pinch: { enabled: true }, - mode: "x" as const, + label: (ctx) => `Messwert: ${ctx.parsed.y}`, + title: (items) => + `Zeitpunkt: ${new Date(items[0].parsed.x).toLocaleString("de-DE")}`, }, }, }, scales: { x: { - type: "time" as const, + type: "time", time: { - unit: "day" as const, // nur Datum in Achse - tooltipFormat: "dd.MM.yyyy HH:mm", // aber Uhrzeit im Tooltip sichtbar - displayFormats: { - day: "dd.MM.yyyy", - }, - }, - - adapters: { - date: { - locale: de, - }, - }, - title: { - display: true, - text: "Zeit", - }, - }, - y: { - title: { - display: true, - text: `Messwert [${selectedInput?.unit ?? ""}]`, + unit: "day", + 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" } }, }, }; return ( -
- +
+
+ + + dispatch(setZeitraum(v))}> +
+ + + {zeitraum === "DIA0" + ? "Alle Messwerte" + : zeitraum === "DIA1" + ? "Stündlich" + : "Täglich"} + + + + + {["DIA0", "DIA1", "DIA2"].map((option) => ( + + {option === "DIA0" + ? "Alle Messwerte" + : option === "DIA1" + ? "Stündlich" + : "Täglich"} + + ))} + +
+
+ + +
+ +
+ +
); } diff --git a/package-lock.json b/package-lock.json index 5f17a2f..721af71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cpl-v4", - "version": "1.6.597", + "version": "1.6.598", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cpl-v4", - "version": "1.6.597", + "version": "1.6.598", "dependencies": { "@fontsource/roboto": "^5.1.0", "@headlessui/react": "^2.2.4", diff --git a/package.json b/package.json index c5a9dcb..bb3d585 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cpl-v4", - "version": "1.6.597", + "version": "1.6.598", "private": true, "scripts": { "dev": "next dev", diff --git a/pages/_app.tsx b/pages/_app.tsx index 3a230e9..b36d02d 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -87,7 +87,14 @@ function AppContent({ } else if (pathname.includes("digitalOutputs")) { dispatch(getDigitalOutputsThunk()); } else if (pathname.includes("analogHistory")) { - dispatch(getAnalogInputsHistoryThunk()); + dispatch( + getAnalogInputsHistoryThunk({ + eingang: 1, // Beispielwert, ggf. dynamisch setzen + zeitraum: "tag", // Beispielwert, ggf. dynamisch setzen + vonDatum: new Date().toISOString().split("T")[0], // heutiges Datum + bisDatum: new Date().toISOString().split("T")[0], // heutiges Datum + }) + ); } else if (pathname.includes("dashboard")) { dispatch(getLast20MessagesThunk()); } else if (pathname.includes("einstellungen")) { diff --git a/redux/slices/analogInputsChartSlice.ts b/redux/slices/analogInputsChartSlice.ts deleted file mode 100644 index 8270e5d..0000000 --- a/redux/slices/analogInputsChartSlice.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; - -export type Zeitraum = "DIA0" | "DIA1" | "DIA2"; - -interface ChartState { - zeitraum: Zeitraum; - vonDatum: string; - bisDatum: string; - isLoading: boolean; -} - -const initialState: ChartState = { - zeitraum: "DIA0", - vonDatum: "", - bisDatum: "", - isLoading: false, -}; - -const analogInputsChartSlice = createSlice({ - name: "analogInputsChart", - initialState, - reducers: { - setZeitraum: (state, action: PayloadAction) => { - state.zeitraum = action.payload; - }, - setVonDatum: (state, action: PayloadAction) => { - state.vonDatum = action.payload; - }, - setBisDatum: (state, action: PayloadAction) => { - state.bisDatum = action.payload; - }, - setIsLoading: (state, action: PayloadAction) => { - state.isLoading = action.payload; - }, - }, -}); - -export const { setZeitraum, setVonDatum, setBisDatum, setIsLoading } = - analogInputsChartSlice.actions; -export default analogInputsChartSlice.reducer; diff --git a/redux/slices/analogInputsHistorySlice.ts b/redux/slices/analogInputsHistorySlice.ts index 7bfca5d..bce8e7d 100644 --- a/redux/slices/analogInputsHistorySlice.ts +++ b/redux/slices/analogInputsHistorySlice.ts @@ -1,4 +1,3 @@ -// /redux/slices/analogInputsHistorySlice.ts import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { getAnalogInputsHistoryThunk } from "../thunks/getAnalogInputsHistoryThunk"; @@ -7,27 +6,37 @@ export type AnalogInputsHistoryEntry = { m: number; }; -interface AnalogInputsHistoryState { - data: Record; +export interface InputHistoryState { + zeitraum: string; + vonDatum: string; + bisDatum: string; isLoading: boolean; error: string | null; - zeitraum: string; // z.B. "DIA0", "DIA1", "DIA2" + data: Record; } -const initialState: AnalogInputsHistoryState = { - data: {}, +const initialState: InputHistoryState = { + zeitraum: "DIA0", + vonDatum: "", + bisDatum: "", isLoading: false, error: null, - zeitraum: "DIA0", + data: {}, }; -const analogInputsHistorySlice = createSlice({ +export const analogInputsHistorySlice = createSlice({ name: "analogInputsHistory", initialState, reducers: { setZeitraum: (state, action: PayloadAction) => { state.zeitraum = action.payload; }, + setVonDatum: (state, action: PayloadAction) => { + state.vonDatum = action.payload; + }, + setBisDatum: (state, action: PayloadAction) => { + state.bisDatum = action.payload; + }, }, extraReducers: (builder) => { builder @@ -35,22 +44,11 @@ const analogInputsHistorySlice = createSlice({ state.isLoading = true; state.error = null; }) - .addCase( - getAnalogInputsHistoryThunk.fulfilled, - ( - state, - action: PayloadAction<{ - eingang: number; - zeitraum: string; - daten: AnalogInputsHistoryEntry[]; - }> - ) => { - const key = String(action.payload.eingang + 99); // z.B. 100 für AE1 - state.data[key] = action.payload.daten; - state.zeitraum = action.payload.zeitraum; - state.isLoading = false; - } - ) + .addCase(getAnalogInputsHistoryThunk.fulfilled, (state, action) => { + const key = String(action.payload.eingang + 99); + state.data[key] = action.payload.daten; + state.isLoading = false; + }) .addCase(getAnalogInputsHistoryThunk.rejected, (state, action) => { state.isLoading = false; state.error = action.payload as string; @@ -58,6 +56,7 @@ const analogInputsHistorySlice = createSlice({ }, }); -export const { setZeitraum } = analogInputsHistorySlice.actions; +export const { setZeitraum, setVonDatum, setBisDatum } = + analogInputsHistorySlice.actions; export default analogInputsHistorySlice.reducer; diff --git a/redux/thunks/getAnalogInputsChartDataThunk.ts b/redux/thunks/getAnalogInputsChartDataThunk.ts index 2b31af5..9a28966 100644 --- a/redux/thunks/getAnalogInputsChartDataThunk.ts +++ b/redux/thunks/getAnalogInputsChartDataThunk.ts @@ -1,6 +1,5 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; import { RootState } from "@/redux/store"; -import { Zeitraum } from "@/redux/slices/analogInputsChartSlice"; export const getAnalogInputsChartDataThunk = createAsyncThunk( "analogInputsChart/fetchChartData", diff --git a/redux/thunks/getAnalogInputsHistoryThunk.ts b/redux/thunks/getAnalogInputsHistoryThunk.ts index 7e28e66..c9b6383 100644 --- a/redux/thunks/getAnalogInputsHistoryThunk.ts +++ b/redux/thunks/getAnalogInputsHistoryThunk.ts @@ -4,22 +4,32 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; import { fetchAnalogInputsHistory } from "@/services/fetchAnalogInputsHistoryService"; import { AnalogInputsHistoryEntry } from "../slices/analogInputsHistorySlice"; -export const getAnalogInputsHistoryThunk = createAsyncThunk< - { - eingang: number; - zeitraum: string; - daten: AnalogInputsHistoryEntry[]; - }, - { eingang: number; zeitraum: string } ->("analogInputsHistory/fetch", async ({ eingang, zeitraum }, thunkAPI) => { - try { - const response = await fetchAnalogInputsHistory(eingang, zeitraum); - return { +export const getAnalogInputsHistoryThunk = createAsyncThunk( + "analogInputsHistory/fetch", + async ( + { eingang, zeitraum, - daten: response.daten, - }; - } catch (error: any) { - return thunkAPI.rejectWithValue(error.message ?? "Fehler beim Laden"); + vonDatum, + bisDatum, + }: { + eingang: number; + zeitraum: string; + vonDatum: string; + bisDatum: string; + }, + thunkAPI + ) => { + try { + const response = await fetchAnalogInputsHistory(eingang, zeitraum); + + return { + eingang, + zeitraum, + daten: response.daten as AnalogInputsHistoryEntry[], + }; + } catch (error: any) { + return thunkAPI.rejectWithValue(error.message ?? "Fehler beim Laden"); + } } -}); +);