Compare commits

56 Commits

Author SHA1 Message Date
ISA
3edb8a053c fix(ui): DateRangePicker-Popper immer oben anzeigen 2025-09-11 14:54:32 +02:00
ISA
aedc7ccae5 test: playwright test erfolgreich 2025-09-11 14:28:27 +02:00
ISA
bdaf0ec263 Test: implement header, footer und Nav everywhere in *.test.ts 2025-09-11 11:32:09 +02:00
ISA
538f9ca487 fix: renamed -98V to -96V everywhere 2025-09-11 10:36:52 +02:00
ISA
5ef7e648eb test: Tests erfolgreich mit base url von playwright.config.ts 2025-09-11 10:11:11 +02:00
ISA
74880d9ccc test: analogInputs.test.ts and dashboard.test.ts with import components 2025-09-11 08:50:39 +02:00
ISA
7f035f0c18 chore: only *.test.ts for pages and components test 2025-09-11 08:44:15 +02:00
ISA
7fe04f55fe test: layout header, footer and sidebar 2025-09-11 08:38:00 +02:00
ISA
2ceebea533 fix: logo und tests WIP 2025-09-11 08:36:38 +02:00
ISA
95c884bc07 style: header, navigation und _app.tsx 2025-09-10 13:33:23 +02:00
ISA
05b416855b style: Header wie Footer Hintergrundfarbe 2025-09-10 13:16:07 +02:00
ISA
b4dd42c8a5 style: Header wie Footer Hintergrunffarbe 2025-09-10 13:14:32 +02:00
ISA
41910e450e style: Detailansicht Modal dark mode 2025-09-10 12:25:40 +02:00
ISA
f2a5f2083a style: AnalogInputsChartModal dark mode 2025-09-10 12:09:48 +02:00
ISA
92b712d7ce style: AnalogInputsSettingsModal dark mode 2025-09-10 11:56:40 +02:00
ISA
be9954ac29 style: DigitalOutputsModal für dark mode 2025-09-10 11:49:58 +02:00
ISA
f25063074d style: Digitale Ausgänge Switch 2025-09-10 11:45:36 +02:00
ISA
9192111b12 style: DigitalInputsModal dark mode 2025-09-10 11:30:18 +02:00
ISA
6f88a11771 style: DigitalInputsModal dark mode 2025-09-10 11:20:22 +02:00
ISA
4c45c3b9ca chore: Modale mit bi bi-x-circle-fill Icon schließen 2025-09-10 11:02:47 +02:00
ISA
484902b788 style: KVZ Modal optisch an die anderen angepasst 2025-09-10 09:58:25 +02:00
ISA
3266e8b2d5 fix: Modal jetzt wieder mit fester, konsistenter Höhe (min(640px, 80vh)) und internem Scroll statt Größenänderung pro Tab. Body-Klassen angepasst (overflow-y-auto, entfernte dynamische h-Utilities). 2025-09-10 09:44:40 +02:00
ISA
77f14313ae feat: KVZ Button für User sichtbar wenn aktiviert ist 2025-09-10 08:42:28 +02:00
ISA
f43ddccc46 feat: DateRangePicker in KVZ für Meldungen 2025-09-10 08:27:33 +02:00
ISA
28612f9cd0 feat: TDR Meldungen DateRangePicker 2025-09-10 08:13:37 +02:00
ISA
d6703c8870 style: actionbar in RSL und ISO 2025-09-10 07:35:11 +02:00
ISA
18c9c886ec WIP: von bis Zeitraum in ISO und TDR, aber TDR ist WIP 2025-09-09 14:48:03 +02:00
ISA
4c6fe0db03 Test: test file mit *.test.ts 2025-09-09 11:25:04 +02:00
ISA
6cb753c040 Tests: TDR ISO und RSL 2025-09-09 11:11:38 +02:00
ISA
52551b3243 style: TDR 2025-09-09 10:45:59 +02:00
ISA
f7d1a36e0f style: dark mode ISO, RSL und TDR 2025-09-09 10:35:34 +02:00
ISA
8580032ff9 feat: MUI test 2025-09-09 08:33:35 +02:00
ISA
001b237dd7 style: dark mode Modal KÜ Einstellungen 2025-09-08 15:38:55 +02:00
ISA
af21b180f1 WIP: dark mode Modale 2025-09-08 15:33:26 +02:00
ISA
fefff9419d WIP: dark mode Berichte 2025-09-08 15:22:06 +02:00
ISA
27c60c6742 WIP: dark mode Modale 2025-09-08 15:12:38 +02:00
ISA
c8ec763aac WIP: dark mode Baugrüppenträger sttus 2025-09-08 15:04:52 +02:00
ISA
d163df0d96 WIP: dark mode 2025-09-08 15:01:34 +02:00
ISA
12d3a17f60 fix: TDR 2 Minuten eingestellt laut eingabe 2025-09-08 13:31:32 +02:00
ISA
f3339ccafd fix: TDR 2 Minuten eingestellt laut eingaben 2025-09-08 13:30:23 +02:00
ISA
fab8a02ce9 fix: TDR 2 Minuten eingestellt laut eingaben 2025-09-08 13:28:59 +02:00
ISA
eb0585072d WIP: dark mode 2025-09-08 13:14:04 +02:00
ISA
a596422056 fix: Beim Aufruf der TDR-Detailseite erscheint im Hintergrund auf der KÜ ein Schleifenwiderstand von 0 KOhm. In der Daten Javascriptdatei steht jedoch der richtige Wert. 2025-09-08 12:09:30 +02:00
ISA
531fa93b70 fix: 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” 2025-09-08 11:48:23 +02:00
ISA
72341abb23 fix: Timer für jeder KÜ separate und nicht eine für alle, aktuell wird prozentzahl bei allen das gleiche angezeigt 2025-09-08 10:46:20 +02:00
ISA
9c218b2a1d WIP: Timer für jeder KÜ separate und nicht eine für alle, aktuell wird prozentzahl bei allen das gleiche angezeigt 2025-09-08 10:41:46 +02:00
ISA
d38d3191c5 Test: Jenkinsfile 2025-09-08 08:49:48 +02:00
ISA
112f537904 test: Jenkinsfile 2025-09-08 08:45:06 +02:00
ISA
25b6c5c3b0 fix: Jenkinsfile 2025-09-08 08:30:09 +02:00
ISA
398d13bf1b fix: Vereinfacht: Jenkinsfile 2025-09-08 08:25:00 +02:00
ISA
91b7c8d40f fix. Jenkinsfile 2025-09-08 08:21:14 +02:00
ISA
7dfef4b16a test: Jenkinsfile 2025-09-08 08:04:55 +02:00
ISA
0397f23196 fix: Jenkinsfile 2025-09-08 07:46:38 +02:00
ISA
0865d61450 Jenkinsfile auf Woodpecker-Parität umgestellt: 2025-09-08 07:40:52 +02:00
ISA
2eb8f3a255 fix: Jenkinsfile 2025-09-08 07:28:43 +02:00
ISA
22321a7ac9 Admin User nach einer Zeit von einer Stunde löschen (Cookie oder Local Storrage) , automatisch abmelden 2025-09-08 07:08:59 +02:00
114 changed files with 5417 additions and 3291 deletions

View File

@@ -6,6 +6,6 @@ NEXT_PUBLIC_USE_MOCK_BACKEND_LOOP_START=false
NEXT_PUBLIC_EXPORT_STATIC=false
NEXT_PUBLIC_USE_CGI=false
# App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.6.855
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)

View File

@@ -5,5 +5,5 @@ NEXT_PUBLIC_CPL_API_PATH=/CPL
NEXT_PUBLIC_EXPORT_STATIC=true
NEXT_PUBLIC_USE_CGI=true
# App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.6.855
NEXT_PUBLIC_APP_VERSION=1.6.913
NEXT_PUBLIC_CPL_MODE=production

File diff suppressed because it is too large Load Diff

112
Jenkinsfile vendored
View File

@@ -1,65 +1,95 @@
pipeline {
agent any
tools { nodejs 'node20' } // exakt der Name aus "Manage Jenkins → Tools"
environment {
CI = "true"
NODE_ENV = "production"
NEXT_TELEMETRY_DISABLED = "1"
PORT = "3000"
}
options {
timestamps()
}
stages {
stage('Versions') {
steps { sh 'node -v && npm -v' }
}
stage('Verify mocks') {
stage('Checkout') {
steps {
checkout scm
sh '''
set -euxo pipefail
npm ci
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)
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
'''
}
}
stage('Build & E2E (chromium)') {
environment {
CI = 'true'
NODE_ENV = 'production'
NEXT_TELEMETRY_DISABLED = '1'
PORT = '3000'
}
stage('verify-mocks') {
steps {
sh '''
set -euxo pipefail
# Install devDependencies as well (rimraf, cross-env, etc.)
env npm_config_production=false npm ci
set -eux
docker run --rm -v "$PWD":/ws -w /ws \
mcr.microsoft.com/playwright:v1.54.2-jammy bash -lc "
pwd
node -v && npm -v
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)
"
'''
}
}
# Build Next.js
npm run build
# Start local static simulator in background
npm run server:sim &
# Ensure Playwright browsers and OS deps are installed (best-effort)
npx playwright install-deps || true
npx playwright install
# 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();"
# Run tests (chromium only to match Woodpecker)
npx playwright test --project=chromium
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 'curl -d "Tests erfolgreich in Jenkins" https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35'
sh '''
docker run --rm curlimages/curl:8.9.1 \
-d "Tests erfolgreich in Jenkins" \
https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35
'''
}
failure {
sh 'curl -d "Tests fehlgeschlagen in Jenkins" https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35'
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.'
}
}
}
}
}

View File

@@ -168,11 +168,11 @@ Beispielaufruf im DEV-Modus (über UI gesteuert, nicht manuell notwendig):
### 🔌 System
- Live-Anzeige von:
- +5V, +15V, -15V, -98V Spannungen
- +5V, +15V, -15V, -96V Spannungen
- CPU- und ADC-Temperaturen
- Verlaufskurven über Zeit (Chart.js)
- 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

View File

@@ -77,6 +77,8 @@ const DateRangePicker: React.FC<DateRangePickerProps> = ({
minDate={sixMonthsAgo}
maxDate={today}
dateFormat="dd.MM.yyyy"
portalId="root-portal"
popperClassName="custom-datepicker-popper"
className={`border px-2 py-1 rounded ${inputWidthClass} ${
compact ? "text-xs" : "text-sm"
}`}
@@ -107,6 +109,8 @@ const DateRangePicker: React.FC<DateRangePickerProps> = ({
minDate={sixMonthsAgo}
maxDate={today}
dateFormat="dd.MM.yyyy"
portalId="root-portal"
popperClassName="custom-datepicker-popper"
className={`border px-2 py-1 rounded ${inputWidthClass} ${
compact ? "text-xs" : "text-sm"
}`}

View File

@@ -1,13 +1,16 @@
"use client";
import React from "react";
import { useAppDispatch } from "@/redux/store";
import { setEvents } from "@/redux/slices/deviceEventsSlice";
import {
setEvents,
initPersistedTimings,
} from "@/redux/slices/deviceEventsSlice";
declare global {
interface Window {
loopMeasurementEvent?: number[];
tdrMeasurementEvent?: number[];
alignmentEvent?: number[];
comparisonEvent?: number[]; // renamed from alignmentEvent
}
}
@@ -18,6 +21,25 @@ export default function DeviceEventsBridge() {
React.useEffect(() => {
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 ksx = Array.isArray(window.loopMeasurementEvent)
? window.loopMeasurementEvent
@@ -25,8 +47,8 @@ export default function DeviceEventsBridge() {
const ksy = Array.isArray(window.tdrMeasurementEvent)
? window.tdrMeasurementEvent
: undefined;
const ksz = Array.isArray(window.alignmentEvent)
? window.alignmentEvent
const ksz = Array.isArray(window.comparisonEvent)
? window.comparisonEvent
: undefined;
// Build a stable signature of first 32 values per array
const to32 = (a?: number[]) => {

View File

@@ -5,14 +5,14 @@ import { useAppSelector } from "@/redux/store";
export default function GlobalActivityOverlay() {
const anyLoop = useAppSelector((s) => s.deviceEvents.anyLoopActive);
const anyTdr = useAppSelector((s) => s.deviceEvents.anyTdrActive);
const anyAlign = useAppSelector((s) => s.deviceEvents.anyAlignmentActive);
const anyCompare = useAppSelector((s) => s.deviceEvents.anyComparisonActive);
const ksx = useAppSelector((s) => s.deviceEvents.ksx);
const ksy = useAppSelector((s) => s.deviceEvents.ksy);
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
const alignmentStartedAt = useAppSelector(
(s) => s.deviceEvents.alignmentStartedAt
const comparisonStartedAt = useAppSelector(
(s) => s.deviceEvents.comparisonStartedAt
);
const fmt = (arr: number[]) =>
@@ -24,13 +24,13 @@ export default function GlobalActivityOverlay() {
// Simple 1s ticker so progress bars advance while overlay is shown
const [now, setNow] = useState<number>(Date.now());
useEffect(() => {
const active = anyLoop || anyTdr || anyAlign;
const active = anyLoop || anyTdr || anyCompare;
if (!active) return;
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, [anyLoop, anyTdr, anyAlign]);
}, [anyLoop, anyTdr, anyCompare]);
const active = anyLoop || anyTdr || anyAlign;
const active = anyLoop || anyTdr || anyCompare;
if (!active) return null;
const clamp = (v: number, min = 0, max = 1) =>
@@ -102,13 +102,13 @@ export default function GlobalActivityOverlay() {
</div>
)}
{anyAlign && (
{anyCompare && (
<div>
<div className="text-sm text-gray-800 mb-1">
Abgleich läuft (: {fmt(ksz)}) kann bis zu 10 Minuten dauern
Comparison läuft (: {fmt(ksz)}) kann bis zu 10 Minuten dauern
</div>
{(() => {
const { pct } = compute(alignmentStartedAt, ALIGN_MS);
const { pct } = compute(comparisonStartedAt, ALIGN_MS);
return (
<div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">

View File

@@ -55,45 +55,57 @@ function Footer() {
}, [showSlider]);
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="flex flex-row space-x-2">
<div className="flex flex-row space-x-2 items-center">
<Icon
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 className="flex flex-row space-x-2">
<Icon icon="charm:phone" className="text-xl text-blue-400" />
<p className="text-sm">Telefon: 04402 972577-0</p>
<div className="flex flex-row space-x-2 items-center">
<Icon icon="charm:phone" className="text-xl text-accent" />
<p className="text-sm text-fg-muted">Telefon: 04402 972577-0</p>
</div>
<div className="flex flex-row space-x-2">
<Icon icon="mdi:email-outline" className="text-xl text-blue-400" />
<p className="text-sm">kontakt@littwin-systemtechnik.de</p>
<div className="flex flex-row space-x-2 items-center">
<Icon icon="mdi:email-outline" className="text-xl text-accent" />
<p className="text-sm text-fg-muted">
kontakt@littwin-systemtechnik.de
</p>
</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)}
>
<Icon icon="bi:book" className="text-xl text-blue-400" />
<p className="text-sm">Handbücher</p>
<Icon
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
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"
}`}
>
<div className="p-4 flex justify-between items-center border-b">
<h3 className="text-lg font-semibold">PDF Handbücher</h3>
<div className="p-4 flex justify-between items-center border-b border-base">
<h3 className="text-base font-semibold text-[var(--color-fg)]">
PDF Handbücher
</h3>
<button
className="text-gray-500 hover:text-gray-800"
className="text-[var(--color-muted)] hover:text-[var(--color-fg)] transition"
onClick={() => setShowSlider(false)}
aria-label="Schließen"
>
<Icon icon="carbon:close" className="text-2xl" />
<Icon icon="carbon:close" className="text-xl" />
</button>
</div>
@@ -102,7 +114,7 @@ function Footer() {
{pdfFiles.map((fileName) => (
<li
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)}
>
{fileName}

View File

@@ -1,5 +1,5 @@
"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 Image from "next/image";
import { useRouter } from "next/router";
@@ -15,16 +15,18 @@ function Header() {
const router = useRouter();
const [showSettingsModal, setShowSettingsModal] = useState(false);
const [isAdminLoggedIn, setIsAdminLoggedIn] = useState(false);
const autoLogoutTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Removed duplicate declaration of deviceName
const handleCloseSettingsModal = () => setShowSettingsModal(false);
const handleLogout = () => {
const handleLogout = useCallback(() => {
sessionStorage.removeItem("token"); // Token entfernen
localStorage.setItem("isAdminLoggedIn", "false"); // Admin-Status entfernen
localStorage.removeItem("adminLoginTime"); // Login-Zeitpunkt entfernen
setIsAdminLoggedIn(false); // Zustand sofort aktualisieren
router.push("/offline.html"); // Weiterleitung
};
}, [router]);
useEffect(() => {
// Initialer Check beim Laden der Komponente
@@ -43,6 +45,56 @@ function Header() {
clearInterval(interval); // Intervall stoppen, wenn die Komponente entladen wird
};
}, [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>();
@@ -57,27 +109,49 @@ function Header() {
}, [deviceName, dispatch]);
//----------------------------------------------------------------
// Dark/Light Mode Toggle
// Dark/Light Mode Toggle (persisted)
const [isDark, setIsDark] = useState(false);
// Initial state from html class / localStorage (set by _document script before hydration)
useEffect(() => {
if (typeof window !== "undefined") {
const html = document.documentElement;
if (isDark) {
html.classList.add("dark");
} else {
html.classList.remove("dark");
}
if (typeof window === "undefined") return;
const html = document.documentElement;
const stored = localStorage.getItem("theme");
const active = stored ? stored === "dark" : html.classList.contains("dark");
setIsDark(active);
}, []);
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]);
// 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 (
<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
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:
top-[90%] sm:top-[90%] md:top-[90%] lg:top-[90%] xl:top-[90%]"
style={{
height: "10vh", // Dynamische Höhe des Containers
height: "12vh", // Erhöhte Höhe des Containers für größeres Logo
width: "auto",
aspectRatio: "1", // Beibehaltung des Seitenverhältnisses
}}
@@ -86,7 +160,7 @@ function Header() {
src="/images/Logo.png"
alt="Logo"
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"
priority={false}
/>
@@ -102,10 +176,10 @@ function Header() {
priority
/>
<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
</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}
</p>
</div>
@@ -117,7 +191,7 @@ function Header() {
<button
aria-label={isDark ? "Light Mode" : "Dark Mode"}
onClick={() => setIsDark((d) => !d)}
className="rounded-full p-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition"
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"}
>
{isDark ? (
@@ -139,7 +213,8 @@ function Header() {
<div className="flex items-center justify-end w-1/4 space-x-1">
<button
onClick={handleLogout}
className="bg-littwin-blue text-white px-4 py-2 rounded"
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
</button>
@@ -149,7 +224,7 @@ function Header() {
{/* Warnhinweis, wenn der Admin angemeldet ist */}
{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
</div>
)}

View File

@@ -367,9 +367,9 @@ export default function AnalogInputsChart({
<div
className={`flex flex-col gap-2 h-full ${loading ? "cursor-wait" : ""}`}
>
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-2">
<div className="flex justify-between items-center p-2 rounded-lg space-x-2 bg-[var(--color-surface-alt)] border border-base">
<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 ?? ""}
</Dialog.Title>
</div>
@@ -385,7 +385,7 @@ export default function AnalogInputsChart({
{/* ✅ Zeitraum-Auswahl (Listbox nur lokal) */}
<Listbox value={localZeitraum} onChange={setLocalZeitraum}>
<div className="relative w-48">
<Listbox.Button className="w-full border px-3 py-1 rounded bg-white flex justify-between items-center text-sm">
<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>
{localZeitraum === "DIA0"
? "Alle Messwerte"
@@ -393,14 +393,14 @@ export default function AnalogInputsChart({
? "Stündlich"
: "Täglich"}
</span>
<i className="bi bi-chevron-down text-gray-400" />
<i className="bi bi-chevron-down text-[var(--color-muted)]" />
</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) => (
<Listbox.Option
key={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"
? "Alle Messwerte"
@@ -416,7 +416,7 @@ export default function AnalogInputsChart({
{/* ✅ Button: lädt die Daten & aktualisiert Redux */}
<button
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
</button>
@@ -427,11 +427,9 @@ export default function AnalogInputsChart({
{/* Chart-Anzeige */}
<div className="flex-1 min-h-0 w-full">
{!selectedAnalogInput?.id ? (
<div className="flex items-center justify-center h-full text-gray-500 text-lg gap-2">
<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" />
<span>
Bitte wählen Sie einen Eingang aus, um die Messkurve anzuzeigen
</span>
<span>Bitte Eingang auswählen</span>
</div>
) : (
<Line

View File

@@ -35,82 +35,50 @@ export default function AnalogInputsChartModal({
<div className="fixed inset-0 bg-black/50" aria-hidden="true" />
{/* Centered panel */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="relative">
<Dialog.Panel className="relative outline-none">
<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={{
width: isFullscreen ? "90vw" : "70rem",
height: isFullscreen ? "90vh" : "35rem",
padding: "1rem",
transition: "all 0.3s ease-in-out",
display: "flex",
flexDirection: "column",
height: isFullscreen ? "90vh" : "38rem",
}}
>
{/* Controls top-right (fullscreen + close) */}
<div
style={{
position: "absolute",
top: "0.625rem",
right: "0.625rem",
display: "flex",
gap: "0.75rem",
}}
>
<button
onClick={() => setIsFullscreen((v) => !v)}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
title={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
aria-label={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
>
<i
className={
isFullscreen
? "bi bi-fullscreen-exit"
: "bi bi-arrows-fullscreen"
}
></i>
</button>
<button
onClick={() => dispatch(setIsChartModalOpen(false))}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
title="Schließen"
aria-label="Modal schließen"
>
<i className="bi bi-x-circle-fill"></i>
</button>
</div>
{/* Title row (align like IsoChartView) */}
<div className="flex justify-between items-center mb-2 pr-24">
<Dialog.Title className="text-lg font-semibold text-gray-700">
{/* Header */}
<header className="flex items-center justify-between px-6 py-4 border-b border-base select-none">
<h2 className="text-base font-bold">
Messkurve Messwerteingang {selectedId ?? ""}
</Dialog.Title>
</div>
{/* Chart container (structure similar to IsoChartView) */}
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
height: "90%",
}}
>
{/* Optional: place an action bar here if needed */}
<div style={{ flex: 1, height: "90%" }}>
<AnalogInputsChart loading={loading} setLoading={setLoading} />
</h2>
<div className="flex items-center gap-3">
<button
onClick={() => setIsFullscreen((v) => !v)}
className="icon-btn text-xl"
aria-label={isFullscreen ? "Vollbild verlassen" : "Vollbild"}
type="button"
title={isFullscreen ? "Vollbild verlassen" : "Vollbild"}
>
<i
className={
isFullscreen
? "bi bi-fullscreen-exit"
: "bi bi-arrows-fullscreen"
}
/>
</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>
</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>
</Dialog.Panel>

View File

@@ -108,125 +108,118 @@ export default function AnalogInputsSettingsModal() {
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="bg-white rounded-lg shadow-lg p-6 w-1/2 max-w-lg">
<div className="mb-4 border-b pb-2 flex justify-between items-center">
<h2 className="text-base font-bold">
<div className="bg-[var(--color-surface)] border border-base rounded-xl shadow-xl w-[32rem] max-w-full p-0 flex flex-col">
<header className="modal-header flex items-center justify-between px-6 py-4 border-b border-base">
<h2 className="text-base font-bold text-fg">
Einstellungen Messwerteingang {selectedInput.id}
</h2>
<button
onClick={() => dispatch(setIsSettingsModalOpen(false))}
className="text-2xl hover:text-gray-400"
className="icon-btn text-2xl"
aria-label="Modal schließen"
type="button"
>
<i className="bi bi-x-circle-fill"></i>
<i className="bi bi-x-circle-fill" />
</button>
</div>
{/* Bezeichnung */}
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
<span className="font-normal">Bezeichnung:</span>
<input
type="text"
className="w-full border rounded px-3 py-1 mb-4"
value={label}
onChange={(e) => setLabel(e.target.value)}
/>
</div>
{/* Offset */}
<div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4">
<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">
</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">
<span className="font-normal text-fg-secondary">Bezeichnung:</span>
<input
type="text"
className="border border-base rounded px-2 py-1 w-full bg-[var(--color-surface-alt)] text-fg"
value={label}
onChange={(e) => setLabel(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">Offset:</span>
<input
type="number"
className="border rounded px-2 py-1 pr-20 w-full text-right"
value={loggerInterval}
onChange={(e) => setLoggerInterval(e.target.value)}
step="0.001"
className="border border-base rounded px-2 py-1 w-full text-right bg-[var(--color-surface-alt)] text-fg"
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">
Minuten
</div>
<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>
<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>
{/* Speichern-Button */}
<div className="flex justify-end gap-2 mt-6">
<footer className="px-6 py-4 border-t border-base flex justify-end">
<button
onClick={handleSave}
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"}
</button>
</div>
</footer>
</div>
</div>
);

View File

@@ -37,8 +37,8 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
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 laptop:p-1 xl:p-1 ${
loading ? "cursor-wait" : ""
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 opacity-70" : ""
}`}
>
<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" : ""
}`}
>
<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>
<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
</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
</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
</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
</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
</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
</th>
</tr>
@@ -92,12 +92,12 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
loading
? "cursor-wait"
: analogInput.id === activeId
? "bg-blue-100 dark:bg-gray-700 dark:text-white"
: "hover:bg-gray-100 dark:hover:bg-gray-800"
? "bg-[var(--color-accent-soft)] dark:bg-[var(--color-surface-alt)]/60 text-[var(--color-fg)]"
: "hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30"
}`}
>
<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)}
>
<div className="flex items-center gap-1 ">
@@ -109,7 +109,7 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
</div>
</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)}
>
{typeof analogInput.value === "number"
@@ -118,19 +118,19 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
</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)}
>
{analogInput.unit || "-"}
</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)}
>
{analogInput.label || "----"}
</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
onClick={() => {
handleSelect(analogInput.id!, analogInput);
@@ -141,7 +141,7 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
<Icon icon={settingsIcon} className="text-xl" />
</button>
</td>
<td className="border p-2 text-center bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<td className="border p-2 text-center bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
<button
onClick={() => {
handleSelect(analogInput.id!, analogInput);

View File

@@ -36,8 +36,8 @@ function AnalogInputsView() {
}`}
>
<div className="grid grid-cols-1 gap-4 justify-items-start">
<div className="bg-white dark:bg-gray-900 rounded-lg p-4 max-w-3xl text-gray-900 dark:text-gray-100">
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
<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">
<h2 className="text-xl font-semibold mb-4 text-[var(--color-fg)] tracking-wide">
Messwerteingänge
</h2>
<AnalogInputsTable loading={loading} />

View File

@@ -43,7 +43,7 @@ const Baugruppentraeger: React.FC = () => {
baugruppen.push(
<div
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">
{slots.map((version, index) => {

View File

@@ -25,7 +25,7 @@ const DashboardView: React.FC = () => {
}, [dispatch]);
//-------------------------------------
return (
<div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)] laptop:h-[calc(100vh-10vh-5vh)] xl:h-[calc(100vh-10vh-6vh)] laptop:gap-0 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<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 */}
<div className="flex justify-between items-center w-full lg:w-2/3">
<div className="flex justify-between gap-1">
@@ -33,7 +33,7 @@ const DashboardView: React.FC = () => {
icon="ri:calendar-schedule-line"
className="text-littwin-blue text-4xl xl:text-2xl"
/>
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100 xl:text-base">
<h1 className="text-xl font-bold xl:text-base text-[var(--color-fg)] tracking-wide">
Letzten 20 Meldungen
</h1>
</div>

View File

@@ -48,22 +48,22 @@ export default function Last20MessagesTable({ className }: Props) {
return (
<div className={`flex flex-col gap-3 p-4 ${className}`}>
<div className="overflow-auto max-h-[80vh]">
<table className="min-w-full border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10">
<table className="min-w-full border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
<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>
<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
</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
</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
</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
</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
</th>
</tr>
@@ -72,24 +72,24 @@ export default function Last20MessagesTable({ className }: Props) {
{filteredMessages.slice(0, 20).map((msg, index) => (
<tr
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
className="w-4 h-4 rounded"
style={{ backgroundColor: msg.c }}
></div>
</td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
{msg.t}
</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}
</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}
</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}
</td>
</tr>
@@ -97,7 +97,7 @@ export default function Last20MessagesTable({ className }: Props) {
</tbody>
</table>
{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.
</div>
)}

View File

@@ -38,7 +38,7 @@ const NetworkInfo: React.FC = () => {
return (
<div className="w-full flex-direction: row flex">
<div className=" flex-grow flex justify-between items-center mt-1 bg-white 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">
<Image
src="/images/IP-icon.svg"
@@ -49,12 +49,8 @@ const NetworkInfo: React.FC = () => {
priority
/>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">
IP-Adresse
</p>
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
{ip}
</p>
<p className="text-xs text-[var(--color-fg-muted)]">IP-Adresse</p>
<p className="text-sm font-medium text-[var(--color-fg)]">{ip}</p>
</div>
</div>
@@ -68,10 +64,8 @@ const NetworkInfo: React.FC = () => {
priority
/>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Subnet-Maske
</p>
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
<p className="text-xs text-[var(--color-fg-muted)]">Subnet-Maske</p>
<p className="text-sm font-medium text-[var(--color-fg)]">
{subnet}
</p>
</div>
@@ -87,8 +81,8 @@ const NetworkInfo: React.FC = () => {
priority
/>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Gateway</p>
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
<p className="text-xs text-[var(--color-fg-muted)]">Gateway</p>
<p className="text-sm font-medium text-[var(--color-fg)]">
{gateway}
</p>
</div>
@@ -97,8 +91,8 @@ const NetworkInfo: React.FC = () => {
<div className="flex items-center space-x-4">
<div className="text-xs font-bold text-littwin-blue">OPC-UA</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Status</p>
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
<p className="text-xs text-[var(--color-fg-muted)]">Status</p>
<p className="text-sm font-medium text-[var(--color-fg)]">
{opcUaZustand}
</p>
</div>

View File

@@ -4,9 +4,7 @@ import { Icon } from "@iconify/react";
import { useSelector } from "react-redux";
import { RootState } from "../../../redux/store";
type VersionInfoProps = {
className?: string;
};
type VersionInfoProps = { className?: string };
const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
const appVersion =
@@ -17,26 +15,26 @@ const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
);
return (
<div
className={`bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 w-full laptop:p-2 ${className}`}
>
<h2 className="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">
<div className={`card w-full p-3 laptop:p-2 ${className}`}>
<h2 className="text-base font-semibold mb-2 text-[var(--color-fg)]">
Versionsinformationen
</h2>
<div className="flex flex-row p-2 space-x-2">
<Icon icon="bx:code-block" className="text-xl text-blue-400" />
<p className="text-sm text-gray-600 dark:text-gray-300">
Applikationsversion: {appVersion}
</p>
</div>
<div className="flex flex-row p-2 space-x-2">
<Icon icon="mdi:web" className="text-xl text-blue-400" />
<p className="text-sm text-gray-600 dark:text-gray-300">
Webversion: {webVersion}
</p>
</div>
<ul className="space-y-1">
<li className="flex items-start gap-2">
<Icon icon="bx:code-block" className="text-xl text-accent" />
<p className="text-sm text-fg-muted">
Applikationsversion:{" "}
<span className="text-[var(--color-fg)]">{appVersion}</span>
</p>
</li>
<li className="flex items-start gap-2">
<Icon icon="mdi:web" className="text-xl text-accent" />
<p className="text-sm text-fg-muted">
Webversion:{" "}
<span className="text-[var(--color-fg)]">{webVersion}</span>
</p>
</li>
</ul>
</div>
);
};

View File

@@ -30,7 +30,7 @@ export default function DigitalInputsWidget({
//console.log("DigitalInputs", inputs);
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">
<Icon
icon={inputIcon}
@@ -38,19 +38,19 @@ export default function DigitalInputsWidget({
/>
Meldungseingänge {inputRange.start + 1} {inputRange.end}
</h2>
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white dark:bg-gray-900">
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b">
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-[var(--color-surface)]">
<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>
<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
</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
</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
</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
</th>
</tr>
@@ -59,9 +59,9 @@ export default function DigitalInputsWidget({
{inputs.map((input) => (
<tr
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 ">
<Icon
icon={loginIcon}
@@ -70,7 +70,7 @@ export default function DigitalInputsWidget({
{input.id}
</div>
</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 ? (
<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 ">
@@ -91,10 +91,10 @@ export default function DigitalInputsWidget({
</div>
)}
</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}
</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={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"

View File

@@ -182,132 +182,133 @@ export default function InputModal({
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="bg-white rounded-lg shadow-lg p-6 w-1/2 max-w-lg">
<div className="mb-4 border-b pb-2 flex justify-between items-center">
<h2 className="text-base font-bold">
<div className="bg-[var(--color-surface)] border border-base rounded-xl shadow-xl w-[32rem] max-w-full p-0 flex flex-col">
<header className="modal-header flex items-center justify-between px-6 py-4 border-b border-base">
<h2 className="text-base font-bold text-fg">
Einstellungen Meldungseingang {selectedInput.id}
</h2>
<button
onClick={handleClose}
className="text-2xl hover:text-gray-400"
className="icon-btn text-2xl"
aria-label="Modal schließen"
type="button"
>
<i className="bi bi-x-circle-fill"></i>
<i className="bi bi-x-circle-fill" />
</button>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
<div>
<span className="font-normal">Bezeichnung:</span>
</div>
<div>
<input
type="text"
value={label}
onChange={(e) => setLabel(e.target.value)}
className="border border-gray-300 rounded px-2 py-1 w-full"
maxLength={32}
/>
</div>
<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"
}`}
</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>
<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"
maxLength={32}
/>
</button>
<span>{invertiert ? "Ein" : "Aus"}</span>
</div>
<div>
<span className="font-normal">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-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"
</div>
<div>
<span className="font-normal text-fg-secondary">
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 border border-base transition-colors duration-200 ${
invertiert ? "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 ${
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>{eingangOffline ? "Ein" : "Aus"}</span>
<span className="absolute right-2 top-1/2 transform -translate-y-1/2 text-muted text-sm">
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 className="mt-6 flex justify-end gap-2">
<footer className="px-6 py-4 border-t border-base flex justify-end">
<button
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
</button>
</div>
</footer>
</div>
</div>
);

View File

@@ -94,44 +94,48 @@ export default function DigitalOutputsModal({
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="bg-white rounded-lg shadow-lg p-6 w-1/2 max-w-lg">
<div className="mb-4 border-b pb-2 flex justify-between items-center">
<h2 className="text-base font-bold">
<div className="bg-[var(--color-surface)] border border-base rounded-xl shadow-xl w-[32rem] max-w-full p-0 flex flex-col">
<header className="modal-header flex items-center justify-between px-6 py-4 border-b border-base">
<h2 className="text-base font-bold text-fg">
Einstellungen Schaltausgang {selectedOutput.id}
</h2>
<button
onClick={closeOutputModal}
className="text-2xl hover:text-gray-400"
className="icon-btn text-2xl"
aria-label="Modal schließen"
type="button"
>
<i className="bi bi-x-circle-fill"></i>
<i className="bi bi-x-circle-fill" />
</button>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
<div>
<span className="font-normal">Bezeichnung:</span>
</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>
<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>
<input
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"
/>
{errorMsg && <p className="text-red-600 text-sm mb-2">{errorMsg}</p>}
</div>
{errorMsg && <p className="text-red-600 text-sm mb-2">{errorMsg}</p>}
<div className="flex justify-end gap-2 mt-6">
<footer className="px-6 py-4 border-t border-base flex justify-end">
<button
onClick={handleSave}
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"}
</button>
</div>
</footer>
</div>
</div>
);

View File

@@ -66,7 +66,7 @@ export default function DigitalOutputsWidget({
};
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">
<Icon
icon={outputIcon}
@@ -74,19 +74,19 @@ export default function DigitalOutputsWidget({
/>
Schaltausgänge
</h2>
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white dark:bg-gray-900 rounded-lg">
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b">
<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-[var(--color-surface)] text-[var(--color-fg)] border-b border-base">
<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
</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
</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
</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
</th>
</tr>
@@ -95,33 +95,39 @@ export default function DigitalOutputsWidget({
{digitalOutputs.map((output) => (
<tr
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={outputIcon}
className="text-gray-600 mr-1 text-base"
className="text-[var(--color-muted)] mr-1 text-base"
/>
{output.id}
</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}
</td>
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<Icon
icon={switchIcon}
className={`cursor-pointer text-base transition ${
output.status
? "text-littwin-blue"
: "text-gray-500 scale-x-[-1]"
} dark:hover:text-littwin-blue`}
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
<button
type="button"
role="switch"
aria-checked={output.status}
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 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={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)}
/>
</td>

View File

@@ -119,19 +119,23 @@ function KabelueberwachungView() {
return (
<div>
<div className="mb-4">
{[1, 2, 3, 4].map((rack) => (
<button
key={rack}
onClick={() => changeRack(rack)}
className={`mr-2 ${
Number(activeRack) === Number(rack)
? "bg-littwin-blue text-white p-1 rounded-sm"
: "bg-gray-300 p-1 text-sm"
}`}
>
Rack {rack}
</button>
))}
{[1, 2, 3, 4].map((rack) => {
const isActive = Number(activeRack) === Number(rack);
return (
<button
key={rack}
onClick={() => changeRack(rack)}
aria-pressed={isActive}
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"
: "btn-muted text-fg opacity-90 hover:opacity-100"
}`}
>
Rack {rack}
</button>
);
})}
</div>
<div className="flex flex-row space-x-8 xl:space-x-0 2xl:space-x-8 qhd:space-x-16 ml-[5%] mt-[5%]">
{(

View File

@@ -41,7 +41,6 @@ export const useIsoChartLoader = () => {
)};${formatDate(bisDatum)};${slotNumber};${type};`;
}
console.log("API URL:", url);
return url;
};
@@ -143,19 +142,10 @@ export const useIsoDataLoader = () => {
const waitTime = Math.max(0, MIN_LOADING_TIME_MS - elapsedTime);
await new Promise((resolve) => setTimeout(resolve, waitTime));
console.log("▶️ Automatisches Laden - Isolationswiderstand-Daten für:");
console.log(" Slot:", slotNumber);
console.log(" Modus:", selectedMode);
console.log(" Von:", vonDatum);
console.log(" Bis:", bisDatum);
if (Array.isArray(jsonData) && jsonData.length > 0) {
dispatch(setIsoMeasurementCurveChartData(jsonData));
dispatch(setChartOpen(true));
} else {
console.log(
"⚠️ Keine Messdaten im gewählten Zeitraum gefunden (automatisches Laden)"
);
dispatch(setIsoMeasurementCurveChartData([]));
dispatch(setChartOpen(false));
}
@@ -170,8 +160,6 @@ export const useIsoDataLoader = () => {
};
//-----------------------------------------------------------------------------------IsoChartActionBar
// ...existing code...
const IsoChartActionBar = forwardRef((_props, ref) => {
IsoChartActionBar.displayName = "IsoChartActionBar";
const dispatch = useAppDispatch();
@@ -281,106 +269,90 @@ const IsoChartActionBar = forwardRef((_props, ref) => {
}
};
useImperativeHandle(ref, () => ({
handleFetchData,
}));
useImperativeHandle(ref, () => ({ handleFetchData }));
const isMeldungen = chartTitle === "Meldungen";
return (
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-2">
<div className="flex items-center">
<label className="text-sm font-semibold">
{slotNumber !== null ? slotNumber + 1 : "-"}
</label>
<div className="toolbar w-full justify-between flex-wrap">
<div className="flex items-center gap-2 pr-4">
<span className=" font-semibold uppercase tracking-wide text-muted">
</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 className="flex items-center space-x-2">
{/* DateRangePicker für beide Ansichten sichtbar, da Meldungen auch datumsabhängig sind */}
<div
style={{
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
}}
>
<DateRangePicker />
</div>
{/* DIA0-DIA2 Dropdown - Platz reservieren, aber ausblenden wenn Meldungen */}
<div
style={{
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
}}
>
<Listbox
value={selectedMode}
onChange={(value) => {
dispatch(setSelectedMode(value));
dispatch(setBrushRange({ startIndex: 0, endIndex: 0 }));
}}
>
<div className="relative w-48">
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
<span>
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündliche Werte",
DIA2: "Tägliche Werte",
}[selectedMode]
}
</span>
<svg
className="w-5 h-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
{["DIA0", "DIA1", "DIA2"].map((mode) => (
<Listbox.Option
key={mode}
value={mode}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
>
<div className="flex items-center gap-3 flex-1 justify-end">
{/* Always show date range; requirement: in Meldungen only Von/Bis + Anzeigen */}
<DateRangePicker />
{!isMeldungen && (
<>
<Listbox
value={selectedMode}
onChange={(value) => {
dispatch(setSelectedMode(value));
dispatch(setBrushRange({ startIndex: 0, endIndex: 0 }));
}}
>
<div className="relative w-48">
<Listbox.Button className="dropdown-surface w-full flex items-center justify-between">
<span className="dropdown-text-fix">
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündliche Werte",
DIA2: "Tägliche Werte",
}[mode]
DIA1: "Stündlich",
DIA2: "Täglich",
}[selectedMode]
}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
{/* Dropdown für Auswahl zwischen "Messkurve" und "Meldungen" - immer anzeigen */}
{/* Dropdown für Auswahl zwischen "Messkurve" und "Meldungen" entfernt */}
{/* Daten laden Button lädt je nach Ansicht Messkurve oder Meldungen */}
<button
style={{
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
}}
onClick={handleFetchData}
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm"
>
Daten laden
</button>
</span>
<i className="bi bi-chevron-down opacity-70" />
</Listbox.Button>
<Listbox.Options className="dropdown-options absolute z-50 mt-1 w-full max-h-60 overflow-auto">
{["DIA0", "DIA1", "DIA2"].map((mode) => (
<Listbox.Option
key={mode}
value={mode}
className={({ selected, active }) =>
`px-3 py-1.5 cursor-pointer rounded-sm m-0.5 ${
selected
? "dropdown-option-active"
: active
? "dropdown-option-hover"
: ""
}`
}
>
{
{
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>
);

View File

@@ -1,4 +1,4 @@
"use client"; // IsoChartView.tsx
"use client";
import React, { useEffect, useRef } from "react";
import { Listbox } from "@headlessui/react";
@@ -7,23 +7,18 @@ import IsoMeasurementChart from "./IsoMeasurementChart";
import IsoChartActionBar from "./IsoChartActionBar";
import Report from "./Report";
import { useSelector, useDispatch } from "react-redux";
import { AppDispatch } from "@/redux/store";
import { RootState } from "@/redux/store";
import { AppDispatch, RootState } from "@/redux/store";
import {
setChartOpen,
setFullScreen,
setSlotNumber,
setChartTitle,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { resetBrushRange } from "@/redux/slices/brushSlice";
import {
setVonDatum,
setBisDatum,
setSelectedMode,
setSelectedSlotType,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { resetBrushRange } from "@/redux/slices/brushSlice";
import { resetDateRange } from "@/redux/slices/dateRangePickerSlice";
interface IsoChartViewProps {
@@ -32,85 +27,59 @@ interface IsoChartViewProps {
slotIndex: number;
}
type ActionBarRefType = { handleFetchData: () => void };
const IsoChartView: React.FC<IsoChartViewProps> = ({
isOpen,
onClose,
slotIndex,
}) => {
const dispatch = useDispatch<AppDispatch>();
// removed unused loadData
const { isFullScreen, chartTitle } = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice
);
// **Modal schließen + Redux-Status zurücksetzen**
const handleClose = () => {
const actionBarRef = useRef<ActionBarRefType>(null);
const initDates = () => {
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
// Reset Datum
const toISO = (d: Date) => d.toLocaleDateString("sv-SE");
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
};
// Reset DateRangePicker
const handleClose = () => {
initDates();
dispatch(resetDateRange());
// Reset Dropdowns
dispatch(setSelectedMode("DIA0")); // Reset to Alle Messwerte
dispatch(setSelectedMode("DIA0"));
dispatch(setSelectedSlotType("isolationswiderstand"));
dispatch(setChartTitle("Messkurve")); // Reset zu Messkurve
// Sonstiges Reset
dispatch(setChartTitle("Messkurve"));
dispatch(setChartOpen(false));
dispatch(setFullScreen(false));
dispatch(resetBrushRange());
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(() => {
if (isOpen) {
dispatch(setSlotNumber(slotIndex));
// inline initDates to avoid extra dependency
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
// Set slot number first
dispatch(setSlotNumber(slotIndex));
// Set dates
const toISO = (d: Date) => d.toLocaleDateString("sv-SE");
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
// Set ISO specific settings
dispatch(setSelectedSlotType("isolationswiderstand"));
dispatch(setSelectedMode("DIA0")); // Set to Alle Messwerte on open
// Set default to Messkurve
dispatch(setSelectedMode("DIA0"));
dispatch(setChartTitle("Messkurve"));
// Automatisch Daten laden wie Button-Klick
const timer = setTimeout(() => {
actionBarRef.current?.handleFetchData();
}, 120);
// Cleanup timer
return () => clearTimeout(timer);
const t = setTimeout(() => actionBarRef.current?.handleFetchData(), 120);
return () => clearTimeout(t);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, slotIndex, dispatch]);
return (
@@ -119,74 +88,63 @@ const IsoChartView: React.FC<IsoChartViewProps> = ({
onRequestClose={handleClose}
ariaHideApp={false}
style={{
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
overlay: {
backgroundColor: "rgba(0,0,0,0.55)",
backdropFilter: "blur(2px)",
},
content: {
top: "50%",
left: "50%",
bottom: "auto",
marginRight: "-50%",
inset: "50% auto auto 50%",
transform: "translate(-50%, -50%)",
width: isFullScreen ? "90vw" : "70rem",
height: isFullScreen ? "90vh" : "35rem",
padding: "1rem",
transition: "all 0.3s ease-in-out",
width: isFullScreen ? "90vw" : "72rem",
height: isFullScreen ? "90vh" : "38rem",
padding: 0,
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
borderRadius: "14px",
display: "flex",
flexDirection: "column",
overflow: "hidden",
},
}}
contentLabel="Isolationswiderstand"
>
{/* Action-Buttons */}
<div
style={{
position: "absolute",
top: "0.625rem",
right: "0.625rem",
display: "flex",
gap: "0.75rem",
}}
>
{/* Fullscreen-Button */}
<button
onClick={toggleFullScreen}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i
className={
isFullScreen ? "bi bi-fullscreen-exit" : "bi bi-arrows-fullscreen"
}
></i>
</button>
{/* Schließen-Button */}
<button
onClick={handleClose}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i className="bi bi-x-circle-fill"></i>
</button>
</div>
{/* Chart-Container */}
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
height: "100%",
}}
>
<div className="flex justify-between items-center mb-2 pr-24">
<h3 className="text-lg font-semibold">Isolationswiderstand</h3>
<header className="modal-header relative pr-56">
<h3 className="text-sm font-semibold tracking-wide">
Isolationswiderstand
</h3>
<div className="absolute top-2 right-2 flex gap-2">
<button
onClick={toggleFullScreen}
className="icon-btn"
aria-label={isFullScreen ? "Vollbild verlassen" : "Vollbild"}
type="button"
>
<i
className={
isFullScreen
? "bi bi-fullscreen-exit"
: "bi bi-arrows-fullscreen"
}
/>
</button>
<button
onClick={handleClose}
className="icon-btn"
aria-label="Schließen"
type="button"
>
<i
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
className="bi bi-x-circle-fill"
/>
</button>
</div>
<div className="absolute top-2 right-28">
<Listbox
value={chartTitle}
onChange={(value: "Messkurve" | "Meldungen") =>
@@ -194,52 +152,36 @@ const IsoChartView: React.FC<IsoChartViewProps> = ({
}
>
<div className="relative w-40">
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
<span>
{chartTitle === "Meldungen" ? "Meldungen" : "Messkurve"}
</span>
<svg
className="w-5 h-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
<Listbox.Button className="dropdown-surface w-full flex items-center justify-between h-8">
<span className="dropdown-text-fix">{chartTitle}</span>
<i className="bi bi-chevron-down text-sm opacity-70" />
</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) => (
<Listbox.Option
key={option}
value={option}
className={({
selected,
active,
}: {
selected: boolean;
active: boolean;
}) =>
`px-4 py-1 cursor-pointer ${
className={({ selected, active }) =>
`px-3 py-1.5 cursor-pointer rounded-sm m-0.5 ${
selected
? "bg-littwin-blue text-white"
? "dropdown-option-active"
: active
? "bg-gray-200"
? "dropdown-option-hover"
: ""
}`
}
>
{option === "Meldungen" ? "Meldungen" : "Messkurve"}
{option}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
</header>
<div className="flex flex-col flex-1 p-3 gap-3">
<IsoChartActionBar ref={actionBarRef} />
<div style={{ flex: 1, height: "90%" }}>
<div className="flex-1 relative">
{chartTitle === "Messkurve" ? (
<IsoMeasurementChart />
) : (

View File

@@ -224,43 +224,47 @@ const Report: React.FC<ReportProps> = ({ moduleType, autoLoad = true }) => {
gewählten Zeitraum gefunden.
</div>
) : (
<div className="flex-1 overflow-auto ">
<table className="min-w-full border text-sm">
<thead className="bg-gray-100 text-left sticky top-0 z-10">
<tr>
<th className="p-2 border">Prio</th>
<th className="p-2 border">Zeitstempel</th>
<th className="p-2 border">Quelle</th>
<th className="p-2 border">Meldung</th>
<th className="p-2 border">Status</th>
</tr>
</thead>
<tbody>
{filteredMessages.map((msg, index) => (
<tr key={index} className="hover:bg-gray-200">
<td className="border p-2">
<div
className="w-4 h-4 rounded"
style={{ backgroundColor: msg.c }}
></div>
</td>
<td className="border p-2">
{new Date(msg.t).toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})}
</td>
<td className="border p-2">{msg.i}</td>
<td className="border p-2">{msg.m}</td>
<td className="border p-2">{msg.v}</td>
<div className="flex-1 overflow-auto table-scroll-region">
<div className="data-table-wrapper">
<table className="data-table">
<thead>
<tr>
<th style={{ width: "60px" }}>Prio</th>
<th style={{ minWidth: "180px" }}>Zeitstempel</th>
<th style={{ minWidth: "140px" }}>Quelle</th>
<th style={{ minWidth: "260px" }}>Meldung</th>
<th style={{ minWidth: "120px" }}>Status</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{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>
)}

View File

@@ -1,6 +1,9 @@
"use client"; // KVZChartView.tsx
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 { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "@/redux/store";
@@ -31,27 +34,31 @@ const KVZChartView: React.FC<KVZChartViewProps> = ({
slotIndex,
}) => {
const dispatch = useDispatch<AppDispatch>();
const isFullScreen = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
const { isFullScreen, slotNumber, vonDatum, bisDatum } = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice
);
const slotNumber = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice.slotNumber
const { vonDatum: pickerVonDatum, bisDatum: pickerBisDatum } = useSelector(
(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(() => {
if (isOpen) {
dispatch(setSlotNumber(slotIndex));
}
}, [isOpen, slotIndex, dispatch]);
const handleClose = () => {
if (!isOpen) return;
dispatch(setSlotNumber(slotIndex));
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(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(setBisDatum(toISO(today)));
dispatch(setSelectedMode("DIA1"));
@@ -59,93 +66,114 @@ const KVZChartView: React.FC<KVZChartViewProps> = ({
dispatch(setChartOpen(false));
dispatch(setFullScreen(false));
dispatch(resetBrushRange());
onClose();
};
const toggleFullScreen = () => {
dispatch(setFullScreen(!isFullScreen));
};
const toggleFullScreen = () => 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 (
<ReactModal
isOpen={isOpen}
onRequestClose={handleClose}
ariaHideApp={false}
style={{
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
overlay: {
backgroundColor: "rgba(0,0,0,0.55)",
backdropFilter: "blur(2px)",
},
content: {
top: "50%",
left: "50%",
bottom: "auto",
marginRight: "-50%",
inset: "50% auto auto 50%",
transform: "translate(-50%, -50%)",
width: isFullScreen ? "90vw" : "50rem",
height: isFullScreen ? "90vh" : "28rem",
padding: "1rem",
transition: "all 0.3s ease-in-out",
width: isFullScreen ? "90vw" : "72rem",
height: isFullScreen ? "90vh" : "38rem",
padding: 0,
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
borderRadius: "14px",
display: "flex",
flexDirection: "column",
overflow: "hidden",
},
}}
contentLabel="KVZ Zustände & Meldungen"
>
{/* Action Buttons */}
<div
style={{
position: "absolute",
top: "0.625rem",
right: "0.625rem",
display: "flex",
gap: "0.75rem",
}}
>
<button
onClick={toggleFullScreen}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i
className={
isFullScreen ? "bi bi-fullscreen-exit" : "bi bi-arrows-fullscreen"
}
></i>
</button>
<button
onClick={handleClose}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i className="bi bi-x-circle-fill"></i>
</button>
</div>
{/* Content */}
<div className="flex flex-col h-full">
<h3 className="text-lg font-semibold mb-1">KVz Zustände & Meldungen</h3>
{/* LED Bereich */}
<div className="w-full flex justify-between mb-4">
<div className="flex items-center">
<label className="text-sm font-semibold">
<header className="modal-header relative pr-32">
<h3 className="text-sm font-semibold tracking-wide">
KVZ Zustände & Meldungen
</h3>
<div className="absolute top-2 right-2 flex gap-2">
<button
onClick={toggleFullScreen}
className="icon-btn"
aria-label={isFullScreen ? "Vollbild verlassen" : "Vollbild"}
type="button"
>
<i
className={
isFullScreen
? "bi bi-fullscreen-exit"
: "bi bi-arrows-fullscreen"
}
/>
</button>
<button
onClick={handleClose}
className="icon-btn"
aria-label="Schließen"
type="button"
>
<i
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
className="bi bi-x-circle-fill"
/>
</button>
</div>
</header>
<div className="flex flex-col flex-1 p-3 gap-3">
{/* Toolbar */}
<div className="w-full flex flex-wrap items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold opacity-80 select-none text-fg-secondary">
{slotNumber !== null ? slotNumber + 1 : "-"}
</label>
</span>
</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} />
</div>
<div></div>
</div>
{/* Meldungen Bereich */}
<div className="flex-1 border rounded bg-white overflow-hidden">
<Report moduleType="KVZ" />
<div className="flex-1 relative border border-base rounded bg-[var(--color-surface-alt)] text-fg overflow-hidden p-2">
<div className="w-full h-full rounded bg-[var(--color-surface)] overflow-hidden">
<Report moduleType="KVZ" />
</div>
</div>
</div>
</ReactModal>

View File

@@ -281,26 +281,24 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
handleFetchData,
}));
// Sichtbarkeits-Flags
const isMesskurve = chartTitle === "Messkurve";
const isMeldungen = chartTitle === "Meldungen";
return (
<div className="flex justify-between p-1 bg-gray-100 rounded-lg ">
<div className="flex items-center">
<label className="text-sm font-semibold">
<div className="toolbar w-full flex flex-wrap items-center gap-2">
<div className="flex items-center mr-2 min-w-[4rem]">
<span className="text-xs font-semibold opacity-80 select-none">
{slotNumber !== null ? slotNumber + 1 : "-"}
</label>
</span>
</div>
<div className="flex items-center space-x-2">
{/* DateRangePicker für beide Ansichten sichtbar */}
<div>
<DateRangePicker compact />
</div>
<div className="flex items-center gap-3 flex-1 justify-end">
{/* DateRangePicker immer sichtbar */}
<DateRangePicker />
{/* DIA0/DIA1/DIA2 Dropdown nur sichtbar bei Messkurve */}
<div
style={{
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
}}
>
{/* Modus-Dropdown nur für Messkurve */}
<div className={isMesskurve ? "" : "hidden"}>
<Listbox
value={selectedMode}
onChange={(value) => {
@@ -309,39 +307,29 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
}}
>
<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>
<Listbox.Button className="dropdown-surface w-full flex items-center justify-between">
<span className="dropdown-text-fix">
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündliche Werte",
DIA2: "Tägliche Werte",
DIA1: "Stündlich",
DIA2: "Täglich",
}[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>
<i className="bi bi-chevron-down opacity-70" />
</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) => (
<Listbox.Option
key={mode}
value={mode}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer ${
`px-3 py-1.5 cursor-pointer rounded-sm m-0.5 ${
selected
? "bg-littwin-blue text-white"
? "dropdown-option-active"
: active
? "bg-gray-200"
? "dropdown-option-hover"
: ""
}`
}
@@ -349,8 +337,8 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündliche Werte",
DIA2: "Tägliche Werte",
DIA1: "Stündlich",
DIA2: "Täglich",
}[mode]
}
</Listbox.Option>
@@ -359,40 +347,50 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
</div>
</Listbox>
</div>
{/* Dropdown für Messkurve / Meldungen in View-Header umgezogen */}
{/* Buttons nur sichtbar bei Messkurve, Platz bleibt erhalten */}
<div
style={{
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
}}
className="flex items-center space-x-2"
>
<button
onClick={handleStartRSL}
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap"
disabled={isLoading || rslRunning}
>
{rslRunning ? "RSL läuft..." : "RSL Messung starten"}
</button>
{/* Buttons */}
{isMesskurve && (
<div className="flex items-center gap-2">
<button
onClick={handleStartRSL}
className="btn-primary h-8 font-medium px-3"
disabled={isLoading || rslRunning}
type="button"
>
{rslRunning ? "RSL läuft…" : "RSL Messung starten"}
</button>
<button
onClick={handleFetchData}
className="btn-primary h-8 font-medium px-3"
disabled={rslRunning}
type="button"
>
Daten laden
</button>
</div>
)}
{isMeldungen && (
<button
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}
type="button"
>
Daten laden
Anzeigen
</button>
</div>
)}
</div>
{rslRunning && (
<div className="fixed inset-0 z-[1000] flex flex-col items-center justify-center bg-white/80 backdrop-blur-sm">
<div className="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">
<p className="text-lg font-semibold">RSL Messung läuft</p>
<p className="text-sm text-gray-700">
Bitte warten (noch {TOTAL_DURATION - rslProgress}s)
<p className="text-sm font-semibold">RSL Messung läuft</p>
<p className="text-xs opacity-80">
Bitte warten{" "}
{Math.min(100, Math.round((rslProgress / TOTAL_DURATION) * 100))}%
</p>
</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
className="h-full bg-littwin-blue transition-all ease-linear"
style={{ width: `${(rslProgress / TOTAL_DURATION) * 100}%` }}

View File

@@ -1,31 +1,24 @@
"use client"; // LoopChartView.tsx
import React, { useEffect } from "react";
import React, { useEffect, useRef } from "react";
import { Listbox } from "@headlessui/react";
import ReactModal from "react-modal";
import LoopMeasurementChart from "./LoopMeasurementChart";
import Report from "../IsoMeasurementChart/Report";
import LoopChartActionBar from "./LoopChartActionBar";
import { useRef } from "react";
import { useSelector, useDispatch } from "react-redux";
import { AppDispatch } from "@/redux/store";
import { RootState } from "@/redux/store";
import { AppDispatch, RootState } from "@/redux/store";
import {
setChartOpen,
setFullScreen,
setSlotNumber,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { setChartTitle as setLoopChartTitle } from "@/redux/slices/loopChartTypeSlice";
import { resetBrushRange } from "@/redux/slices/brushSlice";
import { useLoopChartLoader } from "./LoopChartActionBar";
import {
setVonDatum,
setBisDatum,
setSelectedMode,
setSelectedSlotType,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { setChartTitle as setLoopChartTitle } from "@/redux/slices/loopChartTypeSlice";
import { resetBrushRange } from "@/redux/slices/brushSlice";
import { resetDateRange } from "@/redux/slices/dateRangePickerSlice";
interface LoopChartViewProps {
@@ -34,11 +27,7 @@ interface LoopChartViewProps {
slotIndex: number;
}
const LoopChartView: React.FC<LoopChartViewProps> = ({
isOpen,
onClose,
slotIndex,
}) => {
function LoopChartView({ isOpen, onClose, slotIndex }: LoopChartViewProps) {
const dispatch = useDispatch<AppDispatch>();
const chartTitle = useSelector(
(state: RootState) => state.loopChartType.chartTitle
@@ -48,9 +37,6 @@ const LoopChartView: React.FC<LoopChartViewProps> = ({
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
);
// useLoopChartLoader hook
const loadLoopChartData = useLoopChartLoader();
// slotNumber nicht direkt benötigt wird intern über Redux genutzt
// **Modal schließen + Redux-Status zurücksetzen**
@@ -58,64 +44,35 @@ const LoopChartView: React.FC<LoopChartViewProps> = ({
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
// Reset Datum
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
// Reset DateRangePicker
dispatch(resetDateRange());
// Reset Dropdowns
dispatch(setSelectedMode("DIA0")); // Reset to Alle Messwerte
dispatch(setSelectedMode("DIA0"));
dispatch(setSelectedSlotType("schleifenwiderstand"));
// Sonstiges Reset
dispatch(setChartOpen(false));
dispatch(setFullScreen(false));
dispatch(resetBrushRange());
onClose();
};
// **Vollbildmodus umschalten**
const toggleFullScreen = () => {
dispatch(setFullScreen(!isFullScreen));
};
const toggleFullScreen = () => dispatch(setFullScreen(!isFullScreen));
// Modal öffnen - RSL spezifische Einstellungen
const actionBarRef = useRef<{ handleFetchData: () => void }>(null);
useEffect(() => {
if (isOpen) {
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
// Set slot number first
dispatch(setSlotNumber(slotIndex));
// Set dates
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
// Set RSL specific settings
dispatch(setSelectedSlotType("schleifenwiderstand"));
dispatch(setSelectedMode("DIA0")); // Set to Alle Messwerte on open
// Automatisch Daten laden wie Button-Klick
const timer = setTimeout(() => {
actionBarRef.current?.handleFetchData();
}, 120);
// Cleanup timer
return () => clearTimeout(timer);
dispatch(setSelectedMode("DIA0"));
const t = setTimeout(() => actionBarRef.current?.handleFetchData(), 120);
return () => clearTimeout(t);
}
//ESLint ignore
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, slotIndex, dispatch]);
return (
@@ -124,76 +81,63 @@ const LoopChartView: React.FC<LoopChartViewProps> = ({
onRequestClose={handleClose}
ariaHideApp={false}
style={{
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
overlay: {
backgroundColor: "rgba(0,0,0,0.55)",
backdropFilter: "blur(2px)",
},
content: {
top: "50%",
left: "50%",
bottom: "auto",
marginRight: "-50%",
inset: "50% auto auto 50%",
transform: "translate(-50%, -50%)",
width: isFullScreen ? "90vw" : "70rem",
height: isFullScreen ? "90vh" : "35rem",
padding: "1rem",
transition: "all 0.3s ease-in-out",
width: isFullScreen ? "90vw" : "72rem",
height: isFullScreen ? "90vh" : "38rem",
padding: 0,
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
borderRadius: "14px",
display: "flex",
flexDirection: "column",
overflow: "hidden",
},
}}
contentLabel="Schleifenwiderstand"
>
{/* Action-Buttons */}
<div
style={{
position: "absolute",
top: "0.625rem",
right: "0.625rem",
display: "flex",
gap: "0.75rem",
}}
>
{/* Fullscreen-Button */}
<button
onClick={toggleFullScreen}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i
className={
isFullScreen ? "bi bi-fullscreen-exit" : "bi bi-arrows-fullscreen"
}
></i>
</button>
{/* Schließen-Button */}
<button
onClick={handleClose}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i className="bi bi-x-circle-fill"></i>
</button>
</div>
{/* Chart-Container */}
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
height: "100%",
}}
>
<div className="flex justify-between items-center mb-2 pr-24">
<h3 className="text-lg font-semibold">
{chartTitle === "Messkurve" ? "Schleifenwiderstand" : "Meldungen"}
</h3>
<header className="modal-header relative pr-56">
<h3 className="text-sm font-semibold tracking-wide">
Schleifenwiderstand
</h3>
<div className="absolute top-2 right-2 flex gap-2">
<button
onClick={toggleFullScreen}
className="icon-btn"
aria-label={isFullScreen ? "Vollbild verlassen" : "Vollbild"}
type="button"
>
<i
className={
isFullScreen
? "bi bi-fullscreen-exit"
: "bi bi-arrows-fullscreen"
}
/>
</button>
<button
onClick={handleClose}
className="icon-btn"
aria-label="Schließen"
type="button"
>
<i
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
className="bi bi-x-circle-fill"
/>
</button>
</div>
<div className="absolute top-2 right-28">
<Listbox
value={chartTitle}
onChange={(value: "Messkurve" | "Meldungen") =>
@@ -201,37 +145,21 @@ const LoopChartView: React.FC<LoopChartViewProps> = ({
}
>
<div className="relative w-40">
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
<span>{chartTitle}</span>
<svg
className="w-5 h-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
<Listbox.Button className="dropdown-surface w-full flex items-center justify-between h-8">
<span className="dropdown-text-fix">{chartTitle}</span>
<i className="bi bi-chevron-down text-sm opacity-70" />
</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) => (
<Listbox.Option
key={option}
value={option}
className={({
selected,
active,
}: {
selected: boolean;
active: boolean;
}) =>
`px-4 py-1 cursor-pointer ${
className={({ selected, active }) =>
`px-3 py-1.5 cursor-pointer rounded-sm m-0.5 ${
selected
? "bg-littwin-blue text-white"
? "dropdown-option-active"
: active
? "bg-gray-200"
? "dropdown-option-hover"
: ""
}`
}
@@ -243,17 +171,19 @@ const LoopChartView: React.FC<LoopChartViewProps> = ({
</div>
</Listbox>
</div>
</header>
<div className="flex flex-col flex-1 p-3 gap-3">
<LoopChartActionBar ref={actionBarRef} />
<div style={{ flex: 1, height: "90%" }}>
<div className="flex-1 relative">
{chartTitle === "Messkurve" ? (
<LoopMeasurementChart />
) : (
<Report moduleType="RSL" autoLoad={false} />
<Report moduleType="RSL" autoLoad={chartTitle === "Meldungen"} />
)}
</div>
</div>
</ReactModal>
);
};
}
export default LoopChartView;

View File

@@ -7,7 +7,6 @@ import { AppDispatch } from "../../../../../../redux/store";
import { Chart, registerables } from "chart.js";
import "chartjs-adapter-date-fns";
import { getColor } from "../../../../../../utils/colors";
import TDRChartActionBar from "./TDRChartActionBar";
import { getReferenceCurveBySlotThunk } from "../../../../../../redux/thunks/getReferenceCurveBySlotThunk";
const TDRChart: React.FC<{ isFullScreen: boolean }> = ({ isFullScreen }) => {
@@ -213,8 +212,6 @@ const TDRChart: React.FC<{ isFullScreen: boolean }> = ({ isFullScreen }) => {
return (
<div style={{ width: "100%", height: isFullScreen ? "90%" : "28rem" }}>
<TDRChartActionBar />
{tdrChartData.length === 0 ? (
<div className="flex items-center justify-center h-full text-gray-500 italic">
Keine Daten verfügbar für diesen Slot

View File

@@ -1,127 +1,149 @@
"use client";
// /components/main/kabelueberwachung/kue705FO/Charts/TDRChart/TDRChartActionBar.tsx
import React, { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import React, { useEffect, useState } from "react";
import DateRangePicker from "@/components/common/DateRangePicker";
import { useAppDispatch } from "@/redux/store";
import { useSelector } from "react-redux";
import { RootState } from "@/redux/store";
import { Listbox } from "@headlessui/react";
import { getMessagesThunk } from "@/redux/thunks/getMessagesThunk";
import { setLoading } from "@/redux/slices/kabelueberwachungChartSlice";
import { fetchTDMDataBySlotThunk } from "@/redux/thunks/getTDMListBySlotThunk";
import { getTDRChartDataByIdThunk } from "@/redux/thunks/getTDRChartDataByIdThunk";
import { getReferenceCurveBySlotThunk } from "@/redux/thunks/getReferenceCurveBySlotThunk"; // ⬅ import ergänzen
import { Listbox } from "@headlessui/react";
import { getReferenceCurveBySlotThunk } from "@/redux/thunks/getReferenceCurveBySlotThunk";
const TDRChartActionBar: React.FC = () => {
const dispatch = useAppDispatch();
// ✅ Redux: selectedSlot aus kueChartMode (0-basiert)
const { vonDatum, bisDatum, chartTitle } = useSelector(
(s: RootState) => s.kabelueberwachungChartSlice
);
const { vonDatum: pickerVon, bisDatum: pickerBis } = useSelector(
(s: RootState) => s.dateRangePicker
);
const selectedSlot = useSelector(
(state: RootState) => state.kueChartModeSlice.selectedSlot
(s: RootState) => s.kueChartModeSlice.selectedSlot
);
const tdmChartData = useSelector(
(state: RootState) => state.tdmSingleChartSlice.data
(s: RootState) => s.tdmSingleChartSlice.data
);
const idsForSlot =
selectedSlot !== null ? tdmChartData[selectedSlot] ?? [] : [];
const tdrDataById = useSelector(
(state: RootState) => state.tdrDataByIdSlice.dataById
(s: RootState) => s.tdrDataByIdSlice.dataById
);
const [selectedId, setSelectedId] = useState<number | null>(null);
const currentChartData = selectedId !== null ? tdrDataById[selectedId] : [];
// 📌 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 () => {
if (
selectedSlot === null ||
selectedId === null ||
!currentChartData?.length
!currentChartData.length
)
return;
const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development";
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) {
const url = `/CPL?KTR${slotNumber}=${selectedId}`;
const url = `/CPL?KTR${selectedSlot + 1}=${selectedId}`;
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (!response.ok)
throw new Error(
`Fehler beim Setzen der Referenz: ${response.statusText}`
);
}
}
// Optional: lokale Speicherung und Redux-Update
localStorage.setItem(
`ref-curve-slot${selectedSlot}`,
JSON.stringify(currentChartData)
);
dispatch(getReferenceCurveBySlotThunk(selectedSlot));
alert("Referenzkurve wurde erfolgreich gesetzt!");
} catch (error) {
console.error("Fehler beim Setzen der Referenzkurve:", error);
} catch (err) {
console.error("Fehler beim Setzen der Referenzkurve", err);
alert("Fehler beim Setzen der Referenzkurve.");
}
};
// 📌 TDR Messung starten
const handleStartTDR = async () => {
if (selectedSlot === null) {
alert("⚠️ Bitte zuerst einen KÜ auswählen!");
return;
}
const cgiUrl = `${window.location.origin}/CPL?/${window.location.pathname}&KTT${selectedSlot}=1`;
try {
console.log("🚀 Starte TDR Messung für Slot:", selectedSlot);
console.log("📡 CGI URL:", cgiUrl);
const response = await fetch(cgiUrl);
if (!response.ok) {
throw new Error(`CGI-Fehler: ${response.status}`);
const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development";
if (isDev) {
await new Promise((r) => setTimeout(r, 150));
startTdrProgress();
return;
}
console.log("✅ TDR Messung gestartet für Slot", selectedSlot);
//alert(`✅ TDR Messung für Slot ${selectedSlot + 1} gestartet`);
const response = await fetch(cgiUrl);
if (!response.ok) throw new Error(`CGI-Fehler: ${response.status}`);
startTdrProgress();
} catch (err) {
console.error("❌ Fehler beim Starten der TDR Messung:", err);
//alert("❌ Fehler beim Starten der TDR Messung.");
console.error("❌ Fehler beim Starten der TDR Messung", err);
}
};
// 📥 Beim Slot-Wechsel TDM-Liste + letzte ID laden
// Load TDM list when slot changes
useEffect(() => {
if (selectedSlot !== null) {
dispatch(fetchTDMDataBySlotThunk(selectedSlot)).then((action) => {
// action can be a PayloadAction with payload or a rejected action
const payload = (
action as {
payload?: { data?: { id: number; t: string; d: number }[] };
}
).payload;
const slotData = payload?.data;
if ((slotData ?? []).length > 0) {
const lastId = (slotData ?? [])[0].id;
const slotData = payload?.data ?? [];
if (slotData.length > 0) {
const lastId = slotData[0].id; // latest first
setSelectedId(lastId);
dispatch(getTDRChartDataByIdThunk(lastId));
}
@@ -130,109 +152,160 @@ const TDRChartActionBar: React.FC = () => {
}, [selectedSlot, dispatch]);
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="text-sm font-semibold">
{selectedSlot !== null ? `${selectedSlot + 1}` : "Kein KÜ gewählt"}
</div>
{/* ✅ Referenz setzen */}
{selectedId !== null && (
<button
onClick={handleSetReference}
className="border border-littwin-blue text-littwin-blue bg-white rounded px-3 py-1 text-sm hover:bg-gray-200"
>
TDR-Kurve als Referenz speichern
</button>
)}
{/* 🚀 TDR starten */}
<button
onClick={handleStartTDR}
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap "
disabled={selectedSlot === null}
>
TDR-Messung starten
</button>
{/* 🔽 Dropdown für Messungen */}
<div className="flex items-center space-x-2">
<Listbox
value={selectedId}
onChange={(id) => {
setSelectedId(id);
if (id !== null) {
dispatch(getTDRChartDataByIdThunk(id));
}
}}
disabled={idsForSlot.length === 0}
>
<div className="relative w-96">
<Listbox.Button className="w-full border px-2 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
{selectedId
? (() => {
const selected = idsForSlot.find(
(e) => e.id === selectedId
);
return selected
? `${new Date(selected.t).toLocaleString("de-DE", {
<>
<div className="toolbar w-full justify-between flex-wrap">
{/* KÜ number left, controls right, like IsoChartActionBar */}
<div className="flex items-center gap-2 pr-4">
<span className="font-semibold uppercase tracking-wide text-muted">
</span>
<span className="font-medium px-2 py-0.5 rounded bg-surface-alt border border-base min-w-[3rem] text-center">
{selectedSlot !== null ? selectedSlot + 1 : "-"}
</span>
</div>
<div className="flex items-center gap-3 flex-1 justify-end">
{isMeldungen ? (
<>
<DateRangePicker />
<button
type="button"
onClick={handleFetchMessages}
className="btn-primary h-8 font-medium px-4"
disabled={selectedSlot === null}
>
Anzeigen
</button>
</>
) : (
<>
{selectedId !== null && (
<button
onClick={handleSetReference}
type="button"
className="btn-primary h-8 px-3 font-medium"
disabled={selectedSlot === null}
>
TDR-Kurve als Referenz speichern
</button>
)}
<button
onClick={handleStartTDR}
type="button"
disabled={selectedSlot === null || tdrRunning}
className={`btn-primary h-8 px-4 whitespace-nowrap ${
tdrRunning ? "opacity-90" : ""
}`}
>
{tdrRunning
? `TDR läuft... (${Math.min(
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",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})} Fehlerstelle: ${selected.d} m`
: "Wähle Messung";
})()
: "Wähle Messung"}
</span>
<svg
className="w-5 h-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
{idsForSlot.map((entry) => (
<Listbox.Option
key={entry.id}
value={entry.id}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer whitespace-nowrap overflow-hidden text-ellipsis ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
>
{new Date(entry.t).toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})}{" "}
Fehlerstelle: {entry.d} m
</Listbox.Option>
))}
</Listbox.Options>
}
);
const fullText = `${dateLabel} Fehlerstelle: ${entry.d} m`;
return (
<Listbox.Option
key={entry.id}
value={entry.id}
title={fullText}
className={({ selected, active }) => {
const base =
"px-3 h-8 cursor-pointer rounded-sm m-0.5 flex items-center justify-start transition-colors text-[13px]";
if (selected)
return `${base} dropdown-option-active font-medium`;
if (active)
return `${base} dropdown-option-hover`;
return `${base}`;
}}
>
<span className="truncate w-full">{fullText}</span>
</Listbox.Option>
);
})}
</Listbox.Options>
</div>
</Listbox>
</div>
</>
)}
</div>
{/* 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">
TDR Messung läuft... kann bis zu zwei Minuten dauern
</p>
<p className="text-sm text-white/80">
Bitte warten{" "}
{Math.min(
100,
Math.round((tdrProgress / TDR_TOTAL_DURATION) * 100)
)}
%
</p>
</div>
<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>
</Listbox>
)}
</div>
</div>
</>
);
};

View File

@@ -3,30 +3,25 @@
import React, { useEffect } from "react";
import ReactModal from "react-modal";
import TDRChart from "./TDRChart";
import TDRChartActionBar from "./TDRChartActionBar";
import { useSelector, useDispatch } from "react-redux";
import { AppDispatch } from "@/redux/store";
import { RootState } from "@/redux/store";
import { AppDispatch, RootState } from "@/redux/store";
import {
setChartOpen,
setFullScreen,
setSlotNumber,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { resetBrushRange } from "@/redux/slices/brushSlice";
import {
setVonDatum,
setBisDatum,
setSelectedMode,
setSelectedSlotType,
setChartTitle,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { resetBrushRange } from "@/redux/slices/brushSlice";
import {
setSelectedSlot,
setActiveMode,
} from "@/redux/slices/kueChartModeSlice";
import { Listbox } from "@headlessui/react";
import { setChartTitle } from "@/redux/slices/kabelueberwachungChartSlice";
import Report from "../IsoMeasurementChart/Report";
interface TDRChartViewProps {
@@ -41,64 +36,48 @@ const TDRChartView: React.FC<TDRChartViewProps> = ({
slotIndex,
}) => {
const dispatch = useDispatch<AppDispatch>();
const { isFullScreen, chartTitle } = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice
(s: RootState) => s.kabelueberwachungChartSlice
);
// **Modal öffnen - TDR spezifische Einstellungen**
// Initialize defaults when opening
useEffect(() => {
if (isOpen) {
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
// Set TDR mode and slot
dispatch(setActiveMode("TDR"));
dispatch(setSelectedSlot(slotIndex));
// Also set slot number for general chart slice
dispatch(setSlotNumber(slotIndex));
// Set dates
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
// TDR specific settings (if needed)
dispatch(setSelectedSlotType("isolationswiderstand"));
}
}, [isOpen, slotIndex, dispatch]);
// **Modal schließen + Redux-Status zurücksetzen**
const handleClose = () => {
if (!isOpen) return;
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
// Reset Datum
dispatch(setActiveMode("TDR"));
dispatch(setSelectedSlot(slotIndex));
dispatch(setSlotNumber(slotIndex));
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
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(setSelectedSlotType("isolationswiderstand"));
// Sonstiges Reset
dispatch(setChartOpen(false));
dispatch(setFullScreen(false));
dispatch(resetBrushRange());
onClose();
};
// **Vollbildmodus umschalten**
const toggleFullScreen = () => {
dispatch(setFullScreen(!isFullScreen));
};
const toggleFullScreen = () => dispatch(setFullScreen(!isFullScreen));
return (
<ReactModal
@@ -106,77 +85,61 @@ const TDRChartView: React.FC<TDRChartViewProps> = ({
onRequestClose={handleClose}
ariaHideApp={false}
style={{
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
overlay: {
backgroundColor: "rgba(0,0,0,0.55)",
backdropFilter: "blur(2px)",
},
content: {
top: "50%",
left: "50%",
bottom: "auto",
marginRight: "-50%",
inset: "50% auto auto 50%",
transform: "translate(-50%, -50%)",
width: isFullScreen ? "90vw" : "70rem",
height: isFullScreen ? "90vh" : "35rem",
padding: "1rem",
transition: "all 0.3s ease-in-out",
width: isFullScreen ? "90vw" : "72rem",
height: isFullScreen ? "90vh" : "38rem",
padding: 0,
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
borderRadius: "14px",
display: "flex",
flexDirection: "column",
overflow: "hidden",
},
}}
contentLabel="TDR Messung"
>
{/* Action-Buttons */}
<div
style={{
position: "absolute",
top: "0.625rem",
right: "0.625rem",
display: "flex",
gap: "0.75rem",
}}
>
{/* Fullscreen-Button */}
<button
onClick={toggleFullScreen}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i
className={
isFullScreen ? "bi bi-fullscreen-exit" : "bi bi-arrows-fullscreen"
}
></i>
</button>
{/* Schließen-Button */}
<button
onClick={handleClose}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i className="bi bi-x-circle-fill"></i>
</button>
</div>
{/* Chart-Container */}
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
height: "100%",
}}
>
<div className="flex justify-between items-center mb-2 pr-24">
<h3 className="text-lg font-semibold">
{chartTitle === "Messkurve" ? "TDR-Messung" : "Meldungen"}
</h3>
{/* Dropdown Messkurve / Meldungen */}
<header className="modal-header relative pr-56">
<h3 className="text-sm font-semibold tracking-wide">TDR-Messung</h3>
<div className="absolute top-2 right-2 flex gap-2">
<button
onClick={toggleFullScreen}
className="icon-btn"
aria-label={isFullScreen ? "Vollbild verlassen" : "Vollbild"}
type="button"
>
<i
className={
isFullScreen
? "bi bi-fullscreen-exit"
: "bi bi-arrows-fullscreen"
}
/>
</button>
<button
onClick={handleClose}
className="icon-btn"
aria-label="Schließen"
type="button"
>
<i
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
className="bi bi-x-circle-fill"
/>
</button>
</div>
<div className="absolute top-2 right-28">
<Listbox
value={chartTitle}
onChange={(value: "Messkurve" | "Meldungen") =>
@@ -184,31 +147,21 @@ const TDRChartView: React.FC<TDRChartViewProps> = ({
}
>
<div className="relative w-40">
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
<span>{chartTitle}</span>
<svg
className="w-5 h-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
<Listbox.Button className="dropdown-surface w-full flex items-center justify-between h-8">
<span className="dropdown-text-fix">{chartTitle}</span>
<i className="bi bi-chevron-down text-sm opacity-70" />
</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) => (
<Listbox.Option
key={option}
value={option}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer ${
`px-3 py-1.5 cursor-pointer rounded-sm m-0.5 ${
selected
? "bg-littwin-blue text-white"
? "dropdown-option-active"
: active
? "bg-gray-200"
? "dropdown-option-hover"
: ""
}`
}
@@ -220,8 +173,11 @@ const TDRChartView: React.FC<TDRChartViewProps> = ({
</div>
</Listbox>
</div>
{/* Chart oder Meldungen */}
<div style={{ flex: 1, height: "90%" }}>
</header>
<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" ? (
<TDRChart isFullScreen={isFullScreen} />
) : (

View File

@@ -31,7 +31,6 @@ import useKueVersion from "./hooks/useKueVersion";
import useIsoDisplay from "./hooks/useIsoDisplay";
import useLoopDisplay from "./hooks/useLoopDisplay";
import useModulName from "./hooks/useModulName";
import { useAdminAuth } from "../../settingsPageComponents/hooks/useAdminAuth";
//--------handlers----------------
// Keep needed imports
@@ -57,7 +56,8 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
const { kueName } = useSelector((state: RootState) => state.kueDataSlice);
// 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(
@@ -174,18 +174,6 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
const openTdrModal = () => {
setActiveButton("TDR");
setloopTitleText("Entfernung [km]");
const latestTdrDistanceMeters =
Array.isArray(tdmChartData?.[slotIndex]) &&
tdmChartData[slotIndex].length > 0 &&
typeof tdmChartData[slotIndex][0].d === "number"
? tdmChartData[slotIndex][0].d
: 0;
const latestTdrDistance = Number(
(latestTdrDistanceMeters / 1000).toFixed(3)
);
setLoopDisplayValue(latestTdrDistance);
setShowTdrModal(true);
};
@@ -272,30 +260,16 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
return () => window.removeEventListener("resize", measure);
}, [moduleName48, scrollFeatureEnabled]);
//---------------------------------
const tdmChartData = useSelector(
(state: RootState) => state.tdmChartSlice.data
);
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);
// TDR Distanz wird im Display nicht angezeigt Daten für Modal werden separat geladen
//---------------------------------
const loopValue =
activeButton === "TDR"
? latestTdrDistance
: typeof schleifenwiderstand === "number"
const rslValue =
typeof schleifenwiderstand === "number"
? schleifenwiderstand
: Number(schleifenwiderstand);
const { loopDisplayValue, setLoopDisplayValue } = useLoopDisplay(
loopValue,
rslValue,
activeButton
);
@@ -303,10 +277,10 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
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
// 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 =
kvzPresence?.[slotIndex] === 1 &&
kvzActive?.[slotIndex] === 1 &&
isAdminLoggedIn;
kvzPresence?.[slotIndex] === 1 && kvzActive?.[slotIndex] === 1;
// Removed useChartData(loopMeasurementCurveChartData) as the state was unused
@@ -409,7 +383,7 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
.toFixed(2)
.replace(".", ",")} MOhm`}
</span>
{/* 3. Zeile: Schleifenwert, in Rot bei Schleifenfehler, sonst normal */}
{/* 3. Zeile: Schleifenwert (RSL) immer anzeigen, unabhängig von aktivem Button */}
<span
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
Number(kueAlarm2?.[slotIndex]) === 1 ? "text-red-500" : ""

View File

@@ -12,22 +12,48 @@ export default function SlotActivityOverlay({
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
const alignmentStartedAt = useAppSelector(
(s) => s.deviceEvents.alignmentStartedAt
const comparisonStartedAt = useAppSelector(
(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 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
const [now, setNow] = useState<number>(Date.now());
useEffect(() => {
const any = loopActive || tdrActive || alignActive;
const any = loopActive || tdrActive || comparisonActive;
if (!any) return;
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, [loopActive, tdrActive, alignActive]);
}, [loopActive, tdrActive, comparisonActive]);
const clamp = (v: number, min = 0, max = 1) =>
Math.max(min, Math.min(max, v));
@@ -40,10 +66,10 @@ export default function SlotActivityOverlay({
// Durations
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
if (!loopActive && !tdrActive && !alignActive) return null;
if (!loopActive && !tdrActive && !comparisonActive) return null;
return (
<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 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 (
<div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
@@ -77,7 +104,8 @@ export default function SlotActivityOverlay({
<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 (
<div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
@@ -94,11 +122,13 @@ export default function SlotActivityOverlay({
})()}
</div>
)}
{alignActive && (
{comparisonActive && (
<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 (
<div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">

View File

@@ -1,19 +1,19 @@
// components/main/kabelueberwachung/kue705FO/hooks/useLoopDisplay.ts
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 = (
schleifenwiderstand: number,
rslValue: number,
activeButton: "Schleife" | "TDR" | "ISO"
) => {
const [loopDisplayValue, setLoopDisplayValue] =
useState<number>(schleifenwiderstand);
const [loopDisplayValue, setLoopDisplayValue] = useState<number>(rslValue);
useEffect(() => {
if (activeButton === "Schleife") {
setLoopDisplayValue(schleifenwiderstand);
setLoopDisplayValue(rslValue);
}
// For ISO and TDR, the value is set manually via setLoopDisplayValue
}, [schleifenwiderstand, activeButton]);
}, [rslValue, activeButton]);
return { loopDisplayValue, setLoopDisplayValue };
};

View File

@@ -41,63 +41,83 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
window.kabelModalOpen = showModal;
}
}, [showModal]);
//-----------------------------------------------------
//------------------------------------------------------
return (
<ReactModal
isOpen={showModal}
onRequestClose={onClose}
shouldCloseOnOverlayClick
ariaHideApp={false}
style={{
overlay: {
backgroundColor: "rgba(0, 0, 0, 0.5)",
backgroundColor: "rgba(0,0,0,0.55)",
zIndex: 100,
backdropFilter: "blur(2px)",
},
content: {
top: "50%",
left: "50%",
inset: "50% auto auto 50%",
transform: "translate(-50%, -50%)",
width: "90%",
maxWidth: "850px",
padding: "0px",
border: "none",
borderRadius: "8px",
position: "relative",
bottom: "auto",
right: "auto",
width: "min(900px,92vw)",
// Feste / konsistente Höhe, unabhängig vom Tab-Inhalt
// Wenn Viewport kleiner ist, begrenze auf 80vh
height: "min(640px, 80vh)",
maxHeight: "80vh",
padding: 0,
border: "1px solid var(--color-border)",
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">
<h2 className="text-base font-bold">Einstellungen {slot + 1}</h2>
<button onClick={onClose} className="text-2xl hover:text-gray-200">
<i className="bi bi-x-circle-fill"></i>
<div className="modal-header">
<h2 className="text-sm font-semibold tracking-wide text-fg">
Einstellungen {slot + 1}
</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>
</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: "TDR ", key: "tdr" as const },
{ label: "TDR", key: "tdr" as const },
{ label: "KVz", key: "kvz" as const },
{ label: "Knotenpunkte", key: "knoten" as const },
].map(({ label, key }) => (
<button
key={key}
onClick={() => setActiveTab(key)}
className={`px-4 py-1 rounded-t font-bold text-sm ${
activeTab === key
? "bg-white text-littwin-blue"
: "text-gray-500 hover:text-black"
}`}
>
{label}
</button>
))}
].map(({ label, key }) => {
const isActive = activeTab === key;
return (
<button
key={key}
type="button"
onClick={() => setActiveTab(key)}
className={`tab-btn ${isActive ? "tab-btn-active" : ""}`}
aria-current={isActive ? "page" : undefined}
>
{label}
</button>
);
})}
</div>
<div className="p-4 bg-white rounded-b-md h-[20rem] laptop:h-[24rem] 2xl:h-[30rem] overflow-y-auto">
{/* Einheitliche Body-Höhe mit internem Scroll statt dynamischer Außenhöhe */}
<div className="modal-body-scroll px-5 py-4 flex-1 text-fg overflow-y-auto">
{activeTab === "kue" && (
<KueEinstellung
slot={slot}

View File

@@ -14,22 +14,22 @@ export default function MeldungenTabelle({
}) {
return (
<div className="overflow-auto max-h-[80vh]">
<table className="min-w-full border">
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10">
<table className="min-w-full border border-base table-surface text-fg">
<thead className="text-left sticky top-0 z-10 bg-surface-alt/90 backdrop-blur supports-[backdrop-filter]:bg-surface-alt/70">
<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
</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
</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
</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
</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
</th>
</tr>
@@ -38,15 +38,15 @@ export default function MeldungenTabelle({
{messages.map((msg, index) => (
<tr
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
className="w-4 h-4 rounded"
style={{ backgroundColor: msg.c }}
></div>
</td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<td className="border border-base p-2 bg-surface text-fg">
{new Date(msg.t).toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
@@ -56,13 +56,13 @@ export default function MeldungenTabelle({
second: "2-digit",
})}
</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}
</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}
</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}
</td>
</tr>
@@ -70,7 +70,7 @@ export default function MeldungenTabelle({
</tbody>
</table>
{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.
</div>
)}

View File

@@ -60,14 +60,14 @@ export default function MeldungenView() {
/>
<button
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
</button>
<Listbox value={sourceFilter} onChange={setSourceFilter}>
<div className="relative ml-6 w-64">
<Listbox.Button className="bg-white text-gray-900 w-full border px-4 py-2 rounded text-left flex justify-between items-center dark:bg-gray-900 dark:text-gray-100">
<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>
<svg
className="w-5 h-5 text-gray-400"
@@ -83,19 +83,19 @@ export default function MeldungenView() {
/>
</svg>
</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) => (
<Listbox.Option
key={src}
value={src}
className={({ selected, active, disabled }) =>
`px-4 py-2 cursor-pointer text-gray-900 dark:text-gray-100 ${
selected
? "bg-littwin-blue text-white"
`px-4 py-2 cursor-pointer text-[var(--color-fg)] transition-colors ${
disabled
? "opacity-50 text-[var(--color-muted)] cursor-not-allowed"
: selected
? "bg-accent text-white"
: active
? "bg-blue-100 dark:bg-gray-700 dark:text-white"
: disabled
? "opacity-50 text-gray-400 dark:text-gray-500 cursor-not-allowed"
? "bg-[var(--color-surface-alt)]"
: ""
}`
}

View File

@@ -10,14 +10,14 @@ import { useAdminAuth } from "./hooks/useAdminAuth";
const DatabaseSettings: React.FC = () => {
const { isAdminLoggedIn } = useAdminAuth(true);
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>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<button
type="button"
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
</button>
@@ -25,7 +25,7 @@ const DatabaseSettings: React.FC = () => {
<button
type="button"
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
</button>
@@ -41,7 +41,7 @@ const DatabaseSettings: React.FC = () => {
<button
type="button"
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
</button>
@@ -49,7 +49,7 @@ const DatabaseSettings: React.FC = () => {
<button
type="button"
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
</button>

View File

@@ -63,8 +63,11 @@ const GeneralSettings: React.FC = () => {
setMac1(systemSettings.mac1 || "");
}, [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 (
<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">
Allgemeine Einstellungen
</h2>
@@ -74,25 +77,18 @@ const GeneralSettings: React.FC = () => {
<label className="block text-xs md:text-sm font-medium">Name:</label>
<input
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}
onChange={(e) => setName(e.target.value)}
/>
</div>
{/* MAC Adresse */}
<div>
<label className="block text-xs md:text-sm font-medium">
MAC Adresse 1:
</label>
<input
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
/>
<input type="text" className={inputCls} value={mac1} disabled />
</div>
{/* Systemzeit */}
<div className="col-span-2">
<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">
<input
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$/, "")}
disabled
/>
<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()}
>
Systemzeit übernehmen
@@ -120,7 +116,7 @@ const GeneralSettings: React.FC = () => {
<label className="block text-xs md:text-sm font-medium">IP:</label>
<input
type="text"
className="border border-gray-300 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}
onChange={(e) => setIp(e.target.value)}
/>
@@ -131,7 +127,7 @@ const GeneralSettings: React.FC = () => {
</label>
<input
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}
onChange={(e) => setSubnet(e.target.value)}
/>
@@ -142,7 +138,7 @@ const GeneralSettings: React.FC = () => {
</label>
<input
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}
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">
<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()}
>
Neustart CPL
@@ -160,7 +156,7 @@ const GeneralSettings: React.FC = () => {
{isAdminLoggedIn && (
<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={() => {
const confirmed = window.confirm(
"⚠️ Wollen Sie wirklich ein Firmwareupdate für alle KÜ-Module starten?"

View File

@@ -27,7 +27,7 @@ const NTPSettings: React.FC = () => {
}
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>
<div className="grid md:grid-cols-2 gap-3">
@@ -35,7 +35,7 @@ const NTPSettings: React.FC = () => {
<label className="block text-xs font-medium">NTP Server 1</label>
<input
type="text"
className="border border-gray-300 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}
onChange={(e) => setNtp1(e.target.value)}
/>
@@ -45,7 +45,7 @@ const NTPSettings: React.FC = () => {
<label className="block text-xs font-medium">NTP Server 2</label>
<input
type="text"
className="border border-gray-300 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}
onChange={(e) => setNtp2(e.target.value)}
/>
@@ -55,7 +55,7 @@ const NTPSettings: React.FC = () => {
<label className="block text-xs font-medium">NTP Server 3</label>
<input
type="text"
className="border border-gray-300 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}
onChange={(e) => setNtp3(e.target.value)}
/>
@@ -65,7 +65,7 @@ const NTPSettings: React.FC = () => {
<label className="block text-xs font-medium">Zeitzone</label>
<input
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}
onChange={(e) => setNtpTimezone(e.target.value)}
/>

View File

@@ -21,7 +21,7 @@ export default function OPCUAInterfaceSettings() {
);
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">
<Image
src="/images/OPCUA.jpg"
@@ -44,9 +44,11 @@ export default function OPCUAInterfaceSettings() {
<label className="mr-3 font-medium text-sm">Server Status:</label>
<button
onClick={() => dispatch(toggleOpcUaServer())}
className={`px-3 py-1 rounded text-sm ${
opcuaSettings.isEnabled ? "bg-littwin-blue" : "bg-gray-300"
} text-white`}
className={`px-3 py-1 rounded text-sm font-medium transition-colors text-white ${
opcuaSettings.isEnabled
? "bg-accent hover:brightness-110"
: "bg-[var(--color-muted)] hover:bg-[var(--color-fg)]/20"
}`}
>
{opcuaSettings.isEnabled ? "Aktiviert" : "Deaktiviert"}
</button>
@@ -62,7 +64,7 @@ export default function OPCUAInterfaceSettings() {
<select
value={opcuaSettings.encryption}
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="Basic256">Basic256</option>
@@ -74,7 +76,7 @@ export default function OPCUAInterfaceSettings() {
{/* ✅ OPCUA Zustand */}
<div className="mb-3">
<label className="block font-medium text-sm mb-1">OPCUA Zustand</label>
<div className="p-1 border border-gray-300 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}
</div>
</div>
@@ -85,7 +87,7 @@ export default function OPCUAInterfaceSettings() {
<div className="flex">
<input
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}
onChange={(e) => setNodesetName(e.target.value)}
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">
Aktuelle OPC-Clients
</label>
<div className="p-1 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-sm text-gray-900 dark:text-gray-100">
<div className="p-1 border border-base rounded-md bg-[var(--color-surface)] text-sm text-[var(--color-fg)]">
{opcUaActiveClientCount}
</div>
</div>
@@ -120,12 +122,12 @@ export default function OPCUAInterfaceSettings() {
{opcuaSettings.users.map((user) => (
<li
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>
<button
onClick={() => dispatch(removeOpcUaUser(user.id))}
className="text-red-500"
className="text-danger hover:underline"
>
Löschen
</button>
@@ -140,18 +142,18 @@ export default function OPCUAInterfaceSettings() {
placeholder="Benutzername"
value={newUsername}
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
type="password"
placeholder="Passwort"
value={newPassword}
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
onClick={handleAddUser}
className="bg-littwin-blue text-white p-1 rounded text-sm"
className="btn-primary p-1 rounded text-sm"
>
Hinzufügen
</button>

View File

@@ -22,6 +22,12 @@ const UserManagementSettings: React.FC = () => {
() => {
setLoginSuccess(true);
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) => {
setLoginSuccess(false);
@@ -38,7 +44,7 @@ const UserManagementSettings: React.FC = () => {
};
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>
{/* Admin Login/Logout */}
@@ -46,8 +52,15 @@ const UserManagementSettings: React.FC = () => {
{isAdminLoggedIn ? (
<button
type="button"
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
onClick={logoutAdmin}
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
onClick={() => {
try {
localStorage.removeItem("adminLoginTime");
} catch {
// ignore
}
logoutAdmin();
}}
>
Admin abmelden
</button>
@@ -57,7 +70,7 @@ const UserManagementSettings: React.FC = () => {
<input
type="text"
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}
onChange={(e) => setUsername(e.target.value)}
onKeyDown={handleKeyDown}
@@ -65,14 +78,14 @@ const UserManagementSettings: React.FC = () => {
<input
type="password"
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}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={handleKeyDown}
/>
<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}
>
Admin anmelden
@@ -83,9 +96,9 @@ const UserManagementSettings: React.FC = () => {
</div>
{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)

View File

@@ -13,7 +13,7 @@ const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 ">
<div className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 p-6 rounded shadow-md text-center w-80">
<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">
Firmwareupdate
@@ -26,9 +26,9 @@ const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
</h2>
Bitte Fenster nicht schließen
<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
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}%` }}
></div>
</div>

View File

@@ -27,6 +27,10 @@ import {
Legend,
Filler,
TimeScale,
type ChartDataset,
type ChartOptions,
type ChartData,
type Chart,
} from "chart.js";
import "chartjs-adapter-date-fns";
@@ -65,7 +69,7 @@ type ReduxDataEntry = {
m?: number; // aktueller Messwert (optional, falls vorhanden)
};
const chartOptions = {
const chartOptions: ChartOptions<"line"> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
@@ -78,9 +82,11 @@ const chartOptions = {
mode: "index" as const,
intersect: false,
callbacks: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
label: function (ctx: any) {
return `Messwert: ${ctx.parsed.y}`;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
title: function (items: any[]) {
const date = items[0].parsed.x;
return `Zeitpunkt: ${new Date(date).toLocaleString("de-DE")}`;
@@ -140,13 +146,13 @@ export const DetailModal = ({
}: Props) => {
// Stable empty reference to avoid React-Redux dev warning about selector returning new [] each call
const EMPTY_REDUX_DATA: ReadonlyArray<ReduxDataEntry> = Object.freeze([]);
const chartRef = useRef<any>(null);
const [chartData, setChartData] = useState<any>({
const chartRef = useRef<Chart<"line"> | null>(null);
const [chartData, setChartData] = useState<ChartData<"line">>({
datasets: [],
});
const [isLoading, setIsLoading] = 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) => {
switch (selectedKey) {
@@ -156,7 +162,7 @@ export const DetailModal = ({
return state.systemspannung15Vplus[zeitraum];
case "-15V":
return state.systemspannung15Vminus[zeitraum];
case "-98V":
case "-96V":
return state.systemspannung98Vminus[zeitraum];
case "ADC Temp":
return state.temperaturAdWandler[zeitraum];
@@ -192,7 +198,7 @@ export const DetailModal = ({
case "-15V":
dispatch(getSystemspannung15VminusThunk(zeitraum));
break;
case "-98V":
case "-96V":
dispatch(getSystemspannung98VminusThunk(zeitraum));
break;
case "ADC Temp":
@@ -221,11 +227,8 @@ export const DetailModal = ({
// Periodische UI-Updates alle 2 Sekunden während Wartezeit
useEffect(() => {
if (isOpen && (!chartData.datasets || chartData.datasets.length === 0)) {
const interval = setInterval(() => {
setForceUpdate((prev) => prev + 1); // Force re-render für cursor-wait Update
}, 2000);
return () => clearInterval(interval);
// Optional: periodische Re-Renders wurden deaktiviert, da nicht mehr notwendig
// (kann wieder aktiviert werden falls Cursor-Animation erwünscht ist)
}
}, [isOpen, chartData.datasets]);
@@ -274,7 +277,12 @@ export const DetailModal = ({
useEffect(() => {
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");
}
}, [selectedKey]);
@@ -290,14 +298,13 @@ export const DetailModal = ({
if (chartRef.current && isLoading) {
const chartInstance = chartRef.current;
// Save previous callback to restore later
const prevCallback = chartInstance.options.animation?.onComplete;
chartInstance.options.animation = {
...chartInstance.options.animation,
onComplete: () => {
setIsLoading(false);
if (typeof prevCallback === "function") prevCallback();
},
const animation: any = chartInstance.options.animation || {}; // eslint-disable-line @typescript-eslint/no-explicit-any
const prevCallback = animation.onComplete;
animation.onComplete = () => {
setIsLoading(false);
if (typeof prevCallback === "function") prevCallback();
};
chartInstance.options.animation = animation;
chartInstance.update();
}
}, [chartData, isLoading]);
@@ -338,7 +345,7 @@ export const DetailModal = ({
}
// Create datasets array for multiple lines
const datasets = [];
const datasets: ChartDataset<"line">[] = [];
// Check which data fields are available and create datasets accordingly
const hasMinimum = filtered.some(
@@ -416,7 +423,8 @@ export const DetailModal = ({
});
}
const newChartData = {
const newChartData: ChartData<"line"> = {
labels: [],
datasets: datasets,
};
@@ -428,7 +436,13 @@ export const DetailModal = ({
setChartData({ datasets: [] });
setShouldUpdateChart(false); // Reset flag
}
}, [reduxData, selectedKey, shouldUpdateChart]);
}, [
reduxData,
selectedKey,
shouldUpdateChart,
pickerVonDatum,
pickerBisDatum,
]);
if (!isOpen || !selectedKey) return null;
@@ -442,19 +456,25 @@ export const DetailModal = ({
}`}
>
<div
className={`bg-white p-6 rounded-xl overflow-auto shadow-2xl transition-all duration-300 ${
isFullScreen ? "w-[95vw] h-[90vh]" : "w-[50%] h-[60%]"
role="dialog"
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" : ""}`}
>
<div className="relative">
<h2 className="text-xl font-semibold">
{/* Header */}
<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}
</h2>
<div className="absolute top-0 right-0 flex gap-3">
<div className="flex items-center gap-3 text-lg">
<button
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
className={
@@ -462,28 +482,35 @@ export const DetailModal = ({
? "bi bi-fullscreen-exit"
: "bi bi-arrows-fullscreen"
}
></i>
/>
</button>
<button
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>
</div>
</div>
</header>
<SystemChartActionBar
zeitraum={zeitraum}
setZeitraum={setZeitraum}
onFetchData={handleFetchData}
isLoading={isLoading}
/>
<div className="h-[85%] bg-white dark:bg-gray-800 rounded shadow border border-gray-200 dark:border-gray-700 p-2">
<Line ref={chartRef} data={chartData} options={chartOptions} />
{/* Body */}
<div className="flex-1 min-h-0 flex flex-col px-6 pt-4 pb-5 bg-[var(--color-surface)] overflow-hidden">
<div className="mb-3">
<SystemChartActionBar
zeitraum={zeitraum}
setZeitraum={setZeitraum}
onFetchData={handleFetchData}
isLoading={isLoading}
className="mb-0"
/>
</div>
<div className="flex-1 min-h-0 rounded-lg border border-base bg-[var(--color-surface-alt)] px-3 py-2 shadow-inner">
<Line ref={chartRef} data={chartData} options={chartOptions} />
</div>
</div>
{/* Optional Footer (currently empty, reserved for future) */}
</div>
</div>
);

View File

@@ -30,7 +30,7 @@ const SystemChartActionBar: React.FC<Props> = ({
<label className="font-medium text-sm">Zeitraum:</label>
<Listbox value={zeitraum} onChange={setZeitraum}>
<div className="relative w-48">
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
<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" }[
@@ -39,7 +39,7 @@ const SystemChartActionBar: React.FC<Props> = ({
}
</span>
<svg
className="w-5 h-5 text-gray-400"
className="w-5 h-5 text-[var(--color-fg-muted)]"
viewBox="0 0 20 20"
fill="currentColor"
>
@@ -50,20 +50,18 @@ const SystemChartActionBar: React.FC<Props> = ({
/>
</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">
<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 }) =>
`px-4 py-1 cursor-pointer ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
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`;
}}
>
{
{

View File

@@ -28,7 +28,7 @@ export type HistoryEntry = {
"+5V": number;
"+15V": number;
"-15V": number;
"-98V": number;
"-96V": number;
"ADC Temp": number;
"CPU Temp": number;
};
@@ -110,8 +110,8 @@ export const SystemCharts = ({ history }: Props) => {
fill: false,
},
{
label: "-98V",
data: history.map((h) => formatValue(h["-98V"])),
label: "-96V",
data: history.map((h) => formatValue(h["-96V"])),
borderColor: "rgba(234,179,8,1)",
backgroundColor: "rgba(234,179,8,0.5)",
fill: false,

View File

@@ -16,7 +16,7 @@ export const SystemOverviewGrid = ({ voltages, onOpenDetail }: Props) => {
return (
<div
key={key}
className="p-4 border rounded shadow bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100"
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>
<p>

View File

@@ -53,7 +53,7 @@ const SystemPage = () => {
case "-15V":
dispatch(getSystemspannung15VminusThunk(zeitraum));
break;
case "-98V":
case "-96V":
dispatch(getSystemspannung98VminusThunk(zeitraum));
break;
case "ADC Temp":
@@ -71,8 +71,8 @@ const SystemPage = () => {
};
return (
<div className="p-4 bg-white dark:bg-gray-900">
<h1 className="text-xl font-bold mb-4">
<div className="p-4 bg-[var(--color-background)] text-[var(--color-fg)]">
<h1 className="text-xl font-bold mb-4 tracking-wide">
System Spannungen & Temperaturen
</h1>
@@ -80,7 +80,7 @@ const SystemPage = () => {
<div className="flex justify-center items-center h-[400px]">
<div className="text-center">
<ClipLoader size={50} color="#3B82F6" />
<p className="mt-4 text-gray-500">
<p className="mt-4 text-[var(--color-fg-muted)]">
Lade Systemdaten bitte warten
</p>
</div>

View File

@@ -36,12 +36,12 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
];
return (
<aside className="bg-white dark:bg-gray-900 h-full">
<nav className={`h-full flex-shrink-0 mt-16 ${className || "w-48"}`}>
<aside className="h-full bg-[var(--color-surface)] dark:bg-[var(--color-surface)] ">
<nav className={`h-full flex-shrink-0 mt-24 ${className || "w-48"}`}>
{menuItems.map((item) => (
<div key={item.name}>
{item.disabled ? (
<div className="block px-4 py-2 mb-4 font-bold whitespace-nowrap text-gray-400 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}
</div>
) : (
@@ -49,11 +49,13 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
href={formatPath(item.path)}
prefetch={false}
onClick={() => setActiveLink(item.path)}
className={`block px-4 py-2 mb-4 font-bold whitespace-nowrap transition duration-300 text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg ${
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)
? "bg-sky-500 text-white rounded-r-full xl:mr-4 xl:w-full dark:bg-sky-600 dark:text-white"
: "text-black hover:bg-gray-200 rounded-r-full dark:text-gray-200 dark:hover:bg-gray-800"
}`}
? "bg-[var(--color-accent)] text-white shadow-sm xl:mr-4 xl:w-full"
: "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}
</Link>

View File

@@ -87,8 +87,7 @@ in Rot, wenn Schleifenfehler ansteht
- [x] TODO: RSL starten in RSL Messung starten umbenennen
- [x] TODO: TDR-Messung starten statt TDR aktivieren in ChartBar
- [x] TODO: KÜ TDR-aktiviert alert entfernen
- [ ] TODO: Systemdaten unter Detailansicht ein Verlaufsdiagramm hinzufügen mit Datumsauswahl
- [ ] TODO: Playwright testen mit der Entwicklung
- [x] TODO: Systemdaten unter Detailansicht ein Verlaufsdiagramm hinzufügen mit Datumsauswahl
# Kai Schmidt:
@@ -98,10 +97,26 @@ in Rot, wenn Schleifenfehler ansteht
[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.
[ ] TODO: Admin User nach einer Zeit von einer Stunde löschen (Cookie oder Local Storrage)
[x] TODO: lange Modulnamen bei KÜ ermöglichen (48 Zeichen) bei Version ab V4.30. Laufschrift möglich?
[ ] 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

View File

@@ -5,7 +5,7 @@ Meine Tabelle ist falsch. Ich werde sie anpassen. Korrekt ist:
108: +15V
110: +5V
114: -15V
115: -98V
115: -96V
116: Temperatur AD Wandler
117: Temperatur Prozessor
------------------------------------

View File

@@ -8,7 +8,7 @@ In der **Systemseite** werden die aktuellen **Versorgungsspannungen** und **Temp
Die Seite zeigt:
- **Live-Werte** aller Spannungen (+5V, +15V, -15V, -98V)
- **Live-Werte** aller Spannungen (+5V, +15V, -15V, -96V)
- **Temperaturen** von CPU und ADC
- **Verlauf** der Werte in einem **Liniendiagramm**

View File

@@ -8,7 +8,7 @@ In der **Systemseite** werden die aktuellen **Versorgungsspannungen** und **Temp
Die Seite zeigt:
- **Live-Werte** aller Spannungen (+5V, +15V, -15V, -98V)
- **Live-Werte** aller Spannungen (+5V, +15V, -15V, -96V)
- **Temperaturen** von CPU und ADC
- **Verlauf** der Werte in einem **Liniendiagramm**

View File

@@ -2,7 +2,7 @@
"win_da_state": [
1,
1,
1,
0,
1
],
"win_da_bezeichnung": [

View File

@@ -269,7 +269,12 @@ var tdrMeasurementEvent = [
0, 0, 0, 0, 0, 0,
];
//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,
];
// expose for browser simulation
if (typeof window !== "undefined") {
window.comparisonEvent = comparisonEvent;
}

View File

@@ -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
ae11.value=system[1]; //5V

View File

@@ -1,6 +1,6 @@
108: +15V
110: +5V
114: -15V
115: -98V
115: -96V
116: Temperatur AD Wandler
117: Temperatur Prozessor

View File

@@ -1,21 +1,203 @@
{
"kvzPresence": [
1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"kvzActive": [
1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"kvzStatus": [
1, 0, 2, 1, 2, 0, 1, 0, 2, 1, 0, 2, 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, 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
1,
0,
2,
1,
2,
0,
1,
0,
2,
1,
0,
2,
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,
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
],
"timestamp": "2025-07-31T11:39:22.951Z",
"timestamp": "2025-09-10T06:37:13.443Z",
"description": {
"kvzPresence": "32 Slots: 1=KVZ Gerät vorhanden, 0=nicht vorhanden. Slots 0,2 haben KVZ-Geräte",
"kvzActive": "32 Slots: 1=KVZ aktiviert, 0=deaktiviert. Nur Slot 0 ist aktiviert",

View File

@@ -4,7 +4,7 @@
Dieses Script lädt sowohl Systemdaten (z.B. Spannungen und Temperaturen) als auch die Messdaten der 8 analogen Eingänge eines CPL-Geräts
für die letzten 30 Tage per HTTP/HTTPS-API herunter und speichert sie als Mockdaten im lokalen Dateisystem.
- Systemdaten: Für die Inputs 108 (+15V), 110 (+5V), 114 (-15V), 115 (-98V), 116 (Temperatur AD Wandler), 117 (Temperatur Prozessor)
- Systemdaten: Für die Inputs 108 (+15V), 110 (+5V), 114 (-15V), 115 (-96V), 116 (Temperatur AD Wandler), 117 (Temperatur Prozessor)
werden die Daten für die DIA-Typen DIA0, DIA1, DIA2 jeweils in das Verzeichnis
mocks/device-cgi-simulator/chartsData/<systemVerzeichnis>/DIAx.json geschrieben.

555
package-lock.json generated
View File

@@ -1,13 +1,15 @@
{
"name": "cpl-v4",
"version": "1.6.855",
"version": "1.6.913",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cpl-v4",
"version": "1.6.855",
"version": "1.6.913",
"dependencies": {
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.1.0",
"@headlessui/react": "^2.2.4",
"@iconify-icons/ri": "^1.2.10",
@@ -15,6 +17,7 @@
"@iconify/icons-mdi": "^1.2.48",
"@iconify/json": "^2.2.253",
"@iconify/react": "^5.0.2",
"@mui/material": "^6.0.0",
"@reduxjs/toolkit": "^2.3.0",
"autoprefixer": "^10.4.20",
"bcryptjs": "^2.4.3",
@@ -112,7 +115,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
@@ -174,7 +176,6 @@
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz",
"integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.27.5",
"@babel/types": "^7.27.3",
@@ -215,7 +216,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"dev": true,
"dependencies": {
"@babel/traverse": "^7.27.1",
"@babel/types": "^7.27.1"
@@ -254,7 +254,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -263,7 +262,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -294,7 +292,6 @@
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
"dev": true,
"dependencies": {
"@babel/types": "^7.27.3"
},
@@ -539,7 +536,6 @@
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.2",
@@ -553,7 +549,6 @@
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
"integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.3",
@@ -571,7 +566,6 @@
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
"integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
@@ -644,6 +638,167 @@
"tslib": "^2.4.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.3.3",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"license": "MIT"
},
"node_modules/@emotion/babel-plugin/node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0"
}
},
"node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/react": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"license": "MIT"
},
"node_modules/@emotion/styled": {
"version": "11.14.1",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/is-prop-valid": "^1.3.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2"
},
"peerDependencies": {
"@emotion/react": "^11.0.0-rc.0",
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/unitless": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
"license": "MIT"
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
"license": "MIT"
},
"node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
@@ -1374,7 +1529,6 @@
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@@ -1388,7 +1542,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
@@ -1397,7 +1550,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
@@ -1405,14 +1557,12 @@
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -1423,6 +1573,222 @@
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="
},
"node_modules/@mui/core-downloads-tracker": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.5.0.tgz",
"integrity": "sha512-LGb8t8i6M2ZtS3Drn3GbTI1DVhDY6FJ9crEey2lZ0aN2EMZo8IZBZj9wRf4vqbZHaWjsYgtbOnJw5V8UWbmK2Q==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/material": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.5.0.tgz",
"integrity": "sha512-yjvtXoFcrPLGtgKRxFaH6OQPtcLPhkloC0BML6rBG5UeldR0nPULR/2E2BfXdo5JNV7j7lOzrrLX2Qf/iSidow==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/core-downloads-tracker": "^6.5.0",
"@mui/system": "^6.5.0",
"@mui/types": "~7.2.24",
"@mui/utils": "^6.4.9",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^19.0.0",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^6.5.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@mui/material-pigment-css": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/react-is": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
"integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==",
"license": "MIT"
},
"node_modules/@mui/private-theming": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.9.tgz",
"integrity": "sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/utils": "^6.4.9",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/styled-engine": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.5.0.tgz",
"integrity": "sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@emotion/cache": "^11.13.5",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/system": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.5.0.tgz",
"integrity": "sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/private-theming": "^6.4.9",
"@mui/styled-engine": "^6.5.0",
"@mui/types": "~7.2.24",
"@mui/utils": "^6.4.9",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/types": {
"version": "7.2.24",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz",
"integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.9.tgz",
"integrity": "sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/types": "~7.2.24",
"@types/prop-types": "^15.7.14",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^19.0.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils/node_modules/react-is": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
"integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==",
"license": "MIT"
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz",
@@ -1681,6 +2047,16 @@
"node": ">=18"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@react-aria/focus": {
"version": "3.20.5",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.5.tgz",
@@ -2236,17 +2612,21 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"devOptional": true
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="
},
"node_modules/@types/react": {
"version": "18.3.23",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -2270,6 +2650,15 @@
"@types/react": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/redux-mock-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.5.0.tgz",
@@ -3347,6 +3736,21 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/babel-preset-current-node-syntax": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz",
@@ -3574,7 +3978,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"engines": {
"node": ">=6"
}
@@ -3825,6 +4228,31 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"license": "MIT",
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/cosmiconfig/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/create-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
@@ -4143,7 +4571,6 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"dependencies": {
"ms": "^2.1.3"
},
@@ -4418,7 +4845,6 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dev": true,
"dependencies": {
"is-arrayish": "^0.2.1"
}
@@ -4600,6 +5026,18 @@
"node": ">=6"
}
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
@@ -5007,18 +5445,6 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
"node_modules/eslint/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -5359,6 +5785,12 @@
"node": ">=8"
}
},
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
"license": "MIT"
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -5514,7 +5946,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -5689,7 +6120,6 @@
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true,
"engines": {
"node": ">=4"
}
@@ -5826,7 +6256,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -5834,6 +6263,21 @@
"node": ">= 0.4"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/hoist-non-react-statics/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
@@ -5945,7 +6389,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -5961,7 +6404,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"engines": {
"node": ">=4"
}
@@ -6062,8 +6504,7 @@
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
"dev": true
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
},
"node_modules/is-async-function": {
"version": "2.1.1",
@@ -6152,7 +6593,6 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"dependencies": {
"hasown": "^2.0.2"
},
@@ -7652,7 +8092,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"bin": {
"jsesc": "bin/jsesc"
},
@@ -7669,8 +8108,7 @@
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
@@ -7832,8 +8270,7 @@
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/locate-path": {
"version": "5.0.0",
@@ -8578,7 +9015,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"dependencies": {
"callsites": "^3.0.0"
},
@@ -8590,7 +9026,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
@@ -8646,8 +9081,7 @@
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-scurry": {
"version": "1.11.1",
@@ -8671,6 +9105,15 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
@@ -9456,7 +9899,6 @@
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
@@ -10190,6 +10632,12 @@
}
}
},
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -10281,7 +10729,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},

View File

@@ -1,6 +1,6 @@
{
"name": "cpl-v4",
"version": "1.6.855",
"version": "1.6.913",
"private": true,
"scripts": {
"dev": "next dev -p 3000",
@@ -71,7 +71,10 @@
"react-spinners": "^0.14.1",
"react-toastify": "^10.0.6",
"recharts": "^2.15.1",
"redux": "^5.0.1"
"redux": "^5.0.1",
"@mui/material": "^6.0.0",
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0"
},
"devDependencies": {
"@playwright/test": "^1.54.2",

View File

@@ -13,7 +13,6 @@ import Footer from "@/components/footer/Footer";
import { store } from "@/redux/store";
import Script from "next/script";
import DeviceEventsBridge from "@/components/common/DeviceEventsBridge";
import { usePathname } from "next/navigation";
// Thunks importieren
import { getKueDataThunk } from "@/redux/thunks/getKueDataThunk";
@@ -39,22 +38,36 @@ if (typeof window !== "undefined") {
}
import "@/styles/globals.css";
import CssBaseline from "@mui/material/CssBaseline";
import { ThemeProvider } from "@mui/material/styles";
import { muiTheme, buildTheme } from "@/styles/muiTheme";
function MyApp({ Component, pageProps }: AppProps) {
// Rebuild theme on client if dark mode toggles (simple example)
const [theme, setTheme] = useState(muiTheme);
useEffect(() => {
const observer = new MutationObserver(() => setTheme(buildTheme()));
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
}, []);
return (
<Provider store={store}>
{/* Load global data: dev -> API mock JS; prod -> real device JS from public */}
{process.env.NODE_ENV === "development" ? (
<Script
src="/api/cpl/kabelueberwachungAPIHandler"
strategy="afterInteractive"
/>
) : (
<Script src="/CPL/SERVICE/kueData.js" strategy="afterInteractive" />
)}
<AppContent Component={Component} pageProps={pageProps} />
{/* Bridge window events -> Redux (works across all pages) */}
<DeviceEventsBridge />
<ThemeProvider theme={theme}>
<CssBaseline />
{process.env.NODE_ENV === "development" ? (
<Script
src="/api/cpl/kabelueberwachungAPIHandler"
strategy="afterInteractive"
/>
) : (
<Script src="/CPL/SERVICE/kueData.js" strategy="afterInteractive" />
)}
<AppContent Component={Component} pageProps={pageProps} />
<DeviceEventsBridge />
</ThemeProvider>
</Provider>
);
}
@@ -67,7 +80,6 @@ function AppContent({
pageProps: AppProps["pageProps"];
}): JSX.Element {
const dispatch = useAppDispatch();
const pathnameHook = usePathname();
const [sessionExpired] = useState(false);
const mode = "DIA0"; // oder aus Router oder Session
const type = 0; // Beispiel: 0 für "loop", 1 für "iso" (bitte ggf. anpassen)
@@ -148,11 +160,11 @@ function AppContent({
}, [pathname, dispatch]);
return (
<div className="flex flex-col h-screen overflow-hidden bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<div className="flex flex-col h-screen overflow-hidden bg-[var(--color-background)] text-[var(--color-fg)]">
<Header />
<div className="flex flex-grow w-full">
<Navigation className="w-56" />
<main className="w-full flex-grow bg-white dark:bg-gray-900">
<Navigation className="w-56 mr-8" />
<main className="w-full flex-grow bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border border-base rounded-lg m-1 p-1 overflow-auto relative">
{sessionExpired && (
<div className="bg-red-500 text-white p-4 text-center">
Ihre Sitzung ist abgelaufen oder die Verbindung ist

View File

@@ -8,6 +8,13 @@ export default function Document() {
<link rel="icon" href="/favicon.png" type="image/png" />
</Head>
<body>
{/* Theme init (executed before React hydration) */}
<script
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: `(() => { try { const ls = localStorage.getItem('theme'); const mql = window.matchMedia('(prefers-color-scheme: dark)'); const wantDark = ls === 'dark' || (!ls && mql.matches); if (wantDark) document.documentElement.classList.add('dark'); } catch(e) {} })();`,
}}
/>
<Main /> {/* Hier wird der Seiteninhalt eingebettet */}
<NextScript /> {/* Fügt Next.js-Skripte für die Seite hinzu */}
</body>

View File

@@ -1,56 +1,27 @@
import type { Page } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
import { Page, expect } from "@playwright/test";
/**
* Footer assertions.
*/
export async function footerTest(page: Page) {
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^Littwin Systemtechnik GmbH & Co\. KG$/ })
.locator("svg")
);
await highlightAndExpectVisible(
page,
page.getByText("Littwin Systemtechnik GmbH &")
);
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^Telefon: 04402 972577-0$/ })
.locator("svg")
);
await highlightAndExpectVisible(
page,
page.getByText("Telefon: 04402 972577-")
);
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^kontakt@littwin-systemtechnik\.de$/ })
.locator("svg")
);
await highlightAndExpectVisible(
page,
page.getByText("kontakt@littwin-systemtechnik")
);
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^Handbücher$/ })
.locator("svg")
);
await highlightAndExpectVisible(
page,
page.getByText("Handbücher", { exact: true })
);
// Auf Footer-Bereich einschränken, damit Selektoren eindeutig bleiben
const footer = page.getByRole("contentinfo");
await expect(footer).toBeVisible();
await expect(
footer.getByText("Littwin Systemtechnik GmbH & Co. KG")
).toBeVisible();
await expect(footer.getByText("Telefon: 04402 972577-0")).toBeVisible();
await expect(
footer.getByText("kontakt@littwin-systemtechnik.de")
).toBeVisible();
// Exaktes Label im Footer, nicht die Überschrift "PDF Handbücher"
await expect(footer.getByText("Handbücher", { exact: true })).toBeVisible();
await page.getByText("Handbücher", { exact: true }).click();
await highlightAndExpectVisible(
page,
await expect(
page.getByRole("heading", { name: "PDF Handbücher" })
);
await highlightAndExpectVisible(page, page.getByText("KUE705FO.PDF"));
await page.getByRole("contentinfo").getByRole("button").click();
).toBeVisible();
await expect(page.getByRole("button", { name: "Schließen" })).toBeVisible();
await expect(page.getByText("KUE705FO.PDF")).toBeVisible();
await page.getByRole("button", { name: "Schließen" }).click();
}

View File

@@ -1,14 +1,22 @@
import type { Page } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
import { Page, expect } from "@playwright/test";
/**
* Reusable header assertions.
* Add more checks here if the header grows (logout button, admin badge etc.).
*/
export async function headerTest(page: Page) {
await highlightAndExpectVisible(
page,
// Haupttitel
await expect(
page.getByRole("heading", { name: "Meldestation" })
);
await highlightAndExpectVisible(page, page.getByRole("banner"));
await highlightAndExpectVisible(
page,
).toBeVisible();
// Logo (alt="Logo")
await expect(
page.getByRole("img", { name: "Logo", exact: true })
);
).toBeVisible();
// Talas Logo
await expect(page.getByRole("img", { name: "TALAS Logo" })).toBeVisible();
// Theme Toggle (Label wechselt Dark/Light). Wir akzeptieren beide.
const darkBtn = page.getByRole("button", { name: /Dark Mode|Light Mode/ });
await expect(darkBtn).toBeVisible();
await expect(page.getByText("CPLV4 Ismail Rastede")).toBeVisible();
}

View File

@@ -1,19 +0,0 @@
import { test, expect } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
test("AnalogInputsChartModal opens after clicking chart button", async ({
page,
}) => {
await page.goto("/analogInputs");
// Öffne Modal via Chart-Button (📈)
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Messkurve anzeigen" })
);
await page
.getByRole("button", { name: "Messkurve anzeigen" })
.first()
.click();
await highlightAndExpectVisible(page, page.getByRole("dialog"));
await expect(page.getByRole("dialog")).toBeVisible();
});

View File

@@ -1,15 +0,0 @@
import { test } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
test("AnalogInputsDatePicker renders two inputs", async ({ page }) => {
await page.goto("/analogInputs");
// Öffne erst die Chart-Ansicht (enthält den DatePicker)
const chartBtn = page
.getByRole("button", { name: "Messkurve anzeigen" })
.first();
await highlightAndExpectVisible(page, chartBtn);
await chartBtn.click();
await highlightAndExpectVisible(page, page.getByRole("dialog"));
await highlightAndExpectVisible(page, page.getByText("Von"));
await highlightAndExpectVisible(page, page.getByText("Bis"));
});

View File

@@ -1,17 +0,0 @@
import { test } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
test("AnalogInputsSettingsModal opens after clicking settings", async ({
page,
}) => {
await page.goto("/analogInputs");
// Wähle die erste Tabellenzeile und in der 5. Spalte (Einstellungen) den Button
const firstRow = page.locator("table tbody tr").first();
const settingsButton = firstRow.locator("td").nth(4).locator("button");
await highlightAndExpectVisible(page, settingsButton);
await settingsButton.click();
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Modal schließen" })
);
});

View File

@@ -1,9 +0,0 @@
import { test } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
test("AnalogInputsTable renders rows", async ({ page }) => {
await page.goto("/analogInputs");
await highlightAndExpectVisible(page, page.getByRole("table"));
// Mindestens eine Tabellenzeile sichtbar
await highlightAndExpectVisible(page, page.locator("tbody tr").first());
});

View File

@@ -1,12 +0,0 @@
import { test, expect } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
test("AnalogInputsView shows heading and table", async ({ page }) => {
await page.goto("/analogInputs");
await highlightAndExpectVisible(
page,
page.getByRole("heading", { name: "Messwerteingänge" })
);
await highlightAndExpectVisible(page, page.getByRole("table"));
await expect(page.getByRole("table")).toBeVisible();
});

View File

@@ -1,5 +0,0 @@
import { test } from "@playwright/test";
test.fixme("XioPM visual presence", async ({ page }) => {
await page.goto("/analogInputs");
});

View File

@@ -1,57 +1,20 @@
import type { Page } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
import { Page, expect } from "@playwright/test";
/**
* Sidebar / Navigation visibility + core links.
*/
export async function navTest(page: Page) {
await highlightAndExpectVisible(
page,
page.getByRole("link", { name: "Übersicht" })
);
await highlightAndExpectVisible(
page,
page.getByRole("link", { name: "Kabelüberwachung" })
);
await highlightAndExpectVisible(
page,
page.getByRole("link", { name: "Meldungseingänge" })
);
await highlightAndExpectVisible(
page,
page.getByRole("link", { name: "Schaltausgänge" })
);
await highlightAndExpectVisible(
page,
page.getByRole("link", { name: "Messwerteingänge" })
);
await highlightAndExpectVisible(
page,
page.getByRole("link", { name: "Berichte" })
);
await highlightAndExpectVisible(
page,
page.getByRole("link", { name: "System" })
);
await highlightAndExpectVisible(
page,
page.getByRole("link", { name: "Einstellungen" })
);
}
/*
// Sidebar Links sichtbar
const sidebarLinks2 = [
{ role: "link", name: "Übersicht" },
{ role: "link", name: "Kabelüberwachung" },
{ role: "link", name: "Meldungseingänge" },
{ role: "link", name: "Schaltausgänge" },
{ role: "link", name: "Messwerteingänge" },
{ role: "link", name: "Berichte" },
{ role: "link", name: "System" },
{ role: "link", name: "Einstellungen" },
const links = [
"Übersicht",
"Kabelüberwachung",
"Meldungseingänge",
"Schaltausgänge",
"Messwerteingänge",
"Berichte",
"System",
"Einstellungen",
];
for (const link of sidebarLinks2) {
const locator = page.getByRole(link.role as any, { name: link.name });
await highlightAndExpectVisible(page, locator);
await expect(locator).toBeVisible();
await page.waitForTimeout(50);
for (const name of links) {
await expect(page.getByRole("link", { name })).toBeVisible();
}
*/
}

View File

@@ -1,20 +0,0 @@
import { test } from "../fixtures";
import { runDashboardTest } from "./pages/dashboard/dashboardTest";
import { runCableMonitoringTest } from "./pages/kabelueberwachung/kabelueberwachungTest";
import { runDigitalInputsTest } from "./pages/digitalInputs/digitalInputsTest";
import { runDigitalOutputsTest } from "./pages/digitalOutputs/digitalOutputsTest";
import { runAnalogInputsTest } from "./pages/analogInputs/analogInputsTest";
import { runMeldungenTest } from "./pages/meldungen/meldungenTest";
import { runSystemTest } from "./pages/system/systemTest";
import { runSettingsPageTest } from "./pages/settingsPage/settingsPageTest";
test("Dashboard, AnalogInputs und SettingsPage", async ({ page }) => {
await runDashboardTest(page);
await runCableMonitoringTest(page);
await runDigitalInputsTest(page);
await runDigitalOutputsTest(page);
await runAnalogInputsTest(page);
await runMeldungenTest(page);
await runSystemTest(page);
await runSettingsPageTest(page);
});

View File

@@ -0,0 +1,139 @@
import { test, expect } from "@playwright/test";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
test.use({
viewport: {
height: 1080,
width: 1920,
},
});
test("analogInputs", async ({ page }) => {
await page.goto("/analogInputs");
// Gemeinsame Layout-Checks
await headerTest(page);
await navTest(page);
await footerTest(page);
// Seitenspezifische Checks
await expect(
page.getByRole("heading", { name: "Messwerteingänge" }).first()
).toBeVisible();
await expect(page.locator(".text-littwin-blue")).toBeVisible();
await expect(
page.getByRole("heading", { name: "Messwerteingänge" }).nth(1)
).toBeVisible();
await expect(page.getByRole("cell", { name: "Eingang" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Messwert" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Einheit" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Bezeichnung" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Einstellungen" })).toBeVisible();
await expect(
page.getByRole("cell", { name: "Messkurve", exact: true })
).toBeVisible();
await expect(
page.getByRole("cell", { name: "1", exact: true })
).toBeVisible();
await expect(
page.getByRole("cell", { name: "2", exact: true })
).toBeVisible();
await expect(
page.getByRole("cell", { name: "3", exact: true })
).toBeVisible();
await expect(
page.getByRole("cell", { name: "4", exact: true })
).toBeVisible();
await expect(
page.getByRole("cell", { name: "5", exact: true })
).toBeVisible();
await expect(
page.getByRole("cell", { name: "6", exact: true })
).toBeVisible();
await expect(
page.getByRole("cell", { name: "7", exact: true })
).toBeVisible();
await expect(
page.getByRole("cell", { name: "8", exact: true })
).toBeVisible();
await expect(page.getByRole("cell", { name: "126.63" })).toBeVisible();
await expect(page.getByRole("cell", { name: "5.67" })).toBeVisible();
await expect(page.getByRole("cell", { name: "-" }).first()).toBeVisible();
await expect(page.getByRole("cell", { name: "AE 1" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Temperatur" })).toBeVisible();
await expect(page.getByRole("cell", { name: "AE 3" })).toBeVisible();
await expect(page.getByRole("cell", { name: "AE 4" })).toBeVisible();
await expect(page.getByRole("cell", { name: "AE 5" })).toBeVisible();
await expect(page.getByRole("cell", { name: "AE 6" })).toBeVisible();
await expect(page.getByRole("cell", { name: "AE 7" })).toBeVisible();
await expect(page.getByRole("cell", { name: "AE 8" })).toBeVisible();
await expect(page.locator(".border.p-2.text-center").first()).toBeVisible();
await expect(page.locator("tr:nth-child(2) > td:nth-child(5)")).toBeVisible();
await expect(page.locator("tr:nth-child(8) > td:nth-child(5)")).toBeVisible();
await expect(page.locator("td:nth-child(6)").first()).toBeVisible();
await expect(page.locator("tr:nth-child(8) > td:nth-child(6)")).toBeVisible();
await page.locator(".border.p-2.text-center").first().click();
await expect(
page.getByRole("button", { name: "Modal schließen" })
).toBeVisible();
await page
.getByRole("heading", { name: "Einstellungen Messwerteingang" })
.click();
await page.getByText("Bezeichnung:").click();
await page.getByText("Offset:").click();
await page.getByText("Faktor:").click();
await page.getByText("Einheit:").click();
await page.getByText("Speicherintervall:").click();
await page.getByRole("textbox").click();
await page
.locator("div")
.filter({ hasText: /^Offset:$/ })
.getByRole("spinbutton")
.click();
await page
.locator("div")
.filter({ hasText: /^Faktor:$/ })
.getByRole("spinbutton")
.click();
await page.getByRole("button", { name: "V", exact: true }).click();
await page.getByRole("button", { name: "V", exact: true }).click();
await page
.locator("div")
.filter({ hasText: /^Minuten$/ })
.getByRole("spinbutton")
.click();
await expect(page.getByRole("button", { name: "Speichern" })).toBeVisible();
await page.getByRole("button", { name: "Modal schließen" }).click();
await page.locator("tr:nth-child(8) > td:nth-child(5)").click();
await expect(
page.getByRole("button", { name: "Modal schließen" })
).toBeVisible();
await page
.getByRole("heading", { name: "Einstellungen Messwerteingang" })
.click();
await page.getByText("Bezeichnung:").click();
await page.getByText("Offset:").click();
await page.getByText("Faktor:").click();
await page.getByText("Einheit:").click();
await page.getByText("Speicherintervall:").click();
await page.getByRole("textbox").click();
await page
.locator("div")
.filter({ hasText: /^Offset:$/ })
.getByRole("spinbutton")
.click();
await page
.locator("div")
.filter({ hasText: /^Faktor:$/ })
.getByRole("spinbutton")
.click();
await page.getByRole("button", { name: "mA" }).click();
await page.getByRole("button", { name: "mA" }).click();
await page
.locator("div")
.filter({ hasText: /^Minuten$/ })
.getByRole("spinbutton")
.click();
await expect(page.getByRole("button", { name: "Speichern" })).toBeVisible();
await page.getByRole("button", { name: "Modal schließen" }).click();
});

View File

@@ -1,254 +0,0 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
// Kombinierte Helper-Funktion: injiziert CSS (nur einmal), hebt hervor und prüft Sichtbarkeit
export async function runAnalogInputsTest(page: Page) {
await page.goto("/analogInputs");
//----------------------
await headerTest(page);
await navTest(page);
await footerTest(page);
await page.waitForTimeout(400);
//----------------------
await highlightAndExpectVisible(
page,
page.getByRole("heading", { name: "Messwerteingänge" }).nth(1)
);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Eingang" })
);
// ...existing code...
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Messwert" })
);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Einheit" })
);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Bezeichnung" })
);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Einstellungen" })
);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Messkurve", exact: true })
);
await highlightAndExpectVisible(page, page.getByText("1", { exact: true }));
await highlightAndExpectVisible(page, page.getByText("2", { exact: true }));
await highlightAndExpectVisible(page, page.getByText("3", { exact: true }));
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "4", exact: true }).locator("path")
);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "5", exact: true })
);
await highlightAndExpectVisible(page, page.getByText("6", { exact: true }));
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "7", exact: true })
);
await expect(
page.getByRole("cell", { name: "8", exact: true })
).toBeVisible();
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "8", exact: true })
);
await expect(page.locator(".border.p-2.text-center").first()).toBeVisible();
await highlightAndExpectVisible(
page,
page.locator(".border.p-2.text-center").first()
);
// Markiere die gesamte erste Datenzeile (Row mit "AE 1" falls vorhanden)
await highlightAndExpectVisible(
page,
page
.getByRole("row", { name: "2 5.67 °C Temperatur" })
.getByRole("button")
.first()
);
await expect(page.locator("tr:nth-child(3) > td:nth-child(5)")).toBeVisible();
await highlightAndExpectVisible(
page,
page.locator("tr:nth-child(3) > td:nth-child(5)")
);
await expect(
page
.getByRole("row", { name: "0.01 V AE 4 Messkurve anzeigen" })
.getByRole("button")
.first()
).toBeVisible();
await highlightAndExpectVisible(
page,
page
.getByRole("row", { name: "0.01 V AE 4 Messkurve anzeigen" })
.getByRole("button")
.first()
);
await expect(
page
.getByRole("row", { name: "8 -0.00 mA AE 8 Messkurve" })
.getByLabel("Messkurve anzeigen")
).toBeVisible();
await highlightAndExpectVisible(
page,
page
.getByRole("row", { name: "8 -0.00 mA AE 8 Messkurve" })
.getByLabel("Messkurve anzeigen")
);
// Einstellungen-Button in der ersten Datenzeile klicken und auf Modal warten
const firstRow = page.getByRole("row", { name: /1\s+.*AE\s*1/i });
const settingsButtonInRow = firstRow.getByRole("button").first();
await settingsButtonInRow.waitFor({ state: "visible", timeout: 10000 });
await settingsButtonInRow.click();
await expect(
page.getByRole("heading", { name: /Einstellungen Messwerteingang/ })
).toBeVisible({ timeout: 15000 });
await highlightAndExpectVisible(
page,
page.getByRole("heading", { name: /Einstellungen Messwerteingang/ })
);
await highlightAndExpectVisible(page, page.getByText("Bezeichnung:"));
await highlightAndExpectVisible(page, page.getByText("Offset:"), 5000);
await highlightAndExpectVisible(page, page.getByText("Faktor:"), 5000);
await highlightAndExpectVisible(page, page.getByText("Einheit:"), 5000);
await highlightAndExpectVisible(
page,
page.getByText("Speicherintervall:"),
5000
);
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Speichern" })
);
await expect(
page.getByRole("button", { name: "Modal schließen" })
).toBeVisible();
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Modal schließen" })
);
await expect(
page.getByText(
"Einstellungen Messwerteingang 1Bezeichnung:Offset:Faktor:Einheit:"
)
).toBeVisible();
await highlightAndExpectVisible(
page,
page.getByText(
"Einstellungen Messwerteingang 1Bezeichnung:Offset:Faktor:Einheit:"
)
);
await page.getByRole("button", { name: "Modal schließen" }).click();
await expect(
page
.getByRole("row", { name: "1 126.63 V AE 1 Messkurve" })
.getByLabel("Messkurve anzeigen")
).toBeVisible();
await page
.getByRole("row", { name: "1 126.63 V AE 1 Messkurve" })
.getByLabel("Messkurve anzeigen")
.click();
await expect(
page.getByText(
"Messkurve Messwerteingang 1Eingang 1VonBisAlle MesswerteDaten laden"
)
).toBeVisible();
await highlightAndExpectVisible(
page,
page.getByText(
"Messkurve Messwerteingang 1Eingang 1VonBisAlle MesswerteDaten laden"
)
);
await highlightAndExpectVisible(
page,
page.getByRole("heading", { name: "Messkurve Messwerteingang" })
);
await highlightAndExpectVisible(page, page.locator("canvas"));
await highlightAndExpectVisible(page, page.getByText("Eingang 1VonBisAlle"));
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Daten laden" })
);
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Alle Messwerte " })
);
await page.getByRole("button", { name: "Alle Messwerte " }).click();
await expect(page.getByRole("option", { name: "Stündlich" })).toBeVisible();
await page.getByRole("option", { name: "Stündlich" }).click();
await expect(page.getByRole("button", { name: "Stündlich" })).toBeVisible();
await page.getByRole("button", { name: "Stündlich" }).click();
await expect(page.getByRole("option", { name: "Täglich" })).toBeVisible();
await page.getByRole("option", { name: "Täglich" }).click();
await expect(page.getByRole("button", { name: "Fullscreen" })).toBeVisible();
await page.getByRole("button", { name: "Fullscreen" }).click();
await expect(
page.getByRole("button", { name: "Exit fullscreen" })
).toBeVisible();
await page.getByRole("button", { name: "Exit fullscreen" }).click();
await expect(page.getByRole("button", { name: "Fullscreen" })).toBeVisible();
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Fullscreen" })
);
// Modal schließen nur, wenn noch vorhanden
const modalCloseBtn = page.getByRole("button", { name: "Modal schließen" });
if ((await modalCloseBtn.count()) > 0 && (await modalCloseBtn.isVisible())) {
await highlightAndExpectVisible(page, modalCloseBtn);
await expect(modalCloseBtn).toBeVisible();
await modalCloseBtn.click();
}
}

View File

@@ -0,0 +1,70 @@
import { test, expect } from "@playwright/test";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
test.use({
viewport: {
height: 900,
width: 1600,
},
});
test("Dashboard", async ({ page }) => {
await page.goto("/dashboard");
// Gemeinsame Layout-Checks
await headerTest(page);
await navTest(page);
await footerTest(page);
// Seitenspezifische Checks
await expect(page.getByRole("main").locator("svg").first()).toBeVisible();
await expect(
page.getByRole("heading", { name: "Letzten 20 Meldungen" })
).toBeVisible();
await expect(page.getByRole("cell", { name: "Prio" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Zeitstempel" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Quelle" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Meldung" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Status" })).toBeVisible();
await expect(
page.getByRole("heading", { name: "Versionsinformationen" })
).toBeVisible();
await expect(page.getByRole("main").locator("path").nth(2)).toBeVisible();
await expect(
page
.getByRole("listitem")
.filter({ hasText: "Webversion:" })
.locator("path")
).toBeVisible();
await expect(page.locator("div").filter({ hasText: /^1$/ })).toBeVisible();
await expect(page.getByText("8", { exact: true })).toBeVisible();
await expect(page.getByText("9", { exact: true })).toBeVisible();
await expect(
page.locator(
"div:nth-child(2) > .flex.gap-1 > div:nth-child(8) > .border > .bg-littwin-blue.flex-grow > div:nth-child(2)"
)
).toBeVisible();
await expect(page.getByText("17KÜ705FO")).toBeVisible();
await expect(
page.locator(
"div:nth-child(3) > .flex.gap-1 > div:nth-child(8) > .border > .bg-littwin-blue.flex-grow > div:nth-child(2)"
)
).toBeVisible();
await expect(page.getByText("25KÜ705FO")).toBeVisible();
await expect(page.getByText("32KÜ705FO")).toBeVisible();
await expect(page.getByRole("img", { name: "IP Address" })).toBeVisible();
await expect(page.getByText("IP-Adresse")).toBeVisible();
await expect(page.getByRole("main")).toContainText("10.10.0.243");
await expect(page.getByRole("main")).toContainText("255.255.255.0");
await expect(page.getByRole("main")).toContainText("10.10.0.1");
await page.getByText("Server betriebsbereit").click();
await page
.getByRole("row", { name: "2025-09-05 11:52:44" })
.getByRole("cell")
.first()
.click();
await expect(page.locator("tbody")).toContainText("2025-09-05 11:52:44");
await expect(page.locator("tbody")).toContainText("CableLine13");
await expect(page.locator("tbody")).toContainText("Isofehler gehend");
await expect(page.locator("tbody")).toContainText("0");
});

View File

@@ -1,90 +0,0 @@
import type { Page } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
export async function runDashboardTest(page: Page) {
await page.goto("/dashboard");
//----------------------
await headerTest(page);
await navTest(page);
await footerTest(page);
await page.waitForTimeout(400);
//----------------------
await highlightAndExpectVisible(
page,
page.getByRole("heading", { name: "Letzten 20 Meldungen" })
);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Prio" })
);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Zeitstempel" })
);
// ...existing code...
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Quelle" })
);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Meldung" })
);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Status" })
);
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^2KÜ705FO$/ })
.first()
);
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^3KÜ705FO$/ })
.first()
);
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^32KÜ705FO$/ })
.first()
);
await highlightAndExpectVisible(
page,
page.getByRole("img", { name: "IP Address" })
);
await highlightAndExpectVisible(page, page.getByText("IP-Adresse"));
await highlightAndExpectVisible(page, page.getByRole("main"));
await highlightAndExpectVisible(page, page.getByText("10.10.0.243"));
await highlightAndExpectVisible(page, page.getByText("Subnet-Maske"));
await highlightAndExpectVisible(
page,
page.getByRole("img", { name: "subnet mask" })
);
await highlightAndExpectVisible(page, page.getByText("255.255.255.0"));
await highlightAndExpectVisible(page, page.getByText("Gateway"));
await highlightAndExpectVisible(
page,
page.getByRole("img", { name: "gateway" })
);
await highlightAndExpectVisible(page, page.getByText("10.10.0.1"));
await highlightAndExpectVisible(page, page.getByText("OPC-UA"));
await highlightAndExpectVisible(
page,
page.getByRole("paragraph").filter({ hasText: "Status" })
);
await highlightAndExpectVisible(
page,
page.getByText("Server betriebsbereit")
);
}

View File

@@ -0,0 +1,232 @@
import { test, expect } from "@playwright/test";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
test.use({
viewport: {
height: 1080,
width: 1920,
},
});
test("digitalInputs", async ({ page }) => {
await page.goto("/digitalInputs");
// Gemeinsame Layout-Checks
await headerTest(page);
await navTest(page);
await footerTest(page);
//Snapshot
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- heading "Meldungseingänge" [level=1]
- heading /Meldungseingänge 1 \\d+/ [level=2]
- table:
- rowgroup:
- row "Eingang Zustand Bezeichnung Aktion":
- cell "Eingang"
- cell "Zustand"
- cell "Bezeichnung"
- cell "Aktion"
- rowgroup:
- row "1 ● DE 1":
- cell "1"
- cell "●"
- cell "DE 1"
- cell
- row "2 ● DE 2":
- cell "2"
- cell "●"
- cell "DE 2"
- cell
- row "3 ● DE 3":
- cell "3"
- cell "●"
- cell "DE 3"
- cell
- row "4 ● DE 4":
- cell "4"
- cell "●"
- cell "DE 4"
- cell
- row "5 ● DE 5":
- cell "5"
- cell "●"
- cell "DE 5"
- cell
- row "6 ● DE 6":
- cell "6"
- cell "●"
- cell "DE 6"
- cell
- row "7 ● DE 7":
- cell "7"
- cell "●"
- cell "DE 7"
- cell
- row "8 ● DE 8":
- cell "8"
- cell "●"
- cell "DE 8"
- cell
- row "9 ● DE 9":
- cell "9"
- cell "●"
- cell "DE 9"
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- heading /Meldungseingänge \\d+ \\d+/ [level=2]
- table:
- rowgroup:
- row "Eingang Zustand Bezeichnung Aktion":
- cell "Eingang"
- cell "Zustand"
- cell "Bezeichnung"
- cell "Aktion"
- rowgroup:
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
- row /\\d+ ● DE \\d+/:
- cell /\\d+/
- cell "●"
- cell /DE \\d+/
- cell
`);
//Snapshot
await page
.getByRole("row", { name: "1 ● DE 1", exact: true })
.getByRole("cell")
.nth(3)
.click();
await page
.getByRole("row", { name: "1 ● DE 1", exact: true })
.locator("svg")
.nth(1)
.click();
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- heading "Einstellungen Meldungseingang 1" [level=2]
- button "Modal schließen"
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- text: "Bezeichnung:"
- textbox: DE 1
- text: "Invertierung:"
- switch [checked]
- text: "Ein Filterzeit:"
- spinbutton /Maximal \\d+ ms erlaubt/
- text: "ms Gewichtung:"
- spinbutton /Maximal \\d+ erlaubt/
- text: "Out of Service:"
- switch
- text: Aus
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(
`- button "Speichern"`
);
});

View File

@@ -1,106 +0,0 @@
import type { Page } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
export async function runDigitalInputsTest(page: Page) {
await page.goto("/digitalInputs");
//----------------------
await headerTest(page);
await navTest(page);
await footerTest(page);
await page.waitForTimeout(400);
//--------------------
await highlightAndExpectVisible(
page,
page.getByRole("heading", { name: "Meldungseingänge", exact: true })
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("heading", { name: "Meldungseingänge 1 " }).locator("svg")
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("heading", { name: "Meldungseingänge 1 " })
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Eingang" }).first()
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Zustand" }).first()
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Bezeichnung" }).first()
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Aktion" }).first()
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "1", exact: true }).locator("svg")
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(page, page.getByText("1", { exact: true }));
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page
.getByRole("row", { name: "1 ● DE 1", exact: true })
.getByRole("cell")
.nth(1)
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("row", { name: "1 ● DE 1", exact: true }).locator("span")
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "DE 1", exact: true })
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page
.getByRole("row", { name: "1 ● DE 1", exact: true })
.locator("svg")
.nth(1)
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "13", exact: true }).locator("svg")
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(page, page.getByText("13", { exact: true }));
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("row", { name: "● DE 13" }).getByRole("cell").nth(1)
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("row", { name: "● DE 13" }).locator("span")
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "DE 13" })
);
await page.waitForTimeout(1000);
}

View File

@@ -0,0 +1,144 @@
import { test, expect } from "@playwright/test";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
test.use({
viewport: {
height: 800,
width: 1280,
},
});
test("digitalOutputs", async ({ page }) => {
await page.goto("/dashboard");
// Gemeinsame Layout-Checks
await headerTest(page);
await navTest(page);
await footerTest(page);
await page.getByRole("link", { name: "Schaltausgänge" }).click();
await expect(page.locator("h1")).toBeVisible();
await expect(
page.locator("h2").filter({ hasText: "Schaltausgänge" }).locator("svg")
).toBeVisible();
await expect(
page.locator("h2").filter({ hasText: "Schaltausgänge" })
).toBeVisible();
await expect(
page.getByRole("cell", { name: "Ausgang", exact: true })
).toBeVisible();
await expect(page.getByRole("cell", { name: "Bezeichnung" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Schalter" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Aktion" })).toBeVisible();
await expect(
page.getByRole("cell", { name: "1", exact: true }).locator("svg")
).toBeVisible();
await expect(
page.getByRole("cell", { name: "1", exact: true })
).toBeVisible();
await expect(
page.getByRole("cell", { name: "2", exact: true })
).toBeVisible();
await expect(
page.getByRole("cell", { name: "3", exact: true })
).toBeVisible();
await expect(
page.getByRole("cell", { name: "4", exact: true })
).toBeVisible();
await expect(page.getByRole("cell", { name: "Ausgang1" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Ausgang2" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Ausgang3" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Ausgang4" })).toBeVisible();
await expect(
page.getByRole("row", { name: "Ausgang1" }).getByRole("cell").nth(2)
).toBeVisible();
await expect(
page.getByRole("row", { name: "Ausgang2" }).getByRole("cell").nth(2)
).toBeVisible();
await expect(
page.getByRole("row", { name: "Ausgang3" }).getByRole("cell").nth(2)
).toBeVisible();
await expect(
page.getByRole("row", { name: "Ausgang4" }).getByRole("cell").nth(2)
).toBeVisible();
await expect(
page.getByRole("row", { name: "Ausgang1" }).getByRole("cell").nth(3)
).toBeVisible();
await expect(
page.getByRole("row", { name: "Ausgang2" }).getByRole("cell").nth(3)
).toBeVisible();
await expect(
page.getByRole("row", { name: "Ausgang3" }).getByRole("cell").nth(3)
).toBeVisible();
await expect(
page.getByRole("row", { name: "Ausgang4" }).getByRole("cell").nth(3)
).toBeVisible();
await page
.getByRole("row", { name: "Ausgang1" })
.locator("svg")
.nth(1)
.click();
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- heading "Einstellungen Schaltausgang 1" [level=2]
- button "Modal schließen"
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- text: "Bezeichnung:"
- textbox "z. B. Licht Relais 1"
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(
`- button "Speichern"`
);
await page.getByRole("button", { name: "Modal schließen" }).click();
await page
.getByRole("row", { name: "Ausgang2" })
.locator("svg")
.nth(1)
.click();
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- heading "Einstellungen Schaltausgang 2" [level=2]
- button "Modal schließen"
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- text: "Bezeichnung:"
- textbox "z. B. Licht Relais 1"
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(
`- button "Speichern"`
);
await page.getByRole("button", { name: "Modal schließen" }).click();
await page
.getByRole("row", { name: "Ausgang3" })
.locator("svg")
.nth(1)
.click();
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- heading "Einstellungen Schaltausgang 3" [level=2]
- button "Modal schließen"
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- text: "Bezeichnung:"
- textbox "z. B. Licht Relais 1"
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(
`- button "Speichern"`
);
await page.getByRole("button", { name: "Modal schließen" }).click();
await page
.getByRole("row", { name: "Ausgang4" })
.locator("svg")
.nth(1)
.click();
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- heading "Einstellungen Schaltausgang 4" [level=2]
- button "Modal schließen"
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- text: "Bezeichnung:"
- textbox "z. B. Licht Relais 1"
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(
`- button "Speichern"`
);
await page.getByRole("button", { name: "Modal schließen" }).click();
});

View File

@@ -1,64 +0,0 @@
import type { Page } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
export async function runDigitalOutputsTest(page: Page) {
await page.goto("/digitalOutputs");
//----------------------
await headerTest(page);
await navTest(page);
await footerTest(page);
// Wait a moment for initial redux fetch and render in slower CI environments
await page.waitForTimeout(400);
//----------------------
await highlightAndExpectVisible(page, page.locator("h1"));
page.locator("h1").click();
await highlightAndExpectVisible(
page,
page.locator("h2").filter({ hasText: "Schaltausgänge" })
);
page
.locator("h2")
.filter({ hasText: "Schaltausgänge" })
.locator("svg")
.click();
page.locator("h2").filter({ hasText: "Schaltausgänge" }).click();
// Wait for the outputs table to render and be visible
const table = page.locator("table");
await table.first().waitFor({ state: "visible", timeout: 15000 });
// Prefer robust selection: select the row by its first cell text matching the id "2"
const rowAusgang2 = table
.getByRole("row")
.filter({ has: page.getByRole("cell", { name: /^\s*2\s*$/ }) })
.first();
await rowAusgang2.waitFor({ state: "visible", timeout: 15000 });
await rowAusgang2.click();
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Schalter" })
);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Schalter" })
);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Aktion" })
);
// Interact with the switch icon within each row deterministically
for (const id of [1, 2, 3, 4]) {
const row = table
.getByRole("row")
.filter({
has: page.getByRole("cell", { name: new RegExp(`^\\s*${id}\\s*$`) }),
})
.first();
await row.waitFor({ state: "visible", timeout: 10000 });
const switchIcon = row.locator("td >> nth=2").locator("svg");
await switchIcon.first().click();
}
}

View File

@@ -0,0 +1,104 @@
import { test, expect } from "@playwright/test";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
test.use({
viewport: {
height: 900,
width: 1600,
},
});
test("Kabelüberwachung", async ({ page }) => {
await page.goto("/kabelueberwachung");
// Gemeinsame Layout-Checks
await headerTest(page);
await navTest(page);
await footerTest(page);
await expect(page.getByRole("button", { name: "Rack 1" })).toBeVisible();
await expect(page.getByRole("button", { name: "Rack 2" })).toBeVisible();
await expect(page.getByRole("button", { name: "Rack 3" })).toBeVisible();
await expect(page.getByRole("button", { name: "Rack 4" })).toBeVisible();
//--
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- text: "1"
- heading /KÜ\\d+-FO/ [level=3]
- button "⚙"
- text: "/Betrieb Alarm Erdschluss ISO: \\\\d+,\\\\d+ MOhm RSL: \\\\d+,\\\\d+ kOhm Kabel 1 V4\\\\.\\\\d+ Detailansicht/"
- button "ISO"
- button "RSL"
- button "TDR"
- button "KVZ"
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- text: "2"
- heading /KÜ\\d+-FO/ [level=3]
- button "⚙"
- text: "/Betrieb Alarm Messpannung ISO: \\\\d+,\\\\d+ MOhm RSL: \\\\d+,\\\\d+ kOhm Kabel 2 V4\\\\.\\\\d+ Detailansicht/"
- button "ISO"
- button "RSL"
- button
- button "KVZ"
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- text: "3"
- heading /KÜ\\d+-FO/ [level=3]
- button "⚙"
- text: "/Betrieb Alarm Erdschluss ISO: \\\\d+,\\\\d+ MOhm RSL: \\\\d+,\\\\d+ kOhm Kabel 3 V4\\\\.\\\\d+ Detailansicht/"
- button "ISO"
- button "RSL"
- button "TDR"
- button "KVZ"
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- text: "4"
- heading /KÜ\\d+-FO/ [level=3]
- button "⚙"
- text: "/Betrieb Alarm Aderbruch ISO: \\\\d+,\\\\d+ MOhm RSL: \\\\d+,\\\\d+ kOhm Kabel 4 V4\\\\.\\\\d+ Detailansicht/"
- button "ISO"
- button "RSL"
- button "TDR"
- button "KVZ nicht verfügbar" [disabled]
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- text: "5"
- heading /KÜ\\d+-FO/ [level=3]
- button "⚙"
- text: "/Betrieb Alarm ISO: \\\\d+,\\\\d+ MOhm RSL: \\\\d+,\\\\d+ kOhm Kabel 5 V4\\\\.\\\\d+ Detailansicht/"
- button "ISO"
- button "RSL"
- button "TDR"
- button "KVZ nicht verfügbar" [disabled]
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- text: "6"
- heading /KÜ\\d+-FO/ [level=3]
- button "⚙"
- text: "/Betrieb Alarm ISO: \\\\d+,\\\\d+ MOhm RSL: \\\\d+,\\\\d+ kOhm Kabel 6 V4\\\\.\\\\d+ Detailansicht/"
- button "ISO"
- button "RSL"
- button "TDR"
- button "KVZ nicht verfügbar" [disabled]
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- text: "7"
- heading /KÜ\\d+-FO/ [level=3]
- button "⚙"
- text: "/Betrieb Alarm Schleifenfehler ISO: \\\\d+,\\\\d+ MOhm RSL: \\\\d+,\\\\d+ kOhm Kabel 7 V4\\\\.\\\\d+ Detailansicht/"
- button "ISO"
- button "RSL"
- button "TDR"
- button "KVZ nicht verfügbar" [disabled]
`);
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- text: "8"
- heading /KÜ\\d+-FO/ [level=3]
- button "⚙"
- text: "/Betrieb Alarm Isolationsfehler ISO: \\\\d+,\\\\d+ MOhm RSL: \\\\d+,\\\\d+ kOhm Kabel_8 in Salzgitter bei Hannover Kabel_8 in Salzgitter bei Hannover V4\\\\.\\\\d+ Detailansicht/"
- button "ISO"
- button "RSL"
- button "TDR"
- button "KVZ nicht verfügbar" [disabled]
`);
});

View File

@@ -1,366 +0,0 @@
import type { Page } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
export async function runCableMonitoringTest(page: Page) {
await page.goto("/kabelueberwachung");
//----------------------
await headerTest(page);
await navTest(page);
await footerTest(page);
await page.waitForTimeout(400);
//----------------------
// Rack Buttons
for (const rack of [1, 2, 3, 4, 1]) {
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: `Rack ${rack}` })
);
await page.waitForTimeout(400);
}
// Kabel 1
await highlightAndExpectVisible(page, page.getByText("1", { exact: true }));
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator("div").filter({ hasText: /^8$/ })
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^1KÜ705-FO⚙$/ })
.getByRole("heading")
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(page, page.getByText("1", { exact: true }));
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^1KÜ705-FO⚙$/ })
.getByRole("button")
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator(".flex.flex-col > span").first()
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator(".w-\\[0\\.625rem\\]").first()
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator("span:nth-child(2)").first()
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page
.locator(".w-\\[0\\.625rem\\].h-\\[0\\.625rem\\].rounded-full.bg-red-500")
.first()
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(page, page.getByText("Erdschluss").first());
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator(".text-center > span:nth-child(2)").first()
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByText(/^RSL: \d+,\d{3} kOhm$/).first()
);
await page.waitForTimeout(400);
// Use a unique locator to avoid strict mode violation (two elements contain text "Kabel 1").
// The card exposes a title attribute "Kabel 1", so prefer getByTitle for a single match.
await highlightAndExpectVisible(page, page.getByTitle("Kabel 1").first());
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator(".text-black.text-\\[0\\.625rem\\].font-semibold").first()
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator(".bg-littwin-blue.text-white.text-\\[0\\.625rem\\]").first()
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator(".flex > button:nth-child(2)").first()
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator(".bg-littwin-blue.text-white.cursor-pointer").first()
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(page, page.getByText("V4.20"));
await page.waitForTimeout(400);
// Kabel 8
await highlightAndExpectVisible(page, page.getByText("8", { exact: true }));
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^8KÜ705-FO⚙$/ })
.getByRole("heading")
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^8KÜ705-FO⚙$/ })
.getByRole("button")
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page
.locator(
"div:nth-child(8) > .relative.bg-gray-300 > .relative.w-\\[7\\.075rem\\] > .flex.flex-col.mt-\\[0\\.625rem\\] > .flex.items-center.space-x-\\[0\\.25rem\\] > .flex.flex-col.items-start > span"
)
.first()
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator(
"div:nth-child(8) > .relative.bg-gray-300 > .relative.w-\\[7\\.075rem\\] > .flex.flex-col.mt-\\[0\\.625rem\\] > .flex.items-center.space-x-\\[0\\.25rem\\] > .flex.flex-col.items-start > span:nth-child(2)"
)
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator(
"div:nth-child(8) > .relative.bg-gray-300 > .relative.w-\\[7\\.075rem\\] > .flex.flex-col.mt-\\[0\\.625rem\\] > .flex.items-center.space-x-\\[0\\.25rem\\] > .flex.flex-col.items-center > .w-\\[0\\.625rem\\].h-\\[0\\.625rem\\].bg-green-500"
)
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator(
"div:nth-child(8) > .relative.bg-gray-300 > .relative.w-\\[7\\.075rem\\] > .flex.flex-col.mt-\\[0\\.625rem\\] > .flex.items-center.space-x-\\[0\\.25rem\\] > .flex.flex-col.items-center > .w-\\[0\\.625rem\\].h-\\[0\\.625rem\\].rounded-full.bg-red-500"
)
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(page, page.getByText("Isolationsfehler"));
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator(
"div:nth-child(8) > .relative.bg-gray-300 > .relative.w-\\[7\\.075rem\\] > .relative.mt-\\[3\\.125rem\\] > .text-center > span:nth-child(2)"
)
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByText(/^RSL: \d+,\d{3} kOhm$/).nth(3)
);
await page.waitForTimeout(400);
// For cable 8 the UI shows a long name and exposes it via title attribute; target it explicitly
await highlightAndExpectVisible(
page,
page.getByTitle("Kabel_8 in Salzgitter bei Hannover").first()
);
await page.waitForTimeout(400);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator(".bg-littwin-blue.text-white.text-\\[0\\.625rem\\]").first()
);
await page.waitForTimeout(400);
// ... (weitere Schritte können nach diesem Muster ergänzt werden)
// Beispiel für weitere Schritte aus dem Kommentar:
await highlightAndExpectVisible(
page,
page.locator(".bg-littwin-blue.text-white.text-\\[0\\.625rem\\]").first()
);
await page.waitForTimeout(400);
await page
.locator(".bg-littwin-blue.text-white.text-\\[0\\.625rem\\]")
.first()
.click();
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("heading", { name: "Isolationswiderstand" })
);
await page.waitForTimeout(400);
await page.getByRole("heading", { name: "Isolationswiderstand" }).click();
await page.waitForTimeout(400);
await highlightAndExpectVisible(page, page.getByText("KÜ 1"));
await page.waitForTimeout(400);
await page.getByText("KÜ 1").click();
await page.waitForTimeout(400);
await highlightAndExpectVisible(page, page.getByText("Von"));
await page.waitForTimeout(400);
await page.getByText("Von").click();
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator("div").filter({ hasText: /^Von$/ }).getByRole("textbox")
);
await page.waitForTimeout(400);
await page
.locator("div")
.filter({ hasText: /^Von$/ })
.getByRole("textbox")
.click();
await page.waitForTimeout(400);
await highlightAndExpectVisible(page, page.getByText("Bis"));
await page.waitForTimeout(400);
await page.getByText("Bis").click();
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.locator("div").filter({ hasText: /^Bis$/ }).getByRole("textbox")
);
await page.waitForTimeout(400);
await page
.locator("div")
.filter({ hasText: /^Bis$/ })
.getByRole("textbox")
.click();
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Alle Messwerte" })
);
await page.waitForTimeout(400);
await page.getByRole("button", { name: "Alle Messwerte" }).click();
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("option", { name: "Stündliche Werte" })
);
await page.waitForTimeout(400);
await page.getByRole("option", { name: "Stündliche Werte" }).click();
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Stündliche Werte" })
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Daten laden" })
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(page, page.locator("canvas"));
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "" })
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "" })
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Messkurve" })
);
await page.waitForTimeout(400);
await page.getByRole("button", { name: "Messkurve" }).click();
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("option", { name: "Meldungen" })
);
await page.waitForTimeout(400);
await page.getByRole("option", { name: "Meldungen" }).click();
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Prio" })
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Zeitstempel" })
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Quelle" })
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Meldung" })
);
await page.waitForTimeout(400);
await highlightAndExpectVisible(
page,
page.getByRole("cell", { name: "Status" })
);
await page.waitForTimeout(400);
}

View File

@@ -0,0 +1,108 @@
import { test, expect } from "@playwright/test";
test.use({
viewport: {
height: 800,
width: 1280,
},
});
test("test", async ({ page }) => {
await page.goto("/kabelueberwachung");
await page.locator(".bg-littwin-blue.text-white").first().click();
await expect(page.getByText("IsolationswiderstandMesskurve")).toBeVisible();
await expect(page.getByRole("button", { name: "Messkurve " })).toBeVisible();
await expect(page.getByText("KÜ1")).toBeVisible();
await expect(page.getByText("Von")).toBeVisible();
await expect(
page.locator("div").filter({ hasText: /^Von$/ }).getByRole("textbox")
).toBeVisible();
await page
.locator("div")
.filter({ hasText: /^Von$/ })
.getByRole("textbox")
.click();
await expect(page.getByLabel("Sunday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Saturday", { exact: true })).toBeVisible();
await page.getByRole("textbox").first().click();
await page.getByRole("textbox").first().click();
await page.getByText("IsolationswiderstandMesskurve").click();
await expect(page.getByText("Bis")).toBeVisible();
await expect(
page.locator("div").filter({ hasText: /^Bis$/ }).getByRole("textbox")
).toBeVisible();
await page
.locator("div")
.filter({ hasText: /^Bis$/ })
.getByRole("textbox")
.click();
await expect(page.getByLabel("Sunday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Saturday", { exact: true })).toBeVisible();
await page.getByRole("textbox").nth(1).click();
await page.getByText("IsolationswiderstandMesskurve").click();
await expect(
page.getByRole("button", { name: "Alle Messwerte " })
).toBeVisible();
await page.getByRole("button", { name: "Alle Messwerte " }).click();
// await expect(page.getByRole("button", { name: "Daten laden" })).toBeVisible();
await page.getByRole("button", { name: "Alle Messwerte " }).click();
await expect(page.locator("canvas")).toBeVisible();
await page.getByRole("button", { name: "Alle Messwerte " }).click();
await page.getByRole("option", { name: "Stündlich" }).click();
await expect(page.getByRole("button", { name: "Stündlich " })).toBeVisible();
await page.getByRole("button", { name: "Stündlich " }).click();
await page.getByRole("option", { name: "Täglich" }).click();
await expect(page.getByRole("button", { name: "Täglich " })).toBeVisible();
await page.getByRole("button", { name: "Messkurve " }).click();
await page.getByRole("option", { name: "Meldungen" }).click();
await expect(page.getByRole("button", { name: "Meldungen " })).toBeVisible();
await expect(page.getByText("VonBisAnzeigen")).toBeVisible();
await page
.locator("div")
.filter({ hasText: /^Von$/ })
.getByRole("textbox")
.click();
await expect(page.getByRole("textbox").first()).toBeVisible();
await page
.locator("div")
.filter({ hasText: /^Bis$/ })
.getByRole("textbox")
.click();
await page.getByText("IsolationswiderstandMeldungen").click();
await expect(page.getByRole("button", { name: "Anzeigen" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Prio" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Zeitstempel" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Quelle" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Meldung" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Status" })).toBeVisible();
await expect(
page
.getByRole("row", { name: "12:10:42 CableLine1 Modul online online" })
.getByRole("cell")
.first()
).toBeVisible();
await expect(
page
.getByRole("row", { name: "12:10:42 CableLine1 Aderbruch kommend 1" })
.getByRole("cell")
.first()
).toBeVisible();
await expect(
page
.getByRole("row", { name: "12:10:42 CableLine1 Aderbruch gehend 0" })
.getByRole("cell")
.first()
).toBeVisible();
await expect(
page
.getByRole("row", { name: "12:10:42 CableLine1 Isofehler gehend 0" })
.getByRole("cell")
.first()
).toBeVisible();
await page.getByRole("button", { name: "Vollbild" }).click();
await page.getByRole("button", { name: "Vollbild verlassen" }).click();
await page
.getByRole("dialog", { name: "Isolationswiderstand" })
.getByLabel("Schließen")
.click();
});

View File

@@ -0,0 +1,81 @@
import { test, expect } from "@playwright/test";
test.use({
viewport: {
height: 800,
width: 1280,
},
});
test("test", async ({ page }) => {
await page.goto("http://localhost:3000/kabelueberwachung");
await page.getByRole("button", { name: "KVZ", exact: true }).first().click();
await expect(
page
.getByRole("dialog", { name: "KVZ Zustände & Meldungen" })
.getByRole("banner")
).toBeVisible();
await expect(page.getByText("KÜ 1")).toBeVisible();
await expect(page.getByText("Von")).toBeVisible();
await expect(
page.locator("div").filter({ hasText: /^Von$/ }).getByRole("textbox")
).toBeVisible();
await page
.locator("div")
.filter({ hasText: /^Von$/ })
.getByRole("textbox")
.click();
await expect(page.getByLabel("Sunday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Saturday", { exact: true })).toBeVisible();
await expect(page.getByText("Bis")).toBeVisible();
await page
.getByRole("dialog", { name: "KVZ Zustände & Meldungen" })
.getByRole("banner")
.click();
await expect(page.getByText("Bis")).toBeVisible();
await expect(
page.locator("div").filter({ hasText: /^Bis$/ }).getByRole("textbox")
).toBeVisible();
await page
.locator("div")
.filter({ hasText: /^Bis$/ })
.getByRole("textbox")
.click();
await expect(page.getByLabel("Sunday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Saturday", { exact: true })).toBeVisible();
await page
.getByRole("dialog", { name: "KVZ Zustände & Meldungen" })
.getByRole("banner")
.click();
await expect(page.getByRole("button", { name: "Anzeigen" })).toBeVisible();
await expect(page.getByText("KVZ1")).toBeVisible();
await expect(page.getByTitle("Slot 0 LED1: Ein")).toBeVisible();
await expect(page.getByText("KVZ2", { exact: true })).toBeVisible();
await expect(page.getByTitle("Slot 0 LED2: Aus")).toBeVisible();
await expect(page.getByText("KVZ3", { exact: true })).toBeVisible();
await expect(page.getByTitle("Slot 0 LED3: Unbekannt")).toBeVisible();
await expect(page.getByText("KVZ4", { exact: true })).toBeVisible();
await expect(page.getByTitle("Slot 0 LED4: Ein")).toBeVisible();
await expect(page.getByRole("cell", { name: "Prio" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Zeitstempel" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Quelle" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Meldung" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Status" })).toBeVisible();
await expect(
page
.getByRole("row", { name: "12:10:42 CableLine1 Modul online online" })
.getByRole("cell")
.first()
).toBeVisible();
await expect(
page
.getByRole("row", { name: "12:10:42 CableLine1 Aderbruch kommend 1" })
.getByRole("cell")
.first()
).toBeVisible();
await page.getByRole("button", { name: "Vollbild" }).click();
await page
.getByRole("dialog", { name: "KVZ Zustände & Meldungen" })
.getByLabel("Schließen")
.click();
});

View File

@@ -0,0 +1,90 @@
import { test, expect } from "@playwright/test";
test.use({
viewport: {
height: 800,
width: 1280,
},
});
test("test", async ({ page }) => {
await page.goto("http://localhost:3000/kabelueberwachung");
await page.locator(".flex > button:nth-child(2)").first().click();
await expect(page.getByLabel("Schleifenwiderstand").getByRole("banner"))
.toMatchAriaSnapshot(`
- banner:
- heading "Schleifenwiderstand" [level=3]
- button "Vollbild"
- button "Schließen"
- button "Messkurve "
`);
await expect(page.getByLabel("Schleifenwiderstand")).toMatchAriaSnapshot(`
- text: KÜ 1 Von
- textbox: /\\d+\\.\\d+\\.\\d+/
- text: Bis
- textbox: /\\d+\\.\\d+\\.\\d+/
- button "Alle Messwerte "
- button "RSL Messung starten"
- button "Daten laden"
`);
await page.getByRole("button", { name: "Alle Messwerte " }).click();
await page.getByRole("option", { name: "Stündlich" }).click();
await expect(page.getByLabel("Schleifenwiderstand")).toMatchAriaSnapshot(`
- text: KÜ 1 Von
- textbox: /\\d+\\.\\d+\\.\\d+/
- text: Bis
- textbox: /\\d+\\.\\d+\\.\\d+/
- button "Stündlich "
- button "RSL Messung starten"
- button "Daten laden"
`);
await page.getByRole("button", { name: "Stündlich " }).click();
await page.getByRole("option", { name: "Täglich" }).click();
await expect(page.getByLabel("Schleifenwiderstand")).toMatchAriaSnapshot(`
- text: KÜ 1 Von
- textbox: /\\d+\\.\\d+\\.\\d+/
- text: Bis
- textbox: /\\d+\\.\\d+\\.\\d+/
- button "Täglich "
- button "RSL Messung starten"
- button "Daten laden"
`);
await page.locator("canvas").click({
position: {
x: 128,
y: 41,
},
});
await expect(page.locator("canvas")).toBeVisible();
await page.getByRole("button", { name: "Messkurve " }).click();
await page.getByRole("option", { name: "Meldungen" }).click();
await expect(page.getByText("SchleifenwiderstandMeldungen")).toBeVisible();
await expect(page.getByText("Von")).toBeVisible();
await expect(
page.locator("div").filter({ hasText: /^Von$/ }).getByRole("textbox")
).toBeVisible();
await expect(page.getByText("Bis")).toBeVisible();
await expect(
page.locator("div").filter({ hasText: /^Bis$/ }).getByRole("textbox")
).toBeVisible();
await expect(page.getByRole("button", { name: "Anzeigen" })).toBeVisible();
await expect(
page
.getByLabel("Schleifenwiderstand")
.locator("div")
.filter({
hasText:
"PrioZeitstempelQuelleMeldungStatus03.09.2025, 12:10:42CableLine1Modul",
})
.nth(4)
).toBeVisible();
await page.getByRole("cell", { name: "Prio" }).click();
await page.getByRole("cell", { name: "Zeitstempel" }).click();
await page.getByRole("cell", { name: "Quelle" }).click();
await page.getByRole("cell", { name: "Meldung" }).click();
await page.getByRole("cell", { name: "Status" }).click();
await page
.getByRole("dialog", { name: "Schleifenwiderstand" })
.getByLabel("Schließen")
.click();
});

View File

@@ -0,0 +1,64 @@
import { test, expect } from "@playwright/test";
test.use({
viewport: {
height: 800,
width: 1280,
},
});
test("test", async ({ page }) => {
await page.goto("http://localhost:3000/kabelueberwachung");
await page
.locator(".bg-littwin-blue.text-white.cursor-pointer")
.first()
.click();
await expect(page.getByLabel("TDR Messung").getByRole("banner"))
.toMatchAriaSnapshot(`
- banner:
- heading "TDR-Messung" [level=3]
- button "Vollbild"
- button "Schließen"
- button "Messkurve "
`);
await expect(page.getByText("KÜ1")).toBeVisible();
await expect(
page.getByRole("button", { name: "TDR-Kurve als Referenz" })
).toBeVisible();
await expect(
page.getByRole("button", { name: "TDR-Messung starten" })
).toBeVisible();
await expect(
page.getByRole("button", { name: "27.03.2025, 23:42:41 " })
).toBeVisible();
await expect(page.locator("canvas")).toBeVisible();
await page.getByRole("button", { name: "Messkurve " }).click();
await page.getByRole("option", { name: "Meldungen" }).click();
await expect(page.getByText("Von")).toBeVisible();
await expect(
page.locator("div").filter({ hasText: /^Von$/ }).getByRole("textbox")
).toBeVisible();
await expect(page.getByText("Bis")).toBeVisible();
await expect(
page.locator("div").filter({ hasText: /^Bis$/ }).getByRole("textbox")
).toBeVisible();
await expect(page.getByRole("button", { name: "Anzeigen" })).toBeVisible();
await page.getByRole("cell", { name: "Prio" }).click();
await page.getByRole("cell", { name: "Zeitstempel" }).click();
await page.getByRole("cell", { name: "Quelle" }).click();
await page.getByRole("cell", { name: "Meldung" }).click();
await page.getByRole("cell", { name: "Status" }).click();
await page
.getByLabel("TDR Messung")
.locator("div")
.filter({
hasText:
"PrioZeitstempelQuelleMeldungStatus03.09.2025, 12:10:42CableLine1Modul",
})
.nth(3)
.click();
await page
.getByRole("dialog", { name: "TDR Messung" })
.getByLabel("Schließen")
.click();
});

View File

@@ -0,0 +1,91 @@
import { test, expect } from "@playwright/test";
test.use({
viewport: {
height: 800,
width: 1280,
},
});
test("test", async ({ page }) => {
await page.goto("kabelueberwachung");
await page
.locator("div")
.filter({ hasText: /^1KÜ705-FO⚙$/ })
.getByRole("button")
.click();
await expect(
page.locator("div").filter({ hasText: /^Einstellungen KÜ 1$/ })
).toBeVisible();
await expect(page.getByText("AllgemeinTDRKVzKnotenpunkte")).toBeVisible();
await expect(page.getByText("Kabelbezeichnung:")).toBeVisible();
await expect(
page.getByRole("textbox", { name: "Feld kann nicht bearbeitet" })
).toBeVisible();
await expect(page.getByText("Kabelname:")).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^Kabelname:$/ })
.getByRole("textbox")
).toBeVisible();
await expect(page.getByText("Speicherintervall:")).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^Minuten$/ })
.getByRole("spinbutton")
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Isolationsmessung" })
).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^Grenzwert:MOhm$/ })
.locator("label")
).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^MOhm$/ })
.getByRole("spinbutton")
).toBeVisible();
await expect(page.getByText("Verzögerung:")).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^Sekunden$/ })
.getByRole("spinbutton")
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Schleifenmessung" })
).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^Grenzwert:kOhm$/ })
.locator("label")
).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^kOhm$/ })
.getByRole("spinbutton")
).toBeVisible();
await expect(page.getByText("Schleifenmessintervall:")).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^Stunden$/ })
.getByRole("spinbutton")
).toBeVisible();
await expect(
page.getByRole("button", { name: "Display einschalten" })
).toBeVisible();
await expect(page.getByRole("button", { name: "Speichern" })).toBeVisible();
await expect(
page.getByRole("button", { name: "Modal schließen" })
).toBeVisible();
await page.getByRole("button", { name: "Modal schließen" }).click();
});

View File

@@ -0,0 +1,26 @@
import { test, expect } from "@playwright/test";
test.use({
viewport: {
height: 800,
width: 1280,
},
});
test("test", async ({ page }) => {
await page.goto("http://localhost:3000/kabelueberwachung");
await page
.locator("div")
.filter({ hasText: /^1KÜ705-FO⚙$/ })
.getByRole("button")
.click();
await page.getByRole("button", { name: "KVz", exact: true }).click();
await page
.locator("div")
.filter({
hasText: /^Nur Admin-Benutzer können diese Einstellungen ändern\.$/,
})
.nth(1)
.click();
await page.getByRole("button", { name: "Modal schließen" }).click();
});

View File

@@ -0,0 +1,35 @@
import { test, expect } from "@playwright/test";
test.use({
viewport: {
height: 800,
width: 1280,
},
});
test("test", async ({ page }) => {
await page.goto("http://localhost:3000/kabelueberwachung");
await page
.locator("div")
.filter({ hasText: /^1KÜ705-FO⚙$/ })
.getByRole("button")
.click();
await page.getByRole("button", { name: "Knotenpunkte" }).click();
await page.getByText("Knoten 1:").click();
await page
.locator("div")
.filter({ hasText: /^Knoten 1:↳ Verbindung:m$/ })
.locator("span")
.first()
.click();
await page.getByText("Knoten 2:").click();
await page
.locator("div")
.filter({ hasText: /^Knoten 2:↳ Verbindung:m$/ })
.locator("span")
.first()
.click();
await page.getByText("Knoten 3:").click();
await page.getByText("Knoten 10:").click();
await page.getByRole("button", { name: "Modal schließen" }).click();
});

View File

@@ -0,0 +1,24 @@
import { test, expect } from "@playwright/test";
test.use({
viewport: {
height: 800,
width: 1280,
},
});
test("test", async ({ page }) => {
await page.goto("http://localhost:3000/kabelueberwachung");
await page
.locator("div")
.filter({ hasText: /^1KÜ705-FO⚙$/ })
.getByRole("button")
.click();
await page
.getByLabel("Einstellungen KÜ")
.getByRole("button", { name: "TDR" })
.click();
await expect(
page.getByText("TDR Dämpfung:dBGeschwindigkeit:m/µsTrigger:Speichern")
).toBeVisible();
});

View File

@@ -0,0 +1,84 @@
import { test, expect } from "@playwright/test";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
test.use({
viewport: {
height: 800,
width: 1280,
},
});
test("Berichte/Meldungen", async ({ page }) => {
await page.goto("/meldungen");
// Gemeinsame Layout-Checks
await headerTest(page);
await navTest(page);
await footerTest(page);
await expect(page.getByRole("heading", { name: "Berichte" })).toBeVisible();
await expect(page.getByText("Von")).toBeVisible();
await expect(
page.locator("div").filter({ hasText: /^Von$/ }).getByRole("textbox")
).toBeVisible();
await expect(page.getByText("Bis")).toBeVisible();
await expect(
page.locator("div").filter({ hasText: /^Bis$/ }).getByRole("textbox")
).toBeVisible();
await expect(page.getByRole("button", { name: "Anzeigen" })).toBeVisible();
await expect(
page.getByRole("button", { name: "Alle Quellen" })
).toBeVisible();
await page
.locator("div")
.filter({ hasText: /^Von$/ })
.getByRole("textbox")
.click();
await expect(page.getByLabel("Sunday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Monday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Tuesday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Wednesday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Thursday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Friday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Saturday", { exact: true })).toBeVisible();
await page
.locator("div")
.filter({ hasText: /^Von$/ })
.getByRole("textbox")
.click();
await page
.locator("div")
.filter({ hasText: /^Von$/ })
.getByRole("textbox")
.click();
await page.getByRole("heading", { name: "Berichte" }).click();
await page
.locator("div")
.filter({ hasText: /^Bis$/ })
.getByRole("textbox")
.click();
await expect(page.getByLabel("Sunday", { exact: true })).toBeVisible();
await page.getByLabel("Monday", { exact: true }).click();
await page.getByLabel("Tuesday", { exact: true }).click();
await page.getByLabel("Wednesday", { exact: true }).click();
await page.getByLabel("Thursday", { exact: true }).click();
await page.getByLabel("Friday", { exact: true }).click();
await page.getByLabel("Saturday", { exact: true }).click();
await page.getByRole("heading", { name: "Berichte" }).click();
await page.getByRole("button", { name: "Alle Quellen" }).click();
await page.getByRole("button", { name: "Alle Quellen" }).click();
await page.getByRole("cell", { name: "Prio" }).click();
await page.getByRole("cell", { name: "Zeitstempel" }).click();
await page.getByRole("cell", { name: "Quelle" }).click();
await page.getByRole("cell", { name: "Meldung" }).click();
await page.getByRole("cell", { name: "Status" }).click();
await expect(
page.getByRole("row", { name: "05.09.2025, 11:52:44" }).locator("div")
).toBeVisible();
await expect(
page
.getByRole("row", { name: "05.09.2025, 11:51:14" })
.getByRole("cell")
.first()
).toBeVisible();
});

View File

@@ -1,124 +0,0 @@
import type { Page } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
export async function runMeldungenTest(page: Page) {
await page.goto("/meldungen");
//----------------------
await headerTest(page);
await navTest(page);
await footerTest(page);
await page.waitForTimeout(400);
//----------------------
// Berichte Heading
const berichteHeading = page.getByRole("heading", { name: "Berichte" });
await highlightAndExpectVisible(page, berichteHeading);
await berichteHeading.click();
await page.waitForTimeout(100);
// Von
const vonText = page.getByText("Von");
await highlightAndExpectVisible(page, vonText);
await vonText.click();
await page.waitForTimeout(50);
const vonTextbox = page
.locator("div")
.filter({ hasText: /^Von$/ })
.getByRole("textbox");
await highlightAndExpectVisible(page, vonTextbox);
await vonTextbox.click();
await page.waitForTimeout(50);
/* const julyDate = page
.getByLabel("Choose Date")
.locator("div")
.filter({ hasText: "July" })
.first();
await highlightAndExpectVisible(page, julyDate);
await expect(julyDate).toBeVisible(); */
await page.waitForTimeout(50);
await vonTextbox.click();
await page.waitForTimeout(50);
const bisTextbox = page
.locator("div")
.filter({ hasText: /^Bis$/ })
.getByRole("textbox");
await highlightAndExpectVisible(page, bisTextbox);
await bisTextbox.click();
await page.waitForTimeout(50);
/* const augustDate = page
.getByLabel("Choose Date")
.locator("div")
.filter({ hasText: "August" })
.first();
await highlightAndExpectVisible(page, augustDate);
await expect(augustDate).toBeVisible();
await page.waitForTimeout(50); */
await bisTextbox.click();
await page.waitForTimeout(50);
await highlightAndExpectVisible(page, berichteHeading);
await berichteHeading.click();
await page.waitForTimeout(50);
// Buttons sichtbar
/* const anzeigenBtn = page.getByRole("button", { name: "Anzeigen" });
await highlightAndExpectVisible(page, anzeigenBtn);
await expect(anzeigenBtn).toBeVisible();
await page.waitForTimeout(50); */
const alleQuellenBtn = page.getByRole("button", { name: "Alle Quellen" });
await highlightAndExpectVisible(page, alleQuellenBtn);
// await expect(alleQuellenBtn).toBeVisible();
await alleQuellenBtn.click();
await page.waitForTimeout(50);
await alleQuellenBtn.click();
await page.waitForTimeout(50);
// Tabellenzellen
const tableCells = [
page.getByRole("cell", { name: "Prio" }),
page.getByRole("cell", { name: "Zeitstempel" }),
page.getByRole("cell", { name: "Quelle" }),
page.getByRole("cell", { name: "Meldung" }),
page.getByRole("cell", { name: "Status" }),
];
for (const cell of tableCells) {
await highlightAndExpectVisible(page, cell);
await cell.click();
await page.waitForTimeout(30);
}
// Interact with the first few data rows generically instead of hardcoded timestamps
const table = page.locator("table");
await table.first().waitFor({ state: "visible", timeout: 15000 });
const rows = table.locator("tbody tr");
const rowCount = await rows.count();
const maxRows = Math.min(5, rowCount);
for (let i = 0; i < maxRows; i++) {
const row = rows.nth(i);
const firstCell = row.locator("td").first();
await highlightAndExpectVisible(page, firstCell);
await firstCell.click();
// click a couple of other cells if present
const secondCell = row.locator("td").nth(1);
if (await secondCell.count()) {
await highlightAndExpectVisible(page, secondCell);
await secondCell.click();
}
const thirdCell = row.locator("td").nth(2);
if (await thirdCell.count()) {
await highlightAndExpectVisible(page, thirdCell);
await thirdCell.click();
}
await page.waitForTimeout(20);
}
}

View File

@@ -0,0 +1,141 @@
import { test, expect } from "@playwright/test";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
test.use({
viewport: {
height: 800,
width: 1280,
},
});
test("Einstellungen", async ({ page }) => {
await page.goto("/einstellungen");
// Gemeinsame Layout-Checks
await headerTest(page);
await navTest(page);
await footerTest(page);
await page.getByRole("button", { name: "Allgemeine Einstellungen" }).click();
await expect(
page.getByRole("heading", { name: "Allgemeine Einstellungen" })
).toBeVisible();
await expect(page.getByText("Name:")).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^Name:$/ })
.getByRole("textbox")
).toBeVisible();
await expect(page.getByText("MAC Adresse 1:")).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^MAC Adresse 1:$/ })
.getByRole("textbox")
).toHaveValue("0 48 86 81 46 143");
await expect(page.getByText("Systemzeit:")).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^Systemzeit übernehmen$/ })
.getByRole("textbox")
).toHaveValue("23.10.24 15:10:28");
await expect(
page.getByRole("button", { name: "Systemzeit übernehmen" })
).toBeVisible();
await expect(page.getByText("IP:")).toBeVisible();
await expect(
page.locator("div").filter({ hasText: /^IP:$/ }).getByRole("textbox")
).toBeVisible();
await expect(page.getByText("Subnet:")).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^Subnet:$/ })
.getByRole("textbox")
).toBeVisible();
await expect(page.getByText("Gateway:")).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^Gateway:$/ })
.getByRole("textbox")
).toBeVisible();
await expect(
page.getByRole("button", { name: "Neustart CPL" })
).toBeVisible();
await expect(page.getByRole("button", { name: "Speichern" })).toBeVisible();
await page.getByRole("button", { name: "OPCUA" }).click();
await expect(
page.getByRole("img", { name: "OPCUA Logo" }).first()
).toBeVisible();
await expect(
page.getByRole("img", { name: "OPCUA Logo" }).nth(1)
).toBeVisible();
await expect(page.getByText("Server Status:")).toBeVisible();
await expect(page.getByRole("button", { name: "Aktiviert" })).toBeVisible();
await expect(page.getByText("1")).toBeVisible();
await expect(page.getByText("Nodeset Name")).toBeVisible();
await expect(page.getByText("OPCUA Zustand")).toBeVisible();
await expect(page.getByRole("textbox")).toHaveValue(
"CPL V4 OPC UA Application Deutsche Bahn"
);
await expect(page.getByText("Aktuelle OPC-Clients")).toBeVisible();
await expect(page.getByText("0", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Datenbank" }).click();
await expect(
page.getByRole("heading", { name: "Datenbank Einstellungen" })
).toBeVisible();
await expect(
page.getByRole("button", { name: "Meldungen löschen" })
).toBeVisible();
await expect(
page.getByRole("button", { name: "Messwerte Logger löschen" })
).toBeVisible();
await page.getByRole("button", { name: "NTP" }).click();
await expect(
page.getByRole("heading", { name: "NTP Einstellungen" })
).toBeVisible();
await expect(page.getByText("NTP Server 1")).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^NTP Server 1$/ })
.getByRole("textbox")
).toBeVisible();
await expect(page.getByText("NTP Server 2")).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^NTP Server 2$/ })
.getByRole("textbox")
).toBeVisible();
await expect(page.getByText("NTP Server 3")).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^NTP Server 3$/ })
.getByRole("textbox")
).toBeVisible();
await expect(page.getByText("Zeitzone")).toBeVisible();
await expect(
page
.locator("div")
.filter({ hasText: /^Zeitzone$/ })
.getByRole("textbox")
).toBeVisible();
await expect(page.getByText("NTP aktiv:")).toBeVisible();
await expect(page.getByRole("button", { name: "Speichern" })).toBeVisible();
await page.getByRole("button", { name: "Benutzerverwaltung" }).click();
await expect(
page.getByRole("heading", { name: "Login Admin-Bereich" })
).toBeVisible();
await expect(
page.getByRole("textbox", { name: "Benutzername" })
).toBeVisible();
await expect(page.getByRole("textbox", { name: "Passwort" })).toBeVisible();
await expect(
page.getByRole("button", { name: "Admin anmelden" })
).toBeVisible();
});

View File

@@ -1,175 +0,0 @@
import type { Page } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
export async function runSettingsPageTest(page: Page) {
await page.goto("/einstellungen");
//----------------------
await headerTest(page);
await navTest(page);
await footerTest(page);
await page.waitForTimeout(400);
//----------------------
//await page.getByRole("button").filter({ hasText: /^$/ }).click();
await page.getByRole("button", { name: "Allgemeine Einstellungen" }).click();
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Allgemeine Einstellungen" })
);
await highlightAndExpectVisible(
page,
page.getByRole("heading", { name: "Allgemeine Einstellungen" })
);
await highlightAndExpectVisible(page, page.getByText("Name:"));
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^Name:$/ })
.getByRole("textbox")
);
await highlightAndExpectVisible(page, page.getByText("MAC Adresse 1:"));
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Systemzeit übernehmen" })
);
await highlightAndExpectVisible(page, page.getByText("IP:"));
await highlightAndExpectVisible(
page,
page.locator("div").filter({ hasText: /^IP:$/ }).getByRole("textbox")
);
await highlightAndExpectVisible(page, page.getByText("Subnet:"));
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^Subnet:$/ })
.getByRole("textbox")
);
await highlightAndExpectVisible(page, page.getByText("Gateway:"));
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^Gateway:$/ })
.getByRole("textbox")
);
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Neustart CPL" })
);
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Speichern" })
);
await page.getByRole("button", { name: "OPCUA" }).click();
await highlightAndExpectVisible(
page,
page.getByRole("img", { name: "OPCUA Logo" }).first()
);
await highlightAndExpectVisible(
page,
page.getByRole("img", { name: "OPCUA Logo" }).nth(1)
);
await highlightAndExpectVisible(page, page.getByText("Server Status:"));
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Aktiviert" })
);
await highlightAndExpectVisible(page, page.getByText("OPCUA Zustand"));
await highlightAndExpectVisible(page, page.getByText("1"));
await highlightAndExpectVisible(page, page.getByText("Nodeset Name"));
await highlightAndExpectVisible(page, page.getByText("Aktuelle OPC-Clients"));
await highlightAndExpectVisible(page, page.getByText("0", { exact: true }));
await page.getByRole("button", { name: "Datenbank" }).click();
await highlightAndExpectVisible(
page,
page.getByRole("heading", { name: "Datenbank Einstellungen" })
);
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Meldungen löschen" })
);
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Messwerte Logger löschen" })
);
await page.getByRole("button", { name: "NTP" }).click();
await highlightAndExpectVisible(
page,
page.getByRole("heading", { name: "NTP Einstellungen" })
);
await highlightAndExpectVisible(page, page.getByText("NTP Server 1"));
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^NTP Server 1$/ })
.getByRole("textbox")
);
await highlightAndExpectVisible(page, page.getByText("NTP Server 3"));
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^NTP Server 3$/ })
.getByRole("textbox")
);
await highlightAndExpectVisible(page, page.getByText("NTP Server 2"));
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^NTP Server 2$/ })
.getByRole("textbox")
);
await highlightAndExpectVisible(page, page.getByText("Zeitzone"));
await highlightAndExpectVisible(
page,
page
.locator("div")
.filter({ hasText: /^Zeitzone$/ })
.getByRole("textbox")
);
await highlightAndExpectVisible(page, page.getByText("NTP aktiv:"));
await highlightAndExpectVisible(page, page.getByRole("checkbox"));
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Speichern" })
);
await page.getByRole("button", { name: "Benutzerverwaltung" }).click();
await highlightAndExpectVisible(
page,
page.getByRole("heading", { name: "Login Admin-Bereich" })
);
await highlightAndExpectVisible(
page,
page.getByRole("textbox", { name: "Benutzername" })
);
await highlightAndExpectVisible(
page,
page.getByRole("textbox", { name: "Passwort" })
);
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Admin anmelden" })
);
await page.getByRole("textbox", { name: "Benutzername" }).click();
await page.getByRole("textbox", { name: "Benutzername" }).fill("admin");
await page.getByRole("textbox", { name: "Passwort" }).click();
await page.getByRole("textbox", { name: "Passwort" }).fill("admin");
await page.getByRole("button", { name: "Admin anmelden" }).click();
await highlightAndExpectVisible(page, page.getByText("Login erfolgreich!"));
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Abmelden" })
);
await highlightAndExpectVisible(
page,
page.getByRole("button", { name: "Admin anmelden" })
);
}
//---------------------------------------------------------------------

View File

@@ -0,0 +1,104 @@
import { test, expect } from "@playwright/test";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
test.use({
viewport: {
height: 800,
width: 1280,
},
});
test("System", async ({ page }) => {
await page.goto("/system");
// Gemeinsame Layout-Checks
await headerTest(page);
await navTest(page);
await footerTest(page);
await expect(
page.getByRole("heading", { name: "System Spannungen &" })
).toBeVisible();
await expect(page.getByRole("heading", { name: "+15V" })).toBeVisible();
await expect(page.getByText("15.06 VDetailansicht")).toBeVisible();
await expect(page.getByRole("heading", { name: "+5V" })).toBeVisible();
await expect(page.getByText("4.98 VDetailansicht")).toBeVisible();
await expect(page.getByRole("heading", { name: "-15V" })).toBeVisible();
await expect(page.getByText("-15.09 VDetailansicht")).toBeVisible();
await expect(page.getByRole("heading", { name: "-96V" })).toBeVisible();
await expect(page.getByText("-96.48 VDetailansicht")).toBeVisible();
await expect(page.getByRole("heading", { name: "ADC Temp" })).toBeVisible();
await expect(page.getByText("59.78 °CDetailansicht")).toBeVisible();
await expect(page.getByRole("heading", { name: "CPU Temp" })).toBeVisible();
await expect(page.getByText("56.92 °CDetailansicht")).toBeVisible();
await expect(page.getByRole("img").nth(2)).toBeVisible();
await expect(page.getByRole("img").nth(3)).toBeVisible();
await page
.getByRole("paragraph")
.filter({ hasText: "15.06 VDetailansicht" })
.getByRole("button")
.click();
await expect(page.getByRole("dialog")).toMatchAriaSnapshot(`
- 'heading "Detailansicht: +15V" [level=2]'
- button "Vollbild"
- button "Modal schließen"
`);
await expect(page.getByRole("dialog")).toMatchAriaSnapshot(`
- text: Von
- textbox: /\\d+\\.\\d+\\.\\d+/
- text: Bis
- textbox: /\\d+\\.\\d+\\.\\d+/
- text: "Zeitraum:"
- button "Alle Messwerte":
- img
- button "Daten laden"
`);
await expect(page.getByRole("dialog")).toMatchAriaSnapshot(`
- text: Von
- textbox: /\\d+\\.\\d+\\.\\d+/
- text: Bis
- textbox: /\\d+\\.\\d+\\.\\d+/
- text: "Zeitraum:"
- button "Alle Messwerte":
- img
- button "Daten laden"
- img
`);
await page.getByRole("button", { name: "Alle Messwerte" }).click();
await page.getByRole("option", { name: "Stündlich" }).click();
await expect(page.getByRole("button", { name: "Stündlich" })).toBeVisible();
await page.getByRole("button", { name: "Stündlich" }).click();
await page.getByRole("option", { name: "Täglich" }).click();
await expect(page.getByRole("button", { name: "Täglich" })).toBeVisible();
await page
.locator("div")
.filter({ hasText: /^Von$/ })
.getByRole("textbox")
.click();
await expect(page.getByLabel("Sunday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Monday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Tuesday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Wednesday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Thursday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Friday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Saturday", { exact: true })).toBeVisible();
await page
.locator("div")
.filter({ hasText: /^Bis$/ })
.getByRole("textbox")
.click();
await expect(page.getByLabel("Sunday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Monday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Tuesday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Wednesday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Thursday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Friday", { exact: true })).toBeVisible();
await expect(page.getByLabel("Saturday", { exact: true })).toBeVisible();
await page
.locator("header")
.filter({ hasText: "Detailansicht: +15V" })
.click();
await page.getByRole("button", { name: "Vollbild" }).click();
await page.getByRole("button", { name: "Vollbild verlassen" }).click();
await page.getByRole("button", { name: "Modal schließen" }).click();
});

View File

@@ -1,109 +0,0 @@
import type { Page } from "@playwright/test";
import { highlightAndExpectVisible } from "@playwright/utils/highlight";
import { navTest } from "@/playwright/tests/components/navigation/navTest";
import { headerTest } from "@/playwright/tests/components/header/headerTest";
import { footerTest } from "@/playwright/tests/components/footer/footerTest";
export async function runSystemTest(page: Page) {
await page.goto("/system");
//----------------------
await headerTest(page);
await navTest(page);
await footerTest(page);
await page.waitForTimeout(400);
//----------------------
// System Spannungen &
const systemSpannung = page.getByRole("heading", {
name: "System Spannungen &",
});
await highlightAndExpectVisible(page, systemSpannung);
await systemSpannung.click();
await page.waitForTimeout(100);
// +15V
const plus15V = page.getByRole("heading", { name: "+15V" });
await highlightAndExpectVisible(page, plus15V);
await plus15V.click();
await page.waitForTimeout(100);
// 15.06 VDetailansicht
const v15Detail = page.getByText("15.06 VDetailansicht");
await highlightAndExpectVisible(page, v15Detail);
await v15Detail.click();
await page.waitForTimeout(100);
// +5V
const plus5V = page.getByRole("heading", { name: "+5V" });
await highlightAndExpectVisible(page, plus5V);
await plus5V.click();
await page.waitForTimeout(100);
// 4.98 VDetailansicht
const v5Detail = page.getByText("4.98 VDetailansicht");
await highlightAndExpectVisible(page, v5Detail);
await v5Detail.click();
await page.waitForTimeout(100);
// -15V
const minus15V = page.getByRole("heading", { name: "-15V" });
await highlightAndExpectVisible(page, minus15V);
await minus15V.click();
await page.waitForTimeout(100);
// -15.09 VDetailansicht
const vMinus15Detail = page.getByText("-15.09 VDetailansicht");
await highlightAndExpectVisible(page, vMinus15Detail);
await vMinus15Detail.click();
await page.waitForTimeout(100);
// -98V
const minus98V = page.getByRole("heading", { name: "-98V" });
await highlightAndExpectVisible(page, minus98V);
await minus98V.click();
await page.waitForTimeout(100);
// -96.48 VDetailansicht
const vMinus98Detail = page.getByText("-96.48 VDetailansicht");
await highlightAndExpectVisible(page, vMinus98Detail);
await vMinus98Detail.click();
await page.waitForTimeout(100);
// ADC Temp
const adcTemp = page.getByRole("heading", { name: "ADC Temp" });
await highlightAndExpectVisible(page, adcTemp);
await adcTemp.click();
await page.waitForTimeout(100);
// 59.78 °CDetailansicht
const adcTempDetail = page.getByText("59.78 °CDetailansicht");
await highlightAndExpectVisible(page, adcTempDetail);
await adcTempDetail.click();
await page.waitForTimeout(100);
// CPU Temp
const cpuTemp = page.getByRole("heading", { name: "CPU Temp" });
await highlightAndExpectVisible(page, cpuTemp);
await cpuTemp.click();
await page.waitForTimeout(100);
await highlightAndExpectVisible(page, cpuTemp);
await cpuTemp.click();
await page.waitForTimeout(100);
// 56.92 °CDetailansicht
const cpuTempDetail = page.getByText("56.92 °CDetailansicht");
await highlightAndExpectVisible(page, cpuTempDetail);
await cpuTempDetail.click();
await page.waitForTimeout(100);
// img nth(2)
const img2 = page.getByRole("img").nth(2);
await highlightAndExpectVisible(page, img2);
await img2.click({ position: { x: 72, y: 53 } });
await page.waitForTimeout(100);
// img nth(3)
const img3 = page.getByRole("img").nth(3);
await highlightAndExpectVisible(page, img3);
await img3.click({ position: { x: 272, y: 93 } });
await page.waitForTimeout(100);
}

Some files were not shown because too many files have changed in this diff Show More