Made Aircrafts fetch from Server. Added OSM Objects to mission

This commit is contained in:
PxlLoewe
2025-05-07 22:31:49 -07:00
parent 99531e9abf
commit 1948d34963
23 changed files with 477 additions and 318 deletions

View File

@@ -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 (

View File

@@ -86,7 +86,12 @@ export const Chat = () => {
</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}>
{asPublicUser(user.publicUser).fullName}
</option>

View File

@@ -75,9 +75,14 @@ export const Report = () => {
Chatpartner auswählen
</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}>
{asPublicUser(user).fullName}
{asPublicUser(user.publicUser).fullName}
</option>
))}
</select>

View File

@@ -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] };
}
}),
}));

View File

@@ -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;
}

View File

@@ -16,7 +16,7 @@ interface ConnectionStore {
disconnect: () => void;
}
export const useDispatchConnectionStore = create<ConnectionStore>((set) => ({
export const usePilotConnectionStore = create<ConnectionStore>((set) => ({
status: "disconnected",
message: "",
selectedStation: null,
@@ -40,23 +40,23 @@ export const useDispatchConnectionStore = create<ConnectionStore>((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,
});

View 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 },
);
}
}

View File

@@ -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")}
>
<span className="text-white text-base font-medium">
{aircraft.bosName}
{aircraft.Station.bosCallsign}
<br />
{aircraft.bosNutzung}
{aircraft.Station.bosUse}
</span>
</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 map = useMap();
const markerRef = useRef<LMarker>(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 `<div
@@ -327,12 +337,12 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
${aircraft.fmsStatus}
</span>
<span class="text-white text-[15px] text-nowrap">
${aircraft.bosName}
${aircraft.Station.bosCallsign}
</span>
<div
data-id="${aircraft.id}"
data-anchor-lat="${aircraft.location.lat}"
data-anchor-lng="${aircraft.location.lng}"
data-anchor-lat="${aircraft.posLat}"
data-anchor-lng="${aircraft.posLng}"
id="marker-domain-aircraft-${aircraft.id}"
class="${cn(
"map-collision absolute w-[200%] h-[200%] top-0 left-0 transform pointer-events-none",
@@ -345,16 +355,18 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
return (
<Fragment key={aircraft.id}>
<Marker
ref={markerRef}
position={[aircraft.location.lat, aircraft.location.lng]}
icon={
new DivIcon({
iconAnchor: [0, 0],
html: getMarkerHTML(aircraft, anchor),
})
}
/>
{aircraft.simulatorConnected && (
<Marker
ref={markerRef}
position={[aircraft.posLat!, aircraft.posLng!]}
icon={
new DivIcon({
iconAnchor: [0, 0],
html: getMarkerHTML(aircraft, anchor),
})
}
/>
)}
{openAircraftMarker.some((m) => m.id === aircraft.id) && !hideMarker && (
<SmartPopup
options={{
@@ -362,7 +374,7 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
}}
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 <AircraftMarker key={aircraft.id} aircraft={aircraft} />;
})}
</>

View File

@@ -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 (
<Popup
position={[contextMenu.lat, contextMenu.lng]}
@@ -70,6 +109,31 @@ export const ContextMenu = () => {
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 = () => {
<button
className="btn btn-sm rounded-full bg-rescuetrack aspect-square"
onClick={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();
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();
}}
>
<Search size={20} />

View File

@@ -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<LMarker>(null);
const intervalRef = useRef<NodeJS.Timeout>(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<any>(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 (
<Polygon
key={element.id}
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}
/>
);
@@ -86,8 +112,30 @@ export const SearchElements = () => {
return (
<>
{searchElements.map((element) => {
return <SearchElement key={element.id} element={element} />;
{openMissionMarker.map(({ id }) => {
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 && (
<Marker

View File

@@ -1,7 +1,12 @@
"use client";
import React, { useState } from "react";
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 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 queryClient = useQueryClient();
const changeAircraftMutation = useMutation({
mutationFn: async ({
id,
update,
}: {
id: number;
update: Prisma.ConnectedAircraftUpdateInput;
}) => {
await editConnectedAircraftAPI(id, update);
queryClient.invalidateQueries({
queryKey: ["aircrafts"],
});
},
});
return (
<div className="flex gap-2 p-4 justify-center items-center min-h-[136px] h-full">
{Array.from({ length: 9 }, (_, i) => (i + 1).toString())
.filter((status) => status !== "5") // Exclude status 5
.map((status) => (
<div
<button
disabled={!dispatcherConnected}
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={{
backgroundColor:
hoveredStatus === status
@@ -60,15 +90,19 @@ const FMSStatusSelector = ({ aircraft }: { aircraft: Aircraft }) => {
}}
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}
</div>
</button>
))}
</div>
);

View File

@@ -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 = ({
</span>
</div>
))}
{aircrafts.map((aircraft) => (
<div
key={aircraft.id}
className="relative w-auto inline-flex items-center gap-2 text-nowrap cursor-pointer"
style={{
backgroundColor: FMS_STATUS_COLORS[aircraft.fmsStatus],
}}
onClick={() => {
setOpenAircraftMarker({
open: [
{
id: aircraft.id,
tab: "aircraft",
},
],
close: [],
});
map.setView([aircraft.location.lat, aircraft.location.lng], 12, {
animate: true,
});
}}
>
<span
className="mx-2 my-0.5 text-gt font-bold"
{aircrafts
.filter((a) => a.simulatorConnected)
.map((aircraft) => (
<div
key={aircraft.id}
className="relative w-auto inline-flex items-center gap-2 text-nowrap cursor-pointer"
style={{
color: FMS_STATUS_TEXT_COLORS[aircraft.fmsStatus],
backgroundColor: FMS_STATUS_COLORS[aircraft.fmsStatus],
}}
onClick={() => {
setOpenAircraftMarker({
open: [
{
id: aircraft.id,
tab: "aircraft",
},
],
close: [],
});
map.setView([aircraft.posLat!, aircraft.posLng!], 12, {
animate: true,
});
}}
>
{aircraft.fmsStatus}
</span>
<span>{aircraft.bosName}</span>
</div>
))}
<span
className="mx-2 my-0.5 text-gt font-bold"
style={{
color: FMS_STATUS_TEXT_COLORS[aircraft.fmsStatus],
}}
>
{aircraft.fmsStatus}
</span>
<span>{aircraft.Station.bosCallsign}</span>
</div>
))}
</div>
</>
);
@@ -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];

View File

@@ -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 (
<form className="space-y-4">
{/* Koorinaten Section */}
@@ -313,9 +296,13 @@ 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>
{missionFormValues?.addressOSMways?.length && (
<p className="text-sm text-info">
In diesem Einsatz gibt es {missionFormValues?.addressOSMways?.length}{" "}
Gebäude
</p>
)}
<div className="form-control min-h-[140px]">
<div className="flex gap-2">
@@ -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();

View File

@@ -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 {

View 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;
};