Added HPG validation

This commit is contained in:
PxlLoewe
2025-06-03 13:44:21 -07:00
parent 4acdb48344
commit ebb72c6517
14 changed files with 223 additions and 116 deletions

View File

@@ -49,6 +49,10 @@ export const sendAlert = async (
...mission, ...mission,
Stations, Stations,
}); });
io.to(`desktop:${aircraft.userId}`).emit("mission-alert", {
missionId: mission.id,
});
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { id: aircraft.userId }, where: { id: aircraft.userId },
}); });

View File

@@ -18,7 +18,6 @@ export const jwtMiddleware = async (socket: Socket, next: (err?: ExtendedError)
next(); next();
} catch (err) { } catch (err) {
console.error(err);
next(new Error("Authentication error")); next(new Error("Authentication error"));
} }
}; };

View File

@@ -13,6 +13,7 @@ import { Router } from "express";
import { io } from "../index"; import { io } from "../index";
import { sendNtfyMission } from "modules/ntfy"; import { sendNtfyMission } from "modules/ntfy";
import { sendAlert } from "modules/mission"; import { sendAlert } from "modules/mission";
import { userInfo } from "os";
const router: Router = Router(); const router: Router = Router();
@@ -192,11 +193,17 @@ router.post("/:id/send-alert", async (req, res) => {
} }
}); });
router.post("/:id/send-sds", async (req, res) => { router.post("/send-sds", async (req, res) => {
const sdsMessage = req.body as MissionSdsLog; const { sdsMessage, missionId } = req.body as {
missionId?: number;
sdsMessage: MissionSdsLog;
};
io.to(`station:${sdsMessage.data.stationId}`).emit("sds-message", sdsMessage);
if (missionId) {
const newMission = await prisma.mission.update({ const newMission = await prisma.mission.update({
where: { where: {
id: Number(req.params.id), id: Number(missionId),
}, },
data: { data: {
missionLog: { missionLog: {
@@ -205,12 +212,71 @@ router.post("/:id/send-sds", async (req, res) => {
}, },
}); });
io.to(`station:${sdsMessage.data.stationId}`).emit("sds-message", sdsMessage);
res.json({ res.json({
message: "SDS message sent", message: "SDS message sent",
mission: newMission, mission: newMission,
}); });
io.to("dispatchers").emit("update-mission", newMission); io.to("dispatchers").emit("update-mission", newMission);
} else {
res.json({
message: "SDS message sent",
});
}
});
router.post("/:id/hpg-validation-result", async (req, res) => {
try {
const missionId = req.params.id;
const result = req.body as {
state: HpgValidationState;
lat: number;
lng: number;
alertWhenValid?: boolean;
userId?: number;
};
if (!result) return;
const newMission = await prisma.mission.update({
where: { id: Number(missionId) },
data: {
// save position of new mission
addressLat: result.state === "POSITION_AMANDED" ? result.lat : undefined,
addressLng: result.state === "POSITION_AMANDED" ? result.lng : undefined,
hpgLocationLat: result.lat,
hpgLocationLng: result.lng,
hpgValidationState: result.state,
},
});
io.to("dispatchers").emit("update-mission", newMission);
const noActionRequired = result.state === "VALID";
if (noActionRequired) {
io.to(`user:${result.userId}`).emit("notification", {
type: "hpg-validation",
status: "success",
message: `HPG Validierung erfolgreich`,
} as NotificationPayload);
if (result.alertWhenValid) {
if (!req.user) return;
sendAlert(Number(missionId), {}, req.user);
}
} else {
io.to(`user:${result.userId}`).emit("notification", {
type: "hpg-validation",
status: "failed",
message: result.state,
} as NotificationPayload);
}
res.json({
message: `HPG Validation result processed`,
});
} catch (error) {
console.error("Error in HPG validation result:", error);
res.status(500).json({ error: "Failed to process HPG validation result" });
return;
}
}); });
router.post("/:id/validate-hpg", async (req, res) => { router.post("/:id/validate-hpg", async (req, res) => {
@@ -255,50 +321,14 @@ router.post("/:id/validate-hpg", async (req, res) => {
res.json({ res.json({
message: "HPG validierung gestartet", message: "HPG validierung gestartet",
}); });
console.log(
io.to(`desktop:${activeAircraftinMission}`).emit( `HPG Validation for ${user?.publicId} (${mission?.hpgSelectedMissionString}) started`,
"hpg-validation",
{
hpgMissionType: mission?.hpgMissionString,
lat: mission?.addressLat,
lng: mission?.addressLng,
},
async (result: { state: HpgValidationState; lat: number; lng: number }) => {
console.log("response from user:", result);
const newMission = await prisma.mission.update({
where: { id: Number(id) },
data: {
// save position of new mission
addressLat: result.state === "POSITION_AMANDED" ? result.lat : mission.addressLat,
addressLng: result.state === "POSITION_AMANDED" ? result.lng : mission.addressLng,
hpgLocationLat: result.lat,
hpgLocationLng: result.lng,
hpgValidationState: result.state,
},
});
io.to("dispatchers").emit("update-mission", newMission);
const noActionRequired = result.state === "VALID";
if (noActionRequired) {
io.to(`user:${req.user?.id}`).emit("notification", {
type: "hpg-validation",
status: "success",
message: `HPG Validierung erfolgreich`,
} as NotificationPayload);
if (config?.alertWhenValid) {
if (!req.user) return;
sendAlert(Number(id), {}, req.user);
}
} else {
io.to(`user:${req.user?.id}`).emit("notification", {
type: "hpg-validation",
status: "failed",
message: `HPG Validation fehlgeschlagen`,
} as NotificationPayload);
}
},
); );
io.to(`desktop:${activeAircraftinMission?.userId}`).emit("hpg-validation", {
missionId: parseInt(id),
userId: req.user?.id,
alertWhenValid: config?.alertWhenValid || false,
});
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.json({ error: (error as Error).message || "Failed to validate HPG" }); res.json({ error: (error as Error).message || "Failed to validate HPG" });

View File

@@ -63,10 +63,6 @@ export const handleConnectPilot =
userId: userId, userId: userId,
loginTime: new Date().toISOString(), loginTime: new Date().toISOString(),
stationId: parseInt(stationId), stationId: parseInt(stationId),
// TODO: remove this after testing
posLat: 51.45,
posLng: 9.77,
posH145active: true,
}, },
}); });
socket.join("dispatchers"); // Join the dispatchers room socket.join("dispatchers"); // Join the dispatchers room

View File

@@ -41,8 +41,6 @@ export const SituationBoard = () => {
}); });
const { setOpenAircraftMarker, setOpenMissionMarker, setMap } = useMapStore((state) => state); const { setOpenAircraftMarker, setOpenMissionMarker, setMap } = useMapStore((state) => state);
console.log("station", connectedAircrafts);
return ( return (
<div className={cn("dropdown dropdown-top", situationTabOpen && "dropdown-open")}> <div className={cn("dropdown dropdown-top", situationTabOpen && "dropdown-open")}>
<div className="indicator"> <div className="indicator">

View File

@@ -1,4 +1,4 @@
import { Marker, useMap } from "react-leaflet"; import { Marker, Polyline, useMap } from "react-leaflet";
import { DivIcon, Marker as LMarker, Popup as LPopup } from "leaflet"; import { DivIcon, Marker as LMarker, Popup as LPopup } from "leaflet";
import { useMapStore } from "_store/mapStore"; import { useMapStore } from "_store/mapStore";
import { Fragment, useCallback, useEffect, useRef, useState, useMemo } from "react"; import { Fragment, useCallback, useEffect, useRef, useState, useMemo } from "react";
@@ -13,7 +13,7 @@ import FMSStatusHistory, {
} from "./_components/AircraftMarkerTabs"; } from "./_components/AircraftMarkerTabs";
import { ConnectedAircraft, Station } from "@repo/db"; import { ConnectedAircraft, Station } from "@repo/db";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getConnectedAircraftsAPI } from "_querys/aircrafts"; import { getConnectedAircraftPositionLogAPI, getConnectedAircraftsAPI } from "_querys/aircrafts";
import { getMissionsAPI } from "_querys/missions"; import { getMissionsAPI } from "_querys/missions";
import { checkSimulatorConnected } from "_helpers/simulatorConnected"; import { checkSimulatorConnected } from "_helpers/simulatorConnected";
@@ -263,9 +263,18 @@ const AircraftMarker = ({ aircraft }: { aircraft: ConnectedAircraft & { Station:
const popupRef = useRef<LPopup>(null); const popupRef = useRef<LPopup>(null);
const { openAircraftMarker, setOpenAircraftMarker } = useMapStore((store) => store); const { openAircraftMarker, setOpenAircraftMarker } = useMapStore((store) => store);
const { data: positionLog } = useQuery({
queryKey: ["positionlog", aircraft.id],
queryFn: () =>
getConnectedAircraftPositionLogAPI({
id: aircraft.id,
}),
refetchInterval: 10000,
});
useEffect(() => { useEffect(() => {
const handleClick = () => { const handleClick = () => {
console.log("Marker clicked", aircraft.id);
const open = openAircraftMarker.some((m) => m.id === aircraft.id); const open = openAircraftMarker.some((m) => m.id === aircraft.id);
if (open) { if (open) {
setOpenAircraftMarker({ setOpenAircraftMarker({
@@ -289,7 +298,7 @@ const AircraftMarker = ({ aircraft }: { aircraft: ConnectedAircraft & { Station:
return () => { return () => {
marker?.off("click", handleClick); marker?.off("click", handleClick);
}; };
}, [aircraft.id, openAircraftMarker, setOpenAircraftMarker]); }, [aircraft.id, openAircraftMarker, setOpenAircraftMarker, markerRef.current]);
const [anchor, setAnchor] = useState<"topleft" | "topright" | "bottomleft" | "bottomright">( const [anchor, setAnchor] = useState<"topleft" | "topright" | "bottomleft" | "bottomright">(
"topleft", "topleft",
@@ -372,12 +381,12 @@ const AircraftMarker = ({ aircraft }: { aircraft: ConnectedAircraft & { Station:
</div>`; </div>`;
}; };
if (!aircraft.posLat || !aircraft.posLng) return null;
return ( return (
<Fragment key={aircraft.id}> <Fragment key={aircraft.id}>
{
<Marker <Marker
ref={markerRef} ref={markerRef}
position={[aircraft.posLat!, aircraft.posLng!]} position={[aircraft.posLat, aircraft.posLng]}
icon={ icon={
new DivIcon({ new DivIcon({
iconAnchor: [0, 0], iconAnchor: [0, 0],
@@ -385,8 +394,8 @@ const AircraftMarker = ({ aircraft }: { aircraft: ConnectedAircraft & { Station:
}) })
} }
/> />
}
{openAircraftMarker.some((m) => m.id === aircraft.id) && !hideMarker && ( {openAircraftMarker.some((m) => m.id === aircraft.id) && !hideMarker && (
<>
<SmartPopup <SmartPopup
options={{ options={{
ignoreCluster: true, ignoreCluster: true,
@@ -404,6 +413,14 @@ const AircraftMarker = ({ aircraft }: { aircraft: ConnectedAircraft & { Station:
<AircraftPopupContent aircraft={aircraft} /> <AircraftPopupContent aircraft={aircraft} />
</div> </div>
</SmartPopup> </SmartPopup>
<Polyline
pathOptions={{
color: "var(--color-rescuetrack)",
weight: 3,
}}
positions={positionLog?.map((pos) => [pos.lat, pos.lng]) || []}
/>
</>
)} )}
</Fragment> </Fragment>
); );

View File

@@ -29,6 +29,7 @@ import {
Hash, Hash,
ListCollapse, ListCollapse,
LocateFixed, LocateFixed,
Lollipop,
MapPin, MapPin,
Mountain, Mountain,
Navigation, Navigation,
@@ -252,6 +253,14 @@ const RettungsmittelTab = ({
<CircleGaugeIcon size={16} /> ALT: {aircraft.posAlt} ft <CircleGaugeIcon size={16} /> ALT: {aircraft.posAlt} ft
</span> </span>
</div> </div>
<div className="flex items-center text-sm font-semibold justify-between pr-2 mt-2">
<span className="flex items-center gap-2">
<Lollipop size={16} />{" "}
<span className={cn(aircraft.posH145active && "text-green-500")}>
{aircraft.posH145active ? "H145 Aktiv" : "H145 Inaktiv"}
</span>
</span>
</div>
</div> </div>
); );
}; };
@@ -306,8 +315,14 @@ const SDSTab = ({
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected"; const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
const sendSdsMutation = useMutation({ const sendSdsMutation = useMutation({
mutationFn: async ({ id, message }: { id: number; message: MissionSdsLog }) => { mutationFn: async ({
await sendSdsMessageAPI(id, message); missionId,
sdsMessage,
}: {
missionId?: number;
sdsMessage: MissionSdsLog;
}) => {
await sendSdsMessageAPI({ missionId, sdsMessage });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["missions"], queryKey: ["missions"],
}); });
@@ -348,11 +363,10 @@ const SDSTab = ({
<button <button
className="btn btn-sm btn-primary btn-outline" className="btn btn-sm btn-primary btn-outline"
onClick={() => { onClick={() => {
if (!mission) return;
sendSdsMutation sendSdsMutation
.mutateAsync({ .mutateAsync({
id: mission.id, missionId: mission?.id,
message: { sdsMessage: {
type: "sds-log", type: "sds-log",
auto: false, auto: false,
timeStamp: new Date().toISOString(), timeStamp: new Date().toISOString(),

View File

@@ -1,4 +1,4 @@
import { ConnectedAircraft, Prisma, PublicUser, Station } from "@repo/db"; import { ConnectedAircraft, PositionLog, Prisma, PublicUser, Station } from "@repo/db";
import axios from "axios"; import axios from "axios";
import { serverApi } from "_helpers/axios"; import { serverApi } from "_helpers/axios";
@@ -17,3 +17,13 @@ export const editConnectedAircraftAPI = async (
const respone = await serverApi.patch<ConnectedAircraft>(`/aircrafts/${id}`, mission); const respone = await serverApi.patch<ConnectedAircraft>(`/aircrafts/${id}`, mission);
return respone.data; return respone.data;
}; };
export const getConnectedAircraftPositionLogAPI = async ({ id }: { id: number }) => {
const res = await axios.get<PositionLog[]>("/api/aircrafts/positionlog", {
params: { connectedAircraftId: id },
});
if (res.status !== 200) {
throw new Error("Failed to fetch aircraft position log");
}
return res.data;
};

View File

@@ -29,8 +29,14 @@ export const editMissionAPI = async (id: number, mission: Prisma.MissionUpdateIn
const respone = await serverApi.patch<Mission>(`/mission/${id}`, mission); const respone = await serverApi.patch<Mission>(`/mission/${id}`, mission);
return respone.data; return respone.data;
}; };
export const sendSdsMessageAPI = async (id: number, sdsMessage: MissionSdsLog) => { export const sendSdsMessageAPI = async ({
const respone = await serverApi.post<Mission>(`/mission/${id}/send-sds`, sdsMessage); missionId,
sdsMessage,
}: {
missionId?: number;
sdsMessage: MissionSdsLog;
}) => {
const respone = await serverApi.post<Mission>(`/mission/send-sds`, { sdsMessage, missionId });
return respone.data; return respone.data;
}; };

View File

@@ -65,7 +65,6 @@ dispatchSocket.on("force-disconnect", (reason: string) => {
}); });
}); });
dispatchSocket.on("dispatchers-update", (dispatch: ConnectedDispatcher) => { dispatchSocket.on("dispatchers-update", (dispatch: ConnectedDispatcher) => {
console.log("dispatchers-update", dispatch);
useDispatchConnectionStore.setState({ useDispatchConnectionStore.setState({
connectedDispatcher: dispatch, connectedDispatcher: dispatch,
}); });

View File

@@ -0,0 +1,33 @@
import { prisma, Prisma } from "@repo/db";
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest): Promise<NextResponse> {
try {
const connectedAircraftId = request.nextUrl.searchParams.get("connectedAircraftId");
const aircraft = await prisma.connectedAircraft.findUnique({
where: {
id: connectedAircraftId ? parseInt(connectedAircraftId) : undefined,
},
});
if (!aircraft) return NextResponse.json({ error: "Aircraft not found" }, { status: 404 });
const positionLog = await prisma.positionLog.findMany({
where: {
id: {
in: aircraft.positionLogIds,
},
timestamp: {
gte: new Date(Date.now() - 2 * 60 * 60 * 1000), // Last 2 hours
},
},
});
return NextResponse.json(positionLog, {
status: 200,
});
} catch (error) {
console.error(error);
return NextResponse.json({ error: "Failed to fetch Aircrafts" }, { status: 500 });
}
}

View File

@@ -24,13 +24,10 @@ export const PUT = async (req: Request) => {
position: PositionLog; position: PositionLog;
h145: boolean; h145: boolean;
}; };
console.log("position", userId);
if (!position) { if (!position) {
return Response.json({ message: "Missing id or position" }); return Response.json({ message: "Missing id or position" });
} }
console.log("position", position);
const activeAircraft = await prisma.connectedAircraft.findFirst({ const activeAircraft = await prisma.connectedAircraft.findFirst({
where: { where: {
userId, userId,

View File

@@ -135,6 +135,8 @@ export const MissionForm = () => {
} }
}, [missionFormValues, form, defaultFormValues]); }, [missionFormValues, form, defaultFormValues]);
console.log("Mission HPG String", form.watch("hpgMissionString"));
const saveMission = async ( const saveMission = async (
mission: MissionOptionalDefaults, mission: MissionOptionalDefaults,
{ alertWhenValid = false, createNewMission = false } = {}, { alertWhenValid = false, createNewMission = false } = {},
@@ -287,9 +289,9 @@ export const MissionForm = () => {
); );
form.setValue("hpgMissionString", ""); form.setValue("hpgMissionString", "");
}} }}
defaultValue="" value={form.watch("missionKeywordCategory") || "please_select"}
> >
<option disabled value=""> <option disabled value="please_select">
Einsatz Kategorie auswählen... Einsatz Kategorie auswählen...
</option> </option>
{Object.keys(KEYWORD_CATEGORY).map((use) => ( {Object.keys(KEYWORD_CATEGORY).map((use) => (
@@ -310,9 +312,9 @@ export const MissionForm = () => {
form.setValue("missionKeywordAbbreviation", keyword?.abreviation || (null as any)); form.setValue("missionKeywordAbbreviation", keyword?.abreviation || (null as any));
form.setValue("hpgMissionString", "default"); form.setValue("hpgMissionString", "default");
}} }}
defaultValue="default" value={form.watch("missionKeywordAbbreviation") || "please_select"}
> >
<option disabled value={""}> <option disabled value={"please_select"}>
Einsatzstichwort auswählen... Einsatzstichwort auswählen...
</option> </option>
{keywords && {keywords &&
@@ -331,7 +333,7 @@ export const MissionForm = () => {
<select <select
{...form.register("hpgMissionString")} {...form.register("hpgMissionString")}
className="select select-primary select-bordered w-full" className="select select-primary select-bordered w-full"
defaultValue="default" value={form.watch("hpgMissionString") || ""}
> >
<option disabled value=""> <option disabled value="">
Einsatz Szenario auswählen... Einsatz Szenario auswählen...

View File

@@ -7,10 +7,12 @@ import dynamic from "next/dynamic";
import { Chat } from "../_components/left/Chat"; import { Chat } from "../_components/left/Chat";
import { Report } from "../_components/left/Report"; import { Report } from "../_components/left/Report";
import { SituationBoard } from "_components/left/SituationBoard"; import { SituationBoard } from "_components/left/SituationBoard";
const Map = dynamic(() => import("../_components/map/Map"), { ssr: false }); const Map = dynamic(() => import("../_components/map/Map"), { ssr: false });
const DispatchPage = () => { const DispatchPage = () => {
const { isOpen } = usePannelStore(); const { isOpen } = usePannelStore();
/* return null; */
return ( return (
<div className="relative flex-1 flex transition-all duration-500 ease w-full"> <div className="relative flex-1 flex transition-all duration-500 ease w-full">
{/* <MapToastCard2 /> */} {/* <MapToastCard2 /> */}