feat: Implement connected user API and integrate chat and report components
- Added API routes for fetching connected users, keywords, missions, and stations. - Created a new QueryProvider component for managing query states and socket events. - Introduced connection stores for dispatch and pilot, managing socket connections and states. - Updated Prisma schema for connected aircraft model. - Enhanced UI with toast notifications for status updates and chat interactions. - Implemented query functions for fetching connected users and keywords with error handling.
This commit is contained in:
@@ -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));
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
47
apps/dispatch/app/_components/QueryProvider.tsx
Normal file
47
apps/dispatch/app/_components/QueryProvider.tsx
Normal file
@@ -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 (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
117
apps/dispatch/app/_components/StationStatusToast.tsx
Normal file
117
apps/dispatch/app/_components/StationStatusToast.tsx
Normal file
@@ -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<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;
|
||||
@@ -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<string>("");
|
||||
const [addTabValue, setAddTabValue] = useState<string>("default");
|
||||
const [message, setMessage] = useState<string>("");
|
||||
const [connectedUser, setConnectedUser] = useState<
|
||||
(ConnectedAircraft | ConnectedDispatcher)[] | null
|
||||
>(null);
|
||||
const timeout = useRef<NodeJS.Timeout | null>(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 (
|
||||
<div className={cn("dropdown dropdown-right", chatOpen && "dropdown-open")}>
|
||||
<div className="indicator">
|
||||
@@ -100,8 +76,16 @@ export const Chat = () => {
|
||||
onChange={(e) => setAddTabValue(e.target.value)}
|
||||
>
|
||||
{!connectedUser?.length && (
|
||||
<option disabled={true}>Keine Chatpartner gefunden</option>
|
||||
<option disabled value="default">
|
||||
Keine Chatpartner gefunden
|
||||
</option>
|
||||
)}
|
||||
{connectedUser?.length && (
|
||||
<option disabled value="default">
|
||||
Chatpartner auswählen
|
||||
</option>
|
||||
)}
|
||||
|
||||
{connectedUser?.map((user) => (
|
||||
<option key={user.userId} value={user.userId}>
|
||||
{asPublicUser(user.publicUser).fullName}
|
||||
@@ -1,53 +1,36 @@
|
||||
"use client";
|
||||
import { ExclamationTriangleIcon, PaperPlaneIcon } from "@radix-ui/react-icons";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { cn } from "helpers/cn";
|
||||
import { getConenctedUsers, serverApi } from "helpers/axios";
|
||||
import { serverApi } from "helpers/axios";
|
||||
import { useLeftMenuStore } from "_store/leftMenuStore";
|
||||
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 Report = () => {
|
||||
const { setChatOpen, setReportTabOpen, reportTabOpen, setOwnId } =
|
||||
useLeftMenuStore();
|
||||
const [sending, setSending] = useState(false);
|
||||
const session = useSession();
|
||||
const [selectedPlayer, setSelectedPlayer] = useState<string>("");
|
||||
const [selectedPlayer, setSelectedPlayer] = useState<string>("default");
|
||||
const [message, setMessage] = useState<string>("");
|
||||
const [connectedUser, setConnectedUser] = useState<
|
||||
(ConnectedAircraft | ConnectedDispatcher)[] | null
|
||||
>(null);
|
||||
const timeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
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) => user.userId !== session.data?.user.id,
|
||||
);
|
||||
setConnectedUser(filteredConnectedUser);
|
||||
}
|
||||
if (!selectedPlayer && data[0]) setSelectedPlayer(data[0].userId);
|
||||
};
|
||||
|
||||
timeout.current = setInterval(() => {
|
||||
fetchConnectedUser();
|
||||
}, 1000);
|
||||
fetchConnectedUser();
|
||||
|
||||
return () => {
|
||||
if (timeout.current) {
|
||||
clearInterval(timeout.current);
|
||||
timeout.current = null;
|
||||
}
|
||||
};
|
||||
}, [selectedPlayer, session.data?.user.id]);
|
||||
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,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -83,7 +66,14 @@ export const Report = () => {
|
||||
onChange={(e) => setSelectedPlayer(e.target.value)}
|
||||
>
|
||||
{!connectedUser?.length && (
|
||||
<option disabled={true}>Keine Spieler gefunden</option>
|
||||
<option disabled value="default">
|
||||
Keine Chatpartner gefunden
|
||||
</option>
|
||||
)}
|
||||
{connectedUser?.length && (
|
||||
<option disabled value="default">
|
||||
Chatpartner auswählen
|
||||
</option>
|
||||
)}
|
||||
{connectedUser?.map((user) => (
|
||||
<option key={user.userId} value={user.userId}>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { create } from "zustand";
|
||||
import { socket } from "../dispatch/socket";
|
||||
import { dispatchSocket } from "../../dispatch/socket";
|
||||
|
||||
interface ConnectionStore {
|
||||
status: "connected" | "disconnected" | "connecting" | "error";
|
||||
@@ -20,11 +20,11 @@ export const useDispatchConnectionStore = create<ConnectionStore>((set) => ({
|
||||
connect: async (uid, selectedZone, logoffTime) =>
|
||||
new Promise((resolve) => {
|
||||
set({ status: "connecting", message: "" });
|
||||
socket.auth = { uid };
|
||||
dispatchSocket.auth = { uid };
|
||||
set({ selectedZone });
|
||||
socket.connect();
|
||||
socket.once("connect", () => {
|
||||
socket.emit("connect-dispatch", {
|
||||
dispatchSocket.connect();
|
||||
dispatchSocket.once("connect", () => {
|
||||
dispatchSocket.emit("connect-dispatch", {
|
||||
logoffTime,
|
||||
selectedZone,
|
||||
});
|
||||
@@ -32,26 +32,26 @@ export const useDispatchConnectionStore = create<ConnectionStore>((set) => ({
|
||||
});
|
||||
}),
|
||||
disconnect: () => {
|
||||
socket.disconnect();
|
||||
dispatchSocket.disconnect();
|
||||
},
|
||||
}));
|
||||
|
||||
socket.on("connect", () => {
|
||||
dispatchSocket.on("connect", () => {
|
||||
useDispatchConnectionStore.setState({ status: "connected", message: "" });
|
||||
});
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
dispatchSocket.on("connect_error", (err) => {
|
||||
useDispatchConnectionStore.setState({
|
||||
status: "error",
|
||||
message: err.message,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
dispatchSocket.on("disconnect", () => {
|
||||
useDispatchConnectionStore.setState({ status: "disconnected", message: "" });
|
||||
});
|
||||
|
||||
socket.on("force-disconnect", (reason: string) => {
|
||||
dispatchSocket.on("force-disconnect", (reason: string) => {
|
||||
console.log("force-disconnect", reason);
|
||||
useDispatchConnectionStore.setState({
|
||||
status: "disconnected",
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import { ChatMessage } from "@repo/db";
|
||||
import { socket } from "dispatch/socket";
|
||||
import { dispatchSocket } from "dispatch/socket";
|
||||
import { pilotSocket } from "pilot/socket";
|
||||
|
||||
interface ChatStore {
|
||||
reportTabOpen: boolean;
|
||||
@@ -33,7 +34,7 @@ export const useLeftMenuStore = create<ChatStore>((set, get) => ({
|
||||
chats: {},
|
||||
sendMessage: (userId: string, message: string) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
socket.emit(
|
||||
dispatchSocket.emit(
|
||||
"send-message",
|
||||
{ userId, message },
|
||||
({ error }: { error?: string }) => {
|
||||
@@ -96,7 +97,16 @@ export const useLeftMenuStore = create<ChatStore>((set, get) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
socket.on(
|
||||
dispatchSocket.on(
|
||||
"chat-message",
|
||||
({ userId, message }: { userId: string; message: ChatMessage }) => {
|
||||
const store = useLeftMenuStore.getState();
|
||||
console.log("chat-message", userId, message);
|
||||
// Update the chat store with the new message
|
||||
store.addMessage(userId, message);
|
||||
},
|
||||
);
|
||||
pilotSocket.on(
|
||||
"chat-message",
|
||||
({ userId, message }: { userId: string; message: ChatMessage }) => {
|
||||
const store = useLeftMenuStore.getState();
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { Mission, Prisma } from "@repo/db";
|
||||
import { MissionOptionalDefaults } from "@repo/db/zod";
|
||||
import { serverApi } from "helpers/axios";
|
||||
import { create } from "zustand";
|
||||
import { toast } from "react-hot-toast";
|
||||
import axios from "axios";
|
||||
|
||||
interface MissionStore {
|
||||
missions: Mission[];
|
||||
setMissions: (missions: Mission[]) => void;
|
||||
getMissions: () => Promise<undefined>;
|
||||
createMission: (mission: MissionOptionalDefaults) => Promise<Mission>;
|
||||
deleteMission: (id: number) => Promise<void>;
|
||||
editMission: (id: number, mission: Partial<Mission>) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useMissionsStore = create<MissionStore>((set) => ({
|
||||
missions: [],
|
||||
setMissions: (missions) => set({ missions }),
|
||||
createMission: async (mission) => {
|
||||
const { data } = await serverApi.put<Mission>("/mission", mission);
|
||||
|
||||
set((state) => ({ missions: [...state.missions, data] }));
|
||||
return data;
|
||||
},
|
||||
editMission: async (id, mission) => {
|
||||
const { data, status } = await serverApi.patch<Mission>(
|
||||
`/mission/${id}`,
|
||||
mission,
|
||||
);
|
||||
if (status.toString().startsWith("2") && data) {
|
||||
set((state) => ({
|
||||
missions: state.missions.map((m) => (m.id === id ? data : m)),
|
||||
}));
|
||||
toast.success("Mission updated successfully");
|
||||
} else {
|
||||
toast.error("Failed to update mission");
|
||||
}
|
||||
},
|
||||
deleteMission: async (id) => {
|
||||
serverApi
|
||||
.delete(`/mission/${id}`)
|
||||
.then((res) => {
|
||||
if (res.status.toString().startsWith("2")) {
|
||||
set((state) => ({
|
||||
missions: state.missions.filter((mission) => mission.id !== id),
|
||||
}));
|
||||
toast.success("Mission deleted successfully");
|
||||
} else {
|
||||
toast.error("Failed to delete mission");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error("Failed to delete mission");
|
||||
});
|
||||
},
|
||||
getMissions: async () => {
|
||||
const { data } = await serverApi.post<Mission[]>("/mission", {
|
||||
filter: {
|
||||
OR: [{ state: "draft" }, { state: "running" }],
|
||||
} as Prisma.MissionWhereInput,
|
||||
});
|
||||
set({ missions: data });
|
||||
return undefined;
|
||||
},
|
||||
}));
|
||||
|
||||
useMissionsStore
|
||||
.getState()
|
||||
.getMissions()
|
||||
.then(() => {
|
||||
console.log("Missions loaded");
|
||||
});
|
||||
63
apps/dispatch/app/_store/pilot/connectionStore.ts
Normal file
63
apps/dispatch/app/_store/pilot/connectionStore.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { create } from "zustand";
|
||||
import { dispatchSocket } from "../../dispatch/socket";
|
||||
import { Station } from "@repo/db";
|
||||
import { pilotSocket } from "pilot/socket";
|
||||
|
||||
interface ConnectionStore {
|
||||
status: "connected" | "disconnected" | "connecting" | "error";
|
||||
message: string;
|
||||
selectedStation: Station | null;
|
||||
connect: (
|
||||
uid: string,
|
||||
stationId: string,
|
||||
logoffTime: string,
|
||||
station: Station,
|
||||
) => Promise<void>;
|
||||
disconnect: () => void;
|
||||
}
|
||||
|
||||
export const useDispatchConnectionStore = create<ConnectionStore>((set) => ({
|
||||
status: "disconnected",
|
||||
message: "",
|
||||
selectedStation: null,
|
||||
connect: async (uid, stationId, logoffTime, station) =>
|
||||
new Promise((resolve) => {
|
||||
set({ status: "connecting", message: "", selectedStation: station });
|
||||
dispatchSocket.auth = { uid };
|
||||
dispatchSocket.connect();
|
||||
dispatchSocket.once("connect", () => {
|
||||
dispatchSocket.emit("connect-pilot", {
|
||||
logoffTime,
|
||||
stationId,
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
disconnect: () => {
|
||||
dispatchSocket.disconnect();
|
||||
},
|
||||
}));
|
||||
|
||||
dispatchSocket.on("connect", () => {
|
||||
pilotSocket.disconnect();
|
||||
useDispatchConnectionStore.setState({ status: "connected", message: "" });
|
||||
});
|
||||
|
||||
dispatchSocket.on("connect_error", (err) => {
|
||||
useDispatchConnectionStore.setState({
|
||||
status: "error",
|
||||
message: err.message,
|
||||
});
|
||||
});
|
||||
|
||||
dispatchSocket.on("disconnect", () => {
|
||||
useDispatchConnectionStore.setState({ status: "disconnected", message: "" });
|
||||
});
|
||||
|
||||
dispatchSocket.on("force-disconnect", (reason: string) => {
|
||||
console.log("force-disconnect", reason);
|
||||
useDispatchConnectionStore.setState({
|
||||
status: "disconnected",
|
||||
message: reason,
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import { socket } from "../dispatch/socket";
|
||||
|
||||
export const stationStore = create((set) => {
|
||||
export const useStationStore = create((set) => {
|
||||
return {
|
||||
stations: [],
|
||||
setStations: (stations: any) => set({ stations }),
|
||||
|
||||
28
apps/dispatch/app/api/connected-user/route.ts
Normal file
28
apps/dispatch/app/api/connected-user/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@repo/db";
|
||||
|
||||
export async function GET(): Promise<NextResponse> {
|
||||
try {
|
||||
const connectedDispatcher = await prisma.connectedDispatcher.findMany({
|
||||
where: {
|
||||
logoutTime: null,
|
||||
},
|
||||
});
|
||||
|
||||
const connectedAircraft = await prisma.connectedAircraft.findMany({
|
||||
where: {
|
||||
logoutTime: null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json([...connectedDispatcher, ...connectedAircraft], {
|
||||
status: 200,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch connected User" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
25
apps/dispatch/app/api/keywords/route.ts
Normal file
25
apps/dispatch/app/api/keywords/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@repo/db";
|
||||
|
||||
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
const filter = searchParams.get("filter");
|
||||
|
||||
try {
|
||||
const data = await prisma.keyword.findMany({
|
||||
where: {
|
||||
id: id ? Number(id) : undefined,
|
||||
...(filter ? JSON.parse(filter) : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(data, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch keyword" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
25
apps/dispatch/app/api/missions/route.ts
Normal file
25
apps/dispatch/app/api/missions/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@repo/db";
|
||||
|
||||
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
const filter = searchParams.get("filter");
|
||||
|
||||
try {
|
||||
const data = await prisma.mission.findMany({
|
||||
where: {
|
||||
id: id ? Number(id) : undefined,
|
||||
...(filter ? JSON.parse(filter) : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(data, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch mission" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
25
apps/dispatch/app/api/stations/route.ts
Normal file
25
apps/dispatch/app/api/stations/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@repo/db";
|
||||
|
||||
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get("id");
|
||||
const filter = searchParams.get("filter");
|
||||
|
||||
try {
|
||||
const data = await prisma.station.findMany({
|
||||
where: {
|
||||
id: id ? Number(id) : undefined,
|
||||
...(filter ? JSON.parse(filter) : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(data, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch station" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useMissionsStore } from "_store/missionsStore";
|
||||
import { Marker, useMap } from "react-leaflet";
|
||||
import { DivIcon, Marker as LMarker, Popup as LPopup } from "leaflet";
|
||||
import { useMapStore } from "_store/mapStore";
|
||||
@@ -31,6 +30,8 @@ import Einsatzdetails, {
|
||||
Patientdetails,
|
||||
Rettungsmittel,
|
||||
} from "./_components/MissionMarkerTabs";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getMissionsAPI } from "querys/missions";
|
||||
|
||||
export const MISSION_STATUS_COLORS: Record<MissionState | "attention", string> =
|
||||
{
|
||||
@@ -367,7 +368,13 @@ const MissionMarker = ({ mission }: { mission: Mission }) => {
|
||||
};
|
||||
|
||||
export const MissionLayer = () => {
|
||||
const missions = useMissionsStore((state) => state.missions);
|
||||
const { data: missions = [] } = useQuery({
|
||||
queryKey: ["missions"],
|
||||
queryFn: () =>
|
||||
getMissionsAPI({
|
||||
OR: [{ state: "draft" }, { state: "running" }],
|
||||
}),
|
||||
});
|
||||
|
||||
// IDEA: Add Marker to Map Layer / LayerGroup
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Mission } from "@repo/db";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { SmartPopup, useSmartPopup } from "_components/SmartPopup";
|
||||
import { Aircraft, useAircraftsStore } from "_store/aircraftsStore";
|
||||
import { useMapStore } from "_store/mapStore";
|
||||
import { useMissionsStore } from "_store/missionsStore";
|
||||
import {
|
||||
FMS_STATUS_COLORS,
|
||||
FMS_STATUS_TEXT_COLORS,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
MISSION_STATUS_TEXT_COLORS,
|
||||
} from "dispatch/_components/map/MissionMarkers";
|
||||
import { cn } from "helpers/cn";
|
||||
import { getMissionsAPI } from "querys/missions";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMap } from "react-leaflet";
|
||||
|
||||
@@ -141,7 +142,13 @@ const PopupContent = ({
|
||||
export const MarkerCluster = () => {
|
||||
const map = useMap();
|
||||
const aircrafts = useAircraftsStore((state) => state.aircrafts);
|
||||
const missions = useMissionsStore((state) => state.missions);
|
||||
const { data: missions } = useQuery({
|
||||
queryKey: ["missions"],
|
||||
queryFn: () =>
|
||||
getMissionsAPI({
|
||||
OR: [{ state: "draft" }, { state: "running" }],
|
||||
}),
|
||||
});
|
||||
const [cluster, setCluster] = useState<
|
||||
{
|
||||
aircrafts: Aircraft[];
|
||||
@@ -185,7 +192,7 @@ export const MarkerCluster = () => {
|
||||
];
|
||||
}
|
||||
});
|
||||
missions.forEach((mission) => {
|
||||
missions?.forEach((mission) => {
|
||||
const lat = mission.addressLat;
|
||||
const lng = mission.addressLng;
|
||||
const existingClusterIndex = newCluster.findIndex(
|
||||
|
||||
@@ -24,13 +24,24 @@ import {
|
||||
Mission,
|
||||
MissionLog,
|
||||
MissionMessageLog,
|
||||
Prisma,
|
||||
} from "@repo/db";
|
||||
import { useMissionsStore } from "_store/missionsStore";
|
||||
import { usePannelStore } from "_store/pannelStore";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { deleteMissionAPI, editMissionAPI } from "querys/missions";
|
||||
|
||||
const Einsatzdetails = ({ mission }: { mission: Mission }) => {
|
||||
const { deleteMission } = useMissionsStore((state) => state);
|
||||
const queryClient = useQueryClient();
|
||||
const deleteMissionMutation = useMutation({
|
||||
mutationKey: ["missions"],
|
||||
mutationFn: deleteMissionAPI,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["missions"],
|
||||
});
|
||||
},
|
||||
});
|
||||
const { setMissionFormValues, setOpen } = usePannelStore((state) => state);
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
@@ -93,7 +104,7 @@ const Einsatzdetails = ({ mission }: { mission: Mission }) => {
|
||||
<button
|
||||
className="btn btn-sm btn-error btn-outline"
|
||||
onClick={() => {
|
||||
deleteMission(mission.id);
|
||||
deleteMissionMutation.mutate(mission.id);
|
||||
}}
|
||||
>
|
||||
<Trash size={18} />
|
||||
@@ -180,8 +191,22 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
||||
const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
||||
const session = useSession();
|
||||
const [isAddingNote, setIsAddingNote] = useState(false);
|
||||
const { editMission } = useMissionsStore((state) => state);
|
||||
const [note, setNote] = useState("");
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const editMissionMutation = useMutation({
|
||||
mutationFn: ({
|
||||
id,
|
||||
mission,
|
||||
}: {
|
||||
id: number;
|
||||
mission: Partial<Prisma.MissionUpdateInput>;
|
||||
}) => editMissionAPI(id, mission),
|
||||
mutationKey: ["missions"],
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["missions"] });
|
||||
},
|
||||
});
|
||||
|
||||
if (!session.data?.user) return null;
|
||||
return (
|
||||
@@ -220,13 +245,18 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
||||
},
|
||||
} as MissionMessageLog,
|
||||
];
|
||||
editMission(mission.id, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
missionLog: newMissionLog as any,
|
||||
}).then(() => {
|
||||
setIsAddingNote(false);
|
||||
setNote("");
|
||||
});
|
||||
editMissionMutation
|
||||
.mutateAsync({
|
||||
id: mission.id,
|
||||
mission: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
missionLog: newMissionLog as any,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
setIsAddingNote(false);
|
||||
setNote("");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Plus size={20} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatchConnectionStore } from "_store/connectionStore";
|
||||
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
||||
import {
|
||||
Disc,
|
||||
Mic,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useDispatchConnectionStore } from "../../../../_store/connectionStore";
|
||||
import { useDispatchConnectionStore } from "../../../../_store/dispatch/connectionStore";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
export const ConnectionBtn = () => {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"use server";
|
||||
|
||||
export interface Dispatcher {
|
||||
userId: string;
|
||||
lastSeen: string;
|
||||
loginTime: string;
|
||||
logoffTime: string;
|
||||
selectedZone: string;
|
||||
name: string;
|
||||
socketId: string;
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { BellRing, BookmarkPlus } from "lucide-react";
|
||||
import { Select } from "_components/Select";
|
||||
import { Keyword, KEYWORD_CATEGORY, missionType, Station } from "@repo/db";
|
||||
import { getKeywords, getStations } from "dispatch/_components/pannel/action";
|
||||
import { KEYWORD_CATEGORY, missionType, Prisma } from "@repo/db";
|
||||
import {
|
||||
MissionOptionalDefaults,
|
||||
MissionOptionalDefaultsSchema,
|
||||
@@ -13,13 +12,52 @@ import {
|
||||
import { usePannelStore } from "_store/pannelStore";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useMissionsStore } from "_store/missionsStore";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { createMissionAPI, editMissionAPI } from "querys/missions";
|
||||
import { getKeywordsAPI } from "querys/keywords";
|
||||
import { getStationsAPI } from "querys/stations";
|
||||
|
||||
export const MissionForm = () => {
|
||||
const { isEditingMission, editingMissionId, setEditingMission } =
|
||||
usePannelStore();
|
||||
const createMission = useMissionsStore((state) => state.createMission);
|
||||
const { deleteMission } = useMissionsStore((state) => state);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: keywords } = useQuery({
|
||||
queryKey: ["keywords"],
|
||||
queryFn: () => getKeywordsAPI(),
|
||||
});
|
||||
|
||||
const { data: stations } = useQuery({
|
||||
queryKey: ["stations"],
|
||||
queryFn: () => getStationsAPI(),
|
||||
});
|
||||
|
||||
const createMissionMutation = useMutation({
|
||||
mutationFn: createMissionAPI,
|
||||
mutationKey: ["missions"],
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["missions"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const editMissionMutation = useMutation({
|
||||
mutationFn: ({
|
||||
id,
|
||||
mission,
|
||||
}: {
|
||||
id: number;
|
||||
mission: Partial<Prisma.MissionUpdateInput>;
|
||||
}) => editMissionAPI(id, mission),
|
||||
mutationKey: ["missions"],
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["missions"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const session = useSession();
|
||||
const defaultFormValues = React.useMemo(
|
||||
() =>
|
||||
@@ -85,18 +123,6 @@ export const MissionForm = () => {
|
||||
}
|
||||
}, [missionFormValues, form, defaultFormValues]);
|
||||
|
||||
const [stations, setStations] = useState<Station[]>([]);
|
||||
const [keywords, setKeywords] = useState<Keyword[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getKeywords().then((data) => {
|
||||
setKeywords(data);
|
||||
});
|
||||
getStations().then((data) => {
|
||||
setStations(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
console.log(form.formState.errors);
|
||||
return (
|
||||
<form className="space-y-4">
|
||||
@@ -165,7 +191,7 @@ export const MissionForm = () => {
|
||||
placeholder="Wähle ein oder mehrere Rettungsmittel aus"
|
||||
isMulti
|
||||
form={form}
|
||||
options={stations.map((s) => ({
|
||||
options={stations?.map((s) => ({
|
||||
label: s.bosCallsign,
|
||||
value: s.id.toString(),
|
||||
}))}
|
||||
@@ -217,7 +243,7 @@ export const MissionForm = () => {
|
||||
{...form.register("missionKeywordAbbreviation")}
|
||||
className="select select-primary select-bordered w-full mb-4"
|
||||
onChange={(e) => {
|
||||
const keyword = keywords.find(
|
||||
const keyword = keywords?.find(
|
||||
(k) => k.abreviation === e.target.value,
|
||||
);
|
||||
form.setValue("missionKeywordName", keyword?.name || null);
|
||||
@@ -232,15 +258,16 @@ export const MissionForm = () => {
|
||||
<option disabled value={""}>
|
||||
Einsatzstichwort auswählen...
|
||||
</option>
|
||||
{keywords
|
||||
.filter(
|
||||
(k) => k.category === form.watch("missionKeywordCategory"),
|
||||
)
|
||||
.map((keyword) => (
|
||||
<option key={keyword.id} value={keyword.abreviation}>
|
||||
{keyword.name}
|
||||
</option>
|
||||
))}
|
||||
{keywords &&
|
||||
keywords
|
||||
.filter(
|
||||
(k) => k.category === form.watch("missionKeywordCategory"),
|
||||
)
|
||||
.map((keyword) => (
|
||||
<option key={keyword.id} value={keyword.abreviation}>
|
||||
{keyword.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{/* TODO: Nur anzeigen wenn eine Station mit HPG ausgewählt ist */}
|
||||
<select
|
||||
@@ -251,16 +278,17 @@ export const MissionForm = () => {
|
||||
<option disabled value="">
|
||||
Einsatz Szenerie auswählen...
|
||||
</option>
|
||||
{keywords
|
||||
.find((k) => k.name === form.watch("missionKeywordName"))
|
||||
?.hpgMissionTypes?.map((missionString) => {
|
||||
const [name] = missionString.split(":");
|
||||
return (
|
||||
<option key={missionString} value={missionString}>
|
||||
{name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
{keywords &&
|
||||
keywords
|
||||
.find((k) => k.name === form.watch("missionKeywordName"))
|
||||
?.hpgMissionTypes?.map((missionString) => {
|
||||
const [name] = missionString.split(":");
|
||||
return (
|
||||
<option key={missionString} value={missionString}>
|
||||
{name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
@@ -277,8 +305,6 @@ export const MissionForm = () => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Patienteninformationen Section */}
|
||||
<div className="form-control">
|
||||
<h2 className="text-lg font-bold mb-2">Patienteninformationen</h2>
|
||||
<textarea
|
||||
@@ -287,22 +313,24 @@ export const MissionForm = () => {
|
||||
className="textarea textarea-primary textarea-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-error">
|
||||
Du musst noch ein Gebäude auswählen, um den Einsatz zu erstellen.
|
||||
</p>
|
||||
|
||||
<div className="form-control min-h-[140px] max-w-[320px]">
|
||||
<div className="form-control min-h-[140px]">
|
||||
<div className="flex gap-2">
|
||||
{isEditingMission && editingMissionId ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-block"
|
||||
className="btn btn-primary flex-1"
|
||||
onClick={form.handleSubmit(
|
||||
async (mission: MissionOptionalDefaults) => {
|
||||
try {
|
||||
deleteMission(Number(editingMissionId));
|
||||
const newMission = await createMission(mission);
|
||||
const newMission = await editMissionMutation.mutateAsync({
|
||||
id: Number(editingMissionId),
|
||||
mission:
|
||||
mission as unknown as Partial<Prisma.MissionUpdateInput>,
|
||||
});
|
||||
toast.success(
|
||||
`Einsatz ${newMission.id} erfolgreich aktualisiert`,
|
||||
);
|
||||
@@ -327,7 +355,10 @@ export const MissionForm = () => {
|
||||
onClick={form.handleSubmit(
|
||||
async (mission: MissionOptionalDefaults) => {
|
||||
try {
|
||||
const newMission = await createMission(mission);
|
||||
const newMission =
|
||||
await createMissionMutation.mutateAsync(
|
||||
mission as unknown as Prisma.MissionCreateInput,
|
||||
);
|
||||
toast.success(`Einsatz ${newMission.id} erstellt`);
|
||||
// TODO: Einsatz alarmieren
|
||||
setOpen(false);
|
||||
@@ -343,11 +374,15 @@ export const MissionForm = () => {
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-block"
|
||||
className="btn btn-primary flex-1"
|
||||
onClick={form.handleSubmit(
|
||||
async (mission: MissionOptionalDefaults) => {
|
||||
try {
|
||||
const newMission = await createMission(mission);
|
||||
const newMission =
|
||||
await createMissionMutation.mutateAsync(
|
||||
mission as unknown as Prisma.MissionCreateInput,
|
||||
);
|
||||
|
||||
toast.success(`Einsatz ${newMission.id} erstellt`);
|
||||
form.reset();
|
||||
setOpen(false);
|
||||
|
||||
@@ -2,10 +2,41 @@ import { usePannelStore } from "_store/pannelStore";
|
||||
import { cn } from "helpers/cn";
|
||||
import { MissionForm } from "./MissionForm";
|
||||
import { Rss, Trash2Icon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getMissionsAPI } from "querys/missions";
|
||||
|
||||
export const Pannel = () => {
|
||||
const { setOpen, setMissionFormValues } = usePannelStore();
|
||||
const { isEditingMission, setEditingMission } = usePannelStore();
|
||||
const { isEditingMission, setEditingMission, missionFormValues } =
|
||||
usePannelStore();
|
||||
const missions = useQuery({
|
||||
queryKey: ["missions"],
|
||||
queryFn: () =>
|
||||
getMissionsAPI({
|
||||
OR: [{ state: "draft" }, { state: "running" }],
|
||||
}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditingMission && missionFormValues) {
|
||||
const mission = missions.data?.find(
|
||||
(mission) => mission.id === missionFormValues.id,
|
||||
);
|
||||
if (!mission) {
|
||||
setEditingMission(false, null);
|
||||
setMissionFormValues({});
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isEditingMission,
|
||||
missions,
|
||||
setMissionFormValues,
|
||||
setEditingMission,
|
||||
setOpen,
|
||||
missionFormValues,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={cn("flex-1 max-w-[600px] z-9999999")}>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@repo/db";
|
||||
|
||||
export const getKeywords = async () => {
|
||||
const keywords = prisma.keyword.findMany();
|
||||
|
||||
return keywords;
|
||||
};
|
||||
|
||||
export const getStations = async () => {
|
||||
const stations = await prisma.station.findMany();
|
||||
console.log(stations);
|
||||
return stations;
|
||||
};
|
||||
@@ -4,8 +4,8 @@ import { Pannel } from "dispatch/_components/pannel/Pannel";
|
||||
import { usePannelStore } from "_store/pannelStore";
|
||||
import { cn } from "helpers/cn";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Chat } from "./_components/left/Chat";
|
||||
import { Report } from "./_components/left/Report";
|
||||
import { Chat } from "../_components/left/Chat";
|
||||
import { Report } from "../_components/left/Report";
|
||||
const Map = dynamic(() => import("./_components/map/Map"), { ssr: false });
|
||||
|
||||
const DispatchPage = () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { pilotSocket } from "pilot/socket";
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
export const socket = io(process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL, {
|
||||
export const dispatchSocket = io(process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL, {
|
||||
autoConnect: false,
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ import "./globals.css";
|
||||
import { NextAuthSessionProvider } from "./_components/AuthSessionProvider";
|
||||
import { getServerSession } from "./api/auth/[...nextauth]/auth";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { QueryProvider } from "_components/QueryProvider";
|
||||
|
||||
const geistSans = localFont({
|
||||
src: "./fonts/GeistVF.woff",
|
||||
@@ -45,9 +47,11 @@ export default async function RootLayout({
|
||||
position="top-left"
|
||||
reverseOrder={false}
|
||||
/>
|
||||
<NextAuthSessionProvider session={session}>
|
||||
{children}
|
||||
</NextAuthSessionProvider>
|
||||
<QueryProvider>
|
||||
<NextAuthSessionProvider session={session}>
|
||||
{children}
|
||||
</NextAuthSessionProvider>
|
||||
</QueryProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useConnectionStore } from "_store/connectionStore";
|
||||
import { useDispatchConnectionStore } from "_store/pilot/connectionStore";
|
||||
import {
|
||||
Disc,
|
||||
Mic,
|
||||
@@ -20,7 +20,7 @@ import { ConnectionQuality } from "livekit-client";
|
||||
import { ROOMS } from "_data/livekitRooms";
|
||||
|
||||
export const Audio = () => {
|
||||
const connection = useConnectionStore();
|
||||
const connection = useDispatchConnectionStore();
|
||||
const {
|
||||
isTalking,
|
||||
toggleTalking,
|
||||
@@ -36,7 +36,7 @@ export const Audio = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const joinRoom = async () => {
|
||||
if (!connection.isConnected) return;
|
||||
if (connection.status != "connected") return;
|
||||
if (state === "connected") return;
|
||||
connect(selectedRoom);
|
||||
};
|
||||
@@ -46,7 +46,8 @@ export const Audio = () => {
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, [connection.isConnected]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [connection.status]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
"use client";
|
||||
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 { cn } from "helpers/cn";
|
||||
import { getConenctedUsers } from "helpers/axios";
|
||||
import {
|
||||
asPublicUser,
|
||||
ConnectedAircraft,
|
||||
ConnectedDispatcher,
|
||||
PublicUser,
|
||||
} from "@repo/db";
|
||||
|
||||
export const Chat = () => {
|
||||
const {
|
||||
chatOpen,
|
||||
setChatOpen,
|
||||
sendMessage,
|
||||
addChat,
|
||||
chats,
|
||||
setOwnId,
|
||||
selectedChat,
|
||||
setSelectedChat,
|
||||
setChatNotification,
|
||||
} = useLeftMenuStore();
|
||||
const [sending, setSending] = useState(false);
|
||||
const session = useSession();
|
||||
const [addTabValue, setAddTabValue] = useState<string>("");
|
||||
const [message, setMessage] = useState<string>("");
|
||||
const [connectedUser, setConnectedUser] = useState<
|
||||
(ConnectedAircraft | ConnectedDispatcher)[] | null
|
||||
>(null);
|
||||
const timeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
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)
|
||||
);
|
||||
});
|
||||
setConnectedUser(filteredConnectedUser);
|
||||
}
|
||||
if (!addTabValue && data[0]) setAddTabValue(data[0].userId);
|
||||
};
|
||||
|
||||
timeout.current = setInterval(() => {
|
||||
fetchConnectedUser();
|
||||
}, 1000000);
|
||||
fetchConnectedUser();
|
||||
|
||||
return () => {
|
||||
if (timeout.current) {
|
||||
clearInterval(timeout.current);
|
||||
timeout.current = null;
|
||||
console.log("cleared");
|
||||
}
|
||||
};
|
||||
}, [addTabValue, chats, session]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("dropdown dropdown-center", chatOpen && "dropdown-open")}
|
||||
>
|
||||
<div className="indicator">
|
||||
{Object.values(chats).some((c) => c.notification) && (
|
||||
<span className="indicator-item status status-info"></span>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-soft btn-sm btn-primary"
|
||||
onClick={() => {
|
||||
console.log("clicked");
|
||||
setChatOpen(!chatOpen);
|
||||
if (selectedChat) {
|
||||
setChatNotification(selectedChat, false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChatBubbleIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{chatOpen && (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="dropdown-content card bg-base-200 w-150 shadow-md z-[1100]"
|
||||
>
|
||||
<div className="card-body">
|
||||
<div className="join">
|
||||
<select
|
||||
className="select select-sm w-full"
|
||||
value={addTabValue}
|
||||
onChange={(e) => setAddTabValue(e.target.value)}
|
||||
>
|
||||
{!connectedUser?.length && (
|
||||
<option disabled={true}>Keine Chatpartner gefunden</option>
|
||||
)}
|
||||
{connectedUser?.map((user) => (
|
||||
<option key={user.userId} value={user.userId}>
|
||||
{(user.publicUser as unknown as PublicUser).firstname}{" "}
|
||||
{(user.publicUser as unknown as PublicUser).lastname}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-sm btn-soft btn-primary join-item"
|
||||
onClick={() => {
|
||||
const user = connectedUser?.find(
|
||||
(user) => user.userId === addTabValue,
|
||||
);
|
||||
if (!user) return;
|
||||
addChat(addTabValue, asPublicUser(user.publicUser).fullName);
|
||||
setSelectedChat(addTabValue);
|
||||
}}
|
||||
>
|
||||
<span className="text-xl">+</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="tabs tabs-lift">
|
||||
{Object.keys(chats).map((userId) => {
|
||||
const chat = chats[userId];
|
||||
if (!chat) return null;
|
||||
return (
|
||||
<Fragment key={userId}>
|
||||
<input
|
||||
type="radio"
|
||||
name="my_tabs_3"
|
||||
className="tab"
|
||||
aria-label={`<${chat.name}>`}
|
||||
checked={selectedChat === userId}
|
||||
onClick={() => {
|
||||
setChatNotification(userId, false);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
// Handle tab change
|
||||
setSelectedChat(userId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="tab-content bg-base-100 border-base-300 p-6">
|
||||
{chat.messages.map((chatMessage) => {
|
||||
const isSender =
|
||||
chatMessage.senderId === session.data?.user.id;
|
||||
return (
|
||||
<div
|
||||
key={chatMessage.id}
|
||||
className={`chat ${isSender ? "chat-end" : "chat-start"}`}
|
||||
>
|
||||
<p className="chat-footer opacity-50">
|
||||
{new Date(
|
||||
chatMessage.timestamp,
|
||||
).toLocaleTimeString()}
|
||||
</p>
|
||||
<div className="chat-bubble">
|
||||
{chatMessage.text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!chat.messages.length && (
|
||||
<p className="text-xs opacity-50">
|
||||
Noch keine Nachrichten
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="join">
|
||||
<div className="w-full">
|
||||
<label className="input join-item w-full">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="w-full"
|
||||
onChange={(e) => {
|
||||
setMessage(e.target.value);
|
||||
}}
|
||||
value={message}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-soft join-item"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (message.length < 1) return;
|
||||
if (!selectedChat) return;
|
||||
setSending(true);
|
||||
sendMessage(selectedChat, message)
|
||||
.then(() => {
|
||||
setMessage("");
|
||||
setSending(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setSending(false);
|
||||
});
|
||||
return false;
|
||||
}}
|
||||
disabled={sending}
|
||||
role="button"
|
||||
onSubmit={() => false}
|
||||
>
|
||||
{sending ? (
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
) : (
|
||||
<PaperPlaneIcon />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +1,57 @@
|
||||
"use client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useDispatchConnectionStore } from "../../../_store/connectionStore";
|
||||
import { useRef, useState } from "react";
|
||||
import { useDispatchConnectionStore } from "../../../_store/pilot/connectionStore";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getStationsAPI } from "querys/stations";
|
||||
|
||||
export const ConnectionBtn = () => {
|
||||
const modalRef = useRef<HTMLDialogElement>(null);
|
||||
const connection = useDispatchConnectionStore((state) => state);
|
||||
const [form, setForm] = useState({
|
||||
logoffTime: "",
|
||||
selectedZone: "LST_01",
|
||||
const [form, setForm] = useState<{
|
||||
logoffTime: string | null;
|
||||
selectedStationId: number | null;
|
||||
}>({
|
||||
logoffTime: null,
|
||||
selectedStationId: null,
|
||||
});
|
||||
|
||||
const { data: stations } = useQuery({
|
||||
queryKey: ["stations"],
|
||||
queryFn: () => getStationsAPI(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
/* getStations().then((data) => {
|
||||
setStations(data);
|
||||
if (data[0]) {
|
||||
setForm({
|
||||
...form,
|
||||
selectedStationId: data[0].id,
|
||||
});
|
||||
}
|
||||
}); */
|
||||
}, [connection.status, form]);
|
||||
|
||||
const session = useSession();
|
||||
const uid = session.data?.user?.id;
|
||||
if (!uid) return null;
|
||||
return (
|
||||
<>
|
||||
{!connection.isConnected ? (
|
||||
<div className="rounded-box bg-base-200 flex justify-center items-center gap-2 p-1">
|
||||
{connection.message.length > 0 && (
|
||||
<span className="mx-2 text-error">{connection.message}</span>
|
||||
)}
|
||||
|
||||
{connection.status === "disconnected" && (
|
||||
<button
|
||||
className="btn btn-soft btn-info"
|
||||
className="btn btn-sm btn-soft btn-info "
|
||||
onClick={() => modalRef.current?.showModal()}
|
||||
>
|
||||
Verbinden
|
||||
</button>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{connection.status == "connected" && (
|
||||
<button
|
||||
className="btn btn-soft btn-success"
|
||||
onClick={() => modalRef.current?.showModal()}
|
||||
@@ -33,17 +62,38 @@ export const ConnectionBtn = () => {
|
||||
|
||||
<dialog ref={modalRef} className="modal">
|
||||
<div className="modal-box flex flex-col items-center justify-center">
|
||||
{connection.isConnected ? (
|
||||
{connection.status == "connected" ? (
|
||||
<h3 className="text-lg font-bold mb-5">
|
||||
Verbunden als{" "}
|
||||
<span className="text-info">
|
||||
<{connection.selectedZone}>
|
||||
<{connection.selectedStation?.bosCallsign}>
|
||||
</span>
|
||||
</h3>
|
||||
) : (
|
||||
<h3 className="text-lg font-bold mb-5">Als Disponent anmelden</h3>
|
||||
)}
|
||||
<fieldset className="fieldset w-full">
|
||||
<label className="floating-label w-full text-base">
|
||||
<span>Station</span>
|
||||
<select
|
||||
onChange={(e) =>
|
||||
setForm({
|
||||
...form,
|
||||
selectedStationId: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
value={form.selectedStationId ?? ""}
|
||||
className="input w-full"
|
||||
>
|
||||
{stations?.map((station) => (
|
||||
<option key={station.id} value={station.id}>
|
||||
{station.bosCallsign}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset className="fieldset w-full mt-2">
|
||||
<label className="floating-label w-full text-base">
|
||||
<span>Logoff Zeit (UTC+1)</span>
|
||||
<input
|
||||
@@ -53,12 +103,12 @@ export const ConnectionBtn = () => {
|
||||
logoffTime: e.target.value,
|
||||
})
|
||||
}
|
||||
value={form.logoffTime}
|
||||
value={form.logoffTime ?? ""}
|
||||
type="time"
|
||||
className="input w-full"
|
||||
/>
|
||||
</label>
|
||||
{!connection.isConnected && (
|
||||
{connection.status == "disconnected" && (
|
||||
<p className="fieldset-label">
|
||||
Du kannst diese Zeit später noch anpassen.
|
||||
</p>
|
||||
@@ -67,7 +117,7 @@ export const ConnectionBtn = () => {
|
||||
<div className="modal-action flex justify-between w-full">
|
||||
<form method="dialog" className="w-full flex justify-between">
|
||||
<button className="btn btn-soft">Abbrechen</button>
|
||||
{connection.isConnected ? (
|
||||
{connection.status == "connected" ? (
|
||||
<button
|
||||
className="btn btn-soft btn-error"
|
||||
type="submit"
|
||||
@@ -83,7 +133,16 @@ export const ConnectionBtn = () => {
|
||||
type="submit"
|
||||
onSubmit={() => false}
|
||||
onClick={() => {
|
||||
connection.connect(uid, form.selectedZone, form.logoffTime);
|
||||
connection.connect(
|
||||
uid,
|
||||
form.selectedStationId?.toString() || "",
|
||||
form.logoffTime || "",
|
||||
stations?.find(
|
||||
(station) =>
|
||||
station.id ===
|
||||
parseInt(form.selectedStationId?.toString() || ""),
|
||||
)!,
|
||||
);
|
||||
}}
|
||||
className="btn btn-soft btn-info"
|
||||
>
|
||||
@@ -94,7 +153,7 @@ export const ConnectionBtn = () => {
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ThemeSwap } from "./ThemeSwap";
|
||||
import { Audio } from "./Audio";
|
||||
import { useState } from "react";
|
||||
import { ExitIcon, ExternalLinkIcon } from "@radix-ui/react-icons";
|
||||
import { Chat } from "./Chat";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Navbar() {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
@@ -24,11 +24,6 @@ export default function Navbar() {
|
||||
<div className="flex items-center gap-2">
|
||||
<a className="btn btn-ghost text-xl">VAR Leitstelle V2</a>
|
||||
</div>
|
||||
<div className="bg-base-200 rounded-box flex items-center gap-2 p-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Chat />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -39,12 +34,20 @@ export default function Navbar() {
|
||||
</div>
|
||||
<ThemeSwap isDark={isDark} toggleTheme={toggleTheme} />
|
||||
<div className="flex items-center">
|
||||
<button className="btn btn-ghost">
|
||||
<ExternalLinkIcon className="w-4 h-4" /> HUB
|
||||
</button>
|
||||
<button className="btn btn-ghost">
|
||||
<ExitIcon className="w-4 h-4" />
|
||||
</button>
|
||||
<Link
|
||||
href={process.env.NEXT_PUBLIC_HUB_URL || "#!"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<button className="btn btn-ghost">
|
||||
<ExternalLinkIcon className="w-4 h-4" /> HUB
|
||||
</button>
|
||||
</Link>
|
||||
<Link href={"/logout"}>
|
||||
<button className="btn btn-ghost">
|
||||
<ExitIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface Dispatcher {
|
||||
userId: string;
|
||||
lastSeen: string;
|
||||
loginTime: string;
|
||||
logoffTime: string;
|
||||
selectedZone: string;
|
||||
name: string;
|
||||
socketId: string;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import Navbar from "../dispatch/_components/navbar/Navbar";
|
||||
import Navbar from "./_components/navbar/Navbar";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getServerSession } from "../api/auth/[...nextauth]/auth";
|
||||
|
||||
|
||||
37
apps/dispatch/app/pilot/page.tsx
Normal file
37
apps/dispatch/app/pilot/page.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { Pannel } from "dispatch/_components/pannel/Pannel";
|
||||
import { usePannelStore } from "_store/pannelStore";
|
||||
import { cn } from "helpers/cn";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Chat } from "../_components/left/Chat";
|
||||
import { Report } from "../_components/left/Report";
|
||||
|
||||
const DispatchPage = () => {
|
||||
const { isOpen } = usePannelStore();
|
||||
return (
|
||||
<div className="relative flex-1 flex transition-all duration-500 ease w-full">
|
||||
{/* <MapToastCard2 /> */}
|
||||
<div className="flex flex-1 relative">
|
||||
<div className="absolute left-0 top-1/2 transform -translate-y-1/2 pl-4 z-999999">
|
||||
<Chat />
|
||||
<div className="mt-2">
|
||||
<Report />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute right-0 w-[500px] z-999 transition-transform",
|
||||
isOpen ? "translate-x-0" : "translate-x-full",
|
||||
)}
|
||||
>
|
||||
<Pannel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DispatchPage.displayName = "DispatchPage";
|
||||
|
||||
export default DispatchPage;
|
||||
6
apps/dispatch/app/pilot/socket.ts
Normal file
6
apps/dispatch/app/pilot/socket.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { io } from "socket.io-client";
|
||||
import { dispatchSocket } from "dispatch/socket";
|
||||
|
||||
export const pilotSocket = io(process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL, {
|
||||
autoConnect: false,
|
||||
});
|
||||
19
apps/dispatch/app/querys/connected-user.ts
Normal file
19
apps/dispatch/app/querys/connected-user.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ConnectedAircraft, ConnectedDispatcher, Prisma } from "@repo/db";
|
||||
import axios from "axios";
|
||||
|
||||
export const getConnectedUserAPI = async (
|
||||
filter?: Prisma.KeywordWhereInput,
|
||||
) => {
|
||||
const res = await axios.get<(ConnectedAircraft | ConnectedDispatcher)[]>(
|
||||
"/api/connected-user",
|
||||
{
|
||||
params: {
|
||||
filter: JSON.stringify(filter),
|
||||
},
|
||||
},
|
||||
);
|
||||
if (res.status !== 200) {
|
||||
throw new Error("Failed to fetch Connected User");
|
||||
}
|
||||
return res.data;
|
||||
};
|
||||
14
apps/dispatch/app/querys/keywords.ts
Normal file
14
apps/dispatch/app/querys/keywords.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Keyword, Prisma } from "@repo/db";
|
||||
import axios from "axios";
|
||||
|
||||
export const getKeywordsAPI = async (filter?: Prisma.KeywordWhereInput) => {
|
||||
const res = await axios.get<Keyword[]>("/api/keywords", {
|
||||
params: {
|
||||
filter: JSON.stringify(filter),
|
||||
},
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
throw new Error("Failed to fetch keywords");
|
||||
}
|
||||
return res.data;
|
||||
};
|
||||
32
apps/dispatch/app/querys/missions.ts
Normal file
32
apps/dispatch/app/querys/missions.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Mission, Prisma } from "@repo/db";
|
||||
import axios from "axios";
|
||||
import { serverApi } from "helpers/axios";
|
||||
|
||||
export const getMissionsAPI = async (filter?: Prisma.MissionWhereInput) => {
|
||||
const res = await axios.get<Mission[]>("/api/missions", {
|
||||
params: {
|
||||
filter: JSON.stringify(filter),
|
||||
},
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
throw new Error("Failed to fetch stations");
|
||||
}
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const createMissionAPI = async (mission: Prisma.MissionCreateInput) => {
|
||||
const response = await serverApi.put<Mission>("/mission", mission);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const editMissionAPI = async (
|
||||
id: number,
|
||||
mission: Prisma.MissionUpdateInput,
|
||||
) => {
|
||||
const respone = await serverApi.patch<Mission>(`/mission/${id}`, mission);
|
||||
return respone.data;
|
||||
};
|
||||
|
||||
export const deleteMissionAPI = async (id: number) => {
|
||||
await serverApi.delete(`/mission/${id}`);
|
||||
};
|
||||
14
apps/dispatch/app/querys/stations.ts
Normal file
14
apps/dispatch/app/querys/stations.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Prisma, Station } from "@repo/db";
|
||||
import axios from "axios";
|
||||
|
||||
export const getStationsAPI = async (filter?: Prisma.StationWhereInput) => {
|
||||
const res = await axios.get<Station[]>("/api/stations", {
|
||||
params: {
|
||||
filter: JSON.stringify(filter),
|
||||
},
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
throw new Error("Failed to fetch stations");
|
||||
}
|
||||
return res.data;
|
||||
};
|
||||
@@ -17,7 +17,7 @@
|
||||
"@repo/db": "*",
|
||||
"@repo/ui": "*",
|
||||
"@tailwindcss/postcss": "^4.0.14",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@tanstack/react-query": "^5.75.4",
|
||||
"axios": "^1.9.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"livekit-client": "^2.9.7",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
LightningBoltIcon,
|
||||
LockOpen1Icon,
|
||||
HobbyKnifeIcon,
|
||||
ExclamationTriangleIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Button } from "../../../../../_components/ui/Button";
|
||||
import { Select } from "../../../../../_components/ui/Select";
|
||||
@@ -22,6 +23,7 @@ import { PaginatedTable, PaginatedTableRef } from "_components/PaginatedTable";
|
||||
import { min } from "date-fns";
|
||||
import { cn } from "../../../../../../helper/cn";
|
||||
import { ChartBarBigIcon, PlaneIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface ProfileFormProps {
|
||||
user: User;
|
||||
@@ -150,6 +152,13 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({
|
||||
<div className="flex-1">
|
||||
<h2 className="card-title">
|
||||
<MixerHorizontalIcon className="w-5 h-5" /> Dispo-Verbindungs Historie
|
||||
<button
|
||||
onClick={() => {
|
||||
dispoTableRef.current?.refresh();
|
||||
}}
|
||||
>
|
||||
refresh
|
||||
</button>
|
||||
</h2>
|
||||
<PaginatedTable
|
||||
ref={dispoTableRef}
|
||||
@@ -222,7 +231,22 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({
|
||||
userId: user.id,
|
||||
}}
|
||||
prismaModel={"connectedAircraft"}
|
||||
include={{ Station: true }}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "Station.bosCallsign",
|
||||
header: "Station",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Link
|
||||
className="link link-hover"
|
||||
href={`/admin/station/${(row.original as any).id}`}
|
||||
>
|
||||
{(row.original as any).Station.bosCallsign}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "loginTime",
|
||||
header: "Login",
|
||||
@@ -392,6 +416,9 @@ export const AdminForm = ({ user, dispoTime, pilotTime }: AdminFormProps) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="card-title">
|
||||
<ExclamationTriangleIcon className="w-5 h-5" /> Reports
|
||||
</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -47,6 +47,7 @@ const Page = async ({ params }: { params: { id: string } }) => {
|
||||
select: {
|
||||
loginTime: true,
|
||||
logoutTime: true,
|
||||
Station: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -37,10 +37,10 @@ export const resetPassword = async (id: string) => {
|
||||
return { password };
|
||||
};
|
||||
|
||||
export const deleteDispoHistory = async (id: string) => {
|
||||
return await prisma.connectedDispatcher.deleteMany({
|
||||
export const deleteDispoHistory = async (id: number) => {
|
||||
return await prisma.connectedDispatcher.delete({
|
||||
where: {
|
||||
userId: id,
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Binary file not shown.
297
package-lock.json
generated
297
package-lock.json
generated
@@ -27,7 +27,7 @@
|
||||
"@repo/db": "*",
|
||||
"@repo/ui": "*",
|
||||
"@tailwindcss/postcss": "^4.0.14",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@tanstack/react-query": "^5.75.4",
|
||||
"axios": "^1.9.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"livekit-client": "^2.9.7",
|
||||
@@ -533,9 +533,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
|
||||
"integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==",
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
|
||||
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1366,9 +1366,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz",
|
||||
"integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1384,13 +1384,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.4"
|
||||
"@img/sharp-libvips-darwin-arm64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz",
|
||||
"integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1406,13 +1406,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.4"
|
||||
"@img/sharp-libvips-darwin-x64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1426,9 +1426,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1442,9 +1442,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
|
||||
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz",
|
||||
"integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1458,9 +1458,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1473,10 +1473,26 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz",
|
||||
"integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
|
||||
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz",
|
||||
"integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1490,9 +1506,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1506,9 +1522,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1522,9 +1538,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1538,9 +1554,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
||||
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz",
|
||||
"integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1556,13 +1572,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.0.5"
|
||||
"@img/sharp-libvips-linux-arm": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz",
|
||||
"integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1578,13 +1594,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.4"
|
||||
"@img/sharp-libvips-linux-arm64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
|
||||
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz",
|
||||
"integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1600,13 +1616,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.4"
|
||||
"@img/sharp-libvips-linux-s390x": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz",
|
||||
"integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1622,13 +1638,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.0.4"
|
||||
"@img/sharp-libvips-linux-x64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz",
|
||||
"integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1644,13 +1660,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz",
|
||||
"integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1666,20 +1682,20 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
|
||||
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz",
|
||||
"integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.2.0"
|
||||
"@emnapi/runtime": "^1.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
@@ -1689,9 +1705,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
|
||||
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz",
|
||||
"integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1708,9 +1724,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz",
|
||||
"integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1898,9 +1914,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.2.tgz",
|
||||
"integrity": "sha512-yWgopCfA9XDR8ZH3taB5nRKtKJ1Q5fYsTOuYkzIIoS8TJ0UAUKAGF73JnGszbjk2ufAQDj6mDdgsJAFx5CLtYQ==",
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.1.tgz",
|
||||
"integrity": "sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
@@ -1914,9 +1930,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.2.tgz",
|
||||
"integrity": "sha512-HNBRnz+bkZ+KfyOExpUxTMR0Ow8nkkcE6IlsdEa9W/rI7gefud19+Sn1xYKwB9pdCdxIP1lPru/ZfjfA+iT8pw==",
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.1.tgz",
|
||||
"integrity": "sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1930,9 +1946,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.2.tgz",
|
||||
"integrity": "sha512-mJOUwp7al63tDpLpEFpKwwg5jwvtL1lhRW2fI1Aog0nYCPAhxbJsaZKdoVyPZCy8MYf/iQVNDuk/+i29iLCzIA==",
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.1.tgz",
|
||||
"integrity": "sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1946,9 +1962,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.2.tgz",
|
||||
"integrity": "sha512-5ZZ0Zwy3SgMr7MfWtRE7cQWVssfOvxYfD9O7XHM7KM4nrf5EOeqwq67ZXDgo86LVmffgsu5tPO57EeFKRnrfSQ==",
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.1.tgz",
|
||||
"integrity": "sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1962,9 +1978,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.2.tgz",
|
||||
"integrity": "sha512-cgKWBuFMLlJ4TWcFHl1KOaVVUAF8vy4qEvX5KsNd0Yj5mhu989QFCq1WjuaEbv/tO1ZpsQI6h/0YR8bLwEi+nA==",
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.1.tgz",
|
||||
"integrity": "sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1978,9 +1994,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.2.tgz",
|
||||
"integrity": "sha512-c3kWSOSsVL8rcNBBfOq1+/j2PKs2nsMwJUV4icUxRgGBwUOfppeh7YhN5s79enBQFU+8xRgVatFkhHU1QW7yUA==",
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.1.tgz",
|
||||
"integrity": "sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1994,9 +2010,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.2.tgz",
|
||||
"integrity": "sha512-PXTW9PLTxdNlVYgPJ0equojcq1kNu5NtwcNjRjHAB+/sdoKZ+X8FBu70fdJFadkxFIGekQTyRvPMFF+SOJaQjw==",
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.1.tgz",
|
||||
"integrity": "sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2010,9 +2026,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.2.tgz",
|
||||
"integrity": "sha512-nG644Es5llSGEcTaXhnGWR/aThM/hIaz0jx4MDg4gWC8GfTCp8eDBWZ77CVuv2ha/uL9Ce+nPTfYkSLG67/sHg==",
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.1.tgz",
|
||||
"integrity": "sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2026,9 +2042,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.2.tgz",
|
||||
"integrity": "sha512-52nWy65S/R6/kejz3jpvHAjZDPKIbEQu4x9jDBzmB9jJfuOy5rspjKu4u77+fI4M/WzLXrrQd57hlFGzz1ubcQ==",
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.1.tgz",
|
||||
"integrity": "sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2910,9 +2926,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.69.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.69.0.tgz",
|
||||
"integrity": "sha512-Kn410jq6vs1P8Nm+ZsRj9H+U3C0kjuEkYLxbiCyn3MDEiYor1j2DGVULqAz62SLZtUZ/e9Xt6xMXiJ3NJ65WyQ==",
|
||||
"version": "5.75.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.75.4.tgz",
|
||||
"integrity": "sha512-pcqOUgWG9oGlzkfRQQMMsEFmtQu0wq81A414CtELZGq+ztVwSTAaoB3AZRAXQJs88LmNMk2YpUKuQbrvzNDyRg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2920,12 +2936,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.69.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.69.0.tgz",
|
||||
"integrity": "sha512-Ift3IUNQqTcaFa1AiIQ7WCb/PPy8aexZdq9pZWLXhfLcLxH0+PZqJ2xFImxCpdDZrFRZhLJrh76geevS5xjRhA==",
|
||||
"version": "5.75.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.75.4.tgz",
|
||||
"integrity": "sha512-Vf65pzYRkf8fk9SP1ncIZjvaXszBhtsvpf+h45Y/9kOywOrVZfBGUpCdffdsVzbmBzmz6TCFes9bM0d3pRrIsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.69.0"
|
||||
"@tanstack/query-core": "5.75.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -10965,12 +10981,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.2.2",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.2.2.tgz",
|
||||
"integrity": "sha512-dgp8Kcx5XZRjMw2KNwBtUzhngRaURPioxoNIVl5BOyJbhi9CUgEtKDO7fx5wh8Z8vOVX1nYZ9meawJoRrlASYA==",
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.3.1.tgz",
|
||||
"integrity": "sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.2.2",
|
||||
"@next/env": "15.3.1",
|
||||
"@swc/counter": "0.1.3",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"busboy": "1.6.0",
|
||||
@@ -10985,15 +11001,15 @@
|
||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.2.2",
|
||||
"@next/swc-darwin-x64": "15.2.2",
|
||||
"@next/swc-linux-arm64-gnu": "15.2.2",
|
||||
"@next/swc-linux-arm64-musl": "15.2.2",
|
||||
"@next/swc-linux-x64-gnu": "15.2.2",
|
||||
"@next/swc-linux-x64-musl": "15.2.2",
|
||||
"@next/swc-win32-arm64-msvc": "15.2.2",
|
||||
"@next/swc-win32-x64-msvc": "15.2.2",
|
||||
"sharp": "^0.33.5"
|
||||
"@next/swc-darwin-arm64": "15.3.1",
|
||||
"@next/swc-darwin-x64": "15.3.1",
|
||||
"@next/swc-linux-arm64-gnu": "15.3.1",
|
||||
"@next/swc-linux-arm64-musl": "15.3.1",
|
||||
"@next/swc-linux-x64-gnu": "15.3.1",
|
||||
"@next/swc-linux-x64-musl": "15.3.1",
|
||||
"@next/swc-win32-arm64-msvc": "15.3.1",
|
||||
"@next/swc-win32-x64-msvc": "15.3.1",
|
||||
"sharp": "^0.34.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
@@ -15868,16 +15884,16 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
||||
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz",
|
||||
"integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
"detect-libc": "^2.0.3",
|
||||
"semver": "^7.6.3"
|
||||
"semver": "^7.7.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
@@ -15886,25 +15902,26 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.33.5",
|
||||
"@img/sharp-darwin-x64": "0.33.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.0.5",
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.0.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
|
||||
"@img/sharp-linux-arm": "0.33.5",
|
||||
"@img/sharp-linux-arm64": "0.33.5",
|
||||
"@img/sharp-linux-s390x": "0.33.5",
|
||||
"@img/sharp-linux-x64": "0.33.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.33.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.33.5",
|
||||
"@img/sharp-wasm32": "0.33.5",
|
||||
"@img/sharp-win32-ia32": "0.33.5",
|
||||
"@img/sharp-win32-x64": "0.33.5"
|
||||
"@img/sharp-darwin-arm64": "0.34.1",
|
||||
"@img/sharp-darwin-x64": "0.34.1",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.1.0",
|
||||
"@img/sharp-libvips-darwin-x64": "1.1.0",
|
||||
"@img/sharp-libvips-linux-arm": "1.1.0",
|
||||
"@img/sharp-libvips-linux-arm64": "1.1.0",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.1.0",
|
||||
"@img/sharp-libvips-linux-s390x": "1.1.0",
|
||||
"@img/sharp-libvips-linux-x64": "1.1.0",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.1.0",
|
||||
"@img/sharp-linux-arm": "0.34.1",
|
||||
"@img/sharp-linux-arm64": "0.34.1",
|
||||
"@img/sharp-linux-s390x": "0.34.1",
|
||||
"@img/sharp-linux-x64": "0.34.1",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.1",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.1",
|
||||
"@img/sharp-wasm32": "0.34.1",
|
||||
"@img/sharp-win32-ia32": "0.34.1",
|
||||
"@img/sharp-win32-x64": "0.34.1"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
|
||||
@@ -10,6 +10,6 @@ model ConnectedAircraft {
|
||||
positionLogIds Int[] @default([])
|
||||
|
||||
// relations:
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
station Station @relation(fields: [stationId], references: [id])
|
||||
User User @relation(fields: [userId], references: [id])
|
||||
Station Station @relation(fields: [stationId], references: [id])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user