Compare commits
125 Commits
9db92a2728
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3edb8a053c | ||
|
|
aedc7ccae5 | ||
|
|
bdaf0ec263 | ||
|
|
538f9ca487 | ||
|
|
5ef7e648eb | ||
|
|
74880d9ccc | ||
|
|
7f035f0c18 | ||
|
|
7fe04f55fe | ||
|
|
2ceebea533 | ||
|
|
95c884bc07 | ||
|
|
05b416855b | ||
|
|
b4dd42c8a5 | ||
|
|
41910e450e | ||
|
|
f2a5f2083a | ||
|
|
92b712d7ce | ||
|
|
be9954ac29 | ||
|
|
f25063074d | ||
|
|
9192111b12 | ||
|
|
6f88a11771 | ||
|
|
4c45c3b9ca | ||
|
|
484902b788 | ||
|
|
3266e8b2d5 | ||
|
|
77f14313ae | ||
|
|
f43ddccc46 | ||
|
|
28612f9cd0 | ||
|
|
d6703c8870 | ||
|
|
18c9c886ec | ||
|
|
4c6fe0db03 | ||
|
|
6cb753c040 | ||
|
|
52551b3243 | ||
|
|
f7d1a36e0f | ||
|
|
8580032ff9 | ||
|
|
001b237dd7 | ||
|
|
af21b180f1 | ||
|
|
fefff9419d | ||
|
|
27c60c6742 | ||
|
|
c8ec763aac | ||
|
|
d163df0d96 | ||
|
|
12d3a17f60 | ||
|
|
f3339ccafd | ||
|
|
fab8a02ce9 | ||
|
|
eb0585072d | ||
|
|
a596422056 | ||
|
|
531fa93b70 | ||
|
|
72341abb23 | ||
|
|
9c218b2a1d | ||
|
|
d38d3191c5 | ||
|
|
112f537904 | ||
|
|
25b6c5c3b0 | ||
|
|
398d13bf1b | ||
|
|
91b7c8d40f | ||
|
|
7dfef4b16a | ||
|
|
0397f23196 | ||
|
|
0865d61450 | ||
|
|
2eb8f3a255 | ||
|
|
22321a7ac9 | ||
|
|
c03802e97f | ||
|
|
1485c0c92c | ||
|
|
44ecbfa417 | ||
|
|
927a807c4d | ||
|
|
29a79ce0a9 | ||
|
|
2166744c63 | ||
|
|
81239f41ae | ||
|
|
584593ba71 | ||
|
|
4b83ff01cf | ||
|
|
8c88aa843c | ||
|
|
8a9cd72718 | ||
|
|
2484d057fb | ||
|
|
941b914fa9 | ||
|
|
bd683d021a | ||
|
|
124b1c1e59 | ||
|
|
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 |
@@ -6,6 +6,6 @@ NEXT_PUBLIC_USE_MOCK_BACKEND_LOOP_START=false
|
|||||||
NEXT_PUBLIC_EXPORT_STATIC=false
|
NEXT_PUBLIC_EXPORT_STATIC=false
|
||||||
NEXT_PUBLIC_USE_CGI=false
|
NEXT_PUBLIC_USE_CGI=false
|
||||||
# App-Versionsnummer
|
# App-Versionsnummer
|
||||||
NEXT_PUBLIC_APP_VERSION=1.6.785
|
NEXT_PUBLIC_APP_VERSION=1.6.913
|
||||||
NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter)
|
NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter)
|
||||||
|
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ NEXT_PUBLIC_CPL_API_PATH=/CPL
|
|||||||
NEXT_PUBLIC_EXPORT_STATIC=true
|
NEXT_PUBLIC_EXPORT_STATIC=true
|
||||||
NEXT_PUBLIC_USE_CGI=true
|
NEXT_PUBLIC_USE_CGI=true
|
||||||
# App-Versionsnummer
|
# App-Versionsnummer
|
||||||
NEXT_PUBLIC_APP_VERSION=1.6.785
|
NEXT_PUBLIC_APP_VERSION=1.6.913
|
||||||
NEXT_PUBLIC_CPL_MODE=production
|
NEXT_PUBLIC_CPL_MODE=production
|
||||||
@@ -1,34 +1,58 @@
|
|||||||
# .woodpecker.yml — Option B (Browser im Workspace, stabil für CI)
|
when:
|
||||||
|
- event: push
|
||||||
|
- event: pull_request
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: install-dependencies-and-browsers
|
- name: clone
|
||||||
image: node:22
|
image: woodpeckerci/plugin-git
|
||||||
environment:
|
settings:
|
||||||
PLAYWRIGHT_BROWSERS_PATH: "0"
|
depth: 0
|
||||||
CI: "true"
|
lfs: true
|
||||||
E2E_BASE_URL: "http://localhost:3000"
|
submodules: true
|
||||||
LANG: "C.UTF-8"
|
|
||||||
TZ: "Europe/Berlin"
|
- name: verify-mocks
|
||||||
PW_HEADLESS: "1" # erzwingt headless über die Config
|
image: mcr.microsoft.com/playwright:v1.54.2-jammy
|
||||||
commands:
|
commands:
|
||||||
- echo "📦 Installing deps..."
|
- pwd
|
||||||
|
- node -v && npm -v
|
||||||
|
# Skip lifecycle scripts in CI to avoid running husky's prepare step
|
||||||
- npm ci
|
- npm ci
|
||||||
- echo "🧩 Installing Playwright (Chromium) into workspace..."
|
# Zeig mir, ob die Datei wirklich im Checkout liegt:
|
||||||
- npx playwright install chromium
|
- 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: run-tests
|
- name: e2e-dev
|
||||||
image: node:22
|
image: mcr.microsoft.com/playwright:v1.54.2-jammy
|
||||||
environment:
|
environment:
|
||||||
PLAYWRIGHT_BROWSERS_PATH: "0"
|
|
||||||
CI: "true"
|
CI: "true"
|
||||||
E2E_BASE_URL: "http://localhost:3000"
|
NODE_ENV: "production"
|
||||||
LANG: "C.UTF-8"
|
NEXT_TELEMETRY_DISABLED: "1"
|
||||||
TZ: "Europe/Berlin"
|
PORT: "3000"
|
||||||
PW_HEADLESS: "1"
|
|
||||||
commands:
|
commands:
|
||||||
- echo "🔧 Installing system dependencies for Playwright..."
|
- node -v && npm -v
|
||||||
- npx playwright install-deps
|
# Skip lifecycle scripts in CI to avoid running husky's prepare step (husky is a devDep)
|
||||||
- echo "🌱 Starting dev server (npm run dev)..."
|
- env npm_config_production=false npm ci
|
||||||
- npm run dev &
|
- npm run build
|
||||||
|
# Start local static simulator in background
|
||||||
- echo "🧪 Running Playwright tests (Chromium only)..."
|
- npm run server:sim &
|
||||||
|
# Wait until simulator responds on port 3000 (no curl dependency)
|
||||||
|
- node -e "const http=require('http');let n=120;function ping(){http.get('http://localhost:3000',res=>{console.log('Server is up');process.exit(0)}).on('error',()=>{if(n--<=0){console.error('Server did not start');process.exit(1)}setTimeout(ping,1000)});}ping();"
|
||||||
- npx playwright test --project=chromium
|
- 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
|
||||||
|
|||||||
1199
CHANGELOG.md
1199
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
106
Jenkinsfile
vendored
106
Jenkinsfile
vendored
@@ -1,21 +1,95 @@
|
|||||||
pipeline {
|
pipeline {
|
||||||
agent any
|
agent any
|
||||||
|
|
||||||
environment {
|
environment {
|
||||||
NODE_PATH = '/var/jenkins_home/.npm/node_modules'
|
CI = "true"
|
||||||
NPM_CONFIG_CACHE = '/var/jenkins_home/.npm'
|
NODE_ENV = "production"
|
||||||
|
NEXT_TELEMETRY_DISABLED = "1"
|
||||||
|
PORT = "3000"
|
||||||
|
}
|
||||||
|
|
||||||
|
options {
|
||||||
|
timestamps()
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Checkout') {
|
||||||
|
steps {
|
||||||
|
checkout scm
|
||||||
|
sh '''
|
||||||
|
set -eux
|
||||||
|
git status --short || true
|
||||||
|
# Submodule & LFS falls vorhanden
|
||||||
|
git submodule update --init --recursive || true
|
||||||
|
git lfs install || true
|
||||||
|
git lfs fetch || true
|
||||||
|
git lfs checkout || true
|
||||||
|
'''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stages {
|
stage('verify-mocks') {
|
||||||
stage('Install dependencies') {
|
steps {
|
||||||
steps {
|
sh '''
|
||||||
sh 'npm install --prefer-offline --no-audit'
|
set -eux
|
||||||
}
|
docker run --rm -v "$PWD":/ws -w /ws \
|
||||||
}
|
mcr.microsoft.com/playwright:v1.54.2-jammy bash -lc "
|
||||||
stage('Run Playwright tests') {
|
pwd
|
||||||
steps {
|
node -v && npm -v
|
||||||
sh 'npx playwright test'
|
npm ci --ignore-scripts
|
||||||
}
|
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)
|
||||||
|
"
|
||||||
|
'''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
stage('e2e-dev') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
set -eux
|
||||||
|
docker run --rm -v "$PWD":/ws -w /ws -p 3000:3000 \
|
||||||
|
mcr.microsoft.com/playwright:v1.54.2-jammy bash -lc "
|
||||||
|
node -v && npm -v
|
||||||
|
env npm_config_production=false npm ci
|
||||||
|
npm run build
|
||||||
|
npm run server:sim &
|
||||||
|
# Auf Port 3000 warten
|
||||||
|
node -e \\"const http=require('http');let n=120;function ping(){http.get('http://localhost:3000',res=>{console.log('Server is up');process.exit(0)}).on('error',()=>{if(n--<=0){console.error('Server did not start');process.exit(1)}setTimeout(ping,1000)});}ping();\\"
|
||||||
|
npx playwright test --project=chromium
|
||||||
|
"
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
sh '''
|
||||||
|
docker run --rm curlimages/curl:8.9.1 \
|
||||||
|
-d "Tests erfolgreich in Jenkins" \
|
||||||
|
https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
sh '''
|
||||||
|
docker run --rm curlimages/curl:8.9.1 \
|
||||||
|
-d "Tests fehlgeschlagen in Jenkins" \
|
||||||
|
https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9Z35
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
always {
|
||||||
|
script {
|
||||||
|
if (fileExists('playwright-report')) {
|
||||||
|
archiveArtifacts artifacts: 'playwright-report/**', onlyIfSuccessful: false
|
||||||
|
} else {
|
||||||
|
echo 'Kein playwright-report gefunden.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -168,11 +168,11 @@ Beispielaufruf im DEV-Modus (über UI gesteuert, nicht manuell notwendig):
|
|||||||
### 🔌 System
|
### 🔌 System
|
||||||
|
|
||||||
- Live-Anzeige von:
|
- Live-Anzeige von:
|
||||||
- +5V, +15V, -15V, -98V Spannungen
|
- +5V, +15V, -15V, -96V Spannungen
|
||||||
- CPU- und ADC-Temperaturen
|
- CPU- und ADC-Temperaturen
|
||||||
- Verlaufskurven über Zeit (Chart.js)
|
- Verlaufskurven über Zeit (Chart.js)
|
||||||
- Spannungen und Temperaturen werden jetzt in zwei separaten Charts nebeneinander dargestellt
|
- Spannungen und Temperaturen werden jetzt in zwei separaten Charts nebeneinander dargestellt
|
||||||
- Spannungswerte (+5V, +15V, -15V, -98V) werden mit zwei Nachkommastellen angezeigt
|
- Spannungswerte (+5V, +15V, -15V, -96V) werden mit zwei Nachkommastellen angezeigt
|
||||||
|
|
||||||
### ⚙️ Einstellungen
|
### ⚙️ Einstellungen
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 117 KiB |
@@ -77,6 +77,8 @@ const DateRangePicker: React.FC<DateRangePickerProps> = ({
|
|||||||
minDate={sixMonthsAgo}
|
minDate={sixMonthsAgo}
|
||||||
maxDate={today}
|
maxDate={today}
|
||||||
dateFormat="dd.MM.yyyy"
|
dateFormat="dd.MM.yyyy"
|
||||||
|
portalId="root-portal"
|
||||||
|
popperClassName="custom-datepicker-popper"
|
||||||
className={`border px-2 py-1 rounded ${inputWidthClass} ${
|
className={`border px-2 py-1 rounded ${inputWidthClass} ${
|
||||||
compact ? "text-xs" : "text-sm"
|
compact ? "text-xs" : "text-sm"
|
||||||
}`}
|
}`}
|
||||||
@@ -107,6 +109,8 @@ const DateRangePicker: React.FC<DateRangePickerProps> = ({
|
|||||||
minDate={sixMonthsAgo}
|
minDate={sixMonthsAgo}
|
||||||
maxDate={today}
|
maxDate={today}
|
||||||
dateFormat="dd.MM.yyyy"
|
dateFormat="dd.MM.yyyy"
|
||||||
|
portalId="root-portal"
|
||||||
|
popperClassName="custom-datepicker-popper"
|
||||||
className={`border px-2 py-1 rounded ${inputWidthClass} ${
|
className={`border px-2 py-1 rounded ${inputWidthClass} ${
|
||||||
compact ? "text-xs" : "text-sm"
|
compact ? "text-xs" : "text-sm"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useAppDispatch } from "@/redux/store";
|
import { useAppDispatch } from "@/redux/store";
|
||||||
import { setEvents } from "@/redux/slices/deviceEventsSlice";
|
import {
|
||||||
|
setEvents,
|
||||||
|
initPersistedTimings,
|
||||||
|
} from "@/redux/slices/deviceEventsSlice";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
loopMeasurementEvent?: number[];
|
loopMeasurementEvent?: number[];
|
||||||
tdrMeasurementEvent?: number[];
|
tdrMeasurementEvent?: number[];
|
||||||
alignmentEvent?: number[];
|
comparisonEvent?: number[]; // renamed from alignmentEvent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +21,25 @@ export default function DeviceEventsBridge() {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let lastSig = "";
|
let lastSig = "";
|
||||||
|
// Hydrate persisted timings once
|
||||||
|
try {
|
||||||
|
const raw =
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
localStorage.getItem("deviceEventsTimingsV1");
|
||||||
|
if (raw) {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
dispatch(
|
||||||
|
initPersistedTimings({
|
||||||
|
loop: parsed.loop,
|
||||||
|
tdr: parsed.tdr,
|
||||||
|
compare: parsed.compare || parsed.align,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn("DeviceEventsBridge hydration failed", e);
|
||||||
|
}
|
||||||
const readAndDispatch = () => {
|
const readAndDispatch = () => {
|
||||||
const ksx = Array.isArray(window.loopMeasurementEvent)
|
const ksx = Array.isArray(window.loopMeasurementEvent)
|
||||||
? window.loopMeasurementEvent
|
? window.loopMeasurementEvent
|
||||||
@@ -25,8 +47,8 @@ export default function DeviceEventsBridge() {
|
|||||||
const ksy = Array.isArray(window.tdrMeasurementEvent)
|
const ksy = Array.isArray(window.tdrMeasurementEvent)
|
||||||
? window.tdrMeasurementEvent
|
? window.tdrMeasurementEvent
|
||||||
: undefined;
|
: undefined;
|
||||||
const ksz = Array.isArray(window.alignmentEvent)
|
const ksz = Array.isArray(window.comparisonEvent)
|
||||||
? window.alignmentEvent
|
? window.comparisonEvent
|
||||||
: undefined;
|
: undefined;
|
||||||
// Build a stable signature of first 32 values per array
|
// Build a stable signature of first 32 values per array
|
||||||
const to32 = (a?: number[]) => {
|
const to32 = (a?: number[]) => {
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { useAppSelector } from "@/redux/store";
|
|||||||
export default function GlobalActivityOverlay() {
|
export default function GlobalActivityOverlay() {
|
||||||
const anyLoop = useAppSelector((s) => s.deviceEvents.anyLoopActive);
|
const anyLoop = useAppSelector((s) => s.deviceEvents.anyLoopActive);
|
||||||
const anyTdr = useAppSelector((s) => s.deviceEvents.anyTdrActive);
|
const anyTdr = useAppSelector((s) => s.deviceEvents.anyTdrActive);
|
||||||
const anyAlign = useAppSelector((s) => s.deviceEvents.anyAlignmentActive);
|
const anyCompare = useAppSelector((s) => s.deviceEvents.anyComparisonActive);
|
||||||
const ksx = useAppSelector((s) => s.deviceEvents.ksx);
|
const ksx = useAppSelector((s) => s.deviceEvents.ksx);
|
||||||
const ksy = useAppSelector((s) => s.deviceEvents.ksy);
|
const ksy = useAppSelector((s) => s.deviceEvents.ksy);
|
||||||
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
|
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
|
||||||
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
|
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
|
||||||
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
|
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
|
||||||
const alignmentStartedAt = useAppSelector(
|
const comparisonStartedAt = useAppSelector(
|
||||||
(s) => s.deviceEvents.alignmentStartedAt
|
(s) => s.deviceEvents.comparisonStartedAt
|
||||||
);
|
);
|
||||||
|
|
||||||
const fmt = (arr: number[]) =>
|
const fmt = (arr: number[]) =>
|
||||||
@@ -24,13 +24,13 @@ export default function GlobalActivityOverlay() {
|
|||||||
// Simple 1s ticker so progress bars advance while overlay is shown
|
// Simple 1s ticker so progress bars advance while overlay is shown
|
||||||
const [now, setNow] = useState<number>(Date.now());
|
const [now, setNow] = useState<number>(Date.now());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const active = anyLoop || anyTdr || anyAlign;
|
const active = anyLoop || anyTdr || anyCompare;
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [anyLoop, anyTdr, anyAlign]);
|
}, [anyLoop, anyTdr, anyCompare]);
|
||||||
|
|
||||||
const active = anyLoop || anyTdr || anyAlign;
|
const active = anyLoop || anyTdr || anyCompare;
|
||||||
if (!active) return null;
|
if (!active) return null;
|
||||||
|
|
||||||
const clamp = (v: number, min = 0, max = 1) =>
|
const clamp = (v: number, min = 0, max = 1) =>
|
||||||
@@ -102,13 +102,13 @@ export default function GlobalActivityOverlay() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{anyAlign && (
|
{anyCompare && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-gray-800 mb-1">
|
<div className="text-sm text-gray-800 mb-1">
|
||||||
Abgleich läuft… (KÜ: {fmt(ksz)}) kann bis zu 10 Minuten dauern
|
Comparison läuft… (KÜ: {fmt(ksz)}) kann bis zu 10 Minuten dauern
|
||||||
</div>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const { pct } = compute(alignmentStartedAt, ALIGN_MS);
|
const { pct } = compute(comparisonStartedAt, ALIGN_MS);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||||
|
|||||||
@@ -55,45 +55,57 @@ function Footer() {
|
|||||||
}, [showSlider]);
|
}, [showSlider]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="relative bg-gray-300 p-4 xl:p-2 2xl:p-4 overflow-hidden text-black laptop:h-[5vh] ">
|
<footer className="relative bg-[var(--color-surface-alt)] border-t border-base p-4 xl:p-2 2xl:p-4 overflow-hidden text-[var(--color-fg)] laptop:h-[5vh] theme-transition">
|
||||||
<div className="container mx-auto flex justify-between">
|
<div className="container mx-auto flex justify-between">
|
||||||
<div className="flex flex-row space-x-2">
|
<div className="flex flex-row space-x-2 items-center">
|
||||||
<Icon
|
<Icon
|
||||||
icon="material-symbols:factory-outline"
|
icon="material-symbols:factory-outline"
|
||||||
className="text-xl text-blue-400"
|
className="text-xl text-accent"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm">Littwin Systemtechnik GmbH & Co. KG</p>
|
<p className="text-sm text-fg-muted">
|
||||||
|
Littwin Systemtechnik GmbH & Co. KG
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row space-x-2">
|
<div className="flex flex-row space-x-2 items-center">
|
||||||
<Icon icon="charm:phone" className="text-xl text-blue-400" />
|
<Icon icon="charm:phone" className="text-xl text-accent" />
|
||||||
<p className="text-sm">Telefon: 04402 972577-0</p>
|
<p className="text-sm text-fg-muted">Telefon: 04402 972577-0</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row space-x-2">
|
<div className="flex flex-row space-x-2 items-center">
|
||||||
<Icon icon="mdi:email-outline" className="text-xl text-blue-400" />
|
<Icon icon="mdi:email-outline" className="text-xl text-accent" />
|
||||||
<p className="text-sm">kontakt@littwin-systemtechnik.de</p>
|
<p className="text-sm text-fg-muted">
|
||||||
|
kontakt@littwin-systemtechnik.de
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex flex-row space-x-2 cursor-pointer"
|
className="flex flex-row space-x-2 cursor-pointer items-center group"
|
||||||
onClick={() => setShowSlider(true)}
|
onClick={() => setShowSlider(true)}
|
||||||
>
|
>
|
||||||
<Icon icon="bi:book" className="text-xl text-blue-400" />
|
<Icon
|
||||||
<p className="text-sm">Handbücher</p>
|
icon="bi:book"
|
||||||
|
className="text-xl text-accent group-hover:brightness-110 transition"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-fg-muted group-hover:text-[var(--color-fg)] transition">
|
||||||
|
Handbücher
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
className={`fixed top-0 right-0 w-64 h-full bg-white shadow-lg transform transition-transform duration-300 ${
|
className={`fixed top-0 right-0 w-64 h-full bg-[var(--color-surface)] border-l border-base shadow-lg transform transition-transform duration-300 ${
|
||||||
showSlider ? "translate-x-0" : "translate-x-full"
|
showSlider ? "translate-x-0" : "translate-x-full"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="p-4 flex justify-between items-center border-b">
|
<div className="p-4 flex justify-between items-center border-b border-base">
|
||||||
<h3 className="text-lg font-semibold">PDF Handbücher</h3>
|
<h3 className="text-base font-semibold text-[var(--color-fg)]">
|
||||||
|
PDF Handbücher
|
||||||
|
</h3>
|
||||||
<button
|
<button
|
||||||
className="text-gray-500 hover:text-gray-800"
|
className="text-[var(--color-muted)] hover:text-[var(--color-fg)] transition"
|
||||||
onClick={() => setShowSlider(false)}
|
onClick={() => setShowSlider(false)}
|
||||||
|
aria-label="Schließen"
|
||||||
>
|
>
|
||||||
<Icon icon="carbon:close" className="text-2xl" />
|
<Icon icon="carbon:close" className="text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -102,7 +114,7 @@ function Footer() {
|
|||||||
{pdfFiles.map((fileName) => (
|
{pdfFiles.map((fileName) => (
|
||||||
<li
|
<li
|
||||||
key={fileName}
|
key={fileName}
|
||||||
className="cursor-pointer text-blue-500 hover:underline mb-2"
|
className="cursor-pointer text-accent hover:underline mb-2 text-sm"
|
||||||
onClick={() => loadPDF(fileName)}
|
onClick={() => loadPDF(fileName)}
|
||||||
>
|
>
|
||||||
{fileName}
|
{fileName}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client"; // components/Header.jsx
|
"use client"; // components/Header.jsx
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -15,16 +15,18 @@ function Header() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
||||||
const [isAdminLoggedIn, setIsAdminLoggedIn] = useState(false);
|
const [isAdminLoggedIn, setIsAdminLoggedIn] = useState(false);
|
||||||
|
const autoLogoutTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
// Removed duplicate declaration of deviceName
|
// Removed duplicate declaration of deviceName
|
||||||
|
|
||||||
const handleCloseSettingsModal = () => setShowSettingsModal(false);
|
const handleCloseSettingsModal = () => setShowSettingsModal(false);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = useCallback(() => {
|
||||||
sessionStorage.removeItem("token"); // Token entfernen
|
sessionStorage.removeItem("token"); // Token entfernen
|
||||||
localStorage.setItem("isAdminLoggedIn", "false"); // Admin-Status entfernen
|
localStorage.setItem("isAdminLoggedIn", "false"); // Admin-Status entfernen
|
||||||
|
localStorage.removeItem("adminLoginTime"); // Login-Zeitpunkt entfernen
|
||||||
setIsAdminLoggedIn(false); // Zustand sofort aktualisieren
|
setIsAdminLoggedIn(false); // Zustand sofort aktualisieren
|
||||||
router.push("/offline.html"); // Weiterleitung
|
router.push("/offline.html"); // Weiterleitung
|
||||||
};
|
}, [router]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initialer Check beim Laden der Komponente
|
// Initialer Check beim Laden der Komponente
|
||||||
@@ -43,6 +45,56 @@ function Header() {
|
|||||||
clearInterval(interval); // Intervall stoppen, wenn die Komponente entladen wird
|
clearInterval(interval); // Intervall stoppen, wenn die Komponente entladen wird
|
||||||
};
|
};
|
||||||
}, [isAdminLoggedIn]);
|
}, [isAdminLoggedIn]);
|
||||||
|
|
||||||
|
// Auto-Logout nach 1 Minute (Test): nutzt adminLoginTime aus localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
// Timer bereinigen, wenn sich der Status ändert
|
||||||
|
if (autoLogoutTimerRef.current) {
|
||||||
|
clearTimeout(autoLogoutTimerRef.current);
|
||||||
|
autoLogoutTimerRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdminLoggedIn) return;
|
||||||
|
|
||||||
|
const iso = localStorage.getItem("adminLoginTime");
|
||||||
|
const loginTime = iso ? new Date(iso).getTime() : Date.now();
|
||||||
|
if (!iso) {
|
||||||
|
// Falls älterer Login ohne Zeitstempel, setze jetzt
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
"adminLoginTime",
|
||||||
|
new Date(loginTime).toISOString()
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
void 0; // ignore write errors (e.g., storage disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1 Minute ab Login (60_000 ms), eine Stunde (3_600_000 ms) im Produktivbetrieb
|
||||||
|
const target = loginTime + 3_600_000;
|
||||||
|
const delay = Math.max(0, target - Date.now());
|
||||||
|
|
||||||
|
// Fallback: wenn Datum in Vergangenheit (z.B. Uhrzeit geändert), sofort abmelden
|
||||||
|
autoLogoutTimerRef.current = setTimeout(() => {
|
||||||
|
// Versuche den Button zu klicken, falls vorhanden
|
||||||
|
const btn = document.querySelector<HTMLButtonElement>(
|
||||||
|
'button[aria-label="Abmelden"]'
|
||||||
|
);
|
||||||
|
if (btn) {
|
||||||
|
btn.click();
|
||||||
|
} else {
|
||||||
|
// Fallback direkt
|
||||||
|
handleLogout();
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (autoLogoutTimerRef.current) {
|
||||||
|
clearTimeout(autoLogoutTimerRef.current);
|
||||||
|
autoLogoutTimerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isAdminLoggedIn, handleLogout]);
|
||||||
//----------------------------------------------------------------
|
//----------------------------------------------------------------
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
@@ -57,27 +109,49 @@ function Header() {
|
|||||||
}, [deviceName, dispatch]);
|
}, [deviceName, dispatch]);
|
||||||
//----------------------------------------------------------------
|
//----------------------------------------------------------------
|
||||||
|
|
||||||
// Dark/Light Mode Toggle
|
// Dark/Light Mode Toggle (persisted)
|
||||||
const [isDark, setIsDark] = useState(false);
|
const [isDark, setIsDark] = useState(false);
|
||||||
|
// Initial state from html class / localStorage (set by _document script before hydration)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window === "undefined") return;
|
||||||
const html = document.documentElement;
|
const html = document.documentElement;
|
||||||
if (isDark) {
|
const stored = localStorage.getItem("theme");
|
||||||
html.classList.add("dark");
|
const active = stored ? stored === "dark" : html.classList.contains("dark");
|
||||||
} else {
|
setIsDark(active);
|
||||||
html.classList.remove("dark");
|
}, []);
|
||||||
}
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
const html = document.documentElement;
|
||||||
|
if (isDark) {
|
||||||
|
html.classList.add("dark");
|
||||||
|
localStorage.setItem("theme", "dark");
|
||||||
|
} else {
|
||||||
|
html.classList.remove("dark");
|
||||||
|
localStorage.setItem("theme", "light");
|
||||||
}
|
}
|
||||||
}, [isDark]);
|
}, [isDark]);
|
||||||
|
|
||||||
|
// Keyboard shortcut Alt + D to toggle theme
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
if (e.altKey && (e.key === "d" || e.key === "D")) {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDark((d) => !d);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handler);
|
||||||
|
return () => window.removeEventListener("keydown", handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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 ">
|
<header className="bg-[var(--color-surface-alt)] dark:bg-[var(--color-surface-alt)] flex justify-between items-center w-full h-[13vh] laptop:h-[10vh] relative text-[var(--color-fg)] border-b border-base theme-transition">
|
||||||
<div
|
<div
|
||||||
className="absolute transform -translate-y-1/2
|
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:
|
left-[8%] sm:left-[8%] md:left-[8%] lg:left-[8%] xl:left-[6%] 2xl:left-[2%] laptop:left-[4%] laptop:
|
||||||
top-[90%] sm:top-[90%] md:top-[90%] lg:top-[90%] xl:top-[90%]"
|
top-[90%] sm:top-[90%] md:top-[90%] lg:top-[90%] xl:top-[90%]"
|
||||||
style={{
|
style={{
|
||||||
height: "10vh", // Dynamische Höhe des Containers
|
height: "12vh", // Erhöhte Höhe des Containers für größeres Logo
|
||||||
width: "auto",
|
width: "auto",
|
||||||
aspectRatio: "1", // Beibehaltung des Seitenverhältnisses
|
aspectRatio: "1", // Beibehaltung des Seitenverhältnisses
|
||||||
}}
|
}}
|
||||||
@@ -86,7 +160,7 @@ function Header() {
|
|||||||
src="/images/Logo.png"
|
src="/images/Logo.png"
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
fill
|
fill
|
||||||
sizes="(max-width: 640px) 7vh, (max-width: 1024px) 8vh, (max-width: 1280px) 9vh, 10vh"
|
sizes="(max-width: 640px) 12vh, (max-width: 1024px) 14vh, (max-width: 1280px) 16vh, 18vh"
|
||||||
className="object-contain"
|
className="object-contain"
|
||||||
priority={false}
|
priority={false}
|
||||||
/>
|
/>
|
||||||
@@ -102,10 +176,10 @@ function Header() {
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col leading-tight whitespace-nowrap">
|
<div className="flex flex-col leading-tight whitespace-nowrap">
|
||||||
<h2 className="text-xl laptop:text-base xl:text-lg font-bold text-gray-900 dark:text-gray-100">
|
<h2 className="text-xl laptop:text-base xl:text-lg font-bold text-[var(--color-fg)]">
|
||||||
Meldestation
|
Meldestation
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 dark:text-gray-300 text-lg laptop:text-sm xl:text-base truncate max-w-[20vw]">
|
<p className="text-[var(--color-fg-muted)] text-lg laptop:text-sm xl:text-base truncate max-w-[20vw]">
|
||||||
{deviceName}
|
{deviceName}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,7 +191,7 @@ function Header() {
|
|||||||
<button
|
<button
|
||||||
aria-label={isDark ? "Light Mode" : "Dark Mode"}
|
aria-label={isDark ? "Light Mode" : "Dark Mode"}
|
||||||
onClick={() => setIsDark((d) => !d)}
|
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"
|
className="rounded-full p-2 bg-[var(--color-surface-alt)]/80 hover:bg-[var(--color-surface-alt)] dark:bg-[var(--color-surface-alt)]/60 dark:hover:bg-[var(--color-surface-alt)] transition border border-[var(--color-border)]"
|
||||||
title={isDark ? "Light Mode" : "Dark Mode"}
|
title={isDark ? "Light Mode" : "Dark Mode"}
|
||||||
>
|
>
|
||||||
{isDark ? (
|
{isDark ? (
|
||||||
@@ -139,7 +213,8 @@ function Header() {
|
|||||||
<div className="flex items-center justify-end w-1/4 space-x-1">
|
<div className="flex items-center justify-end w-1/4 space-x-1">
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded"
|
aria-label="Abmelden"
|
||||||
|
className="px-4 py-2 rounded bg-[var(--color-accent)] text-white hover:brightness-110 shadow-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-ring)] focus:ring-offset-2 focus:ring-offset-[var(--color-background)] transition"
|
||||||
>
|
>
|
||||||
Abmelden
|
Abmelden
|
||||||
</button>
|
</button>
|
||||||
@@ -149,7 +224,7 @@ function Header() {
|
|||||||
|
|
||||||
{/* Warnhinweis, wenn der Admin angemeldet ist */}
|
{/* Warnhinweis, wenn der Admin angemeldet ist */}
|
||||||
{isAdminLoggedIn && (
|
{isAdminLoggedIn && (
|
||||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-full xl:w-1/4 2xl:w-1/4 h-8 bg-yellow-400 text-center py-2 text-black font-bold">
|
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-full xl:w-1/4 2xl:w-1/4 h-8 bg-[var(--color-warning)] text-center py-2 text-black font-bold tracking-wide">
|
||||||
Admin-Modus aktiv
|
Admin-Modus aktiv
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -367,9 +367,9 @@ export default function AnalogInputsChart({
|
|||||||
<div
|
<div
|
||||||
className={`flex flex-col gap-2 h-full ${loading ? "cursor-wait" : ""}`}
|
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-between items-center p-2 rounded-lg space-x-2 bg-[var(--color-surface-alt)] border border-base">
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<Dialog.Title className="text-lg font-semibold text-gray-700">
|
<Dialog.Title className="text-lg font-semibold text-fg">
|
||||||
Eingang {selectedId ?? "–"}
|
Eingang {selectedId ?? "–"}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
</div>
|
</div>
|
||||||
@@ -385,7 +385,7 @@ export default function AnalogInputsChart({
|
|||||||
{/* ✅ Zeitraum-Auswahl (Listbox nur lokal) */}
|
{/* ✅ Zeitraum-Auswahl (Listbox nur lokal) */}
|
||||||
<Listbox value={localZeitraum} onChange={setLocalZeitraum}>
|
<Listbox value={localZeitraum} onChange={setLocalZeitraum}>
|
||||||
<div className="relative w-48">
|
<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">
|
<Listbox.Button className="w-full border border-base px-3 py-1 rounded bg-[var(--color-surface)] text-fg flex justify-between items-center text-sm">
|
||||||
<span>
|
<span>
|
||||||
{localZeitraum === "DIA0"
|
{localZeitraum === "DIA0"
|
||||||
? "Alle Messwerte"
|
? "Alle Messwerte"
|
||||||
@@ -393,14 +393,14 @@ export default function AnalogInputsChart({
|
|||||||
? "Stündlich"
|
? "Stündlich"
|
||||||
: "Täglich"}
|
: "Täglich"}
|
||||||
</span>
|
</span>
|
||||||
<i className="bi bi-chevron-down text-gray-400" />
|
<i className="bi bi-chevron-down text-[var(--color-muted)]" />
|
||||||
</Listbox.Button>
|
</Listbox.Button>
|
||||||
<Listbox.Options className="absolute z-10 mt-1 w-full border bg-white shadow rounded text-sm">
|
<Listbox.Options className="absolute z-10 mt-1 w-full border border-base bg-[var(--color-surface)] shadow rounded text-sm">
|
||||||
{["DIA0", "DIA1", "DIA2"].map((option) => (
|
{["DIA0", "DIA1", "DIA2"].map((option) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={option}
|
key={option}
|
||||||
value={option}
|
value={option}
|
||||||
className="px-4 py-1 cursor-pointer hover:bg-gray-200"
|
className="px-4 py-1 cursor-pointer hover:bg-[var(--color-surface-alt)] text-fg"
|
||||||
>
|
>
|
||||||
{option === "DIA0"
|
{option === "DIA0"
|
||||||
? "Alle Messwerte"
|
? "Alle Messwerte"
|
||||||
@@ -416,7 +416,7 @@ export default function AnalogInputsChart({
|
|||||||
{/* ✅ Button: lädt die Daten & aktualisiert Redux */}
|
{/* ✅ Button: lädt die Daten & aktualisiert Redux */}
|
||||||
<button
|
<button
|
||||||
onClick={handleFetchData}
|
onClick={handleFetchData}
|
||||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm"
|
className="btn-primary px-4 py-1 rounded text-sm"
|
||||||
>
|
>
|
||||||
Daten laden
|
Daten laden
|
||||||
</button>
|
</button>
|
||||||
@@ -427,11 +427,9 @@ export default function AnalogInputsChart({
|
|||||||
{/* Chart-Anzeige */}
|
{/* Chart-Anzeige */}
|
||||||
<div className="flex-1 min-h-0 w-full">
|
<div className="flex-1 min-h-0 w-full">
|
||||||
{!selectedAnalogInput?.id ? (
|
{!selectedAnalogInput?.id ? (
|
||||||
<div className="flex items-center justify-center h-full text-gray-500 text-lg gap-2">
|
<div className="flex items-center justify-center h-full text-fg-secondary text-lg gap-2">
|
||||||
<i className="bi bi-info-circle text-2xl mr-2" />
|
<i className="bi bi-info-circle text-2xl mr-2" />
|
||||||
<span>
|
<span>Bitte Eingang auswählen</span>
|
||||||
Bitte wählen Sie einen Eingang aus, um die Messkurve anzuzeigen
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Line
|
<Line
|
||||||
|
|||||||
@@ -35,82 +35,50 @@ export default function AnalogInputsChartModal({
|
|||||||
<div className="fixed inset-0 bg-black/50" aria-hidden="true" />
|
<div className="fixed inset-0 bg-black/50" aria-hidden="true" />
|
||||||
{/* Centered panel */}
|
{/* Centered panel */}
|
||||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||||
<Dialog.Panel className="relative">
|
<Dialog.Panel className="relative outline-none">
|
||||||
<div
|
<div
|
||||||
className="bg-white rounded-xl shadow-xl border border-gray-200"
|
className={`rounded-xl shadow-xl border border-base bg-[var(--color-surface)] text-fg flex flex-col transition-all duration-300 overflow-hidden`}
|
||||||
style={{
|
style={{
|
||||||
width: isFullscreen ? "90vw" : "70rem",
|
width: isFullscreen ? "90vw" : "70rem",
|
||||||
height: isFullscreen ? "90vh" : "35rem",
|
height: isFullscreen ? "90vh" : "38rem",
|
||||||
padding: "1rem",
|
|
||||||
transition: "all 0.3s ease-in-out",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Controls top-right (fullscreen + close) */}
|
{/* Header */}
|
||||||
<div
|
<header className="flex items-center justify-between px-6 py-4 border-b border-base select-none">
|
||||||
style={{
|
<h2 className="text-base font-bold">
|
||||||
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 ?? "–"}
|
Messkurve Messwerteingang {selectedId ?? "–"}
|
||||||
</Dialog.Title>
|
</h2>
|
||||||
</div>
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
{/* Chart container (structure similar to IsoChartView) */}
|
onClick={() => setIsFullscreen((v) => !v)}
|
||||||
<div
|
className="icon-btn text-xl"
|
||||||
style={{
|
aria-label={isFullscreen ? "Vollbild verlassen" : "Vollbild"}
|
||||||
flex: 1,
|
type="button"
|
||||||
display: "flex",
|
title={isFullscreen ? "Vollbild verlassen" : "Vollbild"}
|
||||||
flexDirection: "column",
|
>
|
||||||
height: "90%",
|
<i
|
||||||
}}
|
className={
|
||||||
>
|
isFullscreen
|
||||||
{/* Optional: place an action bar here if needed */}
|
? "bi bi-fullscreen-exit"
|
||||||
<div style={{ flex: 1, height: "90%" }}>
|
: "bi bi-arrows-fullscreen"
|
||||||
<AnalogInputsChart loading={loading} setLoading={setLoading} />
|
}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => dispatch(setIsChartModalOpen(false))}
|
||||||
|
className="icon-btn text-2xl"
|
||||||
|
aria-label="Modal schließen"
|
||||||
|
type="button"
|
||||||
|
title="Schließen"
|
||||||
|
>
|
||||||
|
<i className="bi bi-x-circle-fill" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div className="flex-1 min-h-0 px-4 pt-3 pb-4 bg-[var(--color-surface)]">
|
||||||
|
<AnalogInputsChart loading={loading} setLoading={setLoading} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
|||||||
@@ -108,125 +108,118 @@ export default function AnalogInputsSettingsModal() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-50 flex justify-center items-center z-50">
|
<div className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-50 flex justify-center items-center z-50">
|
||||||
<div className="bg-white rounded-lg shadow-lg p-6 w-1/2 max-w-lg">
|
<div className="bg-[var(--color-surface)] border border-base rounded-xl shadow-xl w-[32rem] max-w-full p-0 flex flex-col">
|
||||||
<div className="mb-4 border-b pb-2 flex justify-between items-center">
|
<header className="modal-header flex items-center justify-between px-6 py-4 border-b border-base">
|
||||||
<h2 className="text-base font-bold">
|
<h2 className="text-base font-bold text-fg">
|
||||||
Einstellungen Messwerteingang {selectedInput.id}
|
Einstellungen Messwerteingang {selectedInput.id}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => dispatch(setIsSettingsModalOpen(false))}
|
onClick={() => dispatch(setIsSettingsModalOpen(false))}
|
||||||
className="text-2xl hover:text-gray-400"
|
className="icon-btn text-2xl"
|
||||||
aria-label="Modal schließen"
|
aria-label="Modal schließen"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<i className="bi bi-x-circle-fill"></i>
|
<i className="bi bi-x-circle-fill" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</header>
|
||||||
|
<div className="modal-body-scroll px-6 py-5 flex-1 text-fg">
|
||||||
{/* Bezeichnung */}
|
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
<span className="font-normal text-fg-secondary">Bezeichnung:</span>
|
||||||
<span className="font-normal">Bezeichnung:</span>
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
className="border border-base rounded px-2 py-1 w-full bg-[var(--color-surface-alt)] text-fg"
|
||||||
className="w-full border rounded px-3 py-1 mb-4"
|
value={label}
|
||||||
value={label}
|
onChange={(e) => setLabel(e.target.value)}
|
||||||
onChange={(e) => setLabel(e.target.value)}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4">
|
||||||
|
<span className="font-normal text-fg-secondary">Offset:</span>
|
||||||
{/* Offset */}
|
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4">
|
|
||||||
<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">
|
|
||||||
<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 */}
|
|
||||||
<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">
|
|
||||||
<span className="font-normal">Speicherintervall:</span>
|
|
||||||
<div className="relative w-full">
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="border rounded px-2 py-1 pr-20 w-full text-right"
|
step="0.001"
|
||||||
value={loggerInterval}
|
className="border border-base rounded px-2 py-1 w-full text-right bg-[var(--color-surface-alt)] text-fg"
|
||||||
onChange={(e) => setLoggerInterval(e.target.value)}
|
value={offset}
|
||||||
|
onChange={(e) => setOffset(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<span className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
|
</div>
|
||||||
Minuten
|
<div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4">
|
||||||
|
<span className="font-normal text-fg-secondary">Faktor:</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.001"
|
||||||
|
className="border border-base rounded px-2 py-1 w-full text-right bg-[var(--color-surface-alt)] text-fg"
|
||||||
|
value={factor}
|
||||||
|
onChange={(e) => setFactor(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4">
|
||||||
|
<span className="font-normal text-fg-secondary">Einheit:</span>
|
||||||
|
<Listbox value={unit} onChange={setUnit}>
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Listbox.Button className="w-full border border-base px-2 py-1 rounded text-left bg-[var(--color-surface-alt)] text-fg flex justify-between items-center text-sm font-sans">
|
||||||
|
<span>{unit}</span>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 text-muted"
|
||||||
|
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 border-base rounded bg-[var(--color-surface-alt)] shadow max-h-60 overflow-auto text-sm text-fg 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-base-muted"
|
||||||
|
: "text-fg"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{opt}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||||
|
<span className="font-normal text-fg-secondary">
|
||||||
|
Speicherintervall:
|
||||||
</span>
|
</span>
|
||||||
|
<div className="relative w-full">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="border border-base rounded px-2 py-1 pr-20 w-full text-right bg-[var(--color-surface-alt)] text-fg"
|
||||||
|
value={loggerInterval}
|
||||||
|
onChange={(e) => setLoggerInterval(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span className="absolute right-2 top-1/2 transform -translate-y-1/2 text-muted text-sm">
|
||||||
|
Minuten
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<footer className="px-6 py-4 border-t border-base flex justify-end">
|
||||||
{/* Speichern-Button */}
|
|
||||||
<div className="flex justify-end gap-2 mt-6">
|
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded flex items-center"
|
className="btn-primary px-4 py-2 rounded flex items-center"
|
||||||
>
|
>
|
||||||
{isSaving ? "Speichern..." : "Speichern"}
|
{isSaving ? "Speichern..." : "Speichern"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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 ${
|
className={`text-[var(--color-fg)] bg-[var(--color-surface)] dark:bg-[var(--color-surface)] shadow-sm border border-[var(--color-border)] p-3 rounded-lg laptop:p-1 xl:p-1 ${
|
||||||
loading ? "cursor-wait" : ""
|
loading ? "cursor-wait opacity-70" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
||||||
@@ -54,24 +54,24 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
|||||||
loading ? "cursor-wait" : ""
|
loading ? "cursor-wait" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b items-center">
|
<thead className="bg-[var(--color-surface-alt)]/60 dark:bg-[var(--color-surface-alt)]/30 text-[var(--color-fg)] border-b border-[var(--color-border)] items-center">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
Eingang
|
Eingang
|
||||||
</th>
|
</th>
|
||||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
Messwert
|
Messwert
|
||||||
</th>
|
</th>
|
||||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
Einheit
|
Einheit
|
||||||
</th>
|
</th>
|
||||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
Bezeichnung
|
Bezeichnung
|
||||||
</th>
|
</th>
|
||||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
Einstellungen
|
Einstellungen
|
||||||
</th>
|
</th>
|
||||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
Messkurve
|
Messkurve
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -92,12 +92,12 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
|||||||
loading
|
loading
|
||||||
? "cursor-wait"
|
? "cursor-wait"
|
||||||
: analogInput.id === activeId
|
: analogInput.id === activeId
|
||||||
? "bg-blue-100 dark:bg-gray-700 dark:text-white"
|
? "bg-[var(--color-accent-soft)] dark:bg-[var(--color-surface-alt)]/60 text-[var(--color-fg)]"
|
||||||
: "hover:bg-gray-100 dark:hover:bg-gray-800"
|
: "hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
|
||||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1 ">
|
<div className="flex items-center gap-1 ">
|
||||||
@@ -109,7 +109,7 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="border p-2 text-right bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
className="border p-2 text-right bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
|
||||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||||
>
|
>
|
||||||
{typeof analogInput.value === "number"
|
{typeof analogInput.value === "number"
|
||||||
@@ -118,19 +118,19 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td
|
<td
|
||||||
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
|
||||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||||
>
|
>
|
||||||
{analogInput.unit || "-"}
|
{analogInput.unit || "-"}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
|
||||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||||
>
|
>
|
||||||
{analogInput.label || "----"}
|
{analogInput.label || "----"}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="border p-2 text-center bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border p-2 text-center bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSelect(analogInput.id!, analogInput);
|
handleSelect(analogInput.id!, analogInput);
|
||||||
@@ -141,7 +141,7 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
|||||||
<Icon icon={settingsIcon} className="text-xl" />
|
<Icon icon={settingsIcon} className="text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 text-center bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border p-2 text-center bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSelect(analogInput.id!, analogInput);
|
handleSelect(analogInput.id!, analogInput);
|
||||||
|
|||||||
@@ -35,14 +35,12 @@ function AnalogInputsView() {
|
|||||||
loading ? "cursor-wait" : ""
|
loading ? "cursor-wait" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="container mx-auto">
|
<div className="grid grid-cols-1 gap-4 justify-items-start">
|
||||||
<div className="grid grid-cols-1 gap-4 justify-items-start">
|
<div className="rounded-lg p-4 max-w-3xl text-[var(--color-fg)] bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border border-[var(--color-border)] shadow-sm">
|
||||||
<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-[var(--color-fg)] tracking-wide">
|
||||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
Messwerteingänge
|
||||||
Messwerteingänge
|
</h2>
|
||||||
</h2>
|
<AnalogInputsTable loading={loading} />
|
||||||
<AnalogInputsTable loading={loading} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const Baugruppentraeger: React.FC = () => {
|
|||||||
baugruppen.push(
|
baugruppen.push(
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="flex bg-white shadow-md rounded-lg mb-4 xl:mb-0 lg:mb-0 border border-gray-200 w-full laptop:scale-y-75 xl:scale-y-90"
|
className="flex card mb-4 xl:mb-0 lg:mb-0 w-full laptop:scale-y-75 xl:scale-y-90"
|
||||||
>
|
>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{slots.map((version, index) => {
|
{slots.map((version, index) => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const DashboardView: React.FC = () => {
|
|||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
//-------------------------------------
|
//-------------------------------------
|
||||||
return (
|
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">
|
<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-[var(--color-background)] text-[var(--color-fg)]">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex justify-between items-center w-full lg:w-2/3">
|
<div className="flex justify-between items-center w-full lg:w-2/3">
|
||||||
<div className="flex justify-between gap-1">
|
<div className="flex justify-between gap-1">
|
||||||
@@ -33,7 +33,7 @@ const DashboardView: React.FC = () => {
|
|||||||
icon="ri:calendar-schedule-line"
|
icon="ri:calendar-schedule-line"
|
||||||
className="text-littwin-blue text-4xl xl:text-2xl"
|
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">
|
<h1 className="text-xl font-bold xl:text-base text-[var(--color-fg)] tracking-wide">
|
||||||
Letzten 20 Meldungen
|
Letzten 20 Meldungen
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,22 +48,22 @@ export default function Last20MessagesTable({ className }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className={`flex flex-col gap-3 p-4 ${className}`}>
|
<div className={`flex flex-col gap-3 p-4 ${className}`}>
|
||||||
<div className="overflow-auto max-h-[80vh]">
|
<div className="overflow-auto max-h-[80vh]">
|
||||||
<table className="min-w-full border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<table className="min-w-full border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10">
|
<thead className="text-left sticky top-0 z-10 bg-[var(--color-surface-alt)]/70 dark:bg-[var(--color-surface-alt)]/25 text-[var(--color-fg)]">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Prio
|
Prio
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Zeitstempel
|
Zeitstempel
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Quelle
|
Quelle
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Meldung
|
Meldung
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -72,24 +72,24 @@ export default function Last20MessagesTable({ className }: Props) {
|
|||||||
{filteredMessages.slice(0, 20).map((msg, index) => (
|
{filteredMessages.slice(0, 20).map((msg, index) => (
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
className="hover:bg-gray-100 dark:hover:bg-gray-800"
|
className="hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30 transition"
|
||||||
>
|
>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
<div
|
<div
|
||||||
className="w-4 h-4 rounded"
|
className="w-4 h-4 rounded"
|
||||||
style={{ backgroundColor: msg.c }}
|
style={{ backgroundColor: msg.c }}
|
||||||
></div>
|
></div>
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
{msg.t}
|
{msg.t}
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
{msg.i}
|
{msg.i}
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
{msg.m}
|
{msg.m}
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
{msg.v}
|
{msg.v}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -97,7 +97,7 @@ export default function Last20MessagesTable({ className }: Props) {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{messages.length === 0 && (
|
{messages.length === 0 && (
|
||||||
<div className="mt-4 text-center text-gray-500 italic dark:text-gray-400">
|
<div className="mt-4 text-center italic text-[var(--color-fg-muted)]">
|
||||||
Keine Meldungen im gewählten Zeitraum vorhanden.
|
Keine Meldungen im gewählten Zeitraum vorhanden.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const NetworkInfo: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex-direction: row flex">
|
<div className="w-full flex-direction: row flex">
|
||||||
<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-grow flex justify-between items-center mt-1 p-2 rounded-lg shadow-sm bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border border-[var(--color-border)] laptop:m-0 laptop:scale-y-75 2xl:scale-y-75">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<Image
|
<Image
|
||||||
src="/images/IP-icon.svg"
|
src="/images/IP-icon.svg"
|
||||||
@@ -49,12 +49,8 @@ const NetworkInfo: React.FC = () => {
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-[var(--color-fg-muted)]">IP-Adresse</p>
|
||||||
IP-Adresse
|
<p className="text-sm font-medium text-[var(--color-fg)]">{ip}</p>
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
|
||||||
{ip}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -68,10 +64,8 @@ const NetworkInfo: React.FC = () => {
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-[var(--color-fg-muted)]">Subnet-Maske</p>
|
||||||
Subnet-Maske
|
<p className="text-sm font-medium text-[var(--color-fg)]">
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
|
||||||
{subnet}
|
{subnet}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,8 +81,8 @@ const NetworkInfo: React.FC = () => {
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Gateway</p>
|
<p className="text-xs text-[var(--color-fg-muted)]">Gateway</p>
|
||||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
<p className="text-sm font-medium text-[var(--color-fg)]">
|
||||||
{gateway}
|
{gateway}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,8 +91,8 @@ const NetworkInfo: React.FC = () => {
|
|||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="text-xs font-bold text-littwin-blue">OPC-UA</div>
|
<div className="text-xs font-bold text-littwin-blue">OPC-UA</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Status</p>
|
<p className="text-xs text-[var(--color-fg-muted)]">Status</p>
|
||||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
<p className="text-sm font-medium text-[var(--color-fg)]">
|
||||||
{opcUaZustand}
|
{opcUaZustand}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import { Icon } from "@iconify/react";
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { RootState } from "../../../redux/store";
|
import { RootState } from "../../../redux/store";
|
||||||
|
|
||||||
type VersionInfoProps = {
|
type VersionInfoProps = { className?: string };
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
|
const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
|
||||||
const appVersion =
|
const appVersion =
|
||||||
@@ -17,26 +15,26 @@ const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`card w-full p-3 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-base font-semibold mb-2 text-[var(--color-fg)]">
|
||||||
>
|
|
||||||
<h2 className="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">
|
|
||||||
Versionsinformationen
|
Versionsinformationen
|
||||||
</h2>
|
</h2>
|
||||||
|
<ul className="space-y-1">
|
||||||
<div className="flex flex-row p-2 space-x-2">
|
<li className="flex items-start gap-2">
|
||||||
<Icon icon="bx:code-block" className="text-xl text-blue-400" />
|
<Icon icon="bx:code-block" className="text-xl text-accent" />
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
<p className="text-sm text-fg-muted">
|
||||||
Applikationsversion: {appVersion}
|
Applikationsversion:{" "}
|
||||||
</p>
|
<span className="text-[var(--color-fg)]">{appVersion}</span>
|
||||||
</div>
|
</p>
|
||||||
|
</li>
|
||||||
<div className="flex flex-row p-2 space-x-2">
|
<li className="flex items-start gap-2">
|
||||||
<Icon icon="mdi:web" className="text-xl text-blue-400" />
|
<Icon icon="mdi:web" className="text-xl text-accent" />
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
<p className="text-sm text-fg-muted">
|
||||||
Webversion: {webVersion}
|
Webversion:{" "}
|
||||||
</p>
|
<span className="text-[var(--color-fg)]">{webVersion}</span>
|
||||||
</div>
|
</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function DigitalInputsWidget({
|
|||||||
//console.log("DigitalInputs", inputs);
|
//console.log("DigitalInputs", inputs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
<div className="text-[var(--color-fg)] bg-[var(--color-surface)] dark:bg-[var(--color-surface)] shadow-sm border border-[var(--color-border)] 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">
|
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
||||||
<Icon
|
<Icon
|
||||||
icon={inputIcon}
|
icon={inputIcon}
|
||||||
@@ -38,19 +38,19 @@ export default function DigitalInputsWidget({
|
|||||||
/>
|
/>
|
||||||
Meldungseingänge {inputRange.start + 1} – {inputRange.end}
|
Meldungseingänge {inputRange.start + 1} – {inputRange.end}
|
||||||
</h2>
|
</h2>
|
||||||
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white dark:bg-gray-900">
|
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-[var(--color-surface)]">
|
||||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b">
|
<thead className="bg-[var(--color-surface-alt)]/60 dark:bg-[var(--color-surface-alt)]/25 text-[var(--color-fg)] border-b border-[var(--color-border)]">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Eingang
|
Eingang
|
||||||
</th>
|
</th>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Zustand
|
Zustand
|
||||||
</th>
|
</th>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Bezeichnung
|
Bezeichnung
|
||||||
</th>
|
</th>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Aktion
|
Aktion
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -59,9 +59,9 @@ export default function DigitalInputsWidget({
|
|||||||
{inputs.map((input) => (
|
{inputs.map((input) => (
|
||||||
<tr
|
<tr
|
||||||
key={input.id}
|
key={input.id}
|
||||||
className="border-b hover:bg-gray-100 dark:hover:bg-gray-800"
|
className="border-b border-[var(--color-border)] hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30 transition"
|
||||||
>
|
>
|
||||||
<td className="px-1 py-0 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="px-1 py-0 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
<div className="flex items-center gap-1 ">
|
<div className="flex items-center gap-1 ">
|
||||||
<Icon
|
<Icon
|
||||||
icon={loginIcon}
|
icon={loginIcon}
|
||||||
@@ -70,7 +70,7 @@ export default function DigitalInputsWidget({
|
|||||||
{input.id}
|
{input.id}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
{input.eingangOffline ? (
|
{input.eingangOffline ? (
|
||||||
<div className="relative group inline-block">
|
<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 ">
|
<span className="text-red-500 sm:text-sm md:text-base lg:text-lg xl:text-xl 2xl:text-2xl laptop:text-sm ">
|
||||||
@@ -91,10 +91,10 @@ export default function DigitalInputsWidget({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
{input.label}
|
{input.label}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
<Icon
|
<Icon
|
||||||
icon={settingsIcon}
|
icon={settingsIcon}
|
||||||
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"
|
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"
|
||||||
|
|||||||
@@ -182,132 +182,133 @@ export default function InputModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-50 flex justify-center items-center z-50">
|
<div className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-50 flex justify-center items-center z-50">
|
||||||
<div className="bg-white rounded-lg shadow-lg p-6 w-1/2 max-w-lg">
|
<div className="bg-[var(--color-surface)] border border-base rounded-xl shadow-xl w-[32rem] max-w-full p-0 flex flex-col">
|
||||||
<div className="mb-4 border-b pb-2 flex justify-between items-center">
|
<header className="modal-header flex items-center justify-between px-6 py-4 border-b border-base">
|
||||||
<h2 className="text-base font-bold">
|
<h2 className="text-base font-bold text-fg">
|
||||||
Einstellungen Meldungseingang {selectedInput.id}
|
Einstellungen Meldungseingang {selectedInput.id}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
className="text-2xl hover:text-gray-400"
|
className="icon-btn text-2xl"
|
||||||
aria-label="Modal schließen"
|
aria-label="Modal schließen"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<i className="bi bi-x-circle-fill"></i>
|
<i className="bi bi-x-circle-fill" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</header>
|
||||||
|
<div className="modal-body-scroll px-6 py-5 flex-1 text-fg">
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||||
<div>
|
<div>
|
||||||
<span className="font-normal">Bezeichnung:</span>
|
<span className="font-normal text-fg-secondary">
|
||||||
</div>
|
Bezeichnung:
|
||||||
<div>
|
</span>
|
||||||
<input
|
</div>
|
||||||
type="text"
|
<div>
|
||||||
value={label}
|
<input
|
||||||
onChange={(e) => setLabel(e.target.value)}
|
type="text"
|
||||||
className="border border-gray-300 rounded px-2 py-1 w-full"
|
value={label}
|
||||||
maxLength={32}
|
onChange={(e) => setLabel(e.target.value)}
|
||||||
/>
|
className="border border-base rounded px-2 py-1 w-full bg-[var(--color-surface-alt)] text-fg"
|
||||||
</div>
|
maxLength={32}
|
||||||
|
|
||||||
<div>
|
|
||||||
<span className="font-normal">Invertierung:</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="switch"
|
|
||||||
aria-checked={invertiert}
|
|
||||||
onClick={() => setInvertiert(!invertiert)}
|
|
||||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-200 ${
|
|
||||||
invertiert ? "bg-littwin-blue" : "bg-gray-300"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform duration-200 ${
|
|
||||||
invertiert ? "translate-x-6" : "translate-x-1"
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</div>
|
||||||
<span>{invertiert ? "Ein" : "Aus"}</span>
|
<div>
|
||||||
</div>
|
<span className="font-normal text-fg-secondary">
|
||||||
|
Invertierung:
|
||||||
<div>
|
</span>
|
||||||
<span className="font-normal">Filterzeit:</span>
|
</div>
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative">
|
<button
|
||||||
<input
|
type="button"
|
||||||
type="number"
|
role="switch"
|
||||||
min={0}
|
aria-checked={invertiert}
|
||||||
max={2000}
|
onClick={() => setInvertiert(!invertiert)}
|
||||||
value={timeFilter}
|
className={`relative inline-flex h-6 w-11 items-center rounded-full border border-base transition-colors duration-200 ${
|
||||||
onChange={(e) => {
|
invertiert ? "bg-littwin-blue" : "bg-base-muted"
|
||||||
const val = Number(e.target.value);
|
|
||||||
if (val <= 2000) {
|
|
||||||
setTimeFilter(val);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="border border-gray-300 rounded px-2 py-1 pr-10 w-full text-right"
|
|
||||||
title="Maximal 2000 ms erlaubt"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
|
|
||||||
ms
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span className="font-normal">Gewichtung:</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={1000}
|
|
||||||
value={weighting}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = Number(e.target.value);
|
|
||||||
if (val <= 1000) {
|
|
||||||
setWeighting(val);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="border border-gray-300 rounded px-2 py-1 w-full text-right"
|
|
||||||
title="Maximal 1000 erlaubt"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative group inline-block">
|
|
||||||
<span className="font-normal">Out of Service:</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="switch"
|
|
||||||
aria-checked={!!eingangOffline}
|
|
||||||
onClick={() => setEingangOffline(eingangOffline ? 0 : 1)}
|
|
||||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-200 ${
|
|
||||||
eingangOffline ? "bg-littwin-blue" : "bg-gray-300"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform duration-200 ${
|
|
||||||
eingangOffline ? "translate-x-6" : "translate-x-1"
|
|
||||||
}`}
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute left-1 top-1/2 -translate-y-1/2 h-4 w-4 rounded-full bg-white shadow transition-transform duration-200 ${
|
||||||
|
invertiert ? "translate-x-5" : "translate-x-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<span className="text-fg">{invertiert ? "Ein" : "Aus"}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-normal text-fg-secondary">Filterzeit:</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={2000}
|
||||||
|
value={timeFilter}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = Number(e.target.value);
|
||||||
|
if (val <= 2000) {
|
||||||
|
setTimeFilter(val);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="border border-base rounded px-2 py-1 pr-10 w-full text-right bg-[var(--color-surface-alt)] text-fg"
|
||||||
|
title="Maximal 2000 ms erlaubt"
|
||||||
/>
|
/>
|
||||||
</button>
|
<span className="absolute right-2 top-1/2 transform -translate-y-1/2 text-muted text-sm">
|
||||||
<span>{eingangOffline ? "Ein" : "Aus"}</span>
|
ms
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-normal text-fg-secondary">Gewichtung:</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={1000}
|
||||||
|
value={weighting}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = Number(e.target.value);
|
||||||
|
if (val <= 1000) {
|
||||||
|
setWeighting(val);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="border border-base rounded px-2 py-1 w-full text-right bg-[var(--color-surface-alt)] text-fg"
|
||||||
|
title="Maximal 1000 erlaubt"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative group inline-block">
|
||||||
|
<span className="font-normal text-fg-secondary">
|
||||||
|
Out of Service:
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={!!eingangOffline}
|
||||||
|
onClick={() => setEingangOffline(eingangOffline ? 0 : 1)}
|
||||||
|
className={`relative inline-flex h-6 w-11 items-center rounded-full border border-base transition-colors duration-200 ${
|
||||||
|
eingangOffline ? "bg-littwin-blue" : "bg-base-muted"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute left-1 top-1/2 -translate-y-1/2 h-4 w-4 rounded-full bg-white shadow transition-transform duration-200 ${
|
||||||
|
eingangOffline ? "translate-x-5" : "translate-x-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<span className="text-fg">{eingangOffline ? "Ein" : "Aus"}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<footer className="px-6 py-4 border-t border-base flex justify-end">
|
||||||
<div className="mt-6 flex justify-end gap-2">
|
|
||||||
<button
|
<button
|
||||||
onClick={handleSpeichern}
|
onClick={handleSpeichern}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded flex items-center"
|
className="btn-primary px-4 py-2 rounded flex items-center"
|
||||||
>
|
>
|
||||||
Speichern
|
Speichern
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -94,44 +94,48 @@ export default function DigitalOutputsModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-50 flex justify-center items-center z-50">
|
<div className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-50 flex justify-center items-center z-50">
|
||||||
<div className="bg-white rounded-lg shadow-lg p-6 w-1/2 max-w-lg">
|
<div className="bg-[var(--color-surface)] border border-base rounded-xl shadow-xl w-[32rem] max-w-full p-0 flex flex-col">
|
||||||
<div className="mb-4 border-b pb-2 flex justify-between items-center">
|
<header className="modal-header flex items-center justify-between px-6 py-4 border-b border-base">
|
||||||
<h2 className="text-base font-bold">
|
<h2 className="text-base font-bold text-fg">
|
||||||
Einstellungen Schaltausgang {selectedOutput.id}
|
Einstellungen Schaltausgang {selectedOutput.id}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={closeOutputModal}
|
onClick={closeOutputModal}
|
||||||
className="text-2xl hover:text-gray-400"
|
className="icon-btn text-2xl"
|
||||||
aria-label="Modal schließen"
|
aria-label="Modal schließen"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<i className="bi bi-x-circle-fill"></i>
|
<i className="bi bi-x-circle-fill" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</header>
|
||||||
|
<div className="modal-body-scroll px-6 py-5 flex-1 text-fg">
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||||
<div>
|
<div>
|
||||||
<span className="font-normal">Bezeichnung:</span>
|
<span className="font-normal text-fg-secondary">
|
||||||
|
Bezeichnung:
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={label}
|
||||||
|
onChange={(e) => setLabel(e.target.value)}
|
||||||
|
className="border border-base rounded px-2 py-1 w-full bg-[var(--color-surface-alt)] text-fg"
|
||||||
|
placeholder="z. B. Licht Relais 1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
{errorMsg && <p className="text-red-600 text-sm mb-2">{errorMsg}</p>}
|
||||||
type="text"
|
|
||||||
value={label}
|
|
||||||
onChange={(e) => setLabel(e.target.value)}
|
|
||||||
className="w-full border border-gray-300 rounded px-3 py-2"
|
|
||||||
placeholder="z. B. Licht Relais 1"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<footer className="px-6 py-4 border-t border-base flex justify-end">
|
||||||
{errorMsg && <p className="text-red-600 text-sm mb-2">{errorMsg}</p>}
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 mt-6">
|
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded flex items-center"
|
className="btn-primary px-4 py-2 rounded flex items-center"
|
||||||
>
|
>
|
||||||
{isSaving ? "Speichern..." : "Speichern"}
|
{isSaving ? "Speichern..." : "Speichern"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const DigitalOutputsView: React.FC = () => {
|
|||||||
const [isOutputModalOpen, setIsOutputModalOpen] = useState(false);
|
const [isOutputModalOpen, setIsOutputModalOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
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(() => {
|
const interval = setInterval(() => {
|
||||||
dispatch(getDigitalOutputsThunk());
|
dispatch(getDigitalOutputsThunk());
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function DigitalOutputsWidget({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
<div className="bg-[var(--color-surface)] text-[var(--color-fg)] shadow-md border border-base 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">
|
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
||||||
<Icon
|
<Icon
|
||||||
icon={outputIcon}
|
icon={outputIcon}
|
||||||
@@ -74,19 +74,19 @@ export default function DigitalOutputsWidget({
|
|||||||
/>
|
/>
|
||||||
Schaltausgänge
|
Schaltausgänge
|
||||||
</h2>
|
</h2>
|
||||||
<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">
|
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-[var(--color-surface)] rounded-lg">
|
||||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b">
|
<thead className="bg-[var(--color-surface)] text-[var(--color-fg)] border-b border-base">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Ausgang
|
Ausgang
|
||||||
</th>
|
</th>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Bezeichnung
|
Bezeichnung
|
||||||
</th>
|
</th>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Schalter
|
Schalter
|
||||||
</th>
|
</th>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Aktion
|
Aktion
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -95,33 +95,39 @@ export default function DigitalOutputsWidget({
|
|||||||
{digitalOutputs.map((output) => (
|
{digitalOutputs.map((output) => (
|
||||||
<tr
|
<tr
|
||||||
key={output.id}
|
key={output.id}
|
||||||
className="border-b hover:bg-gray-100 dark:hover:bg-gray-800"
|
className="border-b border-base hover:bg-[var(--color-surface-alt)] transition-colors"
|
||||||
>
|
>
|
||||||
<td className="flex items-center px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="flex items-center px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
<Icon
|
<Icon
|
||||||
icon={outputIcon}
|
icon={outputIcon}
|
||||||
className="text-gray-600 mr-1 text-base"
|
className="text-[var(--color-muted)] mr-1 text-base"
|
||||||
/>
|
/>
|
||||||
{output.id}
|
{output.id}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
{output.label}
|
{output.label}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
<Icon
|
<button
|
||||||
icon={switchIcon}
|
type="button"
|
||||||
className={`cursor-pointer text-base transition ${
|
role="switch"
|
||||||
output.status
|
aria-checked={output.status}
|
||||||
? "text-littwin-blue"
|
|
||||||
: "text-gray-500 scale-x-[-1]"
|
|
||||||
} dark:hover:text-littwin-blue`}
|
|
||||||
onClick={() => handleToggle(output.id)}
|
onClick={() => handleToggle(output.id)}
|
||||||
/>
|
className={`relative inline-flex h-4 w-7 items-center rounded-full border border-base transition-colors duration-200 ${
|
||||||
|
output.status ? "bg-littwin-blue" : "bg-base-muted"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute left-0.5 top-1/2 -translate-y-1/2 h-3 w-3 rounded-full bg-white shadow transition-transform duration-200 ${
|
||||||
|
output.status ? "translate-x-3.5" : "translate-x-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
<Icon
|
<Icon
|
||||||
icon={settingsIcon}
|
icon={settingsIcon}
|
||||||
className="text-gray-400 text-base cursor-pointer dark:text-gray-300 dark:hover:text-white"
|
className="text-[var(--color-muted)] text-base cursor-pointer hover:text-[var(--color-fg)]"
|
||||||
onClick={() => openOutputModal(output)}
|
onClick={() => openOutputModal(output)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -119,19 +119,23 @@ function KabelueberwachungView() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
{[1, 2, 3, 4].map((rack) => (
|
{[1, 2, 3, 4].map((rack) => {
|
||||||
<button
|
const isActive = Number(activeRack) === Number(rack);
|
||||||
key={rack}
|
return (
|
||||||
onClick={() => changeRack(rack)}
|
<button
|
||||||
className={`mr-2 ${
|
key={rack}
|
||||||
Number(activeRack) === Number(rack)
|
onClick={() => changeRack(rack)}
|
||||||
? "bg-littwin-blue text-white p-1 rounded-sm"
|
aria-pressed={isActive}
|
||||||
: "bg-gray-300 p-1 text-sm"
|
className={`mr-2 px-2 py-1 rounded-sm text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-accent/50 ${
|
||||||
}`}
|
isActive
|
||||||
>
|
? "btn-primary"
|
||||||
Rack {rack}
|
: "btn-muted text-fg opacity-90 hover:opacity-100"
|
||||||
</button>
|
}`}
|
||||||
))}
|
>
|
||||||
|
Rack {rack}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</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%]">
|
<div className="flex flex-row space-x-8 xl:space-x-0 2xl:space-x-8 qhd:space-x-16 ml-[5%] mt-[5%]">
|
||||||
{(
|
{(
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export const useIsoChartLoader = () => {
|
|||||||
)};${formatDate(bisDatum)};${slotNumber};${type};`;
|
)};${formatDate(bisDatum)};${slotNumber};${type};`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("API URL:", url);
|
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,19 +142,10 @@ export const useIsoDataLoader = () => {
|
|||||||
const waitTime = Math.max(0, MIN_LOADING_TIME_MS - elapsedTime);
|
const waitTime = Math.max(0, MIN_LOADING_TIME_MS - elapsedTime);
|
||||||
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
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) {
|
if (Array.isArray(jsonData) && jsonData.length > 0) {
|
||||||
dispatch(setIsoMeasurementCurveChartData(jsonData));
|
dispatch(setIsoMeasurementCurveChartData(jsonData));
|
||||||
dispatch(setChartOpen(true));
|
dispatch(setChartOpen(true));
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
|
||||||
"⚠️ Keine Messdaten im gewählten Zeitraum gefunden (automatisches Laden)"
|
|
||||||
);
|
|
||||||
dispatch(setIsoMeasurementCurveChartData([]));
|
dispatch(setIsoMeasurementCurveChartData([]));
|
||||||
dispatch(setChartOpen(false));
|
dispatch(setChartOpen(false));
|
||||||
}
|
}
|
||||||
@@ -170,8 +160,6 @@ export const useIsoDataLoader = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------------IsoChartActionBar
|
//-----------------------------------------------------------------------------------IsoChartActionBar
|
||||||
// ...existing code...
|
|
||||||
|
|
||||||
const IsoChartActionBar = forwardRef((_props, ref) => {
|
const IsoChartActionBar = forwardRef((_props, ref) => {
|
||||||
IsoChartActionBar.displayName = "IsoChartActionBar";
|
IsoChartActionBar.displayName = "IsoChartActionBar";
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -281,106 +269,90 @@ const IsoChartActionBar = forwardRef((_props, ref) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({ handleFetchData }));
|
||||||
handleFetchData,
|
|
||||||
}));
|
const isMeldungen = chartTitle === "Meldungen";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-2">
|
<div className="toolbar w-full justify-between flex-wrap">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center gap-2 pr-4">
|
||||||
<label className="text-sm font-semibold">
|
<span className=" font-semibold uppercase tracking-wide text-muted">
|
||||||
KÜ {slotNumber !== null ? slotNumber + 1 : "-"}
|
KÜ
|
||||||
</label>
|
</span>
|
||||||
|
<span className=" font-medium px-2 py-0.5 rounded bg-surface-alt border border-base min-w-[3rem] text-center">
|
||||||
|
{slotNumber !== null ? slotNumber + 1 : "-"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-3 flex-1 justify-end">
|
||||||
<div className="flex items-center space-x-2">
|
{/* Always show date range; requirement: in Meldungen only Von/Bis + Anzeigen */}
|
||||||
{/* DateRangePicker – für beide Ansichten sichtbar, da Meldungen auch datumsabhängig sind */}
|
<DateRangePicker />
|
||||||
<div
|
{!isMeldungen && (
|
||||||
style={{
|
<>
|
||||||
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
|
<Listbox
|
||||||
}}
|
value={selectedMode}
|
||||||
>
|
onChange={(value) => {
|
||||||
<DateRangePicker />
|
dispatch(setSelectedMode(value));
|
||||||
</div>
|
dispatch(setBrushRange({ startIndex: 0, endIndex: 0 }));
|
||||||
|
}}
|
||||||
{/* DIA0-DIA2 Dropdown - Platz reservieren, aber ausblenden wenn Meldungen */}
|
>
|
||||||
<div
|
<div className="relative w-48">
|
||||||
style={{
|
<Listbox.Button className="dropdown-surface w-full flex items-center justify-between">
|
||||||
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
|
<span className="dropdown-text-fix">
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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",
|
DIA0: "Alle Messwerte",
|
||||||
DIA1: "Stündliche Werte",
|
DIA1: "Stündlich",
|
||||||
DIA2: "Tägliche Werte",
|
DIA2: "Täglich",
|
||||||
}[mode]
|
}[selectedMode]
|
||||||
}
|
}
|
||||||
</Listbox.Option>
|
</span>
|
||||||
))}
|
<i className="bi bi-chevron-down opacity-70" />
|
||||||
</Listbox.Options>
|
</Listbox.Button>
|
||||||
</div>
|
<Listbox.Options className="dropdown-options absolute z-50 mt-1 w-full max-h-60 overflow-auto">
|
||||||
</Listbox>
|
{["DIA0", "DIA1", "DIA2"].map((mode) => (
|
||||||
</div>
|
<Listbox.Option
|
||||||
|
key={mode}
|
||||||
{/* Dropdown für Auswahl zwischen "Messkurve" und "Meldungen" - immer anzeigen */}
|
value={mode}
|
||||||
{/* Dropdown für Auswahl zwischen "Messkurve" und "Meldungen" entfernt */}
|
className={({ selected, active }) =>
|
||||||
|
`px-3 py-1.5 cursor-pointer rounded-sm m-0.5 ${
|
||||||
{/* Daten laden Button – lädt je nach Ansicht Messkurve oder Meldungen */}
|
selected
|
||||||
<button
|
? "dropdown-option-active"
|
||||||
style={{
|
: active
|
||||||
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
|
? "dropdown-option-hover"
|
||||||
}}
|
: ""
|
||||||
onClick={handleFetchData}
|
}`
|
||||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm"
|
}
|
||||||
>
|
>
|
||||||
Daten laden
|
{
|
||||||
</button>
|
{
|
||||||
|
DIA0: "Alle Messwerte",
|
||||||
|
DIA1: "Stündlich",
|
||||||
|
DIA2: "Täglich",
|
||||||
|
}[mode as "DIA0" | "DIA1" | "DIA2"]
|
||||||
|
}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
<button
|
||||||
|
onClick={handleFetchData}
|
||||||
|
className="btn-primary h-8 font-medium px-3"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Daten laden
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isMeldungen && (
|
||||||
|
<button
|
||||||
|
onClick={handleFetchData}
|
||||||
|
className="btn-primary h-8 font-medium px-4"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Anzeigen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"; // IsoChartView.tsx
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { Listbox } from "@headlessui/react";
|
import { Listbox } from "@headlessui/react";
|
||||||
@@ -7,23 +7,18 @@ import IsoMeasurementChart from "./IsoMeasurementChart";
|
|||||||
import IsoChartActionBar from "./IsoChartActionBar";
|
import IsoChartActionBar from "./IsoChartActionBar";
|
||||||
import Report from "./Report";
|
import Report from "./Report";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { AppDispatch } from "@/redux/store";
|
import { AppDispatch, RootState } from "@/redux/store";
|
||||||
import { RootState } from "@/redux/store";
|
|
||||||
import {
|
import {
|
||||||
setChartOpen,
|
setChartOpen,
|
||||||
setFullScreen,
|
setFullScreen,
|
||||||
setSlotNumber,
|
setSlotNumber,
|
||||||
setChartTitle,
|
setChartTitle,
|
||||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
|
||||||
|
|
||||||
import { resetBrushRange } from "@/redux/slices/brushSlice";
|
|
||||||
|
|
||||||
import {
|
|
||||||
setVonDatum,
|
setVonDatum,
|
||||||
setBisDatum,
|
setBisDatum,
|
||||||
setSelectedMode,
|
setSelectedMode,
|
||||||
setSelectedSlotType,
|
setSelectedSlotType,
|
||||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
} from "@/redux/slices/kabelueberwachungChartSlice";
|
||||||
|
import { resetBrushRange } from "@/redux/slices/brushSlice";
|
||||||
import { resetDateRange } from "@/redux/slices/dateRangePickerSlice";
|
import { resetDateRange } from "@/redux/slices/dateRangePickerSlice";
|
||||||
|
|
||||||
interface IsoChartViewProps {
|
interface IsoChartViewProps {
|
||||||
@@ -32,85 +27,59 @@ interface IsoChartViewProps {
|
|||||||
slotIndex: number;
|
slotIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActionBarRefType = { handleFetchData: () => void };
|
||||||
|
|
||||||
const IsoChartView: React.FC<IsoChartViewProps> = ({
|
const IsoChartView: React.FC<IsoChartViewProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
slotIndex,
|
slotIndex,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
// removed unused loadData
|
|
||||||
|
|
||||||
const { isFullScreen, chartTitle } = useSelector(
|
const { isFullScreen, chartTitle } = useSelector(
|
||||||
(state: RootState) => state.kabelueberwachungChartSlice
|
(state: RootState) => state.kabelueberwachungChartSlice
|
||||||
);
|
);
|
||||||
|
|
||||||
// **Modal schließen + Redux-Status zurücksetzen**
|
const actionBarRef = useRef<ActionBarRefType>(null);
|
||||||
const handleClose = () => {
|
|
||||||
|
const initDates = () => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const thirtyDaysAgo = new Date();
|
const thirtyDaysAgo = new Date();
|
||||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||||
|
const toISO = (d: Date) => d.toLocaleDateString("sv-SE");
|
||||||
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
|
|
||||||
|
|
||||||
// Reset Datum
|
|
||||||
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||||
dispatch(setBisDatum(toISO(today)));
|
dispatch(setBisDatum(toISO(today)));
|
||||||
|
};
|
||||||
|
|
||||||
// Reset DateRangePicker
|
const handleClose = () => {
|
||||||
|
initDates();
|
||||||
dispatch(resetDateRange());
|
dispatch(resetDateRange());
|
||||||
|
dispatch(setSelectedMode("DIA0"));
|
||||||
// Reset Dropdowns
|
|
||||||
dispatch(setSelectedMode("DIA0")); // Reset to Alle Messwerte
|
|
||||||
dispatch(setSelectedSlotType("isolationswiderstand"));
|
dispatch(setSelectedSlotType("isolationswiderstand"));
|
||||||
dispatch(setChartTitle("Messkurve")); // Reset zu Messkurve
|
dispatch(setChartTitle("Messkurve"));
|
||||||
|
|
||||||
// Sonstiges Reset
|
|
||||||
dispatch(setChartOpen(false));
|
dispatch(setChartOpen(false));
|
||||||
dispatch(setFullScreen(false));
|
dispatch(setFullScreen(false));
|
||||||
dispatch(resetBrushRange());
|
dispatch(resetBrushRange());
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
// **Vollbildmodus umschalten**
|
const toggleFullScreen = () => dispatch(setFullScreen(!isFullScreen));
|
||||||
const toggleFullScreen = () => {
|
|
||||||
dispatch(setFullScreen(!isFullScreen));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Modal öffnen - ISO spezifische Einstellungen
|
|
||||||
type ActionBarRefType = { handleFetchData: () => void };
|
|
||||||
const actionBarRef = useRef<ActionBarRefType>(null);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
dispatch(setSlotNumber(slotIndex));
|
||||||
|
// inline initDates to avoid extra dependency
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const thirtyDaysAgo = new Date();
|
const thirtyDaysAgo = new Date();
|
||||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||||
|
const toISO = (d: Date) => d.toLocaleDateString("sv-SE");
|
||||||
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
|
|
||||||
|
|
||||||
// Set slot number first
|
|
||||||
dispatch(setSlotNumber(slotIndex));
|
|
||||||
|
|
||||||
// Set dates
|
|
||||||
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||||
dispatch(setBisDatum(toISO(today)));
|
dispatch(setBisDatum(toISO(today)));
|
||||||
|
|
||||||
// Set ISO specific settings
|
|
||||||
dispatch(setSelectedSlotType("isolationswiderstand"));
|
dispatch(setSelectedSlotType("isolationswiderstand"));
|
||||||
dispatch(setSelectedMode("DIA0")); // Set to Alle Messwerte on open
|
dispatch(setSelectedMode("DIA0"));
|
||||||
|
|
||||||
// Set default to Messkurve
|
|
||||||
dispatch(setChartTitle("Messkurve"));
|
dispatch(setChartTitle("Messkurve"));
|
||||||
|
const t = setTimeout(() => actionBarRef.current?.handleFetchData(), 120);
|
||||||
// Automatisch Daten laden wie Button-Klick
|
return () => clearTimeout(t);
|
||||||
const timer = setTimeout(() => {
|
|
||||||
actionBarRef.current?.handleFetchData();
|
|
||||||
}, 120);
|
|
||||||
|
|
||||||
// Cleanup timer
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [isOpen, slotIndex, dispatch]);
|
}, [isOpen, slotIndex, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -119,74 +88,63 @@ const IsoChartView: React.FC<IsoChartViewProps> = ({
|
|||||||
onRequestClose={handleClose}
|
onRequestClose={handleClose}
|
||||||
ariaHideApp={false}
|
ariaHideApp={false}
|
||||||
style={{
|
style={{
|
||||||
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
overlay: {
|
||||||
|
backgroundColor: "rgba(0,0,0,0.55)",
|
||||||
|
backdropFilter: "blur(2px)",
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
top: "50%",
|
inset: "50% auto auto 50%",
|
||||||
left: "50%",
|
|
||||||
bottom: "auto",
|
|
||||||
marginRight: "-50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
transform: "translate(-50%, -50%)",
|
||||||
width: isFullScreen ? "90vw" : "70rem",
|
width: isFullScreen ? "90vw" : "72rem",
|
||||||
height: isFullScreen ? "90vh" : "35rem",
|
height: isFullScreen ? "90vh" : "38rem",
|
||||||
padding: "1rem",
|
padding: 0,
|
||||||
transition: "all 0.3s ease-in-out",
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
borderRadius: "14px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
contentLabel="Isolationswiderstand"
|
||||||
>
|
>
|
||||||
{/* Action-Buttons */}
|
<header className="modal-header relative pr-56">
|
||||||
<div
|
<h3 className="text-sm font-semibold tracking-wide">
|
||||||
style={{
|
Isolationswiderstand
|
||||||
position: "absolute",
|
</h3>
|
||||||
top: "0.625rem",
|
<div className="absolute top-2 right-2 flex gap-2">
|
||||||
right: "0.625rem",
|
<button
|
||||||
display: "flex",
|
onClick={toggleFullScreen}
|
||||||
gap: "0.75rem",
|
className="icon-btn"
|
||||||
}}
|
aria-label={isFullScreen ? "Vollbild verlassen" : "Vollbild"}
|
||||||
>
|
type="button"
|
||||||
{/* Fullscreen-Button */}
|
>
|
||||||
<button
|
<i
|
||||||
onClick={toggleFullScreen}
|
className={
|
||||||
style={{
|
isFullScreen
|
||||||
background: "transparent",
|
? "bi bi-fullscreen-exit"
|
||||||
border: "none",
|
: "bi bi-arrows-fullscreen"
|
||||||
fontSize: "1.5rem",
|
}
|
||||||
cursor: "pointer",
|
/>
|
||||||
}}
|
</button>
|
||||||
>
|
<button
|
||||||
<i
|
onClick={handleClose}
|
||||||
className={
|
className="icon-btn"
|
||||||
isFullScreen ? "bi bi-fullscreen-exit" : "bi bi-arrows-fullscreen"
|
aria-label="Schließen"
|
||||||
}
|
type="button"
|
||||||
></i>
|
>
|
||||||
</button>
|
<i
|
||||||
|
style={{
|
||||||
{/* Schließen-Button */}
|
background: "transparent",
|
||||||
<button
|
border: "none",
|
||||||
onClick={handleClose}
|
fontSize: "1.5rem",
|
||||||
style={{
|
cursor: "pointer",
|
||||||
background: "transparent",
|
}}
|
||||||
border: "none",
|
className="bi bi-x-circle-fill"
|
||||||
fontSize: "1.5rem",
|
/>
|
||||||
cursor: "pointer",
|
</button>
|
||||||
}}
|
</div>
|
||||||
>
|
<div className="absolute top-2 right-28">
|
||||||
<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
|
<Listbox
|
||||||
value={chartTitle}
|
value={chartTitle}
|
||||||
onChange={(value: "Messkurve" | "Meldungen") =>
|
onChange={(value: "Messkurve" | "Meldungen") =>
|
||||||
@@ -194,52 +152,36 @@ const IsoChartView: React.FC<IsoChartViewProps> = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="relative w-40">
|
<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">
|
<Listbox.Button className="dropdown-surface w-full flex items-center justify-between h-8">
|
||||||
<span>
|
<span className="dropdown-text-fix">{chartTitle}</span>
|
||||||
{chartTitle === "Meldungen" ? "Meldungen" : "Messkurve"}
|
<i className="bi bi-chevron-down text-sm opacity-70" />
|
||||||
</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.Button>
|
||||||
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
|
<Listbox.Options className="dropdown-options absolute z-50 mt-1 w-full max-h-60 overflow-auto text-sm">
|
||||||
{(["Messkurve", "Meldungen"] as const).map((option) => (
|
{(["Messkurve", "Meldungen"] as const).map((option) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={option}
|
key={option}
|
||||||
value={option}
|
value={option}
|
||||||
className={({
|
className={({ selected, active }) =>
|
||||||
selected,
|
`px-3 py-1.5 cursor-pointer rounded-sm m-0.5 ${
|
||||||
active,
|
|
||||||
}: {
|
|
||||||
selected: boolean;
|
|
||||||
active: boolean;
|
|
||||||
}) =>
|
|
||||||
`px-4 py-1 cursor-pointer ${
|
|
||||||
selected
|
selected
|
||||||
? "bg-littwin-blue text-white"
|
? "dropdown-option-active"
|
||||||
: active
|
: active
|
||||||
? "bg-gray-200"
|
? "dropdown-option-hover"
|
||||||
: ""
|
: ""
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{option === "Meldungen" ? "Meldungen" : "Messkurve"}
|
{option}
|
||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
))}
|
))}
|
||||||
</Listbox.Options>
|
</Listbox.Options>
|
||||||
</div>
|
</div>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="flex flex-col flex-1 p-3 gap-3">
|
||||||
<IsoChartActionBar ref={actionBarRef} />
|
<IsoChartActionBar ref={actionBarRef} />
|
||||||
<div style={{ flex: 1, height: "90%" }}>
|
<div className="flex-1 relative">
|
||||||
{chartTitle === "Messkurve" ? (
|
{chartTitle === "Messkurve" ? (
|
||||||
<IsoMeasurementChart />
|
<IsoMeasurementChart />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -224,43 +224,47 @@ const Report: React.FC<ReportProps> = ({ moduleType, autoLoad = true }) => {
|
|||||||
gewählten Zeitraum gefunden.
|
gewählten Zeitraum gefunden.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 overflow-auto ">
|
<div className="flex-1 overflow-auto table-scroll-region">
|
||||||
<table className="min-w-full border text-sm">
|
<div className="data-table-wrapper">
|
||||||
<thead className="bg-gray-100 text-left sticky top-0 z-10">
|
<table className="data-table">
|
||||||
<tr>
|
<thead>
|
||||||
<th className="p-2 border">Prio</th>
|
<tr>
|
||||||
<th className="p-2 border">Zeitstempel</th>
|
<th style={{ width: "60px" }}>Prio</th>
|
||||||
<th className="p-2 border">Quelle</th>
|
<th style={{ minWidth: "180px" }}>Zeitstempel</th>
|
||||||
<th className="p-2 border">Meldung</th>
|
<th style={{ minWidth: "140px" }}>Quelle</th>
|
||||||
<th className="p-2 border">Status</th>
|
<th style={{ minWidth: "260px" }}>Meldung</th>
|
||||||
</tr>
|
<th style={{ minWidth: "120px" }}>Status</th>
|
||||||
</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>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{filteredMessages.map((msg, index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
className="prio-dot"
|
||||||
|
style={{ backgroundColor: msg.c }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{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>{msg.i}</td>
|
||||||
|
<td className="truncate max-w-[22ch]" title={msg.m}>
|
||||||
|
{msg.m}
|
||||||
|
</td>
|
||||||
|
<td>{msg.v}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
"use client"; // KVZChartView.tsx
|
"use client"; // KVZChartView.tsx
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
import DateRangePicker from "@/components/common/DateRangePicker";
|
||||||
|
import { getMessagesThunk } from "@/redux/thunks/getMessagesThunk";
|
||||||
|
import { setLoading } from "@/redux/slices/kabelueberwachungChartSlice";
|
||||||
import ReactModal from "react-modal";
|
import ReactModal from "react-modal";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { AppDispatch, RootState } from "@/redux/store";
|
import { AppDispatch, RootState } from "@/redux/store";
|
||||||
@@ -31,27 +34,31 @@ const KVZChartView: React.FC<KVZChartViewProps> = ({
|
|||||||
slotIndex,
|
slotIndex,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
const isFullScreen = useSelector(
|
const { isFullScreen, slotNumber, vonDatum, bisDatum } = useSelector(
|
||||||
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
|
(state: RootState) => state.kabelueberwachungChartSlice
|
||||||
);
|
);
|
||||||
const slotNumber = useSelector(
|
const { vonDatum: pickerVonDatum, bisDatum: pickerBisDatum } = useSelector(
|
||||||
(state: RootState) => state.kabelueberwachungChartSlice.slotNumber
|
(state: RootState) => state.dateRangePicker
|
||||||
);
|
);
|
||||||
|
|
||||||
// Beim Öffnen Slot setzen (damit konsistent zu anderen Modals)
|
// Beim Öffnen: Slot + Standard-Datumsbereich setzen (30 Tage) – analog zu anderen Modals
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (!isOpen) return;
|
||||||
dispatch(setSlotNumber(slotIndex));
|
dispatch(setSlotNumber(slotIndex));
|
||||||
}
|
|
||||||
}, [isOpen, slotIndex, dispatch]);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const thirtyDaysAgo = new Date();
|
const thirtyDaysAgo = new Date();
|
||||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||||
const toISO = (d: Date) => d.toLocaleDateString("sv-SE");
|
const toISO = (d: Date) => d.toLocaleDateString("sv-SE");
|
||||||
|
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||||
|
dispatch(setBisDatum(toISO(today)));
|
||||||
|
}, [isOpen, slotIndex, dispatch]);
|
||||||
|
|
||||||
// Zurücksetzen – entspricht Verhalten der anderen Modals
|
const handleClose = () => {
|
||||||
|
// Reset auf Default (wie andere Modals es tun)
|
||||||
|
const today = new Date();
|
||||||
|
const thirtyDaysAgo = new Date();
|
||||||
|
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||||
|
const toISO = (d: Date) => d.toLocaleDateString("sv-SE");
|
||||||
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||||
dispatch(setBisDatum(toISO(today)));
|
dispatch(setBisDatum(toISO(today)));
|
||||||
dispatch(setSelectedMode("DIA1"));
|
dispatch(setSelectedMode("DIA1"));
|
||||||
@@ -59,93 +66,114 @@ const KVZChartView: React.FC<KVZChartViewProps> = ({
|
|||||||
dispatch(setChartOpen(false));
|
dispatch(setChartOpen(false));
|
||||||
dispatch(setFullScreen(false));
|
dispatch(setFullScreen(false));
|
||||||
dispatch(resetBrushRange());
|
dispatch(resetBrushRange());
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleFullScreen = () => {
|
const toggleFullScreen = () => dispatch(setFullScreen(!isFullScreen));
|
||||||
dispatch(setFullScreen(!isFullScreen));
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const handleFetchMessages = async () => {
|
||||||
|
const fromDate = pickerVonDatum ?? vonDatum;
|
||||||
|
const toDate = pickerBisDatum ?? bisDatum;
|
||||||
|
try {
|
||||||
|
dispatch(setLoading(true));
|
||||||
|
await dispatch(getMessagesThunk({ fromDate, toDate })).unwrap();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Fehler beim Laden der KVZ Meldungen", e);
|
||||||
|
} finally {
|
||||||
|
dispatch(setLoading(false));
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<ReactModal
|
<ReactModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onRequestClose={handleClose}
|
onRequestClose={handleClose}
|
||||||
ariaHideApp={false}
|
ariaHideApp={false}
|
||||||
style={{
|
style={{
|
||||||
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
overlay: {
|
||||||
|
backgroundColor: "rgba(0,0,0,0.55)",
|
||||||
|
backdropFilter: "blur(2px)",
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
top: "50%",
|
inset: "50% auto auto 50%",
|
||||||
left: "50%",
|
|
||||||
bottom: "auto",
|
|
||||||
marginRight: "-50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
transform: "translate(-50%, -50%)",
|
||||||
width: isFullScreen ? "90vw" : "50rem",
|
width: isFullScreen ? "90vw" : "72rem",
|
||||||
height: isFullScreen ? "90vh" : "28rem",
|
height: isFullScreen ? "90vh" : "38rem",
|
||||||
padding: "1rem",
|
padding: 0,
|
||||||
transition: "all 0.3s ease-in-out",
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
borderRadius: "14px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
contentLabel="KVZ Zustände & Meldungen"
|
||||||
>
|
>
|
||||||
{/* Action Buttons */}
|
<header className="modal-header relative pr-32">
|
||||||
<div
|
<h3 className="text-sm font-semibold tracking-wide">
|
||||||
style={{
|
KVZ Zustände & Meldungen
|
||||||
position: "absolute",
|
</h3>
|
||||||
top: "0.625rem",
|
<div className="absolute top-2 right-2 flex gap-2">
|
||||||
right: "0.625rem",
|
<button
|
||||||
display: "flex",
|
onClick={toggleFullScreen}
|
||||||
gap: "0.75rem",
|
className="icon-btn"
|
||||||
}}
|
aria-label={isFullScreen ? "Vollbild verlassen" : "Vollbild"}
|
||||||
>
|
type="button"
|
||||||
<button
|
>
|
||||||
onClick={toggleFullScreen}
|
<i
|
||||||
style={{
|
className={
|
||||||
background: "transparent",
|
isFullScreen
|
||||||
border: "none",
|
? "bi bi-fullscreen-exit"
|
||||||
fontSize: "1.5rem",
|
: "bi bi-arrows-fullscreen"
|
||||||
cursor: "pointer",
|
}
|
||||||
}}
|
/>
|
||||||
>
|
</button>
|
||||||
<i
|
<button
|
||||||
className={
|
onClick={handleClose}
|
||||||
isFullScreen ? "bi bi-fullscreen-exit" : "bi bi-arrows-fullscreen"
|
className="icon-btn"
|
||||||
}
|
aria-label="Schließen"
|
||||||
></i>
|
type="button"
|
||||||
</button>
|
>
|
||||||
<button
|
<i
|
||||||
onClick={handleClose}
|
style={{
|
||||||
style={{
|
background: "transparent",
|
||||||
background: "transparent",
|
border: "none",
|
||||||
border: "none",
|
fontSize: "1.5rem",
|
||||||
fontSize: "1.5rem",
|
cursor: "pointer",
|
||||||
cursor: "pointer",
|
}}
|
||||||
}}
|
className="bi bi-x-circle-fill"
|
||||||
>
|
/>
|
||||||
<i className="bi bi-x-circle-fill"></i>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
|
<div className="flex flex-col flex-1 p-3 gap-3">
|
||||||
{/* Content */}
|
{/* Toolbar */}
|
||||||
<div className="flex flex-col h-full">
|
<div className="w-full flex flex-wrap items-center gap-4">
|
||||||
<h3 className="text-lg font-semibold mb-1">KVz Zustände & Meldungen</h3>
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-semibold opacity-80 select-none text-fg-secondary">
|
||||||
{/* 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 : "-"}
|
KÜ {slotNumber !== null ? slotNumber + 1 : "-"}
|
||||||
</label>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: "12rem" }}>
|
<div className="flex items-center gap-3 flex-1 min-w-[20rem]">
|
||||||
|
<div className="relative z-[1500]">
|
||||||
|
<DateRangePicker />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleFetchMessages}
|
||||||
|
className="btn-primary h-8 font-medium px-4"
|
||||||
|
>
|
||||||
|
Anzeigen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end w-48">
|
||||||
<FallSensors slotIndex={slotIndex} />
|
<FallSensors slotIndex={slotIndex} />
|
||||||
</div>
|
</div>
|
||||||
<div></div>
|
|
||||||
</div>
|
</div>
|
||||||
{/* Meldungen Bereich */}
|
<div className="flex-1 relative border border-base rounded bg-[var(--color-surface-alt)] text-fg overflow-hidden p-2">
|
||||||
<div className="flex-1 border rounded bg-white overflow-hidden">
|
<div className="w-full h-full rounded bg-[var(--color-surface)] overflow-hidden">
|
||||||
<Report moduleType="KVZ" />
|
<Report moduleType="KVZ" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ReactModal>
|
</ReactModal>
|
||||||
|
|||||||
@@ -281,26 +281,24 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
|
|||||||
handleFetchData,
|
handleFetchData,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Sichtbarkeits-Flags
|
||||||
|
const isMesskurve = chartTitle === "Messkurve";
|
||||||
|
const isMeldungen = chartTitle === "Meldungen";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between p-1 bg-gray-100 rounded-lg ">
|
<div className="toolbar w-full flex flex-wrap items-center gap-2">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center mr-2 min-w-[4rem]">
|
||||||
<label className="text-sm font-semibold">
|
<span className="text-xs font-semibold opacity-80 select-none">
|
||||||
KÜ {slotNumber !== null ? slotNumber + 1 : "-"}
|
KÜ {slotNumber !== null ? slotNumber + 1 : "-"}
|
||||||
</label>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center gap-3 flex-1 justify-end">
|
||||||
{/* DateRangePicker – für beide Ansichten sichtbar */}
|
{/* DateRangePicker immer sichtbar */}
|
||||||
<div>
|
<DateRangePicker />
|
||||||
<DateRangePicker compact />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* DIA0/DIA1/DIA2 Dropdown – nur sichtbar bei Messkurve */}
|
{/* Modus-Dropdown nur für Messkurve */}
|
||||||
<div
|
<div className={isMesskurve ? "" : "hidden"}>
|
||||||
style={{
|
|
||||||
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Listbox
|
<Listbox
|
||||||
value={selectedMode}
|
value={selectedMode}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@@ -309,39 +307,29 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative w-48">
|
<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">
|
<Listbox.Button className="dropdown-surface w-full flex items-center justify-between">
|
||||||
<span>
|
<span className="dropdown-text-fix">
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
DIA0: "Alle Messwerte",
|
DIA0: "Alle Messwerte",
|
||||||
DIA1: "Stündliche Werte",
|
DIA1: "Stündlich",
|
||||||
DIA2: "Tägliche Werte",
|
DIA2: "Täglich",
|
||||||
}[selectedMode]
|
}[selectedMode]
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<svg
|
<i className="bi bi-chevron-down opacity-70" />
|
||||||
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.Button>
|
||||||
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
|
<Listbox.Options className="dropdown-options absolute z-50 mt-1 w-full max-h-60 overflow-auto">
|
||||||
{["DIA0", "DIA1", "DIA2"].map((mode) => (
|
{["DIA0", "DIA1", "DIA2"].map((mode) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={mode}
|
key={mode}
|
||||||
value={mode}
|
value={mode}
|
||||||
className={({ selected, active }) =>
|
className={({ selected, active }) =>
|
||||||
`px-4 py-1 cursor-pointer ${
|
`px-3 py-1.5 cursor-pointer rounded-sm m-0.5 ${
|
||||||
selected
|
selected
|
||||||
? "bg-littwin-blue text-white"
|
? "dropdown-option-active"
|
||||||
: active
|
: active
|
||||||
? "bg-gray-200"
|
? "dropdown-option-hover"
|
||||||
: ""
|
: ""
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
@@ -349,8 +337,8 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
|
|||||||
{
|
{
|
||||||
{
|
{
|
||||||
DIA0: "Alle Messwerte",
|
DIA0: "Alle Messwerte",
|
||||||
DIA1: "Stündliche Werte",
|
DIA1: "Stündlich",
|
||||||
DIA2: "Tägliche Werte",
|
DIA2: "Täglich",
|
||||||
}[mode]
|
}[mode]
|
||||||
}
|
}
|
||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
@@ -359,40 +347,50 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
|
|||||||
</div>
|
</div>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
</div>
|
</div>
|
||||||
{/* Dropdown für Messkurve / Meldungen in View-Header umgezogen */}
|
|
||||||
|
|
||||||
{/* Buttons – nur sichtbar bei Messkurve, Platz bleibt erhalten */}
|
{/* Buttons */}
|
||||||
<div
|
{isMesskurve && (
|
||||||
style={{
|
<div className="flex items-center gap-2">
|
||||||
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
|
<button
|
||||||
}}
|
onClick={handleStartRSL}
|
||||||
className="flex items-center space-x-2"
|
className="btn-primary h-8 font-medium px-3"
|
||||||
>
|
disabled={isLoading || rslRunning}
|
||||||
<button
|
type="button"
|
||||||
onClick={handleStartRSL}
|
>
|
||||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap"
|
{rslRunning ? "RSL läuft…" : "RSL Messung starten"}
|
||||||
disabled={isLoading || rslRunning}
|
</button>
|
||||||
>
|
<button
|
||||||
{rslRunning ? "RSL läuft..." : "RSL starten"}
|
onClick={handleFetchData}
|
||||||
</button>
|
className="btn-primary h-8 font-medium px-3"
|
||||||
|
disabled={rslRunning}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Daten laden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isMeldungen && (
|
||||||
<button
|
<button
|
||||||
onClick={handleFetchData}
|
onClick={handleFetchData}
|
||||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap"
|
className="btn-primary h-8 font-medium px-4"
|
||||||
disabled={rslRunning}
|
disabled={rslRunning}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
Daten laden
|
Anzeigen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{rslRunning && (
|
{rslRunning && (
|
||||||
<div className="fixed inset-0 z-[1000] flex flex-col items-center justify-center bg-white/80 backdrop-blur-sm">
|
<div className="fixed inset-0 z-[1000] flex flex-col items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||||
<div className="mb-4 text-center space-y-1">
|
<div className="mb-4 text-center space-y-1">
|
||||||
<p className="text-lg font-semibold">RSL Messung läuft</p>
|
<p className="text-sm font-semibold">RSL Messung läuft</p>
|
||||||
<p className="text-sm text-gray-700">
|
<p className="text-xs opacity-80">
|
||||||
Bitte warten… (noch {TOTAL_DURATION - rslProgress}s)
|
Bitte warten…{" "}
|
||||||
|
{Math.min(100, Math.round((rslProgress / TOTAL_DURATION) * 100))}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2/3 max-w-xl h-4 bg-gray-200 rounded overflow-hidden shadow-inner">
|
<div className="w-2/3 max-w-xl h-3 bg-[var(--color-border)] rounded overflow-hidden shadow-inner">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-littwin-blue transition-all ease-linear"
|
className="h-full bg-littwin-blue transition-all ease-linear"
|
||||||
style={{ width: `${(rslProgress / TOTAL_DURATION) * 100}%` }}
|
style={{ width: `${(rslProgress / TOTAL_DURATION) * 100}%` }}
|
||||||
|
|||||||
@@ -1,31 +1,24 @@
|
|||||||
"use client"; // LoopChartView.tsx
|
"use client"; // LoopChartView.tsx
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { Listbox } from "@headlessui/react";
|
import { Listbox } from "@headlessui/react";
|
||||||
import ReactModal from "react-modal";
|
import ReactModal from "react-modal";
|
||||||
import LoopMeasurementChart from "./LoopMeasurementChart";
|
import LoopMeasurementChart from "./LoopMeasurementChart";
|
||||||
import Report from "../IsoMeasurementChart/Report";
|
import Report from "../IsoMeasurementChart/Report";
|
||||||
import LoopChartActionBar from "./LoopChartActionBar";
|
import LoopChartActionBar from "./LoopChartActionBar";
|
||||||
import { useRef } from "react";
|
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { AppDispatch } from "@/redux/store";
|
import { AppDispatch, RootState } from "@/redux/store";
|
||||||
import { RootState } from "@/redux/store";
|
|
||||||
import {
|
import {
|
||||||
setChartOpen,
|
setChartOpen,
|
||||||
setFullScreen,
|
setFullScreen,
|
||||||
setSlotNumber,
|
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,
|
setVonDatum,
|
||||||
setBisDatum,
|
setBisDatum,
|
||||||
setSelectedMode,
|
setSelectedMode,
|
||||||
setSelectedSlotType,
|
setSelectedSlotType,
|
||||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
} from "@/redux/slices/kabelueberwachungChartSlice";
|
||||||
|
import { setChartTitle as setLoopChartTitle } from "@/redux/slices/loopChartTypeSlice";
|
||||||
|
import { resetBrushRange } from "@/redux/slices/brushSlice";
|
||||||
import { resetDateRange } from "@/redux/slices/dateRangePickerSlice";
|
import { resetDateRange } from "@/redux/slices/dateRangePickerSlice";
|
||||||
|
|
||||||
interface LoopChartViewProps {
|
interface LoopChartViewProps {
|
||||||
@@ -34,11 +27,7 @@ interface LoopChartViewProps {
|
|||||||
slotIndex: number;
|
slotIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoopChartView: React.FC<LoopChartViewProps> = ({
|
function LoopChartView({ isOpen, onClose, slotIndex }: LoopChartViewProps) {
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
slotIndex,
|
|
||||||
}) => {
|
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
const chartTitle = useSelector(
|
const chartTitle = useSelector(
|
||||||
(state: RootState) => state.loopChartType.chartTitle
|
(state: RootState) => state.loopChartType.chartTitle
|
||||||
@@ -48,9 +37,6 @@ const LoopChartView: React.FC<LoopChartViewProps> = ({
|
|||||||
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
|
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
|
||||||
);
|
);
|
||||||
|
|
||||||
// useLoopChartLoader hook
|
|
||||||
const loadLoopChartData = useLoopChartLoader();
|
|
||||||
|
|
||||||
// slotNumber nicht direkt benötigt – wird intern über Redux genutzt
|
// slotNumber nicht direkt benötigt – wird intern über Redux genutzt
|
||||||
|
|
||||||
// **Modal schließen + Redux-Status zurücksetzen**
|
// **Modal schließen + Redux-Status zurücksetzen**
|
||||||
@@ -58,64 +44,35 @@ const LoopChartView: React.FC<LoopChartViewProps> = ({
|
|||||||
const today = new Date();
|
const today = new Date();
|
||||||
const thirtyDaysAgo = new Date();
|
const thirtyDaysAgo = new Date();
|
||||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||||
|
|
||||||
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
|
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
|
||||||
|
|
||||||
// Reset Datum
|
|
||||||
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||||
dispatch(setBisDatum(toISO(today)));
|
dispatch(setBisDatum(toISO(today)));
|
||||||
|
|
||||||
// Reset DateRangePicker
|
|
||||||
dispatch(resetDateRange());
|
dispatch(resetDateRange());
|
||||||
|
dispatch(setSelectedMode("DIA0"));
|
||||||
// Reset Dropdowns
|
|
||||||
dispatch(setSelectedMode("DIA0")); // Reset to Alle Messwerte
|
|
||||||
dispatch(setSelectedSlotType("schleifenwiderstand"));
|
dispatch(setSelectedSlotType("schleifenwiderstand"));
|
||||||
|
|
||||||
// Sonstiges Reset
|
|
||||||
dispatch(setChartOpen(false));
|
dispatch(setChartOpen(false));
|
||||||
dispatch(setFullScreen(false));
|
dispatch(setFullScreen(false));
|
||||||
dispatch(resetBrushRange());
|
dispatch(resetBrushRange());
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
// **Vollbildmodus umschalten**
|
const toggleFullScreen = () => dispatch(setFullScreen(!isFullScreen));
|
||||||
const toggleFullScreen = () => {
|
|
||||||
dispatch(setFullScreen(!isFullScreen));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Modal öffnen - RSL spezifische Einstellungen
|
|
||||||
const actionBarRef = useRef<{ handleFetchData: () => void }>(null);
|
const actionBarRef = useRef<{ handleFetchData: () => void }>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const thirtyDaysAgo = new Date();
|
const thirtyDaysAgo = new Date();
|
||||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||||
|
|
||||||
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
|
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
|
||||||
|
|
||||||
// Set slot number first
|
|
||||||
dispatch(setSlotNumber(slotIndex));
|
dispatch(setSlotNumber(slotIndex));
|
||||||
|
|
||||||
// Set dates
|
|
||||||
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||||
dispatch(setBisDatum(toISO(today)));
|
dispatch(setBisDatum(toISO(today)));
|
||||||
|
|
||||||
// Set RSL specific settings
|
|
||||||
dispatch(setSelectedSlotType("schleifenwiderstand"));
|
dispatch(setSelectedSlotType("schleifenwiderstand"));
|
||||||
dispatch(setSelectedMode("DIA0")); // Set to Alle Messwerte on open
|
dispatch(setSelectedMode("DIA0"));
|
||||||
|
const t = setTimeout(() => actionBarRef.current?.handleFetchData(), 120);
|
||||||
// Automatisch Daten laden wie Button-Klick
|
return () => clearTimeout(t);
|
||||||
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]);
|
}, [isOpen, slotIndex, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -124,76 +81,63 @@ const LoopChartView: React.FC<LoopChartViewProps> = ({
|
|||||||
onRequestClose={handleClose}
|
onRequestClose={handleClose}
|
||||||
ariaHideApp={false}
|
ariaHideApp={false}
|
||||||
style={{
|
style={{
|
||||||
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
overlay: {
|
||||||
|
backgroundColor: "rgba(0,0,0,0.55)",
|
||||||
|
backdropFilter: "blur(2px)",
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
top: "50%",
|
inset: "50% auto auto 50%",
|
||||||
left: "50%",
|
|
||||||
bottom: "auto",
|
|
||||||
marginRight: "-50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
transform: "translate(-50%, -50%)",
|
||||||
width: isFullScreen ? "90vw" : "70rem",
|
width: isFullScreen ? "90vw" : "72rem",
|
||||||
height: isFullScreen ? "90vh" : "35rem",
|
height: isFullScreen ? "90vh" : "38rem",
|
||||||
padding: "1rem",
|
padding: 0,
|
||||||
transition: "all 0.3s ease-in-out",
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
borderRadius: "14px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
contentLabel="Schleifenwiderstand"
|
||||||
>
|
>
|
||||||
{/* Action-Buttons */}
|
<header className="modal-header relative pr-56">
|
||||||
<div
|
<h3 className="text-sm font-semibold tracking-wide">
|
||||||
style={{
|
Schleifenwiderstand
|
||||||
position: "absolute",
|
</h3>
|
||||||
top: "0.625rem",
|
<div className="absolute top-2 right-2 flex gap-2">
|
||||||
right: "0.625rem",
|
<button
|
||||||
display: "flex",
|
onClick={toggleFullScreen}
|
||||||
gap: "0.75rem",
|
className="icon-btn"
|
||||||
}}
|
aria-label={isFullScreen ? "Vollbild verlassen" : "Vollbild"}
|
||||||
>
|
type="button"
|
||||||
{/* Fullscreen-Button */}
|
>
|
||||||
<button
|
<i
|
||||||
onClick={toggleFullScreen}
|
className={
|
||||||
style={{
|
isFullScreen
|
||||||
background: "transparent",
|
? "bi bi-fullscreen-exit"
|
||||||
border: "none",
|
: "bi bi-arrows-fullscreen"
|
||||||
fontSize: "1.5rem",
|
}
|
||||||
cursor: "pointer",
|
/>
|
||||||
}}
|
</button>
|
||||||
>
|
<button
|
||||||
<i
|
onClick={handleClose}
|
||||||
className={
|
className="icon-btn"
|
||||||
isFullScreen ? "bi bi-fullscreen-exit" : "bi bi-arrows-fullscreen"
|
aria-label="Schließen"
|
||||||
}
|
type="button"
|
||||||
></i>
|
>
|
||||||
</button>
|
<i
|
||||||
|
style={{
|
||||||
{/* Schließen-Button */}
|
background: "transparent",
|
||||||
<button
|
border: "none",
|
||||||
onClick={handleClose}
|
fontSize: "1.5rem",
|
||||||
style={{
|
cursor: "pointer",
|
||||||
background: "transparent",
|
}}
|
||||||
border: "none",
|
className="bi bi-x-circle-fill"
|
||||||
fontSize: "1.5rem",
|
/>
|
||||||
cursor: "pointer",
|
</button>
|
||||||
}}
|
</div>
|
||||||
>
|
<div className="absolute top-2 right-28">
|
||||||
<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
|
<Listbox
|
||||||
value={chartTitle}
|
value={chartTitle}
|
||||||
onChange={(value: "Messkurve" | "Meldungen") =>
|
onChange={(value: "Messkurve" | "Meldungen") =>
|
||||||
@@ -201,37 +145,21 @@ const LoopChartView: React.FC<LoopChartViewProps> = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="relative w-40">
|
<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">
|
<Listbox.Button className="dropdown-surface w-full flex items-center justify-between h-8">
|
||||||
<span>{chartTitle}</span>
|
<span className="dropdown-text-fix">{chartTitle}</span>
|
||||||
<svg
|
<i className="bi bi-chevron-down text-sm opacity-70" />
|
||||||
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.Button>
|
||||||
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
|
<Listbox.Options className="dropdown-options absolute z-50 mt-1 w-full max-h-60 overflow-auto text-sm">
|
||||||
{(["Messkurve", "Meldungen"] as const).map((option) => (
|
{(["Messkurve", "Meldungen"] as const).map((option) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={option}
|
key={option}
|
||||||
value={option}
|
value={option}
|
||||||
className={({
|
className={({ selected, active }) =>
|
||||||
selected,
|
`px-3 py-1.5 cursor-pointer rounded-sm m-0.5 ${
|
||||||
active,
|
|
||||||
}: {
|
|
||||||
selected: boolean;
|
|
||||||
active: boolean;
|
|
||||||
}) =>
|
|
||||||
`px-4 py-1 cursor-pointer ${
|
|
||||||
selected
|
selected
|
||||||
? "bg-littwin-blue text-white"
|
? "dropdown-option-active"
|
||||||
: active
|
: active
|
||||||
? "bg-gray-200"
|
? "dropdown-option-hover"
|
||||||
: ""
|
: ""
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
@@ -243,17 +171,19 @@ const LoopChartView: React.FC<LoopChartViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="flex flex-col flex-1 p-3 gap-3">
|
||||||
<LoopChartActionBar ref={actionBarRef} />
|
<LoopChartActionBar ref={actionBarRef} />
|
||||||
<div style={{ flex: 1, height: "90%" }}>
|
<div className="flex-1 relative">
|
||||||
{chartTitle === "Messkurve" ? (
|
{chartTitle === "Messkurve" ? (
|
||||||
<LoopMeasurementChart />
|
<LoopMeasurementChart />
|
||||||
) : (
|
) : (
|
||||||
<Report moduleType="RSL" autoLoad={false} />
|
<Report moduleType="RSL" autoLoad={chartTitle === "Meldungen"} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ReactModal>
|
</ReactModal>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default LoopChartView;
|
export default LoopChartView;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { AppDispatch } from "../../../../../../redux/store";
|
|||||||
import { Chart, registerables } from "chart.js";
|
import { Chart, registerables } from "chart.js";
|
||||||
import "chartjs-adapter-date-fns";
|
import "chartjs-adapter-date-fns";
|
||||||
import { getColor } from "../../../../../../utils/colors";
|
import { getColor } from "../../../../../../utils/colors";
|
||||||
import TDRChartActionBar from "./TDRChartActionBar";
|
|
||||||
import { getReferenceCurveBySlotThunk } from "../../../../../../redux/thunks/getReferenceCurveBySlotThunk";
|
import { getReferenceCurveBySlotThunk } from "../../../../../../redux/thunks/getReferenceCurveBySlotThunk";
|
||||||
|
|
||||||
const TDRChart: React.FC<{ isFullScreen: boolean }> = ({ isFullScreen }) => {
|
const TDRChart: React.FC<{ isFullScreen: boolean }> = ({ isFullScreen }) => {
|
||||||
@@ -213,8 +212,6 @@ const TDRChart: React.FC<{ isFullScreen: boolean }> = ({ isFullScreen }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%", height: isFullScreen ? "90%" : "28rem" }}>
|
<div style={{ width: "100%", height: isFullScreen ? "90%" : "28rem" }}>
|
||||||
<TDRChartActionBar />
|
|
||||||
|
|
||||||
{tdrChartData.length === 0 ? (
|
{tdrChartData.length === 0 ? (
|
||||||
<div className="flex items-center justify-center h-full text-gray-500 italic">
|
<div className="flex items-center justify-center h-full text-gray-500 italic">
|
||||||
⚠️ Keine Daten verfügbar für diesen Slot
|
⚠️ Keine Daten verfügbar für diesen Slot
|
||||||
|
|||||||
@@ -1,127 +1,149 @@
|
|||||||
|
"use client";
|
||||||
// /components/main/kabelueberwachung/kue705FO/Charts/TDRChart/TDRChartActionBar.tsx
|
// /components/main/kabelueberwachung/kue705FO/Charts/TDRChart/TDRChartActionBar.tsx
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
import React, { useState, useEffect } from "react";
|
import DateRangePicker from "@/components/common/DateRangePicker";
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { useAppDispatch } from "@/redux/store";
|
import { useAppDispatch } from "@/redux/store";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
import { RootState } from "@/redux/store";
|
import { RootState } from "@/redux/store";
|
||||||
|
import { Listbox } from "@headlessui/react";
|
||||||
|
import { getMessagesThunk } from "@/redux/thunks/getMessagesThunk";
|
||||||
|
import { setLoading } from "@/redux/slices/kabelueberwachungChartSlice";
|
||||||
import { fetchTDMDataBySlotThunk } from "@/redux/thunks/getTDMListBySlotThunk";
|
import { fetchTDMDataBySlotThunk } from "@/redux/thunks/getTDMListBySlotThunk";
|
||||||
import { getTDRChartDataByIdThunk } from "@/redux/thunks/getTDRChartDataByIdThunk";
|
import { getTDRChartDataByIdThunk } from "@/redux/thunks/getTDRChartDataByIdThunk";
|
||||||
import { getReferenceCurveBySlotThunk } from "@/redux/thunks/getReferenceCurveBySlotThunk"; // ⬅ import ergänzen
|
import { getReferenceCurveBySlotThunk } from "@/redux/thunks/getReferenceCurveBySlotThunk";
|
||||||
import { Listbox } from "@headlessui/react";
|
|
||||||
|
|
||||||
const TDRChartActionBar: React.FC = () => {
|
const TDRChartActionBar: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
// ✅ Redux: selectedSlot aus kueChartMode (0-basiert)
|
const { vonDatum, bisDatum, chartTitle } = useSelector(
|
||||||
|
(s: RootState) => s.kabelueberwachungChartSlice
|
||||||
|
);
|
||||||
|
const { vonDatum: pickerVon, bisDatum: pickerBis } = useSelector(
|
||||||
|
(s: RootState) => s.dateRangePicker
|
||||||
|
);
|
||||||
|
|
||||||
const selectedSlot = useSelector(
|
const selectedSlot = useSelector(
|
||||||
(state: RootState) => state.kueChartModeSlice.selectedSlot
|
(s: RootState) => s.kueChartModeSlice.selectedSlot
|
||||||
);
|
);
|
||||||
|
|
||||||
const tdmChartData = useSelector(
|
const tdmChartData = useSelector(
|
||||||
(state: RootState) => state.tdmSingleChartSlice.data
|
(s: RootState) => s.tdmSingleChartSlice.data
|
||||||
);
|
);
|
||||||
|
|
||||||
const idsForSlot =
|
const idsForSlot =
|
||||||
selectedSlot !== null ? tdmChartData[selectedSlot] ?? [] : [];
|
selectedSlot !== null ? tdmChartData[selectedSlot] ?? [] : [];
|
||||||
|
|
||||||
const tdrDataById = useSelector(
|
const tdrDataById = useSelector(
|
||||||
(state: RootState) => state.tdrDataByIdSlice.dataById
|
(s: RootState) => s.tdrDataByIdSlice.dataById
|
||||||
);
|
);
|
||||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||||
const currentChartData = selectedId !== null ? tdrDataById[selectedId] : [];
|
const currentChartData = selectedId !== null ? tdrDataById[selectedId] : [];
|
||||||
|
|
||||||
// 📌 Referenz setzen (nutzt Slotnummer + 1 für die API)
|
const isMeldungen = chartTitle === "Meldungen";
|
||||||
|
|
||||||
|
// Progress for running TDR measurement
|
||||||
|
const TDR_TOTAL_DURATION = parseInt(
|
||||||
|
process.env.NEXT_PUBLIC_TDR_DURATION_SECONDS || "120",
|
||||||
|
10
|
||||||
|
);
|
||||||
|
const [tdrRunning, setTdrRunning] = useState(false);
|
||||||
|
const [tdrProgress, setTdrProgress] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!tdrRunning) return;
|
||||||
|
setTdrProgress(0);
|
||||||
|
const started = Date.now();
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const elapsed = Math.floor((Date.now() - started) / 1000);
|
||||||
|
if (elapsed >= TDR_TOTAL_DURATION) {
|
||||||
|
setTdrProgress(TDR_TOTAL_DURATION);
|
||||||
|
setTdrRunning(false);
|
||||||
|
clearInterval(interval);
|
||||||
|
} else {
|
||||||
|
setTdrProgress(elapsed);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [tdrRunning, TDR_TOTAL_DURATION]);
|
||||||
|
|
||||||
|
const startTdrProgress = () => {
|
||||||
|
setTdrRunning(true);
|
||||||
|
setTdrProgress(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFetchMessages = async () => {
|
||||||
|
const fromDate = pickerVon ?? vonDatum;
|
||||||
|
const toDate = pickerBis ?? bisDatum;
|
||||||
|
try {
|
||||||
|
dispatch(setLoading(true));
|
||||||
|
await dispatch(getMessagesThunk({ fromDate, toDate })).unwrap();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("❌ Fehler beim Laden der Meldungen", e);
|
||||||
|
alert("❌ Fehler beim Laden der Meldungen.");
|
||||||
|
} finally {
|
||||||
|
dispatch(setLoading(false));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSetReference = async () => {
|
const handleSetReference = async () => {
|
||||||
if (
|
if (
|
||||||
selectedSlot === null ||
|
selectedSlot === null ||
|
||||||
selectedId === null ||
|
selectedId === null ||
|
||||||
!currentChartData?.length
|
!currentChartData.length
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development";
|
||||||
try {
|
try {
|
||||||
const slotNumber = selectedSlot + 1; // Slot ist 0-basiert, API will 1-basiert
|
|
||||||
const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development";
|
|
||||||
|
|
||||||
if (isDev) {
|
|
||||||
await fetch("/api/cpl/updateTdrReferenceCurveAPIHandler", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
slot: slotNumber,
|
|
||||||
data: currentChartData,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const url = `/CPL?/${window.location.pathname}&KTR${slotNumber}=${selectedId}`;
|
|
||||||
await fetch(url, { method: "GET" });
|
|
||||||
}
|
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
const url = `/CPL?KTR${slotNumber}=${selectedId}`;
|
const url = `/CPL?KTR${selectedSlot + 1}=${selectedId}`;
|
||||||
const response = await fetch(url, { method: "GET" });
|
const response = await fetch(url, { method: "GET" });
|
||||||
|
if (!response.ok)
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Fehler beim Setzen der Referenz: ${response.statusText}`
|
`Fehler beim Setzen der Referenz: ${response.statusText}`
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: lokale Speicherung und Redux-Update
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
`ref-curve-slot${selectedSlot}`,
|
`ref-curve-slot${selectedSlot}`,
|
||||||
JSON.stringify(currentChartData)
|
JSON.stringify(currentChartData)
|
||||||
);
|
);
|
||||||
|
|
||||||
dispatch(getReferenceCurveBySlotThunk(selectedSlot));
|
dispatch(getReferenceCurveBySlotThunk(selectedSlot));
|
||||||
|
|
||||||
alert("Referenzkurve wurde erfolgreich gesetzt!");
|
alert("Referenzkurve wurde erfolgreich gesetzt!");
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error("Fehler beim Setzen der Referenzkurve:", error);
|
console.error("Fehler beim Setzen der Referenzkurve", err);
|
||||||
alert("Fehler beim Setzen der Referenzkurve.");
|
alert("Fehler beim Setzen der Referenzkurve.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 📌 TDR Messung starten
|
|
||||||
const handleStartTDR = async () => {
|
const handleStartTDR = async () => {
|
||||||
if (selectedSlot === null) {
|
if (selectedSlot === null) {
|
||||||
alert("⚠️ Bitte zuerst einen KÜ auswählen!");
|
alert("⚠️ Bitte zuerst einen KÜ auswählen!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cgiUrl = `${window.location.origin}/CPL?/${window.location.pathname}&KTT${selectedSlot}=1`;
|
const cgiUrl = `${window.location.origin}/CPL?/${window.location.pathname}&KTT${selectedSlot}=1`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("🚀 Starte TDR Messung für Slot:", selectedSlot);
|
const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development";
|
||||||
console.log("📡 CGI URL:", cgiUrl);
|
if (isDev) {
|
||||||
|
await new Promise((r) => setTimeout(r, 150));
|
||||||
const response = await fetch(cgiUrl);
|
startTdrProgress();
|
||||||
|
return;
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`CGI-Fehler: ${response.status}`);
|
|
||||||
}
|
}
|
||||||
|
const response = await fetch(cgiUrl);
|
||||||
console.log("✅ TDR Messung gestartet für Slot", selectedSlot);
|
if (!response.ok) throw new Error(`CGI-Fehler: ${response.status}`);
|
||||||
alert(`✅ TDR Messung für Slot ${selectedSlot + 1} gestartet`);
|
startTdrProgress();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ Fehler beim Starten der TDR Messung:", 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
|
// Load TDM list when slot changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedSlot !== null) {
|
if (selectedSlot !== null) {
|
||||||
dispatch(fetchTDMDataBySlotThunk(selectedSlot)).then((action) => {
|
dispatch(fetchTDMDataBySlotThunk(selectedSlot)).then((action) => {
|
||||||
// action can be a PayloadAction with payload or a rejected action
|
|
||||||
const payload = (
|
const payload = (
|
||||||
action as {
|
action as {
|
||||||
payload?: { data?: { id: number; t: string; d: number }[] };
|
payload?: { data?: { id: number; t: string; d: number }[] };
|
||||||
}
|
}
|
||||||
).payload;
|
).payload;
|
||||||
const slotData = payload?.data;
|
const slotData = payload?.data ?? [];
|
||||||
if ((slotData ?? []).length > 0) {
|
if (slotData.length > 0) {
|
||||||
const lastId = (slotData ?? [])[0].id;
|
const lastId = slotData[0].id; // latest first
|
||||||
setSelectedId(lastId);
|
setSelectedId(lastId);
|
||||||
dispatch(getTDRChartDataByIdThunk(lastId));
|
dispatch(getTDRChartDataByIdThunk(lastId));
|
||||||
}
|
}
|
||||||
@@ -130,109 +152,160 @@ const TDRChartActionBar: React.FC = () => {
|
|||||||
}, [selectedSlot, dispatch]);
|
}, [selectedSlot, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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="toolbar w-full justify-between flex-wrap">
|
||||||
<div className="text-sm font-semibold">
|
{/* KÜ number left, controls right, like IsoChartActionBar */}
|
||||||
{selectedSlot !== null ? `KÜ ${selectedSlot + 1}` : "Kein KÜ gewählt"}
|
<div className="flex items-center gap-2 pr-4">
|
||||||
</div>
|
<span className="font-semibold uppercase tracking-wide text-muted">
|
||||||
|
KÜ
|
||||||
{/* ✅ Referenz setzen */}
|
</span>
|
||||||
{selectedId !== null && (
|
<span className="font-medium px-2 py-0.5 rounded bg-surface-alt border border-base min-w-[3rem] text-center">
|
||||||
<button
|
{selectedSlot !== null ? selectedSlot + 1 : "-"}
|
||||||
onClick={handleSetReference}
|
</span>
|
||||||
className="border border-littwin-blue text-littwin-blue bg-white rounded px-3 py-1 text-sm hover:bg-gray-200"
|
</div>
|
||||||
>
|
<div className="flex items-center gap-3 flex-1 justify-end">
|
||||||
TDR-Kurve als Referenz speichern
|
{isMeldungen ? (
|
||||||
</button>
|
<>
|
||||||
)}
|
<DateRangePicker />
|
||||||
|
<button
|
||||||
{/* 🚀 TDR starten */}
|
type="button"
|
||||||
<button
|
onClick={handleFetchMessages}
|
||||||
onClick={handleStartTDR}
|
className="btn-primary h-8 font-medium px-4"
|
||||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap "
|
disabled={selectedSlot === null}
|
||||||
disabled={selectedSlot === null}
|
>
|
||||||
>
|
Anzeigen
|
||||||
Messung aktivieren
|
</button>
|
||||||
</button>
|
</>
|
||||||
|
) : (
|
||||||
{/* 🔽 Dropdown für Messungen */}
|
<>
|
||||||
<div className="flex items-center space-x-2">
|
{selectedId !== null && (
|
||||||
<Listbox
|
<button
|
||||||
value={selectedId}
|
onClick={handleSetReference}
|
||||||
onChange={(id) => {
|
type="button"
|
||||||
setSelectedId(id);
|
className="btn-primary h-8 px-3 font-medium"
|
||||||
if (id !== null) {
|
disabled={selectedSlot === null}
|
||||||
dispatch(getTDRChartDataByIdThunk(id));
|
>
|
||||||
}
|
TDR-Kurve als Referenz speichern
|
||||||
}}
|
</button>
|
||||||
disabled={idsForSlot.length === 0}
|
)}
|
||||||
>
|
<button
|
||||||
<div className="relative w-96">
|
onClick={handleStartTDR}
|
||||||
<Listbox.Button className="w-full border px-2 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
|
type="button"
|
||||||
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
|
disabled={selectedSlot === null || tdrRunning}
|
||||||
{selectedId
|
className={`btn-primary h-8 px-4 whitespace-nowrap ${
|
||||||
? (() => {
|
tdrRunning ? "opacity-90" : ""
|
||||||
const selected = idsForSlot.find(
|
}`}
|
||||||
(e) => e.id === selectedId
|
>
|
||||||
);
|
{tdrRunning
|
||||||
return selected
|
? `TDR läuft... (${Math.min(
|
||||||
? `${new Date(selected.t).toLocaleString("de-DE", {
|
100,
|
||||||
|
Math.round((tdrProgress / TDR_TOTAL_DURATION) * 100)
|
||||||
|
)}%)`
|
||||||
|
: "TDR-Messung starten"}
|
||||||
|
</button>
|
||||||
|
<div className="ml-auto flex-1 min-w-[14rem] max-w-[30rem]">
|
||||||
|
<Listbox
|
||||||
|
value={selectedId}
|
||||||
|
onChange={(id) => {
|
||||||
|
setSelectedId(id);
|
||||||
|
if (id !== null) dispatch(getTDRChartDataByIdThunk(id));
|
||||||
|
}}
|
||||||
|
disabled={idsForSlot.length === 0}
|
||||||
|
>
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Listbox.Button className="dropdown-surface w-full flex items-center justify-between h-8 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
<span className="dropdown-text-fix whitespace-nowrap overflow-hidden text-ellipsis pr-2">
|
||||||
|
{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>
|
||||||
|
<i className="bi bi-chevron-down text-sm opacity-70" />
|
||||||
|
</Listbox.Button>
|
||||||
|
<Listbox.Options className="dropdown-options absolute z-50 mt-1 w-full max-h-72 overflow-auto text-sm bg-[var(--color-surface)] border border-base rounded-md shadow-lg p-1">
|
||||||
|
{idsForSlot.map((entry) => {
|
||||||
|
const dateLabel = new Date(entry.t).toLocaleString(
|
||||||
|
"de-DE",
|
||||||
|
{
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
})} – Fehlerstelle: ${selected.d} m`
|
}
|
||||||
: "Wähle Messung";
|
);
|
||||||
})()
|
const fullText = `${dateLabel} – Fehlerstelle: ${entry.d} m`;
|
||||||
: "Wähle Messung"}
|
return (
|
||||||
</span>
|
<Listbox.Option
|
||||||
<svg
|
key={entry.id}
|
||||||
className="w-5 h-5 text-gray-400"
|
value={entry.id}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
title={fullText}
|
||||||
viewBox="0 0 20 20"
|
className={({ selected, active }) => {
|
||||||
fill="currentColor"
|
const base =
|
||||||
aria-hidden="true"
|
"px-3 h-8 cursor-pointer rounded-sm m-0.5 flex items-center justify-start transition-colors text-[13px]";
|
||||||
>
|
if (selected)
|
||||||
<path
|
return `${base} dropdown-option-active font-medium`;
|
||||||
fillRule="evenodd"
|
if (active)
|
||||||
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"
|
return `${base} dropdown-option-hover`;
|
||||||
clipRule="evenodd"
|
return `${base}`;
|
||||||
/>
|
}}
|
||||||
</svg>
|
>
|
||||||
</Listbox.Button>
|
<span className="truncate w-full">{fullText}</span>
|
||||||
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
|
</Listbox.Option>
|
||||||
{idsForSlot.map((entry) => (
|
);
|
||||||
<Listbox.Option
|
})}
|
||||||
key={entry.id}
|
</Listbox.Options>
|
||||||
value={entry.id}
|
</div>
|
||||||
className={({ selected, active }) =>
|
</Listbox>
|
||||||
`px-4 py-1 cursor-pointer whitespace-nowrap overflow-hidden text-ellipsis ${
|
</div>
|
||||||
selected
|
</>
|
||||||
? "bg-littwin-blue text-white"
|
)}
|
||||||
: active
|
</div>
|
||||||
? "bg-gray-200"
|
{/* Progress Overlay */}
|
||||||
: ""
|
{tdrRunning && (
|
||||||
}`
|
<div className="fixed inset-0 z-[1000] flex flex-col items-center justify-center bg-[rgba(0,0,0,0.55)] backdrop-blur-sm">
|
||||||
}
|
<div className="mb-4 text-center space-y-1">
|
||||||
>
|
<p className="text-lg font-semibold text-white">
|
||||||
{new Date(entry.t).toLocaleString("de-DE", {
|
TDR Messung läuft... kann bis zu zwei Minuten dauern
|
||||||
day: "2-digit",
|
</p>
|
||||||
month: "2-digit",
|
<p className="text-sm text-white/80">
|
||||||
year: "numeric",
|
Bitte warten…{" "}
|
||||||
hour: "2-digit",
|
{Math.min(
|
||||||
minute: "2-digit",
|
100,
|
||||||
second: "2-digit",
|
Math.round((tdrProgress / TDR_TOTAL_DURATION) * 100)
|
||||||
})}{" "}
|
)}
|
||||||
– Fehlerstelle: {entry.d} m
|
%
|
||||||
</Listbox.Option>
|
</p>
|
||||||
))}
|
</div>
|
||||||
</Listbox.Options>
|
<div className="w-2/3 max-w-xl h-3 bg-white/20 rounded overflow-hidden shadow-inner">
|
||||||
|
<div
|
||||||
|
className="h-full bg-accent transition-all ease-linear"
|
||||||
|
style={{
|
||||||
|
width: `${(tdrProgress / TDR_TOTAL_DURATION) * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Listbox>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,30 +3,25 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import ReactModal from "react-modal";
|
import ReactModal from "react-modal";
|
||||||
import TDRChart from "./TDRChart";
|
import TDRChart from "./TDRChart";
|
||||||
|
import TDRChartActionBar from "./TDRChartActionBar";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { AppDispatch } from "@/redux/store";
|
import { AppDispatch, RootState } from "@/redux/store";
|
||||||
import { RootState } from "@/redux/store";
|
|
||||||
import {
|
import {
|
||||||
setChartOpen,
|
setChartOpen,
|
||||||
setFullScreen,
|
setFullScreen,
|
||||||
setSlotNumber,
|
setSlotNumber,
|
||||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
|
||||||
|
|
||||||
import { resetBrushRange } from "@/redux/slices/brushSlice";
|
|
||||||
|
|
||||||
import {
|
|
||||||
setVonDatum,
|
setVonDatum,
|
||||||
setBisDatum,
|
setBisDatum,
|
||||||
setSelectedMode,
|
setSelectedMode,
|
||||||
setSelectedSlotType,
|
setSelectedSlotType,
|
||||||
|
setChartTitle,
|
||||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
} from "@/redux/slices/kabelueberwachungChartSlice";
|
||||||
|
import { resetBrushRange } from "@/redux/slices/brushSlice";
|
||||||
import {
|
import {
|
||||||
setSelectedSlot,
|
setSelectedSlot,
|
||||||
setActiveMode,
|
setActiveMode,
|
||||||
} from "@/redux/slices/kueChartModeSlice";
|
} from "@/redux/slices/kueChartModeSlice";
|
||||||
import { Listbox } from "@headlessui/react";
|
import { Listbox } from "@headlessui/react";
|
||||||
import { setChartTitle } from "@/redux/slices/kabelueberwachungChartSlice";
|
|
||||||
import Report from "../IsoMeasurementChart/Report";
|
import Report from "../IsoMeasurementChart/Report";
|
||||||
|
|
||||||
interface TDRChartViewProps {
|
interface TDRChartViewProps {
|
||||||
@@ -41,64 +36,48 @@ const TDRChartView: React.FC<TDRChartViewProps> = ({
|
|||||||
slotIndex,
|
slotIndex,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
const { isFullScreen, chartTitle } = useSelector(
|
const { isFullScreen, chartTitle } = useSelector(
|
||||||
(state: RootState) => state.kabelueberwachungChartSlice
|
(s: RootState) => s.kabelueberwachungChartSlice
|
||||||
);
|
);
|
||||||
|
|
||||||
// **Modal öffnen - TDR spezifische Einstellungen**
|
// Initialize defaults when opening
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (!isOpen) return;
|
||||||
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 = () => {
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const thirtyDaysAgo = new Date();
|
const thirtyDaysAgo = new Date();
|
||||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||||
|
|
||||||
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
|
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
|
||||||
|
|
||||||
// Reset Datum
|
dispatch(setActiveMode("TDR"));
|
||||||
|
dispatch(setSelectedSlot(slotIndex));
|
||||||
|
dispatch(setSlotNumber(slotIndex));
|
||||||
|
|
||||||
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||||
dispatch(setBisDatum(toISO(today)));
|
dispatch(setBisDatum(toISO(today)));
|
||||||
|
|
||||||
// Reset Dropdowns
|
if (chartTitle !== "Messkurve" && chartTitle !== "Meldungen") {
|
||||||
|
dispatch(setChartTitle("Messkurve"));
|
||||||
|
}
|
||||||
|
// Only run when opened or slot changes or chartTitle invalid
|
||||||
|
}, [isOpen, slotIndex, chartTitle, dispatch]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
// Reset generic chart slice to DIA1 isolationswiderstand defaults (same pattern as other modals)
|
||||||
|
const today = new Date();
|
||||||
|
const thirtyDaysAgo = new Date();
|
||||||
|
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||||
|
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
|
||||||
|
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
|
||||||
|
dispatch(setBisDatum(toISO(today)));
|
||||||
dispatch(setSelectedMode("DIA1"));
|
dispatch(setSelectedMode("DIA1"));
|
||||||
dispatch(setSelectedSlotType("isolationswiderstand"));
|
dispatch(setSelectedSlotType("isolationswiderstand"));
|
||||||
|
|
||||||
// Sonstiges Reset
|
|
||||||
dispatch(setChartOpen(false));
|
dispatch(setChartOpen(false));
|
||||||
dispatch(setFullScreen(false));
|
dispatch(setFullScreen(false));
|
||||||
dispatch(resetBrushRange());
|
dispatch(resetBrushRange());
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
// **Vollbildmodus umschalten**
|
const toggleFullScreen = () => dispatch(setFullScreen(!isFullScreen));
|
||||||
const toggleFullScreen = () => {
|
|
||||||
dispatch(setFullScreen(!isFullScreen));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactModal
|
<ReactModal
|
||||||
@@ -106,77 +85,61 @@ const TDRChartView: React.FC<TDRChartViewProps> = ({
|
|||||||
onRequestClose={handleClose}
|
onRequestClose={handleClose}
|
||||||
ariaHideApp={false}
|
ariaHideApp={false}
|
||||||
style={{
|
style={{
|
||||||
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
overlay: {
|
||||||
|
backgroundColor: "rgba(0,0,0,0.55)",
|
||||||
|
backdropFilter: "blur(2px)",
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
top: "50%",
|
inset: "50% auto auto 50%",
|
||||||
left: "50%",
|
|
||||||
bottom: "auto",
|
|
||||||
marginRight: "-50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
transform: "translate(-50%, -50%)",
|
||||||
width: isFullScreen ? "90vw" : "70rem",
|
width: isFullScreen ? "90vw" : "72rem",
|
||||||
height: isFullScreen ? "90vh" : "35rem",
|
height: isFullScreen ? "90vh" : "38rem",
|
||||||
padding: "1rem",
|
padding: 0,
|
||||||
transition: "all 0.3s ease-in-out",
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
borderRadius: "14px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
contentLabel="TDR Messung"
|
||||||
>
|
>
|
||||||
{/* Action-Buttons */}
|
<header className="modal-header relative pr-56">
|
||||||
<div
|
<h3 className="text-sm font-semibold tracking-wide">TDR-Messung</h3>
|
||||||
style={{
|
<div className="absolute top-2 right-2 flex gap-2">
|
||||||
position: "absolute",
|
<button
|
||||||
top: "0.625rem",
|
onClick={toggleFullScreen}
|
||||||
right: "0.625rem",
|
className="icon-btn"
|
||||||
display: "flex",
|
aria-label={isFullScreen ? "Vollbild verlassen" : "Vollbild"}
|
||||||
gap: "0.75rem",
|
type="button"
|
||||||
}}
|
>
|
||||||
>
|
<i
|
||||||
{/* Fullscreen-Button */}
|
className={
|
||||||
<button
|
isFullScreen
|
||||||
onClick={toggleFullScreen}
|
? "bi bi-fullscreen-exit"
|
||||||
style={{
|
: "bi bi-arrows-fullscreen"
|
||||||
background: "transparent",
|
}
|
||||||
border: "none",
|
/>
|
||||||
fontSize: "1.5rem",
|
</button>
|
||||||
cursor: "pointer",
|
<button
|
||||||
}}
|
onClick={handleClose}
|
||||||
>
|
className="icon-btn"
|
||||||
<i
|
aria-label="Schließen"
|
||||||
className={
|
type="button"
|
||||||
isFullScreen ? "bi bi-fullscreen-exit" : "bi bi-arrows-fullscreen"
|
>
|
||||||
}
|
<i
|
||||||
></i>
|
style={{
|
||||||
</button>
|
background: "transparent",
|
||||||
|
border: "none",
|
||||||
{/* Schließen-Button */}
|
fontSize: "1.5rem",
|
||||||
<button
|
cursor: "pointer",
|
||||||
onClick={handleClose}
|
}}
|
||||||
style={{
|
className="bi bi-x-circle-fill"
|
||||||
background: "transparent",
|
/>
|
||||||
border: "none",
|
</button>
|
||||||
fontSize: "1.5rem",
|
</div>
|
||||||
cursor: "pointer",
|
<div className="absolute top-2 right-28">
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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" ? "TDR-Messung" : "Meldungen"}
|
|
||||||
</h3>
|
|
||||||
{/* Dropdown Messkurve / Meldungen */}
|
|
||||||
<Listbox
|
<Listbox
|
||||||
value={chartTitle}
|
value={chartTitle}
|
||||||
onChange={(value: "Messkurve" | "Meldungen") =>
|
onChange={(value: "Messkurve" | "Meldungen") =>
|
||||||
@@ -184,31 +147,21 @@ const TDRChartView: React.FC<TDRChartViewProps> = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="relative w-40">
|
<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">
|
<Listbox.Button className="dropdown-surface w-full flex items-center justify-between h-8">
|
||||||
<span>{chartTitle}</span>
|
<span className="dropdown-text-fix">{chartTitle}</span>
|
||||||
<svg
|
<i className="bi bi-chevron-down text-sm opacity-70" />
|
||||||
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.Button>
|
||||||
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
|
<Listbox.Options className="dropdown-options absolute z-50 mt-1 w-full max-h-60 overflow-auto text-sm">
|
||||||
{(["Messkurve", "Meldungen"] as const).map((option) => (
|
{(["Messkurve", "Meldungen"] as const).map((option) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={option}
|
key={option}
|
||||||
value={option}
|
value={option}
|
||||||
className={({ selected, active }) =>
|
className={({ selected, active }) =>
|
||||||
`px-4 py-1 cursor-pointer ${
|
`px-3 py-1.5 cursor-pointer rounded-sm m-0.5 ${
|
||||||
selected
|
selected
|
||||||
? "bg-littwin-blue text-white"
|
? "dropdown-option-active"
|
||||||
: active
|
: active
|
||||||
? "bg-gray-200"
|
? "dropdown-option-hover"
|
||||||
: ""
|
: ""
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
@@ -220,8 +173,11 @@ const TDRChartView: React.FC<TDRChartViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
</div>
|
</div>
|
||||||
{/* Chart oder Meldungen */}
|
</header>
|
||||||
<div style={{ flex: 1, height: "90%" }}>
|
<div className="flex flex-col flex-1 p-3 gap-3">
|
||||||
|
{/* Action Bar (wie bei ISO / Loop) */}
|
||||||
|
<TDRChartActionBar />
|
||||||
|
<div className="flex-1 relative">
|
||||||
{chartTitle === "Messkurve" ? (
|
{chartTitle === "Messkurve" ? (
|
||||||
<TDRChart isFullScreen={isFullScreen} />
|
<TDRChart isFullScreen={isFullScreen} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
"use client"; // components/modules/kue705FO/Kue705FO.tsx
|
"use client"; // components/modules/kue705FO/Kue705FO.tsx
|
||||||
import React, { useState, useMemo } from "react";
|
import React, { useState, useMemo, useEffect, useRef } from "react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const Marquee: any = dynamic(() => import("react-fast-marquee"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import KueModal from "./modals/SettingsModalWrapper";
|
import KueModal from "./modals/SettingsModalWrapper";
|
||||||
// import FallSensors from "../../fall-detection-sensors/FallSensors";
|
// import FallSensors from "../../fall-detection-sensors/FallSensors";
|
||||||
@@ -26,7 +31,6 @@ import useKueVersion from "./hooks/useKueVersion";
|
|||||||
import useIsoDisplay from "./hooks/useIsoDisplay";
|
import useIsoDisplay from "./hooks/useIsoDisplay";
|
||||||
import useLoopDisplay from "./hooks/useLoopDisplay";
|
import useLoopDisplay from "./hooks/useLoopDisplay";
|
||||||
import useModulName from "./hooks/useModulName";
|
import useModulName from "./hooks/useModulName";
|
||||||
import { useAdminAuth } from "../../settingsPageComponents/hooks/useAdminAuth";
|
|
||||||
|
|
||||||
//--------handlers----------------
|
//--------handlers----------------
|
||||||
// Keep needed imports
|
// Keep needed imports
|
||||||
@@ -52,7 +56,21 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
|||||||
const { kueName } = useSelector((state: RootState) => state.kueDataSlice);
|
const { kueName } = useSelector((state: RootState) => state.kueDataSlice);
|
||||||
|
|
||||||
// Admin authentication hook for security - using showModal as true for continuous auth check
|
// Admin authentication hook for security - using showModal as true for continuous auth check
|
||||||
const { isAdminLoggedIn } = useAdminAuth(true);
|
// Admin Auth hook retained (result not currently needed after KVZ visibility change)
|
||||||
|
// const { isAdminLoggedIn } = useAdminAuth(true);
|
||||||
|
|
||||||
|
// Modulname (max 48 Zeichen) vorbereiten
|
||||||
|
const moduleNameRaw = useMemo(
|
||||||
|
() => kueName?.[slotIndex] || `Modul ${slotIndex + 1}`,
|
||||||
|
[kueName, slotIndex]
|
||||||
|
);
|
||||||
|
const moduleName48 = useMemo(
|
||||||
|
() =>
|
||||||
|
typeof moduleNameRaw === "string"
|
||||||
|
? moduleNameRaw.slice(0, 48)
|
||||||
|
: String(moduleNameRaw),
|
||||||
|
[moduleNameRaw]
|
||||||
|
);
|
||||||
|
|
||||||
const [activeButton, setActiveButton] = useState<"Schleife" | "TDR" | "ISO">(
|
const [activeButton, setActiveButton] = useState<"Schleife" | "TDR" | "ISO">(
|
||||||
"Schleife"
|
"Schleife"
|
||||||
@@ -156,18 +174,6 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
|||||||
const openTdrModal = () => {
|
const openTdrModal = () => {
|
||||||
setActiveButton("TDR");
|
setActiveButton("TDR");
|
||||||
setloopTitleText("Entfernung [km]");
|
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);
|
setShowTdrModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -206,31 +212,64 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
|||||||
);
|
);
|
||||||
const { setCurrentModulName } = useModulName(slotIndex, modulName);
|
const { setCurrentModulName } = useModulName(slotIndex, modulName);
|
||||||
//---------------------------------
|
//---------------------------------
|
||||||
//---------------------------------
|
// Version-gate für Laufschrift: erst ab V4.30 aktiv
|
||||||
const tdmChartData = useSelector(
|
const parseVersion = (v?: string): [number, number, number] => {
|
||||||
(state: RootState) => state.tdmChartSlice.data
|
if (!v) return [0, 0, 0];
|
||||||
|
const m = String(v).match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
||||||
|
if (!m) return [0, 0, 0];
|
||||||
|
const major = parseInt(m[1] || "0", 10) || 0;
|
||||||
|
const minor = parseInt(m[2] || "0", 10) || 0;
|
||||||
|
const patch = parseInt(m[3] || "0", 10) || 0;
|
||||||
|
return [major, minor, patch];
|
||||||
|
};
|
||||||
|
const gte = (a: [number, number, number], b: [number, number, number]) => {
|
||||||
|
if (a[0] !== b[0]) return a[0] > b[0];
|
||||||
|
if (a[1] !== b[1]) return a[1] > b[1];
|
||||||
|
return a[2] >= b[2];
|
||||||
|
};
|
||||||
|
const marqueeOverride =
|
||||||
|
process.env.NEXT_PUBLIC_ENABLE_KUE_MARQUEE === "1" ||
|
||||||
|
process.env.NEXT_PUBLIC_ENABLE_KUE_MARQUEE === "true";
|
||||||
|
const scrollFeatureEnabled = useMemo(
|
||||||
|
() => marqueeOverride || gte(parseVersion(kueVersion), [4, 30, 0]),
|
||||||
|
[kueVersion, marqueeOverride]
|
||||||
);
|
);
|
||||||
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));
|
// Überlängen-Erkennung für Laufschrift
|
||||||
//setLoopDisplayValue(latestTdrDistance);
|
const nameContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const measureTextRef = useRef<HTMLSpanElement | null>(null);
|
||||||
|
const [shouldScroll, setShouldScroll] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const measure = () => {
|
||||||
|
if (!scrollFeatureEnabled) {
|
||||||
|
setShouldScroll(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const container = nameContainerRef.current;
|
||||||
|
const text = measureTextRef.current;
|
||||||
|
if (!container || !text) {
|
||||||
|
setShouldScroll(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const needs = text.scrollWidth > container.clientWidth + 2;
|
||||||
|
setShouldScroll(needs);
|
||||||
|
};
|
||||||
|
measure();
|
||||||
|
window.addEventListener("resize", measure);
|
||||||
|
return () => window.removeEventListener("resize", measure);
|
||||||
|
}, [moduleName48, scrollFeatureEnabled]);
|
||||||
|
//---------------------------------
|
||||||
|
// TDR Distanz wird im Display nicht angezeigt – Daten für Modal werden separat geladen
|
||||||
|
|
||||||
//---------------------------------
|
//---------------------------------
|
||||||
|
|
||||||
const loopValue =
|
const rslValue =
|
||||||
activeButton === "TDR"
|
typeof schleifenwiderstand === "number"
|
||||||
? latestTdrDistance
|
|
||||||
: typeof schleifenwiderstand === "number"
|
|
||||||
? schleifenwiderstand
|
? schleifenwiderstand
|
||||||
: Number(schleifenwiderstand);
|
: Number(schleifenwiderstand);
|
||||||
|
|
||||||
const { loopDisplayValue, setLoopDisplayValue } = useLoopDisplay(
|
const { loopDisplayValue, setLoopDisplayValue } = useLoopDisplay(
|
||||||
loopValue,
|
rslValue,
|
||||||
activeButton
|
activeButton
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -238,10 +277,10 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
|||||||
const isTdrActiveForSlot = tdrActive?.[slotIndex] === 1;
|
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
|
// KVz aktiv Status für diesen Slot prüfen - nur wenn Admin authentifiziert ist, KVz vorhanden ist UND aktiviert ist
|
||||||
|
// Anpassung: KVZ Button soll sichtbar/benutzbar bleiben, auch wenn Admin sich abmeldet,
|
||||||
|
// sobald KVZ Präsenz + Aktiv-Flag gesetzt sind. Admin wird nur zum Aktivieren benötigt.
|
||||||
const isKvzActiveForSlot =
|
const isKvzActiveForSlot =
|
||||||
kvzPresence?.[slotIndex] === 1 &&
|
kvzPresence?.[slotIndex] === 1 && kvzActive?.[slotIndex] === 1;
|
||||||
kvzActive?.[slotIndex] === 1 &&
|
|
||||||
isAdminLoggedIn;
|
|
||||||
|
|
||||||
// Removed useChartData(loopMeasurementCurveChartData) as the state was unused
|
// Removed useChartData(loopMeasurementCurveChartData) as the state was unused
|
||||||
|
|
||||||
@@ -340,15 +379,19 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
|||||||
>
|
>
|
||||||
{isoDisplayValue === "Abgleich"
|
{isoDisplayValue === "Abgleich"
|
||||||
? "ISO: Abgleich"
|
? "ISO: Abgleich"
|
||||||
: `ISO: ${Number(isolationswert)} MOhm`}
|
: `ISO: ${Number(isolationswert)
|
||||||
|
.toFixed(2)
|
||||||
|
.replace(".", ",")} MOhm`}
|
||||||
</span>
|
</span>
|
||||||
{/* 3. Zeile: Schleifenwert, in Rot bei Schleifenfehler, sonst normal */}
|
{/* 3. Zeile: Schleifenwert (RSL) immer anzeigen, unabhängig von aktivem Button */}
|
||||||
<span
|
<span
|
||||||
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
|
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
|
||||||
Number(kueAlarm2?.[slotIndex]) === 1 ? "text-red-500" : ""
|
Number(kueAlarm2?.[slotIndex]) === 1 ? "text-red-500" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{`RSL: ${loopDisplayValue} kOhm`}
|
{`RSL: ${Number(loopDisplayValue)
|
||||||
|
.toFixed(3)
|
||||||
|
.replace(".", ",")} kOhm`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -356,8 +399,38 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
|||||||
<div className="absolute top-0 left-[4.688rem] w-[0.188rem] h-full bg-white z-0"></div>
|
<div className="absolute top-0 left-[4.688rem] w-[0.188rem] h-full bg-white z-0"></div>
|
||||||
<div className="absolute top-[2.5rem] left-[4.688rem] w-[2.5rem] h-[0.188rem] bg-white z-0"></div>
|
<div className="absolute top-[2.5rem] left-[4.688rem] w-[2.5rem] h-[0.188rem] bg-white z-0"></div>
|
||||||
|
|
||||||
<div className="absolute bottom-[1.25rem] left-0 right-0 text-black text-[0.625rem] bg-gray-300 p-[0.063rem] text-center">
|
{/* Hidden measuring span for overflow detection (kept measurable) */}
|
||||||
{kueName?.[slotIndex] || `Modul ${slotIndex + 1}`}
|
<span
|
||||||
|
ref={measureTextRef}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: -9999,
|
||||||
|
top: -9999,
|
||||||
|
visibility: "hidden",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{moduleName48}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={nameContainerRef}
|
||||||
|
className="absolute bottom-[1.25rem] left-0 right-0 text-black text-[0.625rem] bg-gray-300 p-[0.063rem] overflow-hidden"
|
||||||
|
>
|
||||||
|
{shouldScroll && scrollFeatureEnabled ? (
|
||||||
|
<Marquee pauseOnHover gradient={false} speed={40}>
|
||||||
|
<span className="pr-8 whitespace-nowrap" title={moduleName48}>
|
||||||
|
{moduleName48}
|
||||||
|
</span>
|
||||||
|
</Marquee>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="block text-center whitespace-nowrap"
|
||||||
|
title={moduleName48}
|
||||||
|
>
|
||||||
|
{moduleName48}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute bottom-[0.063rem] right-[0.063rem] text-black text-[0.5rem]">
|
<div className="absolute bottom-[0.063rem] right-[0.063rem] text-black text-[0.5rem]">
|
||||||
|
|||||||
@@ -12,22 +12,48 @@ export default function SlotActivityOverlay({
|
|||||||
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
|
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
|
||||||
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
|
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
|
||||||
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
|
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
|
||||||
const alignmentStartedAt = useAppSelector(
|
const comparisonStartedAt = useAppSelector(
|
||||||
(s) => s.deviceEvents.alignmentStartedAt
|
(s) => s.deviceEvents.comparisonStartedAt
|
||||||
|
);
|
||||||
|
const loopStartedAtBySlot = useAppSelector(
|
||||||
|
(s) => s.deviceEvents.loopStartedAtBySlot
|
||||||
|
);
|
||||||
|
const tdrStartedAtBySlot = useAppSelector(
|
||||||
|
(s) => s.deviceEvents.tdrStartedAtBySlot
|
||||||
|
);
|
||||||
|
const comparisonStartedAtBySlot = useAppSelector(
|
||||||
|
(s) => s.deviceEvents.comparisonStartedAtBySlot
|
||||||
);
|
);
|
||||||
|
|
||||||
const loopActive = Array.isArray(ksx) && ksx[slotIndex] === 1;
|
const loopActive = Array.isArray(ksx) && ksx[slotIndex] === 1;
|
||||||
const tdrActive = Array.isArray(ksy) && ksy[slotIndex] === 1;
|
const tdrActive = Array.isArray(ksy) && ksy[slotIndex] === 1;
|
||||||
const alignActive = Array.isArray(ksz) && ksz[slotIndex] === 1;
|
const comparisonActive = Array.isArray(ksz) && ksz[slotIndex] === 1;
|
||||||
|
|
||||||
|
// Persist whenever arrays change
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
"deviceEventsTimingsV1",
|
||||||
|
JSON.stringify({
|
||||||
|
loop: loopStartedAtBySlot,
|
||||||
|
tdr: tdrStartedAtBySlot,
|
||||||
|
compare: comparisonStartedAtBySlot,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn("Failed to persist timings", e);
|
||||||
|
}
|
||||||
|
}, [loopStartedAtBySlot, tdrStartedAtBySlot, comparisonStartedAtBySlot]);
|
||||||
|
|
||||||
// Progress ticker
|
// Progress ticker
|
||||||
const [now, setNow] = useState<number>(Date.now());
|
const [now, setNow] = useState<number>(Date.now());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const any = loopActive || tdrActive || alignActive;
|
const any = loopActive || tdrActive || comparisonActive;
|
||||||
if (!any) return;
|
if (!any) return;
|
||||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [loopActive, tdrActive, alignActive]);
|
}, [loopActive, tdrActive, comparisonActive]);
|
||||||
|
|
||||||
const clamp = (v: number, min = 0, max = 1) =>
|
const clamp = (v: number, min = 0, max = 1) =>
|
||||||
Math.max(min, Math.min(max, v));
|
Math.max(min, Math.min(max, v));
|
||||||
@@ -40,10 +66,10 @@ export default function SlotActivityOverlay({
|
|||||||
|
|
||||||
// Durations
|
// Durations
|
||||||
const LOOP_MS = 2 * 60 * 1000; // ~2 min
|
const LOOP_MS = 2 * 60 * 1000; // ~2 min
|
||||||
const TDR_MS = 30 * 1000; // ~30 s
|
const TDR_MS = 110 * 1000; // ~2 min laut die Eigaben
|
||||||
const ALIGN_MS = 10 * 60 * 1000; // ~10 min
|
const ALIGN_MS = 10 * 60 * 1000; // ~10 min
|
||||||
|
|
||||||
if (!loopActive && !tdrActive && !alignActive) return null;
|
if (!loopActive && !tdrActive && !comparisonActive) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/70 backdrop-blur-sm">
|
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/70 backdrop-blur-sm">
|
||||||
@@ -56,7 +82,8 @@ export default function SlotActivityOverlay({
|
|||||||
<div>
|
<div>
|
||||||
<div className="text-[0.7rem] text-gray-800 mb-1">Schleife</div>
|
<div className="text-[0.7rem] text-gray-800 mb-1">Schleife</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const { pct } = compute(loopStartedAt, LOOP_MS);
|
const started = loopStartedAtBySlot[slotIndex] ?? loopStartedAt;
|
||||||
|
const { pct } = compute(started, LOOP_MS);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||||
@@ -77,7 +104,8 @@ export default function SlotActivityOverlay({
|
|||||||
<div>
|
<div>
|
||||||
<div className="text-[0.7rem] text-gray-800 mb-1">TDR</div>
|
<div className="text-[0.7rem] text-gray-800 mb-1">TDR</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const { pct } = compute(tdrStartedAt, TDR_MS);
|
const started = tdrStartedAtBySlot[slotIndex] ?? tdrStartedAt;
|
||||||
|
const { pct } = compute(started, TDR_MS);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||||
@@ -94,11 +122,13 @@ export default function SlotActivityOverlay({
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{alignActive && (
|
{comparisonActive && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[0.7rem] text-gray-800 mb-1">Abgleich</div>
|
<div className="text-[0.7rem] text-gray-800 mb-1">Abgleich</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const { pct } = compute(alignmentStartedAt, ALIGN_MS);
|
const started =
|
||||||
|
comparisonStartedAtBySlot[slotIndex] ?? comparisonStartedAt;
|
||||||
|
const { pct } = compute(started, ALIGN_MS);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
// components/main/kabelueberwachung/kue705FO/hooks/useLoopDisplay.ts
|
// components/main/kabelueberwachung/kue705FO/hooks/useLoopDisplay.ts
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
// Keeps and updates the loop (RSL) display value only when "Schleife" active.
|
||||||
|
// For ISO or TDR views we do not overwrite the displayed RSL value.
|
||||||
const useLoopDisplay = (
|
const useLoopDisplay = (
|
||||||
schleifenwiderstand: number,
|
rslValue: number,
|
||||||
activeButton: "Schleife" | "TDR" | "ISO"
|
activeButton: "Schleife" | "TDR" | "ISO"
|
||||||
) => {
|
) => {
|
||||||
const [loopDisplayValue, setLoopDisplayValue] =
|
const [loopDisplayValue, setLoopDisplayValue] = useState<number>(rslValue);
|
||||||
useState<number>(schleifenwiderstand);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeButton === "Schleife") {
|
if (activeButton === "Schleife") {
|
||||||
setLoopDisplayValue(schleifenwiderstand);
|
setLoopDisplayValue(rslValue);
|
||||||
}
|
}
|
||||||
// For ISO and TDR, the value is set manually via setLoopDisplayValue
|
}, [rslValue, activeButton]);
|
||||||
}, [schleifenwiderstand, activeButton]);
|
|
||||||
|
|
||||||
return { loopDisplayValue, setLoopDisplayValue };
|
return { loopDisplayValue, setLoopDisplayValue };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,63 +41,83 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
|
|||||||
window.kabelModalOpen = showModal;
|
window.kabelModalOpen = showModal;
|
||||||
}
|
}
|
||||||
}, [showModal]);
|
}, [showModal]);
|
||||||
//-----------------------------------------------------
|
|
||||||
|
|
||||||
//------------------------------------------------------
|
|
||||||
return (
|
return (
|
||||||
<ReactModal
|
<ReactModal
|
||||||
isOpen={showModal}
|
isOpen={showModal}
|
||||||
onRequestClose={onClose}
|
onRequestClose={onClose}
|
||||||
|
shouldCloseOnOverlayClick
|
||||||
ariaHideApp={false}
|
ariaHideApp={false}
|
||||||
style={{
|
style={{
|
||||||
overlay: {
|
overlay: {
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
backgroundColor: "rgba(0,0,0,0.55)",
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
|
backdropFilter: "blur(2px)",
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
top: "50%",
|
inset: "50% auto auto 50%",
|
||||||
left: "50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
transform: "translate(-50%, -50%)",
|
||||||
width: "90%",
|
width: "min(900px,92vw)",
|
||||||
maxWidth: "850px",
|
// Feste / konsistente Höhe, unabhängig vom Tab-Inhalt
|
||||||
padding: "0px",
|
// Wenn Viewport kleiner ist, begrenze auf 80vh
|
||||||
border: "none",
|
height: "min(640px, 80vh)",
|
||||||
borderRadius: "8px",
|
maxHeight: "80vh",
|
||||||
position: "relative",
|
padding: 0,
|
||||||
bottom: "auto",
|
border: "1px solid var(--color-border)",
|
||||||
right: "auto",
|
background: "var(--color-surface)",
|
||||||
|
borderRadius: "12px",
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
contentLabel={`Einstellungen KÜ ${slot + 1}`}
|
||||||
>
|
>
|
||||||
<div className="p-2 flex justify-between items-center rounded-t-md">
|
<div className="modal-header">
|
||||||
<h2 className="text-base font-bold">Einstellungen KÜ {slot + 1}</h2>
|
<h2 className="text-sm font-semibold tracking-wide text-fg">
|
||||||
<button onClick={onClose} className="text-2xl hover:text-gray-200">
|
Einstellungen KÜ {slot + 1}
|
||||||
<i className="bi bi-x-circle-fill"></i>
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="icon-btn"
|
||||||
|
aria-label="Modal schließen"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
style={{
|
||||||
|
background: "transparent",
|
||||||
|
border: "none",
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
className="bi bi-x-circle-fill"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-start bg-gray-100 space-x-2 p-2">
|
<div className="flex justify-start bg-surface-alt px-3 pt-2 gap-2 border-b border-base">
|
||||||
{[
|
{[
|
||||||
{ label: "Allgemein", key: "kue" as const },
|
{ label: "Allgemein", key: "kue" as const },
|
||||||
{ label: "TDR ", key: "tdr" as const },
|
{ label: "TDR", key: "tdr" as const },
|
||||||
{ label: "KVz", key: "kvz" as const },
|
{ label: "KVz", key: "kvz" as const },
|
||||||
{ label: "Knotenpunkte", key: "knoten" as const },
|
{ label: "Knotenpunkte", key: "knoten" as const },
|
||||||
].map(({ label, key }) => (
|
].map(({ label, key }) => {
|
||||||
<button
|
const isActive = activeTab === key;
|
||||||
key={key}
|
return (
|
||||||
onClick={() => setActiveTab(key)}
|
<button
|
||||||
className={`px-4 py-1 rounded-t font-bold text-sm ${
|
key={key}
|
||||||
activeTab === key
|
type="button"
|
||||||
? "bg-white text-littwin-blue"
|
onClick={() => setActiveTab(key)}
|
||||||
: "text-gray-500 hover:text-black"
|
className={`tab-btn ${isActive ? "tab-btn-active" : ""}`}
|
||||||
}`}
|
aria-current={isActive ? "page" : undefined}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Einheitliche Body-Höhe mit internem Scroll statt dynamischer Außenhöhe */}
|
||||||
<div className="p-4 bg-white rounded-b-md h-[20rem] laptop:h-[24rem] 2xl:h-[30rem] overflow-y-auto">
|
<div className="modal-body-scroll px-5 py-4 flex-1 text-fg overflow-y-auto">
|
||||||
{activeTab === "kue" && (
|
{activeTab === "kue" && (
|
||||||
<KueEinstellung
|
<KueEinstellung
|
||||||
slot={slot}
|
slot={slot}
|
||||||
|
|||||||
@@ -14,22 +14,22 @@ export default function MeldungenTabelle({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-auto max-h-[80vh]">
|
<div className="overflow-auto max-h-[80vh]">
|
||||||
<table className="min-w-full border">
|
<table className="min-w-full border border-base table-surface text-fg">
|
||||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10">
|
<thead className="text-left sticky top-0 z-10 bg-surface-alt/90 backdrop-blur supports-[backdrop-filter]:bg-surface-alt/70">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||||
Prio
|
Prio
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||||
Zeitstempel
|
Zeitstempel
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||||
Quelle
|
Quelle
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||||
Meldung
|
Meldung
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -38,15 +38,15 @@ export default function MeldungenTabelle({
|
|||||||
{messages.map((msg, index) => (
|
{messages.map((msg, index) => (
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
className="hover:bg-gray-100 dark:hover:bg-gray-700"
|
className="transition-colors hover:bg-surface-alt/60"
|
||||||
>
|
>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border border-base p-2 bg-surface text-fg">
|
||||||
<div
|
<div
|
||||||
className="w-4 h-4 rounded"
|
className="w-4 h-4 rounded"
|
||||||
style={{ backgroundColor: msg.c }}
|
style={{ backgroundColor: msg.c }}
|
||||||
></div>
|
></div>
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border border-base p-2 bg-surface text-fg">
|
||||||
{new Date(msg.t).toLocaleString("de-DE", {
|
{new Date(msg.t).toLocaleString("de-DE", {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
@@ -56,13 +56,13 @@ export default function MeldungenTabelle({
|
|||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
})}
|
})}
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border border-base p-2 bg-surface text-fg">
|
||||||
{msg.i}
|
{msg.i}
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border border-base p-2 bg-surface text-fg">
|
||||||
{msg.m}
|
{msg.m}
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border border-base p-2 bg-surface text-fg">
|
||||||
{msg.v}
|
{msg.v}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -70,7 +70,7 @@ export default function MeldungenTabelle({
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{messages.length === 0 && (
|
{messages.length === 0 && (
|
||||||
<div className="mt-4 text-center text-gray-500 italic">
|
<div className="mt-4 text-center text-fg-muted italic">
|
||||||
Keine Meldungen im gewählten Zeitraum vorhanden.
|
Keine Meldungen im gewählten Zeitraum vorhanden.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -60,14 +60,14 @@ export default function MeldungenView() {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => dispatch(getMessagesThunk({ fromDate, toDate }))}
|
onClick={() => dispatch(getMessagesThunk({ fromDate, toDate }))}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded h-fit"
|
className="btn-primary px-4 py-2 h-fit"
|
||||||
>
|
>
|
||||||
Anzeigen
|
Anzeigen
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Listbox value={sourceFilter} onChange={setSourceFilter}>
|
<Listbox value={sourceFilter} onChange={setSourceFilter}>
|
||||||
<div className="relative ml-6 w-64">
|
<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">
|
<Listbox.Button className="bg-[var(--color-surface)] text-[var(--color-fg)] w-full border border-base px-4 py-2 rounded text-left flex justify-between items-center">
|
||||||
<span>{sourceFilter}</span>
|
<span>{sourceFilter}</span>
|
||||||
<svg
|
<svg
|
||||||
className="w-5 h-5 text-gray-400"
|
className="w-5 h-5 text-gray-400"
|
||||||
@@ -83,19 +83,19 @@ export default function MeldungenView() {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Listbox.Button>
|
</Listbox.Button>
|
||||||
<Listbox.Options className="bg-white absolute z-50 mt-1 w-full border rounded dark:bg-gray-900">
|
<Listbox.Options className="bg-[var(--color-surface)] absolute z-50 mt-1 w-full border border-base rounded shadow-sm">
|
||||||
{sources.map((src) => (
|
{sources.map((src) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={src}
|
key={src}
|
||||||
value={src}
|
value={src}
|
||||||
className={({ selected, active, disabled }) =>
|
className={({ selected, active, disabled }) =>
|
||||||
`px-4 py-2 cursor-pointer text-gray-900 dark:text-gray-100 ${
|
`px-4 py-2 cursor-pointer text-[var(--color-fg)] transition-colors ${
|
||||||
selected
|
disabled
|
||||||
? "bg-littwin-blue text-white"
|
? "opacity-50 text-[var(--color-muted)] cursor-not-allowed"
|
||||||
|
: selected
|
||||||
|
? "bg-accent text-white"
|
||||||
: active
|
: active
|
||||||
? "bg-blue-100 dark:bg-gray-700 dark:text-white"
|
? "bg-[var(--color-surface-alt)]"
|
||||||
: disabled
|
|
||||||
? "opacity-50 text-gray-400 dark:text-gray-500 cursor-not-allowed"
|
|
||||||
: ""
|
: ""
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import { useAdminAuth } from "./hooks/useAdminAuth";
|
|||||||
const DatabaseSettings: React.FC = () => {
|
const DatabaseSettings: React.FC = () => {
|
||||||
const { isAdminLoggedIn } = useAdminAuth(true);
|
const { isAdminLoggedIn } = useAdminAuth(true);
|
||||||
return (
|
return (
|
||||||
<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">
|
<div className="p-6 bg-[var(--color-surface-alt)] max-w-5xl mr-auto rounded shadow text-[var(--color-fg)]">
|
||||||
<h2 className="text-lg font-bold mb-6">Datenbank Einstellungen</h2>
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClearMessages}
|
onClick={handleClearMessages}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded shadow "
|
className="btn-accent px-4 py-2 rounded shadow"
|
||||||
>
|
>
|
||||||
Meldungen löschen
|
Meldungen löschen
|
||||||
</button>
|
</button>
|
||||||
@@ -25,7 +25,7 @@ const DatabaseSettings: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClearLogger}
|
onClick={handleClearLogger}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded shadow "
|
className="btn-accent px-4 py-2 rounded shadow"
|
||||||
>
|
>
|
||||||
Messwerte Logger löschen
|
Messwerte Logger löschen
|
||||||
</button>
|
</button>
|
||||||
@@ -41,7 +41,7 @@ const DatabaseSettings: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClearDatabase}
|
onClick={handleClearDatabase}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded shadow "
|
className="btn-accent px-4 py-2 rounded shadow"
|
||||||
>
|
>
|
||||||
Datenbank vollständig leeren
|
Datenbank vollständig leeren
|
||||||
</button>
|
</button>
|
||||||
@@ -49,7 +49,7 @@ const DatabaseSettings: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClearConfig}
|
onClick={handleClearConfig}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded shadow "
|
className="btn-accent px-4 py-2 rounded shadow"
|
||||||
>
|
>
|
||||||
Konfiguration löschen
|
Konfiguration löschen
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -63,8 +63,11 @@ const GeneralSettings: React.FC = () => {
|
|||||||
setMac1(systemSettings.mac1 || "");
|
setMac1(systemSettings.mac1 || "");
|
||||||
}, [systemSettings]);
|
}, [systemSettings]);
|
||||||
|
|
||||||
|
const inputCls =
|
||||||
|
"border border-base focus:border-accent rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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 ">
|
<div className="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto overflow-y-auto max-h-[calc(100vh-200px)] text-[var(--color-fg)]">
|
||||||
<h2 className="text-sm md:text-md font-bold mb-2">
|
<h2 className="text-sm md:text-md font-bold mb-2">
|
||||||
Allgemeine Einstellungen
|
Allgemeine Einstellungen
|
||||||
</h2>
|
</h2>
|
||||||
@@ -74,25 +77,18 @@ const GeneralSettings: React.FC = () => {
|
|||||||
<label className="block text-xs md:text-sm font-medium">Name:</label>
|
<label className="block text-xs md:text-sm font-medium">Name:</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
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"
|
className={inputCls}
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* MAC Adresse */}
|
{/* MAC Adresse */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs md:text-sm font-medium">
|
<label className="block text-xs md:text-sm font-medium">
|
||||||
MAC Adresse 1:
|
MAC Adresse 1:
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input type="text" className={inputCls} value={mac1} disabled />
|
||||||
type="text"
|
|
||||||
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
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Systemzeit */}
|
{/* Systemzeit */}
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<label className="block text-xs md:text-sm font-medium mb-1">
|
<label className="block text-xs md:text-sm font-medium mb-1">
|
||||||
@@ -101,13 +97,13 @@ const GeneralSettings: React.FC = () => {
|
|||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
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"
|
className={inputCls}
|
||||||
value={systemUhr.replace(/\s*Uhr$/, "")}
|
value={systemUhr.replace(/\s*Uhr$/, "")}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||||
onClick={() => handleSetDateTime()}
|
onClick={() => handleSetDateTime()}
|
||||||
>
|
>
|
||||||
Systemzeit übernehmen
|
Systemzeit übernehmen
|
||||||
@@ -120,7 +116,7 @@ const GeneralSettings: React.FC = () => {
|
|||||||
<label className="block text-xs md:text-sm font-medium">IP:</label>
|
<label className="block text-xs md:text-sm font-medium">IP:</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
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"
|
className={inputCls}
|
||||||
value={ip}
|
value={ip}
|
||||||
onChange={(e) => setIp(e.target.value)}
|
onChange={(e) => setIp(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -131,7 +127,7 @@ const GeneralSettings: React.FC = () => {
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
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"
|
className={inputCls}
|
||||||
value={subnet}
|
value={subnet}
|
||||||
onChange={(e) => setSubnet(e.target.value)}
|
onChange={(e) => setSubnet(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -142,7 +138,7 @@ const GeneralSettings: React.FC = () => {
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
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"
|
className={inputCls}
|
||||||
value={gateway}
|
value={gateway}
|
||||||
onChange={(e) => setGateway(e.target.value)}
|
onChange={(e) => setGateway(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -152,7 +148,7 @@ const GeneralSettings: React.FC = () => {
|
|||||||
<div className="col-span-2 flex flex-wrap md:justify-between gap-1 mt-2">
|
<div className="col-span-2 flex flex-wrap md:justify-between gap-1 mt-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||||
onClick={() => handleReboot()}
|
onClick={() => handleReboot()}
|
||||||
>
|
>
|
||||||
Neustart CPL
|
Neustart CPL
|
||||||
@@ -160,7 +156,7 @@ const GeneralSettings: React.FC = () => {
|
|||||||
{isAdminLoggedIn && (
|
{isAdminLoggedIn && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const confirmed = window.confirm(
|
const confirmed = window.confirm(
|
||||||
"⚠️ Wollen Sie wirklich ein Firmwareupdate für alle KÜ-Module starten?"
|
"⚠️ Wollen Sie wirklich ein Firmwareupdate für alle KÜ-Module starten?"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const NTPSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto text-[var(--color-fg)]">
|
||||||
<h2 className="text-sm md:text-md font-bold mb-4">NTP Einstellungen</h2>
|
<h2 className="text-sm md:text-md font-bold mb-4">NTP Einstellungen</h2>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-3">
|
<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>
|
<label className="block text-xs font-medium">NTP Server 1</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
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"
|
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||||
value={ntp1}
|
value={ntp1}
|
||||||
onChange={(e) => setNtp1(e.target.value)}
|
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>
|
<label className="block text-xs font-medium">NTP Server 2</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
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"
|
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||||
value={ntp2}
|
value={ntp2}
|
||||||
onChange={(e) => setNtp2(e.target.value)}
|
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>
|
<label className="block text-xs font-medium">NTP Server 3</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
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"
|
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||||
value={ntp3}
|
value={ntp3}
|
||||||
onChange={(e) => setNtp3(e.target.value)}
|
onChange={(e) => setNtp3(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -65,7 +65,7 @@ const NTPSettings: React.FC = () => {
|
|||||||
<label className="block text-xs font-medium">Zeitzone</label>
|
<label className="block text-xs font-medium">Zeitzone</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
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"
|
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||||
value={ntpTimezone}
|
value={ntpTimezone}
|
||||||
onChange={(e) => setNtpTimezone(e.target.value)}
|
onChange={(e) => setNtpTimezone(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto text-[var(--color-fg)]">
|
||||||
<div className="flex justify-between items-center mb-3">
|
<div className="flex justify-between items-center mb-3">
|
||||||
<Image
|
<Image
|
||||||
src="/images/OPCUA.jpg"
|
src="/images/OPCUA.jpg"
|
||||||
@@ -44,9 +44,11 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
<label className="mr-3 font-medium text-sm">Server Status:</label>
|
<label className="mr-3 font-medium text-sm">Server Status:</label>
|
||||||
<button
|
<button
|
||||||
onClick={() => dispatch(toggleOpcUaServer())}
|
onClick={() => dispatch(toggleOpcUaServer())}
|
||||||
className={`px-3 py-1 rounded text-sm ${
|
className={`px-3 py-1 rounded text-sm font-medium transition-colors text-white ${
|
||||||
opcuaSettings.isEnabled ? "bg-littwin-blue" : "bg-gray-300"
|
opcuaSettings.isEnabled
|
||||||
} text-white`}
|
? "bg-accent hover:brightness-110"
|
||||||
|
: "bg-[var(--color-muted)] hover:bg-[var(--color-fg)]/20"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{opcuaSettings.isEnabled ? "Aktiviert" : "Deaktiviert"}
|
{opcuaSettings.isEnabled ? "Aktiviert" : "Deaktiviert"}
|
||||||
</button>
|
</button>
|
||||||
@@ -62,7 +64,7 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
<select
|
<select
|
||||||
value={opcuaSettings.encryption}
|
value={opcuaSettings.encryption}
|
||||||
onChange={(e) => dispatch(setOpcUaEncryption(e.target.value))}
|
onChange={(e) => dispatch(setOpcUaEncryption(e.target.value))}
|
||||||
className="w-full p-1 border border-gray-300 rounded-md text-sm"
|
className="w-full p-1 border border-base rounded-md text-sm bg-[var(--color-surface)] text-[var(--color-fg)]"
|
||||||
>
|
>
|
||||||
<option value="None">Keine</option>
|
<option value="None">Keine</option>
|
||||||
<option value="Basic256">Basic256</option>
|
<option value="Basic256">Basic256</option>
|
||||||
@@ -74,7 +76,7 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
{/* ✅ OPCUA Zustand */}
|
{/* ✅ OPCUA Zustand */}
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="block font-medium text-sm mb-1">OPCUA Zustand</label>
|
<label className="block font-medium text-sm mb-1">OPCUA Zustand</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">
|
<div className="p-1 border border-base rounded-md bg-[var(--color-surface)] text-sm text-[var(--color-fg)]">
|
||||||
{opcuaSettings.opcUaZustand}
|
{opcuaSettings.opcUaZustand}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,7 +87,7 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
<div className="flex">
|
<div className="flex">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
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"
|
className="flex-grow p-1 border border-base rounded-l-md text-sm bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||||
value={nodesetName}
|
value={nodesetName}
|
||||||
onChange={(e) => setNodesetName(e.target.value)}
|
onChange={(e) => setNodesetName(e.target.value)}
|
||||||
disabled={opcuaSettings.isEnabled} // Disable input when server is enabled
|
disabled={opcuaSettings.isEnabled} // Disable input when server is enabled
|
||||||
@@ -106,7 +108,7 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
<label className="block font-medium text-sm mb-1">
|
<label className="block font-medium text-sm mb-1">
|
||||||
Aktuelle OPC-Clients
|
Aktuelle OPC-Clients
|
||||||
</label>
|
</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">
|
<div className="p-1 border border-base rounded-md bg-[var(--color-surface)] text-sm text-[var(--color-fg)]">
|
||||||
{opcUaActiveClientCount}
|
{opcUaActiveClientCount}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,12 +122,12 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
{opcuaSettings.users.map((user) => (
|
{opcuaSettings.users.map((user) => (
|
||||||
<li
|
<li
|
||||||
key={user.id}
|
key={user.id}
|
||||||
className="p-1 bg-white shadow-sm rounded-md flex justify-between items-center text-sm"
|
className="p-1 bg-[var(--color-surface)] border border-base rounded-md flex justify-between items-center text-sm text-[var(--color-fg)]"
|
||||||
>
|
>
|
||||||
<span className="font-medium">{user.username}</span>
|
<span className="font-medium">{user.username}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => dispatch(removeOpcUaUser(user.id))}
|
onClick={() => dispatch(removeOpcUaUser(user.id))}
|
||||||
className="text-red-500"
|
className="text-danger hover:underline"
|
||||||
>
|
>
|
||||||
Löschen
|
Löschen
|
||||||
</button>
|
</button>
|
||||||
@@ -140,18 +142,18 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
placeholder="Benutzername"
|
placeholder="Benutzername"
|
||||||
value={newUsername}
|
value={newUsername}
|
||||||
onChange={(e) => setNewUsername(e.target.value)}
|
onChange={(e) => setNewUsername(e.target.value)}
|
||||||
className="p-1 border rounded flex-grow text-sm"
|
className="p-1 border border-base rounded flex-grow text-sm bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] focus:outline-none focus:border-accent"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Passwort"
|
placeholder="Passwort"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
className="p-1 border rounded flex-grow text-sm"
|
className="p-1 border border-base rounded flex-grow text-sm bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] focus:outline-none focus:border-accent"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleAddUser}
|
onClick={handleAddUser}
|
||||||
className="bg-littwin-blue text-white p-1 rounded text-sm"
|
className="btn-primary p-1 rounded text-sm"
|
||||||
>
|
>
|
||||||
Hinzufügen
|
Hinzufügen
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ const UserManagementSettings: React.FC = () => {
|
|||||||
() => {
|
() => {
|
||||||
setLoginSuccess(true);
|
setLoginSuccess(true);
|
||||||
setError("");
|
setError("");
|
||||||
|
// Speichere die System-Uhrzeit (Login-Zeitpunkt) im localStorage
|
||||||
|
try {
|
||||||
|
localStorage.setItem("adminLoginTime", new Date().toISOString());
|
||||||
|
} catch {
|
||||||
|
// Ignoriere Speicherfehler (z. B. in Private Mode)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
(errorMsg) => {
|
(errorMsg) => {
|
||||||
setLoginSuccess(false);
|
setLoginSuccess(false);
|
||||||
@@ -38,7 +44,7 @@ const UserManagementSettings: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto text-[var(--color-fg)]">
|
||||||
<h2 className="text-sm md:text-md font-bold mb-4">Login Admin-Bereich</h2>
|
<h2 className="text-sm md:text-md font-bold mb-4">Login Admin-Bereich</h2>
|
||||||
|
|
||||||
{/* Admin Login/Logout */}
|
{/* Admin Login/Logout */}
|
||||||
@@ -46,8 +52,15 @@ const UserManagementSettings: React.FC = () => {
|
|||||||
{isAdminLoggedIn ? (
|
{isAdminLoggedIn ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||||
onClick={logoutAdmin}
|
onClick={() => {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem("adminLoginTime");
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
logoutAdmin();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Admin abmelden
|
Admin abmelden
|
||||||
</button>
|
</button>
|
||||||
@@ -57,7 +70,7 @@ const UserManagementSettings: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Benutzername"
|
placeholder="Benutzername"
|
||||||
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"
|
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
@@ -65,14 +78,14 @@ const UserManagementSettings: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Passwort"
|
placeholder="Passwort"
|
||||||
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"
|
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||||
onClick={handleLogin}
|
onClick={handleLogin}
|
||||||
>
|
>
|
||||||
Admin anmelden
|
Admin anmelden
|
||||||
@@ -83,9 +96,9 @@ const UserManagementSettings: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loginSuccess && (
|
{loginSuccess && (
|
||||||
<p className="text-green-600 text-xs mt-2">Login erfolgreich!</p>
|
<p className="text-success text-xs mt-2">Login erfolgreich!</p>
|
||||||
)}
|
)}
|
||||||
{error && <p className="text-red-500 text-xs mt-2">{error}</p>}
|
{error && <p className="text-danger text-xs mt-2">{error}</p>}
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
// Benutzerverwaltungstabelle (kommt später)
|
// Benutzerverwaltungstabelle (kommt später)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 ">
|
<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">
|
<div className="p-6 rounded shadow-sm text-center w-80 bg-[var(--color-surface)] dark:bg-[var(--color-surface)] text-[var(--color-fg)] border border-[var(--color-border)]">
|
||||||
{/*
|
{/*
|
||||||
<h2 className="text-lg font-bold mb-4">
|
<h2 className="text-lg font-bold mb-4">
|
||||||
Firmwareupdate
|
Firmwareupdate
|
||||||
@@ -26,9 +26,9 @@ const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
|
|||||||
</h2>
|
</h2>
|
||||||
Bitte Fenster nicht schließen
|
Bitte Fenster nicht schließen
|
||||||
<h2></h2>
|
<h2></h2>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-4">
|
<div className="w-full h-4 rounded-full bg-[var(--color-surface-alt)]/80 dark:bg-[var(--color-surface-alt)]/30 border border-[var(--color-border)] overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="bg-littwin-blue h-4 rounded-full transition-all duration-100"
|
className="h-full rounded-full transition-all duration-200 bg-[var(--color-accent)]"
|
||||||
style={{ width: `${progress}%` }}
|
style={{ width: `${progress}%` }}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,15 +4,11 @@ import React, { useEffect, useRef, useState, useCallback } from "react";
|
|||||||
import { Line } from "react-chartjs-2";
|
import { Line } from "react-chartjs-2";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { RootState, useAppDispatch } from "@/redux/store";
|
import { RootState, useAppDispatch } from "@/redux/store";
|
||||||
import { Listbox } from "@headlessui/react";
|
|
||||||
import { setFullScreen } from "@/redux/slices/kabelueberwachungChartSlice";
|
import { setFullScreen } from "@/redux/slices/kabelueberwachungChartSlice";
|
||||||
import DateRangePicker from "@/components/common/DateRangePicker";
|
import { resetDateRange } from "@/redux/slices/dateRangePickerSlice";
|
||||||
import {
|
|
||||||
setVonDatum,
|
|
||||||
setBisDatum,
|
|
||||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
|
||||||
|
|
||||||
// Import Thunks
|
// Import Thunks
|
||||||
|
import SystemChartActionBar from "@/components/main/system/SystemChartActionBar";
|
||||||
import { getSystemspannung5VplusThunk } from "@/redux/thunks/getSystemspannung5VplusThunk";
|
import { getSystemspannung5VplusThunk } from "@/redux/thunks/getSystemspannung5VplusThunk";
|
||||||
import { getSystemspannung15VplusThunk } from "@/redux/thunks/getSystemspannung15VplusThunk";
|
import { getSystemspannung15VplusThunk } from "@/redux/thunks/getSystemspannung15VplusThunk";
|
||||||
import { getSystemspannung15VminusThunk } from "@/redux/thunks/getSystemspannung15VminusThunk";
|
import { getSystemspannung15VminusThunk } from "@/redux/thunks/getSystemspannung15VminusThunk";
|
||||||
@@ -31,6 +27,10 @@ import {
|
|||||||
Legend,
|
Legend,
|
||||||
Filler,
|
Filler,
|
||||||
TimeScale,
|
TimeScale,
|
||||||
|
type ChartDataset,
|
||||||
|
type ChartOptions,
|
||||||
|
type ChartData,
|
||||||
|
type Chart,
|
||||||
} from "chart.js";
|
} from "chart.js";
|
||||||
|
|
||||||
import "chartjs-adapter-date-fns";
|
import "chartjs-adapter-date-fns";
|
||||||
@@ -69,7 +69,7 @@ type ReduxDataEntry = {
|
|||||||
m?: number; // aktueller Messwert (optional, falls vorhanden)
|
m?: number; // aktueller Messwert (optional, falls vorhanden)
|
||||||
};
|
};
|
||||||
|
|
||||||
const chartOptions = {
|
const chartOptions: ChartOptions<"line"> = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
plugins: {
|
plugins: {
|
||||||
@@ -82,9 +82,11 @@ const chartOptions = {
|
|||||||
mode: "index" as const,
|
mode: "index" as const,
|
||||||
intersect: false,
|
intersect: false,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
label: function (ctx: any) {
|
label: function (ctx: any) {
|
||||||
return `Messwert: ${ctx.parsed.y}`;
|
return `Messwert: ${ctx.parsed.y}`;
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
title: function (items: any[]) {
|
title: function (items: any[]) {
|
||||||
const date = items[0].parsed.x;
|
const date = items[0].parsed.x;
|
||||||
return `Zeitpunkt: ${new Date(date).toLocaleString("de-DE")}`;
|
return `Zeitpunkt: ${new Date(date).toLocaleString("de-DE")}`;
|
||||||
@@ -144,13 +146,13 @@ export const DetailModal = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
// Stable empty reference to avoid React-Redux dev warning about selector returning new [] each call
|
// Stable empty reference to avoid React-Redux dev warning about selector returning new [] each call
|
||||||
const EMPTY_REDUX_DATA: ReadonlyArray<ReduxDataEntry> = Object.freeze([]);
|
const EMPTY_REDUX_DATA: ReadonlyArray<ReduxDataEntry> = Object.freeze([]);
|
||||||
const chartRef = useRef<any>(null);
|
const chartRef = useRef<Chart<"line"> | null>(null);
|
||||||
const [chartData, setChartData] = useState<any>({
|
const [chartData, setChartData] = useState<ChartData<"line">>({
|
||||||
datasets: [],
|
datasets: [],
|
||||||
});
|
});
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [shouldUpdateChart, setShouldUpdateChart] = useState(false);
|
const [shouldUpdateChart, setShouldUpdateChart] = useState(false);
|
||||||
const [forceUpdate, setForceUpdate] = useState(0); // Für periodische UI-Updates
|
// const [forceUpdate, setForceUpdate] = useState(0); // Für periodische UI-Updates (derzeit nicht benötigt)
|
||||||
|
|
||||||
const reduxData = useSelector((state: RootState) => {
|
const reduxData = useSelector((state: RootState) => {
|
||||||
switch (selectedKey) {
|
switch (selectedKey) {
|
||||||
@@ -160,7 +162,7 @@ export const DetailModal = ({
|
|||||||
return state.systemspannung15Vplus[zeitraum];
|
return state.systemspannung15Vplus[zeitraum];
|
||||||
case "-15V":
|
case "-15V":
|
||||||
return state.systemspannung15Vminus[zeitraum];
|
return state.systemspannung15Vminus[zeitraum];
|
||||||
case "-98V":
|
case "-96V":
|
||||||
return state.systemspannung98Vminus[zeitraum];
|
return state.systemspannung98Vminus[zeitraum];
|
||||||
case "ADC Temp":
|
case "ADC Temp":
|
||||||
return state.temperaturAdWandler[zeitraum];
|
return state.temperaturAdWandler[zeitraum];
|
||||||
@@ -196,7 +198,7 @@ export const DetailModal = ({
|
|||||||
case "-15V":
|
case "-15V":
|
||||||
dispatch(getSystemspannung15VminusThunk(zeitraum));
|
dispatch(getSystemspannung15VminusThunk(zeitraum));
|
||||||
break;
|
break;
|
||||||
case "-98V":
|
case "-96V":
|
||||||
dispatch(getSystemspannung98VminusThunk(zeitraum));
|
dispatch(getSystemspannung98VminusThunk(zeitraum));
|
||||||
break;
|
break;
|
||||||
case "ADC Temp":
|
case "ADC Temp":
|
||||||
@@ -214,8 +216,8 @@ export const DetailModal = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
setZeitraum("DIA0");
|
setZeitraum("DIA0");
|
||||||
dispatch(setVonDatum(""));
|
// Reset DateRangePicker to its defaults (it sets 30 days → today on mount)
|
||||||
dispatch(setBisDatum(""));
|
dispatch(resetDateRange());
|
||||||
|
|
||||||
// Chart-Daten zurücksetzen beim Öffnen
|
// Chart-Daten zurücksetzen beim Öffnen
|
||||||
setChartData({ datasets: [] });
|
setChartData({ datasets: [] });
|
||||||
@@ -225,11 +227,8 @@ export const DetailModal = ({
|
|||||||
// Periodische UI-Updates alle 2 Sekunden während Wartezeit
|
// Periodische UI-Updates alle 2 Sekunden während Wartezeit
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && (!chartData.datasets || chartData.datasets.length === 0)) {
|
if (isOpen && (!chartData.datasets || chartData.datasets.length === 0)) {
|
||||||
const interval = setInterval(() => {
|
// Optional: periodische Re-Renders wurden deaktiviert, da nicht mehr notwendig
|
||||||
setForceUpdate((prev) => prev + 1); // Force re-render für cursor-wait Update
|
// (kann wieder aktiviert werden falls Cursor-Animation erwünscht ist)
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}
|
}
|
||||||
}, [isOpen, chartData.datasets]);
|
}, [isOpen, chartData.datasets]);
|
||||||
|
|
||||||
@@ -260,8 +259,7 @@ export const DetailModal = ({
|
|||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
dispatch(setFullScreen(false));
|
dispatch(setFullScreen(false));
|
||||||
dispatch(setVonDatum(""));
|
dispatch(resetDateRange());
|
||||||
dispatch(setBisDatum(""));
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -279,7 +277,12 @@ export const DetailModal = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chartRef.current && selectedKey) {
|
if (chartRef.current && selectedKey) {
|
||||||
chartRef.current.options.plugins.title.text = `Verlauf ${selectedKey}`;
|
const opts = chartRef.current.options as ChartOptions<"line"> & {
|
||||||
|
plugins?: { title?: { text?: string } };
|
||||||
|
};
|
||||||
|
if (opts.plugins?.title) {
|
||||||
|
opts.plugins.title.text = `Verlauf ${selectedKey}`;
|
||||||
|
}
|
||||||
chartRef.current.update("none");
|
chartRef.current.update("none");
|
||||||
}
|
}
|
||||||
}, [selectedKey]);
|
}, [selectedKey]);
|
||||||
@@ -295,37 +298,66 @@ export const DetailModal = ({
|
|||||||
if (chartRef.current && isLoading) {
|
if (chartRef.current && isLoading) {
|
||||||
const chartInstance = chartRef.current;
|
const chartInstance = chartRef.current;
|
||||||
// Save previous callback to restore later
|
// Save previous callback to restore later
|
||||||
const prevCallback = chartInstance.options.animation?.onComplete;
|
const animation: any = chartInstance.options.animation || {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
chartInstance.options.animation = {
|
const prevCallback = animation.onComplete;
|
||||||
...chartInstance.options.animation,
|
animation.onComplete = () => {
|
||||||
onComplete: () => {
|
setIsLoading(false);
|
||||||
setIsLoading(false);
|
if (typeof prevCallback === "function") prevCallback();
|
||||||
if (typeof prevCallback === "function") prevCallback();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
chartInstance.options.animation = animation;
|
||||||
chartInstance.update();
|
chartInstance.update();
|
||||||
}
|
}
|
||||||
}, [chartData, isLoading]);
|
}, [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)
|
// Update chart data when Redux data changes (only after button click)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldUpdateChart && reduxData && reduxData.length > 0) {
|
if (shouldUpdateChart && reduxData && reduxData.length > 0) {
|
||||||
console.log("Redux data for chart:", reduxData);
|
// Filter data by selected date range (inclusive end date)
|
||||||
|
let filtered = reduxData;
|
||||||
|
try {
|
||||||
|
if (pickerVonDatum && pickerBisDatum) {
|
||||||
|
const start = new Date(`${pickerVonDatum}T00:00:00`);
|
||||||
|
const end = new Date(`${pickerBisDatum}T23:59:59`);
|
||||||
|
const s = start.getTime();
|
||||||
|
const e = end.getTime();
|
||||||
|
filtered = reduxData.filter((entry) => {
|
||||||
|
const t = new Date(entry.t).getTime();
|
||||||
|
return t >= s && t <= e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Zeitfilter konnte nicht angewendet werden:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Redux data for chart (filtered):", filtered.length);
|
||||||
|
if (!filtered.length) {
|
||||||
|
setChartData({ datasets: [] });
|
||||||
|
setShouldUpdateChart(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create datasets array for multiple lines
|
// Create datasets array for multiple lines
|
||||||
const datasets = [];
|
const datasets: ChartDataset<"line">[] = [];
|
||||||
|
|
||||||
// Check which data fields are available and create datasets accordingly
|
// Check which data fields are available and create datasets accordingly
|
||||||
const hasMinimum = reduxData.some(
|
const hasMinimum = filtered.some(
|
||||||
(entry) => entry.i !== undefined && entry.i !== null && entry.i !== 0
|
(entry) => entry.i !== undefined && entry.i !== null && entry.i !== 0
|
||||||
);
|
);
|
||||||
const hasMaximum = reduxData.some(
|
const hasMaximum = filtered.some(
|
||||||
(entry) => entry.a !== undefined && entry.a !== null
|
(entry) => entry.a !== undefined && entry.a !== null
|
||||||
);
|
);
|
||||||
const hasAverage = reduxData.some(
|
const hasAverage = filtered.some(
|
||||||
(entry) => entry.g !== undefined && entry.g !== null
|
(entry) => entry.g !== undefined && entry.g !== null
|
||||||
);
|
);
|
||||||
const hasCurrent = reduxData.some(
|
const hasCurrent = filtered.some(
|
||||||
(entry) => entry.m !== undefined && entry.m !== null
|
(entry) => entry.m !== undefined && entry.m !== null
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -333,7 +365,7 @@ export const DetailModal = ({
|
|||||||
if (hasMinimum) {
|
if (hasMinimum) {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
label: "Messwert Minimum",
|
label: "Messwert Minimum",
|
||||||
data: reduxData.map((entry) => ({
|
data: filtered.map((entry) => ({
|
||||||
x: new Date(entry.t).getTime(),
|
x: new Date(entry.t).getTime(),
|
||||||
y: entry.i || 0,
|
y: entry.i || 0,
|
||||||
})),
|
})),
|
||||||
@@ -348,7 +380,7 @@ export const DetailModal = ({
|
|||||||
if (hasMaximum) {
|
if (hasMaximum) {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
label: "Messwert Maximum",
|
label: "Messwert Maximum",
|
||||||
data: reduxData.map((entry) => ({
|
data: filtered.map((entry) => ({
|
||||||
x: new Date(entry.t).getTime(),
|
x: new Date(entry.t).getTime(),
|
||||||
y: entry.a || 0,
|
y: entry.a || 0,
|
||||||
})),
|
})),
|
||||||
@@ -364,7 +396,7 @@ export const DetailModal = ({
|
|||||||
if (hasAverage) {
|
if (hasAverage) {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
label: "Durchschnitt",
|
label: "Durchschnitt",
|
||||||
data: reduxData.map((entry) => ({
|
data: filtered.map((entry) => ({
|
||||||
x: new Date(entry.t).getTime(),
|
x: new Date(entry.t).getTime(),
|
||||||
y: entry.g || 0,
|
y: entry.g || 0,
|
||||||
})),
|
})),
|
||||||
@@ -379,7 +411,7 @@ export const DetailModal = ({
|
|||||||
if (hasCurrent) {
|
if (hasCurrent) {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
label: "Messwert",
|
label: "Messwert",
|
||||||
data: reduxData.map((entry) => ({
|
data: filtered.map((entry) => ({
|
||||||
x: new Date(entry.t).getTime(),
|
x: new Date(entry.t).getTime(),
|
||||||
y: entry.m || 0,
|
y: entry.m || 0,
|
||||||
})),
|
})),
|
||||||
@@ -391,7 +423,8 @@ export const DetailModal = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newChartData = {
|
const newChartData: ChartData<"line"> = {
|
||||||
|
labels: [],
|
||||||
datasets: datasets,
|
datasets: datasets,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -403,7 +436,13 @@ export const DetailModal = ({
|
|||||||
setChartData({ datasets: [] });
|
setChartData({ datasets: [] });
|
||||||
setShouldUpdateChart(false); // Reset flag
|
setShouldUpdateChart(false); // Reset flag
|
||||||
}
|
}
|
||||||
}, [reduxData, selectedKey, shouldUpdateChart]);
|
}, [
|
||||||
|
reduxData,
|
||||||
|
selectedKey,
|
||||||
|
shouldUpdateChart,
|
||||||
|
pickerVonDatum,
|
||||||
|
pickerBisDatum,
|
||||||
|
]);
|
||||||
|
|
||||||
if (!isOpen || !selectedKey) return null;
|
if (!isOpen || !selectedKey) return null;
|
||||||
|
|
||||||
@@ -417,19 +456,25 @@ export const DetailModal = ({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`bg-white p-6 rounded-xl overflow-auto shadow-2xl transition-all duration-300 ${
|
role="dialog"
|
||||||
isFullScreen ? "w-[95vw] h-[90vh]" : "w-[50%] h-[60%]"
|
aria-modal="true"
|
||||||
|
className={`bg-[var(--color-surface)] text-fg border border-base rounded-xl shadow-xl flex flex-col overflow-hidden transition-all duration-300 ${
|
||||||
|
isFullScreen
|
||||||
|
? "w-[90vw] h-[90vh]"
|
||||||
|
: "w-[70rem] max-w-[95vw] h-[40rem]"
|
||||||
} ${!hasChartData ? "cursor-wait" : ""}`}
|
} ${!hasChartData ? "cursor-wait" : ""}`}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
{/* Header */}
|
||||||
<h2 className="text-xl font-semibold">
|
<header className="flex items-center justify-between px-6 py-4 border-b border-base select-none bg-[var(--color-surface)]">
|
||||||
|
<h2 className="text-base font-bold tracking-wide">
|
||||||
Detailansicht: {selectedKey}
|
Detailansicht: {selectedKey}
|
||||||
</h2>
|
</h2>
|
||||||
|
<div className="flex items-center gap-3 text-lg">
|
||||||
<div className="absolute top-0 right-0 flex gap-3">
|
|
||||||
<button
|
<button
|
||||||
onClick={toggleFullScreen}
|
onClick={toggleFullScreen}
|
||||||
className="text-2xl text-gray-600 hover:text-gray-800"
|
className="icon-btn text-[1.4rem] hover:text-fg transition"
|
||||||
|
aria-label={isFullScreen ? "Vollbild verlassen" : "Vollbild"}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
className={
|
className={
|
||||||
@@ -437,86 +482,35 @@ export const DetailModal = ({
|
|||||||
? "bi bi-fullscreen-exit"
|
? "bi bi-fullscreen-exit"
|
||||||
: "bi bi-arrows-fullscreen"
|
: "bi bi-arrows-fullscreen"
|
||||||
}
|
}
|
||||||
></i>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
className="text-2xl text-gray-600 hover:text-gray-800"
|
className="icon-btn text-[1.4rem] transition"
|
||||||
|
aria-label="Modal schließen"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<i className="bi bi-x-circle-fill"></i>
|
<i className="bi bi-x-circle-fill" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
<div className="flex items-center justify-start gap-4 mb-4 flex-wrap">
|
{/* Body */}
|
||||||
<DateRangePicker />
|
<div className="flex-1 min-h-0 flex flex-col px-6 pt-4 pb-5 bg-[var(--color-surface)] overflow-hidden">
|
||||||
<label className="font-medium">Zeitraum:</label>
|
<div className="mb-3">
|
||||||
<Listbox value={zeitraum} onChange={setZeitraum}>
|
<SystemChartActionBar
|
||||||
<div className="relative w-48">
|
zeitraum={zeitraum}
|
||||||
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
|
setZeitraum={setZeitraum}
|
||||||
<span>
|
onFetchData={handleFetchData}
|
||||||
{
|
isLoading={isLoading}
|
||||||
{
|
className="mb-0"
|
||||||
DIA0: "Alle Messwerte",
|
/>
|
||||||
DIA1: "Stündlich",
|
</div>
|
||||||
DIA2: "Täglich",
|
<div className="flex-1 min-h-0 rounded-lg border border-base bg-[var(--color-surface-alt)] px-3 py-2 shadow-inner">
|
||||||
}[zeitraum]
|
<Line ref={chartRef} data={chartData} options={chartOptions} />
|
||||||
}
|
</div>
|
||||||
</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 dark:bg-gray-800 shadow max-h-60 overflow-auto text-sm border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100">
|
|
||||||
{["DIA0", "DIA1", "DIA2"].map((option) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={option}
|
|
||||||
value={option}
|
|
||||||
className={({ selected, active }) =>
|
|
||||||
`px-4 py-1 cursor-pointer ${
|
|
||||||
selected
|
|
||||||
? "bg-littwin-blue text-white"
|
|
||||||
: active
|
|
||||||
? "bg-gray-200 dark:bg-gray-700"
|
|
||||||
: ""
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
{
|
|
||||||
DIA0: "Alle Messwerte",
|
|
||||||
DIA1: "Stündlich",
|
|
||||||
DIA2: "Täglich",
|
|
||||||
}[option]
|
|
||||||
}
|
|
||||||
</Listbox.Option>
|
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
|
||||||
</div>
|
|
||||||
</Listbox>
|
|
||||||
<button
|
|
||||||
onClick={handleFetchData}
|
|
||||||
className={`px-4 py-1 bg-littwin-blue text-white rounded text-sm ${
|
|
||||||
isLoading ? "cursor-wait" : ""
|
|
||||||
}`}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? "Laden..." : "Daten laden"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-[85%] bg-white dark:bg-gray-800 rounded shadow border border-gray-200 dark:border-gray-700 p-2">
|
|
||||||
<Line ref={chartRef} data={chartData} options={chartOptions} />
|
|
||||||
</div>
|
</div>
|
||||||
|
{/* Optional Footer (currently empty, reserved for future) */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
94
components/main/system/SystemChartActionBar.tsx
Normal file
94
components/main/system/SystemChartActionBar.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
"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 border-base px-3 py-1 rounded text-left bg-[var(--color-surface-alt)] text-fg flex justify-between items-center text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-accent)]/40 transition">
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
{ DIA0: "Alle Messwerte", DIA1: "Stündlich", DIA2: "Täglich" }[
|
||||||
|
zeitraum
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 text-[var(--color-fg-muted)]"
|
||||||
|
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 border-base rounded bg-[var(--color-surface)] text-fg shadow-lg max-h-60 overflow-auto text-sm focus:outline-none">
|
||||||
|
{["DIA0", "DIA1", "DIA2"].map((option) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={option}
|
||||||
|
value={option}
|
||||||
|
className={({ selected, active }) => {
|
||||||
|
const base = "px-4 py-1 cursor-pointer text-sm";
|
||||||
|
if (selected) return `${base} bg-littwin-blue text-white`; // selected highlight
|
||||||
|
if (active)
|
||||||
|
return `${base} bg-[var(--color-surface-alt)] text-fg`;
|
||||||
|
return `${base} text-fg`;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
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;
|
||||||
@@ -28,7 +28,7 @@ export type HistoryEntry = {
|
|||||||
"+5V": number;
|
"+5V": number;
|
||||||
"+15V": number;
|
"+15V": number;
|
||||||
"-15V": number;
|
"-15V": number;
|
||||||
"-98V": number;
|
"-96V": number;
|
||||||
"ADC Temp": number;
|
"ADC Temp": number;
|
||||||
"CPU Temp": number;
|
"CPU Temp": number;
|
||||||
};
|
};
|
||||||
@@ -110,8 +110,8 @@ export const SystemCharts = ({ history }: Props) => {
|
|||||||
fill: false,
|
fill: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "-98V",
|
label: "-96V",
|
||||||
data: history.map((h) => formatValue(h["-98V"])),
|
data: history.map((h) => formatValue(h["-96V"])),
|
||||||
borderColor: "rgba(234,179,8,1)",
|
borderColor: "rgba(234,179,8,1)",
|
||||||
backgroundColor: "rgba(234,179,8,0.5)",
|
backgroundColor: "rgba(234,179,8,0.5)",
|
||||||
fill: false,
|
fill: false,
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ export const SystemOverviewGrid = ({ voltages, onOpenDetail }: Props) => {
|
|||||||
const formatValue = (value: number) => value.toFixed(2);
|
const formatValue = (value: number) => value.toFixed(2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 gap-4 mb-8">
|
<div className="grid grid-cols-2 gap-4 mb-2">
|
||||||
{Object.entries(voltages).map(([key, value]) => {
|
{Object.entries(voltages).map(([key, value]) => {
|
||||||
const unit = key.includes("Temp") ? "\u00b0C" : "V";
|
const unit = key.includes("Temp") ? "\u00b0C" : "V";
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
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"
|
className="p-4 border rounded shadow-sm bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border-[var(--color-border)] text-[var(--color-fg)] hover:bg-[var(--color-surface-alt)]/60 dark:hover:bg-[var(--color-surface-alt)]/30 transition"
|
||||||
>
|
>
|
||||||
<h2 className="font-semibold">{key}</h2>
|
<h2 className="font-semibold">{key}</h2>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -42,40 +42,37 @@ const SystemPage = () => {
|
|||||||
|
|
||||||
const handleOpenDetail = (key: string) => {
|
const handleOpenDetail = (key: string) => {
|
||||||
setSelectedKey(key);
|
setSelectedKey(key);
|
||||||
const handleOpenDetail = (key: string) => {
|
setIsModalOpen(true);
|
||||||
setSelectedKey(key);
|
switch (key) {
|
||||||
setIsModalOpen(true);
|
case "+5V":
|
||||||
switch (key) {
|
dispatch(getSystemspannung5VplusThunk(zeitraum));
|
||||||
case "+5V":
|
break;
|
||||||
dispatch(getSystemspannung5VplusThunk(zeitraum));
|
case "+15V":
|
||||||
break;
|
dispatch(getSystemspannung15VplusThunk(zeitraum));
|
||||||
case "+15V":
|
break;
|
||||||
dispatch(getSystemspannung15VplusThunk(zeitraum));
|
case "-15V":
|
||||||
break;
|
dispatch(getSystemspannung15VminusThunk(zeitraum));
|
||||||
case "-15V":
|
break;
|
||||||
dispatch(getSystemspannung15VminusThunk(zeitraum));
|
case "-96V":
|
||||||
break;
|
dispatch(getSystemspannung98VminusThunk(zeitraum));
|
||||||
case "-98V":
|
break;
|
||||||
dispatch(getSystemspannung98VminusThunk(zeitraum));
|
case "ADC Temp":
|
||||||
break;
|
dispatch(getTemperaturAdWandlerThunk(zeitraum));
|
||||||
case "ADC Temp":
|
break;
|
||||||
dispatch(getTemperaturAdWandlerThunk(zeitraum));
|
case "CPU Temp":
|
||||||
break;
|
dispatch(getTemperaturProzessorThunk(zeitraum));
|
||||||
case "CPU Temp":
|
break;
|
||||||
dispatch(getTemperaturProzessorThunk(zeitraum));
|
default:
|
||||||
break;
|
break;
|
||||||
default:
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
const handleCloseDetail = () => {
|
const handleCloseDetail = () => {
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-white dark:bg-gray-900">
|
<div className="p-4 bg-[var(--color-background)] text-[var(--color-fg)]">
|
||||||
<h1 className="text-xl font-bold mb-4">
|
<h1 className="text-xl font-bold mb-4 tracking-wide">
|
||||||
System Spannungen & Temperaturen
|
System Spannungen & Temperaturen
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -83,7 +80,7 @@ const SystemPage = () => {
|
|||||||
<div className="flex justify-center items-center h-[400px]">
|
<div className="flex justify-center items-center h-[400px]">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<ClipLoader size={50} color="#3B82F6" />
|
<ClipLoader size={50} color="#3B82F6" />
|
||||||
<p className="mt-4 text-gray-500">
|
<p className="mt-4 text-[var(--color-fg-muted)]">
|
||||||
Lade Systemdaten … bitte warten
|
Lade Systemdaten … bitte warten
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="bg-white dark:bg-gray-900 h-full">
|
<aside className="h-full bg-[var(--color-surface)] dark:bg-[var(--color-surface)] ">
|
||||||
<nav className={`h-full flex-shrink-0 mt-16 ${className || "w-48"}`}>
|
<nav className={`h-full flex-shrink-0 mt-24 ${className || "w-48"}`}>
|
||||||
{menuItems.map((item) => (
|
{menuItems.map((item) => (
|
||||||
<div key={item.name}>
|
<div key={item.name}>
|
||||||
{item.disabled ? (
|
{item.disabled ? (
|
||||||
<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">
|
<div className="block px-4 py-2 mb-4 font-bold whitespace-nowrap text-[var(--color-fg-muted)] opacity-60 cursor-not-allowed text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg">
|
||||||
{item.name}
|
{item.name}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -49,11 +49,13 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
|
|||||||
href={formatPath(item.path)}
|
href={formatPath(item.path)}
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
onClick={() => setActiveLink(item.path)}
|
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 ${
|
className={`block px-4 py-2 mb-4 font-semibold whitespace-nowrap transition duration-200 rounded-r-full pr-6 relative text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg
|
||||||
|
${
|
||||||
activeLink.startsWith(item.path)
|
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"
|
? "bg-[var(--color-accent)] text-white shadow-sm xl:mr-4 xl:w-full"
|
||||||
: "text-black hover:bg-gray-200 rounded-r-full dark:text-gray-200 dark:hover:bg-gray-800"
|
: "text-[var(--color-fg-muted)] hover:text-[var(--color-fg)] hover:bg-[var(--color-surface-alt)]/80 dark:hover:bg-[var(--color-surface-alt)]/40"
|
||||||
}`}
|
}
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
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.
41
docs/TODO.md
41
docs/TODO.md
@@ -79,3 +79,44 @@ in Rot, wenn Schleifenfehler ansteht
|
|||||||
# 15.08.2025
|
# 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
|
- [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
|
||||||
|
- [x] TODO: Systemdaten unter Detailansicht ein Verlaufsdiagramm hinzufügen mit Datumsauswahl
|
||||||
|
|
||||||
|
# Kai Schmidt:
|
||||||
|
|
||||||
|
# Folgende Erweiterung / Neuerungen:
|
||||||
|
|
||||||
|
[x] TODO: Messverlauf bei Systemwerten (Temperatur und Spannungen) mit Datumsauswahl
|
||||||
|
|
||||||
|
[x] 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.
|
||||||
|
|
||||||
|
[x] TODO: lange Modulnamen bei KÜ ermöglichen (48 Zeichen) bei Version ab V4.30. Laufschrift möglich?
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
|
|
||||||
|
# 08.09.2025
|
||||||
|
|
||||||
|
[x] TODO: Beim Ausführen einer TDR-Messung (Klick auf blauen Button in der TDR-Detailseite) erscheint keine Rückmeldung. Dort müsste ein Hinweis erscheinen “TDR-Messung wird ausgeführt und kann bis zu zwei Minuten dauern”
|
||||||
|
|
||||||
|
## 09.09.2025
|
||||||
|
|
||||||
|
[x] TODO: Admin User nach einer Zeit von einer Stunde löschen (Cookie oder Local Storrage)
|
||||||
|
|
||||||
|
[ ] TODO: Darkmode ermöglichen
|
||||||
|
|
||||||
|
[ ] TODO: Wenn im Browser Darkmode eingschaltet ist muss die Webseite erkennbar sein.
|
||||||
|
|
||||||
|
[ ] TODO: KÜ TDR-aktiviert alert entfernen
|
||||||
|
|
||||||
|
[ ] TODO: Playwright testen mit der Entwicklung
|
||||||
|
|
||||||
|
# 11.09.2025
|
||||||
|
|
||||||
|
[ ] TODO: KÜ ISO Modal -> Meldungen z-index datePicker von bis
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Meine Tabelle ist falsch. Ich werde sie anpassen. Korrekt ist:
|
|||||||
108: +15V
|
108: +15V
|
||||||
110: +5V
|
110: +5V
|
||||||
114: -15V
|
114: -15V
|
||||||
115: -98V
|
115: -96V
|
||||||
116: Temperatur AD Wandler
|
116: Temperatur AD Wandler
|
||||||
117: Temperatur Prozessor
|
117: Temperatur Prozessor
|
||||||
------------------------------------
|
------------------------------------
|
||||||
@@ -8,7 +8,7 @@ In der **Systemseite** werden die aktuellen **Versorgungsspannungen** und **Temp
|
|||||||
|
|
||||||
Die Seite zeigt:
|
Die Seite zeigt:
|
||||||
|
|
||||||
- **Live-Werte** aller Spannungen (+5V, +15V, -15V, -98V)
|
- **Live-Werte** aller Spannungen (+5V, +15V, -15V, -96V)
|
||||||
- **Temperaturen** von CPU und ADC
|
- **Temperaturen** von CPU und ADC
|
||||||
- **Verlauf** der Werte in einem **Liniendiagramm**
|
- **Verlauf** der Werte in einem **Liniendiagramm**
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ In der **Systemseite** werden die aktuellen **Versorgungsspannungen** und **Temp
|
|||||||
|
|
||||||
Die Seite zeigt:
|
Die Seite zeigt:
|
||||||
|
|
||||||
- **Live-Werte** aller Spannungen (+5V, +15V, -15V, -98V)
|
- **Live-Werte** aller Spannungen (+5V, +15V, -15V, -96V)
|
||||||
- **Temperaturen** von CPU und ADC
|
- **Temperaturen** von CPU und ADC
|
||||||
- **Verlauf** der Werte in einem **Liniendiagramm**
|
- **Verlauf** der Werte in einem **Liniendiagramm**
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
|||||||
win_da_state = [1, 1, 1, 1];
|
|
||||||
win_da_bezeichnung = ["Ausgang2", "Ausgang2", "Ausgang3", "Ausgang4"];
|
|
||||||
@@ -1,4 +1,14 @@
|
|||||||
{
|
{
|
||||||
"win_da_state": [1, 1, 1, 1],
|
"win_da_state": [
|
||||||
"win_da_bezeichnung": ["Ausgang2", "Ausgang2", "Ausgang3", "Ausgang4"]
|
1,
|
||||||
}
|
1,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"win_da_bezeichnung": [
|
||||||
|
"Ausgang1",
|
||||||
|
"Ausgang2",
|
||||||
|
"Ausgang3",
|
||||||
|
"Ausgang4"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -92,7 +92,7 @@ var win_kueLoopInterval = [
|
|||||||
//---------------------------------------------------
|
//---------------------------------------------------
|
||||||
//KÜ Modul Version soll /100 und davor V angezeigt werden z.B. 4.19V
|
//KÜ Modul Version soll /100 und davor V angezeigt werden z.B. 4.19V
|
||||||
var win_kueVersion = [
|
var win_kueVersion = [
|
||||||
420, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419,
|
420, 419, 419, 419, 419, 419, 419, 430, 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, 419, 419, 419, 419, 419, 419,
|
||||||
419, 419,
|
419, 419,
|
||||||
];
|
];
|
||||||
@@ -142,7 +142,7 @@ var win_kueName = [
|
|||||||
"Kabel 5",
|
"Kabel 5",
|
||||||
"Kabel 6",
|
"Kabel 6",
|
||||||
"Kabel 7",
|
"Kabel 7",
|
||||||
"Kabel 8",
|
"Kabel_8 in Salzgitter bei Hannover",
|
||||||
"Kabel 9",
|
"Kabel 9",
|
||||||
"Kabel 10",
|
"Kabel 10",
|
||||||
"Kabel 11",
|
"Kabel 11",
|
||||||
@@ -269,7 +269,12 @@ var tdrMeasurementEvent = [
|
|||||||
0, 0, 0, 0, 0, 0,
|
0, 0, 0, 0, 0, 0,
|
||||||
];
|
];
|
||||||
//Event Abgleich
|
//Event Abgleich
|
||||||
var alignmentEvent = [
|
var comparisonEvent = [
|
||||||
|
// renamed from alignmentEvent
|
||||||
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, 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,
|
||||||
];
|
];
|
||||||
|
// expose for browser simulation
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.comparisonEvent = comparisonEvent;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// /device-cgi-simulator/SERVICE/SystemMockData.js
|
// /device-cgi-simulator/SERVICE/systemMockData.js
|
||||||
var win_appVersion = "0.02";
|
var win_appVersion = "0.02";
|
||||||
var win_deviceName = "CPLV4 Ismail Rastede";
|
var win_deviceName = "CPLV4 Ismail Rastede";
|
||||||
var win_mac1 = "0 48 86 81 46 143";
|
var win_mac1 = "0 48 86 81 46 143";
|
||||||
@@ -5,7 +5,7 @@ var win_systemVoltTempMockData = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
var win_systemVoltTempMockData = ["+15V","+5V", "-15V","-98V","ADC Temperatur", "CPU Temperatur"];
|
var win_systemVoltTempMockData = ["+15V","+5V", "-15V","-96V","ADC Temperatur", "CPU Temperatur"];
|
||||||
|
|
||||||
ae09.value=system[0]; //+15V
|
ae09.value=system[0]; //+15V
|
||||||
ae11.value=system[1]; //5V
|
ae11.value=system[1]; //5V
|
||||||
|
|||||||
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