diff --git a/apps/dispatch-server/routes/aircraft.ts b/apps/dispatch-server/routes/aircraft.ts new file mode 100644 index 00000000..df2a2b89 --- /dev/null +++ b/apps/dispatch-server/routes/aircraft.ts @@ -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; diff --git a/apps/dispatch-server/routes/mission.ts b/apps/dispatch-server/routes/mission.ts index 3c8596f6..0bb31872 100644 --- a/apps/dispatch-server/routes/mission.ts +++ b/apps/dispatch-server/routes/mission.ts @@ -42,7 +42,7 @@ router.put("/", async (req, res) => { const newMission = await prisma.mission.create({ data: req.body, }); - io.to("missions").emit("new-mission", newMission); + io.to("dispatchers").emit("new-mission", newMission); res.status(201).json(newMission); } catch (error) { res.status(500).json({ error: "Failed to create mission" }); @@ -57,7 +57,7 @@ router.patch("/:id", async (req, res) => { where: { id: Number(id) }, data: req.body, }); - io.to("missions").emit("update-mission", updatedMission); + io.to("dispatchers").emit("update-mission", updatedMission); res.json(updatedMission); } catch (error) { console.error(error); @@ -72,7 +72,7 @@ router.delete("/:id", async (req, res) => { await prisma.mission.delete({ where: { id: Number(id) }, }); - io.to("missions").emit("delete-mission", id); + io.to("dispatchers").emit("delete-mission", id); res.status(204).send(); } catch (error) { console.error(error); diff --git a/apps/dispatch-server/routes/router.ts b/apps/dispatch-server/routes/router.ts index 874ac22e..be4ad1af 100644 --- a/apps/dispatch-server/routes/router.ts +++ b/apps/dispatch-server/routes/router.ts @@ -3,6 +3,7 @@ import livekitRouter from "./livekit"; import dispatcherRotuer from "./dispatcher"; import missionRouter from "./mission"; import statusRouter from "./status"; +import aircraftsRouter from "./aircraft"; const router = Router(); @@ -10,5 +11,6 @@ router.use("/livekit", livekitRouter); router.use("/dispatcher", dispatcherRotuer); router.use("/mission", missionRouter); router.use("/status", statusRouter); +router.use("/aircrafts", aircraftsRouter); export default router; diff --git a/apps/dispatch-server/socket-events/connect-dispatch.ts b/apps/dispatch-server/socket-events/connect-dispatch.ts index 9fec06db..7378bcc1 100644 --- a/apps/dispatch-server/socket-events/connect-dispatch.ts +++ b/apps/dispatch-server/socket-events/connect-dispatch.ts @@ -79,7 +79,6 @@ export const handleConnectDispatch = socket.join("dispatchers"); // Dem Dispatcher-Raum beitreten socket.join(`user:${user.id}`); // Dem User-Raum beitreten - socket.join("missions"); io.to("dispatchers").emit("dispatchers-update"); io.to("pilots").emit("dispatchers-update"); diff --git a/apps/dispatch/app/_components/QueryProvider.tsx b/apps/dispatch/app/_components/QueryProvider.tsx index 5d46b394..b47b0b53 100644 --- a/apps/dispatch/app/_components/QueryProvider.tsx +++ b/apps/dispatch/app/_components/QueryProvider.tsx @@ -30,7 +30,13 @@ export function QueryProvider({ children }: { children: ReactNode }) { const invalidateConnectedUsers = () => { 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("dispatchers-update", invalidateConnectedUsers); dispatchSocket.on("pilots-update", invalidateConnectedUsers); + dispatchSocket.on("update-connectedAircraft", invalidateConenctedAircrafts); }, [queryClient]); return ( diff --git a/apps/dispatch/app/_components/left/Chat.tsx b/apps/dispatch/app/_components/left/Chat.tsx index 3ebef93d..505561cb 100644 --- a/apps/dispatch/app/_components/left/Chat.tsx +++ b/apps/dispatch/app/_components/left/Chat.tsx @@ -86,7 +86,12 @@ export const Chat = () => { )} - {connectedUser?.map((user) => ( + {[ + ...(connectedUser?.filter( + (user, idx, arr) => + arr.findIndex((u) => u.userId === user.userId) === idx, + ) || []), + ].map((user) => ( {asPublicUser(user.publicUser).fullName} diff --git a/apps/dispatch/app/_components/left/Report.tsx b/apps/dispatch/app/_components/left/Report.tsx index 40667fd2..18bf6e12 100644 --- a/apps/dispatch/app/_components/left/Report.tsx +++ b/apps/dispatch/app/_components/left/Report.tsx @@ -75,9 +75,14 @@ export const Report = () => { Chatpartner auswählen )} - {connectedUser?.map((user) => ( + {[ + ...(connectedUser?.filter( + (user, idx, arr) => + arr.findIndex((u) => u.userId === user.userId) === idx, + ) || []), + ].map((user) => ( - {asPublicUser(user).fullName} + {asPublicUser(user.publicUser).fullName} ))} diff --git a/apps/dispatch/app/_store/aircraftsStore.ts b/apps/dispatch/app/_store/aircraftsStore.ts deleted file mode 100644 index 87223ee0..00000000 --- a/apps/dispatch/app/_store/aircraftsStore.ts +++ /dev/null @@ -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((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] }; - } - }), -})); diff --git a/apps/dispatch/app/_store/mapStore.ts b/apps/dispatch/app/_store/mapStore.ts index ac6c2ec8..a93c730b 100644 --- a/apps/dispatch/app/_store/mapStore.ts +++ b/apps/dispatch/app/_store/mapStore.ts @@ -1,3 +1,4 @@ +import { OSMWay } from "@repo/db"; import { create } from "zustand"; interface MapStore { @@ -18,30 +19,14 @@ interface MapStore { close: number[]; }) => void; openAircraftMarker: { - id: string; + id: number; tab: "home" | "fms" | "aircraft" | "mission" | "chat"; }[]; setOpenAircraftMarker: (aircraft: { open: MapStore["openAircraftMarker"]; - close: string[]; + close: number[]; }) => void; - searchElements: { - 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; - }[]; + searchElements: OSMWay[]; setSearchElements: (elements: MapStore["searchElements"]) => void; setContextMenu: (popup: MapStore["contextMenu"]) => void; searchPopup: { @@ -54,8 +39,8 @@ interface MapStore { [aircraftId: string]: "home" | "fms" | "aircraft" | "mission" | "chat"; }; setAircraftTab: ( - aircraftId: string, - tab: MapStore["aircraftTabs"][string], + aircraftId: number, + tab: MapStore["aircraftTabs"][number], ) => void; } diff --git a/apps/dispatch/app/_store/pilot/connectionStore.ts b/apps/dispatch/app/_store/pilot/connectionStore.ts index 5c28284e..a90bbe9c 100644 --- a/apps/dispatch/app/_store/pilot/connectionStore.ts +++ b/apps/dispatch/app/_store/pilot/connectionStore.ts @@ -16,7 +16,7 @@ interface ConnectionStore { disconnect: () => void; } -export const useDispatchConnectionStore = create((set) => ({ +export const usePilotConnectionStore = create((set) => ({ status: "disconnected", message: "", selectedStation: null, @@ -40,23 +40,23 @@ export const useDispatchConnectionStore = create((set) => ({ dispatchSocket.on("connect", () => { pilotSocket.disconnect(); - useDispatchConnectionStore.setState({ status: "connected", message: "" }); + usePilotConnectionStore.setState({ status: "connected", message: "" }); }); dispatchSocket.on("connect_error", (err) => { - useDispatchConnectionStore.setState({ + usePilotConnectionStore.setState({ status: "error", message: err.message, }); }); dispatchSocket.on("disconnect", () => { - useDispatchConnectionStore.setState({ status: "disconnected", message: "" }); + usePilotConnectionStore.setState({ status: "disconnected", message: "" }); }); dispatchSocket.on("force-disconnect", (reason: string) => { console.log("force-disconnect", reason); - useDispatchConnectionStore.setState({ + usePilotConnectionStore.setState({ status: "disconnected", message: reason, }); diff --git a/apps/dispatch/app/api/aircrafts/route.ts b/apps/dispatch/app/api/aircrafts/route.ts new file mode 100644 index 00000000..9cee2ba7 --- /dev/null +++ b/apps/dispatch/app/api/aircrafts/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@repo/db"; + +export async function GET(): Promise { + 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 }, + ); + } +} diff --git a/apps/dispatch/app/dispatch/_components/map/AircraftMarker.tsx b/apps/dispatch/app/dispatch/_components/map/AircraftMarker.tsx index 7e2e3e1d..dbbcfafd 100644 --- a/apps/dispatch/app/dispatch/_components/map/AircraftMarker.tsx +++ b/apps/dispatch/app/dispatch/_components/map/AircraftMarker.tsx @@ -1,4 +1,3 @@ -import { Aircraft, useAircraftsStore } from "_store/aircraftsStore"; import { Marker, useMap } from "react-leaflet"; import { DivIcon, Marker as LMarker, Popup as LPopup } from "leaflet"; import { useMapStore } from "_store/mapStore"; @@ -26,6 +25,9 @@ import FMSStatusHistory, { FMSStatusSelector, RettungsmittelTab, } 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 } = { "0": "rgb(140,10,10)", @@ -54,7 +56,11 @@ export const FMS_STATUS_TEXT_COLORS: { [key: string]: string } = { N: "rgb(153,153,153)", }; -const AircraftPopupContent = ({ aircraft }: { aircraft: Aircraft }) => { +const AircraftPopupContent = ({ + aircraft, +}: { + aircraft: ConnectedAircraft & { Station: Station }; +}) => { const setAircraftTab = useMapStore((state) => state.setAircraftTab); const currentTab = useMapStore( (state) => state.aircraftTabs[aircraft.id] || "home", @@ -182,9 +188,9 @@ const AircraftPopupContent = ({ aircraft }: { aircraft: Aircraft }) => { onClick={() => handleTabChange("aircraft")} > - {aircraft.bosName} + {aircraft.Station.bosCallsign} - {aircraft.bosNutzung} + {aircraft.Station.bosUse} { ); }; -const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => { +const AircraftMarker = ({ + aircraft, +}: { + aircraft: ConnectedAircraft & { Station: Station }; +}) => { const [hideMarker, setHideMarker] = useState(false); const map = useMap(); const markerRef = useRef(null); @@ -293,7 +303,7 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => { }, [map, openAircraftMarker, handleConflict]); const getMarkerHTML = ( - aircraft: Aircraft, + aircraft: ConnectedAircraft & { Station: Station }, anchor: "topleft" | "topright" | "bottomleft" | "bottomright", ) => { return ` { ${aircraft.fmsStatus} - ${aircraft.bosName} + ${aircraft.Station.bosCallsign} { return ( - + {aircraft.simulatorConnected && ( + + )} {openAircraftMarker.some((m) => m.id === aircraft.id) && !hideMarker && ( { }} id={`aircraft-${aircraft.id}`} ref={popupRef} - position={[aircraft.location.lat, aircraft.location.lng]} + position={[aircraft.posLat!, aircraft.posLng!]} autoClose={false} closeOnClick={false} autoPan={false} @@ -379,12 +391,14 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => { }; 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 ( <> - {aircrafts.map((aircraft) => { + {aircrafts?.map((aircraft) => { return ; })} > diff --git a/apps/dispatch/app/dispatch/_components/map/ContextMenu.tsx b/apps/dispatch/app/dispatch/_components/map/ContextMenu.tsx index 54e0045e..e8247ef0 100644 --- a/apps/dispatch/app/dispatch/_components/map/ContextMenu.tsx +++ b/apps/dispatch/app/dispatch/_components/map/ContextMenu.tsx @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { OSMWay } from "@repo/db"; import { useMapStore } from "_store/mapStore"; import { usePannelStore } from "_store/pannelStore"; import { MapPinned, Search } from "lucide-react"; @@ -29,6 +31,43 @@ export const ContextMenu = () => { 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 ( { place_rank: number; 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); setMissionFormValues({ addressLat: contextMenu.lat, @@ -78,6 +142,7 @@ export const ContextMenu = () => { addressStreet: `${data.address.road}, ${data.address.house_number || "keine HN"}`, addressZip: data.address.postcode, state: "draft", + addressOSMways: [(exactAddress || closestToContext) as any], }); }} > @@ -86,39 +151,7 @@ export const ContextMenu = () => { { - 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(); - 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, - }; - }), - ); + addOSMobjects(); }} > diff --git a/apps/dispatch/app/dispatch/_components/map/SearchElements.tsx b/apps/dispatch/app/dispatch/_components/map/SearchElements.tsx index 8f0e58b0..fb8068ca 100644 --- a/apps/dispatch/app/dispatch/_components/map/SearchElements.tsx +++ b/apps/dispatch/app/dispatch/_components/map/SearchElements.tsx @@ -1,22 +1,45 @@ import { useMapStore } from "_store/mapStore"; import { Marker as LMarker } from "leaflet"; -import { useEffect, useRef } from "react"; -import { Marker, Polygon, Polyline, Popup } from "react-leaflet"; +import { Fragment, useEffect, useRef } from "react"; +import { Marker, Polygon, Popup } from "react-leaflet"; import L from "leaflet"; +import { useQuery } from "@tanstack/react-query"; +import { getMissionsAPI } from "querys/missions"; +import { OSMWay } from "@repo/db"; export const SearchElements = () => { - const { searchElements, searchPopup, setSearchPopup, setContextMenu } = - useMapStore(); + const { + searchElements, + searchPopup, + setSearchPopup, + setContextMenu, + openMissionMarker, + } = useMapStore(); + const missions = useQuery({ + queryKey: ["missions"], + queryFn: () => + getMissionsAPI({ + OR: [ + { + state: "draft", + }, + { + state: "running", + }, + ], + }), + }); const poppupRef = useRef(null); - const intervalRef = useRef(null); const searchPopupElement = searchElements.find( - (element) => element.id === searchPopup?.elementId, + (element) => element.wayID === searchPopup?.elementId, ); const SearchElement = ({ element, + isActive = false, }: { element: (typeof searchElements)[1]; + isActive?: boolean; }) => { const ref = useRef(null); @@ -24,11 +47,11 @@ export const SearchElements = () => { if (ref.current) { ref.current.on("click", () => { const center = ref.current.getBounds().getCenter(); - if (searchPopup?.elementId !== element.id) { + if (searchPopup?.elementId !== element.wayID) { setSearchPopup({ lat: center.lat, lng: center.lng, - elementId: element.id, + elementId: element.wayID, }); } else { setSearchPopup(null); @@ -40,9 +63,12 @@ export const SearchElements = () => { return ( [node.lat, node.lon])} - color={searchPopup?.elementId === element.id ? "#ff4500" : "#46b7a3"} + color={ + searchPopup?.elementId === element.wayID || isActive + ? "#ff4500" + : "#46b7a3" + } ref={ref} /> ); @@ -86,8 +112,30 @@ export const SearchElements = () => { return ( <> - {searchElements.map((element) => { - return ; + {openMissionMarker.map(({ id }) => { + const mission = missions.data?.find((m) => m.id === id); + if (!mission) return null; + return ( + + {(mission.addressOSMways as (OSMWay | null)[]) + .filter((element): element is OSMWay => element !== null) + .map((element: OSMWay, i) => ( + + ))} + + ); + })} + {searchElements.map((element, i) => { + return ( + + ); })} {searchPopup && ( { 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(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 ( {Array.from({ length: 9 }, (_, i) => (i + 1).toString()) .filter((status) => status !== "5") // Exclude status 5 .map((status) => ( - { }} onMouseEnter={() => setHoveredStatus(status)} onMouseLeave={() => setHoveredStatus(null)} - onClick={() => { + onClick={async () => { // Handle status change logic here - console.log( - `Status changed to: ${status} for aircraft ${aircraft.id}`, - ); + await changeAircraftMutation.mutateAsync({ + id: aircraft.id, + update: { + fmsStatus: status, + }, + }); + toast.success(`Status changed to ${status}`); }} > {status} - + ))} ); diff --git a/apps/dispatch/app/dispatch/_components/map/_components/MarkerCluster.tsx b/apps/dispatch/app/dispatch/_components/map/_components/MarkerCluster.tsx index 60c05fe1..2b079aaa 100644 --- a/apps/dispatch/app/dispatch/_components/map/_components/MarkerCluster.tsx +++ b/apps/dispatch/app/dispatch/_components/map/_components/MarkerCluster.tsx @@ -1,7 +1,6 @@ -import { Mission } from "@repo/db"; +import { ConnectedAircraft, Mission, Station } 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 { FMS_STATUS_COLORS, @@ -12,6 +11,7 @@ import { MISSION_STATUS_TEXT_COLORS, } from "dispatch/_components/map/MissionMarkers"; import { cn } from "helpers/cn"; +import { getConnectedAircraftsAPI } from "querys/aircrafts"; import { getMissionsAPI } from "querys/missions"; import { useEffect, useState } from "react"; import { useMap } from "react-leaflet"; @@ -20,7 +20,7 @@ const PopupContent = ({ aircrafts, missions, }: { - aircrafts: Aircraft[]; + aircrafts: (ConnectedAircraft & { Station: Station })[]; missions: Mission[]; }) => { const { anchor } = useSmartPopup(); @@ -101,39 +101,41 @@ const PopupContent = ({ ))} - {aircrafts.map((aircraft) => ( - { - setOpenAircraftMarker({ - open: [ - { - id: aircraft.id, - tab: "aircraft", - }, - ], - close: [], - }); - map.setView([aircraft.location.lat, aircraft.location.lng], 12, { - animate: true, - }); - }} - > - a.simulatorConnected) + .map((aircraft) => ( + { + setOpenAircraftMarker({ + open: [ + { + id: aircraft.id, + tab: "aircraft", + }, + ], + close: [], + }); + map.setView([aircraft.posLat!, aircraft.posLng!], 12, { + animate: true, + }); }} > - {aircraft.fmsStatus} - - {aircraft.bosName} - - ))} + + {aircraft.fmsStatus} + + {aircraft.Station.bosCallsign} + + ))} > ); @@ -141,7 +143,11 @@ const PopupContent = ({ export const MarkerCluster = () => { const map = useMap(); - const aircrafts = useAircraftsStore((state) => state.aircrafts); + const { data: aircrafts } = useQuery({ + queryKey: ["aircrafts"], + queryFn: getConnectedAircraftsAPI, + }); + const { data: missions } = useQuery({ queryKey: ["missions"], queryFn: () => @@ -151,7 +157,7 @@ export const MarkerCluster = () => { }); const [cluster, setCluster] = useState< { - aircrafts: Aircraft[]; + aircrafts: (ConnectedAircraft & { Station: Station })[]; missions: Mission[]; lat: number; lng: number; @@ -162,36 +168,38 @@ export const MarkerCluster = () => { const handleZoom = () => { const zoom = map.getZoom(); let newCluster: typeof cluster = []; - aircrafts.forEach((aircraft) => { - const lat = aircraft.location.lat; - const lng = aircraft.location.lng; + aircrafts + ?.filter((a) => a.simulatorConnected) + .forEach((aircraft) => { + const lat = aircraft.posLat!; + const lng = aircraft.posLng!; - const existingClusterIndex = newCluster.findIndex( - (c) => Math.abs(c.lat - lat) < 1 && Math.abs(c.lng - lng) < 1, - ); - const existingCluster = newCluster[existingClusterIndex]; - if (existingCluster) { - newCluster = [...newCluster].map((c, i) => { - if (i === existingClusterIndex) { - return { - ...c, - aircrafts: [...c.aircrafts, aircraft], - }; - } - return c; - }); - } else { - newCluster = [ - ...newCluster, - { - aircrafts: [aircraft], - missions: [], - lat, - lng, - }, - ]; - } - }); + const existingClusterIndex = newCluster.findIndex( + (c) => Math.abs(c.lat - lat) < 1 && Math.abs(c.lng - lng) < 1, + ); + const existingCluster = newCluster[existingClusterIndex]; + if (existingCluster) { + newCluster = [...newCluster].map((c, i) => { + if (i === existingClusterIndex) { + return { + ...c, + aircrafts: [...c.aircrafts, aircraft], + }; + } + return c; + }); + } else { + newCluster = [ + ...newCluster, + { + aircrafts: [aircraft], + missions: [], + lat, + lng, + }, + ]; + } + }); missions?.forEach((mission) => { const lat = mission.addressLat; const lng = mission.addressLng; @@ -224,10 +232,7 @@ export const MarkerCluster = () => { }); const clusterWithAvgPos = newCluster.map((c) => { - const aircraftPos = c.aircrafts.map((a) => [ - a.location.lat, - a.location.lng, - ]); + const aircraftPos = c.aircrafts.map((a) => [a.posLat, a.posLng]); const missionPos = c.missions.map((m) => [m.addressLat, m.addressLng]); const allPos = [...aircraftPos, ...missionPos]; diff --git a/apps/dispatch/app/dispatch/_components/pannel/MissionForm.tsx b/apps/dispatch/app/dispatch/_components/pannel/MissionForm.tsx index d458a2a1..52f93fc4 100644 --- a/apps/dispatch/app/dispatch/_components/pannel/MissionForm.tsx +++ b/apps/dispatch/app/dispatch/_components/pannel/MissionForm.tsx @@ -16,11 +16,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { createMissionAPI, editMissionAPI } from "querys/missions"; import { getKeywordsAPI } from "querys/keywords"; import { getStationsAPI } from "querys/stations"; +import { useMapStore } from "_store/mapStore"; export const MissionForm = () => { const { isEditingMission, editingMissionId, setEditingMission } = usePannelStore(); const queryClient = useQueryClient(); + const setSeachOSMElements = useMapStore((s) => s.setSearchElements); const { data: keywords } = useQuery({ queryKey: ["keywords"], @@ -90,28 +92,10 @@ export const MissionForm = () => { } }, [session.data?.user.id, form]); - /* const formValues = form.watch(); - useEffect(() => { - if (formValues) { - setMissionFormValues(formValues); - } - }, [formValues, setMissionFormValues]); */ - useEffect(() => { if (missionFormValues) { if (Object.keys(missionFormValues).length === 0) { - console.log("resetting form"); - form.reset(/* { - addressStreet: "", - addressCity: "", - addressZip: "", - missionStationIds: [], - missionKeywordName: "", - missionKeywordAbbreviation: "", - missionKeywordCategory: "", - - ...defaultFormValues, - } */); + form.reset(); return; } for (const key in missionFormValues) { @@ -123,7 +107,6 @@ export const MissionForm = () => { } }, [missionFormValues, form, defaultFormValues]); - console.log(form.formState.errors); return ( {/* Koorinaten Section */} @@ -313,9 +296,13 @@ export const MissionForm = () => { className="textarea textarea-primary textarea-bordered w-full" /> - - Du musst noch ein Gebäude auswählen, um den Einsatz zu erstellen. - + + {missionFormValues?.addressOSMways?.length && ( + + In diesem Einsatz gibt es {missionFormValues?.addressOSMways?.length}{" "} + Gebäude + + )} @@ -334,6 +321,7 @@ export const MissionForm = () => { toast.success( `Einsatz ${newMission.id} erfolgreich aktualisiert`, ); + setSeachOSMElements([]); // Reset search elements setEditingMission(false, null); // Reset editing state form.reset(); // Reset the form setOpen(false); @@ -359,6 +347,7 @@ export const MissionForm = () => { await createMissionMutation.mutateAsync( mission as unknown as Prisma.MissionCreateInput, ); + setSeachOSMElements([]); // Reset search elements toast.success(`Einsatz ${newMission.id} erstellt`); // TODO: Einsatz alarmieren setOpen(false); @@ -382,6 +371,7 @@ export const MissionForm = () => { await createMissionMutation.mutateAsync( mission as unknown as Prisma.MissionCreateInput, ); + setSeachOSMElements([]); // Reset search elements toast.success(`Einsatz ${newMission.id} erstellt`); form.reset(); diff --git a/apps/dispatch/app/globals.css b/apps/dispatch/app/globals.css index 06986ec6..87de6377 100644 --- a/apps/dispatch/app/globals.css +++ b/apps/dispatch/app/globals.css @@ -6,6 +6,16 @@ @theme { --color-rescuetrack: #46b7a3; --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 { diff --git a/apps/dispatch/app/querys/aircrafts.ts b/apps/dispatch/app/querys/aircrafts.ts new file mode 100644 index 00000000..8929d33c --- /dev/null +++ b/apps/dispatch/app/querys/aircrafts.ts @@ -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( + `/aircrafts/${id}`, + mission, + ); + return respone.data; +}; diff --git a/grafana/grafana.db b/grafana/grafana.db index e48da3f2..ee2e88a9 100644 Binary files a/grafana/grafana.db and b/grafana/grafana.db differ diff --git a/packages/database/prisma/json/OSMway.ts b/packages/database/prisma/json/OSMway.ts index 1aa49b4f..e8daa1f6 100644 --- a/packages/database/prisma/json/OSMway.ts +++ b/packages/database/prisma/json/OSMway.ts @@ -1,8 +1,8 @@ -export interface MissionVehicleLog { - wayID: string; - tags: string[]; +export interface OSMWay { + wayID: number; + tags: Record; nodes: { lat: number; lon: number; - }; + }[]; } diff --git a/packages/database/prisma/json/index.ts b/packages/database/prisma/json/index.ts index 2f99fcc5..b407e3c3 100644 --- a/packages/database/prisma/json/index.ts +++ b/packages/database/prisma/json/index.ts @@ -1,3 +1,4 @@ export * from "./ParticipantLog"; export * from "./MissionVehicleLog"; export * from "./User"; +export * from "./OSMway"; diff --git a/packages/database/prisma/schema/connectedAircraft.prisma b/packages/database/prisma/schema/connectedAircraft.prisma index 5509f6f8..31f8c28a 100644 --- a/packages/database/prisma/schema/connectedAircraft.prisma +++ b/packages/database/prisma/schema/connectedAircraft.prisma @@ -3,6 +3,16 @@ model ConnectedAircraft { userId String publicUser Json 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 loginTime DateTime @default(now()) esimatedLogoutTime DateTime?
- Du musst noch ein Gebäude auswählen, um den Einsatz zu erstellen. -
+ In diesem Einsatz gibt es {missionFormValues?.addressOSMways?.length}{" "} + Gebäude +