added Callback and custon notification Toast, client notification event handler
This commit is contained in:
@@ -0,0 +1,422 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "../AircraftMarker";
|
||||
import {
|
||||
ConnectedAircraft,
|
||||
getPublicUser,
|
||||
Mission,
|
||||
MissionLog,
|
||||
MissionSdsLog,
|
||||
MissionStationLog,
|
||||
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";
|
||||
import { PersonIcon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
Ban,
|
||||
BellRing,
|
||||
CircleGaugeIcon,
|
||||
Clock,
|
||||
CompassIcon,
|
||||
Component,
|
||||
GaugeIcon,
|
||||
Hash,
|
||||
ListCollapse,
|
||||
LocateFixed,
|
||||
MapPin,
|
||||
Mountain,
|
||||
Navigation,
|
||||
Plus,
|
||||
RadioTower,
|
||||
Sunset,
|
||||
TextSearch,
|
||||
} from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { editMissionAPI, sendSdsMessageAPI } from "querys/missions";
|
||||
|
||||
const FMSStatusHistory = ({
|
||||
aircraft,
|
||||
mission,
|
||||
}: {
|
||||
aircraft: ConnectedAircraft & { Station: Station };
|
||||
mission?: Mission;
|
||||
}) => {
|
||||
console.log("FMSStatusHistory", mission?.missionLog);
|
||||
const log = ((mission?.missionLog as unknown as MissionLog[]) || [])
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.type === "station-log" &&
|
||||
entry.data.stationId === aircraft.Station.id,
|
||||
)
|
||||
.reverse()
|
||||
.splice(0, 6) as MissionStationLog[];
|
||||
|
||||
const aircraftUser =
|
||||
typeof aircraft.publicUser === "string"
|
||||
? JSON.parse(aircraft.publicUser)
|
||||
: aircraft.publicUser;
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<ul className="text-base-content font-semibold">
|
||||
<li className="flex items-center gap-2 mb-1">
|
||||
<PersonIcon className="w-5 h-5" /> {aircraftUser.fullName} (
|
||||
{aircraftUser.publicId})
|
||||
</li>
|
||||
</ul>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<ul className="space-y-2">
|
||||
{log.map((entry, index) => (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<span
|
||||
className="font-bold text-base"
|
||||
style={{
|
||||
color: FMS_STATUS_TEXT_COLORS[entry.data.newFMSstatus],
|
||||
}}
|
||||
>
|
||||
{entry.data.newFMSstatus}
|
||||
</span>
|
||||
<span className="text-base-content">
|
||||
{new Date(entry.timeStamp).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 flex-col gap-2 mt-2 p-4 text-base-content">
|
||||
<div className="flex gap-2 justify-center items-center h-full">
|
||||
{Array.from({ length: 9 }, (_, i) => (i + 1).toString())
|
||||
.filter((status) => status !== "5") // Exclude status 5
|
||||
.map((status) => (
|
||||
<button
|
||||
disabled={!dispatcherConnected}
|
||||
key={status}
|
||||
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
|
||||
? FMS_STATUS_COLORS[status]
|
||||
: aircraft.fmsStatus === status
|
||||
? FMS_STATUS_COLORS[status]
|
||||
: "var(--color-base-200)",
|
||||
color:
|
||||
aircraft.fmsStatus === status
|
||||
? "white"
|
||||
: hoveredStatus === status
|
||||
? "white"
|
||||
: "gray",
|
||||
}}
|
||||
onMouseEnter={() => setHoveredStatus(status)}
|
||||
onMouseLeave={() => setHoveredStatus(null)}
|
||||
onClick={async () => {
|
||||
await changeAircraftMutation.mutateAsync({
|
||||
id: aircraft.id,
|
||||
update: {
|
||||
fmsStatus: status,
|
||||
},
|
||||
});
|
||||
toast.success(`Status changed to ${status}`);
|
||||
}}
|
||||
>
|
||||
{status}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-1 p-2 justify-center items-center">
|
||||
{["E", "C", "F", "J", "L", "c", "d", "h", "o", "u"].map((status) => (
|
||||
<button
|
||||
disabled={!dispatcherConnected}
|
||||
key={status}
|
||||
className={cn(
|
||||
"flex justify-center items-center min-w-10 min-h-10 cursor-pointer text-lg font-bold",
|
||||
!dispatcherConnected && "cursor-not-allowed",
|
||||
)}
|
||||
style={{
|
||||
backgroundColor:
|
||||
hoveredStatus === status
|
||||
? FMS_STATUS_COLORS[6]
|
||||
: aircraft.fmsStatus === status
|
||||
? FMS_STATUS_COLORS[status]
|
||||
: "var(--color-base-200)",
|
||||
color:
|
||||
aircraft.fmsStatus === status
|
||||
? "white"
|
||||
: hoveredStatus === status
|
||||
? "white"
|
||||
: "gray",
|
||||
}}
|
||||
onMouseEnter={() => setHoveredStatus(status)}
|
||||
onMouseLeave={() => setHoveredStatus(null)}
|
||||
onClick={async () => {
|
||||
await changeAircraftMutation.mutateAsync({
|
||||
id: aircraft.id,
|
||||
update: {
|
||||
fmsStatus: status,
|
||||
},
|
||||
});
|
||||
toast.success(`Status changed to ${status}`);
|
||||
}}
|
||||
>
|
||||
{status}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RettungsmittelTab = ({
|
||||
aircraft,
|
||||
}: {
|
||||
aircraft: ConnectedAircraft & { Station: Station };
|
||||
}) => {
|
||||
const station = aircraft.Station;
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
<ul className="text-base-content font-semibold">
|
||||
<li className="flex items-center gap-2 mb-1">
|
||||
<Component size={16} /> Aktuelle Rufgruppe: LST_01
|
||||
</li>
|
||||
<li className="flex items-center gap-2 mb-1">
|
||||
<RadioTower size={16} /> Leitstellenbereich: Florian Berlin
|
||||
</li>
|
||||
</ul>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="flex items-center text-sm font-semibold justify-between pr-2 mt-2 mb-2">
|
||||
<span className="flex items-center gap-2">
|
||||
<Clock size={16} /> {station.is24h ? "24h Betrieb" : "Tagbetrieb"}
|
||||
</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<Sunset size={16} /> NVG: {station.hasNvg ? "Ja" : "Nein"}
|
||||
</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<Mountain size={16} /> Winch: {station.hasWinch ? "Ja" : "Nein"}
|
||||
</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<TextSearch size={16} /> {station.aircraftRegistration}
|
||||
</span>
|
||||
</div>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="flex items-center text-sm font-semibold justify-between pr-2 mt-2">
|
||||
<span className="flex items-center gap-2">
|
||||
<CompassIcon size={16} /> HDG: {aircraft.posHeading}°
|
||||
</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<GaugeIcon size={16} /> SPD: {aircraft.posSpeed} kt
|
||||
</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<CircleGaugeIcon size={16} /> ALT: {aircraft.posAlt} ft
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MissionTab = ({ mission }: { mission: Mission }) => {
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
<ul className="text-base-content font-semibold">
|
||||
<li className="flex items-center gap-2 mb-1">
|
||||
<BellRing size={16} /> {mission.missionKeywordCategory}
|
||||
</li>
|
||||
<li className="flex items-center gap-2 mb-1">
|
||||
<ListCollapse size={16} />
|
||||
{mission.missionKeywordName}
|
||||
</li>
|
||||
<li className="flex items-center gap-2 mt-3">
|
||||
<Hash size={16} />
|
||||
__{new Date().toISOString().slice(0, 10).replace(/-/g, "")}
|
||||
{mission.id}
|
||||
</li>
|
||||
</ul>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="text-sm font-semibold">
|
||||
<p className="flex items-center gap-2">
|
||||
<MapPin size={16} /> {mission.addressLat} {mission.addressLng}
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<Navigation size={16} /> {mission.addressStreet}
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<LocateFixed size={16} /> {mission.addressZip} {mission.addressCity}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SDSTab = ({
|
||||
aircraft,
|
||||
mission,
|
||||
}: {
|
||||
aircraft: ConnectedAircraft & {
|
||||
Station: Station;
|
||||
};
|
||||
mission?: Mission;
|
||||
}) => {
|
||||
const session = useSession();
|
||||
const [isChatOpen, setIsChatOpen] = useState(false);
|
||||
const [note, setNote] = useState("");
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const sendSdsMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
message,
|
||||
}: {
|
||||
id: number;
|
||||
message: MissionSdsLog;
|
||||
}) => {
|
||||
await sendSdsMessageAPI(id, message);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["missions"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const log =
|
||||
(mission?.missionLog as unknown as MissionLog[])
|
||||
?.slice()
|
||||
.reverse()
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.type === "sds-log" &&
|
||||
entry.data.stationId === aircraft.Station.id,
|
||||
) || [];
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{!isChatOpen ? (
|
||||
<button
|
||||
className="text-base-content text-base cursor-pointer"
|
||||
onClick={() => setIsChatOpen(true)}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<Plus size={18} /> Notiz hinzufügen
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder=""
|
||||
className="input input-sm text-base-content flex-1"
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-sm btn-primary btn-outline"
|
||||
onClick={() => {
|
||||
if (!mission) return;
|
||||
sendSdsMutation
|
||||
.mutateAsync({
|
||||
id: mission.id,
|
||||
message: {
|
||||
type: "sds-log",
|
||||
auto: false,
|
||||
timeStamp: new Date().toISOString(),
|
||||
data: {
|
||||
stationId: aircraft.Station.id,
|
||||
station: aircraft.Station,
|
||||
message: note,
|
||||
user: getPublicUser(session.data!.user),
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
setIsChatOpen(false);
|
||||
setNote("");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Plus size={20} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-outline"
|
||||
onClick={() => {
|
||||
setIsChatOpen(false);
|
||||
setNote("");
|
||||
}}
|
||||
>
|
||||
<Ban size={20} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="divider m-0" />
|
||||
<ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto">
|
||||
{log.map((entry, index) => {
|
||||
const sdsEntry = entry as MissionSdsLog;
|
||||
return (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<span className="text-base-content">
|
||||
{new Date(sdsEntry.timeStamp).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
<span
|
||||
className="font-bold text-base"
|
||||
style={{
|
||||
color: FMS_STATUS_TEXT_COLORS[6],
|
||||
}}
|
||||
>
|
||||
{sdsEntry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}
|
||||
{sdsEntry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}
|
||||
</span>
|
||||
<span className="text-base-content">{sdsEntry.data.message}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
{!log.length && (
|
||||
<p className="text-gray-500 w-full text-center my-10 font-semibold">
|
||||
Kein SDS-Verlauf verfügbar
|
||||
</p>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { FMSStatusSelector, RettungsmittelTab, MissionTab, SDSTab };
|
||||
export default FMSStatusHistory;
|
||||
321
apps/dispatch/app/_components/map/_components/MarkerCluster.tsx
Normal file
321
apps/dispatch/app/_components/map/_components/MarkerCluster.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import {
|
||||
ConnectedAircraft,
|
||||
HpgValidationState,
|
||||
Mission,
|
||||
Station,
|
||||
} from "@repo/db";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { SmartPopup, useSmartPopup } from "_components/SmartPopup";
|
||||
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
||||
import { useMapStore } from "_store/mapStore";
|
||||
import {
|
||||
FMS_STATUS_COLORS,
|
||||
FMS_STATUS_TEXT_COLORS,
|
||||
} from "_components/map/AircraftMarker";
|
||||
import {
|
||||
MISSION_STATUS_COLORS,
|
||||
MISSION_STATUS_TEXT_COLORS,
|
||||
} from "_components/map/MissionMarkers";
|
||||
import { cn } from "helpers/cn";
|
||||
import { checkSimulatorConnected } from "helpers/simulatorConnected";
|
||||
import { getConnectedAircraftsAPI } from "querys/aircrafts";
|
||||
import { getMissionsAPI } from "querys/missions";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMap } from "react-leaflet";
|
||||
|
||||
const PopupContent = ({
|
||||
aircrafts,
|
||||
missions,
|
||||
}: {
|
||||
aircrafts: (ConnectedAircraft & { Station: Station })[];
|
||||
missions: Mission[];
|
||||
}) => {
|
||||
const { anchor } = useSmartPopup();
|
||||
const { setOpenAircraftMarker, setOpenMissionMarker } = useMapStore(
|
||||
(state) => state,
|
||||
);
|
||||
const map = useMap();
|
||||
|
||||
let borderColor = "";
|
||||
|
||||
if (anchor.includes("top")) {
|
||||
if (missions.length > 0) {
|
||||
borderColor = MISSION_STATUS_TEXT_COLORS[missions[0]!.state];
|
||||
} else if (aircrafts.length > 0) {
|
||||
borderColor = FMS_STATUS_TEXT_COLORS[aircrafts[0]!.fmsStatus] || "white";
|
||||
}
|
||||
} else if (anchor.includes("bottom")) {
|
||||
if (aircrafts.length > 0) {
|
||||
borderColor =
|
||||
FMS_STATUS_TEXT_COLORS[aircrafts[aircrafts.length - 1]!.fmsStatus] ||
|
||||
"white";
|
||||
} else if (missions.length > 0) {
|
||||
borderColor = MISSION_STATUS_TEXT_COLORS[missions[0]!.state];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex flex-col text-white min-w-[200px]">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute w-[calc(100%+2px)] h-4 z-99 pointer-events-none",
|
||||
anchor.includes("left") ? "-left-[2px]" : "-right-[2px]",
|
||||
anchor.includes("top") ? "-top-[2px]" : "-bottom-[2px]",
|
||||
)}
|
||||
style={{
|
||||
borderLeft: anchor.includes("left")
|
||||
? `3px solid ${borderColor}`
|
||||
: "",
|
||||
borderRight: anchor.includes("right")
|
||||
? `3px solid ${borderColor}`
|
||||
: "",
|
||||
borderTop: anchor.includes("top") ? `3px solid ${borderColor}` : "",
|
||||
borderBottom: anchor.includes("bottom")
|
||||
? `3px solid ${borderColor}`
|
||||
: "",
|
||||
}}
|
||||
/>
|
||||
{missions.map((mission) => {
|
||||
const markerColor =
|
||||
mission.hpgValidationState ===
|
||||
(HpgValidationState.POSITION_AMANDED ||
|
||||
HpgValidationState.INVALID ||
|
||||
HpgValidationState.HPG_DISCONNECT ||
|
||||
HpgValidationState.HPG_BUSY ||
|
||||
HpgValidationState.HPG_INVALID_MISSION)
|
||||
? MISSION_STATUS_COLORS["attention"]
|
||||
: MISSION_STATUS_COLORS[mission.state];
|
||||
return (
|
||||
<div
|
||||
key={mission.id}
|
||||
className={cn(
|
||||
"relative inline-flex items-center gap-2 text-nowrap w-full",
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: markerColor,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="mx-2 my-0.5 flex-1 cursor-pointer"
|
||||
onClick={() => {
|
||||
setOpenMissionMarker({
|
||||
open: [
|
||||
{
|
||||
id: mission.id,
|
||||
tab: "home",
|
||||
},
|
||||
],
|
||||
close: [],
|
||||
});
|
||||
map.setView([mission.addressLat, mission.addressLng], 12, {
|
||||
animate: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{mission.missionKeywordAbbreviation}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{aircrafts
|
||||
.filter((a) => checkSimulatorConnected(a.lastHeartbeat))
|
||||
.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.posLat!, aircraft.posLng!], 12, {
|
||||
animate: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const MarkerCluster = () => {
|
||||
const map = useMap();
|
||||
const { data: aircrafts } = useQuery({
|
||||
queryKey: ["aircrafts"],
|
||||
queryFn: getConnectedAircraftsAPI,
|
||||
});
|
||||
|
||||
const dispatcherConnected =
|
||||
useDispatchConnectionStore((s) => s.status) === "connected";
|
||||
const { data: missions = [] } = useQuery({
|
||||
queryKey: ["missions"],
|
||||
queryFn: () =>
|
||||
getMissionsAPI({
|
||||
OR: [{ state: "draft" }, { state: "running" }],
|
||||
}),
|
||||
});
|
||||
|
||||
const filteredMissions = useMemo(() => {
|
||||
if (!dispatcherConnected) {
|
||||
return missions.filter((m: Mission) => m.state === "running");
|
||||
}
|
||||
return missions;
|
||||
}, [missions, dispatcherConnected]);
|
||||
|
||||
const [cluster, setCluster] = useState<
|
||||
{
|
||||
aircrafts: (ConnectedAircraft & { Station: Station })[];
|
||||
missions: Mission[];
|
||||
lat: number;
|
||||
lng: number;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
// Compute clusters based on zoom and data using useMemo
|
||||
const clusters = useMemo(() => {
|
||||
const zoom = map.getZoom();
|
||||
if (zoom >= 8) return [];
|
||||
let newCluster: typeof cluster = [];
|
||||
aircrafts
|
||||
?.filter((a) => checkSimulatorConnected(a.lastHeartbeat))
|
||||
.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,
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
filteredMissions?.forEach((mission) => {
|
||||
const lat = mission.addressLat;
|
||||
const lng = mission.addressLng;
|
||||
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,
|
||||
missions: [...c.missions, mission],
|
||||
};
|
||||
}
|
||||
return c;
|
||||
});
|
||||
} else {
|
||||
newCluster = [
|
||||
...newCluster,
|
||||
{
|
||||
aircrafts: [],
|
||||
missions: [mission],
|
||||
lat,
|
||||
lng,
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
const clusterWithAvgPos = newCluster.map((c) => {
|
||||
const aircraftPos = c.aircrafts.map((a) => [a.posLat, a.posLng]);
|
||||
const missionPos = c.missions.map((m) => [m.addressLat, m.addressLng]);
|
||||
const allPos = [...aircraftPos, ...missionPos];
|
||||
|
||||
const avgLat =
|
||||
allPos.reduce((sum, pos) => sum + pos[0]!, 0) / allPos.length;
|
||||
const avgLng =
|
||||
allPos.reduce((sum, pos) => sum + pos[1]!, 0) / allPos.length;
|
||||
|
||||
return {
|
||||
...c,
|
||||
lat: avgLat,
|
||||
lng: avgLng,
|
||||
};
|
||||
});
|
||||
|
||||
return clusterWithAvgPos;
|
||||
}, [aircrafts, filteredMissions, map]);
|
||||
|
||||
// Update clusters on zoom change
|
||||
useEffect(() => {
|
||||
const handleZoom = () => {
|
||||
const zoom = map.getZoom();
|
||||
if (zoom >= 8) {
|
||||
setCluster([]);
|
||||
} else {
|
||||
setCluster(clusters);
|
||||
}
|
||||
};
|
||||
map.on("zoomend", handleZoom);
|
||||
// Set initial clusters
|
||||
handleZoom();
|
||||
return () => {
|
||||
map.off("zoomend", handleZoom);
|
||||
};
|
||||
}, [map, clusters]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{cluster.map((c, i) => (
|
||||
<SmartPopup
|
||||
options={{
|
||||
ignoreMarker: true,
|
||||
}}
|
||||
id={`cluster-${i}`}
|
||||
wrapperClassName="relative"
|
||||
key={i}
|
||||
position={[c.lat, c.lng]}
|
||||
autoPan={false}
|
||||
autoClose={false}
|
||||
className="w-[202px]"
|
||||
>
|
||||
<PopupContent aircrafts={c.aircrafts} missions={c.missions} />
|
||||
</SmartPopup>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,673 @@
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FMS_STATUS_TEXT_COLORS } from "../AircraftMarker";
|
||||
import { toast } from "react-hot-toast";
|
||||
import {
|
||||
Ban,
|
||||
BellRing,
|
||||
Flag,
|
||||
Hash,
|
||||
ListCollapse,
|
||||
LocateFixed,
|
||||
MapPin,
|
||||
Navigation,
|
||||
Plus,
|
||||
Repeat2,
|
||||
Trash,
|
||||
User,
|
||||
SmartphoneNfc,
|
||||
CheckCheck,
|
||||
Cross,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
getPublicUser,
|
||||
HpgValidationState,
|
||||
Mission,
|
||||
MissionLog,
|
||||
MissionMessageLog,
|
||||
Prisma,
|
||||
Station,
|
||||
} from "@repo/db";
|
||||
import { usePannelStore } from "_store/pannelStore";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
deleteMissionAPI,
|
||||
editMissionAPI,
|
||||
sendMissionAPI,
|
||||
} from "querys/missions";
|
||||
import { getConnectedAircraftsAPI } from "querys/aircrafts";
|
||||
import { getStationsAPI } from "querys/stations";
|
||||
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
||||
|
||||
const Einsatzdetails = ({ mission }: { mission: Mission }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const deleteMissionMutation = useMutation({
|
||||
mutationKey: ["missions"],
|
||||
mutationFn: deleteMissionAPI,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["missions"],
|
||||
});
|
||||
},
|
||||
});
|
||||
const sendAlertMutation = useMutation({
|
||||
mutationKey: ["missions"],
|
||||
mutationFn: (id: number) => sendMissionAPI(id, {}),
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
toast.error("Fehler beim Alarmieren");
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast.success(data.message);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["missions"],
|
||||
});
|
||||
},
|
||||
});
|
||||
const editMissionMutation = useMutation({
|
||||
mutationKey: ["missions"],
|
||||
mutationFn: ({
|
||||
id,
|
||||
mission,
|
||||
}: {
|
||||
id: number;
|
||||
mission: Prisma.MissionUpdateInput;
|
||||
}) => editMissionAPI(id, mission),
|
||||
onSuccess: () => {
|
||||
toast.success("Gespeichert");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["missions"],
|
||||
});
|
||||
},
|
||||
});
|
||||
const dispatcherConnected =
|
||||
useDispatchConnectionStore((s) => s.status) === "connected";
|
||||
const { setMissionFormValues, setOpen } = usePannelStore((state) => state);
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold">
|
||||
<Flag /> Einsatzdetails
|
||||
</h2>
|
||||
{mission.state !== "draft" && dispatcherConnected && (
|
||||
<div className="space-x-2">
|
||||
<div
|
||||
className="tooltip tooltip-primary tooltip-left font-semibold"
|
||||
data-tip="Einsatzdaten übernehmen"
|
||||
>
|
||||
<button
|
||||
className="btn btn-xs btn-primary btn-dash flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setMissionFormValues({
|
||||
...mission,
|
||||
id: undefined,
|
||||
hpgAmbulanceState: null,
|
||||
hpgFireEngineState: null,
|
||||
hpgPoliceState: null,
|
||||
hpgLocationLat: undefined,
|
||||
hpgLocationLng: undefined,
|
||||
state: "draft",
|
||||
});
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<Repeat2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="tooltip tooltip-warning tooltip-left font-semibold z-[9999]"
|
||||
data-tip="Einsatz abschließen"
|
||||
>
|
||||
<button
|
||||
className="btn btn-xs btn-warning flex items-center gap-2"
|
||||
onClick={() => {
|
||||
editMissionMutation.mutate({
|
||||
id: mission.id,
|
||||
mission: {
|
||||
state: "finished",
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<CheckCheck size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ul className="text-base-content font-semibold">
|
||||
<li className="flex items-center gap-2 mb-1">
|
||||
<BellRing size={16} /> {mission.missionKeywordCategory}
|
||||
</li>
|
||||
<li className="flex items-center gap-2 mb-1">
|
||||
<ListCollapse size={16} />
|
||||
{mission.missionKeywordName}
|
||||
</li>
|
||||
<li className="flex items-center gap-2 mt-3">
|
||||
<Hash size={16} />
|
||||
{mission.publicId}
|
||||
</li>
|
||||
</ul>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="text-sm font-semibold">
|
||||
<p className="flex items-center gap-2">
|
||||
<MapPin size={16} /> {mission.addressLat} {mission.addressLng}
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<Navigation size={16} /> {mission.addressStreet}
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<LocateFixed size={16} /> {mission.addressZip} {mission.addressCity}
|
||||
</p>
|
||||
</div>
|
||||
{mission.state === "draft" && (
|
||||
<div>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
|
||||
<div className="form-control mb-2">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-sm checkbox-primary"
|
||||
/>
|
||||
<span className="label-text font-semibold leading-6">
|
||||
Ohne HPG-Mission alarmieren
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
{(mission.hpgValidationState === HpgValidationState.VALID ||
|
||||
mission.hpgValidationState ===
|
||||
HpgValidationState.NOT_VALIDATED) && (
|
||||
<button
|
||||
className="btn btn-sm btn-info btn-outline flex-3"
|
||||
onClick={() => sendAlertMutation.mutate(mission.id)}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<BellRing size={16} /> Alarmieren
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{(mission.hpgValidationState === HpgValidationState.PENDING ||
|
||||
mission.hpgValidationState === HpgValidationState.HPG_BUSY ||
|
||||
mission.hpgValidationState ===
|
||||
HpgValidationState.HPG_DISCONNECT ||
|
||||
mission.hpgValidationState === HpgValidationState.INVALID ||
|
||||
HpgValidationState.HPG_INVALID_MISSION) &&
|
||||
mission.hpgValidationState !== HpgValidationState.NOT_VALIDATED &&
|
||||
mission.hpgValidationState !==
|
||||
HpgValidationState.POSITION_AMANDED &&
|
||||
mission.hpgValidationState !== HpgValidationState.VALID && (
|
||||
<button
|
||||
className="btn btn-sm btn-info btn-outline flex-3"
|
||||
onClick={() => sendAlertMutation.mutate(mission.id)}
|
||||
disabled
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{mission.hpgValidationState ===
|
||||
HpgValidationState.PENDING && (
|
||||
<div>
|
||||
<span className="loading loading-spinner loading-md"></span>{" "}
|
||||
HPG-Validierung läuft...
|
||||
</div>
|
||||
)}
|
||||
{mission.hpgValidationState ===
|
||||
HpgValidationState.HPG_BUSY && "HPG-Client busy"}
|
||||
{mission.hpgValidationState ===
|
||||
HpgValidationState.HPG_DISCONNECT &&
|
||||
"HPG-Client nicht verbunden"}
|
||||
{mission.hpgValidationState ===
|
||||
HpgValidationState.INVALID && "HPG-Client fehlerhaft"}
|
||||
{mission.hpgValidationState ===
|
||||
HpgValidationState.HPG_INVALID_MISSION &&
|
||||
"Fehlerhafte HPG-Mission"}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{mission.hpgValidationState ===
|
||||
HpgValidationState.POSITION_AMANDED && (
|
||||
<button
|
||||
className="btn btn-sm btn-warning btn-outline flex-3"
|
||||
onClick={() => sendAlertMutation.mutate(mission.id)}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<BellRing size={16} /> Mit neuer Position alarmieren
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="btn btn-sm btn-primary btn-dash flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setMissionFormValues({
|
||||
...mission,
|
||||
id: undefined,
|
||||
hpgAmbulanceState: null,
|
||||
hpgFireEngineState: null,
|
||||
hpgPoliceState: null,
|
||||
hpgLocationLat: undefined,
|
||||
hpgLocationLng: undefined,
|
||||
state: "draft",
|
||||
});
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<Repeat2 size={18} /> Daten übernehmen
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-error btn-outline"
|
||||
onClick={() => {
|
||||
deleteMissionMutation.mutate(mission.id);
|
||||
}}
|
||||
>
|
||||
<Trash size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Patientdetails = ({ mission }: { mission: Mission }) => {
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold mb-3">
|
||||
<User /> Patientendetails
|
||||
</h2>
|
||||
<p className="text-base-content font-semibold">
|
||||
{mission.missionPatientInfo}
|
||||
</p>
|
||||
<div className="divider my-2" />
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold mb-3">
|
||||
<Cross /> Einsatzinformationen
|
||||
</h2>
|
||||
<p className="text-base-content font-semibold">
|
||||
{mission.missionAdditionalInfo}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedStation, setSelectedStation] = useState<
|
||||
Station | "ambulance" | "police" | "firebrigade" | null
|
||||
>(null);
|
||||
const { data: conenctedAircrafts } = useQuery({
|
||||
queryKey: ["aircrafts"],
|
||||
queryFn: getConnectedAircraftsAPI,
|
||||
});
|
||||
const updateMissionMutation = useMutation({
|
||||
mutationKey: ["missions", "stations-mission", mission.id],
|
||||
mutationFn: ({
|
||||
id,
|
||||
missionEdit,
|
||||
}: {
|
||||
id: number;
|
||||
missionEdit: Prisma.MissionUpdateInput;
|
||||
}) => editMissionAPI(id, missionEdit),
|
||||
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
toast.error("Fehler beim Speichern");
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Cache invalidieren → Query wird neu ausgeführt
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["missions"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { data: missionStations, refetch: refetchMissionStationIds } = useQuery(
|
||||
{
|
||||
queryKey: ["stations-mission", mission.id],
|
||||
queryFn: () =>
|
||||
getStationsAPI({
|
||||
id: {
|
||||
in: mission.missionStationIds,
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
useEffect(() => {
|
||||
refetchMissionStationIds();
|
||||
}, [mission.missionStationIds, refetchMissionStationIds]);
|
||||
|
||||
const { data: allStations } = useQuery({
|
||||
queryKey: ["stations"],
|
||||
queryFn: () => getStationsAPI(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (allStations) {
|
||||
const stationsNotItMission = allStations.filter(
|
||||
(s) => !mission.missionStationIds.includes(s.id),
|
||||
);
|
||||
if (stationsNotItMission[0]) {
|
||||
setSelectedStation(stationsNotItMission[0]);
|
||||
}
|
||||
}
|
||||
}, [allStations, mission.missionStationIds]);
|
||||
|
||||
const sendAlertMutation = useMutation({
|
||||
mutationKey: ["missions"],
|
||||
mutationFn: ({ id, stationId }: { id: number; stationId?: number }) =>
|
||||
sendMissionAPI(id, { stationId }),
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
toast.error("Fehler beim Alarmieren");
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast.success(data.message);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold mb-3">
|
||||
<SmartphoneNfc /> Rettungsmittel
|
||||
</h2>
|
||||
<div
|
||||
className="tooltip tooltip-primary tooltip-left font-semibold"
|
||||
data-tip="Einsatz erneut alarmieren"
|
||||
>
|
||||
<button
|
||||
className="btn btn-xs btn-primary btn-outline"
|
||||
onClick={() => {
|
||||
sendAlertMutation.mutate({ id: mission.id });
|
||||
}}
|
||||
>
|
||||
<BellRing size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto">
|
||||
{missionStations?.map((station, index) => {
|
||||
const connectedAircraft = conenctedAircrafts?.find(
|
||||
(aircraft) => aircraft.stationId === station.id,
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
{connectedAircraft && (
|
||||
<span
|
||||
className="font-bold text-base"
|
||||
style={{
|
||||
color: FMS_STATUS_TEXT_COLORS[connectedAircraft.fmsStatus],
|
||||
}}
|
||||
>
|
||||
{connectedAircraft.fmsStatus}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-base-content">
|
||||
<div>
|
||||
<span className="font-bold">{station.bosCallsign}</span>
|
||||
{/* {item.min > 0 && (
|
||||
<>
|
||||
<br />
|
||||
Ankunft in ca. {item.min} min
|
||||
</>
|
||||
)} */}
|
||||
</div>
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="flex items-center gap-2">
|
||||
{/* TODO: make it a small multiselect */}
|
||||
<select
|
||||
className="select select-sm select-primary select-bordered flex-1"
|
||||
onChange={(e) => {
|
||||
const selected = allStations?.find(
|
||||
(s) => s.id.toString() === e.target.value,
|
||||
);
|
||||
if (selected) {
|
||||
setSelectedStation(selected);
|
||||
} else {
|
||||
setSelectedStation(
|
||||
e.target.value as "ambulance" | "police" | "firebrigade",
|
||||
);
|
||||
}
|
||||
}}
|
||||
value={
|
||||
typeof selectedStation === "string"
|
||||
? selectedStation
|
||||
: selectedStation?.id
|
||||
}
|
||||
>
|
||||
{allStations
|
||||
?.filter((s) => !mission.missionStationIds.includes(s.id))
|
||||
?.map((station) => (
|
||||
<option
|
||||
key={station.id}
|
||||
value={station.id}
|
||||
onClick={() => {
|
||||
setSelectedStation(station);
|
||||
}}
|
||||
>
|
||||
{station.bosCallsign}
|
||||
</option>
|
||||
))}
|
||||
<option disabled>Fahrzeuge:</option>
|
||||
<option value="firebrigade">Feuerwehr</option>
|
||||
<option value="ambulance">RTW</option>
|
||||
<option value="police">Polizei</option>
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-sm btn-primary btn-outline"
|
||||
onClick={async () => {
|
||||
if (typeof selectedStation === "string") {
|
||||
toast.error("Fahrzeuge werden aktuell nicht unterstützt");
|
||||
} else {
|
||||
if (!selectedStation?.id) return;
|
||||
await updateMissionMutation.mutateAsync({
|
||||
id: mission.id,
|
||||
missionEdit: {
|
||||
missionStationIds: {
|
||||
push: selectedStation?.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
await sendAlertMutation.mutate({
|
||||
id: mission.id,
|
||||
stationId: selectedStation?.id ?? 0,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="text-base-content flex items-center gap-2">
|
||||
<BellRing size={16} /> Nachalarmieren
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
||||
const session = useSession();
|
||||
const [isAddingNote, setIsAddingNote] = useState(false);
|
||||
const [note, setNote] = useState("");
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const dispatcherConnected =
|
||||
useDispatchConnectionStore((s) => s.status) === "connected";
|
||||
|
||||
const editMissionMutation = useMutation({
|
||||
mutationFn: ({
|
||||
id,
|
||||
mission,
|
||||
}: {
|
||||
id: number;
|
||||
mission: Partial<Prisma.MissionUpdateInput>;
|
||||
}) => editMissionAPI(id, mission),
|
||||
mutationKey: ["missions"],
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["missions"] });
|
||||
},
|
||||
});
|
||||
console.log(mission.missionLog);
|
||||
|
||||
if (!session.data?.user) return null;
|
||||
return (
|
||||
<div className="p-4">
|
||||
{dispatcherConnected && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
{!isAddingNote ? (
|
||||
<button
|
||||
className="text-base-content text-base cursor-pointer"
|
||||
onClick={() => setIsAddingNote(true)}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<Plus size={18} /> Notiz hinzufügen
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder=""
|
||||
className="input input-sm text-base-content flex-1"
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-sm btn-primary btn-outline"
|
||||
onClick={() => {
|
||||
const newMissionLog = [
|
||||
...mission.missionLog,
|
||||
{
|
||||
type: "message-log",
|
||||
auto: false,
|
||||
timeStamp: new Date().toISOString(),
|
||||
data: {
|
||||
message: note,
|
||||
user: getPublicUser(session.data?.user),
|
||||
},
|
||||
} as MissionMessageLog,
|
||||
];
|
||||
editMissionMutation
|
||||
.mutateAsync({
|
||||
id: mission.id,
|
||||
mission: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
missionLog: newMissionLog as any,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
setIsAddingNote(false);
|
||||
setNote("");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Plus size={20} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-outline"
|
||||
onClick={() => {
|
||||
setIsAddingNote(false);
|
||||
setNote("");
|
||||
}}
|
||||
>
|
||||
<Ban size={20} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="divider m-0" />
|
||||
</div>
|
||||
)}
|
||||
<ul className="space-y-1 max-h-[300px] overflow-y-auto overflow-x-auto">
|
||||
{(mission.missionLog as unknown as MissionLog[])
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((entry, index) => {
|
||||
if (entry.type === "station-log")
|
||||
return (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<span className="text-base-content">
|
||||
{new Date(entry.timeStamp).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
<span
|
||||
className="font-bold text-base"
|
||||
style={{
|
||||
color: FMS_STATUS_TEXT_COLORS[entry.data.newFMSstatus],
|
||||
}}
|
||||
>
|
||||
{entry.data.newFMSstatus}
|
||||
</span>
|
||||
<span className="text-base-content">
|
||||
{entry.data.station.bosCallsign}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
if (entry.type === "message-log" || entry.type === "sds-log")
|
||||
return (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<span className="text-base-content">
|
||||
{new Date(entry.timeStamp).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
<span
|
||||
className="font-bold text-base flex items-center gap-0.5"
|
||||
style={{
|
||||
color: FMS_STATUS_TEXT_COLORS[6],
|
||||
}}
|
||||
>
|
||||
{entry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}
|
||||
{entry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}
|
||||
{entry.type === "sds-log" && (
|
||||
<>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{entry.data.station.bosCallsignShort}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-base-content">
|
||||
{entry.data.message}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
|
||||
return null;
|
||||
})}
|
||||
</ul>
|
||||
{!mission.missionLog.length && (
|
||||
<p className="text-gray-500 w-full text-center my-10 font-semibold">
|
||||
Keine Notizen verfügbar
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { FMSStatusHistory, Patientdetails, Rettungsmittel };
|
||||
export default Einsatzdetails;
|
||||
Reference in New Issue
Block a user