feat: Messverlauf bei Systemwerten (Temperatur und Spannungen) mit Datumsauswahl

This commit is contained in:
ISA
2025-09-03 13:38:05 +02:00
parent f4f4c28cb7
commit a9ccdfc9ab
7 changed files with 156 additions and 88 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.828
NEXT_PUBLIC_APP_VERSION=1.6.829
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.828
NEXT_PUBLIC_APP_VERSION=1.6.829
NEXT_PUBLIC_CPL_MODE=production

View File

@@ -1,3 +1,8 @@
## [1.6.829] 2025-09-03
- feat(mocks): mesages_all.json mock script
---
## [1.6.828] 2025-09-03
- feat(mocks): retime chart mocks to today; add global/all-slot scripts

View File

@@ -4,15 +4,11 @@ import React, { useEffect, useRef, useState, useCallback } from "react";
import { Line } from "react-chartjs-2";
import { useSelector } from "react-redux";
import { RootState, useAppDispatch } from "@/redux/store";
import { Listbox } from "@headlessui/react";
import { setFullScreen } from "@/redux/slices/kabelueberwachungChartSlice";
import DateRangePicker from "@/components/common/DateRangePicker";
import {
setVonDatum,
setBisDatum,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { resetDateRange } from "@/redux/slices/dateRangePickerSlice";
// Import Thunks
import SystemChartActionBar from "@/components/main/system/SystemChartActionBar";
import { getSystemspannung5VplusThunk } from "@/redux/thunks/getSystemspannung5VplusThunk";
import { getSystemspannung15VplusThunk } from "@/redux/thunks/getSystemspannung15VplusThunk";
import { getSystemspannung15VminusThunk } from "@/redux/thunks/getSystemspannung15VminusThunk";
@@ -214,8 +210,8 @@ export const DetailModal = ({
useEffect(() => {
if (isOpen) {
setZeitraum("DIA0");
dispatch(setVonDatum(""));
dispatch(setBisDatum(""));
// Reset DateRangePicker to its defaults (it sets 30 days → today on mount)
dispatch(resetDateRange());
// Chart-Daten zurücksetzen beim Öffnen
setChartData({ datasets: [] });
@@ -260,8 +256,7 @@ export const DetailModal = ({
const handleClose = () => {
dispatch(setFullScreen(false));
dispatch(setVonDatum(""));
dispatch(setBisDatum(""));
dispatch(resetDateRange());
onClose();
};
@@ -307,25 +302,55 @@ export const DetailModal = ({
}
}, [chartData, isLoading]);
// DateRange from global DateRangePicker slice
const pickerVonDatum = useSelector(
(state: RootState) => state.dateRangePicker.vonDatum
);
const pickerBisDatum = useSelector(
(state: RootState) => state.dateRangePicker.bisDatum
);
// Update chart data when Redux data changes (only after button click)
useEffect(() => {
if (shouldUpdateChart && reduxData && reduxData.length > 0) {
console.log("Redux data for chart:", reduxData);
// Filter data by selected date range (inclusive end date)
let filtered = reduxData;
try {
if (pickerVonDatum && pickerBisDatum) {
const start = new Date(`${pickerVonDatum}T00:00:00`);
const end = new Date(`${pickerBisDatum}T23:59:59`);
const s = start.getTime();
const e = end.getTime();
filtered = reduxData.filter((entry) => {
const t = new Date(entry.t).getTime();
return t >= s && t <= e;
});
}
} catch (err) {
console.warn("Zeitfilter konnte nicht angewendet werden:", err);
}
console.log("Redux data for chart (filtered):", filtered.length);
if (!filtered.length) {
setChartData({ datasets: [] });
setShouldUpdateChart(false);
return;
}
// Create datasets array for multiple lines
const datasets = [];
// Check which data fields are available and create datasets accordingly
const hasMinimum = reduxData.some(
const hasMinimum = filtered.some(
(entry) => entry.i !== undefined && entry.i !== null && entry.i !== 0
);
const hasMaximum = reduxData.some(
const hasMaximum = filtered.some(
(entry) => entry.a !== undefined && entry.a !== null
);
const hasAverage = reduxData.some(
const hasAverage = filtered.some(
(entry) => entry.g !== undefined && entry.g !== null
);
const hasCurrent = reduxData.some(
const hasCurrent = filtered.some(
(entry) => entry.m !== undefined && entry.m !== null
);
@@ -333,7 +358,7 @@ export const DetailModal = ({
if (hasMinimum) {
datasets.push({
label: "Messwert Minimum",
data: reduxData.map((entry) => ({
data: filtered.map((entry) => ({
x: new Date(entry.t).getTime(),
y: entry.i || 0,
})),
@@ -348,7 +373,7 @@ export const DetailModal = ({
if (hasMaximum) {
datasets.push({
label: "Messwert Maximum",
data: reduxData.map((entry) => ({
data: filtered.map((entry) => ({
x: new Date(entry.t).getTime(),
y: entry.a || 0,
})),
@@ -364,7 +389,7 @@ export const DetailModal = ({
if (hasAverage) {
datasets.push({
label: "Durchschnitt",
data: reduxData.map((entry) => ({
data: filtered.map((entry) => ({
x: new Date(entry.t).getTime(),
y: entry.g || 0,
})),
@@ -379,7 +404,7 @@ export const DetailModal = ({
if (hasCurrent) {
datasets.push({
label: "Messwert",
data: reduxData.map((entry) => ({
data: filtered.map((entry) => ({
x: new Date(entry.t).getTime(),
y: entry.m || 0,
})),
@@ -449,70 +474,12 @@ export const DetailModal = ({
</div>
</div>
<div className="flex items-center justify-start gap-4 mb-4 flex-wrap">
<DateRangePicker />
<label className="font-medium">Zeitraum:</label>
<Listbox value={zeitraum} onChange={setZeitraum}>
<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ündlich",
DIA2: "Täglich",
}[zeitraum]
}
</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"
<SystemChartActionBar
zeitraum={zeitraum}
setZeitraum={setZeitraum}
onFetchData={handleFetchData}
isLoading={isLoading}
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white dark:bg-gray-800 shadow max-h-60 overflow-auto text-sm border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100">
{["DIA0", "DIA1", "DIA2"].map((option) => (
<Listbox.Option
key={option}
value={option}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200 dark:bg-gray-700"
: ""
}`
}
>
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündlich",
DIA2: "Täglich",
}[option]
}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
<button
onClick={handleFetchData}
className={`px-4 py-1 bg-littwin-blue text-white rounded text-sm ${
isLoading ? "cursor-wait" : ""
}`}
disabled={isLoading}
>
{isLoading ? "Laden..." : "Daten laden"}
</button>
</div>
<div className="h-[85%] bg-white dark:bg-gray-800 rounded shadow border border-gray-200 dark:border-gray-700 p-2">
<Line ref={chartRef} data={chartData} options={chartOptions} />

View File

@@ -0,0 +1,96 @@
"use client";
// components/main/system/SystemChartActionBar.tsx
import React from "react";
import DateRangePicker from "@/components/common/DateRangePicker";
import { Listbox } from "@headlessui/react";
type Props = {
zeitraum: "DIA0" | "DIA1" | "DIA2";
setZeitraum: (typ: "DIA0" | "DIA1" | "DIA2") => void;
onFetchData: () => void;
isLoading?: boolean;
className?: string;
};
const SystemChartActionBar: React.FC<Props> = ({
zeitraum,
setZeitraum,
onFetchData,
isLoading = false,
className = "",
}) => {
return (
<div
className={`flex items-center justify-start gap-3 mb-4 flex-wrap ${className}`}
>
{/* DateRangePicker nutzt globalen Redux-Slice */}
<DateRangePicker compact />
{/* Zeitraum (DIA0/DIA1/DIA2) */}
<label className="font-medium text-sm">Zeitraum:</label>
<Listbox value={zeitraum} onChange={setZeitraum}>
<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ündlich", DIA2: "Täglich" }[
zeitraum
]
}
</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((option) => (
<Listbox.Option
key={option}
value={option}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
>
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündlich",
DIA2: "Täglich",
}[option as "DIA0" | "DIA1" | "DIA2"]
}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
{/* Daten laden */}
<button
onClick={onFetchData}
className={`px-4 py-1 bg-littwin-blue text-white rounded text-sm ${
isLoading ? "cursor-wait opacity-70" : ""
}`}
disabled={isLoading}
aria-busy={isLoading}
>
{isLoading ? "Laden..." : "Daten laden"}
</button>
</div>
);
};
export default SystemChartActionBar;

4
package-lock.json generated
View File

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

View File

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