addd HPG state

This commit is contained in:
PxlLoewe
2025-05-21 14:48:07 -07:00
parent f52f870eed
commit 64ce254239
8 changed files with 165 additions and 28 deletions

View File

@@ -1,4 +1,4 @@
import { prisma } from "@repo/db"; import { HpgValidationState, Prisma, prisma } from "@repo/db";
import { Router } from "express"; import { Router } from "express";
import { io } from "../index"; import { io } from "../index";
import { sendNtfyMission } from "modules/ntfy"; import { sendNtfyMission } from "modules/ntfy";
@@ -203,4 +203,75 @@ router.post("/:id/send-alert", async (req, res) => {
} }
}); });
router.post("/:id/validate-hpg", async (req, res) => {
try {
const { id } = req.params;
const mission = await prisma.mission.findFirstOrThrow({
where: {
id: Number(id),
},
});
const activeAircraftinMission = await prisma.connectedAircraft.findFirst({
where: {
stationId: {
in: mission?.missionStationIds,
},
posH145active: true,
logoutTime: null,
},
include: {
Station: true,
},
});
/* if (activeAircraftinMission.length === 0) {
res.status(400).json({ error: "No active aircraft in mission" });
return;
} */
res.json({
message: "HPG validation started",
});
io.to(`desktop:${activeAircraftinMission}`).emit(
"hpg-validation",
{
hpgMissionType: mission?.hpgMissionString,
lat: mission?.addressLat,
lng: mission?.addressLng,
},
async (result: {
state: HpgValidationState;
lat: number;
lng: number;
}) => {
console.log("response from user:", result);
const newMission = await prisma.mission.update({
where: { id: Number(id) },
data: {
// save position of new mission
addressLat:
result.state === "POSITION_AMANDED"
? result.lat
: mission.addressLat,
addressLng:
result.state === "POSITION_AMANDED"
? result.lng
: mission.addressLng,
hpgLocationLat: result.lat,
hpgLocationLng: result.lng,
hpgValidationState: result.state,
},
});
io.to("dispatchers").emit("update-mission", newMission);
},
);
} catch (error) {
console.error(error);
res.json({ error: (error as Error).message || "Failed to validate HPG" });
}
});
export default router; export default router;

View File

@@ -1,13 +1,34 @@
import { User } from "@repo/db"; import { prisma, User } from "@repo/db";
import { Socket, Server } from "socket.io"; import { Socket, Server } from "socket.io";
export const handleConnectDesktop = (socket: Socket, io: Server) => () => { export const handleConnectDesktop = (socket: Socket, io: Server) => () => {
const user = socket.data.user as User; const user = socket.data.user as User;
console.log("connect-desktop", user.publicId);
const connectedAircraft = prisma.connectedAircraft.findFirst({
where: {
userId: user.id,
logoutTime: null,
},
include: {
Station: true,
},
});
const conenctedDispatchers = prisma.connectedDispatcher.findFirst({
where: {
userId: user.id,
logoutTime: null,
},
})
socket.join(`user:${user.id}`); socket.join(`user:${user.id}`);
socket.join(`desktop:${user.id}`); socket.join(`desktop:${user.id}`);
socket.on("ptt", (data) => { socket.on("ptt", (data) => {
socket.to(`user:${user.id}`).emit("ptt", data); socket.to(`user:${user.id}`).emit("ptt", data);
socket.to("pilots").emit("other-ptt", {
publicUser: user.publicId,
source:
})
}); });
}; };

View File

@@ -21,6 +21,8 @@ import { ROOMS } from "_data/livekitRooms";
export const Audio = () => { export const Audio = () => {
const connection = usePilotConnectionStore(); const connection = usePilotConnectionStore();
const [showSource, setShowSource] = useState(false);
const { const {
isTalking, isTalking,
toggleTalking, toggleTalking,
@@ -31,9 +33,16 @@ export const Audio = () => {
remoteParticipants, remoteParticipants,
room, room,
message, message,
source,
} = useAudioStore(); } = useAudioStore();
const [selectedRoom, setSelectedRoom] = useState<string>("LST_01"); const [selectedRoom, setSelectedRoom] = useState<string>("LST_01");
useEffect(() => {
setShowSource(true);
setTimeout(() => {
setShowSource(false);
}, 2000);
}, [source, isTalking]);
useEffect(() => { useEffect(() => {
const joinRoom = async () => { const joinRoom = async () => {
if (connection.status != "connected") return; if (connection.status != "connected") return;
@@ -55,6 +64,9 @@ export const Audio = () => {
{state === "error" && ( {state === "error" && (
<div className="h-4 flex items-center">{message}</div> <div className="h-4 flex items-center">{message}</div>
)} )}
{showSource && source && (
<div className="h-4 flex items-center">{source}</div>
)}
<button <button
onClick={() => { onClick={() => {
if (state === "connected") toggleTalking(); if (state === "connected") toggleTalking();

View File

@@ -5,6 +5,7 @@ import { toast } from "react-hot-toast";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactNode, useEffect, useState } from "react"; import { ReactNode, useEffect, useState } from "react";
import { dispatchSocket } from "dispatch/socket"; import { dispatchSocket } from "dispatch/socket";
import { Mission } from "@repo/db";
export function QueryProvider({ children }: { children: ReactNode }) { export function QueryProvider({ children }: { children: ReactNode }) {
const [queryClient] = useState( const [queryClient] = useState(
@@ -22,7 +23,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
}), }),
); );
useEffect(() => { useEffect(() => {
const invalidateMission = () => { const invalidateMission = (mission: Mission) => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["missions"], queryKey: ["missions"],
}); });

View File

@@ -45,7 +45,7 @@ export const useAudioStore = create<TalkState>((set, get) => ({
if (!room) return; if (!room) return;
room.localParticipant.setMicrophoneEnabled(!isTalking); room.localParticipant.setMicrophoneEnabled(!isTalking);
set((state) => ({ isTalking: !state.isTalking })); set((state) => ({ isTalking: !state.isTalking, source: "web-app" }));
}, },
connect: async (roomName) => { connect: async (roomName) => {
set({ state: "connecting" }); set({ state: "connecting" });

View File

@@ -105,6 +105,9 @@ export const MissionForm = () => {
}); });
const { missionFormValues, setOpen } = usePannelStore((state) => state); const { missionFormValues, setOpen } = usePannelStore((state) => state);
const missionInfoText = form.watch("missionAdditionalInfo");
const hpgMissionString = form.watch("hpgMissionString");
useEffect(() => { useEffect(() => {
if (session.data?.user.id) { if (session.data?.user.id) {
form.setValue("createdUserId", session.data.user.id); form.setValue("createdUserId", session.data.user.id);
@@ -178,7 +181,7 @@ export const MissionForm = () => {
</div> </div>
<input <input
type="text" type="text"
{...form.register("missionAdditionalInfo")} {...form.register("addressAdditionalInfo")}
placeholder="Zusätzliche Adressinformationen" placeholder="Zusätzliche Adressinformationen"
className="input input-primary input-bordered w-full mt-4" className="input input-primary input-bordered w-full mt-4"
/> />
@@ -332,10 +335,16 @@ export const MissionForm = () => {
onClick={form.handleSubmit( onClick={form.handleSubmit(
async (mission: MissionOptionalDefaults) => { async (mission: MissionOptionalDefaults) => {
try { try {
const hpgSzenario = mission.hpgMissionString?.split(":")[0];
const newMission = await editMissionMutation.mutateAsync({ const newMission = await editMissionMutation.mutateAsync({
id: Number(editingMissionId), id: Number(editingMissionId),
mission: mission: {
mission as unknown as Partial<Prisma.MissionUpdateInput>, ...(mission as unknown as Prisma.MissionCreateInput),
missionAdditionalInfo:
!mission.missionAdditionalInfo.length && hpgSzenario
? `HPG-Szenario: ${hpgSzenario}`
: mission.missionAdditionalInfo,
},
}); });
toast.success( toast.success(
`Einsatz ${newMission.id} erfolgreich aktualisiert`, `Einsatz ${newMission.id} erfolgreich aktualisiert`,
@@ -362,14 +371,18 @@ export const MissionForm = () => {
onClick={form.handleSubmit( onClick={form.handleSubmit(
async (mission: MissionOptionalDefaults) => { async (mission: MissionOptionalDefaults) => {
try { try {
const hpgSzenario =
mission.hpgMissionString?.split(":")[0];
const newMission = const newMission =
await createMissionMutation.mutateAsync( await createMissionMutation.mutateAsync({
mission as unknown as Prisma.MissionCreateInput, ...(mission as unknown as Prisma.MissionCreateInput),
); missionAdditionalInfo:
!mission.missionAdditionalInfo.length && hpgSzenario
? `HPG-Szenario: ${hpgSzenario}`
: mission.missionAdditionalInfo,
});
await sendAlertMutation.mutateAsync(newMission.id); await sendAlertMutation.mutateAsync(newMission.id);
setSeachOSMElements([]); // Reset search elements setSeachOSMElements([]); // Reset search elements
toast.success(`Einsatz ${newMission.id} erstellt`);
// TODO: Einsatz alarmieren
setOpen(false); setOpen(false);
} catch (error) { } catch (error) {
toast.error( toast.error(
@@ -387,10 +400,16 @@ export const MissionForm = () => {
onClick={form.handleSubmit( onClick={form.handleSubmit(
async (mission: MissionOptionalDefaults) => { async (mission: MissionOptionalDefaults) => {
try { try {
const hpgSzenario =
mission.hpgMissionString?.split(":")[0];
const newMission = const newMission =
await createMissionMutation.mutateAsync( await createMissionMutation.mutateAsync({
mission as unknown as Prisma.MissionCreateInput, ...(mission as unknown as Prisma.MissionCreateInput),
); missionAdditionalInfo:
!mission.missionAdditionalInfo.length && hpgSzenario
? `HPG-Szenario: ${hpgSzenario}`
: mission.missionAdditionalInfo,
});
setSeachOSMElements([]); // Reset search elements setSeachOSMElements([]); // Reset search elements
toast.success(`Einsatz ${newMission.publicId} erstellt`); toast.success(`Einsatz ${newMission.publicId} erstellt`);

Binary file not shown.

View File

@@ -1,30 +1,32 @@
model Mission { model Mission {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
publicId String @default("") publicId String @default("")
type missionType @default(primär) type missionType @default(primär)
state MissionState @default(draft) state MissionState @default(draft)
addressLat Float addressLat Float
addressLng Float addressLng Float
addressStreet String? addressStreet String?
addressCity String? addressCity String?
addressZip String? addressZip String?
addressOSMways Json[] @default([]) addressAdditionalInfo String?
addressOSMways Json[] @default([])
missionKeywordCategory String? missionKeywordCategory String?
missionKeywordName String? missionKeywordName String?
missionKeywordAbbreviation String? missionKeywordAbbreviation String?
missionPatientInfo String missionPatientInfo String
missionAdditionalInfo String missionAdditionalInfo String
missionStationIds Int[] @default([]) missionStationIds Int[] @default([])
missionStationUserIds String[] @default([]) missionStationUserIds String[] @default([])
missionLog Json[] @default([]) missionLog Json[] @default([])
hpgMissionString String? hpgMissionString String?
hpgAmbulanceState HpgState? hpgAmbulanceState HpgState?
hpgFireEngineState HpgState? hpgFireEngineState HpgState?
hpgPoliceState HpgState? hpgPoliceState HpgState?
hpgLocationLat Float? @default(0) hpgLocationLat Float? @default(0)
hpgLocationLng Float? @default(0) hpgLocationLng Float? @default(0)
createdAt DateTime @default(now()) hpgValidationState HpgValidationState @default(NOT_VALIDATED)
updatedAt DateTime @updatedAt createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdUserId String createdUserId String
// relations: // relations:
@@ -75,3 +77,14 @@ enum HpgState {
arrived arrived
onway onway
} }
enum HpgValidationState {
NOT_VALIDATED
PENDING
VALID
INVALID
HPG_DISCONNECT
HPG_BUSY
HPG_INVALID_MISSION
POSITION_AMANDED
}