added Mission Closed Toeast, enhanced logic

This commit is contained in:
PxlLoewe
2025-07-14 23:57:33 -07:00
parent d7ca0eb166
commit 7be21a738a
7 changed files with 209 additions and 18 deletions

View File

@@ -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);
}
});

View File

@@ -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);

View File

@@ -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) => <MissionAutoCloseToast event={notification} t={t} mapStore={mapStore} />,
{
duration: 60000,
},
);
break;
default:
toast("unbekanntes Notification-Event");
break;

View File

@@ -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<Prisma.MissionUpdateInput> }) =>
editMissionAPI(id, mission),
mutationKey: ["missions"],
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["missions"],
});
},
});
return (
<BaseNotification icon={<Clock />} className="flex flex-row">
<div className="flex-1">
<h1 className="text-warning font-bold">Inaktiver Einsatz wurde automatisch geschlossen</h1>
<p>{event.message}</p>
</div>
<div className="ml-11">
<button
className="btn"
onClick={async () => {
if (!session?.user) return;
const mission = await editMissionMutation.mutateAsync({
id: event.data.missionId,
mission: {
state: "running",
missionLog: {
push: {
type: "reopened-log",
timeStamp: new Date().toISOString(),
data: {
user: getPublicUser(session?.user, {
ignorePrivacy: true,
}) as unknown as JsonValueType,
},
},
},
},
});
mapStore.setMap({
zoom: 14,
center: {
lat: mission.addressLat,
lng: mission.addressLng,
},
});
mapStore.setOpenMissionMarker({
open: [
{
id: mission.id,
tab: "home",
},
],
close: [],
});
toast.dismiss(t.id);
}}
>
schließen widerrufen
</button>
<button className="btn btn-ghost btn-sm" onClick={() => toast.remove(t.id)}>
<X size={16} />
</button>
</div>
</BaseNotification>
);
};

View File

@@ -735,8 +735,13 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
<span className="text-base-content">{entry.data.message}</span>
</li>
);
if (entry.type === "alert-log") {
const alertReceiver = entry.auto
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 }) => {
</>
)}
</span>
{entry.type === "alert-log" && (
<span className="text-base-content">Einsatz alarmiert</span>
)}
{entry.type === "completed-log" && (
<span className="text-base-content">Einsatz abgeschlossen</span>
)}
{entry.type === "reopened-log" && (
<span className="text-base-content">Einsatz wiedereröffnet</span>
)}
</li>
);
}

View File

@@ -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;

View File

@@ -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;