diff --git a/apps/dispatch-server/routes/aircraft.ts b/apps/dispatch-server/routes/aircraft.ts
index c3ff5d44..fa665438 100644
--- a/apps/dispatch-server/routes/aircraft.ts
+++ b/apps/dispatch-server/routes/aircraft.ts
@@ -2,6 +2,7 @@ import {
AdminMessage,
getPublicUser,
MissionLog,
+ MissionSdsStatusLog,
NotificationPayload,
Prisma,
prisma,
@@ -130,6 +131,44 @@ router.patch("/:id", async (req, res) => {
}
});
+router.post("/:id/send-sds-message", async (req, res) => {
+ const { id } = req.params;
+ const { sdsMessage } = req.body as { sdsMessage: MissionSdsStatusLog };
+
+ if (!sdsMessage.data.stationId || !id) {
+ res.status(400).json({ error: "Missing aircraftId or stationId" });
+ return;
+ }
+
+ await prisma.mission.updateMany({
+ where: {
+ state: "running",
+ missionStationIds: {
+ has: sdsMessage.data.stationId,
+ },
+ },
+ data: {
+ missionLog: {
+ push: sdsMessage as unknown as Prisma.InputJsonValue,
+ },
+ },
+ });
+
+ io.to(
+ sdsMessage.data.direction === "to-lst" ? "dispatchers" : `station:${sdsMessage.data.stationId}`,
+ ).emit(sdsMessage.data.direction === "to-lst" ? "notification" : "sds-status", {
+ type: "station-status",
+ status: sdsMessage.data.status,
+ message: "SDS Status Message",
+ data: {
+ aircraftId: parseInt(id),
+ stationId: sdsMessage.data.stationId,
+ },
+ } as NotificationPayload);
+
+ res.sendStatus(204);
+});
+
// Kick a connectedAircraft by ID
router.delete("/:id", async (req, res) => {
const { id } = req.params;
diff --git a/apps/dispatch/Dockerfile b/apps/dispatch/Dockerfile
index 692710a6..1236c9d6 100644
--- a/apps/dispatch/Dockerfile
+++ b/apps/dispatch/Dockerfile
@@ -74,9 +74,6 @@ COPY --from=installer --chown=nextjs:nodejs /usr/app/apps/dispatch/public ./apps
USER nextjs
# Expose the application port
-EXPOSE 3001
-
-ENV PORT=3001
-ENV HOSTNAME="0.0.0.0"
+EXPOSE 3000
CMD ["node", "apps/dispatch/server.js"]
\ No newline at end of file
diff --git a/apps/dispatch/app/(app)/dispatch/_components/navbar/_components/Connection.tsx b/apps/dispatch/app/(app)/dispatch/_components/navbar/_components/Connection.tsx
index f1450f19..039d7ac2 100644
--- a/apps/dispatch/app/(app)/dispatch/_components/navbar/_components/Connection.tsx
+++ b/apps/dispatch/app/(app)/dispatch/_components/navbar/_components/Connection.tsx
@@ -14,7 +14,7 @@ export const ConnectionBtn = () => {
const connection = useDispatchConnectionStore((state) => state);
const [form, setForm] = useState({
logoffTime: "",
- selectedZone: "LST_01",
+ selectedZone: "VAR_LST_RD_01",
ghostMode: false,
});
const changeDispatcherMutation = useMutation({
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/Base.tsx b/apps/dispatch/app/(app)/pilot/_components/mrt/Base.tsx
new file mode 100644
index 00000000..7624c177
--- /dev/null
+++ b/apps/dispatch/app/(app)/pilot/_components/mrt/Base.tsx
@@ -0,0 +1,15 @@
+import { useMrtStore } from "_store/pilot/MrtStore";
+import Image from "next/image";
+import DAY_BASE_IMG from "./images/Base_NoScreen_Day.png";
+import NIGHT_BASE_IMG from "./images/Base_NoScreen_Night.png";
+
+export const MrtBase = () => {
+ const { nightMode } = useMrtStore((state) => state);
+ return (
+
+ );
+};
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/MRT.png b/apps/dispatch/app/(app)/pilot/_components/mrt/MRT.png
deleted file mode 100644
index a9c552b1..00000000
Binary files a/apps/dispatch/app/(app)/pilot/_components/mrt/MRT.png and /dev/null differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/MRT_MESSAGE.png b/apps/dispatch/app/(app)/pilot/_components/mrt/MRT_MESSAGE.png
deleted file mode 100644
index a0e80ae6..00000000
Binary files a/apps/dispatch/app/(app)/pilot/_components/mrt/MRT_MESSAGE.png and /dev/null differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/Mrt.tsx b/apps/dispatch/app/(app)/pilot/_components/mrt/Mrt.tsx
index 5d271921..0fda6281 100644
--- a/apps/dispatch/app/(app)/pilot/_components/mrt/Mrt.tsx
+++ b/apps/dispatch/app/(app)/pilot/_components/mrt/Mrt.tsx
@@ -1,22 +1,9 @@
import { CSSProperties } from "react";
-import MrtImage from "./MRT.png";
-import MrtMessageImage from "./MRT_MESSAGE.png";
-import { useButtons } from "./useButtons";
-import { useSounds } from "./useSounds";
import "./mrt.css";
-import Image from "next/image";
-import { useMrtStore } from "_store/pilot/MrtStore";
-
-const MRT_BUTTON_STYLES: CSSProperties = {
- cursor: "pointer",
- zIndex: "9999",
- backgroundColor: "transparent",
- border: "none",
-};
-const MRT_DISPLAYLINE_STYLES: CSSProperties = {
- color: "white",
- zIndex: 1,
-};
+import { MrtBase } from "./Base";
+import { MrtDisplay } from "./MrtDisplay";
+import { MrtButtons } from "./MrtButtons";
+import { MrtPopups } from "./MrtPopups";
export interface DisplayLineProps {
lineStyle?: CSSProperties;
@@ -27,45 +14,7 @@ export interface DisplayLineProps {
textSize: "1" | "2" | "3" | "4";
}
-const DisplayLine = ({
- style = {},
- textLeft,
- textMid,
- textRight,
- textSize,
- lineStyle,
-}: DisplayLineProps) => {
- const INNER_TEXT_PARTS: CSSProperties = {
- fontFamily: "Melder",
- flex: "1",
- flexBasis: "auto",
- overflowWrap: "break-word",
- ...lineStyle,
- };
-
- return (
-
- {textLeft}
- {textMid}
- {textRight}
-
- );
-};
-
export const Mrt = () => {
- useSounds();
- const { handleButton } = useButtons();
- const { lines, page } = useMrtStore((state) => state);
-
return (
{
maxHeight: "100%",
maxWidth: "100%",
color: "white",
- gridTemplateColumns: "21.83% 4.43% 24.42% 18.08% 5.93% 1.98% 6.00% 1.69% 6.00% 9.35%",
- gridTemplateRows: "21.58% 11.87% 3.55% 5.00% 6.84% 0.53% 3.03% 11.84% 3.55% 11.84% 20.39%",
+ gridTemplateColumns:
+ "9.75% 4.23% 8.59% 7.30% 1.16% 7.30% 1.23% 7.16% 1.09% 7.30% 3.68% 4.23% 5.59% 6.07% 1.91% 6.07% 1.84% 6.21% 9.28%",
+ gridTemplateRows:
+ "21.55% 11.83% 3.55% 2.50% 9.46% 2.76% 0.66% 4.99% 6.83% 3.55% 1.97% 9.99% 4.20% 11.04% 5.12%",
}}
>
- {page !== "sds" && (
-
- )}
- {page === "sds" && (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {lines[0] && (
-
- )}
- {lines[1] && (
-
- )}
- {lines[2] && (
-
- )}
- {lines[3] && (
-
- )}
+
+
+
+
);
};
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/MrtButtons.tsx b/apps/dispatch/app/(app)/pilot/_components/mrt/MrtButtons.tsx
index c7ace86d..a7942466 100644
--- a/apps/dispatch/app/(app)/pilot/_components/mrt/MrtButtons.tsx
+++ b/apps/dispatch/app/(app)/pilot/_components/mrt/MrtButtons.tsx
@@ -32,7 +32,6 @@ const MrtButton = ({ onClick, onHold, style }: MrtButtonProps) => {
const handleMouseUp = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
-
onClick();
}
};
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/MrtDisplay.css b/apps/dispatch/app/(app)/pilot/_components/mrt/MrtDisplay.css
new file mode 100644
index 00000000..132d6522
--- /dev/null
+++ b/apps/dispatch/app/(app)/pilot/_components/mrt/MrtDisplay.css
@@ -0,0 +1,16 @@
+.transition-image {
+ clip-path: inset(0 0 100% 0);
+}
+
+.transition-image.animate-reveal {
+ animation: revealFromTop 0.6s linear forwards;
+}
+
+@keyframes revealFromTop {
+ from {
+ clip-path: inset(0 0 100% 0);
+ }
+ to {
+ clip-path: inset(0 0 0 0);
+ }
+}
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/MrtPopups.tsx b/apps/dispatch/app/(app)/pilot/_components/mrt/MrtPopups.tsx
new file mode 100644
index 00000000..1e1dca31
--- /dev/null
+++ b/apps/dispatch/app/(app)/pilot/_components/mrt/MrtPopups.tsx
@@ -0,0 +1,152 @@
+import { SetPopupParams, useMrtStore } from "_store/pilot/MrtStore";
+import Image, { StaticImageData } from "next/image";
+import { useEffect, useState } from "react";
+import IAMGE_POPUP_LOGIN from "./images/POPUP_login.png";
+import GROUP_SELECTION_POPUP_LOGIN from "./images/POPUP_group_selection.png";
+import IAMGE_POPUP_SDS_RECEIVED from "./images/POPUP_SDS_incomming.png";
+import IAMGE_POPUP_SDS_SENT from "./images/POPUP_SDS_sent.png";
+import IAMGE_POPUP_STATUS_SENT from "./images/POPUP_Status_sent.png";
+import { ROOMS } from "_data/livekitRooms";
+import { cn, useDebounce } from "@repo/shared-components";
+import { usePilotConnectionStore } from "_store/pilot/connectionStore";
+import { fmsStatusDescription, fmsStatusDescriptionShort } from "_data/fmsStatusDescription";
+import { pilotSocket } from "(app)/pilot/socket";
+import { StationStatus } from "@repo/db";
+import { useSounds } from "./useSounds";
+import { useButtons } from "./useButtons";
+import { useAudioStore } from "_store/audioStore";
+
+export const MrtPopups = () => {
+ const { sdsReceivedSoundRef } = useSounds();
+ const { handleKlick } = useButtons();
+ const { selectedRoom } = useAudioStore();
+ const { popup, page, setPopup, setStringifiedData, stringifiedData } = useMrtStore(
+ (state) => state,
+ );
+ const { connectedAircraft, status } = usePilotConnectionStore((state) => state);
+ const [popupImage, setPopupImage] = useState(null);
+
+ useEffect(() => {
+ switch (popup) {
+ case "status-sent":
+ setPopupImage(IAMGE_POPUP_STATUS_SENT);
+ break;
+ case "sds-sent":
+ setPopupImage(IAMGE_POPUP_SDS_SENT);
+ break;
+ case "sds-received":
+ setPopupImage(IAMGE_POPUP_SDS_RECEIVED);
+ break;
+ case "login":
+ setPopupImage(IAMGE_POPUP_LOGIN);
+ break;
+ case "group-selection":
+ setPopupImage(GROUP_SELECTION_POPUP_LOGIN);
+ break;
+ case undefined:
+ case null:
+ setPopupImage(null);
+ break;
+ }
+ }, [popup]);
+
+ useDebounce(
+ () => {
+ if (popup == "login") return;
+ if (popup == "sds-received") return;
+ if (popup == "group-selection") return;
+ setPopup(null);
+ },
+ 3000,
+ [popup],
+ );
+
+ useDebounce(
+ () => {
+ if (popup == "group-selection") {
+ if (selectedRoom?.id === stringifiedData.groupSelectionGroupId) {
+ setPopup(null);
+ } else {
+ handleKlick("3l")();
+ }
+ }
+ },
+ 5000,
+ [page, stringifiedData.groupSelectionGroupId, selectedRoom],
+ );
+
+ useEffect(() => {
+ if (status === "connecting" && page !== "off" && page !== "startup") {
+ setPopup({ popup: "login" });
+ }
+ }, [status, setPopup, page]);
+
+ useDebounce(
+ () => {
+ if (status === "connected") {
+ setPopup(null);
+ }
+ },
+ 5000,
+ [status],
+ );
+
+ useEffect(() => {
+ pilotSocket.on("sds-status", (data: StationStatus) => {
+ setStringifiedData({ sdsText: data.status + " - " + fmsStatusDescriptionShort[data.status] });
+ setPopup({ popup: "sds-received" });
+ if (sdsReceivedSoundRef.current) {
+ sdsReceivedSoundRef.current.currentTime = 0;
+ sdsReceivedSoundRef.current.play();
+ }
+ });
+ }, [setPopup, setStringifiedData, sdsReceivedSoundRef]);
+
+ if (!popupImage || !popup) return null;
+
+ const DisplayText = ({ pageName }: { pageName: SetPopupParams["popup"] }) => {
+ const group = ROOMS.find((r) => r.id === stringifiedData.groupSelectionGroupId);
+
+ return (
+
+ {pageName == "status-sent" && (
+
+ {fmsStatusDescription[connectedAircraft?.fmsStatus || "0"]}
+
+ )}
+ {pageName == "sds-sent" && (
+
+ {fmsStatusDescription[stringifiedData.sentSdsText || "0"]}
+
+ )}
+ {pageName == "sds-received" && (
+
+ {stringifiedData.sdsText}
+
+ )}
+ {pageName == "group-selection" && (
+ <>
+
{group?.name}
+
+ {stringifiedData.groupSelectionGroupId}
+
+ >
+ )}
+
+ );
+ };
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/Base_NoScreen_Day.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/Base_NoScreen_Day.png
new file mode 100644
index 00000000..1aea5305
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/Base_NoScreen_Day.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/Base_NoScreen_Night.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/Base_NoScreen_Night.png
new file mode 100644
index 00000000..1f44f4d5
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/Base_NoScreen_Night.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/Call_Placeholder.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/Call_Placeholder.png
new file mode 100644
index 00000000..2ed41eb6
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/Call_Placeholder.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/Home_Placeholder.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/Home_Placeholder.png
new file mode 100644
index 00000000..865da37b
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/Home_Placeholder.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/PAGE_Call.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/PAGE_Call.png
new file mode 100644
index 00000000..956f2986
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/PAGE_Call.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/PAGE_Home.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/PAGE_Home.png
new file mode 100644
index 00000000..0bcd0d69
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/PAGE_Home.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/PAGE_Off.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/PAGE_Off.png
new file mode 100644
index 00000000..e472fcd7
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/PAGE_Off.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/PAGE_Startup.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/PAGE_Startup.png
new file mode 100644
index 00000000..a27ba7ad
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/PAGE_Startup.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_SDS_incomming.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_SDS_incomming.png
new file mode 100644
index 00000000..4baaed05
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_SDS_incomming.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_SDS_sent.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_SDS_sent.png
new file mode 100644
index 00000000..2bd13b40
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_SDS_sent.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_Status_sent.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_Status_sent.png
new file mode 100644
index 00000000..bfb645fb
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_Status_sent.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_group_selection.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_group_selection.png
new file mode 100644
index 00000000..72834534
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_group_selection.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_login.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_login.png
new file mode 100644
index 00000000..18bbd22d
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_login.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/SDS_Ausgang_Placeholder.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/SDS_Ausgang_Placeholder.png
new file mode 100644
index 00000000..4dcc42c7
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/SDS_Ausgang_Placeholder.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/SDS_Eingang_Placeholder.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/SDS_Eingang_Placeholder.png
new file mode 100644
index 00000000..3949b142
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/SDS_Eingang_Placeholder.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/Splash_Placeholder.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/Splash_Placeholder.png
new file mode 100644
index 00000000..6fb2bf6b
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/Splash_Placeholder.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/Status_Ausgang_Placeholder.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/Status_Ausgang_Placeholder.png
new file mode 100644
index 00000000..c8df4f08
Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/Status_Ausgang_Placeholder.png differ
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/useButtons.ts b/apps/dispatch/app/(app)/pilot/_components/mrt/useButtons.ts
index 8718295f..04fa70a6 100644
--- a/apps/dispatch/app/(app)/pilot/_components/mrt/useButtons.ts
+++ b/apps/dispatch/app/(app)/pilot/_components/mrt/useButtons.ts
@@ -1,15 +1,58 @@
-import { Prisma } from "@repo/db";
+import { getPublicUser, MissionSdsStatusLog, Prisma } from "@repo/db";
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
import { useMrtStore } from "_store/pilot/MrtStore";
import { pilotSocket } from "(app)/pilot/socket";
import { editConnectedAircraftAPI } from "_querys/aircrafts";
import { useEffect } from "react";
-import { useMutation } from "@tanstack/react-query";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useSounds } from "./useSounds";
+import { sendSdsStatusMessageAPI } from "_querys/missions";
+import { useSession } from "next-auth/react";
+import { ROOMS } from "_data/livekitRooms";
+import { useAudioStore } from "_store/audioStore";
+
+type ButtonTypes =
+ | "1"
+ | "2"
+ | "3"
+ | "4"
+ | "5"
+ | "6"
+ | "7"
+ | "8"
+ | "9"
+ | "0"
+ | "home"
+ | "3l"
+ | "3r"
+ | "wheel-knob"
+ | "arrow-up"
+ | "arrow-down"
+ | "arrow-left"
+ | "arrow-right"
+ | "end-call";
export const useButtons = () => {
- const station = usePilotConnectionStore((state) => state.selectedStation);
- const connectedAircraft = usePilotConnectionStore((state) => state.connectedAircraft);
- const connectionStatus = usePilotConnectionStore((state) => state.status);
+ const session = useSession();
+ const { connect, setSelectedRoom, selectedRoom } = useAudioStore((state) => state);
+
+ const { longBtnPressSoundRef, statusSentSoundRef } = useSounds();
+ const queryClient = useQueryClient();
+ const {
+ status: pilotState,
+ selectedStation,
+ connectedAircraft,
+ } = usePilotConnectionStore((state) => state);
+
+ const sendSdsStatusMutation = useMutation({
+ mutationFn: async ({ sdsMessage }: { sdsMessage: MissionSdsStatusLog }) => {
+ if (!connectedAircraft?.id) throw new Error("No connected aircraft");
+ await sendSdsStatusMessageAPI({ sdsMessage, aircraftId: connectedAircraft?.id });
+ queryClient.invalidateQueries({
+ queryKey: ["missions"],
+ });
+ },
+ });
const updateAircraftMutation = useMutation({
mutationKey: ["edit-pilot-connected-aircraft"],
mutationFn: ({
@@ -21,56 +64,161 @@ export const useButtons = () => {
}) => editConnectedAircraftAPI(aircraftId, data),
});
- const { setPage } = useMrtStore((state) => state);
+ const { setPage, setPopup, page, popup, setStringifiedData, stringifiedData } = useMrtStore(
+ (state) => state,
+ );
- const handleButton =
- (button: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0" | "home") => () => {
- if (connectionStatus !== "connected") return;
- if (!station) return;
- if (!connectedAircraft?.id) return;
- if (
- button === "1" ||
- button === "2" ||
- button === "3" ||
- button === "4" ||
- button === "5" ||
- button === "6" ||
- button === "7" ||
- button === "8" ||
- button === "9" ||
- button === "0"
- ) {
- setPage({ page: "sending-status", station });
+ const role =
+ (pilotState == "connected" && selectedStation?.bosCallsignShort) ||
+ session.data?.user?.publicId;
- setTimeout(async () => {
- await updateAircraftMutation.mutateAsync({
- aircraftId: connectedAircraft.id,
- data: {
- fmsStatus: button,
- },
- });
- setPage({
- page: "home",
- station,
+ const handleHold = (button: ButtonTypes) => async () => {
+ /* if (connectionStatus !== "connected") return; */
+ if (button === "end-call") {
+ setPage({ page: "off" });
+ setPopup(null);
+ }
+ if (button === "1" && page === "off") {
+ setPage({ page: "startup" });
+ return;
+ }
+ if (!selectedStation) return;
+ if (!session.data?.user) return;
+ if (!connectedAircraft?.id) return;
+ if (
+ button === "1" ||
+ button === "2" ||
+ button === "3" ||
+ button === "4" ||
+ button === "6" ||
+ button === "7" ||
+ button === "8"
+ ) {
+ longBtnPressSoundRef.current?.play();
+ const delay = Math.random() * 1500 + 500;
+ setTimeout(async () => {
+ await updateAircraftMutation.mutateAsync({
+ aircraftId: connectedAircraft.id,
+ data: {
fmsStatus: button,
- });
- }, 1000);
- } else {
- setPage({ page: "home", fmsStatus: connectedAircraft.fmsStatus || "6", station });
- }
- };
+ },
+ });
+ setPopup({ popup: "status-sent" });
+ statusSentSoundRef.current?.play();
+ }, delay);
+ } else if (button === "5" || button === "9" || button === "0") {
+ longBtnPressSoundRef.current?.play();
+ const delay = Math.random() * 1500 + 500;
+ setTimeout(async () => {
+ await sendSdsStatusMutation.mutateAsync({
+ sdsMessage: {
+ type: "sds-status-log",
+ auto: false,
+ timeStamp: new Date().toISOString(),
+ data: {
+ direction: "to-lst",
+ stationId: selectedStation.id,
+ station: selectedStation,
+ user: getPublicUser(session.data?.user),
+ status: button,
+ },
+ },
+ });
+ setStringifiedData({ sentSdsText: button });
+ statusSentSoundRef.current?.play();
+ setPopup({ popup: "sds-sent" });
+ }, delay);
+ }
+ };
+
+ const handleKlick = (button: ButtonTypes) => async () => {
+ console.log("Button clicked:", button);
+ //implement Kurzwahl when button is clicked short to dial
+
+ switch (button) {
+ case "0":
+ case "1":
+ case "2":
+ case "3":
+ case "4":
+ case "5":
+ case "6":
+ case "7":
+ case "8":
+ case "9":
+ //handle short press number buttons for kurzwahl
+ if (popup === "group-selection") {
+ if (stringifiedData.groupSelectionGroupId?.length === 4) {
+ setStringifiedData({ groupSelectionGroupId: button });
+ } else {
+ setStringifiedData({
+ groupSelectionGroupId: (stringifiedData.groupSelectionGroupId || "") + button,
+ });
+ }
+ }
+ if (page === "home" && !popup) {
+ setPopup({ popup: "group-selection" });
+ setStringifiedData({ groupSelectionGroupId: button });
+ }
+ break;
+ case "3r":
+ if (popup === "sds-received" || popup === "group-selection") {
+ setPopup(null);
+ } else if (page === "home") {
+ setPopup({ popup: "group-selection" });
+ setStringifiedData({ groupSelectionGroupId: selectedRoom?.id || ROOMS[0]!.id });
+ } else if (page === "voice-call") {
+ setPage({ page: "home" });
+ }
+ break;
+ case "wheel-knob":
+ setPopup(popup === "group-selection" ? null : { popup: "group-selection" });
+ setStringifiedData({ groupSelectionGroupId: selectedRoom?.id || ROOMS[0]!.id });
+ break;
+ case "arrow-right":
+ if (popup === "group-selection") {
+ let currentGroupIndex = ROOMS.findIndex(
+ (r) => r.id === stringifiedData.groupSelectionGroupId,
+ );
+ if (currentGroupIndex === ROOMS.length - 1) currentGroupIndex = -1;
+ const nextGroup = ROOMS[currentGroupIndex + 1];
+ if (nextGroup) {
+ setStringifiedData({ groupSelectionGroupId: nextGroup.id });
+ }
+ }
+ break;
+ case "arrow-left":
+ if (popup === "group-selection") {
+ let currentGroupIndex = ROOMS.findIndex(
+ (r) => r.id === stringifiedData.groupSelectionGroupId,
+ );
+ if (currentGroupIndex === 0) currentGroupIndex = ROOMS.length;
+ const previousGroup = ROOMS[currentGroupIndex - 1];
+ if (previousGroup) {
+ setStringifiedData({ groupSelectionGroupId: previousGroup.id });
+ }
+ }
+ break;
+ case "3l":
+ if (popup === "group-selection") {
+ const group = ROOMS.find((r) => r.id === stringifiedData.groupSelectionGroupId);
+ if (group && role) {
+ setSelectedRoom(group);
+ connect(group, role);
+ setPopup(null);
+ }
+ }
+ }
+ return false;
+ };
useEffect(() => {
pilotSocket.on("connect", () => {
- if (!station) return;
- setPage({ page: "home", fmsStatus: "6", station });
+ const { page } = useMrtStore.getState();
+ if (!selectedStation || page !== "off") return;
+ setPage({ page: "startup" });
});
+ }, [setPage, selectedStation, setPopup]);
- pilotSocket.on("aircraft-update", () => {
- if (!station) return;
- setPage({ page: "new-status", station });
- });
- }, [setPage, station]);
-
- return { handleButton };
+ return { handleKlick, handleHold };
};
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/useSounds.ts b/apps/dispatch/app/(app)/pilot/_components/mrt/useSounds.ts
index d40e5dd3..77fcfefa 100644
--- a/apps/dispatch/app/(app)/pilot/_components/mrt/useSounds.ts
+++ b/apps/dispatch/app/(app)/pilot/_components/mrt/useSounds.ts
@@ -1,52 +1,22 @@
"use client";
-import { usePilotConnectionStore } from "_store/pilot/connectionStore";
-import { useMrtStore } from "_store/pilot/MrtStore";
import { useEffect, useRef } from "react";
export const useSounds = () => {
- const mrtState = useMrtStore((state) => state);
- const { connectedAircraft, selectedStation } = usePilotConnectionStore((state) => state);
-
- const setPage = useMrtStore((state) => state.setPage);
- const MRTstatusSoundRef = useRef(null);
- const MrtMessageReceivedSoundRef = useRef(null);
+ const longBtnPressSoundRef = useRef(null);
+ const statusSentSoundRef = useRef(null);
+ const sdsReceivedSoundRef = useRef(null);
useEffect(() => {
if (typeof window !== "undefined") {
- MRTstatusSoundRef.current = new Audio("/sounds/MRT-status.mp3");
- MrtMessageReceivedSoundRef.current = new Audio("/sounds/MRT-message-received.mp3");
- MRTstatusSoundRef.current.onended = () => {
- if (!selectedStation || !connectedAircraft?.fmsStatus) return;
- setPage({
- page: "home",
- station: selectedStation,
- fmsStatus: connectedAircraft?.fmsStatus,
- });
- };
- MrtMessageReceivedSoundRef.current.onended = () => {
- if (!selectedStation || !connectedAircraft?.fmsStatus) return;
- if (mrtState.page === "sds") return;
- setPage({
- page: "home",
- station: selectedStation,
- fmsStatus: connectedAircraft?.fmsStatus,
- });
- };
+ longBtnPressSoundRef.current = new Audio("/sounds/1504.wav");
+ statusSentSoundRef.current = new Audio("/sounds/403.wav");
+ sdsReceivedSoundRef.current = new Audio("/sounds/775.wav");
}
- }, [connectedAircraft?.fmsStatus, selectedStation, setPage, mrtState.page]);
+ }, []);
- const fmsStatus = connectedAircraft?.fmsStatus || "NaN";
-
- useEffect(() => {
- if (!connectedAircraft) return;
- if (mrtState.page === "new-status") {
- if (fmsStatus === "J" || fmsStatus === "c") {
- MrtMessageReceivedSoundRef.current?.play();
- } else {
- MRTstatusSoundRef.current?.play();
- }
- } else if (mrtState.page === "sds") {
- MrtMessageReceivedSoundRef.current?.play();
- }
- }, [mrtState, fmsStatus, connectedAircraft, selectedStation]);
+ return {
+ longBtnPressSoundRef,
+ statusSentSoundRef,
+ sdsReceivedSoundRef,
+ };
};
diff --git a/apps/dispatch/app/(app)/pilot/page.tsx b/apps/dispatch/app/(app)/pilot/page.tsx
index 5ec04be4..34686bf2 100644
--- a/apps/dispatch/app/(app)/pilot/page.tsx
+++ b/apps/dispatch/app/(app)/pilot/page.tsx
@@ -23,7 +23,7 @@ const Map = dynamic(() => import("_components/map/Map"), {
});
const PilotPage = () => {
- const { connectedAircraft, status, } = usePilotConnectionStore((state) => state);
+ const { connectedAircraft, status } = usePilotConnectionStore((state) => state);
const { latestMission } = useDmeStore((state) => state);
// Query will be cached anyway, due to this, displayed Markers are in sync with own Aircraft connection-warning
const { data: aircrafts } = useQuery({
@@ -94,10 +94,20 @@ const PilotPage = () => {
-
+
-
MRT & DME
+
{
const {
+ selectedRoom,
speakingParticipants,
resetSpeakingParticipants,
isTalking,
@@ -37,8 +38,8 @@ export const Audio = () => {
room,
message,
removeMessage,
+ setSelectedRoom,
} = useAudioStore();
- const [selectedRoom, setSelectedRoom] = useState
("LST_01");
useSounds({
isReceiving: speakingParticipants.length > 0,
@@ -48,7 +49,7 @@ export const Audio = () => {
});
const { selectedStation, status: pilotState } = usePilotConnectionStore((state) => state);
- const { selectedZone, status: dispatcherState } = useDispatchConnectionStore((state) => state);
+ const { status: dispatcherState } = useDispatchConnectionStore((state) => state);
const session = useSession();
const [isReceivingBlick, setIsReceivingBlick] = useState(false);
const [recentSpeakers, setRecentSpeakers] = useState([]);
@@ -93,7 +94,7 @@ export const Audio = () => {
const canStopOtherSpeakers = dispatcherState === "connected";
const role =
- (dispatcherState === "connected" && selectedZone) ||
+ (dispatcherState === "connected" && "VAR LST") ||
(pilotState == "connected" && selectedStation?.bosCallsignShort) ||
session.data?.user?.publicId;
@@ -185,20 +186,20 @@ export const Audio = () => {
{ROOMS.map((r) => (
- -
+
-
))}
diff --git a/apps/dispatch/app/_components/QueryProvider.tsx b/apps/dispatch/app/_components/QueryProvider.tsx
index d5b2799a..345d2fb1 100644
--- a/apps/dispatch/app/_components/QueryProvider.tsx
+++ b/apps/dispatch/app/_components/QueryProvider.tsx
@@ -63,14 +63,15 @@ export function QueryProvider({ children }: { children: ReactNode }) {
};
const handleNotification = (notification: NotificationPayload) => {
+ console.log("Received notification:", notification);
const playNotificationSound = () => {
if (notificationSound.current) {
- notificationSound.current.currentTime = 0;
- notificationSound.current
- .play()
- .catch((e) => console.error("Notification sound error:", e));
- }
- }
+ notificationSound.current.currentTime = 0;
+ notificationSound.current
+ .play()
+ .catch((e) => console.error("Notification sound error:", e));
+ }
+ };
switch (notification.type) {
case "hpg-validation":
@@ -90,6 +91,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
});
break;
case "station-status":
+ console.log("station Status", QUICK_RESPONSE[notification.status]);
if (!QUICK_RESPONSE[notification.status]) return;
toast.custom((e) => , {
duration: 60000,
@@ -106,7 +108,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
break;
case "mission-closed":
toast("Dein aktueller Einsatz wurde geschlossen.");
-
+
break;
default:
toast("unbekanntes Notification-Event");
diff --git a/apps/dispatch/app/_components/customToasts/StationStatusToast.tsx b/apps/dispatch/app/_components/customToasts/StationStatusToast.tsx
index 0b163e79..a3d3b64a 100644
--- a/apps/dispatch/app/_components/customToasts/StationStatusToast.tsx
+++ b/apps/dispatch/app/_components/customToasts/StationStatusToast.tsx
@@ -1,14 +1,16 @@
-import { Prisma, StationStatus } from "@repo/db";
+import { getPublicUser, MissionSdsStatusLog, StationStatus } from "@repo/db";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { BaseNotification } from "_components/customToasts/BaseNotification";
import { FMS_STATUS_COLORS } from "_helpers/fmsStatusColors";
-import { editConnectedAircraftAPI, getConnectedAircraftsAPI } from "_querys/aircrafts";
+import { getConnectedAircraftsAPI } from "_querys/aircrafts";
import { getLivekitRooms } from "_querys/livekit";
+import { sendSdsStatusMessageAPI } from "_querys/missions";
import { getStationsAPI } from "_querys/stations";
import { useAudioStore } from "_store/audioStore";
import { useMapStore } from "_store/mapStore";
import { X } from "lucide-react";
-import { useEffect, useRef, useState } from "react";
+import { useSession } from "next-auth/react";
+import { useEffect, useRef } from "react";
import { Toast, toast } from "react-hot-toast";
export const QUICK_RESPONSE: Record = {
@@ -22,6 +24,8 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
const status5Sounds = useRef(null);
const status9Sounds = useRef(null);
+ const session = useSession();
+
const { data: livekitRooms } = useQuery({
queryKey: ["livekit-rooms"],
queryFn: () => getLivekitRooms(),
@@ -46,7 +50,7 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
status9Sounds.current = new Audio("/sounds/status-9.mp3");
}
}, []);
- const [aircraftDataAcurate, setAircraftDataAccurate] = useState(false);
+
//const mapStore = useMapStore((s) => s);
const { setOpenAircraftMarker, setMap } = useMapStore((store) => store);
@@ -65,29 +69,16 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
const station = stations?.find((s) => s.id === event.data?.stationId);
const queryClient = useQueryClient();
- const changeAircraftMutation = useMutation({
- mutationFn: async ({
- id,
- update,
- }: {
- id: number;
- update: Prisma.ConnectedAircraftUpdateInput;
- }) => {
- await editConnectedAircraftAPI(id, update);
+ const sendSdsStatusMutation = useMutation({
+ mutationFn: async ({ sdsMessage }: { sdsMessage: MissionSdsStatusLog }) => {
+ if (!connectedAircraft?.id) throw new Error("No connected aircraft");
+ await sendSdsStatusMessageAPI({ sdsMessage, aircraftId: connectedAircraft?.id });
queryClient.invalidateQueries({
- queryKey: ["aircrafts"],
+ queryKey: ["missions"],
});
},
});
- useEffect(() => {
- if (event.status !== connectedAircraft?.fmsStatus && aircraftDataAcurate) {
- toast.remove(t.id);
- } else if (event.status == connectedAircraft?.fmsStatus && !aircraftDataAcurate) {
- setAircraftDataAccurate(true);
- }
- }, [aircraftDataAcurate, connectedAircraft, event.status, t.id]);
-
useEffect(() => {
let soundRef: React.RefObject | null = null;
switch (event.status) {
@@ -103,7 +94,8 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
default:
soundRef = null;
}
- if (audioRoom !== livekitUser?.roomName) {
+
+ if (audioRoom && livekitUser?.roomName && audioRoom !== livekitUser?.roomName) {
toast.remove(t.id);
return;
}
@@ -121,7 +113,8 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
};
}, [event.status, livekitUser?.roomName, audioRoom, t.id]);
- if (!connectedAircraft || !station) return null;
+ console.log(connectedAircraft, station);
+ if (!connectedAircraft || !station || !session.data) return null;
return (
@@ -162,10 +155,18 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
toast.error("Keine Flugzeug-ID gefunden");
return;
}
- await changeAircraftMutation.mutateAsync({
- id: event.data?.aircraftId,
- update: {
- fmsStatus: status,
+ await sendSdsStatusMutation.mutateAsync({
+ sdsMessage: {
+ type: "sds-status-log",
+ auto: false,
+ data: {
+ direction: "to-aircraft",
+ stationId: event.data.stationId!,
+ station: station,
+ user: getPublicUser(session.data?.user),
+ status,
+ },
+ timeStamp: new Date().toISOString(),
},
});
toast.remove(t.id);
diff --git a/apps/dispatch/app/_components/map/AircraftMarker.tsx b/apps/dispatch/app/_components/map/AircraftMarker.tsx
index 9c0adaf3..3961690d 100644
--- a/apps/dispatch/app/_components/map/AircraftMarker.tsx
+++ b/apps/dispatch/app/_components/map/AircraftMarker.tsx
@@ -2,7 +2,7 @@ import { Marker, Polyline, useMap } from "react-leaflet";
import { DivIcon, Marker as LMarker, Popup as LPopup } from "leaflet";
import { useMapStore } from "_store/mapStore";
import { Fragment, useCallback, useEffect, useRef, useState, useMemo } from "react";
-import { checkSimulatorConnected, cn } from "@repo/shared-components";
+import { cn } from "@repo/shared-components";
import { ChevronsRightLeft, House, MessageSquareText, Minimize2 } from "lucide-react";
import { SmartPopup, calculateAnchor, useSmartPopup } from "_components/SmartPopup";
import FMSStatusHistory, {
@@ -396,27 +396,11 @@ const AircraftMarker = ({ aircraft }: { aircraft: ConnectedAircraft & { Station:
};
export const AircraftLayer = () => {
- const [aircrafts, setAircrafts] = useState<(ConnectedAircraft & { Station: Station })[]>([]);
-
- useEffect(() => {
- const fetchAircrafts = async () => {
- try {
- const res = await fetch("/api/aircrafts");
- if (!res.ok) {
- throw new Error("Failed to fetch aircrafts");
- }
- const data: (ConnectedAircraft & { Station: Station })[] = await res.json();
- setAircrafts(data.filter((a) => checkSimulatorConnected(a)));
- } catch (error) {
- console.error("Failed to fetch aircrafts:", error);
- }
- };
-
- fetchAircrafts();
- const interval = setInterval(fetchAircrafts, 10_000);
-
- return () => clearInterval(interval);
- }, []);
+ const { data: aircrafts } = useQuery({
+ queryKey: ["connected-aircrafts", "map"],
+ queryFn: () => getConnectedAircraftsAPI(),
+ refetchInterval: 15000,
+ });
const { setMap } = useMapStore((state) => state);
const map = useMap();
const {
diff --git a/apps/dispatch/app/_components/map/_components/AircraftMarkerTabs.tsx b/apps/dispatch/app/_components/map/_components/AircraftMarkerTabs.tsx
index 97e529ab..0dae2e20 100644
--- a/apps/dispatch/app/_components/map/_components/AircraftMarkerTabs.tsx
+++ b/apps/dispatch/app/_components/map/_components/AircraftMarkerTabs.tsx
@@ -9,6 +9,7 @@ import {
Mission,
MissionLog,
MissionSdsLog,
+ MissionSdsStatusLog,
MissionStationLog,
Prisma,
PublicUser,
@@ -40,7 +41,7 @@ import {
TextSearch,
} from "lucide-react";
import { useSession } from "next-auth/react";
-import { sendSdsMessageAPI } from "_querys/missions";
+import { sendSdsMessageAPI, sendSdsStatusMessageAPI } from "_querys/missions";
import { getLivekitRooms } from "_querys/livekit";
import { findLeitstelleForPosition } from "_helpers/findLeitstelleinPoint";
import { formatDistance } from "date-fns";
@@ -54,9 +55,13 @@ const FMSStatusHistory = ({
mission?: Mission;
}) => {
const log = ((mission?.missionLog as unknown as MissionLog[]) || [])
- .filter((entry) => entry.type === "station-log" && entry.data.stationId === aircraft.Station.id)
+ .filter(
+ (entry) =>
+ (entry.type === "station-log" || entry.type == "sds-status-log") &&
+ entry.data.stationId === aircraft.Station.id,
+ )
.reverse()
- .splice(0, 6) as MissionStationLog[];
+ .splice(0, 6) as (MissionStationLog | MissionSdsStatusLog)[];
const aircraftUser: PublicUser =
typeof aircraft.publicUser === "string" ? JSON.parse(aircraft.publicUser) : aircraft.publicUser;
@@ -103,10 +108,13 @@ const FMSStatusHistory = ({
- {entry.data.newFMSstatus}
+ {entry.type === "sds-status-log" ? entry.data.status : entry.data.newFMSstatus}
{new Date(entry.timeStamp).toLocaleTimeString([], {
@@ -126,6 +134,7 @@ const FMSStatusSelector = ({
}: {
aircraft: ConnectedAircraft & { Station: Station };
}) => {
+ const session = useSession();
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
const [hoveredStatus, setHoveredStatus] = useState(null);
const queryClient = useQueryClient();
@@ -144,6 +153,20 @@ const FMSStatusSelector = ({
},
});
+ const sendSdsStatusMutation = useMutation({
+ mutationFn: async ({ sdsMessage }: { sdsMessage: MissionSdsStatusLog }) => {
+ if (!aircraft?.id) throw new Error("No connected aircraft");
+ await sendSdsStatusMessageAPI({ sdsMessage, aircraftId: aircraft?.id });
+ queryClient.invalidateQueries({
+ queryKey: ["missions"],
+ });
+ },
+ });
+
+ if (!session.data?.user) {
+ return null;
+ }
+
return (
@@ -213,12 +236,21 @@ const FMSStatusSelector = ({
onMouseEnter={() => setHoveredStatus(status)}
onMouseLeave={() => setHoveredStatus(null)}
onClick={async () => {
- await changeAircraftMutation.mutateAsync({
- id: aircraft.id,
- update: {
- fmsStatus: status,
+ await sendSdsStatusMutation.mutateAsync({
+ sdsMessage: {
+ type: "sds-status-log",
+ auto: false,
+ timeStamp: new Date().toISOString(),
+ data: {
+ status: status,
+ direction: "to-aircraft",
+ stationId: aircraft.Station.id,
+ station: aircraft.Station,
+ user: getPublicUser(session.data?.user),
+ },
},
});
+ toast.success(`SDS Status ${status} gesendet`);
}}
>
{status}
@@ -378,7 +410,9 @@ const SDSTab = ({
?.slice()
.reverse()
.filter(
- (entry) => entry.type === "sds-log" && entry.data.stationId === aircraft.Station.id,
+ (entry) =>
+ (entry.type === "sds-log" || entry.type == "sds-status-log") &&
+ entry.data.stationId === aircraft.Station.id,
) || [],
[mission?.missionLog, aircraft.Station.id],
);
@@ -471,7 +505,7 @@ const SDSTab = ({
)}
{log.map((entry, index) => {
- const sdsEntry = entry as MissionSdsLog;
+ const sdsEntry = entry as MissionSdsLog | MissionSdsStatusLog;
return (
-
@@ -489,7 +523,9 @@ const SDSTab = ({
{sdsEntry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}
{sdsEntry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}
- {sdsEntry.data.message}
+
+ {sdsEntry.type == "sds-log" ? sdsEntry.data.message : sdsEntry.data.status}
+
);
})}
diff --git a/apps/dispatch/app/_components/map/_components/MissionMarkerTabs.tsx b/apps/dispatch/app/_components/map/_components/MissionMarkerTabs.tsx
index dcb042e1..a02ef87c 100644
--- a/apps/dispatch/app/_components/map/_components/MissionMarkerTabs.tsx
+++ b/apps/dispatch/app/_components/map/_components/MissionMarkerTabs.tsx
@@ -726,7 +726,11 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
{entry.data.station.bosCallsign}
);
- if (entry.type === "message-log" || entry.type === "sds-log")
+ if (
+ entry.type === "message-log" ||
+ entry.type === "sds-log" ||
+ entry.type === "sds-status-log"
+ )
return (
-
@@ -741,9 +745,10 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
color: FMS_STATUS_TEXT_COLORS[6],
}}
>
- {entry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}
- {entry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}
- {entry.type === "sds-log" && (
+ {entry.type == "sds-status-log" && entry.data.direction == "to-lst"
+ ? entry.data.station.bosCallsignShort
+ : `${entry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}${entry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}`}
+ {(entry.type === "sds-log" || entry.type === "sds-status-log") && (
<>
- {entry.data.station.bosCallsignShort}
+ {entry.type == "sds-status-log" && entry.data.direction == "to-aircraft"
+ ? entry.data.station.bosCallsignShort
+ : "LST"}
>
)}
- {entry.data.message}
+
+ {entry.type === "sds-log" || entry.type === "message-log"
+ ? entry.data.message
+ : entry.data.status}
+
);
if (
diff --git a/apps/dispatch/app/_data/fmsStatusDescription.ts b/apps/dispatch/app/_data/fmsStatusDescription.ts
index 3ca69b0c..c83c4551 100644
--- a/apps/dispatch/app/_data/fmsStatusDescription.ts
+++ b/apps/dispatch/app/_data/fmsStatusDescription.ts
@@ -24,3 +24,30 @@ export const fmsStatusDescription: { [key: string]: string } = {
o: "Warten, alle Abfrageplätze belegt",
u: "Verstanden",
};
+
+export const fmsStatusDescriptionShort: { [key: string]: string } = {
+ NaN: "Keine D.",
+ "0": "Prio. Sprechen",
+ "1": "E.-bereit Funk",
+ "2": "E.-bereit Wache",
+ "3": "E.-übernahme",
+ "4": "Einsatzort",
+ "5": "Sprechwunsch",
+ "6": "Nicht e.-bereit",
+ "7": "Einsatzgeb.",
+ "8": "Bed. Verfügbar",
+ "9": "F-anmeldung",
+ E: "Einsatzabbruch",
+ C: "Melden Sie Einsatzübernahme",
+ F: "Kommen Sie über Draht",
+ H: "Fahren Sie Wache an",
+ J: "Sprechen Sie",
+ L: "Geben Sie Lagemeldung",
+ P: "Einsatz mit Polizei",
+ U: "Ungültige Statusfolge",
+ c: "Status korrigieren",
+ d: "Nennen Sie Transportziel",
+ h: "Zielklinik verständigt",
+ o: "Warten, alle Abfrageplätze belegt",
+ u: "Verstanden",
+};
diff --git a/apps/dispatch/app/_data/livekitRooms.ts b/apps/dispatch/app/_data/livekitRooms.ts
index 1bbb1ae0..f98a61aa 100644
--- a/apps/dispatch/app/_data/livekitRooms.ts
+++ b/apps/dispatch/app/_data/livekitRooms.ts
@@ -1 +1,7 @@
-export const ROOMS = ["LST_01", "LST_02", "LST_03", "LST_04", "LST_05"];
+export const ROOMS = [
+ { name: "VAR_LST_RD_01", id: "2201" },
+ { name: "VAR_LST_RD_02", id: "2202" },
+ { name: "VAR_LST_RD_03", id: "2203" },
+ { name: "VAR_LST_RD_04", id: "2204" },
+ { name: "VAR_LST_RD_05", id: "2205" },
+];
diff --git a/apps/dispatch/app/_querys/missions.ts b/apps/dispatch/app/_querys/missions.ts
index 240591c1..bab144c4 100644
--- a/apps/dispatch/app/_querys/missions.ts
+++ b/apps/dispatch/app/_querys/missions.ts
@@ -1,4 +1,4 @@
-import { Mission, MissionSdsLog, Prisma } from "@repo/db";
+import { Mission, MissionSdsLog, MissionSdsStatusLog, Prisma } from "@repo/db";
import axios from "axios";
import { serverApi } from "_helpers/axios";
@@ -29,6 +29,20 @@ export const editMissionAPI = async (id: number, mission: Prisma.MissionUpdateIn
const respone = await serverApi.patch(`/mission/${id}`, mission);
return respone.data;
};
+
+export const sendSdsStatusMessageAPI = async ({
+ sdsMessage,
+ aircraftId,
+}: {
+ aircraftId: number;
+ sdsMessage: MissionSdsStatusLog;
+}) => {
+ const respone = await serverApi.post(`/aircrafts/${aircraftId}/send-sds-message`, {
+ sdsMessage,
+ });
+ return respone.data;
+};
+
export const sendSdsMessageAPI = async ({
missionId,
sdsMessage,
diff --git a/apps/dispatch/app/_store/audioStore.ts b/apps/dispatch/app/_store/audioStore.ts
index 7ce25c64..714264b2 100644
--- a/apps/dispatch/app/_store/audioStore.ts
+++ b/apps/dispatch/app/_store/audioStore.ts
@@ -21,12 +21,15 @@ import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { changeDispatcherAPI } from "_querys/dispatcher";
import { getRadioStream } from "_helpers/radioEffect";
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
+import { ROOMS } from "_data/livekitRooms";
let interval: NodeJS.Timeout;
+const connectedSound = new Audio("/sounds/403.wav");
+
type TalkState = {
addSpeakingParticipant: (participant: Participant) => void;
- connect: (roomName: string, role: string) => void;
+ connect: (room: (typeof ROOMS)[number] | undefined, role: string) => void;
connectionQuality: ConnectionQuality;
disconnect: () => void;
isTalking: boolean;
@@ -44,6 +47,8 @@ type TalkState = {
radioVolume: number;
dmeVolume: number;
};
+ selectedRoom?: (typeof ROOMS)[number];
+ setSelectedRoom: (room: (typeof ROOMS)[number]) => void;
speakingParticipants: Participant[];
state: "connecting" | "connected" | "disconnected" | "error";
toggleTalking: () => void;
@@ -72,6 +77,10 @@ export const useAudioStore = create((set, get) => ({
remoteParticipants: 0,
connectionQuality: ConnectionQuality.Unknown,
room: null,
+ selectedRoom: ROOMS[0],
+ setSelectedRoom: (room) => {
+ set({ selectedRoom: room });
+ },
resetSpeakingParticipants: (source: string) => {
set({
speakingParticipants: [],
@@ -117,11 +126,11 @@ export const useAudioStore = create((set, get) => ({
(oldSettings.micDeviceId !== newSettings.micDeviceId ||
oldSettings.micVolume !== newSettings.micVolume)
) {
- const { room, disconnect, connect } = get();
+ const { room, disconnect, connect, selectedRoom } = get();
const role = room?.localParticipant.attributes.role;
- if (room?.name || role) {
+ if (selectedRoom || role) {
disconnect();
- connect(room?.name || "", role || "user");
+ connect(selectedRoom, role || "user");
}
}
},
@@ -160,7 +169,7 @@ export const useAudioStore = create((set, get) => ({
set((state) => ({ isTalking: !state.isTalking, transmitBlocked: false }));
},
- connect: async (roomName, role) => {
+ connect: async (_room, role) => {
set({ state: "connecting" });
try {
@@ -172,10 +181,12 @@ export const useAudioStore = create((set, get) => ({
connectedRoom.removeAllListeners();
}
+ const { selectedRoom } = get();
+
const url = process.env.NEXT_PUBLIC_LIVEKIT_URL;
if (!url) return console.error("NEXT_PUBLIC_LIVEKIT_URL not set");
- const token = await getToken(roomName);
+ const token = await getToken(_room?.name || selectedRoom?.name || "VAR_LST_RD_01");
if (!token) throw new Error("Fehlende Berechtigung");
const room = new Room({});
await room.prepareConnection(url, token);
@@ -186,7 +197,7 @@ export const useAudioStore = create((set, get) => ({
if (dispatchState.status === "connected" && dispatchState.connectedDispatcher?.id) {
changeDispatcherAPI(dispatchState.connectedDispatcher?.id, {
- zone: roomName,
+ zone: _room?.name || selectedRoom?.name || "VAR_LST_RD_01",
ghostMode: dispatchState.ghostMode,
});
}
@@ -208,7 +219,7 @@ export const useAudioStore = create((set, get) => ({
source: Track.Source.Microphone,
});
await publishedTrack.mute();
-
+ connectedSound.play().catch((e) => console.error("Fehler beim Abspielen des Sounds", e));
set({ localRadioTrack: publishedTrack });
set({ state: "connected", room, isTalking: false, message: null });
diff --git a/apps/dispatch/app/_store/dispatch/connectionStore.ts b/apps/dispatch/app/_store/dispatch/connectionStore.ts
index c7a646aa..9663e473 100644
--- a/apps/dispatch/app/_store/dispatch/connectionStore.ts
+++ b/apps/dispatch/app/_store/dispatch/connectionStore.ts
@@ -27,7 +27,7 @@ export const useDispatchConnectionStore = create((set) => ({
setHideDraftMissions: (hide) => set({ hideDraftMissions: hide }),
connectedDispatcher: null,
message: "",
- selectedZone: "LST_01",
+ selectedZone: "VAR_LST_RD_01",
logoffTime: "",
ghostMode: false,
connect: async (uid, selectedZone, logoffTime, ghostMode) =>
@@ -48,7 +48,7 @@ export const useDispatchConnectionStore = create((set) => ({
dispatchSocket.on("connect", () => {
const { logoffTime, selectedZone, ghostMode } = useDispatchConnectionStore.getState();
- useAudioStore.getState().connect("LST_01", selectedZone || "Leitstelle");
+ useAudioStore.getState().connect(undefined, selectedZone || "Leitstelle");
dispatchSocket.emit("connect-dispatch", {
logoffTime,
selectedZone,
diff --git a/apps/dispatch/app/_store/pilot/MrtStore.ts b/apps/dispatch/app/_store/pilot/MrtStore.ts
index f5d32dc6..f8432402 100644
--- a/apps/dispatch/app/_store/pilot/MrtStore.ts
+++ b/apps/dispatch/app/_store/pilot/MrtStore.ts
@@ -1,165 +1,92 @@
-import { MissionSdsLog, Station } from "@repo/db";
-import { fmsStatusDescription } from "_data/fmsStatusDescription";
-import { DisplayLineProps } from "(app)/pilot/_components/mrt/Mrt";
import { create } from "zustand";
-interface SetSdsPageParams {
- page: "sds";
- station: Station;
- sdsMessage: MissionSdsLog;
+interface SetOffPageParams {
+ page: "off";
+}
+
+interface SetStartupPageParams {
+ page: "startup";
}
interface SetHomePageParams {
page: "home";
- station: Station;
- fmsStatus: string;
}
-interface SetSendingStatusPageParams {
- page: "sending-status";
- station: Station;
+interface SetVoicecallPageParams {
+ page: "voice-call";
+}
+interface SetSdsReceivedPopupParams {
+ popup: "sds-received";
}
-interface SetNewStatusPageParams {
- page: "new-status";
- station: Station;
+interface SetGroupSelectionPopupParams {
+ popup: "group-selection";
}
-type SetPageParams =
+interface SetStatusSentPopupParams {
+ popup: "status-sent";
+}
+
+interface SetLoginPopupParams {
+ popup: "login";
+}
+
+interface SetSdsSentPopupParams {
+ popup: "sds-sent";
+}
+
+export type SetPageParams =
| SetHomePageParams
- | SetSendingStatusPageParams
- | SetSdsPageParams
- | SetNewStatusPageParams;
+ | SetOffPageParams
+ | SetStartupPageParams
+ | SetVoicecallPageParams;
+
+export type SetPopupParams =
+ | SetStatusSentPopupParams
+ | SetSdsSentPopupParams
+ | SetGroupSelectionPopupParams
+ | SetSdsReceivedPopupParams
+ | SetLoginPopupParams;
+
+interface StringifiedData {
+ sdsText?: string;
+ sentSdsText?: string;
+
+ groupSelectionGroupId?: string;
+ callTextHeader?: string;
+}
interface MrtStore {
page: SetPageParams["page"];
+ popup?: SetPopupParams["popup"];
- lines: DisplayLineProps[];
+ stringifiedData: StringifiedData;
+ setStringifiedData: (data: Partial) => void;
setPage: (pageData: SetPageParams) => void;
- setLines: (lines: MrtStore["lines"]) => void;
+ setPopup: (popupData: SetPopupParams | null) => void;
+
+ // internal
+ updateIntervall?: number;
+ nightMode: boolean;
+ setNightMode: (nightMode: boolean) => void;
}
export const useMrtStore = create((set) => ({
- page: "home",
- pageData: {
- message: "",
+ page: "off",
+ nightMode: false,
+ stringifiedData: {
+ groupSelectionGroupId: "2201",
+ },
+ setNightMode: (nightMode) => set({ nightMode }),
+ setStringifiedData: (data) =>
+ set((state) => ({
+ stringifiedData: { ...state.stringifiedData, ...data },
+ })),
+ setPopup: (popupData) => {
+ set({ popup: popupData ? popupData.popup : undefined });
},
- lines: [
- {
- textLeft: "VAR.#",
- textSize: "2",
- },
- {
- textLeft: "No Data",
- textSize: "3",
- },
- ],
- setLines: (lines) => set({ lines }),
setPage: (pageData) => {
- switch (pageData.page) {
- case "home": {
- const { station, fmsStatus } = pageData as SetHomePageParams;
- set({
- page: "home",
- lines: [
- {
- textLeft: `${station?.bosCallsign}`,
- style: { fontWeight: "bold" },
- textSize: "2",
- },
- { textLeft: "ILS VAR#", textSize: "3" },
- {
- textLeft: fmsStatus,
- style: { fontWeight: "extrabold" },
- textSize: "4",
- },
- {
- textLeft: fmsStatusDescription[fmsStatus],
- textSize: "1",
- },
- ],
- });
- break;
- }
-
- case "sending-status": {
- const { station } = pageData as SetSendingStatusPageParams;
- set({
- page: "sending-status",
- lines: [
- {
- textLeft: `${station?.bosCallsign}`,
- style: { fontWeight: "bold" },
- textSize: "2",
- },
- { textLeft: "ILS VAR#", textSize: "3" },
- {
- textMid: "sending...",
- style: { fontWeight: "bold" },
- textSize: "4",
- },
- {
- textLeft: "Status wird gesendet...",
- textSize: "1",
- },
- ],
- });
- break;
- }
- case "new-status": {
- const { station } = pageData as SetNewStatusPageParams;
- set({
- page: "new-status",
- lines: [
- {
- textLeft: `${station?.bosCallsign}`,
- style: { fontWeight: "bold" },
- textSize: "2",
- },
- { textLeft: "ILS VAR#", textSize: "3" },
- {
- textLeft: "empfangen",
- style: { fontWeight: "bold" },
- textSize: "4",
- },
- ],
- });
- break;
- }
- case "sds": {
- const { sdsMessage } = pageData as SetSdsPageParams;
- const msg = sdsMessage.data.message;
- set({
- page: "sds",
- lines: [
- {
- textLeft: `SDS-Nachricht`,
- style: { fontWeight: "bold" },
- textSize: "2",
- },
- {
- textLeft: msg,
- style: {
- whiteSpace: "normal",
- overflowWrap: "break-word",
- wordBreak: "break-word",
- display: "block",
- maxWidth: "100%",
- maxHeight: "100%",
- overflow: "auto",
- textOverflow: "ellipsis",
- lineHeight: "1.2em",
- },
- textSize: "2",
- },
- ],
- });
- break;
- }
- default:
- set({ page: "home" });
- break;
- }
+ set({ page: pageData.page });
},
}));
diff --git a/apps/dispatch/app/_store/pilot/connectionStore.ts b/apps/dispatch/app/_store/pilot/connectionStore.ts
index e9fc29f8..1ca44645 100644
--- a/apps/dispatch/app/_store/pilot/connectionStore.ts
+++ b/apps/dispatch/app/_store/pilot/connectionStore.ts
@@ -86,7 +86,7 @@ pilotSocket.on("connect", () => {
usePilotConnectionStore.setState({ status: "connected", message: "" });
const { logoffTime, selectedStation, debug } = usePilotConnectionStore.getState();
dispatchSocket.disconnect();
- useAudioStore.getState().connect("LST_01", selectedStation?.bosCallsignShort || "pilot");
+ useAudioStore.getState().connect(undefined, selectedStation?.bosCallsignShort || "pilot");
pilotSocket.emit("connect-pilot", {
logoffTime,
@@ -109,7 +109,7 @@ pilotSocket.on("connect-message", (data) => {
});
pilotSocket.on("disconnect", () => {
- usePilotConnectionStore.setState({ status: "disconnected" });
+ usePilotConnectionStore.setState({ status: "disconnected", connectedAircraft: null });
useAudioStore.getState().disconnect();
});
@@ -142,11 +142,13 @@ pilotSocket.on("mission-alert", (data: Mission & { Stations: Station[] }) => {
});
pilotSocket.on("sds-message", (sdsMessage: MissionSdsLog) => {
+ console.log("Received sds-message via socket:", sdsMessage);
const station = usePilotConnectionStore.getState().selectedStation;
if (!station) return;
- useMrtStore.getState().setPage({
- page: "sds",
- station,
- sdsMessage,
+ useMrtStore.getState().setPopup({
+ popup: "sds-received",
+ });
+ useMrtStore.getState().setStringifiedData({
+ sdsText: sdsMessage.data.message,
});
});
diff --git a/apps/dispatch/app/globals.css b/apps/dispatch/app/globals.css
index 876c3519..979f4a4d 100644
--- a/apps/dispatch/app/globals.css
+++ b/apps/dispatch/app/globals.css
@@ -9,6 +9,11 @@
src: url("/fonts/MelderV2.ttf") format("truetype"); /* Chrome 4+, Firefox 3.5, Opera 10+, Safari 3—5 */
}
+@font-face {
+ font-family: "Bahnschrift";
+ src: url("/fonts/bahnschrift.ttf") format("truetype"); /* Chrome 4+, Firefox 3.5, Opera 10+, Safari 3—5 */
+}
+
@theme {
--color-rescuetrack: #46b7a3;
--color-rescuetrack-highlight: #ff4500;
diff --git a/apps/dispatch/next-env.d.ts b/apps/dispatch/next-env.d.ts
index 9edff1c7..c4b7818f 100644
--- a/apps/dispatch/next-env.d.ts
+++ b/apps/dispatch/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/types/routes.d.ts";
+import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/dispatch/public/fonts/bahnschrift.ttf b/apps/dispatch/public/fonts/bahnschrift.ttf
new file mode 100644
index 00000000..2c121b3d
Binary files /dev/null and b/apps/dispatch/public/fonts/bahnschrift.ttf differ
diff --git a/apps/dispatch/public/sounds/1504.wav b/apps/dispatch/public/sounds/1504.wav
new file mode 100644
index 00000000..edde45b8
Binary files /dev/null and b/apps/dispatch/public/sounds/1504.wav differ
diff --git a/apps/dispatch/public/sounds/403.wav b/apps/dispatch/public/sounds/403.wav
new file mode 100644
index 00000000..3d855dc6
Binary files /dev/null and b/apps/dispatch/public/sounds/403.wav differ
diff --git a/apps/dispatch/public/sounds/775.wav b/apps/dispatch/public/sounds/775.wav
new file mode 100644
index 00000000..bb406ab5
Binary files /dev/null and b/apps/dispatch/public/sounds/775.wav differ
diff --git a/apps/hub/Dockerfile b/apps/hub/Dockerfile
index 7bf8906e..6c5c55af 100644
--- a/apps/hub/Dockerfile
+++ b/apps/hub/Dockerfile
@@ -67,7 +67,4 @@ USER nextjs
# Expose the application port
EXPOSE 3000
-ENV PORT=3000
-ENV HOSTNAME="0.0.0.0"
-
CMD ["node", "apps/hub/server.js"]
\ No newline at end of file
diff --git a/packages/database/prisma/json/MissionVehicleLog.ts b/packages/database/prisma/json/MissionVehicleLog.ts
index 7bceda67..c7de840c 100644
--- a/packages/database/prisma/json/MissionVehicleLog.ts
+++ b/packages/database/prisma/json/MissionVehicleLog.ts
@@ -1,4 +1,5 @@
import { Station } from "../../generated/client";
+import { StationStatus } from "./SocketEvents";
import { PublicUser } from "./User";
export interface MissionVehicleLog {
@@ -37,6 +38,19 @@ export interface MissionSdsLog {
};
}
+export interface MissionSdsStatusLog {
+ type: "sds-status-log";
+ auto: false;
+ timeStamp: string;
+ data: {
+ direction: "to-lst" | "to-aircraft";
+ stationId: number;
+ station: Station;
+ user: PublicUser;
+ status: string;
+ };
+}
+
export interface MissionMessageLog {
type: "message-log";
auto: false;
@@ -90,4 +104,5 @@ export type MissionLog =
| MissionAlertLogAuto
| MissionCompletedLog
| MissionVehicleLog
- | MissionReopenedLog;
+ | MissionReopenedLog
+ | MissionSdsStatusLog;