Made Aircrafts fetch from Server. Added OSM Objects to mission
This commit is contained in:
73
apps/dispatch-server/routes/aircraft.ts
Normal file
73
apps/dispatch-server/routes/aircraft.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { prisma } from "@repo/db";
|
||||||
|
import { Router } from "express";
|
||||||
|
import { io } from "../index";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Get all connectedAircrafts
|
||||||
|
router.post("/", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const filter = req.body?.filter || {};
|
||||||
|
const connectedAircrafts = await prisma.connectedAircraft.findMany({
|
||||||
|
where: filter,
|
||||||
|
});
|
||||||
|
res.json(connectedAircrafts);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch connectedAircrafts" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get a single connectedAircraft by ID
|
||||||
|
router.get("/:id", async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
try {
|
||||||
|
const connectedAircraft = await prisma.connectedAircraft.findUnique({
|
||||||
|
where: { id: Number(id) },
|
||||||
|
});
|
||||||
|
if (connectedAircraft) {
|
||||||
|
res.json(connectedAircraft);
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: "ConnectedAircraft not found" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch connectedAircraft" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update a connectedAircraft by ID
|
||||||
|
router.patch("/:id", async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
try {
|
||||||
|
const updatedConnectedAircraft = await prisma.connectedAircraft.update({
|
||||||
|
where: { id: Number(id) },
|
||||||
|
data: req.body,
|
||||||
|
});
|
||||||
|
io.to("dispatchers").emit(
|
||||||
|
"update-connectedAircraft",
|
||||||
|
updatedConnectedAircraft,
|
||||||
|
);
|
||||||
|
res.json(updatedConnectedAircraft);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: "Failed to update connectedAircraft" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete a connectedAircraft by ID
|
||||||
|
router.delete("/:id", async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
try {
|
||||||
|
await prisma.connectedAircraft.delete({
|
||||||
|
where: { id: Number(id) },
|
||||||
|
});
|
||||||
|
io.to("dispatchers").emit("delete-connectedAircraft", id);
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: "Failed to delete connectedAircraft" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -42,7 +42,7 @@ router.put("/", async (req, res) => {
|
|||||||
const newMission = await prisma.mission.create({
|
const newMission = await prisma.mission.create({
|
||||||
data: req.body,
|
data: req.body,
|
||||||
});
|
});
|
||||||
io.to("missions").emit("new-mission", newMission);
|
io.to("dispatchers").emit("new-mission", newMission);
|
||||||
res.status(201).json(newMission);
|
res.status(201).json(newMission);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: "Failed to create mission" });
|
res.status(500).json({ error: "Failed to create mission" });
|
||||||
@@ -57,7 +57,7 @@ router.patch("/:id", async (req, res) => {
|
|||||||
where: { id: Number(id) },
|
where: { id: Number(id) },
|
||||||
data: req.body,
|
data: req.body,
|
||||||
});
|
});
|
||||||
io.to("missions").emit("update-mission", updatedMission);
|
io.to("dispatchers").emit("update-mission", updatedMission);
|
||||||
res.json(updatedMission);
|
res.json(updatedMission);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -72,7 +72,7 @@ router.delete("/:id", async (req, res) => {
|
|||||||
await prisma.mission.delete({
|
await prisma.mission.delete({
|
||||||
where: { id: Number(id) },
|
where: { id: Number(id) },
|
||||||
});
|
});
|
||||||
io.to("missions").emit("delete-mission", id);
|
io.to("dispatchers").emit("delete-mission", id);
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import livekitRouter from "./livekit";
|
|||||||
import dispatcherRotuer from "./dispatcher";
|
import dispatcherRotuer from "./dispatcher";
|
||||||
import missionRouter from "./mission";
|
import missionRouter from "./mission";
|
||||||
import statusRouter from "./status";
|
import statusRouter from "./status";
|
||||||
|
import aircraftsRouter from "./aircraft";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -10,5 +11,6 @@ router.use("/livekit", livekitRouter);
|
|||||||
router.use("/dispatcher", dispatcherRotuer);
|
router.use("/dispatcher", dispatcherRotuer);
|
||||||
router.use("/mission", missionRouter);
|
router.use("/mission", missionRouter);
|
||||||
router.use("/status", statusRouter);
|
router.use("/status", statusRouter);
|
||||||
|
router.use("/aircrafts", aircraftsRouter);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ export const handleConnectDispatch =
|
|||||||
|
|
||||||
socket.join("dispatchers"); // Dem Dispatcher-Raum beitreten
|
socket.join("dispatchers"); // Dem Dispatcher-Raum beitreten
|
||||||
socket.join(`user:${user.id}`); // Dem User-Raum beitreten
|
socket.join(`user:${user.id}`); // Dem User-Raum beitreten
|
||||||
socket.join("missions");
|
|
||||||
|
|
||||||
io.to("dispatchers").emit("dispatchers-update");
|
io.to("dispatchers").emit("dispatchers-update");
|
||||||
io.to("pilots").emit("dispatchers-update");
|
io.to("pilots").emit("dispatchers-update");
|
||||||
|
|||||||
@@ -30,7 +30,13 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const invalidateConnectedUsers = () => {
|
const invalidateConnectedUsers = () => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["connected-users"],
|
queryKey: ["connected-users", "aircrafts"],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidateConenctedAircrafts = () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["aircrafts"],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,6 +45,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
|||||||
dispatchSocket.on("new-mission", invalidateMission);
|
dispatchSocket.on("new-mission", invalidateMission);
|
||||||
dispatchSocket.on("dispatchers-update", invalidateConnectedUsers);
|
dispatchSocket.on("dispatchers-update", invalidateConnectedUsers);
|
||||||
dispatchSocket.on("pilots-update", invalidateConnectedUsers);
|
dispatchSocket.on("pilots-update", invalidateConnectedUsers);
|
||||||
|
dispatchSocket.on("update-connectedAircraft", invalidateConenctedAircrafts);
|
||||||
}, [queryClient]);
|
}, [queryClient]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -86,7 +86,12 @@ export const Chat = () => {
|
|||||||
</option>
|
</option>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{connectedUser?.map((user) => (
|
{[
|
||||||
|
...(connectedUser?.filter(
|
||||||
|
(user, idx, arr) =>
|
||||||
|
arr.findIndex((u) => u.userId === user.userId) === idx,
|
||||||
|
) || []),
|
||||||
|
].map((user) => (
|
||||||
<option key={user.userId} value={user.userId}>
|
<option key={user.userId} value={user.userId}>
|
||||||
{asPublicUser(user.publicUser).fullName}
|
{asPublicUser(user.publicUser).fullName}
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
@@ -75,9 +75,14 @@ export const Report = () => {
|
|||||||
Chatpartner auswählen
|
Chatpartner auswählen
|
||||||
</option>
|
</option>
|
||||||
)}
|
)}
|
||||||
{connectedUser?.map((user) => (
|
{[
|
||||||
|
...(connectedUser?.filter(
|
||||||
|
(user, idx, arr) =>
|
||||||
|
arr.findIndex((u) => u.userId === user.userId) === idx,
|
||||||
|
) || []),
|
||||||
|
].map((user) => (
|
||||||
<option key={user.userId} value={user.userId}>
|
<option key={user.userId} value={user.userId}>
|
||||||
{asPublicUser(user).fullName}
|
{asPublicUser(user.publicUser).fullName}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
import { create } from "zustand";
|
|
||||||
|
|
||||||
export interface Aircraft {
|
|
||||||
id: string;
|
|
||||||
bosName: string;
|
|
||||||
bosNameShort: string;
|
|
||||||
bosNutzung: string;
|
|
||||||
fmsStatus: string;
|
|
||||||
fmsLog: {
|
|
||||||
status: string;
|
|
||||||
timestamp: string;
|
|
||||||
user: string;
|
|
||||||
}[];
|
|
||||||
location: {
|
|
||||||
lat: number;
|
|
||||||
lng: number;
|
|
||||||
altitude: number;
|
|
||||||
speed: number;
|
|
||||||
};
|
|
||||||
locationHistory: {
|
|
||||||
lat: number;
|
|
||||||
lon: number;
|
|
||||||
altitude: number;
|
|
||||||
speed: number;
|
|
||||||
timestamp: string;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AircraftStore {
|
|
||||||
aircrafts: Aircraft[];
|
|
||||||
setAircrafts: (aircrafts: Aircraft[]) => void;
|
|
||||||
setAircraft: (aircraft: Aircraft) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAircraftsStore = create<AircraftStore>((set) => ({
|
|
||||||
aircrafts: [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
bosName: "Christoph 31",
|
|
||||||
bosNameShort: "CHX31",
|
|
||||||
bosNutzung: "RTH",
|
|
||||||
fmsStatus: "1",
|
|
||||||
fmsLog: [],
|
|
||||||
location: {
|
|
||||||
lat: 52.546781040592776,
|
|
||||||
lng: 13.369535209542219,
|
|
||||||
altitude: 0,
|
|
||||||
speed: 0,
|
|
||||||
},
|
|
||||||
locationHistory: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
bosName: "Christoph Berlin",
|
|
||||||
bosNameShort: "CHX83",
|
|
||||||
bosNutzung: "ITH",
|
|
||||||
fmsStatus: "2",
|
|
||||||
fmsLog: [],
|
|
||||||
location: {
|
|
||||||
lat: 52.54588546048977,
|
|
||||||
lng: 13.46470691054384,
|
|
||||||
altitude: 0,
|
|
||||||
speed: 0,
|
|
||||||
},
|
|
||||||
locationHistory: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
bosName: "Christoph 100",
|
|
||||||
bosNameShort: "CHX100",
|
|
||||||
bosNutzung: "RTH",
|
|
||||||
fmsStatus: "7",
|
|
||||||
fmsLog: [],
|
|
||||||
location: {
|
|
||||||
lat: 52.497519717230155,
|
|
||||||
lng: 13.342040806552554,
|
|
||||||
altitude: 0,
|
|
||||||
speed: 0,
|
|
||||||
},
|
|
||||||
locationHistory: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "4",
|
|
||||||
bosName: "Christophorus 1",
|
|
||||||
bosNameShort: "A4",
|
|
||||||
bosNutzung: "RTH",
|
|
||||||
fmsStatus: "6",
|
|
||||||
fmsLog: [],
|
|
||||||
location: {
|
|
||||||
lat: 52.50175041192073,
|
|
||||||
lng: 13.478628701227349,
|
|
||||||
altitude: 0,
|
|
||||||
speed: 0,
|
|
||||||
},
|
|
||||||
locationHistory: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
setAircrafts: (aircrafts) => set({ aircrafts }),
|
|
||||||
setAircraft: (aircraft) =>
|
|
||||||
set((state) => {
|
|
||||||
const existingAircraftIndex = state.aircrafts.findIndex(
|
|
||||||
(a) => a.id === aircraft.id,
|
|
||||||
);
|
|
||||||
if (existingAircraftIndex !== -1) {
|
|
||||||
const updatedAircrafts = [...state.aircrafts];
|
|
||||||
updatedAircrafts[existingAircraftIndex] = aircraft;
|
|
||||||
return { aircrafts: updatedAircrafts };
|
|
||||||
} else {
|
|
||||||
return { aircrafts: [...state.aircrafts, aircraft] };
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { OSMWay } from "@repo/db";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
interface MapStore {
|
interface MapStore {
|
||||||
@@ -18,30 +19,14 @@ interface MapStore {
|
|||||||
close: number[];
|
close: number[];
|
||||||
}) => void;
|
}) => void;
|
||||||
openAircraftMarker: {
|
openAircraftMarker: {
|
||||||
id: string;
|
id: number;
|
||||||
tab: "home" | "fms" | "aircraft" | "mission" | "chat";
|
tab: "home" | "fms" | "aircraft" | "mission" | "chat";
|
||||||
}[];
|
}[];
|
||||||
setOpenAircraftMarker: (aircraft: {
|
setOpenAircraftMarker: (aircraft: {
|
||||||
open: MapStore["openAircraftMarker"];
|
open: MapStore["openAircraftMarker"];
|
||||||
close: string[];
|
close: number[];
|
||||||
}) => void;
|
}) => void;
|
||||||
searchElements: {
|
searchElements: OSMWay[];
|
||||||
id: number;
|
|
||||||
nodes: {
|
|
||||||
lat: number;
|
|
||||||
lon: number;
|
|
||||||
}[];
|
|
||||||
tags?: {
|
|
||||||
"addr:country"?: string;
|
|
||||||
"addr:city"?: string;
|
|
||||||
"addr:housenumber"?: string;
|
|
||||||
"addr:postcode"?: string;
|
|
||||||
"addr:street"?: string;
|
|
||||||
"addr:suburb"?: string;
|
|
||||||
building?: string;
|
|
||||||
};
|
|
||||||
type: string;
|
|
||||||
}[];
|
|
||||||
setSearchElements: (elements: MapStore["searchElements"]) => void;
|
setSearchElements: (elements: MapStore["searchElements"]) => void;
|
||||||
setContextMenu: (popup: MapStore["contextMenu"]) => void;
|
setContextMenu: (popup: MapStore["contextMenu"]) => void;
|
||||||
searchPopup: {
|
searchPopup: {
|
||||||
@@ -54,8 +39,8 @@ interface MapStore {
|
|||||||
[aircraftId: string]: "home" | "fms" | "aircraft" | "mission" | "chat";
|
[aircraftId: string]: "home" | "fms" | "aircraft" | "mission" | "chat";
|
||||||
};
|
};
|
||||||
setAircraftTab: (
|
setAircraftTab: (
|
||||||
aircraftId: string,
|
aircraftId: number,
|
||||||
tab: MapStore["aircraftTabs"][string],
|
tab: MapStore["aircraftTabs"][number],
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ interface ConnectionStore {
|
|||||||
disconnect: () => void;
|
disconnect: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDispatchConnectionStore = create<ConnectionStore>((set) => ({
|
export const usePilotConnectionStore = create<ConnectionStore>((set) => ({
|
||||||
status: "disconnected",
|
status: "disconnected",
|
||||||
message: "",
|
message: "",
|
||||||
selectedStation: null,
|
selectedStation: null,
|
||||||
@@ -40,23 +40,23 @@ export const useDispatchConnectionStore = create<ConnectionStore>((set) => ({
|
|||||||
|
|
||||||
dispatchSocket.on("connect", () => {
|
dispatchSocket.on("connect", () => {
|
||||||
pilotSocket.disconnect();
|
pilotSocket.disconnect();
|
||||||
useDispatchConnectionStore.setState({ status: "connected", message: "" });
|
usePilotConnectionStore.setState({ status: "connected", message: "" });
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatchSocket.on("connect_error", (err) => {
|
dispatchSocket.on("connect_error", (err) => {
|
||||||
useDispatchConnectionStore.setState({
|
usePilotConnectionStore.setState({
|
||||||
status: "error",
|
status: "error",
|
||||||
message: err.message,
|
message: err.message,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatchSocket.on("disconnect", () => {
|
dispatchSocket.on("disconnect", () => {
|
||||||
useDispatchConnectionStore.setState({ status: "disconnected", message: "" });
|
usePilotConnectionStore.setState({ status: "disconnected", message: "" });
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatchSocket.on("force-disconnect", (reason: string) => {
|
dispatchSocket.on("force-disconnect", (reason: string) => {
|
||||||
console.log("force-disconnect", reason);
|
console.log("force-disconnect", reason);
|
||||||
useDispatchConnectionStore.setState({
|
usePilotConnectionStore.setState({
|
||||||
status: "disconnected",
|
status: "disconnected",
|
||||||
message: reason,
|
message: reason,
|
||||||
});
|
});
|
||||||
|
|||||||
25
apps/dispatch/app/api/aircrafts/route.ts
Normal file
25
apps/dispatch/app/api/aircrafts/route.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { prisma } from "@repo/db";
|
||||||
|
|
||||||
|
export async function GET(): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const connectedAircraft = await prisma.connectedAircraft.findMany({
|
||||||
|
where: {
|
||||||
|
logoutTime: null,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
Station: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(connectedAircraft, {
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to fetch Aircrafts" },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Aircraft, useAircraftsStore } from "_store/aircraftsStore";
|
|
||||||
import { Marker, useMap } from "react-leaflet";
|
import { Marker, useMap } from "react-leaflet";
|
||||||
import { DivIcon, Marker as LMarker, Popup as LPopup } from "leaflet";
|
import { DivIcon, Marker as LMarker, Popup as LPopup } from "leaflet";
|
||||||
import { useMapStore } from "_store/mapStore";
|
import { useMapStore } from "_store/mapStore";
|
||||||
@@ -26,6 +25,9 @@ import FMSStatusHistory, {
|
|||||||
FMSStatusSelector,
|
FMSStatusSelector,
|
||||||
RettungsmittelTab,
|
RettungsmittelTab,
|
||||||
} from "./_components/AircraftMarkerTabs";
|
} from "./_components/AircraftMarkerTabs";
|
||||||
|
import { ConnectedAircraft, Station } from "@repo/db";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getConnectedAircraftsAPI } from "querys/aircrafts";
|
||||||
|
|
||||||
export const FMS_STATUS_COLORS: { [key: string]: string } = {
|
export const FMS_STATUS_COLORS: { [key: string]: string } = {
|
||||||
"0": "rgb(140,10,10)",
|
"0": "rgb(140,10,10)",
|
||||||
@@ -54,7 +56,11 @@ export const FMS_STATUS_TEXT_COLORS: { [key: string]: string } = {
|
|||||||
N: "rgb(153,153,153)",
|
N: "rgb(153,153,153)",
|
||||||
};
|
};
|
||||||
|
|
||||||
const AircraftPopupContent = ({ aircraft }: { aircraft: Aircraft }) => {
|
const AircraftPopupContent = ({
|
||||||
|
aircraft,
|
||||||
|
}: {
|
||||||
|
aircraft: ConnectedAircraft & { Station: Station };
|
||||||
|
}) => {
|
||||||
const setAircraftTab = useMapStore((state) => state.setAircraftTab);
|
const setAircraftTab = useMapStore((state) => state.setAircraftTab);
|
||||||
const currentTab = useMapStore(
|
const currentTab = useMapStore(
|
||||||
(state) => state.aircraftTabs[aircraft.id] || "home",
|
(state) => state.aircraftTabs[aircraft.id] || "home",
|
||||||
@@ -182,9 +188,9 @@ const AircraftPopupContent = ({ aircraft }: { aircraft: Aircraft }) => {
|
|||||||
onClick={() => handleTabChange("aircraft")}
|
onClick={() => handleTabChange("aircraft")}
|
||||||
>
|
>
|
||||||
<span className="text-white text-base font-medium">
|
<span className="text-white text-base font-medium">
|
||||||
{aircraft.bosName}
|
{aircraft.Station.bosCallsign}
|
||||||
<br />
|
<br />
|
||||||
{aircraft.bosNutzung}
|
{aircraft.Station.bosUse}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -225,7 +231,11 @@ const AircraftPopupContent = ({ aircraft }: { aircraft: Aircraft }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
const AircraftMarker = ({
|
||||||
|
aircraft,
|
||||||
|
}: {
|
||||||
|
aircraft: ConnectedAircraft & { Station: Station };
|
||||||
|
}) => {
|
||||||
const [hideMarker, setHideMarker] = useState(false);
|
const [hideMarker, setHideMarker] = useState(false);
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
const markerRef = useRef<LMarker>(null);
|
const markerRef = useRef<LMarker>(null);
|
||||||
@@ -293,7 +303,7 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
|||||||
}, [map, openAircraftMarker, handleConflict]);
|
}, [map, openAircraftMarker, handleConflict]);
|
||||||
|
|
||||||
const getMarkerHTML = (
|
const getMarkerHTML = (
|
||||||
aircraft: Aircraft,
|
aircraft: ConnectedAircraft & { Station: Station },
|
||||||
anchor: "topleft" | "topright" | "bottomleft" | "bottomright",
|
anchor: "topleft" | "topright" | "bottomleft" | "bottomright",
|
||||||
) => {
|
) => {
|
||||||
return `<div
|
return `<div
|
||||||
@@ -327,12 +337,12 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
|||||||
${aircraft.fmsStatus}
|
${aircraft.fmsStatus}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-white text-[15px] text-nowrap">
|
<span class="text-white text-[15px] text-nowrap">
|
||||||
${aircraft.bosName}
|
${aircraft.Station.bosCallsign}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
data-id="${aircraft.id}"
|
data-id="${aircraft.id}"
|
||||||
data-anchor-lat="${aircraft.location.lat}"
|
data-anchor-lat="${aircraft.posLat}"
|
||||||
data-anchor-lng="${aircraft.location.lng}"
|
data-anchor-lng="${aircraft.posLng}"
|
||||||
id="marker-domain-aircraft-${aircraft.id}"
|
id="marker-domain-aircraft-${aircraft.id}"
|
||||||
class="${cn(
|
class="${cn(
|
||||||
"map-collision absolute w-[200%] h-[200%] top-0 left-0 transform pointer-events-none",
|
"map-collision absolute w-[200%] h-[200%] top-0 left-0 transform pointer-events-none",
|
||||||
@@ -345,9 +355,10 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={aircraft.id}>
|
<Fragment key={aircraft.id}>
|
||||||
|
{aircraft.simulatorConnected && (
|
||||||
<Marker
|
<Marker
|
||||||
ref={markerRef}
|
ref={markerRef}
|
||||||
position={[aircraft.location.lat, aircraft.location.lng]}
|
position={[aircraft.posLat!, aircraft.posLng!]}
|
||||||
icon={
|
icon={
|
||||||
new DivIcon({
|
new DivIcon({
|
||||||
iconAnchor: [0, 0],
|
iconAnchor: [0, 0],
|
||||||
@@ -355,6 +366,7 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{openAircraftMarker.some((m) => m.id === aircraft.id) && !hideMarker && (
|
{openAircraftMarker.some((m) => m.id === aircraft.id) && !hideMarker && (
|
||||||
<SmartPopup
|
<SmartPopup
|
||||||
options={{
|
options={{
|
||||||
@@ -362,7 +374,7 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
|||||||
}}
|
}}
|
||||||
id={`aircraft-${aircraft.id}`}
|
id={`aircraft-${aircraft.id}`}
|
||||||
ref={popupRef}
|
ref={popupRef}
|
||||||
position={[aircraft.location.lat, aircraft.location.lng]}
|
position={[aircraft.posLat!, aircraft.posLng!]}
|
||||||
autoClose={false}
|
autoClose={false}
|
||||||
closeOnClick={false}
|
closeOnClick={false}
|
||||||
autoPan={false}
|
autoPan={false}
|
||||||
@@ -379,12 +391,14 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AircraftLayer = () => {
|
export const AircraftLayer = () => {
|
||||||
const aircrafts = useAircraftsStore((state) => state.aircrafts);
|
const { data: aircrafts } = useQuery({
|
||||||
|
queryKey: ["aircrafts"],
|
||||||
|
queryFn: getConnectedAircraftsAPI,
|
||||||
|
});
|
||||||
|
|
||||||
// IDEA: Add Marker to Map Layer / LayerGroup
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{aircrafts.map((aircraft) => {
|
{aircrafts?.map((aircraft) => {
|
||||||
return <AircraftMarker key={aircraft.id} aircraft={aircraft} />;
|
return <AircraftMarker key={aircraft.id} aircraft={aircraft} />;
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { OSMWay } from "@repo/db";
|
||||||
import { useMapStore } from "_store/mapStore";
|
import { useMapStore } from "_store/mapStore";
|
||||||
import { usePannelStore } from "_store/pannelStore";
|
import { usePannelStore } from "_store/pannelStore";
|
||||||
import { MapPinned, Search } from "lucide-react";
|
import { MapPinned, Search } from "lucide-react";
|
||||||
@@ -29,6 +31,43 @@ export const ContextMenu = () => {
|
|||||||
|
|
||||||
if (!contextMenu) return null;
|
if (!contextMenu) return null;
|
||||||
|
|
||||||
|
const addOSMobjects = async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://overpass-api.de/api/interpreter?data=${encodeURIComponent(`
|
||||||
|
[out:json];
|
||||||
|
(
|
||||||
|
way["building"](around:100, ${contextMenu.lat}, ${contextMenu.lng});
|
||||||
|
relation["building"](around:100, ${contextMenu.lat}, ${contextMenu.lng});
|
||||||
|
);
|
||||||
|
out body;
|
||||||
|
>;
|
||||||
|
out skel qt;
|
||||||
|
`)}`,
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
const parsed: OSMWay[] = data.elements
|
||||||
|
.filter((e: any) => e.type === "way")
|
||||||
|
.map((e: any) => {
|
||||||
|
return {
|
||||||
|
wayID: e.id,
|
||||||
|
nodes: e.nodes.map((nodeId: string) => {
|
||||||
|
const node = data.elements.find(
|
||||||
|
(element: any) => element.id === nodeId,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
lat: node.lat,
|
||||||
|
lon: node.lon,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
tags: e.tags,
|
||||||
|
type: e.type,
|
||||||
|
} as OSMWay;
|
||||||
|
});
|
||||||
|
|
||||||
|
setSearchElements(parsed);
|
||||||
|
return parsed;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup
|
<Popup
|
||||||
position={[contextMenu.lat, contextMenu.lng]}
|
position={[contextMenu.lat, contextMenu.lng]}
|
||||||
@@ -70,6 +109,31 @@ export const ContextMenu = () => {
|
|||||||
place_rank: number;
|
place_rank: number;
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
const objects = await addOSMobjects();
|
||||||
|
const exactAddress = objects.find((object) => {
|
||||||
|
return (
|
||||||
|
object.tags["addr:street"] == data.address.road &&
|
||||||
|
object.tags["addr:housenumber"]?.includes(
|
||||||
|
data.address.house_number,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const closestToContext = objects.reduce((prev, curr) => {
|
||||||
|
const prevLat = prev.nodes?.[0]?.lat ?? 0;
|
||||||
|
const prevLon = prev.nodes?.[0]?.lon ?? 0;
|
||||||
|
const currLat = curr.nodes?.[0]?.lat ?? 0;
|
||||||
|
const currLon = curr.nodes?.[0]?.lon ?? 0;
|
||||||
|
const prevDistance = Math.sqrt(
|
||||||
|
Math.pow(prevLat - contextMenu.lat, 2) +
|
||||||
|
Math.pow(prevLon - contextMenu.lng, 2),
|
||||||
|
);
|
||||||
|
const currDistance = Math.sqrt(
|
||||||
|
Math.pow(currLat - contextMenu.lat, 2) +
|
||||||
|
Math.pow(currLon - contextMenu.lng, 2),
|
||||||
|
);
|
||||||
|
return prevDistance < currDistance ? prev : curr;
|
||||||
|
});
|
||||||
|
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
setMissionFormValues({
|
setMissionFormValues({
|
||||||
addressLat: contextMenu.lat,
|
addressLat: contextMenu.lat,
|
||||||
@@ -78,6 +142,7 @@ export const ContextMenu = () => {
|
|||||||
addressStreet: `${data.address.road}, ${data.address.house_number || "keine HN"}`,
|
addressStreet: `${data.address.road}, ${data.address.house_number || "keine HN"}`,
|
||||||
addressZip: data.address.postcode,
|
addressZip: data.address.postcode,
|
||||||
state: "draft",
|
state: "draft",
|
||||||
|
addressOSMways: [(exactAddress || closestToContext) as any],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -86,39 +151,7 @@ export const ContextMenu = () => {
|
|||||||
<button
|
<button
|
||||||
className="btn btn-sm rounded-full bg-rescuetrack aspect-square"
|
className="btn btn-sm rounded-full bg-rescuetrack aspect-square"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const res = await fetch(
|
addOSMobjects();
|
||||||
`https://overpass-api.de/api/interpreter?data=${encodeURIComponent(`
|
|
||||||
[out:json];
|
|
||||||
(
|
|
||||||
way["building"](around:100, ${contextMenu.lat}, ${contextMenu.lng});
|
|
||||||
relation["building"](around:100, ${contextMenu.lat}, ${contextMenu.lng});
|
|
||||||
);
|
|
||||||
out body;
|
|
||||||
>;
|
|
||||||
out skel qt;
|
|
||||||
`)}`,
|
|
||||||
);
|
|
||||||
const data = await res.json();
|
|
||||||
setSearchElements(
|
|
||||||
data.elements
|
|
||||||
.filter((e: any) => e.type === "way")
|
|
||||||
.map((e: any) => {
|
|
||||||
return {
|
|
||||||
id: e.id,
|
|
||||||
nodes: e.nodes.map((nodeId: string) => {
|
|
||||||
const node = data.elements.find(
|
|
||||||
(element: any) => element.id === nodeId,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
lat: node.lat,
|
|
||||||
lon: node.lon,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
tags: e.tags,
|
|
||||||
type: e.type,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Search size={20} />
|
<Search size={20} />
|
||||||
|
|||||||
@@ -1,22 +1,45 @@
|
|||||||
import { useMapStore } from "_store/mapStore";
|
import { useMapStore } from "_store/mapStore";
|
||||||
import { Marker as LMarker } from "leaflet";
|
import { Marker as LMarker } from "leaflet";
|
||||||
import { useEffect, useRef } from "react";
|
import { Fragment, useEffect, useRef } from "react";
|
||||||
import { Marker, Polygon, Polyline, Popup } from "react-leaflet";
|
import { Marker, Polygon, Popup } from "react-leaflet";
|
||||||
import L from "leaflet";
|
import L from "leaflet";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getMissionsAPI } from "querys/missions";
|
||||||
|
import { OSMWay } from "@repo/db";
|
||||||
|
|
||||||
export const SearchElements = () => {
|
export const SearchElements = () => {
|
||||||
const { searchElements, searchPopup, setSearchPopup, setContextMenu } =
|
const {
|
||||||
useMapStore();
|
searchElements,
|
||||||
|
searchPopup,
|
||||||
|
setSearchPopup,
|
||||||
|
setContextMenu,
|
||||||
|
openMissionMarker,
|
||||||
|
} = useMapStore();
|
||||||
|
const missions = useQuery({
|
||||||
|
queryKey: ["missions"],
|
||||||
|
queryFn: () =>
|
||||||
|
getMissionsAPI({
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
state: "draft",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: "running",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
const poppupRef = useRef<LMarker>(null);
|
const poppupRef = useRef<LMarker>(null);
|
||||||
const intervalRef = useRef<NodeJS.Timeout>(null);
|
|
||||||
const searchPopupElement = searchElements.find(
|
const searchPopupElement = searchElements.find(
|
||||||
(element) => element.id === searchPopup?.elementId,
|
(element) => element.wayID === searchPopup?.elementId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const SearchElement = ({
|
const SearchElement = ({
|
||||||
element,
|
element,
|
||||||
|
isActive = false,
|
||||||
}: {
|
}: {
|
||||||
element: (typeof searchElements)[1];
|
element: (typeof searchElements)[1];
|
||||||
|
isActive?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<any>(null);
|
const ref = useRef<any>(null);
|
||||||
|
|
||||||
@@ -24,11 +47,11 @@ export const SearchElements = () => {
|
|||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
ref.current.on("click", () => {
|
ref.current.on("click", () => {
|
||||||
const center = ref.current.getBounds().getCenter();
|
const center = ref.current.getBounds().getCenter();
|
||||||
if (searchPopup?.elementId !== element.id) {
|
if (searchPopup?.elementId !== element.wayID) {
|
||||||
setSearchPopup({
|
setSearchPopup({
|
||||||
lat: center.lat,
|
lat: center.lat,
|
||||||
lng: center.lng,
|
lng: center.lng,
|
||||||
elementId: element.id,
|
elementId: element.wayID,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setSearchPopup(null);
|
setSearchPopup(null);
|
||||||
@@ -40,9 +63,12 @@ export const SearchElements = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Polygon
|
<Polygon
|
||||||
key={element.id}
|
|
||||||
positions={element.nodes.map((node) => [node.lat, node.lon])}
|
positions={element.nodes.map((node) => [node.lat, node.lon])}
|
||||||
color={searchPopup?.elementId === element.id ? "#ff4500" : "#46b7a3"}
|
color={
|
||||||
|
searchPopup?.elementId === element.wayID || isActive
|
||||||
|
? "#ff4500"
|
||||||
|
: "#46b7a3"
|
||||||
|
}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -86,8 +112,30 @@ export const SearchElements = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{searchElements.map((element) => {
|
{openMissionMarker.map(({ id }) => {
|
||||||
return <SearchElement key={element.id} element={element} />;
|
const mission = missions.data?.find((m) => m.id === id);
|
||||||
|
if (!mission) return null;
|
||||||
|
return (
|
||||||
|
<Fragment key={`mission-osm-${mission.id}`}>
|
||||||
|
{(mission.addressOSMways as (OSMWay | null)[])
|
||||||
|
.filter((element): element is OSMWay => element !== null)
|
||||||
|
.map((element: OSMWay, i) => (
|
||||||
|
<SearchElement
|
||||||
|
key={`mission-elem-${element.wayID}-${i}`}
|
||||||
|
element={element}
|
||||||
|
isActive
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{searchElements.map((element, i) => {
|
||||||
|
return (
|
||||||
|
<SearchElement
|
||||||
|
key={`mission-elem-${element.wayID}-${i}`}
|
||||||
|
element={element}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
{searchPopup && (
|
{searchPopup && (
|
||||||
<Marker
|
<Marker
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "../AircraftMarker";
|
import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "../AircraftMarker";
|
||||||
import { Aircraft } from "_store/aircraftsStore";
|
import { ConnectedAircraft, Prisma, Station } from "@repo/db";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { editConnectedAircraftAPI } from "querys/aircrafts";
|
||||||
|
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
||||||
|
import { cn } from "helpers/cn";
|
||||||
|
|
||||||
const FMSStatusHistory = () => {
|
const FMSStatusHistory = () => {
|
||||||
const dummyData = [
|
const dummyData = [
|
||||||
@@ -33,17 +38,42 @@ const FMSStatusHistory = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FMSStatusSelector = ({ aircraft }: { aircraft: Aircraft }) => {
|
const FMSStatusSelector = ({
|
||||||
|
aircraft,
|
||||||
|
}: {
|
||||||
|
aircraft: ConnectedAircraft & { Station: Station };
|
||||||
|
}) => {
|
||||||
|
const dispatcherConnected =
|
||||||
|
useDispatchConnectionStore((s) => s.status) === "connected";
|
||||||
const [hoveredStatus, setHoveredStatus] = useState<string | null>(null);
|
const [hoveredStatus, setHoveredStatus] = useState<string | null>(null);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const changeAircraftMutation = useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
id,
|
||||||
|
update,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
update: Prisma.ConnectedAircraftUpdateInput;
|
||||||
|
}) => {
|
||||||
|
await editConnectedAircraftAPI(id, update);
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["aircrafts"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 p-4 justify-center items-center min-h-[136px] h-full">
|
<div className="flex gap-2 p-4 justify-center items-center min-h-[136px] h-full">
|
||||||
{Array.from({ length: 9 }, (_, i) => (i + 1).toString())
|
{Array.from({ length: 9 }, (_, i) => (i + 1).toString())
|
||||||
.filter((status) => status !== "5") // Exclude status 5
|
.filter((status) => status !== "5") // Exclude status 5
|
||||||
.map((status) => (
|
.map((status) => (
|
||||||
<div
|
<button
|
||||||
|
disabled={!dispatcherConnected}
|
||||||
key={status}
|
key={status}
|
||||||
className="flex justify-center items-center min-w-13 min-h-13 cursor-pointer text-4xl font-bold"
|
className={cn(
|
||||||
|
"flex justify-center items-center min-w-13 min-h-13 cursor-pointer text-4xl font-bold",
|
||||||
|
!dispatcherConnected && "cursor-not-allowed",
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
hoveredStatus === status
|
hoveredStatus === status
|
||||||
@@ -60,15 +90,19 @@ const FMSStatusSelector = ({ aircraft }: { aircraft: Aircraft }) => {
|
|||||||
}}
|
}}
|
||||||
onMouseEnter={() => setHoveredStatus(status)}
|
onMouseEnter={() => setHoveredStatus(status)}
|
||||||
onMouseLeave={() => setHoveredStatus(null)}
|
onMouseLeave={() => setHoveredStatus(null)}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
// Handle status change logic here
|
// Handle status change logic here
|
||||||
console.log(
|
await changeAircraftMutation.mutateAsync({
|
||||||
`Status changed to: ${status} for aircraft ${aircraft.id}`,
|
id: aircraft.id,
|
||||||
);
|
update: {
|
||||||
|
fmsStatus: status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
toast.success(`Status changed to ${status}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{status}
|
{status}
|
||||||
</div>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Mission } from "@repo/db";
|
import { ConnectedAircraft, Mission, Station } from "@repo/db";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { SmartPopup, useSmartPopup } from "_components/SmartPopup";
|
import { SmartPopup, useSmartPopup } from "_components/SmartPopup";
|
||||||
import { Aircraft, useAircraftsStore } from "_store/aircraftsStore";
|
|
||||||
import { useMapStore } from "_store/mapStore";
|
import { useMapStore } from "_store/mapStore";
|
||||||
import {
|
import {
|
||||||
FMS_STATUS_COLORS,
|
FMS_STATUS_COLORS,
|
||||||
@@ -12,6 +11,7 @@ import {
|
|||||||
MISSION_STATUS_TEXT_COLORS,
|
MISSION_STATUS_TEXT_COLORS,
|
||||||
} from "dispatch/_components/map/MissionMarkers";
|
} from "dispatch/_components/map/MissionMarkers";
|
||||||
import { cn } from "helpers/cn";
|
import { cn } from "helpers/cn";
|
||||||
|
import { getConnectedAircraftsAPI } from "querys/aircrafts";
|
||||||
import { getMissionsAPI } from "querys/missions";
|
import { getMissionsAPI } from "querys/missions";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useMap } from "react-leaflet";
|
import { useMap } from "react-leaflet";
|
||||||
@@ -20,7 +20,7 @@ const PopupContent = ({
|
|||||||
aircrafts,
|
aircrafts,
|
||||||
missions,
|
missions,
|
||||||
}: {
|
}: {
|
||||||
aircrafts: Aircraft[];
|
aircrafts: (ConnectedAircraft & { Station: Station })[];
|
||||||
missions: Mission[];
|
missions: Mission[];
|
||||||
}) => {
|
}) => {
|
||||||
const { anchor } = useSmartPopup();
|
const { anchor } = useSmartPopup();
|
||||||
@@ -101,7 +101,9 @@ const PopupContent = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{aircrafts.map((aircraft) => (
|
{aircrafts
|
||||||
|
.filter((a) => a.simulatorConnected)
|
||||||
|
.map((aircraft) => (
|
||||||
<div
|
<div
|
||||||
key={aircraft.id}
|
key={aircraft.id}
|
||||||
className="relative w-auto inline-flex items-center gap-2 text-nowrap cursor-pointer"
|
className="relative w-auto inline-flex items-center gap-2 text-nowrap cursor-pointer"
|
||||||
@@ -118,7 +120,7 @@ const PopupContent = ({
|
|||||||
],
|
],
|
||||||
close: [],
|
close: [],
|
||||||
});
|
});
|
||||||
map.setView([aircraft.location.lat, aircraft.location.lng], 12, {
|
map.setView([aircraft.posLat!, aircraft.posLng!], 12, {
|
||||||
animate: true,
|
animate: true,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -131,7 +133,7 @@ const PopupContent = ({
|
|||||||
>
|
>
|
||||||
{aircraft.fmsStatus}
|
{aircraft.fmsStatus}
|
||||||
</span>
|
</span>
|
||||||
<span>{aircraft.bosName}</span>
|
<span>{aircraft.Station.bosCallsign}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -141,7 +143,11 @@ const PopupContent = ({
|
|||||||
|
|
||||||
export const MarkerCluster = () => {
|
export const MarkerCluster = () => {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
const aircrafts = useAircraftsStore((state) => state.aircrafts);
|
const { data: aircrafts } = useQuery({
|
||||||
|
queryKey: ["aircrafts"],
|
||||||
|
queryFn: getConnectedAircraftsAPI,
|
||||||
|
});
|
||||||
|
|
||||||
const { data: missions } = useQuery({
|
const { data: missions } = useQuery({
|
||||||
queryKey: ["missions"],
|
queryKey: ["missions"],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
@@ -151,7 +157,7 @@ export const MarkerCluster = () => {
|
|||||||
});
|
});
|
||||||
const [cluster, setCluster] = useState<
|
const [cluster, setCluster] = useState<
|
||||||
{
|
{
|
||||||
aircrafts: Aircraft[];
|
aircrafts: (ConnectedAircraft & { Station: Station })[];
|
||||||
missions: Mission[];
|
missions: Mission[];
|
||||||
lat: number;
|
lat: number;
|
||||||
lng: number;
|
lng: number;
|
||||||
@@ -162,9 +168,11 @@ export const MarkerCluster = () => {
|
|||||||
const handleZoom = () => {
|
const handleZoom = () => {
|
||||||
const zoom = map.getZoom();
|
const zoom = map.getZoom();
|
||||||
let newCluster: typeof cluster = [];
|
let newCluster: typeof cluster = [];
|
||||||
aircrafts.forEach((aircraft) => {
|
aircrafts
|
||||||
const lat = aircraft.location.lat;
|
?.filter((a) => a.simulatorConnected)
|
||||||
const lng = aircraft.location.lng;
|
.forEach((aircraft) => {
|
||||||
|
const lat = aircraft.posLat!;
|
||||||
|
const lng = aircraft.posLng!;
|
||||||
|
|
||||||
const existingClusterIndex = newCluster.findIndex(
|
const existingClusterIndex = newCluster.findIndex(
|
||||||
(c) => Math.abs(c.lat - lat) < 1 && Math.abs(c.lng - lng) < 1,
|
(c) => Math.abs(c.lat - lat) < 1 && Math.abs(c.lng - lng) < 1,
|
||||||
@@ -224,10 +232,7 @@ export const MarkerCluster = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const clusterWithAvgPos = newCluster.map((c) => {
|
const clusterWithAvgPos = newCluster.map((c) => {
|
||||||
const aircraftPos = c.aircrafts.map((a) => [
|
const aircraftPos = c.aircrafts.map((a) => [a.posLat, a.posLng]);
|
||||||
a.location.lat,
|
|
||||||
a.location.lng,
|
|
||||||
]);
|
|
||||||
const missionPos = c.missions.map((m) => [m.addressLat, m.addressLng]);
|
const missionPos = c.missions.map((m) => [m.addressLat, m.addressLng]);
|
||||||
const allPos = [...aircraftPos, ...missionPos];
|
const allPos = [...aircraftPos, ...missionPos];
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { createMissionAPI, editMissionAPI } from "querys/missions";
|
import { createMissionAPI, editMissionAPI } from "querys/missions";
|
||||||
import { getKeywordsAPI } from "querys/keywords";
|
import { getKeywordsAPI } from "querys/keywords";
|
||||||
import { getStationsAPI } from "querys/stations";
|
import { getStationsAPI } from "querys/stations";
|
||||||
|
import { useMapStore } from "_store/mapStore";
|
||||||
|
|
||||||
export const MissionForm = () => {
|
export const MissionForm = () => {
|
||||||
const { isEditingMission, editingMissionId, setEditingMission } =
|
const { isEditingMission, editingMissionId, setEditingMission } =
|
||||||
usePannelStore();
|
usePannelStore();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const setSeachOSMElements = useMapStore((s) => s.setSearchElements);
|
||||||
|
|
||||||
const { data: keywords } = useQuery({
|
const { data: keywords } = useQuery({
|
||||||
queryKey: ["keywords"],
|
queryKey: ["keywords"],
|
||||||
@@ -90,28 +92,10 @@ export const MissionForm = () => {
|
|||||||
}
|
}
|
||||||
}, [session.data?.user.id, form]);
|
}, [session.data?.user.id, form]);
|
||||||
|
|
||||||
/* const formValues = form.watch();
|
|
||||||
useEffect(() => {
|
|
||||||
if (formValues) {
|
|
||||||
setMissionFormValues(formValues);
|
|
||||||
}
|
|
||||||
}, [formValues, setMissionFormValues]); */
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (missionFormValues) {
|
if (missionFormValues) {
|
||||||
if (Object.keys(missionFormValues).length === 0) {
|
if (Object.keys(missionFormValues).length === 0) {
|
||||||
console.log("resetting form");
|
form.reset();
|
||||||
form.reset(/* {
|
|
||||||
addressStreet: "",
|
|
||||||
addressCity: "",
|
|
||||||
addressZip: "",
|
|
||||||
missionStationIds: [],
|
|
||||||
missionKeywordName: "",
|
|
||||||
missionKeywordAbbreviation: "",
|
|
||||||
missionKeywordCategory: "",
|
|
||||||
|
|
||||||
...defaultFormValues,
|
|
||||||
} */);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const key in missionFormValues) {
|
for (const key in missionFormValues) {
|
||||||
@@ -123,7 +107,6 @@ export const MissionForm = () => {
|
|||||||
}
|
}
|
||||||
}, [missionFormValues, form, defaultFormValues]);
|
}, [missionFormValues, form, defaultFormValues]);
|
||||||
|
|
||||||
console.log(form.formState.errors);
|
|
||||||
return (
|
return (
|
||||||
<form className="space-y-4">
|
<form className="space-y-4">
|
||||||
{/* Koorinaten Section */}
|
{/* Koorinaten Section */}
|
||||||
@@ -313,9 +296,13 @@ export const MissionForm = () => {
|
|||||||
className="textarea textarea-primary textarea-bordered w-full"
|
className="textarea textarea-primary textarea-bordered w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-error">
|
|
||||||
Du musst noch ein Gebäude auswählen, um den Einsatz zu erstellen.
|
{missionFormValues?.addressOSMways?.length && (
|
||||||
|
<p className="text-sm text-info">
|
||||||
|
In diesem Einsatz gibt es {missionFormValues?.addressOSMways?.length}{" "}
|
||||||
|
Gebäude
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="form-control min-h-[140px]">
|
<div className="form-control min-h-[140px]">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -334,6 +321,7 @@ export const MissionForm = () => {
|
|||||||
toast.success(
|
toast.success(
|
||||||
`Einsatz ${newMission.id} erfolgreich aktualisiert`,
|
`Einsatz ${newMission.id} erfolgreich aktualisiert`,
|
||||||
);
|
);
|
||||||
|
setSeachOSMElements([]); // Reset search elements
|
||||||
setEditingMission(false, null); // Reset editing state
|
setEditingMission(false, null); // Reset editing state
|
||||||
form.reset(); // Reset the form
|
form.reset(); // Reset the form
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -359,6 +347,7 @@ export const MissionForm = () => {
|
|||||||
await createMissionMutation.mutateAsync(
|
await createMissionMutation.mutateAsync(
|
||||||
mission as unknown as Prisma.MissionCreateInput,
|
mission as unknown as Prisma.MissionCreateInput,
|
||||||
);
|
);
|
||||||
|
setSeachOSMElements([]); // Reset search elements
|
||||||
toast.success(`Einsatz ${newMission.id} erstellt`);
|
toast.success(`Einsatz ${newMission.id} erstellt`);
|
||||||
// TODO: Einsatz alarmieren
|
// TODO: Einsatz alarmieren
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -382,6 +371,7 @@ export const MissionForm = () => {
|
|||||||
await createMissionMutation.mutateAsync(
|
await createMissionMutation.mutateAsync(
|
||||||
mission as unknown as Prisma.MissionCreateInput,
|
mission as unknown as Prisma.MissionCreateInput,
|
||||||
);
|
);
|
||||||
|
setSeachOSMElements([]); // Reset search elements
|
||||||
|
|
||||||
toast.success(`Einsatz ${newMission.id} erstellt`);
|
toast.success(`Einsatz ${newMission.id} erstellt`);
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|||||||
@@ -6,6 +6,16 @@
|
|||||||
@theme {
|
@theme {
|
||||||
--color-rescuetrack: #46b7a3;
|
--color-rescuetrack: #46b7a3;
|
||||||
--color-rescuetrack-highlight: #ff4500;
|
--color-rescuetrack-highlight: #ff4500;
|
||||||
|
--color-fms-0: rgb(140, 10, 10);
|
||||||
|
--color-fms-1: rgb(10, 134, 25);
|
||||||
|
--color-fms-2: rgb(10, 134, 25);
|
||||||
|
--color-fms-3: rgb(140, 10, 10);
|
||||||
|
--color-fms-4: rgb(140, 10, 10);
|
||||||
|
--color-fms-5: rgb(231, 77, 22);
|
||||||
|
--color-fms-6: rgb(85, 85, 85);
|
||||||
|
--color-fms-7: rgb(140, 10, 10);
|
||||||
|
--color-fms-8: rgb(186, 105, 0);
|
||||||
|
--color-fms-9: rgb(10, 134, 25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-popup-tip-container {
|
.leaflet-popup-tip-container {
|
||||||
|
|||||||
25
apps/dispatch/app/querys/aircrafts.ts
Normal file
25
apps/dispatch/app/querys/aircrafts.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { ConnectedAircraft, Prisma, Station } from "@repo/db";
|
||||||
|
import axios from "axios";
|
||||||
|
import { serverApi } from "helpers/axios";
|
||||||
|
|
||||||
|
export const getConnectedAircraftsAPI = async () => {
|
||||||
|
const res =
|
||||||
|
await axios.get<(ConnectedAircraft & { Station: Station })[]>(
|
||||||
|
"/api/aircrafts",
|
||||||
|
); // return only connected aircrafts
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error("Failed to fetch stations");
|
||||||
|
}
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editConnectedAircraftAPI = async (
|
||||||
|
id: number,
|
||||||
|
mission: Prisma.ConnectedAircraftUpdateInput,
|
||||||
|
) => {
|
||||||
|
const respone = await serverApi.patch<ConnectedAircraft>(
|
||||||
|
`/aircrafts/${id}`,
|
||||||
|
mission,
|
||||||
|
);
|
||||||
|
return respone.data;
|
||||||
|
};
|
||||||
Binary file not shown.
@@ -1,8 +1,8 @@
|
|||||||
export interface MissionVehicleLog {
|
export interface OSMWay {
|
||||||
wayID: string;
|
wayID: number;
|
||||||
tags: string[];
|
tags: Record<string, string>;
|
||||||
nodes: {
|
nodes: {
|
||||||
lat: number;
|
lat: number;
|
||||||
lon: number;
|
lon: number;
|
||||||
};
|
}[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./ParticipantLog";
|
export * from "./ParticipantLog";
|
||||||
export * from "./MissionVehicleLog";
|
export * from "./MissionVehicleLog";
|
||||||
export * from "./User";
|
export * from "./User";
|
||||||
|
export * from "./OSMway";
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ model ConnectedAircraft {
|
|||||||
userId String
|
userId String
|
||||||
publicUser Json
|
publicUser Json
|
||||||
lastHeartbeat DateTime @default(now())
|
lastHeartbeat DateTime @default(now())
|
||||||
|
fmsStatus String @default("6")
|
||||||
|
// position:
|
||||||
|
posLat Float?
|
||||||
|
posLng Float?
|
||||||
|
posAlt Int?
|
||||||
|
posSpeed Int?
|
||||||
|
posHeading Int?
|
||||||
|
simulator String?
|
||||||
|
simulatorConnected Boolean @default(false)
|
||||||
|
posH145active Boolean @default(false)
|
||||||
stationId Int
|
stationId Int
|
||||||
loginTime DateTime @default(now())
|
loginTime DateTime @default(now())
|
||||||
esimatedLogoutTime DateTime?
|
esimatedLogoutTime DateTime?
|
||||||
|
|||||||
Reference in New Issue
Block a user