Files
CPLv4.0/components/main/digitalInputs/digitalInputsModal.tsx
2025-09-10 11:20:22 +02:00

316 lines
11 KiB
TypeScript

"use client";
// /components/main/digitalInputs/InputModal.tsx
import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "@/redux/store";
import { updateInvert, updateLabel } from "@/redux/slices/digitalInputsSlice";
type InputModalProps = {
selectedInput: {
id: number;
[key: string]: unknown;
} | null;
closeInputModal: () => void;
isOpen: boolean;
};
export default function InputModal({
selectedInput,
closeInputModal,
isOpen,
}: InputModalProps) {
const dispatch = useDispatch();
const reduxInput = useSelector((state: RootState) =>
state.digitalInputsSlice.inputs.find(
(input) => input.id === Number(selectedInput?.id)
)
);
/* console.log("📦 selectedInput.id:", selectedInput?.id);
console.log("📦 reduxInput:", reduxInput); */
const [isInitialLoad, setIsInitialLoad] = useState(true);
const [label, setLabel] = useState("");
const [invertiert, setInvertiert] = useState(false);
const [timeFilter, setTimeFilter] = useState(0);
const [weighting, setWeighting] = useState(0);
const [zaehlerAktiv, setZaehlerAktiv] = useState(false);
const [eingangOffline, setEingangOffline] = useState(0);
useEffect(() => {
if (reduxInput && isInitialLoad) {
//reduxInput
//console.log("📦 reduxInput geladen:", reduxInput);
setLabel(reduxInput.label || "");
setInvertiert(reduxInput.invert);
setTimeFilter(reduxInput.timeFilter);
setWeighting(reduxInput.weighting);
setZaehlerAktiv(reduxInput.zaehlerAktiv);
setEingangOffline(reduxInput.eingangOffline ? 1 : 0);
setIsInitialLoad(false);
}
}, [reduxInput, isInitialLoad]);
useEffect(() => {
if (isOpen && selectedInput) {
setIsInitialLoad(true);
}
}, [isOpen, selectedInput]);
useEffect(() => {
if (isOpen && selectedInput) {
setIsInitialLoad(true);
}
}, [isOpen, selectedInput]);
if (!isOpen || !selectedInput || !reduxInput) return null;
const handleClose = () => {
closeInputModal();
};
const sendCgiUpdate = async (param: string) => {
const url = `/CPL?/eingaenge.html&${param}`;
//console.log("📡 CGI senden:", url);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Fehler bei CGI-Aufruf: ${param}`);
}
};
const handleSpeichern = async () => {
const id = reduxInput.id;
let hasChange = false;
try {
// PRODUKTIONSUMGEBUNG (CGI-Modus)
if (process.env.NEXT_PUBLIC_NODE_ENV === "production") {
if (label !== reduxInput.label) {
await sendCgiUpdate(`DEN${id}=${encodeURIComponent(label)}`);
dispatch(updateLabel({ id, label }));
hasChange = true;
}
if (invertiert !== reduxInput.invert) {
await sendCgiUpdate(`DEI${id}=${invertiert ? 1 : 0}`);
dispatch(updateInvert({ id, invert: invertiert }));
hasChange = true;
}
if (timeFilter !== reduxInput.timeFilter) {
await sendCgiUpdate(`DEF${id}=${timeFilter}`);
hasChange = true;
}
if (weighting !== reduxInput.weighting) {
await sendCgiUpdate(`DEG${id}=${weighting}`);
hasChange = true;
}
if (zaehlerAktiv !== reduxInput.zaehlerAktiv) {
await sendCgiUpdate(`DEZ${id}=${zaehlerAktiv ? 1 : 0}`);
hasChange = true;
}
if (eingangOffline !== (reduxInput.eingangOffline ? 1 : 0)) {
await sendCgiUpdate(`DEO${id}=${eingangOffline ? 1 : 0}`);
hasChange = true;
}
if (!hasChange) {
alert("⚠️ Keine Änderungen erkannt.");
return;
}
alert("✅ Daten erfolgreich an die CPL-Hardware gesendet!");
} else {
// ENTWICKLUNGSUMGEBUNG (lokale API)
type Updates = {
id: number;
label?: string;
invert?: number;
timeFilter?: number;
weighting?: number;
zaehlerAktiv?: number;
};
const updates: Updates = { id };
if (label !== reduxInput.label) {
updates.label = label;
dispatch(updateLabel({ id, label }));
hasChange = true;
}
if (invertiert !== reduxInput.invert) {
updates.invert = invertiert ? 1 : 0;
dispatch(updateInvert({ id, invert: invertiert }));
hasChange = true;
}
if (timeFilter !== reduxInput.timeFilter) {
updates.timeFilter = timeFilter;
hasChange = true;
}
if (weighting !== reduxInput.weighting) {
updates.weighting = weighting;
hasChange = true;
}
if (zaehlerAktiv !== reduxInput.zaehlerAktiv) {
updates.zaehlerAktiv = zaehlerAktiv ? 1 : 0;
hasChange = true;
}
if (!hasChange) {
alert("⚠️ Keine Änderungen erkannt.");
return;
}
const res = await fetch("/api/cpl/updateDigitalInputs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updates),
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.error || "Unbekannter Fehler");
}
alert("✅ Daten lokal gespeichert!");
}
setIsInitialLoad(true);
closeInputModal();
} catch (err: unknown) {
if (err instanceof Error) {
alert("❌ Fehler beim Speichern: " + err.message);
} else {
alert("❌ Fehler beim Speichern: Unbekannter Fehler");
}
}
};
return (
<div className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-50 flex justify-center items-center z-50">
<div className="bg-[var(--color-surface)] border border-base rounded-xl shadow-xl w-[32rem] max-w-full p-0 flex flex-col">
<header className="modal-header flex items-center justify-between px-6 py-4 border-b border-base">
<h2 className="text-base font-bold text-fg">
Einstellungen Meldungseingang {selectedInput.id}
</h2>
<button
onClick={handleClose}
className="icon-btn text-2xl"
aria-label="Modal schließen"
type="button"
>
<i className="bi bi-x-circle-fill" />
</button>
</header>
<div className="modal-body-scroll px-6 py-5 flex-1 text-fg">
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
<div>
<span className="font-normal text-fg-secondary">
Bezeichnung:
</span>
</div>
<div>
<input
type="text"
value={label}
onChange={(e) => setLabel(e.target.value)}
className="border border-base rounded px-2 py-1 w-full bg-[var(--color-surface-alt)] text-fg"
maxLength={32}
/>
</div>
<div>
<span className="font-normal text-fg-secondary">
Invertierung:
</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
role="switch"
aria-checked={invertiert}
onClick={() => setInvertiert(!invertiert)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-200 ${
invertiert ? "bg-littwin-blue" : "bg-base-muted"
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform duration-200 ${
invertiert ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
<span className="text-fg">{invertiert ? "Ein" : "Aus"}</span>
</div>
<div>
<span className="font-normal text-fg-secondary">Filterzeit:</span>
</div>
<div className="relative">
<input
type="number"
min={0}
max={2000}
value={timeFilter}
onChange={(e) => {
const val = Number(e.target.value);
if (val <= 2000) {
setTimeFilter(val);
}
}}
className="border border-base rounded px-2 py-1 pr-10 w-full text-right bg-[var(--color-surface-alt)] text-fg"
title="Maximal 2000 ms erlaubt"
/>
<span className="absolute right-2 top-1/2 transform -translate-y-1/2 text-muted text-sm">
ms
</span>
</div>
<div>
<span className="font-normal text-fg-secondary">Gewichtung:</span>
</div>
<div>
<input
type="number"
min={0}
max={1000}
value={weighting}
onChange={(e) => {
const val = Number(e.target.value);
if (val <= 1000) {
setWeighting(val);
}
}}
className="border border-base rounded px-2 py-1 w-full text-right bg-[var(--color-surface-alt)] text-fg"
title="Maximal 1000 erlaubt"
/>
</div>
<div className="relative group inline-block">
<span className="font-normal text-fg-secondary">
Out of Service:
</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
role="switch"
aria-checked={!!eingangOffline}
onClick={() => setEingangOffline(eingangOffline ? 0 : 1)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-200 ${
eingangOffline ? "bg-littwin-blue" : "bg-base-muted"
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform duration-200 ${
eingangOffline ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
<span className="text-fg">{eingangOffline ? "Ein" : "Aus"}</span>
</div>
</div>
</div>
<footer className="px-6 py-4 border-t border-base flex justify-end">
<button
onClick={handleSpeichern}
className="btn-primary px-4 py-2 rounded flex items-center"
>
Speichern
</button>
</footer>
</div>
</div>
);
}