diff --git a/apps/dispatch-server/index.ts b/apps/dispatch-server/index.ts index c75d907f..4cd7c1e0 100644 --- a/apps/dispatch-server/index.ts +++ b/apps/dispatch-server/index.ts @@ -9,11 +9,12 @@ import { handleConnectDispatch } from "socket-events/connect-dispatch"; import router from "routes/router"; import cors from "cors"; import { handleSendMessage } from "socket-events/send-message"; +import { handleConnectPilot } from "socket-events/connect-pilot"; const app = express(); const server = createServer(app); -const io = new Server(server, { +export const io = new Server(server, { adapter: createAdapter(pubClient, subClient), cors: {}, }); @@ -21,6 +22,7 @@ io.use(jwtMiddleware); io.on("connection", (socket) => { socket.on("connect-dispatch", handleConnectDispatch(socket, io)); + socket.on("connect-pilot", handleConnectPilot(socket, io)); socket.on("send-message", handleSendMessage(socket, io)); }); diff --git a/apps/dispatch-server/routes/mission.ts b/apps/dispatch-server/routes/mission.ts index a3356188..3c8596f6 100644 --- a/apps/dispatch-server/routes/mission.ts +++ b/apps/dispatch-server/routes/mission.ts @@ -1,5 +1,6 @@ import { prisma } from "@repo/db"; import { Router } from "express"; +import { io } from "../index"; const router = Router(); @@ -41,6 +42,7 @@ router.put("/", async (req, res) => { const newMission = await prisma.mission.create({ data: req.body, }); + io.to("missions").emit("new-mission", newMission); res.status(201).json(newMission); } catch (error) { res.status(500).json({ error: "Failed to create mission" }); @@ -55,6 +57,7 @@ router.patch("/:id", async (req, res) => { where: { id: Number(id) }, data: req.body, }); + io.to("missions").emit("update-mission", updatedMission); res.json(updatedMission); } catch (error) { console.error(error); @@ -69,6 +72,7 @@ router.delete("/:id", async (req, res) => { await prisma.mission.delete({ where: { id: Number(id) }, }); + io.to("missions").emit("delete-mission", id); res.status(204).send(); } catch (error) { console.error(error); diff --git a/apps/dispatch-server/socket-events/connect-dispatch.ts b/apps/dispatch-server/socket-events/connect-dispatch.ts index f5f97878..9fec06db 100644 --- a/apps/dispatch-server/socket-events/connect-dispatch.ts +++ b/apps/dispatch-server/socket-events/connect-dispatch.ts @@ -1,5 +1,4 @@ import { getPublicUser, prisma, User } from "@repo/db"; -import { pubClient } from "modules/redis"; import { Server, Socket } from "socket.io"; export const handleConnectDispatch = @@ -80,9 +79,10 @@ export const handleConnectDispatch = socket.join("dispatchers"); // Dem Dispatcher-Raum beitreten socket.join(`user:${user.id}`); // Dem User-Raum beitreten + socket.join("missions"); - io.to("dispatchers").emit("dispatcher-update"); - io.to("pilots").emit("dispatcher-update"); + io.to("dispatchers").emit("dispatchers-update"); + io.to("pilots").emit("dispatchers-update"); socket.on("disconnect", async () => { console.log("Disconnected from dispatch server"); @@ -94,6 +94,8 @@ export const handleConnectDispatch = logoutTime: new Date().toISOString(), }, }); + io.to("dispatchers").emit("dispatchers-update"); + io.to("pilots").emit("dispatchers-update"); }); socket.on("reconnect", async () => { console.log("Reconnected to dispatch server"); diff --git a/apps/dispatch-server/socket-events/connect-pilot.ts b/apps/dispatch-server/socket-events/connect-pilot.ts index 83b40869..5135195a 100644 --- a/apps/dispatch-server/socket-events/connect-pilot.ts +++ b/apps/dispatch-server/socket-events/connect-pilot.ts @@ -1,25 +1,42 @@ import { getPublicUser, prisma } from "@repo/db"; import { Server, Socket } from "socket.io"; -export const handleConnectDispatch = +export const handleConnectPilot = (socket: Socket, io: Server) => async ({ logoffTime, stationId, }: { logoffTime: string; - stationId: number; + stationId: string; }) => { try { + const user = socket.data.user; // User ID aus dem JWT-Token const userId = socket.data.user.id; // User ID aus dem JWT-Token - console.log("User connected to dispatch server"); - const user = await prisma.user.findUnique({ + + if (!user) return Error("User not found"); + + const existingConnection = await prisma.connectedAircraft.findFirst({ where: { - id: userId, + userId: user.id, + logoutTime: null, }, }); - if (!user) return Error("User not found"); + if (existingConnection) { + await io + .to(`user:${user.id}`) + .emit("force-disconnect", "double-connection"); + await prisma.connectedAircraft.updateMany({ + where: { + userId: user.id, + logoutTime: null, + }, + data: { + logoutTime: new Date().toISOString(), + }, + }); + } let parsedLogoffDate = null; if (logoffTime.length > 0) { @@ -44,7 +61,7 @@ export const handleConnectDispatch = lastHeartbeat: new Date().toISOString(), userId: userId, loginTime: new Date().toISOString(), - stationId: stationId, + stationId: parseInt(stationId), /* user: { connect: { id: userId } }, // Ensure the user relationship is set station: { connect: { id: stationId } }, // Ensure the station relationship is set */ }, @@ -54,8 +71,8 @@ export const handleConnectDispatch = socket.join(`user:${userId}`); // Join the user-specific room socket.join(`station:${stationId}`); // Join the station-specific room - io.to("dispatchers").emit("dispatcher-update"); - io.to("pilots").emit("dispatcher-update"); + io.to("dispatchers").emit("pilots-update"); + io.to("pilots").emit("pilots-update"); // Add a listener for station-specific events socket.on(`station:${stationId}:event`, async (data) => { @@ -66,7 +83,7 @@ export const handleConnectDispatch = socket.on("disconnect", async () => { console.log("Disconnected from dispatch server"); - await prisma.connectedDispatcher.update({ + await prisma.connectedAircraft.update({ where: { id: connectedAircraftEntry.id, }, @@ -74,19 +91,8 @@ export const handleConnectDispatch = logoutTime: new Date().toISOString(), }, }); - }); - - socket.on("reconnect", async () => { - console.log("Reconnected to dispatch server"); - await prisma.connectedDispatcher.update({ - where: { - id: connectedAircraftEntry.id, - }, - data: { - lastHeartbeat: new Date().toISOString(), - logoutTime: null, - }, - }); + io.to("dispatchers").emit("pilots-update"); + io.to("pilots").emit("pilots-update"); }); } catch (error) { console.error("Error connecting to dispatch server:", error); diff --git a/apps/dispatch/app/_components/QueryProvider.tsx b/apps/dispatch/app/_components/QueryProvider.tsx new file mode 100644 index 00000000..5d46b394 --- /dev/null +++ b/apps/dispatch/app/_components/QueryProvider.tsx @@ -0,0 +1,47 @@ +// components/TanstackProvider.tsx +"use client"; + +import { toast } from "react-hot-toast"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactNode, useEffect, useState } from "react"; +import { dispatchSocket } from "dispatch/socket"; + +export function QueryProvider({ children }: { children: ReactNode }) { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + mutations: { + onError: (error) => { + toast.error("An error occurred: " + (error as Error).message, { + position: "top-right", + }); + }, + }, + }, + }), + ); + useEffect(() => { + const invalidateMission = () => { + queryClient.invalidateQueries({ + queryKey: ["missions"], + }); + }; + + const invalidateConnectedUsers = () => { + queryClient.invalidateQueries({ + queryKey: ["connected-users"], + }); + }; + + 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); + }, [queryClient]); + + return ( + {children} + ); +} diff --git a/apps/dispatch/app/_components/StationStatusToast.tsx b/apps/dispatch/app/_components/StationStatusToast.tsx new file mode 100644 index 00000000..f60d725c --- /dev/null +++ b/apps/dispatch/app/_components/StationStatusToast.tsx @@ -0,0 +1,117 @@ +import { useState } from "react"; +import { toast } from "react-hot-toast"; + +interface ToastCard { + id: number; + title: string; + content: string; +} + +const MapToastCard2 = () => { + const [cards, setCards] = useState([]); + const [openCardId, setOpenCardId] = useState(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( +
+
+ 😖 Christoph 31 sendet Status 5{" "} + +
+
, + { + duration: 999999999, + }, + ); + // DEBUG + }; + + const removeCard = (id: number) => { + setCards(cards.filter((card) => card.id !== id)); + }; + + const toggleCard = (id: number) => { + setOpenCardId(openCardId === id ? null : id); + }; + + return ( +
+ {/* DEBUG */} + + {/* DEBUG */} + {cards.map((card) => ( +
+ toggleCard(card.id)} + /> +
+ {card.title} + +
+
{card.content}
+
+ ))} +
+ ); +}; + +export default MapToastCard2; diff --git a/apps/dispatch/app/dispatch/_components/left/Chat.tsx b/apps/dispatch/app/_components/left/Chat.tsx similarity index 80% rename from apps/dispatch/app/dispatch/_components/left/Chat.tsx rename to apps/dispatch/app/_components/left/Chat.tsx index e1fa097d..3ebef93d 100644 --- a/apps/dispatch/app/dispatch/_components/left/Chat.tsx +++ b/apps/dispatch/app/_components/left/Chat.tsx @@ -2,10 +2,11 @@ import { ChatBubbleIcon, PaperPlaneIcon } from "@radix-ui/react-icons"; import { useLeftMenuStore } from "_store/leftMenuStore"; import { useSession } from "next-auth/react"; -import { Fragment, useEffect, useRef, useState } from "react"; +import { Fragment, useEffect, useState } from "react"; import { cn } from "helpers/cn"; -import { getConenctedUsers } from "helpers/axios"; -import { asPublicUser, ConnectedAircraft, ConnectedDispatcher } from "@repo/db"; +import { asPublicUser } from "@repo/db"; +import { useQuery } from "@tanstack/react-query"; +import { getConnectedUserAPI } from "querys/connected-user"; export const Chat = () => { const { @@ -22,49 +23,24 @@ export const Chat = () => { } = useLeftMenuStore(); const [sending, setSending] = useState(false); const session = useSession(); - const [addTabValue, setAddTabValue] = useState(""); + const [addTabValue, setAddTabValue] = useState("default"); const [message, setMessage] = useState(""); - const [connectedUser, setConnectedUser] = useState< - (ConnectedAircraft | ConnectedDispatcher)[] | null - >(null); - const timeout = useRef(null); + + const { data: connectedUser } = useQuery({ + queryKey: ["connected-users"], + queryFn: async () => { + const user = await getConnectedUserAPI(); + return user.filter((u) => u.userId !== session.data?.user.id); + }, + refetchInterval: 10000, + refetchOnWindowFocus: true, + }); useEffect(() => { if (!session.data?.user.id) return; setOwnId(session.data.user.id); }, [session, setOwnId]); - useEffect(() => { - const fetchConnectedUser = async () => { - const data = await getConenctedUsers(); - if (data) { - const filteredConnectedUser = data.filter((user) => { - return ( - user.userId !== session.data?.user.id && - !Object.keys(chats).includes(user.userId) - ); - - return true; - }); - setConnectedUser(filteredConnectedUser); - } - if (!addTabValue && data[0]) setAddTabValue(data[0].userId); - }; - - timeout.current = setInterval(() => { - fetchConnectedUser(); - }, 1000); - fetchConnectedUser(); - - return () => { - if (timeout.current) { - clearInterval(timeout.current); - timeout.current = null; - console.log("cleared"); - } - }; - }, [addTabValue, chats, session.data?.user.id]); - return (
@@ -100,8 +76,16 @@ export const Chat = () => { onChange={(e) => setAddTabValue(e.target.value)} > {!connectedUser?.length && ( - + )} + {connectedUser?.length && ( + + )} + {connectedUser?.map((user) => (