added DME, fixed sync and bugs. Rewrote setDisplay logic
This commit is contained in:
@@ -100,7 +100,7 @@ export const handleConnectPilot =
|
||||
},
|
||||
})
|
||||
.catch(console.error);
|
||||
io.to("dispatchers").emit("pilots-update");
|
||||
io.to("dispatchers").emit("update-connectedAircraft");
|
||||
io.to("pilots").emit("pilots-update");
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -30,7 +30,10 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const invalidateConnectedUsers = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["connected-users", "aircrafts"],
|
||||
queryKey: ["connected-users"],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["aircrafts"],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
26
apps/dispatch/app/_data/fmsStatusDescription.ts
Normal file
26
apps/dispatch/app/_data/fmsStatusDescription.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export const fmsStatusDescription: { [key: string]: string } = {
|
||||
NaN: "Keine Daten",
|
||||
"0": "Prio. Sprechwunsch",
|
||||
"1": "Frei auf Funk",
|
||||
"2": "Einsatzbereit am LRZ",
|
||||
"3": "Auf dem Weg",
|
||||
"4": "Am Einsatzort",
|
||||
"5": "Sprechwunsch",
|
||||
"6": "Nicht einsatzbereit",
|
||||
"7": "Patient aufgenommen",
|
||||
"8": "Am Transportziel",
|
||||
"9": "Fremdanmeldung",
|
||||
E: "Indent/Abbruch/Einsatzbefehl abgebrochen",
|
||||
C: "Anmelden zur Übernahme des Einsatzes",
|
||||
F: "Kommen über Draht",
|
||||
H: "Fahren auf Wache",
|
||||
J: "Sprechaufforderung",
|
||||
L: "Lagebericht abgeben",
|
||||
P: "Einsatz mit Polizei/Pause machen",
|
||||
U: "Ungültiger Status",
|
||||
c: "Status korrigieren",
|
||||
d: "Transportziel angeben",
|
||||
h: "Zielklinik verständigt",
|
||||
o: "Warten, alle Abfrageplätze belegt",
|
||||
u: "Verstanden",
|
||||
};
|
||||
@@ -5,6 +5,7 @@ interface ConnectionStore {
|
||||
status: "connected" | "disconnected" | "connecting" | "error";
|
||||
message: string;
|
||||
selectedZone: string;
|
||||
logoffTime: string;
|
||||
connect: (
|
||||
uid: string,
|
||||
selectedZone: string,
|
||||
@@ -17,6 +18,7 @@ export const useDispatchConnectionStore = create<ConnectionStore>((set) => ({
|
||||
status: "disconnected",
|
||||
message: "",
|
||||
selectedZone: "LST_01",
|
||||
logoffTime: "",
|
||||
connect: async (uid, selectedZone, logoffTime) =>
|
||||
new Promise((resolve) => {
|
||||
set({ status: "connecting", message: "" });
|
||||
@@ -58,3 +60,12 @@ dispatchSocket.on("force-disconnect", (reason: string) => {
|
||||
message: reason,
|
||||
});
|
||||
});
|
||||
|
||||
dispatchSocket.on("reconnect", () => {
|
||||
const { logoffTime, selectedZone } = useDispatchConnectionStore.getState();
|
||||
|
||||
dispatchSocket.emit("connect-dispatch", {
|
||||
logoffTime,
|
||||
selectedZone,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,36 @@
|
||||
import { Station } from "@repo/db";
|
||||
import { fmsStatusDescription } from "_data/fmsStatusDescription";
|
||||
import { DisplayLineProps } from "pilot/_components/mrt/Mrt";
|
||||
import { create } from "zustand";
|
||||
import { syncTabs } from "zustand-sync-tabs";
|
||||
|
||||
type Page = "home" | "sending-status" | "new-status" | "error";
|
||||
interface SetHomePageParams {
|
||||
page: "home";
|
||||
station: Station;
|
||||
fmsStatus: string;
|
||||
}
|
||||
|
||||
type PageData = {
|
||||
home: undefined;
|
||||
"sending-status": undefined;
|
||||
"new-status": undefined;
|
||||
error: {
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
interface SetSendingStatusPageParams {
|
||||
page: "sending-status";
|
||||
station: Station;
|
||||
}
|
||||
|
||||
interface SetNewStatusPageParams {
|
||||
page: "new-status";
|
||||
station: Station;
|
||||
}
|
||||
|
||||
type SetPageParams =
|
||||
| SetHomePageParams
|
||||
| SetSendingStatusPageParams
|
||||
| SetNewStatusPageParams;
|
||||
|
||||
interface MrtStore {
|
||||
page: Page;
|
||||
pageData: PageData[Page];
|
||||
page: SetPageParams["page"];
|
||||
|
||||
lines: DisplayLineProps[];
|
||||
|
||||
setPage: <P extends Page>(page: P, pageData?: PageData[P]) => void;
|
||||
setPage: (pageData: SetPageParams) => void;
|
||||
setLines: (lines: MrtStore["lines"]) => void;
|
||||
}
|
||||
|
||||
@@ -30,14 +41,93 @@ export const useMrtStore = create<MrtStore>(
|
||||
pageData: {
|
||||
message: "",
|
||||
},
|
||||
lines: Array.from(Array(10).keys()).map(() => ({
|
||||
textLeft: "",
|
||||
textMid: "",
|
||||
textRight: "",
|
||||
textSize: "1",
|
||||
})),
|
||||
lines: [
|
||||
{
|
||||
textLeft: "VAR.#",
|
||||
textSize: "2",
|
||||
},
|
||||
{
|
||||
textLeft: "No Data",
|
||||
textSize: "3",
|
||||
},
|
||||
],
|
||||
setLines: (lines) => set({ lines }),
|
||||
setPage: (page, pageData) => set({ page, pageData }),
|
||||
setPage: (pageData) => {
|
||||
switch (pageData.page) {
|
||||
case "home": {
|
||||
const { station, fmsStatus } = pageData as SetHomePageParams;
|
||||
set({
|
||||
page: "home",
|
||||
lines: [
|
||||
{
|
||||
textLeft: `VAR#.${station?.bosCallsign}`,
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "2",
|
||||
},
|
||||
{ textLeft: "ILS VAR#", textSize: "3" },
|
||||
{
|
||||
textLeft: fmsStatus,
|
||||
style: { fontWeight: "extrabold" },
|
||||
textSize: "4",
|
||||
},
|
||||
{
|
||||
textLeft: fmsStatusDescription[fmsStatus],
|
||||
textSize: "1",
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "sending-status": {
|
||||
const { station } = pageData as SetSendingStatusPageParams;
|
||||
set({
|
||||
page: "sending-status",
|
||||
lines: [
|
||||
{
|
||||
textLeft: `VAR#.${station?.bosCallsign}`,
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "2",
|
||||
},
|
||||
{ textLeft: "ILS VAR#", textSize: "3" },
|
||||
{
|
||||
textMid: "sending...",
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "4",
|
||||
},
|
||||
{
|
||||
textLeft: "Status wird gesendet...",
|
||||
textSize: "1",
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "new-status": {
|
||||
const { station } = pageData as SetNewStatusPageParams;
|
||||
set({
|
||||
page: "new-status",
|
||||
lines: [
|
||||
{
|
||||
textLeft: `VAR#.${station?.bosCallsign}`,
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "2",
|
||||
},
|
||||
{ textLeft: "ILS VAR#", textSize: "3" },
|
||||
{
|
||||
textLeft: "new status received",
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "4",
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
set({ page: "home" });
|
||||
break;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "mrt-store", // unique name
|
||||
|
||||
@@ -2,7 +2,7 @@ import { create } from "zustand";
|
||||
import { dispatchSocket } from "../../dispatch/socket";
|
||||
import { ConnectedAircraft, Mission, Station } from "@repo/db";
|
||||
import { pilotSocket } from "pilot/socket";
|
||||
import { useMrtStore } from "_store/pilot/MrtStore";
|
||||
import { useDmeStore } from "_store/pilot/dmeStore";
|
||||
|
||||
interface ConnectionStore {
|
||||
status: "connected" | "disconnected" | "connecting" | "error";
|
||||
@@ -75,11 +75,14 @@ pilotSocket.on("aircraft-update", (data) => {
|
||||
usePilotConnectionStore.setState({
|
||||
connectedAircraft: data,
|
||||
});
|
||||
useMrtStore.getState().setLines(getNew);
|
||||
/* useMrtStore.getState().setLines(getNew); */
|
||||
});
|
||||
|
||||
pilotSocket.on("mission-alert", (data) => {
|
||||
pilotSocket.on("mission-alert", (data: Mission & { Stations: Station[] }) => {
|
||||
usePilotConnectionStore.setState({
|
||||
activeMission: data,
|
||||
});
|
||||
useDmeStore.getState().setPage({
|
||||
page: "new-mission",
|
||||
});
|
||||
});
|
||||
|
||||
195
apps/dispatch/app/_store/pilot/dmeStore.ts
Normal file
195
apps/dispatch/app/_store/pilot/dmeStore.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { Mission, Station, User } from "@repo/db";
|
||||
import { DisplayLineProps } from "pilot/_components/dme/Dme";
|
||||
import { create } from "zustand";
|
||||
import { syncTabs } from "zustand-sync-tabs";
|
||||
|
||||
interface SetHomePageParams {
|
||||
page: "home";
|
||||
user: User;
|
||||
station: Station;
|
||||
}
|
||||
|
||||
interface SetErrorPageParams {
|
||||
page: "error";
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface SetNewMissionPageParams {
|
||||
page: "new-mission";
|
||||
}
|
||||
|
||||
interface SetMissionPageParams {
|
||||
page: "mission";
|
||||
mission: Mission & { Stations: Station[] };
|
||||
}
|
||||
|
||||
interface SetAcknowledgePageParams {
|
||||
page: "acknowledge";
|
||||
}
|
||||
|
||||
type SetPageParams =
|
||||
| SetHomePageParams
|
||||
| SetNewMissionPageParams
|
||||
| SetMissionPageParams
|
||||
| SetErrorPageParams
|
||||
| SetAcknowledgePageParams;
|
||||
|
||||
interface MrtStore {
|
||||
page: SetPageParams["page"];
|
||||
|
||||
lines: DisplayLineProps[];
|
||||
|
||||
setPage: (pageData: SetPageParams) => void;
|
||||
setLines: (lines: MrtStore["lines"]) => void;
|
||||
}
|
||||
|
||||
export const useDmeStore = create<MrtStore>(
|
||||
syncTabs(
|
||||
(set) => ({
|
||||
page: "home",
|
||||
pageData: {
|
||||
message: "",
|
||||
},
|
||||
lines: [
|
||||
{
|
||||
textLeft: "VAR.#",
|
||||
textSize: "2",
|
||||
},
|
||||
{
|
||||
textLeft: "No Data",
|
||||
},
|
||||
],
|
||||
setLines: (lines) => set({ lines }),
|
||||
setPage: (pageData) => {
|
||||
switch (pageData.page) {
|
||||
case "home": {
|
||||
set({
|
||||
page: "home",
|
||||
lines: [
|
||||
{ textMid: "⠀" },
|
||||
{
|
||||
textMid: pageData.station.bosCallsign
|
||||
? `VAR#.${pageData.station.bosCallsign}`
|
||||
: "no Data",
|
||||
style: { fontWeight: "bold" },
|
||||
},
|
||||
{ textMid: "⠀" },
|
||||
{
|
||||
textMid: new Date().toLocaleDateString(),
|
||||
},
|
||||
{
|
||||
textMid: new Date().toLocaleTimeString(),
|
||||
style: { fontWeight: "bold" },
|
||||
},
|
||||
{ textMid: "⠀" },
|
||||
{
|
||||
textMid: `${pageData.user.lastname} ${pageData.user.firstname}`,
|
||||
},
|
||||
{ textMid: "⠀" },
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "new-mission": {
|
||||
set({
|
||||
page: "new-mission",
|
||||
lines: [
|
||||
{ textMid: "⠀" },
|
||||
{
|
||||
textMid: "new mission received",
|
||||
style: { fontWeight: "bold" },
|
||||
},
|
||||
{ textMid: "⠀" },
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "mission": {
|
||||
set({
|
||||
page: "mission",
|
||||
lines: [
|
||||
{
|
||||
textLeft: `${pageData.mission.missionKeywordAbbreviation}`,
|
||||
textRight: pageData.mission.Stations.map(
|
||||
(s) => s.bosCallsignShort,
|
||||
).join(","),
|
||||
style: { fontWeight: "bold" },
|
||||
},
|
||||
{
|
||||
textMid: `${pageData.mission.missionKeywordName}`,
|
||||
style: { fontWeight: "bold" },
|
||||
},
|
||||
{ textLeft: `${pageData.mission.addressStreet}` },
|
||||
{
|
||||
textLeft: `${pageData.mission.addressZip} ${pageData.mission.addressCity}`,
|
||||
},
|
||||
{
|
||||
textMid: "Patienteninfos:",
|
||||
style: { fontWeight: "bold" },
|
||||
},
|
||||
{
|
||||
textLeft:
|
||||
pageData.mission.missionPatientInfo || "keine Daten",
|
||||
},
|
||||
{
|
||||
textMid: "Weitere Infos:",
|
||||
style: { fontWeight: "bold" },
|
||||
},
|
||||
{
|
||||
textLeft:
|
||||
pageData.mission.missionAdditionalInfo || "keine Daten",
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "error": {
|
||||
set({
|
||||
page: "error",
|
||||
lines: [
|
||||
{ textMid: "Fehler:" },
|
||||
{
|
||||
textMid: pageData.error,
|
||||
style: { fontWeight: "bold" },
|
||||
},
|
||||
{ textMid: "⠀" },
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "acknowledge": {
|
||||
set({
|
||||
page: "acknowledge",
|
||||
lines: [
|
||||
{ textMid: "⠀" },
|
||||
{
|
||||
textMid: "Einsatz angenommen",
|
||||
style: { fontWeight: "bold" },
|
||||
},
|
||||
{ textMid: "⠀" },
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
set({
|
||||
page: "error",
|
||||
lines: [
|
||||
{ textMid: "Fehler:" },
|
||||
{
|
||||
textMid: `Unbekannte Seite`,
|
||||
style: { fontWeight: "bold" },
|
||||
},
|
||||
{ textMid: "⠀" },
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "dme-store", // unique name
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -107,7 +107,7 @@ const AircraftPopupContent = ({
|
||||
return missions?.find(
|
||||
(m) =>
|
||||
(m.state === "running" || m.state === "draft") &&
|
||||
m.missionStationIds.includes(aircraft.Station.id.toString()),
|
||||
m.missionStationIds.includes(aircraft.Station.id),
|
||||
);
|
||||
}, [missions, aircraft.Station.id]);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Trash,
|
||||
User,
|
||||
SmartphoneNfc,
|
||||
CheckCheck,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
getPublicUser,
|
||||
@@ -23,15 +24,18 @@ import {
|
||||
MissionLog,
|
||||
MissionMessageLog,
|
||||
Prisma,
|
||||
Station,
|
||||
} from "@repo/db";
|
||||
import { usePannelStore } from "_store/pannelStore";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
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";
|
||||
|
||||
const Einsatzdetails = ({ mission }: { mission: Mission }) => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -58,6 +62,22 @@ const Einsatzdetails = ({ mission }: { mission: Mission }) => {
|
||||
});
|
||||
},
|
||||
});
|
||||
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 { setMissionFormValues, setOpen } = usePannelStore((state) => state);
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
@@ -66,28 +86,48 @@ const Einsatzdetails = ({ mission }: { mission: Mission }) => {
|
||||
<Flag /> Einsatzdetails
|
||||
</h2>
|
||||
{mission.state !== "draft" && (
|
||||
<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);
|
||||
}}
|
||||
<div className="space-x-2">
|
||||
<div
|
||||
className="tooltip tooltip-primary tooltip-left font-semibold"
|
||||
data-tip="Einsatzdaten übernehmen"
|
||||
>
|
||||
<Repeat2 size={16} />
|
||||
</button>
|
||||
<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>
|
||||
@@ -177,46 +217,84 @@ const Patientdetails = ({ mission }: { mission: Mission }) => {
|
||||
};
|
||||
|
||||
const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
||||
/* const [stations, setStations] = useState<Station[]>([]);
|
||||
useEffect(() => {
|
||||
getStations().then((data) => {
|
||||
setStations(data);
|
||||
});
|
||||
}, []); */
|
||||
// Mockup data
|
||||
const mockupData = [
|
||||
{ bosCallsign: "Christoph 31", FMSstatus: 2, min: 6 },
|
||||
{ bosCallsign: "RTW", FMSstatus: 3, min: 2 },
|
||||
{ bosCallsign: "Polizei", FMSstatus: 4, min: 0 },
|
||||
];
|
||||
const { data: conenctedAircrafts } = useQuery({
|
||||
queryKey: ["aircrafts"],
|
||||
queryFn: getConnectedAircraftsAPI,
|
||||
});
|
||||
const { data: stations } = useQuery({
|
||||
queryKey: ["mission", "stations-mission", mission.id],
|
||||
queryFn: () =>
|
||||
getStationsAPI({
|
||||
id: {
|
||||
in: mission.missionStationIds,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const sendAlertMutation = useMutation({
|
||||
mutationKey: ["missions"],
|
||||
mutationFn: sendMissionAPI,
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
toast.error("Fehler beim Alarmieren");
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast.success(data.message);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold mb-3">
|
||||
<SmartphoneNfc /> Rettungsmittel
|
||||
</h2>
|
||||
<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(mission.id);
|
||||
}}
|
||||
>
|
||||
<BellRing size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto">
|
||||
{mockupData.map((item, index) => (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<span
|
||||
className="font-bold text-base"
|
||||
style={{
|
||||
color: FMS_STATUS_TEXT_COLORS[item.FMSstatus],
|
||||
}}
|
||||
>
|
||||
{item.FMSstatus}
|
||||
</span>
|
||||
<span className="text-base-content">
|
||||
<span className="font-bold">{item.bosCallsign}</span>
|
||||
{item.min > 0 && (
|
||||
<>
|
||||
<br />
|
||||
Ankunft in ca. {item.min} min
|
||||
</>
|
||||
{stations?.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>
|
||||
</li>
|
||||
))}
|
||||
<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">
|
||||
|
||||
130
apps/dispatch/app/pilot/_components/dme/Dme.tsx
Normal file
130
apps/dispatch/app/pilot/_components/dme/Dme.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { CSSProperties } from "react";
|
||||
import sQuadImageNoReflections from "./Melder_NoReflections.png";
|
||||
import sQuadReflection from "./reflektion.png";
|
||||
import { useButtons } from "./useButtons";
|
||||
import { useSounds } from "./useSounds";
|
||||
import Image from "next/image";
|
||||
import { useDmeStore } from "_store/pilot/dmeStore";
|
||||
|
||||
const DME_BUTTON_STYLES: CSSProperties = {
|
||||
cursor: "pointer",
|
||||
zIndex: "9999",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
};
|
||||
|
||||
export interface DisplayLineProps {
|
||||
style?: CSSProperties;
|
||||
textLeft?: string;
|
||||
textMid?: string;
|
||||
textRight?: string;
|
||||
}
|
||||
|
||||
const DisplayLine = ({
|
||||
style = {},
|
||||
textLeft,
|
||||
textMid,
|
||||
textRight,
|
||||
}: DisplayLineProps) => {
|
||||
const INNER_TEXT_PARTS: CSSProperties = {
|
||||
fontFamily: "Melder",
|
||||
flex: "1",
|
||||
flexBasis: "auto",
|
||||
overflowWrap: "break-word",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "Famirids",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<span style={INNER_TEXT_PARTS}>{textLeft}</span>
|
||||
<span style={{ textAlign: "center", ...INNER_TEXT_PARTS }}>
|
||||
{textMid}
|
||||
</span>
|
||||
<span style={{ textAlign: "end", ...INNER_TEXT_PARTS }}>{textRight}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Dme = () => {
|
||||
useSounds();
|
||||
const { handleButton } = useButtons();
|
||||
const lines = useDmeStore((state) => state.lines);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
aspectRatio: "1037 / 800",
|
||||
color: "black",
|
||||
height: "auto",
|
||||
width: "auto",
|
||||
maxHeight: "100%",
|
||||
maxWidth: "100%",
|
||||
overflow: "hidden",
|
||||
gridTemplateColumns:
|
||||
"25.84% 6.08% 10.99% 11.57% 11.28% 10.90% 18.71% 4.63%",
|
||||
gridTemplateRows: "2.66% 9.3% 8.9% 7.2% 45.05% 27%",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={sQuadImageNoReflections}
|
||||
alt="sQuadImage"
|
||||
style={{
|
||||
zIndex: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
gridArea: "1 / 1 / 10 / 10",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleButton("main")}
|
||||
style={{ gridArea: "2 / 7 / 4 / 8", ...DME_BUTTON_STYLES }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleButton("menu")}
|
||||
style={{ gridArea: "3 / 2 / 4 / 3", ...DME_BUTTON_STYLES }}
|
||||
/>
|
||||
{/* <button
|
||||
onClick={handleButton('left')}
|
||||
style={{ gridArea: '3 / 4 / 4 / 5', ...DME_BUTTON_STYLES }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleButton('right')}
|
||||
style={{ gridArea: '3 / 5 / 4 / 6', ...DME_BUTTON_STYLES }}
|
||||
/> */}
|
||||
<Image
|
||||
src={sQuadReflection}
|
||||
alt="sQuadImage"
|
||||
style={{
|
||||
zIndex: 1,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
gridArea: "1 / 1 / 10 / 10",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
id="display"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
zIndex: "2",
|
||||
gridArea: "5 / 2 / 6 / 6",
|
||||
padding: "5px",
|
||||
overflowX: "hidden",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{lines.map((l, i) => {
|
||||
return <DisplayLine key={`line-${i}`} {...l} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
BIN
apps/dispatch/app/pilot/_components/dme/Melder_NoReflections.png
Normal file
BIN
apps/dispatch/app/pilot/_components/dme/Melder_NoReflections.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 626 KiB |
BIN
apps/dispatch/app/pilot/_components/dme/reflektion.png
Normal file
BIN
apps/dispatch/app/pilot/_components/dme/reflektion.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
BIN
apps/dispatch/app/pilot/_components/dme/squad-x15.jpg
Normal file
BIN
apps/dispatch/app/pilot/_components/dme/squad-x15.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 610 KiB |
33
apps/dispatch/app/pilot/_components/dme/useButtons.ts
Normal file
33
apps/dispatch/app/pilot/_components/dme/useButtons.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||
import { useDmeStore } from "_store/pilot/dmeStore";
|
||||
|
||||
import { useSession } from "next-auth/react";
|
||||
export const useButtons = () => {
|
||||
const { page, setPage } = useDmeStore((state) => state);
|
||||
const user = useSession().data?.user;
|
||||
const station = usePilotConnectionStore((state) => state.selectedStation);
|
||||
|
||||
const handleButton = (button: "main" | "menu" | "left" | "right") => () => {
|
||||
switch (button) {
|
||||
case "main":
|
||||
if (page === "mission") {
|
||||
setPage({ page: "acknowledge" });
|
||||
}
|
||||
break;
|
||||
case "menu":
|
||||
console.log("home", page, { station, user });
|
||||
if (station && user) {
|
||||
setPage({ page: "home", station, user });
|
||||
} else {
|
||||
setPage({ page: "error", error: "No station or user found" });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
setPage({ page: "error", error: "Button now allowed" });
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return { handleButton };
|
||||
};
|
||||
52
apps/dispatch/app/pilot/_components/dme/useSounds.ts
Normal file
52
apps/dispatch/app/pilot/_components/dme/useSounds.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||
import { useDmeStore } from "_store/pilot/dmeStore";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export const useSounds = () => {
|
||||
const { page, setPage } = useDmeStore((state) => state);
|
||||
const mission = usePilotConnectionStore((state) => state.activeMission);
|
||||
|
||||
const newMissionSound = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
newMissionSound.current = new Audio("/sounds/Melder3.wav");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timeouts: NodeJS.Timeout[] = [];
|
||||
|
||||
if (page === "new-mission" && newMissionSound.current) {
|
||||
console.log("new-mission", mission);
|
||||
newMissionSound.current.currentTime = 0;
|
||||
newMissionSound.current.volume = 0.3;
|
||||
newMissionSound.current.play();
|
||||
if (mission) {
|
||||
timeouts.push(
|
||||
setTimeout(() => setPage({ page: "mission", mission }), 500),
|
||||
);
|
||||
}
|
||||
} else if (page === "acknowledge") {
|
||||
newMissionSound.current?.pause();
|
||||
if (mission) {
|
||||
timeouts.push(
|
||||
setTimeout(() => setPage({ page: "mission", mission }), 500),
|
||||
);
|
||||
} else {
|
||||
timeouts.push(
|
||||
setTimeout(
|
||||
() => setPage({ page: "error", error: "Einsatz nicht gebunden" }),
|
||||
500,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
timeouts.forEach((t) => {
|
||||
clearTimeout(t);
|
||||
});
|
||||
};
|
||||
}, [page, setPage, mission]);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CSSProperties } from "react";
|
||||
import { CSSProperties, useEffect } from "react";
|
||||
import MrtImage from "./MRT.png";
|
||||
import { useButtons } from "./useButtons";
|
||||
import { useSounds } from "./useSounds";
|
||||
|
||||
@@ -1,115 +1,23 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { ConnectedAircraft } from "@repo/db";
|
||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||
import { useMrtStore } from "_store/pilot/MrtStore";
|
||||
import { pilotSocket } from "pilot/socket";
|
||||
import { editConnectedAircraftAPI } from "querys/aircrafts";
|
||||
import { Station } from "@repo/db";
|
||||
import { DisplayLineProps } from "pilot/_components/mrt/Mrt";
|
||||
|
||||
export const fmsStatusDescription: { [key: string]: string } = {
|
||||
NaN: "Keine Daten",
|
||||
"0": "Prio. Sprechwunsch",
|
||||
"1": "Frei auf Funk",
|
||||
"2": "Einsatzbereit am LRZ",
|
||||
"3": "Auf dem Weg",
|
||||
"4": "Am Einsatzort",
|
||||
"5": "Sprechwunsch",
|
||||
"6": "Nicht einsatzbereit",
|
||||
"7": "Patient aufgenommen",
|
||||
"8": "Am Transportziel",
|
||||
"9": "Fremdanmeldung",
|
||||
E: "Indent/Abbruch/Einsatzbefehl abgebrochen",
|
||||
C: "Anmelden zur Übernahme des Einsatzes",
|
||||
F: "Kommen über Draht",
|
||||
H: "Fahren auf Wache",
|
||||
J: "Sprechaufforderung",
|
||||
L: "Lagebericht abgeben",
|
||||
P: "Einsatz mit Polizei/Pause machen",
|
||||
U: "Ungültiger Status",
|
||||
c: "Status korrigieren",
|
||||
d: "Transportziel angeben",
|
||||
h: "Zielklinik verständigt",
|
||||
o: "Warten, alle Abfrageplätze belegt",
|
||||
u: "Verstanden",
|
||||
};
|
||||
|
||||
export const getSendingLines = (station: Station): DisplayLineProps[] => {
|
||||
return [
|
||||
{
|
||||
textLeft: `VAR#.${station?.bosCallsign}123`,
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "2",
|
||||
},
|
||||
{ textLeft: "ILS VAR#", textSize: "3" },
|
||||
{
|
||||
textMid: "sending...",
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "4",
|
||||
},
|
||||
{
|
||||
textLeft: "Status wird gesendet...",
|
||||
textSize: "1",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const getHomeLines = (
|
||||
station: Station,
|
||||
fmsStatus: string,
|
||||
): DisplayLineProps[] => {
|
||||
return [
|
||||
{
|
||||
textLeft: `VAR#.${station?.bosCallsign}`,
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "2",
|
||||
},
|
||||
{ textLeft: "ILS VAR#", textSize: "3" },
|
||||
{
|
||||
textLeft: fmsStatus,
|
||||
style: { fontWeight: "extrabold" },
|
||||
textSize: "4",
|
||||
},
|
||||
{
|
||||
textLeft: fmsStatusDescription[fmsStatus],
|
||||
textSize: "1",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const getNewStatusLines = (
|
||||
station: Station,
|
||||
fmsStatus: string,
|
||||
): DisplayLineProps[] => {
|
||||
return [
|
||||
{
|
||||
textLeft: `VAR#.${station?.bosCallsign}`,
|
||||
style: { fontWeight: "bold" },
|
||||
textSize: "2",
|
||||
},
|
||||
{ textLeft: "ILS VAR#", textSize: "3" },
|
||||
{
|
||||
textLeft: fmsStatus,
|
||||
style: { fontWeight: "extrabold" },
|
||||
textSize: "4",
|
||||
},
|
||||
{
|
||||
textLeft: fmsStatusDescription[fmsStatus],
|
||||
textSize: "1",
|
||||
},
|
||||
];
|
||||
};
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const useButtons = () => {
|
||||
const user = useSession().data?.user;
|
||||
const station = usePilotConnectionStore((state) => state.selectedStation);
|
||||
const connectedAircraft = usePilotConnectionStore(
|
||||
(state) => state.connectedAircraft,
|
||||
);
|
||||
const connectionStatus = usePilotConnectionStore((state) => state.status);
|
||||
|
||||
const { page, setLines } = useMrtStore((state) => state);
|
||||
const { page, setPage } = useMrtStore((state) => state);
|
||||
|
||||
const handleButton =
|
||||
(button: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0") =>
|
||||
() => {
|
||||
if (connectionStatus !== "connected") return;
|
||||
if (!station) return;
|
||||
if (!connectedAircraft?.id) return;
|
||||
if (
|
||||
@@ -125,16 +33,32 @@ export const useButtons = () => {
|
||||
button === "0"
|
||||
) {
|
||||
if (page !== "home") return;
|
||||
setLines(getSendingLines(station));
|
||||
setPage({ page: "sending-status", station });
|
||||
|
||||
setTimeout(async () => {
|
||||
await editConnectedAircraftAPI(connectedAircraft!.id, {
|
||||
fmsStatus: button,
|
||||
});
|
||||
setLines(getNewStatusLines(station, button));
|
||||
setPage({
|
||||
page: "home",
|
||||
station,
|
||||
fmsStatus: button,
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
pilotSocket.on("connect", () => {
|
||||
if (!station) return;
|
||||
setPage({ page: "home", fmsStatus: "6", station });
|
||||
});
|
||||
|
||||
pilotSocket.on("aircraft-update", () => {
|
||||
if (!station) return;
|
||||
setPage({ page: "new-status", station });
|
||||
});
|
||||
}, [setPage, station]);
|
||||
|
||||
return { handleButton };
|
||||
};
|
||||
|
||||
@@ -1,66 +1,53 @@
|
||||
"use client";
|
||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||
import { useMrtStore } from "_store/pilot/MrtStore";
|
||||
import { editConnectedAircraftAPI } from "querys/aircrafts";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const MRTstatusSound = new Audio("/sounds/MRT-status.mp3");
|
||||
const MrtMessageReceivedSound = new Audio("/sounds/MRT-message-received.mp3");
|
||||
|
||||
export const useSounds = () => {
|
||||
const mrtState = useMrtStore((state) => state);
|
||||
const { connectedAircraft, selectedStation } = usePilotConnectionStore(
|
||||
(state) => state,
|
||||
);
|
||||
|
||||
const fmsStatus = connectedAircraft?.fmsStatus || "NaN";
|
||||
|
||||
const previousFmsStatus = useRef(fmsStatus || "6");
|
||||
const timeout = useRef<NodeJS.Timeout>(null);
|
||||
const setPage = useMrtStore((state) => state.setPage);
|
||||
const MRTstatusSoundRef = useRef<HTMLAudioElement>(null);
|
||||
const MrtMessageReceivedSoundRef = useRef<HTMLAudioElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleSoundEnd = () => {
|
||||
mrtState.setPage("home");
|
||||
};
|
||||
if (typeof window !== "undefined") {
|
||||
MRTstatusSoundRef.current = new Audio("/sounds/MRT-status.mp3");
|
||||
MrtMessageReceivedSoundRef.current = new Audio(
|
||||
"/sounds/MRT-message-received.mp3",
|
||||
);
|
||||
MRTstatusSoundRef.current.onended = () => {
|
||||
if (!selectedStation || !connectedAircraft?.fmsStatus) return;
|
||||
setPage({
|
||||
page: "home",
|
||||
station: selectedStation,
|
||||
fmsStatus: connectedAircraft?.fmsStatus,
|
||||
});
|
||||
};
|
||||
MrtMessageReceivedSoundRef.current.onended = () => {
|
||||
if (!selectedStation || !connectedAircraft?.fmsStatus) return;
|
||||
setPage({
|
||||
page: "home",
|
||||
station: selectedStation,
|
||||
fmsStatus: connectedAircraft?.fmsStatus,
|
||||
});
|
||||
};
|
||||
}
|
||||
}, [connectedAircraft?.fmsStatus, selectedStation, setPage]);
|
||||
|
||||
const playSound = (sound: HTMLAudioElement) => {
|
||||
sound.play();
|
||||
sound.addEventListener("ended", handleSoundEnd);
|
||||
};
|
||||
const fmsStatus = connectedAircraft?.fmsStatus || "NaN";
|
||||
|
||||
useEffect(() => {
|
||||
if (!connectedAircraft) return;
|
||||
if (mrtState.page === "new-status") {
|
||||
if (fmsStatus === "J") {
|
||||
playSound(MrtMessageReceivedSound);
|
||||
|
||||
timeout.current = setTimeout(() => {
|
||||
editConnectedAircraftAPI(connectedAircraft.id, {
|
||||
fmsStatus: previousFmsStatus.current,
|
||||
});
|
||||
}, 5000);
|
||||
} else if (previousFmsStatus.current !== fmsStatus) {
|
||||
playSound(MRTstatusSound);
|
||||
if (fmsStatus === "J" || fmsStatus === "c") {
|
||||
MrtMessageReceivedSoundRef.current?.play();
|
||||
} else {
|
||||
handleSoundEnd();
|
||||
}
|
||||
if (!timeout.current) {
|
||||
previousFmsStatus.current = fmsStatus || "6";
|
||||
MRTstatusSoundRef.current?.play();
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
if (timeout.current) clearTimeout(timeout.current);
|
||||
[MRTstatusSound, MrtMessageReceivedSound].forEach((sound) => {
|
||||
sound.removeEventListener("ended", handleSoundEnd);
|
||||
sound.pause();
|
||||
sound.currentTime = 0;
|
||||
});
|
||||
};
|
||||
}, [
|
||||
mrtState,
|
||||
fmsStatus,
|
||||
connectedAircraft,
|
||||
selectedStation,
|
||||
previousFmsStatus,
|
||||
timeout,
|
||||
]);
|
||||
}, [mrtState, fmsStatus, connectedAircraft, selectedStation]);
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ export const ConnectionBtn = () => {
|
||||
</span>
|
||||
</h3>
|
||||
) : (
|
||||
<h3 className="text-lg font-bold mb-5">Als Disponent anmelden</h3>
|
||||
<h3 className="text-lg font-bold mb-5">Als Pilot anmelden</h3>
|
||||
)}
|
||||
<fieldset className="fieldset w-full">
|
||||
<label className="floating-label w-full text-base">
|
||||
|
||||
@@ -4,10 +4,9 @@ import { Mrt } from "pilot/_components/mrt/Mrt";
|
||||
import { Chat } from "../_components/left/Chat";
|
||||
import { Report } from "../_components/left/Report";
|
||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||
import { Dme } from "pilot/_components/dme/Dme";
|
||||
|
||||
const DispatchPage = () => {
|
||||
const { activeMission } = usePilotConnectionStore();
|
||||
|
||||
return (
|
||||
<div className="relative flex-1 flex transition-all duration-500 ease w-full">
|
||||
{/* <MapToastCard2 /> */}
|
||||
@@ -19,9 +18,9 @@ const DispatchPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>{JSON.stringify(activeMission)}</div>
|
||||
<div>
|
||||
<div className="flex flex-col">
|
||||
<Mrt />
|
||||
<Dme />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
BIN
apps/dispatch/public/sounds/Melder3.wav
Normal file
BIN
apps/dispatch/public/sounds/Melder3.wav
Normal file
Binary file not shown.
Binary file not shown.
@@ -20,15 +20,18 @@ enum PERMISSION {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
publicId String @unique
|
||||
firstname String
|
||||
lastname String
|
||||
email String @unique
|
||||
password String
|
||||
vatsimCid Int? @map(name: "vatsim_cid")
|
||||
moodleId Int? @map(name: "moodle_id")
|
||||
emailVerified DateTime? @map(name: "email_verified")
|
||||
id String @id @default(uuid())
|
||||
publicId String @unique
|
||||
firstname String
|
||||
lastname String
|
||||
email String @unique
|
||||
password String
|
||||
vatsimCid Int? @map(name: "vatsim_cid")
|
||||
moodleId Int? @map(name: "moodle_id")
|
||||
emailVerified DateTime? @map(name: "email_verified")
|
||||
|
||||
// Settings:
|
||||
|
||||
image String?
|
||||
badges BADGES[] @default([])
|
||||
permissions PERMISSION[] @default([])
|
||||
|
||||
Reference in New Issue
Block a user