Compare commits
289 Commits
b0efd3aa0f
...
feat/messv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6820fa9eed | ||
|
|
3daa6b1dbb | ||
|
|
9c7ad37233 | ||
|
|
0286670b81 | ||
|
|
02a0ce5891 | ||
|
|
47e0efeb80 | ||
|
|
b62c477d50 | ||
|
|
653a31ce63 | ||
|
|
57ffdecb10 | ||
|
|
11bd68200b | ||
|
|
2c92ca0866 | ||
|
|
a9ccdfc9ab | ||
|
|
f4f4c28cb7 | ||
|
|
d6fcf95795 | ||
|
|
6c2707ff47 | ||
|
|
420f1da114 | ||
|
|
5aa7618832 | ||
|
|
35171891a3 | ||
|
|
2df1ee1022 | ||
|
|
7fe842aa93 | ||
|
|
cdf4869548 | ||
|
|
bb115a9a4f | ||
|
|
da87ebc5c8 | ||
|
|
5b4eb7ff51 | ||
|
|
3254563458 | ||
|
|
5252ec5998 | ||
|
|
2d9cd74375 | ||
|
|
f4e0620b49 | ||
|
|
35e34b96d1 | ||
|
|
fb79817136 | ||
|
|
89dc26b0d2 | ||
|
|
c8616f7bbe | ||
|
|
b0b9952a2d | ||
|
|
8da1457e4d | ||
|
|
7dc64ca972 | ||
|
|
1b038ac844 | ||
|
|
cbc476b09a | ||
|
|
306f469634 | ||
|
|
772baea4ed | ||
|
|
f3f6e25e9c | ||
|
|
43fe9e2065 | ||
|
|
30f156934c | ||
|
|
b108d63106 | ||
|
|
b53762cf5c | ||
|
|
629548bfdd | ||
|
|
174d67cfd8 | ||
|
|
57baca292a | ||
|
|
0c02e6f1c9 | ||
|
|
dcf22d08fb | ||
|
|
867031d3c3 | ||
|
|
0c3eb4cc5a | ||
|
|
d4d0c91400 | ||
|
|
9147cec40f | ||
|
|
e732971cdc | ||
|
|
9db92a2728 | ||
|
|
1d815d4265 | ||
|
|
5aece28eb1 | ||
|
|
0f570ac5b0 | ||
|
|
8850b0ffda | ||
|
|
6df31455a9 | ||
|
|
05c1c9c0cf | ||
|
|
9f43fdc820 | ||
|
|
763f5293bc | ||
|
|
0e7b2e53aa | ||
|
|
7a4e6d92c2 | ||
|
|
3e998d6644 | ||
|
|
b6ab6a11f9 | ||
|
|
a8388b27b9 | ||
|
|
b1ac0f87f1 | ||
|
|
036630f598 | ||
|
|
2ce7c54697 | ||
|
|
ae5798bcdf | ||
|
|
93b4859700 | ||
|
|
b011ab9862 | ||
|
|
9649eec907 | ||
|
|
8117ebdf45 | ||
|
|
e8477640e2 | ||
|
|
9e84386a5d | ||
|
|
93809a85a4 | ||
|
|
f9f358a678 | ||
|
|
539ebd5ff6 | ||
|
|
c8f3d91f9c | ||
|
|
37bbd6a9b3 | ||
|
|
2d9a7118c6 | ||
|
|
699ebef7bd | ||
|
|
e69934ff51 | ||
|
|
f25c527e71 | ||
|
|
fb36561cb9 | ||
|
|
6e98a98670 | ||
|
|
5f97731e2b | ||
|
|
c711a6a132 | ||
|
|
b72b9d665b | ||
|
|
9c411be38c | ||
|
|
3a9543f7a7 | ||
|
|
6036c48332 | ||
|
|
7e41e5131f | ||
|
|
6756bbf0f8 | ||
|
|
a955564ee3 | ||
|
|
c496939004 | ||
|
|
bb06618919 | ||
|
|
eae8ea37d0 | ||
|
|
710d780a3a | ||
|
|
8097246049 | ||
|
|
e6aafd6b0c | ||
|
|
984c776b2a | ||
|
|
a84e8c529f | ||
|
|
04b9a0dc1d | ||
|
|
7d6263b6fb | ||
|
|
4e8221c892 | ||
|
|
d75d9ce578 | ||
|
|
b006e3a993 | ||
|
|
5e9c7e9bfe | ||
|
|
3e0b1e98bb | ||
|
|
94051b69f9 | ||
|
|
629385fa5c | ||
|
|
a446ce80ee | ||
|
|
71dd37bb0e | ||
|
|
08370cf898 | ||
|
|
fa92004d94 | ||
|
|
3753babf5f | ||
|
|
87cbdca79c | ||
|
|
bb68327604 | ||
|
|
2db9da2394 | ||
|
|
c3fc8e0a4a | ||
|
|
eff606e59a | ||
|
|
f1ba9d4e4d | ||
|
|
7bc13505b2 | ||
|
|
6da0408140 | ||
|
|
ad6d89847e | ||
|
|
5496254acb | ||
|
|
8fcbf6cfcd | ||
|
|
974f468766 | ||
|
|
0fb6d184bd | ||
|
|
48d634295a | ||
|
|
0246e34de4 | ||
|
|
ba0cb732d9 | ||
|
|
91b76b8e8d | ||
|
|
bb662bf856 | ||
|
|
2765d06836 | ||
|
|
b8b5c36a60 | ||
|
|
31a54deb2d | ||
|
|
71f120aa27 | ||
|
|
77c939697c | ||
|
|
234608973e | ||
|
|
8af8e14878 | ||
|
|
e4b56faf75 | ||
|
|
100dab06ed | ||
|
|
e7d120c477 | ||
|
|
2bf02af96f | ||
|
|
9ca5ee9e66 | ||
|
|
bc20f3869d | ||
|
|
06aa3c8f3e | ||
|
|
8d1b5ceddc | ||
|
|
806eaaeff7 | ||
|
|
c107738625 | ||
|
|
9b05f21ccc | ||
|
|
3b61dcb31b | ||
|
|
f8bfea039c | ||
|
|
136d3151cf | ||
|
|
ba1b0d8e79 | ||
|
|
0b7542488e | ||
|
|
3098ce67f0 | ||
|
|
0a20f91ba6 | ||
|
|
975d3b726f | ||
|
|
423c87ca11 | ||
|
|
c1ed09a21d | ||
|
|
4fe64382f3 | ||
|
|
e86de5cefe | ||
|
|
63e1b85a44 | ||
|
|
cdd26931a1 | ||
|
|
638b7bf519 | ||
|
|
46ba692af0 | ||
|
|
90b9616d50 | ||
|
|
85860ae9f0 | ||
|
|
421e1f5425 | ||
|
|
97eb40e1c6 | ||
|
|
b68eb10ad4 | ||
|
|
86b35e9925 | ||
|
|
ad6642b5e7 | ||
|
|
e76c8d9bd2 | ||
|
|
d4335960bf | ||
|
|
9457233c7d | ||
|
|
9a8a0501a5 | ||
|
|
ce1dacda9b | ||
|
|
7a9fbc97dd | ||
|
|
f79c225b71 | ||
|
|
afdcb6b92f | ||
|
|
4af7836a54 | ||
|
|
cfe838dd07 | ||
|
|
8dafd5fe67 | ||
|
|
e932bee120 | ||
|
|
c1f6c19fdf | ||
|
|
b7ff3b07cd | ||
|
|
9bd69f7a07 | ||
|
|
357fb6c816 | ||
|
|
628cbc405e | ||
|
|
ada2f5e2a7 | ||
|
|
bdbdd27963 | ||
|
|
2272668ace | ||
|
|
bc554d3474 | ||
|
|
4d48100375 | ||
|
|
5cf5e34c4f | ||
|
|
36863d3c6a | ||
|
|
5a0188c635 | ||
|
|
d44fe916da | ||
|
|
2d166a204b | ||
|
|
b7ca20f7c3 | ||
|
|
ed9f693098 | ||
|
|
773e2c12b8 | ||
|
|
03ee4fb08e | ||
|
|
697cae9848 | ||
|
|
30d396896d | ||
|
|
fb68d59da4 | ||
|
|
6b43435097 | ||
|
|
a75347a59f | ||
|
|
7cd0c41ec5 | ||
|
|
c73b7ec252 | ||
|
|
f876bef7a3 | ||
|
|
b1ff138774 | ||
|
|
23a3c173dd | ||
|
|
528773128d | ||
|
|
311d47211e | ||
|
|
b6e4c32287 | ||
|
|
f485d87809 | ||
|
|
658aa0cae5 | ||
|
|
99294f26da | ||
|
|
d278a79030 | ||
|
|
ca84ac6bb5 | ||
|
|
2d3e070830 | ||
|
|
1f1e532233 | ||
|
|
93ae79ac7e | ||
|
|
bb8b345647 | ||
|
|
e9a6d45d1f | ||
|
|
49f9c3737a | ||
|
|
420989dc9f | ||
|
|
3a1d85dbe2 | ||
|
|
4ea12a1f79 | ||
|
|
898f2b14f2 | ||
|
|
7cabbafad5 | ||
|
|
eae69d4392 | ||
|
|
a8c027bd6e | ||
|
|
b7b0829c5b | ||
|
|
0410e8c52d | ||
|
|
fd42502d05 | ||
|
|
340990573f | ||
|
|
eca52f35cb | ||
|
|
14bd72756a | ||
|
|
7797549baa | ||
|
|
93c3bc612d | ||
|
|
b091a8d82a | ||
|
|
454b8bfb8d | ||
|
|
48898fcd09 | ||
|
|
0f233ce6e2 | ||
|
|
976f3126f2 | ||
|
|
2af99f2740 | ||
|
|
fb680a4c66 | ||
|
|
44cfd2ab81 | ||
|
|
3af16b4c29 | ||
|
|
3d37388173 | ||
|
|
92eb28e495 | ||
|
|
99d2a3d451 | ||
|
|
fdd38c74f0 | ||
|
|
8ee7c9c193 | ||
|
|
4e5eeed9a2 | ||
|
|
31223ffc64 | ||
|
|
28441eebf1 | ||
|
|
bf4e38509a | ||
|
|
ba690c1e03 | ||
|
|
b58c961da4 | ||
|
|
10a9167a1f | ||
|
|
ebe72c3ab0 | ||
|
|
859a8f1d64 | ||
|
|
b1eb3c46a8 | ||
|
|
a0e8e47fae | ||
|
|
521bd7ea93 | ||
|
|
3e7d702ab7 | ||
|
|
09bc64e771 | ||
|
|
cee3ee0581 | ||
|
|
4245d7a991 | ||
|
|
937e7b67e9 | ||
|
|
b23d939481 | ||
|
|
e9e929f577 | ||
|
|
f50bff4819 | ||
|
|
b3c5580538 | ||
|
|
1ec2c5cc14 | ||
|
|
a7d1e1e8df | ||
|
|
e46e23fada | ||
|
|
a9f6484fb0 | ||
|
|
1dfa1cc1ba |
@@ -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.512
|
||||
NEXT_PUBLIC_APP_VERSION=1.6.840
|
||||
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.512
|
||||
NEXT_PUBLIC_APP_VERSION=1.6.840
|
||||
NEXT_PUBLIC_CPL_MODE=production
|
||||
@@ -14,7 +14,32 @@
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es2021": true
|
||||
},
|
||||
"globals": {
|
||||
"JSX": "readonly",
|
||||
"NodeJS": "readonly"
|
||||
},
|
||||
"rules": {
|
||||
// deine Regeln hier
|
||||
"no-undef": "off",
|
||||
"no-unreachable": "error",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"@next/next/no-img-element": "warn",
|
||||
"react/no-unescaped-entities": "warn",
|
||||
"no-irregular-whitespace": [
|
||||
"error",
|
||||
{
|
||||
"skipComments": true,
|
||||
"skipStrings": true,
|
||||
"skipTemplates": true,
|
||||
"skipRegExps": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -9,6 +9,15 @@
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
playwright/report/
|
||||
playwright/test-results/
|
||||
playwright/.cache/
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
@@ -34,3 +43,6 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Playwright
|
||||
node_modules/
|
||||
|
||||
48
.woodpecker.yml
Normal file
48
.woodpecker.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
when:
|
||||
- event: push
|
||||
- event: pull_request
|
||||
|
||||
steps:
|
||||
- name: clone
|
||||
image: woodpeckerci/plugin-git
|
||||
settings:
|
||||
depth: 0
|
||||
lfs: true
|
||||
submodules: true
|
||||
|
||||
- name: verify-mocks
|
||||
image: mcr.microsoft.com/playwright:v1.54.2-jammy
|
||||
commands:
|
||||
- pwd
|
||||
- node -v && npm -v
|
||||
- npm ci
|
||||
# Zeig mir, ob die Datei wirklich im Checkout liegt:
|
||||
- echo "=== git ls-files ==="
|
||||
- git ls-files | grep -i "^mocks/device-cgi-simulator/SERVICE/systemMockData.js" || true
|
||||
- echo "=== ls -la ==="
|
||||
- ls -la mocks/device-cgi-simulator/SERVICE || true
|
||||
- echo "=== file exists? ==="
|
||||
- test -f mocks/device-cgi-simulator/SERVICE/systemMockData.js && echo "FOUND" || (echo "MISSING" && exit 1)
|
||||
|
||||
- name: e2e-dev
|
||||
image: mcr.microsoft.com/playwright:v1.54.2-jammy
|
||||
environment:
|
||||
CI: "true"
|
||||
NODE_ENV: "development"
|
||||
NEXT_TELEMETRY_DISABLED: "1"
|
||||
PORT: "3000"
|
||||
commands:
|
||||
- npm ci
|
||||
- npx playwright test --project=chromium
|
||||
- name: notify-success
|
||||
image: alpine/curl:latest
|
||||
when:
|
||||
status: success
|
||||
commands:
|
||||
- curl -d "Tests erfolgreich in woodpecker" https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35
|
||||
- name: notify-failure
|
||||
image: alpine/curl:latest
|
||||
when:
|
||||
status: failure
|
||||
commands:
|
||||
- curl -d "Tests fehlgeschlagen in woodpecker" https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35
|
||||
2174
CHANGELOG.md
2174
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
BIN
Git 2.pptx
Normal file
BIN
Git 2.pptx
Normal file
Binary file not shown.
27
Jenkinsfile
vendored
Normal file
27
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
pipeline {
|
||||
agent any
|
||||
tools { nodejs 'node20' } // exakt der Name aus "Manage Jenkins → Tools"
|
||||
|
||||
stages {
|
||||
stage('Versions') {
|
||||
steps { sh 'node -v && npm -v' }
|
||||
}
|
||||
stage('Install deps') {
|
||||
steps { sh 'npm ci' }
|
||||
}
|
||||
stage('Playwright tests') {
|
||||
steps {
|
||||
sh 'npx playwright install' // Browser-Binärdateien laden
|
||||
sh 'npx playwright test'
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
success {
|
||||
sh 'curl -d "Tests erfolgreich in Jenkins" https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35'
|
||||
}
|
||||
failure {
|
||||
sh 'curl -d "Tests fehlgeschlagen in Jenkins" https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35'
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
Screenshot 2025-08-27 082443.png
Normal file
BIN
Screenshot 2025-08-27 082443.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
43
components/common/ConfirmModal.tsx
Normal file
43
components/common/ConfirmModal.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
// components/common/ConfirmModal.tsx
|
||||
import React from "react";
|
||||
|
||||
interface ConfirmModalProps {
|
||||
open: boolean;
|
||||
title?: string;
|
||||
message: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default function ConfirmModal({
|
||||
open,
|
||||
title,
|
||||
message,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ConfirmModalProps) {
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50">
|
||||
<div className="bg-white p-6 rounded shadow-xl w-[360px] max-w-full text-center">
|
||||
{title && <h2 className="text-lg font-semibold mb-3">{title}</h2>}
|
||||
<p className="mb-6 text-gray-800">{message}</p>
|
||||
<div className="flex justify-center gap-4">
|
||||
<button
|
||||
className="bg-gray-300 hover:bg-gray-400 text-black px-4 py-2 rounded"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Bestätigen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +1,27 @@
|
||||
// /components/main/kabelueberwachung/kue705FO/Charts/LoopMeasurementChart/DateRangePicker.tsx
|
||||
// /components/common/DateRangePicker.tsx
|
||||
import React, { useEffect } from "react";
|
||||
import DatePicker from "react-datepicker";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { RootState } from "@/redux/store";
|
||||
import {
|
||||
setVonDatum,
|
||||
setBisDatum,
|
||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
||||
import { setVonDatum, setBisDatum } from "@/redux/slices/dateRangePickerSlice";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
const DateRangePicker: React.FC = () => {
|
||||
interface DateRangePickerProps {
|
||||
compact?: boolean; // reduziert horizontale Breite
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DateRangePicker: React.FC<DateRangePickerProps> = ({
|
||||
compact = false,
|
||||
className = "",
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const reduxVonDatum = useSelector(
|
||||
(state: RootState) => state.kabelueberwachungChartSlice.vonDatum
|
||||
(state: RootState) => state.dateRangePicker.vonDatum
|
||||
);
|
||||
const reduxBisDatum = useSelector(
|
||||
(state: RootState) => state.kabelueberwachungChartSlice.bisDatum
|
||||
(state: RootState) => state.dateRangePicker.bisDatum
|
||||
);
|
||||
|
||||
const today = new Date();
|
||||
@@ -41,10 +46,22 @@ const DateRangePicker: React.FC = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch, reduxVonDatum, reduxBisDatum]);
|
||||
|
||||
const gapClass = compact ? "space-x-2" : "space-x-4";
|
||||
const labelWidthClass = compact ? "w-6" : "w-auto";
|
||||
const inputWidthClass = compact ? "w-32" : "w-44"; // ca. 128px vs 176px
|
||||
|
||||
return (
|
||||
<div className="flex space-x-4 items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="block text-sm font-semibold">Von</label>
|
||||
<div className={`flex ${gapClass} items-center ${className}`}>
|
||||
<div
|
||||
className={`flex items-center space-x-1 ${compact ? "text-xs" : ""}`}
|
||||
>
|
||||
<label
|
||||
className={`block font-semibold ${
|
||||
compact ? "text-xs" : "text-sm"
|
||||
} ${labelWidthClass}`}
|
||||
>
|
||||
Von
|
||||
</label>
|
||||
<DatePicker
|
||||
selected={reduxVonDatum ? parseISODate(reduxVonDatum) : thirtyDaysAgo}
|
||||
onChange={(date) => {
|
||||
@@ -60,12 +77,21 @@ const DateRangePicker: React.FC = () => {
|
||||
minDate={sixMonthsAgo}
|
||||
maxDate={today}
|
||||
dateFormat="dd.MM.yyyy"
|
||||
className="border px-2 py-1 rounded"
|
||||
className={`border px-2 py-1 rounded ${inputWidthClass} ${
|
||||
compact ? "text-xs" : "text-sm"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="block text-sm font-semibold">Bis</label>
|
||||
<div
|
||||
className={`flex items-center space-x-1 ${compact ? "text-xs" : ""}`}
|
||||
>
|
||||
<label
|
||||
className={`block font-semibold ${
|
||||
compact ? "text-xs" : "text-sm"
|
||||
} ${labelWidthClass}`}
|
||||
>
|
||||
Bis
|
||||
</label>
|
||||
<DatePicker
|
||||
selected={reduxBisDatum ? parseISODate(reduxBisDatum) : today}
|
||||
onChange={(date) => {
|
||||
@@ -81,7 +107,9 @@ const DateRangePicker: React.FC = () => {
|
||||
minDate={sixMonthsAgo}
|
||||
maxDate={today}
|
||||
dateFormat="dd.MM.yyyy"
|
||||
className="border px-2 py-1 rounded"
|
||||
className={`border px-2 py-1 rounded ${inputWidthClass} ${
|
||||
compact ? "text-xs" : "text-sm"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
55
components/common/DeviceEventsBridge.tsx
Normal file
55
components/common/DeviceEventsBridge.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useAppDispatch } from "@/redux/store";
|
||||
import { setEvents } from "@/redux/slices/deviceEventsSlice";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
loopMeasurementEvent?: number[];
|
||||
tdrMeasurementEvent?: number[];
|
||||
alignmentEvent?: number[];
|
||||
}
|
||||
}
|
||||
|
||||
const POLL_MS = 2000; // poll every 2 seconds
|
||||
|
||||
export default function DeviceEventsBridge() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
React.useEffect(() => {
|
||||
let lastSig = "";
|
||||
const readAndDispatch = () => {
|
||||
const ksx = Array.isArray(window.loopMeasurementEvent)
|
||||
? window.loopMeasurementEvent
|
||||
: undefined;
|
||||
const ksy = Array.isArray(window.tdrMeasurementEvent)
|
||||
? window.tdrMeasurementEvent
|
||||
: undefined;
|
||||
const ksz = Array.isArray(window.alignmentEvent)
|
||||
? window.alignmentEvent
|
||||
: undefined;
|
||||
// Build a stable signature of first 32 values per array
|
||||
const to32 = (a?: number[]) => {
|
||||
const out: number[] = [];
|
||||
if (Array.isArray(a)) {
|
||||
for (let i = 0; i < 32; i++) out.push(a[i] ? 1 : 0);
|
||||
} else {
|
||||
for (let i = 0; i < 32; i++) out.push(0);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const sig = `${to32(ksx).join("")}|${to32(ksy).join("")}|${to32(ksz).join(
|
||||
""
|
||||
)}`;
|
||||
if (sig !== lastSig) {
|
||||
lastSig = sig;
|
||||
dispatch(setEvents({ ksx, ksy, ksz }));
|
||||
}
|
||||
};
|
||||
readAndDispatch();
|
||||
const id = setInterval(readAndDispatch, POLL_MS);
|
||||
return () => clearInterval(id);
|
||||
}, [dispatch]);
|
||||
|
||||
return null;
|
||||
}
|
||||
132
components/common/GlobalActivityOverlay.tsx
Normal file
132
components/common/GlobalActivityOverlay.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useAppSelector } from "@/redux/store";
|
||||
|
||||
export default function GlobalActivityOverlay() {
|
||||
const anyLoop = useAppSelector((s) => s.deviceEvents.anyLoopActive);
|
||||
const anyTdr = useAppSelector((s) => s.deviceEvents.anyTdrActive);
|
||||
const anyAlign = useAppSelector((s) => s.deviceEvents.anyAlignmentActive);
|
||||
const ksx = useAppSelector((s) => s.deviceEvents.ksx);
|
||||
const ksy = useAppSelector((s) => s.deviceEvents.ksy);
|
||||
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
|
||||
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
|
||||
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
|
||||
const alignmentStartedAt = useAppSelector(
|
||||
(s) => s.deviceEvents.alignmentStartedAt
|
||||
);
|
||||
|
||||
const fmt = (arr: number[]) =>
|
||||
arr
|
||||
.map((v, i) => (v ? i + 1 : 0))
|
||||
.filter((n) => n !== 0)
|
||||
.join(", ");
|
||||
|
||||
// Simple 1s ticker so progress bars advance while overlay is shown
|
||||
const [now, setNow] = useState<number>(Date.now());
|
||||
useEffect(() => {
|
||||
const active = anyLoop || anyTdr || anyAlign;
|
||||
if (!active) return;
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [anyLoop, anyTdr, anyAlign]);
|
||||
|
||||
const active = anyLoop || anyTdr || anyAlign;
|
||||
if (!active) return null;
|
||||
|
||||
const clamp = (v: number, min = 0, max = 1) =>
|
||||
Math.max(min, Math.min(max, v));
|
||||
|
||||
const compute = (startedAt: number | null, durationMs: number) => {
|
||||
if (!startedAt) return { pct: 0, remaining: durationMs };
|
||||
const elapsed = now - startedAt;
|
||||
const pct = clamp(elapsed / durationMs) * 100;
|
||||
const remaining = Math.max(0, durationMs - Math.max(0, elapsed));
|
||||
return { pct, remaining };
|
||||
};
|
||||
|
||||
// Durations
|
||||
const LOOP_MS = 2 * 60 * 1000; // ~2 min
|
||||
const TDR_MS = 30 * 1000; // ~30 s
|
||||
const ALIGN_MS = 10 * 60 * 1000; // ~10 min
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[2000] flex items-center justify-center bg-white/70 backdrop-blur-sm">
|
||||
<div className="p-4 rounded-md shadow bg-white border border-gray-200 w-[min(90vw,680px)]">
|
||||
<div className="font-semibold mb-3">Bitte warten…</div>
|
||||
<div className="space-y-3">
|
||||
{anyLoop && (
|
||||
<div>
|
||||
<div className="text-sm text-gray-800 mb-1">
|
||||
Schleifenmessung läuft… (KÜ: {fmt(ksx)})
|
||||
</div>
|
||||
{(() => {
|
||||
const { pct } = compute(loopStartedAt, LOOP_MS);
|
||||
return (
|
||||
<div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-littwin-blue transition-all"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 mt-1">
|
||||
{Math.round(pct)}%
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{anyTdr && (
|
||||
<div>
|
||||
<div className="text-sm text-gray-800 mb-1">
|
||||
TDR-Messung läuft… (KÜ: {fmt(ksy)})
|
||||
</div>
|
||||
{(() => {
|
||||
const { pct } = compute(tdrStartedAt, TDR_MS);
|
||||
return (
|
||||
<div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-littwin-blue transition-all"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 mt-1">
|
||||
{Math.round(pct)}%
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{anyAlign && (
|
||||
<div>
|
||||
<div className="text-sm text-gray-800 mb-1">
|
||||
Abgleich läuft… (KÜ: {fmt(ksz)}) kann bis zu 10 Minuten dauern
|
||||
</div>
|
||||
{(() => {
|
||||
const { pct } = compute(alignmentStartedAt, ALIGN_MS);
|
||||
return (
|
||||
<div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-littwin-blue transition-all"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 mt-1">
|
||||
{Math.round(pct)}%
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"; // components/Header.jsx
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Icon } from "@iconify/react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
@@ -56,8 +57,21 @@ function Header() {
|
||||
}, [deviceName, dispatch]);
|
||||
//----------------------------------------------------------------
|
||||
|
||||
// Dark/Light Mode Toggle
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const html = document.documentElement;
|
||||
if (isDark) {
|
||||
html.classList.add("dark");
|
||||
} else {
|
||||
html.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
}, [isDark]);
|
||||
|
||||
return (
|
||||
<header className="bg-gray-300 flex justify-between items-center w-full h-[13vh] laptop:h-[10vh] relative text-black ">
|
||||
<header className="bg-gray-300 dark:bg-gray-800 flex justify-between items-center w-full h-[13vh] laptop:h-[10vh] relative text-black dark:text-white ">
|
||||
<div
|
||||
className="absolute transform -translate-y-1/2
|
||||
left-[8%] sm:left-[8%] md:left-[8%] lg:left-[8%] xl:left-[6%] 2xl:left-[2%] laptop:left-[4%] laptop:
|
||||
@@ -88,38 +102,49 @@ function Header() {
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col leading-tight whitespace-nowrap">
|
||||
<h2 className="text-xl laptop:text-base xl:text-lg font-bold">
|
||||
<h2 className="text-xl laptop:text-base xl:text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
Meldestation
|
||||
</h2>
|
||||
<p className="text-gray-600 text-lg laptop:text-sm xl:text-base truncate max-w-[20vw]">
|
||||
<p className="text-gray-600 dark:text-gray-300 text-lg laptop:text-sm xl:text-base truncate max-w-[20vw]">
|
||||
{deviceName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 w-full lg:w-full flex flex-row gap-4 justify-between">
|
||||
<div className="flex items-center justify-end w-full">
|
||||
{/* Admin-Login */}
|
||||
{/*
|
||||
<div className="flex items-center justify-end w-full gap-4">
|
||||
{/* Dark/Light Mode Toggle */}
|
||||
<button
|
||||
onClick={handleSettingsClick}
|
||||
className="text-3xl text-black mr-0"
|
||||
aria-label={isDark ? "Light Mode" : "Dark Mode"}
|
||||
onClick={() => setIsDark((d) => !d)}
|
||||
className="rounded-full p-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition"
|
||||
title={isDark ? "Light Mode" : "Dark Mode"}
|
||||
>
|
||||
<i className="bi bi-gear"></i>
|
||||
</button>
|
||||
*/}
|
||||
</div>
|
||||
|
||||
{/* Logout-Button */}
|
||||
|
||||
<div className="flex items-center justify-end w-1/4 space-x-1">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded"
|
||||
>
|
||||
Abmelden
|
||||
{isDark ? (
|
||||
<Icon
|
||||
icon="mdi:weather-night"
|
||||
className="text-xl text-yellow-300"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
icon="mdi:white-balance-sunny"
|
||||
className="text-xl text-yellow-500"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Logout-Button - nur anzeigen wenn Admin eingeloggt ist */}
|
||||
{isAdminLoggedIn && (
|
||||
<div className="flex items-center justify-end w-1/4 space-x-1">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded"
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Warnhinweis, wenn der Admin angemeldet ist */}
|
||||
|
||||
@@ -12,7 +12,7 @@ const handleClearDatabase = async () => {
|
||||
}
|
||||
|
||||
// Full URL with host, current path, and clear database command
|
||||
const url = `${window.location.origin}/CPL?${currentPath}&DEDB=1`;
|
||||
const url = `${window.location.origin}/CPL?/${window.location.pathname}&DEDB=1`;
|
||||
|
||||
// Log the full URL to the console for debugging
|
||||
console.log(url);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
"use client";
|
||||
type AnalogInput = {
|
||||
id: number;
|
||||
label: string;
|
||||
unit: string;
|
||||
};
|
||||
import React, { useEffect } from "react";
|
||||
// components/main/analogInputs/AnalogInputsChart.tsx
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState, AppDispatch } from "@/redux/store";
|
||||
import { Dialog } from "@headlessui/react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { getColor } from "@/utils/colors";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
LineElement,
|
||||
@@ -17,14 +15,23 @@ import {
|
||||
Legend,
|
||||
Filler,
|
||||
TimeScale,
|
||||
TooltipItem,
|
||||
} from "chart.js";
|
||||
import "chartjs-adapter-date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import type { RootState, AppDispatch } from "../../../redux/store";
|
||||
import { Listbox } from "@headlessui/react";
|
||||
import { getAnalogInputsHistoryThunk } from "@/redux/thunks/getAnalogInputsHistoryThunk";
|
||||
import {
|
||||
setVonDatum,
|
||||
setBisDatum,
|
||||
setZeitraum,
|
||||
setAutoLoad,
|
||||
} from "@/redux/slices/analogInputs/analogInputsHistorySlice";
|
||||
import { getColor } from "@/utils/colors";
|
||||
import AnalogInputsDatePicker from "./AnalogInputsDatePicker";
|
||||
import type { ChartJSOrUndefined } from "react-chartjs-2/dist/types";
|
||||
|
||||
// Basis-Registrierung (ohne Zoom-Plugin)
|
||||
// ✅ Nur die Basis-ChartJS-Module registrieren
|
||||
ChartJS.register(
|
||||
LineElement,
|
||||
PointElement,
|
||||
@@ -37,148 +44,404 @@ ChartJS.register(
|
||||
);
|
||||
|
||||
export default function AnalogInputsChart({
|
||||
selectedId,
|
||||
setLoading,
|
||||
loading,
|
||||
}: {
|
||||
selectedId: number | null;
|
||||
setLoading: (loading: boolean) => void;
|
||||
loading: boolean;
|
||||
}) {
|
||||
const selectedInput = useSelector(
|
||||
(state: RootState) => state.selectedAnalogInput
|
||||
) as unknown as AnalogInput | null;
|
||||
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
type AnalogInputHistoryPoint = { t: string | number | Date; m: number };
|
||||
|
||||
const { data } = useSelector(
|
||||
(state: RootState) => state.analogInputsHistory
|
||||
) as {
|
||||
data: { [key: string]: AnalogInputHistoryPoint[] };
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getAnalogInputsHistoryThunk());
|
||||
}, [dispatch]);
|
||||
|
||||
// ✅ Zoom-Plugin dynamisch importieren und registrieren
|
||||
useEffect(() => {
|
||||
const loadZoomPlugin = async () => {
|
||||
if (typeof window !== "undefined") {
|
||||
const zoomPlugin = (await import("chartjs-plugin-zoom")).default;
|
||||
if (typeof window !== "undefined") {
|
||||
import("chartjs-plugin-zoom").then((zoom) => {
|
||||
if (!ChartJS.registry.plugins.get("zoom")) {
|
||||
ChartJS.register(zoomPlugin);
|
||||
ChartJS.register(zoom.default);
|
||||
}
|
||||
}
|
||||
};
|
||||
loadZoomPlugin();
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!selectedId) {
|
||||
return (
|
||||
<div className="text-gray-500">Bitte einen Messwerteingang auswählen</div>
|
||||
);
|
||||
}
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
const key = String(selectedId + 99);
|
||||
const inputData = data[key];
|
||||
const chartRef =
|
||||
useRef<
|
||||
ChartJSOrUndefined<"line", { x: Date; y: number | undefined }[], unknown>
|
||||
>(null);
|
||||
|
||||
if (!inputData) {
|
||||
return (
|
||||
<div className="text-red-500">
|
||||
Keine Verlaufsdaten für Messwerteingang {selectedId} gefunden.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Redux Werte für Chart-Daten
|
||||
const { zeitraum, vonDatum, bisDatum, data, autoLoad, selectedId } =
|
||||
useSelector((state: RootState) => state.analogInputsHistory);
|
||||
const selectedAnalogInput = useSelector(
|
||||
(state: RootState) => state.selectedAnalogInput
|
||||
);
|
||||
|
||||
const chartData = {
|
||||
datasets: [
|
||||
{
|
||||
label: `Messkurve ${selectedInput?.label ?? "Eingang"} [${
|
||||
selectedInput?.unit ?? ""
|
||||
}]`,
|
||||
data: inputData.map((point: AnalogInputHistoryPoint) => ({
|
||||
x: point.t,
|
||||
y: point.m,
|
||||
})),
|
||||
fill: false,
|
||||
borderColor: getColor("littwin-blue"),
|
||||
backgroundColor: "rgba(59,130,246,0.5)",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 10,
|
||||
tension: 0.1,
|
||||
},
|
||||
],
|
||||
// Redux initiale Datum-Werte
|
||||
const vonDatumRedux = useSelector(
|
||||
(state: RootState) => state.dateRangePicker.vonDatum
|
||||
);
|
||||
const bisDatumRedux = useSelector(
|
||||
(state: RootState) => state.dateRangePicker.bisDatum
|
||||
);
|
||||
|
||||
// Hilfsfunktion für Default-Datum
|
||||
const getDefaultDate = (type: "from" | "to") => {
|
||||
const today = new Date();
|
||||
if (type === "to") return today.toISOString().slice(0, 10);
|
||||
const fromDateObj = new Date(today);
|
||||
fromDateObj.setDate(today.getDate() - 30);
|
||||
return fromDateObj.toISOString().slice(0, 10);
|
||||
};
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { position: "top" as const },
|
||||
tooltip: {
|
||||
mode: "index" as const,
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
label: function (context: import("chart.js").TooltipItem<"line">) {
|
||||
const y = context.parsed.y;
|
||||
return `Messwert: ${y}`;
|
||||
},
|
||||
title: function (
|
||||
tooltipItems: import("chart.js").TooltipItem<"line">[]
|
||||
) {
|
||||
const date = tooltipItems[0].parsed.x;
|
||||
return `Zeitpunkt: ${new Date(date).toLocaleString("de-DE")}`;
|
||||
// ✅ Lokale States für Picker + Zeitraum
|
||||
const [localVonDatum, setLocalVonDatum] = React.useState(
|
||||
vonDatumRedux || getDefaultDate("from")
|
||||
);
|
||||
const [localBisDatum, setLocalBisDatum] = React.useState(
|
||||
bisDatumRedux || getDefaultDate("to")
|
||||
);
|
||||
const [localZeitraum, setLocalZeitraum] = React.useState(zeitraum);
|
||||
|
||||
// Synchronisiere lokale Werte mit Redux (z.B. nach AutoLoad Reset)
|
||||
useEffect(() => {
|
||||
setLocalVonDatum(vonDatumRedux || getDefaultDate("from"));
|
||||
setLocalBisDatum(bisDatumRedux || getDefaultDate("to"));
|
||||
setLocalZeitraum(zeitraum);
|
||||
}, [vonDatumRedux, bisDatumRedux, zeitraum]);
|
||||
|
||||
// Initiale Default-Werte: 30 Tage zurück (nur wenn Redux-Werte fehlen)
|
||||
useEffect(() => {
|
||||
if (!vonDatumRedux || !bisDatumRedux) {
|
||||
const today = new Date();
|
||||
const toDate = today.toISOString().slice(0, 10);
|
||||
const fromDateObj = new Date(today);
|
||||
fromDateObj.setDate(today.getDate() - 30);
|
||||
const fromDate = fromDateObj.toISOString().slice(0, 10);
|
||||
setLocalVonDatum(fromDate);
|
||||
setLocalBisDatum(toDate);
|
||||
}
|
||||
}, [vonDatumRedux, bisDatumRedux]);
|
||||
|
||||
// ✅ Nur lokale Änderung beim Picker
|
||||
const handleDateChange = (from: string, to: string) => {
|
||||
setLocalVonDatum(from);
|
||||
setLocalBisDatum(to);
|
||||
};
|
||||
|
||||
// ✅ Button → Redux + Fetch triggern
|
||||
const handleFetchData = () => {
|
||||
if (!selectedAnalogInput?.id) return;
|
||||
|
||||
setLoading(true); // Set loading to true when fetching data
|
||||
|
||||
// Fallback auf Redux-Werte, falls lokale Werte leer sind
|
||||
const from = localVonDatum || vonDatumRedux || "";
|
||||
const to = localBisDatum || bisDatumRedux || "";
|
||||
|
||||
// Redux aktualisieren
|
||||
dispatch(setVonDatum(from));
|
||||
dispatch(setBisDatum(to));
|
||||
dispatch(setZeitraum(localZeitraum));
|
||||
|
||||
// Umgebung erkennen und URL generieren
|
||||
const isDev =
|
||||
window.location.hostname === "localhost" ||
|
||||
window.location.hostname === "127.0.0.1";
|
||||
let fetchUrl = "";
|
||||
if (isDev) {
|
||||
fetchUrl = `/api/cpl/getAnalogInputsHistory?eingang=${selectedAnalogInput.id}&zeitraum=${localZeitraum}&von=${from}&bis=${to}`;
|
||||
} else {
|
||||
// Produktion: CPL-Webserver direkt abfragen
|
||||
const [vonJahr, vonMonat, vonTag] = from.split("-");
|
||||
const [bisJahr, bisMonat, bisTag] = to.split("-");
|
||||
const aeEingang = 100 + (selectedAnalogInput.id - 1);
|
||||
let diaType = "DIA1";
|
||||
if (localZeitraum === "DIA0") diaType = "DIA0";
|
||||
if (localZeitraum === "DIA2") diaType = "DIA2";
|
||||
fetchUrl = `${window.location.origin}/CPL?seite.ACP&${diaType}=${vonJahr};${vonMonat};${vonTag};${bisJahr};${bisMonat};${bisTag};${aeEingang};1`;
|
||||
}
|
||||
console.log("Fetch-URL:", fetchUrl);
|
||||
|
||||
// Thunk-Fetch mit neuen Werten
|
||||
dispatch(
|
||||
getAnalogInputsHistoryThunk({
|
||||
eingang: selectedAnalogInput.id,
|
||||
zeitraum: localZeitraum,
|
||||
vonDatum: from,
|
||||
bisDatum: to,
|
||||
})
|
||||
).finally(() => setLoading(false)); // Reset loading after fetch
|
||||
};
|
||||
|
||||
// Auto-trigger fetch when a row is selected and id is not 0 (only once per selection)
|
||||
React.useEffect(() => {
|
||||
if (selectedAnalogInput?.id && selectedAnalogInput.id !== 0) {
|
||||
handleFetchData();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedAnalogInput?.id]);
|
||||
|
||||
// ✅ Chart-Daten aus Redux filtern (Chart reagiert nur nach Button)
|
||||
const chartKey = selectedAnalogInput?.id
|
||||
? String(selectedAnalogInput.id + 99)
|
||||
: null;
|
||||
const inputData = chartKey ? data[chartKey] ?? [] : [];
|
||||
|
||||
const filteredData = inputData.filter((point) => {
|
||||
const date = new Date(point.t);
|
||||
const from = vonDatumRedux ? new Date(vonDatumRedux) : null;
|
||||
const to = bisDatumRedux ? new Date(bisDatumRedux) : null;
|
||||
return (!from || date >= from) && (!to || date <= to);
|
||||
});
|
||||
|
||||
const memoizedChartData = React.useMemo(() => {
|
||||
return {
|
||||
datasets:
|
||||
filteredData.length > 0
|
||||
? zeitraum === "DIA0"
|
||||
? [
|
||||
{
|
||||
label: "Messwert Minimum ", // (i)
|
||||
data: filteredData
|
||||
.filter((p) => typeof p.i === "number")
|
||||
.map((p) => ({ x: new Date(p.t), y: p.i })),
|
||||
borderColor: "gray",
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
label: selectedAnalogInput?.label
|
||||
? //? `Messwert ${selectedAnalogInput.label}` // (m)
|
||||
`Messwert ` // (m)
|
||||
: "Messwert ", // (m)
|
||||
data: filteredData
|
||||
.filter((p) => typeof p.m === "number")
|
||||
.map((p) => ({ x: new Date(p.t), y: p.m })),
|
||||
borderColor: getColor("littwin-blue"),
|
||||
backgroundColor: "rgba(59,130,246,0.3)",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
label: "Messwert Maximum ", // (a)
|
||||
data: filteredData
|
||||
.filter((p) => typeof p.a === "number")
|
||||
.map((p) => ({ x: new Date(p.t), y: p.a })),
|
||||
borderColor: "gray",
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
order: 3,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: "Messwert Minimum", // (i)
|
||||
data: filteredData
|
||||
.filter((p) => typeof p.i === "number")
|
||||
.map((p) => ({ x: new Date(p.t), y: p.i })),
|
||||
borderColor: "gray",
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
label: "Durchschnitt", // (g)
|
||||
data: filteredData
|
||||
.filter((p) => typeof p.g === "number")
|
||||
.map((p) => ({ x: new Date(p.t), y: p.g })),
|
||||
borderColor: getColor("littwin-blue"),
|
||||
backgroundColor: "rgba(59,130,246,0.3)",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
label: "Messwert Maximum", // (a)
|
||||
data: filteredData
|
||||
.filter((p) => typeof p.a === "number")
|
||||
.map((p) => ({ x: new Date(p.t), y: p.a })),
|
||||
borderColor: "gray",
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
order: 3,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
}, [filteredData, zeitraum, selectedAnalogInput]);
|
||||
|
||||
const memoizedChartOptions = React.useMemo(() => {
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: "top" as const },
|
||||
tooltip: {
|
||||
mode: "index" as const,
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
label: (context: TooltipItem<"line">) => {
|
||||
const label = context.dataset.label || "";
|
||||
return `${label}: ${context.parsed.y}`;
|
||||
},
|
||||
title: (items: TooltipItem<"line">[]) => {
|
||||
const date = items[0].parsed.x;
|
||||
return `Zeitpunkt: ${new Date(date).toLocaleString("de-DE")}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
title: {
|
||||
display: true,
|
||||
text: `Verlauf der letzten 30 Tage`,
|
||||
},
|
||||
zoom: {
|
||||
pan: {
|
||||
enabled: true,
|
||||
mode: "x" as const,
|
||||
title: {
|
||||
display: true,
|
||||
text: selectedAnalogInput?.label
|
||||
? `Verlauf: ${selectedAnalogInput.label}`
|
||||
: "Messwert-Verlauf",
|
||||
},
|
||||
zoom: {
|
||||
wheel: { enabled: true },
|
||||
pinch: { enabled: true },
|
||||
mode: "x" as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: "day" as const, // nur Datum in Achse
|
||||
tooltipFormat: "dd.MM.yyyy HH:mm", // aber Uhrzeit im Tooltip sichtbar
|
||||
displayFormats: {
|
||||
day: "dd.MM.yyyy",
|
||||
pan: {
|
||||
enabled: true,
|
||||
mode: "x" as const,
|
||||
},
|
||||
zoom: {
|
||||
wheel: { enabled: true },
|
||||
pinch: { enabled: true },
|
||||
mode: "x" as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: "day" as const,
|
||||
tooltipFormat: "dd.MM.yyyy HH:mm",
|
||||
displayFormats: {
|
||||
day: "dd.MM.yyyy",
|
||||
},
|
||||
},
|
||||
adapters: { date: { locale: de } },
|
||||
title: { display: true, text: "Zeit" },
|
||||
min: vonDatum ? new Date(vonDatum).getTime() : undefined,
|
||||
max: bisDatum ? new Date(bisDatum).getTime() : undefined,
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: `Messwert ${selectedAnalogInput?.unit || ""}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}, [vonDatum, bisDatum, selectedAnalogInput]);
|
||||
|
||||
adapters: {
|
||||
date: {
|
||||
locale: de,
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Zeit",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: `Messwert [${selectedInput?.unit ?? ""}]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
// ✅ AutoLoad nur beim ersten Laden
|
||||
useEffect(() => {
|
||||
if (autoLoad && selectedId) {
|
||||
dispatch(
|
||||
getAnalogInputsHistoryThunk({
|
||||
eingang: selectedId,
|
||||
zeitraum,
|
||||
vonDatum,
|
||||
bisDatum,
|
||||
})
|
||||
);
|
||||
dispatch(setAutoLoad(false));
|
||||
}
|
||||
}, [autoLoad, selectedId, dispatch, zeitraum, vonDatum, bisDatum]);
|
||||
|
||||
// Dynamisches Importieren von chartjs-plugin-zoom nur im Browser
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
import("chartjs-plugin-zoom").then((module) => {
|
||||
ChartJS.register(module.default);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<Line data={chartData} options={chartOptions} />
|
||||
<div
|
||||
className={`flex flex-col gap-2 h-full ${loading ? "cursor-wait" : ""}`}
|
||||
>
|
||||
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-2">
|
||||
<div className="flex justify-start">
|
||||
<Dialog.Title className="text-lg font-semibold text-gray-700">
|
||||
Eingang {selectedId ?? "–"}
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<div className="flex flex-wrap items-center gap-4 mb-2">
|
||||
{/* ✅ Neuer DatePicker mit schönem Styling (lokal, ohne Redux) */}
|
||||
<AnalogInputsDatePicker
|
||||
from={localVonDatum}
|
||||
to={localBisDatum}
|
||||
onChange={handleDateChange}
|
||||
/>
|
||||
|
||||
{/* ✅ Zeitraum-Auswahl (Listbox nur lokal) */}
|
||||
<Listbox value={localZeitraum} onChange={setLocalZeitraum}>
|
||||
<div className="relative w-48">
|
||||
<Listbox.Button className="w-full border px-3 py-1 rounded bg-white flex justify-between items-center text-sm">
|
||||
<span>
|
||||
{localZeitraum === "DIA0"
|
||||
? "Alle Messwerte"
|
||||
: localZeitraum === "DIA1"
|
||||
? "Stündlich"
|
||||
: "Täglich"}
|
||||
</span>
|
||||
<i className="bi bi-chevron-down text-gray-400" />
|
||||
</Listbox.Button>
|
||||
<Listbox.Options className="absolute z-10 mt-1 w-full border bg-white shadow rounded text-sm">
|
||||
{["DIA0", "DIA1", "DIA2"].map((option) => (
|
||||
<Listbox.Option
|
||||
key={option}
|
||||
value={option}
|
||||
className="px-4 py-1 cursor-pointer hover:bg-gray-200"
|
||||
>
|
||||
{option === "DIA0"
|
||||
? "Alle Messwerte"
|
||||
: option === "DIA1"
|
||||
? "Stündlich"
|
||||
: "Täglich"}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</div>
|
||||
</Listbox>
|
||||
|
||||
{/* ✅ Button: lädt die Daten & aktualisiert Redux */}
|
||||
<button
|
||||
onClick={handleFetchData}
|
||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm"
|
||||
>
|
||||
Daten laden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart-Anzeige */}
|
||||
<div className="flex-1 min-h-0 w-full">
|
||||
{!selectedAnalogInput?.id ? (
|
||||
<div className="flex items-center justify-center h-full text-gray-500 text-lg gap-2">
|
||||
<i className="bi bi-info-circle text-2xl mr-2" />
|
||||
<span>
|
||||
Bitte wählen Sie einen Eingang aus, um die Messkurve anzuzeigen
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<Line
|
||||
ref={chartRef}
|
||||
data={memoizedChartData}
|
||||
options={memoizedChartOptions}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
120
components/main/analogInputs/AnalogInputsChartModal.tsx
Normal file
120
components/main/analogInputs/AnalogInputsChartModal.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Dialog } from "@headlessui/react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { RootState } from "@/redux/store";
|
||||
import { setIsChartModalOpen } from "@/redux/slices/analogInputs/analogInputsUiSlice";
|
||||
import AnalogInputsChart from "@/components/main/analogInputs/AnalogInputsChart";
|
||||
|
||||
export default function AnalogInputsChartModal({
|
||||
loading,
|
||||
setLoading,
|
||||
}: {
|
||||
loading: boolean;
|
||||
setLoading: (v: boolean) => void;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const isOpen = useSelector(
|
||||
(state: RootState) => state.analogInputsUi.isChartModalOpen
|
||||
);
|
||||
const selectedId = useSelector(
|
||||
(state: RootState) => state.analogInputsHistory.selectedId
|
||||
);
|
||||
const [isFullscreen, setIsFullscreen] = React.useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={() => dispatch(setIsChartModalOpen(false))}
|
||||
className="relative z-[9999]"
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div className="fixed inset-0 bg-black/50" aria-hidden="true" />
|
||||
{/* Centered panel */}
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<Dialog.Panel className="relative">
|
||||
<div
|
||||
className="bg-white rounded-xl shadow-xl border border-gray-200"
|
||||
style={{
|
||||
width: isFullscreen ? "90vw" : "70rem",
|
||||
height: isFullscreen ? "90vh" : "35rem",
|
||||
padding: "1rem",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* Controls top-right (fullscreen + close) */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "0.625rem",
|
||||
right: "0.625rem",
|
||||
display: "flex",
|
||||
gap: "0.75rem",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsFullscreen((v) => !v)}
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
fontSize: "1.5rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
title={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
|
||||
aria-label={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
|
||||
>
|
||||
<i
|
||||
className={
|
||||
isFullscreen
|
||||
? "bi bi-fullscreen-exit"
|
||||
: "bi bi-arrows-fullscreen"
|
||||
}
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => dispatch(setIsChartModalOpen(false))}
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
fontSize: "1.5rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
title="Schließen"
|
||||
aria-label="Modal schließen"
|
||||
>
|
||||
<i className="bi bi-x-circle-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Title row (align like IsoChartView) */}
|
||||
<div className="flex justify-between items-center mb-2 pr-24">
|
||||
<Dialog.Title className="text-lg font-semibold text-gray-700">
|
||||
Messkurve Messwerteingang {selectedId ?? "–"}
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
|
||||
{/* Chart container (structure similar to IsoChartView) */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "90%",
|
||||
}}
|
||||
>
|
||||
{/* Optional: place an action bar here if needed */}
|
||||
<div style={{ flex: 1, height: "90%" }}>
|
||||
<AnalogInputsChart loading={loading} setLoading={setLoading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
93
components/main/analogInputs/AnalogInputsDatePicker.tsx
Normal file
93
components/main/analogInputs/AnalogInputsDatePicker.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
// components/main/analogInputs/AnalogInputsDatePicker.tsx
|
||||
import React, { useEffect, useState } from "react";
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
type Props = {
|
||||
from: string;
|
||||
to: string;
|
||||
onChange: (from: string, to: string) => void;
|
||||
};
|
||||
|
||||
export default function AnalogInputsDatePicker({ from, to, onChange }: Props) {
|
||||
const today = new Date();
|
||||
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||
|
||||
const sixMonthsAgo = new Date();
|
||||
sixMonthsAgo.setMonth(today.getMonth() - 6);
|
||||
|
||||
// interne Date-Objekte für react-datepicker
|
||||
const parseISO = (dateStr: string) => {
|
||||
if (!dateStr) return null;
|
||||
const [year, month, day] = dateStr.split("-").map(Number);
|
||||
return new Date(year, month - 1, day);
|
||||
};
|
||||
|
||||
const formatISO = (date: Date) => date.toLocaleDateString("sv-SE"); // yyyy-MM-dd
|
||||
|
||||
const [localFromDate, setLocalFromDate] = useState<Date | null>(
|
||||
from ? parseISO(from) : thirtyDaysAgo
|
||||
);
|
||||
const [localToDate, setLocalToDate] = useState<Date | null>(
|
||||
to ? parseISO(to) : today
|
||||
);
|
||||
|
||||
// Wenn Props von außen kommen (z.B. Reset), synchronisieren
|
||||
useEffect(() => {
|
||||
if (from) setLocalFromDate(parseISO(from));
|
||||
if (to) setLocalToDate(parseISO(to));
|
||||
}, [from, to]);
|
||||
|
||||
const handleFromChange = (date: Date | null) => {
|
||||
setLocalFromDate(date);
|
||||
if (date && localToDate) {
|
||||
onChange(formatISO(date), formatISO(localToDate));
|
||||
}
|
||||
};
|
||||
|
||||
const handleToChange = (date: Date | null) => {
|
||||
setLocalToDate(date);
|
||||
if (localFromDate && date) {
|
||||
onChange(formatISO(localFromDate), formatISO(date));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex space-x-4 items-center">
|
||||
{/* Von */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="block text-sm font-semibold">Von</label>
|
||||
<DatePicker
|
||||
selected={localFromDate}
|
||||
onChange={handleFromChange}
|
||||
selectsStart
|
||||
startDate={localFromDate}
|
||||
endDate={localToDate}
|
||||
minDate={sixMonthsAgo}
|
||||
maxDate={today}
|
||||
dateFormat="dd.MM.yyyy"
|
||||
className="border px-2 py-1 rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bis */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="block text-sm font-semibold">Bis</label>
|
||||
<DatePicker
|
||||
selected={localToDate}
|
||||
onChange={handleToChange}
|
||||
selectsEnd
|
||||
startDate={localFromDate}
|
||||
endDate={localToDate}
|
||||
minDate={sixMonthsAgo}
|
||||
maxDate={today}
|
||||
dateFormat="dd.MM.yyyy"
|
||||
className="border px-2 py-1 rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,24 @@
|
||||
"use client"; // /components/main/analogeEingaenge/AnalogInputsSettingsModal.tsx
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Listbox } from "@headlessui/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "@/redux/store";
|
||||
import { setIsSettingsModalOpen } from "@/redux/slices/analogInputs/analogInputsUiSlice";
|
||||
|
||||
interface AnalogInput {
|
||||
id: number;
|
||||
label?: string;
|
||||
offset?: number | string;
|
||||
factor?: number | string;
|
||||
loggerInterval: string;
|
||||
unit?: string;
|
||||
}
|
||||
import type { AnalogInput } from "@/types/analogInput"; // 👈 Importiere den Typ (jetzt definiert und exportiert)
|
||||
|
||||
interface Props {
|
||||
selectedInput: AnalogInput;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
export default function AnalogInputsSettingsModal() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isOpen = useSelector(
|
||||
(state: RootState) => state.analogInputsUi.isSettingsModalOpen
|
||||
);
|
||||
|
||||
const selectedInput = useSelector<RootState, AnalogInput | null>(
|
||||
(state) => state.selectedAnalogInput
|
||||
);
|
||||
|
||||
export default function AnalogInputSettingsModal({
|
||||
selectedInput,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: Props) {
|
||||
const [label, setLabel] = useState("");
|
||||
const [offset, setOffset] = useState("0.000");
|
||||
const [factor, setFactor] = useState("1.000");
|
||||
@@ -28,6 +26,8 @@ export default function AnalogInputSettingsModal({
|
||||
const [unit, setUnit] = useState("V");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const unitOptions = ["V", "mA", "°C", "bar", "%"];
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedInput && isOpen) {
|
||||
setLabel(selectedInput.label || "");
|
||||
@@ -41,12 +41,8 @@ export default function AnalogInputSettingsModal({
|
||||
? selectedInput.factor.toFixed(3)
|
||||
: selectedInput.factor || "1.000"
|
||||
);
|
||||
setLoggerInterval(selectedInput.loggerInterval);
|
||||
setLoggerInterval(selectedInput.loggerInterval || "9");
|
||||
setUnit(selectedInput.unit || "V");
|
||||
console.log(
|
||||
"selectedInput in analoge Eingänge:",
|
||||
selectedInput.loggerInterval
|
||||
);
|
||||
}
|
||||
}, [selectedInput, isOpen]);
|
||||
|
||||
@@ -54,6 +50,7 @@ export default function AnalogInputSettingsModal({
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
|
||||
const slot = selectedInput.id;
|
||||
const isDev = window.location.hostname === "localhost";
|
||||
|
||||
@@ -99,7 +96,7 @@ export default function AnalogInputSettingsModal({
|
||||
alert("Einstellungen gespeichert (Produktion).");
|
||||
}
|
||||
|
||||
onClose();
|
||||
dispatch(setIsSettingsModalOpen(false));
|
||||
location.reload();
|
||||
} catch (err) {
|
||||
alert("Fehler beim Speichern.");
|
||||
@@ -117,7 +114,7 @@ export default function AnalogInputSettingsModal({
|
||||
Einstellungen Messwerteingang {selectedInput.id}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
onClick={() => dispatch(setIsSettingsModalOpen(false))}
|
||||
className="text-2xl hover:text-gray-400"
|
||||
aria-label="Modal schließen"
|
||||
>
|
||||
@@ -127,69 +124,85 @@ export default function AnalogInputSettingsModal({
|
||||
|
||||
{/* Bezeichnung */}
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||
<div>
|
||||
<span className="font-normal">Bezeichnung:</span>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full border rounded px-3 py-1 mb-4"
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-normal">Bezeichnung:</span>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full border rounded px-3 py-1 mb-4"
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Offset */}
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4">
|
||||
<div>
|
||||
<span className="font-normal">Offset:</span>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="number"
|
||||
step="0.001"
|
||||
className="border border-gray-300 rounded px-2 py-1 w-full text-right "
|
||||
value={offset}
|
||||
onChange={(e) => setOffset(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-normal">Offset:</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.001"
|
||||
className="border border-gray-300 rounded px-2 py-1 w-full text-right"
|
||||
value={offset}
|
||||
onChange={(e) => setOffset(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Faktor */}
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4">
|
||||
<div>
|
||||
<span className="font-normal">Faktor:</span>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="number"
|
||||
step="0.001"
|
||||
className="border border-gray-300 rounded px-2 py-1 w-full text-right"
|
||||
value={factor}
|
||||
onChange={(e) => setFactor(e.target.value)}
|
||||
/>{" "}
|
||||
</div>
|
||||
<span className="font-normal">Faktor:</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.001"
|
||||
className="border border-gray-300 rounded px-2 py-1 w-full text-right"
|
||||
value={factor}
|
||||
onChange={(e) => setFactor(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* Einheit */}
|
||||
|
||||
{/* Einheit */}
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4">
|
||||
<span className="font-normal">Einheit:</span>
|
||||
<Listbox value={unit} onChange={setUnit}>
|
||||
<div className="relative w-full">
|
||||
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm text-gray-900 font-sans">
|
||||
<span>{unit}</span>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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 text-gray-900 font-sans">
|
||||
{unitOptions.map((opt) => (
|
||||
<Listbox.Option
|
||||
key={opt}
|
||||
value={opt}
|
||||
className={({ selected, active }) =>
|
||||
`px-4 py-1 cursor-pointer ${
|
||||
selected
|
||||
? "bg-littwin-blue text-white font-medium"
|
||||
: active
|
||||
? "bg-gray-200"
|
||||
: "text-gray-900"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{opt}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
|
||||
{/* Speicherintervall */}
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||
<div>
|
||||
<span className="font-normal">Einheit:</span>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
className="w-full border rounded px-3 py-1 mb-4"
|
||||
value={unit}
|
||||
onChange={(e) => setUnit(e.target.value)}
|
||||
>
|
||||
<option value="V">V</option>
|
||||
<option value="mA">mA</option>
|
||||
<option value="°C">°C</option>
|
||||
<option value="bar">bar</option>
|
||||
<option value="%">%</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{/* Loggerintervall/Speicherintervall */}
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3 ">
|
||||
<span className="font-normal">Speicherintervall:</span>
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
@@ -204,6 +217,7 @@ export default function AnalogInputSettingsModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Speichern-Button */}
|
||||
<div className="flex justify-end gap-2 mt-6">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
|
||||
@@ -7,18 +7,17 @@ import { getAnalogInputsThunk } from "@/redux/thunks/getAnalogInputsThunk";
|
||||
import { Icon } from "@iconify/react";
|
||||
import settingsIcon from "@iconify/icons-mdi/settings";
|
||||
import waveformIcon from "@iconify/icons-mdi/waveform";
|
||||
import { setSelectedAnalogInput } from "@/redux/slices/selectedAnalogInputSlice";
|
||||
|
||||
export default function AnalogInputsTable({
|
||||
import { setSelectedAnalogInput } from "@/redux/slices/analogInputs/selectedAnalogInputSlice";
|
||||
import { setIsSettingsModalOpen } from "@/redux/slices/analogInputs/analogInputsUiSlice";
|
||||
import { setIsChartModalOpen } from "@/redux/slices/analogInputs/analogInputsUiSlice";
|
||||
import {
|
||||
setSelectedId,
|
||||
setSelectedInput,
|
||||
setIsSettingsModalOpen,
|
||||
}: {
|
||||
setSelectedId: (id: number) => void;
|
||||
setSelectedInput: (input: AnalogInput) => void;
|
||||
setIsSettingsModalOpen: (open: boolean) => void;
|
||||
}) {
|
||||
setAutoLoad,
|
||||
} from "@/redux/slices/analogInputs/analogInputsHistorySlice";
|
||||
|
||||
export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
const [activeId, setActiveId] = React.useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -30,13 +29,18 @@ export default function AnalogInputsTable({
|
||||
);
|
||||
|
||||
const handleSelect = (id: number, input: AnalogInput) => {
|
||||
setSelectedId(id);
|
||||
dispatch(setSelectedId(id));
|
||||
setActiveId(id);
|
||||
dispatch(setSelectedAnalogInput(input)); // 🧠 hier kommt die Bezeichnung in Redux
|
||||
dispatch(setSelectedAnalogInput(input));
|
||||
dispatch(setAutoLoad(true));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow-md border border-gray-200 p-3 rounded-lg w-full laptop:p-1 xl:p-1">
|
||||
<div
|
||||
className={`bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 shadow-md border border-gray-200 dark:border-gray-700 p-3 rounded-lg laptop:p-1 xl:p-1 ${
|
||||
loading ? "cursor-wait" : ""
|
||||
}`}
|
||||
>
|
||||
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
||||
<Icon
|
||||
icon={waveformIcon}
|
||||
@@ -45,69 +49,113 @@ export default function AnalogInputsTable({
|
||||
Messwerteingänge
|
||||
</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse">
|
||||
<thead className="bg-gray-100 border-b items-center ">
|
||||
<table
|
||||
className={`text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse w-full ${
|
||||
loading ? "cursor-wait" : ""
|
||||
}`}
|
||||
>
|
||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b items-center">
|
||||
<tr>
|
||||
<th className="border p-1 text-left">Eingang</th>
|
||||
<th className="border p-1 text-left">Messwert</th>
|
||||
<th className="border p-1 text-left">Einheit</th>
|
||||
<th className="border p-1 text-left">Bezeichnung</th>
|
||||
<th className="border p-1 text-left">Aktion</th>
|
||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Eingang
|
||||
</th>
|
||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Messwert
|
||||
</th>
|
||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Einheit
|
||||
</th>
|
||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Bezeichnung
|
||||
</th>
|
||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Einstellungen
|
||||
</th>
|
||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Messkurve
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.values(analogInputs)
|
||||
.filter(
|
||||
(e) =>
|
||||
e && typeof e.id === "number" && typeof e.label === "string"
|
||||
(analogInput) =>
|
||||
analogInput &&
|
||||
typeof analogInput.id === "number" &&
|
||||
typeof analogInput.label === "string"
|
||||
)
|
||||
.slice(0, 8)
|
||||
.map((e, index) => (
|
||||
.map((analogInput, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={`transition cursor-pointer ${
|
||||
e.id === activeId ? "bg-blue-100" : "hover:bg-gray-100"
|
||||
loading
|
||||
? "cursor-wait"
|
||||
: analogInput.id === activeId
|
||||
? "bg-blue-100 dark:bg-gray-700 dark:text-white"
|
||||
: "hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
<td
|
||||
className="border p-2"
|
||||
onClick={() => handleSelect(e.id!, e)}
|
||||
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||
>
|
||||
<div className="flex items-center gap-1 ">
|
||||
<Icon
|
||||
icon={waveformIcon}
|
||||
className="text-gray-600 text-base laptop:text-sm xl:text-sm 2xl:text-lg"
|
||||
/>
|
||||
{e.id ?? "-"}
|
||||
{analogInput.id ?? "-"}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className="border p-2 text-right"
|
||||
onClick={() => handleSelect(e.id!, e)}
|
||||
className="border p-2 text-right bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||
>
|
||||
{typeof e.value === "number" ? e.value.toFixed(2) : "-"}
|
||||
{typeof analogInput.value === "number"
|
||||
? analogInput.value.toFixed(2)
|
||||
: "-"}
|
||||
</td>
|
||||
|
||||
<td className="border p-2">{e.unit || "-"}</td>
|
||||
<td
|
||||
className="border p-2"
|
||||
onClick={() => handleSelect(e.id!, e)}
|
||||
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||
>
|
||||
{e.label || "----"}
|
||||
{analogInput.unit || "-"}
|
||||
</td>
|
||||
<td
|
||||
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||
>
|
||||
{analogInput.label || "----"}
|
||||
</td>
|
||||
|
||||
<td className="border p-2 text-center">
|
||||
<td className="border p-2 text-center bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<button
|
||||
onClick={() => {
|
||||
handleSelect(e.id!, e);
|
||||
setSelectedInput(e);
|
||||
setIsSettingsModalOpen(true);
|
||||
handleSelect(analogInput.id!, analogInput);
|
||||
dispatch(setIsSettingsModalOpen(true));
|
||||
}}
|
||||
className="text-gray-400 hover:text-gray-500"
|
||||
className="text-gray-400 hover:text-gray-500 dark:text-gray-300 dark:hover:text-white"
|
||||
>
|
||||
<Icon icon={settingsIcon} className="text-xl" />
|
||||
</button>
|
||||
</td>
|
||||
<td className="border p-2 text-center bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<button
|
||||
onClick={() => {
|
||||
handleSelect(analogInput.id!, analogInput);
|
||||
dispatch(setIsChartModalOpen(true));
|
||||
}}
|
||||
className="text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
|
||||
title="Messkurve anzeigen"
|
||||
aria-label="Messkurve anzeigen"
|
||||
>
|
||||
<span role="img" aria-hidden="true" className="text-lg">
|
||||
📈
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
54
components/main/analogInputs/AnalogInputsView.tsx
Normal file
54
components/main/analogInputs/AnalogInputsView.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
// components/main/analogInputs/AnalogInputsView.tsx
|
||||
import React, { useState, useEffect } from "react";
|
||||
import AnalogInputsTable from "@/components/main/analogInputs/AnalogInputsTable";
|
||||
import AnalogInputsChartModal from "@/components/main/analogInputs/AnalogInputsChartModal";
|
||||
import AnalogInputsSettingsModal from "@/components/main/analogInputs/AnalogInputsSettingsModal";
|
||||
import { getAnalogInputsThunk } from "@/redux/thunks/getAnalogInputsThunk";
|
||||
import { useAppDispatch } from "@/redux/store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/redux/store";
|
||||
|
||||
function AnalogInputsView() {
|
||||
const [loading, setLoading] = useState(false); // Add loading state
|
||||
|
||||
const selectedInput = useSelector(
|
||||
(state: RootState) => state.selectedAnalogInput
|
||||
);
|
||||
// selectedId is now displayed within the modal header
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
dispatch(getAnalogInputsThunk());
|
||||
const interval = setInterval(() => {
|
||||
dispatch(getAnalogInputsThunk());
|
||||
}, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col justify-start gap-3 p-4 h-[calc(100vh-13vh-8vh)] ${
|
||||
loading ? "cursor-wait" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 justify-items-start">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg p-4 max-w-3xl text-gray-900 dark:text-gray-100">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
||||
Messwerteingänge
|
||||
</h2>
|
||||
<AnalogInputsTable loading={loading} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedInput !== null && <AnalogInputsSettingsModal />}
|
||||
{/* Chart Modal */}
|
||||
<AnalogInputsChartModal loading={loading} setLoading={setLoading} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnalogInputsView;
|
||||
62
components/main/dashboard/DashboardView.tsx
Normal file
62
components/main/dashboard/DashboardView.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
// components/main/dashboard/DashboardView.tsx
|
||||
import React, { useEffect } from "react";
|
||||
import "tailwindcss/tailwind.css";
|
||||
import "@fontsource/roboto";
|
||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
import { Icon } from "@iconify/react";
|
||||
import Last20MessagesTable from "@/components/main/dashboard/Last20MessagesTable";
|
||||
import NetworkInfo from "@/components/main/dashboard/NetworkInfo";
|
||||
import VersionInfo from "@/components/main/dashboard/VersionInfo";
|
||||
import Baugruppentraeger from "@/components/main/dashboard/Baugruppentraeger";
|
||||
import { getLast20MessagesThunk } from "@/redux/thunks/getLast20MessagesThunk";
|
||||
import { useAppDispatch } from "@/redux/store";
|
||||
|
||||
const DashboardView: React.FC = () => {
|
||||
//-------------------------------------
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(getLast20MessagesThunk());
|
||||
const interval = setInterval(() => {
|
||||
dispatch(getLast20MessagesThunk());
|
||||
}, 10000); // oder 5000
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [dispatch]);
|
||||
//-------------------------------------
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)] laptop:h-[calc(100vh-10vh-5vh)] xl:h-[calc(100vh-10vh-6vh)] laptop:gap-0 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center w-full lg:w-2/3">
|
||||
<div className="flex justify-between gap-1">
|
||||
<Icon
|
||||
icon="ri:calendar-schedule-line"
|
||||
className="text-littwin-blue text-4xl xl:text-2xl"
|
||||
/>
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100 xl:text-base">
|
||||
Letzten 20 Meldungen
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hauptbereich mit Meldungstabelle und Baugruppenträger */}
|
||||
<div className="flex flex-col lg:flex-row gap-4 flex-grow overflow-hidden pt-4">
|
||||
<Last20MessagesTable className="w-full lg:w-2/3 h-full" />
|
||||
|
||||
<div className="shadow-md rounded-lg w-full lg:w-1/3 flex flex-col gap-2">
|
||||
<VersionInfo className="w-full p-3 text-sm" />
|
||||
|
||||
{/* Baugruppenträger jetzt mit voller Breite */}
|
||||
<div className="overflow-auto max-h-[50vh]">
|
||||
<Baugruppentraeger />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* NetworkInfo in einem div ,nimmt die gesamte Breite */}
|
||||
<NetworkInfo />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardView;
|
||||
@@ -48,35 +48,56 @@ export default function Last20MessagesTable({ className }: Props) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-3 p-4 ${className}`}>
|
||||
<div className="overflow-auto max-h-[80vh]">
|
||||
<table className="min-w-full border">
|
||||
<thead className="bg-gray-100 text-left sticky top-0 z-10">
|
||||
<table className="min-w-full border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10">
|
||||
<tr>
|
||||
<th className="p-2 border">Prio</th>
|
||||
<th className="p-2 border">Zeitstempel</th>
|
||||
<th className="p-2 border">Quelle</th>
|
||||
<th className="p-2 border">Meldung</th>
|
||||
<th className="p-2 border">Status</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Prio
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Zeitstempel
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Quelle
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Meldung
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredMessages.slice(0, 20).map((msg, index) => (
|
||||
<tr key={index} className="hover:bg-gray-50">
|
||||
<td className="border p-2">
|
||||
<tr
|
||||
key={index}
|
||||
className="hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<div
|
||||
className="w-4 h-4 rounded"
|
||||
style={{ backgroundColor: msg.c }}
|
||||
></div>
|
||||
</td>
|
||||
<td className="border p-2">{msg.t}</td>
|
||||
<td className="border p-2">{msg.i}</td>
|
||||
<td className="border p-2">{msg.m}</td>
|
||||
<td className="border p-2">{msg.v}</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
{msg.t}
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
{msg.i}
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
{msg.m}
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
{msg.v}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{messages.length === 0 && (
|
||||
<div className="mt-4 text-center text-gray-500 italic">
|
||||
<div className="mt-4 text-center text-gray-500 italic dark:text-gray-400">
|
||||
Keine Meldungen im gewählten Zeitraum vorhanden.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -38,7 +38,7 @@ const NetworkInfo: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="w-full flex-direction: row flex">
|
||||
<div className=" flex-grow flex justify-between items-center mt-1 bg-white p-2 rounded-lg shadow-md border border-gray-200 laptop:m-0 laptop:scale-y-75 2xl:scale-y-75">
|
||||
<div className=" flex-grow flex justify-between items-center mt-1 bg-white dark:bg-gray-800 p-2 rounded-lg shadow-md border border-gray-200 dark:border-gray-700 laptop:m-0 laptop:scale-y-75 2xl:scale-y-75">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Image
|
||||
src="/images/IP-icon.svg"
|
||||
@@ -49,8 +49,12 @@ const NetworkInfo: React.FC = () => {
|
||||
priority
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">IP-Adresse</p>
|
||||
<p className="text-sm font-medium text-gray-700">{ip}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
IP-Adresse
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
{ip}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,8 +68,12 @@ const NetworkInfo: React.FC = () => {
|
||||
priority
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Subnet-Maske</p>
|
||||
<p className="text-sm font-medium text-gray-700">{subnet}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Subnet-Maske
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
{subnet}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -79,16 +87,20 @@ const NetworkInfo: React.FC = () => {
|
||||
priority
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Gateway</p>
|
||||
<p className="text-sm font-medium text-gray-700">{gateway}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Gateway</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
{gateway}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-xs font-bold text-littwin-blue">OPC-UA</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Status</p>
|
||||
<p className="text-sm font-medium text-gray-700">{opcUaZustand}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Status</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
{opcUaZustand}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* OPC UA Nodeset Name */}
|
||||
|
||||
@@ -18,22 +18,24 @@ const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-gray-50 rounded-lg shadow-sm border border-gray-200 w-full laptop:p-2 ${className}`}
|
||||
className={`bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 w-full laptop:p-2 ${className}`}
|
||||
>
|
||||
<h2 className="text-lg font-semibold text-gray-700 mb-2">
|
||||
<h2 className="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">
|
||||
Versionsinformationen
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-row p-2 space-x-2">
|
||||
<Icon icon="bx:code-block" className="text-xl text-blue-400" />
|
||||
<p className="text-sm text-gray-600">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Applikationsversion: {appVersion}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row p-2 space-x-2">
|
||||
<Icon icon="mdi:web" className="text-xl text-blue-400" />
|
||||
<p className="text-sm text-gray-600">Webversion: {webVersion}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Webversion: {webVersion}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,10 +18,18 @@ const KabelModulStatus: React.FC<KabelModulStatusProps> = ({
|
||||
// Modultyp basierend auf der Version bestimmen
|
||||
let moduleName = "";
|
||||
let moduleType = "";
|
||||
|
||||
if (moduleVersion === 419) {
|
||||
moduleName = "KÜ705";
|
||||
moduleType = "FO";
|
||||
} else if (moduleVersion === 420) {
|
||||
moduleName = "KÜ705";
|
||||
moduleType = "FO";
|
||||
} else if (moduleVersion === 421) {
|
||||
moduleName = "KÜ705";
|
||||
moduleType = "FO";
|
||||
} else if (moduleVersion === 431) {
|
||||
moduleName = "KÜ705";
|
||||
moduleType = "FO";
|
||||
} else if (moduleVersion === 350) {
|
||||
moduleName = "KÜ605";
|
||||
moduleType = "µC";
|
||||
|
||||
70
components/main/digitalInputs/DigitalInputsView.tsx
Normal file
70
components/main/digitalInputs/DigitalInputsView.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
// components/main/digitalInputs/DigitalInputsView.tsx
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { AppDispatch } from "@/redux/store";
|
||||
|
||||
import InputModal from "@/components/main/digitalInputs/digitalInputsModal";
|
||||
import { getDigitalInputsThunk } from "@/redux/thunks/getDigitalInputsThunk";
|
||||
import DigitalInputsWidget from "@/components/main/digitalInputs/DigitalInputsWidget";
|
||||
|
||||
const DigitalInputsView: React.FC = () => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
interface DigitalInput {
|
||||
id: number;
|
||||
eingangOffline: boolean;
|
||||
status: boolean;
|
||||
label: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const [selectedInput, setSelectedInput] = useState<DigitalInput | null>(null);
|
||||
const [isInputModalOpen, setIsInputModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getDigitalInputsThunk());
|
||||
const interval = setInterval(() => {
|
||||
dispatch(getDigitalInputsThunk());
|
||||
}, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, [dispatch]);
|
||||
|
||||
const openInputModal = (input: DigitalInput) => {
|
||||
setSelectedInput(input);
|
||||
setIsInputModalOpen(true);
|
||||
};
|
||||
|
||||
const closeInputModal = () => {
|
||||
setSelectedInput(null);
|
||||
setIsInputModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)] laptop:h-[calc(100vh-10vh-5vh)] xl:h-[calc(100vh-10vh-6vh)] laptop:gap-0">
|
||||
<h1 className="text-base font-semibold mb-2">Meldungseingänge</h1>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-4 items-start ">
|
||||
<DigitalInputsWidget
|
||||
openInputModal={openInputModal}
|
||||
inputRange={{ start: 0, end: 16 }}
|
||||
/>
|
||||
<DigitalInputsWidget
|
||||
openInputModal={openInputModal}
|
||||
inputRange={{ start: 16, end: 32 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isInputModalOpen && selectedInput && (
|
||||
<InputModal
|
||||
selectedInput={selectedInput}
|
||||
closeInputModal={closeInputModal}
|
||||
isOpen={isInputModalOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DigitalInputsView;
|
||||
@@ -30,7 +30,7 @@ export default function DigitalInputsWidget({
|
||||
//console.log("DigitalInputs", inputs);
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow-md border border-gray-200 p-3 rounded-lg w-full laptop:p-1 xl:p-1">
|
||||
<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 shadow-md border border-gray-200 dark:border-gray-700 p-3 rounded-lg w-full laptop:p-1 xl:p-1">
|
||||
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
||||
<Icon
|
||||
icon={inputIcon}
|
||||
@@ -38,19 +38,30 @@ export default function DigitalInputsWidget({
|
||||
/>
|
||||
Meldungseingänge {inputRange.start + 1} – {inputRange.end}
|
||||
</h2>
|
||||
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse">
|
||||
<thead className="bg-gray-100 border-b">
|
||||
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white dark:bg-gray-900">
|
||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b">
|
||||
<tr>
|
||||
<th className="px-1 py-1 text-left">Eingang</th>
|
||||
<th className="px-1 py-1 text-left">Zustand</th>
|
||||
<th className="px-1 py-1 text-left">Bezeichnung</th>
|
||||
<th className="px-1 py-1 text-left">Aktion</th>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Eingang
|
||||
</th>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Zustand
|
||||
</th>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Bezeichnung
|
||||
</th>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Aktion
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{inputs.map((input) => (
|
||||
<tr key={input.id} className="border-b">
|
||||
<td className="px-1 py-0">
|
||||
<tr
|
||||
key={input.id}
|
||||
className="border-b hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<td className="px-1 py-0 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<div className="flex items-center gap-1 ">
|
||||
<Icon
|
||||
icon={loginIcon}
|
||||
@@ -59,7 +70,7 @@ export default function DigitalInputsWidget({
|
||||
{input.id}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-1 py-1 ">
|
||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
{input.eingangOffline ? (
|
||||
<div className="relative group inline-block">
|
||||
<span className="text-red-500 sm:text-sm md:text-base lg:text-lg xl:text-xl 2xl:text-2xl laptop:text-sm ">
|
||||
@@ -80,11 +91,13 @@ export default function DigitalInputsWidget({
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-1 py-1">{input.label}</td>
|
||||
<td className="px-1 py-1">
|
||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
{input.label}
|
||||
</td>
|
||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<Icon
|
||||
icon={settingsIcon}
|
||||
className="text-gray-400 text-base laptop:text-sm xl:text-sm 2xl:text-lg cursor-pointer"
|
||||
className="text-gray-400 text-base laptop:text-sm xl:text-sm 2xl:text-lg cursor-pointer dark:text-gray-300 dark:hover:text-white"
|
||||
onClick={() => openInputModal(input)}
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -49,22 +49,22 @@ export default function DigitalOutputsModal({
|
||||
|
||||
try {
|
||||
if (isCPL) {
|
||||
// ✅ Name speichern (DANx=...)
|
||||
const nameEncoded = encodeURIComponent(label.trim());
|
||||
const nameUrl = `/CPL?digitalOutputs.html&DAN0${selectedOutput.id}=${nameEncoded}`;
|
||||
|
||||
// ✅ Status speichern (DASx=...)
|
||||
const statusUrl = `/CPL?digitalOutputs.html&DAS0${selectedOutput.id}=${
|
||||
status ? 1 : 0
|
||||
}`;
|
||||
|
||||
// 🟢 Beide nacheinander senden (wichtig bei älteren CPL-Versionen)
|
||||
window.location.href = nameUrl; // Name zuerst (ggf. durch Refresh überschrieben)
|
||||
setTimeout(() => {
|
||||
window.location.href = statusUrl;
|
||||
}, 300); // kleine Verzögerung (optional)
|
||||
try {
|
||||
await fetch(nameUrl, { method: "GET" });
|
||||
await new Promise((res) => setTimeout(res, 300));
|
||||
await fetch(statusUrl, { method: "GET" });
|
||||
|
||||
// 💡 Modal wird nicht automatisch geschlossen — da Seite neu lädt.
|
||||
closeOutputModal(); // Seite bleibt erhalten
|
||||
} catch (err) {
|
||||
console.error("❌ Fehler bei fetch:", err);
|
||||
setErrorMsg("❌ Fehler beim Speichern.");
|
||||
}
|
||||
} else {
|
||||
// 🧪 Lokaler Entwicklungsmodus
|
||||
const res = await fetch("/api/cpl/updateDigitalOutputsHandler", {
|
||||
|
||||
61
components/main/digitalOutputs/DigitalOutputsView.tsx
Normal file
61
components/main/digitalOutputs/DigitalOutputsView.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { AppDispatch } from "@/redux/store";
|
||||
|
||||
import DigitalOutputsModal from "./DigitalOutputsModal";
|
||||
import DigitalOutputsWidget from "./DigitalOutputsWidget";
|
||||
|
||||
import { getDigitalOutputsThunk } from "@/redux/thunks/getDigitalOutputsThunk";
|
||||
import type { DigitalOutput } from "@/types/digitalOutput";
|
||||
|
||||
const DigitalOutputsView: React.FC = () => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const [selectedOutput, setSelectedOutput] = useState<DigitalOutput | null>(
|
||||
null
|
||||
);
|
||||
const [isOutputModalOpen, setIsOutputModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch immediately on mount to ensure data is present without waiting for the first interval
|
||||
dispatch(getDigitalOutputsThunk());
|
||||
|
||||
// Then continue polling periodically
|
||||
const interval = setInterval(() => {
|
||||
dispatch(getDigitalOutputsThunk());
|
||||
}, 3000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [dispatch]);
|
||||
|
||||
const openOutputModal = (output: DigitalOutput) => {
|
||||
setSelectedOutput(output);
|
||||
setIsOutputModalOpen(true);
|
||||
};
|
||||
|
||||
const closeOutputModal = () => {
|
||||
setSelectedOutput(null);
|
||||
setIsOutputModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)] laptop:h-[calc(100vh-10vh-5vh)] xl:h-[calc(100vh-10vh-6vh)] laptop:gap-0">
|
||||
<h1 className="text-base font-semibold mb-2">Schaltausgänge</h1>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-4 items-start">
|
||||
<DigitalOutputsWidget openOutputModal={openOutputModal} />
|
||||
</div>
|
||||
|
||||
{selectedOutput && (
|
||||
<DigitalOutputsModal
|
||||
selectedOutput={selectedOutput}
|
||||
isOpen={isOutputModalOpen}
|
||||
closeOutputModal={closeOutputModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DigitalOutputsView;
|
||||
@@ -32,9 +32,25 @@ export default function DigitalOutputsWidget({
|
||||
|
||||
try {
|
||||
if (isCPL) {
|
||||
window.location.href = `/CPL?digitalOutputs.html&DAS0${id}=${
|
||||
updatedOutputs[id - 1].status ? 1 : 0
|
||||
}`;
|
||||
// Statt redirect:
|
||||
// window.location.href = `/CPL?...`;
|
||||
|
||||
// Verwende fetch:
|
||||
fetch(
|
||||
`/CPL?digitalOutputs.html&DAS0${id}=${
|
||||
updatedOutputs[id - 1].status ? 1 : 0
|
||||
}`,
|
||||
{
|
||||
method: "GET",
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Fehler beim Schalten");
|
||||
// Optional: Feedback anzeigen
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("CPL Fehler:", err);
|
||||
});
|
||||
} else {
|
||||
await fetch("/api/cpl/updateDigitalOutputsHandler", {
|
||||
method: "POST",
|
||||
@@ -50,7 +66,7 @@ export default function DigitalOutputsWidget({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow-md border border-gray-200 p-3 rounded-lg w-full h-fit max-h-[400px] overflow-auto">
|
||||
<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 shadow-md border border-gray-200 dark:border-gray-700 p-3 rounded-lg w-full h-fit max-h-[400px] overflow-auto">
|
||||
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
||||
<Icon
|
||||
icon={outputIcon}
|
||||
@@ -58,41 +74,54 @@ export default function DigitalOutputsWidget({
|
||||
/>
|
||||
Schaltausgänge
|
||||
</h2>
|
||||
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white rounded-lg">
|
||||
<thead className="bg-gray-100 border-b">
|
||||
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white dark:bg-gray-900 rounded-lg">
|
||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b">
|
||||
<tr>
|
||||
<th className="px-1 py-1 text-left">Ausgang</th>
|
||||
<th className="px-1 py-1 text-left">Bezeichnung</th>
|
||||
<th className="px-1 py-1 text-left">Schalter</th>
|
||||
<th className="px-1 py-1 text-left">Aktion</th>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Ausgang
|
||||
</th>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Bezeichnung
|
||||
</th>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Schalter
|
||||
</th>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Aktion
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{digitalOutputs.map((output) => (
|
||||
<tr key={output.id} className="border-b">
|
||||
<td className="flex items-center px-1 py-1">
|
||||
<tr
|
||||
key={output.id}
|
||||
className="border-b hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<td className="flex items-center px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<Icon
|
||||
icon={outputIcon}
|
||||
className="text-gray-600 mr-1 text-base"
|
||||
/>
|
||||
{output.id}
|
||||
</td>
|
||||
<td className="px-1 py-1">{output.label}</td>
|
||||
<td className="px-1 py-1">
|
||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
{output.label}
|
||||
</td>
|
||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<Icon
|
||||
icon={switchIcon}
|
||||
className={`cursor-pointer text-base transition ${
|
||||
output.status
|
||||
? "text-littwin-blue"
|
||||
: "text-gray-500 scale-x-[-1]"
|
||||
}`}
|
||||
} dark:hover:text-littwin-blue`}
|
||||
onClick={() => handleToggle(output.id)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-1 py-1">
|
||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<Icon
|
||||
icon={settingsIcon}
|
||||
className="text-gray-400 text-base cursor-pointer"
|
||||
className="text-gray-400 text-base cursor-pointer dark:text-gray-300 dark:hover:text-white"
|
||||
onClick={() => openOutputModal(output)}
|
||||
/>
|
||||
</td>
|
||||
|
||||
62
components/main/fall-detection-sensors/FallSensors.tsx
Normal file
62
components/main/fall-detection-sensors/FallSensors.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../redux/store";
|
||||
|
||||
// components/main/fall-detection-sensors/FallSensors.tsx
|
||||
interface FallSensorsProps {
|
||||
slotIndex: number;
|
||||
}
|
||||
|
||||
const FallSensors: React.FC<FallSensorsProps> = ({ slotIndex }) => {
|
||||
const { kvzStatus } = useSelector((state: RootState) => state.kueDataSlice);
|
||||
|
||||
// Nur 4 LEDs für den spezifischen Slot anzeigen
|
||||
const leds = Array.from({ length: 4 }, (_, ledIndex) => {
|
||||
const arrayIndex = slotIndex * 4 + ledIndex;
|
||||
const ledValue = kvzStatus?.[arrayIndex];
|
||||
|
||||
// LED Status: 1 = grün, 0 = rot, 2 = grau, undefined = grau
|
||||
if (ledValue === 1) return "green";
|
||||
if (ledValue === 0) return "red";
|
||||
return "gray"; // für 2 oder undefined
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-gray-300 border border-gray-400 rounded p-1 mt-4 w-full ">
|
||||
{/* Überschrift mit KVZ-Labels */}
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-[8px] font-medium text-gray-700">KVZ1</span>
|
||||
<span className="text-[8px] font-medium text-gray-700">KVZ2</span>
|
||||
<span className="text-[8px] font-medium text-gray-700">KVZ3</span>
|
||||
<span className="text-[8px] font-medium text-gray-700">KVZ4</span>
|
||||
</div>
|
||||
|
||||
{/* LEDs */}
|
||||
<div className="flex justify-between items-center">
|
||||
{leds.map((ledStatus, ledIndex) => {
|
||||
// LED Farben: grün (1), rot (0), grau (2)
|
||||
let bgColor = "bg-gray-400"; // Standard grau
|
||||
let statusText = "Unbekannt";
|
||||
|
||||
if (ledStatus === "green") {
|
||||
bgColor = "bg-green-500";
|
||||
statusText = "Ein";
|
||||
} else if (ledStatus === "red") {
|
||||
bgColor = "bg-red-500";
|
||||
statusText = "Aus";
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={ledIndex}
|
||||
className={`w-4 h-4 rounded-full border border-gray-500 ${bgColor} flex-shrink-0`}
|
||||
title={`Slot ${slotIndex} LED${ledIndex + 1}: ${statusText}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FallSensors;
|
||||
176
components/main/kabelueberwachung/KabelueberwachungView.tsx
Normal file
176
components/main/kabelueberwachung/KabelueberwachungView.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
"use client"; // /pages/kabelueberwachung.tsx
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Kue705FO from "@/components/main/kabelueberwachung/kue705FO/Kue705FO";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AppDispatch } from "@/redux/store"; // Adjust the path to your Redux store file
|
||||
import { RootState } from "@/redux/store"; // Adjust the path to your Redux store file
|
||||
import { getKueDataThunk } from "@/redux/thunks/getKueDataThunk";
|
||||
|
||||
function KabelueberwachungView() {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const searchParams = useSearchParams(); // URL-Parameter holen
|
||||
const initialRack = parseInt(searchParams.get("rack") ?? "1") || 1; // Rack-Nummer aus URL oder 1
|
||||
|
||||
const [activeRack, setActiveRack] = useState<number>(initialRack); // Nutze initialRack als Startwert
|
||||
const [alarmStatus, setAlarmStatus] = useState<boolean[]>([]); // Alarmstatus
|
||||
|
||||
// Redux-Variablen aus dem Store abrufen
|
||||
const {
|
||||
kueOnline,
|
||||
kueID,
|
||||
kueIso,
|
||||
kueAlarm1,
|
||||
kueAlarm2,
|
||||
kueResidence,
|
||||
kueCableBreak,
|
||||
kueGroundFault,
|
||||
} = useSelector((state: RootState) => state.kueDataSlice);
|
||||
|
||||
//----------------------------------------------------------------
|
||||
// Alarmstatus basierend auf Redux-Variablen berechnen
|
||||
const updateAlarmStatus = React.useCallback(() => {
|
||||
const updatedAlarmStatus = kueIso.map(
|
||||
(_: number | string, index: number) => {
|
||||
return Boolean(
|
||||
(kueAlarm1 && kueAlarm1[index]) ||
|
||||
(kueAlarm2 && kueAlarm2[index]) ||
|
||||
(kueCableBreak && kueCableBreak[index]) ||
|
||||
(kueGroundFault && kueGroundFault[index])
|
||||
);
|
||||
}
|
||||
);
|
||||
setAlarmStatus(updatedAlarmStatus);
|
||||
}, [kueIso, kueAlarm1, kueAlarm2, kueCableBreak, kueGroundFault]);
|
||||
|
||||
// Alarmstatus initial berechnen und alle 10 Sekunden aktualisieren
|
||||
useEffect(() => {
|
||||
updateAlarmStatus();
|
||||
const interval = setInterval(updateAlarmStatus, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, [updateAlarmStatus]);
|
||||
|
||||
// Modul- und Rack-Daten aufbereiten
|
||||
const allModules = kueIso.map((iso: number | string, index: number) => ({
|
||||
isolationswert: iso,
|
||||
schleifenwiderstand: kueResidence[index],
|
||||
modulName: kueID[index] || `Modul ${index + 1}`, // Eindeutiger Name pro Index
|
||||
kueOnlineStatus: kueOnline[index],
|
||||
alarmStatus: alarmStatus[index],
|
||||
tdrLocation: [], // Placeholder, replace with actual tdrLocation if available
|
||||
win_fallSensorsActive: kueOnline[index] ? 1 : 0, // Beispielwert, anpassen je nach Logik
|
||||
}));
|
||||
//console.log("Alle Module:", allModules);
|
||||
|
||||
const racks = React.useMemo(
|
||||
() => ({
|
||||
rack1: allModules.slice(0, 8),
|
||||
rack2: allModules.slice(8, 16),
|
||||
rack3: allModules.slice(16, 24),
|
||||
rack4: allModules.slice(24, 32),
|
||||
}),
|
||||
[allModules]
|
||||
);
|
||||
|
||||
// Konsolenausgaben für jede Rack-Aufteilung
|
||||
/* console.log(
|
||||
"Rack 1 Module:",
|
||||
racks.rack1.map((slot) => slot.modulName)
|
||||
);
|
||||
console.log(
|
||||
"Rack 2 Module:",
|
||||
racks.rack2.map((slot) => slot.modulName)
|
||||
);
|
||||
console.log(
|
||||
"Rack 3 Module:",
|
||||
racks.rack3.map((slot) => slot.modulName)
|
||||
);
|
||||
console.log(
|
||||
"Rack 4 Module:",
|
||||
racks.rack4.map((slot) => slot.modulName)
|
||||
); */
|
||||
|
||||
// Funktion zum Wechseln des Racks
|
||||
const changeRack = (rack: number) => {
|
||||
setActiveRack(rack);
|
||||
console.log(`Aktives Rack geändert zu: ${rack}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
/* console.log(`Aktives Rack: ${activeRack}`);
|
||||
console.log(
|
||||
`Rack ${activeRack} Modulnamen:`,
|
||||
racks[`rack${activeRack as 1 | 2 | 3 | 4}` as keyof typeof racks].map((slot: any) => slot.modulName)
|
||||
); */
|
||||
}, [activeRack, racks]);
|
||||
|
||||
//-----------------------------------------------------------
|
||||
|
||||
//------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
if (kueIso.length === 0) {
|
||||
console.log("📦 Lade KUE-Daten aus getKueDataThunk...");
|
||||
dispatch(getKueDataThunk());
|
||||
}
|
||||
}, [dispatch, kueIso.length]);
|
||||
//------------------------------------------------------------
|
||||
|
||||
// JSX rendering
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
{[1, 2, 3, 4].map((rack) => (
|
||||
<button
|
||||
key={rack}
|
||||
onClick={() => changeRack(rack)}
|
||||
className={`mr-2 ${
|
||||
Number(activeRack) === Number(rack)
|
||||
? "bg-littwin-blue text-white p-1 rounded-sm"
|
||||
: "bg-gray-300 p-1 text-sm"
|
||||
}`}
|
||||
>
|
||||
Rack {rack}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-row space-x-8 xl:space-x-0 2xl:space-x-8 qhd:space-x-16 ml-[5%] mt-[5%]">
|
||||
{(
|
||||
racks[
|
||||
`rack${activeRack as 1 | 2 | 3 | 4}` as keyof typeof racks
|
||||
] as typeof allModules
|
||||
).map(
|
||||
(
|
||||
slot: {
|
||||
isolationswert: number | string;
|
||||
schleifenwiderstand: number | string;
|
||||
modulName: string;
|
||||
kueOnlineStatus: number;
|
||||
alarmStatus?: boolean;
|
||||
tdrLocation: number[];
|
||||
win_fallSensorsActive: number;
|
||||
},
|
||||
index: number
|
||||
) => {
|
||||
const slotIndex = index + (activeRack - 1) * 8;
|
||||
return (
|
||||
<div key={index} className="flex">
|
||||
<Kue705FO
|
||||
isolationswert={slot.isolationswert}
|
||||
schleifenwiderstand={slot.schleifenwiderstand}
|
||||
modulName={slot.modulName}
|
||||
kueOnline={slot.kueOnlineStatus}
|
||||
alarmStatus={slot.alarmStatus}
|
||||
slotIndex={slotIndex}
|
||||
tdrLocation={slot.tdrLocation}
|
||||
win_fallSensorsActive={slot.win_fallSensorsActive}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default KabelueberwachungView;
|
||||
@@ -0,0 +1,389 @@
|
||||
"use client";
|
||||
// /components/main/kabelueberwachung/kue705FO/Charts/IsoMeasurementChart/IsoChartActionBar.tsx
|
||||
import React, { forwardRef, useImperativeHandle } from "react";
|
||||
import DateRangePicker from "@/components/common/DateRangePicker";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState, useAppDispatch } from "@/redux/store";
|
||||
import {
|
||||
setIsoMeasurementCurveChartData,
|
||||
setSelectedMode,
|
||||
setChartOpen,
|
||||
setLoading,
|
||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
||||
import { setBrushRange } from "@/redux/slices/brushSlice";
|
||||
import { getMessagesThunk } from "@/redux/thunks/getMessagesThunk";
|
||||
import { Listbox } from "@headlessui/react";
|
||||
|
||||
//-----------------------------------------------------------------------------------useIsoChartLoader
|
||||
export const useIsoChartLoader = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { vonDatum, bisDatum, selectedMode, slotNumber } = useSelector(
|
||||
(state: RootState) => state.kabelueberwachungChartSlice
|
||||
);
|
||||
const hasShownNoDataAlert = React.useRef(false);
|
||||
|
||||
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";
|
||||
|
||||
let url: string;
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
url = `/api/cpl/slotDataAPIHandler?slot=${slotNumber}&messart=${typeFolder}&dia=${mode}&vonDatum=${vonDatum}&bisDatum=${bisDatum}`;
|
||||
} else {
|
||||
url = `${window.location.origin}/CPL?seite.ACP&${mode}=${formatDate(
|
||||
vonDatum
|
||||
)};${formatDate(bisDatum)};${slotNumber};${type};`;
|
||||
}
|
||||
|
||||
console.log("API URL:", url);
|
||||
return url;
|
||||
};
|
||||
|
||||
const loadIsoChartData = async () => {
|
||||
if (slotNumber === null) return;
|
||||
|
||||
dispatch(setLoading(true));
|
||||
dispatch(setChartOpen(false));
|
||||
dispatch(setIsoMeasurementCurveChartData([]));
|
||||
|
||||
const startTime = Date.now();
|
||||
const MIN_LOADING_TIME_MS = 1000;
|
||||
|
||||
try {
|
||||
const apiUrl = getApiUrl(selectedMode, slotNumber);
|
||||
const response = await fetch(apiUrl);
|
||||
const data = await response.json();
|
||||
|
||||
const waitTime = Math.max(
|
||||
0,
|
||||
MIN_LOADING_TIME_MS - (Date.now() - startTime)
|
||||
);
|
||||
await new Promise((res) => setTimeout(res, waitTime));
|
||||
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
dispatch(setIsoMeasurementCurveChartData(data));
|
||||
dispatch(setChartOpen(true));
|
||||
} else {
|
||||
dispatch(setIsoMeasurementCurveChartData([]));
|
||||
dispatch(setChartOpen(false));
|
||||
if (!hasShownNoDataAlert.current) {
|
||||
alert("⚠️ Keine Messdaten im gewählten Zeitraum gefunden");
|
||||
hasShownNoDataAlert.current = true;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Fehler beim Laden:", err);
|
||||
alert("❌ Fehler beim Laden.");
|
||||
} finally {
|
||||
dispatch(setLoading(false));
|
||||
}
|
||||
};
|
||||
|
||||
return { loadIsoChartData };
|
||||
};
|
||||
|
||||
//-----------------------------------------------------------------------------------useIsoDataLoader Hook
|
||||
export const useIsoDataLoader = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
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
|
||||
// ...existing code...
|
||||
|
||||
const IsoChartActionBar = forwardRef((_props, ref) => {
|
||||
IsoChartActionBar.displayName = "IsoChartActionBar";
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { vonDatum, bisDatum, selectedMode, slotNumber, chartTitle } =
|
||||
useSelector((state: RootState) => state.kabelueberwachungChartSlice);
|
||||
// Aus DateRangePicker-Slice kommen die Werte, die der User im UI wählt
|
||||
const { vonDatum: pickerVonDatum, bisDatum: pickerBisDatum } = useSelector(
|
||||
(state: RootState) => state.dateRangePicker
|
||||
);
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const [year, month, day] = dateString.split("-");
|
||||
return `${year};${month};${day}`;
|
||||
};
|
||||
|
||||
const getApiUrl = (
|
||||
mode: "DIA0" | "DIA1" | "DIA2",
|
||||
slotNumber: number,
|
||||
fromDate: string,
|
||||
toDate: string
|
||||
) => {
|
||||
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=${fromDate}&bisDatum=${toDate}`
|
||||
: `${window.location.origin}/CPL?seite.ACP&${mode}=${formatDate(
|
||||
fromDate
|
||||
)};${formatDate(toDate)};${slotNumber};${type};`;
|
||||
|
||||
console.log("baseUrl", baseUrl);
|
||||
return baseUrl;
|
||||
};
|
||||
|
||||
const handleFetchData = async () => {
|
||||
if (slotNumber === null) {
|
||||
alert("⚠️ Bitte zuerst einen KÜ auswählen!");
|
||||
return;
|
||||
}
|
||||
// Wenn Meldungen-Ansicht aktiv ist, dann Meldungen laden
|
||||
if (chartTitle === "Meldungen") {
|
||||
try {
|
||||
dispatch(setLoading(true));
|
||||
const fromDate = pickerVonDatum ?? vonDatum;
|
||||
const toDate = pickerBisDatum ?? bisDatum;
|
||||
await dispatch(getMessagesThunk({ fromDate, toDate })).unwrap();
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error("❌ Fehler beim Laden der Meldungen:", message);
|
||||
alert("❌ Fehler beim Laden der Meldungen.");
|
||||
} finally {
|
||||
dispatch(setLoading(false));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Messkurve (ISO) laden
|
||||
const fromDate = pickerVonDatum ?? vonDatum;
|
||||
const toDate = pickerBisDatum ?? bisDatum;
|
||||
const apiUrl = getApiUrl(selectedMode, slotNumber, fromDate, toDate);
|
||||
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("▶️ Lade Isolationswiderstand-Daten für:");
|
||||
console.log(" Slot:", slotNumber);
|
||||
console.log(" Modus:", selectedMode);
|
||||
console.log(" Von:", fromDate);
|
||||
console.log(" Bis:", toDate);
|
||||
console.log(" URL:", apiUrl);
|
||||
console.log(" Daten:", jsonData);
|
||||
|
||||
if (Array.isArray(jsonData) && jsonData.length > 0) {
|
||||
dispatch(setIsoMeasurementCurveChartData(jsonData));
|
||||
dispatch(setChartOpen(true));
|
||||
} else {
|
||||
alert("⚠️ Keine Messdaten im gewählten Zeitraum gefunden.");
|
||||
dispatch(setIsoMeasurementCurveChartData([]));
|
||||
dispatch(setChartOpen(false));
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error("❌ Fehler beim Laden der Daten:", message);
|
||||
alert("❌ Fehler beim Laden der Daten.");
|
||||
} finally {
|
||||
dispatch(setLoading(false));
|
||||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleFetchData,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-2">
|
||||
<div className="flex items-center">
|
||||
<label className="text-sm font-semibold">
|
||||
KÜ {slotNumber !== null ? slotNumber + 1 : "-"}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* DateRangePicker – für beide Ansichten sichtbar, da Meldungen auch datumsabhängig sind */}
|
||||
<div
|
||||
style={{
|
||||
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
<DateRangePicker />
|
||||
</div>
|
||||
|
||||
{/* DIA0-DIA2 Dropdown - Platz reservieren, aber ausblenden wenn Meldungen */}
|
||||
<div
|
||||
style={{
|
||||
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
<Listbox
|
||||
value={selectedMode}
|
||||
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>
|
||||
{
|
||||
{
|
||||
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",
|
||||
DIA1: "Stündliche Werte",
|
||||
DIA2: "Tägliche Werte",
|
||||
}[mode]
|
||||
}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
|
||||
{/* Dropdown für Auswahl zwischen "Messkurve" und "Meldungen" - immer anzeigen */}
|
||||
{/* Dropdown für Auswahl zwischen "Messkurve" und "Meldungen" entfernt */}
|
||||
|
||||
{/* Daten laden Button – lädt je nach Ansicht Messkurve oder Meldungen */}
|
||||
<button
|
||||
style={{
|
||||
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
|
||||
}}
|
||||
onClick={handleFetchData}
|
||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm"
|
||||
>
|
||||
Daten laden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default IsoChartActionBar;
|
||||
@@ -0,0 +1,254 @@
|
||||
"use client"; // IsoChartView.tsx
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Listbox } from "@headlessui/react";
|
||||
import ReactModal from "react-modal";
|
||||
import IsoMeasurementChart from "./IsoMeasurementChart";
|
||||
import IsoChartActionBar from "./IsoChartActionBar";
|
||||
import Report from "./Report";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { AppDispatch } from "@/redux/store";
|
||||
import { RootState } from "@/redux/store";
|
||||
import {
|
||||
setChartOpen,
|
||||
setFullScreen,
|
||||
setSlotNumber,
|
||||
setChartTitle,
|
||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
||||
|
||||
import { resetBrushRange } from "@/redux/slices/brushSlice";
|
||||
|
||||
import {
|
||||
setVonDatum,
|
||||
setBisDatum,
|
||||
setSelectedMode,
|
||||
setSelectedSlotType,
|
||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
||||
import { resetDateRange } from "@/redux/slices/dateRangePickerSlice";
|
||||
|
||||
interface IsoChartViewProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
slotIndex: number;
|
||||
}
|
||||
|
||||
const IsoChartView: React.FC<IsoChartViewProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
slotIndex,
|
||||
}) => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
// removed unused loadData
|
||||
|
||||
const { isFullScreen, chartTitle } = useSelector(
|
||||
(state: RootState) => state.kabelueberwachungChartSlice
|
||||
);
|
||||
|
||||
// **Modal schließen + Redux-Status zurücksetzen**
|
||||
const handleClose = () => {
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||
|
||||
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
|
||||
|
||||
// Reset Datum
|
||||
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||
dispatch(setBisDatum(toISO(today)));
|
||||
|
||||
// Reset DateRangePicker
|
||||
dispatch(resetDateRange());
|
||||
|
||||
// Reset Dropdowns
|
||||
dispatch(setSelectedMode("DIA0")); // Reset to Alle Messwerte
|
||||
dispatch(setSelectedSlotType("isolationswiderstand"));
|
||||
dispatch(setChartTitle("Messkurve")); // Reset zu Messkurve
|
||||
|
||||
// Sonstiges Reset
|
||||
dispatch(setChartOpen(false));
|
||||
dispatch(setFullScreen(false));
|
||||
dispatch(resetBrushRange());
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
// **Vollbildmodus umschalten**
|
||||
const toggleFullScreen = () => {
|
||||
dispatch(setFullScreen(!isFullScreen));
|
||||
};
|
||||
|
||||
// Modal öffnen - ISO spezifische Einstellungen
|
||||
type ActionBarRefType = { handleFetchData: () => void };
|
||||
const actionBarRef = useRef<ActionBarRefType>(null);
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||
|
||||
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
|
||||
|
||||
// Set slot number first
|
||||
dispatch(setSlotNumber(slotIndex));
|
||||
|
||||
// Set dates
|
||||
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||
dispatch(setBisDatum(toISO(today)));
|
||||
|
||||
// Set ISO specific settings
|
||||
dispatch(setSelectedSlotType("isolationswiderstand"));
|
||||
dispatch(setSelectedMode("DIA0")); // Set to Alle Messwerte on open
|
||||
|
||||
// Set default to Messkurve
|
||||
dispatch(setChartTitle("Messkurve"));
|
||||
|
||||
// Automatisch Daten laden wie Button-Klick
|
||||
const timer = setTimeout(() => {
|
||||
actionBarRef.current?.handleFetchData();
|
||||
}, 120);
|
||||
|
||||
// Cleanup timer
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen, slotIndex, dispatch]);
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={isOpen}
|
||||
onRequestClose={handleClose}
|
||||
ariaHideApp={false}
|
||||
style={{
|
||||
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
||||
content: {
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
bottom: "auto",
|
||||
marginRight: "-50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: isFullScreen ? "90vw" : "70rem",
|
||||
height: isFullScreen ? "90vh" : "35rem",
|
||||
padding: "1rem",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Action-Buttons */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "0.625rem",
|
||||
right: "0.625rem",
|
||||
display: "flex",
|
||||
gap: "0.75rem",
|
||||
}}
|
||||
>
|
||||
{/* Fullscreen-Button */}
|
||||
<button
|
||||
onClick={toggleFullScreen}
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
fontSize: "1.5rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className={
|
||||
isFullScreen ? "bi bi-fullscreen-exit" : "bi bi-arrows-fullscreen"
|
||||
}
|
||||
></i>
|
||||
</button>
|
||||
|
||||
{/* Schließen-Button */}
|
||||
<button
|
||||
onClick={handleClose}
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
fontSize: "1.5rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<i className="bi bi-x-circle-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Chart-Container */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2 pr-24">
|
||||
<h3 className="text-lg font-semibold">Isolationswiderstand</h3>
|
||||
<Listbox
|
||||
value={chartTitle}
|
||||
onChange={(value: "Messkurve" | "Meldungen") =>
|
||||
dispatch(setChartTitle(value))
|
||||
}
|
||||
>
|
||||
<div className="relative w-40">
|
||||
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
|
||||
<span>
|
||||
{chartTitle === "Meldungen" ? "Meldungen" : "Messkurve"}
|
||||
</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">
|
||||
{(["Messkurve", "Meldungen"] as const).map((option) => (
|
||||
<Listbox.Option
|
||||
key={option}
|
||||
value={option}
|
||||
className={({
|
||||
selected,
|
||||
active,
|
||||
}: {
|
||||
selected: boolean;
|
||||
active: boolean;
|
||||
}) =>
|
||||
`px-4 py-1 cursor-pointer ${
|
||||
selected
|
||||
? "bg-littwin-blue text-white"
|
||||
: active
|
||||
? "bg-gray-200"
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{option === "Meldungen" ? "Meldungen" : "Messkurve"}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
<IsoChartActionBar ref={actionBarRef} />
|
||||
<div style={{ flex: 1, height: "90%" }}>
|
||||
{chartTitle === "Messkurve" ? (
|
||||
<IsoMeasurementChart />
|
||||
) : (
|
||||
<Report moduleType="ISO" autoLoad={chartTitle === "Meldungen"} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default IsoChartView;
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
interface IsoCustomTooltipProps {
|
||||
active?: boolean;
|
||||
payload?: Array<{
|
||||
dataKey: string;
|
||||
value: number;
|
||||
name?: string;
|
||||
color?: string;
|
||||
unit?: string;
|
||||
// Add other known properties here as needed
|
||||
}>;
|
||||
label?: string;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
const IsoCustomTooltip: React.FC<IsoCustomTooltipProps> = ({
|
||||
active,
|
||||
payload,
|
||||
label,
|
||||
unit,
|
||||
}) => {
|
||||
if (active && payload && payload.length) {
|
||||
const messwertMax = payload.find((p) => p.dataKey === "messwertMaximum");
|
||||
const messwert = payload.find((p) => p.dataKey === "messwert");
|
||||
const messwertMin = payload.find((p) => p.dataKey === "messwertMinimum");
|
||||
const messwertDurchschnitt = payload.find(
|
||||
(p) => p.dataKey === "messwertDurchschnitt"
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: "white",
|
||||
padding: "8px",
|
||||
border: "1px solid lightgrey",
|
||||
borderRadius: "5px",
|
||||
}}
|
||||
>
|
||||
<strong>{new Date(label as string).toLocaleString()}</strong>
|
||||
{messwertMax && (
|
||||
<div style={{ color: "grey" }}>
|
||||
Messwert Maximum: {messwertMax.value.toFixed(2)} {unit}
|
||||
</div>
|
||||
)}
|
||||
{messwert && (
|
||||
<div style={{ color: "#00AEEF", fontWeight: "bold" }}>
|
||||
Messwert: {messwert.value.toFixed(2)} {unit}
|
||||
</div>
|
||||
)}
|
||||
{messwertDurchschnitt && (
|
||||
<div style={{ color: "#00AEEF" }}>
|
||||
Messwert Durchschnitt: {messwertDurchschnitt.value.toFixed(2)}{" "}
|
||||
{unit}
|
||||
</div>
|
||||
)}
|
||||
{messwertMin && (
|
||||
<div style={{ color: "grey" }}>
|
||||
Messwert Minimum: {messwertMin.value.toFixed(2)} {unit}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default IsoCustomTooltip;
|
||||
@@ -0,0 +1,231 @@
|
||||
"use client"; // /components/main/kabelueberwachung/kue705FO/Charts/IsoMeasurementChart/IsoMeasurementChart.tsx
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/redux/store";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
} from "chart.js";
|
||||
|
||||
import "chartjs-adapter-date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
import { differenceInHours, parseISO } from "date-fns";
|
||||
|
||||
ChartJS.register(
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
);
|
||||
import { getColor } from "@/utils/colors";
|
||||
import { PulseLoader } from "react-spinners";
|
||||
|
||||
type IsoMeasurementEntry = {
|
||||
t: string;
|
||||
i: number;
|
||||
m: number;
|
||||
g: number;
|
||||
a: number;
|
||||
};
|
||||
|
||||
const usePreviousData = (data: IsoMeasurementEntry[]) => {
|
||||
const ref = useRef<IsoMeasurementEntry[]>([]);
|
||||
useEffect(() => {
|
||||
ref.current = data;
|
||||
}, [data]);
|
||||
return ref.current;
|
||||
};
|
||||
|
||||
const IsoMeasurementChart = () => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const chartInstance = useRef<ChartJS | null>(null);
|
||||
|
||||
const {
|
||||
isoMeasurementCurveChartData,
|
||||
selectedMode,
|
||||
unit,
|
||||
isFullScreen,
|
||||
isLoading,
|
||||
vonDatum,
|
||||
bisDatum,
|
||||
} = useSelector((state: RootState) => state.kabelueberwachungChartSlice);
|
||||
|
||||
const previousData = usePreviousData(isoMeasurementCurveChartData);
|
||||
|
||||
// Vergleichsfunktion
|
||||
const isEqual = (
|
||||
a: IsoMeasurementEntry[],
|
||||
b: IsoMeasurementEntry[]
|
||||
): boolean => {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (
|
||||
a[i].t !== b[i].t ||
|
||||
a[i].i !== b[i].i ||
|
||||
a[i].m !== b[i].m ||
|
||||
a[i].g !== b[i].g ||
|
||||
a[i].a !== b[i].a
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// ⏱ Zeitspanne in Stunden berechnen
|
||||
const from = vonDatum ? parseISO(vonDatum) : null;
|
||||
const to = bisDatum ? parseISO(bisDatum) : null;
|
||||
const durationInHours = from && to ? differenceInHours(to, from) : 9999;
|
||||
const timeUnit: "hour" | "day" = durationInHours <= 48 ? "hour" : "day";
|
||||
|
||||
useEffect(() => {
|
||||
import("chartjs-plugin-zoom").then((zoomPlugin) => {
|
||||
if (!ChartJS.registry.plugins.get("zoom")) {
|
||||
ChartJS.register(zoomPlugin.default);
|
||||
}
|
||||
|
||||
if (!canvasRef.current) return;
|
||||
const ctx = canvasRef.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
if (isEqual(isoMeasurementCurveChartData, previousData)) {
|
||||
return; // keine echte Datenänderung → nicht neu zeichnen
|
||||
}
|
||||
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.destroy();
|
||||
}
|
||||
|
||||
const chartData = {
|
||||
labels: isoMeasurementCurveChartData
|
||||
.map((entry) => new Date(entry.t))
|
||||
.reverse(),
|
||||
datasets: [
|
||||
{
|
||||
label: "Messwert Minimum",
|
||||
data: isoMeasurementCurveChartData.map((e) => e.i).reverse(),
|
||||
borderColor: "lightgrey",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 10,
|
||||
tension: 0.1,
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
label: "Messwert Maximum",
|
||||
data: isoMeasurementCurveChartData.map((e) => e.a).reverse(),
|
||||
borderColor: "gray",
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
order: 3,
|
||||
},
|
||||
selectedMode === "DIA0"
|
||||
? {
|
||||
label: "Messwert",
|
||||
data: isoMeasurementCurveChartData.map((e) => e.m).reverse(),
|
||||
borderColor: getColor("littwin-blue"),
|
||||
backgroundColor: "rgba(59,130,246,0.5)",
|
||||
borderWidth: 3,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 10,
|
||||
tension: 0.1,
|
||||
order: 2,
|
||||
}
|
||||
: {
|
||||
label: "Messwert Durchschnitt",
|
||||
data: isoMeasurementCurveChartData.map((e) => e.g).reverse(),
|
||||
borderColor: getColor("littwin-blue"),
|
||||
backgroundColor: "rgba(59,130,246,0.5)",
|
||||
borderWidth: 3,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 10,
|
||||
tension: 0.1,
|
||||
order: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: "top" as const },
|
||||
tooltip: {
|
||||
yAlign: "bottom" as const,
|
||||
mode: "index" as const,
|
||||
intersect: false,
|
||||
},
|
||||
zoom: {
|
||||
pan: { enabled: true, mode: "x" as const },
|
||||
zoom: {
|
||||
wheel: { enabled: true },
|
||||
pinch: { enabled: true },
|
||||
mode: "x" as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: timeUnit,
|
||||
tooltipFormat: "dd.MM.yyyy HH:mm:ss",
|
||||
displayFormats: {
|
||||
hour: "HH:mm",
|
||||
day: "dd.MM.",
|
||||
},
|
||||
locale: de,
|
||||
},
|
||||
title: { display: true, text: "Zeit" },
|
||||
},
|
||||
y: {
|
||||
title: { display: true, text: unit },
|
||||
ticks: { precision: 0 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
chartInstance.current = new ChartJS(ctx, {
|
||||
type: "line",
|
||||
data: chartData,
|
||||
options,
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isoMeasurementCurveChartData, selectedMode, vonDatum, bisDatum]);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: isFullScreen ? "90%" : "400px" }}>
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center h-96">
|
||||
<PulseLoader color="#3B82F6" />
|
||||
</div>
|
||||
)}
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: isLoading ? "none" : "block",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IsoMeasurementChart;
|
||||
@@ -0,0 +1,54 @@
|
||||
### 🧭 Zoom-Verhalten beim Schleifen-/Isolationsdiagramm
|
||||
|
||||
In dieser Komponente wird das automatische Nachladen der Messwerte temporär deaktiviert, wenn der Benutzer per Maus in das Diagramm zoomt oder pannt. Nach 30 Sekunden ohne Zoom/Pan-Aktion wird die automatische Aktualisierung wieder aktiviert. Dieses Verhalten dient dazu, den Zoom-Zustand nicht durch neue Daten zu verlieren.
|
||||
|
||||
---
|
||||
|
||||
### 📁 Enthaltene Komponenten
|
||||
|
||||
- `LoopChartActionBar.tsx`
|
||||
→ Auswahlleiste für Slot-Nummer, Zeitraum (über `DateRangePicker`), Messmodus (`DIA0`, `DIA1`, `DIA2`) und Slot-Typ (Schleife/Isolation).
|
||||
→ Ruft alle 10 Sekunden neue Messdaten ab – außer der Zoom-Modus pausiert das.
|
||||
- `LoopMeasurementChart.tsx`
|
||||
→ Das eigentliche Liniendiagramm mit Chart.js + Zoom-Plugin.
|
||||
→ Erkennt Zoom/Pan und setzt `chartUpdatePaused`, bis 30 Sekunden Inaktivität vergangen sind.
|
||||
|
||||
- `DateRangePicker.tsx`
|
||||
→ Zeigt zwei Felder für Von-/Bis-Datum. Nutzt Redux, um globale Zeitfenster zu setzen.
|
||||
|
||||
- `CustomTooltip.tsx`
|
||||
→ Zeigt beim Hover über die Kurve kontextbezogene Werte wie Messwert, Min, Max und Durchschnitt (DIA0/1/2).
|
||||
|
||||
---
|
||||
|
||||
### 🟢 UML-Aktivitätsdiagramm (Zoom → Pause → Timer → Auto-Update)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([Start])
|
||||
ZoomEvent[[Zoom oder Pan erkannt]]
|
||||
SetPause[Setze chartUpdatePaused = true]
|
||||
StartTimer[Starte 30s Timer]
|
||||
Check[Timer abgelaufen?]
|
||||
SetResume[Setze chartUpdatePaused = false]
|
||||
FetchData[[Datenabruf wieder erlaubt]]
|
||||
End([Ende])
|
||||
|
||||
Start --> ZoomEvent --> SetPause --> StartTimer --> Check
|
||||
Check -- Nein --> StartTimer
|
||||
Check -- Ja --> SetResume --> FetchData --> End
|
||||
```
|
||||
|
||||
stateDiagram-v2
|
||||
[*] --> AktualisierungAktiv
|
||||
|
||||
AktualisierungAktiv --> ZoomPause : Zoom/Pan erkannt
|
||||
ZoomPause --> AktualisierungAktiv : 30 Sekunden Inaktivität
|
||||
|
||||
state AktualisierungAktiv {
|
||||
[*] --> Normalbetrieb
|
||||
}
|
||||
|
||||
state ZoomPause {
|
||||
[*] --> CountdownLäuft
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
"use client"; // Report.tsx
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { RootState, AppDispatch } from "@/redux/store";
|
||||
import { getMessagesThunk } from "@/redux/thunks/getMessagesThunk";
|
||||
|
||||
// Gleiche Datenstruktur wie MeldungenView
|
||||
type Meldung = {
|
||||
t: string; // timestamp
|
||||
s: number; // status/priority
|
||||
c: string; // color
|
||||
m: string; // message
|
||||
i: string; // source/info
|
||||
v: string; // value/status text
|
||||
};
|
||||
|
||||
type ModuleType = "ISO" | "TDR" | "RSL" | "KVZ";
|
||||
|
||||
interface ReportProps {
|
||||
moduleType: ModuleType;
|
||||
autoLoad?: boolean;
|
||||
}
|
||||
|
||||
const Report: React.FC<ReportProps> = ({ moduleType, autoLoad = true }) => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filteredMessages, setFilteredMessages] = useState<Meldung[]>([]);
|
||||
|
||||
const { vonDatum, bisDatum, slotNumber } = useSelector(
|
||||
(state: RootState) => state.kabelueberwachungChartSlice
|
||||
);
|
||||
|
||||
// Nachrichten aus dem globalen Store
|
||||
const messages = useSelector((state: RootState) => state.messages.data);
|
||||
|
||||
// Nachrichten für den aktuellen Slot filtern
|
||||
const filterMessagesForSlot = useCallback(
|
||||
(allMessages: Meldung[], slot: number) => {
|
||||
if (slot === null) return [];
|
||||
|
||||
// Primärer Filter: Exakte CableLineX Übereinstimmung (X = slot + 1)
|
||||
const primaryIdentifier = `CableLine${slot + 1}`;
|
||||
|
||||
console.log(
|
||||
`🔍 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}`,
|
||||
`KÜ${slot + 1}`,
|
||||
`Kue${slot + 1}`,
|
||||
`Cable${slot + 1}`,
|
||||
`Line${slot + 1}`,
|
||||
];
|
||||
|
||||
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;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Modul-spezifische Schlüsselwörter (alle lowercase, ö => oe normalisiert)
|
||||
const moduleKeywordMap = useMemo<Record<ModuleType, string[]>>(
|
||||
() => ({
|
||||
ISO: [
|
||||
"modul online",
|
||||
"aderbruch",
|
||||
"erdschluss",
|
||||
"isofehler",
|
||||
"iso fehler",
|
||||
"iso-fehler",
|
||||
"isolationsfehler",
|
||||
"isolationfehler",
|
||||
"isolation fehler",
|
||||
],
|
||||
TDR: ["modul online", "tdr aktiv", "tdr entfernung"],
|
||||
RSL: ["modul online", "aderbruch", "schleifenfehler"],
|
||||
KVZ: ["modul online", "aderbruch", "kvz störung", "kvz stoerung"],
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const normalize = (text: string) =>
|
||||
text
|
||||
.toLowerCase()
|
||||
.replace(/ö/g, "oe")
|
||||
.replace(/ä/g, "ae")
|
||||
.replace(/ü/g, "ue");
|
||||
|
||||
// Daten laden
|
||||
const loadMessages = useCallback(async () => {
|
||||
if (slotNumber === null) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Redux Thunk verwenden (wie in MeldungenView)
|
||||
await dispatch(
|
||||
getMessagesThunk({
|
||||
fromDate: vonDatum,
|
||||
toDate: bisDatum,
|
||||
})
|
||||
).unwrap();
|
||||
} catch (err) {
|
||||
console.error("Fehler beim Laden der Berichte:", err);
|
||||
setError("Fehler beim Laden der Meldungen.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [dispatch, vonDatum, bisDatum, slotNumber]);
|
||||
|
||||
// Filter anwenden wenn sich Nachrichten oder Slot ändern
|
||||
useEffect(() => {
|
||||
if (slotNumber !== null && messages.length > 0) {
|
||||
const slotFiltered = filterMessagesForSlot(messages, slotNumber);
|
||||
// Modul-Filter anwenden
|
||||
const keywords = moduleKeywordMap[moduleType].map(normalize);
|
||||
const moduleFiltered = slotFiltered.filter((m) => {
|
||||
const msgNorm = normalize(m.m);
|
||||
return keywords.some((kw) => msgNorm.includes(kw));
|
||||
});
|
||||
// Fallback: Wenn keine Keyword-Treffer, zeige Slot-Filter-Ergebnis
|
||||
setFilteredMessages(
|
||||
moduleFiltered.length > 0 ? moduleFiltered : slotFiltered
|
||||
);
|
||||
} else {
|
||||
setFilteredMessages([]);
|
||||
}
|
||||
}, [
|
||||
messages,
|
||||
slotNumber,
|
||||
filterMessagesForSlot,
|
||||
moduleType,
|
||||
moduleKeywordMap,
|
||||
]);
|
||||
|
||||
// Automatisches Laden beim Mount und bei Änderungen (optional)
|
||||
useEffect(() => {
|
||||
if (!autoLoad) return;
|
||||
if (slotNumber !== null) {
|
||||
loadMessages();
|
||||
}
|
||||
}, [loadMessages, slotNumber, autoLoad]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-4 h-4 border-2 border-t-2 border-blue-500 rounded-full animate-spin" />
|
||||
<span>Lade Meldungen...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="text-center text-red-500 p-4">{error}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col p-4">
|
||||
{filteredMessages.length === 0 ? (
|
||||
<div className="text-center text-gray-500 ">
|
||||
Keine Meldungen für CableLine
|
||||
{slotNumber !== null ? slotNumber + 1 : "-"} (Filter: {moduleType}) im
|
||||
gewählten Zeitraum gefunden.
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-auto ">
|
||||
<table className="min-w-full border text-sm">
|
||||
<thead className="bg-gray-100 text-left sticky top-0 z-10">
|
||||
<tr>
|
||||
<th className="p-2 border">Prio</th>
|
||||
<th className="p-2 border">Zeitstempel</th>
|
||||
<th className="p-2 border">Quelle</th>
|
||||
<th className="p-2 border">Meldung</th>
|
||||
<th className="p-2 border">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredMessages.map((msg, index) => (
|
||||
<tr key={index} className="hover:bg-gray-200">
|
||||
<td className="border p-2">
|
||||
<div
|
||||
className="w-4 h-4 rounded"
|
||||
style={{ backgroundColor: msg.c }}
|
||||
></div>
|
||||
</td>
|
||||
<td className="border p-2">
|
||||
{new Date(msg.t).toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})}
|
||||
</td>
|
||||
<td className="border p-2">{msg.i}</td>
|
||||
<td className="border p-2">{msg.m}</td>
|
||||
<td className="border p-2">{msg.v}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* <div className="mt-4 text-sm text-gray-500 text-center mt-4">
|
||||
{filteredMessages.length} Meldung(en) (Filter: {moduleType}) gefunden
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Report;
|
||||
@@ -0,0 +1,155 @@
|
||||
"use client"; // KVZChartView.tsx
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import ReactModal from "react-modal";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AppDispatch, RootState } from "@/redux/store";
|
||||
import {
|
||||
setChartOpen,
|
||||
setFullScreen,
|
||||
setSlotNumber,
|
||||
setVonDatum,
|
||||
setBisDatum,
|
||||
setSelectedMode,
|
||||
setSelectedSlotType,
|
||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
||||
import { resetBrushRange } from "@/redux/slices/brushSlice";
|
||||
import FallSensors from "../../../../fall-detection-sensors/FallSensors";
|
||||
import Report from "../IsoMeasurementChart/Report";
|
||||
|
||||
interface KVZChartViewProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
slotIndex: number;
|
||||
}
|
||||
|
||||
// Modal zur Anzeige der KVz Zustände (Sturzsensoren / Fall Detection LEDs)
|
||||
// Stil und Verhalten analog zu ISO / RSL / TDR Modals
|
||||
const KVZChartView: React.FC<KVZChartViewProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
slotIndex,
|
||||
}) => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const isFullScreen = useSelector(
|
||||
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
|
||||
);
|
||||
const slotNumber = useSelector(
|
||||
(state: RootState) => state.kabelueberwachungChartSlice.slotNumber
|
||||
);
|
||||
|
||||
// Beim Öffnen Slot setzen (damit konsistent zu anderen Modals)
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
dispatch(setSlotNumber(slotIndex));
|
||||
}
|
||||
}, [isOpen, slotIndex, dispatch]);
|
||||
|
||||
const handleClose = () => {
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||
const toISO = (d: Date) => d.toLocaleDateString("sv-SE");
|
||||
|
||||
// Zurücksetzen – entspricht Verhalten der anderen Modals
|
||||
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||
dispatch(setBisDatum(toISO(today)));
|
||||
dispatch(setSelectedMode("DIA1"));
|
||||
dispatch(setSelectedSlotType("isolationswiderstand"));
|
||||
dispatch(setChartOpen(false));
|
||||
dispatch(setFullScreen(false));
|
||||
dispatch(resetBrushRange());
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const toggleFullScreen = () => {
|
||||
dispatch(setFullScreen(!isFullScreen));
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={isOpen}
|
||||
onRequestClose={handleClose}
|
||||
ariaHideApp={false}
|
||||
style={{
|
||||
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
||||
content: {
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
bottom: "auto",
|
||||
marginRight: "-50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: isFullScreen ? "90vw" : "50rem",
|
||||
height: isFullScreen ? "90vh" : "28rem",
|
||||
padding: "1rem",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Action Buttons */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "0.625rem",
|
||||
right: "0.625rem",
|
||||
display: "flex",
|
||||
gap: "0.75rem",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={toggleFullScreen}
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
fontSize: "1.5rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className={
|
||||
isFullScreen ? "bi bi-fullscreen-exit" : "bi bi-arrows-fullscreen"
|
||||
}
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
fontSize: "1.5rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<i className="bi bi-x-circle-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex flex-col h-full">
|
||||
<h3 className="text-lg font-semibold mb-1">KVz Zustände & Meldungen</h3>
|
||||
|
||||
{/* LED Bereich */}
|
||||
<div className="w-full flex justify-between mb-4">
|
||||
<div className="flex items-center">
|
||||
<label className="text-sm font-semibold">
|
||||
KÜ {slotNumber !== null ? slotNumber + 1 : "-"}
|
||||
</label>
|
||||
</div>
|
||||
<div style={{ width: "12rem" }}>
|
||||
<FallSensors slotIndex={slotIndex} />
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
{/* Meldungen Bereich */}
|
||||
<div className="flex-1 border rounded bg-white overflow-hidden">
|
||||
<Report moduleType="KVZ" />
|
||||
</div>
|
||||
</div>
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default KVZChartView;
|
||||
@@ -1,21 +1,29 @@
|
||||
"use client";
|
||||
// /components/main/kabelueberwachung/kue705FO/Charts/LoopMeasurementChart/LoopChartActionBar.tsx
|
||||
import React from "react";
|
||||
import DateRangePicker from "./DateRangePicker";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import { useAppDispatch } from "@/redux/store";
|
||||
import { getMessagesThunk } from "@/redux/thunks/getMessagesThunk";
|
||||
import { RSL_DURATION_SECONDS, NODE_ENV } from "@/utils/env";
|
||||
import DateRangePicker from "@/components/common/DateRangePicker";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/redux/store";
|
||||
import {
|
||||
setLoopMeasurementCurveChartData,
|
||||
setSelectedMode,
|
||||
setSelectedSlotType,
|
||||
setChartOpen,
|
||||
setLoading,
|
||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
||||
import { setBrushRange } from "@/redux/slices/brushSlice";
|
||||
import { setChartTitle } from "@/redux/slices/loopChartTypeSlice";
|
||||
import { Listbox } from "@headlessui/react";
|
||||
|
||||
//-----------------------------------------------------------------------------------useLoopChartLoader
|
||||
export const useLoopChartLoader = () => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const { vonDatum, bisDatum, selectedMode, selectedSlotType, slotNumber } =
|
||||
useSelector((state: RootState) => state.kabelueberwachungChartSlice);
|
||||
const hasShownNoDataAlert = React.useRef(false);
|
||||
@@ -30,8 +38,7 @@ export const useLoopChartLoader = () => {
|
||||
type: number,
|
||||
slotNumber: number
|
||||
) => {
|
||||
const typeFolder =
|
||||
type === 3 ? "isolationswiderstand" : "schleifenwiderstand";
|
||||
const typeFolder = "schleifenwiderstand";
|
||||
|
||||
let url: string;
|
||||
|
||||
@@ -92,34 +99,67 @@ export const useLoopChartLoader = () => {
|
||||
};
|
||||
|
||||
//-----------------------------------------------------------------------------------LoopChartActionBar
|
||||
const LoopChartActionBar: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const LoopChartActionBar = forwardRef((_props, ref) => {
|
||||
const dispatch = useAppDispatch();
|
||||
// RSL Progress State – Dauer konfigurierbar über NEXT_PUBLIC_RSL_DURATION_SECONDS
|
||||
const TOTAL_DURATION = RSL_DURATION_SECONDS;
|
||||
const [rslRunning, setRslRunning] = useState(false);
|
||||
const [rslProgress, setRslProgress] = useState(0);
|
||||
|
||||
// Fortschritt aktualisieren
|
||||
useEffect(() => {
|
||||
if (!rslRunning) return;
|
||||
setRslProgress(0);
|
||||
const startedAt = Date.now();
|
||||
const interval = setInterval(() => {
|
||||
const elapsed = Math.floor((Date.now() - startedAt) / 1000);
|
||||
if (elapsed >= TOTAL_DURATION) {
|
||||
setRslProgress(TOTAL_DURATION);
|
||||
setRslRunning(false);
|
||||
clearInterval(interval);
|
||||
// Optional automatische Daten-Nachladung anstoßen
|
||||
} else {
|
||||
setRslProgress(elapsed);
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [rslRunning, TOTAL_DURATION]);
|
||||
|
||||
const startRslProgress = () => {
|
||||
setRslRunning(true);
|
||||
setRslProgress(0);
|
||||
};
|
||||
|
||||
const {
|
||||
vonDatum,
|
||||
bisDatum,
|
||||
selectedMode,
|
||||
selectedSlotType,
|
||||
|
||||
slotNumber,
|
||||
|
||||
isLoading,
|
||||
} = useSelector((state: RootState) => state.kabelueberwachungChartSlice);
|
||||
|
||||
const { chartTitle } = useSelector((state: RootState) => state.loopChartType);
|
||||
// Vom DateRangePicker-Slice (vom UI gewählte Werte)
|
||||
const { vonDatum: pickerVonDatum, bisDatum: pickerBisDatum } = useSelector(
|
||||
(state: RootState) => state.dateRangePicker
|
||||
);
|
||||
|
||||
const getApiUrl = (
|
||||
mode: "DIA0" | "DIA1" | "DIA2",
|
||||
type: number,
|
||||
slotNumber: number
|
||||
slotNumber: number,
|
||||
fromDate: string,
|
||||
toDate: string
|
||||
) => {
|
||||
const typeFolder =
|
||||
type === 3 ? "isolationswiderstand" : "schleifenwiderstand";
|
||||
const typeFolder = "schleifenwiderstand";
|
||||
|
||||
const baseUrl =
|
||||
process.env.NODE_ENV === "development"
|
||||
? `/api/cpl/slotDataAPIHandler?slot=${slotNumber}&messart=${typeFolder}&dia=${mode}&vonDatum=${vonDatum}&bisDatum=${bisDatum}`
|
||||
? `/api/cpl/slotDataAPIHandler?slot=${slotNumber}&messart=${typeFolder}&dia=${mode}&vonDatum=${fromDate}&bisDatum=${toDate}`
|
||||
: `${window.location.origin}/CPL?seite.ACP&${mode}=${formatDate(
|
||||
vonDatum
|
||||
)};${formatDate(bisDatum)};${slotNumber};${type};`;
|
||||
fromDate
|
||||
)};${formatDate(toDate)};${slotNumber};${type};`;
|
||||
console.log("baseUrl", baseUrl);
|
||||
|
||||
return baseUrl;
|
||||
@@ -130,15 +170,64 @@ const LoopChartActionBar: React.FC = () => {
|
||||
return `${year};${month};${day}`;
|
||||
};
|
||||
|
||||
const handleStartRSL = async () => {
|
||||
if (slotNumber === null) {
|
||||
alert("⚠️ Bitte zuerst einen KÜ auswählen!");
|
||||
return;
|
||||
}
|
||||
|
||||
const cgiUrl = `${window.location.origin}/CPL?kabelueberwachung.html&KS_${slotNumber}=1`;
|
||||
|
||||
try {
|
||||
console.log("🚀 Starte RSL Messung für Slot:", slotNumber);
|
||||
console.log("📡 CGI URL:", cgiUrl);
|
||||
|
||||
if (NODE_ENV === "development") {
|
||||
// DEV: externes Gerät mocken – sofort Erfolg simulieren
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
console.log("✅ [DEV] RSL Mock-Start ok für Slot", slotNumber);
|
||||
startRslProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(cgiUrl);
|
||||
if (!response.ok) throw new Error(`CGI-Fehler: ${response.status}`);
|
||||
console.log("✅ RSL Messung gestartet für Slot", slotNumber);
|
||||
startRslProgress();
|
||||
} catch (err) {
|
||||
console.error("❌ Fehler beim Starten der RSL Messung:", err);
|
||||
alert("❌ Fehler beim Starten der RSL Messung.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleFetchData = async () => {
|
||||
const type = selectedSlotType === "schleifenwiderstand" ? 4 : 3;
|
||||
|
||||
if (slotNumber === null) {
|
||||
alert("⚠️ Bitte zuerst einen Steckplatz auswählen!");
|
||||
alert("⚠️ Bitte zuerst einen KÜ auswählen!");
|
||||
return;
|
||||
}
|
||||
|
||||
const apiUrl = getApiUrl(selectedMode, type, slotNumber);
|
||||
// Meldungen laden, wenn Meldungen-Ansicht aktiv ist
|
||||
if (chartTitle === "Meldungen") {
|
||||
try {
|
||||
dispatch(setLoading(true));
|
||||
const fromDate = pickerVonDatum ?? vonDatum;
|
||||
const toDate = pickerBisDatum ?? bisDatum;
|
||||
await dispatch(getMessagesThunk({ fromDate, toDate })).unwrap();
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error("❌ Fehler beim Laden der Meldungen:", message);
|
||||
alert("❌ Fehler beim Laden der Meldungen.");
|
||||
} finally {
|
||||
dispatch(setLoading(false));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const fromDate = pickerVonDatum ?? vonDatum;
|
||||
const toDate = pickerBisDatum ?? bisDatum;
|
||||
const apiUrl = getApiUrl(selectedMode, type, slotNumber, fromDate, toDate);
|
||||
if (!apiUrl) return;
|
||||
|
||||
dispatch(setLoading(true));
|
||||
@@ -165,8 +254,8 @@ const LoopChartActionBar: React.FC = () => {
|
||||
console.log(" Slot:", slotNumber);
|
||||
console.log(" Typ:", selectedSlotType, "→", type);
|
||||
console.log(" Modus:", selectedMode);
|
||||
console.log(" Von:", vonDatum);
|
||||
console.log(" Bis:", bisDatum);
|
||||
console.log(" Von:", fromDate);
|
||||
console.log(" Bis:", toDate);
|
||||
console.log(" URL:", apiUrl);
|
||||
console.log(" Daten:", jsonData);
|
||||
|
||||
@@ -188,69 +277,132 @@ const LoopChartActionBar: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleFetchData,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-2">
|
||||
<div className="flex justify-between p-1 bg-gray-100 rounded-lg ">
|
||||
<div className="flex items-center">
|
||||
<label className="text-sm font-semibold">
|
||||
Steckplatz {slotNumber ?? "-"}
|
||||
KÜ {slotNumber !== null ? slotNumber + 1 : "-"}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<DateRangePicker />
|
||||
{/* DateRangePicker – für beide Ansichten sichtbar */}
|
||||
<div>
|
||||
<DateRangePicker compact />
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={selectedMode}
|
||||
onChange={(e) => {
|
||||
dispatch(
|
||||
setSelectedMode(e.target.value as "DIA0" | "DIA1" | "DIA2")
|
||||
);
|
||||
dispatch(setBrushRange({ startIndex: 0, endIndex: 0 }));
|
||||
{/* DIA0/DIA1/DIA2 Dropdown – nur sichtbar bei Messkurve */}
|
||||
<div
|
||||
style={{
|
||||
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
|
||||
}}
|
||||
className="px-3 py-1 bg-white border rounded text-sm"
|
||||
>
|
||||
<option value="DIA0">Alle Messwerte</option>
|
||||
<option value="DIA1">Stündliche Werte</option>
|
||||
<option value="DIA2">Tägliche Werte</option>
|
||||
</select>
|
||||
<Listbox
|
||||
value={selectedMode}
|
||||
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>
|
||||
{
|
||||
{
|
||||
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",
|
||||
DIA1: "Stündliche Werte",
|
||||
DIA2: "Tägliche Werte",
|
||||
}[mode]
|
||||
}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
{/* Dropdown für Messkurve / Meldungen in View-Header umgezogen */}
|
||||
|
||||
<select
|
||||
value={selectedSlotType}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value as
|
||||
| "isolationswiderstand"
|
||||
| "schleifenwiderstand";
|
||||
dispatch(setSelectedSlotType(value));
|
||||
dispatch(
|
||||
setChartTitle(
|
||||
value === "isolationswiderstand"
|
||||
? "Isolationsmessung"
|
||||
: "Schleifenmessung"
|
||||
)
|
||||
);
|
||||
{/* Buttons – nur sichtbar bei Messkurve, Platz bleibt erhalten */}
|
||||
<div
|
||||
style={{
|
||||
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
|
||||
}}
|
||||
className="px-3 py-1 bg-white border rounded text-sm"
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<option value="isolationswiderstand">Isolationswiderstand</option>
|
||||
<option value="schleifenwiderstand">Schleifenwiderstand</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={handleFetchData}
|
||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm"
|
||||
>
|
||||
Daten laden
|
||||
</button>
|
||||
|
||||
{isLoading && (
|
||||
<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" />
|
||||
<span>Lade Daten...</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={handleStartRSL}
|
||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap"
|
||||
disabled={isLoading || rslRunning}
|
||||
>
|
||||
{rslRunning ? "RSL läuft..." : "RSL Messung starten"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleFetchData}
|
||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap"
|
||||
disabled={rslRunning}
|
||||
>
|
||||
Daten laden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{rslRunning && (
|
||||
<div className="fixed inset-0 z-[1000] flex flex-col items-center justify-center bg-white/80 backdrop-blur-sm">
|
||||
<div className="mb-4 text-center space-y-1">
|
||||
<p className="text-lg font-semibold">RSL Messung läuft</p>
|
||||
<p className="text-sm text-gray-700">
|
||||
Bitte warten… (noch {TOTAL_DURATION - rslProgress}s)
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-2/3 max-w-xl h-4 bg-gray-200 rounded overflow-hidden shadow-inner">
|
||||
<div
|
||||
className="h-full bg-littwin-blue transition-all ease-linear"
|
||||
style={{ width: `${(rslProgress / TOTAL_DURATION) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
LoopChartActionBar.displayName = "LoopChartActionBar";
|
||||
export default LoopChartActionBar;
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
"use client"; // LoopChartView.tsx
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { Listbox } from "@headlessui/react";
|
||||
import ReactModal from "react-modal";
|
||||
import LoopMeasurementChart from "./LoopMeasurementChart";
|
||||
import Report from "../IsoMeasurementChart/Report";
|
||||
import LoopChartActionBar from "./LoopChartActionBar";
|
||||
import { useRef } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { AppDispatch } from "@/redux/store";
|
||||
import { RootState } from "@/redux/store";
|
||||
import {
|
||||
setChartOpen,
|
||||
setFullScreen,
|
||||
setSlotNumber,
|
||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
||||
import { setChartTitle as setLoopChartTitle } from "@/redux/slices/loopChartTypeSlice";
|
||||
|
||||
import { resetBrushRange } from "@/redux/slices/brushSlice";
|
||||
import { useLoopChartLoader } from "./LoopChartActionBar";
|
||||
|
||||
import {
|
||||
setVonDatum,
|
||||
setBisDatum,
|
||||
setSelectedMode,
|
||||
setSelectedSlotType,
|
||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
||||
import { resetDateRange } from "@/redux/slices/dateRangePickerSlice";
|
||||
|
||||
interface LoopChartViewProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
slotIndex: number;
|
||||
}
|
||||
|
||||
const LoopChartView: React.FC<LoopChartViewProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
slotIndex,
|
||||
}) => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const chartTitle = useSelector(
|
||||
(state: RootState) => state.loopChartType.chartTitle
|
||||
);
|
||||
|
||||
const isFullScreen = useSelector(
|
||||
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
|
||||
);
|
||||
|
||||
// useLoopChartLoader hook
|
||||
const loadLoopChartData = useLoopChartLoader();
|
||||
|
||||
// slotNumber nicht direkt benötigt – wird intern über Redux genutzt
|
||||
|
||||
// **Modal schließen + Redux-Status zurücksetzen**
|
||||
const handleClose = () => {
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||
|
||||
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
|
||||
|
||||
// Reset Datum
|
||||
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||
dispatch(setBisDatum(toISO(today)));
|
||||
|
||||
// Reset DateRangePicker
|
||||
dispatch(resetDateRange());
|
||||
|
||||
// Reset Dropdowns
|
||||
dispatch(setSelectedMode("DIA0")); // Reset to Alle Messwerte
|
||||
dispatch(setSelectedSlotType("schleifenwiderstand"));
|
||||
|
||||
// Sonstiges Reset
|
||||
dispatch(setChartOpen(false));
|
||||
dispatch(setFullScreen(false));
|
||||
dispatch(resetBrushRange());
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
// **Vollbildmodus umschalten**
|
||||
const toggleFullScreen = () => {
|
||||
dispatch(setFullScreen(!isFullScreen));
|
||||
};
|
||||
|
||||
// Modal öffnen - RSL spezifische Einstellungen
|
||||
const actionBarRef = useRef<{ handleFetchData: () => void }>(null);
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||
|
||||
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
|
||||
|
||||
// Set slot number first
|
||||
dispatch(setSlotNumber(slotIndex));
|
||||
|
||||
// Set dates
|
||||
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||
dispatch(setBisDatum(toISO(today)));
|
||||
|
||||
// Set RSL specific settings
|
||||
dispatch(setSelectedSlotType("schleifenwiderstand"));
|
||||
dispatch(setSelectedMode("DIA0")); // Set to Alle Messwerte on open
|
||||
|
||||
// Automatisch Daten laden wie Button-Klick
|
||||
const timer = setTimeout(() => {
|
||||
actionBarRef.current?.handleFetchData();
|
||||
}, 120);
|
||||
|
||||
// Cleanup timer
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
//ESLint ignore
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen, slotIndex, dispatch]);
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={isOpen}
|
||||
onRequestClose={handleClose}
|
||||
ariaHideApp={false}
|
||||
style={{
|
||||
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
||||
content: {
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
bottom: "auto",
|
||||
marginRight: "-50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: isFullScreen ? "90vw" : "70rem",
|
||||
height: isFullScreen ? "90vh" : "35rem",
|
||||
padding: "1rem",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Action-Buttons */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "0.625rem",
|
||||
right: "0.625rem",
|
||||
display: "flex",
|
||||
gap: "0.75rem",
|
||||
}}
|
||||
>
|
||||
{/* Fullscreen-Button */}
|
||||
<button
|
||||
onClick={toggleFullScreen}
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
fontSize: "1.5rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className={
|
||||
isFullScreen ? "bi bi-fullscreen-exit" : "bi bi-arrows-fullscreen"
|
||||
}
|
||||
></i>
|
||||
</button>
|
||||
|
||||
{/* Schließen-Button */}
|
||||
<button
|
||||
onClick={handleClose}
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
fontSize: "1.5rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<i className="bi bi-x-circle-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Chart-Container */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2 pr-24">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{chartTitle === "Messkurve" ? "Schleifenwiderstand" : "Meldungen"}
|
||||
</h3>
|
||||
<Listbox
|
||||
value={chartTitle}
|
||||
onChange={(value: "Messkurve" | "Meldungen") =>
|
||||
dispatch(setLoopChartTitle(value))
|
||||
}
|
||||
>
|
||||
<div className="relative w-40">
|
||||
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
|
||||
<span>{chartTitle}</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">
|
||||
{(["Messkurve", "Meldungen"] as const).map((option) => (
|
||||
<Listbox.Option
|
||||
key={option}
|
||||
value={option}
|
||||
className={({
|
||||
selected,
|
||||
active,
|
||||
}: {
|
||||
selected: boolean;
|
||||
active: boolean;
|
||||
}) =>
|
||||
`px-4 py-1 cursor-pointer ${
|
||||
selected
|
||||
? "bg-littwin-blue text-white"
|
||||
: active
|
||||
? "bg-gray-200"
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{option}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
<LoopChartActionBar ref={actionBarRef} />
|
||||
<div style={{ flex: 1, height: "90%" }}>
|
||||
{chartTitle === "Messkurve" ? (
|
||||
<LoopMeasurementChart />
|
||||
) : (
|
||||
<Report moduleType="RSL" autoLoad={false} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoopChartView;
|
||||
@@ -31,7 +31,7 @@ ChartJS.register(
|
||||
Legend,
|
||||
Filler
|
||||
);
|
||||
import { getColor } from "../../../../../../utils/colors";
|
||||
import { getColor } from "@/utils/colors";
|
||||
import { PulseLoader } from "react-spinners";
|
||||
|
||||
type LoopMeasurementEntry = {
|
||||
@@ -118,24 +118,20 @@ const LoopMeasurementChart = () => {
|
||||
{
|
||||
label: "Messwert Minimum",
|
||||
data: loopMeasurementCurveChartData.map((e) => e.i).reverse(),
|
||||
borderColor: "lightgrey",
|
||||
backgroundColor: "rgba(211,211,211,0.5)",
|
||||
borderWidth: 2,
|
||||
borderColor: "gray",
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 10,
|
||||
tension: 0.1,
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
label: "Messwert Maximum",
|
||||
data: loopMeasurementCurveChartData.map((e) => e.a).reverse(),
|
||||
borderColor: "lightgrey",
|
||||
backgroundColor: "rgba(211,211,211,0.5)",
|
||||
borderWidth: 2,
|
||||
borderColor: "gray",
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 10,
|
||||
tension: 0.1,
|
||||
order: 1,
|
||||
order: 3,
|
||||
},
|
||||
selectedMode === "DIA0"
|
||||
? {
|
||||
@@ -147,7 +143,7 @@ const LoopMeasurementChart = () => {
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 10,
|
||||
tension: 0.1,
|
||||
order: 3,
|
||||
order: 2,
|
||||
}
|
||||
: {
|
||||
label: "Messwert Durchschnitt",
|
||||
@@ -158,7 +154,7 @@ const LoopMeasurementChart = () => {
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 10,
|
||||
tension: 0.1,
|
||||
order: 3,
|
||||
order: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import { RootState } from "@/redux/store";
|
||||
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";
|
||||
|
||||
const TDRChartActionBar: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -29,13 +30,6 @@ const TDRChartActionBar: React.FC = () => {
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const currentChartData = selectedId !== null ? tdrDataById[selectedId] : [];
|
||||
|
||||
// 🔄 Dropdown-Auswahl: neue Messung laden
|
||||
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const id = parseInt(e.target.value);
|
||||
setSelectedId(id);
|
||||
dispatch(getTDRChartDataByIdThunk(id));
|
||||
};
|
||||
|
||||
// 📌 Referenz setzen (nutzt Slotnummer + 1 für die API)
|
||||
const handleSetReference = async () => {
|
||||
if (
|
||||
@@ -59,7 +53,7 @@ const TDRChartActionBar: React.FC = () => {
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
const url = `/CPL?KTR${slotNumber}=${selectedId}`;
|
||||
const url = `/CPL?/${window.location.pathname}&KTR${slotNumber}=${selectedId}`;
|
||||
await fetch(url, { method: "GET" });
|
||||
}
|
||||
if (!isDev) {
|
||||
@@ -88,6 +82,33 @@ const TDRChartActionBar: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 📌 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 response = await fetch(cgiUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`CGI-Fehler: ${response.status}`);
|
||||
}
|
||||
|
||||
console.log("✅ TDR Messung gestartet für Slot", selectedSlot);
|
||||
//alert(`✅ TDR Messung für Slot ${selectedSlot + 1} gestartet`);
|
||||
} catch (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
|
||||
useEffect(() => {
|
||||
if (selectedSlot !== null) {
|
||||
@@ -112,48 +133,104 @@ const TDRChartActionBar: React.FC = () => {
|
||||
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-4">
|
||||
{/* 🧩 Slot-Anzeige (1-basiert für Benutzer) */}
|
||||
<div className="text-sm font-semibold">
|
||||
{selectedSlot !== null
|
||||
? `Steckplatz ${selectedSlot + 1}`
|
||||
: "Kein Steckplatz gewählt"}
|
||||
{selectedSlot !== null ? `KÜ ${selectedSlot + 1}` : "Kein KÜ gewählt"}
|
||||
</div>
|
||||
|
||||
{/* ✅ Referenz setzen */}
|
||||
{selectedId !== null && (
|
||||
<button
|
||||
onClick={handleSetReference}
|
||||
className="border border-littwin-blue text-littwin-blue bg-white rounded px-3 py-1 text-sm hover:bg-blue-100"
|
||||
className="border border-littwin-blue text-littwin-blue bg-white rounded px-3 py-1 text-sm hover:bg-gray-200"
|
||||
>
|
||||
TDR-Kurve als Referenz speichern
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 🚀 TDR starten */}
|
||||
<button
|
||||
onClick={handleStartTDR}
|
||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap "
|
||||
disabled={selectedSlot === null}
|
||||
>
|
||||
TDR-Messung starten
|
||||
</button>
|
||||
|
||||
{/* 🔽 Dropdown für Messungen */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<label htmlFor="tdrIdSelect" className="text-sm font-semibold">
|
||||
TDR Messung
|
||||
</label>
|
||||
<select
|
||||
id="tdrIdSelect"
|
||||
value={selectedId ?? ""}
|
||||
onChange={handleSelectChange}
|
||||
className="border rounded px-2 py-1 text-sm"
|
||||
<Listbox
|
||||
value={selectedId}
|
||||
onChange={(id) => {
|
||||
setSelectedId(id);
|
||||
if (id !== null) {
|
||||
dispatch(getTDRChartDataByIdThunk(id));
|
||||
}
|
||||
}}
|
||||
disabled={idsForSlot.length === 0}
|
||||
>
|
||||
<option value="">-- Wähle Messung --</option>
|
||||
{idsForSlot.map((entry) => (
|
||||
<option key={entry.id} value={entry.id}>
|
||||
{new Date(entry.t).toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})}{" "}
|
||||
– Fehlerstelle: {entry.d} m
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="relative w-96">
|
||||
<Listbox.Button className="w-full border px-2 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
|
||||
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{selectedId
|
||||
? (() => {
|
||||
const selected = idsForSlot.find(
|
||||
(e) => e.id === selectedId
|
||||
);
|
||||
return selected
|
||||
? `${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`
|
||||
: "Wähle Messung";
|
||||
})()
|
||||
: "Wähle Messung"}
|
||||
</span>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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">
|
||||
{idsForSlot.map((entry) => (
|
||||
<Listbox.Option
|
||||
key={entry.id}
|
||||
value={entry.id}
|
||||
className={({ selected, active }) =>
|
||||
`px-4 py-1 cursor-pointer whitespace-nowrap overflow-hidden text-ellipsis ${
|
||||
selected
|
||||
? "bg-littwin-blue text-white"
|
||||
: active
|
||||
? "bg-gray-200"
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{new Date(entry.t).toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})}{" "}
|
||||
– Fehlerstelle: {entry.d} m
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
"use client"; // /components/modules/kue705FO/charts/ChartSwitcher.tsx
|
||||
"use client"; // TDRChartView.tsx
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import ReactModal from "react-modal";
|
||||
import LoopChartActionBar from "./LoopMeasurementChart/LoopChartActionBar";
|
||||
import LoopMeasurementChart from "./LoopMeasurementChart/LoopMeasurementChart";
|
||||
import TDRChart from "./TDRChart/TDRChart";
|
||||
import TDRChart from "./TDRChart";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { AppDispatch } from "@/redux/store";
|
||||
import { RootState } from "@/redux/store";
|
||||
import {
|
||||
setChartOpen,
|
||||
setFullScreen,
|
||||
setSlotNumber,
|
||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
||||
|
||||
import { resetBrushRange } from "@/redux/slices/brushSlice";
|
||||
import { useLoopChartLoader } from "./LoopMeasurementChart/LoopChartActionBar";
|
||||
|
||||
import {
|
||||
setVonDatum,
|
||||
@@ -23,25 +21,55 @@ import {
|
||||
setSelectedSlotType,
|
||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
||||
|
||||
interface ChartSwitcherProps {
|
||||
import {
|
||||
setSelectedSlot,
|
||||
setActiveMode,
|
||||
} from "@/redux/slices/kueChartModeSlice";
|
||||
import { Listbox } from "@headlessui/react";
|
||||
import { setChartTitle } from "@/redux/slices/kabelueberwachungChartSlice";
|
||||
import Report from "../IsoMeasurementChart/Report";
|
||||
|
||||
interface TDRChartViewProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
slotIndex: number;
|
||||
}
|
||||
|
||||
const ChartSwitcher: React.FC<ChartSwitcherProps> = ({ isOpen, onClose }) => {
|
||||
const TDRChartView: React.FC<TDRChartViewProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
slotIndex,
|
||||
}) => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const chartTitle = useSelector(
|
||||
(state: RootState) => state.loopChartType.chartTitle
|
||||
|
||||
const { isFullScreen, chartTitle } = useSelector(
|
||||
(state: RootState) => state.kabelueberwachungChartSlice
|
||||
);
|
||||
|
||||
// **Redux-States für aktive Messkurve (TDR oder Schleife)**
|
||||
const activeMode = useSelector(
|
||||
(state: RootState) => state.kueChartModeSlice.activeMode
|
||||
);
|
||||
const isFullScreen = useSelector(
|
||||
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
|
||||
);
|
||||
// **Modal öffnen - TDR spezifische Einstellungen**
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||
|
||||
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
|
||||
|
||||
// Set TDR mode and slot
|
||||
dispatch(setActiveMode("TDR"));
|
||||
dispatch(setSelectedSlot(slotIndex));
|
||||
|
||||
// Also set slot number for general chart slice
|
||||
dispatch(setSlotNumber(slotIndex));
|
||||
|
||||
// Set dates
|
||||
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||
dispatch(setBisDatum(toISO(today)));
|
||||
|
||||
// TDR specific settings (if needed)
|
||||
dispatch(setSelectedSlotType("isolationswiderstand"));
|
||||
}
|
||||
}, [isOpen, slotIndex, dispatch]);
|
||||
|
||||
// **Modal schließen + Redux-Status zurücksetzen**
|
||||
const handleClose = () => {
|
||||
@@ -72,41 +100,6 @@ const ChartSwitcher: React.FC<ChartSwitcherProps> = ({ isOpen, onClose }) => {
|
||||
dispatch(setFullScreen(!isFullScreen));
|
||||
};
|
||||
|
||||
// **Slot und Messkurve setzen**
|
||||
// const setChartType = (chartType: "TDR" | "Schleife") => {
|
||||
// dispatch(setSelectedSlot(slotIndex));
|
||||
// dispatch(setSelectedChartType(chartType));
|
||||
// };
|
||||
|
||||
// useLoopChartLoader hook
|
||||
const loadLoopChartData = useLoopChartLoader();
|
||||
|
||||
// Slot number from Redux
|
||||
const slotNumber = useSelector(
|
||||
(state: RootState) => state.kabelueberwachungChartSlice.slotNumber
|
||||
);
|
||||
|
||||
// immer beim Öffnen das Modal die letzten 30 Tage anzeigen
|
||||
useEffect(() => {
|
||||
if (isOpen && activeMode === "Schleife" && slotNumber !== null) {
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||
|
||||
const toISO = (date: Date) => date.toLocaleDateString("sv-SE"); // YYYY-MM-DD
|
||||
|
||||
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||
dispatch(setBisDatum(toISO(today)));
|
||||
|
||||
// Warten, bis Redux gesetzt ist → dann Daten laden
|
||||
setTimeout(() => {
|
||||
loadLoopChartData.loadLoopChartData();
|
||||
}, 10); // kleiner Delay, damit Redux-State sicher aktualisiert ist
|
||||
}
|
||||
//ESLint ignore
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen, activeMode, slotNumber, dispatch]);
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={isOpen}
|
||||
@@ -179,23 +172,65 @@ const ChartSwitcher: React.FC<ChartSwitcherProps> = ({ isOpen, onClose }) => {
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{activeMode === "Schleife" ? (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold">{chartTitle}</h3>
|
||||
<LoopChartActionBar />
|
||||
<div style={{ flex: 1, height: "90%" }}>
|
||||
<LoopMeasurementChart />
|
||||
<div className="flex justify-between items-center mb-2 pr-24">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{chartTitle === "Messkurve" ? "TDR-Messung" : "Meldungen"}
|
||||
</h3>
|
||||
{/* Dropdown Messkurve / Meldungen */}
|
||||
<Listbox
|
||||
value={chartTitle}
|
||||
onChange={(value: "Messkurve" | "Meldungen") =>
|
||||
dispatch(setChartTitle(value))
|
||||
}
|
||||
>
|
||||
<div className="relative w-40">
|
||||
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
|
||||
<span>{chartTitle}</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">
|
||||
{(["Messkurve", "Meldungen"] as const).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"
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{option}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold">TDR-Messung</h3>
|
||||
</Listbox>
|
||||
</div>
|
||||
{/* Chart oder Meldungen */}
|
||||
<div style={{ flex: 1, height: "90%" }}>
|
||||
{chartTitle === "Messkurve" ? (
|
||||
<TDRChart isFullScreen={isFullScreen} />
|
||||
</>
|
||||
)}
|
||||
) : (
|
||||
<Report moduleType="TDR" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChartSwitcher;
|
||||
export default TDRChartView;
|
||||
@@ -1,10 +1,21 @@
|
||||
"use client"; // components/modules/kue705FO/Kue705FO.tsx
|
||||
import React, { useState, useRef, useMemo } from "react";
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import KueModal from "./modals/SettingsModalWrapper";
|
||||
// import FallSensors from "../../fall-detection-sensors/FallSensors";
|
||||
import "bootstrap-icons/font/bootstrap-icons.css"; // Import Bootstrap Icons
|
||||
import { Kue705FOProps } from "../../../../types/Kue705FOProps";
|
||||
import ChartSwitcher from "./Charts/ChartSwitcher";
|
||||
// Import the new specialized ChartView components
|
||||
import IsoChartView from "./Charts/IsoMeasurementChart/IsoChartView";
|
||||
import LoopChartView from "./Charts/LoopMeasurementChart/LoopChartView";
|
||||
import TDRChartView from "./Charts/TDRChart/TDRChartView";
|
||||
import KVZChartView from "./Charts/KVZChart/KVZChartView";
|
||||
import SlotActivityOverlay from "./SlotActivityOverlay";
|
||||
// Keep ChartSwitcher import for backwards compatibility if needed
|
||||
// import ChartSwitcher from "./Charts/ChartSwitcher";
|
||||
// Remove separate chart imports since we use ChartView components
|
||||
// import IsoMeasurementChart from "./Charts/IsoMeasurementChart/IsoMeasurementChart";
|
||||
// import LoopMeasurementChart from "./Charts/LoopMeasurementChart/LoopMeasurementChart";
|
||||
//-------Redux Toolkit--------
|
||||
import { RootState } from "../../../../redux/store";
|
||||
import { useDispatch } from "react-redux";
|
||||
@@ -15,16 +26,16 @@ import useKueVersion from "./hooks/useKueVersion";
|
||||
import useIsoDisplay from "./hooks/useIsoDisplay";
|
||||
import useLoopDisplay from "./hooks/useLoopDisplay";
|
||||
import useModulName from "./hooks/useModulName";
|
||||
|
||||
import type { Chart } from "chart.js";
|
||||
import { useAdminAuth } from "../../settingsPageComponents/hooks/useAdminAuth";
|
||||
|
||||
//--------handlers----------------
|
||||
import handleButtonClick from "./kue705FO-Funktionen/handleButtonClick";
|
||||
// Keep needed imports
|
||||
import handleOpenModal from "./handlers/handleOpenModal";
|
||||
import handleCloseModal from "./handlers/handleCloseModal";
|
||||
import handleOpenChartModal from "./handlers/handleOpenChartModal";
|
||||
import handleCloseChartModal from "./handlers/handleCloseChartModal";
|
||||
import handleRefreshClick from "./handlers/handleRefreshClick";
|
||||
// Remove unused chart modal handlers since we use direct ChartView components
|
||||
// import handleOpenChartModal from "./handlers/handleOpenChartModal";
|
||||
// import handleCloseChartModal from "./handlers/handleCloseChartModal";
|
||||
// import handleRefreshClick from "./handlers/handleRefreshClick";
|
||||
|
||||
const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
isolationswert,
|
||||
@@ -32,7 +43,6 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
modulName,
|
||||
kueOnline,
|
||||
slotIndex,
|
||||
tdrLocation,
|
||||
}) => {
|
||||
/* console.log(
|
||||
`Rendering Kue705FO - SlotIndex: ${slotIndex}, ModulName: ${modulName}`
|
||||
@@ -41,35 +51,43 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
const dispatch = useDispatch();
|
||||
const { kueName } = useSelector((state: RootState) => state.kueDataSlice);
|
||||
|
||||
const [activeButton, setActiveButton] = useState<"Schleife" | "TDR">(
|
||||
// Admin authentication hook for security - using showModal as true for continuous auth check
|
||||
const { isAdminLoggedIn } = useAdminAuth(true);
|
||||
|
||||
const [activeButton, setActiveButton] = useState<"Schleife" | "TDR" | "ISO">(
|
||||
"Schleife"
|
||||
);
|
||||
|
||||
const [loopTitleText, setloopTitleText] = useState(
|
||||
"Schleifenwiderstand [kOhm]"
|
||||
);
|
||||
const [, setloopTitleText] = useState("Schleifenwiderstand [kOhm]");
|
||||
const [isoDisplayText] = useState("Aderbruch");
|
||||
const [groundFaultDisplayText] = useState("Erdschluss");
|
||||
const [loopFaultDisplayText] = useState("Schleifenfehler");
|
||||
const [isoFaultDisplayText] = useState("Isolationsfehler");
|
||||
const [isoGreaterThan200] = useState(">200 MOhm");
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showChartModal, setShowChartModal] = useState(false);
|
||||
// Separate modal states for each ChartView
|
||||
const [showIsoModal, setShowIsoModal] = useState(false);
|
||||
const [showRslModal, setShowRslModal] = useState(false);
|
||||
const [showTdrModal, setShowTdrModal] = useState(false);
|
||||
const [showKvzModal, setShowKvzModal] = useState(false);
|
||||
// Keep original showChartModal for backwards compatibility if needed
|
||||
// const [showChartModal, setShowChartModal] = useState(false);
|
||||
// Removed unused loopMeasurementCurveChartData state
|
||||
|
||||
//------- Redux-Variablen abrufen--------------------------------
|
||||
const {
|
||||
kueVersion: reduxKueVersion,
|
||||
tdrActive,
|
||||
kueCableBreak: kueCableBreakRaw,
|
||||
kueGroundFault: kueGroundFaultRaw,
|
||||
kueAlarm1: kueAlarm1Raw,
|
||||
kueAlarm2: kueAlarm2Raw,
|
||||
kueOverflow: kueOverflowRaw,
|
||||
kuePSTmMinus96V, // <- richtig, weil so im State vorhanden
|
||||
tdrActive, // <- TDR aktiv Status hinzugefügt
|
||||
kvzPresence, // <- KVz Presence Array hinzugefügt
|
||||
kvzActive, // <- KVz Active Array hinzugefügt
|
||||
// kvzStatus, // <- KVz LED Status Array (jetzt nur im KVZ Modal verwendet)
|
||||
} = useSelector((state: RootState) => state.kueDataSlice);
|
||||
|
||||
//---------------------------------------------
|
||||
@@ -96,15 +114,71 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
//-------------------------handlers-------------------------
|
||||
const openModal = () => handleOpenModal(setShowModal);
|
||||
const closeModal = () => handleCloseModal(setShowModal);
|
||||
const openChartModal = () =>
|
||||
handleOpenChartModal(setShowChartModal, dispatch, slotIndex, activeButton);
|
||||
const refreshClick = () =>
|
||||
handleRefreshClick(activeButton, slotIndex, setLoading);
|
||||
// Create a ref for the chart instance to pass as the second argument
|
||||
const chartInstance = useRef<Chart | null>(null);
|
||||
|
||||
const closeChartModal = () =>
|
||||
handleCloseChartModal(setShowChartModal, chartInstance);
|
||||
// New ChartView handlers - direct modal opening
|
||||
const openIsoModal = () => {
|
||||
setActiveButton("ISO");
|
||||
// Set Redux state for ISO type
|
||||
dispatch({
|
||||
type: "kabelueberwachungChart/setSelectedSlotType",
|
||||
payload: 1, // 1 = Isolationswiderstand
|
||||
});
|
||||
dispatch({
|
||||
type: "kabelueberwachungChart/setSlotNumber",
|
||||
payload: slotIndex,
|
||||
});
|
||||
setShowIsoModal(true);
|
||||
};
|
||||
|
||||
const closeIsoModal = () => {
|
||||
setShowIsoModal(false);
|
||||
};
|
||||
|
||||
const openRslModal = () => {
|
||||
setActiveButton("Schleife");
|
||||
setloopTitleText("Schleifenwiderstand [kOhm]");
|
||||
setLoopDisplayValue(Number(schleifenwiderstand));
|
||||
dispatch({
|
||||
type: "kabelueberwachungChart/setSelectedSlotType",
|
||||
payload: 2,
|
||||
}); // RSL type
|
||||
dispatch({
|
||||
type: "kabelueberwachungChart/setSlotNumber",
|
||||
payload: slotIndex,
|
||||
});
|
||||
setShowRslModal(true);
|
||||
};
|
||||
|
||||
const closeRslModal = () => {
|
||||
setShowRslModal(false);
|
||||
};
|
||||
|
||||
const openTdrModal = () => {
|
||||
setActiveButton("TDR");
|
||||
setloopTitleText("Entfernung [km]");
|
||||
|
||||
const latestTdrDistanceMeters =
|
||||
Array.isArray(tdmChartData?.[slotIndex]) &&
|
||||
tdmChartData[slotIndex].length > 0 &&
|
||||
typeof tdmChartData[slotIndex][0].d === "number"
|
||||
? tdmChartData[slotIndex][0].d
|
||||
: 0;
|
||||
|
||||
const latestTdrDistance = Number(
|
||||
(latestTdrDistanceMeters / 1000).toFixed(3)
|
||||
);
|
||||
setLoopDisplayValue(latestTdrDistance);
|
||||
setShowTdrModal(true);
|
||||
};
|
||||
|
||||
const closeTdrModal = () => {
|
||||
setShowTdrModal(false);
|
||||
};
|
||||
|
||||
const openKvzModal = () => {
|
||||
setShowKvzModal(true);
|
||||
};
|
||||
const closeKvzModal = () => setShowKvzModal(false);
|
||||
//----------------------------------
|
||||
//hooks einbinden
|
||||
const kueVersion = useKueVersion(slotIndex, reduxKueVersion);
|
||||
@@ -160,17 +234,26 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
activeButton
|
||||
);
|
||||
|
||||
// TDR aktiv Status für diesen Slot prüfen
|
||||
const isTdrActiveForSlot = tdrActive?.[slotIndex] === 1;
|
||||
|
||||
// KVz aktiv Status für diesen Slot prüfen - nur wenn Admin authentifiziert ist, KVz vorhanden ist UND aktiviert ist
|
||||
const isKvzActiveForSlot =
|
||||
kvzPresence?.[slotIndex] === 1 &&
|
||||
kvzActive?.[slotIndex] === 1 &&
|
||||
isAdminLoggedIn;
|
||||
|
||||
// Removed useChartData(loopMeasurementCurveChartData) as the state was unused
|
||||
|
||||
//---------------------------------
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative bg-gray-300 w-[7.25rem] h-[24.375rem] border border-gray-400 transform laptop:-translate-y-12 2xl:-translate-y-0
|
||||
scale-100 sm:scale-95 md:scale-100 lg:scale-105 xl:scale-90 2xl:scale-125 top-3 qhd:scale-150 qhd:-translate-y-0
|
||||
|
||||
"
|
||||
className="relative bg-gray-300 w-[7.25rem] h-[23.375rem] border border-gray-400 transform laptop:-translate-y-12 2xl:-translate-y-0
|
||||
scale-100 sm:scale-95 md:scale-100 lg:scale-105 xl:scale-90 2xl:scale-125 top-3 qhd:scale-150 qhd:-translate-y-0"
|
||||
>
|
||||
{/* Per-slot activity overlay */}
|
||||
<SlotActivityOverlay slotIndex={slotIndex} />
|
||||
{kueOnline === 1 ? (
|
||||
<>
|
||||
<div className="relative w-[7.075rem] h-[15.156rem] bg-littwin-blue border-[0.094rem] border-gray-400 z-0">
|
||||
@@ -220,33 +303,57 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Anzeige des Isolation */}
|
||||
<div className="relative mt-[3.125rem] mx-auto bg-black text-white w-[6.25rem] h-[2.5rem] flex items-center justify-center text-[1.125rem] z-10">
|
||||
<div className="text-center">
|
||||
{/* Schwarzes Display mit drei Zeilen: Alarm, ISO, Schleife */}
|
||||
<div className="relative mt-[3.125rem] mx-auto bg-black text-white w-[6.8rem] h-[3.1rem] flex flex-col items-center justify-between z-10 p-1">
|
||||
<div className="text-center w-full flex flex-col justify-between items-center h-full">
|
||||
{/* 1. Zeile: Alarmtext in Rot, sonst "Status: OK" */}
|
||||
<span
|
||||
className={
|
||||
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
|
||||
Number(kuePSTmMinus96V?.[slotIndex]) === 1 ||
|
||||
Number(kueCableBreak?.[slotIndex]) === 1 ||
|
||||
Number(kueGroundFault?.[slotIndex]) === 1 ||
|
||||
Number(kueAlarm1?.[slotIndex]) === 1 ||
|
||||
Number(kueAlarm2?.[slotIndex]) === 1
|
||||
? "text-red-500 text-[0.875rem]"
|
||||
: Number(kueOverflow?.[slotIndex]) === 1
|
||||
? "text-white text-[0.875rem]"
|
||||
: ""
|
||||
}
|
||||
? "text-red-500"
|
||||
: "text-green-500"
|
||||
}`}
|
||||
>
|
||||
{isoDisplayValue}
|
||||
{Number(kuePSTmMinus96V?.[slotIndex]) === 1
|
||||
? "Messpannung"
|
||||
: Number(kueCableBreak?.[slotIndex]) === 1
|
||||
? "Aderbruch"
|
||||
: Number(kueGroundFault?.[slotIndex]) === 1
|
||||
? "Erdschluss"
|
||||
: Number(kueAlarm1?.[slotIndex]) === 1
|
||||
? "Isolationsfehler"
|
||||
: Number(kueAlarm2?.[slotIndex]) === 1
|
||||
? "Schleifenfehler"
|
||||
: " "}
|
||||
{"\u00A0"}
|
||||
{/* Status: OK*/}
|
||||
</span>
|
||||
{/* 2. Zeile: ISO-Wert, immer anzeigen */}
|
||||
<span
|
||||
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
|
||||
Number(kueAlarm1?.[slotIndex]) === 1 ? "text-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
{isoDisplayValue === "Abgleich"
|
||||
? "ISO: Abgleich"
|
||||
: `ISO: ${Number(isolationswert)
|
||||
.toFixed(2)
|
||||
.replace(".", ",")} MOhm`}
|
||||
</span>
|
||||
{/* 3. Zeile: Schleifenwert, in Rot bei Schleifenfehler, sonst normal */}
|
||||
<span
|
||||
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
|
||||
Number(kueAlarm2?.[slotIndex]) === 1 ? "text-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
{`RSL: ${Number(loopDisplayValue)
|
||||
.toFixed(3)
|
||||
.replace(".", ",")} kOhm`}
|
||||
</span>
|
||||
|
||||
{Number(kuePSTmMinus96V?.[slotIndex]) !== 1 &&
|
||||
Number(kueCableBreak?.[slotIndex]) !== 1 &&
|
||||
Number(kueGroundFault?.[slotIndex]) !== 1 &&
|
||||
Number(kueAlarm1?.[slotIndex]) !== 1 &&
|
||||
Number(kueAlarm2?.[slotIndex]) !== 1 &&
|
||||
Number(kueOverflow?.[slotIndex]) !== 1 && (
|
||||
<div className="text-[0.5rem]">ISO MOhm</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -261,103 +368,132 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
{kueVersion}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex mt-3 space-x-[0.063rem] ">
|
||||
<button
|
||||
onClick={() =>
|
||||
handleButtonClick(
|
||||
"Schleife",
|
||||
setActiveButton,
|
||||
setloopTitleText,
|
||||
(value) =>
|
||||
setLoopDisplayValue(
|
||||
typeof value === "number" ? value : Number(value)
|
||||
), // Hier sicherstellen, dass nur number übergeben wird
|
||||
Number(schleifenwiderstand), // <- Stelle sicher, dass es eine Zahl ist
|
||||
tdrLocation,
|
||||
dispatch,
|
||||
slotIndex
|
||||
)
|
||||
}
|
||||
className={`w-[50%] h-[1.563rem] text-white text-[0.625rem] flex items-center justify-center ${
|
||||
activeButton === "Schleife" ? "bg-littwin-blue" : "bg-gray-400"
|
||||
}`}
|
||||
>
|
||||
Schleife
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveButton("TDR");
|
||||
setloopTitleText("Entfernung [Km]");
|
||||
|
||||
const latestTdrDistanceMeters =
|
||||
Array.isArray(tdmChartData?.[slotIndex]) &&
|
||||
tdmChartData[slotIndex].length > 0 &&
|
||||
typeof tdmChartData[slotIndex][0].d === "number"
|
||||
? tdmChartData[slotIndex][0].d
|
||||
: 0;
|
||||
|
||||
const latestTdrDistance = Number(
|
||||
(latestTdrDistanceMeters / 1000).toFixed(3)
|
||||
);
|
||||
setLoopDisplayValue(latestTdrDistance);
|
||||
}}
|
||||
className={`w-[50%] h-[1.563rem] text-white text-[0.625rem] flex items-center justify-center ${
|
||||
Array.isArray(tdrActive) && tdrActive[slotIndex] === 0
|
||||
? "bg-gray-200 cursor-not-allowed" // Deaktiviert: Hellgrau
|
||||
: activeButton === "TDR"
|
||||
? "bg-littwin-blue" // Aktiviert: Littwin Blau
|
||||
: "bg-gray-400" // Nicht geklickt: Dunkelgrau
|
||||
}`}
|
||||
disabled={Array.isArray(tdrActive) && tdrActive[slotIndex] === 0} // Button deaktiviert, wenn TDR für diesen Slot nicht aktiv ist
|
||||
>
|
||||
TDR
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* loopDisplay: Zeigt Schleifenwiderstand oder TDR-Distanz an, je nach Modus */}
|
||||
<div className="absolute bottom-[0.063rem] left-[0.068rem] w-[7.074rem] h-[6.1rem] bg-gray-300 border-[0.094rem] border-gray-400 p-[0.063rem]">
|
||||
<span className="text-black text-[0.438rem] absolute top-[0.125rem] left-[0.063rem] mt-1">
|
||||
{loopTitleText}
|
||||
{/* Modal für Einstellungen */}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-gray-500">
|
||||
{/* Das soll rausgenommen werden
|
||||
<p>Kein Modul im Slot {slotIndex + 0}</p>
|
||||
*/}
|
||||
</div>
|
||||
)}
|
||||
{/* Messkurven-Button unter dem Modul */}
|
||||
{kueOnline === 1 && (
|
||||
<>
|
||||
{/*
|
||||
|
||||
Überschrift: Detailansicht
|
||||
ISO und RSL als Buttons (Firmenblau) nebeneinander
|
||||
TDR und KVz Buttons (Firmenblau) nebeneinander
|
||||
Wenn kein TDR oder kein KVz: nur grauer Button ohne Text
|
||||
|
||||
|
||||
*/}
|
||||
<div className="flex flex-col items-center w-full px-2 mt-2 space-y-2">
|
||||
{/* Detailansicht Header */}
|
||||
<span className="text-black text-[0.625rem] font-semibold">
|
||||
Detailansicht
|
||||
</span>
|
||||
|
||||
<div className="relative w-full h-[2.813rem] bg-gray-100 border border-gray-400 flex items-center justify-center mt-4">
|
||||
{/* ISO and RSL Buttons */}
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={refreshClick} // Dynamische Funktion basierend auf aktivem Button
|
||||
className="absolute -top-[0.063rem] -right-[0.063rem] w-[1.25rem] h-[1.25rem] bg-gray-400 flex items-center justify-center"
|
||||
disabled={loading} // Disable button while loading
|
||||
onClick={openIsoModal}
|
||||
className="bg-littwin-blue text-white text-[0.625rem] flex items-center justify-center p-2 min-w-[2.5rem]"
|
||||
>
|
||||
<span className="text-white text-[1.125rem]">⟳</span>
|
||||
ISO
|
||||
</button>
|
||||
<button
|
||||
onClick={openRslModal}
|
||||
className="bg-littwin-blue text-white text-[0.625rem] flex items-center justify-center p-2 min-w-[2.5rem]"
|
||||
>
|
||||
RSL
|
||||
</button>
|
||||
|
||||
<div className="absolute bottom-[0.313rem] left-1/2 transform -translate-x-1/2 w-[6.25rem] flex justify-center items-center">
|
||||
<div className="text-center text-black text-[0.625rem]">
|
||||
<p>
|
||||
{loopDisplayValue +
|
||||
(activeButton === "Schleife" ? " KOhm" : " Km")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={openChartModal} // Öffnet das Chart-Modal
|
||||
className="w-full h-[1.563rem] bg-littwin-blue text-white text-[0.625rem] flex items-center justify-center mt-[0.063rem]"
|
||||
>
|
||||
Messkurve
|
||||
</button>
|
||||
{/* TDR and KVz Buttons */}
|
||||
<div className="flex space-x-2 p-1">
|
||||
{/* TDR Button - blau mit Text wenn aktiv, grau ohne Text wenn inaktiv */}
|
||||
<button
|
||||
onClick={isTdrActiveForSlot ? openTdrModal : undefined}
|
||||
className={`${
|
||||
isTdrActiveForSlot
|
||||
? "bg-littwin-blue text-white cursor-pointer"
|
||||
: "bg-gray-400 cursor-default"
|
||||
} text-[0.625rem] flex items-center justify-center p-2 min-w-[2.5rem]`}
|
||||
>
|
||||
{isTdrActiveForSlot ? "TDR" : "\u00A0\u00A0\u00A0"}
|
||||
</button>
|
||||
|
||||
{/* KVz Button - blau mit Text wenn aktiv, grau ohne Text wenn inaktiv */}
|
||||
<button
|
||||
onClick={isKvzActiveForSlot ? openKvzModal : undefined}
|
||||
className={`${
|
||||
isKvzActiveForSlot
|
||||
? "bg-littwin-blue text-white cursor-pointer"
|
||||
: "bg-gray-400 cursor-default"
|
||||
} text-[0.625rem] flex items-center justify-center p-2 min-w-[2.5rem]`}
|
||||
disabled={!isKvzActiveForSlot}
|
||||
title={
|
||||
isKvzActiveForSlot ? "KVZ öffnen" : "KVZ nicht verfügbar"
|
||||
}
|
||||
>
|
||||
{isKvzActiveForSlot ? "KVZ" : "\u00A0\u00A0\u00A0"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Messkurve Button */}
|
||||
|
||||
{/* TDR Messkurve und Schleife Messkurve Buttons */}
|
||||
<div className="flex flex-col space-y-2 w-full"></div>
|
||||
</div>
|
||||
|
||||
{/* Modal für Messkurve */}
|
||||
{showChartModal && (
|
||||
<ChartSwitcher
|
||||
isOpen={showChartModal}
|
||||
onClose={closeChartModal}
|
||||
{/* ISO Chart Modal */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-[0.125rem] bg-gray-400"></div>
|
||||
|
||||
{/* ISO Chart Modal */}
|
||||
<IsoChartView
|
||||
isOpen={showIsoModal}
|
||||
onClose={closeIsoModal}
|
||||
slotIndex={slotIndex}
|
||||
/>
|
||||
|
||||
{/* RSL Chart Modal */}
|
||||
<LoopChartView
|
||||
isOpen={showRslModal}
|
||||
onClose={closeRslModal}
|
||||
slotIndex={slotIndex}
|
||||
/>
|
||||
|
||||
{/* TDR Chart Modal - nur wenn TDR aktiv ist */}
|
||||
{isTdrActiveForSlot && (
|
||||
<TDRChartView
|
||||
isOpen={showTdrModal}
|
||||
onClose={closeTdrModal}
|
||||
slotIndex={slotIndex}
|
||||
/>
|
||||
)}
|
||||
{isKvzActiveForSlot && (
|
||||
<KVZChartView
|
||||
isOpen={showKvzModal}
|
||||
onClose={closeKvzModal}
|
||||
slotIndex={slotIndex}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-gray-500">
|
||||
)}
|
||||
|
||||
{/* Früher inline Panel – jetzt eigenes Modal (KVZChartView) */}
|
||||
{/* {showKvzPanel && isKvzActiveForSlot && (
|
||||
<div className="flex flex-col items-center ">
|
||||
<FallSensors slotIndex={slotIndex} />
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{/* Offline-View */}
|
||||
{kueOnline !== 1 && (
|
||||
<div className="flex items-center justify-center ">
|
||||
{/* Das soll rausgenommen werden
|
||||
<p>Kein Modul im Slot {slotIndex + 0}</p>
|
||||
*/}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useAppSelector } from "@/redux/store";
|
||||
|
||||
export default function SlotActivityOverlay({
|
||||
slotIndex,
|
||||
}: {
|
||||
slotIndex: number;
|
||||
}) {
|
||||
const ksx = useAppSelector((s) => s.deviceEvents.ksx);
|
||||
const ksy = useAppSelector((s) => s.deviceEvents.ksy);
|
||||
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
|
||||
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
|
||||
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
|
||||
const alignmentStartedAt = useAppSelector(
|
||||
(s) => s.deviceEvents.alignmentStartedAt
|
||||
);
|
||||
|
||||
const loopActive = Array.isArray(ksx) && ksx[slotIndex] === 1;
|
||||
const tdrActive = Array.isArray(ksy) && ksy[slotIndex] === 1;
|
||||
const alignActive = Array.isArray(ksz) && ksz[slotIndex] === 1;
|
||||
|
||||
// Progress ticker
|
||||
const [now, setNow] = useState<number>(Date.now());
|
||||
useEffect(() => {
|
||||
const any = loopActive || tdrActive || alignActive;
|
||||
if (!any) return;
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [loopActive, tdrActive, alignActive]);
|
||||
|
||||
const clamp = (v: number, min = 0, max = 1) =>
|
||||
Math.max(min, Math.min(max, v));
|
||||
const compute = (startedAt: number | null, durationMs: number) => {
|
||||
if (!startedAt) return { pct: 0 };
|
||||
const elapsed = now - startedAt;
|
||||
const pct = clamp(elapsed / durationMs) * 100;
|
||||
return { pct };
|
||||
};
|
||||
|
||||
// Durations
|
||||
const LOOP_MS = 2 * 60 * 1000; // ~2 min
|
||||
const TDR_MS = 30 * 1000; // ~30 s
|
||||
const ALIGN_MS = 10 * 60 * 1000; // ~10 min
|
||||
|
||||
if (!loopActive && !tdrActive && !alignActive) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/70 backdrop-blur-sm">
|
||||
<div className="p-2 rounded-md shadow bg-white/90 border border-gray-200 w-[min(90%,12rem)]">
|
||||
<div className="text-[0.75rem] font-semibold mb-2 text-gray-800">
|
||||
Bitte warten…
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{loopActive && (
|
||||
<div>
|
||||
<div className="text-[0.7rem] text-gray-800 mb-1">Schleife</div>
|
||||
{(() => {
|
||||
const { pct } = compute(loopStartedAt, LOOP_MS);
|
||||
return (
|
||||
<div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-littwin-blue transition-all"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[0.65rem] text-gray-700 mt-1">
|
||||
{Math.round(pct)}%
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
{tdrActive && (
|
||||
<div>
|
||||
<div className="text-[0.7rem] text-gray-800 mb-1">TDR</div>
|
||||
{(() => {
|
||||
const { pct } = compute(tdrStartedAt, TDR_MS);
|
||||
return (
|
||||
<div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-littwin-blue transition-all"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[0.65rem] text-gray-700 mt-1">
|
||||
{Math.round(pct)}%
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
{alignActive && (
|
||||
<div>
|
||||
<div className="text-[0.7rem] text-gray-800 mb-1">Abgleich</div>
|
||||
{(() => {
|
||||
const { pct } = compute(alignmentStartedAt, ALIGN_MS);
|
||||
return (
|
||||
<div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-littwin-blue transition-all"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[0.65rem] text-gray-700 mt-1">
|
||||
{Math.round(pct)}%
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +1,32 @@
|
||||
// /komponents/main/kabelueberwachung/kue705FO/handlers/firmwareUpdate.ts
|
||||
const firmwareUpdate = (slot: number) => {
|
||||
// @/components/main/kabelueberwachung/kue705FO/handlers/firmwareUpdate.ts
|
||||
export default async function firmwareUpdate(
|
||||
slot: number
|
||||
): Promise<{ message: string }> {
|
||||
const isDev =
|
||||
typeof window !== "undefined" && window.location.hostname === "localhost";
|
||||
const url = isDev
|
||||
? `${window.location.origin}/api/cpl/kueSingleModuleUpdateMock?slot=${
|
||||
slot + 1
|
||||
}`
|
||||
: `${window.location.origin}/CPL?/kabelueberwachung.html&KSU${slot}=1`;
|
||||
: `${window.location.origin}/CPL?Service/ae.ACP&KSU${slot}=1`;
|
||||
|
||||
fetch(url, { method: "GET" })
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
alert(
|
||||
data.message || `Update an Slot ${slot + 1} erfolgreich gestartet!`
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Fehler:", error);
|
||||
alert("Fehler beim Update!");
|
||||
});
|
||||
};
|
||||
try {
|
||||
const response = await fetch(url, { method: "GET" });
|
||||
|
||||
export default firmwareUpdate;
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fehler: Status ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
//alert(data.message || `Update an Slot ${slot + 1} erfolgreich gestartet!`);
|
||||
const message =
|
||||
data.message || `Update an Slot ${slot + 1} erfolgreich gestartet!`;
|
||||
console.log(message);
|
||||
return { message };
|
||||
} catch (error) {
|
||||
console.error("Fehler:", error);
|
||||
//alert("Fehler beim Update!");
|
||||
return { message: "Fehler beim Update!" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const handleOpenChartModal = (
|
||||
setShowChartModal: Dispatch<SetStateAction<boolean>>,
|
||||
dispatch: ReturnType<typeof useDispatch>,
|
||||
slotIndex: number,
|
||||
activeButton: "Schleife" | "TDR"
|
||||
activeButton: "Schleife" | "TDR" | "ISO"
|
||||
) => {
|
||||
setShowChartModal(true);
|
||||
dispatch(setChartOpen(true));
|
||||
@@ -26,6 +26,8 @@ const handleOpenChartModal = (
|
||||
|
||||
if (activeButton === "TDR") {
|
||||
dispatch(setActiveMode("TDR"));
|
||||
} else if (activeButton === "ISO") {
|
||||
dispatch(setActiveMode("ISO"));
|
||||
} else {
|
||||
dispatch(setActiveMode("Schleife"));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { goLoop } from "@/utils/goLoop";
|
||||
import { goTDR } from "@/utils/goTDR";
|
||||
|
||||
const handleRefreshClick = (
|
||||
activeButton: "Schleife" | "TDR",
|
||||
activeButton: "Schleife" | "TDR" | "ISO",
|
||||
slotIndex: number,
|
||||
setLoading: Dispatch<SetStateAction<boolean>>
|
||||
) => {
|
||||
@@ -13,6 +13,7 @@ const handleRefreshClick = (
|
||||
} else if (activeButton === "TDR") {
|
||||
goTDR(slotIndex, setLoading);
|
||||
}
|
||||
// ISO has no refresh functionality
|
||||
};
|
||||
|
||||
export default handleRefreshClick;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
|
||||
|
||||
const useLoopDisplay = (
|
||||
schleifenwiderstand: number,
|
||||
activeButton: "Schleife" | "TDR"
|
||||
activeButton: "Schleife" | "TDR" | "ISO"
|
||||
) => {
|
||||
const [loopDisplayValue, setLoopDisplayValue] =
|
||||
useState<number>(schleifenwiderstand);
|
||||
@@ -12,6 +12,7 @@ const useLoopDisplay = (
|
||||
if (activeButton === "Schleife") {
|
||||
setLoopDisplayValue(schleifenwiderstand);
|
||||
}
|
||||
// For ISO and TDR, the value is set manually via setLoopDisplayValue
|
||||
}, [schleifenwiderstand, activeButton]);
|
||||
|
||||
return { loopDisplayValue, setLoopDisplayValue };
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
"use client";
|
||||
|
||||
// components/main/kabelueberwachung/kue705FO/modals/KueEinstellung.tsx
|
||||
import { useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import type { RootState } from "../../../../../redux/store";
|
||||
import type { RootState, AppDispatch } from "@/redux/store";
|
||||
import handleSave from "../handlers/handleSave";
|
||||
import handleDisplayEinschalten from "../handlers/handleDisplayEinschalten";
|
||||
import handleDisplayEinschalten from "@/components/main/kabelueberwachung/kue705FO/handlers/handleDisplayEinschalten";
|
||||
import firmwareUpdate from "../handlers/firmwareUpdate";
|
||||
import { useAdminAuth } from "../../../settingsPageComponents/hooks/useAdminAuth";
|
||||
import ProgressModal from "@/components/main/settingsPageComponents/modals/ProgressModal";
|
||||
import { toast } from "react-toastify";
|
||||
import ConfirmModal from "@/components/common/ConfirmModal";
|
||||
import {
|
||||
openConfirmModal,
|
||||
closeConfirmModal,
|
||||
} from "@/redux/slices/confirmModalSlice";
|
||||
import { startFirmwareUpdateThunk } from "@/redux/thunks/startFirmwareUpdateThunk";
|
||||
|
||||
interface Props {
|
||||
slot: number;
|
||||
@@ -15,25 +22,12 @@ interface Props {
|
||||
onModulNameChange?: (id: string) => void;
|
||||
}
|
||||
|
||||
const memoryIntervalOptions = [
|
||||
{ value: 0, label: "Kein" },
|
||||
{ value: 1, label: "1 Minute" },
|
||||
{ value: 5, label: "5 Minuten" },
|
||||
{ value: 10, label: "10 Minuten" },
|
||||
{ value: 15, label: "15 Minuten" },
|
||||
{ value: 30, label: "30 Minuten" },
|
||||
{ value: 60, label: "60 Minuten" },
|
||||
{ value: 360, label: "6 Stunden" },
|
||||
{ value: 720, label: "12 Stunden" },
|
||||
];
|
||||
|
||||
export default function KueEinstellung({
|
||||
slot,
|
||||
|
||||
onClose = () => {},
|
||||
onModulNameChange,
|
||||
}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const {
|
||||
kueID,
|
||||
kueName,
|
||||
@@ -43,18 +37,27 @@ export default function KueEinstellung({
|
||||
kueLoopInterval,
|
||||
memoryInterval,
|
||||
} = useSelector((state: RootState) => state.kueDataSlice);
|
||||
const reduxAdmin = useSelector(
|
||||
(state: RootState) => state.authSlice.isAdminLoggedIn
|
||||
);
|
||||
const [isAdminLoggedIn] = useState(() => reduxAdmin);
|
||||
|
||||
const { isAdminLoggedIn } = useAdminAuth(true);
|
||||
const showConfirmModal = useSelector(
|
||||
(state: RootState) => state.confirmModal.open
|
||||
);
|
||||
|
||||
const formCacheKey = `slot_${slot}`;
|
||||
if (typeof window !== "undefined") {
|
||||
window.__kueCache = window.__kueCache || {};
|
||||
}
|
||||
const cached =
|
||||
typeof window !== "undefined" ? window.__kueCache?.[formCacheKey] : null;
|
||||
const isUpdating = useSelector(
|
||||
(state: RootState) => state.firmwareProgress.isUpdating
|
||||
);
|
||||
const progress = useSelector(
|
||||
(state: RootState) => state.firmwareProgress.progress
|
||||
);
|
||||
|
||||
const [formData, setFormData] = useState(() => {
|
||||
if (cached) return cached;
|
||||
if (typeof window !== "undefined") {
|
||||
const cache = window.__kueCache?.[`slot_${slot}`];
|
||||
if (cache) return cache;
|
||||
}
|
||||
return {
|
||||
kueID: kueID[slot] || "",
|
||||
kueName: kueName[slot] || "",
|
||||
@@ -70,17 +73,12 @@ export default function KueEinstellung({
|
||||
const updated = { ...formData, [key]: value };
|
||||
setFormData(updated);
|
||||
if (typeof window !== "undefined") {
|
||||
window.__kueCache![formCacheKey] = updated;
|
||||
window.__kueCache = window.__kueCache || {};
|
||||
window.__kueCache[`slot_${slot}`] = updated;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveWrapper = async () => {
|
||||
const updatedKueID = [...kueID];
|
||||
//updatedKueID[slot] = formData.kueID;
|
||||
/* if (Object.isFrozen(kueID)) {
|
||||
console.warn("kueID ist readonly!");
|
||||
}
|
||||
*/
|
||||
const updatedKueName = [...kueName];
|
||||
updatedKueName[slot] = formData.kueName;
|
||||
|
||||
@@ -100,7 +98,7 @@ export default function KueEinstellung({
|
||||
updatedMemoryInterval[slot] = Number(formData.memoryInterval);
|
||||
|
||||
const newData = {
|
||||
kueID: updatedKueID[slot],
|
||||
kueID: kueID[slot],
|
||||
kueName: updatedKueName[slot],
|
||||
limit1: updatedLimit1[slot].toString(),
|
||||
delay1: updatedDelay1[slot].toString(),
|
||||
@@ -108,13 +106,11 @@ export default function KueEinstellung({
|
||||
loopInterval: updatedLoopInterval[slot].toString(),
|
||||
memoryInterval: updatedMemoryInterval[slot].toString(),
|
||||
};
|
||||
|
||||
setFormData(newData);
|
||||
if (typeof window !== "undefined") {
|
||||
window.__kueCache![`slot_${slot}`] = newData;
|
||||
}
|
||||
|
||||
// 🔧 handleSave aufrufen mit allen Daten
|
||||
await handleSave({
|
||||
slot,
|
||||
ids: kueID,
|
||||
@@ -122,7 +118,7 @@ export default function KueEinstellung({
|
||||
isolationsgrenzwerte: updatedLimit1,
|
||||
verzoegerung: updatedDelay1,
|
||||
untereSchleifenGrenzwerte: updatedLimit2Low,
|
||||
obereSchleifenGrenzwerte: updatedLimit2Low, // ggf. anpassen, falls du später High-Werte brauchst
|
||||
obereSchleifenGrenzwerte: updatedLimit2Low,
|
||||
schleifenintervall: updatedLoopInterval,
|
||||
speicherintervall: updatedMemoryInterval,
|
||||
originalValues: {
|
||||
@@ -165,20 +161,23 @@ export default function KueEinstellung({
|
||||
/>
|
||||
</div>
|
||||
{/* Speicherintervall */}
|
||||
{/* Speicherintervall */}
|
||||
{/* Speicherintervall */}
|
||||
<div className="mb-4 grid grid-cols-3 items-center gap-2 w-full">
|
||||
<label className="">Speicherintervall:</label>
|
||||
<select
|
||||
className="w-full border rounded p-1"
|
||||
value={formData.memoryInterval}
|
||||
onChange={(e) => handleChange("memoryInterval", e.target.value)}
|
||||
>
|
||||
{memoryIntervalOptions.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<label className="w-48">Speicherintervall:</label>
|
||||
<div className="relative w-36">
|
||||
<input
|
||||
type="number"
|
||||
className="border rounded px-2 py-1 pr-20 w-full text-right"
|
||||
value={formData.memoryInterval}
|
||||
onChange={(e) => handleChange("memoryInterval", e.target.value)}
|
||||
/>
|
||||
<span className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
|
||||
Minuten
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Isolationsmessung */}
|
||||
<div className="mb-4 w-full">
|
||||
<h3 className="font-bold mb-2">Isolationsmessung</h3>
|
||||
@@ -243,22 +242,85 @@ export default function KueEinstellung({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 p-0 rounded">
|
||||
<div className="flex flex-wrap justify-end gap-2 p-0 rounded">
|
||||
{isAdminLoggedIn && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (
|
||||
window.confirm(
|
||||
"Warnung: Das Firmware-Update kann einige Minuten dauern und das Gerät neu starten.\nMöchten Sie wirklich fortfahren?"
|
||||
)
|
||||
) {
|
||||
firmwareUpdate(slot);
|
||||
<>
|
||||
<button
|
||||
onClick={() => dispatch(openConfirmModal())}
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded flex items-center"
|
||||
>
|
||||
Firmware Update
|
||||
</button>
|
||||
|
||||
{/* Konfiguration sichern */}
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await fetch(
|
||||
`/CPL?kabelueberwachung.html&KSB${slot
|
||||
.toString()
|
||||
.padStart(2, "0")}=1`
|
||||
);
|
||||
toast.success("✅ Konfiguration gesichert.");
|
||||
} catch (err) {
|
||||
console.error("KSB Fehler", err);
|
||||
toast.error("❌ Fehler beim Sichern der Konfiguration");
|
||||
}
|
||||
}}
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded flex items-center"
|
||||
>
|
||||
Konfig. sichern
|
||||
</button>
|
||||
|
||||
{/* Konfiguration zurücksichern */}
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await fetch(
|
||||
`/CPL?kabelueberwachung.html&KSR${slot
|
||||
.toString()
|
||||
.padStart(2, "0")}=1`
|
||||
);
|
||||
toast.success("✅ Konfiguration wiederhergestellt.");
|
||||
} catch (err) {
|
||||
console.error("KSR Fehler", err);
|
||||
toast.error(
|
||||
"❌ Fehler beim Wiederherstellen der Konfiguration"
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded flex items-center"
|
||||
>
|
||||
Konfig. zurücksichern
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{showConfirmModal && (
|
||||
<ConfirmModal
|
||||
open={showConfirmModal}
|
||||
title="Firmware-Update starten?"
|
||||
message="⚠️ Das Firmware-Update kann einige Minuten dauern. Möchten Sie wirklich fortfahren?"
|
||||
onCancel={() => dispatch(closeConfirmModal())}
|
||||
onConfirm={async () => {
|
||||
dispatch(closeConfirmModal());
|
||||
toast.info("Firmware-Update gestartet. Bitte warten...");
|
||||
dispatch(startFirmwareUpdateThunk(slot)); // Start Redux-Prozess
|
||||
|
||||
try {
|
||||
await firmwareUpdate(slot);
|
||||
} catch (err) {
|
||||
console.error("Firmware-Update-Fehler:", err);
|
||||
toast.error("❌ Fehler beim Firmwareupdate");
|
||||
}
|
||||
}}
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded flex items-center"
|
||||
>
|
||||
Firmware Update
|
||||
</button>
|
||||
/>
|
||||
)}
|
||||
{isUpdating && (
|
||||
<ProgressModal
|
||||
visible={isUpdating}
|
||||
progress={progress}
|
||||
slot={slot + 1}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleDisplayEinschalten(slot)}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState, useAppDispatch } from "../../../../../redux/store";
|
||||
import { updateKvzData } from "../../../../../redux/thunks/kvzThunks";
|
||||
import { useAdminAuth } from "../../../settingsPageComponents/hooks/useAdminAuth";
|
||||
|
||||
type KvzData = {
|
||||
// Hier können später weitere KVz-spezifische Einstellungen hinzugefügt werden
|
||||
kvzSettings: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
slot: number;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export default function KvzModalView({ slot, onClose }: Props) {
|
||||
const { isAdminLoggedIn } = useAdminAuth(true);
|
||||
const dispatch = useAppDispatch();
|
||||
const kvzSlice = useSelector((state: RootState) => state.kueDataSlice);
|
||||
|
||||
// KVZ System: 32 Slots mit je 4 LEDs
|
||||
const isKvzPresent = kvzSlice.kvzPresence?.[slot] === 1;
|
||||
const isKvzActive = kvzSlice.kvzActive?.[slot] === 1;
|
||||
|
||||
// LED Status für diesen Slot (4 LEDs pro Slot)
|
||||
const getKvzLedStatus = (ledIndex: number) => {
|
||||
const arrayIndex = slot * 4 + ledIndex;
|
||||
return kvzSlice.kvzStatus?.[arrayIndex] === 1;
|
||||
};
|
||||
|
||||
const [localKvzActive, setLocalKvzActive] = useState(() => isKvzActive);
|
||||
|
||||
// Synchronisiere localState mit Redux State
|
||||
React.useEffect(() => {
|
||||
setLocalKvzActive(isKvzActive);
|
||||
}, [isKvzActive]);
|
||||
|
||||
const handleKvzToggle = async () => {
|
||||
const newState = !localKvzActive;
|
||||
setLocalKvzActive(newState);
|
||||
|
||||
try {
|
||||
// API Update mit neuem Thunk - kvzActive statt kvzPresence
|
||||
await dispatch(
|
||||
updateKvzData([{ key: "kvzActive", slot, value: newState ? 1 : 0 }])
|
||||
);
|
||||
|
||||
const msg = newState
|
||||
? "✅ KVz wurde aktiviert."
|
||||
: "⚠️ KVz wurde deaktiviert.";
|
||||
alert(msg);
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
console.error("Fehler beim KVz-Toggle:", error);
|
||||
alert("Fehler beim Umschalten der KVz-Funktion.");
|
||||
// State zurücksetzen bei Fehler
|
||||
setLocalKvzActive(!newState);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 text-sm">
|
||||
{/* KVz-Funktion - nur anzeigen wenn KVZ vorhanden ist */}
|
||||
{isAdminLoggedIn && isKvzPresent && (
|
||||
<div className="mb-4 mt-4 grid grid-cols-3 items-center gap-2 w-full">
|
||||
<span className="text-sm font-medium">KVz-Funktion:</span>
|
||||
<div className="col-span-2 flex items-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={localKvzActive}
|
||||
onClick={handleKvzToggle}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-200 ${
|
||||
localKvzActive ? "bg-littwin-blue" : "bg-gray-300"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform duration-200 ${
|
||||
localKvzActive ? "translate-x-6" : "translate-x-1"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-sm text-gray-600">
|
||||
{localKvzActive ? "aktiviert" : "deaktiviert"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Meldung wenn KVZ nicht vorhanden */}
|
||||
{!isKvzPresent && (
|
||||
<div className="mb-4 mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-yellow-600">⚠️</span>
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>Kein KVZ-Gerät vorhanden</strong>
|
||||
<br />
|
||||
Für Slot {slot + 1} ist kein KVZ-Gerät installiert oder
|
||||
konfiguriert.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Zukünftige KVz-Einstellungen können hier hinzugefügt werden */}
|
||||
{!isAdminLoggedIn && (
|
||||
<div className="mt-6 mb-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
Nur Admin-Benutzer können diese Einstellungen ändern.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Speichern Button */}
|
||||
|
||||
{/* <div className="mt-36">
|
||||
<div className="flex justify-end gap-2 p-3 rounded">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded flex items-center"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { useState, useEffect } from "react";
|
||||
import ReactModal from "react-modal";
|
||||
import KueEinstellung from "./KueEinstellung";
|
||||
import TdrEinstellung from "./TdrEinstellung";
|
||||
import KvzModalView from "./KvzModalView";
|
||||
import Knotenpunkte from "./Knotenpunkte";
|
||||
|
||||
interface KueModalProps {
|
||||
@@ -14,18 +15,20 @@ interface KueModalProps {
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__lastKueTab?: "kue" | "tdr" | "knoten";
|
||||
__lastKueTab?: "kue" | "tdr" | "kvz" | "knoten";
|
||||
kabelModalOpen?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
|
||||
const [activeTab, setActiveTab] = useState<"kue" | "tdr" | "knoten">(() => {
|
||||
if (typeof window !== "undefined" && window.__lastKueTab) {
|
||||
return window.__lastKueTab;
|
||||
const [activeTab, setActiveTab] = useState<"kue" | "tdr" | "kvz" | "knoten">(
|
||||
() => {
|
||||
if (typeof window !== "undefined" && window.__lastKueTab) {
|
||||
return window.__lastKueTab;
|
||||
}
|
||||
return "kue";
|
||||
}
|
||||
return "kue";
|
||||
});
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -38,7 +41,9 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
|
||||
window.kabelModalOpen = showModal;
|
||||
}
|
||||
}, [showModal]);
|
||||
//-----------------------------------------------------
|
||||
|
||||
//------------------------------------------------------
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={showModal}
|
||||
@@ -65,9 +70,7 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
|
||||
}}
|
||||
>
|
||||
<div className="p-2 flex justify-between items-center rounded-t-md">
|
||||
<h2 className="text-base font-bold">
|
||||
Einstellungen Steckplatz {slot + 1}
|
||||
</h2>
|
||||
<h2 className="text-base font-bold">Einstellungen KÜ {slot + 1}</h2>
|
||||
<button onClick={onClose} className="text-2xl hover:text-gray-200">
|
||||
<i className="bi bi-x-circle-fill"></i>
|
||||
</button>
|
||||
@@ -77,6 +80,7 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
|
||||
{[
|
||||
{ label: "Allgemein", key: "kue" as const },
|
||||
{ label: "TDR ", key: "tdr" as const },
|
||||
{ label: "KVz", key: "kvz" as const },
|
||||
{ label: "Knotenpunkte", key: "knoten" as const },
|
||||
].map(({ label, key }) => (
|
||||
<button
|
||||
@@ -105,6 +109,7 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
|
||||
{activeTab === "tdr" && (
|
||||
<TdrEinstellung slot={slot} onClose={onClose} />
|
||||
)}
|
||||
{activeTab === "kvz" && <KvzModalView slot={slot} onClose={onClose} />}
|
||||
{activeTab === "knoten" && (
|
||||
<Knotenpunkte slot={slot} onClose={onClose} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
// components/main/kabelueberwachung/kue705FO/modals/SuccessProgressModal.tsx
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
duration?: number; // in Sekunden
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const SuccessProgressModal: React.FC<Props> = ({
|
||||
visible,
|
||||
duration = 10,
|
||||
onClose,
|
||||
}) => {
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
setProgress(0);
|
||||
const interval = setInterval(() => {
|
||||
setProgress((prev) => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(interval);
|
||||
setTimeout(onClose, 500); // Schließen nach kurzer Verzögerung
|
||||
return 100;
|
||||
}
|
||||
return prev + 100 / duration;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [visible, duration, onClose]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-black bg-opacity-40 flex items-center justify-center">
|
||||
<div className="bg-white p-6 rounded-lg shadow-md text-center w-72">
|
||||
<h2 className="text-lg font-bold text-green-600 mb-4">
|
||||
✅ Firmwareupdate erfolgreich abgeschlossen.
|
||||
</h2>
|
||||
<div className="w-full bg-gray-200 rounded h-3 overflow-hidden">
|
||||
<div
|
||||
className="h-3 bg-green-500 transition-all duration-100"
|
||||
style={{ width: `${progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-sm mt-2">{Math.floor(progress)}%</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuccessProgressModal;
|
||||
@@ -12,8 +12,9 @@ declare global {
|
||||
}
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { RootState } from "../../../../../redux/store";
|
||||
import { setKueData } from "../../../../../redux/slices/kueDataSlice";
|
||||
|
||||
import { useAdminAuth } from "../../../settingsPageComponents/hooks/useAdminAuth";
|
||||
|
||||
@@ -24,6 +25,7 @@ interface Props {
|
||||
|
||||
export default function TdrEinstellung({ slot, onClose }: Props) {
|
||||
const { isAdminLoggedIn } = useAdminAuth(true);
|
||||
const dispatch = useDispatch();
|
||||
const tdrSlice = useSelector((state: RootState) => state.kueDataSlice);
|
||||
|
||||
const cacheKey = `slot_${slot}`;
|
||||
@@ -126,6 +128,11 @@ export default function TdrEinstellung({ slot, onClose }: Props) {
|
||||
setTdrActive(newState);
|
||||
updateCache(tdrData, newState);
|
||||
|
||||
// Redux State sofort aktualisieren für UI-Update
|
||||
const updatedTdrActive = [...(tdrSlice.tdrActive || [])];
|
||||
updatedTdrActive[slot] = newState ? 1 : 0;
|
||||
dispatch(setKueData({ tdrActive: updatedTdrActive }));
|
||||
|
||||
const isDev = window.location.hostname === "localhost";
|
||||
const slotParam = `KTX${slot}=${newState ? 1 : 0}`;
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ const DateRangePickerMeldungen: React.FC<Props> = ({
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="text-sm font-semibold">Von</label>
|
||||
<DatePicker
|
||||
portalId="root-portal" // beliebige ID
|
||||
popperClassName="custom-datepicker-popper"
|
||||
selected={parseISO(fromDate)}
|
||||
onChange={(date) => date && setFromDate(formatDate(date))}
|
||||
selectsStart
|
||||
@@ -42,6 +44,8 @@ const DateRangePickerMeldungen: React.FC<Props> = ({
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="text-sm font-semibold">Bis</label>
|
||||
<DatePicker
|
||||
portalId="root-portal" // beliebige ID
|
||||
popperClassName="custom-datepicker-popper"
|
||||
selected={parseISO(toDate)}
|
||||
onChange={(date) => date && setToDate(formatDate(date))}
|
||||
selectsEnd
|
||||
|
||||
79
components/main/reports/MeldungenTabelle.tsx
Normal file
79
components/main/reports/MeldungenTabelle.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
type Meldung = {
|
||||
t: string;
|
||||
s: number;
|
||||
c: string;
|
||||
m: string;
|
||||
i: string;
|
||||
v: string;
|
||||
};
|
||||
|
||||
export default function MeldungenTabelle({
|
||||
messages,
|
||||
}: {
|
||||
messages: Meldung[];
|
||||
}) {
|
||||
return (
|
||||
<div className="overflow-auto max-h-[80vh]">
|
||||
<table className="min-w-full border">
|
||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10">
|
||||
<tr>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Prio
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Zeitstempel
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Quelle
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Meldung
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{messages.map((msg, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className="hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<div
|
||||
className="w-4 h-4 rounded"
|
||||
style={{ backgroundColor: msg.c }}
|
||||
></div>
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
{new Date(msg.t).toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})}
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
{msg.i}
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
{msg.m}
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
{msg.v}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{messages.length === 0 && (
|
||||
<div className="mt-4 text-center text-gray-500 italic">
|
||||
Keine Meldungen im gewählten Zeitraum vorhanden.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
114
components/main/reports/MeldungenView.tsx
Normal file
114
components/main/reports/MeldungenView.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
// components/main/reports/MeldungenView.tsx
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getMessagesThunk } from "@/redux/thunks/getMessagesThunk";
|
||||
import type { AppDispatch } from "@/redux/store";
|
||||
import type { RootState } from "@/redux/store";
|
||||
import DateRangePickerMeldungen from "./DateRangePickerMeldungen";
|
||||
import MeldungenTabelle from "./MeldungenTabelle";
|
||||
import { Listbox } from "@headlessui/react";
|
||||
|
||||
type Meldung = {
|
||||
t: string;
|
||||
s: number;
|
||||
c: string;
|
||||
m: string;
|
||||
i: string;
|
||||
v: string;
|
||||
};
|
||||
|
||||
export default function MeldungenView() {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const messages = useSelector((state: RootState) => state.messages.data);
|
||||
|
||||
const [sourceFilter, setSourceFilter] = useState("Alle Quellen");
|
||||
|
||||
const today = new Date();
|
||||
const prior30 = new Date();
|
||||
prior30.setDate(today.getDate() - 30);
|
||||
|
||||
const formatDate = (d: Date) => d.toISOString().split("T")[0];
|
||||
|
||||
const [fromDate, setFromDate] = useState<string>(formatDate(prior30));
|
||||
const [toDate, setToDate] = useState<string>(formatDate(today));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getMessagesThunk({ fromDate, toDate }));
|
||||
}, []);
|
||||
|
||||
const filteredMessages =
|
||||
sourceFilter === "Alle Quellen"
|
||||
? messages
|
||||
: messages.filter((m: Meldung) => m.i === sourceFilter);
|
||||
|
||||
const allSources = Array.from(
|
||||
new Set(messages.map((m: Meldung) => m.i))
|
||||
).sort();
|
||||
const sources = ["Alle Quellen", ...allSources];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)]">
|
||||
<h1 className="text-xl font-bold mb-4">Berichte</h1>
|
||||
|
||||
<div className="flex flex-wrap gap-6 mb-6 items-center">
|
||||
<DateRangePickerMeldungen
|
||||
fromDate={fromDate}
|
||||
toDate={toDate}
|
||||
setFromDate={setFromDate}
|
||||
setToDate={setToDate}
|
||||
/>
|
||||
<button
|
||||
onClick={() => dispatch(getMessagesThunk({ fromDate, toDate }))}
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded h-fit"
|
||||
>
|
||||
Anzeigen
|
||||
</button>
|
||||
|
||||
<Listbox value={sourceFilter} onChange={setSourceFilter}>
|
||||
<div className="relative ml-6 w-64">
|
||||
<Listbox.Button className="bg-white text-gray-900 w-full border px-4 py-2 rounded text-left flex justify-between items-center dark:bg-gray-900 dark:text-gray-100">
|
||||
<span>{sourceFilter}</span>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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="bg-white absolute z-50 mt-1 w-full border rounded dark:bg-gray-900">
|
||||
{sources.map((src) => (
|
||||
<Listbox.Option
|
||||
key={src}
|
||||
value={src}
|
||||
className={({ selected, active, disabled }) =>
|
||||
`px-4 py-2 cursor-pointer text-gray-900 dark:text-gray-100 ${
|
||||
selected
|
||||
? "bg-littwin-blue text-white"
|
||||
: active
|
||||
? "bg-blue-100 dark:bg-gray-700 dark:text-white"
|
||||
: disabled
|
||||
? "opacity-50 text-gray-400 dark:text-gray-500 cursor-not-allowed"
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{src}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
|
||||
<MeldungenTabelle messages={filteredMessages} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { useAdminAuth } from "./hooks/useAdminAuth";
|
||||
const DatabaseSettings: React.FC = () => {
|
||||
const { isAdminLoggedIn } = useAdminAuth(true);
|
||||
return (
|
||||
<div className="p-6 bg-gray-100 max-w-5xl mr-auto rounded shadow">
|
||||
<div className="p-6 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto rounded shadow text-gray-900 dark:text-gray-100">
|
||||
<h2 className="text-lg font-bold mb-6">Datenbank Einstellungen</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
|
||||
@@ -13,12 +13,17 @@ import { getSystemSettingsThunk } from "../../../redux/thunks/getSystemSettingsT
|
||||
import handleGeneralSubmit from "./handlers/handleGeneralSubmit";
|
||||
import handleKueFirmwareUpdate from "@/components/main/settingsPageComponents/handlers/handleKueFirmwareUpdate";
|
||||
import { useAdminAuth } from "@/components/main/settingsPageComponents/hooks/useAdminAuth";
|
||||
import ProgressModal from "@/components/main/settingsPageComponents/modals/ProgressModal";
|
||||
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
const GeneralSettings: React.FC = () => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const systemSettings = useSelector(
|
||||
(state: RootState) => state.systemSettingsSlice
|
||||
);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const { isAdminLoggedIn } = useAdminAuth(true);
|
||||
|
||||
@@ -59,7 +64,7 @@ const GeneralSettings: React.FC = () => {
|
||||
}, [systemSettings]);
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-3 bg-gray-100 max-w-5xl mr-auto overflow-y-auto max-h-[calc(100vh-200px)] ">
|
||||
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto overflow-y-auto max-h-[calc(100vh-200px)] dark:text-gray-100 ">
|
||||
<h2 className="text-sm md:text-md font-bold mb-2">
|
||||
Allgemeine Einstellungen
|
||||
</h2>
|
||||
@@ -69,7 +74,7 @@ const GeneralSettings: React.FC = () => {
|
||||
<label className="block text-xs md:text-sm font-medium">Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
|
||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
@@ -82,7 +87,7 @@ const GeneralSettings: React.FC = () => {
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
|
||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
value={mac1}
|
||||
disabled
|
||||
/>
|
||||
@@ -96,7 +101,7 @@ const GeneralSettings: React.FC = () => {
|
||||
<div className="flex flex-row gap-2">
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
|
||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
value={systemUhr.replace(/\s*Uhr$/, "")}
|
||||
disabled
|
||||
/>
|
||||
@@ -115,7 +120,7 @@ const GeneralSettings: React.FC = () => {
|
||||
<label className="block text-xs md:text-sm font-medium">IP:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
|
||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
value={ip}
|
||||
onChange={(e) => setIp(e.target.value)}
|
||||
/>
|
||||
@@ -126,7 +131,7 @@ const GeneralSettings: React.FC = () => {
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
|
||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
value={subnet}
|
||||
onChange={(e) => setSubnet(e.target.value)}
|
||||
/>
|
||||
@@ -137,56 +142,12 @@ const GeneralSettings: React.FC = () => {
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
|
||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
value={gateway}
|
||||
onChange={(e) => setGateway(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Admin Login */}
|
||||
{/*
|
||||
<div className="col-span-2 flex flex-col gap-1">
|
||||
{isAdminLoggedIn ? (
|
||||
<button
|
||||
type="button"
|
||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
onClick={logoutAdmin}
|
||||
>
|
||||
Admin abmelden
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-row gap-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Benutzername"
|
||||
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Passwort"
|
||||
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
onClick={handleLogin}
|
||||
>
|
||||
Admin anmelden
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
*/}
|
||||
|
||||
{/* Feedback */}
|
||||
{/* You can add feedback here if needed */}
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="col-span-2 flex flex-wrap md:justify-between gap-1 mt-2">
|
||||
<button
|
||||
@@ -205,7 +166,45 @@ const GeneralSettings: React.FC = () => {
|
||||
"⚠️ Wollen Sie wirklich ein Firmwareupdate für alle KÜ-Module starten?"
|
||||
);
|
||||
if (confirmed) {
|
||||
handleKueFirmwareUpdate();
|
||||
setIsUpdating(true);
|
||||
setProgress(0);
|
||||
|
||||
const updateDuration = 300; // Sekunden (5 Minuten)
|
||||
const intervalMs = 1000;
|
||||
let elapsed = 0;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
elapsed++;
|
||||
const newProgress = Math.min(
|
||||
(elapsed / updateDuration) * 100,
|
||||
100
|
||||
);
|
||||
setProgress(newProgress);
|
||||
if (elapsed >= updateDuration) {
|
||||
clearInterval(interval);
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, intervalMs);
|
||||
|
||||
handleKueFirmwareUpdate()
|
||||
.then(() => {
|
||||
clearInterval(interval);
|
||||
setProgress(100);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsUpdating(false);
|
||||
setProgress(100);
|
||||
|
||||
setTimeout(() => {
|
||||
alert("✅ Firmwareupdate erfolgreich abgeschlossen.");
|
||||
}, 300); // Nach Modal-Schließung
|
||||
}, 500);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Update-Fehler:", error);
|
||||
clearInterval(interval);
|
||||
setIsUpdating(false);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -213,6 +212,8 @@ const GeneralSettings: React.FC = () => {
|
||||
</button>
|
||||
)}
|
||||
|
||||
<ProgressModal visible={isUpdating} progress={progress} />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
|
||||
@@ -27,7 +27,7 @@ const NTPSettings: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-3 bg-gray-100 max-w-5xl mr-auto">
|
||||
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100">
|
||||
<h2 className="text-sm md:text-md font-bold mb-4">NTP Einstellungen</h2>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
@@ -35,7 +35,7 @@ const NTPSettings: React.FC = () => {
|
||||
<label className="block text-xs font-medium">NTP Server 1</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
|
||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
value={ntp1}
|
||||
onChange={(e) => setNtp1(e.target.value)}
|
||||
/>
|
||||
@@ -45,7 +45,7 @@ const NTPSettings: React.FC = () => {
|
||||
<label className="block text-xs font-medium">NTP Server 2</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
|
||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
value={ntp2}
|
||||
onChange={(e) => setNtp2(e.target.value)}
|
||||
/>
|
||||
@@ -55,7 +55,7 @@ const NTPSettings: React.FC = () => {
|
||||
<label className="block text-xs font-medium">NTP Server 3</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
|
||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
value={ntp3}
|
||||
onChange={(e) => setNtp3(e.target.value)}
|
||||
/>
|
||||
@@ -65,18 +65,19 @@ const NTPSettings: React.FC = () => {
|
||||
<label className="block text-xs font-medium">Zeitzone</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
|
||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
value={ntpTimezone}
|
||||
onChange={(e) => setNtpTimezone(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 flex items-center gap-2 mt-2">
|
||||
<label className="text-xs font-medium">NTP aktiv:</label>
|
||||
<label className="text-xs font-medium ">NTP aktiv:</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={active}
|
||||
onChange={(e) => setActive(e.target.checked)}
|
||||
className="accent-littwin-blue w-4 h-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ export default function OPCUAInterfaceSettings() {
|
||||
(state: RootState) => state.opcuaSettingsSlice
|
||||
);
|
||||
|
||||
// Anzahl der aktuellen OPC-Clients (Mock, bis Backend liefert)
|
||||
const opcUaActiveClientCount = opcuaSettings.opcUaActiveClientCount ?? 3; // 3 als Beispielwert
|
||||
|
||||
// Lokale Zustände für das neue Benutzerformular
|
||||
|
||||
const [nodesetName, setNodesetName] = useState(
|
||||
@@ -18,7 +21,7 @@ export default function OPCUAInterfaceSettings() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-3 bg-gray-100 max-w-5xl mr-auto ">
|
||||
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100 ">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<Image
|
||||
src="/images/OPCUA.jpg"
|
||||
@@ -71,7 +74,7 @@ export default function OPCUAInterfaceSettings() {
|
||||
{/* ✅ OPCUA Zustand */}
|
||||
<div className="mb-3">
|
||||
<label className="block font-medium text-sm mb-1">OPCUA Zustand</label>
|
||||
<div className="p-1 border border-gray-300 rounded-md bg-white text-sm">
|
||||
<div className="p-1 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-sm text-gray-900 dark:text-gray-100">
|
||||
{opcuaSettings.opcUaZustand}
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,22 +85,32 @@ export default function OPCUAInterfaceSettings() {
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-grow p-1 border border-gray-300 rounded-l-md text-sm"
|
||||
className="flex-grow p-1 border border-gray-300 dark:border-gray-700 rounded-l-md text-sm !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
value={nodesetName}
|
||||
onChange={(e) => setNodesetName(e.target.value)}
|
||||
disabled={opcuaSettings.isEnabled} // Disable input when server is enabled
|
||||
/>
|
||||
{/*
|
||||
<button
|
||||
onClick={handleNodesetUpdate}
|
||||
className="px-3 py-1 bg-littwin-blue text-white rounded-r-md text-sm"
|
||||
>
|
||||
Übernehmen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNodesetUpdate}
|
||||
className="px-3 py-1 bg-littwin-blue text-white rounded-r-md text-sm"
|
||||
>
|
||||
Übernehmen
|
||||
</button>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ✅ Anzahl der aktuellen OPC-Clients */}
|
||||
<div className="mb-3">
|
||||
<label className="block font-medium text-sm mb-1">
|
||||
Aktuelle OPC-Clients
|
||||
</label>
|
||||
<div className="p-1 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-sm text-gray-900 dark:text-gray-100">
|
||||
{opcUaActiveClientCount}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ✅ Benutzerverwaltung */}
|
||||
{/*
|
||||
|
||||
|
||||
85
components/main/settingsPageComponents/SettingsView.tsx
Normal file
85
components/main/settingsPageComponents/SettingsView.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
// components/main/settingsPageComponents/SettingsView.tsx
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useAppDispatch } from "@/redux/store";
|
||||
import { getSystemSettingsThunk } from "@/redux/thunks/getSystemSettingsThunk";
|
||||
import GeneralSettings from "./GeneralSettings";
|
||||
import OPCUAInterfaceSettings from "./OPCUAInterfaceSettings";
|
||||
import DatabaseSettings from "./DatabaseSettings";
|
||||
import NTPSettings from "./NTPSettings";
|
||||
import UserManagementSettings from "./UserManagementSettings";
|
||||
|
||||
export default function SettingsView() {
|
||||
const [activeTab, setActiveTab] = useState("tab1");
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getSystemSettingsThunk());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex border-b border-gray-200">
|
||||
<button
|
||||
className={`px-4 py-2 ${
|
||||
activeTab === "tab1"
|
||||
? "border-b-2 border-littwin-blue text-littwin-blue"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("tab1")}
|
||||
>
|
||||
Allgemeine Einstellungen
|
||||
</button>
|
||||
<button
|
||||
className={`px-4 py-2 ${
|
||||
activeTab === "tab2"
|
||||
? "border-b-2 border-littwin-blue text-littwin-blue"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("tab2")}
|
||||
>
|
||||
OPCUA
|
||||
</button>
|
||||
<button
|
||||
className={`px-4 py-2 ${
|
||||
activeTab === "tab3"
|
||||
? "border-b-2 border-littwin-blue text-littwin-blue"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("tab3")}
|
||||
>
|
||||
Datenbank
|
||||
</button>
|
||||
<button
|
||||
className={`px-4 py-2 ${
|
||||
activeTab === "tab4"
|
||||
? "border-b-2 border-littwin-blue text-littwin-blue"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("tab4")}
|
||||
>
|
||||
NTP
|
||||
</button>
|
||||
<button
|
||||
className={`px-4 py-2 ${
|
||||
activeTab === "tab5"
|
||||
? "border-b-2 border-littwin-blue text-littwin-blue"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("tab5")}
|
||||
>
|
||||
Benutzerverwaltung
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
{activeTab === "tab1" && <GeneralSettings />}
|
||||
{activeTab === "tab2" && <OPCUAInterfaceSettings />}
|
||||
{activeTab === "tab3" && <DatabaseSettings />}
|
||||
{activeTab === "tab4" && <NTPSettings />}
|
||||
{activeTab === "tab5" && <UserManagementSettings />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -31,8 +31,14 @@ const UserManagementSettings: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
handleLogin();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-3 bg-gray-100 max-w-5xl mr-auto">
|
||||
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100">
|
||||
<h2 className="text-sm md:text-md font-bold mb-4">Login Admin-Bereich</h2>
|
||||
|
||||
{/* Admin Login/Logout */}
|
||||
@@ -51,16 +57,18 @@ const UserManagementSettings: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Benutzername"
|
||||
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
|
||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Passwort"
|
||||
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
|
||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -11,7 +11,7 @@ const handleKueFirmwareUpdate = async () => {
|
||||
const result = await res.text();
|
||||
|
||||
console.log("Firmwareupdate gesendet:", result);
|
||||
alert("Firmwareupdate wurde an alle KÜ-Module gesendet.");
|
||||
// alert("Firmwareupdate wurde an alle KÜ-Module gesendet.");
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Firmwareupdate:", error);
|
||||
alert("Fehler beim Firmwareupdate.");
|
||||
|
||||
@@ -29,6 +29,17 @@ export function useAdminAuth(showModal: boolean) {
|
||||
function logoutAdmin() {
|
||||
sessionStorage.removeItem("token");
|
||||
localStorage.setItem("isAdminLoggedIn", "false");
|
||||
|
||||
// KVz localStorage-Werte löschen für alle Slots
|
||||
const keysToRemove = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.startsWith("kvz_slot_")) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
||||
|
||||
setAdminLoggedIn(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
// @/components/main/settingsPageComponents/modals/ProgressModal.tsx
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
progress: number;
|
||||
slot?: number;
|
||||
};
|
||||
|
||||
const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 ">
|
||||
<div className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 p-6 rounded shadow-md text-center w-80">
|
||||
{/*
|
||||
<h2 className="text-lg font-bold mb-4">
|
||||
Firmwareupdate
|
||||
{typeof slot === "number" ? ` KÜ ${slot}` : ""} läuft ...
|
||||
</h2>
|
||||
*/}
|
||||
<h2 className="text-lg font-bold mb-4">
|
||||
Firmwareupdate läuft ...
|
||||
{typeof slot === "number" ? ` ` : ""}
|
||||
</h2>
|
||||
Bitte Fenster nicht schließen
|
||||
<h2></h2>
|
||||
<div className="w-full bg-gray-200 rounded-full h-4">
|
||||
<div
|
||||
className="bg-littwin-blue h-4 rounded-full transition-all duration-100"
|
||||
style={{ width: `${progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<p className="mt-4 text-sm">{Math.round(progress)}% abgeschlossen</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgressModal;
|
||||
490
components/main/system/DetailModal.tsx
Normal file
490
components/main/system/DetailModal.tsx
Normal file
@@ -0,0 +1,490 @@
|
||||
"use client";
|
||||
// /components/main/system/DetailModal.tsx
|
||||
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 { setFullScreen } 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";
|
||||
import { getSystemspannung98VminusThunk } from "@/redux/thunks/getSystemspannung98VminusThunk";
|
||||
import { getTemperaturAdWandlerThunk } from "@/redux/thunks/getTemperaturAdWandlerThunk";
|
||||
import { getTemperaturProzessorThunk } from "@/redux/thunks/getTemperaturProzessorThunk";
|
||||
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
LineElement,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
TimeScale,
|
||||
} from "chart.js";
|
||||
|
||||
import "chartjs-adapter-date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
|
||||
ChartJS.register(
|
||||
LineElement,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
TimeScale
|
||||
);
|
||||
|
||||
// Tailwind-basierte Farbdefinitionen für Chart.js
|
||||
const chartColors = {
|
||||
gray: {
|
||||
line: "#6B7280", // tailwind gray-500
|
||||
background: "rgba(107, 114, 128, 0.2)", // tailwind gray-500 mit opacity
|
||||
},
|
||||
littwinBlue: {
|
||||
line: "#00AEEF", // littwin-blue
|
||||
background: "rgba(0, 174, 239, 0.2)", // littwin-blue mit opacity
|
||||
},
|
||||
};
|
||||
|
||||
type ReduxDataEntry = {
|
||||
//Alle DIA0 t,m,i,a , DIA1 und DIA2 t,i,a,g
|
||||
t: string; // Zeitstempel
|
||||
i: number; // Minimum
|
||||
a: number; // Maximum
|
||||
g?: number; // Durchschnitt (optional, falls vorhanden)
|
||||
m?: number; // aktueller Messwert (optional, falls vorhanden)
|
||||
};
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: "top" as const },
|
||||
title: {
|
||||
display: true,
|
||||
text: "Verlauf",
|
||||
},
|
||||
tooltip: {
|
||||
mode: "index" as const,
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
label: function (ctx: any) {
|
||||
return `Messwert: ${ctx.parsed.y}`;
|
||||
},
|
||||
title: function (items: any[]) {
|
||||
const date = items[0].parsed.x;
|
||||
return `Zeitpunkt: ${new Date(date).toLocaleString("de-DE")}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
zoom: {
|
||||
pan: { enabled: true, mode: "x" as const },
|
||||
zoom: {
|
||||
wheel: { enabled: true },
|
||||
pinch: { enabled: true },
|
||||
mode: "x" as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "time" as const,
|
||||
time: {
|
||||
unit: "day" as const,
|
||||
tooltipFormat: "dd.MM.yyyy HH:mm",
|
||||
displayFormats: {
|
||||
day: "dd.MM.yyyy",
|
||||
},
|
||||
},
|
||||
adapters: {
|
||||
date: { locale: de },
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Zeit",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: "Messwert",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
selectedKey: string | null;
|
||||
onClose: () => void;
|
||||
zeitraum: "DIA0" | "DIA1" | "DIA2";
|
||||
setZeitraum: (typ: "DIA0" | "DIA1" | "DIA2") => void;
|
||||
};
|
||||
|
||||
export const DetailModal = ({
|
||||
isOpen,
|
||||
selectedKey,
|
||||
onClose,
|
||||
zeitraum,
|
||||
setZeitraum,
|
||||
}: Props) => {
|
||||
// Stable empty reference to avoid React-Redux dev warning about selector returning new [] each call
|
||||
const EMPTY_REDUX_DATA: ReadonlyArray<ReduxDataEntry> = Object.freeze([]);
|
||||
const chartRef = useRef<any>(null);
|
||||
const [chartData, setChartData] = useState<any>({
|
||||
datasets: [],
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [shouldUpdateChart, setShouldUpdateChart] = useState(false);
|
||||
const [forceUpdate, setForceUpdate] = useState(0); // Für periodische UI-Updates
|
||||
|
||||
const reduxData = useSelector((state: RootState) => {
|
||||
switch (selectedKey) {
|
||||
case "+5V":
|
||||
return state.systemspannung5Vplus[zeitraum];
|
||||
case "+15V":
|
||||
return state.systemspannung15Vplus[zeitraum];
|
||||
case "-15V":
|
||||
return state.systemspannung15Vminus[zeitraum];
|
||||
case "-98V":
|
||||
return state.systemspannung98Vminus[zeitraum];
|
||||
case "ADC Temp":
|
||||
return state.temperaturAdWandler[zeitraum];
|
||||
case "CPU Temp":
|
||||
return state.temperaturProzessor[zeitraum];
|
||||
default:
|
||||
return EMPTY_REDUX_DATA;
|
||||
}
|
||||
}) as ReduxDataEntry[];
|
||||
|
||||
const isFullScreen = useSelector(
|
||||
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// API-Request beim Klick auf "Daten laden" - memoized für useEffect dependency
|
||||
const handleFetchData = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
// Clear previous chart data
|
||||
setChartData({ datasets: [] });
|
||||
|
||||
// Flag setzen, dass Chart nach Datenempfang aktualisiert werden soll
|
||||
setShouldUpdateChart(true);
|
||||
|
||||
switch (selectedKey) {
|
||||
case "+5V":
|
||||
dispatch(getSystemspannung5VplusThunk(zeitraum));
|
||||
break;
|
||||
case "+15V":
|
||||
dispatch(getSystemspannung15VplusThunk(zeitraum));
|
||||
break;
|
||||
case "-15V":
|
||||
dispatch(getSystemspannung15VminusThunk(zeitraum));
|
||||
break;
|
||||
case "-98V":
|
||||
dispatch(getSystemspannung98VminusThunk(zeitraum));
|
||||
break;
|
||||
case "ADC Temp":
|
||||
dispatch(getTemperaturAdWandlerThunk(zeitraum));
|
||||
break;
|
||||
case "CPU Temp":
|
||||
dispatch(getTemperaturProzessorThunk(zeitraum));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [selectedKey, zeitraum, dispatch]);
|
||||
|
||||
// Reset Zeitraum auf DIA0 und Datumswerte wenn Modal geöffnet wird
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setZeitraum("DIA0");
|
||||
// Reset DateRangePicker to its defaults (it sets 30 days → today on mount)
|
||||
dispatch(resetDateRange());
|
||||
|
||||
// Chart-Daten zurücksetzen beim Öffnen
|
||||
setChartData({ datasets: [] });
|
||||
}
|
||||
}, [isOpen, setZeitraum, dispatch]);
|
||||
|
||||
// Periodische UI-Updates alle 2 Sekunden während Wartezeit
|
||||
useEffect(() => {
|
||||
if (isOpen && (!chartData.datasets || chartData.datasets.length === 0)) {
|
||||
const interval = setInterval(() => {
|
||||
setForceUpdate((prev) => prev + 1); // Force re-render für cursor-wait Update
|
||||
}, 2000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [isOpen, chartData.datasets]);
|
||||
|
||||
// Automatisches "Daten laden" alle 4 Sekunden, maximal 2 Versuche
|
||||
useEffect(() => {
|
||||
if (isOpen && (!chartData.datasets || chartData.datasets.length === 0)) {
|
||||
let attempts = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (attempts < 2) {
|
||||
console.log("Auto-clicking 'Daten laden' button...");
|
||||
handleFetchData();
|
||||
attempts++;
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 4000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [isOpen, chartData.datasets, handleFetchData]);
|
||||
|
||||
const toggleFullScreen = () => {
|
||||
dispatch(setFullScreen(!isFullScreen));
|
||||
setTimeout(() => {
|
||||
chartRef.current?.resize();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(setFullScreen(false));
|
||||
dispatch(resetDateRange());
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadZoomPlugin = async () => {
|
||||
if (typeof window !== "undefined") {
|
||||
const zoomPlugin = (await import("chartjs-plugin-zoom")).default;
|
||||
if (!ChartJS.registry.plugins.get("zoom")) {
|
||||
ChartJS.register(zoomPlugin);
|
||||
}
|
||||
}
|
||||
};
|
||||
loadZoomPlugin();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (chartRef.current && selectedKey) {
|
||||
chartRef.current.options.plugins.title.text = `Verlauf ${selectedKey}`;
|
||||
chartRef.current.update("none");
|
||||
}
|
||||
}, [selectedKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (chartRef.current) {
|
||||
chartRef.current.resetZoom();
|
||||
}
|
||||
}, [zeitraum]);
|
||||
|
||||
// Chart.js animation complete callback to set isLoading false
|
||||
useEffect(() => {
|
||||
if (chartRef.current && isLoading) {
|
||||
const chartInstance = chartRef.current;
|
||||
// Save previous callback to restore later
|
||||
const prevCallback = chartInstance.options.animation?.onComplete;
|
||||
chartInstance.options.animation = {
|
||||
...chartInstance.options.animation,
|
||||
onComplete: () => {
|
||||
setIsLoading(false);
|
||||
if (typeof prevCallback === "function") prevCallback();
|
||||
},
|
||||
};
|
||||
chartInstance.update();
|
||||
}
|
||||
}, [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) {
|
||||
// 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 = filtered.some(
|
||||
(entry) => entry.i !== undefined && entry.i !== null && entry.i !== 0
|
||||
);
|
||||
const hasMaximum = filtered.some(
|
||||
(entry) => entry.a !== undefined && entry.a !== null
|
||||
);
|
||||
const hasAverage = filtered.some(
|
||||
(entry) => entry.g !== undefined && entry.g !== null
|
||||
);
|
||||
const hasCurrent = filtered.some(
|
||||
(entry) => entry.m !== undefined && entry.m !== null
|
||||
);
|
||||
|
||||
// Zuerst Hintergrund-Linien (Minimum/Maximum) - grau
|
||||
if (hasMinimum) {
|
||||
datasets.push({
|
||||
label: "Messwert Minimum",
|
||||
data: filtered.map((entry) => ({
|
||||
x: new Date(entry.t).getTime(),
|
||||
y: entry.i || 0,
|
||||
})),
|
||||
borderColor: "gray",
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
order: 1,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasMaximum) {
|
||||
datasets.push({
|
||||
label: "Messwert Maximum",
|
||||
data: filtered.map((entry) => ({
|
||||
x: new Date(entry.t).getTime(),
|
||||
y: entry.a || 0,
|
||||
})),
|
||||
borderColor: "gray",
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
order: 3,
|
||||
});
|
||||
}
|
||||
|
||||
// Dann Vordergrund-Linien (Durchschnitt/Messwert) - littwin-blue
|
||||
if (hasAverage) {
|
||||
datasets.push({
|
||||
label: "Durchschnitt",
|
||||
data: filtered.map((entry) => ({
|
||||
x: new Date(entry.t).getTime(),
|
||||
y: entry.g || 0,
|
||||
})),
|
||||
borderColor: chartColors.littwinBlue.line,
|
||||
backgroundColor: chartColors.littwinBlue.background,
|
||||
tension: 0.1,
|
||||
fill: false,
|
||||
order: 2,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasCurrent) {
|
||||
datasets.push({
|
||||
label: "Messwert",
|
||||
data: filtered.map((entry) => ({
|
||||
x: new Date(entry.t).getTime(),
|
||||
y: entry.m || 0,
|
||||
})),
|
||||
borderColor: chartColors.littwinBlue.line,
|
||||
backgroundColor: chartColors.littwinBlue.background,
|
||||
tension: 0.1,
|
||||
fill: false,
|
||||
order: 2,
|
||||
});
|
||||
}
|
||||
|
||||
const newChartData = {
|
||||
datasets: datasets,
|
||||
};
|
||||
|
||||
console.log("Chart datasets:", datasets.length, "lines");
|
||||
setChartData(newChartData);
|
||||
setShouldUpdateChart(false); // Reset flag
|
||||
} else if (shouldUpdateChart && (!reduxData || reduxData.length === 0)) {
|
||||
console.log("No Redux data available");
|
||||
setChartData({ datasets: [] });
|
||||
setShouldUpdateChart(false); // Reset flag
|
||||
}
|
||||
}, [reduxData, selectedKey, shouldUpdateChart]);
|
||||
|
||||
if (!isOpen || !selectedKey) return null;
|
||||
|
||||
// Prüfen ob Chart Daten haben (für cursor-wait)
|
||||
const hasChartData = chartData.datasets && chartData.datasets.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50 ${
|
||||
!hasChartData ? "cursor-wait" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`bg-white p-6 rounded-xl overflow-auto shadow-2xl transition-all duration-300 ${
|
||||
isFullScreen ? "w-[95vw] h-[90vh]" : "w-[50%] h-[60%]"
|
||||
} ${!hasChartData ? "cursor-wait" : ""}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<h2 className="text-xl font-semibold">
|
||||
Detailansicht: {selectedKey}
|
||||
</h2>
|
||||
|
||||
<div className="absolute top-0 right-0 flex gap-3">
|
||||
<button
|
||||
onClick={toggleFullScreen}
|
||||
className="text-2xl text-gray-600 hover:text-gray-800"
|
||||
>
|
||||
<i
|
||||
className={
|
||||
isFullScreen
|
||||
? "bi bi-fullscreen-exit"
|
||||
: "bi bi-arrows-fullscreen"
|
||||
}
|
||||
></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="text-2xl text-gray-600 hover:text-gray-800"
|
||||
>
|
||||
<i className="bi bi-x-circle-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SystemChartActionBar
|
||||
zeitraum={zeitraum}
|
||||
setZeitraum={setZeitraum}
|
||||
onFetchData={handleFetchData}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
96
components/main/system/SystemChartActionBar.tsx
Normal file
96
components/main/system/SystemChartActionBar.tsx
Normal 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;
|
||||
185
components/main/system/SystemCharts.tsx
Normal file
185
components/main/system/SystemCharts.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
// components/main/system/SystemCharts.tsx
|
||||
|
||||
import React from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from "chart.js";
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
);
|
||||
|
||||
export type HistoryEntry = {
|
||||
time: string | number | Date;
|
||||
"+5V": number;
|
||||
"+15V": number;
|
||||
"-15V": number;
|
||||
"-98V": number;
|
||||
"ADC Temp": number;
|
||||
"CPU Temp": number;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
history: HistoryEntry[];
|
||||
zeitraum: "DIA0" | "DIA1" | "DIA2";
|
||||
};
|
||||
export const SystemCharts = ({ history }: Props) => {
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const reversedHistory = [...history].reverse();
|
||||
const labels = reversedHistory.map((h) =>
|
||||
new Date(h.time).toLocaleTimeString()
|
||||
);
|
||||
|
||||
const formatValue = (v: number) => v.toFixed(2);
|
||||
|
||||
// Chart.js animation callback
|
||||
const animation = {
|
||||
onComplete: () => {
|
||||
setIsLoading(false);
|
||||
},
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsLoading(true);
|
||||
}, [history]);
|
||||
|
||||
const baseOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
grid: { color: "rgba(200,200,200,0.2)" },
|
||||
title: { display: true, text: "Wert" },
|
||||
},
|
||||
x: {
|
||||
grid: { color: "rgba(200,200,200,0.2)" },
|
||||
title: { display: true, text: "Zeit" },
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: { position: "bottom" as const },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`grid grid-cols-1 xl:grid-cols-2 gap-8 ${
|
||||
isLoading ? "cursor-wait" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="h-[300px]">
|
||||
<Line
|
||||
data={{
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "+5V",
|
||||
data: history.map((h) => formatValue(h["+5V"])),
|
||||
borderColor: "rgba(59,130,246,1)",
|
||||
backgroundColor: "rgba(59,130,246,0.5)",
|
||||
fill: false,
|
||||
},
|
||||
{
|
||||
label: "+15V",
|
||||
data: history.map((h) => formatValue(h["+15V"])),
|
||||
borderColor: "rgba(34,197,94,1)",
|
||||
backgroundColor: "rgba(34,197,94,0.5)",
|
||||
fill: false,
|
||||
},
|
||||
{
|
||||
label: "-15V",
|
||||
data: history.map((h) => formatValue(h["-15V"])),
|
||||
borderColor: "rgba(239,68,68,1)",
|
||||
backgroundColor: "rgba(239,68,68,0.5)",
|
||||
fill: false,
|
||||
},
|
||||
{
|
||||
label: "-98V",
|
||||
data: history.map((h) => formatValue(h["-98V"])),
|
||||
borderColor: "rgba(234,179,8,1)",
|
||||
backgroundColor: "rgba(234,179,8,0.5)",
|
||||
fill: false,
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
...baseOptions,
|
||||
scales: {
|
||||
...baseOptions.scales,
|
||||
y: {
|
||||
...baseOptions.scales.y,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Spannung (V)", // 👉 Einheit hinzugefügt
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
...baseOptions.plugins,
|
||||
title: { display: true, text: "Systemspannungen" },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-[300px]">
|
||||
<Line
|
||||
data={{
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "ADC Temp",
|
||||
data: history.map((h) => h["ADC Temp"]),
|
||||
borderColor: "rgba(168,85,247,1)",
|
||||
backgroundColor: "rgba(168,85,247,0.5)",
|
||||
fill: false,
|
||||
},
|
||||
{
|
||||
label: "CPU Temp",
|
||||
data: history.map((h) =>
|
||||
parseFloat(formatValue(h["CPU Temp"]))
|
||||
),
|
||||
borderColor: "rgba(251,191,36,1)",
|
||||
backgroundColor: "rgba(251,191,36,0.5)",
|
||||
fill: false,
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
...baseOptions,
|
||||
scales: {
|
||||
...baseOptions.scales,
|
||||
y: {
|
||||
...baseOptions.scales.y,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Temperatur (°C)", // 👉 Einheit hinzugefügt
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
...baseOptions.plugins,
|
||||
title: { display: true, text: "Systemtemperaturen" },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
36
components/main/system/SystemOverviewGrid.tsx
Normal file
36
components/main/system/SystemOverviewGrid.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
// components/main/system/SystemOverviewGrid.tsx
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
voltages: Record<string, number>;
|
||||
onOpenDetail: (key: string) => void;
|
||||
};
|
||||
|
||||
export const SystemOverviewGrid = ({ voltages, onOpenDetail }: Props) => {
|
||||
const formatValue = (value: number) => value.toFixed(2);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 mb-2">
|
||||
{Object.entries(voltages).map(([key, value]) => {
|
||||
const unit = key.includes("Temp") ? "\u00b0C" : "V";
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="p-4 border rounded shadow bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<h2 className="font-semibold">{key}</h2>
|
||||
<p>
|
||||
{formatValue(value)} {unit}
|
||||
<button
|
||||
onClick={() => onOpenDetail(key)}
|
||||
className="ml-2 text-littwin-blue hover:underline text-sm"
|
||||
>
|
||||
Detailansicht
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
109
components/main/system/SystemView.tsx
Normal file
109
components/main/system/SystemView.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
// components/main/system/system.tsx
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AppDispatch, RootState } from "@/redux/store";
|
||||
import { getSystemVoltTempThunk } from "@/redux/thunks/getSystemVoltTempThunk";
|
||||
import { SystemOverviewGrid } from "@/components/main/system/SystemOverviewGrid";
|
||||
import { SystemCharts } from "@/components/main/system/SystemCharts";
|
||||
import { DetailModal } from "@/components/main/system/DetailModal";
|
||||
import type { HistoryEntry } from "@/components/main/system/SystemCharts";
|
||||
import { getSystemspannung5VplusThunk } from "@/redux/thunks/getSystemspannung5VplusThunk";
|
||||
import { getSystemspannung15VplusThunk } from "@/redux/thunks/getSystemspannung15VplusThunk";
|
||||
import { getSystemspannung15VminusThunk } from "@/redux/thunks/getSystemspannung15VminusThunk";
|
||||
import { getSystemspannung98VminusThunk } from "@/redux/thunks/getSystemspannung98VminusThunk";
|
||||
import { getTemperaturAdWandlerThunk } from "@/redux/thunks/getTemperaturAdWandlerThunk";
|
||||
import { getTemperaturProzessorThunk } from "@/redux/thunks/getTemperaturProzessorThunk";
|
||||
import { ClipLoader } from "react-spinners";
|
||||
|
||||
const SystemPage = () => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const voltages = useSelector(
|
||||
(state: RootState) => state.systemVoltTemp.voltages
|
||||
);
|
||||
|
||||
const history = useSelector(
|
||||
(state: RootState) => state.systemVoltTemp.history
|
||||
) as HistoryEntry[];
|
||||
const isLoading = !history.length || Object.keys(voltages).length === 0;
|
||||
|
||||
const [selectedKey, setSelectedKey] = useState<string | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [zeitraum, setZeitraum] = useState<"DIA0" | "DIA1" | "DIA2">("DIA1");
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getSystemVoltTempThunk());
|
||||
|
||||
const interval = setInterval(() => {
|
||||
dispatch(getSystemVoltTempThunk());
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleOpenDetail = (key: string) => {
|
||||
setSelectedKey(key);
|
||||
setIsModalOpen(true);
|
||||
switch (key) {
|
||||
case "+5V":
|
||||
dispatch(getSystemspannung5VplusThunk(zeitraum));
|
||||
break;
|
||||
case "+15V":
|
||||
dispatch(getSystemspannung15VplusThunk(zeitraum));
|
||||
break;
|
||||
case "-15V":
|
||||
dispatch(getSystemspannung15VminusThunk(zeitraum));
|
||||
break;
|
||||
case "-98V":
|
||||
dispatch(getSystemspannung98VminusThunk(zeitraum));
|
||||
break;
|
||||
case "ADC Temp":
|
||||
dispatch(getTemperaturAdWandlerThunk(zeitraum));
|
||||
break;
|
||||
case "CPU Temp":
|
||||
dispatch(getTemperaturProzessorThunk(zeitraum));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
const handleCloseDetail = () => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white dark:bg-gray-900">
|
||||
<h1 className="text-xl font-bold mb-4">
|
||||
System Spannungen & Temperaturen
|
||||
</h1>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center h-[400px]">
|
||||
<div className="text-center">
|
||||
<ClipLoader size={50} color="#3B82F6" />
|
||||
<p className="mt-4 text-gray-500">
|
||||
Lade Systemdaten … bitte warten
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<SystemOverviewGrid
|
||||
voltages={voltages}
|
||||
onOpenDetail={handleOpenDetail}
|
||||
/>
|
||||
<SystemCharts history={history} zeitraum={zeitraum} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<DetailModal
|
||||
isOpen={isModalOpen}
|
||||
selectedKey={selectedKey}
|
||||
onClose={handleCloseDetail}
|
||||
zeitraum={zeitraum}
|
||||
setZeitraum={setZeitraum}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemPage;
|
||||
@@ -26,7 +26,7 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
|
||||
{ name: "Kabelüberwachung ", path: "/kabelueberwachung" },
|
||||
{ name: "Meldungseingänge ", path: "/digitalInputs" }, //vorher Digitale Ein -und Ausgänge
|
||||
{ name: "Schaltausgänge ", path: "/digitalOutputs", disabled: false }, //vorher Digitale Ein -und Ausgänge
|
||||
{ name: "Messwerteingänge ", path: "/analogeEingaenge" }, //vorher Analoge Eingänge
|
||||
{ name: "Messwerteingänge ", path: "/analogInputs" }, //vorher Analoge Eingänge
|
||||
{ name: "Berichte ", path: "/meldungen" },
|
||||
{ name: "System ", path: "/system" },
|
||||
{ name: "Einstellungen ", path: "/einstellungen" },
|
||||
@@ -36,25 +36,26 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
|
||||
];
|
||||
|
||||
return (
|
||||
<aside>
|
||||
<aside className="bg-white dark:bg-gray-900 h-full">
|
||||
<nav className={`h-full flex-shrink-0 mt-16 ${className || "w-48"}`}>
|
||||
{menuItems.map((item) => (
|
||||
<div key={item.name}>
|
||||
{item.disabled ? (
|
||||
<div className="block px-4 py-2 mb-4 font-bold whitespace-nowrap text-gray-400 cursor-not-allowed text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg">
|
||||
<div className="block px-4 py-2 mb-4 font-bold whitespace-nowrap text-gray-400 dark:text-gray-600 cursor-not-allowed text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg">
|
||||
{item.name}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={formatPath(item.path)}>
|
||||
<div
|
||||
className={`block px-4 py-2 mb-4 font-bold whitespace-nowrap transition duration-300 text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg ${
|
||||
activeLink.startsWith(item.path)
|
||||
? "bg-sky-500 text-white rounded-r-full xl:mr-4 xl:w-full"
|
||||
: "text-black hover:bg-gray-200 rounded-r-full"
|
||||
}`}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
<Link
|
||||
href={formatPath(item.path)}
|
||||
prefetch={false}
|
||||
onClick={() => setActiveLink(item.path)}
|
||||
className={`block px-4 py-2 mb-4 font-bold whitespace-nowrap transition duration-300 text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg ${
|
||||
activeLink.startsWith(item.path)
|
||||
? "bg-sky-500 text-white rounded-r-full xl:mr-4 xl:w-full dark:bg-sky-600 dark:text-white"
|
||||
: "text-black hover:bg-gray-200 rounded-r-full dark:text-gray-200 dark:hover:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
80
create_presentation.py
Normal file
80
create_presentation.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches, Pt
|
||||
from pptx.enum.text import PP_ALIGN
|
||||
|
||||
prs = Presentation()
|
||||
|
||||
def add_slide(title, content_lines):
|
||||
slide_layout = prs.slide_layouts[1] # Title and Content
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
title_placeholder = slide.shapes.title
|
||||
content_placeholder = slide.placeholders[1]
|
||||
title_placeholder.text = title
|
||||
tf = content_placeholder.text_frame
|
||||
tf.clear()
|
||||
for line in content_lines:
|
||||
p = tf.add_paragraph()
|
||||
p.text = line
|
||||
p.font.size = Pt(20)
|
||||
p.alignment = PP_ALIGN.LEFT
|
||||
|
||||
# Folie 1: Titel
|
||||
add_slide("Testing CPL V4 Webseiten", ["Von: Ismail Ali", "Datum: 22.08.2025"])
|
||||
|
||||
# Folie 2 entfernt
|
||||
|
||||
# Folie 3: Warum testen wir?
|
||||
add_slide("Warum testen wir?", [
|
||||
"Um sicherzustellen, dass die Weboberfläche richtig funktioniert.",
|
||||
"Fehler frühzeitig erkennen und beheben.",
|
||||
"Qualität und Zuverlässigkeit verbessern."
|
||||
])
|
||||
|
||||
# Folie 4: Was ist Playwright?
|
||||
add_slide("Was ist Playwright?", [
|
||||
"Ein Open-Source-Testframework von Microsoft.",
|
||||
"Ermöglicht automatisierte Tests in verschiedenen Browsern (Chromium, Firefox, WebKit):",
|
||||
"🟦 Chromium 🦊 Firefox 🍏 WebKit",
|
||||
"Simuliert echte Benutzeraktionen wie Klicks, Eingaben und Navigation.",
|
||||
"Unterstützt mehrere Programmiersprachen: JavaScript, TypeScript, Python, Java, .NET (C#).",
|
||||
"Ideal für End-to-End-Tests von Webanwendungen."
|
||||
])
|
||||
|
||||
# Folie 5: Wie habe ich getestet?
|
||||
add_slide("Wie habe ich getestet?", [
|
||||
"Mit Playwright automatisierte Tests geschrieben.",
|
||||
"Playwright Recorder (codegen) verwendet, da es einfacher ist als manuellen Code zu schreiben.",
|
||||
"Verschiedene Seiten des CPL V4 Webservers getestet:",
|
||||
"- Dashboard",
|
||||
"- Analoge Eingänge",
|
||||
"- Einstellungen"
|
||||
])
|
||||
|
||||
# Folie 6: Beispiel-Test (Ausschnitt)
|
||||
add_slide("Beispiel-Test (Ausschnitt)", [
|
||||
"Test prüft, ob wichtige Elemente sichtbar sind.",
|
||||
"Beispiel: Überschrift, Buttons, Tabellenzellen.",
|
||||
"Klicks und Eingaben werden simuliert."
|
||||
])
|
||||
|
||||
# Folie 7: Test-Ergebnisse
|
||||
add_slide("Test-Ergebnisse", [
|
||||
"Alle Tests wurden erfolgreich ausgeführt.",
|
||||
"Keine Fehler gefunden (siehe Test-Report)."
|
||||
])
|
||||
|
||||
# Folie 8: Fazit
|
||||
add_slide("Fazit", [
|
||||
"Automatisierte Tests helfen, Fehler schnell zu finden.",
|
||||
"Playwright ist einfach zu bedienen, auch für Anfänger.",
|
||||
"Tests machen die Entwicklung sicherer und effizienter."
|
||||
])
|
||||
|
||||
# Folie 9: Fragen?
|
||||
add_slide("Fragen?", [
|
||||
"Vielen Dank für die Aufmerksamkeit!",
|
||||
"Gibt es Fragen?"
|
||||
])
|
||||
|
||||
prs.save("Testing_CPLV4_Webserver.pptx")
|
||||
print("Präsentation erstellt: Testing_CPLV4_Webserver.pptx")
|
||||
@@ -1,9 +0,0 @@
|
||||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
describe('Kue705FO Integration Tests', () => {
|
||||
beforeEach(() => {
|
||||
// Besuche die Seite, auf der die Komponente gerendert wird
|
||||
//cy.visit('/path-to-your-component'); // Passe den Pfad an deine App an
|
||||
cy.visit('http://localhost:3000/kabelueberwachung');
|
||||
});
|
||||
|
||||
it('should render the component with default props', () => {
|
||||
// Überprüfe, ob der Modulname und die Slotnummer angezeigt werden
|
||||
cy.contains('KÜ705-FO').should('be.visible');
|
||||
cy.contains('Modul 1').should('be.visible'); // Beispiel für den Modulnamen
|
||||
});
|
||||
|
||||
it('should update display when TDR button is clicked', () => {
|
||||
// Klicke auf den TDR-Button
|
||||
cy.contains('TDR').click();
|
||||
|
||||
// Überprüfe, ob der Text aktualisiert wurde
|
||||
cy.contains('Entfernung [Km]').should('be.visible');
|
||||
});
|
||||
|
||||
it('should switch back to Schleife display', () => {
|
||||
// Klicke auf TDR, dann zurück zu Schleife
|
||||
cy.contains('TDR').click();
|
||||
cy.contains('Schleife').click();
|
||||
|
||||
// Überprüfe, ob der Text aktualisiert wurde
|
||||
cy.contains('Schleifenwiderstand [kOhm]').should('be.visible');
|
||||
});
|
||||
|
||||
it('should disable TDR button when tdrActive is 0', () => {
|
||||
// Dies erfordert eine benutzerdefinierte Backend-Konfiguration oder Redux-Manipulation
|
||||
cy.contains('TDR').should('be.disabled');
|
||||
});
|
||||
|
||||
it('should open and close the settings modal', () => {
|
||||
// Öffne das Modal
|
||||
cy.contains('⚙').click();
|
||||
cy.contains('KUE Einstellung - Slot 1').should('be.visible');
|
||||
|
||||
// Schließe das Modal
|
||||
cy.contains('×').click();
|
||||
cy.contains('KUE Einstellung - Slot 1').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 123 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 109 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 122 KiB |
@@ -1,37 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -1,17 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/e2e.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
142
docs/KVZ/kvz-system-understanding.md
Normal file
142
docs/KVZ/kvz-system-understanding.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# KVZ System - Mein aktuelles Verständnis
|
||||
|
||||
## System Übersicht
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Kabelüberwachung System (32 Slots)"
|
||||
Slot0["Slot 0<br/>Kabelüberwachung"]
|
||||
Slot1["Slot 1<br/>Kabelüberwachung"]
|
||||
Slot2["Slot 2<br/>Kabelüberwachung"]
|
||||
Slot3["Slot 3<br/>Kabelüberwachung"]
|
||||
SlotDots["..."]
|
||||
Slot31["Slot 31<br/>Kabelüberwachung"]
|
||||
end
|
||||
|
||||
subgraph "KVZ Geräte (Optional pro Slot)"
|
||||
KVZ0["KVZ Gerät<br/>für Slot 0"]
|
||||
KVZ1["KVZ Gerät<br/>für Slot 1"]
|
||||
KVZ2["KVZ Gerät<br/>für Slot 2"]
|
||||
KVZ3["KVZ Gerät<br/>für Slot 3"]
|
||||
end
|
||||
|
||||
subgraph "KVZ LEDs (4 pro KVZ Gerät)"
|
||||
subgraph "KVZ0 LEDs"
|
||||
LED0_0["LED 1"]
|
||||
LED0_1["LED 2"]
|
||||
LED0_2["LED 3"]
|
||||
LED0_3["LED 4"]
|
||||
end
|
||||
|
||||
subgraph "KVZ1 LEDs"
|
||||
LED1_0["LED 1"]
|
||||
LED1_1["LED 2"]
|
||||
LED1_2["LED 3"]
|
||||
LED1_3["LED 4"]
|
||||
end
|
||||
end
|
||||
|
||||
Slot0 -.-> KVZ0
|
||||
Slot1 -.-> KVZ1
|
||||
Slot2 -.-> KVZ2
|
||||
Slot3 -.-> KVZ3
|
||||
|
||||
KVZ0 --> LED0_0
|
||||
KVZ0 --> LED0_1
|
||||
KVZ0 --> LED0_2
|
||||
KVZ0 --> LED0_3
|
||||
|
||||
KVZ1 --> LED1_0
|
||||
KVZ1 --> LED1_1
|
||||
KVZ1 --> LED1_2
|
||||
KVZ1 --> LED1_3
|
||||
```
|
||||
|
||||
## Redux Data Structure - Mein Verständnis
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Redux Store"
|
||||
subgraph "kvzPresence Array (32 Elemente)"
|
||||
P0["Index 0: 1<br/>(KVZ vorhanden)"]
|
||||
P1["Index 1: 0<br/>(KVZ nicht vorhanden)"]
|
||||
P2["Index 2: 0"]
|
||||
P3["Index 3: 0"]
|
||||
PDots["..."]
|
||||
P31["Index 31: 0"]
|
||||
end
|
||||
|
||||
subgraph "kvzStatus Array (128 Elemente)"
|
||||
subgraph "Slot 0 LEDs (Index 0-3)"
|
||||
S0_0["Index 0: 1 (grün)"]
|
||||
S0_1["Index 1: 0 (rot)"]
|
||||
S0_2["Index 2: 1 (grün)"]
|
||||
S0_3["Index 3: 0 (rot)"]
|
||||
end
|
||||
|
||||
subgraph "Slot 1 LEDs (Index 4-7)"
|
||||
S1_0["Index 4: 0"]
|
||||
S1_1["Index 5: 0"]
|
||||
S1_2["Index 6: 0"]
|
||||
S1_3["Index 7: 0"]
|
||||
end
|
||||
|
||||
StatusDots["...weitere 120 Elemente"]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## UI Darstellung - Mein aktuelles Verständnis
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "FallSensors UI Component"
|
||||
subgraph "Aktuelle Implementierung (FALSCH?)"
|
||||
UI1["KVZ1: 🟢<br/>(kvzPresence[0] = 1)"]
|
||||
UI2["KVZ2: 🔴<br/>(kvzPresence[1] = 0)"]
|
||||
UI3["KVZ3: 🔴<br/>(kvzPresence[2] = 0)"]
|
||||
UI4["KVZ4: 🔴<br/>(kvzPresence[3] = 0)"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph "Problem"
|
||||
Problem["Alle KVZ zeigen den gleichen Status<br/>basierend auf kvzPresence Array<br/>→ NICHT korrekt!"]
|
||||
end
|
||||
|
||||
UI1 -.-> Problem
|
||||
UI2 -.-> Problem
|
||||
UI3 -.-> Problem
|
||||
UI4 -.-> Problem
|
||||
```
|
||||
|
||||
## Fragen zu meinem Verständnis
|
||||
|
||||
1. **KVZ Geräte Zuordnung**:
|
||||
|
||||
- Ist ein KVZ-Gerät einem Slot zugeordnet oder unabhängig?
|
||||
- Wie viele KVZ-Geräte gibt es insgesamt?
|
||||
|
||||
2. **UI KVZ1-KVZ4**:
|
||||
|
||||
- Repräsentieren KVZ1-KVZ4 in der UI die ersten 4 Slots (0-3)?
|
||||
- Oder sind es 4 separate, unabhängige KVZ-Geräte?
|
||||
|
||||
3. **LED Status Mapping**:
|
||||
|
||||
- Welche LED von welchem KVZ soll in KVZ1, KVZ2, KVZ3, KVZ4 angezeigt werden?
|
||||
- Soll jedes UI-KVZ eine andere LED des gleichen Geräts zeigen?
|
||||
- Oder soll jedes UI-KVZ ein anderes KVZ-Gerät repräsentieren?
|
||||
|
||||
4. **kvzStatus Array**:
|
||||
- Wie soll das 128-Element Array für die UI-Darstellung genutzt werden?
|
||||
- Welche Indizes entsprechen welchen UI-Elementen?
|
||||
|
||||
## Verdacht
|
||||
|
||||
Ich vermute, dass mein aktueller Ansatz falsch ist, weil:
|
||||
|
||||
- KVZ2 sollte nicht eine Kopie von KVZ1 Status sein
|
||||
- Jedes KVZ in der UI sollte einen eigenen, unabhängigen Status haben
|
||||
- Die Zuordnung zwischen Redux Arrays und UI ist unklar
|
||||
|
||||
**Bitte korrigieren Sie mein Verständnis! 🤔**
|
||||
141
docs/KVZ/mock-data.md
Normal file
141
docs/KVZ/mock-data.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# KVZ Mock Data - Dokumentation
|
||||
|
||||
## Mock-Daten Struktur
|
||||
|
||||
Die KVZ Mock-Daten befinden sich in `mocks/kvzData.json` und haben folgende Struktur:
|
||||
|
||||
### Beispiel-Daten
|
||||
|
||||
```json
|
||||
{
|
||||
"kvzPresence": [1, 0, 1, 0, ...], // 32 Elemente
|
||||
"kvzStatus": [1, 0, 1, 0, ...], // 128 Elemente
|
||||
"timestamp": "2025-01-31T12:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### kvzPresence Array (32 Elemente)
|
||||
|
||||
- **Index 0-31**: Repräsentiert Slots 0-31
|
||||
- **Wert 1**: KVZ-Gerät vorhanden
|
||||
- **Wert 0**: KVZ-Gerät nicht vorhanden
|
||||
|
||||
**Aktuelle Mock-Daten:**
|
||||
|
||||
- Slot 0: KVZ vorhanden (1)
|
||||
- Slot 1: KVZ nicht vorhanden (0)
|
||||
- Slot 2: KVZ vorhanden (1)
|
||||
- Slot 3-31: KVZ nicht vorhanden (0)
|
||||
|
||||
### kvzStatus Array (128 Elemente)
|
||||
|
||||
- **Index Berechnung**: `slotIndex * 4 + ledIndex`
|
||||
- **4 LEDs pro Slot**: LED 0, LED 1, LED 2, LED 3
|
||||
- **Wert 1**: LED aktiv (grün)
|
||||
- **Wert 0**: LED inaktiv (rot)
|
||||
|
||||
**Aktuelle Mock-Daten:**
|
||||
|
||||
- Slot 0 LEDs (Index 0-3): [1, 0, 1, 0] → LED1=grün, LED2=rot, LED3=grün, LED4=rot
|
||||
- Slot 1 LEDs (Index 4-7): [0, 0, 0, 0] → Alle LEDs rot (KVZ nicht vorhanden)
|
||||
- Slot 2 LEDs (Index 8-11): [1, 1, 0, 1] → LED1=grün, LED2=grün, LED3=rot, LED4=grün
|
||||
- Slot 3-31: Alle LEDs rot (0)
|
||||
|
||||
## API Endpunkte
|
||||
|
||||
### GET /api/kvz/data
|
||||
|
||||
Holt alle KVZ-Daten.
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"kvzPresence": [...],
|
||||
"kvzStatus": [...],
|
||||
"timestamp": "2025-01-31T12:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/kvz/data
|
||||
|
||||
Ersetzt komplette KVZ-Daten.
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"kvzPresence": [...],
|
||||
"kvzStatus": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/kvz/updateSettings
|
||||
|
||||
Aktualisiert spezifische KVZ-Einstellungen.
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"updates": [
|
||||
{
|
||||
"key": "kvzPresence",
|
||||
"slot": 0,
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"key": "kvzStatus",
|
||||
"slot": 0,
|
||||
"ledIndex": 1,
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Service Functions
|
||||
|
||||
### fetchKvzData()
|
||||
|
||||
```typescript
|
||||
import { fetchKvzData } from "../services/fetchKvzDataService";
|
||||
|
||||
const data = await fetchKvzData();
|
||||
console.log(data.kvzPresence); // [1, 0, 1, 0, ...]
|
||||
```
|
||||
|
||||
### updateKvzSettings()
|
||||
|
||||
```typescript
|
||||
import { updateKvzSettings } from "../services/fetchKvzDataService";
|
||||
|
||||
await updateKvzSettings([
|
||||
{ key: "kvzPresence", slot: 0, value: 1 },
|
||||
{ key: "kvzStatus", slot: 0, ledIndex: 1, value: 1 },
|
||||
]);
|
||||
```
|
||||
|
||||
## Redux Integration
|
||||
|
||||
Die Mock-Daten können in Redux geladen werden:
|
||||
|
||||
```typescript
|
||||
// In einem Thunk oder useEffect
|
||||
const data = await fetchKvzData();
|
||||
dispatch(
|
||||
setKueData({
|
||||
kvzPresence: data.kvzPresence,
|
||||
kvzStatus: data.kvzStatus,
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
## Testen
|
||||
|
||||
Die Mock-Daten ermöglichen es:
|
||||
|
||||
1. **KVZ-Geräte zu simulieren** (Slots 0 und 2 haben KVZ)
|
||||
2. **LED-Status zu testen** (verschiedene Kombinationen)
|
||||
3. **API-Updates zu testen** (Presence und Status ändern)
|
||||
4. **UI-Verhalten zu validieren** (bedingte Anzeige, Farben)
|
||||
BIN
docs/Lastenheft/CPL V4 Lastenheft 03.09.2025.pdf
Normal file
BIN
docs/Lastenheft/CPL V4 Lastenheft 03.09.2025.pdf
Normal file
Binary file not shown.
87
docs/TODO.md
87
docs/TODO.md
@@ -18,3 +18,90 @@
|
||||
- [ ] TODO: Alle Kabelüberwachungsmodule mit ein Button Updaten , in Einstellungen und in Kabelüberwachungsmodul Modal
|
||||

|
||||
Zeit bis Ende August
|
||||
- [ ] TODO: Überall Littwin-Blau und ausgewählt und grau wie bei Navigation bei Mouse over
|
||||
- [ ] ## TODO: Messwerteingänge Diagrammme /Messkurven
|
||||
23.07.2025
|
||||
- [x] TODO: Isolationsfehler in Display anzeigen -> aktuell Zahl ist rot ohne Beschrifftung , es soll Zahl ISO MOhm und Isolationsfehler
|
||||
- [x] TODO: Kilometer Km -> km kleingeschrieben 1000, 1024 wird Großgeschrieben Kilobyte Kb
|
||||
- [x] TODO: Messwerteingänge Mouse couror wait beim laden, damit der user etwas wartet
|
||||
- [ ] ## TODO: In KÜ, unter KÜ Balken/Bereich für Scheleife, Bereich für TDR wenn aktiv ist und Bereich für KVz wenn aktive ist
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
24.07.2025
|
||||
|
||||
- [ ] TODO: Bei System Messkurven Cursor await bis die daten lädt
|
||||
- [ ] TODO: in KÜ DISPLAY Fehler, ISO Wert und Schleifenwwert bei wechsel Zustand anzeigen während Schleifenmessung und Isowemmsung und kalibirirung
|
||||
- [ ] TODO: Bug in DatePicker in KÜ
|
||||
- [ ] TODO: Kurven für ISO, RSL und TDR
|
||||
- [x] TODO: in KÜ Steckplatz in KÜ umbenennen
|
||||
- [x] TODO: Firmware Update Bestätigung in Littwin blau auch progress in littwinblue
|
||||
- [x] TODO: KÜ Firmware in progress Bitte Fenster nicht schließen
|
||||
- [ ] TODO: KÜ Kurve und letzte historische Meldungen anzeigen -> später
|
||||
- [ ] TODO: KVz später
|
||||
|
||||
Anzeige KÜ-Display:
|
||||
|
||||
1. Zeile Alarm: Isolationsfehler, Schleifenfehler, Aderbruch, Erdschluß, Messpannung: Immer in Rot; wenn kein Alarm, bleibt die Zeile leer
|
||||
|
||||
2. Zeile: Isowert: xx MOhm (großes M)
|
||||
|
||||
in Rot, wenn Iso-Fehler ansteht
|
||||
|
||||
Beispiel: ISO: 100 MOHm der beim Abliech: ISO: Abgleich
|
||||
|
||||
3. Zeile: Schleifenwert, xx kOhm (kleines k)
|
||||
|
||||
in Rot, wenn Schleifenfehler ansteht
|
||||
|
||||
## Beispiel:: RSL: 1,7 kOhm oder wenn Schleifenmessung aktiv: RSL: Messung
|
||||
|
||||
## 11.08.2025
|
||||
|
||||
- [x] TODO: Bei Schleife starten messen wie lange es dauert, dann entsprechend progress balken einbauen, 2 Minuten erstellt
|
||||
|
||||
# 13.08.2025
|
||||
|
||||
- [x] TODO: Das Sichern und das Zurücksichern der KÜ-Daten über die Webseiten funktioniert nicht. Anscheinend ruft die Webseite keine ACP-Webseite mit Daten "?KSB02=1" auf sondern nur Daten "KSB02=1". Die CPL will dann die Datei KSB02=1 laden die es ja nicht gibt.
|
||||
- [x] TODO: Kalibrieren Dauer entsprechend progress balken einbauen
|
||||
- [x] TODO: Abgleich Dauer entsprechend progress balken einbauen
|
||||
- [ ] TODO: Benutzer passwort ändern
|
||||
- [ ] TODO: PlayWright
|
||||
- ISO Abgleich 10 Minuten
|
||||
- CPL läuft auf BUS-System, Wenn ein Kabelüberwachung z.B. beschäftigt mit ein Schleifen-Messung oder Iso- Abgleich dann belastet den CPL nicht wenn andere KÜs bedient werden, das erleichtet den CPL sogar
|
||||
|
||||
# 14.08.2025
|
||||
|
||||
- [x] TODO: Messwerteingänge Messkurven in Modal umwandeln
|
||||
|
||||
# 15.08.2025
|
||||
|
||||
- [x] BUGFIX: Messkurven-Modal lädt jetzt automatisch die Kurve beim Öffnen, Dropdown ist auf 'Alle Messwerte' (DIA0) initialisiert, und Filter werden beim Schließen zurückgesetzt. Dateien: IsoChartView.tsx, LoopChartView.tsx
|
||||
|
||||
# 01.09.2025
|
||||
|
||||
- [x] TODO: In KÜs Display ISO 2 Nachkommastellen und RSL 3 Nachkommastellen
|
||||
- [ ] TODO: Schleife, Timer für jeder KÜ separate und nicht eine für alle, aktuell wird prozentzahl bei allen das gleiche angezeigt
|
||||
- [x] TODO: RSL starten in RSL Messung starten umbenennen
|
||||
- [x] TODO: TDR-Messung starten statt TDR aktivieren in ChartBar
|
||||
- [x] TODO: KÜ TDR-aktiviert alert entfernen
|
||||
- [ ] TODO: Systemdaten unter Detailansicht ein Verlaufsdiagramm hinzufügen mit Datumsauswahl
|
||||
- [ ] TODO: Playwright testen mit der Entwicklung
|
||||
|
||||
# Kai Schmidt:
|
||||
|
||||
# Folgende Erweiterung / Neuerungen:
|
||||
|
||||
[ ] TODO: Messverlauf bei Systemwerten (Temperatur und Spannungen) mit Datumsauswahl
|
||||
|
||||
[ ] TODO: Formatierung der Kabelüberwachungswerten in den visuellen Einschüben (Isowert mit Komma und 2 Nachkommastellen; RSL mit Komma und 3 Noachkommastellen) Nachkommastellen immer anzeigen und mit Nullen auffüllen.
|
||||
|
||||
[ ] TODO: Admin User nach einer Zeit von einer Stunde löschen (Cookie oder Local Storrage)
|
||||
|
||||
[ ] TODO: lange Modulnamen bei KÜ ermöglichen (48 Zeichen) bei Version ab V4.30. Laufschrift möglich?
|
||||
|
||||
[ ] TODO: Darkmode ermöglichen
|
||||
|
||||
[ ] TODO: Wenn im Browser Darkmode eingschaltet ist muss die Webseite erkennbar sein.
|
||||
|
||||
@@ -46,7 +46,7 @@ Sie ist **pro Slot aktivierbar** und bietet folgende Einstellungen:
|
||||
|
||||
### 🔁 Knotenpunkte-Anzeige
|
||||
|
||||
Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines Steckplatzes:
|
||||
Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines KÜes:
|
||||
|
||||
| Feld | Beschreibung |
|
||||
| -------------- | -------------------------------------------------- |
|
||||
@@ -80,6 +80,25 @@ Jeder Slot zeigt über zwei LEDs den Betriebs- und Alarmstatus an:
|
||||
|
||||
---
|
||||
|
||||
## ⏳ Messung läuft: Per-Slot Overlay (Schleife, ISO-Abgleich, TDR)
|
||||
|
||||
Während aktive Messungen laufen, wird der betroffene Slot gezielt blockiert, ohne die gesamte Seite zu sperren. So können andere Module weiterhin bedient werden.
|
||||
|
||||
- Per-Slot Overlay erscheint nur auf dem betroffenen Modul auf der Seite „Kabelüberwachung“.
|
||||
- Das Overlay zeigt für jede aktive Messart eine Fortschrittsleiste mit Prozentanzeige (ohne Restzeit):
|
||||
- Schleifenmessung (RSL, KSX) ≈ 2 Minuten
|
||||
- TDR-Messung (KSY) ≈ 30 Sekunden
|
||||
- ISO-Abgleich (KSZ) ≈ 10 Minuten
|
||||
- Die Anzeige aktualisiert sich ca. jede Sekunde.
|
||||
- Bei mehreren gleichzeitig aktiven Messarten werden mehrere Balken untereinander gezeigt.
|
||||
|
||||
Hinweise:
|
||||
|
||||
- Auf anderen Seiten (z. B. System) erscheint statt des per-Slot Overlays ein globales Overlay über die gesamte Anwendung. Auf der Seite „Kabelüberwachung“ ist dieses globale Overlay deaktiviert, damit die Bedienung der übrigen Slots möglich bleibt.
|
||||
- Die Aktivitätsinformationen stammen vom Gerät (KSX/KSY/KSZ Ereignis-Arrays je 32 Slots) und werden zyklisch eingelesen.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Messungssteuerung (manuell)
|
||||
|
||||
Im unteren Bereich jedes Slots befindet sich ein **Kreispfeil-Icon** 🔄 (Reload-Symbol):
|
||||
|
||||
@@ -46,7 +46,7 @@ Sie ist **pro Slot aktivierbar** und bietet folgende Einstellungen:
|
||||
|
||||
### 🔁 Knotenpunkte-Anzeige
|
||||
|
||||
Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines Steckplatzes:
|
||||
Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines KÜes:
|
||||
|
||||
| Feld | Beschreibung |
|
||||
| -------------- | -------------------------------------------------- |
|
||||
|
||||
@@ -46,7 +46,7 @@ Sie ist **pro Slot aktivierbar** und bietet folgende Einstellungen:
|
||||
|
||||
### 🔁 Knotenpunkte-Anzeige
|
||||
|
||||
Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines Steckplatzes:
|
||||
Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines KÜes:
|
||||
|
||||
| Feld | Beschreibung |
|
||||
| -------------- | -------------------------------------------------- |
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
module.exports = {
|
||||
testEnvironment: "jest-environment-jsdom",
|
||||
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
|
||||
testPathIgnorePatterns: ["/node_modules/", "/playwright/"],
|
||||
moduleNameMapper: {
|
||||
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
|
||||
"^bootstrap-icons/font/bootstrap-icons.css$":
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["*"],
|
||||
"@/redux/*": ["redux/*"],
|
||||
"@/utils/*": ["utils/*"],
|
||||
"@/components/*": ["components/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"components/**/*",
|
||||
"redux/**/*",
|
||||
"utils/**/*",
|
||||
"*.js",
|
||||
"*.ts",
|
||||
"*.jsx",
|
||||
"*.tsx"
|
||||
],
|
||||
|
||||
"extends": "./tsconfig.json"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"win_appVersion": "0.02",
|
||||
"win_deviceName": "CPLV4 Ismail Rastede",
|
||||
"win_mac1": "0 48 86 81 46 143",
|
||||
"win_ip": "10.10.0.243",
|
||||
"win_subnet": "255.255.255.0",
|
||||
"win_gateway": "10.10.0.1",
|
||||
"win_cplInternalTimestamp": "23.10.24 15:10:28 Uhr",
|
||||
"win_opcState": "1",
|
||||
"win_opcSessions": "0",
|
||||
"win_opcName": "CPL V4 OPC UA Application Deutsche Bahne",
|
||||
"win_ntp1": "192.53.103.108",
|
||||
"win_ntp2": "0.0.0.0",
|
||||
"win_ntp3": "0.0.0.0",
|
||||
"win_ntpTimezone": "2",
|
||||
"win_ntpActive": "1"
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"win_analogInputsValues": [
|
||||
4.771072,
|
||||
5.665244,
|
||||
0.005467,
|
||||
-0.007468,
|
||||
0.000002,
|
||||
0.000001,
|
||||
0.000001,
|
||||
0.000007
|
||||
],
|
||||
"win_analogInputsLabels": [
|
||||
"AE 1",
|
||||
"AE 2",
|
||||
"AE 3",
|
||||
"AE 4",
|
||||
"AE 5",
|
||||
"AE 6",
|
||||
"AE 7",
|
||||
"AE 8"
|
||||
],
|
||||
"win_analogInputsOffset": [
|
||||
10.988,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"win_analogInputsFactor": [
|
||||
11.988,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"win_analogInputsUnits": [
|
||||
"V",
|
||||
"V",
|
||||
"V",
|
||||
"V",
|
||||
"mA",
|
||||
"mA",
|
||||
"mA",
|
||||
"mA"
|
||||
],
|
||||
"win_analogInputsLoggerIntervall": [
|
||||
7,
|
||||
10,
|
||||
10,
|
||||
10,
|
||||
10,
|
||||
10,
|
||||
10,
|
||||
10
|
||||
],
|
||||
"win_analogInputsWeighting": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
{
|
||||
"win_de_state": [
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"win_de_invert": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"win_de_counter": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"win_de_time_filter": [
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"win_de_weighting": [
|
||||
3,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"win_de_counter_active": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"win_de_offline": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"win_de_label": [
|
||||
"DE1",
|
||||
"DE2",
|
||||
"DE3",
|
||||
"DE4",
|
||||
"DE5",
|
||||
"DE6",
|
||||
"DE7",
|
||||
"DE8",
|
||||
"DE9",
|
||||
"DE10",
|
||||
"DE11",
|
||||
"DE12",
|
||||
"DE13",
|
||||
"DE14",
|
||||
"DE15",
|
||||
"DE16",
|
||||
"DE17",
|
||||
"DE18",
|
||||
"DE19",
|
||||
"DE20",
|
||||
"DE21",
|
||||
"DE22",
|
||||
"DE23",
|
||||
"DE24",
|
||||
"DE25",
|
||||
"DE26",
|
||||
"DE27",
|
||||
"DE28",
|
||||
"DE29",
|
||||
"DE30",
|
||||
"DE31",
|
||||
"DE32"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"win_da_state": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"win_da_bezeichnung": [
|
||||
"Ausgang1",
|
||||
"Ausgang2",
|
||||
"Ausgang3",
|
||||
"Ausgang4"
|
||||
]
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
{
|
||||
"win_kueOnline": [
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1
|
||||
],
|
||||
"win_kuePSTmMinus96V": [
|
||||
0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0
|
||||
],
|
||||
"win_kueCableBreak": [
|
||||
1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 1, 1, 1, 1
|
||||
],
|
||||
"win_kueGroundFault": [
|
||||
1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0
|
||||
],
|
||||
"win_kueAlarm1": [
|
||||
1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0,
|
||||
1, 0, 0, 0, 0, 0, 0
|
||||
],
|
||||
"win_kueAlarm2": [
|
||||
1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0
|
||||
],
|
||||
"win_kueOverflow": [
|
||||
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0
|
||||
],
|
||||
"win_kueIso": [
|
||||
10, 10, 10, 10.5, 10, 10, 10, 10, 10.5, 10, 10, 10, 10, 10, 10, 10, 10, 10,
|
||||
10, 10, 10.5, 10, 10, 10, 10, 10, 10.5, 10, 200, 200, 200, 200
|
||||
],
|
||||
"win_kueLimit1": [
|
||||
3, 9.9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
|
||||
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10
|
||||
],
|
||||
"win_kueDelay1": [
|
||||
3, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420,
|
||||
420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420,
|
||||
420, 420
|
||||
],
|
||||
"win_kueResidence": [
|
||||
0, 0.612, 0, 0.645, 0.822, 0.97, 0, 0, 1.452, 0, 0.734, 0.37, 0.566, 0,
|
||||
0.738, 0.684, 1.166, 0.595, 0, 1.651, 1.18, 1.387, 1.214, 0, 1.475, 0.615,
|
||||
0.494, 1.217, 65, 65, 65, 65
|
||||
],
|
||||
"win_kueLimit2Low": [
|
||||
3, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
|
||||
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
|
||||
0.1, 0.1
|
||||
],
|
||||
"win_kueLimit2High": [
|
||||
3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1
|
||||
],
|
||||
"win_kueLoopInterval": [
|
||||
3, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6
|
||||
],
|
||||
"win_kueVersion": [
|
||||
420, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419,
|
||||
419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419,
|
||||
419, 419
|
||||
],
|
||||
"win_kueID": [
|
||||
"Test3",
|
||||
"B23",
|
||||
"Kabel 3",
|
||||
"Kabel 4",
|
||||
"Kabel 5",
|
||||
"Kabel 6",
|
||||
"FTZ4562",
|
||||
"Kabel 8",
|
||||
"12344",
|
||||
"Kabel 10",
|
||||
"Kabel 11",
|
||||
"Kabel 12",
|
||||
"Kabel 13",
|
||||
"Kabel 14",
|
||||
"Kabel 15",
|
||||
"H56-77",
|
||||
"Kabel 17",
|
||||
"Kabel 18",
|
||||
"Kabel 19",
|
||||
"Kabel 20",
|
||||
"Kabel 21",
|
||||
"Kabel 22",
|
||||
"Kabel 23",
|
||||
"Kabel 24",
|
||||
"Kabel 25",
|
||||
"Kabel 26",
|
||||
"Kabel 27",
|
||||
"Kabel 28",
|
||||
"Kabel 29",
|
||||
"Kabel 30",
|
||||
"Kabel 31",
|
||||
"Kabel 32"
|
||||
],
|
||||
"win_kueName": [
|
||||
"Linie 2",
|
||||
"Edewecht 3",
|
||||
"",
|
||||
"Linie 4",
|
||||
"Linie 5",
|
||||
"",
|
||||
"",
|
||||
"Kabel_8",
|
||||
"Kabel_9",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Kabel 32"
|
||||
],
|
||||
"win_tdrActive": [
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1
|
||||
],
|
||||
"win_tdrAtten": [
|
||||
11, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 2, 2
|
||||
],
|
||||
"win_tdrSpeed": [
|
||||
112, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
|
||||
100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
|
||||
100, 100
|
||||
],
|
||||
"win_tdrTrigger": [
|
||||
102, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
|
||||
80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80
|
||||
],
|
||||
"win_tdrPulse": [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0
|
||||
],
|
||||
"win_tdrAmp": [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0
|
||||
],
|
||||
"win_tdrLocation": [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0
|
||||
],
|
||||
"win_tdrLast": [
|
||||
"2024-10-17 07:51:54:000",
|
||||
"2024-09-30 08:38:50:000",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"2024-09-30 08:36:43:000",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?",
|
||||
"?"
|
||||
],
|
||||
"win_memoryInterval": [
|
||||
5, 0, 15, 0, 0, 15, 15, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0
|
||||
]
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 25068,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 04:56:28",
|
||||
"message": "Isofehler gehend",
|
||||
"status": 0
|
||||
},
|
||||
{
|
||||
"id": 25067,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 04:55:43",
|
||||
"message": "Isofehler kommend",
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"id": 25066,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 04:48:39",
|
||||
"message": "Isofehler gehend",
|
||||
"status": 0
|
||||
},
|
||||
{
|
||||
"id": 25065,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 04:46:02",
|
||||
"message": "Isofehler kommend",
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"id": 25064,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 04:38:58",
|
||||
"message": "Isofehler gehend",
|
||||
"status": 0
|
||||
},
|
||||
{
|
||||
"id": 25063,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 04:36:44",
|
||||
"message": "Isofehler kommend",
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"id": 25062,
|
||||
"code": "02401",
|
||||
"timestamp": "2025-04-22 04:35:38",
|
||||
"message": "Isofehler kommend",
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"id": 25061,
|
||||
"code": "02401",
|
||||
"timestamp": "2025-04-22 04:28:33",
|
||||
"message": "Isofehler gehend",
|
||||
"status": 0
|
||||
},
|
||||
{
|
||||
"id": 25060,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 02:56:28",
|
||||
"message": "Isofehler gehend",
|
||||
"status": 0
|
||||
},
|
||||
{
|
||||
"id": 25059,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 02:56:06",
|
||||
"message": "Isofehler kommend",
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"id": 25058,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 02:40:27",
|
||||
"message": "Isofehler gehend",
|
||||
"status": 0
|
||||
},
|
||||
{
|
||||
"id": 25057,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 02:40:05",
|
||||
"message": "Isofehler kommend",
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"id": 25056,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 02:26:40",
|
||||
"message": "Isofehler gehend",
|
||||
"status": 0
|
||||
},
|
||||
{
|
||||
"id": 25055,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 02:26:17",
|
||||
"message": "Isofehler kommend",
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"id": 25054,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 02:16:56",
|
||||
"message": "Isofehler gehend",
|
||||
"status": 0
|
||||
},
|
||||
{
|
||||
"id": 25053,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 02:16:34",
|
||||
"message": "Isofehler kommend",
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"id": 25052,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 02:09:30",
|
||||
"message": "Isofehler gehend",
|
||||
"status": 0
|
||||
},
|
||||
{
|
||||
"id": 25051,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 02:01:18",
|
||||
"message": "Isofehler kommend",
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"id": 25050,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 01:54:35",
|
||||
"message": "Isofehler gehend",
|
||||
"status": 0
|
||||
},
|
||||
{
|
||||
"id": 25049,
|
||||
"code": "02101",
|
||||
"timestamp": "2025-04-22 01:54:13",
|
||||
"message": "Isofehler kommend",
|
||||
"status": 1
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user