This commit is contained in:
PxlLoewe
2025-10-24 22:41:25 +02:00
10 changed files with 106 additions and 38 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

View File

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