added MRT sds image
This commit is contained in:
@@ -327,9 +327,6 @@ router.post("/:id/validate-hpg", async (req, res) => {
|
||||
res.json({
|
||||
message: "HPG validierung gestartet",
|
||||
});
|
||||
console.log(
|
||||
`HPG Validation for ${user?.publicId} (${mission?.hpgSelectedMissionString}) started`,
|
||||
);
|
||||
io.to(`desktop:${activeAircraftinMission?.userId}`).emit("hpg-validation", {
|
||||
missionId: parseInt(id),
|
||||
userId: req.user?.id,
|
||||
|
||||
@@ -55,14 +55,35 @@ export const handleConnectPilot =
|
||||
}
|
||||
}
|
||||
|
||||
// Set "now" to 2 hours in the future
|
||||
const nowPlus2h = new Date();
|
||||
nowPlus2h.setHours(nowPlus2h.getHours() + 2);
|
||||
|
||||
// Generate a random position in Germany (approximate bounding box)
|
||||
function getRandomGermanPosition() {
|
||||
const minLat = 47.2701;
|
||||
const maxLat = 55.0581;
|
||||
const minLng = 5.8663;
|
||||
const maxLng = 15.0419;
|
||||
const lat = Math.random() * (maxLat - minLat) + minLat;
|
||||
const lng = Math.random() * (maxLng - minLng) + minLng;
|
||||
return { lat, lng };
|
||||
}
|
||||
|
||||
const randomPos =
|
||||
process.env.environment === "development" ? getRandomGermanPosition() : undefined;
|
||||
|
||||
const connectedAircraftEntry = await prisma.connectedAircraft.create({
|
||||
data: {
|
||||
publicUser: getPublicUser(user) as any,
|
||||
esimatedLogoutTime: parsedLogoffDate?.toISOString() || null,
|
||||
lastHeartbeat: new Date().toISOString(),
|
||||
userId: userId,
|
||||
loginTime: new Date().toISOString(),
|
||||
loginTime: nowPlus2h.toISOString(),
|
||||
stationId: parseInt(stationId),
|
||||
lastHeartbeat:
|
||||
process.env.environment === "development" ? nowPlus2h.toISOString() : undefined,
|
||||
posLat: randomPos?.lat,
|
||||
posLng: randomPos?.lng,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import { ConnectedAircraft, Station } from "@repo/db";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getConnectedAircraftPositionLogAPI, getConnectedAircraftsAPI } from "_querys/aircrafts";
|
||||
import { getMissionsAPI } from "_querys/missions";
|
||||
import { checkSimulatorConnected } from "_helpers/simulatorConnected";
|
||||
import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_helpers/fmsStatusColors";
|
||||
|
||||
const AircraftPopupContent = ({
|
||||
@@ -384,14 +383,12 @@ export const AircraftLayer = () => {
|
||||
const { data: aircrafts } = useQuery({
|
||||
queryKey: ["aircrafts"],
|
||||
queryFn: getConnectedAircraftsAPI,
|
||||
refetchInterval: 10000,
|
||||
refetchInterval: 10_000,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{aircrafts
|
||||
?.filter((a) => checkSimulatorConnected(a.lastHeartbeat))
|
||||
?.map((aircraft) => {
|
||||
{aircrafts?.map((aircraft) => {
|
||||
return <AircraftMarker key={aircraft.id} aircraft={aircraft} />;
|
||||
})}
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_helpers/fmsStatusColors";
|
||||
import {
|
||||
ConnectedAircraft,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Station,
|
||||
} from "@repo/db";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { editConnectedAircraftAPI } from "_querys/aircrafts";
|
||||
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
||||
import { cn } from "_helpers/cn";
|
||||
@@ -39,7 +39,9 @@ import {
|
||||
TextSearch,
|
||||
} from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { editMissionAPI, sendSdsMessageAPI } from "_querys/missions";
|
||||
import { sendSdsMessageAPI } from "_querys/missions";
|
||||
import { getLivekitRooms } from "_querys/livekit";
|
||||
import { findLeitstelleForPosition } from "_helpers/findLeitstelleinPoint";
|
||||
|
||||
const FMSStatusHistory = ({
|
||||
aircraft,
|
||||
@@ -216,14 +218,35 @@ const RettungsmittelTab = ({
|
||||
aircraft: ConnectedAircraft & { Station: Station };
|
||||
}) => {
|
||||
const station = aircraft.Station;
|
||||
const { data: livekitRooms } = useQuery({
|
||||
queryKey: ["livekit-rooms"],
|
||||
queryFn: () => getLivekitRooms(),
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
|
||||
const participants =
|
||||
livekitRooms?.flatMap((room) =>
|
||||
room.participants.map((p) => ({
|
||||
...p,
|
||||
roomName: room.room.name,
|
||||
})),
|
||||
) || [];
|
||||
|
||||
const livekitUser = participants.find((p) => (p.attributes.userId = aircraft.userId));
|
||||
|
||||
const lstName = useMemo(() => {
|
||||
if (!aircraft.posLng || !aircraft.posLat) return;
|
||||
return findLeitstelleForPosition(aircraft.posLng, aircraft.posLat);
|
||||
}, [aircraft]);
|
||||
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
<ul className="text-base-content font-semibold">
|
||||
<li className="flex items-center gap-2 mb-1">
|
||||
<Component size={16} /> Aktuelle Rufgruppe: LST_01
|
||||
<Component size={16} /> Aktuelle Rufgruppe: {livekitUser?.roomName || "Nicht verbunden"}
|
||||
</li>
|
||||
<li className="flex items-center gap-2 mb-1">
|
||||
<RadioTower size={16} /> Leitstellenbereich: Florian Berlin
|
||||
<RadioTower size={16} /> Leitstellenbereich: {lstName || station.bosRadioArea}
|
||||
</li>
|
||||
</ul>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
@@ -348,7 +371,7 @@ const SDSTab = ({
|
||||
onClick={() => setIsChatOpen(true)}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<Plus size={18} /> Notiz hinzufügen
|
||||
<Plus size={18} /> SDS senden
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
@@ -379,6 +402,7 @@ const SDSTab = ({
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("SDS-Nachricht gesendet");
|
||||
setIsChatOpen(false);
|
||||
setNote("");
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useMapStore } from "_store/mapStore";
|
||||
import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_helpers/fmsStatusColors";
|
||||
import { MISSION_STATUS_COLORS, MISSION_STATUS_TEXT_COLORS } from "_components/map/MissionMarkers";
|
||||
import { cn } from "_helpers/cn";
|
||||
import { checkSimulatorConnected } from "_helpers/simulatorConnected";
|
||||
import { getConnectedAircraftsAPI } from "_querys/aircrafts";
|
||||
import { getMissionsAPI } from "_querys/missions";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
@@ -96,9 +95,7 @@ const PopupContent = ({
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{aircrafts
|
||||
.filter((a) => checkSimulatorConnected(a.lastHeartbeat))
|
||||
.map((aircraft) => (
|
||||
{aircrafts.map((aircraft) => (
|
||||
<div
|
||||
key={aircraft.id}
|
||||
className="relative w-auto inline-flex items-center gap-2 text-nowrap cursor-pointer"
|
||||
@@ -141,6 +138,7 @@ export const MarkerCluster = () => {
|
||||
const { data: aircrafts } = useQuery({
|
||||
queryKey: ["aircrafts"],
|
||||
queryFn: getConnectedAircraftsAPI,
|
||||
refetchInterval: 10_000,
|
||||
});
|
||||
|
||||
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
|
||||
@@ -178,9 +176,7 @@ export const MarkerCluster = () => {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}[] = [];
|
||||
aircrafts
|
||||
?.filter((a) => checkSimulatorConnected(a.lastHeartbeat))
|
||||
.forEach((aircraft) => {
|
||||
aircrafts?.forEach((aircraft) => {
|
||||
const lat = aircraft.posLat!;
|
||||
const lng = aircraft.posLng!;
|
||||
|
||||
|
||||
@@ -35,14 +35,14 @@ export default function AdminPanel() {
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
const { data: livekitRooms } = useQuery({
|
||||
queryKey: ["connected-audio-users"],
|
||||
queryKey: ["livekit-rooms"],
|
||||
queryFn: () => getLivekitRooms(),
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
const kickLivekitParticipantMutation = useMutation({
|
||||
mutationFn: kickLivekitParticipant,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["connected-audio-users"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["livekit-rooms"] });
|
||||
},
|
||||
});
|
||||
const editUSerMutation = useMutation({
|
||||
@@ -95,8 +95,6 @@ export default function AdminPanel() {
|
||||
return !pilot && !fDispatcher;
|
||||
});
|
||||
|
||||
console.log("Livekit Rooms", livekitRooms);
|
||||
|
||||
const modalRef = useRef<HTMLDialogElement>(null);
|
||||
|
||||
return (
|
||||
|
||||
18
apps/dispatch/app/_helpers/findLeitstelleinPoint.ts
Normal file
18
apps/dispatch/app/_helpers/findLeitstelleinPoint.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { point, multiPolygon, booleanPointInPolygon } from "@turf/turf";
|
||||
import leitstellenGeoJSON from "../_components/map/_geojson/Leitstellen.json"; // Pfad anpassen
|
||||
|
||||
export function findLeitstelleForPosition(lat: number, lng: number) {
|
||||
const heliPoint = point([lat, lng]);
|
||||
|
||||
for (const feature of (leitstellenGeoJSON as any).features) {
|
||||
if (feature.geometry.type === "MultiPolygon") {
|
||||
const polygon = multiPolygon(feature.geometry.coordinates);
|
||||
if (booleanPointInPolygon(heliPoint, polygon)) {
|
||||
console.log("Point is inside polygon:", feature.properties.name);
|
||||
return feature.properties.name ?? "Unbenannte Leitstelle";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // Keine passende Leitstelle gefunden
|
||||
}
|
||||
@@ -1,2 +1,7 @@
|
||||
export const checkSimulatorConnected = (date: Date) =>
|
||||
date && Date.now() - new Date(date).getTime() <= 3000_000;
|
||||
import { ConnectedAircraft } from "@repo/db";
|
||||
|
||||
export const checkSimulatorConnected = (a: ConnectedAircraft) => {
|
||||
if (!a.lastHeartbeat || Date.now() - new Date(a.lastHeartbeat).getTime() > 30_000) return false; // 30 seconds
|
||||
if (!a.posLat || !a.posLng) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { ConnectedAircraft, PositionLog, Prisma, PublicUser, Station } from "@repo/db";
|
||||
import axios from "axios";
|
||||
import { serverApi } from "_helpers/axios";
|
||||
import { checkSimulatorConnected } from "_helpers/simulatorConnected";
|
||||
|
||||
export const getConnectedAircraftsAPI = async () => {
|
||||
const res = await axios.get<(ConnectedAircraft & { Station: Station })[]>("/api/aircrafts"); // return only connected aircrafts
|
||||
if (res.status !== 200) {
|
||||
throw new Error("Failed to fetch stations");
|
||||
}
|
||||
return res.data;
|
||||
return res.data.filter((a) => checkSimulatorConnected(a));
|
||||
};
|
||||
|
||||
export const editConnectedAircraftAPI = async (
|
||||
|
||||
@@ -122,7 +122,7 @@ export const useMrtStore = create<MrtStore>(
|
||||
},
|
||||
{ textLeft: "ILS VAR#", textSize: "3" },
|
||||
{
|
||||
textLeft: "new status received",
|
||||
textLeft: "empfangen",
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "4",
|
||||
},
|
||||
@@ -136,7 +136,7 @@ export const useMrtStore = create<MrtStore>(
|
||||
page: "sds",
|
||||
lines: [
|
||||
{
|
||||
textLeft: `neue SDS-Nachricht`,
|
||||
textLeft: `SDS-Nachricht`,
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "2",
|
||||
},
|
||||
|
||||
@@ -13,9 +13,6 @@ export const GET = async (request: NextRequest) => {
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || !user.permissions.includes("AUDIO_ADMIN"))
|
||||
return Response.json({ message: "Missing permissions" }, { status: 401 });
|
||||
|
||||
const rooms = await RoomManager.listRooms();
|
||||
|
||||
const roomsWithParticipants = rooms.map(async (room) => {
|
||||
|
||||
BIN
apps/dispatch/app/pilot/_components/mrt/MRT_MESSAGE.png
Normal file
BIN
apps/dispatch/app/pilot/_components/mrt/MRT_MESSAGE.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 322 KiB |
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
@@ -18,6 +19,7 @@ const MRT_DISPLAYLINE_STYLES: CSSProperties = {
|
||||
};
|
||||
|
||||
export interface DisplayLineProps {
|
||||
lineStyle?: CSSProperties;
|
||||
style?: CSSProperties;
|
||||
textLeft?: string;
|
||||
textMid?: string;
|
||||
@@ -31,12 +33,14 @@ const DisplayLine = ({
|
||||
textMid,
|
||||
textRight,
|
||||
textSize,
|
||||
lineStyle,
|
||||
}: DisplayLineProps) => {
|
||||
const INNER_TEXT_PARTS: CSSProperties = {
|
||||
fontFamily: "Melder",
|
||||
flex: "1",
|
||||
flexBasis: "auto",
|
||||
overflowWrap: "break-word",
|
||||
...lineStyle,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -46,13 +50,12 @@ const DisplayLine = ({
|
||||
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: "center", ...INNER_TEXT_PARTS }}>{textMid}</span>
|
||||
<span style={{ textAlign: "end", ...INNER_TEXT_PARTS }}>{textRight}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -61,7 +64,7 @@ const DisplayLine = ({
|
||||
export const Mrt = () => {
|
||||
useSounds();
|
||||
const { handleButton } = useButtons();
|
||||
const lines = useMrtStore((state) => state.lines);
|
||||
const { lines, page } = useMrtStore((state) => state);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -75,12 +78,11 @@ export const Mrt = () => {
|
||||
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: "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%",
|
||||
}}
|
||||
>
|
||||
{page !== "sds" && (
|
||||
<Image
|
||||
src={MrtImage}
|
||||
alt="MrtImage"
|
||||
@@ -91,6 +93,24 @@ export const Mrt = () => {
|
||||
gridArea: "1 / 1 / 13 / 13",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{page === "sds" && (
|
||||
<Image
|
||||
src={MrtMessageImage}
|
||||
alt="MrtImage-Message"
|
||||
style={{
|
||||
zIndex: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
gridArea: "1 / 1 / 13 / 13",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleButton("home")}
|
||||
style={{ gridArea: "2 / 4 / 3 / 5", ...MRT_BUTTON_STYLES }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleButton("1")}
|
||||
style={{ gridArea: "2 / 5 / 3 / 6", ...MRT_BUTTON_STYLES }}
|
||||
@@ -135,25 +155,50 @@ export const Mrt = () => {
|
||||
{lines[0] && (
|
||||
<DisplayLine
|
||||
{...lines[0]}
|
||||
style={{
|
||||
style={
|
||||
page === "sds"
|
||||
? {
|
||||
gridArea: "2 / 3 / 3 / 4",
|
||||
marginLeft: "9px",
|
||||
marginTop: "auto",
|
||||
|
||||
...MRT_DISPLAYLINE_STYLES,
|
||||
...lines[0]?.style,
|
||||
}
|
||||
: {
|
||||
gridArea: "4 / 3 / 5 / 4",
|
||||
marginLeft: "9px",
|
||||
marginTop: "auto",
|
||||
...MRT_DISPLAYLINE_STYLES,
|
||||
...lines[0]?.style,
|
||||
}}
|
||||
}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{lines[1] && (
|
||||
<DisplayLine
|
||||
lineStyle={{
|
||||
overflowX: "hidden",
|
||||
maxHeight: "100%",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
{...lines[1]}
|
||||
style={{
|
||||
style={
|
||||
page === "sds"
|
||||
? {
|
||||
gridArea: "4 / 2 / 10 / 4",
|
||||
marginLeft: "3px",
|
||||
...MRT_DISPLAYLINE_STYLES,
|
||||
...lines[1].style,
|
||||
}
|
||||
: {
|
||||
gridArea: "5 / 3 / 7 / 4",
|
||||
marginLeft: "3px",
|
||||
marginTop: "auto",
|
||||
...MRT_DISPLAYLINE_STYLES,
|
||||
...lines[1].style,
|
||||
}}
|
||||
}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{lines[2] && (
|
||||
|
||||
@@ -24,7 +24,7 @@ export const useButtons = () => {
|
||||
const { page, setPage } = useMrtStore((state) => state);
|
||||
|
||||
const handleButton =
|
||||
(button: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0") => () => {
|
||||
(button: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0" | "home") => () => {
|
||||
if (connectionStatus !== "connected") return;
|
||||
if (!station) return;
|
||||
if (!connectedAircraft?.id) return;
|
||||
@@ -40,7 +40,6 @@ export const useButtons = () => {
|
||||
button === "9" ||
|
||||
button === "0"
|
||||
) {
|
||||
if (page !== "home") return;
|
||||
setPage({ page: "sending-status", station });
|
||||
|
||||
setTimeout(async () => {
|
||||
@@ -56,6 +55,8 @@ export const useButtons = () => {
|
||||
fmsStatus: button,
|
||||
});
|
||||
}, 1000);
|
||||
} else {
|
||||
setPage({ page: "home", fmsStatus: connectedAircraft.fmsStatus || "6", station });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -23,9 +23,15 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@tanstack/react-query": "^5.79.0",
|
||||
"@turf/turf": "^7.2.0",
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"@types/leaflet": "^1.9.18",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/react": "^19.1.6",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"axios": "^1.9.0",
|
||||
"clsx": "^2.1.1",
|
||||
"daisyui": "^5.0.43",
|
||||
"geojson": "^0.5.0",
|
||||
"i": "^0.3.7",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
@@ -47,15 +53,9 @@
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"typescript": "^5.8.3",
|
||||
"zod": "^3.25.46",
|
||||
"zustand": "^5.0.5",
|
||||
"zustand-sync-tabs": "^0.2.2",
|
||||
"@types/leaflet": "^1.9.18",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/react": "^19.1.6",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"daisyui": "^5.0.43",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"devDependencies": {}
|
||||
"zustand-sync-tabs": "^0.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
1617
pnpm-lock.yaml
generated
1617
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user