refactor: Komponenten-Hooks strukturiert und in passende UI-Unterverzeichnisse verschoben

- useLineData.js → components/gisPolylines/tooltip/
- useLayerVisibility.js → components/mapLayersControlPanel/hooks/
- useAreaMarkersLayer.js → components/area/hooks/
- useDynamicDeviceLayers.js → components/devices/hooks/
- useDataUpdater.js & useMapComponentState.js → components/hooks/

💡 Ziel: Alle UI-bezogenen Hooks an logische Stellen verschoben, um Wartbarkeit zu verbessern.
🔍 Vorteil: Schnellere Navigation bei UI-Fehlern oder Layout-Anpassungen.
This commit is contained in:
ISA
2025-06-25 07:21:05 +02:00
parent 4070429193
commit 7c4fbc3988
11 changed files with 55 additions and 31 deletions

View File

@@ -0,0 +1,131 @@
// /hooks/layers/useAreaMarkersLayer.js
import { useEffect, useState, useRef } from "react";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import { useDispatch } from "react-redux";
import { updateAreaThunk } from "@/redux/thunks/database/area/updateAreaThunk";
const customIcon = new L.Icon({
iconUrl: "/img/bereich.png",
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowUrl: "/img/marker-shadow.png",
shadowSize: [41, 41],
});
const useAreaMarkersLayer = (map, oms, apiUrl, onUpdateSuccess) => {
const dispatch = useDispatch();
const [areaMarkers, setAreaMarkers] = useState([]);
const prevVisibility = useRef(null);
const updateMarkersVisibility = () => {
if (!map || areaMarkers.length === 0) return;
const mapLayersVisibility = JSON.parse(localStorage.getItem("mapLayersVisibility")) || {};
const areAllLayersInvisible = Object.values(mapLayersVisibility).every(v => !v);
if (areAllLayersInvisible === prevVisibility.current) return;
prevVisibility.current = areAllLayersInvisible;
areaMarkers.forEach(marker => {
if (areAllLayersInvisible) {
marker.addTo(map);
if (oms) oms.addMarker(marker);
} else {
map.removeLayer(marker);
}
});
};
useEffect(() => {
updateMarkersVisibility();
const handleStorageChange = event => {
if (event.key === "mapLayersVisibility") {
updateMarkersVisibility();
}
};
window.addEventListener("storage", handleStorageChange);
const intervalId = setInterval(updateMarkersVisibility, 5000);
return () => {
window.removeEventListener("storage", handleStorageChange);
clearInterval(intervalId);
};
}, [map, areaMarkers, oms]);
useEffect(() => {
const fetchArea = async () => {
try {
const response = await fetch(apiUrl);
if (!response.ok) throw new Error(`API-Fehler: ${response.status} ${response.statusText}`);
const data = await response.json();
const markers = data.map(item => {
const marker = L.marker([item.x, item.y], {
icon: customIcon,
draggable: true,
customType: "areaMarker",
});
marker.bindTooltip(
`<strong>Bereich:</strong> ${item.location_name} <br />
<strong>Standort:</strong> ${item.area_name}`,
{
permanent: false,
direction: "top",
offset: [0, -20],
}
);
marker.on("dragend", async e => {
const { lat, lng } = e.target.getLatLng();
try {
await dispatch(
updateAreaThunk({
idLocation: item.idLocation,
idMap: item.idMaps,
newCoords: { x: lat, y: lng },
})
).unwrap();
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
console.log("✔️ Koordinaten erfolgreich aktualisiert:", { lat, lng });
}
onUpdateSuccess?.(); // optionaler Callback
} catch (error) {
console.error("❌ Fehler beim Aktualisieren der Koordinaten:", error);
}
});
return marker;
});
setAreaMarkers(markers);
} catch (error) {
console.error("Fehler beim Laden der Bereiche:", error.message);
setAreaMarkers([]);
}
};
fetchArea();
}, [apiUrl, dispatch]);
useEffect(() => {
if (map) {
areaMarkers.forEach(marker => marker.addTo(map));
}
return () => {
if (map) {
areaMarkers.forEach(marker => map.removeLayer(marker));
}
};
}, [map, areaMarkers]);
return areaMarkers;
};
export default useAreaMarkersLayer;

View File

@@ -0,0 +1,92 @@
// /hooks/layers/useDynamicDeviceLayers.js
import { useEffect, useRef, useState } from "react";
import L from "leaflet";
import { createAndSetDevices } from "@/utils/devices/createAndSetDevices";
import { checkOverlappingMarkers } from "@/utils/mapUtils";
import plusRoundIcon from "@/components/icons/devices/overlapping/PlusRoundIcon";
import { useSelector } from "react-redux";
import { selectGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice.js";
import { selectGisStationsMeasurements } from "@/redux/slices/webservice/gisStationsMeasurementsSlice.js";
import { selectGisStationsStatusDistrict } from "@/redux/slices/webservice/gisStationsStatusDistrictSlice.js";
/**
* Dynamisch GIS-System-Marker erstellen & Sichtbarkeit steuern.
* @param {object} map - Leaflet Map-Instanz
* @param {Array} GisSystemStatic - Liste der Systeme aus Webservice
* @param {object} mapLayersVisibility - Redux-Objekt mit Layer-Sichtbarkeiten
* @param {object} priorityConfig - Konfig für Prioritäten
* @returns {{ markerStates, layerRefs }} Alle Marker und Referenzen
*/
const useDynamicDeviceLayers = (map, GisSystemStatic, mapLayersVisibility, priorityConfig, oms) => {
const measurements = useSelector(selectGisStationsMeasurements);
const staticDistrictData = useSelector(selectGisStationsStaticDistrict);
const statusDistrict = useSelector(selectGisStationsStatusDistrict);
const [markerStates, setMarkerStates] = useState({});
const layerRefs = useRef({});
// Marker initialisieren (nicht sichtbar machen)
useEffect(() => {
if (!map || GisSystemStatic.length === 0) return;
GisSystemStatic.forEach(({ Name, IdSystem }) => {
const key = `system-${IdSystem}`; // Einheitlicher Key
if (!layerRefs.current[key]) {
layerRefs.current[key] = new L.LayerGroup().addTo(map);
}
createAndSetDevices(
IdSystem,
newMarkers => {
const oldMarkers = markerStates[key];
// Entferne alte Marker aus Karte und OMS
if (oldMarkers && Array.isArray(oldMarkers)) {
oldMarkers.forEach(marker => {
if (map.hasLayer(marker)) {
map.removeLayer(marker);
}
if (oms) {
oms.removeMarker(marker);
}
});
}
// Neue Marker setzen
setMarkerStates(prev => ({ ...prev, [key]: newMarkers }));
},
GisSystemStatic,
priorityConfig,
undefined,
oms
);
});
}, [map, GisSystemStatic, priorityConfig, staticDistrictData, measurements, statusDistrict]);
// Sichtbarkeit nach Redux-Status steuern
useEffect(() => {
if (!map) return;
const editMode = localStorage.getItem("editMode") === "true";
Object.entries(markerStates).forEach(([key, markers]) => {
const isVisible = mapLayersVisibility[key];
markers.forEach(marker => {
const hasLayer = map.hasLayer(marker);
if (editMode || !isVisible) {
if (hasLayer) map.removeLayer(marker);
} else {
if (!hasLayer) marker.addTo(map);
}
});
});
const allMarkers = Object.values(markerStates).filter(Array.isArray).flat();
checkOverlappingMarkers(map, allMarkers, plusRoundIcon);
}, [map, markerStates, mapLayersVisibility]);
return { markerStates, layerRefs };
};
export default useDynamicDeviceLayers;

View File

@@ -0,0 +1,118 @@
// hooks/useLineData.js //fix v1.0.8.1
import { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { selectGisLinesStatusFromWebservice } from "@/redux/slices/webservice/gisLinesStatusSlice";
import { fetchGisLinesThunk } from "@/redux/thunks/database/polylines/fetchGisLinesThunk";
import { fetchGisLinesStatusThunk } from "@/../redux/thunks/webservice/fetchGisLinesStatusThunk";
const useLineData = () => {
const dispatch = useDispatch();
const { data: statisData } = useSelector(selectGisLinesStatusFromWebservice);
const linesData = useSelector(state => state.gisLinesFromDatabase.data);
const [lineColors, setLineColors] = useState({});
const [tooltipContents, setTooltipContents] = useState({});
useEffect(() => {
dispatch(fetchGisLinesThunk());
dispatch(fetchGisLinesStatusThunk());
}, [dispatch]);
useEffect(() => {
if (!statisData || !Array.isArray(statisData)) return;
const colorsByModule = {};
const newTooltipContents = {};
const valueMap = {};
const sortedStatis = [...statisData].sort((a, b) => a.Level - b.Level);
sortedStatis.forEach(statis => {
const key = `${statis.IdLD}-${statis.Modul}`;
if (!valueMap[key]) {
valueMap[key] = {
messages: [],
messwert: undefined,
schleifenwert: undefined,
};
}
if (
statis.DpName.endsWith("_Messwert") &&
statis.Value !== "True" &&
!valueMap[key].messwert
) {
valueMap[key].messwert = statis.Value;
}
if (statis.DpName.endsWith("_Schleifenwert") && !valueMap[key].schleifenwert) {
valueMap[key].schleifenwert = statis.Value;
}
if (statis.Message && statis.Message !== "?") {
valueMap[key].messages.push({
message: statis.Message,
prioColor:
statis.PrioColor && statis.PrioColor !== "#ffffff" ? statis.PrioColor : "green",
});
}
});
sortedStatis.forEach(statis => {
const key = `${statis.IdLD}-${statis.Modul}`;
const matchingLine = linesData.find(
item => item.idLD === statis.IdLD && item.idModul === statis.Modul
);
if (matchingLine) {
const values = valueMap[key];
const messageDisplay = values.messages
.map(
msg =>
`<span class="inline-block text-gray-800"><span class="inline-block w-2 h-2 rounded-full mr-2" style="background-color: ${msg.prioColor};"></span>${msg.message}</span><br>`
)
.join("");
colorsByModule[key] = values.messages.length > 0 ? values.messages[0].prioColor : "green";
newTooltipContents[key] = `
<div class="bg-white rounded-lg m-0 p-2 w-[210px]">
<span class="text-lg font-semibold text-gray-900">${
statis.ModulName || "Unknown"
}</span>
<br>
<span class="text-md font-bold text-gray-800">${statis.ModulTyp || "N/A"}</span>
<br>
<span class="text-md font-bold text-gray-800">Slot: ${statis.Modul || "N/A"}</span>
<br>
<span class="text-md font-bold text-gray-800">Station: ${
matchingLine.name || "N/A"
}</span>
<br>
<div style="max-width: 100%; overflow-wrap: break-word; word-break: break-word; white-space: normal;">
${messageDisplay}
</div>
<br>
${
values.messwert
? `<span class="inline-block text-gray-800">Messwert: ${values.messwert}</span><br>`
: ""
}
${
values.schleifenwert
? `<span class="inline-block text-gray-800">Schleifenwert: ${values.schleifenwert}</span>`
: ""
}
</div>
`;
}
});
setLineColors(colorsByModule);
setTooltipContents(newTooltipContents);
}, [statisData, linesData]);
return { lineColors, tooltipContents };
};
export default useLineData;

View File

@@ -0,0 +1,40 @@
// hooks/useDataUpdater.js
import { useEffect } from "react";
import { useDispatch } from "react-redux";
// import type { AppDispatch } from "../redux/store";
// ✅ Thunks aus korrektem Pfad importieren
import { fetchGisLinesStatusThunk } from "@/redux/thunks/webservice/fetchGisLinesStatusThunk";
import { fetchGisStationsMeasurementsThunk } from "@/redux/thunks/webservice/fetchGisStationsMeasurementsThunk";
import { fetchGisStationsStaticDistrictThunk } from "@/redux/thunks/webservice/fetchGisStationsStaticDistrictThunk";
import { fetchGisStationsStatusDistrictThunk } from "@/redux/thunks/webservice/fetchGisStationsStatusDistrictThunk";
import { fetchGisSystemStaticThunk } from "@/redux/thunks/webservice/fetchGisSystemStaticThunk";
const REFRESH_INTERVAL = parseInt(process.env.NEXT_PUBLIC_REFRESH_INTERVAL || "10000");
export const useDataUpdater = () => {
const dispatch = useDispatch();
useEffect(() => {
const updateAll = () => {
dispatch(fetchGisLinesStatusThunk());
dispatch(fetchGisStationsMeasurementsThunk());
dispatch(fetchGisStationsStaticDistrictThunk());
dispatch(fetchGisStationsStatusDistrictThunk());
dispatch(fetchGisSystemStaticThunk());
};
updateAll();
const interval = setInterval(updateAll, 10000);
return () => clearInterval(interval);
}, [dispatch]);
};
// Das ist eine normale Funktion
export const triggerDataUpdate = dispatch => {
dispatch(fetchGisLinesStatusThunk());
dispatch(fetchGisStationsMeasurementsThunk());
dispatch(fetchGisStationsStaticDistrictThunk());
dispatch(fetchGisStationsStatusDistrictThunk());
dispatch(fetchGisSystemStaticThunk());
};

View File

@@ -0,0 +1,65 @@
// /hooks/useMapComponentState.js
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
// Redux: POI-Typen
import { fetchPoiTypThunk } from "@/redux/thunks/database/pois/fetchPoiTypThunk";
import { selectPoiTypData, selectPoiTypStatus } from "@/redux/slices/database/pois/poiTypSlice";
// Redux: GIS Geräte
import { fetchGisStationsStaticDistrictThunk } from "@/redux/thunks/webservice/fetchGisStationsStaticDistrictThunk";
import { selectGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice";
// Redux: priorityConfig
import { fetchPriorityConfigThunk } from "@/redux/thunks/database/fetchPriorityConfigThunk";
import { selectPriorityConfig } from "@/redux/slices/database/priorityConfigSlice";
export const useMapComponentState = () => {
const dispatch = useDispatch();
// Redux: Sichtbarkeit des POI-Layers
const poiLayerVisible = useSelector(state => state.poiLayerVisible.visible);
// Redux: POI-Typen
const poiTypData = useSelector(selectPoiTypData);
const poiTypStatus = useSelector(selectPoiTypStatus);
// Redux: Geräte
const locationDeviceData = useSelector(selectGisStationsStaticDistrict);
const deviceName = locationDeviceData?.Points?.[0]?.LD_Name || "";
// Redux: Prioritätskonfiguration
const priorityConfig = useSelector(selectPriorityConfig);
// UI-interner Zustand
const [menuItemAdded, setMenuItemAdded] = useState(false);
// POI-Typen laden
useEffect(() => {
if (poiTypStatus === "idle") {
dispatch(fetchPoiTypThunk());
}
}, [dispatch, poiTypStatus]);
// GIS Geräte laden
useEffect(() => {
dispatch(fetchGisStationsStaticDistrictThunk());
}, [dispatch]);
// PriorityConfig laden
useEffect(() => {
dispatch(fetchPriorityConfigThunk());
}, [dispatch]);
return {
poiTypData,
isPoiTypLoaded: poiTypStatus === "succeeded",
deviceName,
locationDeviceData,
priorityConfig,
menuItemAdded,
setMenuItemAdded,
poiLayerVisible,
};
};

View File

@@ -13,11 +13,11 @@ import plusRoundIcon from "../icons/devices/overlapping/PlusRoundIcon.js";
import { restoreMapSettings, checkOverlappingMarkers } from "../../utils/mapUtils.js";
import addItemsToMapContextMenu from "@/components/contextmenu/useMapContextMenu.js";
import useAreaMarkersLayer from "@/hooks/useAreaMarkersLayer.js";
import useAreaMarkersLayer from "@/components/area/hooks/useAreaMarkersLayer.js";
import { setupPolylines } from "@/utils/polylines/setupPolylines.js";
import { setupPOIs } from "@/utils/setupPOIs.js";
import useLineData from "@/hooks/useLineData.js";
import { useMapComponentState } from "@/hooks/useMapComponentState.js";
import useLineData from "@/components/gisPolylines/tooltip/useLineData.js";
import { useMapComponentState } from "@/components/hooks/useMapComponentState.js";
import CoordinatePopup from "@/components/contextmenu/CoordinatePopup.js";
//----------Ui Widgets----------------
import MapLayersControlPanel from "@/components/uiWidgets/mapLayersControlPanel/MapLayersControlPanel.js";
@@ -74,9 +74,9 @@ import { fetchPoiIconsDataThunk } from "@/redux/thunks/database/pois/fetchPoiIco
import { fetchPoiTypThunk } from "@/redux/thunks/database/pois/fetchPoiTypThunk.js";
import { updateAreaThunk } from "@/redux/thunks/database/area/updateAreaThunk";
import useDynamicDeviceLayers from "@/hooks/useDynamicDeviceLayers.js";
import useDynamicDeviceLayers from "@/components/devices/hooks/useDynamicDeviceLayers.js";
import useDataUpdater from "@/hooks/useDataUpdater";
import useDataUpdater from "@/components/hooks/useDataUpdater.js";
import { cleanupPolylinesForMemory } from "@/utils/polylines/cleanupPolylinesForMemory";
import { cleanupMarkers } from "@/utils/common/cleanupMarkers";
import { monitorHeapAndReload } from "@/utils/common/monitorMemory";

View File

@@ -0,0 +1,37 @@
// hooks/useLayerVisibility.js
import { useEffect } from "react";
import { addContextMenuToMarker } from "../utils/addContextMenuToMarker";
const useLayerVisibility = (map, markers, mapLayersVisibility, layerKey, oms) => {
useEffect(() => {
if (!map || !markers || !oms) return;
// Leerzeichen, Bindestriche entfernen & Kleinbuchstaben erzwingen
const normalizedLayerKey = layerKey.replace(/\s|-/g, "").toLowerCase();
// Alle mapLayersVisibility-Keys auch normalisieren
const visibilityKeys = Object.keys(mapLayersVisibility).reduce((acc, key) => {
acc[key.replace(/\s|-/g, "").toLowerCase()] = mapLayersVisibility[key];
return acc;
}, {});
const isVisible = visibilityKeys[normalizedLayerKey] ?? false;
const toggleLayer = (isVisible) => {
markers.forEach((marker) => {
if (isVisible) {
marker.addTo(map);
oms.addMarker(marker);
addContextMenuToMarker(marker);
} else {
map.removeLayer(marker);
oms.removeMarker(marker);
}
});
};
toggleLayer(isVisible);
}, [map, markers, mapLayersVisibility, layerKey, oms]);
};
export default useLayerVisibility;