Compare commits

244 Commits

Author SHA1 Message Date
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
ISA
859a8f1d64 feat: fetch-Services für Spannung und Temperatur für Dev- und Prod-Modus angepasst
- fetchSystemspannung5VplusService: Channel 110 (+5V), prod = /cpl?/dashboard.html
- fetchSystemspannung15VplusService: Channel 108 (+15V)
- fetchSystemspannung15VminusService: Channel 114 (-15V)
- fetchSystemspannung98VminusService: Channel 115 (-98V)
- fetchTemperaturAdWandlerService: Channel 116 (Temperatur AD-Wandler)
- fetchTemperaturProzessorService: Channel 117 (Temperatur Prozessor)

→ Dev-Mode verwendet API-Handler (/api/cpl/...)
→ Production-Mode nutzt CGI-kompatible URLs (/cpl?/dashboard.html&...)

Fehlerbehandlung integriert, Struktur für Wiederverwendung vereinheitlicht
2025-07-03 14:02:16 +02:00
ISA
b1eb3c46a8 feat: Detailansicht auf dynamische Redux-Datenquellen umgestellt
- DetailModal.tsx überarbeitet, um Redux-Daten je nach ausgewähltem Key (+5V, +15V, -15V, -98V, ADC Temp, CPU Temp) anzuzeigen
- Zeitraum-Auswahl (DIA0, DIA1, DIA2) wird berücksichtigt und löst passenden Thunk aus
- Redux-State-Struktur vollständig integriert für Systemspannungen und Temperaturen
- Chart-Anzeige jetzt dynamisch und erweiterbar
2025-07-03 12:24:53 +02:00
ISA
a0e8e47fae feat: APIs erstellt für Systemspannungen 2025-07-03 11:48:52 +02:00
ISA
521bd7ea93 feat: in System 5 Volt DIA0, DIA1 und DIA2 in dropdown anzeigen 2025-07-03 11:14:18 +02:00
ISA
3e7d702ab7 feat: in System 5 Volt DIA0, DIA1 und DIA2 in dropdown anzeigen 2025-07-03 11:13:39 +02:00
ISA
09bc64e771 feat: API für Systemspannung +5V erfolgreich implementiert
- API-Handler `getSystemspannung5VplusHandler.ts` erstellt
- JSON-Daten werden aus dem Verzeichnis `mocks/device-cgi-simulator/chartsData/systemspannung5Vplus/` geladen
- unterstützt die Parameter DIA0, DIA1, DIA2 für unterschiedliche Datenfrequenzen
- Fehlerbehandlung bei ungültigen Typen und fehlenden Dateien eingebaut
- API getestet unter `/api/cpl/getSystemspannung5VplusHandler?typ=DIA0`
2025-07-03 10:23:39 +02:00
ISA
cee3ee0581 feat: API für Systemspannung +5V erfolgreich implementiert
- API-Handler `getSystemspannung5VplusHandler.ts` erstellt
- JSON-Daten werden aus dem Verzeichnis `mocks/device-cgi-simulator/chartsData/systemspannung5Vplus/` geladen
- unterstützt die Parameter DIA0, DIA1, DIA2 für unterschiedliche Datenfrequenzen
- Fehlerbehandlung bei ungültigen Typen und fehlenden Dateien eingebaut
- API getestet unter `/api/cpl/getSystemspannung5VplusHandler?typ=DIA0`
2025-07-03 10:23:04 +02:00
ISA
4245d7a991 fix: KÜ Firmwareupdate 2025-07-03 07:36:10 +02:00
649 changed files with 1807014 additions and 483724 deletions

View File

@@ -6,6 +6,6 @@ NEXT_PUBLIC_USE_MOCK_BACKEND_LOOP_START=false
NEXT_PUBLIC_EXPORT_STATIC=false NEXT_PUBLIC_EXPORT_STATIC=false
NEXT_PUBLIC_USE_CGI=false NEXT_PUBLIC_USE_CGI=false
# App-Versionsnummer # App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.6.526 NEXT_PUBLIC_APP_VERSION=1.6.805
NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter) NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter)

View File

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

View File

@@ -14,7 +14,32 @@
"jsx": true "jsx": true
} }
}, },
"env": {
"browser": true,
"node": true,
"es2021": true
},
"globals": {
"JSX": "readonly",
"NodeJS": "readonly"
},
"rules": { "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 # testing
/coverage /coverage
# playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
playwright/report/
playwright/test-results/
playwright/.cache/
# next.js # next.js
/.next/ /.next/
/out/ /out/
@@ -34,3 +43,6 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
# Playwright
node_modules/

37
.woodpecker.yml Normal file
View File

@@ -0,0 +1,37 @@
clone:
git:
image: woodpeckerci/plugin-git
settings:
depth: 0
lfs: true
submodules: true
when:
- event: push
- event: pull_request
steps:
- name: verify-mocks
image: mcr.microsoft.com/playwright:v1.54.2-jammy
commands:
- pwd
- node -v && npm -v
- npm ci
# Zeig mir, ob die Datei wirklich im Checkout liegt:
- echo "=== git ls-files ==="
- git ls-files | grep -i "^mocks/device-cgi-simulator/SERVICE/systemMockData.js" || true
- echo "=== ls -la ==="
- ls -la mocks/device-cgi-simulator/SERVICE || true
- echo "=== file exists? ==="
- test -f mocks/device-cgi-simulator/SERVICE/systemMockData.js && echo "FOUND" || (echo "MISSING" && exit 1)
- name: e2e-dev
image: mcr.microsoft.com/playwright:v1.54.2-jammy
environment:
CI: "true"
NODE_ENV: "development"
NEXT_TELEMETRY_DISABLED: "1"
PORT: "3000"
commands:
- npm ci
- npx playwright test --project=chromium

File diff suppressed because it is too large Load Diff

BIN
Git 2.pptx Normal file

Binary file not shown.

19
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,19 @@
pipeline {
agent any
tools { nodejs 'node20' } // exakt der Name aus "Manage Jenkins → Tools"
stages {
stage('Versions') {
steps { sh 'node -v && npm -v' }
}
stage('Install deps') {
steps { sh 'npm ci' }
}
stage('Playwright tests') {
steps {
sh 'npx playwright install' // Browser-Binärdateien laden
sh 'npx playwright test'
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -31,7 +31,7 @@ export default function ConfirmModal({
Abbrechen Abbrechen
</button> </button>
<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} onClick={onConfirm}
> >
Bestätigen 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 React, { useEffect } from "react";
import DatePicker from "react-datepicker"; import DatePicker from "react-datepicker";
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import { RootState } from "@/redux/store"; import { RootState } from "@/redux/store";
import { import { setVonDatum, setBisDatum } from "@/redux/slices/dateRangePickerSlice";
setVonDatum,
setBisDatum,
} from "@/redux/slices/kabelueberwachungChartSlice";
import "react-datepicker/dist/react-datepicker.css"; 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 dispatch = useDispatch();
const reduxVonDatum = useSelector( const reduxVonDatum = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice.vonDatum (state: RootState) => state.dateRangePicker.vonDatum
); );
const reduxBisDatum = useSelector( const reduxBisDatum = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice.bisDatum (state: RootState) => state.dateRangePicker.bisDatum
); );
const today = new Date(); const today = new Date();
@@ -41,10 +46,22 @@ const DateRangePicker: React.FC = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, reduxVonDatum, reduxBisDatum]); }, [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 ( return (
<div className="flex space-x-4 items-center"> <div className={`flex ${gapClass} items-center ${className}`}>
<div className="flex items-center space-x-2"> <div
<label className="block text-sm font-semibold">Von</label> className={`flex items-center space-x-1 ${compact ? "text-xs" : ""}`}
>
<label
className={`block font-semibold ${
compact ? "text-xs" : "text-sm"
} ${labelWidthClass}`}
>
Von
</label>
<DatePicker <DatePicker
selected={reduxVonDatum ? parseISODate(reduxVonDatum) : thirtyDaysAgo} selected={reduxVonDatum ? parseISODate(reduxVonDatum) : thirtyDaysAgo}
onChange={(date) => { onChange={(date) => {
@@ -60,12 +77,21 @@ const DateRangePicker: React.FC = () => {
minDate={sixMonthsAgo} minDate={sixMonthsAgo}
maxDate={today} maxDate={today}
dateFormat="dd.MM.yyyy" dateFormat="dd.MM.yyyy"
className="border px-2 py-1 rounded" className={`border px-2 py-1 rounded ${inputWidthClass} ${
compact ? "text-xs" : "text-sm"
}`}
/> />
</div> </div>
<div
<div className="flex items-center space-x-2"> className={`flex items-center space-x-1 ${compact ? "text-xs" : ""}`}
<label className="block text-sm font-semibold">Bis</label> >
<label
className={`block font-semibold ${
compact ? "text-xs" : "text-sm"
} ${labelWidthClass}`}
>
Bis
</label>
<DatePicker <DatePicker
selected={reduxBisDatum ? parseISODate(reduxBisDatum) : today} selected={reduxBisDatum ? parseISODate(reduxBisDatum) : today}
onChange={(date) => { onChange={(date) => {
@@ -81,7 +107,9 @@ const DateRangePicker: React.FC = () => {
minDate={sixMonthsAgo} minDate={sixMonthsAgo}
maxDate={today} maxDate={today}
dateFormat="dd.MM.yyyy" dateFormat="dd.MM.yyyy"
className="border px-2 py-1 rounded" className={`border px-2 py-1 rounded ${inputWidthClass} ${
compact ? "text-xs" : "text-sm"
}`}
/> />
</div> </div>
</div> </div>

View File

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

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

View File

@@ -1,5 +1,6 @@
"use client"; // components/Header.jsx "use client"; // components/Header.jsx
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Icon } from "@iconify/react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import "bootstrap-icons/font/bootstrap-icons.css"; import "bootstrap-icons/font/bootstrap-icons.css";
@@ -56,8 +57,21 @@ function Header() {
}, [deviceName, dispatch]); }, [deviceName, dispatch]);
//---------------------------------------------------------------- //----------------------------------------------------------------
// Dark/Light Mode Toggle
const [isDark, setIsDark] = useState(false);
useEffect(() => {
if (typeof window !== "undefined") {
const html = document.documentElement;
if (isDark) {
html.classList.add("dark");
} else {
html.classList.remove("dark");
}
}
}, [isDark]);
return ( return (
<header className="bg-gray-300 flex justify-between items-center w-full h-[13vh] laptop:h-[10vh] relative text-black "> <header className="bg-gray-300 dark:bg-gray-800 flex justify-between items-center w-full h-[13vh] laptop:h-[10vh] relative text-black dark:text-white ">
<div <div
className="absolute transform -translate-y-1/2 className="absolute transform -translate-y-1/2
left-[8%] sm:left-[8%] md:left-[8%] lg:left-[8%] xl:left-[6%] 2xl:left-[2%] laptop:left-[4%] laptop: left-[8%] sm:left-[8%] md:left-[8%] lg:left-[8%] xl:left-[6%] 2xl:left-[2%] laptop:left-[4%] laptop:
@@ -88,38 +102,49 @@ function Header() {
priority priority
/> />
<div className="flex flex-col leading-tight whitespace-nowrap"> <div className="flex flex-col leading-tight whitespace-nowrap">
<h2 className="text-xl laptop:text-base xl:text-lg font-bold"> <h2 className="text-xl laptop:text-base xl:text-lg font-bold text-gray-900 dark:text-gray-100">
Meldestation Meldestation
</h2> </h2>
<p className="text-gray-600 text-lg laptop:text-sm xl:text-base truncate max-w-[20vw]"> <p className="text-gray-600 dark:text-gray-300 text-lg laptop:text-sm xl:text-base truncate max-w-[20vw]">
{deviceName} {deviceName}
</p> </p>
</div> </div>
</div> </div>
<div className="p-4 w-full lg:w-full flex flex-row gap-4 justify-between"> <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"> <div className="flex items-center justify-end w-full gap-4">
{/* Admin-Login */} {/* Dark/Light Mode Toggle */}
{/*
<button <button
onClick={handleSettingsClick} aria-label={isDark ? "Light Mode" : "Dark Mode"}
className="text-3xl text-black mr-0" onClick={() => setIsDark((d) => !d)}
className="rounded-full p-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition"
title={isDark ? "Light Mode" : "Dark Mode"}
> >
<i className="bi bi-gear"></i> {isDark ? (
</button> <Icon
*/} icon="mdi:weather-night"
</div> className="text-xl text-yellow-300"
/>
{/* Logout-Button */} ) : (
<Icon
<div className="flex items-center justify-end w-1/4 space-x-1"> icon="mdi:white-balance-sunny"
<button className="text-xl text-yellow-500"
onClick={handleLogout} />
className="bg-littwin-blue text-white px-4 py-2 rounded" )}
>
Abmelden
</button> </button>
</div> </div>
{/* Logout-Button - nur anzeigen wenn Admin eingeloggt ist */}
{isAdminLoggedIn && (
<div className="flex items-center justify-end w-1/4 space-x-1">
<button
onClick={handleLogout}
className="bg-littwin-blue text-white px-4 py-2 rounded"
>
Abmelden
</button>
</div>
)}
</div> </div>
{/* Warnhinweis, wenn der Admin angemeldet ist */} {/* Warnhinweis, wenn der Admin angemeldet ist */}

View File

@@ -12,7 +12,7 @@ const handleClearDatabase = async () => {
} }
// Full URL with host, current path, and clear database command // 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 // Log the full URL to the console for debugging
console.log(url); console.log(url);

View File

@@ -1,12 +1,10 @@
"use client"; "use client";
type AnalogInput = { // components/main/analogInputs/AnalogInputsChart.tsx
id: number; import React, { useEffect, useRef } from "react";
label: string; import { useDispatch, useSelector } from "react-redux";
unit: string; import { RootState, AppDispatch } from "@/redux/store";
}; import { Dialog } from "@headlessui/react";
import React, { useEffect } from "react";
import { Line } from "react-chartjs-2"; import { Line } from "react-chartjs-2";
import { getColor } from "@/utils/colors";
import { import {
Chart as ChartJS, Chart as ChartJS,
LineElement, LineElement,
@@ -17,14 +15,23 @@ import {
Legend, Legend,
Filler, Filler,
TimeScale, TimeScale,
TooltipItem,
} from "chart.js"; } from "chart.js";
import "chartjs-adapter-date-fns"; import "chartjs-adapter-date-fns";
import { de } from "date-fns/locale"; import { de } from "date-fns/locale";
import { useSelector, useDispatch } from "react-redux"; import { Listbox } from "@headlessui/react";
import type { RootState, AppDispatch } from "../../../redux/store";
import { getAnalogInputsHistoryThunk } from "@/redux/thunks/getAnalogInputsHistoryThunk"; 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( ChartJS.register(
LineElement, LineElement,
PointElement, PointElement,
@@ -37,148 +44,404 @@ ChartJS.register(
); );
export default function AnalogInputsChart({ 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(() => { useEffect(() => {
dispatch(getAnalogInputsHistoryThunk()); if (typeof window !== "undefined") {
}, [dispatch]); import("chartjs-plugin-zoom").then((zoom) => {
// ✅ Zoom-Plugin dynamisch importieren und registrieren
useEffect(() => {
const loadZoomPlugin = async () => {
if (typeof window !== "undefined") {
const zoomPlugin = (await import("chartjs-plugin-zoom")).default;
if (!ChartJS.registry.plugins.get("zoom")) { if (!ChartJS.registry.plugins.get("zoom")) {
ChartJS.register(zoomPlugin); ChartJS.register(zoom.default);
} }
} });
}; }
loadZoomPlugin();
}, []); }, []);
if (!selectedId) { const dispatch = useDispatch<AppDispatch>();
return (
<div className="text-gray-500">Bitte einen Messwerteingang auswählen</div>
);
}
const key = String(selectedId + 99); const chartRef =
const inputData = data[key]; useRef<
ChartJSOrUndefined<"line", { x: Date; y: number | undefined }[], unknown>
>(null);
if (!inputData) { // Redux Werte für Chart-Daten
return ( const { zeitraum, vonDatum, bisDatum, data, autoLoad, selectedId } =
<div className="text-red-500"> useSelector((state: RootState) => state.analogInputsHistory);
Keine Verlaufsdaten für Messwerteingang {selectedId} gefunden. const selectedAnalogInput = useSelector(
</div> (state: RootState) => state.selectedAnalogInput
); );
}
const chartData = { // Redux initiale Datum-Werte
datasets: [ const vonDatumRedux = useSelector(
{ (state: RootState) => state.dateRangePicker.vonDatum
label: `Messkurve ${selectedInput?.label ?? "Eingang"} [${ );
selectedInput?.unit ?? "" const bisDatumRedux = useSelector(
}]`, (state: RootState) => state.dateRangePicker.bisDatum
data: inputData.map((point: AnalogInputHistoryPoint) => ({ );
x: point.t,
y: point.m, // Hilfsfunktion für Default-Datum
})), const getDefaultDate = (type: "from" | "to") => {
fill: false, const today = new Date();
borderColor: getColor("littwin-blue"), if (type === "to") return today.toISOString().slice(0, 10);
backgroundColor: "rgba(59,130,246,0.5)", const fromDateObj = new Date(today);
borderWidth: 2, fromDateObj.setDate(today.getDate() - 30);
pointRadius: 0, return fromDateObj.toISOString().slice(0, 10);
pointHoverRadius: 10,
tension: 0.1,
},
],
}; };
const chartOptions = { // ✅ Lokale States für Picker + Zeitraum
responsive: true, const [localVonDatum, setLocalVonDatum] = React.useState(
plugins: { vonDatumRedux || getDefaultDate("from")
legend: { position: "top" as const }, );
tooltip: { const [localBisDatum, setLocalBisDatum] = React.useState(
mode: "index" as const, bisDatumRedux || getDefaultDate("to")
intersect: false, );
callbacks: { const [localZeitraum, setLocalZeitraum] = React.useState(zeitraum);
label: function (context: import("chart.js").TooltipItem<"line">) {
const y = context.parsed.y; // Synchronisiere lokale Werte mit Redux (z.B. nach AutoLoad Reset)
return `Messwert: ${y}`; useEffect(() => {
}, setLocalVonDatum(vonDatumRedux || getDefaultDate("from"));
title: function ( setLocalBisDatum(bisDatumRedux || getDefaultDate("to"));
tooltipItems: import("chart.js").TooltipItem<"line">[] setLocalZeitraum(zeitraum);
) { }, [vonDatumRedux, bisDatumRedux, zeitraum]);
const date = tooltipItems[0].parsed.x;
return `Zeitpunkt: ${new Date(date).toLocaleString("de-DE")}`; // 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,
title: { text: selectedAnalogInput?.label
display: true, ? `Verlauf: ${selectedAnalogInput.label}`
text: `Verlauf der letzten 30 Tage`, : "Messwert-Verlauf",
},
zoom: {
pan: {
enabled: true,
mode: "x" as const,
}, },
zoom: { zoom: {
wheel: { enabled: true }, pan: {
pinch: { enabled: true }, enabled: true,
mode: "x" as const, mode: "x" as const,
}, },
}, zoom: {
}, wheel: { enabled: true },
scales: { pinch: { enabled: true },
x: { mode: "x" as const,
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",
}, },
}, },
},
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: { // ✅ AutoLoad nur beim ersten Laden
date: { useEffect(() => {
locale: de, if (autoLoad && selectedId) {
}, dispatch(
}, getAnalogInputsHistoryThunk({
title: { eingang: selectedId,
display: true, zeitraum,
text: "Zeit", vonDatum,
}, bisDatum,
}, })
y: { );
title: { dispatch(setAutoLoad(false));
display: true, }
text: `Messwert [${selectedInput?.unit ?? ""}]`, }, [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 ( return (
<div className="w-full h-full"> <div
<Line data={chartData} options={chartOptions} /> className={`flex flex-col gap-2 h-full ${loading ? "cursor-wait" : ""}`}
>
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-2">
<div className="flex justify-start">
<Dialog.Title className="text-lg font-semibold text-gray-700">
Eingang {selectedId ?? ""}
</Dialog.Title>
</div>
<div className="flex justify-end">
<div className="flex flex-wrap items-center gap-4 mb-2">
{/* ✅ Neuer DatePicker mit schönem Styling (lokal, ohne Redux) */}
<AnalogInputsDatePicker
from={localVonDatum}
to={localBisDatum}
onChange={handleDateChange}
/>
{/* ✅ Zeitraum-Auswahl (Listbox nur lokal) */}
<Listbox value={localZeitraum} onChange={setLocalZeitraum}>
<div className="relative w-48">
<Listbox.Button className="w-full border px-3 py-1 rounded bg-white flex justify-between items-center text-sm">
<span>
{localZeitraum === "DIA0"
? "Alle Messwerte"
: localZeitraum === "DIA1"
? "Stündlich"
: "Täglich"}
</span>
<i className="bi bi-chevron-down text-gray-400" />
</Listbox.Button>
<Listbox.Options className="absolute z-10 mt-1 w-full border bg-white shadow rounded text-sm">
{["DIA0", "DIA1", "DIA2"].map((option) => (
<Listbox.Option
key={option}
value={option}
className="px-4 py-1 cursor-pointer hover:bg-gray-200"
>
{option === "DIA0"
? "Alle Messwerte"
: option === "DIA1"
? "Stündlich"
: "Täglich"}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
{/* ✅ Button: lädt die Daten & aktualisiert Redux */}
<button
onClick={handleFetchData}
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm"
>
Daten laden
</button>
</div>
</div>
</div>
{/* Chart-Anzeige */}
<div className="flex-1 min-h-0 w-full">
{!selectedAnalogInput?.id ? (
<div className="flex items-center justify-center h-full text-gray-500 text-lg gap-2">
<i className="bi bi-info-circle text-2xl mr-2" />
<span>
Bitte wählen Sie einen Eingang aus, um die Messkurve anzuzeigen
</span>
</div>
) : (
<Line
ref={chartRef}
data={memoizedChartData}
options={memoizedChartOptions}
style={{ height: "100%", width: "100%" }}
/>
)}
</div>
</div> </div>
); );
} }

View File

@@ -0,0 +1,120 @@
"use client";
import React from "react";
import { Dialog } from "@headlessui/react";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "@/redux/store";
import { setIsChartModalOpen } from "@/redux/slices/analogInputs/analogInputsUiSlice";
import AnalogInputsChart from "@/components/main/analogInputs/AnalogInputsChart";
export default function AnalogInputsChartModal({
loading,
setLoading,
}: {
loading: boolean;
setLoading: (v: boolean) => void;
}) {
const dispatch = useDispatch();
const isOpen = useSelector(
(state: RootState) => state.analogInputsUi.isChartModalOpen
);
const selectedId = useSelector(
(state: RootState) => state.analogInputsHistory.selectedId
);
const [isFullscreen, setIsFullscreen] = React.useState(false);
if (!isOpen) return null;
return (
<Dialog
open={isOpen}
onClose={() => dispatch(setIsChartModalOpen(false))}
className="relative z-[9999]"
>
{/* Backdrop */}
<div className="fixed inset-0 bg-black/50" aria-hidden="true" />
{/* Centered panel */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="relative">
<div
className="bg-white rounded-xl shadow-xl border border-gray-200"
style={{
width: isFullscreen ? "90vw" : "70rem",
height: isFullscreen ? "90vh" : "35rem",
padding: "1rem",
transition: "all 0.3s ease-in-out",
display: "flex",
flexDirection: "column",
}}
>
{/* Controls top-right (fullscreen + close) */}
<div
style={{
position: "absolute",
top: "0.625rem",
right: "0.625rem",
display: "flex",
gap: "0.75rem",
}}
>
<button
onClick={() => setIsFullscreen((v) => !v)}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
title={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
aria-label={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
>
<i
className={
isFullscreen
? "bi bi-fullscreen-exit"
: "bi bi-arrows-fullscreen"
}
></i>
</button>
<button
onClick={() => dispatch(setIsChartModalOpen(false))}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
title="Schließen"
aria-label="Modal schließen"
>
<i className="bi bi-x-circle-fill"></i>
</button>
</div>
{/* Title row (align like IsoChartView) */}
<div className="flex justify-between items-center mb-2 pr-24">
<Dialog.Title className="text-lg font-semibold text-gray-700">
Messkurve Messwerteingang {selectedId ?? ""}
</Dialog.Title>
</div>
{/* Chart container (structure similar to IsoChartView) */}
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
height: "90%",
}}
>
{/* Optional: place an action bar here if needed */}
<div style={{ flex: 1, height: "90%" }}>
<AnalogInputsChart loading={loading} setLoading={setLoading} />
</div>
</div>
</div>
</Dialog.Panel>
</div>
</Dialog>
);
}

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 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 { import type { AnalogInput } from "@/types/analogInput"; // 👈 Importiere den Typ (jetzt definiert und exportiert)
id: number;
label?: string;
offset?: number | string;
factor?: number | string;
loggerInterval: string;
unit?: string;
}
interface Props { export default function AnalogInputsSettingsModal() {
selectedInput: AnalogInput; const dispatch = useDispatch();
isOpen: boolean;
onClose: () => void; 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 [label, setLabel] = useState("");
const [offset, setOffset] = useState("0.000"); const [offset, setOffset] = useState("0.000");
const [factor, setFactor] = useState("1.000"); const [factor, setFactor] = useState("1.000");
@@ -28,6 +26,8 @@ export default function AnalogInputSettingsModal({
const [unit, setUnit] = useState("V"); const [unit, setUnit] = useState("V");
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const unitOptions = ["V", "mA", "°C", "bar", "%"];
useEffect(() => { useEffect(() => {
if (selectedInput && isOpen) { if (selectedInput && isOpen) {
setLabel(selectedInput.label || ""); setLabel(selectedInput.label || "");
@@ -41,12 +41,8 @@ export default function AnalogInputSettingsModal({
? selectedInput.factor.toFixed(3) ? selectedInput.factor.toFixed(3)
: selectedInput.factor || "1.000" : selectedInput.factor || "1.000"
); );
setLoggerInterval(selectedInput.loggerInterval); setLoggerInterval(selectedInput.loggerInterval || "9");
setUnit(selectedInput.unit || "V"); setUnit(selectedInput.unit || "V");
console.log(
"selectedInput in analoge Eingänge:",
selectedInput.loggerInterval
);
} }
}, [selectedInput, isOpen]); }, [selectedInput, isOpen]);
@@ -54,6 +50,7 @@ export default function AnalogInputSettingsModal({
const handleSave = async () => { const handleSave = async () => {
setIsSaving(true); setIsSaving(true);
const slot = selectedInput.id; const slot = selectedInput.id;
const isDev = window.location.hostname === "localhost"; const isDev = window.location.hostname === "localhost";
@@ -99,7 +96,7 @@ export default function AnalogInputSettingsModal({
alert("Einstellungen gespeichert (Produktion)."); alert("Einstellungen gespeichert (Produktion).");
} }
onClose(); dispatch(setIsSettingsModalOpen(false));
location.reload(); location.reload();
} catch (err) { } catch (err) {
alert("Fehler beim Speichern."); alert("Fehler beim Speichern.");
@@ -117,7 +114,7 @@ export default function AnalogInputSettingsModal({
Einstellungen Messwerteingang {selectedInput.id} Einstellungen Messwerteingang {selectedInput.id}
</h2> </h2>
<button <button
onClick={onClose} onClick={() => dispatch(setIsSettingsModalOpen(false))}
className="text-2xl hover:text-gray-400" className="text-2xl hover:text-gray-400"
aria-label="Modal schließen" aria-label="Modal schließen"
> >
@@ -127,69 +124,85 @@ export default function AnalogInputSettingsModal({
{/* Bezeichnung */} {/* Bezeichnung */}
<div className="grid grid-cols-2 gap-x-4 gap-y-3"> <div className="grid grid-cols-2 gap-x-4 gap-y-3">
<div> <span className="font-normal">Bezeichnung:</span>
<span className="font-normal">Bezeichnung:</span> <input
</div> type="text"
<div> className="w-full border rounded px-3 py-1 mb-4"
<input value={label}
type="text" onChange={(e) => setLabel(e.target.value)}
className="w-full border rounded px-3 py-1 mb-4" />
value={label}
onChange={(e) => setLabel(e.target.value)}
/>
</div>
</div> </div>
{/* Offset */} {/* Offset */}
<div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4"> <div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4">
<div> <span className="font-normal">Offset:</span>
<span className="font-normal">Offset:</span> <input
</div> type="number"
<div> step="0.001"
<input className="border border-gray-300 rounded px-2 py-1 w-full text-right"
type="number" value={offset}
step="0.001" onChange={(e) => setOffset(e.target.value)}
className="border border-gray-300 rounded px-2 py-1 w-full text-right " />
value={offset}
onChange={(e) => setOffset(e.target.value)}
/>
</div>
</div> </div>
{/* Faktor */} {/* Faktor */}
<div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4"> <div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4">
<div> <span className="font-normal">Faktor:</span>
<span className="font-normal">Faktor:</span> <input
</div> type="number"
<div> step="0.001"
<input className="border border-gray-300 rounded px-2 py-1 w-full text-right"
type="number" value={factor}
step="0.001" onChange={(e) => setFactor(e.target.value)}
className="border border-gray-300 rounded px-2 py-1 w-full text-right" />
value={factor}
onChange={(e) => setFactor(e.target.value)}
/>{" "}
</div>
</div> </div>
{/* Einheit */}
{/* Einheit */}
<div className="grid grid-cols-2 gap-x-4 gap-y-3 mb-4">
<span className="font-normal">Einheit:</span>
<Listbox value={unit} onChange={setUnit}>
<div className="relative w-full">
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm text-gray-900 font-sans">
<span>{unit}</span>
<svg
className="w-5 h-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm text-gray-900 font-sans">
{unitOptions.map((opt) => (
<Listbox.Option
key={opt}
value={opt}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer ${
selected
? "bg-littwin-blue text-white font-medium"
: active
? "bg-gray-200"
: "text-gray-900"
}`
}
>
{opt}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
{/* Speicherintervall */}
<div className="grid grid-cols-2 gap-x-4 gap-y-3"> <div 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> <span className="font-normal">Speicherintervall:</span>
<div className="relative w-full"> <div className="relative w-full">
<input <input
@@ -204,6 +217,7 @@ export default function AnalogInputSettingsModal({
</div> </div>
</div> </div>
{/* Speichern-Button */}
<div className="flex justify-end gap-2 mt-6"> <div className="flex justify-end gap-2 mt-6">
<button <button
onClick={handleSave} onClick={handleSave}

View File

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

View File

@@ -0,0 +1,56 @@
"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="container mx-auto">
<div className="grid grid-cols-1 gap-4 justify-items-start">
<div className="bg-white dark:bg-gray-900 rounded-lg p-4 max-w-3xl text-gray-900 dark:text-gray-100">
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
Messwerteingänge
</h2>
<AnalogInputsTable loading={loading} />
</div>
</div>
</div>
{selectedInput !== null && <AnalogInputsSettingsModal />}
{/* Chart Modal */}
<AnalogInputsChartModal loading={loading} setLoading={setLoading} />
</div>
);
}
export default AnalogInputsView;

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-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
{/* Header */}
<div className="flex justify-between items-center w-full lg:w-2/3">
<div className="flex justify-between gap-1">
<Icon
icon="ri:calendar-schedule-line"
className="text-littwin-blue text-4xl xl:text-2xl"
/>
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100 xl:text-base">
Letzten 20 Meldungen
</h1>
</div>
</div>
{/* Hauptbereich mit Meldungstabelle und Baugruppenträger */}
<div className="flex flex-col lg:flex-row gap-4 flex-grow overflow-hidden pt-4">
<Last20MessagesTable className="w-full lg:w-2/3 h-full" />
<div className="shadow-md rounded-lg w-full lg:w-1/3 flex flex-col gap-2">
<VersionInfo className="w-full p-3 text-sm" />
{/* Baugruppenträger jetzt mit voller Breite */}
<div className="overflow-auto max-h-[50vh]">
<Baugruppentraeger />
</div>
</div>
</div>
{/* NetworkInfo in einem div ,nimmt die gesamte Breite */}
<NetworkInfo />
</div>
);
};
export default DashboardView;

View File

@@ -48,35 +48,56 @@ export default function Last20MessagesTable({ className }: Props) {
return ( return (
<div className={`flex flex-col gap-3 p-4 ${className}`}> <div className={`flex flex-col gap-3 p-4 ${className}`}>
<div className="overflow-auto max-h-[80vh]"> <div className="overflow-auto max-h-[80vh]">
<table className="min-w-full border"> <table className="min-w-full border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<thead className="bg-gray-100 text-left sticky top-0 z-10"> <thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10">
<tr> <tr>
<th className="p-2 border">Prio</th> <th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<th className="p-2 border">Zeitstempel</th> Prio
<th className="p-2 border">Quelle</th> </th>
<th className="p-2 border">Meldung</th> <th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<th className="p-2 border">Status</th> Zeitstempel
</th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
Quelle
</th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
Meldung
</th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
Status
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{filteredMessages.slice(0, 20).map((msg, index) => ( {filteredMessages.slice(0, 20).map((msg, index) => (
<tr key={index} className="hover:bg-gray-50"> <tr
<td className="border p-2"> key={index}
className="hover:bg-gray-100 dark:hover:bg-gray-800"
>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<div <div
className="w-4 h-4 rounded" className="w-4 h-4 rounded"
style={{ backgroundColor: msg.c }} style={{ backgroundColor: msg.c }}
></div> ></div>
</td> </td>
<td className="border p-2">{msg.t}</td> <td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<td className="border p-2">{msg.i}</td> {msg.t}
<td className="border p-2">{msg.m}</td> </td>
<td className="border p-2">{msg.v}</td> <td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
{msg.i}
</td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
{msg.m}
</td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
{msg.v}
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
{messages.length === 0 && ( {messages.length === 0 && (
<div className="mt-4 text-center text-gray-500 italic"> <div className="mt-4 text-center text-gray-500 italic dark:text-gray-400">
Keine Meldungen im gewählten Zeitraum vorhanden. Keine Meldungen im gewählten Zeitraum vorhanden.
</div> </div>
)} )}

View File

@@ -38,7 +38,7 @@ const NetworkInfo: React.FC = () => {
return ( return (
<div className="w-full flex-direction: row flex"> <div className="w-full flex-direction: row flex">
<div className=" flex-grow flex justify-between items-center mt-1 bg-white p-2 rounded-lg shadow-md border border-gray-200 laptop:m-0 laptop:scale-y-75 2xl:scale-y-75"> <div className=" flex-grow flex justify-between items-center mt-1 bg-white dark:bg-gray-800 p-2 rounded-lg shadow-md border border-gray-200 dark:border-gray-700 laptop:m-0 laptop:scale-y-75 2xl:scale-y-75">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<Image <Image
src="/images/IP-icon.svg" src="/images/IP-icon.svg"
@@ -49,8 +49,12 @@ const NetworkInfo: React.FC = () => {
priority priority
/> />
<div> <div>
<p className="text-xs text-gray-500">IP-Adresse</p> <p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-sm font-medium text-gray-700">{ip}</p> IP-Adresse
</p>
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
{ip}
</p>
</div> </div>
</div> </div>
@@ -64,8 +68,12 @@ const NetworkInfo: React.FC = () => {
priority priority
/> />
<div> <div>
<p className="text-xs text-gray-500">Subnet-Maske</p> <p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-sm font-medium text-gray-700">{subnet}</p> Subnet-Maske
</p>
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
{subnet}
</p>
</div> </div>
</div> </div>
@@ -79,16 +87,20 @@ const NetworkInfo: React.FC = () => {
priority priority
/> />
<div> <div>
<p className="text-xs text-gray-500">Gateway</p> <p className="text-xs text-gray-500 dark:text-gray-400">Gateway</p>
<p className="text-sm font-medium text-gray-700">{gateway}</p> <p className="text-sm font-medium text-gray-700 dark:text-gray-200">
{gateway}
</p>
</div> </div>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="text-xs font-bold text-littwin-blue">OPC-UA</div> <div className="text-xs font-bold text-littwin-blue">OPC-UA</div>
<div> <div>
<p className="text-xs text-gray-500">Status</p> <p className="text-xs text-gray-500 dark:text-gray-400">Status</p>
<p className="text-sm font-medium text-gray-700">{opcUaZustand}</p> <p className="text-sm font-medium text-gray-700 dark:text-gray-200">
{opcUaZustand}
</p>
</div> </div>
</div> </div>
{/* OPC UA Nodeset Name */} {/* OPC UA Nodeset Name */}

View File

@@ -18,22 +18,24 @@ const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
return ( return (
<div <div
className={`bg-gray-50 rounded-lg shadow-sm border border-gray-200 w-full laptop:p-2 ${className}`} className={`bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 w-full laptop:p-2 ${className}`}
> >
<h2 className="text-lg font-semibold text-gray-700 mb-2"> <h2 className="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">
Versionsinformationen Versionsinformationen
</h2> </h2>
<div className="flex flex-row p-2 space-x-2"> <div className="flex flex-row p-2 space-x-2">
<Icon icon="bx:code-block" className="text-xl text-blue-400" /> <Icon icon="bx:code-block" className="text-xl text-blue-400" />
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600 dark:text-gray-300">
Applikationsversion: {appVersion} Applikationsversion: {appVersion}
</p> </p>
</div> </div>
<div className="flex flex-row p-2 space-x-2"> <div className="flex flex-row p-2 space-x-2">
<Icon icon="mdi:web" className="text-xl text-blue-400" /> <Icon icon="mdi:web" className="text-xl text-blue-400" />
<p className="text-sm text-gray-600">Webversion: {webVersion}</p> <p className="text-sm text-gray-600 dark:text-gray-300">
Webversion: {webVersion}
</p>
</div> </div>
</div> </div>
); );

View File

@@ -18,10 +18,18 @@ const KabelModulStatus: React.FC<KabelModulStatusProps> = ({
// Modultyp basierend auf der Version bestimmen // Modultyp basierend auf der Version bestimmen
let moduleName = ""; let moduleName = "";
let moduleType = ""; let moduleType = "";
if (moduleVersion === 419) { if (moduleVersion === 419) {
moduleName = "KÜ705"; moduleName = "KÜ705";
moduleType = "FO"; 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) { } else if (moduleVersion === 350) {
moduleName = "KÜ605"; moduleName = "KÜ605";
moduleType = "µC"; 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); //console.log("DigitalInputs", inputs);
return ( return (
<div className="bg-white shadow-md border border-gray-200 p-3 rounded-lg w-full laptop:p-1 xl:p-1"> <div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 shadow-md border border-gray-200 dark:border-gray-700 p-3 rounded-lg w-full laptop:p-1 xl:p-1">
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center"> <h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
<Icon <Icon
icon={inputIcon} icon={inputIcon}
@@ -38,19 +38,30 @@ export default function DigitalInputsWidget({
/> />
Meldungseingänge {inputRange.start + 1} {inputRange.end} Meldungseingänge {inputRange.start + 1} {inputRange.end}
</h2> </h2>
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse"> <table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white dark:bg-gray-900">
<thead className="bg-gray-100 border-b"> <thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b">
<tr> <tr>
<th className="px-1 py-1 text-left">Eingang</th> <th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<th className="px-1 py-1 text-left">Zustand</th> Eingang
<th className="px-1 py-1 text-left">Bezeichnung</th> </th>
<th className="px-1 py-1 text-left">Aktion</th> <th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
Zustand
</th>
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
Bezeichnung
</th>
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
Aktion
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{inputs.map((input) => ( {inputs.map((input) => (
<tr key={input.id} className="border-b"> <tr
<td className="px-1 py-0"> key={input.id}
className="border-b hover:bg-gray-100 dark:hover:bg-gray-800"
>
<td className="px-1 py-0 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<div className="flex items-center gap-1 "> <div className="flex items-center gap-1 ">
<Icon <Icon
icon={loginIcon} icon={loginIcon}
@@ -59,7 +70,7 @@ export default function DigitalInputsWidget({
{input.id} {input.id}
</div> </div>
</td> </td>
<td className="px-1 py-1 "> <td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
{input.eingangOffline ? ( {input.eingangOffline ? (
<div className="relative group inline-block"> <div className="relative group inline-block">
<span className="text-red-500 sm:text-sm md:text-base lg:text-lg xl:text-xl 2xl:text-2xl laptop:text-sm "> <span className="text-red-500 sm:text-sm md:text-base lg:text-lg xl:text-xl 2xl:text-2xl laptop:text-sm ">
@@ -80,11 +91,13 @@ export default function DigitalInputsWidget({
</div> </div>
)} )}
</td> </td>
<td className="px-1 py-1">{input.label}</td> <td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<td className="px-1 py-1"> {input.label}
</td>
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<Icon <Icon
icon={settingsIcon} 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)} onClick={() => openInputModal(input)}
/> />
</td> </td>

View File

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

View File

@@ -0,0 +1,57 @@
"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(() => {
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 { try {
if (isCPL) { if (isCPL) {
window.location.href = `/CPL?digitalOutputs.html&DAS0${id}=${ // Statt redirect:
updatedOutputs[id - 1].status ? 1 : 0 // 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 { } else {
await fetch("/api/cpl/updateDigitalOutputsHandler", { await fetch("/api/cpl/updateDigitalOutputsHandler", {
method: "POST", method: "POST",
@@ -50,7 +66,7 @@ export default function DigitalOutputsWidget({
}; };
return ( return (
<div className="bg-white shadow-md border border-gray-200 p-3 rounded-lg w-full h-fit max-h-[400px] overflow-auto"> <div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 shadow-md border border-gray-200 dark:border-gray-700 p-3 rounded-lg w-full h-fit max-h-[400px] overflow-auto">
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center"> <h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
<Icon <Icon
icon={outputIcon} icon={outputIcon}
@@ -58,41 +74,54 @@ export default function DigitalOutputsWidget({
/> />
Schaltausgänge Schaltausgänge
</h2> </h2>
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white rounded-lg"> <table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white dark:bg-gray-900 rounded-lg">
<thead className="bg-gray-100 border-b"> <thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b">
<tr> <tr>
<th className="px-1 py-1 text-left">Ausgang</th> <th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<th className="px-1 py-1 text-left">Bezeichnung</th> Ausgang
<th className="px-1 py-1 text-left">Schalter</th> </th>
<th className="px-1 py-1 text-left">Aktion</th> <th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
Bezeichnung
</th>
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
Schalter
</th>
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
Aktion
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{digitalOutputs.map((output) => ( {digitalOutputs.map((output) => (
<tr key={output.id} className="border-b"> <tr
<td className="flex items-center px-1 py-1"> key={output.id}
className="border-b hover:bg-gray-100 dark:hover:bg-gray-800"
>
<td className="flex items-center px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<Icon <Icon
icon={outputIcon} icon={outputIcon}
className="text-gray-600 mr-1 text-base" className="text-gray-600 mr-1 text-base"
/> />
{output.id} {output.id}
</td> </td>
<td className="px-1 py-1">{output.label}</td> <td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<td className="px-1 py-1"> {output.label}
</td>
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<Icon <Icon
icon={switchIcon} icon={switchIcon}
className={`cursor-pointer text-base transition ${ className={`cursor-pointer text-base transition ${
output.status output.status
? "text-littwin-blue" ? "text-littwin-blue"
: "text-gray-500 scale-x-[-1]" : "text-gray-500 scale-x-[-1]"
}`} } dark:hover:text-littwin-blue`}
onClick={() => handleToggle(output.id)} onClick={() => handleToggle(output.id)}
/> />
</td> </td>
<td className="px-1 py-1"> <td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<Icon <Icon
icon={settingsIcon} icon={settingsIcon}
className="text-gray-400 text-base cursor-pointer" className="text-gray-400 text-base cursor-pointer dark:text-gray-300 dark:hover:text-white"
onClick={() => openOutputModal(output)} onClick={() => openOutputModal(output)}
/> />
</td> </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,176 @@
"use client"; // /pages/kabelueberwachung.tsx
import React, { useState, useEffect } from "react";
import { useSearchParams } from "next/navigation";
import Kue705FO from "@/components/main/kabelueberwachung/kue705FO/Kue705FO";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch } from "@/redux/store"; // Adjust the path to your Redux store file
import { RootState } from "@/redux/store"; // Adjust the path to your Redux store file
import { getKueDataThunk } from "@/redux/thunks/getKueDataThunk";
function KabelueberwachungView() {
const dispatch: AppDispatch = useDispatch();
const searchParams = useSearchParams(); // URL-Parameter holen
const initialRack = parseInt(searchParams.get("rack") ?? "1") || 1; // Rack-Nummer aus URL oder 1
const [activeRack, setActiveRack] = useState<number>(initialRack); // Nutze initialRack als Startwert
const [alarmStatus, setAlarmStatus] = useState<boolean[]>([]); // Alarmstatus
// Redux-Variablen aus dem Store abrufen
const {
kueOnline,
kueID,
kueIso,
kueAlarm1,
kueAlarm2,
kueResidence,
kueCableBreak,
kueGroundFault,
} = useSelector((state: RootState) => state.kueDataSlice);
//----------------------------------------------------------------
// Alarmstatus basierend auf Redux-Variablen berechnen
const updateAlarmStatus = React.useCallback(() => {
const updatedAlarmStatus = kueIso.map(
(_: number | string, index: number) => {
return Boolean(
(kueAlarm1 && kueAlarm1[index]) ||
(kueAlarm2 && kueAlarm2[index]) ||
(kueCableBreak && kueCableBreak[index]) ||
(kueGroundFault && kueGroundFault[index])
);
}
);
setAlarmStatus(updatedAlarmStatus);
}, [kueIso, kueAlarm1, kueAlarm2, kueCableBreak, kueGroundFault]);
// Alarmstatus initial berechnen und alle 10 Sekunden aktualisieren
useEffect(() => {
updateAlarmStatus();
const interval = setInterval(updateAlarmStatus, 10000);
return () => clearInterval(interval);
}, [updateAlarmStatus]);
// Modul- und Rack-Daten aufbereiten
const allModules = kueIso.map((iso: number | string, index: number) => ({
isolationswert: iso,
schleifenwiderstand: kueResidence[index],
modulName: kueID[index] || `Modul ${index + 1}`, // Eindeutiger Name pro Index
kueOnlineStatus: kueOnline[index],
alarmStatus: alarmStatus[index],
tdrLocation: [], // Placeholder, replace with actual tdrLocation if available
win_fallSensorsActive: kueOnline[index] ? 1 : 0, // Beispielwert, anpassen je nach Logik
}));
//console.log("Alle Module:", allModules);
const racks = React.useMemo(
() => ({
rack1: allModules.slice(0, 8),
rack2: allModules.slice(8, 16),
rack3: allModules.slice(16, 24),
rack4: allModules.slice(24, 32),
}),
[allModules]
);
// Konsolenausgaben für jede Rack-Aufteilung
/* console.log(
"Rack 1 Module:",
racks.rack1.map((slot) => slot.modulName)
);
console.log(
"Rack 2 Module:",
racks.rack2.map((slot) => slot.modulName)
);
console.log(
"Rack 3 Module:",
racks.rack3.map((slot) => slot.modulName)
);
console.log(
"Rack 4 Module:",
racks.rack4.map((slot) => slot.modulName)
); */
// Funktion zum Wechseln des Racks
const changeRack = (rack: number) => {
setActiveRack(rack);
console.log(`Aktives Rack geändert zu: ${rack}`);
};
useEffect(() => {
/* console.log(`Aktives Rack: ${activeRack}`);
console.log(
`Rack ${activeRack} Modulnamen:`,
racks[`rack${activeRack as 1 | 2 | 3 | 4}` as keyof typeof racks].map((slot: any) => slot.modulName)
); */
}, [activeRack, racks]);
//-----------------------------------------------------------
//------------------------------------------------------------
useEffect(() => {
if (kueIso.length === 0) {
console.log("📦 Lade KUE-Daten aus getKueDataThunk...");
dispatch(getKueDataThunk());
}
}, [dispatch, kueIso.length]);
//------------------------------------------------------------
// JSX rendering
return (
<div>
<div className="mb-4">
{[1, 2, 3, 4].map((rack) => (
<button
key={rack}
onClick={() => changeRack(rack)}
className={`mr-2 ${
Number(activeRack) === Number(rack)
? "bg-littwin-blue text-white p-1 rounded-sm"
: "bg-gray-300 p-1 text-sm"
}`}
>
Rack {rack}
</button>
))}
</div>
<div className="flex flex-row space-x-8 xl:space-x-0 2xl:space-x-8 qhd:space-x-16 ml-[5%] mt-[5%]">
{(
racks[
`rack${activeRack as 1 | 2 | 3 | 4}` as keyof typeof racks
] as typeof allModules
).map(
(
slot: {
isolationswert: number | string;
schleifenwiderstand: number | string;
modulName: string;
kueOnlineStatus: number;
alarmStatus?: boolean;
tdrLocation: number[];
win_fallSensorsActive: number;
},
index: number
) => {
const slotIndex = index + (activeRack - 1) * 8;
return (
<div key={index} className="flex">
<Kue705FO
isolationswert={slot.isolationswert}
schleifenwiderstand={slot.schleifenwiderstand}
modulName={slot.modulName}
kueOnline={slot.kueOnlineStatus}
alarmStatus={slot.alarmStatus}
slotIndex={slotIndex}
tdrLocation={slot.tdrLocation}
win_fallSensorsActive={slot.win_fallSensorsActive}
/>
</div>
);
}
)}
</div>
</div>
);
}
export default KabelueberwachungView;

View File

@@ -0,0 +1,389 @@
"use client";
// /components/main/kabelueberwachung/kue705FO/Charts/IsoMeasurementChart/IsoChartActionBar.tsx
import React, { forwardRef, useImperativeHandle } from "react";
import DateRangePicker from "@/components/common/DateRangePicker";
import { useSelector } from "react-redux";
import { RootState, useAppDispatch } from "@/redux/store";
import {
setIsoMeasurementCurveChartData,
setSelectedMode,
setChartOpen,
setLoading,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { setBrushRange } from "@/redux/slices/brushSlice";
import { getMessagesThunk } from "@/redux/thunks/getMessagesThunk";
import { Listbox } from "@headlessui/react";
//-----------------------------------------------------------------------------------useIsoChartLoader
export const useIsoChartLoader = () => {
const dispatch = useAppDispatch();
const { vonDatum, bisDatum, selectedMode, slotNumber } = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice
);
const hasShownNoDataAlert = React.useRef(false);
const formatDate = (dateString: string) => {
const [year, month, day] = dateString.split("-");
return `${year};${month};${day}`;
};
const getApiUrl = (mode: "DIA0" | "DIA1" | "DIA2", slotNumber: number) => {
const type = 3; // Fest auf Isolationswiderstand gesetzt
const typeFolder = "isolationswiderstand";
let url: string;
if (process.env.NODE_ENV === "development") {
url = `/api/cpl/slotDataAPIHandler?slot=${slotNumber}&messart=${typeFolder}&dia=${mode}&vonDatum=${vonDatum}&bisDatum=${bisDatum}`;
} else {
url = `${window.location.origin}/CPL?seite.ACP&${mode}=${formatDate(
vonDatum
)};${formatDate(bisDatum)};${slotNumber};${type};`;
}
console.log("API URL:", url);
return url;
};
const loadIsoChartData = async () => {
if (slotNumber === null) return;
dispatch(setLoading(true));
dispatch(setChartOpen(false));
dispatch(setIsoMeasurementCurveChartData([]));
const startTime = Date.now();
const MIN_LOADING_TIME_MS = 1000;
try {
const apiUrl = getApiUrl(selectedMode, slotNumber);
const response = await fetch(apiUrl);
const data = await response.json();
const waitTime = Math.max(
0,
MIN_LOADING_TIME_MS - (Date.now() - startTime)
);
await new Promise((res) => setTimeout(res, waitTime));
if (Array.isArray(data) && data.length > 0) {
dispatch(setIsoMeasurementCurveChartData(data));
dispatch(setChartOpen(true));
} else {
dispatch(setIsoMeasurementCurveChartData([]));
dispatch(setChartOpen(false));
if (!hasShownNoDataAlert.current) {
alert("⚠️ Keine Messdaten im gewählten Zeitraum gefunden");
hasShownNoDataAlert.current = true;
}
}
} catch (err) {
console.error("❌ Fehler beim Laden:", err);
alert("❌ Fehler beim Laden.");
} finally {
dispatch(setLoading(false));
}
};
return { loadIsoChartData };
};
//-----------------------------------------------------------------------------------useIsoDataLoader Hook
export const useIsoDataLoader = () => {
const dispatch = useAppDispatch();
const { vonDatum, bisDatum, selectedMode, slotNumber } = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice
);
const formatDate = (dateString: string) => {
const [year, month, day] = dateString.split("-");
return `${year};${month};${day}`;
};
const getApiUrl = (mode: "DIA0" | "DIA1" | "DIA2", slotNumber: number) => {
const type = 3; // Fest auf Isolationswiderstand gesetzt
const typeFolder = "isolationswiderstand";
const baseUrl =
process.env.NODE_ENV === "development"
? `/api/cpl/slotDataAPIHandler?slot=${slotNumber}&messart=${typeFolder}&dia=${mode}&vonDatum=${vonDatum}&bisDatum=${bisDatum}`
: `${window.location.origin}/CPL?seite.ACP&${mode}=${formatDate(
vonDatum
)};${formatDate(bisDatum)};${slotNumber};${type};`;
return baseUrl;
};
const loadData = async () => {
if (slotNumber === null) {
console.log("⚠️ Kein Slot ausgewählt - automatisches Laden übersprungen");
return;
}
const apiUrl = getApiUrl(selectedMode, slotNumber);
if (!apiUrl) return;
dispatch(setLoading(true));
dispatch(setChartOpen(false));
dispatch(setIsoMeasurementCurveChartData([]));
const MIN_LOADING_TIME_MS = 1000;
const startTime = Date.now();
try {
const response = await fetch(apiUrl, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) throw new Error(`Fehler: ${response.status}`);
const jsonData = await response.json();
const elapsedTime = Date.now() - startTime;
const waitTime = Math.max(0, MIN_LOADING_TIME_MS - elapsedTime);
await new Promise((resolve) => setTimeout(resolve, waitTime));
console.log("▶️ Automatisches Laden - Isolationswiderstand-Daten für:");
console.log(" Slot:", slotNumber);
console.log(" Modus:", selectedMode);
console.log(" Von:", vonDatum);
console.log(" Bis:", bisDatum);
if (Array.isArray(jsonData) && jsonData.length > 0) {
dispatch(setIsoMeasurementCurveChartData(jsonData));
dispatch(setChartOpen(true));
} else {
console.log(
"⚠️ Keine Messdaten im gewählten Zeitraum gefunden (automatisches Laden)"
);
dispatch(setIsoMeasurementCurveChartData([]));
dispatch(setChartOpen(false));
}
} catch (err) {
console.error("❌ Fehler beim automatischen Laden der Daten:", err);
} finally {
dispatch(setLoading(false));
}
};
return { loadData };
};
//-----------------------------------------------------------------------------------IsoChartActionBar
// ...existing code...
const IsoChartActionBar = forwardRef((_props, ref) => {
IsoChartActionBar.displayName = "IsoChartActionBar";
const dispatch = useAppDispatch();
const { vonDatum, bisDatum, selectedMode, slotNumber, chartTitle } =
useSelector((state: RootState) => state.kabelueberwachungChartSlice);
// Aus DateRangePicker-Slice kommen die Werte, die der User im UI wählt
const { vonDatum: pickerVonDatum, bisDatum: pickerBisDatum } = useSelector(
(state: RootState) => state.dateRangePicker
);
const formatDate = (dateString: string) => {
const [year, month, day] = dateString.split("-");
return `${year};${month};${day}`;
};
const getApiUrl = (
mode: "DIA0" | "DIA1" | "DIA2",
slotNumber: number,
fromDate: string,
toDate: string
) => {
const type = 3; // Fest auf Isolationswiderstand gesetzt
const typeFolder = "isolationswiderstand";
const baseUrl =
process.env.NODE_ENV === "development"
? `/api/cpl/slotDataAPIHandler?slot=${slotNumber}&messart=${typeFolder}&dia=${mode}&vonDatum=${fromDate}&bisDatum=${toDate}`
: `${window.location.origin}/CPL?seite.ACP&${mode}=${formatDate(
fromDate
)};${formatDate(toDate)};${slotNumber};${type};`;
console.log("baseUrl", baseUrl);
return baseUrl;
};
const handleFetchData = async () => {
if (slotNumber === null) {
alert("⚠️ Bitte zuerst einen KÜ auswählen!");
return;
}
// Wenn Meldungen-Ansicht aktiv ist, dann Meldungen laden
if (chartTitle === "Meldungen") {
try {
dispatch(setLoading(true));
const fromDate = pickerVonDatum ?? vonDatum;
const toDate = pickerBisDatum ?? bisDatum;
await dispatch(getMessagesThunk({ fromDate, toDate })).unwrap();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error("❌ Fehler beim Laden der Meldungen:", message);
alert("❌ Fehler beim Laden der Meldungen.");
} finally {
dispatch(setLoading(false));
}
return;
}
// Messkurve (ISO) laden
const fromDate = pickerVonDatum ?? vonDatum;
const toDate = pickerBisDatum ?? bisDatum;
const apiUrl = getApiUrl(selectedMode, slotNumber, fromDate, toDate);
if (!apiUrl) return;
dispatch(setLoading(true));
dispatch(setChartOpen(false));
dispatch(setIsoMeasurementCurveChartData([]));
const MIN_LOADING_TIME_MS = 1000;
const startTime = Date.now();
try {
const response = await fetch(apiUrl, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) throw new Error(`Fehler: ${response.status}`);
const jsonData = await response.json();
const elapsedTime = Date.now() - startTime;
const waitTime = Math.max(0, MIN_LOADING_TIME_MS - elapsedTime);
await new Promise((resolve) => setTimeout(resolve, waitTime));
console.log("▶️ Lade Isolationswiderstand-Daten für:");
console.log(" Slot:", slotNumber);
console.log(" Modus:", selectedMode);
console.log(" Von:", fromDate);
console.log(" Bis:", toDate);
console.log(" URL:", apiUrl);
console.log(" Daten:", jsonData);
if (Array.isArray(jsonData) && jsonData.length > 0) {
dispatch(setIsoMeasurementCurveChartData(jsonData));
dispatch(setChartOpen(true));
} else {
alert("⚠️ Keine Messdaten im gewählten Zeitraum gefunden.");
dispatch(setIsoMeasurementCurveChartData([]));
dispatch(setChartOpen(false));
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error("❌ Fehler beim Laden der Daten:", message);
alert("❌ Fehler beim Laden der Daten.");
} finally {
dispatch(setLoading(false));
}
};
useImperativeHandle(ref, () => ({
handleFetchData,
}));
return (
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-2">
<div className="flex items-center">
<label className="text-sm font-semibold">
{slotNumber !== null ? slotNumber + 1 : "-"}
</label>
</div>
<div className="flex items-center space-x-2">
{/* DateRangePicker für beide Ansichten sichtbar, da Meldungen auch datumsabhängig sind */}
<div
style={{
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
}}
>
<DateRangePicker />
</div>
{/* DIA0-DIA2 Dropdown - Platz reservieren, aber ausblenden wenn Meldungen */}
<div
style={{
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
}}
>
<Listbox
value={selectedMode}
onChange={(value) => {
dispatch(setSelectedMode(value));
dispatch(setBrushRange({ startIndex: 0, endIndex: 0 }));
}}
>
<div className="relative w-48">
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
<span>
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündliche Werte",
DIA2: "Tägliche Werte",
}[selectedMode]
}
</span>
<svg
className="w-5 h-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
{["DIA0", "DIA1", "DIA2"].map((mode) => (
<Listbox.Option
key={mode}
value={mode}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
>
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündliche Werte",
DIA2: "Tägliche Werte",
}[mode]
}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
{/* Dropdown für Auswahl zwischen "Messkurve" und "Meldungen" - immer anzeigen */}
{/* Dropdown für Auswahl zwischen "Messkurve" und "Meldungen" entfernt */}
{/* Daten laden Button lädt je nach Ansicht Messkurve oder Meldungen */}
<button
style={{
visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
}}
onClick={handleFetchData}
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm"
>
Daten laden
</button>
</div>
</div>
);
});
export default IsoChartActionBar;

View File

@@ -0,0 +1,254 @@
"use client"; // IsoChartView.tsx
import React, { useEffect, useRef } from "react";
import { Listbox } from "@headlessui/react";
import ReactModal from "react-modal";
import IsoMeasurementChart from "./IsoMeasurementChart";
import IsoChartActionBar from "./IsoChartActionBar";
import Report from "./Report";
import { useSelector, useDispatch } from "react-redux";
import { AppDispatch } from "@/redux/store";
import { RootState } from "@/redux/store";
import {
setChartOpen,
setFullScreen,
setSlotNumber,
setChartTitle,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { resetBrushRange } from "@/redux/slices/brushSlice";
import {
setVonDatum,
setBisDatum,
setSelectedMode,
setSelectedSlotType,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { resetDateRange } from "@/redux/slices/dateRangePickerSlice";
interface IsoChartViewProps {
isOpen: boolean;
onClose: () => void;
slotIndex: number;
}
const IsoChartView: React.FC<IsoChartViewProps> = ({
isOpen,
onClose,
slotIndex,
}) => {
const dispatch = useDispatch<AppDispatch>();
// removed unused loadData
const { isFullScreen, chartTitle } = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice
);
// **Modal schließen + Redux-Status zurücksetzen**
const handleClose = () => {
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
// Reset Datum
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
// Reset DateRangePicker
dispatch(resetDateRange());
// Reset Dropdowns
dispatch(setSelectedMode("DIA0")); // Reset to Alle Messwerte
dispatch(setSelectedSlotType("isolationswiderstand"));
dispatch(setChartTitle("Messkurve")); // Reset zu Messkurve
// Sonstiges Reset
dispatch(setChartOpen(false));
dispatch(setFullScreen(false));
dispatch(resetBrushRange());
onClose();
};
// **Vollbildmodus umschalten**
const toggleFullScreen = () => {
dispatch(setFullScreen(!isFullScreen));
};
// Modal öffnen - ISO spezifische Einstellungen
type ActionBarRefType = { handleFetchData: () => void };
const actionBarRef = useRef<ActionBarRefType>(null);
useEffect(() => {
if (isOpen) {
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
// Set slot number first
dispatch(setSlotNumber(slotIndex));
// Set dates
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
// Set ISO specific settings
dispatch(setSelectedSlotType("isolationswiderstand"));
dispatch(setSelectedMode("DIA0")); // Set to Alle Messwerte on open
// Set default to Messkurve
dispatch(setChartTitle("Messkurve"));
// Automatisch Daten laden wie Button-Klick
const timer = setTimeout(() => {
actionBarRef.current?.handleFetchData();
}, 120);
// Cleanup timer
return () => clearTimeout(timer);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, slotIndex, dispatch]);
return (
<ReactModal
isOpen={isOpen}
onRequestClose={handleClose}
ariaHideApp={false}
style={{
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
content: {
top: "50%",
left: "50%",
bottom: "auto",
marginRight: "-50%",
transform: "translate(-50%, -50%)",
width: isFullScreen ? "90vw" : "70rem",
height: isFullScreen ? "90vh" : "35rem",
padding: "1rem",
transition: "all 0.3s ease-in-out",
display: "flex",
flexDirection: "column",
},
}}
>
{/* Action-Buttons */}
<div
style={{
position: "absolute",
top: "0.625rem",
right: "0.625rem",
display: "flex",
gap: "0.75rem",
}}
>
{/* Fullscreen-Button */}
<button
onClick={toggleFullScreen}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i
className={
isFullScreen ? "bi bi-fullscreen-exit" : "bi bi-arrows-fullscreen"
}
></i>
</button>
{/* Schließen-Button */}
<button
onClick={handleClose}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i className="bi bi-x-circle-fill"></i>
</button>
</div>
{/* Chart-Container */}
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
height: "100%",
}}
>
<div className="flex justify-between items-center mb-2 pr-24">
<h3 className="text-lg font-semibold">Isolationswiderstand</h3>
<Listbox
value={chartTitle}
onChange={(value: "Messkurve" | "Meldungen") =>
dispatch(setChartTitle(value))
}
>
<div className="relative w-40">
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
<span>
{chartTitle === "Meldungen" ? "Meldungen" : "Messkurve"}
</span>
<svg
className="w-5 h-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
{(["Messkurve", "Meldungen"] as const).map((option) => (
<Listbox.Option
key={option}
value={option}
className={({
selected,
active,
}: {
selected: boolean;
active: boolean;
}) =>
`px-4 py-1 cursor-pointer ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
>
{option === "Meldungen" ? "Meldungen" : "Messkurve"}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
<IsoChartActionBar ref={actionBarRef} />
<div style={{ flex: 1, height: "90%" }}>
{chartTitle === "Messkurve" ? (
<IsoMeasurementChart />
) : (
<Report moduleType="ISO" autoLoad={chartTitle === "Meldungen"} />
)}
</div>
</div>
</ReactModal>
);
};
export default IsoChartView;

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

View File

@@ -0,0 +1,155 @@
"use client"; // KVZChartView.tsx
import React, { useEffect } from "react";
import ReactModal from "react-modal";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "@/redux/store";
import {
setChartOpen,
setFullScreen,
setSlotNumber,
setVonDatum,
setBisDatum,
setSelectedMode,
setSelectedSlotType,
} from "@/redux/slices/kabelueberwachungChartSlice";
import { resetBrushRange } from "@/redux/slices/brushSlice";
import FallSensors from "../../../../fall-detection-sensors/FallSensors";
import Report from "../IsoMeasurementChart/Report";
interface KVZChartViewProps {
isOpen: boolean;
onClose: () => void;
slotIndex: number;
}
// Modal zur Anzeige der KVz Zustände (Sturzsensoren / Fall Detection LEDs)
// Stil und Verhalten analog zu ISO / RSL / TDR Modals
const KVZChartView: React.FC<KVZChartViewProps> = ({
isOpen,
onClose,
slotIndex,
}) => {
const dispatch = useDispatch<AppDispatch>();
const isFullScreen = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
);
const slotNumber = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice.slotNumber
);
// Beim Öffnen Slot setzen (damit konsistent zu anderen Modals)
useEffect(() => {
if (isOpen) {
dispatch(setSlotNumber(slotIndex));
}
}, [isOpen, slotIndex, dispatch]);
const handleClose = () => {
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const toISO = (d: Date) => d.toLocaleDateString("sv-SE");
// Zurücksetzen entspricht Verhalten der anderen Modals
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
dispatch(setSelectedMode("DIA1"));
dispatch(setSelectedSlotType("isolationswiderstand"));
dispatch(setChartOpen(false));
dispatch(setFullScreen(false));
dispatch(resetBrushRange());
onClose();
};
const toggleFullScreen = () => {
dispatch(setFullScreen(!isFullScreen));
};
return (
<ReactModal
isOpen={isOpen}
onRequestClose={handleClose}
ariaHideApp={false}
style={{
overlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
content: {
top: "50%",
left: "50%",
bottom: "auto",
marginRight: "-50%",
transform: "translate(-50%, -50%)",
width: isFullScreen ? "90vw" : "50rem",
height: isFullScreen ? "90vh" : "28rem",
padding: "1rem",
transition: "all 0.3s ease-in-out",
display: "flex",
flexDirection: "column",
},
}}
>
{/* Action Buttons */}
<div
style={{
position: "absolute",
top: "0.625rem",
right: "0.625rem",
display: "flex",
gap: "0.75rem",
}}
>
<button
onClick={toggleFullScreen}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i
className={
isFullScreen ? "bi bi-fullscreen-exit" : "bi bi-arrows-fullscreen"
}
></i>
</button>
<button
onClick={handleClose}
style={{
background: "transparent",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
}}
>
<i className="bi bi-x-circle-fill"></i>
</button>
</div>
{/* Content */}
<div className="flex flex-col h-full">
<h3 className="text-lg font-semibold mb-1">KVz Zustände & Meldungen</h3>
{/* LED Bereich */}
<div className="w-full flex justify-between mb-4">
<div className="flex items-center">
<label className="text-sm font-semibold">
{slotNumber !== null ? slotNumber + 1 : "-"}
</label>
</div>
<div style={{ width: "12rem" }}>
<FallSensors slotIndex={slotIndex} />
</div>
<div></div>
</div>
{/* Meldungen Bereich */}
<div className="flex-1 border rounded bg-white overflow-hidden">
<Report moduleType="KVZ" />
</div>
</div>
</ReactModal>
);
};
export default KVZChartView;

View File

@@ -1,21 +1,29 @@
"use client"; "use client";
// /components/main/kabelueberwachung/kue705FO/Charts/LoopMeasurementChart/LoopChartActionBar.tsx // /components/main/kabelueberwachung/kue705FO/Charts/LoopMeasurementChart/LoopChartActionBar.tsx
import React from "react"; import React, {
import DateRangePicker from "./DateRangePicker"; useEffect,
import { useDispatch, useSelector } from "react-redux"; 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 { RootState } from "@/redux/store";
import { import {
setLoopMeasurementCurveChartData, setLoopMeasurementCurveChartData,
setSelectedMode, setSelectedMode,
setSelectedSlotType,
setChartOpen, setChartOpen,
setLoading, setLoading,
} from "@/redux/slices/kabelueberwachungChartSlice"; } from "@/redux/slices/kabelueberwachungChartSlice";
import { setBrushRange } from "@/redux/slices/brushSlice"; import { setBrushRange } from "@/redux/slices/brushSlice";
import { setChartTitle } from "@/redux/slices/loopChartTypeSlice"; import { Listbox } from "@headlessui/react";
//-----------------------------------------------------------------------------------useLoopChartLoader //-----------------------------------------------------------------------------------useLoopChartLoader
export const useLoopChartLoader = () => { export const useLoopChartLoader = () => {
const dispatch = useDispatch(); const dispatch = useAppDispatch();
const { vonDatum, bisDatum, selectedMode, selectedSlotType, slotNumber } = const { vonDatum, bisDatum, selectedMode, selectedSlotType, slotNumber } =
useSelector((state: RootState) => state.kabelueberwachungChartSlice); useSelector((state: RootState) => state.kabelueberwachungChartSlice);
const hasShownNoDataAlert = React.useRef(false); const hasShownNoDataAlert = React.useRef(false);
@@ -30,8 +38,7 @@ export const useLoopChartLoader = () => {
type: number, type: number,
slotNumber: number slotNumber: number
) => { ) => {
const typeFolder = const typeFolder = "schleifenwiderstand";
type === 3 ? "isolationswiderstand" : "schleifenwiderstand";
let url: string; let url: string;
@@ -92,34 +99,67 @@ export const useLoopChartLoader = () => {
}; };
//-----------------------------------------------------------------------------------LoopChartActionBar //-----------------------------------------------------------------------------------LoopChartActionBar
const LoopChartActionBar: React.FC = () => { const LoopChartActionBar = forwardRef((_props, ref) => {
const dispatch = useDispatch(); 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 { const {
vonDatum, vonDatum,
bisDatum, bisDatum,
selectedMode, selectedMode,
selectedSlotType, selectedSlotType,
slotNumber, slotNumber,
isLoading, isLoading,
} = useSelector((state: RootState) => state.kabelueberwachungChartSlice); } = 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 = ( const getApiUrl = (
mode: "DIA0" | "DIA1" | "DIA2", mode: "DIA0" | "DIA1" | "DIA2",
type: number, type: number,
slotNumber: number slotNumber: number,
fromDate: string,
toDate: string
) => { ) => {
const typeFolder = const typeFolder = "schleifenwiderstand";
type === 3 ? "isolationswiderstand" : "schleifenwiderstand";
const baseUrl = const baseUrl =
process.env.NODE_ENV === "development" 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( : `${window.location.origin}/CPL?seite.ACP&${mode}=${formatDate(
vonDatum fromDate
)};${formatDate(bisDatum)};${slotNumber};${type};`; )};${formatDate(toDate)};${slotNumber};${type};`;
console.log("baseUrl", baseUrl); console.log("baseUrl", baseUrl);
return baseUrl; return baseUrl;
@@ -130,15 +170,64 @@ const LoopChartActionBar: React.FC = () => {
return `${year};${month};${day}`; 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 handleFetchData = async () => {
const type = selectedSlotType === "schleifenwiderstand" ? 4 : 3; const type = selectedSlotType === "schleifenwiderstand" ? 4 : 3;
if (slotNumber === null) { if (slotNumber === null) {
alert("⚠️ Bitte zuerst einen Steckplatz auswählen!"); alert("⚠️ Bitte zuerst einen auswählen!");
return; 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; if (!apiUrl) return;
dispatch(setLoading(true)); dispatch(setLoading(true));
@@ -165,8 +254,8 @@ const LoopChartActionBar: React.FC = () => {
console.log(" Slot:", slotNumber); console.log(" Slot:", slotNumber);
console.log(" Typ:", selectedSlotType, "→", type); console.log(" Typ:", selectedSlotType, "→", type);
console.log(" Modus:", selectedMode); console.log(" Modus:", selectedMode);
console.log(" Von:", vonDatum); console.log(" Von:", fromDate);
console.log(" Bis:", bisDatum); console.log(" Bis:", toDate);
console.log(" URL:", apiUrl); console.log(" URL:", apiUrl);
console.log(" Daten:", jsonData); console.log(" Daten:", jsonData);
@@ -188,69 +277,132 @@ const LoopChartActionBar: React.FC = () => {
} }
}; };
useImperativeHandle(ref, () => ({
handleFetchData,
}));
return ( return (
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-2"> <div className="flex justify-between p-1 bg-gray-100 rounded-lg ">
<div className="flex items-center"> <div className="flex items-center">
<label className="text-sm font-semibold"> <label className="text-sm font-semibold">
Steckplatz {slotNumber ?? "-"} {slotNumber !== null ? slotNumber + 1 : "-"}
</label> </label>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<DateRangePicker /> {/* DateRangePicker für beide Ansichten sichtbar */}
<div>
<DateRangePicker compact />
</div>
<select {/* DIA0/DIA1/DIA2 Dropdown nur sichtbar bei Messkurve */}
value={selectedMode} <div
onChange={(e) => { style={{
dispatch( visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
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> <Listbox
<option value="DIA1">Stündliche Werte</option> value={selectedMode}
<option value="DIA2">Tägliche Werte</option> onChange={(value) => {
</select> dispatch(setSelectedMode(value));
dispatch(setBrushRange({ startIndex: 0, endIndex: 0 }));
}}
>
<div className="relative w-48">
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
<span>
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündliche Werte",
DIA2: "Tägliche Werte",
}[selectedMode]
}
</span>
<svg
className="w-5 h-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
{["DIA0", "DIA1", "DIA2"].map((mode) => (
<Listbox.Option
key={mode}
value={mode}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
>
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündliche Werte",
DIA2: "Tägliche Werte",
}[mode]
}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
{/* Dropdown für Messkurve / Meldungen in View-Header umgezogen */}
<select {/* Buttons nur sichtbar bei Messkurve, Platz bleibt erhalten */}
value={selectedSlotType} <div
onChange={(e) => { style={{
const value = e.target.value as visibility: chartTitle === "Messkurve" ? "visible" : "hidden",
| "isolationswiderstand"
| "schleifenwiderstand";
dispatch(setSelectedSlotType(value));
dispatch(
setChartTitle(
value === "isolationswiderstand"
? "Isolationsmessung"
: "Schleifenmessung"
)
);
}} }}
className="px-3 py-1 bg-white border rounded text-sm" className="flex items-center space-x-2"
> >
<option value="isolationswiderstand">Isolationswiderstand</option> <button
<option value="schleifenwiderstand">Schleifenwiderstand</option> onClick={handleStartRSL}
</select> className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap"
disabled={isLoading || rslRunning}
<button >
onClick={handleFetchData} {rslRunning ? "RSL läuft..." : "RSL Messung starten"}
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm" </button>
> <button
Daten laden onClick={handleFetchData}
</button> className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap"
disabled={rslRunning}
{isLoading && ( >
<div className="flex items-center space-x-2 text-sm text-gray-500"> Daten laden
<div className="w-4 h-4 border-2 border-t-2 border-blue-500 rounded-full animate-spin" /> </button>
<span>Lade Daten...</span> </div>
</div>
)}
</div> </div>
{rslRunning && (
<div className="fixed inset-0 z-[1000] flex flex-col items-center justify-center bg-white/80 backdrop-blur-sm">
<div className="mb-4 text-center space-y-1">
<p className="text-lg font-semibold">RSL Messung läuft</p>
<p className="text-sm text-gray-700">
Bitte warten (noch {TOTAL_DURATION - rslProgress}s)
</p>
</div>
<div className="w-2/3 max-w-xl h-4 bg-gray-200 rounded overflow-hidden shadow-inner">
<div
className="h-full bg-littwin-blue transition-all ease-linear"
style={{ width: `${(rslProgress / TOTAL_DURATION) * 100}%` }}
/>
</div>
</div>
)}
</div> </div>
); );
}; });
LoopChartActionBar.displayName = "LoopChartActionBar";
export default LoopChartActionBar; export default LoopChartActionBar;

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import { RootState } from "@/redux/store";
import { fetchTDMDataBySlotThunk } from "@/redux/thunks/getTDMListBySlotThunk"; import { fetchTDMDataBySlotThunk } from "@/redux/thunks/getTDMListBySlotThunk";
import { getTDRChartDataByIdThunk } from "@/redux/thunks/getTDRChartDataByIdThunk"; import { getTDRChartDataByIdThunk } from "@/redux/thunks/getTDRChartDataByIdThunk";
import { getReferenceCurveBySlotThunk } from "@/redux/thunks/getReferenceCurveBySlotThunk"; // ⬅ import ergänzen import { getReferenceCurveBySlotThunk } from "@/redux/thunks/getReferenceCurveBySlotThunk"; // ⬅ import ergänzen
import { Listbox } from "@headlessui/react";
const TDRChartActionBar: React.FC = () => { const TDRChartActionBar: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@@ -29,13 +30,6 @@ const TDRChartActionBar: React.FC = () => {
const [selectedId, setSelectedId] = useState<number | null>(null); const [selectedId, setSelectedId] = useState<number | null>(null);
const currentChartData = selectedId !== null ? tdrDataById[selectedId] : []; const currentChartData = selectedId !== null ? tdrDataById[selectedId] : [];
// 🔄 Dropdown-Auswahl: neue Messung laden
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const id = parseInt(e.target.value);
setSelectedId(id);
dispatch(getTDRChartDataByIdThunk(id));
};
// 📌 Referenz setzen (nutzt Slotnummer + 1 für die API) // 📌 Referenz setzen (nutzt Slotnummer + 1 für die API)
const handleSetReference = async () => { const handleSetReference = async () => {
if ( if (
@@ -59,7 +53,7 @@ const TDRChartActionBar: React.FC = () => {
}), }),
}); });
} else { } else {
const url = `/CPL?KTR${slotNumber}=${selectedId}`; const url = `/CPL?/${window.location.pathname}&KTR${slotNumber}=${selectedId}`;
await fetch(url, { method: "GET" }); await fetch(url, { method: "GET" });
} }
if (!isDev) { if (!isDev) {
@@ -88,6 +82,33 @@ const TDRChartActionBar: React.FC = () => {
} }
}; };
// 📌 TDR Messung starten
const handleStartTDR = async () => {
if (selectedSlot === null) {
alert("⚠️ Bitte zuerst einen KÜ auswählen!");
return;
}
const cgiUrl = `${window.location.origin}/CPL?/${window.location.pathname}&KTT${selectedSlot}=1`;
try {
console.log("🚀 Starte TDR Messung für Slot:", selectedSlot);
console.log("📡 CGI URL:", cgiUrl);
const response = await fetch(cgiUrl);
if (!response.ok) {
throw new Error(`CGI-Fehler: ${response.status}`);
}
console.log("✅ TDR Messung gestartet für Slot", selectedSlot);
//alert(`✅ TDR Messung für Slot ${selectedSlot + 1} gestartet`);
} catch (err) {
console.error("❌ Fehler beim Starten der TDR Messung:", err);
//alert("❌ Fehler beim Starten der TDR Messung.");
}
};
// 📥 Beim Slot-Wechsel TDM-Liste + letzte ID laden // 📥 Beim Slot-Wechsel TDM-Liste + letzte ID laden
useEffect(() => { useEffect(() => {
if (selectedSlot !== null) { if (selectedSlot !== null) {
@@ -112,48 +133,104 @@ const TDRChartActionBar: React.FC = () => {
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-4"> <div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-4">
{/* 🧩 Slot-Anzeige (1-basiert für Benutzer) */} {/* 🧩 Slot-Anzeige (1-basiert für Benutzer) */}
<div className="text-sm font-semibold"> <div className="text-sm font-semibold">
{selectedSlot !== null {selectedSlot !== null ? `${selectedSlot + 1}` : "Kein KÜ gewählt"}
? `Steckplatz ${selectedSlot + 1}`
: "Kein Steckplatz gewählt"}
</div> </div>
{/* ✅ Referenz setzen */} {/* ✅ Referenz setzen */}
{selectedId !== null && ( {selectedId !== null && (
<button <button
onClick={handleSetReference} onClick={handleSetReference}
className="border border-littwin-blue text-littwin-blue bg-white rounded px-3 py-1 text-sm hover:bg-blue-100" className="border border-littwin-blue text-littwin-blue bg-white rounded px-3 py-1 text-sm hover:bg-gray-200"
> >
TDR-Kurve als Referenz speichern TDR-Kurve als Referenz speichern
</button> </button>
)} )}
{/* 🚀 TDR starten */}
<button
onClick={handleStartTDR}
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap "
disabled={selectedSlot === null}
>
TDR-Messung starten
</button>
{/* 🔽 Dropdown für Messungen */} {/* 🔽 Dropdown für Messungen */}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<label htmlFor="tdrIdSelect" className="text-sm font-semibold"> <Listbox
TDR Messung value={selectedId}
</label> onChange={(id) => {
<select setSelectedId(id);
id="tdrIdSelect" if (id !== null) {
value={selectedId ?? ""} dispatch(getTDRChartDataByIdThunk(id));
onChange={handleSelectChange} }
className="border rounded px-2 py-1 text-sm" }}
disabled={idsForSlot.length === 0} disabled={idsForSlot.length === 0}
> >
<option value="">-- Wähle Messung --</option> <div className="relative w-96">
{idsForSlot.map((entry) => ( <Listbox.Button className="w-full border px-2 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
<option key={entry.id} value={entry.id}> <span className="whitespace-nowrap overflow-hidden text-ellipsis">
{new Date(entry.t).toLocaleString("de-DE", { {selectedId
day: "2-digit", ? (() => {
month: "2-digit", const selected = idsForSlot.find(
year: "numeric", (e) => e.id === selectedId
hour: "2-digit", );
minute: "2-digit", return selected
second: "2-digit", ? `${new Date(selected.t).toLocaleString("de-DE", {
})}{" "} day: "2-digit",
Fehlerstelle: {entry.d} m month: "2-digit",
</option> year: "numeric",
))} hour: "2-digit",
</select> minute: "2-digit",
second: "2-digit",
})} Fehlerstelle: ${selected.d} m`
: "Wähle Messung";
})()
: "Wähle Messung"}
</span>
<svg
className="w-5 h-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
{idsForSlot.map((entry) => (
<Listbox.Option
key={entry.id}
value={entry.id}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer whitespace-nowrap overflow-hidden text-ellipsis ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
>
{new Date(entry.t).toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})}{" "}
Fehlerstelle: {entry.d} m
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div> </div>
</div> </div>
); );

View File

@@ -1,20 +1,18 @@
"use client"; // /components/modules/kue705FO/charts/ChartSwitcher.tsx "use client"; // TDRChartView.tsx
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import ReactModal from "react-modal"; import ReactModal from "react-modal";
import LoopChartActionBar from "./LoopMeasurementChart/LoopChartActionBar"; import TDRChart from "./TDRChart";
import LoopMeasurementChart from "./LoopMeasurementChart/LoopMeasurementChart";
import TDRChart from "./TDRChart/TDRChart";
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import { AppDispatch } from "@/redux/store"; import { AppDispatch } from "@/redux/store";
import { RootState } from "@/redux/store"; import { RootState } from "@/redux/store";
import { import {
setChartOpen, setChartOpen,
setFullScreen, setFullScreen,
setSlotNumber,
} from "@/redux/slices/kabelueberwachungChartSlice"; } from "@/redux/slices/kabelueberwachungChartSlice";
import { resetBrushRange } from "@/redux/slices/brushSlice"; import { resetBrushRange } from "@/redux/slices/brushSlice";
import { useLoopChartLoader } from "./LoopMeasurementChart/LoopChartActionBar";
import { import {
setVonDatum, setVonDatum,
@@ -23,25 +21,55 @@ import {
setSelectedSlotType, setSelectedSlotType,
} from "@/redux/slices/kabelueberwachungChartSlice"; } from "@/redux/slices/kabelueberwachungChartSlice";
interface ChartSwitcherProps { import {
setSelectedSlot,
setActiveMode,
} from "@/redux/slices/kueChartModeSlice";
import { Listbox } from "@headlessui/react";
import { setChartTitle } from "@/redux/slices/kabelueberwachungChartSlice";
import Report from "../IsoMeasurementChart/Report";
interface TDRChartViewProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
slotIndex: number; slotIndex: number;
} }
const ChartSwitcher: React.FC<ChartSwitcherProps> = ({ isOpen, onClose }) => { const TDRChartView: React.FC<TDRChartViewProps> = ({
isOpen,
onClose,
slotIndex,
}) => {
const dispatch = useDispatch<AppDispatch>(); const dispatch = useDispatch<AppDispatch>();
const chartTitle = useSelector(
(state: RootState) => state.loopChartType.chartTitle const { isFullScreen, chartTitle } = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice
); );
// **Redux-States für aktive Messkurve (TDR oder Schleife)** // **Modal öffnen - TDR spezifische Einstellungen**
const activeMode = useSelector( useEffect(() => {
(state: RootState) => state.kueChartModeSlice.activeMode if (isOpen) {
); const today = new Date();
const isFullScreen = useSelector( const thirtyDaysAgo = new Date();
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen thirtyDaysAgo.setDate(today.getDate() - 30);
);
const toISO = (date: Date) => date.toLocaleDateString("sv-SE");
// Set TDR mode and slot
dispatch(setActiveMode("TDR"));
dispatch(setSelectedSlot(slotIndex));
// Also set slot number for general chart slice
dispatch(setSlotNumber(slotIndex));
// Set dates
dispatch(setVonDatum(toISO(thirtyDaysAgo)));
dispatch(setBisDatum(toISO(today)));
// TDR specific settings (if needed)
dispatch(setSelectedSlotType("isolationswiderstand"));
}
}, [isOpen, slotIndex, dispatch]);
// **Modal schließen + Redux-Status zurücksetzen** // **Modal schließen + Redux-Status zurücksetzen**
const handleClose = () => { const handleClose = () => {
@@ -72,41 +100,6 @@ const ChartSwitcher: React.FC<ChartSwitcherProps> = ({ isOpen, onClose }) => {
dispatch(setFullScreen(!isFullScreen)); 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 ( return (
<ReactModal <ReactModal
isOpen={isOpen} isOpen={isOpen}
@@ -179,23 +172,65 @@ const ChartSwitcher: React.FC<ChartSwitcherProps> = ({ isOpen, onClose }) => {
height: "100%", height: "100%",
}} }}
> >
{activeMode === "Schleife" ? ( <div className="flex justify-between items-center mb-2 pr-24">
<> <h3 className="text-lg font-semibold">
<h3 className="text-lg font-semibold">{chartTitle}</h3> {chartTitle === "Messkurve" ? "TDR-Messung" : "Meldungen"}
<LoopChartActionBar /> </h3>
<div style={{ flex: 1, height: "90%" }}> {/* Dropdown Messkurve / Meldungen */}
<LoopMeasurementChart /> <Listbox
value={chartTitle}
onChange={(value: "Messkurve" | "Meldungen") =>
dispatch(setChartTitle(value))
}
>
<div className="relative w-40">
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
<span>{chartTitle}</span>
<svg
className="w-5 h-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
{(["Messkurve", "Meldungen"] as const).map((option) => (
<Listbox.Option
key={option}
value={option}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
>
{option}
</Listbox.Option>
))}
</Listbox.Options>
</div> </div>
</> </Listbox>
) : ( </div>
<> {/* Chart oder Meldungen */}
<h3 className="text-lg font-semibold">TDR-Messung</h3> <div style={{ flex: 1, height: "90%" }}>
{chartTitle === "Messkurve" ? (
<TDRChart isFullScreen={isFullScreen} /> <TDRChart isFullScreen={isFullScreen} />
</> ) : (
)} <Report moduleType="TDR" />
)}
</div>
</div> </div>
</ReactModal> </ReactModal>
); );
}; };
export default ChartSwitcher; export default TDRChartView;

View File

@@ -1,10 +1,21 @@
"use client"; // components/modules/kue705FO/Kue705FO.tsx "use client"; // components/modules/kue705FO/Kue705FO.tsx
import React, { useState, useRef, useMemo } from "react"; import React, { useState, useMemo } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import KueModal from "./modals/SettingsModalWrapper"; import KueModal from "./modals/SettingsModalWrapper";
// import FallSensors from "../../fall-detection-sensors/FallSensors";
import "bootstrap-icons/font/bootstrap-icons.css"; // Import Bootstrap Icons import "bootstrap-icons/font/bootstrap-icons.css"; // Import Bootstrap Icons
import { Kue705FOProps } from "../../../../types/Kue705FOProps"; 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-------- //-------Redux Toolkit--------
import { RootState } from "../../../../redux/store"; import { RootState } from "../../../../redux/store";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
@@ -15,16 +26,16 @@ import useKueVersion from "./hooks/useKueVersion";
import useIsoDisplay from "./hooks/useIsoDisplay"; import useIsoDisplay from "./hooks/useIsoDisplay";
import useLoopDisplay from "./hooks/useLoopDisplay"; import useLoopDisplay from "./hooks/useLoopDisplay";
import useModulName from "./hooks/useModulName"; import useModulName from "./hooks/useModulName";
import { useAdminAuth } from "../../settingsPageComponents/hooks/useAdminAuth";
import type { Chart } from "chart.js";
//--------handlers---------------- //--------handlers----------------
import handleButtonClick from "./kue705FO-Funktionen/handleButtonClick"; // Keep needed imports
import handleOpenModal from "./handlers/handleOpenModal"; import handleOpenModal from "./handlers/handleOpenModal";
import handleCloseModal from "./handlers/handleCloseModal"; import handleCloseModal from "./handlers/handleCloseModal";
import handleOpenChartModal from "./handlers/handleOpenChartModal"; // Remove unused chart modal handlers since we use direct ChartView components
import handleCloseChartModal from "./handlers/handleCloseChartModal"; // import handleOpenChartModal from "./handlers/handleOpenChartModal";
import handleRefreshClick from "./handlers/handleRefreshClick"; // import handleCloseChartModal from "./handlers/handleCloseChartModal";
// import handleRefreshClick from "./handlers/handleRefreshClick";
const Kue705FO: React.FC<Kue705FOProps> = ({ const Kue705FO: React.FC<Kue705FOProps> = ({
isolationswert, isolationswert,
@@ -32,7 +43,6 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
modulName, modulName,
kueOnline, kueOnline,
slotIndex, slotIndex,
tdrLocation,
}) => { }) => {
/* console.log( /* console.log(
`Rendering Kue705FO - SlotIndex: ${slotIndex}, ModulName: ${modulName}` `Rendering Kue705FO - SlotIndex: ${slotIndex}, ModulName: ${modulName}`
@@ -41,35 +51,43 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
const dispatch = useDispatch(); const dispatch = useDispatch();
const { kueName } = useSelector((state: RootState) => state.kueDataSlice); const { kueName } = useSelector((state: RootState) => state.kueDataSlice);
const [activeButton, setActiveButton] = useState<"Schleife" | "TDR">( // Admin authentication hook for security - using showModal as true for continuous auth check
const { isAdminLoggedIn } = useAdminAuth(true);
const [activeButton, setActiveButton] = useState<"Schleife" | "TDR" | "ISO">(
"Schleife" "Schleife"
); );
const [loopTitleText, setloopTitleText] = useState( const [, setloopTitleText] = useState("Schleifenwiderstand [kOhm]");
"Schleifenwiderstand [kOhm]"
);
const [isoDisplayText] = useState("Aderbruch"); const [isoDisplayText] = useState("Aderbruch");
const [groundFaultDisplayText] = useState("Erdschluss"); const [groundFaultDisplayText] = useState("Erdschluss");
const [loopFaultDisplayText] = useState("Schleifenfehler"); const [loopFaultDisplayText] = useState("Schleifenfehler");
const [isoFaultDisplayText] = useState("Isolationsfehler"); const [isoFaultDisplayText] = useState("Isolationsfehler");
const [isoGreaterThan200] = useState(">200 MOhm"); const [isoGreaterThan200] = useState(">200 MOhm");
const [loading, setLoading] = useState(false);
const [showModal, setShowModal] = 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 // Removed unused loopMeasurementCurveChartData state
//------- Redux-Variablen abrufen-------------------------------- //------- Redux-Variablen abrufen--------------------------------
const { const {
kueVersion: reduxKueVersion, kueVersion: reduxKueVersion,
tdrActive,
kueCableBreak: kueCableBreakRaw, kueCableBreak: kueCableBreakRaw,
kueGroundFault: kueGroundFaultRaw, kueGroundFault: kueGroundFaultRaw,
kueAlarm1: kueAlarm1Raw, kueAlarm1: kueAlarm1Raw,
kueAlarm2: kueAlarm2Raw, kueAlarm2: kueAlarm2Raw,
kueOverflow: kueOverflowRaw, kueOverflow: kueOverflowRaw,
kuePSTmMinus96V, // <- richtig, weil so im State vorhanden 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); } = useSelector((state: RootState) => state.kueDataSlice);
//--------------------------------------------- //---------------------------------------------
@@ -96,15 +114,71 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
//-------------------------handlers------------------------- //-------------------------handlers-------------------------
const openModal = () => handleOpenModal(setShowModal); const openModal = () => handleOpenModal(setShowModal);
const closeModal = () => handleCloseModal(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 = () => // New ChartView handlers - direct modal opening
handleCloseChartModal(setShowChartModal, chartInstance); const openIsoModal = () => {
setActiveButton("ISO");
// Set Redux state for ISO type
dispatch({
type: "kabelueberwachungChart/setSelectedSlotType",
payload: 1, // 1 = Isolationswiderstand
});
dispatch({
type: "kabelueberwachungChart/setSlotNumber",
payload: slotIndex,
});
setShowIsoModal(true);
};
const closeIsoModal = () => {
setShowIsoModal(false);
};
const openRslModal = () => {
setActiveButton("Schleife");
setloopTitleText("Schleifenwiderstand [kOhm]");
setLoopDisplayValue(Number(schleifenwiderstand));
dispatch({
type: "kabelueberwachungChart/setSelectedSlotType",
payload: 2,
}); // RSL type
dispatch({
type: "kabelueberwachungChart/setSlotNumber",
payload: slotIndex,
});
setShowRslModal(true);
};
const closeRslModal = () => {
setShowRslModal(false);
};
const openTdrModal = () => {
setActiveButton("TDR");
setloopTitleText("Entfernung [km]");
const latestTdrDistanceMeters =
Array.isArray(tdmChartData?.[slotIndex]) &&
tdmChartData[slotIndex].length > 0 &&
typeof tdmChartData[slotIndex][0].d === "number"
? tdmChartData[slotIndex][0].d
: 0;
const latestTdrDistance = Number(
(latestTdrDistanceMeters / 1000).toFixed(3)
);
setLoopDisplayValue(latestTdrDistance);
setShowTdrModal(true);
};
const closeTdrModal = () => {
setShowTdrModal(false);
};
const openKvzModal = () => {
setShowKvzModal(true);
};
const closeKvzModal = () => setShowKvzModal(false);
//---------------------------------- //----------------------------------
//hooks einbinden //hooks einbinden
const kueVersion = useKueVersion(slotIndex, reduxKueVersion); const kueVersion = useKueVersion(slotIndex, reduxKueVersion);
@@ -160,17 +234,26 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
activeButton activeButton
); );
// TDR aktiv Status für diesen Slot prüfen
const isTdrActiveForSlot = tdrActive?.[slotIndex] === 1;
// KVz aktiv Status für diesen Slot prüfen - nur wenn Admin authentifiziert ist, KVz vorhanden ist UND aktiviert ist
const isKvzActiveForSlot =
kvzPresence?.[slotIndex] === 1 &&
kvzActive?.[slotIndex] === 1 &&
isAdminLoggedIn;
// Removed useChartData(loopMeasurementCurveChartData) as the state was unused // Removed useChartData(loopMeasurementCurveChartData) as the state was unused
//--------------------------------- //---------------------------------
return ( return (
<div <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 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 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 ? ( {kueOnline === 1 ? (
<> <>
<div className="relative w-[7.075rem] h-[15.156rem] bg-littwin-blue border-[0.094rem] border-gray-400 z-0"> <div className="relative w-[7.075rem] h-[15.156rem] bg-littwin-blue border-[0.094rem] border-gray-400 z-0">
@@ -220,33 +303,57 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
</div> </div>
</div> </div>
{/* Anzeige des Isolation */} {/* Schwarzes Display mit drei Zeilen: Alarm, ISO, Schleife */}
<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="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"> <div className="text-center w-full flex flex-col justify-between items-center h-full">
{/* 1. Zeile: Alarmtext in Rot, sonst "Status: OK" */}
<span <span
className={ className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
Number(kuePSTmMinus96V?.[slotIndex]) === 1 || Number(kuePSTmMinus96V?.[slotIndex]) === 1 ||
Number(kueCableBreak?.[slotIndex]) === 1 || Number(kueCableBreak?.[slotIndex]) === 1 ||
Number(kueGroundFault?.[slotIndex]) === 1 || Number(kueGroundFault?.[slotIndex]) === 1 ||
Number(kueAlarm1?.[slotIndex]) === 1 || Number(kueAlarm1?.[slotIndex]) === 1 ||
Number(kueAlarm2?.[slotIndex]) === 1 Number(kueAlarm2?.[slotIndex]) === 1
? "text-red-500 text-[0.875rem]" ? "text-red-500"
: Number(kueOverflow?.[slotIndex]) === 1 : "text-green-500"
? "text-white text-[0.875rem]" }`}
: ""
}
> >
{isoDisplayValue} {Number(kuePSTmMinus96V?.[slotIndex]) === 1
? "Messpannung"
: Number(kueCableBreak?.[slotIndex]) === 1
? "Aderbruch"
: Number(kueGroundFault?.[slotIndex]) === 1
? "Erdschluss"
: Number(kueAlarm1?.[slotIndex]) === 1
? "Isolationsfehler"
: Number(kueAlarm2?.[slotIndex]) === 1
? "Schleifenfehler"
: " "}
{"\u00A0"}
{/* Status: OK*/}
</span>
{/* 2. Zeile: ISO-Wert, immer anzeigen */}
<span
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
Number(kueAlarm1?.[slotIndex]) === 1 ? "text-red-500" : ""
}`}
>
{isoDisplayValue === "Abgleich"
? "ISO: Abgleich"
: `ISO: ${Number(isolationswert)
.toFixed(2)
.replace(".", ",")} MOhm`}
</span>
{/* 3. Zeile: Schleifenwert, in Rot bei Schleifenfehler, sonst normal */}
<span
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
Number(kueAlarm2?.[slotIndex]) === 1 ? "text-red-500" : ""
}`}
>
{`RSL: ${Number(loopDisplayValue)
.toFixed(3)
.replace(".", ",")} kOhm`}
</span> </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> </div>
@@ -261,103 +368,132 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
{kueVersion} {kueVersion}
</div> </div>
</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 = {/* Modal für Einstellungen */}
Array.isArray(tdmChartData?.[slotIndex]) && </>
tdmChartData[slotIndex].length > 0 && ) : (
typeof tdmChartData[slotIndex][0].d === "number" <div className="flex items-center justify-center h-full text-gray-500">
? tdmChartData[slotIndex][0].d {/* Das soll rausgenommen werden
: 0; <p>Kein Modul im Slot {slotIndex + 0}</p>
*/}
const latestTdrDistance = Number( </div>
(latestTdrDistanceMeters / 1000).toFixed(3) )}
); {/* Messkurven-Button unter dem Modul */}
setLoopDisplayValue(latestTdrDistance); {kueOnline === 1 && (
}} <>
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 Überschrift: Detailansicht
: activeButton === "TDR" ISO und RSL als Buttons (Firmenblau) nebeneinander
? "bg-littwin-blue" // Aktiviert: Littwin Blau TDR und KVz Buttons (Firmenblau) nebeneinander
: "bg-gray-400" // Nicht geklickt: Dunkelgrau Wenn kein TDR oder kein KVz: nur grauer Button ohne Text
}`}
disabled={Array.isArray(tdrActive) && tdrActive[slotIndex] === 0} // Button deaktiviert, wenn TDR für diesen Slot nicht aktiv ist
> */}
TDR <div className="flex flex-col items-center w-full px-2 mt-2 space-y-2">
</button> {/* Detailansicht Header */}
</div> <span className="text-black text-[0.625rem] font-semibold">
Detailansicht
{/* 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}
</span> </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 <button
onClick={refreshClick} // Dynamische Funktion basierend auf aktivem Button onClick={openIsoModal}
className="absolute -top-[0.063rem] -right-[0.063rem] w-[1.25rem] h-[1.25rem] bg-gray-400 flex items-center justify-center" className="bg-littwin-blue text-white text-[0.625rem] flex items-center justify-center p-2 min-w-[2.5rem]"
disabled={loading} // Disable button while loading
> >
<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> </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> </div>
<button {/* TDR and KVz Buttons */}
onClick={openChartModal} // Öffnet das Chart-Modal <div className="flex space-x-2 p-1">
className="w-full h-[1.563rem] bg-littwin-blue text-white text-[0.625rem] flex items-center justify-center mt-[0.063rem]" {/* TDR Button - blau mit Text wenn aktiv, grau ohne Text wenn inaktiv */}
> <button
Messkurve onClick={isTdrActiveForSlot ? openTdrModal : undefined}
</button> 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> </div>
{/* Modal für Messkurve */} {/* ISO Chart Modal */}
{showChartModal && ( <div className="absolute bottom-0 left-0 right-0 h-[0.125rem] bg-gray-400"></div>
<ChartSwitcher
isOpen={showChartModal} {/* ISO Chart Modal */}
onClose={closeChartModal} <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} 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 {/* Das soll rausgenommen werden
<p>Kein Modul im Slot {slotIndex + 0}</p> <p>Kein Modul im Slot {slotIndex + 0}</p>
*/} */}

View File

@@ -0,0 +1,122 @@
"use client";
import React, { useEffect, useState } from "react";
import { useAppSelector } from "@/redux/store";
export default function SlotActivityOverlay({
slotIndex,
}: {
slotIndex: number;
}) {
const ksx = useAppSelector((s) => s.deviceEvents.ksx);
const ksy = useAppSelector((s) => s.deviceEvents.ksy);
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
const alignmentStartedAt = useAppSelector(
(s) => s.deviceEvents.alignmentStartedAt
);
const loopActive = Array.isArray(ksx) && ksx[slotIndex] === 1;
const tdrActive = Array.isArray(ksy) && ksy[slotIndex] === 1;
const alignActive = Array.isArray(ksz) && ksz[slotIndex] === 1;
// Progress ticker
const [now, setNow] = useState<number>(Date.now());
useEffect(() => {
const any = loopActive || tdrActive || alignActive;
if (!any) return;
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, [loopActive, tdrActive, alignActive]);
const clamp = (v: number, min = 0, max = 1) =>
Math.max(min, Math.min(max, v));
const compute = (startedAt: number | null, durationMs: number) => {
if (!startedAt) return { pct: 0 };
const elapsed = now - startedAt;
const pct = clamp(elapsed / durationMs) * 100;
return { pct };
};
// Durations
const LOOP_MS = 2 * 60 * 1000; // ~2 min
const TDR_MS = 30 * 1000; // ~30 s
const ALIGN_MS = 10 * 60 * 1000; // ~10 min
if (!loopActive && !tdrActive && !alignActive) return null;
return (
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/70 backdrop-blur-sm">
<div className="p-2 rounded-md shadow bg-white/90 border border-gray-200 w-[min(90%,12rem)]">
<div className="text-[0.75rem] font-semibold mb-2 text-gray-800">
Bitte warten
</div>
<div className="space-y-2">
{loopActive && (
<div>
<div className="text-[0.7rem] text-gray-800 mb-1">Schleife</div>
{(() => {
const { pct } = compute(loopStartedAt, LOOP_MS);
return (
<div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
<div
className="h-full bg-littwin-blue transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<div className="text-[0.65rem] text-gray-700 mt-1">
{Math.round(pct)}%
</div>
</div>
);
})()}
</div>
)}
{tdrActive && (
<div>
<div className="text-[0.7rem] text-gray-800 mb-1">TDR</div>
{(() => {
const { pct } = compute(tdrStartedAt, TDR_MS);
return (
<div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
<div
className="h-full bg-littwin-blue transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<div className="text-[0.65rem] text-gray-700 mt-1">
{Math.round(pct)}%
</div>
</div>
);
})()}
</div>
)}
{alignActive && (
<div>
<div className="text-[0.7rem] text-gray-800 mb-1">Abgleich</div>
{(() => {
const { pct } = compute(alignmentStartedAt, ALIGN_MS);
return (
<div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
<div
className="h-full bg-littwin-blue transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<div className="text-[0.65rem] text-gray-700 mt-1">
{Math.round(pct)}%
</div>
</div>
);
})()}
</div>
)}
</div>
</div>
</div>
);
}

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
const useLoopDisplay = ( const useLoopDisplay = (
schleifenwiderstand: number, schleifenwiderstand: number,
activeButton: "Schleife" | "TDR" activeButton: "Schleife" | "TDR" | "ISO"
) => { ) => {
const [loopDisplayValue, setLoopDisplayValue] = const [loopDisplayValue, setLoopDisplayValue] =
useState<number>(schleifenwiderstand); useState<number>(schleifenwiderstand);
@@ -12,6 +12,7 @@ const useLoopDisplay = (
if (activeButton === "Schleife") { if (activeButton === "Schleife") {
setLoopDisplayValue(schleifenwiderstand); setLoopDisplayValue(schleifenwiderstand);
} }
// For ISO and TDR, the value is set manually via setLoopDisplayValue
}, [schleifenwiderstand, activeButton]); }, [schleifenwiderstand, activeButton]);
return { loopDisplayValue, setLoopDisplayValue }; return { loopDisplayValue, setLoopDisplayValue };

View File

@@ -2,9 +2,9 @@
// components/main/kabelueberwachung/kue705FO/modals/KueEinstellung.tsx // components/main/kabelueberwachung/kue705FO/modals/KueEinstellung.tsx
import { useState } from "react"; import { useState } from "react";
import { useDispatch, useSelector } from "react-redux"; 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 handleSave from "../handlers/handleSave";
import handleDisplayEinschalten from "../handlers/handleDisplayEinschalten"; import handleDisplayEinschalten from "@/components/main/kabelueberwachung/kue705FO/handlers/handleDisplayEinschalten";
import firmwareUpdate from "../handlers/firmwareUpdate"; import firmwareUpdate from "../handlers/firmwareUpdate";
import ProgressModal from "@/components/main/settingsPageComponents/modals/ProgressModal"; import ProgressModal from "@/components/main/settingsPageComponents/modals/ProgressModal";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -22,18 +22,6 @@ interface Props {
onModulNameChange?: (id: string) => void; 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({ export default function KueEinstellung({
slot, slot,
onClose = () => {}, onClose = () => {},
@@ -173,20 +161,23 @@ export default function KueEinstellung({
/> />
</div> </div>
{/* Speicherintervall */} {/* Speicherintervall */}
{/* Speicherintervall */}
{/* Speicherintervall */}
<div className="mb-4 grid grid-cols-3 items-center gap-2 w-full"> <div className="mb-4 grid grid-cols-3 items-center gap-2 w-full">
<label className="">Speicherintervall:</label> <label className="w-48">Speicherintervall:</label>
<select <div className="relative w-36">
className="w-full border rounded p-1" <input
value={formData.memoryInterval} type="number"
onChange={(e) => handleChange("memoryInterval", e.target.value)} className="border rounded px-2 py-1 pr-20 w-full text-right"
> value={formData.memoryInterval}
{memoryIntervalOptions.map((opt) => ( onChange={(e) => handleChange("memoryInterval", e.target.value)}
<option key={opt.value} value={opt.value}> />
{opt.label} <span className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
</option> Minuten
))} </span>
</select> </div>
</div> </div>
{/* Isolationsmessung */} {/* Isolationsmessung */}
<div className="mb-4 w-full"> <div className="mb-4 w-full">
<h3 className="font-bold mb-2">Isolationsmessung</h3> <h3 className="font-bold mb-2">Isolationsmessung</h3>
@@ -251,7 +242,7 @@ export default function KueEinstellung({
</div> </div>
</div> </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 && ( {isAdminLoggedIn && (
<> <>
<button <button
@@ -260,6 +251,48 @@ export default function KueEinstellung({
> >
Firmware Update Firmware Update
</button> </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 && ( {showConfirmModal && (
@@ -283,7 +316,11 @@ export default function KueEinstellung({
/> />
)} )}
{isUpdating && ( {isUpdating && (
<ProgressModal visible={isUpdating} progress={progress} /> <ProgressModal
visible={isUpdating}
progress={progress}
slot={slot + 1}
/>
)} )}
<button <button
onClick={() => handleDisplayEinschalten(slot)} onClick={() => handleDisplayEinschalten(slot)}

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 ReactModal from "react-modal";
import KueEinstellung from "./KueEinstellung"; import KueEinstellung from "./KueEinstellung";
import TdrEinstellung from "./TdrEinstellung"; import TdrEinstellung from "./TdrEinstellung";
import KvzModalView from "./KvzModalView";
import Knotenpunkte from "./Knotenpunkte"; import Knotenpunkte from "./Knotenpunkte";
interface KueModalProps { interface KueModalProps {
@@ -14,18 +15,20 @@ interface KueModalProps {
declare global { declare global {
interface Window { interface Window {
__lastKueTab?: "kue" | "tdr" | "knoten"; __lastKueTab?: "kue" | "tdr" | "kvz" | "knoten";
kabelModalOpen?: boolean; kabelModalOpen?: boolean;
} }
} }
export default function KueModal({ showModal, onClose, slot }: KueModalProps) { export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
const [activeTab, setActiveTab] = useState<"kue" | "tdr" | "knoten">(() => { const [activeTab, setActiveTab] = useState<"kue" | "tdr" | "kvz" | "knoten">(
if (typeof window !== "undefined" && window.__lastKueTab) { () => {
return window.__lastKueTab; if (typeof window !== "undefined" && window.__lastKueTab) {
return window.__lastKueTab;
}
return "kue";
} }
return "kue"; );
});
useEffect(() => { useEffect(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@@ -67,9 +70,7 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
}} }}
> >
<div className="p-2 flex justify-between items-center rounded-t-md"> <div className="p-2 flex justify-between items-center rounded-t-md">
<h2 className="text-base font-bold"> <h2 className="text-base font-bold">Einstellungen {slot + 1}</h2>
Einstellungen Steckplatz {slot + 1}
</h2>
<button onClick={onClose} className="text-2xl hover:text-gray-200"> <button onClick={onClose} className="text-2xl hover:text-gray-200">
<i className="bi bi-x-circle-fill"></i> <i className="bi bi-x-circle-fill"></i>
</button> </button>
@@ -79,6 +80,7 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
{[ {[
{ label: "Allgemein", key: "kue" as const }, { label: "Allgemein", key: "kue" as const },
{ label: "TDR ", key: "tdr" as const }, { label: "TDR ", key: "tdr" as const },
{ label: "KVz", key: "kvz" as const },
{ label: "Knotenpunkte", key: "knoten" as const }, { label: "Knotenpunkte", key: "knoten" as const },
].map(({ label, key }) => ( ].map(({ label, key }) => (
<button <button
@@ -107,6 +109,7 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
{activeTab === "tdr" && ( {activeTab === "tdr" && (
<TdrEinstellung slot={slot} onClose={onClose} /> <TdrEinstellung slot={slot} onClose={onClose} />
)} )}
{activeTab === "kvz" && <KvzModalView slot={slot} onClose={onClose} />}
{activeTab === "knoten" && ( {activeTab === "knoten" && (
<Knotenpunkte slot={slot} onClose={onClose} /> <Knotenpunkte slot={slot} onClose={onClose} />
)} )}

View File

@@ -12,8 +12,9 @@ declare global {
} }
import React, { useState } from "react"; import React, { useState } from "react";
import { useSelector } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import { RootState } from "../../../../../redux/store"; import { RootState } from "../../../../../redux/store";
import { setKueData } from "../../../../../redux/slices/kueDataSlice";
import { useAdminAuth } from "../../../settingsPageComponents/hooks/useAdminAuth"; import { useAdminAuth } from "../../../settingsPageComponents/hooks/useAdminAuth";
@@ -24,6 +25,7 @@ interface Props {
export default function TdrEinstellung({ slot, onClose }: Props) { export default function TdrEinstellung({ slot, onClose }: Props) {
const { isAdminLoggedIn } = useAdminAuth(true); const { isAdminLoggedIn } = useAdminAuth(true);
const dispatch = useDispatch();
const tdrSlice = useSelector((state: RootState) => state.kueDataSlice); const tdrSlice = useSelector((state: RootState) => state.kueDataSlice);
const cacheKey = `slot_${slot}`; const cacheKey = `slot_${slot}`;
@@ -126,6 +128,11 @@ export default function TdrEinstellung({ slot, onClose }: Props) {
setTdrActive(newState); setTdrActive(newState);
updateCache(tdrData, 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 isDev = window.location.hostname === "localhost";
const slotParam = `KTX${slot}=${newState ? 1 : 0}`; 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"> <div className="flex items-center space-x-2">
<label className="text-sm font-semibold">Von</label> <label className="text-sm font-semibold">Von</label>
<DatePicker <DatePicker
portalId="root-portal" // beliebige ID
popperClassName="custom-datepicker-popper"
selected={parseISO(fromDate)} selected={parseISO(fromDate)}
onChange={(date) => date && setFromDate(formatDate(date))} onChange={(date) => date && setFromDate(formatDate(date))}
selectsStart selectsStart
@@ -42,6 +44,8 @@ const DateRangePickerMeldungen: React.FC<Props> = ({
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<label className="text-sm font-semibold">Bis</label> <label className="text-sm font-semibold">Bis</label>
<DatePicker <DatePicker
portalId="root-portal" // beliebige ID
popperClassName="custom-datepicker-popper"
selected={parseISO(toDate)} selected={parseISO(toDate)}
onChange={(date) => date && setToDate(formatDate(date))} onChange={(date) => date && setToDate(formatDate(date))}
selectsEnd 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">
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10">
<tr>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
Prio
</th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
Zeitstempel
</th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
Quelle
</th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
Meldung
</th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
Status
</th>
</tr>
</thead>
<tbody>
{messages.map((msg, index) => (
<tr
key={index}
className="hover:bg-gray-100 dark:hover:bg-gray-700"
>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<div
className="w-4 h-4 rounded"
style={{ backgroundColor: msg.c }}
></div>
</td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
{new Date(msg.t).toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})}
</td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
{msg.i}
</td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
{msg.m}
</td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
{msg.v}
</td>
</tr>
))}
</tbody>
</table>
{messages.length === 0 && (
<div className="mt-4 text-center text-gray-500 italic">
Keine Meldungen im gewählten Zeitraum vorhanden.
</div>
)}
</div>
);
}

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="bg-littwin-blue text-white px-4 py-2 rounded h-fit"
>
Anzeigen
</button>
<Listbox value={sourceFilter} onChange={setSourceFilter}>
<div className="relative ml-6 w-64">
<Listbox.Button className="bg-white text-gray-900 w-full border px-4 py-2 rounded text-left flex justify-between items-center dark:bg-gray-900 dark:text-gray-100">
<span>{sourceFilter}</span>
<svg
className="w-5 h-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="bg-white absolute z-50 mt-1 w-full border rounded dark:bg-gray-900">
{sources.map((src) => (
<Listbox.Option
key={src}
value={src}
className={({ selected, active, disabled }) =>
`px-4 py-2 cursor-pointer text-gray-900 dark:text-gray-100 ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-blue-100 dark:bg-gray-700 dark:text-white"
: disabled
? "opacity-50 text-gray-400 dark:text-gray-500 cursor-not-allowed"
: ""
}`
}
>
{src}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
<MeldungenTabelle messages={filteredMessages} />
</div>
);
}

View File

@@ -10,7 +10,7 @@ import { useAdminAuth } from "./hooks/useAdminAuth";
const DatabaseSettings: React.FC = () => { const DatabaseSettings: React.FC = () => {
const { isAdminLoggedIn } = useAdminAuth(true); const { isAdminLoggedIn } = useAdminAuth(true);
return ( return (
<div className="p-6 bg-gray-100 max-w-5xl mr-auto rounded shadow"> <div className="p-6 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto rounded shadow text-gray-900 dark:text-gray-100">
<h2 className="text-lg font-bold mb-6">Datenbank Einstellungen</h2> <h2 className="text-lg font-bold mb-6">Datenbank Einstellungen</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">

View File

@@ -64,7 +64,7 @@ const GeneralSettings: React.FC = () => {
}, [systemSettings]); }, [systemSettings]);
return ( return (
<div className="p-6 md:p-3 bg-gray-100 max-w-5xl mr-auto overflow-y-auto max-h-[calc(100vh-200px)] "> <div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto overflow-y-auto max-h-[calc(100vh-200px)] dark:text-gray-100 ">
<h2 className="text-sm md:text-md font-bold mb-2"> <h2 className="text-sm md:text-md font-bold mb-2">
Allgemeine Einstellungen Allgemeine Einstellungen
</h2> </h2>
@@ -74,7 +74,7 @@ const GeneralSettings: React.FC = () => {
<label className="block text-xs md:text-sm font-medium">Name:</label> <label className="block text-xs md:text-sm font-medium">Name:</label>
<input <input
type="text" type="text"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs" className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
/> />
@@ -87,7 +87,7 @@ const GeneralSettings: React.FC = () => {
</label> </label>
<input <input
type="text" type="text"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs" className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
value={mac1} value={mac1}
disabled disabled
/> />
@@ -101,7 +101,7 @@ const GeneralSettings: React.FC = () => {
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
<input <input
type="text" type="text"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs" className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
value={systemUhr.replace(/\s*Uhr$/, "")} value={systemUhr.replace(/\s*Uhr$/, "")}
disabled disabled
/> />
@@ -120,7 +120,7 @@ const GeneralSettings: React.FC = () => {
<label className="block text-xs md:text-sm font-medium">IP:</label> <label className="block text-xs md:text-sm font-medium">IP:</label>
<input <input
type="text" type="text"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs" className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
value={ip} value={ip}
onChange={(e) => setIp(e.target.value)} onChange={(e) => setIp(e.target.value)}
/> />
@@ -131,7 +131,7 @@ const GeneralSettings: React.FC = () => {
</label> </label>
<input <input
type="text" type="text"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs" className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
value={subnet} value={subnet}
onChange={(e) => setSubnet(e.target.value)} onChange={(e) => setSubnet(e.target.value)}
/> />
@@ -142,7 +142,7 @@ const GeneralSettings: React.FC = () => {
</label> </label>
<input <input
type="text" type="text"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs" className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
value={gateway} value={gateway}
onChange={(e) => setGateway(e.target.value)} onChange={(e) => setGateway(e.target.value)}
/> />

View File

@@ -27,7 +27,7 @@ const NTPSettings: React.FC = () => {
} }
return ( return (
<div className="p-6 md:p-3 bg-gray-100 max-w-5xl mr-auto"> <div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100">
<h2 className="text-sm md:text-md font-bold mb-4">NTP Einstellungen</h2> <h2 className="text-sm md:text-md font-bold mb-4">NTP Einstellungen</h2>
<div className="grid md:grid-cols-2 gap-3"> <div className="grid md:grid-cols-2 gap-3">
@@ -35,7 +35,7 @@ const NTPSettings: React.FC = () => {
<label className="block text-xs font-medium">NTP Server 1</label> <label className="block text-xs font-medium">NTP Server 1</label>
<input <input
type="text" type="text"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs" className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
value={ntp1} value={ntp1}
onChange={(e) => setNtp1(e.target.value)} onChange={(e) => setNtp1(e.target.value)}
/> />
@@ -45,7 +45,7 @@ const NTPSettings: React.FC = () => {
<label className="block text-xs font-medium">NTP Server 2</label> <label className="block text-xs font-medium">NTP Server 2</label>
<input <input
type="text" type="text"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs" className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
value={ntp2} value={ntp2}
onChange={(e) => setNtp2(e.target.value)} onChange={(e) => setNtp2(e.target.value)}
/> />
@@ -55,7 +55,7 @@ const NTPSettings: React.FC = () => {
<label className="block text-xs font-medium">NTP Server 3</label> <label className="block text-xs font-medium">NTP Server 3</label>
<input <input
type="text" type="text"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs" className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
value={ntp3} value={ntp3}
onChange={(e) => setNtp3(e.target.value)} onChange={(e) => setNtp3(e.target.value)}
/> />
@@ -65,18 +65,19 @@ const NTPSettings: React.FC = () => {
<label className="block text-xs font-medium">Zeitzone</label> <label className="block text-xs font-medium">Zeitzone</label>
<input <input
type="text" type="text"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs" className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
value={ntpTimezone} value={ntpTimezone}
onChange={(e) => setNtpTimezone(e.target.value)} onChange={(e) => setNtpTimezone(e.target.value)}
/> />
</div> </div>
<div className="col-span-2 flex items-center gap-2 mt-2"> <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 <input
type="checkbox" type="checkbox"
checked={active} checked={active}
onChange={(e) => setActive(e.target.checked)} onChange={(e) => setActive(e.target.checked)}
className="accent-littwin-blue w-4 h-4"
/> />
</div> </div>

View File

@@ -11,6 +11,9 @@ export default function OPCUAInterfaceSettings() {
(state: RootState) => state.opcuaSettingsSlice (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 // Lokale Zustände für das neue Benutzerformular
const [nodesetName, setNodesetName] = useState( const [nodesetName, setNodesetName] = useState(
@@ -18,7 +21,7 @@ export default function OPCUAInterfaceSettings() {
); );
return ( return (
<div className="p-6 md:p-3 bg-gray-100 max-w-5xl mr-auto "> <div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100 ">
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center mb-3">
<Image <Image
src="/images/OPCUA.jpg" src="/images/OPCUA.jpg"
@@ -71,7 +74,7 @@ export default function OPCUAInterfaceSettings() {
{/* ✅ OPCUA Zustand */} {/* ✅ OPCUA Zustand */}
<div className="mb-3"> <div className="mb-3">
<label className="block font-medium text-sm mb-1">OPCUA Zustand</label> <label className="block font-medium text-sm mb-1">OPCUA Zustand</label>
<div className="p-1 border border-gray-300 rounded-md bg-white text-sm"> <div className="p-1 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-sm text-gray-900 dark:text-gray-100">
{opcuaSettings.opcUaZustand} {opcuaSettings.opcUaZustand}
</div> </div>
</div> </div>
@@ -82,22 +85,32 @@ export default function OPCUAInterfaceSettings() {
<div className="flex"> <div className="flex">
<input <input
type="text" type="text"
className="flex-grow p-1 border border-gray-300 rounded-l-md text-sm" className="flex-grow p-1 border border-gray-300 dark:border-gray-700 rounded-l-md text-sm !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
value={nodesetName} value={nodesetName}
onChange={(e) => setNodesetName(e.target.value)} onChange={(e) => setNodesetName(e.target.value)}
disabled={opcuaSettings.isEnabled} // Disable input when server is enabled disabled={opcuaSettings.isEnabled} // Disable input when server is enabled
/> />
{/* {/*
<button <button
onClick={handleNodesetUpdate} onClick={handleNodesetUpdate}
className="px-3 py-1 bg-littwin-blue text-white rounded-r-md text-sm" className="px-3 py-1 bg-littwin-blue text-white rounded-r-md text-sm"
> >
Übernehmen Übernehmen
</button> </button>
*/} */}
</div> </div>
</div> </div>
{/* ✅ Anzahl der aktuellen OPC-Clients */}
<div className="mb-3">
<label className="block font-medium text-sm mb-1">
Aktuelle OPC-Clients
</label>
<div className="p-1 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-sm text-gray-900 dark:text-gray-100">
{opcUaActiveClientCount}
</div>
</div>
{/* ✅ Benutzerverwaltung */} {/* ✅ Benutzerverwaltung */}
{/* {/*

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

@@ -31,8 +31,14 @@ const UserManagementSettings: React.FC = () => {
); );
}; };
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
handleLogin();
}
};
return ( return (
<div className="p-6 md:p-3 bg-gray-100 max-w-5xl mr-auto"> <div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100">
<h2 className="text-sm md:text-md font-bold mb-4">Login Admin-Bereich</h2> <h2 className="text-sm md:text-md font-bold mb-4">Login Admin-Bereich</h2>
{/* Admin Login/Logout */} {/* Admin Login/Logout */}
@@ -51,16 +57,18 @@ const UserManagementSettings: React.FC = () => {
<input <input
type="text" type="text"
placeholder="Benutzername" placeholder="Benutzername"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs" className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
onKeyDown={handleKeyDown}
/> />
<input <input
type="password" type="password"
placeholder="Passwort" placeholder="Passwort"
className="border border-gray-300 rounded h-8 p-1 w-full text-xs" className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
onKeyDown={handleKeyDown}
/> />
<button <button
type="button" type="button"

View File

@@ -29,6 +29,17 @@ export function useAdminAuth(showModal: boolean) {
function logoutAdmin() { function logoutAdmin() {
sessionStorage.removeItem("token"); sessionStorage.removeItem("token");
localStorage.setItem("isAdminLoggedIn", "false"); 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); setAdminLoggedIn(false);
} }

View File

@@ -1,21 +1,34 @@
"use client"; "use client";
// @/components/main/settingsPageComponents/modals/ProgressModal.tsx
import React from "react"; import React from "react";
type Props = { type Props = {
visible: boolean; visible: boolean;
progress: number; progress: number;
slot?: number;
}; };
const ProgressModal: React.FC<Props> = ({ visible, progress }) => { const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
if (!visible) return null; if (!visible) return null;
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 ">
<div className="bg-white p-6 rounded shadow-md text-center w-80"> <div className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 p-6 rounded shadow-md text-center w-80">
<h2 className="text-lg font-bold mb-4">Firmwareupdate läuft...</h2> {/*
<h2 className="text-lg font-bold mb-4">
Firmwareupdate
{typeof slot === "number" ? ` KÜ ${slot}` : ""} läuft ...
</h2>
*/}
<h2 className="text-lg font-bold mb-4">
Firmwareupdate läuft ...
{typeof slot === "number" ? ` ` : ""}
</h2>
Bitte Fenster nicht schließen
<h2></h2>
<div className="w-full bg-gray-200 rounded-full h-4"> <div className="w-full bg-gray-200 rounded-full h-4">
<div <div
className="bg-blue-500 h-4 rounded-full transition-all duration-100" className="bg-littwin-blue h-4 rounded-full transition-all duration-100"
style={{ width: `${progress}%` }} style={{ width: `${progress}%` }}
></div> ></div>
</div> </div>

View File

@@ -0,0 +1,523 @@
"use client";
// /components/main/system/DetailModal.tsx
import React, { useEffect, useRef, useState, useCallback } from "react";
import { Line } from "react-chartjs-2";
import { useSelector } from "react-redux";
import { RootState, useAppDispatch } from "@/redux/store";
import { Listbox } from "@headlessui/react";
import { setFullScreen } from "@/redux/slices/kabelueberwachungChartSlice";
import DateRangePicker from "@/components/common/DateRangePicker";
import {
setVonDatum,
setBisDatum,
} from "@/redux/slices/kabelueberwachungChartSlice";
// Import Thunks
import { getSystemspannung5VplusThunk } from "@/redux/thunks/getSystemspannung5VplusThunk";
import { getSystemspannung15VplusThunk } from "@/redux/thunks/getSystemspannung15VplusThunk";
import { getSystemspannung15VminusThunk } from "@/redux/thunks/getSystemspannung15VminusThunk";
import { getSystemspannung98VminusThunk } from "@/redux/thunks/getSystemspannung98VminusThunk";
import { getTemperaturAdWandlerThunk } from "@/redux/thunks/getTemperaturAdWandlerThunk";
import { getTemperaturProzessorThunk } from "@/redux/thunks/getTemperaturProzessorThunk";
import {
Chart as ChartJS,
LineElement,
PointElement,
CategoryScale,
LinearScale,
Title,
Tooltip,
Legend,
Filler,
TimeScale,
} from "chart.js";
import "chartjs-adapter-date-fns";
import { de } from "date-fns/locale";
ChartJS.register(
LineElement,
PointElement,
CategoryScale,
LinearScale,
Title,
Tooltip,
Legend,
Filler,
TimeScale
);
// Tailwind-basierte Farbdefinitionen für Chart.js
const chartColors = {
gray: {
line: "#6B7280", // tailwind gray-500
background: "rgba(107, 114, 128, 0.2)", // tailwind gray-500 mit opacity
},
littwinBlue: {
line: "#00AEEF", // littwin-blue
background: "rgba(0, 174, 239, 0.2)", // littwin-blue mit opacity
},
};
type ReduxDataEntry = {
//Alle DIA0 t,m,i,a , DIA1 und DIA2 t,i,a,g
t: string; // Zeitstempel
i: number; // Minimum
a: number; // Maximum
g?: number; // Durchschnitt (optional, falls vorhanden)
m?: number; // aktueller Messwert (optional, falls vorhanden)
};
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: "top" as const },
title: {
display: true,
text: "Verlauf",
},
tooltip: {
mode: "index" as const,
intersect: false,
callbacks: {
label: function (ctx: any) {
return `Messwert: ${ctx.parsed.y}`;
},
title: function (items: any[]) {
const date = items[0].parsed.x;
return `Zeitpunkt: ${new Date(date).toLocaleString("de-DE")}`;
},
},
},
zoom: {
pan: { enabled: true, mode: "x" as const },
zoom: {
wheel: { enabled: true },
pinch: { enabled: true },
mode: "x" as const,
},
},
},
scales: {
x: {
type: "time" as const,
time: {
unit: "day" as const,
tooltipFormat: "dd.MM.yyyy HH:mm",
displayFormats: {
day: "dd.MM.yyyy",
},
},
adapters: {
date: { locale: de },
},
title: {
display: true,
text: "Zeit",
},
},
y: {
title: {
display: true,
text: "Messwert",
},
},
},
};
type Props = {
isOpen: boolean;
selectedKey: string | null;
onClose: () => void;
zeitraum: "DIA0" | "DIA1" | "DIA2";
setZeitraum: (typ: "DIA0" | "DIA1" | "DIA2") => void;
};
export const DetailModal = ({
isOpen,
selectedKey,
onClose,
zeitraum,
setZeitraum,
}: Props) => {
// Stable empty reference to avoid React-Redux dev warning about selector returning new [] each call
const EMPTY_REDUX_DATA: ReadonlyArray<ReduxDataEntry> = Object.freeze([]);
const chartRef = useRef<any>(null);
const [chartData, setChartData] = useState<any>({
datasets: [],
});
const [isLoading, setIsLoading] = useState(false);
const [shouldUpdateChart, setShouldUpdateChart] = useState(false);
const [forceUpdate, setForceUpdate] = useState(0); // Für periodische UI-Updates
const reduxData = useSelector((state: RootState) => {
switch (selectedKey) {
case "+5V":
return state.systemspannung5Vplus[zeitraum];
case "+15V":
return state.systemspannung15Vplus[zeitraum];
case "-15V":
return state.systemspannung15Vminus[zeitraum];
case "-98V":
return state.systemspannung98Vminus[zeitraum];
case "ADC Temp":
return state.temperaturAdWandler[zeitraum];
case "CPU Temp":
return state.temperaturProzessor[zeitraum];
default:
return EMPTY_REDUX_DATA;
}
}) as ReduxDataEntry[];
const isFullScreen = useSelector(
(state: RootState) => state.kabelueberwachungChartSlice.isFullScreen
);
const dispatch = useAppDispatch();
// API-Request beim Klick auf "Daten laden" - memoized für useEffect dependency
const handleFetchData = useCallback(() => {
setIsLoading(true);
// Clear previous chart data
setChartData({ datasets: [] });
// Flag setzen, dass Chart nach Datenempfang aktualisiert werden soll
setShouldUpdateChart(true);
switch (selectedKey) {
case "+5V":
dispatch(getSystemspannung5VplusThunk(zeitraum));
break;
case "+15V":
dispatch(getSystemspannung15VplusThunk(zeitraum));
break;
case "-15V":
dispatch(getSystemspannung15VminusThunk(zeitraum));
break;
case "-98V":
dispatch(getSystemspannung98VminusThunk(zeitraum));
break;
case "ADC Temp":
dispatch(getTemperaturAdWandlerThunk(zeitraum));
break;
case "CPU Temp":
dispatch(getTemperaturProzessorThunk(zeitraum));
break;
default:
break;
}
}, [selectedKey, zeitraum, dispatch]);
// Reset Zeitraum auf DIA0 und Datumswerte wenn Modal geöffnet wird
useEffect(() => {
if (isOpen) {
setZeitraum("DIA0");
dispatch(setVonDatum(""));
dispatch(setBisDatum(""));
// Chart-Daten zurücksetzen beim Öffnen
setChartData({ datasets: [] });
}
}, [isOpen, setZeitraum, dispatch]);
// Periodische UI-Updates alle 2 Sekunden während Wartezeit
useEffect(() => {
if (isOpen && (!chartData.datasets || chartData.datasets.length === 0)) {
const interval = setInterval(() => {
setForceUpdate((prev) => prev + 1); // Force re-render für cursor-wait Update
}, 2000);
return () => clearInterval(interval);
}
}, [isOpen, chartData.datasets]);
// Automatisches "Daten laden" alle 4 Sekunden, maximal 2 Versuche
useEffect(() => {
if (isOpen && (!chartData.datasets || chartData.datasets.length === 0)) {
let attempts = 0;
const interval = setInterval(() => {
if (attempts < 2) {
console.log("Auto-clicking 'Daten laden' button...");
handleFetchData();
attempts++;
} else {
clearInterval(interval);
}
}, 4000);
return () => clearInterval(interval);
}
}, [isOpen, chartData.datasets, handleFetchData]);
const toggleFullScreen = () => {
dispatch(setFullScreen(!isFullScreen));
setTimeout(() => {
chartRef.current?.resize();
}, 50);
};
const handleClose = () => {
dispatch(setFullScreen(false));
dispatch(setVonDatum(""));
dispatch(setBisDatum(""));
onClose();
};
useEffect(() => {
const loadZoomPlugin = async () => {
if (typeof window !== "undefined") {
const zoomPlugin = (await import("chartjs-plugin-zoom")).default;
if (!ChartJS.registry.plugins.get("zoom")) {
ChartJS.register(zoomPlugin);
}
}
};
loadZoomPlugin();
}, []);
useEffect(() => {
if (chartRef.current && selectedKey) {
chartRef.current.options.plugins.title.text = `Verlauf ${selectedKey}`;
chartRef.current.update("none");
}
}, [selectedKey]);
useEffect(() => {
if (chartRef.current) {
chartRef.current.resetZoom();
}
}, [zeitraum]);
// Chart.js animation complete callback to set isLoading false
useEffect(() => {
if (chartRef.current && isLoading) {
const chartInstance = chartRef.current;
// Save previous callback to restore later
const prevCallback = chartInstance.options.animation?.onComplete;
chartInstance.options.animation = {
...chartInstance.options.animation,
onComplete: () => {
setIsLoading(false);
if (typeof prevCallback === "function") prevCallback();
},
};
chartInstance.update();
}
}, [chartData, isLoading]);
// Update chart data when Redux data changes (only after button click)
useEffect(() => {
if (shouldUpdateChart && reduxData && reduxData.length > 0) {
console.log("Redux data for chart:", reduxData);
// Create datasets array for multiple lines
const datasets = [];
// Check which data fields are available and create datasets accordingly
const hasMinimum = reduxData.some(
(entry) => entry.i !== undefined && entry.i !== null && entry.i !== 0
);
const hasMaximum = reduxData.some(
(entry) => entry.a !== undefined && entry.a !== null
);
const hasAverage = reduxData.some(
(entry) => entry.g !== undefined && entry.g !== null
);
const hasCurrent = reduxData.some(
(entry) => entry.m !== undefined && entry.m !== null
);
// Zuerst Hintergrund-Linien (Minimum/Maximum) - grau
if (hasMinimum) {
datasets.push({
label: "Messwert Minimum",
data: reduxData.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: reduxData.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: reduxData.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: reduxData.map((entry) => ({
x: new Date(entry.t).getTime(),
y: entry.m || 0,
})),
borderColor: chartColors.littwinBlue.line,
backgroundColor: chartColors.littwinBlue.background,
tension: 0.1,
fill: false,
order: 2,
});
}
const newChartData = {
datasets: datasets,
};
console.log("Chart datasets:", datasets.length, "lines");
setChartData(newChartData);
setShouldUpdateChart(false); // Reset flag
} else if (shouldUpdateChart && (!reduxData || reduxData.length === 0)) {
console.log("No Redux data available");
setChartData({ datasets: [] });
setShouldUpdateChart(false); // Reset flag
}
}, [reduxData, selectedKey, shouldUpdateChart]);
if (!isOpen || !selectedKey) return null;
// Prüfen ob Chart Daten haben (für cursor-wait)
const hasChartData = chartData.datasets && chartData.datasets.length > 0;
return (
<div
className={`fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50 ${
!hasChartData ? "cursor-wait" : ""
}`}
>
<div
className={`bg-white p-6 rounded-xl overflow-auto shadow-2xl transition-all duration-300 ${
isFullScreen ? "w-[95vw] h-[90vh]" : "w-[50%] h-[60%]"
} ${!hasChartData ? "cursor-wait" : ""}`}
>
<div className="relative">
<h2 className="text-xl font-semibold">
Detailansicht: {selectedKey}
</h2>
<div className="absolute top-0 right-0 flex gap-3">
<button
onClick={toggleFullScreen}
className="text-2xl text-gray-600 hover:text-gray-800"
>
<i
className={
isFullScreen
? "bi bi-fullscreen-exit"
: "bi bi-arrows-fullscreen"
}
></i>
</button>
<button
onClick={handleClose}
className="text-2xl text-gray-600 hover:text-gray-800"
>
<i className="bi bi-x-circle-fill"></i>
</button>
</div>
</div>
<div className="flex items-center justify-start gap-4 mb-4 flex-wrap">
<DateRangePicker />
<label className="font-medium">Zeitraum:</label>
<Listbox value={zeitraum} onChange={setZeitraum}>
<div className="relative w-48">
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
<span>
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündlich",
DIA2: "Täglich",
}[zeitraum]
}
</span>
<svg
className="w-5 h-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white dark:bg-gray-800 shadow max-h-60 overflow-auto text-sm border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100">
{["DIA0", "DIA1", "DIA2"].map((option) => (
<Listbox.Option
key={option}
value={option}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200 dark:bg-gray-700"
: ""
}`
}
>
{
{
DIA0: "Alle Messwerte",
DIA1: "Stündlich",
DIA2: "Täglich",
}[option]
}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
<button
onClick={handleFetchData}
className={`px-4 py-1 bg-littwin-blue text-white rounded text-sm ${
isLoading ? "cursor-wait" : ""
}`}
disabled={isLoading}
>
{isLoading ? "Laden..." : "Daten laden"}
</button>
</div>
<div className="h-[85%] bg-white dark:bg-gray-800 rounded shadow border border-gray-200 dark:border-gray-700 p-2">
<Line ref={chartRef} data={chartData} options={chartOptions} />
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,185 @@
// components/main/system/SystemCharts.tsx
import React from "react";
import { Line } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from "chart.js";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
export type HistoryEntry = {
time: string | number | Date;
"+5V": number;
"+15V": number;
"-15V": number;
"-98V": number;
"ADC Temp": number;
"CPU Temp": number;
};
type Props = {
history: HistoryEntry[];
zeitraum: "DIA0" | "DIA1" | "DIA2";
};
export const SystemCharts = ({ history }: Props) => {
const [isLoading, setIsLoading] = React.useState(true);
const reversedHistory = [...history].reverse();
const labels = reversedHistory.map((h) =>
new Date(h.time).toLocaleTimeString()
);
const formatValue = (v: number) => v.toFixed(2);
// Chart.js animation callback
const animation = {
onComplete: () => {
setIsLoading(false);
},
};
React.useEffect(() => {
setIsLoading(true);
}, [history]);
const baseOptions = {
responsive: true,
maintainAspectRatio: false,
animation,
scales: {
y: {
beginAtZero: false,
grid: { color: "rgba(200,200,200,0.2)" },
title: { display: true, text: "Wert" },
},
x: {
grid: { color: "rgba(200,200,200,0.2)" },
title: { display: true, text: "Zeit" },
},
},
plugins: {
legend: { position: "bottom" as const },
},
};
return (
<div
className={`grid grid-cols-1 xl:grid-cols-2 gap-8 ${
isLoading ? "cursor-wait" : ""
}`}
>
<div className="h-[300px]">
<Line
data={{
labels,
datasets: [
{
label: "+5V",
data: history.map((h) => formatValue(h["+5V"])),
borderColor: "rgba(59,130,246,1)",
backgroundColor: "rgba(59,130,246,0.5)",
fill: false,
},
{
label: "+15V",
data: history.map((h) => formatValue(h["+15V"])),
borderColor: "rgba(34,197,94,1)",
backgroundColor: "rgba(34,197,94,0.5)",
fill: false,
},
{
label: "-15V",
data: history.map((h) => formatValue(h["-15V"])),
borderColor: "rgba(239,68,68,1)",
backgroundColor: "rgba(239,68,68,0.5)",
fill: false,
},
{
label: "-98V",
data: history.map((h) => formatValue(h["-98V"])),
borderColor: "rgba(234,179,8,1)",
backgroundColor: "rgba(234,179,8,0.5)",
fill: false,
},
],
}}
options={{
...baseOptions,
scales: {
...baseOptions.scales,
y: {
...baseOptions.scales.y,
title: {
display: true,
text: "Spannung (V)", // 👉 Einheit hinzugefügt
},
},
},
plugins: {
...baseOptions.plugins,
title: { display: true, text: "Systemspannungen" },
},
}}
/>
</div>
<div className="h-[300px]">
<Line
data={{
labels,
datasets: [
{
label: "ADC Temp",
data: history.map((h) => h["ADC Temp"]),
borderColor: "rgba(168,85,247,1)",
backgroundColor: "rgba(168,85,247,0.5)",
fill: false,
},
{
label: "CPU Temp",
data: history.map((h) =>
parseFloat(formatValue(h["CPU Temp"]))
),
borderColor: "rgba(251,191,36,1)",
backgroundColor: "rgba(251,191,36,0.5)",
fill: false,
},
],
}}
options={{
...baseOptions,
scales: {
...baseOptions.scales,
y: {
...baseOptions.scales.y,
title: {
display: true,
text: "Temperatur (°C)", // 👉 Einheit hinzugefügt
},
},
},
plugins: {
...baseOptions.plugins,
title: { display: true, text: "Systemtemperaturen" },
},
}}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,36 @@
// components/main/system/SystemOverviewGrid.tsx
import React from "react";
type Props = {
voltages: Record<string, number>;
onOpenDetail: (key: string) => void;
};
export const SystemOverviewGrid = ({ voltages, onOpenDetail }: Props) => {
const formatValue = (value: number) => value.toFixed(2);
return (
<div className="grid grid-cols-2 gap-4 mb-2">
{Object.entries(voltages).map(([key, value]) => {
const unit = key.includes("Temp") ? "\u00b0C" : "V";
return (
<div
key={key}
className="p-4 border rounded shadow bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100"
>
<h2 className="font-semibold">{key}</h2>
<p>
{formatValue(value)} {unit}
<button
onClick={() => onOpenDetail(key)}
className="ml-2 text-littwin-blue hover:underline text-sm"
>
Detailansicht
</button>
</p>
</div>
);
})}
</div>
);
};

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 "-98V":
dispatch(getSystemspannung98VminusThunk(zeitraum));
break;
case "ADC Temp":
dispatch(getTemperaturAdWandlerThunk(zeitraum));
break;
case "CPU Temp":
dispatch(getTemperaturProzessorThunk(zeitraum));
break;
default:
break;
}
};
const handleCloseDetail = () => {
setIsModalOpen(false);
};
return (
<div className="p-4 bg-white dark:bg-gray-900">
<h1 className="text-xl font-bold mb-4">
System Spannungen & Temperaturen
</h1>
{isLoading ? (
<div className="flex justify-center items-center h-[400px]">
<div className="text-center">
<ClipLoader size={50} color="#3B82F6" />
<p className="mt-4 text-gray-500">
Lade Systemdaten bitte warten
</p>
</div>
</div>
) : (
<>
<SystemOverviewGrid
voltages={voltages}
onOpenDetail={handleOpenDetail}
/>
<SystemCharts history={history} zeitraum={zeitraum} />
</>
)}
<DetailModal
isOpen={isModalOpen}
selectedKey={selectedKey}
onClose={handleCloseDetail}
zeitraum={zeitraum}
setZeitraum={setZeitraum}
/>
</div>
);
};
export default SystemPage;

View File

@@ -26,7 +26,7 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
{ name: "Kabelüberwachung ", path: "/kabelueberwachung" }, { name: "Kabelüberwachung ", path: "/kabelueberwachung" },
{ name: "Meldungseingänge ", path: "/digitalInputs" }, //vorher Digitale Ein -und Ausgänge { 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: "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: "Berichte ", path: "/meldungen" },
{ name: "System ", path: "/system" }, { name: "System ", path: "/system" },
{ name: "Einstellungen ", path: "/einstellungen" }, { name: "Einstellungen ", path: "/einstellungen" },
@@ -36,25 +36,26 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
]; ];
return ( return (
<aside> <aside className="bg-white dark:bg-gray-900 h-full">
<nav className={`h-full flex-shrink-0 mt-16 ${className || "w-48"}`}> <nav className={`h-full flex-shrink-0 mt-16 ${className || "w-48"}`}>
{menuItems.map((item) => ( {menuItems.map((item) => (
<div key={item.name}> <div key={item.name}>
{item.disabled ? ( {item.disabled ? (
<div className="block px-4 py-2 mb-4 font-bold whitespace-nowrap text-gray-400 cursor-not-allowed text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg"> <div className="block px-4 py-2 mb-4 font-bold whitespace-nowrap text-gray-400 dark:text-gray-600 cursor-not-allowed text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg">
{item.name} {item.name}
</div> </div>
) : ( ) : (
<Link href={formatPath(item.path)}> <Link
<div href={formatPath(item.path)}
className={`block px-4 py-2 mb-4 font-bold whitespace-nowrap transition duration-300 text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg ${ prefetch={false}
activeLink.startsWith(item.path) onClick={() => setActiveLink(item.path)}
? "bg-sky-500 text-white rounded-r-full xl:mr-4 xl:w-full" 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 ${
: "text-black hover:bg-gray-200 rounded-r-full" activeLink.startsWith(item.path)
}`} ? "bg-sky-500 text-white rounded-r-full xl:mr-4 xl:w-full dark:bg-sky-600 dark:text-white"
> : "text-black hover:bg-gray-200 rounded-r-full dark:text-gray-200 dark:hover:bg-gray-800"
{item.name} }`}
</div> >
{item.name}
</Link> </Link>
)} )}
</div> </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)

View File

@@ -18,3 +18,74 @@
- [ ] TODO: Alle Kabelüberwachungsmodule mit ein Button Updaten , in Einstellungen und in Kabelüberwachungsmodul Modal - [ ] 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) ![Zusatzfunktionen Kai 25.06.2025](./TODOsScreenshots/Zusatzfunktionen_25-06-2025.png)
Zeit bis Ende August Zeit bis Ende August
- [ ] TODO: Überall Littwin-Blau und ausgewählt und grau wie bei Navigation bei Mouse over
- [ ] ## TODO: Messwerteingänge Diagrammme /Messkurven
23.07.2025
- [x] TODO: Isolationsfehler in Display anzeigen -> aktuell Zahl ist rot ohne Beschrifftung , es soll Zahl ISO MOhm und Isolationsfehler
- [x] TODO: Kilometer Km -> km kleingeschrieben 1000, 1024 wird Großgeschrieben Kilobyte Kb
- [x] TODO: Messwerteingänge Mouse couror wait beim laden, damit der user etwas wartet
- [ ] ## TODO: In KÜ, unter KÜ Balken/Bereich für Scheleife, Bereich für TDR wenn aktiv ist und Bereich für KVz wenn aktive ist
---
---
24.07.2025
- [ ] TODO: Bei System Messkurven Cursor await bis die daten lädt
- [ ] TODO: in KÜ DISPLAY Fehler, ISO Wert und Schleifenwwert bei wechsel Zustand anzeigen während Schleifenmessung und Isowemmsung und kalibirirung
- [ ] TODO: Bug in DatePicker in KÜ
- [ ] TODO: Kurven für ISO, RSL und TDR
- [x] TODO: in KÜ Steckplatz in KÜ umbenennen
- [x] TODO: Firmware Update Bestätigung in Littwin blau auch progress in littwinblue
- [x] TODO: KÜ Firmware in progress Bitte Fenster nicht schließen
- [ ] TODO: KÜ Kurve und letzte historische Meldungen anzeigen -> später
- [ ] TODO: KVz später
Anzeige KÜ-Display:
1. Zeile Alarm: Isolationsfehler, Schleifenfehler, Aderbruch, Erdschluß, Messpannung: Immer in Rot; wenn kein Alarm, bleibt die Zeile leer
2. Zeile: Isowert: xx MOhm (großes M)
in Rot, wenn Iso-Fehler ansteht
Beispiel: ISO: 100 MOHm der beim Abliech: ISO: Abgleich
3. Zeile: Schleifenwert, xx kOhm (kleines k)
in Rot, wenn Schleifenfehler ansteht
## Beispiel:: RSL: 1,7 kOhm oder wenn Schleifenmessung aktiv: RSL: Messung
## 11.08.2025
- [x] TODO: Bei Schleife starten messen wie lange es dauert, dann entsprechend progress balken einbauen, 2 Minuten erstellt
# 13.08.2025
- [x] TODO: Das Sichern und das Zurücksichern der KÜ-Daten über die Webseiten funktioniert nicht. Anscheinend ruft die Webseite keine ACP-Webseite mit Daten "?KSB02=1" auf sondern nur Daten "KSB02=1". Die CPL will dann die Datei KSB02=1 laden die es ja nicht gibt.
- [x] TODO: Kalibrieren Dauer entsprechend progress balken einbauen
- [x] TODO: Abgleich Dauer entsprechend progress balken einbauen
- [ ] TODO: Benutzer passwort ändern
- [ ] TODO: PlayWright
- ISO Abgleich 10 Minuten
- CPL läuft auf BUS-System, Wenn ein Kabelüberwachung z.B. beschäftigt mit ein Schleifen-Messung oder Iso- Abgleich dann belastet den CPL nicht wenn andere KÜs bedient werden, das erleichtet den CPL sogar
# 14.08.2025
- [x] TODO: Messwerteingänge Messkurven in Modal umwandeln
# 15.08.2025
- [x] BUGFIX: Messkurven-Modal lädt jetzt automatisch die Kurve beim Öffnen, Dropdown ist auf 'Alle Messwerte' (DIA0) initialisiert, und Filter werden beim Schließen zurückgesetzt. Dateien: IsoChartView.tsx, LoopChartView.tsx
# 01.09.2025
- [x] TODO: In KÜs Display ISO 2 Nachkommastellen und RSL 3 Nachkommastellen
- [ ] TODO: Schleife, Timer für jeder KÜ separate und nicht eine für alle, aktuell wird prozentzahl bei allen das gleiche angezeigt
- [x] TODO: RSL starten in RSL Messung starten umbenennen
- [x] TODO: TDR-Messung starten statt TDR aktivieren in ChartBar
- [x] TODO: KÜ TDR-aktiviert alert entfernen
- [ ] TODO: Systemdaten unter Detailansicht ein Verlaufsdiagramm hinzufügen mit Datumsauswahl
- [ ] TODO: Playwright testen mit der Entwicklung

View File

@@ -46,7 +46,7 @@ Sie ist **pro Slot aktivierbar** und bietet folgende Einstellungen:
### 🔁 Knotenpunkte-Anzeige ### 🔁 Knotenpunkte-Anzeige
Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines Steckplatzes: Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines es:
| Feld | Beschreibung | | 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) ## 🔄 Messungssteuerung (manuell)
Im unteren Bereich jedes Slots befindet sich ein **Kreispfeil-Icon** 🔄 (Reload-Symbol): 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 ### 🔁 Knotenpunkte-Anzeige
Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines Steckplatzes: Der Reiter **Knotenpunkte** zeigt die konfigurierte Struktur eines es:
| Feld | Beschreibung | | Feld | Beschreibung |
| -------------- | -------------------------------------------------- | | -------------- | -------------------------------------------------- |

View File

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

View File

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

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
]
}

View File

@@ -1,274 +0,0 @@
{
"win_de_state": [
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"win_de_invert": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"win_de_counter": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"win_de_time_filter": [
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"win_de_weighting": [
3,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"win_de_counter_active": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"win_de_offline": [
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"win_de_label": [
"DE1",
"DE2",
"DE3",
"DE4",
"DE5",
"DE6",
"DE7",
"DE8",
"DE9",
"DE10",
"DE11",
"DE12",
"DE13",
"DE14",
"DE15",
"DE16",
"DE17",
"DE18",
"DE19",
"DE20",
"DE21",
"DE22",
"DE23",
"DE24",
"DE25",
"DE26",
"DE27",
"DE28",
"DE29",
"DE30",
"DE31",
"DE32"
]
}

View File

@@ -1,14 +0,0 @@
{
"win_da_state": [
1,
0,
0,
1
],
"win_da_bezeichnung": [
"Ausgang1",
"Ausgang2",
"Ausgang3",
"Ausgang4"
]
}

View File

@@ -1,200 +0,0 @@
{
"win_kueOnline": [
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1
],
"win_kuePSTmMinus96V": [
0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
],
"win_kueCableBreak": [
1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1
],
"win_kueGroundFault": [
1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
],
"win_kueAlarm1": [
1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0,
1, 0, 0, 0, 0, 0, 0
],
"win_kueAlarm2": [
1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0,
0, 0, 0, 0, 0, 0, 0
],
"win_kueOverflow": [
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
],
"win_kueIso": [
10, 10, 10, 10.5, 10, 10, 10, 10, 10.5, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10.5, 10, 10, 10, 10, 10, 10.5, 10, 200, 200, 200, 200
],
"win_kueLimit1": [
3, 9.9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10
],
"win_kueDelay1": [
3, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420,
420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420,
420, 420
],
"win_kueResidence": [
0, 0.612, 0, 0.645, 0.822, 0.97, 0, 0, 1.452, 0, 0.734, 0.37, 0.566, 0,
0.738, 0.684, 1.166, 0.595, 0, 1.651, 1.18, 1.387, 1.214, 0, 1.475, 0.615,
0.494, 1.217, 65, 65, 65, 65
],
"win_kueLimit2Low": [
3, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1
],
"win_kueLimit2High": [
3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1
],
"win_kueLoopInterval": [
3, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6
],
"win_kueVersion": [
420, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419,
419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419,
419, 419
],
"win_kueID": [
"Test3",
"B23",
"Kabel 3",
"Kabel 4",
"Kabel 5",
"Kabel 6",
"FTZ4562",
"Kabel 8",
"12344",
"Kabel 10",
"Kabel 11",
"Kabel 12",
"Kabel 13",
"Kabel 14",
"Kabel 15",
"H56-77",
"Kabel 17",
"Kabel 18",
"Kabel 19",
"Kabel 20",
"Kabel 21",
"Kabel 22",
"Kabel 23",
"Kabel 24",
"Kabel 25",
"Kabel 26",
"Kabel 27",
"Kabel 28",
"Kabel 29",
"Kabel 30",
"Kabel 31",
"Kabel 32"
],
"win_kueName": [
"Linie 2",
"Edewecht 3",
"",
"Linie 4",
"Linie 5",
"",
"",
"Kabel_8",
"Kabel_9",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"Kabel 32"
],
"win_tdrActive": [
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1
],
"win_tdrAtten": [
11, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2
],
"win_tdrSpeed": [
112, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
100, 100
],
"win_tdrTrigger": [
102, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80
],
"win_tdrPulse": [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
],
"win_tdrAmp": [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
],
"win_tdrLocation": [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
],
"win_tdrLast": [
"2024-10-17 07:51:54:000",
"2024-09-30 08:38:50:000",
"?",
"?",
"?",
"?",
"?",
"?",
"2024-09-30 08:36:43:000",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?",
"?"
],
"win_memoryInterval": [
5, 0, 15, 0, 0, 15, 15, 0, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0
]
}

View File

@@ -1,142 +0,0 @@
[
{
"id": 25068,
"code": "02101",
"timestamp": "2025-04-22 04:56:28",
"message": "Isofehler gehend",
"status": 0
},
{
"id": 25067,
"code": "02101",
"timestamp": "2025-04-22 04:55:43",
"message": "Isofehler kommend",
"status": 1
},
{
"id": 25066,
"code": "02101",
"timestamp": "2025-04-22 04:48:39",
"message": "Isofehler gehend",
"status": 0
},
{
"id": 25065,
"code": "02101",
"timestamp": "2025-04-22 04:46:02",
"message": "Isofehler kommend",
"status": 1
},
{
"id": 25064,
"code": "02101",
"timestamp": "2025-04-22 04:38:58",
"message": "Isofehler gehend",
"status": 0
},
{
"id": 25063,
"code": "02101",
"timestamp": "2025-04-22 04:36:44",
"message": "Isofehler kommend",
"status": 1
},
{
"id": 25062,
"code": "02401",
"timestamp": "2025-04-22 04:35:38",
"message": "Isofehler kommend",
"status": 1
},
{
"id": 25061,
"code": "02401",
"timestamp": "2025-04-22 04:28:33",
"message": "Isofehler gehend",
"status": 0
},
{
"id": 25060,
"code": "02101",
"timestamp": "2025-04-22 02:56:28",
"message": "Isofehler gehend",
"status": 0
},
{
"id": 25059,
"code": "02101",
"timestamp": "2025-04-22 02:56:06",
"message": "Isofehler kommend",
"status": 1
},
{
"id": 25058,
"code": "02101",
"timestamp": "2025-04-22 02:40:27",
"message": "Isofehler gehend",
"status": 0
},
{
"id": 25057,
"code": "02101",
"timestamp": "2025-04-22 02:40:05",
"message": "Isofehler kommend",
"status": 1
},
{
"id": 25056,
"code": "02101",
"timestamp": "2025-04-22 02:26:40",
"message": "Isofehler gehend",
"status": 0
},
{
"id": 25055,
"code": "02101",
"timestamp": "2025-04-22 02:26:17",
"message": "Isofehler kommend",
"status": 1
},
{
"id": 25054,
"code": "02101",
"timestamp": "2025-04-22 02:16:56",
"message": "Isofehler gehend",
"status": 0
},
{
"id": 25053,
"code": "02101",
"timestamp": "2025-04-22 02:16:34",
"message": "Isofehler kommend",
"status": 1
},
{
"id": 25052,
"code": "02101",
"timestamp": "2025-04-22 02:09:30",
"message": "Isofehler gehend",
"status": 0
},
{
"id": 25051,
"code": "02101",
"timestamp": "2025-04-22 02:01:18",
"message": "Isofehler kommend",
"status": 1
},
{
"id": 25050,
"code": "02101",
"timestamp": "2025-04-22 01:54:35",
"message": "Isofehler gehend",
"status": 0
},
{
"id": 25049,
"code": "02101",
"timestamp": "2025-04-22 01:54:13",
"message": "Isofehler kommend",
"status": 1
}
]

View File

@@ -1,22 +0,0 @@
// /mocks/device-cgi-simulator/SERVICE/analogInputsMockData.js
var win_analogInputsValues = [
4.771072, 5.665244, 0.005467, -0.007468, 0.000002, 0.000001, 0.000001,
0.070007,
];
var win_analogInputsLabels = ["AE 11", "AE 2", "AE 3", "AE 4", "AE 5", "AE 6", "AE 7", "AE 8", ];
var win_analogInputsOffset = [21.999, 0.0, 0.0, 0, 0.0, 0.0, 0.0, 0.0];
var win_analogInputsFactor = [21.999, 1.0, 1.0, 1, 1.0, 1.0, 1.0, 1.0];
var win_analogInputsLoggerIntervall = [21, 10, 10, 10, 10, 10, 10, 10];
var win_analogInputsUnits = ["V", "V", "V", "V", "mA", "mA", "mA", "mA"];
var win_analogInputsWeighting = [0, 0, 0, 0, 0, 0, 0, 0];
/*
ID (z. B. 1, 2, ... 8) → Identifikation des Eingangs
Wert (z. B. 0, 22.91, 21) → Der analoge Wert
Bezeichnung (z. B. "----", "Feuchtigkeit", "Temperatur") → Name des Sensors
uW (Unterer Warnwert) → 1 = grün, 0 = grau
uG (Unterer Grenzwert) → 1 = grün, 0 = grau
oW (Oberer Warnwert) → 1 = orange, 0 = grau
oG (Oberer Grenzwert) → 1 = grün, 0 = grau
*/

View File

@@ -1,17 +1,17 @@
{ {
"win_analogInputsValues": [ "win_analogInputsValues": [
"126.812080", "126.630287",
"5.680176", "5.667177",
"-0.015003", "-0.000531",
"0.009538", "0.012762",
"-0.000001",
"0.000009",
"-0.000002", "-0.000002",
"0.000003", "-0.000012"
"-0.000005",
"0.000000"
], ],
"win_analogInputsLabels": [ "win_analogInputsLabels": [
"'AE 1'", "'AE 1'",
"'AE 2'", "Temperatur",
"'AE 3'", "'AE 3'",
"'AE 4'", "'AE 4'",
"'AE 5'", "'AE 5'",
@@ -21,7 +21,7 @@
], ],
"win_analogInputsUnits": [ "win_analogInputsUnits": [
"'V'", "'V'",
"'V'", "°C",
"'V'", "'V'",
"'V'", "'V'",
"'mA'", "'mA'",
@@ -31,7 +31,7 @@
], ],
"win_analogInputsFactor": [ "win_analogInputsFactor": [
"21.999", "21.999",
"1.000", 1,
"1.000", "1.000",
"1.000", "1.000",
"1.000", "1.000",
@@ -41,7 +41,7 @@
], ],
"win_analogInputsOffset": [ "win_analogInputsOffset": [
"21.999", "21.999",
"0.000", 0,
"0.000", "0.000",
"0.000", "0.000",
"0.000", "0.000",
@@ -49,10 +49,19 @@
"0.000", "0.000",
"0.000" "0.000"
], ],
"win_analogInputsWeighting": ["0", "0", "0", "0", "0", "0", "0", "0"], "win_analogInputsWeighting": [
"0",
"0",
"0",
"0",
"0",
"0",
"0",
"0"
],
"win_analogInputsLoggerIntervall": [ "win_analogInputsLoggerIntervall": [
"21", "21",
"10", 10,
"10", "10",
"10", "10",
"10", "10",
@@ -60,4 +69,4 @@
"10", "10",
"10" "10"
] ]
} }

View File

@@ -1,63 +0,0 @@
// auto-generated from update API
var win_de_state = [
1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
];
var win_de_invert = [
0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
];
var win_de_counter = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
];
var win_de_time_filter = [
3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
];
var win_de_weighting = [
3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
];
var win_de_counter_active = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
];
var win_de_offline = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
];
var win_de_label = [
"DE1",
"DE2",
"DE3",
"DE4",
"DE5",
"DE6",
"DE7",
"DE8",
"DE9",
"DE10",
"DE11",
"DE12",
"DE13",
"DE14",
"DE15",
"DE16",
"DE17",
"DE18",
"DE19",
"DE20",
"DE21",
"DE22",
"DE23",
"DE24",
"DE25",
"DE26",
"DE27",
"DE28",
"DE29",
"DE30",
"DE31",
"DE32",
];

View File

@@ -1,64 +1,50 @@
{ {
"win_de_state": [ "win_de_state": [
1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "0,0,1,0,0,0,0,0",
0, 0, 0, 0, 0, 0, 0 "0,0,0,0,0,0,0,0",
], "0,0,0,0,0,0,0,0",
"win_de_invert": [ "0,0,0,0,0,0,0,0"
0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
],
"win_de_counter": [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
],
"win_de_time_filter": [
3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
],
"win_de_weighting": [
3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
],
"win_de_counter_active": [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
],
"win_de_offline": [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
], ],
"win_de_label": [ "win_de_label": [
"DE1", "'DE 1','DE 2','DE 3','DE 4','DE 5','DE 6','DE 7','DE 8'",
"DE2", "'DE 9','DE 10','DE 11','DE 12','DE 13','DE 14','DE 15','DE 16'",
"DE3", "'DE 17','DE 18','DE 19','DE 20','DE 21','DE 22','DE 23','DE 24'",
"DE4", "'DE 25','DE 26','DE 27','DE 28','DE 29','DE 30','DE 31','DE 32'"
"DE5", ],
"DE6", "win_de_counter": [
"DE7", "0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000",
"DE8", "0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000",
"DE9", "0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000",
"DE10", "0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000"
"DE11", ],
"DE12", "win_de_time_filter": [
"DE13", "598.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000",
"DE14", "0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000",
"DE15", "0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000",
"DE16", "0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000"
"DE17", ],
"DE18", "win_de_weighting": [
"DE19", "998,0,0,0,0,0,0,0",
"DE20", "0,0,0,0,0,0,0,0",
"DE21", "0,0,0,0,0,0,0,0",
"DE22", "0,0,0,0,0,0,0,0"
"DE23", ],
"DE24", "win_de_invert": [
"DE25", "1,0,0,0,0,0,0,0",
"DE26", "0,0,0,0,0,0,0,0",
"DE27", "0,0,0,0,0,0,0,0",
"DE28", "0,0,0,0,0,0,0,0"
"DE29", ],
"DE30", "win_de_counter_active": [
"DE31", "0,0,0,0,0,0,0,0",
"DE32" "0,0,0,0,0,0,0,0",
"0,0,0,0,0,0,0,0",
"0,0,0,0,0,0,0,0"
],
"win_de_offline": [
"0,0,0,0,0,0,0,0",
"0,0,0,0,0,0,0,0",
"0,0,0,0,0,0,0,0",
"0,0,0,0,0,0,0,0"
] ]
} }

View File

@@ -15,8 +15,8 @@ var win_kuePSTmMinus96V = [
]; ];
//Aderbruch 1 = Fehler, 0 = kein Fehler //Aderbruch 1 = Fehler, 0 = kein Fehler
var win_kueCableBreak = [ var win_kueCableBreak = [
1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0,
]; ];
//Erdschluss 1 = Fehler, 0 = kein Fehler //Erdschluss 1 = Fehler, 0 = kein Fehler
var win_kueGroundFault = [ var win_kueGroundFault = [
@@ -25,12 +25,12 @@ var win_kueGroundFault = [
]; ];
//Isolationsfehler 1 = Fehler, 0 = kein Fehler, Alarm kommt wenn kueIso < kueLimit1 //Isolationsfehler 1 = Fehler, 0 = kein Fehler, Alarm kommt wenn kueIso < kueLimit1
var win_kueAlarm1 = [ var win_kueAlarm1 = [
1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]; ];
//Schleifenfehler 1 = Fehler, 0 = kein Fehler //Schleifenfehler 1 = Fehler, 0 = kein Fehler
var win_kueAlarm2 = [ var win_kueAlarm2 = [
1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]; ];
//Überlauf 1 = Fehler, 0 = kein Fehler , hier wird in Display ">200 MOhm" angezeigt //Überlauf 1 = Fehler, 0 = kein Fehler , hier wird in Display ">200 MOhm" angezeigt
@@ -62,9 +62,9 @@ die Filterzeit startet beim nächsten Unterschreiten des Grenzwerts neu. Die Fil
kurzfristige Schwankungen oder Störungen fälschlicherweise als Fehler gemeldet werden. kurzfristige Schwankungen oder Störungen fälschlicherweise als Fehler gemeldet werden.
*/ */
var win_kueDelay1 = [ var win_kueDelay1 = [
3, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420,
420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420,
420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420,
420, 420,
]; ];
//--------------------------------------------------- //---------------------------------------------------
//Schleifenwiderstand in Display (resDisplay) Einheit: KOhm //Schleifenwiderstand in Display (resDisplay) Einheit: KOhm
@@ -171,7 +171,7 @@ var win_kueName = [
//-------------TDR--------------------------------------------------- //-------------TDR---------------------------------------------------
var win_tdrActive = [ var win_tdrActive = [
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
]; ];
//---------------------------------------------------- //----------------------------------------------------
@@ -245,3 +245,31 @@ var win_memoryInterval = [
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0,
]; ];
//Speicherintervall (Kein, 1 MInute, 5 Minuten, 10 Minuten, 15 Minuten, 30 Minuten, 60 Minuten, 360 Minuten (6h), 720 Minuten (12h) //Speicherintervall (Kein, 1 MInute, 5 Minuten, 10 Minuten, 15 Minuten, 30 Minuten, 60 Minuten, 360 Minuten (6h), 720 Minuten (12h)
// Fall Sensoren Mock Data existing in the system for 32 cables
var win_fallSensorsActive = [
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1,
];
var win_fallSensors = [
{ id: "KVZ1", status: 0 },
{ id: "KVZ2", status: 1 },
{ id: "KVZ3", status: 1 },
{ id: "KVZ4", status: 1 },
];
// Event Schleifenmessung KSX
var loopMeasurementEvent = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
];
//Event TDR-Messung
var tdrMeasurementEvent = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
];
//Event Abgleich
var alignmentEvent = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
];

View File

@@ -1,4 +1,4 @@
// /device-cgi-simulator/SERVICE/SystemMockData.js // /device-cgi-simulator/SERVICE/systemMockData.js
var win_appVersion = "0.02"; var win_appVersion = "0.02";
var win_deviceName = "CPLV4 Ismail Rastede"; var win_deviceName = "CPLV4 Ismail Rastede";
var win_mac1 = "0 48 86 81 46 143"; var win_mac1 = "0 48 86 81 46 143";

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