WIP: von bis Zeitraum in ISO und TDR, aber TDR ist WIP

This commit is contained in:
ISA
2025-09-09 14:48:03 +02:00
parent 4c6fe0db03
commit 18c9c886ec
9 changed files with 262 additions and 239 deletions

View File

@@ -6,6 +6,6 @@ NEXT_PUBLIC_USE_MOCK_BACKEND_LOOP_START=false
NEXT_PUBLIC_EXPORT_STATIC=false NEXT_PUBLIC_EXPORT_STATIC=false
NEXT_PUBLIC_USE_CGI=false NEXT_PUBLIC_USE_CGI=false
# App-Versionsnummer # App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.6.884 NEXT_PUBLIC_APP_VERSION=1.6.885
NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter) NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter)

View File

@@ -5,5 +5,5 @@ NEXT_PUBLIC_CPL_API_PATH=/CPL
NEXT_PUBLIC_EXPORT_STATIC=true NEXT_PUBLIC_EXPORT_STATIC=true
NEXT_PUBLIC_USE_CGI=true NEXT_PUBLIC_USE_CGI=true
# App-Versionsnummer # App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.6.884 NEXT_PUBLIC_APP_VERSION=1.6.885
NEXT_PUBLIC_CPL_MODE=production NEXT_PUBLIC_CPL_MODE=production

View File

@@ -1,3 +1,8 @@
## [1.6.885] 2025-09-09
- test: rename test files *.test.ts
---
## [1.6.884] 2025-09-09 ## [1.6.884] 2025-09-09
- Tests: TDR ISO und RSL - Tests: TDR ISO und RSL

View File

@@ -271,7 +271,7 @@ const IsoChartActionBar = forwardRef((_props, ref) => {
useImperativeHandle(ref, () => ({ handleFetchData })); useImperativeHandle(ref, () => ({ handleFetchData }));
const hiddenWhenMeldungen = chartTitle !== "Messkurve"; const isMeldungen = chartTitle === "Meldungen";
return ( return (
<div className="toolbar w-full justify-between flex-wrap"> <div className="toolbar w-full justify-between flex-wrap">
@@ -283,23 +283,11 @@ const IsoChartActionBar = forwardRef((_props, ref) => {
{slotNumber !== null ? slotNumber + 1 : "-"} {slotNumber !== null ? slotNumber + 1 : "-"}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 flex-1 justify-end"> <div className="flex items-center gap-3 flex-1 justify-end">
<div {/* Always show date range; requirement: in Meldungen only Von/Bis + Anzeigen */}
className={
hiddenWhenMeldungen
? "opacity-0 pointer-events-none"
: "opacity-100"
}
>
<DateRangePicker /> <DateRangePicker />
</div> {!isMeldungen && (
<div <>
className={`transition-opacity ${
hiddenWhenMeldungen
? "opacity-0 pointer-events-none"
: "opacity-100"
}`}
>
<Listbox <Listbox
value={selectedMode} value={selectedMode}
onChange={(value) => { onChange={(value) => {
@@ -320,7 +308,7 @@ const IsoChartActionBar = forwardRef((_props, ref) => {
</span> </span>
<i className="bi bi-chevron-down opacity-70" /> <i className="bi bi-chevron-down opacity-70" />
</Listbox.Button> </Listbox.Button>
<Listbox.Options className="dropdown-options absolute z-50 mt-1 w-full max-h-60 overflow-auto "> <Listbox.Options className="dropdown-options absolute z-50 mt-1 w-full max-h-60 overflow-auto">
{["DIA0", "DIA1", "DIA2"].map((mode) => ( {["DIA0", "DIA1", "DIA2"].map((mode) => (
<Listbox.Option <Listbox.Option
key={mode} key={mode}
@@ -347,16 +335,24 @@ const IsoChartActionBar = forwardRef((_props, ref) => {
</Listbox.Options> </Listbox.Options>
</div> </div>
</Listbox> </Listbox>
</div>
<button <button
onClick={handleFetchData} onClick={handleFetchData}
className={`btn-primary h-8 font-medium px-3 ${ className="btn-primary h-8 font-medium px-3"
hiddenWhenMeldungen ? "invisible" : "visible"
}`}
type="button" type="button"
> >
Daten laden Daten laden
</button> </button>
</>
)}
{isMeldungen && (
<button
onClick={handleFetchData}
className="btn-primary h-8 font-medium px-4"
type="button"
>
Anzeigen
</button>
)}
</div> </div>
</div> </div>
); );

View File

@@ -283,6 +283,7 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
// Sichtbarkeits-Flags // Sichtbarkeits-Flags
const isMesskurve = chartTitle === "Messkurve"; const isMesskurve = chartTitle === "Messkurve";
const isMeldungen = chartTitle === "Meldungen";
return ( return (
<div className="toolbar w-full flex flex-wrap items-center gap-2"> <div className="toolbar w-full flex flex-wrap items-center gap-2">
@@ -347,8 +348,9 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
</Listbox> </Listbox>
</div> </div>
{/* Buttons nur für Messkurve */} {/* Buttons */}
<div className={isMesskurve ? "flex items-center gap-2" : "hidden"}> {isMesskurve && (
<div className="flex items-center gap-2">
<button <button
onClick={handleStartRSL} onClick={handleStartRSL}
className="btn-primary h-8 font-medium px-3" className="btn-primary h-8 font-medium px-3"
@@ -366,6 +368,17 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
Daten laden Daten laden
</button> </button>
</div> </div>
)}
{isMeldungen && (
<button
onClick={handleFetchData}
className="btn-primary h-8 font-medium px-4"
disabled={rslRunning}
type="button"
>
Anzeigen
</button>
)}
</div> </div>
{rslRunning && ( {rslRunning && (

View File

@@ -1,49 +1,57 @@
"use client";
// /components/main/kabelueberwachung/kue705FO/Charts/TDRChart/TDRChartActionBar.tsx // /components/main/kabelueberwachung/kue705FO/Charts/TDRChart/TDRChartActionBar.tsx
import React, { useEffect, useState } from "react";
import React, { useState, useEffect } from "react"; import DateRangePicker from "@/components/common/DateRangePicker";
import { useSelector } from "react-redux";
import { useAppDispatch } from "@/redux/store"; import { useAppDispatch } from "@/redux/store";
import { useSelector } from "react-redux";
import { RootState } from "@/redux/store"; import { RootState } from "@/redux/store";
import { Listbox } from "@headlessui/react";
import { getMessagesThunk } from "@/redux/thunks/getMessagesThunk";
import { setLoading } from "@/redux/slices/kabelueberwachungChartSlice";
import { fetchTDMDataBySlotThunk } from "@/redux/thunks/getTDMListBySlotThunk"; import { fetchTDMDataBySlotThunk } from "@/redux/thunks/getTDMListBySlotThunk";
import { getTDRChartDataByIdThunk } from "@/redux/thunks/getTDRChartDataByIdThunk"; import { getTDRChartDataByIdThunk } from "@/redux/thunks/getTDRChartDataByIdThunk";
import { getReferenceCurveBySlotThunk } from "@/redux/thunks/getReferenceCurveBySlotThunk"; // ⬅ import ergänzen import { getReferenceCurveBySlotThunk } from "@/redux/thunks/getReferenceCurveBySlotThunk";
import { Listbox } from "@headlessui/react";
const TDRChartActionBar: React.FC = () => { const TDRChartActionBar: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
// ✅ Redux: selectedSlot aus kueChartMode (0-basiert) const { vonDatum, bisDatum, chartTitle } = useSelector(
(s: RootState) => s.kabelueberwachungChartSlice
);
const { vonDatum: pickerVon, bisDatum: pickerBis } = useSelector(
(s: RootState) => s.dateRangePicker
);
const selectedSlot = useSelector( const selectedSlot = useSelector(
(state: RootState) => state.kueChartModeSlice.selectedSlot (s: RootState) => s.kueChartModeSlice.selectedSlot
); );
const tdmChartData = useSelector( const tdmChartData = useSelector(
(state: RootState) => state.tdmSingleChartSlice.data (s: RootState) => s.tdmSingleChartSlice.data
); );
const idsForSlot = const idsForSlot =
selectedSlot !== null ? tdmChartData[selectedSlot] ?? [] : []; selectedSlot !== null ? tdmChartData[selectedSlot] ?? [] : [];
const tdrDataById = useSelector( const tdrDataById = useSelector(
(state: RootState) => state.tdrDataByIdSlice.dataById (s: RootState) => s.tdrDataByIdSlice.dataById
); );
const [selectedId, setSelectedId] = useState<number | null>(null); const [selectedId, setSelectedId] = useState<number | null>(null);
const currentChartData = selectedId !== null ? tdrDataById[selectedId] : []; const currentChartData = selectedId !== null ? tdrDataById[selectedId] : [];
// ▶ Fortschrittsanzeige für laufende TDR-Messung (max. 120s bzw. konfigurierbar) const isMeldungen = chartTitle === "Meldungen";
// Progress for running TDR measurement
const TDR_TOTAL_DURATION = parseInt( const TDR_TOTAL_DURATION = parseInt(
process.env.NEXT_PUBLIC_TDR_DURATION_SECONDS || "120", process.env.NEXT_PUBLIC_TDR_DURATION_SECONDS || "120",
10 10
); );
const [tdrRunning, setTdrRunning] = useState(false); const [tdrRunning, setTdrRunning] = useState(false);
const [tdrProgress, setTdrProgress] = useState(0); // Sekunden const [tdrProgress, setTdrProgress] = useState(0);
useEffect(() => { useEffect(() => {
if (!tdrRunning) return; if (!tdrRunning) return;
setTdrProgress(0); setTdrProgress(0);
const startedAt = Date.now(); const started = Date.now();
const interval = setInterval(() => { const interval = setInterval(() => {
const elapsed = Math.floor((Date.now() - startedAt) / 1000); const elapsed = Math.floor((Date.now() - started) / 1000);
if (elapsed >= TDR_TOTAL_DURATION) { if (elapsed >= TDR_TOTAL_DURATION) {
setTdrProgress(TDR_TOTAL_DURATION); setTdrProgress(TDR_TOTAL_DURATION);
setTdrRunning(false); setTdrRunning(false);
@@ -60,102 +68,82 @@ const TDRChartActionBar: React.FC = () => {
setTdrProgress(0); setTdrProgress(0);
}; };
// 📌 Referenz setzen (nutzt Slotnummer + 1 für die API) const handleFetchMessages = async () => {
const fromDate = pickerVon ?? vonDatum;
const toDate = pickerBis ?? bisDatum;
try {
dispatch(setLoading(true));
await dispatch(getMessagesThunk({ fromDate, toDate })).unwrap();
} catch (e) {
console.error("❌ Fehler beim Laden der Meldungen", e);
alert("❌ Fehler beim Laden der Meldungen.");
} finally {
dispatch(setLoading(false));
}
};
const handleSetReference = async () => { const handleSetReference = async () => {
if ( if (
selectedSlot === null || selectedSlot === null ||
selectedId === null || selectedId === null ||
!currentChartData?.length !currentChartData.length
) )
return; return;
try {
const slotNumber = selectedSlot + 1; // Slot ist 0-basiert, API will 1-basiert
const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development"; const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development";
try {
if (isDev) {
await fetch("/api/cpl/updateTdrReferenceCurveAPIHandler", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
slot: slotNumber,
data: currentChartData,
}),
});
} else {
const url = `/CPL?/${window.location.pathname}&KTR${slotNumber}=${selectedId}`;
await fetch(url, { method: "GET" });
}
if (!isDev) { if (!isDev) {
const url = `/CPL?KTR${slotNumber}=${selectedId}`; const url = `/CPL?KTR${selectedSlot + 1}=${selectedId}`;
const response = await fetch(url, { method: "GET" }); const response = await fetch(url, { method: "GET" });
if (!response.ok)
if (!response.ok) {
throw new Error( throw new Error(
`Fehler beim Setzen der Referenz: ${response.statusText}` `Fehler beim Setzen der Referenz: ${response.statusText}`
); );
} }
}
// Optional: lokale Speicherung und Redux-Update
localStorage.setItem( localStorage.setItem(
`ref-curve-slot${selectedSlot}`, `ref-curve-slot${selectedSlot}`,
JSON.stringify(currentChartData) JSON.stringify(currentChartData)
); );
dispatch(getReferenceCurveBySlotThunk(selectedSlot)); dispatch(getReferenceCurveBySlotThunk(selectedSlot));
alert("Referenzkurve wurde erfolgreich gesetzt!"); alert("Referenzkurve wurde erfolgreich gesetzt!");
} catch (error) { } catch (err) {
console.error("Fehler beim Setzen der Referenzkurve:", error); console.error("Fehler beim Setzen der Referenzkurve", err);
alert("Fehler beim Setzen der Referenzkurve."); alert("Fehler beim Setzen der Referenzkurve.");
} }
}; };
// 📌 TDR Messung starten
const handleStartTDR = async () => { const handleStartTDR = async () => {
if (selectedSlot === null) { if (selectedSlot === null) {
alert("⚠️ Bitte zuerst einen KÜ auswählen!"); alert("⚠️ Bitte zuerst einen KÜ auswählen!");
return; return;
} }
const cgiUrl = `${window.location.origin}/CPL?/${window.location.pathname}&KTT${selectedSlot}=1`; const cgiUrl = `${window.location.origin}/CPL?/${window.location.pathname}&KTT${selectedSlot}=1`;
try { try {
console.log("🚀 Starte TDR Messung für Slot:", selectedSlot);
console.log("📡 CGI URL:", cgiUrl);
const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development"; const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development";
if (isDev) { if (isDev) {
// Dev / Simulator: sofort starten & Progress anzeigen
await new Promise((r) => setTimeout(r, 150)); await new Promise((r) => setTimeout(r, 150));
console.log("✅ [DEV] TDR Mock-Start ok für Slot", selectedSlot);
startTdrProgress(); startTdrProgress();
return; return;
} }
const response = await fetch(cgiUrl); const response = await fetch(cgiUrl);
if (!response.ok) throw new Error(`CGI-Fehler: ${response.status}`); if (!response.ok) throw new Error(`CGI-Fehler: ${response.status}`);
console.log("✅ TDR Messung gestartet für Slot", selectedSlot);
startTdrProgress(); startTdrProgress();
} catch (err) { } catch (err) {
console.error("❌ Fehler beim Starten der TDR Messung:", err); console.error("❌ Fehler beim Starten der TDR Messung", err);
//alert("❌ Fehler beim Starten der TDR Messung.");
} }
}; };
// 📥 Beim Slot-Wechsel TDM-Liste + letzte ID laden // Load TDM list when slot changes
useEffect(() => { useEffect(() => {
if (selectedSlot !== null) { if (selectedSlot !== null) {
dispatch(fetchTDMDataBySlotThunk(selectedSlot)).then((action) => { dispatch(fetchTDMDataBySlotThunk(selectedSlot)).then((action) => {
// action can be a PayloadAction with payload or a rejected action
const payload = ( const payload = (
action as { action as {
payload?: { data?: { id: number; t: string; d: number }[] }; payload?: { data?: { id: number; t: string; d: number }[] };
} }
).payload; ).payload;
const slotData = payload?.data; const slotData = payload?.data ?? [];
if ((slotData ?? []).length > 0) { if (slotData.length > 0) {
const lastId = (slotData ?? [])[0].id; const lastId = slotData[0].id; // latest first
setSelectedId(lastId); setSelectedId(lastId);
dispatch(getTDRChartDataByIdThunk(lastId)); dispatch(getTDRChartDataByIdThunk(lastId));
} }
@@ -166,7 +154,6 @@ const TDRChartActionBar: React.FC = () => {
return ( return (
<> <>
<div className="toolbar w-full flex items-center gap-3 flex-wrap"> <div className="toolbar w-full flex items-center gap-3 flex-wrap">
{/* Slot Badge */}
<div className="flex items-center gap-2 pr-4"> <div className="flex items-center gap-2 pr-4">
<span className="font-semibold uppercase tracking-wide text-muted"> <span className="font-semibold uppercase tracking-wide text-muted">
@@ -175,17 +162,29 @@ const TDRChartActionBar: React.FC = () => {
{selectedSlot !== null ? selectedSlot + 1 : "-"} {selectedSlot !== null ? selectedSlot + 1 : "-"}
</span> </span>
</div> </div>
{/* Referenz speichern */} {/* Date range always visible */}
<DateRangePicker />
{isMeldungen ? (
<button
type="button"
onClick={handleFetchMessages}
className="btn-primary h-8 font-medium px-4"
disabled={selectedSlot === null}
>
Anzeigen
</button>
) : (
<>
{selectedId !== null && ( {selectedId !== null && (
<button <button
onClick={handleSetReference} onClick={handleSetReference}
type="button" type="button"
className="btn-primary h-8 px-3 font-medium" className="btn-primary h-8 px-3 font-medium"
disabled={selectedSlot === null}
> >
TDR-Kurve als Referenz speichern TDR-Kurve als Referenz speichern
</button> </button>
)} )}
{/* Start TDR */}
<button <button
onClick={handleStartTDR} onClick={handleStartTDR}
type="button" type="button"
@@ -201,7 +200,6 @@ const TDRChartActionBar: React.FC = () => {
)}%)` )}%)`
: "TDR-Messung starten"} : "TDR-Messung starten"}
</button> </button>
{/* Messung Dropdown */}
<div className="ml-auto flex-1 min-w-[14rem] max-w-[30rem]"> <div className="ml-auto flex-1 min-w-[14rem] max-w-[30rem]">
<Listbox <Listbox
value={selectedId} value={selectedId}
@@ -220,14 +218,17 @@ const TDRChartActionBar: React.FC = () => {
(e) => e.id === selectedId (e) => e.id === selectedId
); );
return selected return selected
? `${new Date(selected.t).toLocaleString("de-DE", { ? `${new Date(selected.t).toLocaleString(
"de-DE",
{
day: "2-digit", day: "2-digit",
month: "2-digit", month: "2-digit",
year: "numeric", year: "numeric",
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
})} Fehlerstelle: ${selected.d} m` }
)} Fehlerstelle: ${selected.d} m`
: "Wähle Messung"; : "Wähle Messung";
})() })()
: "Wähle Messung"} : "Wähle Messung"}
@@ -236,14 +237,17 @@ const TDRChartActionBar: React.FC = () => {
</Listbox.Button> </Listbox.Button>
<Listbox.Options className="dropdown-options absolute z-50 mt-1 w-full max-h-72 overflow-auto text-sm bg-[var(--color-surface)] border border-base rounded-md shadow-lg p-1"> <Listbox.Options className="dropdown-options absolute z-50 mt-1 w-full max-h-72 overflow-auto text-sm bg-[var(--color-surface)] border border-base rounded-md shadow-lg p-1">
{idsForSlot.map((entry) => { {idsForSlot.map((entry) => {
const dateLabel = new Date(entry.t).toLocaleString("de-DE", { const dateLabel = new Date(entry.t).toLocaleString(
"de-DE",
{
day: "2-digit", day: "2-digit",
month: "2-digit", month: "2-digit",
year: "numeric", year: "numeric",
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
}); }
);
const fullText = `${dateLabel} Fehlerstelle: ${entry.d} m`; const fullText = `${dateLabel} Fehlerstelle: ${entry.d} m`;
return ( return (
<Listbox.Option <Listbox.Option
@@ -256,7 +260,7 @@ const TDRChartActionBar: React.FC = () => {
if (selected) if (selected)
return `${base} dropdown-option-active font-medium`; return `${base} dropdown-option-active font-medium`;
if (active) return `${base} dropdown-option-hover`; if (active) return `${base} dropdown-option-hover`;
return `${base}`; // neutral text color comes from parent/theme return `${base}`;
}} }}
> >
<span className="truncate w-full">{fullText}</span> <span className="truncate w-full">{fullText}</span>
@@ -267,6 +271,8 @@ const TDRChartActionBar: React.FC = () => {
</div> </div>
</Listbox> </Listbox>
</div> </div>
</>
)}
</div> </div>
{tdrRunning && ( {tdrRunning && (
<div className="fixed inset-0 z-[1000] flex flex-col items-center justify-center bg-[rgba(0,0,0,0.55)] backdrop-blur-sm"> <div className="fixed inset-0 z-[1000] flex flex-col items-center justify-center bg-[rgba(0,0,0,0.55)] backdrop-blur-sm">

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.884", "version": "1.6.885",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.884", "version": "1.6.885",
"dependencies": { "dependencies": {
"@emotion/react": "^11.13.0", "@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.884", "version": "1.6.885",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3000", "dev": "next dev -p 3000",

View File

@@ -78,9 +78,12 @@ test.describe("ISO Modal", () => {
dialog.getByRole("cell", { name: header }) dialog.getByRole("cell", { name: header })
); );
} }
// In Meldungen view only Von/Bis + Anzeigen (no mode dropdown)
await expect(dialog.getByText("Von")).not.toBeVisible(); await expect(dialog.getByText("Von")).toBeVisible();
await expect(dialog.getByText("Bis")).not.toBeVisible(); await expect(dialog.getByText("Bis")).toBeVisible();
await expect(
dialog.getByRole("button", { name: "Anzeigen" })
).toBeVisible();
await viewToggle.click(); await viewToggle.click();
await page.getByRole("option", { name: "Messkurve" }).click(); await page.getByRole("option", { name: "Messkurve" }).click();