diff --git a/apps/dispatch/app/(auth)/logout/page.tsx b/apps/dispatch/app/(auth)/logout/page.tsx
index 8090a370..5732aff0 100644
--- a/apps/dispatch/app/(auth)/logout/page.tsx
+++ b/apps/dispatch/app/(auth)/logout/page.tsx
@@ -10,7 +10,7 @@ export default () => {
}, []);
return (
-
logging out...
+ ausloggen...
);
};
diff --git a/apps/dispatch/app/_components/QueryProvider.tsx b/apps/dispatch/app/_components/QueryProvider.tsx
index 2b2af938..d5b2799a 100644
--- a/apps/dispatch/app/_components/QueryProvider.tsx
+++ b/apps/dispatch/app/_components/QueryProvider.tsx
@@ -3,7 +3,7 @@
import { toast } from "react-hot-toast";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { ReactNode, useEffect, useState } from "react";
+import { ReactNode, useEffect, useRef, useState } from "react";
import { dispatchSocket } from "(app)/dispatch/socket";
import { NotificationPayload } from "@repo/db";
import { HPGnotificationToast } from "_components/customToasts/HPGnotification";
@@ -15,6 +15,7 @@ import { MissionAutoCloseToast } from "_components/customToasts/MissionAutoClose
export function QueryProvider({ children }: { children: ReactNode }) {
const mapStore = useMapStore((s) => s);
+ const notificationSound = useRef
(null);
const [queryClient] = useState(
() =>
@@ -22,7 +23,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
defaultOptions: {
mutations: {
onError: (error) => {
- toast.error("An error occurred: " + (error as Error).message, {
+ toast.error("Ein Fehler ist aufgetreten: " + (error as Error).message, {
position: "top-right",
});
},
@@ -30,6 +31,9 @@ export function QueryProvider({ children }: { children: ReactNode }) {
},
}),
);
+ useEffect(() => {
+ notificationSound.current = new Audio("/sounds/notification.mp3");
+ }, []);
useEffect(() => {
const invalidateMission = () => {
queryClient.invalidateQueries({
@@ -59,8 +63,18 @@ export function QueryProvider({ children }: { children: ReactNode }) {
};
const handleNotification = (notification: NotificationPayload) => {
+ const playNotificationSound = () => {
+ if (notificationSound.current) {
+ notificationSound.current.currentTime = 0;
+ notificationSound.current
+ .play()
+ .catch((e) => console.error("Notification sound error:", e));
+ }
+ }
+
switch (notification.type) {
case "hpg-validation":
+ playNotificationSound();
toast.custom(
(t) => ,
{
@@ -70,6 +84,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
break;
case "admin-message":
+ playNotificationSound();
toast.custom((t) => , {
duration: 999999,
});
@@ -81,12 +96,17 @@ export function QueryProvider({ children }: { children: ReactNode }) {
});
break;
case "mission-auto-close":
+ playNotificationSound();
toast.custom(
(t) => ,
{
duration: 60000,
},
);
+ break;
+ case "mission-closed":
+ toast("Dein aktueller Einsatz wurde geschlossen.");
+
break;
default:
toast("unbekanntes Notification-Event");
diff --git a/apps/dispatch/app/_components/customToasts/HPGnotValidated.tsx b/apps/dispatch/app/_components/customToasts/HPGnotValidated.tsx
new file mode 100644
index 00000000..4b6e6e4e
--- /dev/null
+++ b/apps/dispatch/app/_components/customToasts/HPGnotValidated.tsx
@@ -0,0 +1,24 @@
+import { BaseNotification } from "_components/customToasts/BaseNotification"
+import { TriangleAlert } from "lucide-react"
+import toast, { Toast } from "react-hot-toast"
+
+
+export const HPGnotValidatedToast = ({_toast}: {_toast: Toast}) => {
+ return } className="flex flex-row">
+
+
Einsatz nicht HPG-validiert
+
Vergleiche die Position des Einsatzes mit der HPG-Position in Hubschrauber
+
+
+
+
+
+}
+
+export const showToast = () => {
+ toast.custom((t) => {
+ return ();
+ }, {duration: 1000 * 60 * 10}); // 10 minutes
+}
\ No newline at end of file
diff --git a/apps/dispatch/app/_components/map/ContextMenu.tsx b/apps/dispatch/app/_components/map/ContextMenu.tsx
index 962286cc..b5a1d132 100644
--- a/apps/dispatch/app/_components/map/ContextMenu.tsx
+++ b/apps/dispatch/app/_components/map/ContextMenu.tsx
@@ -3,15 +3,22 @@ import { OSMWay } from "@repo/db";
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { useMapStore } from "_store/mapStore";
import { usePannelStore } from "_store/pannelStore";
-import { MapPin, MapPinned, Radius, Ruler, Search, RulerDimensionLine, Scan } from "lucide-react";
+import { MapPin, MapPinned, Search, Car, Ambulance, Siren, Flame } from "lucide-react";
import { getOsmAddress } from "_querys/osm";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { Popup, useMap } from "react-leaflet";
import { findClosestPolygon } from "_helpers/findClosestPolygon";
+import { xPlaneObjectsAvailable } from "_helpers/xPlaneObjectsAvailable";
+import { useQuery } from "@tanstack/react-query";
+import { getConnectedAircraftsAPI } from "_querys/aircrafts";
export const ContextMenu = () => {
const map = useMap();
+ const { data: aircrafts } = useQuery({
+ queryKey: ["connectedAircrafts"],
+ queryFn: getConnectedAircraftsAPI,
+ });
const {
contextMenu,
searchElements,
@@ -26,15 +33,16 @@ export const ContextMenu = () => {
setOpen,
isOpen: isPannelOpen,
} = usePannelStore((state) => state);
- const [showRulerOptions, setShowRulerOptions] = useState(false);
+ const [showObjectOptions, setShowObjectOptions] = useState(false);
const [rulerHover, setRulerHover] = useState(false);
const [rulerOptionsHover, setRulerOptionsHover] = useState(false);
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
useEffect(() => {
- setShowRulerOptions(rulerHover || rulerOptionsHover);
- }, [rulerHover, rulerOptionsHover]);
+ const showObjectOptions = rulerHover || rulerOptionsHover;
+ setShowObjectOptions(showObjectOptions);
+ }, [isPannelOpen, rulerHover, rulerOptionsHover, setOpen]);
useEffect(() => {
const handleContextMenu = (e: any) => {
@@ -150,9 +158,12 @@ export const ContextMenu = () => {
style={{ transform: "translateY(-50%)" }}
onMouseEnter={() => setRulerHover(true)}
onMouseLeave={() => setRulerHover(false)}
- disabled
+ disabled={
+ !isPannelOpen ||
+ !xPlaneObjectsAvailable(missionFormValues?.missionStationIds, aircrafts)
+ }
>
-
+
{/* Bottom Button */}
- {/* Ruler Options - shown when Ruler button is hovered or options are hovered */}
- {showRulerOptions && (
+ {/* XPlane Object Options - shown when Ruler button is hovered or options are hovered */}
+ {showObjectOptions && (
setRulerOptionsHover(true)}
onMouseLeave={() => setRulerOptionsHover(false)}
>
-
+
diff --git a/apps/dispatch/app/_components/map/MapAdditionals.tsx b/apps/dispatch/app/_components/map/MapAdditionals.tsx
index f60a2829..f2c8b7df 100644
--- a/apps/dispatch/app/_components/map/MapAdditionals.tsx
+++ b/apps/dispatch/app/_components/map/MapAdditionals.tsx
@@ -1,6 +1,6 @@
"use client";
import { usePannelStore } from "_store/pannelStore";
-import { Marker } from "react-leaflet";
+import { Marker, useMap } from "react-leaflet";
import L from "leaflet";
import { useQuery } from "@tanstack/react-query";
import { getMissionsAPI } from "_querys/missions";
@@ -8,10 +8,13 @@ import { HPGValidationRequired } from "_helpers/hpgValidationRequired";
import { getConnectedAircraftsAPI } from "_querys/aircrafts";
import { useMapStore } from "_store/mapStore";
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
+import { XplaneObject } from "@repo/db";
+import { useEffect, useState } from "react";
export const MapAdditionals = () => {
- const { isOpen, missionFormValues } = usePannelStore((state) => state);
+ const { isOpen, missionFormValues, setMissionFormValues } = usePannelStore((state) => state);
const dispatcherConnectionState = useDispatchConnectionStore((state) => state.status);
+ const { openMissionMarker } = useMapStore((state) => state);
const { data: missions = [] } = useQuery({
queryKey: ["missions"],
@@ -21,13 +24,28 @@ export const MapAdditionals = () => {
}),
refetchInterval: 10_000,
});
- const mapStore = useMapStore((state) => state);
+ const { setOpenMissionMarker } = useMapStore((state) => state);
+ const [showDetailedAdditionals, setShowDetailedAdditionals] = useState(false);
const { data: aircrafts } = useQuery({
queryKey: ["aircrafts"],
queryFn: () => getConnectedAircraftsAPI(),
refetchInterval: 10000,
});
+ const leafletMap = useMap();
+
+ useEffect(() => {
+ const handleZoomEnd = () => {
+ const currentZoom = leafletMap.getZoom();
+ setShowDetailedAdditionals(currentZoom > 10);
+ };
+
+ leafletMap.on("zoomend", handleZoomEnd);
+
+ return () => {
+ leafletMap.off("zoomend", handleZoomEnd);
+ };
+ }, [leafletMap]);
const markersNeedingAttention = missions.filter(
(m) =>
@@ -37,7 +55,7 @@ export const MapAdditionals = () => {
m.hpgLocationLat &&
dispatcherConnectionState === "connected" &&
m.hpgLocationLng &&
- mapStore.openMissionMarker.find((openMission) => openMission.id === m.id),
+ openMissionMarker.find((openMission) => openMission.id === m.id),
);
return (
@@ -50,9 +68,78 @@ export const MapAdditionals = () => {
iconSize: [40, 40],
iconAnchor: [20, 35],
})}
- interactive={false}
+ draggable={true}
+ eventHandlers={{
+ dragend: (e) => {
+ const marker = e.target;
+ const position = marker.getLatLng();
+ setMissionFormValues({
+ ...missionFormValues,
+ addressLat: position.lat,
+ addressLng: position.lng,
+ });
+ },
+ }}
/>
)}
+ {showDetailedAdditionals &&
+ openMissionMarker.map((mission) => {
+ if (missionFormValues?.id === mission.id) return null;
+ const missionData = missions.find((m) => m.id === mission.id);
+ if (!missionData?.addressLat || !missionData?.addressLng) return null;
+ return (missionData.xPlaneObjects as unknown as XplaneObject[]).map((obj, index) => (
+
+ ));
+ })}
+ {isOpen &&
+ missionFormValues?.xPlaneObjects &&
+ (missionFormValues.xPlaneObjects as unknown as XplaneObject[]).map((obj, index) => (
+
{
+ const marker = e.target;
+ const position = marker.getLatLng();
+ console.log("Marker dragged to:", position);
+ setMissionFormValues({
+ ...missionFormValues,
+ xPlaneObjects: (missionFormValues.xPlaneObjects as unknown as XplaneObject[]).map(
+ (obj, objIndex) =>
+ objIndex === index ? { ...obj, lat: position.lat, lon: position.lng } : obj,
+ ),
+ });
+ },
+
+ contextmenu: (e) => {
+ e.originalEvent.preventDefault();
+ const updatedObjects = (
+ missionFormValues.xPlaneObjects as unknown as XplaneObject[]
+ ).filter((_, objIndex) => objIndex !== index);
+ setMissionFormValues({
+ ...missionFormValues,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ xPlaneObjects: updatedObjects as unknown as any[],
+ });
+ },
+ }}
+ />
+ ))}
{markersNeedingAttention.map((mission) => (
{
})}
eventHandlers={{
click: () =>
- mapStore.setOpenMissionMarker({
+ setOpenMissionMarker({
open: [
{
id: mission.id,
diff --git a/apps/dispatch/app/_components/map/XPlaneObject.tsx b/apps/dispatch/app/_components/map/XPlaneObject.tsx
new file mode 100644
index 00000000..c1f17230
--- /dev/null
+++ b/apps/dispatch/app/_components/map/XPlaneObject.tsx
@@ -0,0 +1,3 @@
+export const XPlaneObjects = () => {
+ return XPlaneObjects
;
+};
diff --git a/apps/dispatch/app/_components/map/_components/AircraftMarkerTabs.tsx b/apps/dispatch/app/_components/map/_components/AircraftMarkerTabs.tsx
index 94484eb2..97e529ab 100644
--- a/apps/dispatch/app/_components/map/_components/AircraftMarkerTabs.tsx
+++ b/apps/dispatch/app/_components/map/_components/AircraftMarkerTabs.tsx
@@ -296,6 +296,12 @@ const StationTab = ({ aircraft }: { aircraft: ConnectedAircraft & { Station: Sta
{aircraft.posH145active ? "H145 Aktiv" : "H145 Inaktiv"}
+
+ {" "}
+
+ {aircraft.posXplanePluginActive ? "X-Plane Plugin Aktiv" : "X-Plane Plugin Inaktiv"}
+
+
);
diff --git a/apps/dispatch/app/_components/navbar/AdminPanel.tsx b/apps/dispatch/app/_components/navbar/AdminPanel.tsx
index 43af976b..ae2d9396 100644
--- a/apps/dispatch/app/_components/navbar/AdminPanel.tsx
+++ b/apps/dispatch/app/_components/navbar/AdminPanel.tsx
@@ -7,6 +7,7 @@ import { getConnectedDispatcherAPI, kickDispatcherAPI } from "_querys/dispatcher
import { getLivekitRooms, kickLivekitParticipant } from "_querys/livekit";
import { ParticipantInfo } from "livekit-server-sdk";
import {
+ Dot,
LockKeyhole,
Plane,
RedoDot,
@@ -144,7 +145,12 @@ export default function AdminPanel() {
{!livekitParticipant ? (
Nicht verbunden
) : (
-
{livekitParticipant.room}
+
+ {livekitParticipant.room}{" "}
+ {livekitParticipant?.participant.tracks.some((t) => !t.muted) && (
+
+ )}
+
)}
@@ -209,7 +215,12 @@ export default function AdminPanel() {
{!livekitParticipant ? (
Nicht verbunden
) : (
- {livekitParticipant.room}
+
+ {livekitParticipant.room}{" "}
+ {livekitParticipant?.participant.tracks.some((t) => !t.muted) && (
+
+ )}
+
)}
|
@@ -274,8 +285,13 @@ export default function AdminPanel() {
|
Nicht verbunden
|
-
- {p.room}
+ |
+
+ {p.room}
+ {p.participant.tracks.some((t) => !t.muted) && (
+
+ )}
+
|
|