Fix Marker Cluster & MissionContent

This commit is contained in:
nocnico
2025-05-22 16:31:24 +02:00
parent 05e74077a5
commit eb86b8407e
2 changed files with 143 additions and 167 deletions

View File

@@ -1,21 +1,10 @@
import { import { ConnectedAircraft, HpgValidationState, Mission, Station } from "@repo/db";
ConnectedAircraft,
HpgValidationState,
Mission,
Station,
} from "@repo/db";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { SmartPopup, useSmartPopup } from "_components/SmartPopup"; import { SmartPopup, useSmartPopup } from "_components/SmartPopup";
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore"; import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { useMapStore } from "_store/mapStore"; import { useMapStore } from "_store/mapStore";
import { import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_components/map/AircraftMarker";
FMS_STATUS_COLORS, import { MISSION_STATUS_COLORS, MISSION_STATUS_TEXT_COLORS } from "_components/map/MissionMarkers";
FMS_STATUS_TEXT_COLORS,
} from "_components/map/AircraftMarker";
import {
MISSION_STATUS_COLORS,
MISSION_STATUS_TEXT_COLORS,
} from "_components/map/MissionMarkers";
import { cn } from "helpers/cn"; import { cn } from "helpers/cn";
import { checkSimulatorConnected } from "helpers/simulatorConnected"; import { checkSimulatorConnected } from "helpers/simulatorConnected";
import { getConnectedAircraftsAPI } from "querys/aircrafts"; import { getConnectedAircraftsAPI } from "querys/aircrafts";
@@ -31,9 +20,7 @@ const PopupContent = ({
missions: Mission[]; missions: Mission[];
}) => { }) => {
const { anchor } = useSmartPopup(); const { anchor } = useSmartPopup();
const { setOpenAircraftMarker, setOpenMissionMarker } = useMapStore( const { setOpenAircraftMarker, setOpenMissionMarker } = useMapStore((state) => state);
(state) => state,
);
const map = useMap(); const map = useMap();
let borderColor = ""; let borderColor = "";
@@ -46,9 +33,7 @@ const PopupContent = ({
} }
} else if (anchor.includes("bottom")) { } else if (anchor.includes("bottom")) {
if (aircrafts.length > 0) { if (aircrafts.length > 0) {
borderColor = borderColor = FMS_STATUS_TEXT_COLORS[aircrafts[aircrafts.length - 1]!.fmsStatus] || "white";
FMS_STATUS_TEXT_COLORS[aircrafts[aircrafts.length - 1]!.fmsStatus] ||
"white";
} else if (missions.length > 0) { } else if (missions.length > 0) {
borderColor = MISSION_STATUS_TEXT_COLORS[missions[0]!.state]; borderColor = MISSION_STATUS_TEXT_COLORS[missions[0]!.state];
} }
@@ -64,16 +49,10 @@ const PopupContent = ({
anchor.includes("top") ? "-top-[2px]" : "-bottom-[2px]", anchor.includes("top") ? "-top-[2px]" : "-bottom-[2px]",
)} )}
style={{ style={{
borderLeft: anchor.includes("left") borderLeft: anchor.includes("left") ? `3px solid ${borderColor}` : "",
? `3px solid ${borderColor}` borderRight: anchor.includes("right") ? `3px solid ${borderColor}` : "",
: "",
borderRight: anchor.includes("right")
? `3px solid ${borderColor}`
: "",
borderTop: anchor.includes("top") ? `3px solid ${borderColor}` : "", borderTop: anchor.includes("top") ? `3px solid ${borderColor}` : "",
borderBottom: anchor.includes("bottom") borderBottom: anchor.includes("bottom") ? `3px solid ${borderColor}` : "",
? `3px solid ${borderColor}`
: "",
}} }}
/> />
{missions.map((mission) => { {missions.map((mission) => {
@@ -89,9 +68,7 @@ const PopupContent = ({
return ( return (
<div <div
key={mission.id} key={mission.id}
className={cn( className={cn("relative inline-flex items-center gap-2 text-nowrap w-full")}
"relative inline-flex items-center gap-2 text-nowrap w-full",
)}
style={{ style={{
backgroundColor: markerColor, backgroundColor: markerColor,
cursor: "pointer", cursor: "pointer",
@@ -166,8 +143,7 @@ export const MarkerCluster = () => {
queryFn: getConnectedAircraftsAPI, queryFn: getConnectedAircraftsAPI,
}); });
const dispatcherConnected = const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
useDispatchConnectionStore((s) => s.status) === "connected";
const { data: missions = [] } = useQuery({ const { data: missions = [] } = useQuery({
queryKey: ["missions"], queryKey: ["missions"],
queryFn: () => queryFn: () =>
@@ -183,20 +159,25 @@ export const MarkerCluster = () => {
return missions; return missions;
}, [missions, dispatcherConnected]); }, [missions, dispatcherConnected]);
const [cluster, setCluster] = useState< // Track zoom level in state
{ const [zoom, setZoom] = useState(() => map.getZoom());
useEffect(() => {
const handleZoom = () => setZoom(map.getZoom());
map.on("zoomend", handleZoom);
return () => {
map.off("zoomend", handleZoom);
};
}, [map]);
const clusters = useMemo(() => {
if (zoom >= 8) return [];
let newCluster: {
aircrafts: (ConnectedAircraft & { Station: Station })[]; aircrafts: (ConnectedAircraft & { Station: Station })[];
missions: Mission[]; missions: Mission[];
lat: number; lat: number;
lng: number; lng: number;
}[] }[] = [];
>([]);
// Compute clusters based on zoom and data using useMemo
const clusters = useMemo(() => {
const zoom = map.getZoom();
if (zoom >= 8) return [];
let newCluster: typeof cluster = [];
aircrafts aircrafts
?.filter((a) => checkSimulatorConnected(a.lastHeartbeat)) ?.filter((a) => checkSimulatorConnected(a.lastHeartbeat))
.forEach((aircraft) => { .forEach((aircraft) => {
@@ -265,10 +246,8 @@ export const MarkerCluster = () => {
const missionPos = c.missions.map((m) => [m.addressLat, m.addressLng]); const missionPos = c.missions.map((m) => [m.addressLat, m.addressLng]);
const allPos = [...aircraftPos, ...missionPos]; const allPos = [...aircraftPos, ...missionPos];
const avgLat = const avgLat = allPos.reduce((sum, pos) => sum + pos[0]!, 0) / allPos.length;
allPos.reduce((sum, pos) => sum + pos[0]!, 0) / allPos.length; const avgLng = allPos.reduce((sum, pos) => sum + pos[1]!, 0) / allPos.length;
const avgLng =
allPos.reduce((sum, pos) => sum + pos[1]!, 0) / allPos.length;
return { return {
...c, ...c,
@@ -278,29 +257,11 @@ export const MarkerCluster = () => {
}); });
return clusterWithAvgPos; return clusterWithAvgPos;
}, [aircrafts, filteredMissions, map]); }, [aircrafts, filteredMissions, zoom]);
// Update clusters on zoom change
useEffect(() => {
const handleZoom = () => {
const zoom = map.getZoom();
if (zoom >= 8) {
setCluster([]);
} else {
setCluster(clusters);
}
};
map.on("zoomend", handleZoom);
// Set initial clusters
handleZoom();
return () => {
map.off("zoomend", handleZoom);
};
}, [map, clusters]);
return ( return (
<> <>
{cluster.map((c, i) => ( {clusters.map((c, i) => (
<SmartPopup <SmartPopup
options={{ options={{
ignoreMarker: true, ignoreMarker: true,

View File

@@ -187,31 +187,36 @@ const Einsatzdetails = ({
</span> </span>
</button> </button>
)} )}
{hpgNeedsAttention && ( {!ignoreHpg &&
<button hpgNeedsAttention &&
className="btn btn-sm btn-info btn-outline flex-3" mission.hpgValidationState !== HpgValidationState.POSITION_AMANDED && (
onClick={() => sendAlertMutation.mutate(mission.id)} <button
disabled className="btn btn-sm btn-info btn-outline flex-3"
> onClick={() => sendAlertMutation.mutate(mission.id)}
<span className="flex items-center gap-2"> disabled
{mission.hpgValidationState === HpgValidationState.PENDING && ( >
<div> <span className="flex items-center gap-2">
<span className="loading loading-spinner loading-md"></span> HPG-Validierung {mission.hpgValidationState === HpgValidationState.PENDING && (
läuft... <div>
</div> <span className="loading loading-spinner loading-md"></span> HPG-Validierung
)} läuft...
{mission.hpgValidationState === HpgValidationState.HPG_BUSY && "HPG-Client busy"} </div>
{mission.hpgValidationState === HpgValidationState.HPG_DISCONNECT && )}
"HPG-Client nicht verbunden"} {mission.hpgValidationState === HpgValidationState.HPG_BUSY &&
{mission.hpgValidationState === HpgValidationState.INVALID && "HPG-Client busy"}
"HPG-Client fehlerhaft"} {mission.hpgValidationState === HpgValidationState.HPG_DISCONNECT &&
{mission.hpgValidationState === HpgValidationState.HPG_INVALID_MISSION && "HPG-Client nicht verbunden"}
"Fehlerhafte HPG-Mission"} {mission.hpgValidationState === HpgValidationState.INVALID &&
</span> "HPG-Client fehlerhaft"}
</button> {mission.hpgValidationState === HpgValidationState.HPG_INVALID_MISSION &&
)} "Fehlerhafte HPG-Mission"}
{mission.hpgValidationState === HpgValidationState.NOT_VALIDATED &&
"HPG-Validierung nicht angestoßen"}
</span>
</button>
)}
{mission.hpgValidationState === HpgValidationState.POSITION_AMANDED && ( {!ignoreHpg && mission.hpgValidationState === HpgValidationState.POSITION_AMANDED && (
<button <button
className="btn btn-sm btn-warning btn-outline flex-3" className="btn btn-sm btn-warning btn-outline flex-3"
onClick={() => sendAlertMutation.mutate(mission.id)} onClick={() => sendAlertMutation.mutate(mission.id)}
@@ -339,25 +344,31 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
}, },
}); });
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
return ( return (
<div className="p-4 text-base-content"> <div className="p-4 text-base-content">
<div className="flex items-center w-full justify-between"> <div className="flex items-center w-full justify-between mb-2">
<h2 className="flex items-center gap-2 text-lg font-bold mb-3"> <h2 className="flex items-center gap-2 text-lg font-bold">
<SmartphoneNfc /> Rettungsmittel <SmartphoneNfc /> Rettungsmittel
</h2> </h2>
<div {mission.state !== "draft" && dispatcherConnected && (
className="tooltip tooltip-primary tooltip-left font-semibold" <div className="space-x-2">
data-tip="Einsatz erneut alarmieren" <div
> className="tooltip tooltip-primary tooltip-left font-semibold"
<button data-tip="Einsatz erneut alarmieren"
className="btn btn-xs btn-primary btn-outline" >
onClick={() => { <button
sendAlertMutation.mutate({ id: mission.id }); className="btn btn-xs btn-primary btn-outline"
}} onClick={() => {
> sendAlertMutation.mutate({ id: mission.id });
<BellRing size={16} /> }}
</button> >
</div> <BellRing size={16} />
</button>
</div>
</div>
)}
</div> </div>
<ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto"> <ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto">
{missionStations?.map((station, index) => { {missionStations?.map((station, index) => {
@@ -392,66 +403,70 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
); );
})} })}
</ul> </ul>
<div className="divider mt-0 mb-0" /> {dispatcherConnected && (
<div className="flex items-center gap-2"> <div>
{/* TODO: make it a small multiselect */} <div className="divider mt-0 mb-0" />
<select <div className="flex items-center gap-2">
className="select select-sm select-primary select-bordered flex-1" {/* TODO: make it a small multiselect */}
onChange={(e) => { <select
const selected = allStations?.find((s) => s.id.toString() === e.target.value); className="select select-sm select-primary select-bordered flex-1"
if (selected) { onChange={(e) => {
setSelectedStation(selected); const selected = allStations?.find((s) => s.id.toString() === e.target.value);
} else { if (selected) {
setSelectedStation(e.target.value as "ambulance" | "police" | "firebrigade"); setSelectedStation(selected);
} } else {
}} setSelectedStation(e.target.value as "ambulance" | "police" | "firebrigade");
value={typeof selectedStation === "string" ? selectedStation : selectedStation?.id} }
> }}
{allStations value={typeof selectedStation === "string" ? selectedStation : selectedStation?.id}
?.filter((s) => !mission.missionStationIds.includes(s.id)) >
?.map((station) => ( {allStations
<option ?.filter((s) => !mission.missionStationIds.includes(s.id))
key={station.id} ?.map((station) => (
value={station.id} <option
onClick={() => { key={station.id}
setSelectedStation(station); value={station.id}
}} onClick={() => {
> setSelectedStation(station);
{station.bosCallsign} }}
</option> >
))} {station.bosCallsign}
<option disabled>Fahrzeuge:</option> </option>
<option value="firebrigade">Feuerwehr</option> ))}
<option value="ambulance">RTW</option> <option disabled>Fahrzeuge:</option>
<option value="police">Polizei</option> <option value="firebrigade">Feuerwehr</option>
</select> <option value="ambulance">RTW</option>
<button <option value="police">Polizei</option>
className="btn btn-sm btn-primary btn-outline" </select>
onClick={async () => { <button
if (typeof selectedStation === "string") { className="btn btn-sm btn-primary btn-outline"
toast.error("Fahrzeuge werden aktuell nicht unterstützt"); onClick={async () => {
} else { if (typeof selectedStation === "string") {
if (!selectedStation?.id) return; toast.error("Fahrzeuge werden aktuell nicht unterstützt");
await updateMissionMutation.mutateAsync({ } else {
id: mission.id, if (!selectedStation?.id) return;
missionEdit: { await updateMissionMutation.mutateAsync({
missionStationIds: { id: mission.id,
push: selectedStation?.id, missionEdit: {
}, missionStationIds: {
}, push: selectedStation?.id,
}); },
await sendAlertMutation.mutate({ },
id: mission.id, });
stationId: selectedStation?.id ?? 0, await sendAlertMutation.mutate({
}); id: mission.id,
} stationId: selectedStation?.id ?? 0,
}} });
> }
<span className="text-base-content flex items-center gap-2"> }}
<BellRing size={16} /> Nachalarmieren >
</span> <span className="text-base-content flex items-center gap-2">
</button> <BellRing size={16} /> Nachalarmieren
</div> </span>
</button>
</div>
</div>
)}
</div> </div>
); );
}; };