fix: Vergleich und Speicherung von Änderungen im KUE-Modul korrigiert

- Originalwerte werden jetzt direkt aus window.win_kueXYZ geladen
- Vergleiche in handleSave.ts angepasst für stabile Zahl/String-Auswertung
- Fehlerhafte Meldung „Keine Änderungen vorgenommen“ behoben
- Nur geänderte Werte werden per GET-API gesendet
This commit is contained in:
ISA
2025-04-22 11:59:44 +02:00
parent d0568dc576
commit cfbc56206c
11 changed files with 519 additions and 392 deletions

View File

@@ -1,7 +1,7 @@
"use client"; // components/modules/kue705FO/Kue705FO.tsx
import React, { useState, useEffect, useRef, useMemo } from "react";
import { useSelector } from "react-redux";
import KueModal from "./KueModal";
import KueModal from "./modals/KueModal";
import "bootstrap-icons/font/bootstrap-icons.css"; // Import Bootstrap Icons
import { Kue705FOProps } from "../../../../types/components/Kue705FOProps";
import ChartSwitcher from "./Charts/ChartSwitcher";

View File

@@ -1,304 +0,0 @@
"use client"; // components/modales/KueModal.jsx
import ReactModal from "react-modal";
import { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import type { RootState } from "../../../../redux/store";
import "bootstrap-icons/font/bootstrap-icons.css";
import handleSave, { OriginalValues } from "./handlers/handleSave";
import handleDisplayEinschalten from "./handlers/handleDisplayEinschalten";
import firmwareUpdate from "./handlers/firmwareUpdate";
import decodeToken from "../../../../utils/decodeToken";
import { setKueData } from "../../../../redux/slices/kueDataSlice";
interface KueModalProps {
showModal: boolean;
onClose: () => void;
slot: number;
onModulNameChange: (id: string) => void;
}
function KueModal({
showModal,
onClose,
slot,
onModulNameChange,
}: KueModalProps): JSX.Element {
const dispatch = useDispatch();
const isAdminLoggedIn = useSelector(
(state: any) => state.authSlice.isAdminLoggedIn
);
const [isAdmin, setIsAdmin] = useState(false);
const {
kueID,
kueLimit1,
kueDelay1,
kueLimit2Low,
kueLimit2High,
kueLoopInterval,
} = useSelector((state: RootState) => state.kueDataSlice);
const [bezeichnungen, setBezeichnungen] = useState(Array(32).fill(""));
const [originalValues, setOriginalValues] = useState<OriginalValues>({
kueID: Array(32).fill(""),
kueBezeichnungen: Array(32).fill(""),
isolationsgrenzwerte: Array(32).fill(10.0),
verzoegerung: Array(32).fill(1.0),
untereSchleifenGrenzwerte: Array(32).fill(0.1),
obereSchleifenGrenzwerte: Array(32).fill(1.0),
schleifenintervall: Array(32).fill(24),
});
useEffect(() => {
if (showModal) {
setBezeichnungen(
kueID
? kueID.map((name: string) => name.trim() || "---")
: bezeichnungen
);
setOriginalValues({
kueID: [...kueID],
kueBezeichnungen: [...bezeichnungen],
isolationsgrenzwerte: [...kueLimit1],
verzoegerung: [...kueDelay1],
untereSchleifenGrenzwerte: [...kueLimit2Low],
obereSchleifenGrenzwerte: [...kueLimit2High],
schleifenintervall: [...kueLoopInterval],
});
}
}, [
showModal,
kueID,
kueLimit1,
kueDelay1,
kueLimit2Low,
kueLimit2High,
kueLoopInterval,
]);
useEffect(() => {
const token = sessionStorage.getItem("token");
if (token) {
const decoded = decodeToken(token);
setIsAdmin(decoded?.role?.toLowerCase() === "admin");
}
}, [showModal]);
const handleSaveWrapper = () => {
handleSave({
ids: [...kueID],
bezeichnungen,
isolationsgrenzwerte: [...kueLimit1],
verzoegerung: [...kueDelay1],
untereSchleifenGrenzwerte: [...kueLimit2Low],
obereSchleifenGrenzwerte: [...kueLimit2High],
schleifenintervall: [...kueLoopInterval],
originalValues,
slot,
dispatch,
onModulNameChange,
onClose,
});
};
// Korrekte updateArray Methode ohne Thunk
const updateArray = (
key: keyof RootState["kueDataSlice"],
array: number[],
value: number
) => {
const updatedArray = [...array];
updatedArray[slot] = value;
dispatch(setKueData({ [key]: updatedArray }));
};
const handleDisplayEinschaltenWrapper = () => {
handleDisplayEinschalten(slot);
};
return (
<ReactModal
isOpen={showModal}
onRequestClose={onClose}
ariaHideApp={false}
style={{
overlay: {
backgroundColor: "rgba(0, 0, 0, 0.5)",
zIndex: 100,
},
content: {
top: "50%",
left: "50%",
right: "auto",
bottom: "auto",
transform: "translate(-50%, -50%)",
width: "90%",
maxWidth: "800px",
padding: "10px",
borderRadius: "8px",
border: "none",
position: "relative", // wichtig
},
}}
>
<div className="relative bg-littwin-blue text-white p-1 rounded-t-lg">
<h2 className="text-sm font-bold">KUE Einstellung - Slot {slot + 1}</h2>
<button
onClick={onClose}
style={{
position: "absolute",
top: "0rem",
right: "0.5rem",
background: "transparent",
border: "none",
fontSize: "1.5rem",
color: "#000",
cursor: "pointer",
zIndex: 10,
}}
>
<i className="bi bi-x-circle-fill"></i>
</button>
</div>
<div className="p-2 mb-4 text-black">
<label className="font-bold">Kabelbezeichnung:</label>
<input
type="text"
className="border rounded p-1 w-full text-sm"
value={kueID[slot] || ""}
onChange={(e) => {
const newIds = [...kueID];
newIds[slot] = e.target.value;
dispatch(setKueData({ kueID: newIds }));
}}
/>
</div>
<div className="p-2 text-black">
<h3 className="font-bold text-center mb-4">Isolationsmessung</h3>
<table className="w-full text-left border-collapse mb-4">
<thead className="bg-gray-100">
<tr>
<th className="p-2 border text-sm text-center">
Grenzwert (MOhm)
</th>
<th className="p-2 border text-sm text-center">
Verzögerung (sek)
</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-2 border text-center">
<input
type="number"
step="0.1"
className="w-[6rem] border rounded p-1 text-sm"
value={kueLimit1[slot] ?? ""}
onChange={(e) =>
updateArray(
"kueLimit1",
kueLimit1,
parseFloat(e.target.value)
)
}
/>
</td>
<td className="p-2 border text-center">
<input
type="number"
step="0.1"
className="w-[6rem] border rounded p-1 text-sm"
value={kueDelay1[slot] ?? ""}
onChange={(e) =>
updateArray(
"kueDelay1",
kueDelay1,
parseFloat(e.target.value)
)
}
/>
</td>
</tr>
</tbody>
</table>
<h3 className="font-bold text-center mb-4">Schleifenmessung</h3>
<table className="w-full text-left border-collapse">
<thead className="bg-gray-100">
<tr>
<th className="p-2 border text-sm text-center">
Grenzwert (kOhm)
</th>
<th className="p-2 border text-sm text-center">
Schleifenintervall (h)
</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-2 border text-center">
<input
type="number"
step="0.1"
className="w-[6rem] border rounded p-1 text-sm"
value={kueLimit2Low[slot] ?? ""}
onChange={(e) =>
updateArray(
"kueLimit2Low",
kueLimit2Low,
parseFloat(e.target.value)
)
}
/>
</td>
<td className="p-2 border text-center">
<input
type="number"
step="0.1"
className="w-[6rem] border rounded p-1 text-sm"
value={kueLoopInterval[slot] ?? ""}
onChange={(e) =>
updateArray(
"kueLoopInterval",
kueLoopInterval,
parseFloat(e.target.value)
)
}
/>
</td>
</tr>
</tbody>
</table>
</div>
<div className="flex justify-end bg-gray-100 p-4 rounded-b-lg">
{isAdminLoggedIn && (
<button
onClick={() => firmwareUpdate(slot)}
className="bg-littwin-blue text-white p-2 rounded flex items-center mr-2"
>
Firmware Update
</button>
)}
<button
onClick={handleDisplayEinschaltenWrapper}
className="bg-littwin-blue text-white p-2 rounded flex items-center mr-2"
>
<span className="mr-2">📺</span> Display einschalten
</button>
<button
onClick={handleSaveWrapper}
className="bg-littwin-blue text-white p-2 rounded flex items-center"
>
<span className="mr-2">💾</span> Speichern
</button>
</div>
</ReactModal>
);
}
export default KueModal;

View File

@@ -3,7 +3,6 @@ import { setKueData } from "../../../../../redux/slices/kueDataSlice";
export interface OriginalValues {
kueID: string[];
kueBezeichnungen: string[];
isolationsgrenzwerte: number[];
verzoegerung: number[];
untereSchleifenGrenzwerte: number[];
@@ -13,7 +12,6 @@ export interface OriginalValues {
interface HandleSaveParams {
ids: string[];
bezeichnungen: string[];
isolationsgrenzwerte: number[];
verzoegerung: number[];
untereSchleifenGrenzwerte: number[];
@@ -26,9 +24,17 @@ interface HandleSaveParams {
onClose: () => void;
}
const isDifferent = (a: any, b: any): boolean => {
const aNum = Number(a);
const bNum = Number(b);
if (!isNaN(aNum) && !isNaN(bNum)) {
return Math.abs(aNum - bNum) > 0.0001;
}
return String(a).trim() !== String(b).trim();
};
const handleSave = async ({
ids,
bezeichnungen,
isolationsgrenzwerte,
verzoegerung,
untereSchleifenGrenzwerte,
@@ -40,88 +46,73 @@ const handleSave = async ({
onModulNameChange,
onClose,
}: HandleSaveParams): Promise<void> => {
const changes: Partial<{
KID: string;
KIA: string;
KL_: number;
KD_: number;
KR_: number;
KRO_: number;
KRI: number;
}> = {};
const changesForFile: Record<string, any> = {};
if (ids[slot] !== originalValues.kueID[slot]) {
changes.KID = ids[slot];
}
if (bezeichnungen[slot] !== originalValues.kueBezeichnungen[slot]) {
changes.KIA = bezeichnungen[slot];
if (isDifferent(ids[slot], originalValues.kueID[slot])) {
changesForFile.win_kueID = ids[slot];
}
if (
isolationsgrenzwerte[slot] !== originalValues.isolationsgrenzwerte[slot]
isDifferent(
isolationsgrenzwerte[slot],
originalValues.isolationsgrenzwerte[slot]
)
) {
changes.KL_ = isolationsgrenzwerte[slot];
changesForFile.win_kueLimit1 = isolationsgrenzwerte[slot];
}
if (verzoegerung[slot] !== originalValues.verzoegerung[slot]) {
changes.KD_ = verzoegerung[slot];
if (isDifferent(verzoegerung[slot], originalValues.verzoegerung[slot])) {
changesForFile.win_kueDelay1 = verzoegerung[slot];
}
if (
untereSchleifenGrenzwerte[slot] !==
originalValues.untereSchleifenGrenzwerte[slot]
isDifferent(
untereSchleifenGrenzwerte[slot],
originalValues.untereSchleifenGrenzwerte[slot]
)
) {
changes.KR_ = untereSchleifenGrenzwerte[slot];
changesForFile.win_kueLimit2Low = untereSchleifenGrenzwerte[slot];
}
if (
obereSchleifenGrenzwerte[slot] !==
originalValues.obereSchleifenGrenzwerte[slot]
isDifferent(
obereSchleifenGrenzwerte[slot],
originalValues.obereSchleifenGrenzwerte[slot]
)
) {
changes.KRO_ = obereSchleifenGrenzwerte[slot];
changesForFile.win_kueLimit2High = obereSchleifenGrenzwerte[slot];
}
if (schleifenintervall[slot] !== originalValues.schleifenintervall[slot]) {
changes.KRI = schleifenintervall[slot];
if (
isDifferent(
schleifenintervall[slot],
originalValues.schleifenintervall[slot]
)
) {
changesForFile.win_kueLoopInterval = schleifenintervall[slot];
}
if (Object.keys(changes).length > 0) {
if (Object.keys(changesForFile).length > 0) {
const isDev = window.location.hostname === "localhost";
if (isDev) {
await fetch("/api/cpl/updateKueDataAPIHandler", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
slot,
changes: {
win_kueID: ids[slot],
win_kueLimit1: isolationsgrenzwerte[slot],
win_kueDelay1: verzoegerung[slot],
win_kueLimit2Low: untereSchleifenGrenzwerte[slot],
win_kueLimit2High: obereSchleifenGrenzwerte[slot],
win_kueLoopInterval: schleifenintervall[slot],
},
}),
});
} else {
let url = `/cpl?/kabelueberwachung.html&slot=${slot}`;
Object.entries(changes).forEach(([paramKey, paramValue]) => {
if (paramValue !== undefined) {
url += `&${paramKey}${slot}=${encodeURIComponent(paramValue)}`;
for (const [key, value] of Object.entries(changesForFile)) {
const params = new URLSearchParams({
key,
value: String(value),
slot: String(slot),
});
const response = await fetch(
`/api/cpl/updateKueDataAPIHandler?${params.toString()}`
);
if (!response.ok) {
alert(`❌ Fehler beim Schreiben der Datei (${key})`);
return;
}
});
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
alert("Fehler beim Speichern der Daten!");
return;
}
}
alert("Daten erfolgreich gespeichert!");
alert("Daten erfolgreich gespeichert!");
onModulNameChange(ids[slot]);
dispatch(
setKueData({
kueID: [...ids],
kueBezeichnungen: [...bezeichnungen],
isolationsgrenzwerte: [...isolationsgrenzwerte],
verzoegerung: [...verzoegerung],
untereSchleifenGrenzwerte: [...untereSchleifenGrenzwerte],
@@ -130,7 +121,7 @@ const handleSave = async ({
})
);
} else {
alert("Keine Änderungen vorgenommen.");
alert(" Keine Änderungen vorgenommen.");
}
onClose();

View File

@@ -0,0 +1,9 @@
import React from "react";
interface Props {
slot: number;
}
export default function Knotenpunkte({ slot }: Props) {
return <div>Knotenpunkte Slot {slot + 1}</div>;
}

View File

@@ -0,0 +1,201 @@
"use client";
import { useDispatch, useSelector } from "react-redux";
import type { RootState } from "../../../../../redux/store";
import { setKueData } from "../../../../../redux/slices/kueDataSlice";
import handleSave, { OriginalValues } from "../handlers/handleSave";
import handleDisplayEinschalten from "../handlers/handleDisplayEinschalten";
import firmwareUpdate from "../handlers/firmwareUpdate";
interface Props {
slot: number;
onClose?: () => void;
onModulNameChange?: (id: string) => void;
}
export default function KueEinstellung({
slot,
onClose = () => {},
onModulNameChange = () => {},
}: Props) {
const dispatch = useDispatch();
const {
kueID,
kueLimit1,
kueDelay1,
kueLimit2Low,
kueLoopInterval,
kueLimit2High,
} = useSelector((state: RootState) => state.kueDataSlice);
const isAdminLoggedIn = useSelector(
(state: any) => state.authSlice.isAdminLoggedIn
);
const handleSaveWrapper = () => {
const originalValues: OriginalValues = {
kueID: [...(window.win_kueID ?? [])],
isolationsgrenzwerte: [...(window.win_kueLimit1 ?? [])],
verzoegerung: [...(window.win_kueDelay1 ?? [])],
untereSchleifenGrenzwerte: [...(window.win_kueLimit2Low ?? [])],
obereSchleifenGrenzwerte: [...(window.win_kueLimit2High ?? [])],
schleifenintervall: [...(window.win_kueLoopInterval ?? [])],
};
handleSave({
ids: [...kueID],
isolationsgrenzwerte: [...kueLimit1],
verzoegerung: [...kueDelay1],
untereSchleifenGrenzwerte: [...kueLimit2Low],
obereSchleifenGrenzwerte: [...kueLimit2High],
schleifenintervall: [...kueLoopInterval],
originalValues,
slot,
dispatch,
onModulNameChange,
onClose,
});
};
const updateArray = (
key: keyof RootState["kueDataSlice"],
array: number[],
value: number
) => {
const updated = [...array];
updated[slot] = value;
dispatch(setKueData({ [key]: updated }));
};
return (
<div className="text-black space-y-6">
<div>
<label className="font-bold">Kabelbezeichnung:</label>
<input
type="text"
className="w-full border rounded p-1 text-sm"
value={kueID[slot] || ""}
onChange={(e) => {
const newIds = [...kueID];
newIds[slot] = e.target.value;
dispatch(setKueData({ kueID: newIds }));
}}
/>
</div>
<div>
<h3 className="font-bold text-center mb-2">Isolationsmessung</h3>
<table className="w-full border-collapse text-sm">
<thead className="bg-gray-100">
<tr>
<th className="border p-2 text-center">Grenzwert (MOhm)</th>
<th className="border p-2 text-center">Verzögerung (sek)</th>
</tr>
</thead>
<tbody>
<tr>
<td className="border p-2 text-center">
<input
type="number"
step="0.1"
className="w-[6rem] border rounded p-1"
value={kueLimit1[slot] ?? ""}
onChange={(e) =>
updateArray(
"kueLimit1",
kueLimit1,
parseFloat(e.target.value)
)
}
/>
</td>
<td className="border p-2 text-center">
<input
type="number"
step="0.1"
className="w-[6rem] border rounded p-1"
value={kueDelay1[slot] ?? ""}
onChange={(e) =>
updateArray(
"kueDelay1",
kueDelay1,
parseFloat(e.target.value)
)
}
/>
</td>
</tr>
</tbody>
</table>
</div>
<div>
<h3 className="font-bold text-center mb-2">Schleifenmessung</h3>
<table className="w-full border-collapse text-sm">
<thead className="bg-gray-100">
<tr>
<th className="border p-2 text-center">Grenzwert (kOhm)</th>
<th className="border p-2 text-center">Schleifenintervall (h)</th>
</tr>
</thead>
<tbody>
<tr>
<td className="border p-2 text-center">
<input
type="number"
step="0.1"
className="w-[6rem] border rounded p-1"
value={kueLimit2Low[slot] ?? ""}
onChange={(e) =>
updateArray(
"kueLimit2Low",
kueLimit2Low,
parseFloat(e.target.value)
)
}
/>
</td>
<td className="border p-2 text-center">
<input
type="number"
step="0.1"
className="w-[6rem] border rounded p-1"
value={kueLoopInterval[slot] ?? ""}
onChange={(e) =>
updateArray(
"kueLoopInterval",
kueLoopInterval,
parseFloat(e.target.value)
)
}
/>
</td>
</tr>
</tbody>
</table>
</div>
<div className="flex justify-end gap-2 bg-gray-100 p-3 rounded">
{isAdminLoggedIn && (
<button
onClick={() => firmwareUpdate(slot)}
className="bg-littwin-blue text-white px-4 py-2 rounded flex items-center"
>
<span className="mr-2">🔧</span> Firmware Update
</button>
)}
<button
onClick={() => handleDisplayEinschalten(slot)}
className="bg-littwin-blue text-white px-4 py-2 rounded flex items-center"
>
<span className="mr-2">📺</span> Display einschalten
</button>
<button
onClick={handleSaveWrapper}
className="bg-littwin-blue text-white px-4 py-2 rounded flex items-center"
>
<span className="mr-2">💾</span> Speichern
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,108 @@
"use client";
import { useState, useEffect } from "react";
import ReactModal from "react-modal";
import KueEinstellung from "./KueEinstellung";
import TdrEinstellung from "./TdrEinstellung";
import Knotenpunkte from "./Knotenpunkte";
interface KueModalProps {
showModal: boolean;
onClose: () => void;
slot: number;
onModulNameChange: (id: string) => void; // NEU!
}
export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
const [activeTab, setActiveTab] = useState<"kue" | "tdr" | "knoten">("kue");
useEffect(() => {
if (showModal) {
setActiveTab("kue");
}
}, [showModal]);
return (
<ReactModal
isOpen={showModal}
onRequestClose={onClose}
ariaHideApp={false}
style={{
overlay: {
backgroundColor: "rgba(0, 0, 0, 0.5)",
zIndex: 100,
},
content: {
top: "50%",
left: "50%",
right: "auto",
bottom: "auto",
transform: "translate(-50%, -50%)",
width: "90%",
maxWidth: "850px",
padding: "0px",
border: "none",
borderRadius: "8px",
position: "relative",
},
}}
>
<div className="bg-littwin-blue text-white p-2 flex justify-between items-center rounded-t-md">
<h2 className="text-sm font-bold">Einstellungen Slot {slot + 1}</h2>
<button
onClick={onClose}
className="text-white text-xl hover:text-gray-200"
>
<i className="bi bi-x-circle-fill"></i>
</button>
</div>
<div className="flex justify-center bg-gray-100 space-x-2 p-2">
<button
onClick={() => setActiveTab("kue")}
className={`px-4 py-1 rounded-t font-bold text-sm ${
activeTab === "kue"
? "bg-white text-blue-600"
: "text-gray-500 hover:text-black"
}`}
>
KUE Einstellung
</button>
<button
onClick={() => setActiveTab("tdr")}
className={`px-4 py-1 rounded-t font-bold text-sm ${
activeTab === "tdr"
? "bg-white text-blue-600"
: "text-gray-500 hover:text-black"
}`}
>
TDR Einstellung
</button>
<button
onClick={() => setActiveTab("knoten")}
className={`px-4 py-1 rounded-t font-bold text-sm ${
activeTab === "knoten"
? "bg-white text-blue-600"
: "text-gray-500 hover:text-black"
}`}
>
Knotenpunkte
</button>
</div>
<div className="p-4 bg-white rounded-b-md max-h-[75vh] overflow-y-auto">
{activeTab === "kue" && (
<KueEinstellung
slot={slot}
onModulNameChange={(id) => {
console.log("Modulname geändert:", id);
}}
onClose={onClose}
/>
)}
{activeTab === "tdr" && <TdrEinstellung slot={slot} />}
{activeTab === "knoten" && <Knotenpunkte slot={slot} />}
</div>
</ReactModal>
);
}

View File

@@ -0,0 +1,9 @@
import React from "react";
interface Props {
slot: number;
}
export default function TdrEinstellung({ slot }: Props) {
return <div>TdrEinstellung Slot {slot + 1}</div>;
}