Compare commits

...

6 Commits

Author SHA1 Message Date
ISA
4a42c428f0 style: Alarm UI Widget 2025-09-17 13:26:02 +02:00
ISA
1d3d04d49c style: Alarm Ui Widget 2025-09-17 12:59:37 +02:00
ISA
dd9980409c test: Test pass 2025-09-17 12:13:03 +02:00
ISA
ea6d71a4f5 feat: Alarm UI Widget 2025-09-17 09:16:04 +02:00
ISA
13ca1cece0 feat: Die Alarmanzeige ist jetzt als eigene Komponente (AlarmIndicator.js) im Verzeichnis uiWidgets erstellt und in MapComponent.js eingebunden.
Wenn ein Alarm mit AlarmLink vorhanden ist, wird das Alarm-Icon angezeigt und öffnet beim Klick den Link in einem neuen Tab.
2025-09-17 07:44:49 +02:00
ISA
f22bb4b232 chore: UI Widget Alarm Link in GisStationsStatusDistrict.json eingefügt 2025-09-17 07:33:21 +02:00
11 changed files with 209 additions and 25 deletions

View File

@@ -23,4 +23,4 @@ NEXT_PUBLIC_USE_MOCKS=true
# z.B. http://10.10.0.13/xyz/index.aspx -> basePath in config.json auf /xyz setzen # z.B. http://10.10.0.13/xyz/index.aspx -> basePath in config.json auf /xyz setzen
# basePath wird jetzt in public/config.json gepflegt # basePath wird jetzt in public/config.json gepflegt
# App-Versionsnummer # App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.1.382 NEXT_PUBLIC_APP_VERSION=1.1.388

View File

@@ -24,4 +24,4 @@ NEXT_PUBLIC_USE_MOCKS=false
# basePath wird jetzt in public/config.json gepflegt # basePath wird jetzt in public/config.json gepflegt
# App-Versionsnummer # App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.1.382 NEXT_PUBLIC_APP_VERSION=1.1.388

View File

@@ -34,6 +34,7 @@ import { useMapComponentState } from "@/components/hooks/useMapComponentState.js
import CoordinatePopup from "@/components/contextmenu/CoordinatePopup.js"; import CoordinatePopup from "@/components/contextmenu/CoordinatePopup.js";
//----------Ui Widgets---------------- //----------Ui Widgets----------------
import MapLayersControlPanel from "@/components/uiWidgets/mapLayersControlPanel/MapLayersControlPanel.js"; import MapLayersControlPanel from "@/components/uiWidgets/mapLayersControlPanel/MapLayersControlPanel.js";
import AlarmIndicator from "@/components/uiWidgets/AlarmIndicator";
import CoordinateInput from "@/components/uiWidgets/CoordinateInput.js"; import CoordinateInput from "@/components/uiWidgets/CoordinateInput.js";
import VersionInfoModal from "@/components/uiWidgets/VersionInfoModal.js"; import VersionInfoModal from "@/components/uiWidgets/VersionInfoModal.js";
import AreaDropdown from "@/components/uiWidgets/AreaDropdown"; import AreaDropdown from "@/components/uiWidgets/AreaDropdown";
@@ -132,7 +133,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
const poiLayerVisible = useSelector(state => state.poiLayerVisible.visible); const poiLayerVisible = useSelector(state => state.poiLayerVisible.visible);
const zoomTrigger = useSelector(state => state.zoomTrigger.trigger); const zoomTrigger = useSelector(state => state.zoomTrigger.trigger);
const poiReadTrigger = useSelector(state => state.poiReadFromDbTrigger.trigger); const poiReadTrigger = useSelector(state => state.poiReadFromDbTrigger.trigger);
const GisStationsStaticDistrict = useSelector(selectGisStationsStaticDistrict); // entfernt, da weiter unten dynamisch und mit Fallback deklariert
const gisSystemStaticStatus = useSelector(state => state.gisSystemStatic.status); const gisSystemStaticStatus = useSelector(state => state.gisSystemStatic.status);
const polylineEventsDisabled = useSelector(state => state.polylineEventsDisabled.disabled); const polylineEventsDisabled = useSelector(state => state.polylineEventsDisabled.disabled);
const mapLayersVisibility = useSelector(selectMapLayersState) || {}; const mapLayersVisibility = useSelector(selectMapLayersState) || {};
@@ -143,12 +144,37 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
selectGisLinesStatusFromWebservice selectGisLinesStatusFromWebservice
); );
// Alarm Status aus GisStationsStatusDistrict // Alarm Status und Link dynamisch aus GisStationsStaticDistrict
const gisStationsStatusDistrict = useSelector(state => state.gisStationsStatusDistrict.data); const gisStationsStatusDistrict = useSelector(state => state.gisStationsStatusDistrict.data);
// Unterstützt sowohl Array-Shape (Statis[]) als auch Objekt mit Statis-Array const GisStationsStaticDistrict = useSelector(selectGisStationsStaticDistrict) || {};
const hasActiveAlarm = Array.isArray(gisStationsStatusDistrict) const pointsArr = GisStationsStaticDistrict.Points || [];
? gisStationsStatusDistrict.some(item => item?.Alarm === 1) let hasActiveAlarm = false;
: gisStationsStatusDistrict?.Statis?.some(item => item?.Alarm === 1) || false; let alarmLink = "";
let alarmText = "";
let alarmIdLD = null;
if (Array.isArray(gisStationsStatusDistrict)) {
const alarmObj = gisStationsStatusDistrict.find(item => item?.Alarm === 1);
hasActiveAlarm = !!alarmObj;
alarmIdLD = alarmObj?.IdLD;
alarmText = alarmObj?.Me || "Alarm aktiv";
} else if (gisStationsStatusDistrict?.Statis) {
const alarmObj = gisStationsStatusDistrict.Statis.find(item => item?.Alarm === 1);
hasActiveAlarm = !!alarmObj;
alarmIdLD = alarmObj?.IdLD;
alarmText = alarmObj?.Me || "Alarm aktiv";
}
if (hasActiveAlarm && alarmIdLD) {
const staticObj = pointsArr.find(p => p.IdLD === alarmIdLD);
if (staticObj && staticObj.Link) {
// Link kann relativ sein, ggf. mit Host ergänzen
const isAbsolute =
staticObj.Link.startsWith("http://") || staticObj.Link.startsWith("https://");
alarmLink = isAbsolute
? staticObj.Link
: // : `${window.location.origin}/talas5/devices/${staticObj.Link}`;
`http://10.10.0.13/talas5/devices/${staticObj.Link}`; // nur zum Testen
}
}
const poiIconsData = useSelector(selectPoiIconsData); const poiIconsData = useSelector(selectPoiIconsData);
const poiIconsStatus = useSelector(selectPoiIconsStatus); const poiIconsStatus = useSelector(selectPoiIconsStatus);
const poiTypData = useSelector(selectPoiTypData); const poiTypData = useSelector(selectPoiTypData);
@@ -1185,17 +1211,8 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
<div id="map" ref={mapRef} className="z-0" style={{ height: "100vh", width: "100vw" }}></div> <div id="map" ref={mapRef} className="z-0" style={{ height: "100vh", width: "100vw" }}></div>
{/* Top-right controls: layers, info, expand, edit, and base map stack */} {/* Top-right controls: layers, info, expand, edit, and base map stack */}
<div className="absolute top-3 right-3 z-50 pointer-events-auto flex items-center gap-2"> <div className="absolute top-3 right-3 z-50 pointer-events-auto flex items-center gap-2">
{/* Alarm-Icon - nur anzeigen wenn Alarm aktiv */} {/* Alarm-Icon - nur anzeigen wenn Alarm aktiv und Link vorhanden */}
{hasActiveAlarm && ( <AlarmIndicator hasAlarm={hasActiveAlarm} alarmLink={alarmLink} alarmText={alarmText} />
<button
onClick={() => {}}
aria-label="Alarm aktiv"
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title="Alarm aktiv"
>
<AlarmIcon className="h-8 w-8 animate-pulse text-red-500" />
</button>
)}
{/* Marker-Icon (line-md) */} {/* Marker-Icon (line-md) */}
<button <button
onClick={() => setOverlay(prev => (prev === "area" ? null : "area"))} onClick={() => setOverlay(prev => (prev === "area" ? null : "area"))}

View File

@@ -0,0 +1,53 @@
import React from "react";
import AlarmIcon from "@/components/icons/material-symbols/AlarmIcon";
import Tooltip from "@mui/material/Tooltip";
import styles from "./AlarmIndicator.module.css";
/**
* AlarmIndicator zeigt ein Alarm-Icon, das bei Klick den AlarmLink in neuem Tab öffnet.
* @param {boolean} hasAlarm - Ob ein Alarm aktiv ist
* @param {string} alarmLink - Link zur Alarm-Detailseite
* @param {string} [alarmText] - Optionaler Tooltip-Text
* @param {string} [animation] - "shake" | "rotate" | "blink" | "pulse" (default: "shake")
* @param {number} [pulseDuration] - Animationsdauer in Sekunden (default: 0.5)
*/
const AlarmIndicator = ({
hasAlarm,
alarmLink,
alarmText,
animation = "pulse",
pulseDuration = 0.5, // default: 1
}) => {
if (!hasAlarm || !alarmLink) return null;
// Animation-Klasse wählen
let animationClass = styles.fastPulse;
let style = { animationDuration: `${pulseDuration}s` };
if (animation === "shake") {
animationClass = styles.shakeAlarm;
} else if (animation === "rotate") {
animationClass = styles.rotateAlarm;
} else if (animation === "blink") {
animationClass = styles.blinkAlarm;
} else if (animation === "pulse") {
animationClass = styles.fastPulse;
}
return (
<Tooltip title={alarmText || "Alarm aktiv"}>
<span
style={{ cursor: "pointer", color: "red" }}
onClick={e => {
e.stopPropagation();
window.open(alarmLink, "_blank");
}}
aria-label="Alarm aktiv"
>
<AlarmIcon
className={`h-14 w-14 mr-6 ${animationClass} text-red-800 bg-red-300`}
style={style}
/>
</span>
</Tooltip>
);
};
export default AlarmIndicator;

View File

@@ -0,0 +1,62 @@
.fastPulse {
animation: fast-pulse 0.5s infinite;
}
@keyframes fast-pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.15);
}
}
.shakeAlarm {
animation: shake-alarm 0.5s infinite cubic-bezier(0.36, 0.07, 0.19, 0.97);
}
@keyframes shake-alarm {
10%,
90% {
transform: translateX(-1px);
}
20%,
80% {
transform: translateX(2px);
}
30%,
50%,
70% {
transform: translateX(-4px);
}
40%,
60% {
transform: translateX(4px);
}
}
.rotateAlarm {
animation: rotate-alarm 1s linear infinite;
}
@keyframes rotate-alarm {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.blinkAlarm {
animation: blink-alarm 0.7s steps(2, start) infinite;
}
@keyframes blink-alarm {
to {
visibility: hidden;
}
}

View File

@@ -0,0 +1,15 @@
@keyframes fast-pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.15);
}
}
.fast-pulse {
animation: fast-pulse 0.5s infinite;
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "nodemap", "name": "nodemap",
"version": "1.1.382", "version": "1.1.388",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "nodemap", "name": "nodemap",
"version": "1.1.382", "version": "1.1.388",
"dependencies": { "dependencies": {
"@emotion/react": "^11.13.3", "@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "nodemap", "name": "nodemap",
"version": "1.1.382", "version": "1.1.388",
"dependencies": { "dependencies": {
"@emotion/react": "^11.13.3", "@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",

View File

@@ -29,6 +29,14 @@ async function selectStation(page, value) {
} }
test("MapComponent", async ({ page }) => { test("MapComponent", async ({ page }) => {
// Login auf 13.er TALAS
await page.goto("http://10.10.0.13/talas5/login.aspx");
await page.locator("#m_textboxUserName_I").click();
await page.locator("#m_textboxUserName_I").fill("admin");
await page.locator("#m_textboxUserName_I").press("Tab");
await page.locator("#m_textboxPassword_I").fill("admin");
await page.getByRole("cell", { name: "Anmelden Anmelden" }).locator("span").click();
// Set initial localStorage BEFORE navigation so the app reads them on load // Set initial localStorage BEFORE navigation so the app reads them on load
await page.addInitScript(() => { await page.addInitScript(() => {
localStorage.setItem("editMode", "false"); localStorage.setItem("editMode", "false");
@@ -98,7 +106,17 @@ test("MapComponent", async ({ page }) => {
// 7) Marker setzen und Stationen wählen // 7) Marker setzen und Stationen wählen
await page.getByLabel("Marker").click(); await page.getByLabel("Marker").click();
await expect(page.getByText("Station wählenBitte wählen…")).toBeVisible(); // ...existing code...
// ...existing code...
await expect(page.getByText("Station wählen")).toBeVisible();
const select = page.locator("select");
await expect(select).toBeVisible();
await expect(select).toBeEnabled();
// Prüfe, ob die gewünschten Optionen existieren (attached)
await expect(select.locator('option[value="50977"]')).toBeAttached();
await expect(select.locator('option[value="50986"]')).toBeAttached();
// ----------------------------------------------
await selectStation(page, "50977"); await selectStation(page, "50977");
await page.getByLabel("Marker").click(); await page.getByLabel("Marker").click();
await selectStation(page, "50986"); await selectStation(page, "50986");
@@ -112,6 +130,25 @@ test("MapComponent", async ({ page }) => {
//wait 3 seconds //wait 3 seconds
// plusIcon // plusIcon
await page.getByTestId("zoom-in").click(); //plus await page.getByTestId("zoom-in").click(); //plus
//--------------------------------------------
// Prüfe Alarm-Icon
await page.goto("http://10.10.0.13/talas5/login.aspx");
await page.locator("#m_textboxUserName_I").click();
await page.locator("#m_textboxUserName_I").fill("admin");
await page.locator("#m_textboxUserName_I").press("Tab");
await page.locator("#m_textboxPassword_I").fill("admin");
await page.getByRole("cell", { name: "Anmelden Anmelden" }).locator("span").click();
console.log("Login auf 13.er TALAS erfolgreich");
//warte 10 Sekunden
await page.waitForTimeout(10000);
await page.goto("http://localhost:3000/?m=12&u=484");
const page1Promise = page.waitForEvent("popup");
await page.getByLabel("Alarm aktiv").locator("path").click();
const page1 = await page1Promise;
await expect(
page1.getByText("Standort Rastede > Bereich Littwin > TALAS CPL V3.5", { exact: true })
).toBeVisible();
}); });
/* Powershell Befehl ->das führt langsam aus mit 1 Sekunde Pause zwischen den Aktionen /* Powershell Befehl ->das führt langsam aus mit 1 Sekunde Pause zwischen den Aktionen

View File

@@ -18,7 +18,7 @@
}, },
{ {
"LD_Name": "GMA-isa-test", "LD_Name": "GMA-isa-test",
"IdLD": 50981, "IdLD": 50922,
"Device": "Glättemeldeanlage", "Device": "Glättemeldeanlage",
"Link": "gma.aspx?ver=1&id=50981", "Link": "gma.aspx?ver=1&id=50981",
"Location_Name": "Littwin", "Location_Name": "Littwin",

View File

@@ -317,7 +317,7 @@
"Me": "KÜG 06: Aderbruch kommend", "Me": "KÜG 06: Aderbruch kommend",
"Feld": 4, "Feld": 4,
"Icon": 0, "Icon": 0,
"Alarm": 0 "Alarm": 1
}, },
{ {
"IdLD": 50977, "IdLD": 50977,