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 { io } from "../index";
@@ -39,11 +45,58 @@ router.get("/:id", async (req, res) => {
// Update a connectedAircraft by ID
router.patch("/:id", async (req, res) => {
const { id } = req.params;
const aircraftUpdate = req.body as Prisma.ConnectedAircraftUpdateInput;
try {
const oldConnectedAircraft = await prisma.connectedAircraft.findUnique({
where: { id: Number(id) },
include: {
Station: true,
User: true,
},
});
const updatedConnectedAircraft = await prisma.connectedAircraft.update({
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(
"update-connectedAircraft",
updatedConnectedAircraft,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,16 @@
"use client";
import React, { useState } from "react";
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 { useMutation, useQueryClient } from "@tanstack/react-query";
import { editConnectedAircraftAPI } from "querys/aircrafts";
@@ -9,6 +18,7 @@ import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { cn } from "helpers/cn";
import { PersonIcon } from "@radix-ui/react-icons";
import {
Ban,
BellRing,
CircleGaugeIcon,
Clock,
@@ -21,31 +31,30 @@ import {
MapPin,
Mountain,
Navigation,
Plus,
RadioTower,
Sunset,
TextSearch,
} from "lucide-react";
import { useSession } from "next-auth/react";
import { editMissionAPI } from "querys/missions";
const FMSStatusHistory = ({
aircraft,
mission,
}: {
aircraft: ConnectedAircraft & { Station: Station };
mission?: Mission;
}) => {
const dummyData = [
{ status: "3", time: "12:00" },
{ status: "4", time: "12:30" },
{ status: "7", time: "14:54" },
{ status: "8", time: "15:30" },
{ status: "1", time: "16:30" },
{ status: "4", time: "17:30" },
{ status: "7", time: "18:54" },
{ status: "8", time: "19:30" },
{ 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" },
];
console.log("FMSStatusHistory", mission?.missionLog);
const log = ((mission?.missionLog as unknown as MissionLog[]) || [])
.filter(
(entry) =>
entry.type === "station-log" &&
entry.data.stationId === aircraft.Station.id,
)
.reverse()
.splice(0, 6) as MissionStationLog[];
const aircraftUser =
typeof aircraft.publicUser === "string"
@@ -62,20 +71,22 @@ const FMSStatusHistory = ({
</ul>
<div className="divider mt-0 mb-0" />
<ul className="space-y-2">
{dummyData
.reverse()
.slice(0, 6)
.map((entry, index) => (
{log.map((entry, index) => (
<li key={index} className="flex items-center gap-2">
<span
className="font-bold text-base"
style={{
color: FMS_STATUS_TEXT_COLORS[entry.status],
color: FMS_STATUS_TEXT_COLORS[entry.data.newFMSstatus],
}}
>
{entry.status}
{entry.data.newFMSstatus}
</span>
<span className="text-base-content">
{new Date(entry.timeStamp).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
<span className="text-base-content">{entry.time}</span>
</li>
))}
</ul>
@@ -272,5 +283,143 @@ const MissionTab = ({ mission }: { mission: Mission }) => {
</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;

View File

@@ -17,6 +17,8 @@ import {
User,
SmartphoneNfc,
CheckCheck,
ArrowRight,
Cross,
} from "lucide-react";
import {
getPublicUser,
@@ -212,6 +214,13 @@ const Patientdetails = ({ mission }: { mission: Mission }) => {
<p className="text-base-content font-semibold">
{mission.missionPatientInfo}
</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>
);
};
@@ -333,6 +342,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
queryClient.invalidateQueries({ queryKey: ["missions"] });
},
});
console.log(mission.missionLog);
if (!session.data?.user) return null;
return (
@@ -400,7 +410,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
)}
</div>
<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[])
.slice()
.reverse()
@@ -408,7 +418,12 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
if (entry.type === "station-log")
return (
<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
className="font-bold text-base"
style={{
@@ -422,7 +437,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
</span>
</li>
);
if (entry.type === "message-log")
if (entry.type === "message-log" || entry.type === "sds-log")
return (
<li key={index} className="flex items-center gap-2">
<span className="text-base-content">
@@ -432,13 +447,33 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
})}
</span>
<span
className="font-bold text-base"
className="font-bold text-base flex items-center gap-0.5"
style={{
color: FMS_STATUS_TEXT_COLORS[6],
}}
>
{entry.data.user.firstname?.[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 className="text-base-content">
{entry.data.message}

View File

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

View File

@@ -1,2 +1,2 @@
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();
}}
>
Verbindung Trennen
Trennen
</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";
export interface MissionStationLog {
@@ -6,11 +6,23 @@ export interface MissionStationLog {
auto: true;
timeStamp: string;
data: {
stationId: string;
stationId: number;
oldFMSstatus: string;
newFMSstatus: string;
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;