WIP: von bis Zeitraum in ISO und TDR, aber TDR ist WIP
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
@@ -320,7 +308,7 @@ const IsoChartActionBar = forwardRef((_props, ref) => {
|
||||
</span>
|
||||
<i className="bi bi-chevron-down opacity-70" />
|
||||
</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) => (
|
||||
<Listbox.Option
|
||||
key={mode}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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">
|
||||
KÜ
|
||||
@@ -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
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cpl-v4",
|
||||
"version": "1.6.884",
|
||||
"version": "1.6.885",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000",
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user