added mrt, dev

This commit is contained in:
PxlLoewe
2025-05-18 23:21:08 -07:00
parent 6b58f564b2
commit 1b16b365bd
25 changed files with 514 additions and 5 deletions

View File

@@ -48,6 +48,10 @@ router.patch("/:id", async (req, res) => {
"update-connectedAircraft",
updatedConnectedAircraft,
);
io.to(`user:${updatedConnectedAircraft.userId}`).emit(
"aircraft-update",
updatedConnectedAircraft,
);
res.json(updatedConnectedAircraft);
} catch (error) {
console.error(error);

View File

@@ -76,6 +76,11 @@ export const handleConnectPilot =
io.to("dispatchers").emit("pilots-update");
io.to("pilots").emit("pilots-update");
io.to(`user:${connectedAircraftEntry.userId}`).emit(
"aircraft-update",
connectedAircraftEntry,
);
// Add a listener for station-specific events
socket.on(`station:${stationId}:event`, async (data) => {
console.log(`Received event for station ${stationId}:`, data);

View File

@@ -0,0 +1,46 @@
import { DisplayLineProps } from "pilot/_components/mrt/Mrt";
import { create } from "zustand";
import { syncTabs } from "zustand-sync-tabs";
type Page = "home" | "sending-status" | "new-status" | "error";
type PageData = {
home: undefined;
"sending-status": undefined;
"new-status": undefined;
error: {
message: string;
};
};
interface MrtStore {
page: Page;
pageData: PageData[Page];
lines: DisplayLineProps[];
setPage: <P extends Page>(page: P, pageData?: PageData[P]) => void;
setLines: (lines: MrtStore["lines"]) => void;
}
export const useMrtStore = create<MrtStore>(
syncTabs(
(set) => ({
page: "home",
pageData: {
message: "",
},
lines: Array.from(Array(10).keys()).map(() => ({
textLeft: "",
textMid: "",
textRight: "",
textSize: "1",
})),
setLines: (lines) => set({ lines }),
setPage: (page, pageData) => set({ page, pageData }),
}),
{
name: "mrt-store", // unique name
},
),
);

View File

@@ -1,12 +1,14 @@
import { create } from "zustand";
import { dispatchSocket } from "../../dispatch/socket";
import { Mission, Station } from "@repo/db";
import { ConnectedAircraft, Mission, Station } from "@repo/db";
import { pilotSocket } from "pilot/socket";
import { useMrtStore } from "_store/pilot/MrtStore";
interface ConnectionStore {
status: "connected" | "disconnected" | "connecting" | "error";
message: string;
selectedStation: Station | null;
connectedAircraft: ConnectedAircraft | null;
activeMission:
| (Mission & {
Stations: Station[];
@@ -25,6 +27,7 @@ export const usePilotConnectionStore = create<ConnectionStore>((set) => ({
status: "disconnected",
message: "",
selectedStation: null,
connectedAircraft: null,
activeMission: null,
connect: async (uid, stationId, logoffTime, station) =>
new Promise((resolve) => {
@@ -68,6 +71,13 @@ pilotSocket.on("force-disconnect", (reason: string) => {
});
});
pilotSocket.on("aircraft-update", (data) => {
usePilotConnectionStore.setState({
connectedAircraft: data,
});
useMrtStore.getState().setLines(getNew);
});
pilotSocket.on("mission-alert", (data) => {
usePilotConnectionStore.setState({
activeMission: data,

View File

@@ -3,6 +3,11 @@
themes: dark, nord;
}
@font-face {
font-family: "Melder";
src: url("/fonts/MelderV2.ttf") format("truetype"); /* Chrome 4+, Firefox 3.5, Opera 10+, Safari 3—5 */
}
@theme {
--color-rescuetrack: #46b7a3;
--color-rescuetrack-highlight: #ff4500;

View File

@@ -4,7 +4,6 @@ import "./globals.css";
import { NextAuthSessionProvider } from "./_components/AuthSessionProvider";
import { getServerSession } from "./api/auth/[...nextauth]/auth";
import { Toaster } from "react-hot-toast";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { QueryProvider } from "_components/QueryProvider";
const geistSans = localFont({

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

@@ -0,0 +1,183 @@
import { CSSProperties } from "react";
import MrtImage from "./MRT.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,
};
export interface DisplayLineProps {
style?: CSSProperties;
textLeft?: string;
textMid?: string;
textRight?: string;
textSize: "1" | "2" | "3" | "4";
}
const DisplayLine = ({
style = {},
textLeft,
textMid,
textRight,
textSize,
}: DisplayLineProps) => {
const INNER_TEXT_PARTS: CSSProperties = {
fontFamily: "Melder",
flex: "1",
flexBasis: "auto",
overflowWrap: "break-word",
};
return (
<div
className={`text-${textSize}`}
style={{
fontFamily: "Famirids",
display: "flex",
flexWrap: "wrap",
...style,
}}
>
<span style={INNER_TEXT_PARTS}>{textLeft}</span>
<span style={{ textAlign: "center", ...INNER_TEXT_PARTS }}>
{textMid}
</span>
<span style={{ textAlign: "end", ...INNER_TEXT_PARTS }}>{textRight}</span>
</div>
);
};
export const Mrt = () => {
useSounds();
const { handleButton } = useButtons();
const lines = useMrtStore((state) => state.lines);
return (
<div
id="mrt-container"
style={{
containerName: "mrtContainer",
display: "grid",
aspectRatio: "1466 / 760",
height: "auto",
width: "auto",
maxHeight: "100%",
maxWidth: "100%",
overflow: "hidden",
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%",
}}
>
<Image
src={MrtImage}
alt="MrtImage"
style={{
zIndex: 0,
height: "100%",
width: "100%",
gridArea: "1 / 1 / 13 / 13",
}}
/>
<button
onClick={handleButton("1")}
style={{ gridArea: "2 / 5 / 3 / 6", ...MRT_BUTTON_STYLES }}
/>
<button
onClick={handleButton("2")}
style={{ gridArea: "2 / 7 / 3 / 7", ...MRT_BUTTON_STYLES }}
/>
<button
onClick={handleButton("3")}
style={{ gridArea: "2 / 9 / 3 / 10", ...MRT_BUTTON_STYLES }}
/>
<button
onClick={handleButton("4")}
style={{ gridArea: "4 / 5 / 6 / 6", ...MRT_BUTTON_STYLES }}
/>
<button
onClick={handleButton("5")}
style={{ gridArea: "4 / 7 / 6 / 7", ...MRT_BUTTON_STYLES }}
/>
<button
onClick={handleButton("6")}
style={{ gridArea: "4 / 9 / 6 / 10", ...MRT_BUTTON_STYLES }}
/>
<button
onClick={handleButton("7")}
style={{ gridArea: "8 / 5 / 9 / 6", ...MRT_BUTTON_STYLES }}
/>
<button
onClick={handleButton("8")}
style={{ gridArea: "8 / 7 / 9 / 7", ...MRT_BUTTON_STYLES }}
/>
<button
onClick={handleButton("9")}
style={{ gridArea: "8 / 9 / 9 / 10", ...MRT_BUTTON_STYLES }}
/>
<button
onClick={handleButton("0")}
style={{ gridArea: "10 / 7 / 11 / 8", ...MRT_BUTTON_STYLES }}
/>
{lines[0] && (
<DisplayLine
{...lines[0]}
style={{
gridArea: "4 / 3 / 5 / 4",
marginLeft: "9px",
marginTop: "auto",
...MRT_DISPLAYLINE_STYLES,
...lines[0]?.style,
}}
/>
)}
{lines[1] && (
<DisplayLine
{...lines[1]}
style={{
gridArea: "5 / 3 / 7 / 4",
marginLeft: "3px",
marginTop: "auto",
...MRT_DISPLAYLINE_STYLES,
...lines[1].style,
}}
/>
)}
{lines[2] && (
<DisplayLine
{...lines[2]}
style={{
gridArea: "8 / 2 / 9 / 4",
...MRT_DISPLAYLINE_STYLES,
...lines[2]?.style,
}}
/>
)}
{lines[3] && (
<DisplayLine
{...lines[3]}
style={{
gridArea: "9 / 2 / 10 / 4",
marginRight: "10px",
...MRT_DISPLAYLINE_STYLES,
...lines[3]?.style,
}}
/>
)}
</div>
);
};

View File

@@ -0,0 +1,32 @@
#mrt-container {
container-name: mrtContainer;
}
@container mrtContainer (inline-size < 600px) {
#mrt-container .text-1 {
font-size: 12px;
color: red;
}
#mrt-container .text-2 {
font-size: 99px;
}
#mrt-container .text-3 {
font-size: 25px;
}
#mrt-container .text-4 {
font-size: 45px;
}
}
#mrt-container .text-1 {
font-size: 10px;
}
#mrt-container .text-2 {
font-size: 12px;
}
#mrt-container .text-3 {
font-size: 15px;
}
#mrt-container .text-4 {
font-size: 25px;
}

View File

@@ -0,0 +1,140 @@
import { useSession } from "next-auth/react";
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
import { useMrtStore } from "_store/pilot/MrtStore";
import { editConnectedAircraftAPI } from "querys/aircrafts";
import { Station } from "@repo/db";
import { DisplayLineProps } from "pilot/_components/mrt/Mrt";
export const fmsStatusDescription: { [key: string]: string } = {
NaN: "Keine Daten",
"0": "Prio. Sprechwunsch",
"1": "Frei auf Funk",
"2": "Einsatzbereit am LRZ",
"3": "Auf dem Weg",
"4": "Am Einsatzort",
"5": "Sprechwunsch",
"6": "Nicht einsatzbereit",
"7": "Patient aufgenommen",
"8": "Am Transportziel",
"9": "Fremdanmeldung",
E: "Indent/Abbruch/Einsatzbefehl abgebrochen",
C: "Anmelden zur Übernahme des Einsatzes",
F: "Kommen über Draht",
H: "Fahren auf Wache",
J: "Sprechaufforderung",
L: "Lagebericht abgeben",
P: "Einsatz mit Polizei/Pause machen",
U: "Ungültiger Status",
c: "Status korrigieren",
d: "Transportziel angeben",
h: "Zielklinik verständigt",
o: "Warten, alle Abfrageplätze belegt",
u: "Verstanden",
};
export const getSendingLines = (station: Station): DisplayLineProps[] => {
return [
{
textLeft: `VAR#.${station?.bosCallsign}123`,
style: { fontWeight: "bold" },
textSize: "2",
},
{ textLeft: "ILS VAR#", textSize: "3" },
{
textMid: "sending...",
style: { fontWeight: "bold" },
textSize: "4",
},
{
textLeft: "Status wird gesendet...",
textSize: "1",
},
];
};
export const getHomeLines = (
station: Station,
fmsStatus: string,
): DisplayLineProps[] => {
return [
{
textLeft: `VAR#.${station?.bosCallsign}`,
style: { fontWeight: "bold" },
textSize: "2",
},
{ textLeft: "ILS VAR#", textSize: "3" },
{
textLeft: fmsStatus,
style: { fontWeight: "extrabold" },
textSize: "4",
},
{
textLeft: fmsStatusDescription[fmsStatus],
textSize: "1",
},
];
};
export const getNewStatusLines = (
station: Station,
fmsStatus: string,
): DisplayLineProps[] => {
return [
{
textLeft: `VAR#.${station?.bosCallsign}`,
style: { fontWeight: "bold" },
textSize: "2",
},
{ textLeft: "ILS VAR#", textSize: "3" },
{
textLeft: fmsStatus,
style: { fontWeight: "extrabold" },
textSize: "4",
},
{
textLeft: fmsStatusDescription[fmsStatus],
textSize: "1",
},
];
};
export const useButtons = () => {
const user = useSession().data?.user;
const station = usePilotConnectionStore((state) => state.selectedStation);
const connectedAircraft = usePilotConnectionStore(
(state) => state.connectedAircraft,
);
const { page, setLines } = useMrtStore((state) => state);
const handleButton =
(button: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0") =>
() => {
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"
) {
if (page !== "home") return;
setLines(getSendingLines(station));
setTimeout(async () => {
await editConnectedAircraftAPI(connectedAircraft!.id, {
fmsStatus: button,
});
setLines(getNewStatusLines(station, button));
}, 1000);
}
};
return { handleButton };
};

View File

@@ -0,0 +1,66 @@
"use client";
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
import { useMrtStore } from "_store/pilot/MrtStore";
import { editConnectedAircraftAPI } from "querys/aircrafts";
import { useEffect, useRef } from "react";
const MRTstatusSound = new Audio("/sounds/MRT-status.mp3");
const MrtMessageReceivedSound = new Audio("/sounds/MRT-message-received.mp3");
export const useSounds = () => {
const mrtState = useMrtStore((state) => state);
const { connectedAircraft, selectedStation } = usePilotConnectionStore(
(state) => state,
);
const fmsStatus = connectedAircraft?.fmsStatus || "NaN";
const previousFmsStatus = useRef(fmsStatus || "6");
const timeout = useRef<NodeJS.Timeout>(null);
useEffect(() => {
const handleSoundEnd = () => {
mrtState.setPage("home");
};
const playSound = (sound: HTMLAudioElement) => {
sound.play();
sound.addEventListener("ended", handleSoundEnd);
};
if (!connectedAircraft) return;
if (mrtState.page === "new-status") {
if (fmsStatus === "J") {
playSound(MrtMessageReceivedSound);
timeout.current = setTimeout(() => {
editConnectedAircraftAPI(connectedAircraft.id, {
fmsStatus: previousFmsStatus.current,
});
}, 5000);
} else if (previousFmsStatus.current !== fmsStatus) {
playSound(MRTstatusSound);
} else {
handleSoundEnd();
}
if (!timeout.current) {
previousFmsStatus.current = fmsStatus || "6";
}
}
return () => {
if (timeout.current) clearTimeout(timeout.current);
[MRTstatusSound, MrtMessageReceivedSound].forEach((sound) => {
sound.removeEventListener("ended", handleSoundEnd);
sound.pause();
sound.currentTime = 0;
});
};
}, [
mrtState,
fmsStatus,
connectedAircraft,
selectedStation,
previousFmsStatus,
timeout,
]);
};

View File

@@ -1,5 +1,6 @@
"use client";
import { Mrt } from "pilot/_components/mrt/Mrt";
import { Chat } from "../_components/left/Chat";
import { Report } from "../_components/left/Report";
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
@@ -19,6 +20,9 @@ const DispatchPage = () => {
</div>
</div>
<div>{JSON.stringify(activeMission)}</div>
<div>
<Mrt />
</div>
</div>
);
};

View File

@@ -32,7 +32,8 @@
"react-leaflet": "^5.0.0-rc.2",
"socket.io-client": "^4.8.1",
"tailwindcss": "^4.0.14",
"zustand": "^5.0.3"
"zustand": "^5.0.3",
"zustand-sync-tabs": "^0.2.2"
},
"devDependencies": {
"@repo/eslint-config": "*",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -107,7 +107,7 @@ export const ReportAdmin = ({
</Button>
<Button
role="button"
className="btn btn-warning"
className="btn btn-sm btn-warning"
onSubmit={() => false}
onClick={async () => {
await editReport(report.id, {

Binary file not shown.

16
package-lock.json generated
View File

@@ -42,7 +42,8 @@
"react-leaflet": "^5.0.0-rc.2",
"socket.io-client": "^4.8.1",
"tailwindcss": "^4.0.14",
"zustand": "^5.0.3"
"zustand": "^5.0.3",
"zustand-sync-tabs": "^0.2.2"
},
"devDependencies": {
"@repo/eslint-config": "*",
@@ -18008,6 +18009,19 @@
}
}
},
"node_modules/zustand-sync-tabs": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/zustand-sync-tabs/-/zustand-sync-tabs-0.2.2.tgz",
"integrity": "sha512-i2pQPm5SGBIq50v7tt6XFXZSR/iJNErOslS5HzMKJVd06+xDyBCXioxEap0Ke6u98+a49fU5QRqzypo21wMe9A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/mayank1513"
},
"peerDependencies": {
"zustand": "3-5"
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",