feat: Struktur für Charts verbessert und Komponenten getrennt

- `LoopMeasurementChart.tsx` und `TDRChart.tsx` erstellt für separate Diagramm-Darstellungen.
- Neue Struktur unter `/components/modules/kue705FO/charts/` eingeführt.
- `ChartModal.tsx` bleibt für generelle Nutzung erhalten.
- Erhöhte Wartbarkeit und Modularität durch Trennung der Chart-Komponenten.
This commit is contained in:
ISA
2025-02-13 11:55:52 +01:00
parent d67ad97f83
commit 5c7b5555c4
17 changed files with 411 additions and 91 deletions

View File

@@ -3,13 +3,13 @@ import { render, fireEvent, screen } from "@testing-library/react";
import configureStore from "redux-mock-store"; import configureStore from "redux-mock-store";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import Kue705FO from "../../../components/modules/Kue705FO"; import Kue705FO from "../../../components/modules/kue705FO/Kue705FO";
// Mocks für externe Abhängigkeiten // Mocks für externe Abhängigkeiten
jest.mock("chart.js/auto", () => ({ jest.mock("chart.js/auto", () => ({
default: { default: {
register: jest.fn(), register: jest.fn(),
}, },
Chart: jest.fn().mockImplementation(() => ({ Chart: jest.fn().mockImplementation(() => ({
destroy: jest.fn(), destroy: jest.fn(),
update: jest.fn(), update: jest.fn(),

View File

@@ -1,66 +0,0 @@
import React from "react";
import ReactModal from "react-modal";
import TDRPopup from "./LoopTDRChartActionBar";
interface ChartModalProps {
isOpen: boolean;
onClose: () => void;
chartRef: React.RefObject<HTMLCanvasElement>;
}
const ChartModal: React.FC<ChartModalProps> = ({
isOpen,
onClose,
chartRef,
}) => {
return (
<ReactModal
isOpen={isOpen}
onRequestClose={onClose}
ariaHideApp={false}
style={{
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
content: {
top: "50%",
left: "50%",
right: "auto",
bottom: "auto",
marginRight: "-50%",
transform: "translate(-50%, -50%)",
width: "80%",
maxWidth: "50rem",
height: "30rem",
padding: "1rem",
overflow: "hidden",
},
}}
>
<button
onClick={onClose}
style={{
position: "absolute",
top: "0.625rem",
right: "0.625rem",
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i className="bi bi-x-circle-fill"></i>
</button>
<div style={{ maxHeight: "100%", overflow: "auto" }}>
<TDRPopup />
</div>
<canvas
id="myChart"
ref={chartRef}
style={{ width: "100%", height: "20rem" }}
></canvas>
</ReactModal>
);
};
export default ChartModal;

View File

@@ -4,10 +4,12 @@ import { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import { setVariables } from "../../../redux/slices/variablesSlice"; import { setVariables } from "../../../redux/slices/variablesSlice";
import "bootstrap-icons/font/bootstrap-icons.css"; // Import Bootstrap Icons import "bootstrap-icons/font/bootstrap-icons.css"; // Import Bootstrap Icons
import handleSave, { OriginalValues } from "./handlers/handleSave"; import handleSave, {
import handleDisplayEinschalten from "./handlers/handleDisplayEinschalten"; OriginalValues,
import handleChange from "./handlers/handleChange"; } from "../../modules/kue705FO/handlers/handleSave";
import firmwareUpdate from "./handlers/firmwareUpdate"; import handleDisplayEinschalten from "../../modules/kue705FO/handlers/handleDisplayEinschalten";
import handleChange from "../../modules/kue705FO/handlers/handleChange";
import firmwareUpdate from "../../modules/kue705FO/handlers/firmwareUpdate";
import decodeToken from "../../../utils/decodeToken"; import decodeToken from "../../../utils/decodeToken";
// Props-Typen definieren // Props-Typen definieren
interface KueModalProps { interface KueModalProps {

View File

@@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import DateRangePicker from "./DateRangePicker"; import DateRangePicker from "./DateRangePicker";
import ChartModal from "./ChartModal"; // Importiere das Chart-Modal import ChartModal from "../../modules/kue705FO/charts/ChartModal"; // Importiere das Chart-Modal
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { setChartData } from "../../../redux/slices/chartDataSlice"; import { setChartData } from "../../../redux/slices/chartDataSlice";
import { RootState } from "../../../redux/store"; import { RootState } from "../../../redux/store";
@@ -93,6 +93,7 @@ const LoopTDRChartActionBar: React.FC = () => {
<ChartModal <ChartModal
isOpen={showChartModal} isOpen={showChartModal}
onClose={() => setShowChartModal(false)} onClose={() => setShowChartModal(false)}
chartRef={React.createRef<HTMLCanvasElement>()}
/> />
)} )}
</div> </div>

View File

@@ -3,24 +3,25 @@ import React, { useState, useEffect, useRef } from "react";
import ReactModal from "react-modal"; import ReactModal from "react-modal";
import Chart from "chart.js/auto"; import Chart from "chart.js/auto";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import KueModal from "../modales/kueModal/KueModal"; import KueModal from "../../modales/kueModal/KueModal";
import "bootstrap-icons/font/bootstrap-icons.css"; // Import Bootstrap Icons import "bootstrap-icons/font/bootstrap-icons.css"; // Import Bootstrap Icons
import { RootState } from "../../redux/store"; import { RootState } from "../../../redux/store";
import { DataTDR } from "../../redux/types/chartDataTypesTDR"; import { DataTDR } from "../../../redux/types/chartDataTypesTDR";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { import {
setSelectedChartData, setSelectedChartData,
setSelectedFileName, setSelectedFileName,
} from "../../redux/slices/variablesSlice"; } from "../../../redux/slices/variablesSlice";
import TDRPopup from "../modales/kueModal/LoopTDRChartActionBar"; import TDRPopup from "../../modales/kueModal/LoopTDRChartActionBar";
import { createLoopChart, createTDRChart } from "../../utils/chartUtils"; import { createLoopChart, createTDRChart } from "../../../utils/chartUtils";
import { getAlarmDisplayText } from "../../utils/alarmUtils"; import { getAlarmDisplayText } from "../../../utils/alarmUtils";
import { goLoop } from "../../utils/goLoop"; import { goLoop } from "../../../utils/goLoop";
import { goTDR } from "../../utils/goTDR"; import { goTDR } from "../../../utils/goTDR";
import { loadTDRChartData } from "../../utils/loadTDRChartData"; import { loadTDRChartData } from "../../../utils/loadTDRChartData";
import { loadLoopChartData } from "../../utils/loadLoopChartData"; import { loadLoopChartData } from "../../../utils/loadLoopChartData";
import { Kue705FOProps } from "../../types/components/Kue705FOProps"; import { Kue705FOProps } from "../../../types/components/Kue705FOProps";
import ChartModal from "../modales/kueModal/ChartModal"; import ChartModal from "./charts/ChartModal";
import { setActiveMode } from "../../../redux/slices/chartDataSlice";
const Kue705FO: React.FC<Kue705FOProps> = ({ const Kue705FO: React.FC<Kue705FOProps> = ({
isolationswert, isolationswert,
@@ -102,6 +103,7 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
setActiveButton("Schleife"); setActiveButton("Schleife");
setloopTitleText("Schleifenwiderstand [kOhm]"); setloopTitleText("Schleifenwiderstand [kOhm]");
setLoopDisplayValue(schleifenwiderstand); // Setze den Wert auf schleifenwiderstand setLoopDisplayValue(schleifenwiderstand); // Setze den Wert auf schleifenwiderstand
dispatch(setActiveMode("Schleife")); // 🔥 Speichert den Modus in Redux
} else if (button === "TDR") { } else if (button === "TDR") {
setActiveButton("TDR"); setActiveButton("TDR");
setloopTitleText("Entfernung [Km]"); setloopTitleText("Entfernung [Km]");
@@ -110,6 +112,7 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
? tdrLocation[slotIndex] ? tdrLocation[slotIndex]
: "0" : "0"
); // Setze den Wert auf tdrLocation oder "0" als Fallback ); // Setze den Wert auf tdrLocation oder "0" als Fallback
dispatch(setActiveMode("TDR")); // 🔥 Speichert den Modus in Redux
} }
}; };

View File

@@ -0,0 +1,171 @@
import React, { useEffect, useRef } from "react";
import ReactModal from "react-modal";
import { useSelector } from "react-redux";
import { RootState } from "../../../../redux/store";
import Chart from "chart.js/auto";
import "chartjs-adapter-date-fns";
import { parseISO } from "date-fns";
import LoopTDRPopup from "../../../modales/kueModal/LoopTDRChartActionBar";
interface ChartModalProps {
isOpen: boolean;
onClose: () => void;
}
const ChartModal: React.FC<ChartModalProps> = ({ isOpen, onClose }) => {
const chartRef = useRef<HTMLCanvasElement>(null);
const chartInstance = useRef<Chart | null>(null);
// Redux State abrufen
const chartData = useSelector((state: RootState) => state.chartData.data);
const activeMode = useSelector(
(state: RootState) => state.chartData.activeMode
);
useEffect(() => {
if (chartRef.current && chartData.length > 0) {
if (chartInstance.current) {
chartInstance.current.destroy();
}
const ctx = chartRef.current.getContext("2d");
if (ctx) {
const labels = chartData.map((entry) =>
entry.timestamp ? parseISO(entry.timestamp) : new Date()
);
chartInstance.current = new Chart(ctx, {
type: "line",
data: {
labels,
datasets: [
{
label: "Isolationswiderstand (MOhm)",
data: chartData.map((entry) => entry.isolation ?? 0),
borderColor: "rgba(0, 123, 255, 1)",
backgroundColor: "rgba(0, 123, 255, 0.2)",
tension: 0.1,
yAxisID: "y-left",
},
{
label: "Schleifenwiderstand (kOhm)",
data: chartData.map((entry) => entry.loop ?? 0),
borderColor: "rgba(108, 117, 125, 1)",
backgroundColor: "rgba(108, 117, 125, 0.2)",
tension: 0.1,
yAxisID: "y-right",
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: "time",
time: {
unit: "hour",
tooltipFormat: "dd.MM.yyyy HH:mm",
},
title: {
display: true,
text: "Zeit",
},
grid: {
drawBorder: true, // 🔥 Stellt sicher, dass die X-Achse sichtbar ist
},
},
y: {
id: "y-left",
position: "left",
title: {
display: true,
text: "MOhm",
},
min: 0,
max:
Math.max(...chartData.map((entry) => entry.isolation ?? 1)) +
10,
grid: {
drawOnChartArea: true, // 🔥 Korrektur, damit es sich nicht überlagert
},
},
"y-right": {
id: "y-right",
position: "right",
title: {
display: true,
text: "kOhm",
},
min: 0,
max: Math.max(...chartData.map((entry) => entry.loop ?? 1)) + 1,
grid: {
drawOnChartArea: false, // 🔥 Verhindert doppelte Linien
},
},
},
},
});
}
}
return () => {
if (chartInstance.current) {
chartInstance.current.destroy();
chartInstance.current = null;
}
};
}, [chartData, activeMode]);
return (
<ReactModal
isOpen={isOpen}
onRequestClose={onClose}
ariaHideApp={false}
style={{
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
content: {
top: "50%",
left: "50%",
right: "auto",
bottom: "auto",
marginRight: "-50%",
transform: "translate(-50%, -50%)",
width: "80%",
maxWidth: "50rem",
height: "30rem",
padding: "1rem",
overflow: "hidden",
},
}}
>
<button
onClick={onClose}
style={{
position: "absolute",
top: "0.625rem",
right: "0.625rem",
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i className="bi bi-x-circle-fill"></i>
</button>
{/* Beibehaltung der UI mit Kalender und Buttons */}
<div style={{ maxHeight: "100%", overflow: "auto" }}>
<LoopTDRPopup />
</div>
{/* Canvas für das Chart */}
<canvas
ref={chartRef}
style={{ width: "100%", height: "20rem", marginTop: "1rem" }}
></canvas>
</ReactModal>
);
};
export default ChartModal;

View File

@@ -0,0 +1,110 @@
import React, { useEffect, useRef } from "react";
import { useSelector } from "react-redux";
import { RootState } from "../../../redux/store";
import Chart from "chart.js/auto";
import "chartjs-adapter-date-fns";
import { parseISO } from "date-fns";
const LoopMeasurementChart: React.FC = () => {
const chartRef = useRef<HTMLCanvasElement>(null);
const chartInstance = useRef<Chart | null>(null);
// Redux-Daten abrufen
const chartData = useSelector((state: RootState) => state.chartData.data);
useEffect(() => {
if (chartRef.current && chartData.length > 0) {
if (chartInstance.current) {
chartInstance.current.destroy(); // Bestehendes Chart zerstören
}
const ctx = chartRef.current.getContext("2d");
if (ctx) {
const labels = chartData.map((entry) =>
entry.timestamp ? parseISO(entry.timestamp) : new Date()
);
chartInstance.current = new Chart(ctx, {
type: "line",
data: {
labels,
datasets: [
{
label: "Schleifenwiderstand (kOhm)",
data: chartData.map((entry) => entry.loop ?? 0),
borderColor: "rgba(75, 192, 192, 1)", // Türkis
backgroundColor: "rgba(75, 192, 192, 0.2)",
tension: 0.1,
yAxisID: "y-left",
},
{
label: "Zusätzliche Schleifenmesswerte",
data: chartData.map((entry) => entry.additional ?? 0),
borderColor: "rgba(255, 159, 64, 1)", // Orange
backgroundColor: "rgba(255, 159, 64, 0.2)",
tension: 0.1,
yAxisID: "y-right",
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: "time",
time: {
unit: "hour",
tooltipFormat: "dd.MM.yyyy HH:mm",
},
title: {
display: true,
text: "Zeit",
},
grid: {
drawBorder: true,
},
},
y: {
id: "y-left",
position: "left",
title: {
display: true,
text: "kOhm",
},
min: 0,
max: Math.max(...chartData.map((entry) => entry.loop ?? 1)) + 1,
},
"y-right": {
id: "y-right",
position: "right",
title: {
display: true,
text: "Zusätzliche Werte",
},
min: 0,
max:
Math.max(...chartData.map((entry) => entry.additional ?? 1)) +
1,
grid: {
drawOnChartArea: false,
},
},
},
},
});
}
}
return () => {
if (chartInstance.current) {
chartInstance.current.destroy();
chartInstance.current = null;
}
};
}, [chartData]);
return <canvas ref={chartRef} style={{ width: "100%", height: "20rem" }} />;
};
export default LoopMeasurementChart;

View File

@@ -0,0 +1,71 @@
import React, { useEffect, useRef } from "react";
import { useSelector } from "react-redux";
import { RootState } from "../../../redux/store";
import Chart from "chart.js/auto";
import "chartjs-adapter-date-fns";
import { parseISO } from "date-fns";
const TDRChart: React.FC = () => {
const chartRef = useRef<HTMLCanvasElement>(null);
const chartInstance = useRef<Chart | null>(null);
const chartData = useSelector((state: RootState) => state.chartData.data);
useEffect(() => {
if (chartRef.current && chartData.length > 0) {
if (chartInstance.current) {
chartInstance.current.destroy();
}
const ctx = chartRef.current.getContext("2d");
if (ctx) {
chartInstance.current = new Chart(ctx, {
type: "line",
data: {
labels: chartData.map((entry) => parseISO(entry.timestamp)),
datasets: [
{
label: "Isolationswiderstand (MOhm)",
data: chartData.map((entry) => entry.isolation ?? 0),
borderColor: "rgba(0, 123, 255, 1)",
backgroundColor: "rgba(0, 123, 255, 0.2)",
tension: 0.1,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: "time",
time: {
unit: "hour",
tooltipFormat: "dd.MM.yyyy HH:mm",
},
title: { display: true, text: "Zeit" },
},
y: {
title: { display: true, text: "MOhm" },
min: 0,
max:
Math.max(...chartData.map((entry) => entry.isolation ?? 1)) +
10,
},
},
},
});
}
}
return () => {
if (chartInstance.current) {
chartInstance.current.destroy();
chartInstance.current = null;
}
};
}, [chartData]);
return <canvas ref={chartRef} style={{ width: "100%", height: "20rem" }} />;
};
export default TDRChart;

View File

@@ -5,5 +5,5 @@
2: Patch oder Hotfix (Bugfixes oder kleine Änderungen). 2: Patch oder Hotfix (Bugfixes oder kleine Änderungen).
*/ */
const webVersion = "1.0.6.9"; const webVersion = "1.0.6.10";
export default webVersion; export default webVersion;

20
package-lock.json generated
View File

@@ -17,6 +17,7 @@
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"chart.js": "^4.4.5", "chart.js": "^4.4.5",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-plugin-zoom": "^2.0.1", "chartjs-plugin-zoom": "^2.0.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@@ -25,6 +26,7 @@
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"next": "^14.2.23", "next": "^14.2.23",
"react": "^18.3.1", "react": "^18.3.1",
"react-chartjs-2": "^5.3.0",
"react-date-picker": "^11.0.0", "react-date-picker": "^11.0.0",
"react-datepicker": "^8.0.0", "react-datepicker": "^8.0.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@@ -2606,6 +2608,15 @@
"pnpm": ">=8" "pnpm": ">=8"
} }
}, },
"node_modules/chartjs-adapter-date-fns": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
"peerDependencies": {
"chart.js": ">=2.8.0",
"date-fns": ">=2.0.0"
}
},
"node_modules/chartjs-plugin-zoom": { "node_modules/chartjs-plugin-zoom": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz", "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz",
@@ -6892,6 +6903,15 @@
} }
} }
}, },
"node_modules/react-chartjs-2": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
"peerDependencies": {
"chart.js": "^4.1.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-date-picker": { "node_modules/react-date-picker": {
"version": "11.0.0", "version": "11.0.0",
"resolved": "https://registry.npmjs.org/react-date-picker/-/react-date-picker-11.0.0.tgz", "resolved": "https://registry.npmjs.org/react-date-picker/-/react-date-picker-11.0.0.tgz",

View File

@@ -22,6 +22,7 @@
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"chart.js": "^4.4.5", "chart.js": "^4.4.5",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-plugin-zoom": "^2.0.1", "chartjs-plugin-zoom": "^2.0.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@@ -30,6 +31,7 @@
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"next": "^14.2.23", "next": "^14.2.23",
"react": "^18.3.1", "react": "^18.3.1",
"react-chartjs-2": "^5.3.0",
"react-date-picker": "^11.0.0", "react-date-picker": "^11.0.0",
"react-datepicker": "^8.0.0", "react-datepicker": "^8.0.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import Kue705FO from "../components/modules/Kue705FO"; import Kue705FO from "../components/modules/kue705FO/Kue705FO";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
function Kabelueberwachung() { function Kabelueberwachung() {

View File

@@ -2,10 +2,12 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface ChartDataState { interface ChartDataState {
data: any[]; data: any[];
activeMode: "Schleife" | "TDR"; // 🔥 Neuer Zustand für den aktiven Modus
} }
const initialState: ChartDataState = { const initialState: ChartDataState = {
data: [], data: [],
activeMode: "Schleife", // Standard ist Schleife
}; };
export const chartDataSlice = createSlice({ export const chartDataSlice = createSlice({
@@ -18,8 +20,12 @@ export const chartDataSlice = createSlice({
clearChartData: (state) => { clearChartData: (state) => {
state.data = []; state.data = [];
}, },
setActiveMode: (state, action: PayloadAction<"Schleife" | "TDR">) => {
state.activeMode = action.payload; // 🔥 Speichert den Modus (Schleife oder TDR)
},
}, },
}); });
export const { setChartData, clearChartData } = chartDataSlice.actions; export const { setChartData, clearChartData, setActiveMode } =
chartDataSlice.actions;
export default chartDataSlice.reducer; export default chartDataSlice.reducer;