added Callback and custon notification Toast, client notification event handler
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": true,
|
||||
"printWidth": 80,
|
||||
"printWidth": 100,
|
||||
"singleQuote": false
|
||||
}
|
||||
|
||||
11
apps/dispatch-server/.d.ts
vendored
11
apps/dispatch-server/.d.ts
vendored
@@ -6,3 +6,14 @@ declare module "next-auth/jwt" {
|
||||
email: string;
|
||||
}
|
||||
}
|
||||
declare module "cookie-parser";
|
||||
|
||||
import type { User } from "@repo/db";
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: User | null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,18 @@ import cors from "cors";
|
||||
import { handleSendMessage } from "socket-events/send-message";
|
||||
import { handleConnectPilot } from "socket-events/connect-pilot";
|
||||
import { handleConnectDesktop } from "socket-events/connect-desktop";
|
||||
import cookieParser from "cookie-parser";
|
||||
import { authMiddleware } from "modules/expressMiddleware";
|
||||
import { prisma, User } from "@repo/db";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: User | null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const app = express();
|
||||
const server = createServer(app);
|
||||
@@ -31,6 +43,8 @@ io.on("connection", (socket) => {
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
app.use(authMiddleware as any);
|
||||
app.use(router);
|
||||
|
||||
server.listen(process.env.PORT, () => {
|
||||
|
||||
24
apps/dispatch-server/modules/expressMiddleware.ts
Normal file
24
apps/dispatch-server/modules/expressMiddleware.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { prisma, User } from "@repo/db";
|
||||
import { NextFunction } from "express";
|
||||
|
||||
interface AttachUserRequest extends Request {
|
||||
user?: User | null;
|
||||
}
|
||||
|
||||
interface AttachUserMiddleware {
|
||||
(req: AttachUserRequest, res: Response, next: NextFunction): Promise<void>;
|
||||
}
|
||||
|
||||
export const authMiddleware: AttachUserMiddleware = async (req, res, next) => {
|
||||
const authHeader = (req.headers as any).authorization;
|
||||
if (authHeader && authHeader.startsWith("User ")) {
|
||||
const userId = authHeader.split(" ")[1];
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
req.user = user;
|
||||
}
|
||||
next();
|
||||
};
|
||||
101
apps/dispatch-server/modules/mission.ts
Normal file
101
apps/dispatch-server/modules/mission.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { ConnectedAircraft, Mission, prisma } from "@repo/db";
|
||||
import { io } from "index";
|
||||
import { sendNtfyMission } from "modules/ntfy";
|
||||
|
||||
export const sendAlert = async (
|
||||
id: number,
|
||||
{
|
||||
stationId,
|
||||
}: {
|
||||
stationId?: number;
|
||||
},
|
||||
): Promise<{
|
||||
connectedAircrafts: ConnectedAircraft[];
|
||||
mission: Mission;
|
||||
}> => {
|
||||
const mission = await prisma.mission.findUnique({
|
||||
where: { id: id },
|
||||
});
|
||||
const Stations = await prisma.station.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: mission?.missionStationIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!mission) {
|
||||
throw new Error("Mission not found");
|
||||
}
|
||||
|
||||
// connectedAircrafts the alert is sent to
|
||||
const connectedAircrafts = await prisma.connectedAircraft.findMany({
|
||||
where: {
|
||||
stationId: stationId
|
||||
? stationId
|
||||
: {
|
||||
in: mission.missionStationIds,
|
||||
},
|
||||
logoutTime: null,
|
||||
},
|
||||
include: {
|
||||
Station: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const aircraft of connectedAircrafts) {
|
||||
console.log(`Sending mission to: station:${aircraft.stationId}`);
|
||||
io.to(`station:${aircraft.stationId}`).emit("mission-alert", {
|
||||
...mission,
|
||||
Stations,
|
||||
});
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: aircraft.userId },
|
||||
});
|
||||
if (!user) continue;
|
||||
if (user.settingsNtfyRoom) {
|
||||
await sendNtfyMission(
|
||||
mission,
|
||||
Stations,
|
||||
aircraft.Station,
|
||||
user.settingsNtfyRoom,
|
||||
);
|
||||
}
|
||||
const existingMissionOnStationUser =
|
||||
await prisma.missionOnStationUsers.findFirst({
|
||||
where: {
|
||||
missionId: mission.id,
|
||||
userId: aircraft.userId,
|
||||
stationId: aircraft.stationId,
|
||||
},
|
||||
});
|
||||
if (!existingMissionOnStationUser)
|
||||
await prisma.missionOnStationUsers.create({
|
||||
data: {
|
||||
missionId: mission.id,
|
||||
userId: aircraft.userId,
|
||||
stationId: aircraft.stationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// for statistics only
|
||||
await prisma.missionsOnStations
|
||||
.createMany({
|
||||
data: mission.missionStationIds.map((stationId) => ({
|
||||
missionId: mission.id,
|
||||
stationId,
|
||||
})),
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore if the entry already exists
|
||||
});
|
||||
|
||||
await prisma.mission.update({
|
||||
where: { id: Number(id) },
|
||||
data: {
|
||||
state: "running",
|
||||
},
|
||||
});
|
||||
return { connectedAircrafts, mission };
|
||||
};
|
||||
@@ -10,6 +10,7 @@
|
||||
"devDependencies": {
|
||||
"@repo/db": "*",
|
||||
"@repo/typescript-config": "*",
|
||||
"@types/cookie-parser": "^1.4.8",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
@@ -21,6 +22,7 @@
|
||||
"@redis/json": "^1.0.7",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"axios": "^1.7.9",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^4.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { HpgValidationState, Prisma, prisma } from "@repo/db";
|
||||
import {
|
||||
HpgValidationState,
|
||||
MissionSdsLog,
|
||||
MissionStationLog,
|
||||
NotificationPayload,
|
||||
Prisma,
|
||||
prisma,
|
||||
User,
|
||||
} from "@repo/db";
|
||||
import { Router } from "express";
|
||||
import { io } from "../index";
|
||||
import { sendNtfyMission } from "modules/ntfy";
|
||||
import { sendAlert } from "modules/mission";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -107,90 +116,8 @@ router.post("/:id/send-alert", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { stationId } = req.body as { stationId?: number };
|
||||
try {
|
||||
const mission = await prisma.mission.findUnique({
|
||||
where: { id: Number(id) },
|
||||
});
|
||||
const Stations = await prisma.station.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: mission?.missionStationIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!mission) {
|
||||
res.status(404).json({ error: "Mission not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
// connectedAircrafts the alert is sent to
|
||||
const connectedAircrafts = await prisma.connectedAircraft.findMany({
|
||||
where: {
|
||||
stationId: stationId
|
||||
? stationId
|
||||
: {
|
||||
in: mission.missionStationIds,
|
||||
},
|
||||
logoutTime: null,
|
||||
},
|
||||
include: {
|
||||
Station: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const aircraft of connectedAircrafts) {
|
||||
console.log(`Sending mission to: station:${aircraft.stationId}`);
|
||||
io.to(`station:${aircraft.stationId}`).emit("mission-alert", {
|
||||
...mission,
|
||||
Stations,
|
||||
});
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: aircraft.userId },
|
||||
});
|
||||
if (!user) continue;
|
||||
if (user.settingsNtfyRoom) {
|
||||
await sendNtfyMission(
|
||||
mission,
|
||||
Stations,
|
||||
aircraft.Station,
|
||||
user.settingsNtfyRoom,
|
||||
);
|
||||
}
|
||||
const existingMissionOnStationUser =
|
||||
await prisma.missionOnStationUsers.findFirst({
|
||||
where: {
|
||||
missionId: mission.id,
|
||||
userId: aircraft.userId,
|
||||
stationId: aircraft.stationId,
|
||||
},
|
||||
});
|
||||
if (!existingMissionOnStationUser)
|
||||
await prisma.missionOnStationUsers.create({
|
||||
data: {
|
||||
missionId: mission.id,
|
||||
userId: aircraft.userId,
|
||||
stationId: aircraft.stationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// for statistics only
|
||||
await prisma.missionsOnStations
|
||||
.createMany({
|
||||
data: mission.missionStationIds.map((stationId) => ({
|
||||
missionId: mission.id,
|
||||
stationId,
|
||||
})),
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore if the entry already exists
|
||||
});
|
||||
|
||||
await prisma.mission.update({
|
||||
where: { id: Number(id) },
|
||||
data: {
|
||||
state: "running",
|
||||
},
|
||||
const { connectedAircrafts, mission } = await sendAlert(Number(id), {
|
||||
stationId,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
@@ -203,9 +130,36 @@ router.post("/:id/send-alert", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/:id/send-sds", async (req, res) => {
|
||||
const sdsMessage = req.body as MissionSdsLog;
|
||||
const newMission = await prisma.mission.update({
|
||||
where: {
|
||||
id: Number(req.params.id),
|
||||
},
|
||||
data: {
|
||||
missionLog: {
|
||||
push: sdsMessage as any,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
io.to(`station:${sdsMessage.data.stationId}`).emit("sds-message", sdsMessage);
|
||||
res.json({
|
||||
message: "SDS message sent",
|
||||
mission: newMission,
|
||||
});
|
||||
io.to("dispatchers").emit("update-mission", newMission);
|
||||
});
|
||||
|
||||
router.post("/:id/validate-hpg", async (req, res) => {
|
||||
try {
|
||||
console.log(req.user);
|
||||
const { id } = req.params;
|
||||
const config = req.body as
|
||||
| {
|
||||
alertWhenValid?: boolean;
|
||||
}
|
||||
| undefined;
|
||||
const mission = await prisma.mission.findFirstOrThrow({
|
||||
where: {
|
||||
id: Number(id),
|
||||
@@ -225,16 +179,11 @@ router.post("/:id/validate-hpg", async (req, res) => {
|
||||
},
|
||||
});
|
||||
|
||||
/* if (activeAircraftinMission.length === 0) {
|
||||
res.status(400).json({ error: "No active aircraft in mission" });
|
||||
return;
|
||||
} */
|
||||
|
||||
res.json({
|
||||
message: "HPG validation started",
|
||||
});
|
||||
|
||||
io.to(`desktop:${activeAircraftinMission}`).emit(
|
||||
/* io.to(`desktop:${activeAircraftinMission}`).emit(
|
||||
"hpg-validation",
|
||||
{
|
||||
hpgMissionType: mission?.hpgMissionString,
|
||||
@@ -266,8 +215,41 @@ router.post("/:id/validate-hpg", async (req, res) => {
|
||||
},
|
||||
});
|
||||
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) {
|
||||
sendAlert(Number(id), {});
|
||||
}
|
||||
} else {
|
||||
io.to(`user:${req.user?.id}`).emit("notification", {
|
||||
type: "hpg-validation",
|
||||
status: "failed",
|
||||
message: `HPG Validation fehlgeschlagen`,
|
||||
} as NotificationPayload);
|
||||
}
|
||||
},
|
||||
);
|
||||
); */
|
||||
setTimeout(() => {
|
||||
io.to(`user:${req.user?.id}`).emit("notification", {
|
||||
type: "hpg-validation",
|
||||
status: "success",
|
||||
message: "HPG_BUSY",
|
||||
data: {
|
||||
mission,
|
||||
},
|
||||
} as NotificationPayload);
|
||||
io.to(`user:${req.user?.id}`).emit("notification", {
|
||||
type: "hpg-validation",
|
||||
status: "failed",
|
||||
message: `HPG Validation fehlgeschlagen`,
|
||||
} as NotificationPayload);
|
||||
}, 5000);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.json({ error: (error as Error).message || "Failed to validate HPG" });
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"baseUrl": ".",
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": ["**/*.ts", "./index.ts"],
|
||||
"include": ["**/*.ts", "./index.ts", "**/*.d.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -5,9 +5,12 @@ import { toast } from "react-hot-toast";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { dispatchSocket } from "dispatch/socket";
|
||||
import { Mission } from "@repo/db";
|
||||
import { Mission, NotificationPayload } from "@repo/db";
|
||||
import { HPGnotificationToast } from "_components/customToasts/HPGnotification";
|
||||
import { useMapStore } from "_store/mapStore";
|
||||
|
||||
export function QueryProvider({ children }: { children: ReactNode }) {
|
||||
const mapStore = useMapStore((s) => s);
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
@@ -48,15 +51,42 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
||||
});
|
||||
};
|
||||
|
||||
const handleNotification = (notification: NotificationPayload) => {
|
||||
console.log("notification", notification);
|
||||
switch (notification.type) {
|
||||
case "hpg-validation":
|
||||
toast.custom(
|
||||
(t) => <HPGnotificationToast event={notification} mapStore={mapStore} t={t} />,
|
||||
{
|
||||
duration: 9999,
|
||||
},
|
||||
);
|
||||
|
||||
break;
|
||||
default:
|
||||
toast(notification.message);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
dispatchSocket.on("update-mission", invalidateMission);
|
||||
dispatchSocket.on("delete-mission", invalidateMission);
|
||||
dispatchSocket.on("new-mission", invalidateMission);
|
||||
dispatchSocket.on("dispatchers-update", invalidateConnectedUsers);
|
||||
dispatchSocket.on("pilots-update", invalidateConnectedUsers);
|
||||
dispatchSocket.on("update-connectedAircraft", invalidateConenctedAircrafts);
|
||||
}, [queryClient]);
|
||||
dispatchSocket.on("notification", handleNotification);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
return () => {
|
||||
dispatchSocket.off("update-mission", invalidateMission);
|
||||
dispatchSocket.off("delete-mission", invalidateMission);
|
||||
dispatchSocket.off("new-mission", invalidateMission);
|
||||
dispatchSocket.off("dispatchers-update", invalidateConnectedUsers);
|
||||
dispatchSocket.off("pilots-update", invalidateConnectedUsers);
|
||||
dispatchSocket.off("update-connectedAircraft", invalidateConenctedAircrafts);
|
||||
dispatchSocket.off("notification", handleNotification);
|
||||
};
|
||||
}, [queryClient, mapStore]);
|
||||
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { cn } from "helpers/cn";
|
||||
|
||||
export const BaseNotification = ({
|
||||
children,
|
||||
className,
|
||||
icon,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
icon?: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn("alert alert-vertical flex flex-row gap-4")}>
|
||||
{icon}
|
||||
|
||||
<div className={className}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { NotificationPayload } from "@repo/db";
|
||||
import { BaseNotification } from "_components/customToasts/BaseNotification";
|
||||
import { MapStore, useMapStore } from "_store/mapStore";
|
||||
import { Check, Cross } from "lucide-react";
|
||||
import toast, { Toast } from "react-hot-toast";
|
||||
|
||||
export const HPGnotificationToast = ({
|
||||
event,
|
||||
t,
|
||||
mapStore,
|
||||
}: {
|
||||
event: NotificationPayload;
|
||||
t: Toast;
|
||||
mapStore: MapStore;
|
||||
}) => {
|
||||
const handleClick = () => {
|
||||
toast.dismiss(t.id);
|
||||
mapStore.setOpenMissionMarker({
|
||||
open: [{ id: event.data.mission.id, tab: "home" }],
|
||||
close: [],
|
||||
});
|
||||
mapStore.setMap({
|
||||
center: [event.data.mission.addressLat, event.data.mission.addressLng],
|
||||
zoom: 14,
|
||||
});
|
||||
};
|
||||
|
||||
if (event.status === "failed") {
|
||||
return (
|
||||
<BaseNotification icon={<Cross />} className="flex flex-row">
|
||||
<div className="flex-1">
|
||||
<h1 className="text-red-500 font-bold">HPG validierung fehlgeschlagen</h1>
|
||||
<p>{event.message}</p>
|
||||
</div>
|
||||
<div className="ml-11">
|
||||
<button className="btn" onClick={handleClick}>
|
||||
anzeigen
|
||||
</button>
|
||||
</div>
|
||||
</BaseNotification>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<BaseNotification icon={<Check />} className="flex flex-row">
|
||||
<div className="flex-1">
|
||||
<h1 className="text-green-600 font-bold">HPG validierung erfolgreich</h1>
|
||||
<p className="text-sm">{event.message}</p>
|
||||
</div>
|
||||
<div className="ml-11">
|
||||
<button className="btn" onClick={handleClick}>
|
||||
anzeigen
|
||||
</button>
|
||||
</div>
|
||||
</BaseNotification>
|
||||
);
|
||||
}
|
||||
};
|
||||
78
apps/dispatch/app/_components/map/Map.tsx
Normal file
78
apps/dispatch/app/_components/map/Map.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { useMapStore } from "_store/mapStore";
|
||||
import { MapContainer } from "react-leaflet";
|
||||
import { BaseMaps } from "_components/map/BaseMaps";
|
||||
import { ContextMenu } from "_components/map/ContextMenu";
|
||||
import { MissionLayer } from "_components/map/MissionMarkers";
|
||||
import { SearchElements } from "_components/map/SearchElements";
|
||||
import { AircraftLayer } from "_components/map/AircraftMarker";
|
||||
import { MarkerCluster } from "_components/map/_components/MarkerCluster";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Map as TMap } from "leaflet";
|
||||
|
||||
const Map = () => {
|
||||
const ref = useRef<TMap | null>(null);
|
||||
const { map, setMap } = useMapStore();
|
||||
|
||||
useEffect(() => {
|
||||
// Sync map zoom and center with the map store
|
||||
if (ref.current) {
|
||||
ref.current.setView(map.center, map.zoom);
|
||||
/* ref.current.on("moveend", () => {
|
||||
const center = ref.current?.getCenter();
|
||||
const zoom = ref.current?.getZoom();
|
||||
if (center && zoom) {
|
||||
setMap({
|
||||
center: [center.lat, center.lng],
|
||||
zoom,
|
||||
});
|
||||
}
|
||||
});
|
||||
ref.current.on("zoomend", () => {
|
||||
const zoom = ref.current?.getZoom();
|
||||
const center = ref.current?.getCenter();
|
||||
|
||||
if (zoom && center) {
|
||||
setMap({
|
||||
center,
|
||||
zoom,
|
||||
});
|
||||
}
|
||||
}); */
|
||||
}
|
||||
}, [map, setMap]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Map center or zoom changed");
|
||||
|
||||
if (ref.current) {
|
||||
const center = ref.current?.getCenter();
|
||||
const zoom = ref.current?.getZoom();
|
||||
console.log("Map center or zoom changed", center.equals(map.center), zoom === map.zoom);
|
||||
if (!center.equals(map.center) || zoom !== map.zoom) {
|
||||
console.log("Updating map center and zoom");
|
||||
ref.current.setView(map.center, map.zoom);
|
||||
}
|
||||
}
|
||||
}, [map.center, map.zoom]);
|
||||
|
||||
return (
|
||||
<MapContainer
|
||||
ref={ref}
|
||||
className="flex-1"
|
||||
center={map.center}
|
||||
zoom={map.zoom}
|
||||
fadeAnimation={false}
|
||||
>
|
||||
<BaseMaps />
|
||||
<SearchElements />
|
||||
<ContextMenu />
|
||||
<MarkerCluster />
|
||||
<MissionLayer />
|
||||
<AircraftLayer />
|
||||
</MapContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Map;
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
TextSearch,
|
||||
} from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { editMissionAPI } from "querys/missions";
|
||||
import { editMissionAPI, sendSdsMessageAPI } from "querys/missions";
|
||||
|
||||
const FMSStatusHistory = ({
|
||||
aircraft,
|
||||
@@ -298,17 +298,18 @@ const SDSTab = ({
|
||||
const [note, setNote] = useState("");
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const editMissionMutation = useMutation({
|
||||
mutationFn: ({
|
||||
const sendSdsMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
mission,
|
||||
message,
|
||||
}: {
|
||||
id: number;
|
||||
mission: Partial<Prisma.MissionUpdateInput>;
|
||||
}) => editMissionAPI(id, mission),
|
||||
mutationKey: ["missions"],
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["missions"] });
|
||||
message: MissionSdsLog;
|
||||
}) => {
|
||||
await sendSdsMessageAPI(id, message);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["missions"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -347,26 +348,19 @@ const SDSTab = ({
|
||||
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
|
||||
sendSdsMutation
|
||||
.mutateAsync({
|
||||
id: mission.id,
|
||||
mission: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
missionLog: newMissionLog as any,
|
||||
message: {
|
||||
type: "sds-log",
|
||||
auto: false,
|
||||
timeStamp: new Date().toISOString(),
|
||||
data: {
|
||||
stationId: aircraft.Station.id,
|
||||
station: aircraft.Station,
|
||||
message: note,
|
||||
user: getPublicUser(session.data!.user),
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
@@ -11,11 +11,11 @@ import { useMapStore } from "_store/mapStore";
|
||||
import {
|
||||
FMS_STATUS_COLORS,
|
||||
FMS_STATUS_TEXT_COLORS,
|
||||
} from "dispatch/_components/map/AircraftMarker";
|
||||
} from "_components/map/AircraftMarker";
|
||||
import {
|
||||
MISSION_STATUS_COLORS,
|
||||
MISSION_STATUS_TEXT_COLORS,
|
||||
} from "dispatch/_components/map/MissionMarkers";
|
||||
} from "_components/map/MissionMarkers";
|
||||
import { cn } from "helpers/cn";
|
||||
import { checkSimulatorConnected } from "helpers/simulatorConnected";
|
||||
import { getConnectedAircraftsAPI } from "querys/aircrafts";
|
||||
@@ -367,32 +367,25 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
||||
},
|
||||
});
|
||||
|
||||
const dispatcherConnected =
|
||||
useDispatchConnectionStore((s) => s.status) === "connected";
|
||||
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold mb-3">
|
||||
<SmartphoneNfc /> Rettungsmittel
|
||||
</h2>
|
||||
{dispatcherConnected && (
|
||||
<div className="space-x-2">
|
||||
<div
|
||||
className="tooltip tooltip-primary tooltip-left font-semibold"
|
||||
data-tip="Einsatz erneut alarmieren"
|
||||
>
|
||||
<button
|
||||
className="btn btn-xs btn-primary btn-outline"
|
||||
onClick={() => {
|
||||
sendAlertMutation.mutate({ id: mission.id });
|
||||
}}
|
||||
>
|
||||
<BellRing size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="tooltip tooltip-primary tooltip-left font-semibold"
|
||||
data-tip="Einsatz erneut alarmieren"
|
||||
>
|
||||
<button
|
||||
className="btn btn-xs btn-primary btn-outline"
|
||||
onClick={() => {
|
||||
sendAlertMutation.mutate({ id: mission.id });
|
||||
}}
|
||||
>
|
||||
<BellRing size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto">
|
||||
{missionStations?.map((station, index) => {
|
||||
@@ -427,24 +420,74 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{dispatcherConnected && (
|
||||
<div>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="flex items-center gap-2">
|
||||
{/* TODO: make it a small multiselect */}
|
||||
<select className="select select-sm select-primary select-bordered flex-1">
|
||||
<option value="1">Feuerwehr</option>
|
||||
<option value="2">RTW</option>
|
||||
<option value="3">Polizei</option>
|
||||
</select>
|
||||
<button className="btn btn-sm btn-primary btn-outline">
|
||||
<span className="text-base-content flex items-center gap-2">
|
||||
<BellRing size={16} /> Nachalarmieren
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="flex items-center gap-2">
|
||||
{/* TODO: make it a small multiselect */}
|
||||
<select
|
||||
className="select select-sm select-primary select-bordered flex-1"
|
||||
onChange={(e) => {
|
||||
const selected = allStations?.find(
|
||||
(s) => s.id.toString() === e.target.value,
|
||||
);
|
||||
if (selected) {
|
||||
setSelectedStation(selected);
|
||||
} else {
|
||||
setSelectedStation(
|
||||
e.target.value as "ambulance" | "police" | "firebrigade",
|
||||
);
|
||||
}
|
||||
}}
|
||||
value={
|
||||
typeof selectedStation === "string"
|
||||
? selectedStation
|
||||
: selectedStation?.id
|
||||
}
|
||||
>
|
||||
{allStations
|
||||
?.filter((s) => !mission.missionStationIds.includes(s.id))
|
||||
?.map((station) => (
|
||||
<option
|
||||
key={station.id}
|
||||
value={station.id}
|
||||
onClick={() => {
|
||||
setSelectedStation(station);
|
||||
}}
|
||||
>
|
||||
{station.bosCallsign}
|
||||
</option>
|
||||
))}
|
||||
<option disabled>Fahrzeuge:</option>
|
||||
<option value="firebrigade">Feuerwehr</option>
|
||||
<option value="ambulance">RTW</option>
|
||||
<option value="police">Polizei</option>
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-sm btn-primary btn-outline"
|
||||
onClick={async () => {
|
||||
if (typeof selectedStation === "string") {
|
||||
toast.error("Fahrzeuge werden aktuell nicht unterstützt");
|
||||
} else {
|
||||
if (!selectedStation?.id) return;
|
||||
await updateMissionMutation.mutateAsync({
|
||||
id: mission.id,
|
||||
missionEdit: {
|
||||
missionStationIds: {
|
||||
push: selectedStation?.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
await sendAlertMutation.mutate({
|
||||
id: mission.id,
|
||||
stationId: selectedStation?.id ?? 0,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="text-base-content flex items-center gap-2">
|
||||
<BellRing size={16} /> Nachalarmieren
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -47,6 +47,7 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
||||
const { room, isTalking } = get();
|
||||
if (!room) return;
|
||||
room.localParticipant.setMicrophoneEnabled(!isTalking);
|
||||
|
||||
if (!isTalking) {
|
||||
// If old status was not talking, we need to emit the PTT event
|
||||
if (pilotSocket.connected) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { create } from "zustand";
|
||||
import { dispatchSocket } from "../../dispatch/socket";
|
||||
import toast from "react-hot-toast";
|
||||
import { HPGnotificationToast } from "_components/customToasts/HPGnotification";
|
||||
import { NotificationPayload } from "@repo/db";
|
||||
|
||||
interface ConnectionStore {
|
||||
status: "connected" | "disconnected" | "connecting" | "error";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { OSMWay } from "@repo/db";
|
||||
import { create } from "zustand";
|
||||
|
||||
interface MapStore {
|
||||
export interface MapStore {
|
||||
contextMenu: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
@@ -10,6 +10,7 @@ interface MapStore {
|
||||
center: L.LatLngExpression;
|
||||
zoom: number;
|
||||
};
|
||||
setMap: (map: MapStore["map"]) => void;
|
||||
openMissionMarker: {
|
||||
id: number;
|
||||
tab: "home" | "details" | "patient" | "log";
|
||||
@@ -67,19 +68,24 @@ export const useMapStore = create<MapStore>((set, get) => ({
|
||||
center: [51.5, 10.5],
|
||||
zoom: 6,
|
||||
},
|
||||
setMap: (map) => {
|
||||
set(() => ({
|
||||
map,
|
||||
}));
|
||||
},
|
||||
searchPopup: null,
|
||||
searchElements: [],
|
||||
setSearchPopup: (popup) =>
|
||||
set((state) => ({
|
||||
set(() => ({
|
||||
searchPopup: popup,
|
||||
})),
|
||||
contextMenu: null,
|
||||
setContextMenu: (contextMenu) =>
|
||||
set((state) => ({
|
||||
set(() => ({
|
||||
contextMenu,
|
||||
})),
|
||||
setSearchElements: (elements) =>
|
||||
set((state) => ({
|
||||
set(() => ({
|
||||
searchElements: elements,
|
||||
})),
|
||||
aircraftTabs: {},
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { Station } from "@repo/db";
|
||||
import { MissionSdsLog, Station } from "@repo/db";
|
||||
import { fmsStatusDescription } from "_data/fmsStatusDescription";
|
||||
import { DisplayLineProps } from "pilot/_components/mrt/Mrt";
|
||||
import { create } from "zustand";
|
||||
import { syncTabs } from "zustand-sync-tabs";
|
||||
|
||||
interface SetSdsPageParams {
|
||||
page: "sds";
|
||||
station: Station;
|
||||
sdsMessage: MissionSdsLog;
|
||||
}
|
||||
|
||||
interface SetHomePageParams {
|
||||
page: "home";
|
||||
station: Station;
|
||||
@@ -23,6 +29,7 @@ interface SetNewStatusPageParams {
|
||||
type SetPageParams =
|
||||
| SetHomePageParams
|
||||
| SetSendingStatusPageParams
|
||||
| SetSdsPageParams
|
||||
| SetNewStatusPageParams;
|
||||
|
||||
interface MrtStore {
|
||||
@@ -123,6 +130,25 @@ export const useMrtStore = create<MrtStore>(
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "sds": {
|
||||
const { sdsMessage } = pageData as SetSdsPageParams;
|
||||
set({
|
||||
page: "sds",
|
||||
lines: [
|
||||
{
|
||||
textLeft: `neue SDS-Nachricht`,
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "2",
|
||||
},
|
||||
{
|
||||
textLeft: sdsMessage.data.message,
|
||||
style: {},
|
||||
textSize: "1",
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
set({ page: "home" });
|
||||
break;
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { create } from "zustand";
|
||||
import { dispatchSocket } from "../../dispatch/socket";
|
||||
import { ConnectedAircraft, Mission, Station, User } from "@repo/db";
|
||||
import {
|
||||
ConnectedAircraft,
|
||||
Mission,
|
||||
MissionSdsLog,
|
||||
NotificationPayload,
|
||||
Station,
|
||||
User,
|
||||
} from "@repo/db";
|
||||
import { pilotSocket } from "pilot/socket";
|
||||
import { useDmeStore } from "_store/pilot/dmeStore";
|
||||
import { useMrtStore } from "_store/pilot/MrtStore";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface ConnectionStore {
|
||||
status: "connected" | "disconnected" | "connecting" | "error";
|
||||
@@ -103,3 +112,13 @@ pilotSocket.on("mission-alert", (data: Mission & { Stations: Station[] }) => {
|
||||
page: "new-mission",
|
||||
});
|
||||
});
|
||||
|
||||
pilotSocket.on("sds-message", (sdsMessage: MissionSdsLog) => {
|
||||
const station = usePilotConnectionStore.getState().selectedStation;
|
||||
if (!station) return;
|
||||
useMrtStore.getState().setPage({
|
||||
page: "sds",
|
||||
station,
|
||||
sdsMessage,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
"use client";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { useMapStore } from "_store/mapStore";
|
||||
import { MapContainer } from "react-leaflet";
|
||||
import { BaseMaps } from "dispatch/_components/map/BaseMaps";
|
||||
import { ContextMenu } from "dispatch/_components/map/ContextMenu";
|
||||
import { MissionLayer } from "dispatch/_components/map/MissionMarkers";
|
||||
import { SearchElements } from "dispatch/_components/map/SearchElements";
|
||||
import { AircraftLayer } from "dispatch/_components/map/AircraftMarker";
|
||||
import { MarkerCluster } from "dispatch/_components/map/_components/MarkerCluster";
|
||||
|
||||
const Map = () => {
|
||||
const { map } = useMapStore();
|
||||
|
||||
return (
|
||||
<MapContainer
|
||||
className="flex-1"
|
||||
center={map.center}
|
||||
zoom={map.zoom}
|
||||
fadeAnimation={false}
|
||||
>
|
||||
<BaseMaps />
|
||||
<SearchElements />
|
||||
<ContextMenu />
|
||||
<MarkerCluster />
|
||||
<MissionLayer />
|
||||
<AircraftLayer />
|
||||
</MapContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Map;
|
||||
@@ -17,10 +17,12 @@ import {
|
||||
createMissionAPI,
|
||||
editMissionAPI,
|
||||
sendMissionAPI,
|
||||
startHpgValidation,
|
||||
} from "querys/missions";
|
||||
import { getKeywordsAPI } from "querys/keywords";
|
||||
import { getStationsAPI } from "querys/stations";
|
||||
import { useMapStore } from "_store/mapStore";
|
||||
import { getConnectedAircraftsAPI } from "querys/aircrafts";
|
||||
|
||||
export const MissionForm = () => {
|
||||
const { isEditingMission, editingMissionId, setEditingMission } =
|
||||
@@ -33,6 +35,12 @@ export const MissionForm = () => {
|
||||
queryFn: () => getKeywordsAPI(),
|
||||
});
|
||||
|
||||
const { data: aircrafts } = useQuery({
|
||||
queryKey: ["aircrafts"],
|
||||
queryFn: getConnectedAircraftsAPI,
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
|
||||
const { data: stations } = useQuery({
|
||||
queryKey: ["stations"],
|
||||
queryFn: () => getStationsAPI(),
|
||||
@@ -105,8 +113,11 @@ export const MissionForm = () => {
|
||||
});
|
||||
const { missionFormValues, setOpen } = usePannelStore((state) => state);
|
||||
|
||||
const missionInfoText = form.watch("missionAdditionalInfo");
|
||||
const hpgMissionString = form.watch("hpgMissionString");
|
||||
const validationRequired = /* form.watch("missionStationIds")?.some((id) => {
|
||||
const aircraft = aircrafts?.find((a) => a.stationId === id);
|
||||
|
||||
return aircraft?.posH145active;
|
||||
}) && form.watch("hpgMissionString")?.length !== 0; */ true;
|
||||
|
||||
useEffect(() => {
|
||||
if (session.data?.user.id) {
|
||||
@@ -296,14 +307,11 @@ export const MissionForm = () => {
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
{form.watch("hpgMissionString") &&
|
||||
form.watch("hpgMissionString") !== "" && (
|
||||
<p className="text-sm text-error">
|
||||
Szenario wird vor Alarmierung HPG-Validiert. <br />
|
||||
Achte nach dem Vorbereiten / Alarmieren auf den Status der
|
||||
Mission.
|
||||
</p>
|
||||
)}
|
||||
{validationRequired && (
|
||||
<p className="text-sm text-warning">
|
||||
Szenario wird vor Alarmierung HPG-Validiert.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -356,6 +364,9 @@ export const MissionForm = () => {
|
||||
: mission.missionAdditionalInfo,
|
||||
},
|
||||
});
|
||||
if (validationRequired) {
|
||||
await startHpgValidation(newMission.id);
|
||||
}
|
||||
toast.success(
|
||||
`Einsatz ${newMission.id} erfolgreich aktualisiert`,
|
||||
);
|
||||
@@ -391,7 +402,13 @@ export const MissionForm = () => {
|
||||
? `HPG-Szenario: ${hpgSzenario}`
|
||||
: mission.missionAdditionalInfo,
|
||||
});
|
||||
await sendAlertMutation.mutateAsync(newMission.id);
|
||||
if (validationRequired) {
|
||||
await startHpgValidation(newMission.id, {
|
||||
alertWhenValid: true,
|
||||
});
|
||||
} else {
|
||||
await sendAlertMutation.mutateAsync(newMission.id);
|
||||
}
|
||||
setSeachOSMElements([]); // Reset search elements
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
@@ -421,7 +438,7 @@ export const MissionForm = () => {
|
||||
: mission.missionAdditionalInfo,
|
||||
});
|
||||
setSeachOSMElements([]); // Reset search elements
|
||||
|
||||
await startHpgValidation(newMission.id);
|
||||
toast.success(`Einsatz ${newMission.publicId} erstellt`);
|
||||
form.reset();
|
||||
setOpen(false);
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
interface ToastCard {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const MapToastCard2 = () => {
|
||||
const [cards, setCards] = useState<ToastCard[]>([]);
|
||||
const [openCardId, setOpenCardId] = useState<number | null>(null);
|
||||
|
||||
const addCard = () => {
|
||||
const newCard: ToastCard = {
|
||||
id: Date.now(),
|
||||
title: `Einsatz #${cards.length + 1}`,
|
||||
content: `Inhalt von Einsatz #${cards.length + 1}.`,
|
||||
};
|
||||
setCards([...cards, newCard]);
|
||||
// DEBUG
|
||||
/* toast("😖 Christoph 31 sendet Status 4", {
|
||||
duration: 10000,
|
||||
}); */
|
||||
// DEBUG
|
||||
const toastId = toast.custom(
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
lineHeight: 1.3,
|
||||
willChange: "transform",
|
||||
boxShadow:
|
||||
"0 3px 10px rgba(0, 0, 0, 0.1), 0 3px 3px rgba(0, 0, 0, 0.05)",
|
||||
maxWidth: "350px",
|
||||
pointerEvents: "auto",
|
||||
padding: "8px 10px",
|
||||
borderRadius: "8px",
|
||||
background: "var(--color-base-100)",
|
||||
color: "var(--color-base-content)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="toastText flex items-center"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
margin: "4px 10px",
|
||||
color: "inherit",
|
||||
flex: "1 1 auto",
|
||||
whiteSpace: "pre-line",
|
||||
}}
|
||||
>
|
||||
😖 Christoph 31 sendet Status 5{" "}
|
||||
<button
|
||||
className="btn btn-sm btn-soft btn-accent ml-2"
|
||||
onClick={() => toast.remove(toastId)}
|
||||
>
|
||||
U
|
||||
</button>
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
duration: 999999999,
|
||||
},
|
||||
);
|
||||
// DEBUG
|
||||
};
|
||||
|
||||
const removeCard = (id: number) => {
|
||||
setCards(cards.filter((card) => card.id !== id));
|
||||
};
|
||||
|
||||
const toggleCard = (id: number) => {
|
||||
setOpenCardId(openCardId === id ? null : id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute top-4 right-4 z-[1000] flex flex-col space-y-4">
|
||||
{/* DEBUG */}
|
||||
<button
|
||||
onClick={addCard}
|
||||
className="mb-4 p-2 bg-blue-500 text-white rounded self-end"
|
||||
>
|
||||
Debug Einsatz
|
||||
</button>
|
||||
{/* DEBUG */}
|
||||
{cards.map((card) => (
|
||||
<div
|
||||
key={card.id}
|
||||
className="collapse collapse-arrow bg-base-100 border-base-300 border w-120 relative"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="absolute top-0 left-0 opacity-0"
|
||||
checked={openCardId === card.id}
|
||||
onChange={() => toggleCard(card.id)}
|
||||
/>
|
||||
<div className="collapse-title font-semibold flex justify-between items-center">
|
||||
<span>{card.title}</span>
|
||||
<button
|
||||
className="btn btn-sm btn-circle btn-ghost z-10 absolute top-3.5 right-8"
|
||||
onClick={(e) => {
|
||||
removeCard(card.id);
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="collapse-content text-sm">{card.content}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MapToastCard2;
|
||||
@@ -6,7 +6,7 @@ import { cn } from "helpers/cn";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Chat } from "../_components/left/Chat";
|
||||
import { Report } from "../_components/left/Report";
|
||||
const Map = dynamic(() => import("./_components/map/Map"), { ssr: false });
|
||||
const Map = dynamic(() => import("../_components/map/Map"), { ssr: false });
|
||||
|
||||
const DispatchPage = () => {
|
||||
const { isOpen } = usePannelStore();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ConnectedAircraft, ConnectedDispatcher } from "@repo/db";
|
||||
import axios from "axios";
|
||||
import { getSession } from "next-auth/react";
|
||||
|
||||
export const serverApi = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL,
|
||||
@@ -9,6 +10,22 @@ export const serverApi = axios.create({
|
||||
},
|
||||
});
|
||||
|
||||
serverApi.interceptors.request.use(
|
||||
async (config) => {
|
||||
const session = await getSession();
|
||||
const token = session?.user.id; /* session?.accessToken */ // abhängig von deinem NextAuth setup
|
||||
|
||||
if (token) {
|
||||
config.headers.Authorization = `User ${token}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
export const getConenctedUsers = async (): Promise<
|
||||
(ConnectedDispatcher | ConnectedAircraft)[]
|
||||
> => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CSSProperties, useEffect } from "react";
|
||||
import { CSSProperties } from "react";
|
||||
import MrtImage from "./MRT.png";
|
||||
import { useButtons } from "./useButtons";
|
||||
import { useSounds } from "./useSounds";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Chat } from "../_components/left/Chat";
|
||||
import { Report } from "../_components/left/Report";
|
||||
import { Dme } from "pilot/_components/dme/Dme";
|
||||
import dynamic from "next/dynamic";
|
||||
const Map = dynamic(() => import("../dispatch/_components/map/Map"), {
|
||||
const Map = dynamic(() => import("../_components/map/Map"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Mission, Prisma } from "@repo/db";
|
||||
import { Mission, MissionSdsLog, Prisma } from "@repo/db";
|
||||
import axios from "axios";
|
||||
import { serverApi } from "helpers/axios";
|
||||
|
||||
@@ -26,6 +26,29 @@ export const editMissionAPI = async (
|
||||
const respone = await serverApi.patch<Mission>(`/mission/${id}`, mission);
|
||||
return respone.data;
|
||||
};
|
||||
export const sendSdsMessageAPI = async (
|
||||
id: number,
|
||||
sdsMessage: MissionSdsLog,
|
||||
) => {
|
||||
const respone = await serverApi.post<Mission>(
|
||||
`/mission/${id}/send-sds`,
|
||||
sdsMessage,
|
||||
);
|
||||
return respone.data;
|
||||
};
|
||||
|
||||
export const startHpgValidation = async (
|
||||
id: number,
|
||||
config?: {
|
||||
alertWhenValid?: boolean;
|
||||
},
|
||||
) => {
|
||||
const respone = await serverApi.post<Mission>(
|
||||
`/mission/${id}/validate-hpg`,
|
||||
config,
|
||||
);
|
||||
return respone.data;
|
||||
};
|
||||
|
||||
export const sendMissionAPI = async (
|
||||
id: number,
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-leaflet": "^5.0.0-rc.2",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwindcss": "^4.0.14",
|
||||
|
||||
Binary file not shown.
35
package-lock.json
generated
35
package-lock.json
generated
@@ -40,6 +40,7 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-leaflet": "^5.0.0-rc.2",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwindcss": "^4.0.14",
|
||||
@@ -63,6 +64,7 @@
|
||||
"@redis/json": "^1.0.7",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"axios": "^1.7.9",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^4.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
@@ -77,6 +79,7 @@
|
||||
"devDependencies": {
|
||||
"@repo/db": "*",
|
||||
"@repo/typescript-config": "*",
|
||||
"@types/cookie-parser": "^1.4.8",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
@@ -3122,6 +3125,16 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie-parser": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz",
|
||||
"integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
|
||||
@@ -5330,6 +5343,28 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
|
||||
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser/node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
|
||||
21
packages/database/prisma/json/SocketEvents.ts
Normal file
21
packages/database/prisma/json/SocketEvents.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Mission } from "../../generated/client";
|
||||
|
||||
interface ValidationFailed {
|
||||
type: "hpg-validation";
|
||||
status: "failed";
|
||||
message: string;
|
||||
data: {
|
||||
mission: Mission;
|
||||
};
|
||||
}
|
||||
|
||||
interface ValidationSuccess {
|
||||
type: "hpg-validation";
|
||||
status: "success";
|
||||
message: string;
|
||||
data: {
|
||||
mission: Mission;
|
||||
};
|
||||
}
|
||||
|
||||
export type NotificationPayload = ValidationFailed | ValidationSuccess;
|
||||
@@ -2,3 +2,4 @@ export * from "./ParticipantLog";
|
||||
export * from "./MissionVehicleLog";
|
||||
export * from "./User";
|
||||
export * from "./OSMway";
|
||||
export * from "./SocketEvents";
|
||||
|
||||
Reference in New Issue
Block a user