feat: Meldungen in in Iso Chart

This commit is contained in:
ISA
2025-07-31 15:57:50 +02:00
parent cdd26931a1
commit 63e1b85a44
8 changed files with 253 additions and 91 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.667 NEXT_PUBLIC_APP_VERSION=1.6.668
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.667 NEXT_PUBLIC_APP_VERSION=1.6.668
NEXT_PUBLIC_CPL_MODE=production NEXT_PUBLIC_CPL_MODE=production

View File

@@ -1,3 +1,17 @@
## [1.6.668] 2025-07-31
- feat: implement chart modal with report functionality for cable monitoring
- Add chartTitle state management to kabelueberwachungChartSlice with "Messkurve"/"Meldungen" options
- Update IsoChartActionBar dropdown to show current chartTitle value with proper binding
- Implement conditional rendering in IsoChartView between IsoMeasurementChart and Report components
- Create Report.tsx component using same data structure as MeldungenView (Meldung type)
- Add slot-based message filtering for specific cable monitoring units (KÜ)
- Integrate getMessagesThunk for consistent data loading across components
- Style Report component with consistent table layout, German date formatting, and Littwin branding
- Enable seamless switching between measurement chart and filtered messages in modal
---
## [1.6.667] 2025-07-31 ## [1.6.667] 2025-07-31
- feat: TDR --> Messkurven TDR anzeigen und dort Schalter Messung aktivieren - feat: TDR --> Messkurven TDR anzeigen und dort Schalter Messung aktivieren

View File

@@ -88,6 +88,87 @@ export const useIsoChartLoader = () => {
return { loadIsoChartData }; return { loadIsoChartData };
}; };
//-----------------------------------------------------------------------------------useIsoDataLoader Hook
export const useIsoDataLoader = () => {
const dispatch = useDispatch();
const { vonDatum, bisDatum, selectedMode, slotNumber } = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice
);
const formatDate = (dateString: string) => {
const [year, month, day] = dateString.split("-");
return `${year};${month};${day}`;
};
const getApiUrl = (mode: "DIA0" | "DIA1" | "DIA2", slotNumber: number) => {
const type = 3; // Fest auf Isolationswiderstand gesetzt
const typeFolder = "isolationswiderstand";
const baseUrl =
process.env.NODE_ENV === "development"
? `/api/cpl/slotDataAPIHandler?slot=${slotNumber}&messart=${typeFolder}&dia=${mode}&vonDatum=${vonDatum}&bisDatum=${bisDatum}`
: `${window.location.origin}/CPL?seite.ACP&${mode}=${formatDate(
vonDatum
)};${formatDate(bisDatum)};${slotNumber};${type};`;
return baseUrl;
};
const loadData = async () => {
if (slotNumber === null) {
console.log("⚠️ Kein Slot ausgewählt - automatisches Laden übersprungen");
return;
}
const apiUrl = getApiUrl(selectedMode, slotNumber);
if (!apiUrl) return;
dispatch(setLoading(true));
dispatch(setChartOpen(false));
dispatch(setIsoMeasurementCurveChartData([]));
const MIN_LOADING_TIME_MS = 1000;
const startTime = Date.now();
try {
const response = await fetch(apiUrl, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) throw new Error(`Fehler: ${response.status}`);
const jsonData = await response.json();
const elapsedTime = Date.now() - startTime;
const waitTime = Math.max(0, MIN_LOADING_TIME_MS - elapsedTime);
await new Promise((resolve) => setTimeout(resolve, waitTime));
console.log("▶️ Automatisches Laden - Isolationswiderstand-Daten für:");
console.log(" Slot:", slotNumber);
console.log(" Modus:", selectedMode);
console.log(" Von:", vonDatum);
console.log(" Bis:", bisDatum);
if (Array.isArray(jsonData) && jsonData.length > 0) {
dispatch(setIsoMeasurementCurveChartData(jsonData));
dispatch(setChartOpen(true));
} else {
console.log(
"⚠️ Keine Messdaten im gewählten Zeitraum gefunden (automatisches Laden)"
);
dispatch(setIsoMeasurementCurveChartData([]));
dispatch(setChartOpen(false));
}
} catch (err) {
console.error("❌ Fehler beim automatischen Laden der Daten:", err);
} finally {
dispatch(setLoading(false));
}
};
return { loadData };
};
//-----------------------------------------------------------------------------------IsoChartActionBar //-----------------------------------------------------------------------------------IsoChartActionBar
const IsoChartActionBar: React.FC = () => { const IsoChartActionBar: React.FC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -183,67 +264,71 @@ const IsoChartActionBar: React.FC = () => {
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<DateRangePicker /> {/* DateRangePicker - nur bei Messkurve anzeigen */}
{chartTitle === "Messkurve" && <DateRangePicker />}
<Listbox {/* DIA0-DIA2 Dropdown - nur bei Messkurve anzeigen */}
value={selectedMode} {chartTitle === "Messkurve" && (
onChange={(value) => { <Listbox
dispatch(setSelectedMode(value)); value={selectedMode}
dispatch(setBrushRange({ startIndex: 0, endIndex: 0 })); onChange={(value) => {
}} dispatch(setSelectedMode(value));
> dispatch(setBrushRange({ startIndex: 0, endIndex: 0 }));
<div className="relative w-48"> }}
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm"> >
<span> <div className="relative w-48">
{ <Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
{ <span>
DIA0: "Alle Messwerte",
DIA1: "Stündliche Werte",
DIA2: "Tägliche Werte",
}[selectedMode]
}
</span>
<svg
className="w-5 h-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
{["DIA0", "DIA1", "DIA2"].map((mode) => (
<Listbox.Option
key={mode}
value={mode}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
>
{ {
{ {
DIA0: "Alle Messwerte", DIA0: "Alle Messwerte",
DIA1: "Stündliche Werte", DIA1: "Stündliche Werte",
DIA2: "Tägliche Werte", DIA2: "Tägliche Werte",
}[mode] }[selectedMode]
} }
</Listbox.Option> </span>
))} <svg
</Listbox.Options> className="w-5 h-5 text-gray-400"
</div> viewBox="0 0 20 20"
</Listbox> fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
{["DIA0", "DIA1", "DIA2"].map((mode) => (
<Listbox.Option
key={mode}
value={mode}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
>
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündliche Werte",
DIA2: "Tägliche Werte",
}[mode]
}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
)}
{/* Dropdown für Auswahl zwischen "Messkurve" und "Meldungen" */} {/* Dropdown für Auswahl zwischen "Messkurve" und "Meldungen" - immer anzeigen */}
<Listbox <Listbox
value={chartTitle} value={chartTitle}
onChange={(value) => dispatch(setChartTitle(value))} onChange={(value) => dispatch(setChartTitle(value))}
@@ -285,14 +370,18 @@ const IsoChartActionBar: React.FC = () => {
</div> </div>
</Listbox> </Listbox>
<button {/* Daten laden Button - nur bei Messkurve anzeigen */}
onClick={handleFetchData} {chartTitle === "Messkurve" && (
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm" <button
> onClick={handleFetchData}
Daten laden className="px-4 py-1 bg-littwin-blue text-white rounded text-sm"
</button> >
Daten laden
</button>
)}
{isLoading && ( {/* Loading Indicator - nur bei Messkurve anzeigen */}
{chartTitle === "Messkurve" && isLoading && (
<div className="flex items-center space-x-2 text-sm text-gray-500"> <div className="flex items-center space-x-2 text-sm text-gray-500">
<div className="w-4 h-4 border-2 border-t-2 border-blue-500 rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-t-2 border-blue-500 rounded-full animate-spin" />
<span>Lade Daten...</span> <span>Lade Daten...</span>

View File

@@ -3,7 +3,7 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import ReactModal from "react-modal"; import ReactModal from "react-modal";
import IsoMeasurementChart from "./IsoMeasurementChart"; import IsoMeasurementChart from "./IsoMeasurementChart";
import IsoChartActionBar from "./IsoChartActionBar"; import IsoChartActionBar, { useIsoDataLoader } from "./IsoChartActionBar";
import Report from "./Report"; import Report from "./Report";
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import { AppDispatch } from "@/redux/store"; import { AppDispatch } from "@/redux/store";
@@ -36,6 +36,7 @@ const IsoChartView: React.FC<IsoChartViewProps> = ({
slotIndex, slotIndex,
}) => { }) => {
const dispatch = useDispatch<AppDispatch>(); const dispatch = useDispatch<AppDispatch>();
const { loadData } = useIsoDataLoader();
const { isFullScreen, chartTitle } = useSelector( const { isFullScreen, chartTitle } = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice (state: RootState) => state.kabelueberwachungChartSlice
@@ -92,8 +93,18 @@ const IsoChartView: React.FC<IsoChartViewProps> = ({
// Set default to Messkurve // Set default to Messkurve
dispatch(setChartTitle("Messkurve")); dispatch(setChartTitle("Messkurve"));
// Automatisch Daten laden nach kurzer Verzögerung
// um sicherzustellen, dass alle Redux-States gesetzt sind
const timer = setTimeout(() => {
loadData();
}, 100);
// Cleanup timer
return () => clearTimeout(timer);
} }
}, [isOpen, slotIndex, dispatch]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, slotIndex, dispatch]); // loadData bewusst ausgelassen um Endlosschleife zu vermeiden
return ( return (
<ReactModal <ReactModal

View File

@@ -33,11 +33,48 @@ const Report: React.FC = () => {
(allMessages: Meldung[], slot: number) => { (allMessages: Meldung[], slot: number) => {
if (slot === null) return []; if (slot === null) return [];
// Filter basierend auf der Quelle (i-Feld) // Primärer Filter: Exakte CableLineX Übereinstimmung (X = slot + 1)
return allMessages.filter((msg: Meldung) => { const primaryIdentifier = `CableLine${slot + 1}`;
// Verschiedene mögliche Slot-Identifikationen
const slotIdentifiers = [ console.log(
`CableLine${slot + 1}`, `🔍 Filtere Nachrichten für Slot ${slot} (${primaryIdentifier}):`
);
console.log(`📥 Gesamt Nachrichten: ${allMessages.length}`);
// Debug: Zeige alle verfügbaren Quellen
const allSources = [...new Set(allMessages.map((msg) => msg.i))];
console.log(`📋 Alle verfügbaren Quellen:`, allSources);
// Filter basierend auf der Quelle (i-Feld) - EXAKTE Übereinstimmung
const filtered = allMessages.filter((msg: Meldung) => {
// Exakte Übereinstimmung: msg.i sollte genau "CableLineX" sein
const isExactMatch = msg.i === primaryIdentifier;
// Fallback: Falls die Quelle mehr Informationen enthält (z.B. "CableLine1_Sensor")
const isPartialMatch =
msg.i.startsWith(primaryIdentifier) &&
(msg.i === primaryIdentifier ||
msg.i.charAt(primaryIdentifier.length).match(/[^0-9]/));
const isMatch = isExactMatch || isPartialMatch;
if (isMatch) {
console.log(`✅ Gefunden: "${msg.i}" -> ${msg.m}`);
}
return isMatch;
});
console.log(
`📤 Gefilterte Nachrichten für ${primaryIdentifier}: ${filtered.length}`
);
// Falls keine Nachrichten mit CableLineX gefunden, versuche alternative Identifikatoren
if (filtered.length === 0) {
console.log(
`⚠️ Keine Nachrichten für ${primaryIdentifier} gefunden. Versuche alternative Identifikatoren...`
);
const alternativeIdentifiers = [
`Slot${slot + 1}`, `Slot${slot + 1}`,
`${slot + 1}`, `${slot + 1}`,
`Kue${slot + 1}`, `Kue${slot + 1}`,
@@ -45,8 +82,29 @@ const Report: React.FC = () => {
`Line${slot + 1}`, `Line${slot + 1}`,
]; ];
return slotIdentifiers.some((identifier) => msg.i.includes(identifier)); const alternativeFiltered = allMessages.filter((msg: Meldung) => {
}); return alternativeIdentifiers.some((identifier) => {
const isExactMatch = msg.i === identifier;
const isPartialMatch =
msg.i.startsWith(identifier) &&
(msg.i === identifier ||
msg.i.charAt(identifier.length).match(/[^0-9]/));
const isMatch = isExactMatch || isPartialMatch;
if (isMatch) {
console.log(`🔄 Alternative gefunden: "${msg.i}" -> ${msg.m}`);
}
return isMatch;
});
});
console.log(
`📤 Alternative gefilterte Nachrichten: ${alternativeFiltered.length}`
);
return alternativeFiltered;
}
return filtered;
}, },
[] []
); );
@@ -107,25 +165,15 @@ const Report: React.FC = () => {
} }
return ( return (
<div className="w-full h-full flex flex-col"> <div className="w-full h-full flex flex-col p-4">
<div className="flex justify-between items-center mb-4">
<h4 className="text-lg font-semibold">
Meldungen für {slotNumber !== null ? slotNumber + 1 : "-"}
</h4>
<button
onClick={loadMessages}
className="px-3 py-1 bg-littwin-blue text-white rounded text-sm hover:bg-blue-600"
>
Aktualisieren
</button>
</div>
{filteredMessages.length === 0 ? ( {filteredMessages.length === 0 ? (
<div className="text-center text-gray-500 py-8"> <div className="text-center text-gray-500 ">
Keine Meldungen im gewählten Zeitraum gefunden. Keine Meldungen für CableLine
{slotNumber !== null ? slotNumber + 1 : "-"} im gewählten Zeitraum
gefunden.
</div> </div>
) : ( ) : (
<div className="flex-1 overflow-auto max-h-[80vh]"> <div className="flex-1 overflow-auto ">
<table className="min-w-full border text-sm"> <table className="min-w-full border text-sm">
<thead className="bg-gray-100 text-left sticky top-0 z-10"> <thead className="bg-gray-100 text-left sticky top-0 z-10">
<tr> <tr>
@@ -165,7 +213,7 @@ const Report: React.FC = () => {
</div> </div>
)} )}
<div className="mt-4 text-sm text-gray-500 text-center"> <div className="mt-4 text-sm text-gray-500 text-center mt-4">
{filteredMessages.length} Meldung(en) gefunden {filteredMessages.length} Meldung(en) gefunden
</div> </div>
</div> </div>

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.667", "version": "1.6.668",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.667", "version": "1.6.668",
"dependencies": { "dependencies": {
"@fontsource/roboto": "^5.1.0", "@fontsource/roboto": "^5.1.0",
"@headlessui/react": "^2.2.4", "@headlessui/react": "^2.2.4",

View File

@@ -1,6 +1,6 @@
{ {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.667", "version": "1.6.668",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",