StationStatus Toast hinzugefügt #45

This commit is contained in:
PxlLoewe
2025-07-07 01:55:45 -07:00
parent 7682f191c7
commit 9e4a46c595
13 changed files with 170 additions and 140 deletions

View File

@@ -1,4 +1,11 @@
import { AdminMessage, getPublicUser, MissionLog, Prisma, prisma } from "@repo/db";
import {
AdminMessage,
getPublicUser,
MissionLog,
NotificationPayload,
Prisma,
prisma,
} from "@repo/db";
import { Router } from "express";
import { io } from "../index";
@@ -63,6 +70,23 @@ router.patch("/:id", async (req, res) => {
},
},
});
if (
oldConnectedAircraft &&
updatedConnectedAircraft &&
oldConnectedAircraft.fmsStatus !== updatedConnectedAircraft.fmsStatus
) {
io.to("dispatchers").emit("notification", {
type: "station-status",
status: updatedConnectedAircraft.fmsStatus,
message: "FMS status changed",
data: {
stationId: updatedConnectedAircraft.stationId,
aircraftId: updatedConnectedAircraft.id,
},
} as NotificationPayload);
}
if (
mission &&
aircraftUpdate.fmsStatus &&

View File

@@ -45,7 +45,6 @@ export const handleConnectDispatch =
});
}
let parsedLogoffDate = null;
const [logoffHours, logoffMinutes] = logoffTime.split(":").map(Number);
const connectedDispatcherEntry = await prisma.connectedDispatcher.create({

View File

@@ -101,7 +101,7 @@ export const handleConnectPilot =
await addRolesToMember(discordAccount.discordId.toString(), [DISCORD_ROLES.ONLINE_PILOT]);
}
socket.join("dispatchers"); // Join the dispatchers room
socket.join("pilots"); // Join the pilots room
socket.join(`user:${userId}`); // Join the user-specific room
socket.join(`station:${stationId}`); // Join the station-specific room

View File

@@ -9,7 +9,7 @@ import { ConnectedDispatcher } from "tracker/_components/ConnectedDispatcher";
import { useQuery } from "@tanstack/react-query";
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
import { getAircraftsAPI, getConnectedAircraftsAPI } from "_querys/aircrafts";
import { checkSimulatorConnected } from "_helpers/simulatorConnected";
import { checkSimulatorConnected } from "@repo/shared-components";
import { SimConnectionAlert } from "(app)/pilot/_components/SimConnectionAlert";
const Map = dynamic(() => import("_components/map/Map"), {

View File

@@ -2,7 +2,7 @@
"use client";
import { toast } from "react-hot-toast";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
import { ReactNode, useEffect, useState } from "react";
import { dispatchSocket } from "(app)/dispatch/socket";
import { Mission, NotificationPayload } from "@repo/db";
@@ -10,9 +10,11 @@ import { HPGnotificationToast } from "_components/customToasts/HPGnotification";
import { useMapStore } from "_store/mapStore";
import { AdminMessageToast } from "_components/customToasts/AdminMessage";
import { pilotSocket } from "(app)/pilot/socket";
import { StatusToast } from "_components/customToasts/StationStatusToast";
export function QueryProvider({ children }: { children: ReactNode }) {
const mapStore = useMapStore((s) => s);
const [queryClient] = useState(
() =>
new QueryClient({
@@ -73,6 +75,12 @@ export function QueryProvider({ children }: { children: ReactNode }) {
duration: 999999,
});
break;
case "station-status":
if (notification.status !== "5") return;
toast.custom((e) => <StatusToast event={notification} t={e} />, {
duration: 99999999 /* 30000 */,
});
break;
default:
toast("unbekanntes Notification-Event");
break;

View File

@@ -1,117 +1,102 @@
import { useState } from "react";
import { toast } from "react-hot-toast";
import { Prisma, StationStatus } from "@repo/db";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { BaseNotification } from "_components/customToasts/BaseNotification";
import { FMS_STATUS_COLORS } from "_helpers/fmsStatusColors";
import { editConnectedAircraftAPI, getConnectedAircraftsAPI } from "_querys/aircrafts";
import { getStationsAPI } from "_querys/stations";
import { useMapStore } from "_store/mapStore";
import { X } from "lucide-react";
import { Toast, 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>
);
const QUICK_RESPONSE: Record<string, string[]> = {
"5": ["J", "c"],
};
export default MapToastCard2;
export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) => {
const mapStore = useMapStore((s) => s);
const { data: connectedAircrafts } = useQuery({
queryKey: ["aircrafts"],
queryFn: () => getConnectedAircraftsAPI(),
refetchInterval: 10000,
});
const { data: stations } = useQuery({
queryKey: ["stations"],
queryFn: () => getStationsAPI(),
});
const queryClient = useQueryClient();
const changeAircraftMutation = useMutation({
mutationFn: async ({
id,
update,
}: {
id: number;
update: Prisma.ConnectedAircraftUpdateInput;
}) => {
await editConnectedAircraftAPI(id, update);
queryClient.invalidateQueries({
queryKey: ["aircrafts"],
});
},
});
const connectedAircraft = connectedAircrafts?.find((a) => a.id === event.data?.aircraftId);
const station = stations?.find((s) => s.id === event.data?.stationId);
if (!connectedAircraft || !station) return null;
return (
<BaseNotification>
<div className="flex flex-row gap-14 items-center">
<p>
<span
className="underline mr-1 cursor-pointer font-bold"
onClick={() => {
if (!connectedAircraft.posLat || !connectedAircraft.posLng) return;
mapStore.setOpenAircraftMarker({
open: [{ id: connectedAircraft.id, tab: "fms" }],
close: [],
});
mapStore.setMap({
center: [connectedAircraft.posLat, connectedAircraft.posLng],
zoom: 14,
});
}}
>
{station.bosCallsign}
</span>
sendet Status {connectedAircraft.fmsStatus}
</p>
<div className="flex gap-2 items-center">
{QUICK_RESPONSE[String(connectedAircraft.fmsStatus)]?.map((status) => (
<button
key={status}
className={
"flex justify-center items-center min-w-10 min-h-10 cursor-pointer text-lg font-bold"
}
style={{
backgroundColor: FMS_STATUS_COLORS[status],
color: "white",
}}
onClick={async () => {
await changeAircraftMutation.mutateAsync({
id: connectedAircraft.id,
update: {
fmsStatus: status,
},
});
toast.remove(t.id);
toast.success(`Status auf ${status} geändert`);
}}
>
{status}
</button>
))}
<button className="btn btn-ghost btn-sm" onClick={() => toast.remove(t.id)}>
<X size={16} />
</button>
</div>
</div>
</BaseNotification>
);
};

View File

@@ -180,7 +180,7 @@ const FMSStatusSelector = ({
fmsStatus: status,
},
});
toast.success(`Status changed to ${status}`);
toast.success(`Status auf ${status} geändert`);
}}
>
{status}
@@ -219,7 +219,6 @@ const FMSStatusSelector = ({
fmsStatus: status,
},
});
toast.success(`Status changed to ${status}`);
}}
>
{status}

View File

@@ -1,7 +0,0 @@
import { ConnectedAircraft } from "@repo/db";
export const checkSimulatorConnected = (a: ConnectedAircraft) => {
if (!a.lastHeartbeat || Date.now() - new Date(a.lastHeartbeat).getTime() > 30_000) return false; // 30 seconds
if (!a.posLat || !a.posLng) return false;
return true;
};

View File

@@ -1,7 +1,7 @@
import { ConnectedAircraft, PositionLog, Prisma, PublicUser, Station } from "@repo/db";
import axios from "axios";
import { serverApi } from "_helpers/axios";
import { checkSimulatorConnected } from "_helpers/simulatorConnected";
import { checkSimulatorConnected } from "@repo/shared-components";
export const getConnectedAircraftsAPI = async () => {
const res = await axios.get<(ConnectedAircraft & { Station: Station })[]>("/api/aircrafts"); // return only connected aircrafts

View File

@@ -39,24 +39,24 @@ export default async function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} h-screen flex flex-col overflow-hidden`}
>
<Toaster
containerStyle={{
top: 80,
left: 50,
}}
toastOptions={{
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
zIndex: 9999,
},
duration: 5000,
}}
position="top-left"
reverseOrder={false}
/>
<QueryProvider>
<NextAuthSessionProvider session={session}>
<Toaster
containerStyle={{
top: 80,
left: 50,
}}
toastOptions={{
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
zIndex: 9999,
},
duration: 5000,
}}
position="top-left"
reverseOrder={false}
/>
{session?.user.isBanned && (
<ErrorComp title="You are banned from using this service" statusCode={403} />
)}