342 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
ISA
c03802e97f chore: Jenkinsfile 2025-09-05 12:14:38 +02:00
ISA
1485c0c92c fix: allow scripts in woodpecker 2025-09-05 11:56:46 +02:00
ISA
44ecbfa417 fix: woodpecker allow scripts 2025-09-05 11:55:10 +02:00
ISA
927a807c4d fix: woodpecker npm run server:sim 2025-09-05 11:54:00 +02:00
ISA
29a79ce0a9 fix: woodpecker 2025-09-05 11:30:39 +02:00
ISA
2166744c63 fix: woodpecker rimraf not found 2025-09-05 11:27:30 +02:00
ISA
81239f41ae fix: woodpecker compiler error 2025-09-05 11:19:00 +02:00
ISA
584593ba71 fix: .woodpecker.yml 2025-09-05 11:12:11 +02:00
ISA
4b83ff01cf test: playwright mit npm run dev erfolgreich 2025-09-05 10:54:52 +02:00
ISA
8c88aa843c chore: move playwright components folder to tests 2025-09-05 10:25:31 +02:00
ISA
8a9cd72718 feat(kue705FO): scrolling für lange Modulnamen (48 Zeichen) + Version-Gate/Env-Override
- Unterstützt bis zu 48 Zeichen im Modulnamen; bei Überlänge automatische Laufschrift
- Marquee via react-fast-marquee (SSR-sicher per next/dynamic)
- Overflow-Erkennung + Tooltip mit vollem Namen
- Version-Gate: aktiviert ab V4.30
2025-09-05 08:41:10 +02:00
ISA
2484d057fb feat: prepare KÜ 8 for scrolling text 2025-09-05 07:52:36 +02:00
ISA
941b914fa9 test: woodpecker 2025-09-05 07:28:44 +02:00
ISA
bd683d021a feat: local-cpl-sim.mjs port 3000 2025-09-05 07:19:12 +02:00
ISA
124b1c1e59 refactor: retime all messages and charts data 2025-09-05 07:05:01 +02:00
ISA
6820fa9eed feat: local-cpl-sim.mjs Detailansicht Modal in System 2025-09-04 14:04:33 +02:00
ISA
3daa6b1dbb feat: local-cpl-sim.mjs analogInputs /Messwerteingäge / analoge Eingänge 2025-09-04 13:36:13 +02:00
ISA
9c7ad37233 feat: local-cpl-sim.mjs digitalInputs /Messwerteingänge 2025-09-04 13:24:10 +02:00
ISA
0286670b81 feat: local-cpl-sim system 2025-09-04 12:26:06 +02:00
ISA
02a0ce5891 feat: local-cpl-sim meldungen/Berichte 2025-09-04 12:02:04 +02:00
ISA
47e0efeb80 feat: local-cpl-sim.mjs kabelueberwachung 2025-09-04 11:16:06 +02:00
ISA
b62c477d50 feat: local-cpl-sim.mjs Einstellungen done 2025-09-04 11:02:17 +02:00
ISA
653a31ce63 feat: local-cpl-sim.mjs 2025-09-04 10:54:37 +02:00
ISA
57ffdecb10 test: npx playwright test erfolgreich 2025-09-03 15:58:11 +02:00
ISA
11bd68200b refactoring: test files 2025-09-03 15:07:57 +02:00
ISA
2c92ca0866 fix: DigitalOutputsVies.tsx 2025-09-03 14:06:35 +02:00
ISA
a9ccdfc9ab feat: Messverlauf bei Systemwerten (Temperatur und Spannungen) mit Datumsauswahl 2025-09-03 13:38:05 +02:00
ISA
f4f4c28cb7 feat(mocks): mesages_all.json mock script 2025-09-03 12:24:41 +02:00
ISA
d6fcf95795 feat(mocks): retime chart mocks to today; add global/all-slot scripts
Add retimeAnalogInputs.mjs (all slots, single slot, or path)
Add retimeAllCharts.mjs (recursive under chartsData)
Update package.json with npm scripts:
mocks:retime:ai (all analog slots)
mocks:retime:ai:slot (single slot via %SLOT%)
mocks:retime:all (entire chartsData tree)
Preserve relative deltas; set first entry to today (same time); DIA2 daily at 00:00
Skip files/arrays without parsable "t" timestamps
2025-09-03 11:45:44 +02:00
ISA
6c2707ff47 feat: update analogInputs data in mocks 2025-09-03 11:30:44 +02:00
ISA
420f1da114 refactor(api): Pfad für Digitalausgänge vereinheitlicht; Duplikat entfernt
updateDigitalOutputsHandler: JSON-Schreibpfad auf digitalOutputsMockData.json umgestellt
digitalOutputsMockData.json gelöscht (nicht mehr benötigt)
GET-Handler liest bereits aus dem kanonischen Pfad; Verhalten unverändert
2025-09-03 10:37:48 +02:00
ISA
5aa7618832 refactor(api): Legacy-Fallback entfernt; nur noch chartsData/cable-monitoring-data 2025-09-03 10:14:14 +02:00
ISA
35171891a3 fix: AnalogInputsView.tsx style 2025-09-03 09:54:23 +02:00
ISA
2df1ee1022 del: remove old mock files 2025-09-03 09:43:12 +02:00
ISA
7fe842aa93 del: remove 4000value folder from mock 2025-09-03 09:31:04 +02:00
ISA
cdf4869548 doc: TODOs 2025-09-03 07:26:01 +02:00
ISA
bb115a9a4f fix: curl 2025-09-03 07:02:20 +02:00
ISA
da87ebc5c8 fix: Die .woodpecker.yml wurde korrigiert:
trigger entfernt, stattdessen wieder when auf Top-Level.
Für die Notification-Steps wurde das Image alpine/curl:latest hinzugefügt.
Damit sollten die Linter-Fehler verschwinden und die Push-Benachrichtigungen funktionieren.
2025-09-03 06:42:45 +02:00
ISA
5b4eb7ff51 fix: Die .woodpecker.yml wurde korrigiert:
Der clone-Step ist jetzt Teil der Steps.
curl.exe wurde zu curl geändert (Linux-kompatibel).
when wurde zu trigger geändert.
Damit sollte Woodpecker den Build korrekt starten und die Push-Benachrichtigungen funktionieren.
2025-09-03 06:39:15 +02:00
ISA
3254563458 playwright headless true 2025-09-03 06:32:35 +02:00
ISA
5252ec5998 feat(ci): Push-Benachrichtigung bei Test-Erfolg oder -Fehlschlag für Jenkins und Woodpecker integriert 2025-09-03 06:29:41 +02:00
ISA
2d9cd74375 refactor: move headerTest to header folder 2025-09-02 15:03:43 +02:00
ISA
f4e0620b49 fix: playwright Test bugs beheben 2025-09-02 14:40:08 +02:00
ISA
35e34b96d1 WIP: Test fehlgeschlagen 2025-09-02 14:22:58 +02:00
ISA
fb79817136 test: extracted navigation tests to separate file 2025-09-02 11:48:54 +02:00
ISA
89dc26b0d2 Test: nav ausgelagert 2025-09-02 11:25:25 +02:00
ISA
c8616f7bbe Test: done 2025-09-02 11:08:34 +02:00
ISA
b0b9952a2d chore: remove jsconfig.json, project uses only tsconfig.json for path aliases 2025-09-02 10:49:30 +02:00
ISA
8da1457e4d Test: playwright done 2025-09-02 10:40:12 +02:00
ISA
7dc64ca972 test: refactoring playwright test structure 2025-09-02 09:58:46 +02:00
ISA
1b038ac844 fix: Detailansicht Modal sichtbar beim klicken 2025-09-02 08:51:13 +02:00
ISA
cbc476b09a fix: ohne E-Mail 2025-09-02 08:13:28 +02:00
ISA
306f469634 fix: woodpecker 2025-09-02 08:09:01 +02:00
ISA
772baea4ed feat: woodpecker E-Mail 2025-09-02 07:51:01 +02:00
ISA
f3f6e25e9c fix(ci): match case for systemMockData.js (Linux case-sensitive) 2025-09-01 16:40:34 +02:00
ISA
43fe9e2065 test: find mock 2025-09-01 16:28:32 +02:00
ISA
30f156934c fix: TDR Messungstarten statt TDR aktivieren 2025-09-01 16:14:49 +02:00
ISA
b108d63106 test: woodpecker dev mode 2025-09-01 16:04:38 +02:00
ISA
b53762cf5c test: .woodpecker 2025-09-01 15:33:22 +02:00
ISA
629548bfdd Test: .woodpecker.yml 2025-09-01 15:24:48 +02:00
ISA
174d67cfd8 Test: In KÜ RSL: Zahl mit 3 Nachkommastellen 2025-09-01 14:33:31 +02:00
ISA
57baca292a fix: KÜ ISO 2 Nachkommastellen und RSL 3 Nachkommastellen 2025-09-01 14:03:04 +02:00
ISA
0c02e6f1c9 fix: System Footer responsive 2025-09-01 13:43:26 +02:00
ISA
dcf22d08fb fix: playwright headless true 2025-09-01 12:08:18 +02:00
ISA
867031d3c3 fix: playwright August Text 2025-09-01 12:00:09 +02:00
Ismail Ali
0c3eb4cc5a fix: playwright 2025-08-31 21:06:59 +02:00
Ismail Ali
d4d0c91400 fix: Jenkins 2025-08-31 18:22:42 +02:00
Ismail Ali
9147cec40f fix: Jenkins node20 2025-08-31 16:39:29 +02:00
Ismail Ali
e732971cdc Test: Jenkinsfile 2025-08-31 16:04:35 +02:00
ISA
9db92a2728 fix: Die Konfiguration ist jetzt angepasst:
Playwright verwendet immer einen bereits laufenden Dev-Server (reuseExistingServer: true).
Damit gibt es keinen Port-Konflikt mehr,
2025-08-29 14:31:26 +02:00
ISA
1d815d4265 fix: playwright ->npm run dev -p 3000 2025-08-29 14:22:28 +02:00
ISA
5aece28eb1 fix: woodpecker npm run dev 2025-08-29 14:14:40 +02:00
ISA
0f570ac5b0 playwright: headless:true 2025-08-29 13:57:59 +02:00
ISA
8850b0ffda fix: all.test.ts 2025-08-29 13:47:23 +02:00
ISA
6df31455a9 test: systemTest.ts 2025-08-29 11:57:25 +02:00
ISA
05c1c9c0cf Headless wird sicher erzwungen (auch wenn lokal anders).
Der Next.js-Server wird gebaut und via npm start im selben Container gestartet (statt npm run dev).

Robustere Browser-Flags für Container.

Artefakte (Trace/Screenshot/Video) nur bei Fehlern, damit der CI schnell bleibt.

baseURL kommt aus ENV (E2E_BASE_URL) – lokal bleibt’s http://localhost:3000.

PLAYWRIGHT_BROWSERS_PATH=0 bleibt (Option B).
2025-08-29 11:45:09 +02:00
ISA
9f43fdc820 fix: start chromium headless 2025-08-29 11:25:57 +02:00
ISA
763f5293bc test: meldungenTest.ts 2025-08-29 11:18:43 +02:00
ISA
0e7b2e53aa fix: playwright config 2025-08-29 10:38:31 +02:00
ISA
7a4e6d92c2 fix: woodpecker install chromium 2025-08-29 10:22:53 +02:00
ISA
3e998d6644 fix: headless 2025-08-29 10:15:42 +02:00
ISA
b6ab6a11f9 fix. npm install statt npm ci, um Zeit zu sparen 2025-08-29 10:10:49 +02:00
ISA
a8388b27b9 fix: woodpecker node 22 2025-08-29 10:01:36 +02:00
ISA
b1ac0f87f1 fix: woodpecker install browser deps 2025-08-29 09:59:12 +02:00
ISA
036630f598 fix: woodpecker 2025-08-29 09:19:22 +02:00
ISA
2ce7c54697 feat:woodpecker 2025-08-29 08:57:29 +02:00
ISA
ae5798bcdf test: digitalOutputs 2025-08-29 08:47:17 +02:00
ISA
93b4859700 feat: Test nach Navigation zuordnen 2025-08-28 16:22:31 +02:00
ISA
b011ab9862 feat: Jenkinsfile node_Modules verzeichnis in Docker Volume gespeichert 2025-08-28 15:36:32 +02:00
ISA
9649eec907 Test: kabelueberwachungTest.ts 2025-08-28 15:01:52 +02:00
ISA
8117ebdf45 fix: Jenkinsfile 2025-08-28 13:34:11 +02:00
ISA
e8477640e2 fix: test digitalInputsTest.ts 2025-08-28 11:50:27 +02:00
ISA
9e84386a5d test: digitalInputsTest.ts 2025-08-28 11:49:34 +02:00
ISA
93809a85a4 test: 2025-08-28 09:00:11 +02:00
ISA
f9f358a678 test: jenkinsfile 2025-08-28 08:52:47 +02:00
ISA
539ebd5ff6 test: WIP: digitalInputsTest.ts 2025-08-28 08:45:10 +02:00
ISA
c8f3d91f9c fix: Jenkinsfile 2025-08-28 08:13:53 +02:00
ISA
37bbd6a9b3 feat: Jenkinsfile 2025-08-28 07:59:32 +02:00
ISA
2d9a7118c6 WIP: playwright Präsentation 2025-08-28 07:05:17 +02:00
ISA
699ebef7bd fix: KabelModulstatus KÜ V 421 und 431 2025-08-27 09:27:47 +02:00
ISA
e69934ff51 test: Einstellungen Seite mit highlighting 2025-08-25 14:50:25 +02:00
ISA
f25c527e71 test: playwright funktion highlight in separate Datei 2025-08-25 14:40:06 +02:00
ISA
fb36561cb9 fix: Ein Abgleich darf natürlich nicht die Seiten blockieren. 2025-08-20 16:36:05 +02:00
ISA
6e98a98670 Presentation playwright 2025-08-19 16:15:58 +02:00
ISA
5f97731e2b feat: light und dark mode Messwerteingänge 2025-08-18 16:44:51 +02:00
ISA
c711a6a132 feat: header light und dark mode 2025-08-18 16:40:37 +02:00
ISA
b72b9d665b feat: Übersicht Seite dark und light mode 2025-08-18 16:22:01 +02:00
ISA
9c411be38c feat: digitale Eingänge light und dark mode 2025-08-18 16:16:55 +02:00
ISA
3a9543f7a7 feat: Schaltausgänge dark und light mode 2025-08-18 16:13:46 +02:00
ISA
6036c48332 feat: Messwerteingänge light und dark mode 2025-08-18 16:09:14 +02:00
ISA
7e41e5131f feat: Berichte light und dark mode 2025-08-18 16:03:57 +02:00
ISA
6756bbf0f8 feat: einstellungen dark und light mode 2025-08-18 15:35:32 +02:00
ISA
a955564ee3 feat: Einstellungen in dark und light mode 2025-08-18 14:59:22 +02:00
ISA
c496939004 feat: System light and dark mode 2025-08-18 14:48:31 +02:00
ISA
bb06618919 feat: system dark and light mode 2025-08-18 14:44:10 +02:00
ISA
eae8ea37d0 feat: Dashboard light and dark mode 2025-08-18 14:34:24 +02:00
ISA
710d780a3a feat: Navigation dar und light mode 2025-08-18 14:24:16 +02:00
ISA
8097246049 feat: playwright scraper 2025-08-18 14:16:44 +02:00
ISA
e6aafd6b0c fix: window.location.pathname statt gestes Wert 2025-08-18 10:21:52 +02:00
ISA
984c776b2a fix: Kabelüberwachung Konfiguration sichern und zurücksetzen
es muss so sein
https://10.10.0.118/CPL?/kabelueberwachung.html&KSB00=1
und nicht so
https://10.10.0.118/CPL?KSB00=1
2025-08-18 08:53:51 +02:00
ISA
a84e8c529f WIP: Playwright test 2025-08-15 14:47:29 +02:00
ISA
04b9a0dc1d style: Messwertkurven Legende und Liniern style angepasst 2025-08-15 13:59:19 +02:00
ISA
7d6263b6fb Das automatische Nachladen im DetailModal.tsx passiert jetzt nur noch alle 4 Sekunden und maximal 2 Mal. Damit wird dein Embedded-System geschont und es gibt keine Überlastung durch zu viele Anfragen. 2025-08-15 13:23:25 +02:00
ISA
4e8221c892 Fix: Messkurven-Modal (ISO/RSL) lädt Kurve automatisch, setzt Dropdown & DateRangePicker korrekt zurück
- Dropdown für Messkurven (ISO/RSL) wird beim Öffnen auf 'Alle Messwerte' (DIA0) gesetzt
- Messkurve wird beim Öffnen des Modals automatisch geladen
- Beim Schließen werden vonDatum, bisDatum, Dropdown und DateRangePicker zurückgesetzt
- Gleiches Verhalten für ISO- und RSL/Loop-Modal
2025-08-15 11:22:32 +02:00
ISA
d75d9ce578 Fix: Messkurven-Modal (ISO/RSL) lädt Kurve automatisch, setzt Dropdown & DateRangePicker korrekt zurück
- Dropdown für Messkurven (ISO/RSL) wird beim Öffnen auf 'Alle Messwerte' (DIA0) gesetzt
- Messkurve wird beim Öffnen des Modals automatisch geladen
- Beim Schließen werden vonDatum, bisDatum, Dropdown und DateRangePicker zurückgesetzt
- Gleiches Verhalten für ISO- und RSL/Loop-Modal
2025-08-15 10:50:01 +02:00
ISA
b006e3a993 playwright: Einstellungen Seite 2025-08-14 16:46:40 +02:00
ISA
5e9c7e9bfe playwright: test Reihenfolge 2025-08-14 16:20:42 +02:00
ISA
3e0b1e98bb playwright: Reihenfolge 2025-08-14 16:11:50 +02:00
ISA
94051b69f9 playwright: dashboard 2025-08-14 15:56:10 +02:00
ISA
629385fa5c playwright: Dashboard Seite test 2025-08-14 15:06:35 +02:00
ISA
a446ce80ee playwright: analoge Eingänge Test erfolgreich 2025-08-14 14:41:04 +02:00
ISA
71dd37bb0e playwright: analge Eingänge mit highlight color test erfoögreich 2025-08-14 14:23:00 +02:00
ISA
08370cf898 playwright: analoge Eingänge Test erfolgreich 2025-08-14 14:05:21 +02:00
ISA
fa92004d94 Playwright : ausgewählte Element rot färben 2025-08-14 13:39:20 +02:00
ISA
3753babf5f doc: comment in test for analog inputs 2025-08-14 12:08:52 +02:00
ISA
87cbdca79c refactor: playwright and tests in one folder 2025-08-14 12:06:20 +02:00
ISA
bb68327604 Feat: Analogeingänge (Messwerteingänge) Modal 2025-08-14 10:20:33 +02:00
ISA
2db9da2394 feat: close button and maximize modal 2025-08-14 09:42:24 +02:00
ISA
c3fc8e0a4a Messkurve Modal in Messwerteingänge 2025-08-14 09:03:50 +02:00
ISA
eff606e59a fix: KÜ Version 4.20 in daschboard KÜs Status anzeigen 2025-08-14 08:09:43 +02:00
ISA
f1ba9d4e4d docs: Kabelüberwachung Overlay für Events (Abgleich, TDR und RSL) Messung 2025-08-13 16:09:55 +02:00
ISA
7bc13505b2 feat: Overlay nicht über die Seite sondern nur über den KÜ Slot wenn ein Event kommt 2025-08-13 14:54:19 +02:00
ISA
6da0408140 doc in TODO 2025-08-13 14:31:51 +02:00
ISA
ad6d89847e CPL Events Progressbar in Prozent anzeigen 2025-08-13 14:20:38 +02:00
ISA
5496254acb Events Prograssbar in Prozent 2025-08-13 14:15:51 +02:00
ISA
8fcbf6cfcd Progressbar mit Prozent und Zeit 2025-08-13 14:04:21 +02:00
ISA
974f468766 feat: Slot Nummer anzeigen bei Events 2025-08-13 12:17:13 +02:00
ISA
0fb6d184bd feat: Meldung für Events darstellen (Kalibrierung, TDR ud Schleifenmessung) 2025-08-13 12:13:16 +02:00
ISA
48d634295a Events Schleifenmessung, TDR-Messung und Abgleich in public/CPL/kueData.js eingefügt um später zu lesen und entsprechend ' Bitte Warten' Meldung zu erstellen für den User 2025-08-13 10:58:45 +02:00
ISA
0246e34de4 Daten von CPL bekommen DIA0- DIA2 ISO und RSL 2025-08-13 10:29:28 +02:00
ISA
ba0cb732d9 npm run dev und build ohne fehler durchgeführt 2025-08-13 08:54:34 +02:00
ISA
91b76b8e8d OPC-Clients in settings 2025-08-12 13:53:47 +02:00
ISA
bb662bf856 fix: Meldungen werden wieder angezeigt in KÜ Charts 2025-08-12 13:09:16 +02:00
ISA
2765d06836 WIP: Meldungen 2025-08-12 12:18:06 +02:00
ISA
b8b5c36a60 Isolatioswiderstand Chart abhängig von dropdown menu select name und nicht von Titel 2025-08-12 12:11:47 +02:00
ISA
31a54deb2d chore(eslint): ignore irregular whitespace in comments (keep rule strict for code) 2025-08-12 11:11:39 +02:00
ISA
71f120aa27 fetchCableData.mjs
Sends Authorization: Basic <base64(user:pass)> with configurable credentials.
Accepts --user and --pass, or env CPL_USER/CPL_PASS; defaults to Littwin/Littwin.
Uses an https.Agent({ rejectUnauthorized: false }) when --insecure is set.
Corrected output folder to cable-monitoring-data.
CLI parser supports both --key=value and --key value (PowerShell friendly).
Quick usage (PowerShell)

All 32 slots, both types (iso=3, rsl=4), all DIA modes, last 30 days:
npm run mocks:cable
Specific date range (e.g., 2025-07-13 to 2025-08-12), all slots and types:
node .\mocks\scripts\fetchCableData.mjs --from 2025-07-13 --to 2025-08-12 --insecure
Only slot 0, Isolationswiderstand, DIA1:
node .\mocks\scripts\fetchCableData.mjs --slots 0 --modes DIA1 --types iso --from 2025-07-13 --to 2025-08-12 --insecure
Provide credentials explicitly:
node .\mocks\scripts\fetchCableData.mjs --user Littwin --pass Littwin --insecure
Or via environment variables for the session:
$env:CPL_USER = "Littwin"; $env:CPL_PASS = "Littwin"
node .\mocks\scripts\fetchCableData.mjs --insecure
Output structure

mocks/device-cgi-simulator/cable-monitoring-data/slot{0..31}/
isolationswiderstand/DIA0.json, DIA1.json, DIA2.json
schleifenwiderstand/DIA0.json, DIA1.json, DIA2.json
I smoke-tested slot 0, DIA1, iso with login and it produced DIA1.json under slot0/isolationswiderstand. If you need me to also add a convenience npm script with user/pass placeholders, say the credentials source you prefer (env vs args), and I’ll wire it.
2025-08-12 10:50:08 +02:00
ISA
77c939697c loop DatePicke 2025-08-12 09:58:44 +02:00
ISA
234608973e feat(iso): DateRangePicker-Zeitraum bei "Daten laden" anwenden und fix debug für build 2025-08-12 09:35:19 +02:00
ISA
8af8e14878 feat(iso): DateRangePicker-Zeitraum bei "Daten laden" anwenden 2025-08-12 09:33:25 +02:00
ISA
e4b56faf75 feat: RSL starten in Dev mode 15 Sek. und in prod. 120 Sek. 2025-08-12 08:25:22 +02:00
ISA
100dab06ed PlayWright Test 2025-08-12 08:03:35 +02:00
ISA
e7d120c477 RSL-Progress (120s Overlay mit Balken + Blockierung) ist implementiert: Button zeigt RSL läuft…, Daten laden ist gesperrt, Overlay mit Restsekunden und Fortschritt. Countdown endet automatisch. 2025-08-11 16:45:56 +02:00
ISA
2bf02af96f Globales Auto-Highlight wurde eingefügt 2025-08-11 16:20:11 +02:00
ISA
9ca5ee9e66 playwright recording and testing 2025-08-11 16:13:01 +02:00
ISA
bc20f3869d ISO, RSL, TDR, und KVZ Modal nach Wünsch angepasst für KÜs 2025-08-11 14:24:03 +02:00
ISA
06aa3c8f3e ISO & RSL dropdowns moved to headers like TDR; removed old dropdowns from action bars, cleaned imports, fixed TypeScript issues 2025-08-11 13:35:14 +02:00
ISA
8d1b5ceddc LoopChartActionBar verhält sich jetzt wie im ISO-Modal: Bei Auswahl „Meldungen“ 2025-08-11 13:08:34 +02:00
ISA
806eaaeff7 Daten von 118. in mocks geholt 2025-08-11 12:07:47 +02:00
ISA
c107738625 fix: KVZ Button style wie die anderen (ISO, RSL, TDR) und mit eigene Modal 2025-08-11 11:35:03 +02:00
ISA
9b05f21ccc feat: migrate from Cypress to Playwright for E2E testing
- Remove Cypress dependencies and configuration files
- Install @playwright/test with browser support
- Add playwright.config.ts with optimized settings for Next.js
- Migrate existing Cypress tests to Playwright format
- Add new E2E test scripts to package.json
- Configure GitHub Actions workflow for automated testing
- Update .gitignore for Playwright artifacts

BREAKING CHANGE: E2E testing framework changed from Cypress to Playwright
2025-08-01 15:45:59 +02:00
ISA
3b61dcb31b git commit -m "feat: Enhance DetailModal with auto-loading and improved UX
- Add automatic data loading every 2 seconds when no chart data available
- Implement intelligent cursor-wait display for entire modal during loading
- Auto-reset to 'Alle Messwerte' (DIA0) and clear date fields on modal open
- Add Tailwind-based color system for chart lines (gray for min/max, littwin-blue for current/average)
- Improve chart line layering with background/foreground organization
- Add periodic UI updates to ensure responsive loading feedback
- Maintain manual 'Daten laden' button control alongside auto-loading
- Fix TypeScript dependencies and optimize useEffect performance"
2025-08-01 14:05:58 +02:00
ISA
f8bfea039c fix: System ->Detailansicht -> Modal 2025-08-01 13:46:33 +02:00
ISA
136d3151cf fix: Chart System 2025-08-01 13:10:32 +02:00
ISA
ba1b0d8e79 fix: nur Daten abrufen, wenn 'Daten laden' button geklickt wird 2025-08-01 12:23:10 +02:00
ISA
0b7542488e fix: link in console 2025-08-01 12:10:12 +02:00
ISA
3098ce67f0 fix: richtige Link in system fetch service 2025-08-01 11:30:11 +02:00
ISA
0a20f91ba6 feat: fetchSystemData.mjs erweitert und optimiert
Analoge Eingänge und Systemdaten werden jetzt gemeinsam abgerufen und gespeichert
Einheitliche Benennung (input statt eingang) für analoge Eingänge
Datumssplittung als Hilfsfunktion ausgelagert
Kommentare und Beschreibung verbessert
2025-08-01 10:25:05 +02:00
ISA
975d3b726f refactor: mMeldungen angepasst 2025-08-01 09:44:30 +02:00
ISA
423c87ca11 feat: Script zum Abrufen und Speichern von CPL-Meldungen als Mockdaten hinzugefügt 2025-08-01 08:27:45 +02:00
ISA
c1ed09a21d style: apply littwin-blue color to NTP settings checkbox
- Add accent-littwin-blue class to NTP active checkbox
- Increase checkbox size to w-4 h-4 for better visibility
- Maintain consistent brand coloring across UI components
2025-07-31 16:31:16 +02:00
ISA
4fe64382f3 feat: hide logout button when admin is not logged in
- Add conditional rendering for "Abmelden" button based on isAdminLoggedIn state
- Button only appears when localStorage contains "isAdminLoggedIn": "true"
- Improves UI cleanliness by hiding unnecessary logout option for regular users
- Maintains existing admin warning banner and logout functionality when needed
2025-07-31 16:26:25 +02:00
ISA
e86de5cefe feat: implement modal chart system with conditional UI and message filtering
- Add automatic data loading on IsoChartView modal open with timeout to prevent infinite loops
- Implement conditional UI visibility in IsoChartActionBar using CSS visibility property
- Create stable layout where controls reserve space when hidden (DatePicker, DIA dropdown, "Daten laden" button)
- Add Report.tsx component with precise CableLine filtering using exact string matching
- Enhance message filtering with debug logging and fallback identifier support
- Integrate chartTitle state management for seamless switching between "Messkurve" and "Meldungen"
- Add useIsoDataLoader hook for automatic chart data loading without user interaction
- Implement enhanced filtering logic to prevent false matches (CableLine1 vs CableLine16)
- Style Report component with consistent table layout matching MeldungenView design
- Add visual filter indicators and improved error messaging for better UX

Technical improvements:
- Replace conditional rendering with visibility control to maintain layout stability
- Add comprehensive logging for debugging message source filtering
- Implement proper cleanup for timeouts to prevent memory leaks
- Use exact string matching and prefix validation for precise slot identification
2025-07-31 16:02:04 +02:00
ISA
63e1b85a44 feat: Meldungen in in Iso Chart 2025-07-31 15:57:50 +02:00
ISA
cdd26931a1 feat: implement chart modal with report functionality for cable monitoring
- Add chartTitle state management to kabelueberwachungChartSlice with "Messkurve"/"Meldungen" options
- Update IsoChartActionBar dropdown to show current chartTitle value with proper binding
- Implement conditional rendering in IsoChartView between IsoMeasurementChart and Report components
- Create Report.tsx component using same data structure as MeldungenView (Meldung type)
- Add slot-based message filtering for specific cable monitoring units (KÜ)
- Integrate getMessagesThunk for consistent data loading across components
- Style Report component with consistent table layout, German date formatting, and Littwin branding
- Enable seamless switching between measurement chart and filtered messages in modal
2025-07-31 15:25:46 +02:00
ISA
638b7bf519 feat: TDR --> Messkurven TDR anzeigen und dort Schalter Messung aktivieren 2025-07-31 14:45:40 +02:00
ISA
46ba692af0 feat: KVZ JSON Daten für mock auf CPL hochgeladen und getestet 2025-07-31 14:32:52 +02:00
ISA
90b9616d50 style: KVZ LEDs style 2025-07-31 14:11:24 +02:00
ISA
85860ae9f0 style: LEDs style 2025-07-31 13:54:56 +02:00
ISA
421e1f5425 feat: KVZ API JSON Data 2025-07-31 13:44:30 +02:00
ISA
97eb40e1c6 Feat: KVz Bereich in EinstellungsModal in KÜs Modal 2025-07-31 10:42:06 +02:00
ISA
b68eb10ad4 feat: TDR starten Button in KÜ Chart 2025-07-31 10:13:33 +02:00
ISA
86b35e9925 fix: Schleifenwiderstand (TDR) Messung starten Button auf der Produktion 2025-07-31 09:54:22 +02:00
ISA
ad6642b5e7 feat: Display und Chart für KÜs 2025-07-31 09:37:34 +02:00
ISA
e76c8d9bd2 feat; in KÜ Chart RSL und ISO start button 2025-07-28 14:36:03 +02:00
ISA
d4335960bf deat: KVz anzeigen 2025-07-28 14:28:11 +02:00
ISA
9457233c7d fix: Chart Titel Isolationsmessung zu schleifenmessung 2025-07-28 13:58:14 +02:00
ISA
9a8a0501a5 cleanup: Kue705FO 2025-07-28 13:47:21 +02:00
ISA
ce1dacda9b feat: ISO, RSL und TDR separate Charts ohne den Switcher 2025-07-28 13:39:46 +02:00
ISA
7a9fbc97dd fix: KÜ slotnummer in der Messkurven Modal 2025-07-28 08:29:48 +02:00
ISA
f79c225b71 fix(Kue705FO): maintain consistent 3-line display layout
- Keep alarm status line with empty space when no alarm is present
- Use non-breaking space (\u00A0) to preserve line height and layout
- Remove green "Status: OK" text for cleaner display
- Ensure consistent 3-line structure: Alarm/Empty, ISO, RSL
2025-07-25 13:45:16 +02:00
ISA
afdcb6b92f refactor(Kue705FO): integrate chart functionality into detail view buttons
- Remove separate TDR/Schleife Messkurve buttons section
- Add direct chart opening to ISO, RSL, and TDR buttons in detail view
- ISO and RSL buttons now open Schleife chart with proper state setup
- TDR button opens TDR chart with distance calculation
- Remove unused button container but keep structure for future use
- Clean up unused imports and variables needed
2025-07-25 11:56:46 +02:00
ISA
4af7836a54 feat(Kue705FO): replace switch buttons with direct chart access buttons
- Remove Schleife/TDR switch buttons and separate Messkurve button
- Add "TDR Messkurve" and "Schleife Messkurve" buttons for direct chart access
- Each button opens the corresponding chart type directly
- Improve user experience by reducing clicks needed to access specific charts
- Clean up unused imports (handleButtonClick, tdrLocation, tdrActive)
2025-07-25 11:47:38 +02:00
ISA
cfe838dd07 TDR und Schleife Button in KÜs wieder für die Funktionen 2025-07-24 14:56:24 +02:00
ISA
8dafd5fe67 TDR und Schleife Button in KÜs wieder für die Funktionen 2025-07-24 14:55:48 +02:00
ISA
e932bee120 feat:
Anzeige KÜ-Display:

1. Zeile Alarm: Isolationsfehler, Schleifenfehler, Aderbruch, Erdschluß, Messpannung: Immer in Rot; wenn kein Alarm, bleibt die Zeile leer

2. Zeile: Isowert: xx MOhm (großes M)

in Rot, wenn Iso-Fehler ansteht

Beispiel: ISO: 100 MOHm der beim Abliech:  ISO: Abgleich

3. Zeile: Schleifenwert, xx kOhm (kleines k)

in Rot, wenn Schleifenfehler ansteht

Beispiel:: RSL: 1,7 kOhm oder wenn Schleifenmessung aktiv: RSL: Messung
2025-07-24 13:59:44 +02:00
ISA
c1f6c19fdf Feat: Fenster nicht schließen für Firmware Update 2025-07-24 12:15:50 +02:00
ISA
b7ff3b07cd Firmware Update Bestätigung in Littwin blau 2025-07-24 11:55:28 +02:00
ISA
9bd69f7a07 feat Schleifeund TDR in sepaterate Bereiche in KÜ 2025-07-24 09:32:56 +02:00
ISA
357fb6c816 feat: Isowert und Schleifenwiderstanf in schwarzen Display zusammen 2025-07-24 08:25:52 +02:00
ISA
628cbc405e Hide fallsensors 2025-07-23 15:35:48 +02:00
ISA
ada2f5e2a7 feat: Fallsensors 2025-07-23 15:11:13 +02:00
ISA
bdbdd27963 feat: Add cursor wait state to AnalogInputsTable rows during data loading
- Applied `cursor-wait` style to table rows (`<tr>`) in AnalogInputsTable when loading is true.
- Ensured consistent cursor behavior across the entire table and rows
2025-07-23 13:40:43 +02:00
ISA
2272668ace feat: Add cursor wait during chart data loading
- Implemented cursor wait state while chart data is being loaded in `AnalogInputsChart.tsx`.
- Fixed missing dependencies in `useEffect` and defined `loading` state.
- Updated `handleFetchData` to manage
2025-07-23 13:17:31 +02:00
ISA
bc554d3474 feat: Add cursor wait during chart data loading
- Implemented cursor wait state while chart data is being loaded in `AnalogInputsChart.tsx`.
- Fixed missing dependencies in `useEffect` and defined `loading` state.
- Updated `handleFetchData` to manage
2025-07-23 13:16:57 +02:00
ISA
4d48100375 Isolationsfehler in Display anzeigen -> aktuell Zahl ist rot ohne Beschrifftung , es soll Zahl ISO MOhm und Isolationsfehler 2025-07-23 12:30:18 +02:00
ISA
5cf5e34c4f Isolationsfehler in Display anzeigen -> aktuell Zahl ist rot ohne Beschrifftung , es soll Zahl ISO MOhm und Isolationsfehler 2025-07-23 12:28:12 +02:00
ISA
36863d3c6a refactor: order Minimum, Messwert und Maximum, sowie Durchschnitt 2025-07-23 08:07:12 +02:00
ISA
5a0188c635 feat(analogInputs): auto-load chart data when table row is selected
- Added useEffect to AnalogInputsChart to automatically trigger "Daten laden" when a row is selected and selectedAnalogInput.id is not 0.
- Improves UX by syncing table selection with chart data fetch, no manual
2025-07-22 15:09:47 +02:00
ISA
d44fe916da Fix: Always show vonDatum and bisDatum in fetch URL for analog inputs chart
- Ensure local date state is never empty by falling back to default date if Redux is empty
- Prevent missing date values in fetch URL after multiple dropdown or button interactions
- Improves reliability of
2025-07-22 11:59:31 +02:00
ISA
2d166a204b Fix: Preserve chart state during zoom, pan, and date changes
- Added React.useMemo to memoize chartData and chartOptions to prevent unnecessary re-renders.
- Ensured chart zoom and pan states are maintained during interactions.
- Improved performance and user experience by avoiding chart
2025-07-22 10:58:01 +02:00
ISA
b7ca20f7c3 Fix: Preserve chart state during zoom, pan, and date changes
- Added React.useMemo to memoize chartData and chartOptions to prevent unnecessary re-renders.
- Ensured chart zoom and pan states are maintained during interactions.
- Improved performance and user experience by avoiding chart
2025-07-22 10:57:25 +02:00
ISA
ed9f693098 Fix: Preserve chart state during zoom, pan, and date changes
- Added React.useMemo to memoize chartData and chartOptions to prevent unnecessary re-renders.
- Ensured chart zoom and pan states are maintained during interactions.
- Improved performance and user experience by avoiding chart
2025-07-22 10:19:50 +02:00
ISA
773e2c12b8 Fix: Preserve chart state during zoom, pan, and date changes
- Added React.useMemo to memoize chartData and chartOptions to prevent unnecessary re-renders.
- Ensured chart zoom and pan states are maintained during interactions.
- Improved performance and user experience by avoiding chart
2025-07-22 10:19:14 +02:00
ISA
03ee4fb08e feat(AnalogInputsChart): Zeitraum im DatePicker und Redux initialisieren und synchronisieren
- Initialwert für Zeitraum (letzte 30 Tage) im Redux-Store gesetzt
- DatePicker-Änderungen werden im Redux gespeichert
- Fetch-Button verwendet Zeitraum aus Redux und loggt die Fetch-URL
- Chart zeigt Daten entsprechend ausgewähltem Zeitraum
2025-07-21 15:20:04 +02:00
ISA
697cae9848 feat(mock): Script fetchAnalogInputsData auf ES-Module (.mjs) umgestellt, Datum automatisch gesetzt 2025-07-21 14:21:38 +02:00
ISA
30d396896d feat(service): CPL-Request verwendet DIA0, DIA1 oder DIA2 je nach Zeitraum für analoge Eingänge 2025-07-21 13:46:13 +02:00
ISA
fb68d59da4 feat(service): Produktions-URL für CPL angepasst, erkennt Umgebung und baut Anfrage dynamisch 2025-07-21 13:04:46 +02:00
ISA
6b43435097 feat(chart): Zeitauswahl im Listbox nur lokal speichern, Daten-Fetch erst beim Button-Klick 2025-07-21 12:22:18 +02:00
ISA
a75347a59f feat(ui): Hinweis-Icon und Meldung angezeigt, wenn kein Eingang ausgewählt ist 2025-07-21 12:07:09 +02:00
ISA
7cd0c41ec5 fix: Linien Littwin blau und anderen grau für die Chart Linien 2025-07-21 10:46:54 +02:00
ISA
c73b7ec252 feat(analogInputsChart): dynamische Linien je Zeitraum (m/i/a/g)
- Chart zeigt für 'Alle Messwerte' (DIA0) Messwert (m), Minimum (i), Maximum (a)
- Für 'Stündlich' und 'Täglich' (DIA1/DIA2) werden Minimum (i), Maximum (a), Durchschnitt (g) angezeigt
- Farben und Legende entsprechend
2025-07-21 10:32:41 +02:00
ISA
f876bef7a3 feat(analogInputsChart): dynamische Linien je Zeitraum (m/i/a/g)
- Chart zeigt für 'Alle Messwerte' (DIA0) Messwert (m), Minimum (i), Maximum (a)
- Für 'Stündlich' und 'Täglich' (DIA1/DIA2) werden Minimum (i), Maximum (a), Durchschnitt (g) angezeigt
- Farben und Legende entsprechend
2025-07-21 10:32:01 +02:00
ISA
b1ff138774 feat(analogInputsChart): zeige Minimum (i) und Maximum (a) als zusätzliche Linien im Chart
- Chart zeigt jetzt Messwert (m), Minimum (i, grün) und Maximum (a, rot) für ausgewählten Zeitraum
- Tooltip und Legende angepasst
- Typdefinitionen für Chart
2025-07-21 10:22:25 +02:00
ISA
23a3c173dd feat(analogInputsChart): zeige Minimum (i) und Maximum (a) als zusätzliche Linien im Chart
- Chart zeigt jetzt Messwert (m), Minimum (i, grün) und Maximum (a, rot) für ausgewählten Zeitraum
- Tooltip und Legende angepasst
- Typdefinitionen für Chart
2025-07-21 10:21:45 +02:00
ISA
528773128d Nach Betriebsferien einmal sichern 2025-07-21 08:58:24 +02:00
ISA
311d47211e Nach Betriebsferien einmal sichern 2025-07-21 08:57:38 +02:00
Ismail Ali
b6e4c32287 feat(analogInputs): automatisches Laden der Chart-Daten bei Tabellenklick via Redux
- analogInputsHistorySlice um `autoLoad` erweitert, um automatisches Laden zu triggern
- handleSelect in AnalogInputsTable dispatcht jetzt `setAutoLoad(true)`
- AnalogInputsChart lauscht auf `autoLoad` + `selectedId` und lädt Daten automatisch
- `autoLoad` wird nach dem Laden wieder auf false zurückgesetzt
2025-07-15 09:24:52 +02:00
Ismail Ali
f485d87809 feat(analogInputs): automatisches Laden der Chart-Daten bei Tabellenklick via Redux
- analogInputsHistorySlice um `autoLoad` erweitert, um automatisches Laden zu triggern
- handleSelect in AnalogInputsTable dispatcht jetzt `setAutoLoad(true)`
- AnalogInputsChart lauscht auf `autoLoad` + `selectedId` und lädt Daten automatisch
- `autoLoad` wird nach dem Laden wieder auf false zurückgesetzt
2025-07-15 09:24:27 +02:00
Ismail Ali
658aa0cae5 uninstall redux-persist, weil nimmt viel Performance weg 2025-07-14 23:28:40 +02:00
ISA
99294f26da feat: AnalogInputsChart mit DateRangePicker und vollständiger Redux-Integration erweitert
- analogInputsHistorySlice angepasst: zeitraum, vonDatum, bisDatum und data hinzugefügt
- Typdefinitionen im Slice und Thunk korrigiert
- getAnalogInputsHistoryThunk erweitert, um vonDatum und bisDatum zu akzeptieren
- DateRangePicker korrekt in AnalogInputsChart.tsx integriert
- Fehler bei Selector-Zugriffen und Dispatch behoben
2025-07-11 14:01:57 +02:00
ISA
d278a79030 feat: AnalogInputsChart mit DateRangePicker und vollständiger Redux-Integration erweitert
- analogInputsHistorySlice angepasst: zeitraum, vonDatum, bisDatum und data hinzugefügt
- Typdefinitionen im Slice und Thunk korrigiert
- getAnalogInputsHistoryThunk erweitert, um vonDatum und bisDatum zu akzeptieren
- DateRangePicker korrekt in AnalogInputsChart.tsx integriert
- Fehler bei Selector-Zugriffen und Dispatch behoben
2025-07-11 14:01:15 +02:00
ISA
ca84ac6bb5 feat(api): Zeitraum und Eingang als Pflichtparameter für AnalogInputs-API eingeführt
- API-Handler für /api/cpl/getAnalogInputsHistory überarbeitet
- `zeitraum` (DIA0, DIA1, DIA2) und `eingang` (1–8) sind jetzt Pflichtfelder
- Bei fehlenden oder ungültigen Parametern strukturierte Fehlerantwort mit Beispielen
- Daten werden nun gezielt pro Eingang und Zeitraum geladen (z. B. AE3 + DIA1)
- Bessere Fehlerbehandlung bei nicht vorhandenen Dateien
2025-07-11 11:50:56 +02:00
ISA
2d3e070830 feat(api): Zeitraum und Eingang als Pflichtparameter für AnalogInputs-API eingeführt
- API-Handler für /api/cpl/getAnalogInputsHistory überarbeitet
- `zeitraum` (DIA0, DIA1, DIA2) und `eingang` (1–8) sind jetzt Pflichtfelder
- Bei fehlenden oder ungültigen Parametern strukturierte Fehlerantwort mit Beispielen
- Daten werden nun gezielt pro Eingang und Zeitraum geladen (z. B. AE3 + DIA1)
- Bessere Fehlerbehandlung bei nicht vorhandenen Dateien
2025-07-11 11:50:15 +02:00
ISA
1f1e532233 fix: Von/Bis-Datum beim Schließen des DetailModals zurücksetzen
- Redux-State für vonDatum und bisDatum wird bei handleClose geleert
- verhindert unerwünschtes Vorfiltern bei erneutem Öffnen des Modals
2025-07-11 09:45:32 +02:00
ISA
93ae79ac7e feat: Zeitspanne-Funktion mit Von/Bis und Button-Trigger im DetailModal eingebaut
- Chart-Daten werden jetzt erst bei Klick auf „Daten laden“ geladen
- Von/Bis-Zeitauswahl über Redux-State korrekt eingebunden
- Styling der Eingabefelder und Dropdowns vereinheitlicht (eine Zeile)
- Lokalen State für Zeitspanne entfernt und durch Redux ersetzt
2025-07-11 09:33:06 +02:00
ISA
bb8b345647 fix: Messwertlinie (m) im DIA0-Modus in DetailModal sichtbar gemacht 2025-07-11 08:23:15 +02:00
ISA
e9a6d45d1f fix: Anzeige der Messwertlinie (m) im DIA0-Modus in DetailModal korrigiert
- Unterscheidung zwischen Durchschnitt (g) und Einzelwert (m) je nach Modus eingebaut
- Fehler behoben, bei dem im DIA0-Modus keine blaue Linie angezeigt wurde
2025-07-11 08:21:24 +02:00
Ismail Ali
49f9c3737a feat: DetailModal um Min/Max/Durchschnitt ergänzt
- Chart zeigt jetzt zusätzlich zu Messwert auch Minimal-, Maximal- und Durchschnittswerte an
- Datenstruktur an Redux angepasst (i, a, g)
- Darstellung entspricht jetzt LoopMeasurementChart
2025-07-10 19:12:06 +02:00
Ismail Ali
420989dc9f feat: DetailModal um Min/Max/Durchschnitt ergänzt
- Chart zeigt jetzt zusätzlich zu Messwert auch Minimal-, Maximal- und Durchschnittswerte an
- Datenstruktur an Redux angepasst (i, a, g)
- Darstellung entspricht jetzt LoopMeasurementChart
2025-07-10 19:11:38 +02:00
ISA
3a1d85dbe2 eslintrc.json : "@typescript-eslint/no-unused-vars": "warn" 2025-07-10 15:17:55 +02:00
ISA
4ea12a1f79 fix: Bei System: Detailansicht: Zeitraum von bis fehlt. Ganzseitenansicht fehlt noch. gelöst mit zoom und pan 2025-07-10 15:16:54 +02:00
ISA
898f2b14f2 eslint any type only warn no error 2025-07-10 14:10:20 +02:00
ISA
7cabbafad5 feat: Zoom wird beim Wechsel des Zeitraums im Detail-Chart automatisch zurückgesetzt 2025-07-10 14:08:09 +02:00
ISA
eae69d4392 fix(detail-chart): X-Achse zeigt jetzt Datum und Uhrzeit ohne Sekunden (z. B. 10.07.2025 14:32) 2025-07-10 13:28:16 +02:00
ISA
a8c027bd6e fix(system-charts): Zeitachse angepasst – aktuelle Daten jetzt rechts wie bei Kabelüberwachung 2025-07-10 13:22:11 +02:00
ISA
b7b0829c5b fix(system-charts): Y-Achse mit Einheiten ergänzt (V und °C) für bessere Lesbarkeit 2025-07-10 13:12:15 +02:00
ISA
0410e8c52d feat: Speicherintervall-Feld als Zahleneingabe mit Einheit 'Minuten' angepasst 2025-07-10 13:04:36 +02:00
ISA
fd42502d05 fix: KÜ ISO Wert 200 in Display mit Einheit 2025-07-10 12:22:07 +02:00
ISA
340990573f feat: automatische Moduserkennung für fetchAnalogInputsService.ts implementiert
- Modus wird anhand von window.location.hostname bestimmt
- Lokale Umgebung (localhost/127.0.0.1) nutzt Mock-API
- Produktionsumgebung lädt analogInputs.json via CGI
- Kein Bedarf mehr für manuelle .env-Konfiguration
2025-07-09 09:59:53 +02:00
ISA
eca52f35cb feat: Modus-Erkennung über window.location.hostname implementiert
- Automatische Umschaltung zwischen Entwicklungs- und Produktionsmodus
- Hostname-basierte Erkennung: localhost/127.0.0.1 → "dev", sonst → "production"
- fetchDigitalInputsService.ts entsprechend angepasst
- Erleichtert Entwicklung und reduziert manuelle .env-Konfiguration
2025-07-09 09:45:49 +02:00
ISA
14bd72756a feat: CGI-kompatiblen CSV-Parser für digitale Eingänge implementiert
- digitaleInputsMockData.json angepasst: CGI-nahe Simulation mit CSV-Strings und Stringwerten
- fetchDigitalInputsService.ts erweitert:
  - CSV-Zeilen werden automatisch in Arrays umgewandelt
  - Labels wie "'DE1','DE2'" werden korrekt aufgeteilt
  - Daten aus 4 CGI-Blöcken zu 32 Eingängen gemappt
- ermöglicht realitätsnahe Tests in Entwicklungsumgebung ohne Produktion
2025-07-09 08:41:50 +02:00
ISA
7797549baa feat: Umstellung von CGI-Daten für analoge Eingänge von JS auf JSON
- CGI-Platzhalter in `analogInputs.json` eingeführt (z. B. <%=AAV01%>)
- Alte JS-Datei ersetzt durch reine JSON-Struktur
- Anpassung des Service-Handlers (`getAnalogInputsHandler.ts`) auf JSON-Parsing
- Reduziert Ladezeit, vereinfacht Code und entfernt unnötige Script-Einbindung
- Mock-Daten weiterhin in `analogInputsMockData.json` für Entwicklungsmodus verfügbar
2025-07-08 14:44:44 +02:00
ISA
93c3bc612d Bei den Kabelüberwachung kann neben den Button “Firmware Update” noch zwei Button “Konfiguration sichern” und “Konfiguration zurücksichern” im Admin-Modus hinzukommen.
Store: Befehl KSB%i=%i z.B. KSB03=1 sichert die Konfiguration der KÜ 4

Restore: Befehl KSR%i=%i z.B. KSR03=1 sichert die Konfiguration der KÜ 4 zurück
2025-07-08 13:55:55 +02:00
ISA
b091a8d82a refactor: extract Kabelueberwachung logic into KabelueberwachungView for better structure 2025-07-08 13:13:30 +02:00
ISA
454b8bfb8d refactor: move analog inputs logic to AnalogInputsView component
- Verschiebt die gesamte UI-Logik aus pages/analogInputs.tsx in eine eigene Komponente AnalogInputsView.tsx
- pages/analogInputs.tsx dient jetzt nur noch als Router-Einstiegspunkt
- Vereinheitlicht die Struktur wie bei MeldungenView und DashboardView
2025-07-08 11:42:08 +02:00
ISA
48898fcd09 fix: call digitalOutputs from _app.tsx to show immediately without delay 2025-07-08 11:13:25 +02:00
ISA
0f233ce6e2 fix: sofortige visuelle Aktivierung der Navigationsbuttons beim Klick
- activeLink direkt beim Klick auf Link setzen, statt auf usePathname zu warten
- verbessert visuelles Feedback bei Navigation
- behebt kurze Verzögerung beim Wechsel der aktiven Navigation
2025-07-08 10:41:20 +02:00
ISA
976f3126f2 feat: Verwende fetch statt window.location.href für Digitalausgang-Schalteraktionen 2025-07-08 10:09:46 +02:00
ISA
2af99f2740 circle Button 2025-07-08 09:25:39 +02:00
ISA
fb680a4c66 feat: ersetzt Einheit-Select durch Listbox mit littwin-blue Design in AnalogInputsSettingsModal 2025-07-08 08:30:09 +02:00
ISA
44cfd2ab81 refactor: Zeitraum-Dropdown in DetailModal auf Listbox mit Littwin-Design umgestellt
- <select> durch Headless UI Listbox ersetzt
- Optionen DIA0, DIA1, DIA2 mit deutschem Label dargestellt
- Einheitliches Styling mit littwin-blue wie in anderen Komponenten
2025-07-08 07:09:27 +02:00
ISA
3af16b4c29 refactor: LoopChartActionBar Dropdowns auf Listbox mit Littwin-Design umgestellt
- selectedMode (DIA0/DIA1/DIA2) ersetzt durch Headless UI Listbox
- selectedSlotType (Schleifen-/Isolationswiderstand) ebenfalls als Listbox
- Einheitliches Dropdown-Design mit MeldungenView und TDRChartActionBar
- Littwin-blue Stil für ausgewählte Optionen integriert
2025-07-08 07:02:27 +02:00
ISA
3d37388173 refactor: LoopChartActionBar Dropdowns auf Listbox mit Littwin-Design umgestellt
- selectedMode (DIA0/DIA1/DIA2) ersetzt durch Headless UI Listbox
- selectedSlotType (Schleifen-/Isolationswiderstand) ebenfalls als Listbox
- Einheitliches Dropdown-Design mit MeldungenView und TDRChartActionBar
- Littwin-blue Stil für ausgewählte Optionen integriert
2025-07-08 07:01:38 +02:00
ISA
92eb28e495 fix: TDR select List mouseover gray 200 2025-07-07 15:20:11 +02:00
ISA
99d2a3d451 fix: TDR select List mouseover gray 200 2025-07-07 15:17:17 +02:00
ISA
fdd38c74f0 fix: List mouseover -> hover:bg-gray-200 2025-07-07 14:52:53 +02:00
ISA
8ee7c9c193 fix: es soll dann nur wenn der Button Anziegen geklickt wird anzeigenund nicht automatisch nach ein Datumauswahl 2025-07-07 13:51:54 +02:00
ISA
4e5eeed9a2 refactor: API-Handler umbenannt zu messages.ts für klare REST-Struktur
- getMessagesAPIHandler.ts in messages.ts umbenannt
- API ist nun unter /api/cpl/messages erreichbar
- Dateiname entspricht Next.js- und REST-Konventionen
2025-07-07 13:40:27 +02:00
ISA
31223ffc64 style: UI-Filterzeile visuell vereinheitlicht – vertikale Ausrichtung und Höhe angepasst
- 'items-end' durch 'items-center' ersetzt für mittige Ausrichtung der Filterzeile
- Button- und Listbox-Komponenten optisch auf gleiche Höhe gebracht
- Einheitliches Erscheinungsbild von DatePicker, Anzeigen-Button und Quellen-Dropdown
2025-07-07 12:13:26 +02:00
ISA
28441eebf1 fix: Listbox-Filter "Alle Quellen" zeigt nun korrekt alle Meldungen an
- Initialwert von sourceFilter auf "Alle Quellen" gesetzt
- Filterbedingung angepasst, um mit Listbox-Einträgen übereinzustimmen
- Dropdown-UX verbessert durch Icon und Scrollfunktion
2025-07-07 12:05:55 +02:00
ISA
bf4e38509a fix: Zeitstempel in Meldungstabelle inkl. Uhrzeit im deutschen Format (TT.MM.JJJJ, HH:MM:SS)
- msg.t wird jetzt per toLocaleString('de-DE') mit Zeitformatierung angezeigt
- Beispiel: 26.06.2025, 19:26:07
2025-07-07 11:36:20 +02:00
ISA
ba690c1e03 fix: DatePicker über Tabellenkopf anzeigen durch z-index und Portal-Lösung
- react-datepicker auf portalId 'root-portal' umgestellt
- CSS-Klasse 'custom-datepicker-popper' mit z-index: 9999 in globals.css ergänzt
- Problem behoben, dass DatePicker hinter dem sticky Tabellen-Header verborgen war
- Tailwind-Konfiguration um z-[60,70] erweitert, wenn nötig
2025-07-07 11:28:56 +02:00
ISA
b58c961da4 feat: lade nur spezifischen Spannungs-/Temperatur-Thunk beim Öffnen des Detailmodals
- Entfernt globale Thunk-Aufrufe für alle Systemwerte bei Zeitraumwechsel
- Detailansicht lädt nun nur den benötigten Redux-Thunk (z. B. +15V → Channel 108)
- Zeitraumwechsel im Modal löst gezielt nur den zugehörigen Thunk aus
- Reduziert unnötige Datenlast und verbessert Performance bei Embedded-Geräten
2025-07-07 11:13:44 +02:00
ISA
10a9167a1f system 2025-07-07 10:36:40 +02:00
ISA
ebe72c3ab0 refactor: Seitenkomponenten ausgelagert in View-Komponenten
- meldungen.tsx → MeldungenView.tsx erstellt
  → beinhaltet Filterleiste, Tabellenansicht und Datenabruf
- system.tsx → SystemView.tsx ausgelagert
  → verbessert Lesbarkeit und Trennung von Routing und Inhalt
- View-Suffix verwendet für klare Struktur (Page = Entry, View = Inhalt)
2025-07-07 08:27:19 +02:00
517 changed files with 1996860 additions and 679945 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.538
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.538
NEXT_PUBLIC_APP_VERSION=1.6.913
NEXT_PUBLIC_CPL_MODE=production

View File

@@ -14,7 +14,32 @@
"jsx": true
}
},
"env": {
"browser": true,
"node": true,
"es2021": true
},
"globals": {
"JSX": "readonly",
"NodeJS": "readonly"
},
"rules": {
// deine Regeln hier
"no-undef": "off",
"no-unreachable": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": "warn",
"react-hooks/exhaustive-deps": "warn",
"react/react-in-jsx-scope": "off",
"@next/next/no-img-element": "warn",
"react/no-unescaped-entities": "warn",
"no-irregular-whitespace": [
"error",
{
"skipComments": true,
"skipStrings": true,
"skipTemplates": true,
"skipRegExps": true
}
]
}
}

12
.gitignore vendored
View File

@@ -9,6 +9,15 @@
# testing
/coverage
# playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
playwright/report/
playwright/test-results/
playwright/.cache/
# next.js
/.next/
/out/
@@ -34,3 +43,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# Playwright
node_modules/

58
.woodpecker.yml Normal file
View File

@@ -0,0 +1,58 @@
when:
- event: push
- event: pull_request
steps:
- name: clone
image: woodpeckerci/plugin-git
settings:
depth: 0
lfs: true
submodules: true
- name: verify-mocks
image: mcr.microsoft.com/playwright:v1.54.2-jammy
commands:
- pwd
- node -v && npm -v
# Skip lifecycle scripts in CI to avoid running husky's prepare step
- npm ci
# Zeig mir, ob die Datei wirklich im Checkout liegt:
- echo "=== git ls-files ==="
- git ls-files | grep -i "^mocks/device-cgi-simulator/SERVICE/systemMockData.js" || true
- echo "=== ls -la ==="
- ls -la mocks/device-cgi-simulator/SERVICE || true
- echo "=== file exists? ==="
- test -f mocks/device-cgi-simulator/SERVICE/systemMockData.js && echo "FOUND" || (echo "MISSING" && exit 1)
- name: e2e-dev
image: mcr.microsoft.com/playwright:v1.54.2-jammy
environment:
CI: "true"
NODE_ENV: "production"
NEXT_TELEMETRY_DISABLED: "1"
PORT: "3000"
commands:
- node -v && npm -v
# Skip lifecycle scripts in CI to avoid running husky's prepare step (husky is a devDep)
- env npm_config_production=false npm ci
- npm run build
# Start local static simulator in background
- npm run server:sim &
# Wait until simulator responds on port 3000 (no curl dependency)
- node -e "const http=require('http');let n=120;function ping(){http.get('http://localhost:3000',res=>{console.log('Server is up');process.exit(0)}).on('error',()=>{if(n--<=0){console.error('Server did not start');process.exit(1)}setTimeout(ping,1000)});}ping();"
- npx playwright test --project=chromium
- name: notify-success
image: alpine/curl:latest
when:
status: success
commands:
- curl -d "Tests erfolgreich in woodpecker" https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35
- name: notify-failure
image: alpine/curl:latest
when:
status: failure
commands:
- curl -d "Tests fehlgeschlagen in woodpecker" https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35

File diff suppressed because it is too large Load Diff

BIN
Git 2.pptx Normal file

Binary file not shown.

95
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,95 @@
pipeline {
agent any
environment {
CI = "true"
NODE_ENV = "production"
NEXT_TELEMETRY_DISABLED = "1"
PORT = "3000"
}
options {
timestamps()
}
stages {
stage('Checkout') {
steps {
checkout scm
sh '''
set -eux
git status --short || true
# Submodule & LFS falls vorhanden
git submodule update --init --recursive || true
git lfs install || true
git lfs fetch || true
git lfs checkout || true
'''
}
}
stage('verify-mocks') {
steps {
sh '''
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)
"
'''
}
}
stage('e2e-dev') {
steps {
sh '''
set -eux
docker run --rm -v "$PWD":/ws -w /ws -p 3000:3000 \
mcr.microsoft.com/playwright:v1.54.2-jammy bash -lc "
node -v && npm -v
env npm_config_production=false npm ci
npm run build
npm run server:sim &
# Auf Port 3000 warten
node -e \\"const http=require('http');let n=120;function ping(){http.get('http://localhost:3000',res=>{console.log('Server is up');process.exit(0)}).on('error',()=>{if(n--<=0){console.error('Server did not start');process.exit(1)}setTimeout(ping,1000)});}ping();\\"
npx playwright test --project=chromium
"
'''
}
}
}
post {
success {
sh '''
docker run --rm curlimages/curl:8.9.1 \
-d "Tests erfolgreich in Jenkins" \
https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35
'''
}
failure {
sh '''
docker run --rm curlimages/curl:8.9.1 \
-d "Tests fehlgeschlagen in Jenkins" \
https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9Z35
'''
}
always {
script {
if (fileExists('playwright-report')) {
archiveArtifacts artifacts: 'playwright-report/**', onlyIfSuccessful: false
} else {
echo 'Kein playwright-report gefunden.'
}
}
}
}
}

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

@@ -31,7 +31,7 @@ export default function ConfirmModal({
Abbrechen
</button>
<button
className="bg-littwin-blue hover:bg-blue-700 text-white px-4 py-2 rounded"
className="bg-littwin-blue text-white px-4 py-2 rounded"
onClick={onConfirm}
>
Bestätigen

View File

@@ -1,22 +1,27 @@
// /components/main/kabelueberwachung/kue705FO/Charts/LoopMeasurementChart/DateRangePicker.tsx
// /components/common/DateRangePicker.tsx
import React, { useEffect } from "react";
import DatePicker from "react-datepicker";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "@/redux/store";
import {
setVonDatum,
setBisDatum,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { setVonDatum, setBisDatum } from "@/redux/slices/dateRangePickerSlice";
import "react-datepicker/dist/react-datepicker.css";
const DateRangePicker: React.FC = () => {
interface DateRangePickerProps {
compact?: boolean; // reduziert horizontale Breite
className?: string;
}
const DateRangePicker: React.FC<DateRangePickerProps> = ({
compact = false,
className = "",
}) => {
const dispatch = useDispatch();
const reduxVonDatum = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice.vonDatum
(state: RootState) => state.dateRangePicker.vonDatum
);
const reduxBisDatum = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice.bisDatum
(state: RootState) => state.dateRangePicker.bisDatum
);
const today = new Date();
@@ -41,10 +46,22 @@ const DateRangePicker: React.FC = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, reduxVonDatum, reduxBisDatum]);
const gapClass = compact ? "space-x-2" : "space-x-4";
const labelWidthClass = compact ? "w-6" : "w-auto";
const inputWidthClass = compact ? "w-32" : "w-44"; // ca. 128px vs 176px
return (
<div className="flex space-x-4 items-center">
<div className="flex items-center space-x-2">
<label className="block text-sm font-semibold">Von</label>
<div className={`flex ${gapClass} items-center ${className}`}>
<div
className={`flex items-center space-x-1 ${compact ? "text-xs" : ""}`}
>
<label
className={`block font-semibold ${
compact ? "text-xs" : "text-sm"
} ${labelWidthClass}`}
>
Von
</label>
<DatePicker
selected={reduxVonDatum ? parseISODate(reduxVonDatum) : thirtyDaysAgo}
onChange={(date) => {
@@ -60,12 +77,23 @@ const DateRangePicker: React.FC = () => {
minDate={sixMonthsAgo}
maxDate={today}
dateFormat="dd.MM.yyyy"
className="border px-2 py-1 rounded"
portalId="root-portal"
popperClassName="custom-datepicker-popper"
className={`border px-2 py-1 rounded ${inputWidthClass} ${
compact ? "text-xs" : "text-sm"
}`}
/>
</div>
<div className="flex items-center space-x-2">
<label className="block text-sm font-semibold">Bis</label>
<div
className={`flex items-center space-x-1 ${compact ? "text-xs" : ""}`}
>
<label
className={`block font-semibold ${
compact ? "text-xs" : "text-sm"
} ${labelWidthClass}`}
>
Bis
</label>
<DatePicker
selected={reduxBisDatum ? parseISODate(reduxBisDatum) : today}
onChange={(date) => {
@@ -81,7 +109,11 @@ const DateRangePicker: React.FC = () => {
minDate={sixMonthsAgo}
maxDate={today}
dateFormat="dd.MM.yyyy"
className="border px-2 py-1 rounded"
portalId="root-portal"
popperClassName="custom-datepicker-popper"
className={`border px-2 py-1 rounded ${inputWidthClass} ${
compact ? "text-xs" : "text-sm"
}`}
/>
</div>
</div>

View File

@@ -0,0 +1,77 @@
"use client";
import React from "react";
import { useAppDispatch } from "@/redux/store";
import {
setEvents,
initPersistedTimings,
} from "@/redux/slices/deviceEventsSlice";
declare global {
interface Window {
loopMeasurementEvent?: number[];
tdrMeasurementEvent?: number[];
comparisonEvent?: number[]; // renamed from alignmentEvent
}
}
const POLL_MS = 2000; // poll every 2 seconds
export default function DeviceEventsBridge() {
const dispatch = useAppDispatch();
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
: undefined;
const ksy = Array.isArray(window.tdrMeasurementEvent)
? window.tdrMeasurementEvent
: undefined;
const ksz = Array.isArray(window.comparisonEvent)
? window.comparisonEvent
: undefined;
// Build a stable signature of first 32 values per array
const to32 = (a?: number[]) => {
const out: number[] = [];
if (Array.isArray(a)) {
for (let i = 0; i < 32; i++) out.push(a[i] ? 1 : 0);
} else {
for (let i = 0; i < 32; i++) out.push(0);
}
return out;
};
const sig = `${to32(ksx).join("")}|${to32(ksy).join("")}|${to32(ksz).join(
""
)}`;
if (sig !== lastSig) {
lastSig = sig;
dispatch(setEvents({ ksx, ksy, ksz }));
}
};
readAndDispatch();
const id = setInterval(readAndDispatch, POLL_MS);
return () => clearInterval(id);
}, [dispatch]);
return null;
}

View File

@@ -0,0 +1,132 @@
"use client";
import React, { useEffect, useState } from "react";
import { useAppSelector } from "@/redux/store";
export default function GlobalActivityOverlay() {
const anyLoop = useAppSelector((s) => s.deviceEvents.anyLoopActive);
const anyTdr = useAppSelector((s) => s.deviceEvents.anyTdrActive);
const 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 comparisonStartedAt = useAppSelector(
(s) => s.deviceEvents.comparisonStartedAt
);
const fmt = (arr: number[]) =>
arr
.map((v, i) => (v ? i + 1 : 0))
.filter((n) => n !== 0)
.join(", ");
// Simple 1s ticker so progress bars advance while overlay is shown
const [now, setNow] = useState<number>(Date.now());
useEffect(() => {
const active = anyLoop || anyTdr || anyCompare;
if (!active) return;
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, [anyLoop, anyTdr, anyCompare]);
const active = anyLoop || anyTdr || anyCompare;
if (!active) return null;
const clamp = (v: number, min = 0, max = 1) =>
Math.max(min, Math.min(max, v));
const compute = (startedAt: number | null, durationMs: number) => {
if (!startedAt) return { pct: 0, remaining: durationMs };
const elapsed = now - startedAt;
const pct = clamp(elapsed / durationMs) * 100;
const remaining = Math.max(0, durationMs - Math.max(0, elapsed));
return { pct, remaining };
};
// Durations
const LOOP_MS = 2 * 60 * 1000; // ~2 min
const TDR_MS = 30 * 1000; // ~30 s
const ALIGN_MS = 10 * 60 * 1000; // ~10 min
return (
<div className="fixed inset-0 z-[2000] flex items-center justify-center bg-white/70 backdrop-blur-sm">
<div className="p-4 rounded-md shadow bg-white border border-gray-200 w-[min(90vw,680px)]">
<div className="font-semibold mb-3">Bitte warten</div>
<div className="space-y-3">
{anyLoop && (
<div>
<div className="text-sm text-gray-800 mb-1">
Schleifenmessung läuft (: {fmt(ksx)})
</div>
{(() => {
const { pct } = compute(loopStartedAt, LOOP_MS);
return (
<div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
<div
className="h-full bg-littwin-blue transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<div className="text-xs text-gray-600 mt-1">
{Math.round(pct)}%
</div>
</div>
);
})()}
</div>
)}
{anyTdr && (
<div>
<div className="text-sm text-gray-800 mb-1">
TDR-Messung läuft (: {fmt(ksy)})
</div>
{(() => {
const { pct } = compute(tdrStartedAt, TDR_MS);
return (
<div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
<div
className="h-full bg-littwin-blue transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<div className="text-xs text-gray-600 mt-1">
{Math.round(pct)}%
</div>
</div>
);
})()}
</div>
)}
{anyCompare && (
<div>
<div className="text-sm text-gray-800 mb-1">
Comparison läuft (: {fmt(ksz)}) kann bis zu 10 Minuten dauern
</div>
{(() => {
const { pct } = compute(comparisonStartedAt, ALIGN_MS);
return (
<div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
<div
className="h-full bg-littwin-blue transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<div className="text-xs text-gray-600 mt-1">
{Math.round(pct)}%
</div>
</div>
);
})()}
</div>
)}
</div>
</div>
</div>
);
}

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,6 @@
"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";
import "bootstrap-icons/font/bootstrap-icons.css";
@@ -14,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
@@ -42,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>();
@@ -56,14 +109,49 @@ function Header() {
}, [deviceName, dispatch]);
//----------------------------------------------------------------
// 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") 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 flex justify-between items-center w-full h-[13vh] laptop:h-[10vh] relative text-black ">
<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
}}
@@ -72,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}
/>
@@ -88,43 +176,55 @@ function Header() {
priority
/>
<div className="flex flex-col leading-tight whitespace-nowrap">
<h2 className="text-xl laptop:text-base xl:text-lg font-bold">
<h2 className="text-xl laptop:text-base xl:text-lg font-bold text-[var(--color-fg)]">
Meldestation
</h2>
<p className="text-gray-600 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>
</div>
<div className="p-4 w-full lg:w-full flex flex-row gap-4 justify-between">
<div className="flex items-center justify-end w-full">
{/* Admin-Login */}
{/*
<div className="flex items-center justify-end w-full gap-4">
{/* Dark/Light Mode Toggle */}
<button
onClick={handleSettingsClick}
className="text-3xl text-black mr-0"
aria-label={isDark ? "Light Mode" : "Dark Mode"}
onClick={() => setIsDark((d) => !d)}
className="rounded-full p-2 bg-[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"}
>
<i className="bi bi-gear"></i>
</button>
*/}
</div>
{/* Logout-Button */}
<div className="flex items-center justify-end w-1/4 space-x-1">
<button
onClick={handleLogout}
className="bg-littwin-blue text-white px-4 py-2 rounded"
>
Abmelden
{isDark ? (
<Icon
icon="mdi:weather-night"
className="text-xl text-yellow-300"
/>
) : (
<Icon
icon="mdi:white-balance-sunny"
className="text-xl text-yellow-500"
/>
)}
</button>
</div>
{/* Logout-Button - nur anzeigen wenn Admin eingeloggt ist */}
{isAdminLoggedIn && (
<div className="flex items-center justify-end w-1/4 space-x-1">
<button
onClick={handleLogout}
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>
</div>
)}
</div>
{/* 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

@@ -12,7 +12,7 @@ const handleClearDatabase = async () => {
}
// Full URL with host, current path, and clear database command
const url = `${window.location.origin}/CPL?${currentPath}&DEDB=1`;
const url = `${window.location.origin}/CPL?/${window.location.pathname}&DEDB=1`;
// Log the full URL to the console for debugging
console.log(url);

View File

@@ -1,12 +1,10 @@
"use client";
type AnalogInput = {
id: number;
label: string;
unit: string;
};
import React, { useEffect } from "react";
// components/main/analogInputs/AnalogInputsChart.tsx
import React, { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState, AppDispatch } from "@/redux/store";
import { Dialog } from "@headlessui/react";
import { Line } from "react-chartjs-2";
import { getColor } from "@/utils/colors";
import {
Chart as ChartJS,
LineElement,
@@ -17,14 +15,23 @@ import {
Legend,
Filler,
TimeScale,
TooltipItem,
} from "chart.js";
import "chartjs-adapter-date-fns";
import { de } from "date-fns/locale";
import { useSelector, useDispatch } from "react-redux";
import type { RootState, AppDispatch } from "../../../redux/store";
import { Listbox } from "@headlessui/react";
import { getAnalogInputsHistoryThunk } from "@/redux/thunks/getAnalogInputsHistoryThunk";
import {
setVonDatum,
setBisDatum,
setZeitraum,
setAutoLoad,
} from "@/redux/slices/analogInputs/analogInputsHistorySlice";
import { getColor } from "@/utils/colors";
import AnalogInputsDatePicker from "./AnalogInputsDatePicker";
import type { ChartJSOrUndefined } from "react-chartjs-2/dist/types";
// Basis-Registrierung (ohne Zoom-Plugin)
// ✅ Nur die Basis-ChartJS-Module registrieren
ChartJS.register(
LineElement,
PointElement,
@@ -37,148 +44,402 @@ ChartJS.register(
);
export default function AnalogInputsChart({
selectedId,
setLoading,
loading,
}: {
selectedId: number | null;
setLoading: (loading: boolean) => void;
loading: boolean;
}) {
const selectedInput = useSelector(
(state: RootState) => state.selectedAnalogInput
) as unknown as AnalogInput | null;
const dispatch = useDispatch<AppDispatch>();
type AnalogInputHistoryPoint = { t: string | number | Date; m: number };
const { data } = useSelector(
(state: RootState) => state.analogInputsHistory
) as {
data: { [key: string]: AnalogInputHistoryPoint[] };
};
useEffect(() => {
dispatch(getAnalogInputsHistoryThunk());
}, [dispatch]);
// ✅ Zoom-Plugin dynamisch importieren und registrieren
useEffect(() => {
const loadZoomPlugin = async () => {
if (typeof window !== "undefined") {
const zoomPlugin = (await import("chartjs-plugin-zoom")).default;
if (typeof window !== "undefined") {
import("chartjs-plugin-zoom").then((zoom) => {
if (!ChartJS.registry.plugins.get("zoom")) {
ChartJS.register(zoomPlugin);
ChartJS.register(zoom.default);
}
}
};
loadZoomPlugin();
});
}
}, []);
if (!selectedId) {
return (
<div className="text-gray-500">Bitte einen Messwerteingang auswählen</div>
);
}
const dispatch = useDispatch<AppDispatch>();
const key = String(selectedId + 99);
const inputData = data[key];
const chartRef =
useRef<
ChartJSOrUndefined<"line", { x: Date; y: number | undefined }[], unknown>
>(null);
if (!inputData) {
return (
<div className="text-red-500">
Keine Verlaufsdaten für Messwerteingang {selectedId} gefunden.
</div>
);
}
// Redux Werte für Chart-Daten
const { zeitraum, vonDatum, bisDatum, data, autoLoad, selectedId } =
useSelector((state: RootState) => state.analogInputsHistory);
const selectedAnalogInput = useSelector(
(state: RootState) => state.selectedAnalogInput
);
const chartData = {
datasets: [
{
label: `Messkurve ${selectedInput?.label ?? "Eingang"} [${
selectedInput?.unit ?? ""
}]`,
data: inputData.map((point: AnalogInputHistoryPoint) => ({
x: point.t,
y: point.m,
})),
fill: false,
borderColor: getColor("littwin-blue"),
backgroundColor: "rgba(59,130,246,0.5)",
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 10,
tension: 0.1,
},
],
// Redux initiale Datum-Werte
const vonDatumRedux = useSelector(
(state: RootState) => state.dateRangePicker.vonDatum
);
const bisDatumRedux = useSelector(
(state: RootState) => state.dateRangePicker.bisDatum
);
// Hilfsfunktion für Default-Datum
const getDefaultDate = (type: "from" | "to") => {
const today = new Date();
if (type === "to") return today.toISOString().slice(0, 10);
const fromDateObj = new Date(today);
fromDateObj.setDate(today.getDate() - 30);
return fromDateObj.toISOString().slice(0, 10);
};
const chartOptions = {
responsive: true,
plugins: {
legend: { position: "top" as const },
tooltip: {
mode: "index" as const,
intersect: false,
callbacks: {
label: function (context: import("chart.js").TooltipItem<"line">) {
const y = context.parsed.y;
return `Messwert: ${y}`;
},
title: function (
tooltipItems: import("chart.js").TooltipItem<"line">[]
) {
const date = tooltipItems[0].parsed.x;
return `Zeitpunkt: ${new Date(date).toLocaleString("de-DE")}`;
// ✅ Lokale States für Picker + Zeitraum
const [localVonDatum, setLocalVonDatum] = React.useState(
vonDatumRedux || getDefaultDate("from")
);
const [localBisDatum, setLocalBisDatum] = React.useState(
bisDatumRedux || getDefaultDate("to")
);
const [localZeitraum, setLocalZeitraum] = React.useState(zeitraum);
// Synchronisiere lokale Werte mit Redux (z.B. nach AutoLoad Reset)
useEffect(() => {
setLocalVonDatum(vonDatumRedux || getDefaultDate("from"));
setLocalBisDatum(bisDatumRedux || getDefaultDate("to"));
setLocalZeitraum(zeitraum);
}, [vonDatumRedux, bisDatumRedux, zeitraum]);
// Initiale Default-Werte: 30 Tage zurück (nur wenn Redux-Werte fehlen)
useEffect(() => {
if (!vonDatumRedux || !bisDatumRedux) {
const today = new Date();
const toDate = today.toISOString().slice(0, 10);
const fromDateObj = new Date(today);
fromDateObj.setDate(today.getDate() - 30);
const fromDate = fromDateObj.toISOString().slice(0, 10);
setLocalVonDatum(fromDate);
setLocalBisDatum(toDate);
}
}, [vonDatumRedux, bisDatumRedux]);
// ✅ Nur lokale Änderung beim Picker
const handleDateChange = (from: string, to: string) => {
setLocalVonDatum(from);
setLocalBisDatum(to);
};
// ✅ Button → Redux + Fetch triggern
const handleFetchData = () => {
if (!selectedAnalogInput?.id) return;
setLoading(true); // Set loading to true when fetching data
// Fallback auf Redux-Werte, falls lokale Werte leer sind
const from = localVonDatum || vonDatumRedux || "";
const to = localBisDatum || bisDatumRedux || "";
// Redux aktualisieren
dispatch(setVonDatum(from));
dispatch(setBisDatum(to));
dispatch(setZeitraum(localZeitraum));
// Umgebung erkennen und URL generieren
const isDev =
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1";
let fetchUrl = "";
if (isDev) {
fetchUrl = `/api/cpl/getAnalogInputsHistory?eingang=${selectedAnalogInput.id}&zeitraum=${localZeitraum}&von=${from}&bis=${to}`;
} else {
// Produktion: CPL-Webserver direkt abfragen
const [vonJahr, vonMonat, vonTag] = from.split("-");
const [bisJahr, bisMonat, bisTag] = to.split("-");
const aeEingang = 100 + (selectedAnalogInput.id - 1);
let diaType = "DIA1";
if (localZeitraum === "DIA0") diaType = "DIA0";
if (localZeitraum === "DIA2") diaType = "DIA2";
fetchUrl = `${window.location.origin}/CPL?seite.ACP&${diaType}=${vonJahr};${vonMonat};${vonTag};${bisJahr};${bisMonat};${bisTag};${aeEingang};1`;
}
console.log("Fetch-URL:", fetchUrl);
// Thunk-Fetch mit neuen Werten
dispatch(
getAnalogInputsHistoryThunk({
eingang: selectedAnalogInput.id,
zeitraum: localZeitraum,
vonDatum: from,
bisDatum: to,
})
).finally(() => setLoading(false)); // Reset loading after fetch
};
// Auto-trigger fetch when a row is selected and id is not 0 (only once per selection)
React.useEffect(() => {
if (selectedAnalogInput?.id && selectedAnalogInput.id !== 0) {
handleFetchData();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedAnalogInput?.id]);
// ✅ Chart-Daten aus Redux filtern (Chart reagiert nur nach Button)
const chartKey = selectedAnalogInput?.id
? String(selectedAnalogInput.id + 99)
: null;
const inputData = chartKey ? data[chartKey] ?? [] : [];
const filteredData = inputData.filter((point) => {
const date = new Date(point.t);
const from = vonDatumRedux ? new Date(vonDatumRedux) : null;
const to = bisDatumRedux ? new Date(bisDatumRedux) : null;
return (!from || date >= from) && (!to || date <= to);
});
const memoizedChartData = React.useMemo(() => {
return {
datasets:
filteredData.length > 0
? zeitraum === "DIA0"
? [
{
label: "Messwert Minimum ", // (i)
data: filteredData
.filter((p) => typeof p.i === "number")
.map((p) => ({ x: new Date(p.t), y: p.i })),
borderColor: "gray",
borderWidth: 1,
pointRadius: 0,
tension: 0.1,
order: 1,
},
{
label: selectedAnalogInput?.label
? //? `Messwert ${selectedAnalogInput.label}` // (m)
`Messwert ` // (m)
: "Messwert ", // (m)
data: filteredData
.filter((p) => typeof p.m === "number")
.map((p) => ({ x: new Date(p.t), y: p.m })),
borderColor: getColor("littwin-blue"),
backgroundColor: "rgba(59,130,246,0.3)",
borderWidth: 2,
pointRadius: 0,
tension: 0.1,
order: 2,
},
{
label: "Messwert Maximum ", // (a)
data: filteredData
.filter((p) => typeof p.a === "number")
.map((p) => ({ x: new Date(p.t), y: p.a })),
borderColor: "gray",
borderWidth: 1,
pointRadius: 0,
tension: 0.1,
order: 3,
},
]
: [
{
label: "Messwert Minimum", // (i)
data: filteredData
.filter((p) => typeof p.i === "number")
.map((p) => ({ x: new Date(p.t), y: p.i })),
borderColor: "gray",
borderWidth: 1,
pointRadius: 0,
tension: 0.1,
order: 1,
},
{
label: "Durchschnitt", // (g)
data: filteredData
.filter((p) => typeof p.g === "number")
.map((p) => ({ x: new Date(p.t), y: p.g })),
borderColor: getColor("littwin-blue"),
backgroundColor: "rgba(59,130,246,0.3)",
borderWidth: 2,
pointRadius: 0,
tension: 0.1,
order: 2,
},
{
label: "Messwert Maximum", // (a)
data: filteredData
.filter((p) => typeof p.a === "number")
.map((p) => ({ x: new Date(p.t), y: p.a })),
borderColor: "gray",
borderWidth: 1,
pointRadius: 0,
tension: 0.1,
order: 3,
},
]
: [],
};
}, [filteredData, zeitraum, selectedAnalogInput]);
const memoizedChartOptions = React.useMemo(() => {
return {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: "top" as const },
tooltip: {
mode: "index" as const,
intersect: false,
callbacks: {
label: (context: TooltipItem<"line">) => {
const label = context.dataset.label || "";
return `${label}: ${context.parsed.y}`;
},
title: (items: TooltipItem<"line">[]) => {
const date = items[0].parsed.x;
return `Zeitpunkt: ${new Date(date).toLocaleString("de-DE")}`;
},
},
},
},
title: {
display: true,
text: `Verlauf der letzten 30 Tage`,
},
zoom: {
pan: {
enabled: true,
mode: "x" as const,
title: {
display: true,
text: selectedAnalogInput?.label
? `Verlauf: ${selectedAnalogInput.label}`
: "Messwert-Verlauf",
},
zoom: {
wheel: { enabled: true },
pinch: { enabled: true },
mode: "x" as const,
},
},
},
scales: {
x: {
type: "time" as const,
time: {
unit: "day" as const, // nur Datum in Achse
tooltipFormat: "dd.MM.yyyy HH:mm", // aber Uhrzeit im Tooltip sichtbar
displayFormats: {
day: "dd.MM.yyyy",
pan: {
enabled: true,
mode: "x" as const,
},
zoom: {
wheel: { enabled: true },
pinch: { enabled: true },
mode: "x" as const,
},
},
},
scales: {
x: {
type: "time" as const,
time: {
unit: "day" as const,
tooltipFormat: "dd.MM.yyyy HH:mm",
displayFormats: {
day: "dd.MM.yyyy",
},
},
adapters: { date: { locale: de } },
title: { display: true, text: "Zeit" },
min: vonDatum ? new Date(vonDatum).getTime() : undefined,
max: bisDatum ? new Date(bisDatum).getTime() : undefined,
},
y: {
title: {
display: true,
text: `Messwert ${selectedAnalogInput?.unit || ""}`,
},
},
},
};
}, [vonDatum, bisDatum, selectedAnalogInput]);
adapters: {
date: {
locale: de,
},
},
title: {
display: true,
text: "Zeit",
},
},
y: {
title: {
display: true,
text: `Messwert [${selectedInput?.unit ?? ""}]`,
},
},
},
};
// ✅ AutoLoad nur beim ersten Laden
useEffect(() => {
if (autoLoad && selectedId) {
dispatch(
getAnalogInputsHistoryThunk({
eingang: selectedId,
zeitraum,
vonDatum,
bisDatum,
})
);
dispatch(setAutoLoad(false));
}
}, [autoLoad, selectedId, dispatch, zeitraum, vonDatum, bisDatum]);
// Dynamisches Importieren von chartjs-plugin-zoom nur im Browser
useEffect(() => {
if (typeof window !== "undefined") {
import("chartjs-plugin-zoom").then((module) => {
ChartJS.register(module.default);
});
}
}, []);
return (
<div className="w-full h-full">
<Line data={chartData} options={chartOptions} />
<div
className={`flex flex-col gap-2 h-full ${loading ? "cursor-wait" : ""}`}
>
<div className="flex justify-between items-center p-2 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-fg">
Eingang {selectedId ?? ""}
</Dialog.Title>
</div>
<div className="flex justify-end">
<div className="flex flex-wrap items-center gap-4 mb-2">
{/* ✅ Neuer DatePicker mit schönem Styling (lokal, ohne Redux) */}
<AnalogInputsDatePicker
from={localVonDatum}
to={localBisDatum}
onChange={handleDateChange}
/>
{/* ✅ Zeitraum-Auswahl (Listbox nur lokal) */}
<Listbox value={localZeitraum} onChange={setLocalZeitraum}>
<div className="relative w-48">
<Listbox.Button className="w-full border 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"
: localZeitraum === "DIA1"
? "Stündlich"
: "Täglich"}
</span>
<i className="bi bi-chevron-down text-[var(--color-muted)]" />
</Listbox.Button>
<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-[var(--color-surface-alt)] text-fg"
>
{option === "DIA0"
? "Alle Messwerte"
: option === "DIA1"
? "Stündlich"
: "Täglich"}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
{/* ✅ Button: lädt die Daten & aktualisiert Redux */}
<button
onClick={handleFetchData}
className="btn-primary px-4 py-1 rounded text-sm"
>
Daten laden
</button>
</div>
</div>
</div>
{/* Chart-Anzeige */}
<div className="flex-1 min-h-0 w-full">
{!selectedAnalogInput?.id ? (
<div className="flex items-center justify-center h-full text-fg-secondary text-lg gap-2">
<i className="bi bi-info-circle text-2xl mr-2" />
<span>Bitte Eingang auswählen</span>
</div>
) : (
<Line
ref={chartRef}
data={memoizedChartData}
options={memoizedChartOptions}
style={{ height: "100%", width: "100%" }}
/>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,88 @@
"use client";
import React from "react";
import { Dialog } from "@headlessui/react";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "@/redux/store";
import { setIsChartModalOpen } from "@/redux/slices/analogInputs/analogInputsUiSlice";
import AnalogInputsChart from "@/components/main/analogInputs/AnalogInputsChart";
export default function AnalogInputsChartModal({
loading,
setLoading,
}: {
loading: boolean;
setLoading: (v: boolean) => void;
}) {
const dispatch = useDispatch();
const isOpen = useSelector(
(state: RootState) => state.analogInputsUi.isChartModalOpen
);
const selectedId = useSelector(
(state: RootState) => state.analogInputsHistory.selectedId
);
const [isFullscreen, setIsFullscreen] = React.useState(false);
if (!isOpen) return null;
return (
<Dialog
open={isOpen}
onClose={() => dispatch(setIsChartModalOpen(false))}
className="relative z-[9999]"
>
{/* Backdrop */}
<div className="fixed inset-0 bg-black/50" aria-hidden="true" />
{/* Centered panel */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="relative outline-none">
<div
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" : "38rem",
}}
>
{/* 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 ?? ""}
</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>
</div>
</Dialog>
);
}

View File

@@ -0,0 +1,93 @@
"use client";
// components/main/analogInputs/AnalogInputsDatePicker.tsx
import React, { useEffect, useState } from "react";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
type Props = {
from: string;
to: string;
onChange: (from: string, to: string) => void;
};
export default function AnalogInputsDatePicker({ from, to, onChange }: Props) {
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const sixMonthsAgo = new Date();
sixMonthsAgo.setMonth(today.getMonth() - 6);
// interne Date-Objekte für react-datepicker
const parseISO = (dateStr: string) => {
if (!dateStr) return null;
const [year, month, day] = dateStr.split("-").map(Number);
return new Date(year, month - 1, day);
};
const formatISO = (date: Date) => date.toLocaleDateString("sv-SE"); // yyyy-MM-dd
const [localFromDate, setLocalFromDate] = useState<Date | null>(
from ? parseISO(from) : thirtyDaysAgo
);
const [localToDate, setLocalToDate] = useState<Date | null>(
to ? parseISO(to) : today
);
// Wenn Props von außen kommen (z.B. Reset), synchronisieren
useEffect(() => {
if (from) setLocalFromDate(parseISO(from));
if (to) setLocalToDate(parseISO(to));
}, [from, to]);
const handleFromChange = (date: Date | null) => {
setLocalFromDate(date);
if (date && localToDate) {
onChange(formatISO(date), formatISO(localToDate));
}
};
const handleToChange = (date: Date | null) => {
setLocalToDate(date);
if (localFromDate && date) {
onChange(formatISO(localFromDate), formatISO(date));
}
};
return (
<div className="flex space-x-4 items-center">
{/* Von */}
<div className="flex items-center space-x-2">
<label className="block text-sm font-semibold">Von</label>
<DatePicker
selected={localFromDate}
onChange={handleFromChange}
selectsStart
startDate={localFromDate}
endDate={localToDate}
minDate={sixMonthsAgo}
maxDate={today}
dateFormat="dd.MM.yyyy"
className="border px-2 py-1 rounded"
/>
</div>
{/* Bis */}
<div className="flex items-center space-x-2">
<label className="block text-sm font-semibold">Bis</label>
<DatePicker
selected={localToDate}
onChange={handleToChange}
selectsEnd
startDate={localFromDate}
endDate={localToDate}
minDate={sixMonthsAgo}
maxDate={today}
dateFormat="dd.MM.yyyy"
className="border px-2 py-1 rounded"
/>
</div>
</div>
);
}

View File

@@ -1,26 +1,24 @@
"use client"; // /components/main/analogeEingaenge/AnalogInputsSettingsModal.tsx
"use client";
import React, { useEffect, useState } from "react";
import { Listbox } from "@headlessui/react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@/redux/store";
import { setIsSettingsModalOpen } from "@/redux/slices/analogInputs/analogInputsUiSlice";
interface AnalogInput {
id: number;
label?: string;
offset?: number | string;
factor?: number | string;
loggerInterval: string;
unit?: string;
}
import type { AnalogInput } from "@/types/analogInput"; // 👈 Importiere den Typ (jetzt definiert und exportiert)
interface Props {
selectedInput: AnalogInput;
isOpen: boolean;
onClose: () => void;
}
export default function AnalogInputsSettingsModal() {
const dispatch = useDispatch();
const isOpen = useSelector(
(state: RootState) => state.analogInputsUi.isSettingsModalOpen
);
const selectedInput = useSelector<RootState, AnalogInput | null>(
(state) => state.selectedAnalogInput
);
export default function AnalogInputSettingsModal({
selectedInput,
isOpen,
onClose,
}: Props) {
const [label, setLabel] = useState("");
const [offset, setOffset] = useState("0.000");
const [factor, setFactor] = useState("1.000");
@@ -28,6 +26,8 @@ export default function AnalogInputSettingsModal({
const [unit, setUnit] = useState("V");
const [isSaving, setIsSaving] = useState(false);
const unitOptions = ["V", "mA", "°C", "bar", "%"];
useEffect(() => {
if (selectedInput && isOpen) {
setLabel(selectedInput.label || "");
@@ -41,12 +41,8 @@ export default function AnalogInputSettingsModal({
? selectedInput.factor.toFixed(3)
: selectedInput.factor || "1.000"
);
setLoggerInterval(selectedInput.loggerInterval);
setLoggerInterval(selectedInput.loggerInterval || "9");
setUnit(selectedInput.unit || "V");
console.log(
"selectedInput in analoge Eingänge:",
selectedInput.loggerInterval
);
}
}, [selectedInput, isOpen]);
@@ -54,6 +50,7 @@ export default function AnalogInputSettingsModal({
const handleSave = async () => {
setIsSaving(true);
const slot = selectedInput.id;
const isDev = window.location.hostname === "localhost";
@@ -99,7 +96,7 @@ export default function AnalogInputSettingsModal({
alert("Einstellungen gespeichert (Produktion).");
}
onClose();
dispatch(setIsSettingsModalOpen(false));
location.reload();
} catch (err) {
alert("Fehler beim Speichern.");
@@ -111,108 +108,118 @@ export default function AnalogInputSettingsModal({
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={onClose}
className="text-2xl hover:text-gray-400"
onClick={() => dispatch(setIsSettingsModalOpen(false))}
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">
<div>
<span className="font-normal">Bezeichnung:</span>
</div>
<div>
</header>
<div className="modal-body-scroll px-6 py-5 flex-1 text-fg">
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
<span className="font-normal text-fg-secondary">Bezeichnung:</span>
<input
type="text"
className="w-full border rounded px-3 py-1 mb-4"
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>
{/* Offset */}
<div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4">
<div>
<span className="font-normal">Offset:</span>
</div>
<div>
<div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4">
<span className="font-normal text-fg-secondary">Offset:</span>
<input
type="number"
step="0.001"
className="border border-gray-300 rounded px-2 py-1 w-full text-right "
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)}
/>
</div>
</div>
{/* Faktor */}
<div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4">
<div>
<span className="font-normal">Faktor:</span>
</div>
<div>
<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-gray-300 rounded px-2 py-1 w-full text-right"
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>
{/* Einheit */}
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
<div>
<span className="font-normal">Einheit:</span>
</div>
<div>
<select
className="w-full border rounded px-3 py-1 mb-4"
value={unit}
onChange={(e) => setUnit(e.target.value)}
>
<option value="V">V</option>
<option value="mA">mA</option>
<option value="°C">°C</option>
<option value="bar">bar</option>
<option value="%">%</option>
</select>
</div>
</div>
{/* Loggerintervall/Speicherintervall */}
<div className="grid grid-cols-2 gap-x-4 gap-y-3 ">
<span className="font-normal">Speicherintervall:</span>
<div className="relative w-full">
<input
type="number"
className="border rounded px-2 py-1 pr-20 w-full text-right"
value={loggerInterval}
onChange={(e) => setLoggerInterval(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">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>
<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

@@ -7,18 +7,17 @@ import { getAnalogInputsThunk } from "@/redux/thunks/getAnalogInputsThunk";
import { Icon } from "@iconify/react";
import settingsIcon from "@iconify/icons-mdi/settings";
import waveformIcon from "@iconify/icons-mdi/waveform";
import { setSelectedAnalogInput } from "@/redux/slices/selectedAnalogInputSlice";
export default function AnalogInputsTable({
import { setSelectedAnalogInput } from "@/redux/slices/analogInputs/selectedAnalogInputSlice";
import { setIsSettingsModalOpen } from "@/redux/slices/analogInputs/analogInputsUiSlice";
import { setIsChartModalOpen } from "@/redux/slices/analogInputs/analogInputsUiSlice";
import {
setSelectedId,
setSelectedInput,
setIsSettingsModalOpen,
}: {
setSelectedId: (id: number) => void;
setSelectedInput: (input: AnalogInput) => void;
setIsSettingsModalOpen: (open: boolean) => void;
}) {
setAutoLoad,
} from "@/redux/slices/analogInputs/analogInputsHistorySlice";
export default function AnalogInputsTable({ loading }: { loading: boolean }) {
const dispatch = useDispatch<AppDispatch>();
const [activeId, setActiveId] = React.useState<number | null>(null);
useEffect(() => {
@@ -30,13 +29,18 @@ export default function AnalogInputsTable({
);
const handleSelect = (id: number, input: AnalogInput) => {
setSelectedId(id);
dispatch(setSelectedId(id));
setActiveId(id);
dispatch(setSelectedAnalogInput(input)); // 🧠 hier kommt die Bezeichnung in Redux
dispatch(setSelectedAnalogInput(input));
dispatch(setAutoLoad(true));
};
return (
<div className="bg-white shadow-md border border-gray-200 p-3 rounded-lg w-full laptop:p-1 xl:p-1">
<div
className={`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">
<Icon
icon={waveformIcon}
@@ -45,69 +49,113 @@ export default function AnalogInputsTable({
Messwerteingänge
</h2>
<div className="overflow-x-auto">
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse">
<thead className="bg-gray-100 border-b items-center ">
<table
className={`text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse w-full ${
loading ? "cursor-wait" : ""
}`}
>
<thead className="bg-[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">Eingang</th>
<th className="border p-1 text-left">Messwert</th>
<th className="border p-1 text-left">Einheit</th>
<th className="border p-1 text-left">Bezeichnung</th>
<th className="border p-1 text-left">Aktion</th>
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
Eingang
</th>
<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-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
Einheit
</th>
<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-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
Einstellungen
</th>
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
Messkurve
</th>
</tr>
</thead>
<tbody>
{Object.values(analogInputs)
.filter(
(e) =>
e && typeof e.id === "number" && typeof e.label === "string"
(analogInput) =>
analogInput &&
typeof analogInput.id === "number" &&
typeof analogInput.label === "string"
)
.slice(0, 8)
.map((e, index) => (
.map((analogInput, index) => (
<tr
key={index}
className={`transition cursor-pointer ${
e.id === activeId ? "bg-blue-100" : "hover:bg-gray-100"
loading
? "cursor-wait"
: analogInput.id === activeId
? "bg-[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"
onClick={() => handleSelect(e.id!, e)}
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 ">
<Icon
icon={waveformIcon}
className="text-gray-600 text-base laptop:text-sm xl:text-sm 2xl:text-lg"
/>
{e.id ?? "-"}
{analogInput.id ?? "-"}
</div>
</td>
<td
className="border p-2 text-right"
onClick={() => handleSelect(e.id!, e)}
className="border p-2 text-right bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
onClick={() => handleSelect(analogInput.id!, analogInput)}
>
{typeof e.value === "number" ? e.value.toFixed(2) : "-"}
{typeof analogInput.value === "number"
? analogInput.value.toFixed(2)
: "-"}
</td>
<td className="border p-2">{e.unit || "-"}</td>
<td
className="border p-2"
onClick={() => handleSelect(e.id!, e)}
className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
onClick={() => handleSelect(analogInput.id!, analogInput)}
>
{e.label || "----"}
{analogInput.unit || "-"}
</td>
<td
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">
<td className="border p-2 text-center bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
<button
onClick={() => {
handleSelect(e.id!, e);
setSelectedInput(e);
setIsSettingsModalOpen(true);
handleSelect(analogInput.id!, analogInput);
dispatch(setIsSettingsModalOpen(true));
}}
className="text-gray-400 hover:text-gray-500"
className="text-gray-400 hover:text-gray-500 dark:text-gray-300 dark:hover:text-white"
>
<Icon icon={settingsIcon} className="text-xl" />
</button>
</td>
<td className="border p-2 text-center bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
<button
onClick={() => {
handleSelect(analogInput.id!, analogInput);
dispatch(setIsChartModalOpen(true));
}}
className="text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
title="Messkurve anzeigen"
aria-label="Messkurve anzeigen"
>
<span role="img" aria-hidden="true" className="text-lg">
📈
</span>
</button>
</td>
</tr>
))}
</tbody>

View File

@@ -0,0 +1,54 @@
"use client";
// components/main/analogInputs/AnalogInputsView.tsx
import React, { useState, useEffect } from "react";
import AnalogInputsTable from "@/components/main/analogInputs/AnalogInputsTable";
import AnalogInputsChartModal from "@/components/main/analogInputs/AnalogInputsChartModal";
import AnalogInputsSettingsModal from "@/components/main/analogInputs/AnalogInputsSettingsModal";
import { getAnalogInputsThunk } from "@/redux/thunks/getAnalogInputsThunk";
import { useAppDispatch } from "@/redux/store";
import { useSelector } from "react-redux";
import { RootState } from "@/redux/store";
function AnalogInputsView() {
const [loading, setLoading] = useState(false); // Add loading state
const selectedInput = useSelector(
(state: RootState) => state.selectedAnalogInput
);
// selectedId is now displayed within the modal header
const dispatch = useAppDispatch();
useEffect(() => {
if (typeof window !== "undefined") {
dispatch(getAnalogInputsThunk());
const interval = setInterval(() => {
dispatch(getAnalogInputsThunk());
}, 10000);
return () => clearInterval(interval);
}
}, [dispatch]);
return (
<div
className={`flex flex-col justify-start gap-3 p-4 h-[calc(100vh-13vh-8vh)] ${
loading ? "cursor-wait" : ""
}`}
>
<div className="grid grid-cols-1 gap-4 justify-items-start">
<div className="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} />
</div>
</div>
{selectedInput !== null && <AnalogInputsSettingsModal />}
{/* Chart Modal */}
<AnalogInputsChartModal loading={loading} setLoading={setLoading} />
</div>
);
}
export default AnalogInputsView;

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

@@ -0,0 +1,62 @@
"use client";
// components/main/dashboard/DashboardView.tsx
import React, { useEffect } from "react";
import "tailwindcss/tailwind.css";
import "@fontsource/roboto";
import "bootstrap-icons/font/bootstrap-icons.css";
import { Icon } from "@iconify/react";
import Last20MessagesTable from "@/components/main/dashboard/Last20MessagesTable";
import NetworkInfo from "@/components/main/dashboard/NetworkInfo";
import VersionInfo from "@/components/main/dashboard/VersionInfo";
import Baugruppentraeger from "@/components/main/dashboard/Baugruppentraeger";
import { getLast20MessagesThunk } from "@/redux/thunks/getLast20MessagesThunk";
import { useAppDispatch } from "@/redux/store";
const DashboardView: React.FC = () => {
//-------------------------------------
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(getLast20MessagesThunk());
const interval = setInterval(() => {
dispatch(getLast20MessagesThunk());
}, 10000); // oder 5000
return () => clearInterval(interval);
}, [dispatch]);
//-------------------------------------
return (
<div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)] laptop:h-[calc(100vh-10vh-5vh)] xl:h-[calc(100vh-10vh-6vh)] laptop:gap-0 bg-[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">
<Icon
icon="ri:calendar-schedule-line"
className="text-littwin-blue text-4xl xl:text-2xl"
/>
<h1 className="text-xl font-bold xl:text-base text-[var(--color-fg)] tracking-wide">
Letzten 20 Meldungen
</h1>
</div>
</div>
{/* Hauptbereich mit Meldungstabelle und Baugruppenträger */}
<div className="flex flex-col lg:flex-row gap-4 flex-grow overflow-hidden pt-4">
<Last20MessagesTable className="w-full lg:w-2/3 h-full" />
<div className="shadow-md rounded-lg w-full lg:w-1/3 flex flex-col gap-2">
<VersionInfo className="w-full p-3 text-sm" />
{/* Baugruppenträger jetzt mit voller Breite */}
<div className="overflow-auto max-h-[50vh]">
<Baugruppentraeger />
</div>
</div>
</div>
{/* NetworkInfo in einem div ,nimmt die gesamte Breite */}
<NetworkInfo />
</div>
);
};
export default DashboardView;

View File

@@ -48,35 +48,56 @@ export default function Last20MessagesTable({ className }: Props) {
return (
<div className={`flex flex-col gap-3 p-4 ${className}`}>
<div className="overflow-auto max-h-[80vh]">
<table className="min-w-full border">
<thead className="bg-gray-100 text-left sticky top-0 z-10">
<table className="min-w-full border 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">Prio</th>
<th className="p-2 border">Zeitstempel</th>
<th className="p-2 border">Quelle</th>
<th className="p-2 border">Meldung</th>
<th className="p-2 border">Status</th>
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
Prio
</th>
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
Zeitstempel
</th>
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
Quelle
</th>
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
Meldung
</th>
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
Status
</th>
</tr>
</thead>
<tbody>
{filteredMessages.slice(0, 20).map((msg, index) => (
<tr key={index} className="hover:bg-gray-50">
<td className="border p-2">
<tr
key={index}
className="hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30 transition"
>
<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">{msg.t}</td>
<td className="border p-2">{msg.i}</td>
<td className="border p-2">{msg.m}</td>
<td className="border p-2">{msg.v}</td>
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
{msg.t}
</td>
<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-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
{msg.m}
</td>
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
{msg.v}
</td>
</tr>
))}
</tbody>
</table>
{messages.length === 0 && (
<div className="mt-4 text-center text-gray-500 italic">
<div className="mt-4 text-center 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 p-2 rounded-lg shadow-md border border-gray-200 laptop:m-0 laptop:scale-y-75 2xl:scale-y-75">
<div className=" flex-grow flex justify-between items-center mt-1 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,8 +49,8 @@ const NetworkInfo: React.FC = () => {
priority
/>
<div>
<p className="text-xs text-gray-500">IP-Adresse</p>
<p className="text-sm font-medium text-gray-700">{ip}</p>
<p className="text-xs text-[var(--color-fg-muted)]">IP-Adresse</p>
<p className="text-sm font-medium text-[var(--color-fg)]">{ip}</p>
</div>
</div>
@@ -64,8 +64,10 @@ const NetworkInfo: React.FC = () => {
priority
/>
<div>
<p className="text-xs text-gray-500">Subnet-Maske</p>
<p className="text-sm font-medium text-gray-700">{subnet}</p>
<p className="text-xs text-[var(--color-fg-muted)]">Subnet-Maske</p>
<p className="text-sm font-medium text-[var(--color-fg)]">
{subnet}
</p>
</div>
</div>
@@ -79,16 +81,20 @@ const NetworkInfo: React.FC = () => {
priority
/>
<div>
<p className="text-xs text-gray-500">Gateway</p>
<p className="text-sm font-medium text-gray-700">{gateway}</p>
<p className="text-xs text-[var(--color-fg-muted)]">Gateway</p>
<p className="text-sm font-medium text-[var(--color-fg)]">
{gateway}
</p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="text-xs font-bold text-littwin-blue">OPC-UA</div>
<div>
<p className="text-xs text-gray-500">Status</p>
<p className="text-sm font-medium text-gray-700">{opcUaZustand}</p>
<p className="text-xs text-[var(--color-fg-muted)]">Status</p>
<p className="text-sm font-medium text-[var(--color-fg)]">
{opcUaZustand}
</p>
</div>
</div>
{/* OPC UA Nodeset Name */}

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,24 +15,26 @@ const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
);
return (
<div
className={`bg-gray-50 rounded-lg shadow-sm border border-gray-200 w-full laptop:p-2 ${className}`}
>
<h2 className="text-lg font-semibold text-gray-700 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">
Applikationsversion: {appVersion}
</p>
</div>
<div className="flex flex-row p-2 space-x-2">
<Icon icon="mdi:web" className="text-xl text-blue-400" />
<p className="text-sm text-gray-600">Webversion: {webVersion}</p>
</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

@@ -18,10 +18,18 @@ const KabelModulStatus: React.FC<KabelModulStatusProps> = ({
// Modultyp basierend auf der Version bestimmen
let moduleName = "";
let moduleType = "";
if (moduleVersion === 419) {
moduleName = "KÜ705";
moduleType = "FO";
} else if (moduleVersion === 420) {
moduleName = "KÜ705";
moduleType = "FO";
} else if (moduleVersion === 421) {
moduleName = "KÜ705";
moduleType = "FO";
} else if (moduleVersion === 431) {
moduleName = "KÜ705";
moduleType = "FO";
} else if (moduleVersion === 350) {
moduleName = "KÜ605";
moduleType = "µC";

View File

@@ -0,0 +1,70 @@
// components/main/digitalInputs/DigitalInputsView.tsx
"use client";
import React, { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { AppDispatch } from "@/redux/store";
import InputModal from "@/components/main/digitalInputs/digitalInputsModal";
import { getDigitalInputsThunk } from "@/redux/thunks/getDigitalInputsThunk";
import DigitalInputsWidget from "@/components/main/digitalInputs/DigitalInputsWidget";
const DigitalInputsView: React.FC = () => {
const dispatch = useDispatch<AppDispatch>();
interface DigitalInput {
id: number;
eingangOffline: boolean;
status: boolean;
label: string;
[key: string]: unknown;
}
const [selectedInput, setSelectedInput] = useState<DigitalInput | null>(null);
const [isInputModalOpen, setIsInputModalOpen] = useState(false);
useEffect(() => {
dispatch(getDigitalInputsThunk());
const interval = setInterval(() => {
dispatch(getDigitalInputsThunk());
}, 10000);
return () => clearInterval(interval);
}, [dispatch]);
const openInputModal = (input: DigitalInput) => {
setSelectedInput(input);
setIsInputModalOpen(true);
};
const closeInputModal = () => {
setSelectedInput(null);
setIsInputModalOpen(false);
};
return (
<div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)] laptop:h-[calc(100vh-10vh-5vh)] xl:h-[calc(100vh-10vh-6vh)] laptop:gap-0">
<h1 className="text-base font-semibold mb-2">Meldungseingänge</h1>
<div className="grid grid-cols-1 xl:grid-cols-3 gap-4 items-start ">
<DigitalInputsWidget
openInputModal={openInputModal}
inputRange={{ start: 0, end: 16 }}
/>
<DigitalInputsWidget
openInputModal={openInputModal}
inputRange={{ start: 16, end: 32 }}
/>
</div>
{isInputModalOpen && selectedInput && (
<InputModal
selectedInput={selectedInput}
closeInputModal={closeInputModal}
isOpen={isInputModalOpen}
/>
)}
</div>
);
};
export default DigitalInputsView;

View File

@@ -30,7 +30,7 @@ export default function DigitalInputsWidget({
//console.log("DigitalInputs", inputs);
return (
<div className="bg-white shadow-md border border-gray-200 p-3 rounded-lg w-full laptop:p-1 xl:p-1">
<div className="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,30 @@ export default function DigitalInputsWidget({
/>
Meldungseingänge {inputRange.start + 1} {inputRange.end}
</h2>
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse">
<thead className="bg-gray-100 border-b">
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-[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">Eingang</th>
<th className="px-1 py-1 text-left">Zustand</th>
<th className="px-1 py-1 text-left">Bezeichnung</th>
<th className="px-1 py-1 text-left">Aktion</th>
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
Eingang
</th>
<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-[var(--color-surface)] text-[var(--color-fg)]">
Bezeichnung
</th>
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
Aktion
</th>
</tr>
</thead>
<tbody>
{inputs.map((input) => (
<tr key={input.id} className="border-b">
<td className="px-1 py-0">
<tr
key={input.id}
className="border-b 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-[var(--color-surface)] text-[var(--color-fg)]">
<div className="flex items-center gap-1 ">
<Icon
icon={loginIcon}
@@ -59,7 +70,7 @@ export default function DigitalInputsWidget({
{input.id}
</div>
</td>
<td className="px-1 py-1 ">
<td className="px-1 py-1 bg-[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 ">
@@ -80,11 +91,13 @@ export default function DigitalInputsWidget({
</div>
)}
</td>
<td className="px-1 py-1">{input.label}</td>
<td className="px-1 py-1">
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
{input.label}
</td>
<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"
className="text-gray-400 text-base laptop:text-sm xl:text-sm 2xl:text-lg cursor-pointer dark:text-gray-300 dark:hover:text-white"
onClick={() => openInputModal(input)}
/>
</td>

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

@@ -49,22 +49,22 @@ export default function DigitalOutputsModal({
try {
if (isCPL) {
// ✅ Name speichern (DANx=...)
const nameEncoded = encodeURIComponent(label.trim());
const nameUrl = `/CPL?digitalOutputs.html&DAN0${selectedOutput.id}=${nameEncoded}`;
// ✅ Status speichern (DASx=...)
const statusUrl = `/CPL?digitalOutputs.html&DAS0${selectedOutput.id}=${
status ? 1 : 0
}`;
// 🟢 Beide nacheinander senden (wichtig bei älteren CPL-Versionen)
window.location.href = nameUrl; // Name zuerst (ggf. durch Refresh überschrieben)
setTimeout(() => {
window.location.href = statusUrl;
}, 300); // kleine Verzögerung (optional)
try {
await fetch(nameUrl, { method: "GET" });
await new Promise((res) => setTimeout(res, 300));
await fetch(statusUrl, { method: "GET" });
// 💡 Modal wird nicht automatisch geschlossen — da Seite neu lädt.
closeOutputModal(); // Seite bleibt erhalten
} catch (err) {
console.error("❌ Fehler bei fetch:", err);
setErrorMsg("❌ Fehler beim Speichern.");
}
} else {
// 🧪 Lokaler Entwicklungsmodus
const res = await fetch("/api/cpl/updateDigitalOutputsHandler", {
@@ -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

@@ -0,0 +1,61 @@
"use client";
import React, { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { AppDispatch } from "@/redux/store";
import DigitalOutputsModal from "./DigitalOutputsModal";
import DigitalOutputsWidget from "./DigitalOutputsWidget";
import { getDigitalOutputsThunk } from "@/redux/thunks/getDigitalOutputsThunk";
import type { DigitalOutput } from "@/types/digitalOutput";
const DigitalOutputsView: React.FC = () => {
const dispatch = useDispatch<AppDispatch>();
const [selectedOutput, setSelectedOutput] = useState<DigitalOutput | null>(
null
);
const [isOutputModalOpen, setIsOutputModalOpen] = useState(false);
useEffect(() => {
// Fetch immediately on mount to ensure data is present without waiting for the first interval
dispatch(getDigitalOutputsThunk());
// Then continue polling periodically
const interval = setInterval(() => {
dispatch(getDigitalOutputsThunk());
}, 3000);
return () => clearInterval(interval);
}, [dispatch]);
const openOutputModal = (output: DigitalOutput) => {
setSelectedOutput(output);
setIsOutputModalOpen(true);
};
const closeOutputModal = () => {
setSelectedOutput(null);
setIsOutputModalOpen(false);
};
return (
<div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)] laptop:h-[calc(100vh-10vh-5vh)] xl:h-[calc(100vh-10vh-6vh)] laptop:gap-0">
<h1 className="text-base font-semibold mb-2">Schaltausgänge</h1>
<div className="grid grid-cols-1 xl:grid-cols-3 gap-4 items-start">
<DigitalOutputsWidget openOutputModal={openOutputModal} />
</div>
{selectedOutput && (
<DigitalOutputsModal
selectedOutput={selectedOutput}
isOpen={isOutputModalOpen}
closeOutputModal={closeOutputModal}
/>
)}
</div>
);
};
export default DigitalOutputsView;

View File

@@ -32,9 +32,25 @@ export default function DigitalOutputsWidget({
try {
if (isCPL) {
window.location.href = `/CPL?digitalOutputs.html&DAS0${id}=${
updatedOutputs[id - 1].status ? 1 : 0
}`;
// Statt redirect:
// window.location.href = `/CPL?...`;
// Verwende fetch:
fetch(
`/CPL?digitalOutputs.html&DAS0${id}=${
updatedOutputs[id - 1].status ? 1 : 0
}`,
{
method: "GET",
}
)
.then((res) => {
if (!res.ok) throw new Error("Fehler beim Schalten");
// Optional: Feedback anzeigen
})
.catch((err) => {
console.error("CPL Fehler:", err);
});
} else {
await fetch("/api/cpl/updateDigitalOutputsHandler", {
method: "POST",
@@ -50,7 +66,7 @@ export default function DigitalOutputsWidget({
};
return (
<div className="bg-white shadow-md border border-gray-200 p-3 rounded-lg w-full h-fit max-h-[400px] overflow-auto">
<div className="bg-[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}
@@ -58,41 +74,60 @@ export default function DigitalOutputsWidget({
/>
Schaltausgänge
</h2>
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white rounded-lg">
<thead className="bg-gray-100 border-b">
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-[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">Ausgang</th>
<th className="px-1 py-1 text-left">Bezeichnung</th>
<th className="px-1 py-1 text-left">Schalter</th>
<th className="px-1 py-1 text-left">Aktion</th>
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
Ausgang
</th>
<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-[var(--color-surface)] text-[var(--color-fg)]">
Schalter
</th>
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
Aktion
</th>
</tr>
</thead>
<tbody>
{digitalOutputs.map((output) => (
<tr key={output.id} className="border-b">
<td className="flex items-center px-1 py-1">
<tr
key={output.id}
className="border-b border-base hover:bg-[var(--color-surface-alt)] transition-colors"
>
<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">{output.label}</td>
<td className="px-1 py-1">
<Icon
icon={switchIcon}
className={`cursor-pointer text-base transition ${
output.status
? "text-littwin-blue"
: "text-gray-500 scale-x-[-1]"
}`}
onClick={() => handleToggle(output.id)}
/>
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
{output.label}
</td>
<td className="px-1 py-1">
<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-[var(--color-surface)] text-[var(--color-fg)]">
<Icon
icon={settingsIcon}
className="text-gray-400 text-base cursor-pointer"
className="text-[var(--color-muted)] text-base cursor-pointer hover:text-[var(--color-fg)]"
onClick={() => openOutputModal(output)}
/>
</td>

View File

@@ -0,0 +1,62 @@
import React from "react";
import { useSelector } from "react-redux";
import { RootState } from "../../../redux/store";
// components/main/fall-detection-sensors/FallSensors.tsx
interface FallSensorsProps {
slotIndex: number;
}
const FallSensors: React.FC<FallSensorsProps> = ({ slotIndex }) => {
const { kvzStatus } = useSelector((state: RootState) => state.kueDataSlice);
// Nur 4 LEDs für den spezifischen Slot anzeigen
const leds = Array.from({ length: 4 }, (_, ledIndex) => {
const arrayIndex = slotIndex * 4 + ledIndex;
const ledValue = kvzStatus?.[arrayIndex];
// LED Status: 1 = grün, 0 = rot, 2 = grau, undefined = grau
if (ledValue === 1) return "green";
if (ledValue === 0) return "red";
return "gray"; // für 2 oder undefined
});
return (
<div className="bg-gray-300 border border-gray-400 rounded p-1 mt-4 w-full ">
{/* Überschrift mit KVZ-Labels */}
<div className="flex justify-between items-center mb-2">
<span className="text-[8px] font-medium text-gray-700">KVZ1</span>
<span className="text-[8px] font-medium text-gray-700">KVZ2</span>
<span className="text-[8px] font-medium text-gray-700">KVZ3</span>
<span className="text-[8px] font-medium text-gray-700">KVZ4</span>
</div>
{/* LEDs */}
<div className="flex justify-between items-center">
{leds.map((ledStatus, ledIndex) => {
// LED Farben: grün (1), rot (0), grau (2)
let bgColor = "bg-gray-400"; // Standard grau
let statusText = "Unbekannt";
if (ledStatus === "green") {
bgColor = "bg-green-500";
statusText = "Ein";
} else if (ledStatus === "red") {
bgColor = "bg-red-500";
statusText = "Aus";
}
return (
<div
key={ledIndex}
className={`w-4 h-4 rounded-full border border-gray-500 ${bgColor} flex-shrink-0`}
title={`Slot ${slotIndex} LED${ledIndex + 1}: ${statusText}`}
/>
);
})}
</div>
</div>
);
};
export default FallSensors;

View File

@@ -0,0 +1,180 @@
"use client"; // /pages/kabelueberwachung.tsx
import React, { useState, useEffect } from "react";
import { useSearchParams } from "next/navigation";
import Kue705FO from "@/components/main/kabelueberwachung/kue705FO/Kue705FO";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch } from "@/redux/store"; // Adjust the path to your Redux store file
import { RootState } from "@/redux/store"; // Adjust the path to your Redux store file
import { getKueDataThunk } from "@/redux/thunks/getKueDataThunk";
function KabelueberwachungView() {
const dispatch: AppDispatch = useDispatch();
const searchParams = useSearchParams(); // URL-Parameter holen
const initialRack = parseInt(searchParams.get("rack") ?? "1") || 1; // Rack-Nummer aus URL oder 1
const [activeRack, setActiveRack] = useState<number>(initialRack); // Nutze initialRack als Startwert
const [alarmStatus, setAlarmStatus] = useState<boolean[]>([]); // Alarmstatus
// Redux-Variablen aus dem Store abrufen
const {
kueOnline,
kueID,
kueIso,
kueAlarm1,
kueAlarm2,
kueResidence,
kueCableBreak,
kueGroundFault,
} = useSelector((state: RootState) => state.kueDataSlice);
//----------------------------------------------------------------
// Alarmstatus basierend auf Redux-Variablen berechnen
const updateAlarmStatus = React.useCallback(() => {
const updatedAlarmStatus = kueIso.map(
(_: number | string, index: number) => {
return Boolean(
(kueAlarm1 && kueAlarm1[index]) ||
(kueAlarm2 && kueAlarm2[index]) ||
(kueCableBreak && kueCableBreak[index]) ||
(kueGroundFault && kueGroundFault[index])
);
}
);
setAlarmStatus(updatedAlarmStatus);
}, [kueIso, kueAlarm1, kueAlarm2, kueCableBreak, kueGroundFault]);
// Alarmstatus initial berechnen und alle 10 Sekunden aktualisieren
useEffect(() => {
updateAlarmStatus();
const interval = setInterval(updateAlarmStatus, 10000);
return () => clearInterval(interval);
}, [updateAlarmStatus]);
// Modul- und Rack-Daten aufbereiten
const allModules = kueIso.map((iso: number | string, index: number) => ({
isolationswert: iso,
schleifenwiderstand: kueResidence[index],
modulName: kueID[index] || `Modul ${index + 1}`, // Eindeutiger Name pro Index
kueOnlineStatus: kueOnline[index],
alarmStatus: alarmStatus[index],
tdrLocation: [], // Placeholder, replace with actual tdrLocation if available
win_fallSensorsActive: kueOnline[index] ? 1 : 0, // Beispielwert, anpassen je nach Logik
}));
//console.log("Alle Module:", allModules);
const racks = React.useMemo(
() => ({
rack1: allModules.slice(0, 8),
rack2: allModules.slice(8, 16),
rack3: allModules.slice(16, 24),
rack4: allModules.slice(24, 32),
}),
[allModules]
);
// Konsolenausgaben für jede Rack-Aufteilung
/* console.log(
"Rack 1 Module:",
racks.rack1.map((slot) => slot.modulName)
);
console.log(
"Rack 2 Module:",
racks.rack2.map((slot) => slot.modulName)
);
console.log(
"Rack 3 Module:",
racks.rack3.map((slot) => slot.modulName)
);
console.log(
"Rack 4 Module:",
racks.rack4.map((slot) => slot.modulName)
); */
// Funktion zum Wechseln des Racks
const changeRack = (rack: number) => {
setActiveRack(rack);
console.log(`Aktives Rack geändert zu: ${rack}`);
};
useEffect(() => {
/* console.log(`Aktives Rack: ${activeRack}`);
console.log(
`Rack ${activeRack} Modulnamen:`,
racks[`rack${activeRack as 1 | 2 | 3 | 4}` as keyof typeof racks].map((slot: any) => slot.modulName)
); */
}, [activeRack, racks]);
//-----------------------------------------------------------
//------------------------------------------------------------
useEffect(() => {
if (kueIso.length === 0) {
console.log("📦 Lade KUE-Daten aus getKueDataThunk...");
dispatch(getKueDataThunk());
}
}, [dispatch, kueIso.length]);
//------------------------------------------------------------
// JSX rendering
return (
<div>
<div className="mb-4">
{[1, 2, 3, 4].map((rack) => {
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%]">
{(
racks[
`rack${activeRack as 1 | 2 | 3 | 4}` as keyof typeof racks
] as typeof allModules
).map(
(
slot: {
isolationswert: number | string;
schleifenwiderstand: number | string;
modulName: string;
kueOnlineStatus: number;
alarmStatus?: boolean;
tdrLocation: number[];
win_fallSensorsActive: number;
},
index: number
) => {
const slotIndex = index + (activeRack - 1) * 8;
return (
<div key={index} className="flex">
<Kue705FO
isolationswert={slot.isolationswert}
schleifenwiderstand={slot.schleifenwiderstand}
modulName={slot.modulName}
kueOnline={slot.kueOnlineStatus}
alarmStatus={slot.alarmStatus}
slotIndex={slotIndex}
tdrLocation={slot.tdrLocation}
win_fallSensorsActive={slot.win_fallSensorsActive}
/>
</div>
);
}
)}
</div>
</div>
);
}
export default KabelueberwachungView;

View File

@@ -1,201 +0,0 @@
"use client"; // /components/modules/kue705FO/charts/ChartSwitcher.tsx
import React, { useEffect } from "react";
import ReactModal from "react-modal";
import LoopChartActionBar from "./LoopMeasurementChart/LoopChartActionBar";
import LoopMeasurementChart from "./LoopMeasurementChart/LoopMeasurementChart";
import TDRChart from "./TDRChart/TDRChart";
import { useSelector, useDispatch } from "react-redux";
import { AppDispatch } from "@/redux/store";
import { RootState } from "@/redux/store";
import {
setChartOpen,
setFullScreen,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { resetBrushRange } from "@/redux/slices/brushSlice";
import { useLoopChartLoader } from "./LoopMeasurementChart/LoopChartActionBar";
import {
setVonDatum,
setBisDatum,
setSelectedMode,
setSelectedSlotType,
} from "@/redux/slices/kabelueberwachungChartSlice";
interface ChartSwitcherProps {
isOpen: boolean;
onClose: () => void;
slotIndex: number;
}
const ChartSwitcher: React.FC<ChartSwitcherProps> = ({ isOpen, onClose }) => {
const dispatch = useDispatch<AppDispatch>();
const chartTitle = useSelector(
(state: RootState) => state.loopChartType.chartTitle
);
// **Redux-States für aktive Messkurve (TDR oder Schleife)**
const activeMode = useSelector(
(state: RootState) => state.kueChartModeSlice.activeMode
);
const isFullScreen = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
);
// **Modal schließen + Redux-Status zurücksetzen**
const handleClose = () => {
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
// Reset Datum
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
// Reset Dropdowns
dispatch(setSelectedMode("DIA1"));
dispatch(setSelectedSlotType("isolationswiderstand"));
// Sonstiges Reset
dispatch(setChartOpen(false));
dispatch(setFullScreen(false));
dispatch(resetBrushRange());
onClose();
};
// **Vollbildmodus umschalten**
const toggleFullScreen = () => {
dispatch(setFullScreen(!isFullScreen));
};
// **Slot und Messkurve setzen**
// const setChartType = (chartType: "TDR" | "Schleife") => {
// dispatch(setSelectedSlot(slotIndex));
// dispatch(setSelectedChartType(chartType));
// };
// useLoopChartLoader hook
const loadLoopChartData = useLoopChartLoader();
// Slot number from Redux
const slotNumber = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice.slotNumber
);
// immer beim Öffnen das Modal die letzten 30 Tage anzeigen
useEffect(() => {
if (isOpen && activeMode === "Schleife" && slotNumber !== null) {
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const toISO = (date: Date) => date.toLocaleDateString("sv-SE"); // YYYY-MM-DD
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
// Warten, bis Redux gesetzt ist → dann Daten laden
setTimeout(() => {
loadLoopChartData.loadLoopChartData();
}, 10); // kleiner Delay, damit Redux-State sicher aktualisiert ist
}
//ESLint ignore
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, activeMode, slotNumber, dispatch]);
return (
<ReactModal
isOpen={isOpen}
onRequestClose={handleClose}
ariaHideApp={false}
style={{
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
content: {
top: "50%",
left: "50%",
bottom: "auto",
marginRight: "-50%",
transform: "translate(-50%, -50%)",
width: isFullScreen ? "90vw" : "70rem",
height: isFullScreen ? "90vh" : "35rem",
padding: "1rem",
transition: "all 0.3s ease-in-out",
display: "flex",
flexDirection: "column",
},
}}
>
{/* Action-Buttons */}
<div
style={{
position: "absolute",
top: "0.625rem",
right: "0.625rem",
display: "flex",
gap: "0.75rem",
}}
>
{/* Fullscreen-Button */}
<button
onClick={toggleFullScreen}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i
className={
isFullScreen ? "bi bi-fullscreen-exit" : "bi bi-arrows-fullscreen"
}
></i>
</button>
{/* Schließen-Button */}
<button
onClick={handleClose}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i className="bi bi-x-circle-fill"></i>
</button>
</div>
{/* Chart-Container */}
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
height: "100%",
}}
>
{activeMode === "Schleife" ? (
<>
<h3 className="text-lg font-semibold">{chartTitle}</h3>
<LoopChartActionBar />
<div style={{ flex: 1, height: "90%" }}>
<LoopMeasurementChart />
</div>
</>
) : (
<>
<h3 className="text-lg font-semibold">TDR-Messung</h3>
<TDRChart isFullScreen={isFullScreen} />
</>
)}
</div>
</ReactModal>
);
};
export default ChartSwitcher;

View File

@@ -0,0 +1,361 @@
"use client";
// /components/main/kabelueberwachung/kue705FO/Charts/IsoMeasurementChart/IsoChartActionBar.tsx
import React, { forwardRef, useImperativeHandle } from "react";
import DateRangePicker from "@/components/common/DateRangePicker";
import { useSelector } from "react-redux";
import { RootState, useAppDispatch } from "@/redux/store";
import {
setIsoMeasurementCurveChartData,
setSelectedMode,
setChartOpen,
setLoading,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { setBrushRange } from "@/redux/slices/brushSlice";
import { getMessagesThunk } from "@/redux/thunks/getMessagesThunk";
import { Listbox } from "@headlessui/react";
//-----------------------------------------------------------------------------------useIsoChartLoader
export const useIsoChartLoader = () => {
const dispatch = useAppDispatch();
const { vonDatum, bisDatum, selectedMode, slotNumber } = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice
);
const hasShownNoDataAlert = React.useRef(false);
const formatDate = (dateString: string) => {
const [year, month, day] = dateString.split("-");
return `${year};${month};${day}`;
};
const getApiUrl = (mode: "DIA0" | "DIA1" | "DIA2", slotNumber: number) => {
const type = 3; // Fest auf Isolationswiderstand gesetzt
const typeFolder = "isolationswiderstand";
let url: string;
if (process.env.NODE_ENV === "development") {
url = `/api/cpl/slotDataAPIHandler?slot=${slotNumber}&messart=${typeFolder}&dia=${mode}&vonDatum=${vonDatum}&bisDatum=${bisDatum}`;
} else {
url = `${window.location.origin}/CPL?seite.ACP&${mode}=${formatDate(
vonDatum
)};${formatDate(bisDatum)};${slotNumber};${type};`;
}
return url;
};
const loadIsoChartData = async () => {
if (slotNumber === null) return;
dispatch(setLoading(true));
dispatch(setChartOpen(false));
dispatch(setIsoMeasurementCurveChartData([]));
const startTime = Date.now();
const MIN_LOADING_TIME_MS = 1000;
try {
const apiUrl = getApiUrl(selectedMode, slotNumber);
const response = await fetch(apiUrl);
const data = await response.json();
const waitTime = Math.max(
0,
MIN_LOADING_TIME_MS - (Date.now() - startTime)
);
await new Promise((res) => setTimeout(res, waitTime));
if (Array.isArray(data) && data.length > 0) {
dispatch(setIsoMeasurementCurveChartData(data));
dispatch(setChartOpen(true));
} else {
dispatch(setIsoMeasurementCurveChartData([]));
dispatch(setChartOpen(false));
if (!hasShownNoDataAlert.current) {
alert("⚠️ Keine Messdaten im gewählten Zeitraum gefunden");
hasShownNoDataAlert.current = true;
}
}
} catch (err) {
console.error("❌ Fehler beim Laden:", err);
alert("❌ Fehler beim Laden.");
} finally {
dispatch(setLoading(false));
}
};
return { loadIsoChartData };
};
//-----------------------------------------------------------------------------------useIsoDataLoader Hook
export const useIsoDataLoader = () => {
const dispatch = useAppDispatch();
const { vonDatum, bisDatum, selectedMode, slotNumber } = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice
);
const formatDate = (dateString: string) => {
const [year, month, day] = dateString.split("-");
return `${year};${month};${day}`;
};
const getApiUrl = (mode: "DIA0" | "DIA1" | "DIA2", slotNumber: number) => {
const type = 3; // Fest auf Isolationswiderstand gesetzt
const typeFolder = "isolationswiderstand";
const baseUrl =
process.env.NODE_ENV === "development"
? `/api/cpl/slotDataAPIHandler?slot=${slotNumber}&messart=${typeFolder}&dia=${mode}&vonDatum=${vonDatum}&bisDatum=${bisDatum}`
: `${window.location.origin}/CPL?seite.ACP&${mode}=${formatDate(
vonDatum
)};${formatDate(bisDatum)};${slotNumber};${type};`;
return baseUrl;
};
const loadData = async () => {
if (slotNumber === null) {
console.log("⚠️ Kein Slot ausgewählt - automatisches Laden übersprungen");
return;
}
const apiUrl = getApiUrl(selectedMode, slotNumber);
if (!apiUrl) return;
dispatch(setLoading(true));
dispatch(setChartOpen(false));
dispatch(setIsoMeasurementCurveChartData([]));
const MIN_LOADING_TIME_MS = 1000;
const startTime = Date.now();
try {
const response = await fetch(apiUrl, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) throw new Error(`Fehler: ${response.status}`);
const jsonData = await response.json();
const elapsedTime = Date.now() - startTime;
const waitTime = Math.max(0, MIN_LOADING_TIME_MS - elapsedTime);
await new Promise((resolve) => setTimeout(resolve, waitTime));
if (Array.isArray(jsonData) && jsonData.length > 0) {
dispatch(setIsoMeasurementCurveChartData(jsonData));
dispatch(setChartOpen(true));
} else {
dispatch(setIsoMeasurementCurveChartData([]));
dispatch(setChartOpen(false));
}
} catch (err) {
console.error("❌ Fehler beim automatischen Laden der Daten:", err);
} finally {
dispatch(setLoading(false));
}
};
return { loadData };
};
//-----------------------------------------------------------------------------------IsoChartActionBar
const IsoChartActionBar = forwardRef((_props, ref) => {
IsoChartActionBar.displayName = "IsoChartActionBar";
const dispatch = useAppDispatch();
const { vonDatum, bisDatum, selectedMode, slotNumber, chartTitle } =
useSelector((state: RootState) => state.kabelueberwachungChartSlice);
// Aus DateRangePicker-Slice kommen die Werte, die der User im UI wählt
const { vonDatum: pickerVonDatum, bisDatum: pickerBisDatum } = useSelector(
(state: RootState) => state.dateRangePicker
);
const formatDate = (dateString: string) => {
const [year, month, day] = dateString.split("-");
return `${year};${month};${day}`;
};
const getApiUrl = (
mode: "DIA0" | "DIA1" | "DIA2",
slotNumber: number,
fromDate: string,
toDate: string
) => {
const type = 3; // Fest auf Isolationswiderstand gesetzt
const typeFolder = "isolationswiderstand";
const baseUrl =
process.env.NODE_ENV === "development"
? `/api/cpl/slotDataAPIHandler?slot=${slotNumber}&messart=${typeFolder}&dia=${mode}&vonDatum=${fromDate}&bisDatum=${toDate}`
: `${window.location.origin}/CPL?seite.ACP&${mode}=${formatDate(
fromDate
)};${formatDate(toDate)};${slotNumber};${type};`;
console.log("baseUrl", baseUrl);
return baseUrl;
};
const handleFetchData = async () => {
if (slotNumber === null) {
alert("⚠️ Bitte zuerst einen KÜ auswählen!");
return;
}
// Wenn Meldungen-Ansicht aktiv ist, dann Meldungen laden
if (chartTitle === "Meldungen") {
try {
dispatch(setLoading(true));
const fromDate = pickerVonDatum ?? vonDatum;
const toDate = pickerBisDatum ?? bisDatum;
await dispatch(getMessagesThunk({ fromDate, toDate })).unwrap();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error("❌ Fehler beim Laden der Meldungen:", message);
alert("❌ Fehler beim Laden der Meldungen.");
} finally {
dispatch(setLoading(false));
}
return;
}
// Messkurve (ISO) laden
const fromDate = pickerVonDatum ?? vonDatum;
const toDate = pickerBisDatum ?? bisDatum;
const apiUrl = getApiUrl(selectedMode, slotNumber, fromDate, toDate);
if (!apiUrl) return;
dispatch(setLoading(true));
dispatch(setChartOpen(false));
dispatch(setIsoMeasurementCurveChartData([]));
const MIN_LOADING_TIME_MS = 1000;
const startTime = Date.now();
try {
const response = await fetch(apiUrl, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) throw new Error(`Fehler: ${response.status}`);
const jsonData = await response.json();
const elapsedTime = Date.now() - startTime;
const waitTime = Math.max(0, MIN_LOADING_TIME_MS - elapsedTime);
await new Promise((resolve) => setTimeout(resolve, waitTime));
console.log("▶️ Lade Isolationswiderstand-Daten für:");
console.log(" Slot:", slotNumber);
console.log(" Modus:", selectedMode);
console.log(" Von:", fromDate);
console.log(" Bis:", toDate);
console.log(" URL:", apiUrl);
console.log(" Daten:", jsonData);
if (Array.isArray(jsonData) && jsonData.length > 0) {
dispatch(setIsoMeasurementCurveChartData(jsonData));
dispatch(setChartOpen(true));
} else {
alert("⚠️ Keine Messdaten im gewählten Zeitraum gefunden.");
dispatch(setIsoMeasurementCurveChartData([]));
dispatch(setChartOpen(false));
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error("❌ Fehler beim Laden der Daten:", message);
alert("❌ Fehler beim Laden der Daten.");
} finally {
dispatch(setLoading(false));
}
};
useImperativeHandle(ref, () => ({ handleFetchData }));
const isMeldungen = chartTitle === "Meldungen";
return (
<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 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ündlich",
DIA2: "Täglich",
}[selectedMode]
}
</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>
);
});
export default IsoChartActionBar;

View File

@@ -0,0 +1,196 @@
"use client";
import React, { useEffect, useRef } from "react";
import { Listbox } from "@headlessui/react";
import ReactModal from "react-modal";
import IsoMeasurementChart from "./IsoMeasurementChart";
import IsoChartActionBar from "./IsoChartActionBar";
import Report from "./Report";
import { useSelector, useDispatch } from "react-redux";
import { AppDispatch, RootState } from "@/redux/store";
import {
setChartOpen,
setFullScreen,
setSlotNumber,
setChartTitle,
setVonDatum,
setBisDatum,
setSelectedMode,
setSelectedSlotType,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { resetBrushRange } from "@/redux/slices/brushSlice";
import { resetDateRange } from "@/redux/slices/dateRangePickerSlice";
interface IsoChartViewProps {
isOpen: boolean;
onClose: () => void;
slotIndex: number;
}
type ActionBarRefType = { handleFetchData: () => void };
const IsoChartView: React.FC<IsoChartViewProps> = ({
isOpen,
onClose,
slotIndex,
}) => {
const dispatch = useDispatch<AppDispatch>();
const { isFullScreen, chartTitle } = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice
);
const actionBarRef = useRef<ActionBarRefType>(null);
const initDates = () => {
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)));
};
const handleClose = () => {
initDates();
dispatch(resetDateRange());
dispatch(setSelectedMode("DIA0"));
dispatch(setSelectedSlotType("isolationswiderstand"));
dispatch(setChartTitle("Messkurve"));
dispatch(setChartOpen(false));
dispatch(setFullScreen(false));
dispatch(resetBrushRange());
onClose();
};
const toggleFullScreen = () => dispatch(setFullScreen(!isFullScreen));
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 = (d: Date) => d.toLocaleDateString("sv-SE");
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
dispatch(setSelectedSlotType("isolationswiderstand"));
dispatch(setSelectedMode("DIA0"));
dispatch(setChartTitle("Messkurve"));
const t = setTimeout(() => actionBarRef.current?.handleFetchData(), 120);
return () => clearTimeout(t);
}
}, [isOpen, slotIndex, dispatch]);
return (
<ReactModal
isOpen={isOpen}
onRequestClose={handleClose}
ariaHideApp={false}
style={{
overlay: {
backgroundColor: "rgba(0,0,0,0.55)",
backdropFilter: "blur(2px)",
},
content: {
inset: "50% auto auto 50%",
transform: "translate(-50%, -50%)",
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"
>
<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") =>
dispatch(setChartTitle(value))
}
>
<div className="relative w-40">
<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="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-3 py-1.5 cursor-pointer rounded-sm m-0.5 ${
selected
? "dropdown-option-active"
: active
? "dropdown-option-hover"
: ""
}`
}
>
{option}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
</header>
<div className="flex flex-col flex-1 p-3 gap-3">
<IsoChartActionBar ref={actionBarRef} />
<div className="flex-1 relative">
{chartTitle === "Messkurve" ? (
<IsoMeasurementChart />
) : (
<Report moduleType="ISO" autoLoad={chartTitle === "Meldungen"} />
)}
</div>
</div>
</ReactModal>
);
};
export default IsoChartView;

View File

@@ -0,0 +1,70 @@
"use client";
import React from "react";
interface IsoCustomTooltipProps {
active?: boolean;
payload?: Array<{
dataKey: string;
value: number;
name?: string;
color?: string;
unit?: string;
// Add other known properties here as needed
}>;
label?: string;
unit?: string;
}
const IsoCustomTooltip: React.FC<IsoCustomTooltipProps> = ({
active,
payload,
label,
unit,
}) => {
if (active && payload && payload.length) {
const messwertMax = payload.find((p) => p.dataKey === "messwertMaximum");
const messwert = payload.find((p) => p.dataKey === "messwert");
const messwertMin = payload.find((p) => p.dataKey === "messwertMinimum");
const messwertDurchschnitt = payload.find(
(p) => p.dataKey === "messwertDurchschnitt"
);
return (
<div
style={{
background: "white",
padding: "8px",
border: "1px solid lightgrey",
borderRadius: "5px",
}}
>
<strong>{new Date(label as string).toLocaleString()}</strong>
{messwertMax && (
<div style={{ color: "grey" }}>
Messwert Maximum: {messwertMax.value.toFixed(2)} {unit}
</div>
)}
{messwert && (
<div style={{ color: "#00AEEF", fontWeight: "bold" }}>
Messwert: {messwert.value.toFixed(2)} {unit}
</div>
)}
{messwertDurchschnitt && (
<div style={{ color: "#00AEEF" }}>
Messwert Durchschnitt: {messwertDurchschnitt.value.toFixed(2)}{" "}
{unit}
</div>
)}
{messwertMin && (
<div style={{ color: "grey" }}>
Messwert Minimum: {messwertMin.value.toFixed(2)} {unit}
</div>
)}
</div>
);
}
return null;
};
export default IsoCustomTooltip;

View File

@@ -0,0 +1,231 @@
"use client"; // /components/main/kabelueberwachung/kue705FO/Charts/IsoMeasurementChart/IsoMeasurementChart.tsx
import React, { useEffect, useRef } from "react";
import { useSelector } from "react-redux";
import { RootState } from "@/redux/store";
import {
Chart as ChartJS,
LineController,
LineElement,
PointElement,
LinearScale,
TimeScale,
Title,
Tooltip,
Legend,
Filler,
} from "chart.js";
import "chartjs-adapter-date-fns";
import { de } from "date-fns/locale";
import { differenceInHours, parseISO } from "date-fns";
ChartJS.register(
LineController,
LineElement,
PointElement,
LinearScale,
TimeScale,
Title,
Tooltip,
Legend,
Filler
);
import { getColor } from "@/utils/colors";
import { PulseLoader } from "react-spinners";
type IsoMeasurementEntry = {
t: string;
i: number;
m: number;
g: number;
a: number;
};
const usePreviousData = (data: IsoMeasurementEntry[]) => {
const ref = useRef<IsoMeasurementEntry[]>([]);
useEffect(() => {
ref.current = data;
}, [data]);
return ref.current;
};
const IsoMeasurementChart = () => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const chartInstance = useRef<ChartJS | null>(null);
const {
isoMeasurementCurveChartData,
selectedMode,
unit,
isFullScreen,
isLoading,
vonDatum,
bisDatum,
} = useSelector((state: RootState) => state.kabelueberwachungChartSlice);
const previousData = usePreviousData(isoMeasurementCurveChartData);
// Vergleichsfunktion
const isEqual = (
a: IsoMeasurementEntry[],
b: IsoMeasurementEntry[]
): boolean => {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (
a[i].t !== b[i].t ||
a[i].i !== b[i].i ||
a[i].m !== b[i].m ||
a[i].g !== b[i].g ||
a[i].a !== b[i].a
) {
return false;
}
}
return true;
};
// ⏱ Zeitspanne in Stunden berechnen
const from = vonDatum ? parseISO(vonDatum) : null;
const to = bisDatum ? parseISO(bisDatum) : null;
const durationInHours = from && to ? differenceInHours(to, from) : 9999;
const timeUnit: "hour" | "day" = durationInHours <= 48 ? "hour" : "day";
useEffect(() => {
import("chartjs-plugin-zoom").then((zoomPlugin) => {
if (!ChartJS.registry.plugins.get("zoom")) {
ChartJS.register(zoomPlugin.default);
}
if (!canvasRef.current) return;
const ctx = canvasRef.current.getContext("2d");
if (!ctx) return;
if (isEqual(isoMeasurementCurveChartData, previousData)) {
return; // keine echte Datenänderung → nicht neu zeichnen
}
if (chartInstance.current) {
chartInstance.current.destroy();
}
const chartData = {
labels: isoMeasurementCurveChartData
.map((entry) => new Date(entry.t))
.reverse(),
datasets: [
{
label: "Messwert Minimum",
data: isoMeasurementCurveChartData.map((e) => e.i).reverse(),
borderColor: "lightgrey",
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 10,
tension: 0.1,
order: 1,
},
{
label: "Messwert Maximum",
data: isoMeasurementCurveChartData.map((e) => e.a).reverse(),
borderColor: "gray",
borderWidth: 1,
pointRadius: 0,
tension: 0.1,
order: 3,
},
selectedMode === "DIA0"
? {
label: "Messwert",
data: isoMeasurementCurveChartData.map((e) => e.m).reverse(),
borderColor: getColor("littwin-blue"),
backgroundColor: "rgba(59,130,246,0.5)",
borderWidth: 3,
pointRadius: 0,
pointHoverRadius: 10,
tension: 0.1,
order: 2,
}
: {
label: "Messwert Durchschnitt",
data: isoMeasurementCurveChartData.map((e) => e.g).reverse(),
borderColor: getColor("littwin-blue"),
backgroundColor: "rgba(59,130,246,0.5)",
borderWidth: 3,
pointRadius: 0,
pointHoverRadius: 10,
tension: 0.1,
order: 2,
},
],
};
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: "top" as const },
tooltip: {
yAlign: "bottom" as const,
mode: "index" as const,
intersect: false,
},
zoom: {
pan: { enabled: true, mode: "x" as const },
zoom: {
wheel: { enabled: true },
pinch: { enabled: true },
mode: "x" as const,
},
},
},
scales: {
x: {
type: "time" as const,
time: {
unit: timeUnit,
tooltipFormat: "dd.MM.yyyy HH:mm:ss",
displayFormats: {
hour: "HH:mm",
day: "dd.MM.",
},
locale: de,
},
title: { display: true, text: "Zeit" },
},
y: {
title: { display: true, text: unit },
ticks: { precision: 0 },
},
},
};
chartInstance.current = new ChartJS(ctx, {
type: "line",
data: chartData,
options,
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isoMeasurementCurveChartData, selectedMode, vonDatum, bisDatum]);
return (
<div style={{ width: "100%", height: isFullScreen ? "90%" : "400px" }}>
{isLoading && (
<div className="flex items-center justify-center h-96">
<PulseLoader color="#3B82F6" />
</div>
)}
<canvas
ref={canvasRef}
style={{
width: "100%",
height: "100%",
display: isLoading ? "none" : "block",
}}
/>
</div>
);
};
export default IsoMeasurementChart;

View File

@@ -0,0 +1,54 @@
### 🧭 Zoom-Verhalten beim Schleifen-/Isolationsdiagramm
In dieser Komponente wird das automatische Nachladen der Messwerte temporär deaktiviert, wenn der Benutzer per Maus in das Diagramm zoomt oder pannt. Nach 30 Sekunden ohne Zoom/Pan-Aktion wird die automatische Aktualisierung wieder aktiviert. Dieses Verhalten dient dazu, den Zoom-Zustand nicht durch neue Daten zu verlieren.
---
### 📁 Enthaltene Komponenten
- `LoopChartActionBar.tsx`
→ Auswahlleiste für Slot-Nummer, Zeitraum (über `DateRangePicker`), Messmodus (`DIA0`, `DIA1`, `DIA2`) und Slot-Typ (Schleife/Isolation).
→ Ruft alle 10 Sekunden neue Messdaten ab außer der Zoom-Modus pausiert das.
- `LoopMeasurementChart.tsx`
→ Das eigentliche Liniendiagramm mit Chart.js + Zoom-Plugin.
→ Erkennt Zoom/Pan und setzt `chartUpdatePaused`, bis 30 Sekunden Inaktivität vergangen sind.
- `DateRangePicker.tsx`
→ Zeigt zwei Felder für Von-/Bis-Datum. Nutzt Redux, um globale Zeitfenster zu setzen.
- `CustomTooltip.tsx`
→ Zeigt beim Hover über die Kurve kontextbezogene Werte wie Messwert, Min, Max und Durchschnitt (DIA0/1/2).
---
### 🟢 UML-Aktivitätsdiagramm (Zoom → Pause → Timer → Auto-Update)
```mermaid
flowchart TD
Start([Start])
ZoomEvent[[Zoom oder Pan erkannt]]
SetPause[Setze chartUpdatePaused = true]
StartTimer[Starte 30s Timer]
Check[Timer abgelaufen?]
SetResume[Setze chartUpdatePaused = false]
FetchData[[Datenabruf wieder erlaubt]]
End([Ende])
Start --> ZoomEvent --> SetPause --> StartTimer --> Check
Check -- Nein --> StartTimer
Check -- Ja --> SetResume --> FetchData --> End
```
stateDiagram-v2
[*] --> AktualisierungAktiv
AktualisierungAktiv --> ZoomPause : Zoom/Pan erkannt
ZoomPause --> AktualisierungAktiv : 30 Sekunden Inaktivität
state AktualisierungAktiv {
[*] --> Normalbetrieb
}
state ZoomPause {
[*] --> CountdownLäuft
}

View File

@@ -0,0 +1,278 @@
"use client"; // Report.tsx
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { useSelector, useDispatch } from "react-redux";
import { RootState, AppDispatch } from "@/redux/store";
import { getMessagesThunk } from "@/redux/thunks/getMessagesThunk";
// Gleiche Datenstruktur wie MeldungenView
type Meldung = {
t: string; // timestamp
s: number; // status/priority
c: string; // color
m: string; // message
i: string; // source/info
v: string; // value/status text
};
type ModuleType = "ISO" | "TDR" | "RSL" | "KVZ";
interface ReportProps {
moduleType: ModuleType;
autoLoad?: boolean;
}
const Report: React.FC<ReportProps> = ({ moduleType, autoLoad = true }) => {
const dispatch = useDispatch<AppDispatch>();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [filteredMessages, setFilteredMessages] = useState<Meldung[]>([]);
const { vonDatum, bisDatum, slotNumber } = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice
);
// Nachrichten aus dem globalen Store
const messages = useSelector((state: RootState) => state.messages.data);
// Nachrichten für den aktuellen Slot filtern
const filterMessagesForSlot = useCallback(
(allMessages: Meldung[], slot: number) => {
if (slot === null) return [];
// Primärer Filter: Exakte CableLineX Übereinstimmung (X = slot + 1)
const primaryIdentifier = `CableLine${slot + 1}`;
console.log(
`🔍 Filtere Nachrichten für Slot ${slot} (${primaryIdentifier}):`
);
console.log(`📥 Gesamt Nachrichten: ${allMessages.length}`);
// Debug: Zeige alle verfügbaren Quellen
const allSources = [...new Set(allMessages.map((msg) => msg.i))];
console.log(`📋 Alle verfügbaren Quellen:`, allSources);
// Filter basierend auf der Quelle (i-Feld) - EXAKTE Übereinstimmung
const filtered = allMessages.filter((msg: Meldung) => {
// Exakte Übereinstimmung: msg.i sollte genau "CableLineX" sein
const isExactMatch = msg.i === primaryIdentifier;
// Fallback: Falls die Quelle mehr Informationen enthält (z.B. "CableLine1_Sensor")
const isPartialMatch =
msg.i.startsWith(primaryIdentifier) &&
(msg.i === primaryIdentifier ||
msg.i.charAt(primaryIdentifier.length).match(/[^0-9]/));
const isMatch = isExactMatch || isPartialMatch;
if (isMatch) {
console.log(`✅ Gefunden: "${msg.i}" -> ${msg.m}`);
}
return isMatch;
});
console.log(
`📤 Gefilterte Nachrichten für ${primaryIdentifier}: ${filtered.length}`
);
// Falls keine Nachrichten mit CableLineX gefunden, versuche alternative Identifikatoren
if (filtered.length === 0) {
console.log(
`⚠️ Keine Nachrichten für ${primaryIdentifier} gefunden. Versuche alternative Identifikatoren...`
);
const alternativeIdentifiers = [
`Slot${slot + 1}`,
`${slot + 1}`,
`Kue${slot + 1}`,
`Cable${slot + 1}`,
`Line${slot + 1}`,
];
const alternativeFiltered = allMessages.filter((msg: Meldung) => {
return alternativeIdentifiers.some((identifier) => {
const isExactMatch = msg.i === identifier;
const isPartialMatch =
msg.i.startsWith(identifier) &&
(msg.i === identifier ||
msg.i.charAt(identifier.length).match(/[^0-9]/));
const isMatch = isExactMatch || isPartialMatch;
if (isMatch) {
console.log(`🔄 Alternative gefunden: "${msg.i}" -> ${msg.m}`);
}
return isMatch;
});
});
console.log(
`📤 Alternative gefilterte Nachrichten: ${alternativeFiltered.length}`
);
return alternativeFiltered;
}
return filtered;
},
[]
);
// Modul-spezifische Schlüsselwörter (alle lowercase, ö => oe normalisiert)
const moduleKeywordMap = useMemo<Record<ModuleType, string[]>>(
() => ({
ISO: [
"modul online",
"aderbruch",
"erdschluss",
"isofehler",
"iso fehler",
"iso-fehler",
"isolationsfehler",
"isolationfehler",
"isolation fehler",
],
TDR: ["modul online", "tdr aktiv", "tdr entfernung"],
RSL: ["modul online", "aderbruch", "schleifenfehler"],
KVZ: ["modul online", "aderbruch", "kvz störung", "kvz stoerung"],
}),
[]
);
const normalize = (text: string) =>
text
.toLowerCase()
.replace(/ö/g, "oe")
.replace(/ä/g, "ae")
.replace(/ü/g, "ue");
// Daten laden
const loadMessages = useCallback(async () => {
if (slotNumber === null) return;
setLoading(true);
setError(null);
try {
// Redux Thunk verwenden (wie in MeldungenView)
await dispatch(
getMessagesThunk({
fromDate: vonDatum,
toDate: bisDatum,
})
).unwrap();
} catch (err) {
console.error("Fehler beim Laden der Berichte:", err);
setError("Fehler beim Laden der Meldungen.");
} finally {
setLoading(false);
}
}, [dispatch, vonDatum, bisDatum, slotNumber]);
// Filter anwenden wenn sich Nachrichten oder Slot ändern
useEffect(() => {
if (slotNumber !== null && messages.length > 0) {
const slotFiltered = filterMessagesForSlot(messages, slotNumber);
// Modul-Filter anwenden
const keywords = moduleKeywordMap[moduleType].map(normalize);
const moduleFiltered = slotFiltered.filter((m) => {
const msgNorm = normalize(m.m);
return keywords.some((kw) => msgNorm.includes(kw));
});
// Fallback: Wenn keine Keyword-Treffer, zeige Slot-Filter-Ergebnis
setFilteredMessages(
moduleFiltered.length > 0 ? moduleFiltered : slotFiltered
);
} else {
setFilteredMessages([]);
}
}, [
messages,
slotNumber,
filterMessagesForSlot,
moduleType,
moduleKeywordMap,
]);
// Automatisches Laden beim Mount und bei Änderungen (optional)
useEffect(() => {
if (!autoLoad) return;
if (slotNumber !== null) {
loadMessages();
}
}, [loadMessages, slotNumber, autoLoad]);
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="flex items-center space-x-2">
<div className="w-4 h-4 border-2 border-t-2 border-blue-500 rounded-full animate-spin" />
<span>Lade Meldungen...</span>
</div>
</div>
);
}
if (error) {
return <div className="text-center text-red-500 p-4">{error}</div>;
}
return (
<div className="w-full h-full flex flex-col p-4">
{filteredMessages.length === 0 ? (
<div className="text-center text-gray-500 ">
Keine Meldungen für CableLine
{slotNumber !== null ? slotNumber + 1 : "-"} (Filter: {moduleType}) im
gewählten Zeitraum gefunden.
</div>
) : (
<div className="flex-1 overflow-auto table-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>
</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>
)}
{/* <div className="mt-4 text-sm text-gray-500 text-center mt-4">
{filteredMessages.length} Meldung(en) (Filter: {moduleType}) gefunden
</div> */}
</div>
);
};
export default Report;

View File

@@ -0,0 +1,183 @@
"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";
import {
setChartOpen,
setFullScreen,
setSlotNumber,
setVonDatum,
setBisDatum,
setSelectedMode,
setSelectedSlotType,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { resetBrushRange } from "@/redux/slices/brushSlice";
import FallSensors from "../../../../fall-detection-sensors/FallSensors";
import Report from "../IsoMeasurementChart/Report";
interface KVZChartViewProps {
isOpen: boolean;
onClose: () => void;
slotIndex: number;
}
// Modal zur Anzeige der KVz Zustände (Sturzsensoren / Fall Detection LEDs)
// Stil und Verhalten analog zu ISO / RSL / TDR Modals
const KVZChartView: React.FC<KVZChartViewProps> = ({
isOpen,
onClose,
slotIndex,
}) => {
const dispatch = useDispatch<AppDispatch>();
const { isFullScreen, slotNumber, vonDatum, bisDatum } = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice
);
const { vonDatum: pickerVonDatum, bisDatum: pickerBisDatum } = useSelector(
(state: RootState) => state.dateRangePicker
);
// Beim Öffnen: Slot + Standard-Datumsbereich setzen (30 Tage) analog zu anderen Modals
useEffect(() => {
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]);
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"));
dispatch(setSelectedSlotType("isolationswiderstand"));
dispatch(setChartOpen(false));
dispatch(setFullScreen(false));
dispatch(resetBrushRange());
onClose();
};
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.55)",
backdropFilter: "blur(2px)",
},
content: {
inset: "50% auto auto 50%",
transform: "translate(-50%, -50%)",
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"
>
<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 : "-"}
</span>
</div>
<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 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>
);
};
export default KVZChartView;

View File

@@ -1,21 +1,29 @@
"use client";
// /components/main/kabelueberwachung/kue705FO/Charts/LoopMeasurementChart/LoopChartActionBar.tsx
import React from "react";
import DateRangePicker from "./DateRangePicker";
import { useDispatch, useSelector } from "react-redux";
import React, {
useEffect,
useState,
forwardRef,
useImperativeHandle,
} from "react";
import { useAppDispatch } from "@/redux/store";
import { getMessagesThunk } from "@/redux/thunks/getMessagesThunk";
import { RSL_DURATION_SECONDS, NODE_ENV } from "@/utils/env";
import DateRangePicker from "@/components/common/DateRangePicker";
import { useSelector } from "react-redux";
import { RootState } from "@/redux/store";
import {
setLoopMeasurementCurveChartData,
setSelectedMode,
setSelectedSlotType,
setChartOpen,
setLoading,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { setBrushRange } from "@/redux/slices/brushSlice";
import { setChartTitle } from "@/redux/slices/loopChartTypeSlice";
import { Listbox } from "@headlessui/react";
//-----------------------------------------------------------------------------------useLoopChartLoader
export const useLoopChartLoader = () => {
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const { vonDatum, bisDatum, selectedMode, selectedSlotType, slotNumber } =
useSelector((state: RootState) => state.kabelueberwachungChartSlice);
const hasShownNoDataAlert = React.useRef(false);
@@ -30,8 +38,7 @@ export const useLoopChartLoader = () => {
type: number,
slotNumber: number
) => {
const typeFolder =
type === 3 ? "isolationswiderstand" : "schleifenwiderstand";
const typeFolder = "schleifenwiderstand";
let url: string;
@@ -92,34 +99,67 @@ export const useLoopChartLoader = () => {
};
//-----------------------------------------------------------------------------------LoopChartActionBar
const LoopChartActionBar: React.FC = () => {
const dispatch = useDispatch();
const LoopChartActionBar = forwardRef((_props, ref) => {
const dispatch = useAppDispatch();
// RSL Progress State Dauer konfigurierbar über NEXT_PUBLIC_RSL_DURATION_SECONDS
const TOTAL_DURATION = RSL_DURATION_SECONDS;
const [rslRunning, setRslRunning] = useState(false);
const [rslProgress, setRslProgress] = useState(0);
// Fortschritt aktualisieren
useEffect(() => {
if (!rslRunning) return;
setRslProgress(0);
const startedAt = Date.now();
const interval = setInterval(() => {
const elapsed = Math.floor((Date.now() - startedAt) / 1000);
if (elapsed >= TOTAL_DURATION) {
setRslProgress(TOTAL_DURATION);
setRslRunning(false);
clearInterval(interval);
// Optional automatische Daten-Nachladung anstoßen
} else {
setRslProgress(elapsed);
}
}, 1000);
return () => clearInterval(interval);
}, [rslRunning, TOTAL_DURATION]);
const startRslProgress = () => {
setRslRunning(true);
setRslProgress(0);
};
const {
vonDatum,
bisDatum,
selectedMode,
selectedSlotType,
slotNumber,
isLoading,
} = useSelector((state: RootState) => state.kabelueberwachungChartSlice);
const { chartTitle } = useSelector((state: RootState) => state.loopChartType);
// Vom DateRangePicker-Slice (vom UI gewählte Werte)
const { vonDatum: pickerVonDatum, bisDatum: pickerBisDatum } = useSelector(
(state: RootState) => state.dateRangePicker
);
const getApiUrl = (
mode: "DIA0" | "DIA1" | "DIA2",
type: number,
slotNumber: number
slotNumber: number,
fromDate: string,
toDate: string
) => {
const typeFolder =
type === 3 ? "isolationswiderstand" : "schleifenwiderstand";
const typeFolder = "schleifenwiderstand";
const baseUrl =
process.env.NODE_ENV === "development"
? `/api/cpl/slotDataAPIHandler?slot=${slotNumber}&messart=${typeFolder}&dia=${mode}&vonDatum=${vonDatum}&bisDatum=${bisDatum}`
? `/api/cpl/slotDataAPIHandler?slot=${slotNumber}&messart=${typeFolder}&dia=${mode}&vonDatum=${fromDate}&bisDatum=${toDate}`
: `${window.location.origin}/CPL?seite.ACP&${mode}=${formatDate(
vonDatum
)};${formatDate(bisDatum)};${slotNumber};${type};`;
fromDate
)};${formatDate(toDate)};${slotNumber};${type};`;
console.log("baseUrl", baseUrl);
return baseUrl;
@@ -130,15 +170,64 @@ const LoopChartActionBar: React.FC = () => {
return `${year};${month};${day}`;
};
const handleStartRSL = async () => {
if (slotNumber === null) {
alert("⚠️ Bitte zuerst einen KÜ auswählen!");
return;
}
const cgiUrl = `${window.location.origin}/CPL?kabelueberwachung.html&KS_${slotNumber}=1`;
try {
console.log("🚀 Starte RSL Messung für Slot:", slotNumber);
console.log("📡 CGI URL:", cgiUrl);
if (NODE_ENV === "development") {
// DEV: externes Gerät mocken sofort Erfolg simulieren
await new Promise((r) => setTimeout(r, 200));
console.log("✅ [DEV] RSL Mock-Start ok für Slot", slotNumber);
startRslProgress();
return;
}
const response = await fetch(cgiUrl);
if (!response.ok) throw new Error(`CGI-Fehler: ${response.status}`);
console.log("✅ RSL Messung gestartet für Slot", slotNumber);
startRslProgress();
} catch (err) {
console.error("❌ Fehler beim Starten der RSL Messung:", err);
alert("❌ Fehler beim Starten der RSL Messung.");
}
};
const handleFetchData = async () => {
const type = selectedSlotType === "schleifenwiderstand" ? 4 : 3;
if (slotNumber === null) {
alert("⚠️ Bitte zuerst einen Steckplatz auswählen!");
alert("⚠️ Bitte zuerst einen auswählen!");
return;
}
const apiUrl = getApiUrl(selectedMode, type, slotNumber);
// Meldungen laden, wenn Meldungen-Ansicht aktiv ist
if (chartTitle === "Meldungen") {
try {
dispatch(setLoading(true));
const fromDate = pickerVonDatum ?? vonDatum;
const toDate = pickerBisDatum ?? bisDatum;
await dispatch(getMessagesThunk({ fromDate, toDate })).unwrap();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error("❌ Fehler beim Laden der Meldungen:", message);
alert("❌ Fehler beim Laden der Meldungen.");
} finally {
dispatch(setLoading(false));
}
return;
}
const fromDate = pickerVonDatum ?? vonDatum;
const toDate = pickerBisDatum ?? bisDatum;
const apiUrl = getApiUrl(selectedMode, type, slotNumber, fromDate, toDate);
if (!apiUrl) return;
dispatch(setLoading(true));
@@ -165,8 +254,8 @@ const LoopChartActionBar: React.FC = () => {
console.log(" Slot:", slotNumber);
console.log(" Typ:", selectedSlotType, "→", type);
console.log(" Modus:", selectedMode);
console.log(" Von:", vonDatum);
console.log(" Bis:", bisDatum);
console.log(" Von:", fromDate);
console.log(" Bis:", toDate);
console.log(" URL:", apiUrl);
console.log(" Daten:", jsonData);
@@ -188,69 +277,130 @@ const LoopChartActionBar: React.FC = () => {
}
};
useImperativeHandle(ref, () => ({
handleFetchData,
}));
// Sichtbarkeits-Flags
const isMesskurve = chartTitle === "Messkurve";
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">
Steckplatz {slotNumber ?? "-"}
</label>
<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 : "-"}
</span>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center gap-3 flex-1 justify-end">
{/* DateRangePicker immer sichtbar */}
<DateRangePicker />
<select
value={selectedMode}
onChange={(e) => {
dispatch(
setSelectedMode(e.target.value as "DIA0" | "DIA1" | "DIA2")
);
dispatch(setBrushRange({ startIndex: 0, endIndex: 0 }));
}}
className="px-3 py-1 bg-white border rounded text-sm"
>
<option value="DIA0">Alle Messwerte</option>
<option value="DIA1">Stündliche Werte</option>
<option value="DIA2">Tägliche Werte</option>
</select>
{/* Modus-Dropdown nur für Messkurve */}
<div className={isMesskurve ? "" : "hidden"}>
<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ündlich",
DIA2: "Täglich",
}[selectedMode]
}
</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]
}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
<select
value={selectedSlotType}
onChange={(e) => {
const value = e.target.value as
| "isolationswiderstand"
| "schleifenwiderstand";
dispatch(setSelectedSlotType(value));
dispatch(
setChartTitle(
value === "isolationswiderstand"
? "Isolationsmessung"
: "Schleifenmessung"
)
);
}}
className="px-3 py-1 bg-white border rounded text-sm"
>
<option value="isolationswiderstand">Isolationswiderstand</option>
<option value="schleifenwiderstand">Schleifenwiderstand</option>
</select>
<button
onClick={handleFetchData}
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm"
>
Daten laden
</button>
{isLoading && (
<div className="flex items-center space-x-2 text-sm text-gray-500">
<div className="w-4 h-4 border-2 border-t-2 border-blue-500 rounded-full animate-spin" />
<span>Lade Daten...</span>
{/* 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="btn-primary h-8 font-medium px-4"
disabled={rslRunning}
type="button"
>
Anzeigen
</button>
)}
</div>
{rslRunning && (
<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-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-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}%` }}
/>
</div>
</div>
)}
</div>
);
};
});
LoopChartActionBar.displayName = "LoopChartActionBar";
export default LoopChartActionBar;

View File

@@ -0,0 +1,189 @@
"use client"; // LoopChartView.tsx
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 { useSelector, useDispatch } from "react-redux";
import { AppDispatch, RootState } from "@/redux/store";
import {
setChartOpen,
setFullScreen,
setSlotNumber,
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 {
isOpen: boolean;
onClose: () => void;
slotIndex: number;
}
function LoopChartView({ isOpen, onClose, slotIndex }: LoopChartViewProps) {
const dispatch = useDispatch<AppDispatch>();
const chartTitle = useSelector(
(state: RootState) => state.loopChartType.chartTitle
);
const isFullScreen = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
);
// slotNumber nicht direkt benötigt wird intern über Redux genutzt
// **Modal schließen + Redux-Status zurücksetzen**
const handleClose = () => {
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
dispatch(resetDateRange());
dispatch(setSelectedMode("DIA0"));
dispatch(setSelectedSlotType("schleifenwiderstand"));
dispatch(setChartOpen(false));
dispatch(setFullScreen(false));
dispatch(resetBrushRange());
onClose();
};
const toggleFullScreen = () => dispatch(setFullScreen(!isFullScreen));
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");
dispatch(setSlotNumber(slotIndex));
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
dispatch(setSelectedSlotType("schleifenwiderstand"));
dispatch(setSelectedMode("DIA0"));
const t = setTimeout(() => actionBarRef.current?.handleFetchData(), 120);
return () => clearTimeout(t);
}
}, [isOpen, slotIndex, dispatch]);
return (
<ReactModal
isOpen={isOpen}
onRequestClose={handleClose}
ariaHideApp={false}
style={{
overlay: {
backgroundColor: "rgba(0,0,0,0.55)",
backdropFilter: "blur(2px)",
},
content: {
inset: "50% auto auto 50%",
transform: "translate(-50%, -50%)",
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"
>
<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") =>
dispatch(setLoopChartTitle(value))
}
>
<div className="relative w-40">
<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="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-3 py-1.5 cursor-pointer rounded-sm m-0.5 ${
selected
? "dropdown-option-active"
: active
? "dropdown-option-hover"
: ""
}`
}
>
{option}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
</header>
<div className="flex flex-col flex-1 p-3 gap-3">
<LoopChartActionBar ref={actionBarRef} />
<div className="flex-1 relative">
{chartTitle === "Messkurve" ? (
<LoopMeasurementChart />
) : (
<Report moduleType="RSL" autoLoad={chartTitle === "Meldungen"} />
)}
</div>
</div>
</ReactModal>
);
}
export default LoopChartView;

View File

@@ -31,7 +31,7 @@ ChartJS.register(
Legend,
Filler
);
import { getColor } from "../../../../../../utils/colors";
import { getColor } from "@/utils/colors";
import { PulseLoader } from "react-spinners";
type LoopMeasurementEntry = {
@@ -118,24 +118,20 @@ const LoopMeasurementChart = () => {
{
label: "Messwert Minimum",
data: loopMeasurementCurveChartData.map((e) => e.i).reverse(),
borderColor: "lightgrey",
backgroundColor: "rgba(211,211,211,0.5)",
borderWidth: 2,
borderColor: "gray",
borderWidth: 1,
pointRadius: 0,
pointHoverRadius: 10,
tension: 0.1,
order: 1,
},
{
label: "Messwert Maximum",
data: loopMeasurementCurveChartData.map((e) => e.a).reverse(),
borderColor: "lightgrey",
backgroundColor: "rgba(211,211,211,0.5)",
borderWidth: 2,
borderColor: "gray",
borderWidth: 1,
pointRadius: 0,
pointHoverRadius: 10,
tension: 0.1,
order: 1,
order: 3,
},
selectedMode === "DIA0"
? {
@@ -147,7 +143,7 @@ const LoopMeasurementChart = () => {
pointRadius: 0,
pointHoverRadius: 10,
tension: 0.1,
order: 3,
order: 2,
}
: {
label: "Messwert Durchschnitt",
@@ -158,7 +154,7 @@ const LoopMeasurementChart = () => {
pointRadius: 0,
pointHoverRadius: 10,
tension: 0.1,
order: 3,
order: 2,
},
],
};

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,106 +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 { 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] : [];
// 🔄 Dropdown-Auswahl: neue Messung laden
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const id = parseInt(e.target.value);
setSelectedId(id);
dispatch(getTDRChartDataByIdThunk(id));
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));
}
};
// 📌 Referenz setzen (nutzt Slotnummer + 1 für die API)
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?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.");
}
};
// 📥 Beim Slot-Wechsel TDM-Liste + letzte ID laden
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 {
const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development";
if (isDev) {
await new Promise((r) => setTimeout(r, 150));
startTdrProgress();
return;
}
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);
}
};
// 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));
}
@@ -109,53 +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
? `Steckplatz ${selectedSlot + 1}`
: "Kein Steckplatz gewählt"}
<>
<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",
}
);
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>
)}
</div>
{/* ✅ Referenz setzen */}
{selectedId !== null && (
<button
onClick={handleSetReference}
className="border border-littwin-blue text-littwin-blue bg-white rounded px-3 py-1 text-sm hover:bg-blue-100"
>
TDR-Kurve als Referenz speichern
</button>
)}
{/* 🔽 Dropdown für Messungen */}
<div className="flex items-center space-x-2">
<label htmlFor="tdrIdSelect" className="text-sm font-semibold">
TDR Messung
</label>
<select
id="tdrIdSelect"
value={selectedId ?? ""}
onChange={handleSelectChange}
className="border rounded px-2 py-1 text-sm"
disabled={idsForSlot.length === 0}
>
<option value="">-- Wähle Messung --</option>
{idsForSlot.map((entry) => (
<option key={entry.id} value={entry.id}>
{new Date(entry.t).toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})}{" "}
Fehlerstelle: {entry.d} m
</option>
))}
</select>
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,192 @@
"use client"; // TDRChartView.tsx
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, RootState } from "@/redux/store";
import {
setChartOpen,
setFullScreen,
setSlotNumber,
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 Report from "../IsoMeasurementChart/Report";
interface TDRChartViewProps {
isOpen: boolean;
onClose: () => void;
slotIndex: number;
}
const TDRChartView: React.FC<TDRChartViewProps> = ({
isOpen,
onClose,
slotIndex,
}) => {
const dispatch = useDispatch<AppDispatch>();
const { isFullScreen, chartTitle } = useSelector(
(s: RootState) => s.kabelueberwachungChartSlice
);
// Initialize defaults when opening
useEffect(() => {
if (!isOpen) return;
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
dispatch(setActiveMode("TDR"));
dispatch(setSelectedSlot(slotIndex));
dispatch(setSlotNumber(slotIndex));
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
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"));
dispatch(setChartOpen(false));
dispatch(setFullScreen(false));
dispatch(resetBrushRange());
onClose();
};
const toggleFullScreen = () => dispatch(setFullScreen(!isFullScreen));
return (
<ReactModal
isOpen={isOpen}
onRequestClose={handleClose}
ariaHideApp={false}
style={{
overlay: {
backgroundColor: "rgba(0,0,0,0.55)",
backdropFilter: "blur(2px)",
},
content: {
inset: "50% auto auto 50%",
transform: "translate(-50%, -50%)",
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"
>
<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") =>
dispatch(setChartTitle(value))
}
>
<div className="relative w-40">
<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="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-3 py-1.5 cursor-pointer rounded-sm m-0.5 ${
selected
? "dropdown-option-active"
: active
? "dropdown-option-hover"
: ""
}`
}
>
{option}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
</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} />
) : (
<Report moduleType="TDR" />
)}
</div>
</div>
</ReactModal>
);
};
export default TDRChartView;

View File

@@ -1,10 +1,26 @@
"use client"; // components/modules/kue705FO/Kue705FO.tsx
import React, { useState, useRef, useMemo } from "react";
import React, { useState, useMemo, useEffect, useRef } from "react";
import dynamic from "next/dynamic";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Marquee: any = dynamic(() => import("react-fast-marquee"), {
ssr: false,
});
import { useSelector } from "react-redux";
import KueModal from "./modals/SettingsModalWrapper";
// import FallSensors from "../../fall-detection-sensors/FallSensors";
import "bootstrap-icons/font/bootstrap-icons.css"; // Import Bootstrap Icons
import { Kue705FOProps } from "../../../../types/Kue705FOProps";
import ChartSwitcher from "./Charts/ChartSwitcher";
// Import the new specialized ChartView components
import IsoChartView from "./Charts/IsoMeasurementChart/IsoChartView";
import LoopChartView from "./Charts/LoopMeasurementChart/LoopChartView";
import TDRChartView from "./Charts/TDRChart/TDRChartView";
import KVZChartView from "./Charts/KVZChart/KVZChartView";
import SlotActivityOverlay from "./SlotActivityOverlay";
// Keep ChartSwitcher import for backwards compatibility if needed
// import ChartSwitcher from "./Charts/ChartSwitcher";
// Remove separate chart imports since we use ChartView components
// import IsoMeasurementChart from "./Charts/IsoMeasurementChart/IsoMeasurementChart";
// import LoopMeasurementChart from "./Charts/LoopMeasurementChart/LoopMeasurementChart";
//-------Redux Toolkit--------
import { RootState } from "../../../../redux/store";
import { useDispatch } from "react-redux";
@@ -16,15 +32,14 @@ import useIsoDisplay from "./hooks/useIsoDisplay";
import useLoopDisplay from "./hooks/useLoopDisplay";
import useModulName from "./hooks/useModulName";
import type { Chart } from "chart.js";
//--------handlers----------------
import handleButtonClick from "./kue705FO-Funktionen/handleButtonClick";
// Keep needed imports
import handleOpenModal from "./handlers/handleOpenModal";
import handleCloseModal from "./handlers/handleCloseModal";
import handleOpenChartModal from "./handlers/handleOpenChartModal";
import handleCloseChartModal from "./handlers/handleCloseChartModal";
import handleRefreshClick from "./handlers/handleRefreshClick";
// Remove unused chart modal handlers since we use direct ChartView components
// import handleOpenChartModal from "./handlers/handleOpenChartModal";
// import handleCloseChartModal from "./handlers/handleCloseChartModal";
// import handleRefreshClick from "./handlers/handleRefreshClick";
const Kue705FO: React.FC<Kue705FOProps> = ({
isolationswert,
@@ -32,7 +47,6 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
modulName,
kueOnline,
slotIndex,
tdrLocation,
}) => {
/* console.log(
`Rendering Kue705FO - SlotIndex: ${slotIndex}, ModulName: ${modulName}`
@@ -41,35 +55,57 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
const dispatch = useDispatch();
const { kueName } = useSelector((state: RootState) => state.kueDataSlice);
const [activeButton, setActiveButton] = useState<"Schleife" | "TDR">(
// Admin authentication hook for security - using showModal as true for continuous auth check
// Admin Auth hook retained (result not currently needed after KVZ visibility change)
// const { isAdminLoggedIn } = useAdminAuth(true);
// Modulname (max 48 Zeichen) vorbereiten
const moduleNameRaw = useMemo(
() => kueName?.[slotIndex] || `Modul ${slotIndex + 1}`,
[kueName, slotIndex]
);
const moduleName48 = useMemo(
() =>
typeof moduleNameRaw === "string"
? moduleNameRaw.slice(0, 48)
: String(moduleNameRaw),
[moduleNameRaw]
);
const [activeButton, setActiveButton] = useState<"Schleife" | "TDR" | "ISO">(
"Schleife"
);
const [loopTitleText, setloopTitleText] = useState(
"Schleifenwiderstand [kOhm]"
);
const [, setloopTitleText] = useState("Schleifenwiderstand [kOhm]");
const [isoDisplayText] = useState("Aderbruch");
const [groundFaultDisplayText] = useState("Erdschluss");
const [loopFaultDisplayText] = useState("Schleifenfehler");
const [isoFaultDisplayText] = useState("Isolationsfehler");
const [isoGreaterThan200] = useState(">200 MOhm");
const [loading, setLoading] = useState(false);
const [showModal, setShowModal] = useState(false);
const [showChartModal, setShowChartModal] = useState(false);
// Separate modal states for each ChartView
const [showIsoModal, setShowIsoModal] = useState(false);
const [showRslModal, setShowRslModal] = useState(false);
const [showTdrModal, setShowTdrModal] = useState(false);
const [showKvzModal, setShowKvzModal] = useState(false);
// Keep original showChartModal for backwards compatibility if needed
// const [showChartModal, setShowChartModal] = useState(false);
// Removed unused loopMeasurementCurveChartData state
//------- Redux-Variablen abrufen--------------------------------
const {
kueVersion: reduxKueVersion,
tdrActive,
kueCableBreak: kueCableBreakRaw,
kueGroundFault: kueGroundFaultRaw,
kueAlarm1: kueAlarm1Raw,
kueAlarm2: kueAlarm2Raw,
kueOverflow: kueOverflowRaw,
kuePSTmMinus96V, // <- richtig, weil so im State vorhanden
tdrActive, // <- TDR aktiv Status hinzugefügt
kvzPresence, // <- KVz Presence Array hinzugefügt
kvzActive, // <- KVz Active Array hinzugefügt
// kvzStatus, // <- KVz LED Status Array (jetzt nur im KVZ Modal verwendet)
} = useSelector((state: RootState) => state.kueDataSlice);
//---------------------------------------------
@@ -96,15 +132,59 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
//-------------------------handlers-------------------------
const openModal = () => handleOpenModal(setShowModal);
const closeModal = () => handleCloseModal(setShowModal);
const openChartModal = () =>
handleOpenChartModal(setShowChartModal, dispatch, slotIndex, activeButton);
const refreshClick = () =>
handleRefreshClick(activeButton, slotIndex, setLoading);
// Create a ref for the chart instance to pass as the second argument
const chartInstance = useRef<Chart | null>(null);
const closeChartModal = () =>
handleCloseChartModal(setShowChartModal, chartInstance);
// New ChartView handlers - direct modal opening
const openIsoModal = () => {
setActiveButton("ISO");
// Set Redux state for ISO type
dispatch({
type: "kabelueberwachungChart/setSelectedSlotType",
payload: 1, // 1 = Isolationswiderstand
});
dispatch({
type: "kabelueberwachungChart/setSlotNumber",
payload: slotIndex,
});
setShowIsoModal(true);
};
const closeIsoModal = () => {
setShowIsoModal(false);
};
const openRslModal = () => {
setActiveButton("Schleife");
setloopTitleText("Schleifenwiderstand [kOhm]");
setLoopDisplayValue(Number(schleifenwiderstand));
dispatch({
type: "kabelueberwachungChart/setSelectedSlotType",
payload: 2,
}); // RSL type
dispatch({
type: "kabelueberwachungChart/setSlotNumber",
payload: slotIndex,
});
setShowRslModal(true);
};
const closeRslModal = () => {
setShowRslModal(false);
};
const openTdrModal = () => {
setActiveButton("TDR");
setloopTitleText("Entfernung [km]");
setShowTdrModal(true);
};
const closeTdrModal = () => {
setShowTdrModal(false);
};
const openKvzModal = () => {
setShowKvzModal(true);
};
const closeKvzModal = () => setShowKvzModal(false);
//----------------------------------
//hooks einbinden
const kueVersion = useKueVersion(slotIndex, reduxKueVersion);
@@ -132,45 +212,87 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
);
const { setCurrentModulName } = useModulName(slotIndex, modulName);
//---------------------------------
//---------------------------------
const tdmChartData = useSelector(
(state: RootState) => state.tdmChartSlice.data
// Version-gate für Laufschrift: erst ab V4.30 aktiv
const parseVersion = (v?: string): [number, number, number] => {
if (!v) return [0, 0, 0];
const m = String(v).match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
if (!m) return [0, 0, 0];
const major = parseInt(m[1] || "0", 10) || 0;
const minor = parseInt(m[2] || "0", 10) || 0;
const patch = parseInt(m[3] || "0", 10) || 0;
return [major, minor, patch];
};
const gte = (a: [number, number, number], b: [number, number, number]) => {
if (a[0] !== b[0]) return a[0] > b[0];
if (a[1] !== b[1]) return a[1] > b[1];
return a[2] >= b[2];
};
const marqueeOverride =
process.env.NEXT_PUBLIC_ENABLE_KUE_MARQUEE === "1" ||
process.env.NEXT_PUBLIC_ENABLE_KUE_MARQUEE === "true";
const scrollFeatureEnabled = useMemo(
() => marqueeOverride || gte(parseVersion(kueVersion), [4, 30, 0]),
[kueVersion, marqueeOverride]
);
const latestTdrDistanceMeters =
Array.isArray(tdmChartData?.[slotIndex]) &&
tdmChartData[slotIndex].length > 0 &&
typeof tdmChartData[slotIndex][0].d === "number"
? tdmChartData[slotIndex][0].d
: 0;
const latestTdrDistance = Number((latestTdrDistanceMeters / 1000).toFixed(3));
//setLoopDisplayValue(latestTdrDistance);
// Überlängen-Erkennung für Laufschrift
const nameContainerRef = useRef<HTMLDivElement | null>(null);
const measureTextRef = useRef<HTMLSpanElement | null>(null);
const [shouldScroll, setShouldScroll] = useState(false);
useEffect(() => {
const measure = () => {
if (!scrollFeatureEnabled) {
setShouldScroll(false);
return;
}
const container = nameContainerRef.current;
const text = measureTextRef.current;
if (!container || !text) {
setShouldScroll(false);
return;
}
const needs = text.scrollWidth > container.clientWidth + 2;
setShouldScroll(needs);
};
measure();
window.addEventListener("resize", measure);
return () => window.removeEventListener("resize", measure);
}, [moduleName48, scrollFeatureEnabled]);
//---------------------------------
// TDR Distanz wird im Display nicht angezeigt Daten für Modal werden separat geladen
//---------------------------------
const loopValue =
activeButton === "TDR"
? latestTdrDistance
: typeof schleifenwiderstand === "number"
const rslValue =
typeof schleifenwiderstand === "number"
? schleifenwiderstand
: Number(schleifenwiderstand);
const { loopDisplayValue, setLoopDisplayValue } = useLoopDisplay(
loopValue,
rslValue,
activeButton
);
// TDR aktiv Status für diesen Slot prüfen
const isTdrActiveForSlot = tdrActive?.[slotIndex] === 1;
// KVz aktiv Status für diesen Slot prüfen - nur wenn Admin authentifiziert ist, KVz vorhanden ist UND aktiviert ist
// 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;
// Removed useChartData(loopMeasurementCurveChartData) as the state was unused
//---------------------------------
return (
<div
className="relative bg-gray-300 w-[7.25rem] h-[24.375rem] border border-gray-400 transform laptop:-translate-y-12 2xl:-translate-y-0
scale-100 sm:scale-95 md:scale-100 lg:scale-105 xl:scale-90 2xl:scale-125 top-3 qhd:scale-150 qhd:-translate-y-0
"
className="relative bg-gray-300 w-[7.25rem] h-[23.375rem] border border-gray-400 transform laptop:-translate-y-12 2xl:-translate-y-0
scale-100 sm:scale-95 md:scale-100 lg:scale-105 xl:scale-90 2xl:scale-125 top-3 qhd:scale-150 qhd:-translate-y-0"
>
{/* Per-slot activity overlay */}
<SlotActivityOverlay slotIndex={slotIndex} />
{kueOnline === 1 ? (
<>
<div className="relative w-[7.075rem] h-[15.156rem] bg-littwin-blue border-[0.094rem] border-gray-400 z-0">
@@ -220,144 +342,227 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
</div>
</div>
{/* Anzeige des Isolation */}
<div className="relative mt-[3.125rem] mx-auto bg-black text-white w-[6.25rem] h-[2.5rem] flex items-center justify-center text-[1.125rem] z-10">
<div className="text-center">
{/* Schwarzes Display mit drei Zeilen: Alarm, ISO, Schleife */}
<div className="relative mt-[3.125rem] mx-auto bg-black text-white w-[6.8rem] h-[3.1rem] flex flex-col items-center justify-between z-10 p-1">
<div className="text-center w-full flex flex-col justify-between items-center h-full">
{/* 1. Zeile: Alarmtext in Rot, sonst "Status: OK" */}
<span
className={
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
Number(kuePSTmMinus96V?.[slotIndex]) === 1 ||
Number(kueCableBreak?.[slotIndex]) === 1 ||
Number(kueGroundFault?.[slotIndex]) === 1 ||
Number(kueAlarm1?.[slotIndex]) === 1 ||
Number(kueAlarm2?.[slotIndex]) === 1
? "text-red-500 text-[0.875rem]"
: Number(kueOverflow?.[slotIndex]) === 1
? "text-white text-[0.875rem]"
: ""
}
? "text-red-500"
: "text-green-500"
}`}
>
{isoDisplayValue}
{Number(kuePSTmMinus96V?.[slotIndex]) === 1
? "Messpannung"
: Number(kueCableBreak?.[slotIndex]) === 1
? "Aderbruch"
: Number(kueGroundFault?.[slotIndex]) === 1
? "Erdschluss"
: Number(kueAlarm1?.[slotIndex]) === 1
? "Isolationsfehler"
: Number(kueAlarm2?.[slotIndex]) === 1
? "Schleifenfehler"
: " "}
{"\u00A0"}
{/* Status: OK*/}
</span>
{/* 2. Zeile: ISO-Wert, immer anzeigen */}
<span
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
Number(kueAlarm1?.[slotIndex]) === 1 ? "text-red-500" : ""
}`}
>
{isoDisplayValue === "Abgleich"
? "ISO: Abgleich"
: `ISO: ${Number(isolationswert)
.toFixed(2)
.replace(".", ",")} MOhm`}
</span>
{/* 3. Zeile: Schleifenwert (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" : ""
}`}
>
{`RSL: ${Number(loopDisplayValue)
.toFixed(3)
.replace(".", ",")} kOhm`}
</span>
{Number(kuePSTmMinus96V?.[slotIndex]) !== 1 &&
Number(kueCableBreak?.[slotIndex]) !== 1 &&
Number(kueGroundFault?.[slotIndex]) !== 1 &&
Number(kueAlarm1?.[slotIndex]) !== 1 &&
Number(kueAlarm2?.[slotIndex]) !== 1 &&
Number(kueOverflow?.[slotIndex]) !== 1 && (
<div className="text-[0.5rem]">ISO MOhm</div>
)}
</div>
</div>
<div className="absolute top-0 left-[4.688rem] w-[0.188rem] h-full bg-white z-0"></div>
<div className="absolute top-[2.5rem] left-[4.688rem] w-[2.5rem] h-[0.188rem] bg-white z-0"></div>
<div className="absolute bottom-[1.25rem] left-0 right-0 text-black text-[0.625rem] bg-gray-300 p-[0.063rem] text-center">
{kueName?.[slotIndex] || `Modul ${slotIndex + 1}`}
{/* Hidden measuring span for overflow detection (kept measurable) */}
<span
ref={measureTextRef}
style={{
position: "absolute",
left: -9999,
top: -9999,
visibility: "hidden",
whiteSpace: "nowrap",
}}
>
{moduleName48}
</span>
<div
ref={nameContainerRef}
className="absolute bottom-[1.25rem] left-0 right-0 text-black text-[0.625rem] bg-gray-300 p-[0.063rem] overflow-hidden"
>
{shouldScroll && scrollFeatureEnabled ? (
<Marquee pauseOnHover gradient={false} speed={40}>
<span className="pr-8 whitespace-nowrap" title={moduleName48}>
{moduleName48}
</span>
</Marquee>
) : (
<span
className="block text-center whitespace-nowrap"
title={moduleName48}
>
{moduleName48}
</span>
)}
</div>
<div className="absolute bottom-[0.063rem] right-[0.063rem] text-black text-[0.5rem]">
{kueVersion}
</div>
</div>
<div className="flex mt-3 space-x-[0.063rem] ">
<button
onClick={() =>
handleButtonClick(
"Schleife",
setActiveButton,
setloopTitleText,
(value) =>
setLoopDisplayValue(
typeof value === "number" ? value : Number(value)
), // Hier sicherstellen, dass nur number übergeben wird
Number(schleifenwiderstand), // <- Stelle sicher, dass es eine Zahl ist
tdrLocation,
dispatch,
slotIndex
)
}
className={`w-[50%] h-[1.563rem] text-white text-[0.625rem] flex items-center justify-center ${
activeButton === "Schleife" ? "bg-littwin-blue" : "bg-gray-400"
}`}
>
Schleife
</button>
<button
onClick={() => {
setActiveButton("TDR");
setloopTitleText("Entfernung [Km]");
const latestTdrDistanceMeters =
Array.isArray(tdmChartData?.[slotIndex]) &&
tdmChartData[slotIndex].length > 0 &&
typeof tdmChartData[slotIndex][0].d === "number"
? tdmChartData[slotIndex][0].d
: 0;
const latestTdrDistance = Number(
(latestTdrDistanceMeters / 1000).toFixed(3)
);
setLoopDisplayValue(latestTdrDistance);
}}
className={`w-[50%] h-[1.563rem] text-white text-[0.625rem] flex items-center justify-center ${
Array.isArray(tdrActive) && tdrActive[slotIndex] === 0
? "bg-gray-200 cursor-not-allowed" // Deaktiviert: Hellgrau
: activeButton === "TDR"
? "bg-littwin-blue" // Aktiviert: Littwin Blau
: "bg-gray-400" // Nicht geklickt: Dunkelgrau
}`}
disabled={Array.isArray(tdrActive) && tdrActive[slotIndex] === 0} // Button deaktiviert, wenn TDR für diesen Slot nicht aktiv ist
>
TDR
</button>
</div>
{/* loopDisplay: Zeigt Schleifenwiderstand oder TDR-Distanz an, je nach Modus */}
<div className="absolute bottom-[0.063rem] left-[0.068rem] w-[7.074rem] h-[6.1rem] bg-gray-300 border-[0.094rem] border-gray-400 p-[0.063rem]">
<span className="text-black text-[0.438rem] absolute top-[0.125rem] left-[0.063rem] mt-1">
{loopTitleText}
{/* Modal für Einstellungen */}
</>
) : (
<div className="flex items-center justify-center h-full text-gray-500">
{/* Das soll rausgenommen werden
<p>Kein Modul im Slot {slotIndex + 0}</p>
*/}
</div>
)}
{/* Messkurven-Button unter dem Modul */}
{kueOnline === 1 && (
<>
{/*
Überschrift: Detailansicht
ISO und RSL als Buttons (Firmenblau) nebeneinander
TDR und KVz Buttons (Firmenblau) nebeneinander
Wenn kein TDR oder kein KVz: nur grauer Button ohne Text
*/}
<div className="flex flex-col items-center w-full px-2 mt-2 space-y-2">
{/* Detailansicht Header */}
<span className="text-black text-[0.625rem] font-semibold">
Detailansicht
</span>
<div className="relative w-full h-[2.813rem] bg-gray-100 border border-gray-400 flex items-center justify-center mt-4">
{/* ISO and RSL Buttons */}
<div className="flex space-x-2">
<button
onClick={refreshClick} // Dynamische Funktion basierend auf aktivem Button
className="absolute -top-[0.063rem] -right-[0.063rem] w-[1.25rem] h-[1.25rem] bg-gray-400 flex items-center justify-center"
disabled={loading} // Disable button while loading
onClick={openIsoModal}
className="bg-littwin-blue text-white text-[0.625rem] flex items-center justify-center p-2 min-w-[2.5rem]"
>
<span className="text-white text-[1.125rem]"></span>
ISO
</button>
<button
onClick={openRslModal}
className="bg-littwin-blue text-white text-[0.625rem] flex items-center justify-center p-2 min-w-[2.5rem]"
>
RSL
</button>
<div className="absolute bottom-[0.313rem] left-1/2 transform -translate-x-1/2 w-[6.25rem] flex justify-center items-center">
<div className="text-center text-black text-[0.625rem]">
<p>
{loopDisplayValue +
(activeButton === "Schleife" ? " KOhm" : " Km")}
</p>
</div>
</div>
</div>
<button
onClick={openChartModal} // Öffnet das Chart-Modal
className="w-full h-[1.563rem] bg-littwin-blue text-white text-[0.625rem] flex items-center justify-center mt-[0.063rem]"
>
Messkurve
</button>
{/* TDR and KVz Buttons */}
<div className="flex space-x-2 p-1">
{/* TDR Button - blau mit Text wenn aktiv, grau ohne Text wenn inaktiv */}
<button
onClick={isTdrActiveForSlot ? openTdrModal : undefined}
className={`${
isTdrActiveForSlot
? "bg-littwin-blue text-white cursor-pointer"
: "bg-gray-400 cursor-default"
} text-[0.625rem] flex items-center justify-center p-2 min-w-[2.5rem]`}
>
{isTdrActiveForSlot ? "TDR" : "\u00A0\u00A0\u00A0"}
</button>
{/* KVz Button - blau mit Text wenn aktiv, grau ohne Text wenn inaktiv */}
<button
onClick={isKvzActiveForSlot ? openKvzModal : undefined}
className={`${
isKvzActiveForSlot
? "bg-littwin-blue text-white cursor-pointer"
: "bg-gray-400 cursor-default"
} text-[0.625rem] flex items-center justify-center p-2 min-w-[2.5rem]`}
disabled={!isKvzActiveForSlot}
title={
isKvzActiveForSlot ? "KVZ öffnen" : "KVZ nicht verfügbar"
}
>
{isKvzActiveForSlot ? "KVZ" : "\u00A0\u00A0\u00A0"}
</button>
</div>
{/* Messkurve Button */}
{/* TDR Messkurve und Schleife Messkurve Buttons */}
<div className="flex flex-col space-y-2 w-full"></div>
</div>
{/* Modal für Messkurve */}
{showChartModal && (
<ChartSwitcher
isOpen={showChartModal}
onClose={closeChartModal}
{/* ISO Chart Modal */}
<div className="absolute bottom-0 left-0 right-0 h-[0.125rem] bg-gray-400"></div>
{/* ISO Chart Modal */}
<IsoChartView
isOpen={showIsoModal}
onClose={closeIsoModal}
slotIndex={slotIndex}
/>
{/* RSL Chart Modal */}
<LoopChartView
isOpen={showRslModal}
onClose={closeRslModal}
slotIndex={slotIndex}
/>
{/* TDR Chart Modal - nur wenn TDR aktiv ist */}
{isTdrActiveForSlot && (
<TDRChartView
isOpen={showTdrModal}
onClose={closeTdrModal}
slotIndex={slotIndex}
/>
)}
{isKvzActiveForSlot && (
<KVZChartView
isOpen={showKvzModal}
onClose={closeKvzModal}
slotIndex={slotIndex}
/>
)}
</>
) : (
<div className="flex items-center justify-center h-full text-gray-500">
)}
{/* Früher inline Panel jetzt eigenes Modal (KVZChartView) */}
{/* {showKvzPanel && isKvzActiveForSlot && (
<div className="flex flex-col items-center ">
<FallSensors slotIndex={slotIndex} />
</div>
)} */}
{/* Offline-View */}
{kueOnline !== 1 && (
<div className="flex items-center justify-center ">
{/* Das soll rausgenommen werden
<p>Kein Modul im Slot {slotIndex + 0}</p>
*/}

View File

@@ -0,0 +1,152 @@
"use client";
import React, { useEffect, useState } from "react";
import { useAppSelector } from "@/redux/store";
export default function SlotActivityOverlay({
slotIndex,
}: {
slotIndex: number;
}) {
const ksx = useAppSelector((s) => s.deviceEvents.ksx);
const ksy = useAppSelector((s) => s.deviceEvents.ksy);
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
const 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 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 || comparisonActive;
if (!any) return;
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, [loopActive, tdrActive, comparisonActive]);
const clamp = (v: number, min = 0, max = 1) =>
Math.max(min, Math.min(max, v));
const compute = (startedAt: number | null, durationMs: number) => {
if (!startedAt) return { pct: 0 };
const elapsed = now - startedAt;
const pct = clamp(elapsed / durationMs) * 100;
return { pct };
};
// Durations
const LOOP_MS = 2 * 60 * 1000; // ~2 min
const TDR_MS = 110 * 1000; // ~2 min laut die Eigaben
const ALIGN_MS = 10 * 60 * 1000; // ~10 min
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">
<div className="p-2 rounded-md shadow bg-white/90 border border-gray-200 w-[min(90%,12rem)]">
<div className="text-[0.75rem] font-semibold mb-2 text-gray-800">
Bitte warten
</div>
<div className="space-y-2">
{loopActive && (
<div>
<div className="text-[0.7rem] text-gray-800 mb-1">Schleife</div>
{(() => {
const started = loopStartedAtBySlot[slotIndex] ?? loopStartedAt;
const { pct } = compute(started, LOOP_MS);
return (
<div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
<div
className="h-full bg-littwin-blue transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<div className="text-[0.65rem] text-gray-700 mt-1">
{Math.round(pct)}%
</div>
</div>
);
})()}
</div>
)}
{tdrActive && (
<div>
<div className="text-[0.7rem] text-gray-800 mb-1">TDR</div>
{(() => {
const started = tdrStartedAtBySlot[slotIndex] ?? tdrStartedAt;
const { pct } = compute(started, TDR_MS);
return (
<div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
<div
className="h-full bg-littwin-blue transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<div className="text-[0.65rem] text-gray-700 mt-1">
{Math.round(pct)}%
</div>
</div>
);
})()}
</div>
)}
{comparisonActive && (
<div>
<div className="text-[0.7rem] text-gray-800 mb-1">Abgleich</div>
{(() => {
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">
<div
className="h-full bg-littwin-blue transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<div className="text-[0.65rem] text-gray-700 mt-1">
{Math.round(pct)}%
</div>
</div>
);
})()}
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -14,7 +14,7 @@ const handleOpenChartModal = (
setShowChartModal: Dispatch<SetStateAction<boolean>>,
dispatch: ReturnType<typeof useDispatch>,
slotIndex: number,
activeButton: "Schleife" | "TDR"
activeButton: "Schleife" | "TDR" | "ISO"
) => {
setShowChartModal(true);
dispatch(setChartOpen(true));
@@ -26,6 +26,8 @@ const handleOpenChartModal = (
if (activeButton === "TDR") {
dispatch(setActiveMode("TDR"));
} else if (activeButton === "ISO") {
dispatch(setActiveMode("ISO"));
} else {
dispatch(setActiveMode("Schleife"));
}

View File

@@ -4,7 +4,7 @@ import { goLoop } from "@/utils/goLoop";
import { goTDR } from "@/utils/goTDR";
const handleRefreshClick = (
activeButton: "Schleife" | "TDR",
activeButton: "Schleife" | "TDR" | "ISO",
slotIndex: number,
setLoading: Dispatch<SetStateAction<boolean>>
) => {
@@ -13,6 +13,7 @@ const handleRefreshClick = (
} else if (activeButton === "TDR") {
goTDR(slotIndex, setLoading);
}
// ISO has no refresh functionality
};
export default handleRefreshClick;

View File

@@ -1,18 +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,
activeButton: "Schleife" | "TDR"
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);
}
}, [schleifenwiderstand, activeButton]);
}, [rslValue, activeButton]);
return { loopDisplayValue, setLoopDisplayValue };
};

View File

@@ -2,9 +2,9 @@
// components/main/kabelueberwachung/kue705FO/modals/KueEinstellung.tsx
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "../../../../../redux/store";
import type { RootState, AppDispatch } from "@/redux/store";
import handleSave from "../handlers/handleSave";
import handleDisplayEinschalten from "../handlers/handleDisplayEinschalten";
import handleDisplayEinschalten from "@/components/main/kabelueberwachung/kue705FO/handlers/handleDisplayEinschalten";
import firmwareUpdate from "../handlers/firmwareUpdate";
import ProgressModal from "@/components/main/settingsPageComponents/modals/ProgressModal";
import { toast } from "react-toastify";
@@ -22,18 +22,6 @@ interface Props {
onModulNameChange?: (id: string) => void;
}
const memoryIntervalOptions = [
{ value: 0, label: "Kein" },
{ value: 1, label: "1 Minute" },
{ value: 5, label: "5 Minuten" },
{ value: 10, label: "10 Minuten" },
{ value: 15, label: "15 Minuten" },
{ value: 30, label: "30 Minuten" },
{ value: 60, label: "60 Minuten" },
{ value: 360, label: "6 Stunden" },
{ value: 720, label: "12 Stunden" },
];
export default function KueEinstellung({
slot,
onClose = () => {},
@@ -173,20 +161,23 @@ export default function KueEinstellung({
/>
</div>
{/* Speicherintervall */}
{/* Speicherintervall */}
{/* Speicherintervall */}
<div className="mb-4 grid grid-cols-3 items-center gap-2 w-full">
<label className="">Speicherintervall:</label>
<select
className="w-full border rounded p-1"
value={formData.memoryInterval}
onChange={(e) => handleChange("memoryInterval", e.target.value)}
>
{memoryIntervalOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
<label className="w-48">Speicherintervall:</label>
<div className="relative w-36">
<input
type="number"
className="border rounded px-2 py-1 pr-20 w-full text-right"
value={formData.memoryInterval}
onChange={(e) => handleChange("memoryInterval", e.target.value)}
/>
<span className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
Minuten
</span>
</div>
</div>
{/* Isolationsmessung */}
<div className="mb-4 w-full">
<h3 className="font-bold mb-2">Isolationsmessung</h3>
@@ -251,7 +242,7 @@ export default function KueEinstellung({
</div>
</div>
</div>
<div className="flex justify-end gap-2 p-0 rounded">
<div className="flex flex-wrap justify-end gap-2 p-0 rounded">
{isAdminLoggedIn && (
<>
<button
@@ -260,6 +251,48 @@ export default function KueEinstellung({
>
Firmware Update
</button>
{/* Konfiguration sichern */}
<button
onClick={async () => {
try {
await fetch(
`/CPL?kabelueberwachung.html&KSB${slot
.toString()
.padStart(2, "0")}=1`
);
toast.success("✅ Konfiguration gesichert.");
} catch (err) {
console.error("KSB Fehler", err);
toast.error("❌ Fehler beim Sichern der Konfiguration");
}
}}
className="bg-littwin-blue text-white px-4 py-2 rounded flex items-center"
>
Konfig. sichern
</button>
{/* Konfiguration zurücksichern */}
<button
onClick={async () => {
try {
await fetch(
`/CPL?kabelueberwachung.html&KSR${slot
.toString()
.padStart(2, "0")}=1`
);
toast.success("✅ Konfiguration wiederhergestellt.");
} catch (err) {
console.error("KSR Fehler", err);
toast.error(
"❌ Fehler beim Wiederherstellen der Konfiguration"
);
}
}}
className="bg-littwin-blue text-white px-4 py-2 rounded flex items-center"
>
Konfig. zurücksichern
</button>
</>
)}
{showConfirmModal && (

View File

@@ -0,0 +1,133 @@
"use client";
import React, { useState } from "react";
import { useSelector } from "react-redux";
import { RootState, useAppDispatch } from "../../../../../redux/store";
import { updateKvzData } from "../../../../../redux/thunks/kvzThunks";
import { useAdminAuth } from "../../../settingsPageComponents/hooks/useAdminAuth";
type KvzData = {
// Hier können später weitere KVz-spezifische Einstellungen hinzugefügt werden
kvzSettings: string;
};
interface Props {
slot: number;
onClose?: () => void;
}
export default function KvzModalView({ slot, onClose }: Props) {
const { isAdminLoggedIn } = useAdminAuth(true);
const dispatch = useAppDispatch();
const kvzSlice = useSelector((state: RootState) => state.kueDataSlice);
// KVZ System: 32 Slots mit je 4 LEDs
const isKvzPresent = kvzSlice.kvzPresence?.[slot] === 1;
const isKvzActive = kvzSlice.kvzActive?.[slot] === 1;
// LED Status für diesen Slot (4 LEDs pro Slot)
const getKvzLedStatus = (ledIndex: number) => {
const arrayIndex = slot * 4 + ledIndex;
return kvzSlice.kvzStatus?.[arrayIndex] === 1;
};
const [localKvzActive, setLocalKvzActive] = useState(() => isKvzActive);
// Synchronisiere localState mit Redux State
React.useEffect(() => {
setLocalKvzActive(isKvzActive);
}, [isKvzActive]);
const handleKvzToggle = async () => {
const newState = !localKvzActive;
setLocalKvzActive(newState);
try {
// API Update mit neuem Thunk - kvzActive statt kvzPresence
await dispatch(
updateKvzData([{ key: "kvzActive", slot, value: newState ? 1 : 0 }])
);
const msg = newState
? "✅ KVz wurde aktiviert."
: "⚠️ KVz wurde deaktiviert.";
alert(msg);
location.reload();
} catch (error) {
console.error("Fehler beim KVz-Toggle:", error);
alert("Fehler beim Umschalten der KVz-Funktion.");
// State zurücksetzen bei Fehler
setLocalKvzActive(!newState);
}
};
return (
<div className="p-4 text-sm">
{/* KVz-Funktion - nur anzeigen wenn KVZ vorhanden ist */}
{isAdminLoggedIn && isKvzPresent && (
<div className="mb-4 mt-4 grid grid-cols-3 items-center gap-2 w-full">
<span className="text-sm font-medium">KVz-Funktion:</span>
<div className="col-span-2 flex items-center gap-4">
<button
type="button"
role="switch"
aria-checked={localKvzActive}
onClick={handleKvzToggle}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-200 ${
localKvzActive ? "bg-littwin-blue" : "bg-gray-300"
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform duration-200 ${
localKvzActive ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
<span className="text-sm text-gray-600">
{localKvzActive ? "aktiviert" : "deaktiviert"}
</span>
</div>
</div>
)}
{/* Meldung wenn KVZ nicht vorhanden */}
{!isKvzPresent && (
<div className="mb-4 mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded">
<div className="flex items-center gap-2">
<span className="text-yellow-600"></span>
<p className="text-sm text-yellow-800">
<strong>Kein KVZ-Gerät vorhanden</strong>
<br />
Für Slot {slot + 1} ist kein KVZ-Gerät installiert oder
konfiguriert.
</p>
</div>
</div>
)}
{/* Zukünftige KVz-Einstellungen können hier hinzugefügt werden */}
{!isAdminLoggedIn && (
<div className="mt-6 mb-4">
<div className="mb-4">
<p className="text-sm text-gray-600">
Nur Admin-Benutzer können diese Einstellungen ändern.
</p>
</div>
</div>
)}
{/* Speichern Button */}
{/* <div className="mt-36">
<div className="flex justify-end gap-2 p-3 rounded">
<button
onClick={handleSave}
className="bg-littwin-blue text-white px-4 py-2 rounded flex items-center"
>
Speichern
</button>
</div>
</div> */}
</div>
);
}

View File

@@ -3,6 +3,7 @@ import { useState, useEffect } from "react";
import ReactModal from "react-modal";
import KueEinstellung from "./KueEinstellung";
import TdrEinstellung from "./TdrEinstellung";
import KvzModalView from "./KvzModalView";
import Knotenpunkte from "./Knotenpunkte";
interface KueModalProps {
@@ -14,18 +15,20 @@ interface KueModalProps {
declare global {
interface Window {
__lastKueTab?: "kue" | "tdr" | "knoten";
__lastKueTab?: "kue" | "tdr" | "kvz" | "knoten";
kabelModalOpen?: boolean;
}
}
export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
const [activeTab, setActiveTab] = useState<"kue" | "tdr" | "knoten">(() => {
if (typeof window !== "undefined" && window.__lastKueTab) {
return window.__lastKueTab;
const [activeTab, setActiveTab] = useState<"kue" | "tdr" | "kvz" | "knoten">(
() => {
if (typeof window !== "undefined" && window.__lastKueTab) {
return window.__lastKueTab;
}
return "kue";
}
return "kue";
});
);
useEffect(() => {
if (typeof window !== "undefined") {
@@ -38,64 +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 Steckplatz {slot + 1}
<div className="modal-header">
<h2 className="text-sm font-semibold tracking-wide text-fg">
Einstellungen {slot + 1}
</h2>
<button onClick={onClose} className="text-2xl hover:text-gray-200">
<i className="bi bi-x-circle-fill"></i>
<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}
@@ -107,6 +129,7 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
{activeTab === "tdr" && (
<TdrEinstellung slot={slot} onClose={onClose} />
)}
{activeTab === "kvz" && <KvzModalView slot={slot} onClose={onClose} />}
{activeTab === "knoten" && (
<Knotenpunkte slot={slot} onClose={onClose} />
)}

View File

@@ -12,8 +12,9 @@ declare global {
}
import React, { useState } from "react";
import { useSelector } from "react-redux";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "../../../../../redux/store";
import { setKueData } from "../../../../../redux/slices/kueDataSlice";
import { useAdminAuth } from "../../../settingsPageComponents/hooks/useAdminAuth";
@@ -24,6 +25,7 @@ interface Props {
export default function TdrEinstellung({ slot, onClose }: Props) {
const { isAdminLoggedIn } = useAdminAuth(true);
const dispatch = useDispatch();
const tdrSlice = useSelector((state: RootState) => state.kueDataSlice);
const cacheKey = `slot_${slot}`;
@@ -126,6 +128,11 @@ export default function TdrEinstellung({ slot, onClose }: Props) {
setTdrActive(newState);
updateCache(tdrData, newState);
// Redux State sofort aktualisieren für UI-Update
const updatedTdrActive = [...(tdrSlice.tdrActive || [])];
updatedTdrActive[slot] = newState ? 1 : 0;
dispatch(setKueData({ tdrActive: updatedTdrActive }));
const isDev = window.location.hostname === "localhost";
const slotParam = `KTX${slot}=${newState ? 1 : 0}`;

View File

@@ -28,6 +28,8 @@ const DateRangePickerMeldungen: React.FC<Props> = ({
<div className="flex items-center space-x-2">
<label className="text-sm font-semibold">Von</label>
<DatePicker
portalId="root-portal" // beliebige ID
popperClassName="custom-datepicker-popper"
selected={parseISO(fromDate)}
onChange={(date) => date && setFromDate(formatDate(date))}
selectsStart
@@ -42,6 +44,8 @@ const DateRangePickerMeldungen: React.FC<Props> = ({
<div className="flex items-center space-x-2">
<label className="text-sm font-semibold">Bis</label>
<DatePicker
portalId="root-portal" // beliebige ID
popperClassName="custom-datepicker-popper"
selected={parseISO(toDate)}
onChange={(date) => date && setToDate(formatDate(date))}
selectsEnd

View File

@@ -0,0 +1,79 @@
type Meldung = {
t: string;
s: number;
c: string;
m: string;
i: string;
v: string;
};
export default function MeldungenTabelle({
messages,
}: {
messages: Meldung[];
}) {
return (
<div className="overflow-auto max-h-[80vh]">
<table className="min-w-full border 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 border-base bg-surface-alt text-fg font-medium">
Prio
</th>
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
Zeitstempel
</th>
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
Quelle
</th>
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
Meldung
</th>
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
Status
</th>
</tr>
</thead>
<tbody>
{messages.map((msg, index) => (
<tr
key={index}
className="transition-colors hover:bg-surface-alt/60"
>
<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 border-base p-2 bg-surface text-fg">
{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 border-base p-2 bg-surface text-fg">
{msg.i}
</td>
<td className="border border-base p-2 bg-surface text-fg">
{msg.m}
</td>
<td className="border border-base p-2 bg-surface text-fg">
{msg.v}
</td>
</tr>
))}
</tbody>
</table>
{messages.length === 0 && (
<div className="mt-4 text-center text-fg-muted italic">
Keine Meldungen im gewählten Zeitraum vorhanden.
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,114 @@
"use client";
// components/main/reports/MeldungenView.tsx
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getMessagesThunk } from "@/redux/thunks/getMessagesThunk";
import type { AppDispatch } from "@/redux/store";
import type { RootState } from "@/redux/store";
import DateRangePickerMeldungen from "./DateRangePickerMeldungen";
import MeldungenTabelle from "./MeldungenTabelle";
import { Listbox } from "@headlessui/react";
type Meldung = {
t: string;
s: number;
c: string;
m: string;
i: string;
v: string;
};
export default function MeldungenView() {
const dispatch = useDispatch<AppDispatch>();
const messages = useSelector((state: RootState) => state.messages.data);
const [sourceFilter, setSourceFilter] = useState("Alle Quellen");
const today = new Date();
const prior30 = new Date();
prior30.setDate(today.getDate() - 30);
const formatDate = (d: Date) => d.toISOString().split("T")[0];
const [fromDate, setFromDate] = useState<string>(formatDate(prior30));
const [toDate, setToDate] = useState<string>(formatDate(today));
useEffect(() => {
dispatch(getMessagesThunk({ fromDate, toDate }));
}, []);
const filteredMessages =
sourceFilter === "Alle Quellen"
? messages
: messages.filter((m: Meldung) => m.i === sourceFilter);
const allSources = Array.from(
new Set(messages.map((m: Meldung) => m.i))
).sort();
const sources = ["Alle Quellen", ...allSources];
return (
<div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)]">
<h1 className="text-xl font-bold mb-4">Berichte</h1>
<div className="flex flex-wrap gap-6 mb-6 items-center">
<DateRangePickerMeldungen
fromDate={fromDate}
toDate={toDate}
setFromDate={setFromDate}
setToDate={setToDate}
/>
<button
onClick={() => dispatch(getMessagesThunk({ fromDate, toDate }))}
className="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-[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"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="bg-[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-[var(--color-fg)] transition-colors ${
disabled
? "opacity-50 text-[var(--color-muted)] cursor-not-allowed"
: selected
? "bg-accent text-white"
: active
? "bg-[var(--color-surface-alt)]"
: ""
}`
}
>
{src}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
<MeldungenTabelle messages={filteredMessages} />
</div>
);
}

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 max-w-5xl mr-auto rounded shadow">
<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 max-w-5xl mr-auto overflow-y-auto max-h-[calc(100vh-200px)] ">
<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 rounded h-8 p-1 w-full text-xs"
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 rounded h-8 p-1 w-full text-xs"
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 rounded h-8 p-1 w-full text-xs"
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 rounded h-8 p-1 w-full text-xs"
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 rounded h-8 p-1 w-full text-xs"
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 rounded h-8 p-1 w-full text-xs"
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 max-w-5xl mr-auto">
<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 rounded h-8 p-1 w-full text-xs"
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 rounded h-8 p-1 w-full text-xs"
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 rounded h-8 p-1 w-full text-xs"
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,18 +65,19 @@ const NTPSettings: React.FC = () => {
<label className="block text-xs font-medium">Zeitzone</label>
<input
type="text"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
className="border border-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)}
/>
</div>
<div className="col-span-2 flex items-center gap-2 mt-2">
<label className="text-xs font-medium">NTP aktiv:</label>
<label className="text-xs font-medium ">NTP aktiv:</label>
<input
type="checkbox"
checked={active}
onChange={(e) => setActive(e.target.checked)}
className="accent-littwin-blue w-4 h-4"
/>
</div>

View File

@@ -11,6 +11,9 @@ export default function OPCUAInterfaceSettings() {
(state: RootState) => state.opcuaSettingsSlice
);
// Anzahl der aktuellen OPC-Clients (Mock, bis Backend liefert)
const opcUaActiveClientCount = opcuaSettings.opcUaActiveClientCount ?? 3; // 3 als Beispielwert
// Lokale Zustände für das neue Benutzerformular
const [nodesetName, setNodesetName] = useState(
@@ -18,7 +21,7 @@ export default function OPCUAInterfaceSettings() {
);
return (
<div className="p-6 md:p-3 bg-gray-100 max-w-5xl mr-auto ">
<div className="p-6 md:p-3 bg-[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"
@@ -41,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>
@@ -59,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>
@@ -71,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 rounded-md bg-white text-sm">
<div className="p-1 border border-base rounded-md bg-[var(--color-surface)] text-sm text-[var(--color-fg)]">
{opcuaSettings.opcUaZustand}
</div>
</div>
@@ -82,22 +87,32 @@ export default function OPCUAInterfaceSettings() {
<div className="flex">
<input
type="text"
className="flex-grow p-1 border border-gray-300 rounded-l-md text-sm"
className="flex-grow p-1 border border-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
/>
{/*
<button
onClick={handleNodesetUpdate}
className="px-3 py-1 bg-littwin-blue text-white rounded-r-md text-sm"
>
Übernehmen
</button>
<button
onClick={handleNodesetUpdate}
className="px-3 py-1 bg-littwin-blue text-white rounded-r-md text-sm"
>
Übernehmen
</button>
*/}
</div>
</div>
{/* ✅ Anzahl der aktuellen OPC-Clients */}
<div className="mb-3">
<label className="block font-medium text-sm mb-1">
Aktuelle OPC-Clients
</label>
<div className="p-1 border border-base rounded-md bg-[var(--color-surface)] text-sm text-[var(--color-fg)]">
{opcUaActiveClientCount}
</div>
</div>
{/* ✅ Benutzerverwaltung */}
{/*
@@ -107,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>
@@ -127,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

@@ -0,0 +1,85 @@
// components/main/settingsPageComponents/SettingsView.tsx
"use client";
import React, { useEffect, useState } from "react";
import { useAppDispatch } from "@/redux/store";
import { getSystemSettingsThunk } from "@/redux/thunks/getSystemSettingsThunk";
import GeneralSettings from "./GeneralSettings";
import OPCUAInterfaceSettings from "./OPCUAInterfaceSettings";
import DatabaseSettings from "./DatabaseSettings";
import NTPSettings from "./NTPSettings";
import UserManagementSettings from "./UserManagementSettings";
export default function SettingsView() {
const [activeTab, setActiveTab] = useState("tab1");
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(getSystemSettingsThunk());
}, [dispatch]);
return (
<div className="p-4">
<div className="flex border-b border-gray-200">
<button
className={`px-4 py-2 ${
activeTab === "tab1"
? "border-b-2 border-littwin-blue text-littwin-blue"
: ""
}`}
onClick={() => setActiveTab("tab1")}
>
Allgemeine Einstellungen
</button>
<button
className={`px-4 py-2 ${
activeTab === "tab2"
? "border-b-2 border-littwin-blue text-littwin-blue"
: ""
}`}
onClick={() => setActiveTab("tab2")}
>
OPCUA
</button>
<button
className={`px-4 py-2 ${
activeTab === "tab3"
? "border-b-2 border-littwin-blue text-littwin-blue"
: ""
}`}
onClick={() => setActiveTab("tab3")}
>
Datenbank
</button>
<button
className={`px-4 py-2 ${
activeTab === "tab4"
? "border-b-2 border-littwin-blue text-littwin-blue"
: ""
}`}
onClick={() => setActiveTab("tab4")}
>
NTP
</button>
<button
className={`px-4 py-2 ${
activeTab === "tab5"
? "border-b-2 border-littwin-blue text-littwin-blue"
: ""
}`}
onClick={() => setActiveTab("tab5")}
>
Benutzerverwaltung
</button>
</div>
<div className="mt-4">
{activeTab === "tab1" && <GeneralSettings />}
{activeTab === "tab2" && <OPCUAInterfaceSettings />}
{activeTab === "tab3" && <DatabaseSettings />}
{activeTab === "tab4" && <NTPSettings />}
{activeTab === "tab5" && <UserManagementSettings />}
</div>
</div>
);
}

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);
@@ -31,8 +37,14 @@ const UserManagementSettings: React.FC = () => {
);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
handleLogin();
}
};
return (
<div className="p-6 md:p-3 bg-gray-100 max-w-5xl mr-auto">
<div className="p-6 md:p-3 bg-[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 */}
@@ -40,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>
@@ -51,20 +70,22 @@ const UserManagementSettings: React.FC = () => {
<input
type="text"
placeholder="Benutzername"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
className="border border-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}
/>
<input
type="password"
placeholder="Passwort"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs"
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
@@ -75,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

@@ -29,6 +29,17 @@ export function useAdminAuth(showModal: boolean) {
function logoutAdmin() {
sessionStorage.removeItem("token");
localStorage.setItem("isAdminLoggedIn", "false");
// KVz localStorage-Werte löschen für alle Slots
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith("kvz_slot_")) {
keysToRemove.push(key);
}
}
keysToRemove.forEach((key) => localStorage.removeItem(key));
setAdminLoggedIn(false);
}

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 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
@@ -24,9 +24,11 @@ const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
Firmwareupdate läuft ...
{typeof slot === "number" ? ` ` : ""}
</h2>
<div className="w-full bg-gray-200 rounded-full h-4">
Bitte Fenster nicht schließen
<h2></h2>
<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-blue-500 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

@@ -1,9 +1,133 @@
// components/main/system/DetailModal.tsx
"use client";
import React from "react";
// /components/main/system/DetailModal.tsx
import React, { useEffect, useRef, useState, useCallback } from "react";
import { Line } from "react-chartjs-2";
import { useSelector } from "react-redux";
import { RootState } from "@/redux/store";
import { RootState, useAppDispatch } from "@/redux/store";
import { setFullScreen } from "@/redux/slices/kabelueberwachungChartSlice";
import { resetDateRange } from "@/redux/slices/dateRangePickerSlice";
// Import Thunks
import SystemChartActionBar from "@/components/main/system/SystemChartActionBar";
import { getSystemspannung5VplusThunk } from "@/redux/thunks/getSystemspannung5VplusThunk";
import { getSystemspannung15VplusThunk } from "@/redux/thunks/getSystemspannung15VplusThunk";
import { getSystemspannung15VminusThunk } from "@/redux/thunks/getSystemspannung15VminusThunk";
import { getSystemspannung98VminusThunk } from "@/redux/thunks/getSystemspannung98VminusThunk";
import { getTemperaturAdWandlerThunk } from "@/redux/thunks/getTemperaturAdWandlerThunk";
import { getTemperaturProzessorThunk } from "@/redux/thunks/getTemperaturProzessorThunk";
import {
Chart as ChartJS,
LineElement,
PointElement,
CategoryScale,
LinearScale,
Title,
Tooltip,
Legend,
Filler,
TimeScale,
type ChartDataset,
type ChartOptions,
type ChartData,
type Chart,
} from "chart.js";
import "chartjs-adapter-date-fns";
import { de } from "date-fns/locale";
ChartJS.register(
LineElement,
PointElement,
CategoryScale,
LinearScale,
Title,
Tooltip,
Legend,
Filler,
TimeScale
);
// Tailwind-basierte Farbdefinitionen für Chart.js
const chartColors = {
gray: {
line: "#6B7280", // tailwind gray-500
background: "rgba(107, 114, 128, 0.2)", // tailwind gray-500 mit opacity
},
littwinBlue: {
line: "#00AEEF", // littwin-blue
background: "rgba(0, 174, 239, 0.2)", // littwin-blue mit opacity
},
};
type ReduxDataEntry = {
//Alle DIA0 t,m,i,a , DIA1 und DIA2 t,i,a,g
t: string; // Zeitstempel
i: number; // Minimum
a: number; // Maximum
g?: number; // Durchschnitt (optional, falls vorhanden)
m?: number; // aktueller Messwert (optional, falls vorhanden)
};
const chartOptions: ChartOptions<"line"> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: "top" as const },
title: {
display: true,
text: "Verlauf",
},
tooltip: {
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")}`;
},
},
},
zoom: {
pan: { enabled: true, mode: "x" as const },
zoom: {
wheel: { enabled: true },
pinch: { enabled: true },
mode: "x" as const,
},
},
},
scales: {
x: {
type: "time" as const,
time: {
unit: "day" as const,
tooltipFormat: "dd.MM.yyyy HH:mm",
displayFormats: {
day: "dd.MM.yyyy",
},
},
adapters: {
date: { locale: de },
},
title: {
display: true,
text: "Zeit",
},
},
y: {
title: {
display: true,
text: "Messwert",
},
},
},
};
type Props = {
isOpen: boolean;
@@ -13,8 +137,6 @@ type Props = {
setZeitraum: (typ: "DIA0" | "DIA1" | "DIA2") => void;
};
type ReduxDataEntry = { t: string; i: number };
export const DetailModal = ({
isOpen,
selectedKey,
@@ -22,94 +144,373 @@ export const DetailModal = ({
zeitraum,
setZeitraum,
}: Props) => {
const reduxData: ReduxDataEntry[] = useSelector((state: RootState) => {
// 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<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 (derzeit nicht benötigt)
const reduxData = useSelector((state: RootState) => {
switch (selectedKey) {
case "+5V":
return state.systemspannung5Vplus[zeitraum] as ReduxDataEntry[];
return state.systemspannung5Vplus[zeitraum];
case "+15V":
return state.systemspannung15Vplus[zeitraum] as ReduxDataEntry[];
return state.systemspannung15Vplus[zeitraum];
case "-15V":
return state.systemspannung15Vminus[zeitraum] as ReduxDataEntry[];
case "-98V":
return state.systemspannung98Vminus[zeitraum] as ReduxDataEntry[];
return state.systemspannung15Vminus[zeitraum];
case "-96V":
return state.systemspannung98Vminus[zeitraum];
case "ADC Temp":
return state.temperaturAdWandler[zeitraum] as ReduxDataEntry[];
return state.temperaturAdWandler[zeitraum];
case "CPU Temp":
return state.temperaturProzessor[zeitraum] as ReduxDataEntry[];
return state.temperaturProzessor[zeitraum];
default:
return [] as ReduxDataEntry[];
return EMPTY_REDUX_DATA;
}
});
}) as ReduxDataEntry[];
const labels = reduxData.map((e: ReduxDataEntry) => e.t);
const values = reduxData.map((e: ReduxDataEntry) => e.i);
const isFullScreen = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
);
const dispatch = useAppDispatch();
const baseOptions = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: false,
grid: { color: "rgba(200,200,200,0.2)" },
title: { display: true, text: "Wert" },
},
x: {
grid: { color: "rgba(200,200,200,0.2)" },
title: { display: true, text: "Zeit" },
},
},
plugins: {
legend: { position: "bottom" as const },
title: { display: true, text: `Verlauf ${selectedKey}` },
},
// API-Request beim Klick auf "Daten laden" - memoized für useEffect dependency
const handleFetchData = useCallback(() => {
setIsLoading(true);
// Clear previous chart data
setChartData({ datasets: [] });
// Flag setzen, dass Chart nach Datenempfang aktualisiert werden soll
setShouldUpdateChart(true);
switch (selectedKey) {
case "+5V":
dispatch(getSystemspannung5VplusThunk(zeitraum));
break;
case "+15V":
dispatch(getSystemspannung15VplusThunk(zeitraum));
break;
case "-15V":
dispatch(getSystemspannung15VminusThunk(zeitraum));
break;
case "-96V":
dispatch(getSystemspannung98VminusThunk(zeitraum));
break;
case "ADC Temp":
dispatch(getTemperaturAdWandlerThunk(zeitraum));
break;
case "CPU Temp":
dispatch(getTemperaturProzessorThunk(zeitraum));
break;
default:
break;
}
}, [selectedKey, zeitraum, dispatch]);
// Reset Zeitraum auf DIA0 und Datumswerte wenn Modal geöffnet wird
useEffect(() => {
if (isOpen) {
setZeitraum("DIA0");
// Reset DateRangePicker to its defaults (it sets 30 days → today on mount)
dispatch(resetDateRange());
// Chart-Daten zurücksetzen beim Öffnen
setChartData({ datasets: [] });
}
}, [isOpen, setZeitraum, dispatch]);
// Periodische UI-Updates alle 2 Sekunden während Wartezeit
useEffect(() => {
if (isOpen && (!chartData.datasets || chartData.datasets.length === 0)) {
// Optional: periodische Re-Renders wurden deaktiviert, da nicht mehr notwendig
// (kann wieder aktiviert werden falls Cursor-Animation erwünscht ist)
}
}, [isOpen, chartData.datasets]);
// Automatisches "Daten laden" alle 4 Sekunden, maximal 2 Versuche
useEffect(() => {
if (isOpen && (!chartData.datasets || chartData.datasets.length === 0)) {
let attempts = 0;
const interval = setInterval(() => {
if (attempts < 2) {
console.log("Auto-clicking 'Daten laden' button...");
handleFetchData();
attempts++;
} else {
clearInterval(interval);
}
}, 4000);
return () => clearInterval(interval);
}
}, [isOpen, chartData.datasets, handleFetchData]);
const toggleFullScreen = () => {
dispatch(setFullScreen(!isFullScreen));
setTimeout(() => {
chartRef.current?.resize();
}, 50);
};
const handleClose = () => {
dispatch(setFullScreen(false));
dispatch(resetDateRange());
onClose();
};
useEffect(() => {
const loadZoomPlugin = async () => {
if (typeof window !== "undefined") {
const zoomPlugin = (await import("chartjs-plugin-zoom")).default;
if (!ChartJS.registry.plugins.get("zoom")) {
ChartJS.register(zoomPlugin);
}
}
};
loadZoomPlugin();
}, []);
useEffect(() => {
if (chartRef.current && selectedKey) {
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]);
useEffect(() => {
if (chartRef.current) {
chartRef.current.resetZoom();
}
}, [zeitraum]);
// Chart.js animation complete callback to set isLoading false
useEffect(() => {
if (chartRef.current && isLoading) {
const chartInstance = chartRef.current;
// Save previous callback to restore later
const 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]);
// DateRange from global DateRangePicker slice
const pickerVonDatum = useSelector(
(state: RootState) => state.dateRangePicker.vonDatum
);
const pickerBisDatum = useSelector(
(state: RootState) => state.dateRangePicker.bisDatum
);
// Update chart data when Redux data changes (only after button click)
useEffect(() => {
if (shouldUpdateChart && reduxData && reduxData.length > 0) {
// Filter data by selected date range (inclusive end date)
let filtered = reduxData;
try {
if (pickerVonDatum && pickerBisDatum) {
const start = new Date(`${pickerVonDatum}T00:00:00`);
const end = new Date(`${pickerBisDatum}T23:59:59`);
const s = start.getTime();
const e = end.getTime();
filtered = reduxData.filter((entry) => {
const t = new Date(entry.t).getTime();
return t >= s && t <= e;
});
}
} catch (err) {
console.warn("Zeitfilter konnte nicht angewendet werden:", err);
}
console.log("Redux data for chart (filtered):", filtered.length);
if (!filtered.length) {
setChartData({ datasets: [] });
setShouldUpdateChart(false);
return;
}
// Create datasets array for multiple lines
const datasets: ChartDataset<"line">[] = [];
// Check which data fields are available and create datasets accordingly
const hasMinimum = filtered.some(
(entry) => entry.i !== undefined && entry.i !== null && entry.i !== 0
);
const hasMaximum = filtered.some(
(entry) => entry.a !== undefined && entry.a !== null
);
const hasAverage = filtered.some(
(entry) => entry.g !== undefined && entry.g !== null
);
const hasCurrent = filtered.some(
(entry) => entry.m !== undefined && entry.m !== null
);
// Zuerst Hintergrund-Linien (Minimum/Maximum) - grau
if (hasMinimum) {
datasets.push({
label: "Messwert Minimum",
data: filtered.map((entry) => ({
x: new Date(entry.t).getTime(),
y: entry.i || 0,
})),
borderColor: "gray",
borderWidth: 1,
pointRadius: 0,
tension: 0.1,
order: 1,
});
}
if (hasMaximum) {
datasets.push({
label: "Messwert Maximum",
data: filtered.map((entry) => ({
x: new Date(entry.t).getTime(),
y: entry.a || 0,
})),
borderColor: "gray",
borderWidth: 1,
pointRadius: 0,
tension: 0.1,
order: 3,
});
}
// Dann Vordergrund-Linien (Durchschnitt/Messwert) - littwin-blue
if (hasAverage) {
datasets.push({
label: "Durchschnitt",
data: filtered.map((entry) => ({
x: new Date(entry.t).getTime(),
y: entry.g || 0,
})),
borderColor: chartColors.littwinBlue.line,
backgroundColor: chartColors.littwinBlue.background,
tension: 0.1,
fill: false,
order: 2,
});
}
if (hasCurrent) {
datasets.push({
label: "Messwert",
data: filtered.map((entry) => ({
x: new Date(entry.t).getTime(),
y: entry.m || 0,
})),
borderColor: chartColors.littwinBlue.line,
backgroundColor: chartColors.littwinBlue.background,
tension: 0.1,
fill: false,
order: 2,
});
}
const newChartData: ChartData<"line"> = {
labels: [],
datasets: datasets,
};
console.log("Chart datasets:", datasets.length, "lines");
setChartData(newChartData);
setShouldUpdateChart(false); // Reset flag
} else if (shouldUpdateChart && (!reduxData || reduxData.length === 0)) {
console.log("No Redux data available");
setChartData({ datasets: [] });
setShouldUpdateChart(false); // Reset flag
}
}, [
reduxData,
selectedKey,
shouldUpdateChart,
pickerVonDatum,
pickerBisDatum,
]);
if (!isOpen || !selectedKey) return null;
// Prüfen ob Chart Daten haben (für cursor-wait)
const hasChartData = chartData.datasets && chartData.datasets.length > 0;
return (
<div className="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-xl w-[50%] h-[60%] overflow-auto shadow-2xl">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">
<div
className={`fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50 ${
!hasChartData ? "cursor-wait" : ""
}`}
>
<div
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" : ""}`}
>
{/* 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>
<button onClick={onClose} className="text-red-500 hover:text-red-700">
</button>
</div>
<div className="flex items-center gap-3 text-lg">
<button
onClick={toggleFullScreen}
className="icon-btn text-[1.4rem] hover:text-fg transition"
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 text-[1.4rem] transition"
aria-label="Modal schließen"
type="button"
>
<i className="bi bi-x-circle-fill" />
</button>
</div>
</header>
<div className="mb-4">
<label className="mr-2 font-medium">Zeitraum:</label>
<select
className="border px-2 py-1 rounded"
value={zeitraum}
onChange={(e) =>
setZeitraum(e.target.value as "DIA0" | "DIA1" | "DIA2")
}
>
<option value="DIA0">Alle Messwerte</option>
<option value="DIA1">Stündlich</option>
<option value="DIA2">Täglich</option>
</select>
</div>
<div className="h-[85%]">
<Line
data={{
labels,
datasets: [
{
label: selectedKey,
data: values,
borderColor: "rgba(59,130,246,1)",
backgroundColor: "rgba(59,130,246,0.2)",
fill: false,
},
],
}}
options={baseOptions}
/>
{/* 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

@@ -0,0 +1,94 @@
"use client";
// components/main/system/SystemChartActionBar.tsx
import React from "react";
import DateRangePicker from "@/components/common/DateRangePicker";
import { Listbox } from "@headlessui/react";
type Props = {
zeitraum: "DIA0" | "DIA1" | "DIA2";
setZeitraum: (typ: "DIA0" | "DIA1" | "DIA2") => void;
onFetchData: () => void;
isLoading?: boolean;
className?: string;
};
const SystemChartActionBar: React.FC<Props> = ({
zeitraum,
setZeitraum,
onFetchData,
isLoading = false,
className = "",
}) => {
return (
<div
className={`flex items-center justify-start gap-3 mb-4 flex-wrap ${className}`}
>
{/* DateRangePicker nutzt globalen Redux-Slice */}
<DateRangePicker compact />
{/* Zeitraum (DIA0/DIA1/DIA2) */}
<label className="font-medium text-sm">Zeitraum:</label>
<Listbox value={zeitraum} onChange={setZeitraum}>
<div className="relative w-48">
<Listbox.Button className="w-full border border-base px-3 py-1 rounded text-left bg-[var(--color-surface-alt)] text-fg flex justify-between items-center text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-accent)]/40 transition">
<span>
{
{ DIA0: "Alle Messwerte", DIA1: "Stündlich", DIA2: "Täglich" }[
zeitraum
]
}
</span>
<svg
className="w-5 h-5 text-[var(--color-fg-muted)]"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border border-base rounded bg-[var(--color-surface)] text-fg shadow-lg max-h-60 overflow-auto text-sm focus:outline-none">
{["DIA0", "DIA1", "DIA2"].map((option) => (
<Listbox.Option
key={option}
value={option}
className={({ selected, active }) => {
const base = "px-4 py-1 cursor-pointer text-sm";
if (selected) return `${base} bg-littwin-blue text-white`; // selected highlight
if (active)
return `${base} bg-[var(--color-surface-alt)] text-fg`;
return `${base} text-fg`;
}}
>
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündlich",
DIA2: "Täglich",
}[option as "DIA0" | "DIA1" | "DIA2"]
}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
{/* Daten laden */}
<button
onClick={onFetchData}
className={`px-4 py-1 bg-littwin-blue text-white rounded text-sm ${
isLoading ? "cursor-wait opacity-70" : ""
}`}
disabled={isLoading}
aria-busy={isLoading}
>
{isLoading ? "Laden..." : "Daten laden"}
</button>
</div>
);
};
export default SystemChartActionBar;

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;
};
@@ -38,12 +38,29 @@ type Props = {
zeitraum: "DIA0" | "DIA1" | "DIA2";
};
export const SystemCharts = ({ history }: Props) => {
const labels = history.map((h) => new Date(h.time).toLocaleTimeString());
const [isLoading, setIsLoading] = React.useState(true);
const reversedHistory = [...history].reverse();
const labels = reversedHistory.map((h) =>
new Date(h.time).toLocaleTimeString()
);
const formatValue = (v: number) => v.toFixed(2);
// Chart.js animation callback
const animation = {
onComplete: () => {
setIsLoading(false);
},
};
React.useEffect(() => {
setIsLoading(true);
}, [history]);
const baseOptions = {
responsive: true,
maintainAspectRatio: false,
animation,
scales: {
y: {
beginAtZero: false,
@@ -61,7 +78,11 @@ export const SystemCharts = ({ history }: Props) => {
};
return (
<div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
<div
className={`grid grid-cols-1 xl:grid-cols-2 gap-8 ${
isLoading ? "cursor-wait" : ""
}`}
>
<div className="h-[300px]">
<Line
data={{
@@ -89,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,
@@ -99,6 +120,16 @@ export const SystemCharts = ({ history }: Props) => {
}}
options={{
...baseOptions,
scales: {
...baseOptions.scales,
y: {
...baseOptions.scales.y,
title: {
display: true,
text: "Spannung (V)", // 👉 Einheit hinzugefügt
},
},
},
plugins: {
...baseOptions.plugins,
title: { display: true, text: "Systemspannungen" },
@@ -132,6 +163,16 @@ export const SystemCharts = ({ history }: Props) => {
}}
options={{
...baseOptions,
scales: {
...baseOptions.scales,
y: {
...baseOptions.scales.y,
title: {
display: true,
text: "Temperatur (°C)", // 👉 Einheit hinzugefügt
},
},
},
plugins: {
...baseOptions.plugins,
title: { display: true, text: "Systemtemperaturen" },

View File

@@ -10,19 +10,22 @@ export const SystemOverviewGrid = ({ voltages, onOpenDetail }: Props) => {
const formatValue = (value: number) => value.toFixed(2);
return (
<div className="grid grid-cols-2 gap-4 mb-8">
<div className="grid grid-cols-2 gap-4 mb-2">
{Object.entries(voltages).map(([key, value]) => {
const unit = key.includes("Temp") ? "°C" : "V";
const unit = key.includes("Temp") ? "\u00b0C" : "V";
return (
<div key={key} className="p-4 border rounded shadow">
<div
key={key}
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>
{formatValue(value)} {unit}
<button
onClick={() => onOpenDetail(key)}
className="ml-2 text-blue-600 hover:underline text-sm"
className="ml-2 text-littwin-blue hover:underline text-sm"
>
Details
Detailansicht
</button>
</p>
</div>

View File

@@ -0,0 +1,109 @@
// components/main/system/system.tsx
"use client";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "@/redux/store";
import { getSystemVoltTempThunk } from "@/redux/thunks/getSystemVoltTempThunk";
import { SystemOverviewGrid } from "@/components/main/system/SystemOverviewGrid";
import { SystemCharts } from "@/components/main/system/SystemCharts";
import { DetailModal } from "@/components/main/system/DetailModal";
import type { HistoryEntry } from "@/components/main/system/SystemCharts";
import { getSystemspannung5VplusThunk } from "@/redux/thunks/getSystemspannung5VplusThunk";
import { getSystemspannung15VplusThunk } from "@/redux/thunks/getSystemspannung15VplusThunk";
import { getSystemspannung15VminusThunk } from "@/redux/thunks/getSystemspannung15VminusThunk";
import { getSystemspannung98VminusThunk } from "@/redux/thunks/getSystemspannung98VminusThunk";
import { getTemperaturAdWandlerThunk } from "@/redux/thunks/getTemperaturAdWandlerThunk";
import { getTemperaturProzessorThunk } from "@/redux/thunks/getTemperaturProzessorThunk";
import { ClipLoader } from "react-spinners";
const SystemPage = () => {
const dispatch = useDispatch<AppDispatch>();
const voltages = useSelector(
(state: RootState) => state.systemVoltTemp.voltages
);
const history = useSelector(
(state: RootState) => state.systemVoltTemp.history
) as HistoryEntry[];
const isLoading = !history.length || Object.keys(voltages).length === 0;
const [selectedKey, setSelectedKey] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [zeitraum, setZeitraum] = useState<"DIA0" | "DIA1" | "DIA2">("DIA1");
useEffect(() => {
dispatch(getSystemVoltTempThunk());
const interval = setInterval(() => {
dispatch(getSystemVoltTempThunk());
}, 5000);
return () => clearInterval(interval);
}, [dispatch]);
const handleOpenDetail = (key: string) => {
setSelectedKey(key);
setIsModalOpen(true);
switch (key) {
case "+5V":
dispatch(getSystemspannung5VplusThunk(zeitraum));
break;
case "+15V":
dispatch(getSystemspannung15VplusThunk(zeitraum));
break;
case "-15V":
dispatch(getSystemspannung15VminusThunk(zeitraum));
break;
case "-96V":
dispatch(getSystemspannung98VminusThunk(zeitraum));
break;
case "ADC Temp":
dispatch(getTemperaturAdWandlerThunk(zeitraum));
break;
case "CPU Temp":
dispatch(getTemperaturProzessorThunk(zeitraum));
break;
default:
break;
}
};
const handleCloseDetail = () => {
setIsModalOpen(false);
};
return (
<div className="p-4 bg-[var(--color-background)] text-[var(--color-fg)]">
<h1 className="text-xl font-bold mb-4 tracking-wide">
System Spannungen & Temperaturen
</h1>
{isLoading ? (
<div className="flex justify-center items-center h-[400px]">
<div className="text-center">
<ClipLoader size={50} color="#3B82F6" />
<p className="mt-4 text-[var(--color-fg-muted)]">
Lade Systemdaten bitte warten
</p>
</div>
</div>
) : (
<>
<SystemOverviewGrid
voltages={voltages}
onOpenDetail={handleOpenDetail}
/>
<SystemCharts history={history} zeitraum={zeitraum} />
</>
)}
<DetailModal
isOpen={isModalOpen}
selectedKey={selectedKey}
onClose={handleCloseDetail}
zeitraum={zeitraum}
setZeitraum={setZeitraum}
/>
</div>
);
};
export default SystemPage;

View File

@@ -26,7 +26,7 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
{ name: "Kabelüberwachung ", path: "/kabelueberwachung" },
{ name: "Meldungseingänge ", path: "/digitalInputs" }, //vorher Digitale Ein -und Ausgänge
{ name: "Schaltausgänge ", path: "/digitalOutputs", disabled: false }, //vorher Digitale Ein -und Ausgänge
{ name: "Messwerteingänge ", path: "/analogeEingaenge" }, //vorher Analoge Eingänge
{ name: "Messwerteingänge ", path: "/analogInputs" }, //vorher Analoge Eingänge
{ name: "Berichte ", path: "/meldungen" },
{ name: "System ", path: "/system" },
{ name: "Einstellungen ", path: "/einstellungen" },
@@ -36,25 +36,28 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
];
return (
<aside>
<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 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>
) : (
<Link href={formatPath(item.path)}>
<div
className={`block px-4 py-2 mb-4 font-bold whitespace-nowrap transition duration-300 text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg ${
activeLink.startsWith(item.path)
? "bg-sky-500 text-white rounded-r-full xl:mr-4 xl:w-full"
: "text-black hover:bg-gray-200 rounded-r-full"
}`}
>
{item.name}
</div>
<Link
href={formatPath(item.path)}
prefetch={false}
onClick={() => setActiveLink(item.path)}
className={`block px-4 py-2 mb-4 font-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-[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>
)}
</div>

80
create_presentation.py Normal file
View File

@@ -0,0 +1,80 @@
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.text import PP_ALIGN
prs = Presentation()
def add_slide(title, content_lines):
slide_layout = prs.slide_layouts[1] # Title and Content
slide = prs.slides.add_slide(slide_layout)
title_placeholder = slide.shapes.title
content_placeholder = slide.placeholders[1]
title_placeholder.text = title
tf = content_placeholder.text_frame
tf.clear()
for line in content_lines:
p = tf.add_paragraph()
p.text = line
p.font.size = Pt(20)
p.alignment = PP_ALIGN.LEFT
# Folie 1: Titel
add_slide("Testing CPL V4 Webseiten", ["Von: Ismail Ali", "Datum: 22.08.2025"])
# Folie 2 entfernt
# Folie 3: Warum testen wir?
add_slide("Warum testen wir?", [
"Um sicherzustellen, dass die Weboberfläche richtig funktioniert.",
"Fehler frühzeitig erkennen und beheben.",
"Qualität und Zuverlässigkeit verbessern."
])
# Folie 4: Was ist Playwright?
add_slide("Was ist Playwright?", [
"Ein Open-Source-Testframework von Microsoft.",
"Ermöglicht automatisierte Tests in verschiedenen Browsern (Chromium, Firefox, WebKit):",
"🟦 Chromium 🦊 Firefox 🍏 WebKit",
"Simuliert echte Benutzeraktionen wie Klicks, Eingaben und Navigation.",
"Unterstützt mehrere Programmiersprachen: JavaScript, TypeScript, Python, Java, .NET (C#).",
"Ideal für End-to-End-Tests von Webanwendungen."
])
# Folie 5: Wie habe ich getestet?
add_slide("Wie habe ich getestet?", [
"Mit Playwright automatisierte Tests geschrieben.",
"Playwright Recorder (codegen) verwendet, da es einfacher ist als manuellen Code zu schreiben.",
"Verschiedene Seiten des CPL V4 Webservers getestet:",
"- Dashboard",
"- Analoge Eingänge",
"- Einstellungen"
])
# Folie 6: Beispiel-Test (Ausschnitt)
add_slide("Beispiel-Test (Ausschnitt)", [
"Test prüft, ob wichtige Elemente sichtbar sind.",
"Beispiel: Überschrift, Buttons, Tabellenzellen.",
"Klicks und Eingaben werden simuliert."
])
# Folie 7: Test-Ergebnisse
add_slide("Test-Ergebnisse", [
"Alle Tests wurden erfolgreich ausgeführt.",
"Keine Fehler gefunden (siehe Test-Report)."
])
# Folie 8: Fazit
add_slide("Fazit", [
"Automatisierte Tests helfen, Fehler schnell zu finden.",
"Playwright ist einfach zu bedienen, auch für Anfänger.",
"Tests machen die Entwicklung sicherer und effizienter."
])
# Folie 9: Fragen?
add_slide("Fragen?", [
"Vielen Dank für die Aufmerksamkeit!",
"Gibt es Fragen?"
])
prs.save("Testing_CPLV4_Webserver.pptx")
print("Präsentation erstellt: Testing_CPLV4_Webserver.pptx")

View File

@@ -1,9 +0,0 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});

View File

@@ -1,46 +0,0 @@
describe('Kue705FO Integration Tests', () => {
beforeEach(() => {
// Besuche die Seite, auf der die Komponente gerendert wird
//cy.visit('/path-to-your-component'); // Passe den Pfad an deine App an
cy.visit('http://localhost:3000/kabelueberwachung');
});
it('should render the component with default props', () => {
// Überprüfe, ob der Modulname und die Slotnummer angezeigt werden
cy.contains('KÜ705-FO').should('be.visible');
cy.contains('Modul 1').should('be.visible'); // Beispiel für den Modulnamen
});
it('should update display when TDR button is clicked', () => {
// Klicke auf den TDR-Button
cy.contains('TDR').click();
// Überprüfe, ob der Text aktualisiert wurde
cy.contains('Entfernung [Km]').should('be.visible');
});
it('should switch back to Schleife display', () => {
// Klicke auf TDR, dann zurück zu Schleife
cy.contains('TDR').click();
cy.contains('Schleife').click();
// Überprüfe, ob der Text aktualisiert wurde
cy.contains('Schleifenwiderstand [kOhm]').should('be.visible');
});
it('should disable TDR button when tdrActive is 0', () => {
// Dies erfordert eine benutzerdefinierte Backend-Konfiguration oder Redux-Manipulation
cy.contains('TDR').should('be.disabled');
});
it('should open and close the settings modal', () => {
// Öffne das Modal
cy.contains('⚙').click();
cy.contains('KUE Einstellung - Slot 1').should('be.visible');
// Schließe das Modal
cy.contains('×').click();
cy.contains('KUE Einstellung - Slot 1').should('not.exist');
});
});

View File

@@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -1,37 +0,0 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }

View File

@@ -1,17 +0,0 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'

View File

@@ -0,0 +1,142 @@
# KVZ System - Mein aktuelles Verständnis
## System Übersicht
```mermaid
graph TB
subgraph "Kabelüberwachung System (32 Slots)"
Slot0["Slot 0<br/>Kabelüberwachung"]
Slot1["Slot 1<br/>Kabelüberwachung"]
Slot2["Slot 2<br/>Kabelüberwachung"]
Slot3["Slot 3<br/>Kabelüberwachung"]
SlotDots["..."]
Slot31["Slot 31<br/>Kabelüberwachung"]
end
subgraph "KVZ Geräte (Optional pro Slot)"
KVZ0["KVZ Gerät<br/>für Slot 0"]
KVZ1["KVZ Gerät<br/>für Slot 1"]
KVZ2["KVZ Gerät<br/>für Slot 2"]
KVZ3["KVZ Gerät<br/>für Slot 3"]
end
subgraph "KVZ LEDs (4 pro KVZ Gerät)"
subgraph "KVZ0 LEDs"
LED0_0["LED 1"]
LED0_1["LED 2"]
LED0_2["LED 3"]
LED0_3["LED 4"]
end
subgraph "KVZ1 LEDs"
LED1_0["LED 1"]
LED1_1["LED 2"]
LED1_2["LED 3"]
LED1_3["LED 4"]
end
end
Slot0 -.-> KVZ0
Slot1 -.-> KVZ1
Slot2 -.-> KVZ2
Slot3 -.-> KVZ3
KVZ0 --> LED0_0
KVZ0 --> LED0_1
KVZ0 --> LED0_2
KVZ0 --> LED0_3
KVZ1 --> LED1_0
KVZ1 --> LED1_1
KVZ1 --> LED1_2
KVZ1 --> LED1_3
```
## Redux Data Structure - Mein Verständnis
```mermaid
graph TB
subgraph "Redux Store"
subgraph "kvzPresence Array (32 Elemente)"
P0["Index 0: 1<br/>(KVZ vorhanden)"]
P1["Index 1: 0<br/>(KVZ nicht vorhanden)"]
P2["Index 2: 0"]
P3["Index 3: 0"]
PDots["..."]
P31["Index 31: 0"]
end
subgraph "kvzStatus Array (128 Elemente)"
subgraph "Slot 0 LEDs (Index 0-3)"
S0_0["Index 0: 1 (grün)"]
S0_1["Index 1: 0 (rot)"]
S0_2["Index 2: 1 (grün)"]
S0_3["Index 3: 0 (rot)"]
end
subgraph "Slot 1 LEDs (Index 4-7)"
S1_0["Index 4: 0"]
S1_1["Index 5: 0"]
S1_2["Index 6: 0"]
S1_3["Index 7: 0"]
end
StatusDots["...weitere 120 Elemente"]
end
end
```
## UI Darstellung - Mein aktuelles Verständnis
```mermaid
graph LR
subgraph "FallSensors UI Component"
subgraph "Aktuelle Implementierung (FALSCH?)"
UI1["KVZ1: 🟢<br/>(kvzPresence[0] = 1)"]
UI2["KVZ2: 🔴<br/>(kvzPresence[1] = 0)"]
UI3["KVZ3: 🔴<br/>(kvzPresence[2] = 0)"]
UI4["KVZ4: 🔴<br/>(kvzPresence[3] = 0)"]
end
end
subgraph "Problem"
Problem["Alle KVZ zeigen den gleichen Status<br/>basierend auf kvzPresence Array<br/>→ NICHT korrekt!"]
end
UI1 -.-> Problem
UI2 -.-> Problem
UI3 -.-> Problem
UI4 -.-> Problem
```
## Fragen zu meinem Verständnis
1. **KVZ Geräte Zuordnung**:
- Ist ein KVZ-Gerät einem Slot zugeordnet oder unabhängig?
- Wie viele KVZ-Geräte gibt es insgesamt?
2. **UI KVZ1-KVZ4**:
- Repräsentieren KVZ1-KVZ4 in der UI die ersten 4 Slots (0-3)?
- Oder sind es 4 separate, unabhängige KVZ-Geräte?
3. **LED Status Mapping**:
- Welche LED von welchem KVZ soll in KVZ1, KVZ2, KVZ3, KVZ4 angezeigt werden?
- Soll jedes UI-KVZ eine andere LED des gleichen Geräts zeigen?
- Oder soll jedes UI-KVZ ein anderes KVZ-Gerät repräsentieren?
4. **kvzStatus Array**:
- Wie soll das 128-Element Array für die UI-Darstellung genutzt werden?
- Welche Indizes entsprechen welchen UI-Elementen?
## Verdacht
Ich vermute, dass mein aktueller Ansatz falsch ist, weil:
- KVZ2 sollte nicht eine Kopie von KVZ1 Status sein
- Jedes KVZ in der UI sollte einen eigenen, unabhängigen Status haben
- Die Zuordnung zwischen Redux Arrays und UI ist unklar
**Bitte korrigieren Sie mein Verständnis! 🤔**

141
docs/KVZ/mock-data.md Normal file
View File

@@ -0,0 +1,141 @@
# KVZ Mock Data - Dokumentation
## Mock-Daten Struktur
Die KVZ Mock-Daten befinden sich in `mocks/kvzData.json` und haben folgende Struktur:
### Beispiel-Daten
```json
{
"kvzPresence": [1, 0, 1, 0, ...], // 32 Elemente
"kvzStatus": [1, 0, 1, 0, ...], // 128 Elemente
"timestamp": "2025-01-31T12:00:00.000Z"
}
```
### kvzPresence Array (32 Elemente)
- **Index 0-31**: Repräsentiert Slots 0-31
- **Wert 1**: KVZ-Gerät vorhanden
- **Wert 0**: KVZ-Gerät nicht vorhanden
**Aktuelle Mock-Daten:**
- Slot 0: KVZ vorhanden (1)
- Slot 1: KVZ nicht vorhanden (0)
- Slot 2: KVZ vorhanden (1)
- Slot 3-31: KVZ nicht vorhanden (0)
### kvzStatus Array (128 Elemente)
- **Index Berechnung**: `slotIndex * 4 + ledIndex`
- **4 LEDs pro Slot**: LED 0, LED 1, LED 2, LED 3
- **Wert 1**: LED aktiv (grün)
- **Wert 0**: LED inaktiv (rot)
**Aktuelle Mock-Daten:**
- Slot 0 LEDs (Index 0-3): [1, 0, 1, 0] → LED1=grün, LED2=rot, LED3=grün, LED4=rot
- Slot 1 LEDs (Index 4-7): [0, 0, 0, 0] → Alle LEDs rot (KVZ nicht vorhanden)
- Slot 2 LEDs (Index 8-11): [1, 1, 0, 1] → LED1=grün, LED2=grün, LED3=rot, LED4=grün
- Slot 3-31: Alle LEDs rot (0)
## API Endpunkte
### GET /api/kvz/data
Holt alle KVZ-Daten.
**Response:**
```json
{
"kvzPresence": [...],
"kvzStatus": [...],
"timestamp": "2025-01-31T12:00:00.000Z"
}
```
### POST /api/kvz/data
Ersetzt komplette KVZ-Daten.
**Request Body:**
```json
{
"kvzPresence": [...],
"kvzStatus": [...]
}
```
### POST /api/kvz/updateSettings
Aktualisiert spezifische KVZ-Einstellungen.
**Request Body:**
```json
{
"updates": [
{
"key": "kvzPresence",
"slot": 0,
"value": 1
},
{
"key": "kvzStatus",
"slot": 0,
"ledIndex": 1,
"value": 1
}
]
}
```
## Service Functions
### fetchKvzData()
```typescript
import { fetchKvzData } from "../services/fetchKvzDataService";
const data = await fetchKvzData();
console.log(data.kvzPresence); // [1, 0, 1, 0, ...]
```
### updateKvzSettings()
```typescript
import { updateKvzSettings } from "../services/fetchKvzDataService";
await updateKvzSettings([
{ key: "kvzPresence", slot: 0, value: 1 },
{ key: "kvzStatus", slot: 0, ledIndex: 1, value: 1 },
]);
```
## Redux Integration
Die Mock-Daten können in Redux geladen werden:
```typescript
// In einem Thunk oder useEffect
const data = await fetchKvzData();
dispatch(
setKueData({
kvzPresence: data.kvzPresence,
kvzStatus: data.kvzStatus,
})
);
```
## Testen
Die Mock-Daten ermöglichen es:
1. **KVZ-Geräte zu simulieren** (Slots 0 und 2 haben KVZ)
2. **LED-Status zu testen** (verschiedene Kombinationen)
3. **API-Updates zu testen** (Presence und Status ändern)
4. **UI-Verhalten zu validieren** (bedingte Anzeige, Farben)

Binary file not shown.

View File

@@ -18,3 +18,105 @@
- [ ] TODO: Alle Kabelüberwachungsmodule mit ein Button Updaten , in Einstellungen und in Kabelüberwachungsmodul Modal
![Zusatzfunktionen Kai 25.06.2025](./TODOsScreenshots/Zusatzfunktionen_25-06-2025.png)
Zeit bis Ende August
- [ ] TODO: Überall Littwin-Blau und ausgewählt und grau wie bei Navigation bei Mouse over
- [ ] ## TODO: Messwerteingänge Diagrammme /Messkurven
23.07.2025
- [x] TODO: Isolationsfehler in Display anzeigen -> aktuell Zahl ist rot ohne Beschrifftung , es soll Zahl ISO MOhm und Isolationsfehler
- [x] TODO: Kilometer Km -> km kleingeschrieben 1000, 1024 wird Großgeschrieben Kilobyte Kb
- [x] TODO: Messwerteingänge Mouse couror wait beim laden, damit der user etwas wartet
- [ ] ## TODO: In KÜ, unter KÜ Balken/Bereich für Scheleife, Bereich für TDR wenn aktiv ist und Bereich für KVz wenn aktive ist
---
---
24.07.2025
- [ ] TODO: Bei System Messkurven Cursor await bis die daten lädt
- [ ] TODO: in KÜ DISPLAY Fehler, ISO Wert und Schleifenwwert bei wechsel Zustand anzeigen während Schleifenmessung und Isowemmsung und kalibirirung
- [ ] TODO: Bug in DatePicker in KÜ
- [ ] TODO: Kurven für ISO, RSL und TDR
- [x] TODO: in KÜ Steckplatz in KÜ umbenennen
- [x] TODO: Firmware Update Bestätigung in Littwin blau auch progress in littwinblue
- [x] TODO: KÜ Firmware in progress Bitte Fenster nicht schließen
- [ ] TODO: KÜ Kurve und letzte historische Meldungen anzeigen -> später
- [ ] TODO: KVz später
Anzeige KÜ-Display:
1. Zeile Alarm: Isolationsfehler, Schleifenfehler, Aderbruch, Erdschluß, Messpannung: Immer in Rot; wenn kein Alarm, bleibt die Zeile leer
2. Zeile: Isowert: xx MOhm (großes M)
in Rot, wenn Iso-Fehler ansteht
Beispiel: ISO: 100 MOHm der beim Abliech: ISO: Abgleich
3. Zeile: Schleifenwert, xx kOhm (kleines k)
in Rot, wenn Schleifenfehler ansteht
## Beispiel:: RSL: 1,7 kOhm oder wenn Schleifenmessung aktiv: RSL: Messung
## 11.08.2025
- [x] TODO: Bei Schleife starten messen wie lange es dauert, dann entsprechend progress balken einbauen, 2 Minuten erstellt
# 13.08.2025
- [x] TODO: Das Sichern und das Zurücksichern der KÜ-Daten über die Webseiten funktioniert nicht. Anscheinend ruft die Webseite keine ACP-Webseite mit Daten "?KSB02=1" auf sondern nur Daten "KSB02=1". Die CPL will dann die Datei KSB02=1 laden die es ja nicht gibt.
- [x] TODO: Kalibrieren Dauer entsprechend progress balken einbauen
- [x] TODO: Abgleich Dauer entsprechend progress balken einbauen
- [ ] TODO: Benutzer passwort ändern
- [ ] TODO: PlayWright
- ISO Abgleich 10 Minuten
- CPL läuft auf BUS-System, Wenn ein Kabelüberwachung z.B. beschäftigt mit ein Schleifen-Messung oder Iso- Abgleich dann belastet den CPL nicht wenn andere KÜs bedient werden, das erleichtet den CPL sogar
# 14.08.2025
- [x] TODO: Messwerteingänge Messkurven in Modal umwandeln
# 15.08.2025
- [x] BUGFIX: Messkurven-Modal lädt jetzt automatisch die Kurve beim Öffnen, Dropdown ist auf 'Alle Messwerte' (DIA0) initialisiert, und Filter werden beim Schließen zurückgesetzt. Dateien: IsoChartView.tsx, LoopChartView.tsx
# 01.09.2025
- [x] TODO: In KÜs Display ISO 2 Nachkommastellen und RSL 3 Nachkommastellen
- [ ] TODO: Schleife, Timer für jeder KÜ separate und nicht eine für alle, aktuell wird prozentzahl bei allen das gleiche angezeigt
- [x] TODO: RSL starten in RSL Messung starten umbenennen
- [x] TODO: TDR-Messung starten statt TDR aktivieren in ChartBar
- [x] TODO: KÜ TDR-aktiviert alert entfernen
- [x] TODO: Systemdaten unter Detailansicht ein Verlaufsdiagramm hinzufügen mit Datumsauswahl
# Kai Schmidt:
# Folgende Erweiterung / Neuerungen:
[x] TODO: Messverlauf bei Systemwerten (Temperatur und Spannungen) mit Datumsauswahl
[x] TODO: Formatierung der Kabelüberwachungswerten in den visuellen Einschüben (Isowert mit Komma und 2 Nachkommastellen; RSL mit Komma und 3 Noachkommastellen) Nachkommastellen immer anzeigen und mit Nullen auffüllen.
[x] TODO: lange Modulnamen bei KÜ ermöglichen (48 Zeichen) bei Version ab V4.30. Laufschrift möglich?
# ------------------------------------------
# 08.09.2025
[x] TODO: Beim Ausführen einer TDR-Messung (Klick auf blauen Button in der TDR-Detailseite) erscheint keine Rückmeldung. Dort müsste ein Hinweis erscheinen “TDR-Messung wird ausgeführt und kann bis zu zwei Minuten dauern”
## 09.09.2025
[x] TODO: Admin User nach einer Zeit von einer Stunde löschen (Cookie oder Local Storrage)
[ ] TODO: Darkmode ermöglichen
[ ] TODO: Wenn im Browser Darkmode eingschaltet ist muss die Webseite erkennbar sein.
[ ] TODO: KÜ TDR-aktiviert alert entfernen
[ ] TODO: Playwright testen mit der Entwicklung
# 11.09.2025
[ ] TODO: KÜ ISO Modal -> Meldungen z-index datePicker von bis

View File

@@ -46,7 +46,7 @@ Sie ist **pro Slot aktivierbar** und bietet folgende Einstellungen:
### 🔁 Knotenpunkte-Anzeige
Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines Steckplatzes:
Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines es:
| Feld | Beschreibung |
| -------------- | -------------------------------------------------- |
@@ -80,6 +80,25 @@ Jeder Slot zeigt über zwei LEDs den Betriebs- und Alarmstatus an:
---
## ⏳ Messung läuft: Per-Slot Overlay (Schleife, ISO-Abgleich, TDR)
Während aktive Messungen laufen, wird der betroffene Slot gezielt blockiert, ohne die gesamte Seite zu sperren. So können andere Module weiterhin bedient werden.
- Per-Slot Overlay erscheint nur auf dem betroffenen Modul auf der Seite „Kabelüberwachung“.
- Das Overlay zeigt für jede aktive Messart eine Fortschrittsleiste mit Prozentanzeige (ohne Restzeit):
- Schleifenmessung (RSL, KSX) ≈ 2 Minuten
- TDR-Messung (KSY) ≈ 30 Sekunden
- ISO-Abgleich (KSZ) ≈ 10 Minuten
- Die Anzeige aktualisiert sich ca. jede Sekunde.
- Bei mehreren gleichzeitig aktiven Messarten werden mehrere Balken untereinander gezeigt.
Hinweise:
- Auf anderen Seiten (z.B. System) erscheint statt des per-Slot Overlays ein globales Overlay über die gesamte Anwendung. Auf der Seite „Kabelüberwachung“ ist dieses globale Overlay deaktiviert, damit die Bedienung der übrigen Slots möglich bleibt.
- Die Aktivitätsinformationen stammen vom Gerät (KSX/KSY/KSZ Ereignis-Arrays je 32 Slots) und werden zyklisch eingelesen.
---
## 🔄 Messungssteuerung (manuell)
Im unteren Bereich jedes Slots befindet sich ein **Kreispfeil-Icon** 🔄 (Reload-Symbol):

View File

@@ -46,7 +46,7 @@ Sie ist **pro Slot aktivierbar** und bietet folgende Einstellungen:
### 🔁 Knotenpunkte-Anzeige
Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines Steckplatzes:
Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines es:
| Feld | Beschreibung |
| -------------- | -------------------------------------------------- |

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

@@ -46,7 +46,7 @@ Sie ist **pro Slot aktivierbar** und bietet folgende Einstellungen:
### 🔁 Knotenpunkte-Anzeige
Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines Steckplatzes:
Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines es:
| Feld | Beschreibung |
| -------------- | -------------------------------------------------- |

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

@@ -1,6 +1,7 @@
module.exports = {
testEnvironment: "jest-environment-jsdom",
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
testPathIgnorePatterns: ["/node_modules/", "/playwright/"],
moduleNameMapper: {
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
"^bootstrap-icons/font/bootstrap-icons.css$":

View File

@@ -1,23 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["*"],
"@/redux/*": ["redux/*"],
"@/utils/*": ["utils/*"],
"@/components/*": ["components/*"]
}
},
"include": [
"src/**/*",
"components/**/*",
"redux/**/*",
"utils/**/*",
"*.js",
"*.ts",
"*.jsx",
"*.tsx"
],
"extends": "./tsconfig.json"
}

View File

@@ -1,17 +0,0 @@
{
"win_appVersion": "0.02",
"win_deviceName": "CPLV4 Ismail Rastede",
"win_mac1": "0 48 86 81 46 143",
"win_ip": "10.10.0.243",
"win_subnet": "255.255.255.0",
"win_gateway": "10.10.0.1",
"win_cplInternalTimestamp": "23.10.24 15:10:28 Uhr",
"win_opcState": "1",
"win_opcSessions": "0",
"win_opcName": "CPL V4 OPC UA Application Deutsche Bahne",
"win_ntp1": "192.53.103.108",
"win_ntp2": "0.0.0.0",
"win_ntp3": "0.0.0.0",
"win_ntpTimezone": "2",
"win_ntpActive": "1"
}

View File

@@ -1,72 +0,0 @@
{
"win_analogInputsValues": [
4.771072,
5.665244,
0.005467,
-0.007468,
0.000002,
0.000001,
0.000001,
0.000007
],
"win_analogInputsLabels": [
"AE 1",
"AE 2",
"AE 3",
"AE 4",
"AE 5",
"AE 6",
"AE 7",
"AE 8"
],
"win_analogInputsOffset": [
10.988,
0,
0,
0,
0,
0,
0,
0
],
"win_analogInputsFactor": [
11.988,
1,
1,
1,
1,
1,
1,
1
],
"win_analogInputsUnits": [
"V",
"V",
"V",
"V",
"mA",
"mA",
"mA",
"mA"
],
"win_analogInputsLoggerIntervall": [
7,
10,
10,
10,
10,
10,
10,
10
],
"win_analogInputsWeighting": [
0,
0,
0,
0,
0,
0,
0,
0
]
}

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