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 { Router } from "express";
import { io } from "../index"; 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 ( if (
mission && mission &&
aircraftUpdate.fmsStatus && aircraftUpdate.fmsStatus &&

View File

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

View File

@@ -101,7 +101,7 @@ export const handleConnectPilot =
await addRolesToMember(discordAccount.discordId.toString(), [DISCORD_ROLES.ONLINE_PILOT]); 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(`user:${userId}`); // Join the user-specific room
socket.join(`station:${stationId}`); // Join the station-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 { useQuery } from "@tanstack/react-query";
import { usePilotConnectionStore } from "_store/pilot/connectionStore"; import { usePilotConnectionStore } from "_store/pilot/connectionStore";
import { getAircraftsAPI, getConnectedAircraftsAPI } from "_querys/aircrafts"; import { getAircraftsAPI, getConnectedAircraftsAPI } from "_querys/aircrafts";
import { checkSimulatorConnected } from "_helpers/simulatorConnected"; import { checkSimulatorConnected } from "@repo/shared-components";
import { SimConnectionAlert } from "(app)/pilot/_components/SimConnectionAlert"; import { SimConnectionAlert } from "(app)/pilot/_components/SimConnectionAlert";
const Map = dynamic(() => import("_components/map/Map"), { const Map = dynamic(() => import("_components/map/Map"), {

View File

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

View File

@@ -1,117 +1,102 @@
import { useState } from "react"; import { Prisma, StationStatus } from "@repo/db";
import { toast } from "react-hot-toast"; 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 { const QUICK_RESPONSE: Record<string, string[]> = {
id: number; "5": ["J", "c"],
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; 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, fmsStatus: status,
}, },
}); });
toast.success(`Status changed to ${status}`); toast.success(`Status auf ${status} geändert`);
}} }}
> >
{status} {status}
@@ -219,7 +219,6 @@ const FMSStatusSelector = ({
fmsStatus: status, fmsStatus: status,
}, },
}); });
toast.success(`Status changed to ${status}`);
}} }}
> >
{status} {status}

View File

@@ -1,7 +1,7 @@
import { ConnectedAircraft, PositionLog, Prisma, PublicUser, Station } from "@repo/db"; import { ConnectedAircraft, PositionLog, Prisma, PublicUser, Station } from "@repo/db";
import axios from "axios"; import axios from "axios";
import { serverApi } from "_helpers/axios"; import { serverApi } from "_helpers/axios";
import { checkSimulatorConnected } from "_helpers/simulatorConnected"; import { checkSimulatorConnected } from "@repo/shared-components";
export const getConnectedAircraftsAPI = async () => { export const getConnectedAircraftsAPI = async () => {
const res = await axios.get<(ConnectedAircraft & { Station: Station })[]>("/api/aircrafts"); // return only connected aircrafts 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 <body
className={`${geistSans.variable} ${geistMono.variable} h-screen flex flex-col overflow-hidden`} 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> <QueryProvider>
<NextAuthSessionProvider session={session}> <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 && ( {session?.user.isBanned && (
<ErrorComp title="You are banned from using this service" statusCode={403} /> <ErrorComp title="You are banned from using this service" statusCode={403} />
)} )}

View File

@@ -29,4 +29,18 @@ export interface AdminMessage {
}; };
} }
export type NotificationPayload = ValidationFailed | ValidationSuccess | AdminMessage; export interface StationStatus {
type: "station-status";
status: "5";
message: string;
data?: {
stationId: number;
aircraftId: number;
};
}
export type NotificationPayload =
| ValidationFailed
| ValidationSuccess
| AdminMessage
| StationStatus;

View File

@@ -17,7 +17,7 @@ export const PenaltyDropdown = ({
btnTip?: string; btnTip?: string;
Icon: ReactNode; Icon: ReactNode;
}) => { }) => {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(false);
const [reason, setReason] = useState(""); const [reason, setReason] = useState("");
const [until, setUntil] = useState<string>("default"); const [until, setUntil] = useState<string>("default");
return ( return (

View File

@@ -1,3 +1,4 @@
export * from "./cn"; export * from "./cn";
export * from "./event"; export * from "./event";
export * from "./dates"; export * from "./dates";
export * from "./simulatorConnected";