Merge branch 'staging' of https://github.com/VAR-Virtual-Air-Rescue/var-monorepo into staging
This commit is contained in:
@@ -87,6 +87,20 @@ router.patch("/:id", async (req, res) => {
|
|||||||
data: req.body,
|
data: req.body,
|
||||||
});
|
});
|
||||||
io.to("dispatchers").emit("update-mission", { updatedMission });
|
io.to("dispatchers").emit("update-mission", { updatedMission });
|
||||||
|
if (req.body.state === "finished") {
|
||||||
|
updatedMission.missionStationUserIds?.forEach((userId) => {
|
||||||
|
io.to(`user:${userId}`).emit("notification", {
|
||||||
|
type: "mission-closed",
|
||||||
|
status: "closed",
|
||||||
|
message: `Einsatz ${updatedMission.publicId} wurde beendet`,
|
||||||
|
data: {
|
||||||
|
missionId: updatedMission.id,
|
||||||
|
publicMissionId: updatedMission.publicId,
|
||||||
|
},
|
||||||
|
} as NotificationPayload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.json(updatedMission);
|
res.json(updatedMission);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export const handleConnectPilot =
|
|||||||
posLat: randomPos?.lat,
|
posLat: randomPos?.lat,
|
||||||
posLng: randomPos?.lng,
|
posLng: randomPos?.lng,
|
||||||
posXplanePluginActive: debug ? true : undefined,
|
posXplanePluginActive: debug ? true : undefined,
|
||||||
|
posH145active: debug ? true : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -119,11 +119,12 @@ export const MissionForm = () => {
|
|||||||
});
|
});
|
||||||
const { missionFormValues, setOpen } = usePannelStore((state) => state);
|
const { missionFormValues, setOpen } = usePannelStore((state) => state);
|
||||||
|
|
||||||
const validationRequired = HPGValidationRequired(
|
const validationRequired =
|
||||||
form.watch("missionStationIds"),
|
HPGValidationRequired(
|
||||||
aircrafts,
|
form.watch("missionStationIds"),
|
||||||
form.watch("hpgMissionString"),
|
aircrafts,
|
||||||
);
|
form.watch("hpgMissionString"),
|
||||||
|
) && !form.watch("hpgMissionString")?.startsWith("kein Szenario");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session.data?.user.id) {
|
if (session.data?.user.id) {
|
||||||
@@ -145,6 +146,7 @@ export const MissionForm = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const key in missionFormValues) {
|
for (const key in missionFormValues) {
|
||||||
|
console.debug(key, missionFormValues[key as keyof MissionOptionalDefaults]);
|
||||||
if (key === "addressOSMways") continue; // Skip addressOSMways as it is handled separately
|
if (key === "addressOSMways") continue; // Skip addressOSMways as it is handled separately
|
||||||
form.setValue(
|
form.setValue(
|
||||||
key as keyof MissionOptionalDefaults,
|
key as keyof MissionOptionalDefaults,
|
||||||
@@ -370,6 +372,7 @@ export const MissionForm = () => {
|
|||||||
<option disabled value="please_select">
|
<option disabled value="please_select">
|
||||||
Einsatz Szenario auswählen...
|
Einsatz Szenario auswählen...
|
||||||
</option>
|
</option>
|
||||||
|
<option value={"kein Szenario:3_1_1_1-4_1"}>Kein Szenario</option>
|
||||||
{keywords &&
|
{keywords &&
|
||||||
keywords
|
keywords
|
||||||
.find((k) => k.name === form.watch("missionKeywordName"))
|
.find((k) => k.name === form.watch("missionKeywordName"))
|
||||||
@@ -416,11 +419,20 @@ export const MissionForm = () => {
|
|||||||
In diesem Einsatz gibt es {form.watch("addressOSMways").length} Gebäude
|
In diesem Einsatz gibt es {form.watch("addressOSMways").length} Gebäude
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p
|
<div className="flex items-center justify-between">
|
||||||
className={cn("text-sm text-gray-500", form.watch("addressOSMways").length && "text-info")}
|
<p
|
||||||
>
|
className={cn("text-sm text-gray-500", form.watch("xPlaneObjects").length && "text-info")}
|
||||||
In diesem Einsatz gibt es {form.watch("xPlaneObjects").length} Objekte
|
>
|
||||||
</p>
|
In diesem Einsatz gibt es {form.watch("xPlaneObjects").length} Objekte
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
disabled={!(form.watch("xPlaneObjects")?.length > 0)}
|
||||||
|
className="btn btn-xs btn-error mt-2"
|
||||||
|
onClick={() => form.setValue("xPlaneObjects", [])}
|
||||||
|
>
|
||||||
|
löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-control min-h-[140px]">
|
<div className="form-control min-h-[140px]">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|||||||
@@ -3,17 +3,7 @@ import { OSMWay } from "@repo/db";
|
|||||||
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
||||||
import { useMapStore } from "_store/mapStore";
|
import { useMapStore } from "_store/mapStore";
|
||||||
import { usePannelStore } from "_store/pannelStore";
|
import { usePannelStore } from "_store/pannelStore";
|
||||||
import { XplaneObject } from "@repo/db";
|
import { MapPin, MapPinned, Search, Car, Ambulance, Siren, Flame } from "lucide-react";
|
||||||
import {
|
|
||||||
MapPin,
|
|
||||||
MapPinned,
|
|
||||||
Radius,
|
|
||||||
Search,
|
|
||||||
RulerDimensionLine,
|
|
||||||
Scan,
|
|
||||||
Car,
|
|
||||||
Ambulance,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { getOsmAddress } from "_querys/osm";
|
import { getOsmAddress } from "_querys/osm";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
@@ -43,15 +33,16 @@ export const ContextMenu = () => {
|
|||||||
setOpen,
|
setOpen,
|
||||||
isOpen: isPannelOpen,
|
isOpen: isPannelOpen,
|
||||||
} = usePannelStore((state) => state);
|
} = usePannelStore((state) => state);
|
||||||
const [showRulerOptions, setShowRulerOptions] = useState(false);
|
const [showObjectOptions, setShowObjectOptions] = useState(false);
|
||||||
const [rulerHover, setRulerHover] = useState(false);
|
const [rulerHover, setRulerHover] = useState(false);
|
||||||
const [rulerOptionsHover, setRulerOptionsHover] = useState(false);
|
const [rulerOptionsHover, setRulerOptionsHover] = useState(false);
|
||||||
|
|
||||||
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
|
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowRulerOptions(rulerHover || rulerOptionsHover);
|
const showObjectOptions = rulerHover || rulerOptionsHover;
|
||||||
}, [rulerHover, rulerOptionsHover]);
|
setShowObjectOptions(showObjectOptions);
|
||||||
|
}, [isPannelOpen, rulerHover, rulerOptionsHover, setOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleContextMenu = (e: any) => {
|
const handleContextMenu = (e: any) => {
|
||||||
@@ -198,8 +189,8 @@ export const ContextMenu = () => {
|
|||||||
>
|
>
|
||||||
<Search size={20} />
|
<Search size={20} />
|
||||||
</button>
|
</button>
|
||||||
{/* Ruler Options - shown when Ruler button is hovered or options are hovered */}
|
{/* XPlane Object Options - shown when Ruler button is hovered or options are hovered */}
|
||||||
{showRulerOptions && (
|
{showObjectOptions && (
|
||||||
<div
|
<div
|
||||||
className="pointer-events-auto absolute -left-[100px] top-1/2 z-10 flex h-[200px] w-[120px] -translate-y-1/2 flex-col items-center justify-center py-5"
|
className="pointer-events-auto absolute -left-[100px] top-1/2 z-10 flex h-[200px] w-[120px] -translate-y-1/2 flex-col items-center justify-center py-5"
|
||||||
onMouseEnter={() => setRulerOptionsHover(true)}
|
onMouseEnter={() => setRulerOptionsHover(true)}
|
||||||
@@ -207,8 +198,8 @@ export const ContextMenu = () => {
|
|||||||
>
|
>
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full flex-col">
|
||||||
<button
|
<button
|
||||||
className="btn btn-circle bg-rescuetrack tooltip tooltip-left tooltip-accent mb-2 h-10 w-10 translate-x-full opacity-80"
|
className="btn btn-circle bg-rescuetrack tooltip tooltip-left tooltip-accent mb-2 ml-[30px] h-10 w-10 opacity-80"
|
||||||
data-tip="Rettungswagen Platzieren"
|
data-tip="Rettungswagen platzieren"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log("Add Ambulance");
|
console.log("Add Ambulance");
|
||||||
setMissionFormValues({
|
setMissionFormValues({
|
||||||
@@ -216,7 +207,7 @@ export const ContextMenu = () => {
|
|||||||
xPlaneObjects: [
|
xPlaneObjects: [
|
||||||
...(missionFormValues?.xPlaneObjects ?? []),
|
...(missionFormValues?.xPlaneObjects ?? []),
|
||||||
{
|
{
|
||||||
objectName: "test",
|
objectName: "ambulance",
|
||||||
alt: 0,
|
alt: 0,
|
||||||
lat: contextMenu.lat,
|
lat: contextMenu.lat,
|
||||||
lon: contextMenu.lng,
|
lon: contextMenu.lng,
|
||||||
@@ -229,21 +220,45 @@ export const ContextMenu = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-circle bg-rescuetrack tooltip tooltip-left tooltip-accent mb-2 h-10 w-10 opacity-80"
|
className="btn btn-circle bg-rescuetrack tooltip tooltip-left tooltip-accent mb-2 h-10 w-10 opacity-80"
|
||||||
data-tip="Radius Messen"
|
data-tip="LF platzieren"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
/* ... */
|
console.log("Add fire engine");
|
||||||
|
setMissionFormValues({
|
||||||
|
...missionFormValues,
|
||||||
|
xPlaneObjects: [
|
||||||
|
...(missionFormValues?.xPlaneObjects ?? []),
|
||||||
|
{
|
||||||
|
objectName: "fire_engine",
|
||||||
|
alt: 0,
|
||||||
|
lat: contextMenu.lat,
|
||||||
|
lon: contextMenu.lng,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Radius size={20} />
|
<Flame size={20} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-circle bg-rescuetrack tooltip tooltip-left tooltip-accent h-10 w-10 translate-x-full opacity-80"
|
className="btn btn-circle bg-rescuetrack tooltip tooltip-left tooltip-accent ml-[30px] h-10 w-10 opacity-80"
|
||||||
data-tip="Fläche Messen"
|
data-tip="Streifenwagen platzieren"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
/* ... */
|
console.log("Add police");
|
||||||
|
setMissionFormValues({
|
||||||
|
...missionFormValues,
|
||||||
|
xPlaneObjects: [
|
||||||
|
...(missionFormValues?.xPlaneObjects ?? []),
|
||||||
|
{
|
||||||
|
objectName: "police",
|
||||||
|
alt: 0,
|
||||||
|
lat: contextMenu.lat,
|
||||||
|
lon: contextMenu.lng,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Scan size={20} />
|
<Siren size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { HPGValidationRequired } from "_helpers/hpgValidationRequired";
|
|||||||
import { getConnectedAircraftsAPI } from "_querys/aircrafts";
|
import { getConnectedAircraftsAPI } from "_querys/aircrafts";
|
||||||
import { useMapStore } from "_store/mapStore";
|
import { useMapStore } from "_store/mapStore";
|
||||||
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
||||||
|
import { is } from "date-fns/locale";
|
||||||
|
import { XplaneObject } from "@repo/db";
|
||||||
|
|
||||||
export const MapAdditionals = () => {
|
export const MapAdditionals = () => {
|
||||||
const { isOpen, missionFormValues } = usePannelStore((state) => state);
|
const { isOpen, missionFormValues } = usePannelStore((state) => state);
|
||||||
@@ -53,6 +55,20 @@ export const MapAdditionals = () => {
|
|||||||
interactive={false}
|
interactive={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isOpen &&
|
||||||
|
missionFormValues?.xPlaneObjects &&
|
||||||
|
(missionFormValues.xPlaneObjects as unknown as XplaneObject[]).map((obj, index) => (
|
||||||
|
<Marker
|
||||||
|
key={index}
|
||||||
|
position={[obj.lat, obj.lon]}
|
||||||
|
icon={L.icon({
|
||||||
|
iconUrl: `/icons/${obj.objectName}.png`,
|
||||||
|
iconSize: [40, 40],
|
||||||
|
iconAnchor: [20, 35],
|
||||||
|
})}
|
||||||
|
interactive={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
{markersNeedingAttention.map((mission) => (
|
{markersNeedingAttention.map((mission) => (
|
||||||
<Marker
|
<Marker
|
||||||
key={mission.id}
|
key={mission.id}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export const xPlaneObjectsAvailable = (
|
|||||||
) => {
|
) => {
|
||||||
return missionStationIds?.some((id) => {
|
return missionStationIds?.some((id) => {
|
||||||
const aircraft = aircrafts?.find((a) => a.stationId === id);
|
const aircraft = aircrafts?.find((a) => a.stationId === id);
|
||||||
|
|
||||||
return aircraft?.posXplanePluginActive;
|
return aircraft?.posXplanePluginActive;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
BIN
apps/dispatch/public/icons/ambulance.png
Normal file
BIN
apps/dispatch/public/icons/ambulance.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
apps/dispatch/public/icons/fire_engine.png
Normal file
BIN
apps/dispatch/public/icons/fire_engine.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
BIN
apps/dispatch/public/icons/police.png
Normal file
BIN
apps/dispatch/public/icons/police.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 349 KiB |
@@ -50,9 +50,20 @@ export type MissionAutoClose = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MissionClosed = {
|
||||||
|
type: "mission-closed";
|
||||||
|
status: "closed";
|
||||||
|
message: string;
|
||||||
|
data: {
|
||||||
|
missionId: number;
|
||||||
|
publicMissionId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type NotificationPayload =
|
export type NotificationPayload =
|
||||||
| ValidationFailed
|
| ValidationFailed
|
||||||
| ValidationSuccess
|
| ValidationSuccess
|
||||||
| AdminMessage
|
| AdminMessage
|
||||||
| StationStatus
|
| StationStatus
|
||||||
| MissionAutoClose;
|
| MissionAutoClose
|
||||||
|
| MissionClosed;
|
||||||
|
|||||||
Reference in New Issue
Block a user