added Callback and custon notification Toast, client notification event handler
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"printWidth": 80,
|
"printWidth": 100,
|
||||||
"singleQuote": false
|
"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;
|
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 { handleSendMessage } from "socket-events/send-message";
|
||||||
import { handleConnectPilot } from "socket-events/connect-pilot";
|
import { handleConnectPilot } from "socket-events/connect-pilot";
|
||||||
import { handleConnectDesktop } from "socket-events/connect-desktop";
|
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 app = express();
|
||||||
const server = createServer(app);
|
const server = createServer(app);
|
||||||
@@ -31,6 +43,8 @@ io.on("connection", (socket) => {
|
|||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(authMiddleware as any);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
server.listen(process.env.PORT, () => {
|
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": {
|
"devDependencies": {
|
||||||
"@repo/db": "*",
|
"@repo/db": "*",
|
||||||
"@repo/typescript-config": "*",
|
"@repo/typescript-config": "*",
|
||||||
|
"@types/cookie-parser": "^1.4.8",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.13.5",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.17",
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"@redis/json": "^1.0.7",
|
"@redis/json": "^1.0.7",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cron": "^4.1.0",
|
"cron": "^4.1.0",
|
||||||
"dotenv": "^16.4.7",
|
"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 { Router } from "express";
|
||||||
import { io } from "../index";
|
import { io } from "../index";
|
||||||
import { sendNtfyMission } from "modules/ntfy";
|
import { sendNtfyMission } from "modules/ntfy";
|
||||||
|
import { sendAlert } from "modules/mission";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -107,90 +116,8 @@ router.post("/:id/send-alert", async (req, res) => {
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { stationId } = req.body as { stationId?: number };
|
const { stationId } = req.body as { stationId?: number };
|
||||||
try {
|
try {
|
||||||
const mission = await prisma.mission.findUnique({
|
const { connectedAircrafts, mission } = await sendAlert(Number(id), {
|
||||||
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,
|
stationId,
|
||||||
})),
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Ignore if the entry already exists
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.mission.update({
|
|
||||||
where: { id: Number(id) },
|
|
||||||
data: {
|
|
||||||
state: "running",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).json({
|
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) => {
|
router.post("/:id/validate-hpg", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
console.log(req.user);
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
const config = req.body as
|
||||||
|
| {
|
||||||
|
alertWhenValid?: boolean;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
const mission = await prisma.mission.findFirstOrThrow({
|
const mission = await prisma.mission.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: Number(id),
|
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({
|
res.json({
|
||||||
message: "HPG validation started",
|
message: "HPG validation started",
|
||||||
});
|
});
|
||||||
|
|
||||||
io.to(`desktop:${activeAircraftinMission}`).emit(
|
/* io.to(`desktop:${activeAircraftinMission}`).emit(
|
||||||
"hpg-validation",
|
"hpg-validation",
|
||||||
{
|
{
|
||||||
hpgMissionType: mission?.hpgMissionString,
|
hpgMissionType: mission?.hpgMissionString,
|
||||||
@@ -266,8 +215,41 @@ router.post("/:id/validate-hpg", async (req, res) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
io.to("dispatchers").emit("update-mission", newMission);
|
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) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.json({ error: (error as Error).message || "Failed to validate HPG" });
|
res.json({ error: (error as Error).message || "Failed to validate HPG" });
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"jsx": "react"
|
"jsx": "react"
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "./index.ts"],
|
"include": ["**/*.ts", "./index.ts", "**/*.d.ts"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import { toast } from "react-hot-toast";
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
import { dispatchSocket } from "dispatch/socket";
|
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 }) {
|
export function QueryProvider({ children }: { children: ReactNode }) {
|
||||||
|
const mapStore = useMapStore((s) => s);
|
||||||
const [queryClient] = useState(
|
const [queryClient] = useState(
|
||||||
() =>
|
() =>
|
||||||
new QueryClient({
|
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("update-mission", invalidateMission);
|
||||||
dispatchSocket.on("delete-mission", invalidateMission);
|
dispatchSocket.on("delete-mission", invalidateMission);
|
||||||
dispatchSocket.on("new-mission", invalidateMission);
|
dispatchSocket.on("new-mission", invalidateMission);
|
||||||
dispatchSocket.on("dispatchers-update", invalidateConnectedUsers);
|
dispatchSocket.on("dispatchers-update", invalidateConnectedUsers);
|
||||||
dispatchSocket.on("pilots-update", invalidateConnectedUsers);
|
dispatchSocket.on("pilots-update", invalidateConnectedUsers);
|
||||||
dispatchSocket.on("update-connectedAircraft", invalidateConenctedAircrafts);
|
dispatchSocket.on("update-connectedAircraft", invalidateConenctedAircrafts);
|
||||||
}, [queryClient]);
|
dispatchSocket.on("notification", handleNotification);
|
||||||
|
|
||||||
return (
|
return () => {
|
||||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
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,
|
TextSearch,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { editMissionAPI } from "querys/missions";
|
import { editMissionAPI, sendSdsMessageAPI } from "querys/missions";
|
||||||
|
|
||||||
const FMSStatusHistory = ({
|
const FMSStatusHistory = ({
|
||||||
aircraft,
|
aircraft,
|
||||||
@@ -298,17 +298,18 @@ const SDSTab = ({
|
|||||||
const [note, setNote] = useState("");
|
const [note, setNote] = useState("");
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const editMissionMutation = useMutation({
|
const sendSdsMutation = useMutation({
|
||||||
mutationFn: ({
|
mutationFn: async ({
|
||||||
id,
|
id,
|
||||||
mission,
|
message,
|
||||||
}: {
|
}: {
|
||||||
id: number;
|
id: number;
|
||||||
mission: Partial<Prisma.MissionUpdateInput>;
|
message: MissionSdsLog;
|
||||||
}) => editMissionAPI(id, mission),
|
}) => {
|
||||||
mutationKey: ["missions"],
|
await sendSdsMessageAPI(id, message);
|
||||||
onSuccess: () => {
|
queryClient.invalidateQueries({
|
||||||
queryClient.invalidateQueries({ queryKey: ["missions"] });
|
queryKey: ["missions"],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -347,9 +348,10 @@ const SDSTab = ({
|
|||||||
className="btn btn-sm btn-primary btn-outline"
|
className="btn btn-sm btn-primary btn-outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!mission) return;
|
if (!mission) return;
|
||||||
const newMissionLog = [
|
sendSdsMutation
|
||||||
...mission.missionLog,
|
.mutateAsync({
|
||||||
{
|
id: mission.id,
|
||||||
|
message: {
|
||||||
type: "sds-log",
|
type: "sds-log",
|
||||||
auto: false,
|
auto: false,
|
||||||
timeStamp: new Date().toISOString(),
|
timeStamp: new Date().toISOString(),
|
||||||
@@ -359,14 +361,6 @@ const SDSTab = ({
|
|||||||
message: note,
|
message: note,
|
||||||
user: getPublicUser(session.data!.user),
|
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(() => {
|
.then(() => {
|
||||||
@@ -11,11 +11,11 @@ import { useMapStore } from "_store/mapStore";
|
|||||||
import {
|
import {
|
||||||
FMS_STATUS_COLORS,
|
FMS_STATUS_COLORS,
|
||||||
FMS_STATUS_TEXT_COLORS,
|
FMS_STATUS_TEXT_COLORS,
|
||||||
} from "dispatch/_components/map/AircraftMarker";
|
} from "_components/map/AircraftMarker";
|
||||||
import {
|
import {
|
||||||
MISSION_STATUS_COLORS,
|
MISSION_STATUS_COLORS,
|
||||||
MISSION_STATUS_TEXT_COLORS,
|
MISSION_STATUS_TEXT_COLORS,
|
||||||
} from "dispatch/_components/map/MissionMarkers";
|
} from "_components/map/MissionMarkers";
|
||||||
import { cn } from "helpers/cn";
|
import { cn } from "helpers/cn";
|
||||||
import { checkSimulatorConnected } from "helpers/simulatorConnected";
|
import { checkSimulatorConnected } from "helpers/simulatorConnected";
|
||||||
import { getConnectedAircraftsAPI } from "querys/aircrafts";
|
import { getConnectedAircraftsAPI } from "querys/aircrafts";
|
||||||
@@ -367,17 +367,12 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dispatcherConnected =
|
|
||||||
useDispatchConnectionStore((s) => s.status) === "connected";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 text-base-content">
|
<div className="p-4 text-base-content">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center w-full justify-between">
|
||||||
<h2 className="flex items-center gap-2 text-lg font-bold">
|
<h2 className="flex items-center gap-2 text-lg font-bold mb-3">
|
||||||
<SmartphoneNfc /> Rettungsmittel
|
<SmartphoneNfc /> Rettungsmittel
|
||||||
</h2>
|
</h2>
|
||||||
{dispatcherConnected && (
|
|
||||||
<div className="space-x-2">
|
|
||||||
<div
|
<div
|
||||||
className="tooltip tooltip-primary tooltip-left font-semibold"
|
className="tooltip tooltip-primary tooltip-left font-semibold"
|
||||||
data-tip="Einsatz erneut alarmieren"
|
data-tip="Einsatz erneut alarmieren"
|
||||||
@@ -392,8 +387,6 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto">
|
<ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto">
|
||||||
{missionStations?.map((station, index) => {
|
{missionStations?.map((station, index) => {
|
||||||
const connectedAircraft = conenctedAircrafts?.find(
|
const connectedAircraft = conenctedAircrafts?.find(
|
||||||
@@ -427,25 +420,75 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
{dispatcherConnected && (
|
|
||||||
<div>
|
|
||||||
<div className="divider mt-0 mb-0" />
|
<div className="divider mt-0 mb-0" />
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* TODO: make it a small multiselect */}
|
{/* TODO: make it a small multiselect */}
|
||||||
<select className="select select-sm select-primary select-bordered flex-1">
|
<select
|
||||||
<option value="1">Feuerwehr</option>
|
className="select select-sm select-primary select-bordered flex-1"
|
||||||
<option value="2">RTW</option>
|
onChange={(e) => {
|
||||||
<option value="3">Polizei</option>
|
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>
|
</select>
|
||||||
<button className="btn btn-sm btn-primary btn-outline">
|
<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">
|
<span className="text-base-content flex items-center gap-2">
|
||||||
<BellRing size={16} /> Nachalarmieren
|
<BellRing size={16} /> Nachalarmieren
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,6 +47,7 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
|||||||
const { room, isTalking } = get();
|
const { room, isTalking } = get();
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
room.localParticipant.setMicrophoneEnabled(!isTalking);
|
room.localParticipant.setMicrophoneEnabled(!isTalking);
|
||||||
|
|
||||||
if (!isTalking) {
|
if (!isTalking) {
|
||||||
// If old status was not talking, we need to emit the PTT event
|
// If old status was not talking, we need to emit the PTT event
|
||||||
if (pilotSocket.connected) {
|
if (pilotSocket.connected) {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { dispatchSocket } from "../../dispatch/socket";
|
import { dispatchSocket } from "../../dispatch/socket";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { HPGnotificationToast } from "_components/customToasts/HPGnotification";
|
||||||
|
import { NotificationPayload } from "@repo/db";
|
||||||
|
|
||||||
interface ConnectionStore {
|
interface ConnectionStore {
|
||||||
status: "connected" | "disconnected" | "connecting" | "error";
|
status: "connected" | "disconnected" | "connecting" | "error";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { OSMWay } from "@repo/db";
|
import { OSMWay } from "@repo/db";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
interface MapStore {
|
export interface MapStore {
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
lat: number;
|
lat: number;
|
||||||
lng: number;
|
lng: number;
|
||||||
@@ -10,6 +10,7 @@ interface MapStore {
|
|||||||
center: L.LatLngExpression;
|
center: L.LatLngExpression;
|
||||||
zoom: number;
|
zoom: number;
|
||||||
};
|
};
|
||||||
|
setMap: (map: MapStore["map"]) => void;
|
||||||
openMissionMarker: {
|
openMissionMarker: {
|
||||||
id: number;
|
id: number;
|
||||||
tab: "home" | "details" | "patient" | "log";
|
tab: "home" | "details" | "patient" | "log";
|
||||||
@@ -67,19 +68,24 @@ export const useMapStore = create<MapStore>((set, get) => ({
|
|||||||
center: [51.5, 10.5],
|
center: [51.5, 10.5],
|
||||||
zoom: 6,
|
zoom: 6,
|
||||||
},
|
},
|
||||||
|
setMap: (map) => {
|
||||||
|
set(() => ({
|
||||||
|
map,
|
||||||
|
}));
|
||||||
|
},
|
||||||
searchPopup: null,
|
searchPopup: null,
|
||||||
searchElements: [],
|
searchElements: [],
|
||||||
setSearchPopup: (popup) =>
|
setSearchPopup: (popup) =>
|
||||||
set((state) => ({
|
set(() => ({
|
||||||
searchPopup: popup,
|
searchPopup: popup,
|
||||||
})),
|
})),
|
||||||
contextMenu: null,
|
contextMenu: null,
|
||||||
setContextMenu: (contextMenu) =>
|
setContextMenu: (contextMenu) =>
|
||||||
set((state) => ({
|
set(() => ({
|
||||||
contextMenu,
|
contextMenu,
|
||||||
})),
|
})),
|
||||||
setSearchElements: (elements) =>
|
setSearchElements: (elements) =>
|
||||||
set((state) => ({
|
set(() => ({
|
||||||
searchElements: elements,
|
searchElements: elements,
|
||||||
})),
|
})),
|
||||||
aircraftTabs: {},
|
aircraftTabs: {},
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { Station } from "@repo/db";
|
import { MissionSdsLog, Station } from "@repo/db";
|
||||||
import { fmsStatusDescription } from "_data/fmsStatusDescription";
|
import { fmsStatusDescription } from "_data/fmsStatusDescription";
|
||||||
import { DisplayLineProps } from "pilot/_components/mrt/Mrt";
|
import { DisplayLineProps } from "pilot/_components/mrt/Mrt";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { syncTabs } from "zustand-sync-tabs";
|
import { syncTabs } from "zustand-sync-tabs";
|
||||||
|
|
||||||
|
interface SetSdsPageParams {
|
||||||
|
page: "sds";
|
||||||
|
station: Station;
|
||||||
|
sdsMessage: MissionSdsLog;
|
||||||
|
}
|
||||||
|
|
||||||
interface SetHomePageParams {
|
interface SetHomePageParams {
|
||||||
page: "home";
|
page: "home";
|
||||||
station: Station;
|
station: Station;
|
||||||
@@ -23,6 +29,7 @@ interface SetNewStatusPageParams {
|
|||||||
type SetPageParams =
|
type SetPageParams =
|
||||||
| SetHomePageParams
|
| SetHomePageParams
|
||||||
| SetSendingStatusPageParams
|
| SetSendingStatusPageParams
|
||||||
|
| SetSdsPageParams
|
||||||
| SetNewStatusPageParams;
|
| SetNewStatusPageParams;
|
||||||
|
|
||||||
interface MrtStore {
|
interface MrtStore {
|
||||||
@@ -123,6 +130,25 @@ export const useMrtStore = create<MrtStore>(
|
|||||||
});
|
});
|
||||||
break;
|
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:
|
default:
|
||||||
set({ page: "home" });
|
set({ page: "home" });
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { dispatchSocket } from "../../dispatch/socket";
|
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 { pilotSocket } from "pilot/socket";
|
||||||
import { useDmeStore } from "_store/pilot/dmeStore";
|
import { useDmeStore } from "_store/pilot/dmeStore";
|
||||||
|
import { useMrtStore } from "_store/pilot/MrtStore";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
interface ConnectionStore {
|
interface ConnectionStore {
|
||||||
status: "connected" | "disconnected" | "connecting" | "error";
|
status: "connected" | "disconnected" | "connecting" | "error";
|
||||||
@@ -103,3 +112,13 @@ pilotSocket.on("mission-alert", (data: Mission & { Stations: Station[] }) => {
|
|||||||
page: "new-mission",
|
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,
|
createMissionAPI,
|
||||||
editMissionAPI,
|
editMissionAPI,
|
||||||
sendMissionAPI,
|
sendMissionAPI,
|
||||||
|
startHpgValidation,
|
||||||
} from "querys/missions";
|
} from "querys/missions";
|
||||||
import { getKeywordsAPI } from "querys/keywords";
|
import { getKeywordsAPI } from "querys/keywords";
|
||||||
import { getStationsAPI } from "querys/stations";
|
import { getStationsAPI } from "querys/stations";
|
||||||
import { useMapStore } from "_store/mapStore";
|
import { useMapStore } from "_store/mapStore";
|
||||||
|
import { getConnectedAircraftsAPI } from "querys/aircrafts";
|
||||||
|
|
||||||
export const MissionForm = () => {
|
export const MissionForm = () => {
|
||||||
const { isEditingMission, editingMissionId, setEditingMission } =
|
const { isEditingMission, editingMissionId, setEditingMission } =
|
||||||
@@ -33,6 +35,12 @@ export const MissionForm = () => {
|
|||||||
queryFn: () => getKeywordsAPI(),
|
queryFn: () => getKeywordsAPI(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: aircrafts } = useQuery({
|
||||||
|
queryKey: ["aircrafts"],
|
||||||
|
queryFn: getConnectedAircraftsAPI,
|
||||||
|
refetchInterval: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
const { data: stations } = useQuery({
|
const { data: stations } = useQuery({
|
||||||
queryKey: ["stations"],
|
queryKey: ["stations"],
|
||||||
queryFn: () => getStationsAPI(),
|
queryFn: () => getStationsAPI(),
|
||||||
@@ -105,8 +113,11 @@ export const MissionForm = () => {
|
|||||||
});
|
});
|
||||||
const { missionFormValues, setOpen } = usePannelStore((state) => state);
|
const { missionFormValues, setOpen } = usePannelStore((state) => state);
|
||||||
|
|
||||||
const missionInfoText = form.watch("missionAdditionalInfo");
|
const validationRequired = /* form.watch("missionStationIds")?.some((id) => {
|
||||||
const hpgMissionString = form.watch("hpgMissionString");
|
const aircraft = aircrafts?.find((a) => a.stationId === id);
|
||||||
|
|
||||||
|
return aircraft?.posH145active;
|
||||||
|
}) && form.watch("hpgMissionString")?.length !== 0; */ true;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session.data?.user.id) {
|
if (session.data?.user.id) {
|
||||||
@@ -296,12 +307,9 @@ export const MissionForm = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
{form.watch("hpgMissionString") &&
|
{validationRequired && (
|
||||||
form.watch("hpgMissionString") !== "" && (
|
<p className="text-sm text-warning">
|
||||||
<p className="text-sm text-error">
|
Szenario wird vor Alarmierung HPG-Validiert.
|
||||||
Szenario wird vor Alarmierung HPG-Validiert. <br />
|
|
||||||
Achte nach dem Vorbereiten / Alarmieren auf den Status der
|
|
||||||
Mission.
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -356,6 +364,9 @@ export const MissionForm = () => {
|
|||||||
: mission.missionAdditionalInfo,
|
: mission.missionAdditionalInfo,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (validationRequired) {
|
||||||
|
await startHpgValidation(newMission.id);
|
||||||
|
}
|
||||||
toast.success(
|
toast.success(
|
||||||
`Einsatz ${newMission.id} erfolgreich aktualisiert`,
|
`Einsatz ${newMission.id} erfolgreich aktualisiert`,
|
||||||
);
|
);
|
||||||
@@ -391,7 +402,13 @@ export const MissionForm = () => {
|
|||||||
? `HPG-Szenario: ${hpgSzenario}`
|
? `HPG-Szenario: ${hpgSzenario}`
|
||||||
: mission.missionAdditionalInfo,
|
: mission.missionAdditionalInfo,
|
||||||
});
|
});
|
||||||
|
if (validationRequired) {
|
||||||
|
await startHpgValidation(newMission.id, {
|
||||||
|
alertWhenValid: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
await sendAlertMutation.mutateAsync(newMission.id);
|
await sendAlertMutation.mutateAsync(newMission.id);
|
||||||
|
}
|
||||||
setSeachOSMElements([]); // Reset search elements
|
setSeachOSMElements([]); // Reset search elements
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -421,7 +438,7 @@ export const MissionForm = () => {
|
|||||||
: mission.missionAdditionalInfo,
|
: mission.missionAdditionalInfo,
|
||||||
});
|
});
|
||||||
setSeachOSMElements([]); // Reset search elements
|
setSeachOSMElements([]); // Reset search elements
|
||||||
|
await startHpgValidation(newMission.id);
|
||||||
toast.success(`Einsatz ${newMission.publicId} erstellt`);
|
toast.success(`Einsatz ${newMission.publicId} erstellt`);
|
||||||
form.reset();
|
form.reset();
|
||||||
setOpen(false);
|
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 dynamic from "next/dynamic";
|
||||||
import { Chat } from "../_components/left/Chat";
|
import { Chat } from "../_components/left/Chat";
|
||||||
import { Report } from "../_components/left/Report";
|
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 DispatchPage = () => {
|
||||||
const { isOpen } = usePannelStore();
|
const { isOpen } = usePannelStore();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ConnectedAircraft, ConnectedDispatcher } from "@repo/db";
|
import { ConnectedAircraft, ConnectedDispatcher } from "@repo/db";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { getSession } from "next-auth/react";
|
||||||
|
|
||||||
export const serverApi = axios.create({
|
export const serverApi = axios.create({
|
||||||
baseURL: process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL,
|
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<
|
export const getConenctedUsers = async (): Promise<
|
||||||
(ConnectedDispatcher | ConnectedAircraft)[]
|
(ConnectedDispatcher | ConnectedAircraft)[]
|
||||||
> => {
|
> => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CSSProperties, useEffect } from "react";
|
import { CSSProperties } from "react";
|
||||||
import MrtImage from "./MRT.png";
|
import MrtImage from "./MRT.png";
|
||||||
import { useButtons } from "./useButtons";
|
import { useButtons } from "./useButtons";
|
||||||
import { useSounds } from "./useSounds";
|
import { useSounds } from "./useSounds";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Chat } from "../_components/left/Chat";
|
|||||||
import { Report } from "../_components/left/Report";
|
import { Report } from "../_components/left/Report";
|
||||||
import { Dme } from "pilot/_components/dme/Dme";
|
import { Dme } from "pilot/_components/dme/Dme";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
const Map = dynamic(() => import("../dispatch/_components/map/Map"), {
|
const Map = dynamic(() => import("../_components/map/Map"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Mission, Prisma } from "@repo/db";
|
import { Mission, MissionSdsLog, Prisma } from "@repo/db";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { serverApi } from "helpers/axios";
|
import { serverApi } from "helpers/axios";
|
||||||
|
|
||||||
@@ -26,6 +26,29 @@ export const editMissionAPI = async (
|
|||||||
const respone = await serverApi.patch<Mission>(`/mission/${id}`, mission);
|
const respone = await serverApi.patch<Mission>(`/mission/${id}`, mission);
|
||||||
return respone.data;
|
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 (
|
export const sendMissionAPI = async (
|
||||||
id: number,
|
id: number,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-leaflet": "^5.0.0-rc.2",
|
"react-leaflet": "^5.0.0-rc.2",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"tailwindcss": "^4.0.14",
|
"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": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-leaflet": "^5.0.0-rc.2",
|
"react-leaflet": "^5.0.0-rc.2",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"tailwindcss": "^4.0.14",
|
"tailwindcss": "^4.0.14",
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
"@redis/json": "^1.0.7",
|
"@redis/json": "^1.0.7",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cron": "^4.1.0",
|
"cron": "^4.1.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
@@ -77,6 +79,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/db": "*",
|
"@repo/db": "*",
|
||||||
"@repo/typescript-config": "*",
|
"@repo/typescript-config": "*",
|
||||||
|
"@types/cookie-parser": "^1.4.8",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.13.5",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.17",
|
||||||
@@ -3122,6 +3125,16 @@
|
|||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/cors": {
|
||||||
"version": "2.8.17",
|
"version": "2.8.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
|
||||||
@@ -5330,6 +5343,28 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/cookie-signature": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
"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 "./MissionVehicleLog";
|
||||||
export * from "./User";
|
export * from "./User";
|
||||||
export * from "./OSMway";
|
export * from "./OSMway";
|
||||||
|
export * from "./SocketEvents";
|
||||||
|
|||||||
Reference in New Issue
Block a user