Added Aircraft log to mission

This commit is contained in:
PxlLoewe
2025-05-20 14:15:24 -07:00
parent 1696c79122
commit df4cff827e
13 changed files with 354 additions and 84 deletions

View File

@@ -1,4 +1,10 @@
import { prisma } from "@repo/db"; import {
ConnectedAircraft,
getPublicUser,
MissionLog,
Prisma,
prisma,
} from "@repo/db";
import { Router } from "express"; import { Router } from "express";
import { io } from "../index"; import { io } from "../index";
@@ -39,11 +45,58 @@ router.get("/:id", async (req, res) => {
// Update a connectedAircraft by ID // Update a connectedAircraft by ID
router.patch("/:id", async (req, res) => { router.patch("/:id", async (req, res) => {
const { id } = req.params; const { id } = req.params;
const aircraftUpdate = req.body as Prisma.ConnectedAircraftUpdateInput;
try { try {
const oldConnectedAircraft = await prisma.connectedAircraft.findUnique({
where: { id: Number(id) },
include: {
Station: true,
User: true,
},
});
const updatedConnectedAircraft = await prisma.connectedAircraft.update({ const updatedConnectedAircraft = await prisma.connectedAircraft.update({
where: { id: Number(id) }, where: { id: Number(id) },
data: req.body, data: {
...aircraftUpdate,
},
}); });
const mission = await prisma.mission.findFirst({
where: {
state: "running",
missionStationIds: {
has: updatedConnectedAircraft.stationId,
},
},
});
if (
mission &&
aircraftUpdate.fmsStatus &&
oldConnectedAircraft &&
updatedConnectedAircraft &&
oldConnectedAircraft.fmsStatus !== updatedConnectedAircraft.fmsStatus
) {
const newMissionLog = mission.missionLog as any as MissionLog[];
newMissionLog.push({
type: "station-log",
auto: true,
data: {
oldFMSstatus: oldConnectedAircraft.fmsStatus,
newFMSstatus: updatedConnectedAircraft.fmsStatus,
station: oldConnectedAircraft.Station,
stationId: updatedConnectedAircraft.stationId,
user: getPublicUser(oldConnectedAircraft.User),
},
timeStamp: new Date().toISOString(),
});
await prisma.mission.update({
where: { id: mission.id },
data: {
missionLog: newMissionLog as any,
},
});
}
io.to("dispatchers").emit( io.to("dispatchers").emit(
"update-connectedAircraft", "update-connectedAircraft",
updatedConnectedAircraft, updatedConnectedAircraft,

View File

@@ -1,15 +1,13 @@
import { User } from "@repo/db"; import { User } from "@repo/db";
import { Socket, Server } from "socket.io"; import { Socket, Server } from "socket.io";
export const handleConnectDesktop = export const handleConnectDesktop = (socket: Socket, io: Server) => () => {
(socket: Socket, io: Server) => (socket: Socket) => { const user = socket.data.user as User;
const user = socket.data.user as User; console.log("connect-desktop", user.publicId);
console.log("connect-desktop", user.publicId); socket.join(`user:${user.id}`);
socket.join(`user:${user.id}`); socket.join(`desktop:${user.id}`);
socket.join(`desktop:${user.id}`);
socket.on("ptt", (data) => { socket.on("ptt", (data) => {
console.log("ptt", data); socket.to(`user:${user.id}`).emit("ptt", data);
socket.to(`user:${user.id}`).emit("ptt", data); });
}); };
};

View File

@@ -42,6 +42,9 @@ export function QueryProvider({ children }: { children: ReactNode }) {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["aircrafts"], queryKey: ["aircrafts"],
}); });
queryClient.invalidateQueries({
queryKey: ["missions"],
});
}; };
dispatchSocket.on("update-mission", invalidateMission); dispatchSocket.on("update-mission", invalidateMission);

View File

@@ -115,6 +115,7 @@ interface PTTData {
} }
const handlePTT = (data: PTTData) => { const handlePTT = (data: PTTData) => {
console.log("PTT", data);
const { shouldTransmit, source } = data; const { shouldTransmit, source } = data;
const { room } = useAudioStore.getState(); const { room } = useAudioStore.getState();
if (!room) return; if (!room) return;

View File

@@ -6,11 +6,14 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
const id = searchParams.get("id"); const id = searchParams.get("id");
const filter = searchParams.get("filter"); const filter = searchParams.get("filter");
console.log(filter);
const filterParsed = JSON.parse(filter || "{}");
try { try {
const data = await prisma.mission.findMany({ const data = await prisma.mission.findMany({
where: { where: {
id: id ? Number(id) : undefined, id: id ? Number(id) : undefined,
...(filter ? JSON.parse(filter) : {}), ...filterParsed,
}, },
}); });

View File

@@ -25,6 +25,7 @@ import FMSStatusHistory, {
FMSStatusSelector, FMSStatusSelector,
MissionTab, MissionTab,
RettungsmittelTab, RettungsmittelTab,
SDSTab,
} 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";
@@ -100,22 +101,24 @@ const AircraftPopupContent = ({
); );
const { data: missions } = useQuery({ const { data: missions } = useQuery({
queryKey: ["missions"], queryKey: ["missions", "missions-map"],
queryFn: () => getMissionsAPI(), queryFn: () =>
getMissionsAPI({
state: "running",
missionStationIds: {
has: aircraft.Station.id,
},
}),
}); });
const mission = useMemo(() => { console.log("Missions", missions);
return missions?.find(
(m) => const mission = missions && missions[0];
(m.state === "running" || m.state === "draft") &&
m.missionStationIds.includes(aircraft.Station.id),
);
}, [missions, aircraft.Station.id]);
const renderTabContent = useMemo(() => { const renderTabContent = useMemo(() => {
switch (currentTab) { switch (currentTab) {
case "home": case "home":
return <FMSStatusHistory aircraft={aircraft} />; return <FMSStatusHistory aircraft={aircraft} mission={mission} />;
case "fms": case "fms":
return <FMSStatusSelector aircraft={aircraft} />; return <FMSStatusSelector aircraft={aircraft} />;
case "aircraft": case "aircraft":
@@ -127,7 +130,7 @@ const AircraftPopupContent = ({
<span className="text-gray-100">No mission available</span> <span className="text-gray-100">No mission available</span>
); );
case "chat": case "chat":
return <div>Chat Content</div>; return <SDSTab aircraft={aircraft} mission={mission} />;
default: default:
return <span className="text-gray-100">Error</span>; return <span className="text-gray-100">Error</span>;
} }
@@ -395,7 +398,7 @@ const AircraftMarker = ({
return ( return (
<Fragment key={aircraft.id}> <Fragment key={aircraft.id}>
{checkSimulatorConnected(aircraft.lastHeartbeat) && ( {
<Marker <Marker
ref={markerRef} ref={markerRef}
position={[aircraft.posLat!, aircraft.posLng!]} position={[aircraft.posLat!, aircraft.posLng!]}
@@ -406,7 +409,7 @@ const AircraftMarker = ({
}) })
} }
/> />
)} }
{openAircraftMarker.some((m) => m.id === aircraft.id) && !hideMarker && ( {openAircraftMarker.some((m) => m.id === aircraft.id) && !hideMarker && (
<SmartPopup <SmartPopup
options={{ options={{
@@ -439,9 +442,11 @@ export const AircraftLayer = () => {
return ( return (
<> <>
{aircrafts?.map((aircraft) => { {aircrafts
return <AircraftMarker key={aircraft.id} aircraft={aircraft} />; ?.filter((a) => checkSimulatorConnected(a.lastHeartbeat))
})} ?.map((aircraft) => {
return <AircraftMarker key={aircraft.id} aircraft={aircraft} />;
})}
</> </>
); );
}; };

View File

@@ -1,7 +1,16 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useState } from "react";
import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "../AircraftMarker"; import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "../AircraftMarker";
import { ConnectedAircraft, Mission, Prisma, Station } from "@repo/db"; import {
ConnectedAircraft,
getPublicUser,
Mission,
MissionLog,
MissionSdsLog,
MissionStationLog,
Prisma,
Station,
} from "@repo/db";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { editConnectedAircraftAPI } from "querys/aircrafts"; import { editConnectedAircraftAPI } from "querys/aircrafts";
@@ -9,6 +18,7 @@ import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { cn } from "helpers/cn"; import { cn } from "helpers/cn";
import { PersonIcon } from "@radix-ui/react-icons"; import { PersonIcon } from "@radix-ui/react-icons";
import { import {
Ban,
BellRing, BellRing,
CircleGaugeIcon, CircleGaugeIcon,
Clock, Clock,
@@ -21,31 +31,30 @@ import {
MapPin, MapPin,
Mountain, Mountain,
Navigation, Navigation,
Plus,
RadioTower, RadioTower,
Sunset, Sunset,
TextSearch, TextSearch,
} from "lucide-react"; } from "lucide-react";
import { useSession } from "next-auth/react";
import { editMissionAPI } from "querys/missions";
const FMSStatusHistory = ({ const FMSStatusHistory = ({
aircraft, aircraft,
mission,
}: { }: {
aircraft: ConnectedAircraft & { Station: Station }; aircraft: ConnectedAircraft & { Station: Station };
mission?: Mission;
}) => { }) => {
const dummyData = [ console.log("FMSStatusHistory", mission?.missionLog);
{ status: "3", time: "12:00" }, const log = ((mission?.missionLog as unknown as MissionLog[]) || [])
{ status: "4", time: "12:30" }, .filter(
{ status: "7", time: "14:54" }, (entry) =>
{ status: "8", time: "15:30" }, entry.type === "station-log" &&
{ status: "1", time: "16:30" }, entry.data.stationId === aircraft.Station.id,
{ status: "4", time: "17:30" }, )
{ status: "7", time: "18:54" }, .reverse()
{ status: "8", time: "19:30" }, .splice(0, 6) as MissionStationLog[];
{ status: "1", time: "20:30" },
{ status: "4", time: "21:30" },
{ status: "7", time: "22:54" },
{ status: "8", time: "23:30" },
{ status: "1", time: "24:30" },
];
const aircraftUser = const aircraftUser =
typeof aircraft.publicUser === "string" typeof aircraft.publicUser === "string"
@@ -62,22 +71,24 @@ const FMSStatusHistory = ({
</ul> </ul>
<div className="divider mt-0 mb-0" /> <div className="divider mt-0 mb-0" />
<ul className="space-y-2"> <ul className="space-y-2">
{dummyData {log.map((entry, index) => (
.reverse() <li key={index} className="flex items-center gap-2">
.slice(0, 6) <span
.map((entry, index) => ( className="font-bold text-base"
<li key={index} className="flex items-center gap-2"> style={{
<span color: FMS_STATUS_TEXT_COLORS[entry.data.newFMSstatus],
className="font-bold text-base" }}
style={{ >
color: FMS_STATUS_TEXT_COLORS[entry.status], {entry.data.newFMSstatus}
}} </span>
> <span className="text-base-content">
{entry.status} {new Date(entry.timeStamp).toLocaleTimeString([], {
</span> hour: "2-digit",
<span className="text-base-content">{entry.time}</span> minute: "2-digit",
</li> })}
))} </span>
</li>
))}
</ul> </ul>
</div> </div>
); );
@@ -272,5 +283,143 @@ const MissionTab = ({ mission }: { mission: Mission }) => {
</div> </div>
); );
}; };
export { FMSStatusSelector, RettungsmittelTab, MissionTab };
const SDSTab = ({
aircraft,
mission,
}: {
aircraft: ConnectedAircraft & {
Station: Station;
};
mission?: Mission;
}) => {
const session = useSession();
const [isChatOpen, setIsChatOpen] = useState(false);
const [note, setNote] = useState("");
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"] });
},
});
const log = (mission?.missionLog as unknown as MissionLog[])
.slice()
.reverse()
.filter(
(entry) =>
entry.type === "sds-log" &&
entry.data.stationId === aircraft.Station.id,
);
console.log(mission?.missionLog, log);
return (
<div className="p-4">
<div className="flex items-center gap-2">
{!isChatOpen ? (
<button
className="text-base-content text-base cursor-pointer"
onClick={() => setIsChatOpen(true)}
>
<span className="flex items-center gap-2">
<Plus size={18} /> Notiz hinzufügen
</span>
</button>
) : (
<div className="flex items-center gap-2 w-full">
<input
type="text"
placeholder=""
className="input input-sm text-base-content flex-1"
value={note}
onChange={(e) => setNote(e.target.value)}
/>
<button
className="btn btn-sm btn-primary btn-outline"
onClick={() => {
if (!mission) return;
const newMissionLog = [
...mission.missionLog,
{
type: "sds-log",
auto: false,
timeStamp: new Date().toISOString(),
data: {
stationId: aircraft.Station.id,
station: aircraft.Station,
message: note,
user: getPublicUser(session.data!.user),
},
} as MissionSdsLog,
];
editMissionMutation
.mutateAsync({
id: mission.id,
mission: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
missionLog: newMissionLog as any,
},
})
.then(() => {
setIsChatOpen(false);
setNote("");
});
}}
>
<Plus size={20} />
</button>
<button
className="btn btn-sm btn-outline"
onClick={() => {
setIsChatOpen(false);
setNote("");
}}
>
<Ban size={20} />
</button>
</div>
)}
</div>
<div className="divider m-0" />
<ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto">
{log.map((entry, index) => {
const sdsEntry = entry as MissionSdsLog;
return (
<li key={index} className="flex items-center gap-2">
<span className="text-base-content">
{new Date(sdsEntry.timeStamp).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
<span
className="font-bold text-base"
style={{
color: FMS_STATUS_TEXT_COLORS[6],
}}
>
{sdsEntry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}
{sdsEntry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}
</span>
<span className="text-base-content">{sdsEntry.data.message}</span>
</li>
);
})}
{!log.length && (
<p className="text-gray-500">Kein SDS-Verlauf verfügbar</p>
)}
</ul>
</div>
);
};
export { FMSStatusSelector, RettungsmittelTab, MissionTab, SDSTab };
export default FMSStatusHistory; export default FMSStatusHistory;

View File

@@ -17,6 +17,8 @@ import {
User, User,
SmartphoneNfc, SmartphoneNfc,
CheckCheck, CheckCheck,
ArrowRight,
Cross,
} from "lucide-react"; } from "lucide-react";
import { import {
getPublicUser, getPublicUser,
@@ -212,6 +214,13 @@ const Patientdetails = ({ mission }: { mission: Mission }) => {
<p className="text-base-content font-semibold"> <p className="text-base-content font-semibold">
{mission.missionPatientInfo} {mission.missionPatientInfo}
</p> </p>
<div className="divider my-2" />
<h2 className="flex items-center gap-2 text-lg font-bold mb-3">
<Cross /> Einsatzinformationen
</h2>
<p className="text-base-content font-semibold">
{mission.missionAdditionalInfo}
</p>
</div> </div>
); );
}; };
@@ -333,6 +342,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
queryClient.invalidateQueries({ queryKey: ["missions"] }); queryClient.invalidateQueries({ queryKey: ["missions"] });
}, },
}); });
console.log(mission.missionLog);
if (!session.data?.user) return null; if (!session.data?.user) return null;
return ( return (
@@ -400,7 +410,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
)} )}
</div> </div>
<div className="divider m-0" /> <div className="divider m-0" />
<ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto"> <ul className="space-y-1 max-h-[300px] overflow-y-auto overflow-x-auto">
{(mission.missionLog as unknown as MissionLog[]) {(mission.missionLog as unknown as MissionLog[])
.slice() .slice()
.reverse() .reverse()
@@ -408,7 +418,12 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
if (entry.type === "station-log") if (entry.type === "station-log")
return ( return (
<li key={index} className="flex items-center gap-2"> <li key={index} className="flex items-center gap-2">
<span className="text-base-content">{entry.timeStamp}</span> <span className="text-base-content">
{new Date(entry.timeStamp).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
<span <span
className="font-bold text-base" className="font-bold text-base"
style={{ style={{
@@ -422,7 +437,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
</span> </span>
</li> </li>
); );
if (entry.type === "message-log") if (entry.type === "message-log" || entry.type === "sds-log")
return ( return (
<li key={index} className="flex items-center gap-2"> <li key={index} className="flex items-center gap-2">
<span className="text-base-content"> <span className="text-base-content">
@@ -432,13 +447,33 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
})} })}
</span> </span>
<span <span
className="font-bold text-base" className="font-bold text-base flex items-center gap-0.5"
style={{ style={{
color: FMS_STATUS_TEXT_COLORS[6], color: FMS_STATUS_TEXT_COLORS[6],
}} }}
> >
{entry.data.user.firstname?.[0]?.toUpperCase() ?? "?"} {entry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}
{entry.data.user.lastname?.[0]?.toUpperCase() ?? "?"} {entry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}
{entry.type === "sds-log" && (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"
/>
</svg>
{entry.data.station.bosCallsignShort}
</>
)}
</span> </span>
<span className="text-base-content"> <span className="text-base-content">
{entry.data.message} {entry.data.message}

View File

@@ -13,27 +13,36 @@ export const ConnectionBtn = () => {
const session = useSession(); const session = useSession();
const uid = session.data?.user?.id; const uid = session.data?.user?.id;
if (!uid) return null; if (!uid) return null;
return ( return (
<div className="rounded-box bg-base-200 flex justify-center items-center gap-2 p-1"> <div className="rounded-box bg-base-200 flex justify-center items-center gap-2 p-1">
{connection.message.length > 0 && ( {connection.message.length > 0 && (
<span className="mx-2 text-error">{connection.message}</span> <span className="mx-2 text-error">{connection.message}</span>
)} )}
{connection.status === "disconnected" && ( {connection.status == "connected" ? (
<button <button
className="btn btn-sm btn-soft btn-info " className="btn btn-soft btn-error"
onClick={() => modalRef.current?.showModal()} type="submit"
onSubmit={() => false}
onClick={() => {
connection.disconnect();
}}
> >
Verbinden Trennen
</button> </button>
)} ) : (
{connection.status == "connected" && (
<button <button
className="btn btn-soft btn-success" type="submit"
onClick={() => modalRef.current?.showModal()} onSubmit={() => false}
onClick={() => {
modalRef.current?.showModal();
}}
className="btn btn-soft btn-info"
> >
Verbunden {connection.status == "disconnected"
? "Verbinden"
: connection.status}
</button> </button>
)} )}
@@ -93,7 +102,9 @@ export const ConnectionBtn = () => {
}} }}
className="btn btn-soft btn-info" className="btn btn-soft btn-info"
> >
{connection.status == "disconnected" {connection.status == "disconnected" ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(connection.status as any) === ""
? "Verbinden" ? "Verbinden"
: connection.status} : connection.status}
</button> </button>

View File

@@ -1,2 +1,2 @@
export const checkSimulatorConnected = (date: Date) => export const checkSimulatorConnected = (date: Date) =>
date && Date.now() - new Date(date).getTime() <= 30_000; date && Date.now() - new Date(date).getTime() <= 3000_000;

View File

@@ -123,7 +123,7 @@ export const ConnectionBtn = () => {
connection.disconnect(); connection.disconnect();
}} }}
> >
Verbindung Trennen Trennen
</button> </button>
) : ( ) : (
<button <button

Binary file not shown.

View File

@@ -1,4 +1,4 @@
import { Station, User } from "../../generated/client"; import { Station } from "../../generated/client";
import { PublicUser } from "./User"; import { PublicUser } from "./User";
export interface MissionStationLog { export interface MissionStationLog {
@@ -6,11 +6,23 @@ export interface MissionStationLog {
auto: true; auto: true;
timeStamp: string; timeStamp: string;
data: { data: {
stationId: string; stationId: number;
oldFMSstatus: string; oldFMSstatus: string;
newFMSstatus: string; newFMSstatus: string;
station: Station; station: Station;
user: User; user: PublicUser;
};
}
export interface MissionSdsLog {
type: "sds-log";
auto: false;
timeStamp: string;
data: {
station: Station;
user: PublicUser;
stationId: number;
message: string;
}; };
} }
@@ -25,4 +37,4 @@ export interface MissionMessageLog {
}; };
} }
export type MissionLog = MissionStationLog | MissionMessageLog; export type MissionLog = MissionStationLog | MissionMessageLog | MissionSdsLog;