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:
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",
|
||||
|
||||
Reference in New Issue
Block a user