Fix Marker Cluster & MissionContent
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user