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,
});
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);
} catch (error) {
console.error(error);

View File

@@ -97,6 +97,7 @@ export const handleConnectPilot =
posLat: randomPos?.lat,
posLng: randomPos?.lng,
posXplanePluginActive: debug ? true : undefined,
posH145active: debug ? true : undefined,
},
});

View File

@@ -119,11 +119,12 @@ export const MissionForm = () => {
});
const { missionFormValues, setOpen } = usePannelStore((state) => state);
const validationRequired = HPGValidationRequired(
const validationRequired =
HPGValidationRequired(
form.watch("missionStationIds"),
aircrafts,
form.watch("hpgMissionString"),
);
) && !form.watch("hpgMissionString")?.startsWith("kein Szenario");
useEffect(() => {
if (session.data?.user.id) {
@@ -145,6 +146,7 @@ export const MissionForm = () => {
return;
}
for (const key in missionFormValues) {
console.debug(key, missionFormValues[key as keyof MissionOptionalDefaults]);
if (key === "addressOSMways") continue; // Skip addressOSMways as it is handled separately
form.setValue(
key as keyof MissionOptionalDefaults,
@@ -370,6 +372,7 @@ export const MissionForm = () => {
<option disabled value="please_select">
Einsatz Szenario auswählen...
</option>
<option value={"kein Szenario:3_1_1_1-4_1"}>Kein Szenario</option>
{keywords &&
keywords
.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
</p>
<div className="flex items-center justify-between">
<p
className={cn("text-sm text-gray-500", form.watch("addressOSMways").length && "text-info")}
className={cn("text-sm text-gray-500", form.watch("xPlaneObjects").length && "text-info")}
>
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="flex gap-2">

View File

@@ -3,17 +3,7 @@ import { OSMWay } from "@repo/db";
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { useMapStore } from "_store/mapStore";
import { usePannelStore } from "_store/pannelStore";
import { XplaneObject } from "@repo/db";
import {
MapPin,
MapPinned,
Radius,
Search,
RulerDimensionLine,
Scan,
Car,
Ambulance,
} from "lucide-react";
import { MapPin, MapPinned, Search, Car, Ambulance, Siren, Flame } from "lucide-react";
import { getOsmAddress } from "_querys/osm";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
@@ -43,15 +33,16 @@ export const ContextMenu = () => {
setOpen,
isOpen: isPannelOpen,
} = usePannelStore((state) => state);
const [showRulerOptions, setShowRulerOptions] = useState(false);
const [showObjectOptions, setShowObjectOptions] = useState(false);
const [rulerHover, setRulerHover] = useState(false);
const [rulerOptionsHover, setRulerOptionsHover] = useState(false);
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
useEffect(() => {
setShowRulerOptions(rulerHover || rulerOptionsHover);
}, [rulerHover, rulerOptionsHover]);
const showObjectOptions = rulerHover || rulerOptionsHover;
setShowObjectOptions(showObjectOptions);
}, [isPannelOpen, rulerHover, rulerOptionsHover, setOpen]);
useEffect(() => {
const handleContextMenu = (e: any) => {
@@ -198,8 +189,8 @@ export const ContextMenu = () => {
>
<Search size={20} />
</button>
{/* Ruler Options - shown when Ruler button is hovered or options are hovered */}
{showRulerOptions && (
{/* XPlane Object Options - shown when Ruler button is hovered or options are hovered */}
{showObjectOptions && (
<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"
onMouseEnter={() => setRulerOptionsHover(true)}
@@ -207,8 +198,8 @@ export const ContextMenu = () => {
>
<div className="flex w-full flex-col">
<button
className="btn btn-circle bg-rescuetrack tooltip tooltip-left tooltip-accent mb-2 h-10 w-10 translate-x-full opacity-80"
data-tip="Rettungswagen Platzieren"
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"
onClick={() => {
console.log("Add Ambulance");
setMissionFormValues({
@@ -216,7 +207,7 @@ export const ContextMenu = () => {
xPlaneObjects: [
...(missionFormValues?.xPlaneObjects ?? []),
{
objectName: "test",
objectName: "ambulance",
alt: 0,
lat: contextMenu.lat,
lon: contextMenu.lng,
@@ -229,21 +220,45 @@ export const ContextMenu = () => {
</button>
<button
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={() => {
/* ... */
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
className="btn btn-circle bg-rescuetrack tooltip tooltip-left tooltip-accent h-10 w-10 translate-x-full opacity-80"
data-tip="Fläche Messen"
className="btn btn-circle bg-rescuetrack tooltip tooltip-left tooltip-accent ml-[30px] h-10 w-10 opacity-80"
data-tip="Streifenwagen platzieren"
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>
</div>
</div>

View File

@@ -8,6 +8,8 @@ import { HPGValidationRequired } from "_helpers/hpgValidationRequired";
import { getConnectedAircraftsAPI } from "_querys/aircrafts";
import { useMapStore } from "_store/mapStore";
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { is } from "date-fns/locale";
import { XplaneObject } from "@repo/db";
export const MapAdditionals = () => {
const { isOpen, missionFormValues } = usePannelStore((state) => state);
@@ -53,6 +55,20 @@ export const MapAdditionals = () => {
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) => (
<Marker
key={mission.id}

View File

@@ -6,7 +6,6 @@ export const xPlaneObjectsAvailable = (
) => {
return missionStationIds?.some((id) => {
const aircraft = aircrafts?.find((a) => a.stationId === id);
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 =
| ValidationFailed
| ValidationSuccess
| AdminMessage
| StationStatus
| MissionAutoClose;
| MissionAutoClose
| MissionClosed;