From 7be21a738ade8d0e86ffcabb80c90999d9fefd86 Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Mon, 14 Jul 2025 23:57:33 -0700 Subject: [PATCH] added Mission Closed Toeast, enhanced logic --- apps/dispatch-server/modules/chron.ts | 76 ++++++++++++++-- apps/dispatch-server/routes/mission.ts | 2 +- .../app/_components/QueryProvider.tsx | 9 ++ .../customToasts/MissionAutoClose.tsx | 88 +++++++++++++++++++ .../map/_components/MissionMarkerTabs.tsx | 27 ++++-- .../database/prisma/json/MissionVehicleLog.ts | 12 ++- packages/database/prisma/json/SocketEvents.ts | 13 ++- 7 files changed, 209 insertions(+), 18 deletions(-) create mode 100644 apps/dispatch/app/_components/customToasts/MissionAutoClose.tsx diff --git a/apps/dispatch-server/modules/chron.ts b/apps/dispatch-server/modules/chron.ts index 43cbe6dc..02811e7b 100644 --- a/apps/dispatch-server/modules/chron.ts +++ b/apps/dispatch-server/modules/chron.ts @@ -1,4 +1,5 @@ -import { MissionLog, prisma } from "@repo/db"; +import { MissionLog, NotificationPayload, prisma } from "@repo/db"; +import { io } from "index"; import cron from "node-cron"; const removeClosedMissions = async () => { @@ -11,6 +12,7 @@ const removeClosedMissions = async () => { const lastAlert = (mission.missionLog as unknown as MissionLog[]).find((l) => { return l.type === "alert-log"; }); + const lastAlertTime = lastAlert ? new Date(lastAlert.timeStamp) : null; const aircraftsInMission = await prisma.connectedAircraft.findMany({ @@ -21,17 +23,65 @@ const removeClosedMissions = async () => { }, }); + const allConnectedAircraftsInIdleStatus = aircraftsInMission.every((a) => + ["1", "2", "6"].includes(a.fmsStatus), + ); + + const allStationsInMissionChangedFromStatus4to1Or8to1 = mission.missionStationIds.every( + (stationId) => { + const status4Log = (mission.missionLog as unknown as MissionLog[]).findIndex((l) => { + return ( + l.type === "station-log" && + l.data?.stationId === stationId && + l.data?.newFMSstatus === "4" + ); + }); + const status8Log = (mission.missionLog as unknown as MissionLog[]).findIndex((l) => { + return ( + l.type === "station-log" && + l.data?.stationId === stationId && + l.data?.newFMSstatus === "8" + ); + }); + + const status1Log = (mission.missionLog as unknown as MissionLog[]).findIndex((l) => { + return ( + l.type === "station-log" && + l.data?.stationId === stationId && + l.data?.newFMSstatus === "1" + ); + }); + return ( + status4Log !== -1 && + status1Log !== -1 && + (status4Log < status1Log || status8Log < status1Log) + ); + }, + ); + + const missionHastManualReactivation = (mission.missionLog as unknown as MissionLog[]).some( + (l) => l.type === "reopened-log", + ); + if ( - aircraftsInMission.length > 0 && // Check if any aircraft is still active - !aircraftsInMission.some((a) => ["1", "2", "6"].includes(a.fmsStatus)) // Check if any aircraft is in a status that indicates it's not inactive + !allConnectedAircraftsInIdleStatus // If some aircrafts are still active, do not close the mission ) return; const now = new Date(); if (!lastAlertTime) return; - // change State to closed if last alert was more than 180 minutes ago - if (now.getTime() - lastAlertTime.getTime() < 30 * 60 * 1000) return; + // Case 1: Forgotten Mission, last alert more than 3 Hours ago + // Case 2: All stations in mission changed from status 4 to 1 or from status 8 to 1 + if ( + !( + now.getTime() - lastAlertTime.getTime() > 1000 * 60 * 180 || + allStationsInMissionChangedFromStatus4to1Or8to1 + ) || + missionHastManualReactivation + ) + return; + const log: MissionLog = { type: "completed-log", auto: true, @@ -39,7 +89,7 @@ const removeClosedMissions = async () => { data: {}, }; - await prisma.mission.update({ + const updatedMission = await prisma.mission.update({ where: { id: mission.id, }, @@ -50,6 +100,16 @@ const removeClosedMissions = async () => { }, }, }); + io.to("dispatchers").emit("new-mission", { updatedMission }); + io.to("dispatchers").emit("notification", { + type: "mission-auto-close", + status: "chron", + message: `Einsatz ${updatedMission.publicId} wurde aufgrund von Inaktivität geschlossen.`, + data: { + missionId: updatedMission.id, + publicMissionId: updatedMission.publicId, + }, + } as NotificationPayload); console.log(`Mission ${mission.id} closed due to inactivity.`); }); }; @@ -74,11 +134,11 @@ const removeConnectedAircrafts = async () => { }); }; -cron.schedule("*/5 * * * *", async () => { +cron.schedule("*/1 * * * *", async () => { try { await removeClosedMissions(); await removeConnectedAircrafts(); } catch (error) { - console.error("Error removing closed missions:", error); + console.error("Error on cron job:", error); } }); diff --git a/apps/dispatch-server/routes/mission.ts b/apps/dispatch-server/routes/mission.ts index b08f56ca..5542c64a 100644 --- a/apps/dispatch-server/routes/mission.ts +++ b/apps/dispatch-server/routes/mission.ts @@ -86,7 +86,7 @@ router.patch("/:id", async (req, res) => { where: { id: Number(id) }, data: req.body, }); - io.to("dispatchers").emit("update-mission", updatedMission); + io.to("dispatchers").emit("update-mission", { updatedMission }); res.json(updatedMission); } catch (error) { console.error(error); diff --git a/apps/dispatch/app/_components/QueryProvider.tsx b/apps/dispatch/app/_components/QueryProvider.tsx index b973a835..2b2af938 100644 --- a/apps/dispatch/app/_components/QueryProvider.tsx +++ b/apps/dispatch/app/_components/QueryProvider.tsx @@ -11,6 +11,7 @@ import { useMapStore } from "_store/mapStore"; import { AdminMessageToast } from "_components/customToasts/AdminMessage"; import { pilotSocket } from "(app)/pilot/socket"; import { QUICK_RESPONSE, StatusToast } from "_components/customToasts/StationStatusToast"; +import { MissionAutoCloseToast } from "_components/customToasts/MissionAutoClose"; export function QueryProvider({ children }: { children: ReactNode }) { const mapStore = useMapStore((s) => s); @@ -79,6 +80,14 @@ export function QueryProvider({ children }: { children: ReactNode }) { duration: 60000, }); break; + case "mission-auto-close": + toast.custom( + (t) => , + { + duration: 60000, + }, + ); + break; default: toast("unbekanntes Notification-Event"); break; diff --git a/apps/dispatch/app/_components/customToasts/MissionAutoClose.tsx b/apps/dispatch/app/_components/customToasts/MissionAutoClose.tsx new file mode 100644 index 00000000..af1ef5e9 --- /dev/null +++ b/apps/dispatch/app/_components/customToasts/MissionAutoClose.tsx @@ -0,0 +1,88 @@ +import { getPublicUser, MissionAutoClose, Prisma } from "@repo/db"; +import { JsonValueType } from "@repo/db/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { BaseNotification } from "_components/customToasts/BaseNotification"; +import { editMissionAPI } from "_querys/missions"; +import { MapStore } from "_store/mapStore"; +import { Clock, X } from "lucide-react"; +import { useSession } from "next-auth/react"; +import toast, { Toast } from "react-hot-toast"; + +export const MissionAutoCloseToast = ({ + event, + t, + mapStore, +}: { + event: MissionAutoClose; + t: Toast; + mapStore: MapStore; +}) => { + const { data: session } = useSession(); + const queryClient = useQueryClient(); + const editMissionMutation = useMutation({ + mutationFn: ({ id, mission }: { id: number; mission: Partial }) => + editMissionAPI(id, mission), + mutationKey: ["missions"], + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["missions"], + }); + }, + }); + + return ( + } className="flex flex-row"> +
+

Inaktiver Einsatz wurde automatisch geschlossen

+

{event.message}

+
+
+ + +
+
+ ); +}; diff --git a/apps/dispatch/app/_components/map/_components/MissionMarkerTabs.tsx b/apps/dispatch/app/_components/map/_components/MissionMarkerTabs.tsx index a50a8564..d3a0c640 100644 --- a/apps/dispatch/app/_components/map/_components/MissionMarkerTabs.tsx +++ b/apps/dispatch/app/_components/map/_components/MissionMarkerTabs.tsx @@ -735,10 +735,15 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => { {entry.data.message} ); - if (entry.type === "alert-log") { - const alertReceiver = entry.auto - ? null - : entry.data.station?.bosCallsignShort || entry.data.vehicle; + if ( + entry.type === "alert-log" || + entry.type === "completed-log" || + entry.type === "reopened-log" + ) { + const alertReceiver = + entry.auto || entry.type !== "alert-log" + ? null + : entry.data.station?.bosCallsignShort || entry.data.vehicle; return (
  • @@ -755,8 +760,8 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => { > {!entry.auto && ( <> - {entry.data.user.firstname?.[0]?.toUpperCase() ?? "?"} - {entry.data.user.lastname?.[0]?.toUpperCase() ?? "?"} + {entry.data.user?.firstname?.[0]?.toUpperCase() ?? "?"} + {entry.data.user?.lastname?.[0]?.toUpperCase() ?? "?"} )} {entry.auto && "AUTO"} @@ -781,7 +786,15 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => { )} - Einsatz alarmiert + {entry.type === "alert-log" && ( + Einsatz alarmiert + )} + {entry.type === "completed-log" && ( + Einsatz abgeschlossen + )} + {entry.type === "reopened-log" && ( + Einsatz wiedereröffnet + )}
  • ); } diff --git a/packages/database/prisma/json/MissionVehicleLog.ts b/packages/database/prisma/json/MissionVehicleLog.ts index 63646be9..7bceda67 100644 --- a/packages/database/prisma/json/MissionVehicleLog.ts +++ b/packages/database/prisma/json/MissionVehicleLog.ts @@ -73,6 +73,15 @@ export interface MissionCompletedLog { }; } +export interface MissionReopenedLog { + type: "reopened-log"; + auto: false; + timeStamp: string; + data: { + user?: PublicUser; + }; +} + export type MissionLog = | MissionStationLog | MissionMessageLog @@ -80,4 +89,5 @@ export type MissionLog = | MissionAlertLog | MissionAlertLogAuto | MissionCompletedLog - | MissionVehicleLog; + | MissionVehicleLog + | MissionReopenedLog; diff --git a/packages/database/prisma/json/SocketEvents.ts b/packages/database/prisma/json/SocketEvents.ts index 9c524dfe..dc0e6497 100644 --- a/packages/database/prisma/json/SocketEvents.ts +++ b/packages/database/prisma/json/SocketEvents.ts @@ -39,8 +39,19 @@ export interface StationStatus { }; } +export type MissionAutoClose = { + type: "mission-auto-close"; + status: "chron"; + message: string; + data: { + missionId: number; + publicMissionId: string; + }; +}; + export type NotificationPayload = | ValidationFailed | ValidationSuccess | AdminMessage - | StationStatus; + | StationStatus + | MissionAutoClose;