StationStatus Toast hinzugefügt #45
This commit is contained in:
@@ -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 &&
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"), {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ 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`}
|
||||||
>
|
>
|
||||||
|
<QueryProvider>
|
||||||
|
<NextAuthSessionProvider session={session}>
|
||||||
<Toaster
|
<Toaster
|
||||||
containerStyle={{
|
containerStyle={{
|
||||||
top: 80,
|
top: 80,
|
||||||
@@ -55,8 +57,6 @@ export default async function RootLayout({
|
|||||||
position="top-left"
|
position="top-left"
|
||||||
reverseOrder={false}
|
reverseOrder={false}
|
||||||
/>
|
/>
|
||||||
<QueryProvider>
|
|
||||||
<NextAuthSessionProvider session={session}>
|
|
||||||
{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} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user