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_USE_CGI=false
# 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)

View File

@@ -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.884
NEXT_PUBLIC_APP_VERSION=1.6.885
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
- Tests: TDR ISO und RSL

View File

@@ -271,7 +271,7 @@ const IsoChartActionBar = forwardRef((_props, ref) => {
useImperativeHandle(ref, () => ({ handleFetchData }));
const hiddenWhenMeldungen = chartTitle !== "Messkurve";
const isMeldungen = chartTitle === "Meldungen";
return (
<div className="toolbar w-full justify-between flex-wrap">
@@ -283,23 +283,11 @@ const IsoChartActionBar = forwardRef((_props, ref) => {
{slotNumber !== null ? slotNumber + 1 : "-"}
</span>
</div>
<div className="flex items-center gap-2 flex-1 justify-end">
<div
className={
hiddenWhenMeldungen
? "opacity-0 pointer-events-none"
: "opacity-100"
}
>
<div className="flex items-center gap-3 flex-1 justify-end">
{/* Always show date range; requirement: in Meldungen only Von/Bis + Anzeigen */}
<DateRangePicker />
</div>
<div
className={`transition-opacity ${
hiddenWhenMeldungen
? "opacity-0 pointer-events-none"
: "opacity-100"
}`}
>
{!isMeldungen && (
<>
<Listbox
value={selectedMode}
onChange={(value) => {
@@ -347,16 +335,24 @@ const IsoChartActionBar = forwardRef((_props, ref) => {
</Listbox.Options>
</div>
</Listbox>
</div>
<button
onClick={handleFetchData}
className={`btn-primary h-8 font-medium px-3 ${
hiddenWhenMeldungen ? "invisible" : "visible"
}`}
className="btn-primary h-8 font-medium px-3"
type="button"
>
Daten laden
</button>
</>
)}
{isMeldungen && (
<button
onClick={handleFetchData}
className="btn-primary h-8 font-medium px-4"
type="button"
>
Anzeigen
</button>
)}
</div>
</div>
);

View File

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

View File

@@ -1,49 +1,57 @@
"use client";
// /components/main/kabelueberwachung/kue705FO/Charts/TDRChart/TDRChartActionBar.tsx
import React, { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import React, { useEffect, useState } from "react";
import DateRangePicker from "@/components/common/DateRangePicker";
import { useAppDispatch } from "@/redux/store";
import { useSelector } from "react-redux";
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 { getTDRChartDataByIdThunk } from "@/redux/thunks/getTDRChartDataByIdThunk";
import { getReferenceCurveBySlotThunk } from "@/redux/thunks/getReferenceCurveBySlotThunk"; // ⬅ import ergänzen
import { Listbox } from "@headlessui/react";
import { getReferenceCurveBySlotThunk } from "@/redux/thunks/getReferenceCurveBySlotThunk";
const TDRChartActionBar: React.FC = () => {
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(
(state: RootState) => state.kueChartModeSlice.selectedSlot
(s: RootState) => s.kueChartModeSlice.selectedSlot
);
const tdmChartData = useSelector(
(state: RootState) => state.tdmSingleChartSlice.data
(s: RootState) => s.tdmSingleChartSlice.data
);
const idsForSlot =
selectedSlot !== null ? tdmChartData[selectedSlot] ?? [] : [];
const tdrDataById = useSelector(
(state: RootState) => state.tdrDataByIdSlice.dataById
(s: RootState) => s.tdrDataByIdSlice.dataById
);
const [selectedId, setSelectedId] = useState<number | null>(null);
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(
process.env.NEXT_PUBLIC_TDR_DURATION_SECONDS || "120",
10
);
const [tdrRunning, setTdrRunning] = useState(false);
const [tdrProgress, setTdrProgress] = useState(0); // Sekunden
const [tdrProgress, setTdrProgress] = useState(0);
useEffect(() => {
if (!tdrRunning) return;
setTdrProgress(0);
const startedAt = Date.now();
const started = Date.now();
const interval = setInterval(() => {
const elapsed = Math.floor((Date.now() - startedAt) / 1000);
const elapsed = Math.floor((Date.now() - started) / 1000);
if (elapsed >= TDR_TOTAL_DURATION) {
setTdrProgress(TDR_TOTAL_DURATION);
setTdrRunning(false);
@@ -60,102 +68,82 @@ const TDRChartActionBar: React.FC = () => {
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 () => {
if (
selectedSlot === null ||
selectedId === null ||
!currentChartData?.length
!currentChartData.length
)
return;
try {
const slotNumber = selectedSlot + 1; // Slot ist 0-basiert, API will 1-basiert
const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development";
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" });
}
try {
if (!isDev) {
const url = `/CPL?KTR${slotNumber}=${selectedId}`;
const url = `/CPL?KTR${selectedSlot + 1}=${selectedId}`;
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (!response.ok)
throw new Error(
`Fehler beim Setzen der Referenz: ${response.statusText}`
);
}
}
// Optional: lokale Speicherung und Redux-Update
localStorage.setItem(
`ref-curve-slot${selectedSlot}`,
JSON.stringify(currentChartData)
);
dispatch(getReferenceCurveBySlotThunk(selectedSlot));
alert("Referenzkurve wurde erfolgreich gesetzt!");
} catch (error) {
console.error("Fehler beim Setzen der Referenzkurve:", error);
} catch (err) {
console.error("Fehler beim Setzen der Referenzkurve", err);
alert("Fehler beim Setzen der Referenzkurve.");
}
};
// 📌 TDR Messung starten
const handleStartTDR = async () => {
if (selectedSlot === null) {
alert("⚠️ Bitte zuerst einen KÜ auswählen!");
return;
}
const cgiUrl = `${window.location.origin}/CPL?/${window.location.pathname}&KTT${selectedSlot}=1`;
try {
console.log("🚀 Starte TDR Messung für Slot:", selectedSlot);
console.log("📡 CGI URL:", cgiUrl);
const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development";
if (isDev) {
// Dev / Simulator: sofort starten & Progress anzeigen
await new Promise((r) => setTimeout(r, 150));
console.log("✅ [DEV] TDR Mock-Start ok für Slot", selectedSlot);
startTdrProgress();
return;
}
const response = await fetch(cgiUrl);
if (!response.ok) throw new Error(`CGI-Fehler: ${response.status}`);
console.log("✅ TDR Messung gestartet für Slot", selectedSlot);
startTdrProgress();
} catch (err) {
console.error("❌ Fehler beim Starten der TDR Messung:", err);
//alert("❌ Fehler beim Starten der TDR Messung.");
console.error("❌ Fehler beim Starten der TDR Messung", err);
}
};
// 📥 Beim Slot-Wechsel TDM-Liste + letzte ID laden
// Load TDM list when slot changes
useEffect(() => {
if (selectedSlot !== null) {
dispatch(fetchTDMDataBySlotThunk(selectedSlot)).then((action) => {
// action can be a PayloadAction with payload or a rejected action
const payload = (
action as {
payload?: { data?: { id: number; t: string; d: number }[] };
}
).payload;
const slotData = payload?.data;
if ((slotData ?? []).length > 0) {
const lastId = (slotData ?? [])[0].id;
const slotData = payload?.data ?? [];
if (slotData.length > 0) {
const lastId = slotData[0].id; // latest first
setSelectedId(lastId);
dispatch(getTDRChartDataByIdThunk(lastId));
}
@@ -166,7 +154,6 @@ const TDRChartActionBar: React.FC = () => {
return (
<>
<div className="toolbar w-full flex items-center gap-3 flex-wrap">
{/* Slot Badge */}
<div className="flex items-center gap-2 pr-4">
<span className="font-semibold uppercase tracking-wide text-muted">
@@ -175,17 +162,29 @@ const TDRChartActionBar: React.FC = () => {
{selectedSlot !== null ? selectedSlot + 1 : "-"}
</span>
</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 && (
<button
onClick={handleSetReference}
type="button"
className="btn-primary h-8 px-3 font-medium"
disabled={selectedSlot === null}
>
TDR-Kurve als Referenz speichern
</button>
)}
{/* Start TDR */}
<button
onClick={handleStartTDR}
type="button"
@@ -201,7 +200,6 @@ const TDRChartActionBar: React.FC = () => {
)}%)`
: "TDR-Messung starten"}
</button>
{/* Messung Dropdown */}
<div className="ml-auto flex-1 min-w-[14rem] max-w-[30rem]">
<Listbox
value={selectedId}
@@ -220,14 +218,17 @@ const TDRChartActionBar: React.FC = () => {
(e) => e.id === selectedId
);
return selected
? `${new Date(selected.t).toLocaleString("de-DE", {
? `${new Date(selected.t).toLocaleString(
"de-DE",
{
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})} Fehlerstelle: ${selected.d} m`
}
)} Fehlerstelle: ${selected.d} m`
: "Wähle Messung";
})()
: "Wähle Messung"}
@@ -236,14 +237,17 @@ const TDRChartActionBar: React.FC = () => {
</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">
{idsForSlot.map((entry) => {
const dateLabel = new Date(entry.t).toLocaleString("de-DE", {
const dateLabel = new Date(entry.t).toLocaleString(
"de-DE",
{
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
);
const fullText = `${dateLabel} Fehlerstelle: ${entry.d} m`;
return (
<Listbox.Option
@@ -256,7 +260,7 @@ const TDRChartActionBar: React.FC = () => {
if (selected)
return `${base} dropdown-option-active font-medium`;
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>
@@ -267,6 +271,8 @@ const TDRChartActionBar: React.FC = () => {
</div>
</Listbox>
</div>
</>
)}
</div>
{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">

4
package-lock.json generated
View File

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

View File

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

View File

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