From 1ca6007ac56f01d6e6fd33bf5b618cd795beb19c Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Sat, 24 May 2025 12:46:11 -0700 Subject: [PATCH] added HPG VEhicles Mission, Audio settings; mission Context menu --- apps/dispatch-server/routes/mission.ts | 75 +++++-- .../socket-events/connect-desktop.ts | 2 +- apps/dispatch/app/_components/Audio.tsx | 49 ++--- .../app/_components/MicVolumeIndication.tsx | 75 +++++++ apps/dispatch/app/_components/Settings.tsx | 186 ++++++++++++++++++ .../app/_components/map/ContextMenu.tsx | 2 + .../app/_components/map/MissionMarkers.tsx | 5 - .../map/_components/MissionMarkerTabs.tsx | 46 ++++- apps/dispatch/app/_store/audioStore.ts | 34 ++-- .../app/_store/dispatch/connectionStore.ts | 12 +- .../app/_store/pilot/connectionStore.ts | 12 +- apps/dispatch/app/api/user/route.ts | 49 +++++ .../dispatch/_components/navbar/Navbar.tsx | 4 +- .../navbar/_components/Settings.tsx | 103 ---------- .../_components/pannel/MissionForm.tsx | 121 ++++++++---- .../app/helpers/hpgStateToFmsStatus.ts | 11 ++ apps/dispatch/app/helpers/radioAudio.ts | 62 ++++++ .../app/helpers/selectRandomHPGMission.ts | 28 +++ .../app/pilot/_components/navbar/Navbar.tsx | 4 +- .../app/pilot/_components/navbar/Settings.tsx | 103 ---------- .../navbar/{ => _components}/Connection.tsx | 0 .../navbar/{ => _components}/ThemeSwap.tsx | 0 apps/dispatch/app/querys/missions.ts | 23 +-- apps/dispatch/app/querys/user.ts | 12 ++ apps/dispatch/package.json | 1 + package-lock.json | 19 ++ .../database/prisma/schema/mission.prisma | 7 +- packages/database/prisma/schema/user.prisma | 4 +- 28 files changed, 680 insertions(+), 369 deletions(-) create mode 100644 apps/dispatch/app/_components/MicVolumeIndication.tsx create mode 100644 apps/dispatch/app/_components/Settings.tsx create mode 100644 apps/dispatch/app/api/user/route.ts delete mode 100644 apps/dispatch/app/dispatch/_components/navbar/_components/Settings.tsx create mode 100644 apps/dispatch/app/helpers/hpgStateToFmsStatus.ts create mode 100644 apps/dispatch/app/helpers/radioAudio.ts create mode 100644 apps/dispatch/app/helpers/selectRandomHPGMission.ts delete mode 100644 apps/dispatch/app/pilot/_components/navbar/Settings.tsx rename apps/dispatch/app/pilot/_components/navbar/{ => _components}/Connection.tsx (100%) rename apps/dispatch/app/pilot/_components/navbar/{ => _components}/ThemeSwap.tsx (100%) create mode 100644 apps/dispatch/app/querys/user.ts diff --git a/apps/dispatch-server/routes/mission.ts b/apps/dispatch-server/routes/mission.ts index 6a2996f6..5d1b4852 100644 --- a/apps/dispatch-server/routes/mission.ts +++ b/apps/dispatch-server/routes/mission.ts @@ -114,8 +114,48 @@ router.delete("/:id", async (req, res) => { router.post("/:id/send-alert", async (req, res) => { const { id } = req.params; - const { stationId } = req.body as { stationId?: number }; + const { stationId, vehicleName } = req.body as { + stationId?: number; + vehicleName?: "ambulance" | "police" | "firebrigade"; + }; + try { + if (vehicleName) { + const hpgAircrafts = await prisma.connectedAircraft.findMany({ + where: { + stationId: Number(id), + logoutTime: null, + posH145active: true, + }, + }); + + const newMission = await prisma.mission.update({ + where: { + id: Number(id), + }, + data: { + hpgAmbulanceState: vehicleName === "ambulance" ? "DISPATCHED" : undefined, + hpgFireEngineState: vehicleName === "firebrigade" ? "DISPATCHED" : undefined, + hpgPoliceState: vehicleName === "police" ? "DISPATCHED" : undefined, + }, + }); + hpgAircrafts.forEach((aircraft) => { + io.to(`desktop:${aircraft.userId}`).emit("hpg-vehicle-update", { + missionId: id, + vehicleData: { + ambulanceState: newMission.hpgAmbulanceState, + fireEngineState: newMission.hpgFireEngineState, + policeState: newMission.hpgPoliceState, + }, + }); + }); + + res.status(200).json({ + message: `Rettungsmittel disponiert (${hpgAircrafts.length} Nutzer)`, + }); + io.to("dispatchers").emit("update-mission", newMission); + return; + } const { connectedAircrafts, mission } = await sendAlert(Number(id), { stationId, }); @@ -153,7 +193,6 @@ router.post("/:id/send-sds", async (req, res) => { router.post("/:id/validate-hpg", async (req, res) => { try { - console.log(req.user); const { id } = req.params; const config = req.body as | { @@ -178,37 +217,39 @@ router.post("/:id/validate-hpg", async (req, res) => { Station: true, }, }); + const user = await prisma.user.findFirst({ + where: { + id: activeAircraftinMission?.userId, + }, + }); + const clients = await io.in(`desktop:${activeAircraftinMission?.userId}`).fetchSockets(); + if (!clients.length) { + res.status(400).json({ + error: `Keine Desktop Verbindung für ${user?.publicId} gefunden`, + }); + return; + } res.json({ message: "HPG validation started", }); - /* io.to(`desktop:${activeAircraftinMission}`).emit( + io.to(`desktop:${activeAircraftinMission}`).emit( "hpg-validation", { hpgMissionType: mission?.hpgMissionString, lat: mission?.addressLat, lng: mission?.addressLng, }, - async (result: { - state: HpgValidationState; - lat: number; - lng: number; - }) => { + 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, + 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, @@ -234,7 +275,7 @@ router.post("/:id/validate-hpg", async (req, res) => { } as NotificationPayload); } }, - ); */ + ); // TODO: remove this after testing setTimeout(() => { io.to(`user:${req.user?.id}`).emit("notification", { diff --git a/apps/dispatch-server/socket-events/connect-desktop.ts b/apps/dispatch-server/socket-events/connect-desktop.ts index 779eb341..9cddd34e 100644 --- a/apps/dispatch-server/socket-events/connect-desktop.ts +++ b/apps/dispatch-server/socket-events/connect-desktop.ts @@ -1,4 +1,4 @@ -import { getPublicUser, prisma, User } from "@repo/db"; +import { getPublicUser, HpgState, prisma, User } from "@repo/db"; import { Socket, Server } from "socket.io"; interface PTTData { diff --git a/apps/dispatch/app/_components/Audio.tsx b/apps/dispatch/app/_components/Audio.tsx index f3b5bc42..c19300f4 100644 --- a/apps/dispatch/app/_components/Audio.tsx +++ b/apps/dispatch/app/_components/Audio.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { usePilotConnectionStore } from "_store/pilot/connectionStore"; import { Disc, @@ -18,10 +18,12 @@ import { useAudioStore } from "_store/audioStore"; import { cn } from "helpers/cn"; import { ConnectionQuality } from "livekit-client"; import { ROOMS } from "_data/livekitRooms"; +import { useSession } from "next-auth/react"; export const Audio = () => { const connection = usePilotConnectionStore(); const [showSource, setShowSource] = useState(false); + const serverSession = useSession(); const { isTalking, @@ -46,6 +48,7 @@ export const Audio = () => { clearTimeout(timeout); }; }, [source, isTalking]); + useEffect(() => { const joinRoom = async () => { if (connection.status != "connected") return; @@ -64,17 +67,12 @@ export const Audio = () => { return ( <>
- {state === "error" && ( -
{message}
- )} - {showSource && source && ( -
{source}
- )} + {state === "error" &&
{message}
} + {showSource && source &&
{source}
} @@ -142,10 +126,7 @@ export const Audio = () => { disconnect(); }} > - + Disconnect diff --git a/apps/dispatch/app/_components/MicVolumeIndication.tsx b/apps/dispatch/app/_components/MicVolumeIndication.tsx new file mode 100644 index 00000000..c1dde18b --- /dev/null +++ b/apps/dispatch/app/_components/MicVolumeIndication.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { cn } from "helpers/cn"; +import { useEffect, useState } from "react"; + +type MicrophoneLevelProps = { + deviceId: string; + volumeInput: number; // Verstärkung der Lautstärke +}; + +export default function MicrophoneLevel({ deviceId, volumeInput }: MicrophoneLevelProps) { + const [volumeLevel, setVolumeLevel] = useState(0); + + useEffect(() => { + let audioContext: AudioContext | null = null; + let analyser: AnalyserNode | null = null; + let source: MediaStreamAudioSourceNode | null = null; + let rafId: number; + + async function start() { + audioContext = new AudioContext(); + const stream = await navigator.mediaDevices.getUserMedia({ + audio: { deviceId: deviceId ? { exact: deviceId } : undefined }, + }); + source = audioContext.createMediaStreamSource(stream); + analyser = audioContext.createAnalyser(); + analyser.fftSize = 256; + source.connect(analyser); + + const dataArray = new Uint8Array(analyser.frequencyBinCount); + + const updateVolume = () => { + if (!analyser) return; + analyser.getByteFrequencyData(dataArray); + const avg = dataArray.reduce((a, b) => a + b, 0) / dataArray.length; + setVolumeLevel(avg * volumeInput); + rafId = requestAnimationFrame(updateVolume); + }; + + updateVolume(); + } + + start(); + + return () => { + cancelAnimationFrame(rafId); + audioContext?.close(); + }; + }, [deviceId, volumeInput]); + + const barWidth = Math.max((volumeLevel / 70) * 100 - 35, 0); + + return ( +
+
+
100 && "bg-red-400")} + style={{ + width: `${barWidth > 100 ? 100 : barWidth}%`, + transition: "width 0.2s", + }} + /> +
+
+

+ Lautstärke sollte beim Sprechen in dem Grünen bereich bleiben +

+
+ ); +} diff --git a/apps/dispatch/app/_components/Settings.tsx b/apps/dispatch/app/_components/Settings.tsx new file mode 100644 index 00000000..ece681de --- /dev/null +++ b/apps/dispatch/app/_components/Settings.tsx @@ -0,0 +1,186 @@ +"use client"; +import { useEffect, useRef, useState } from "react"; +import { GearIcon } from "@radix-ui/react-icons"; +import { SettingsIcon, Volume2 } from "lucide-react"; +import MicVolumeBar from "_components/MicVolumeIndication"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { editUserAPI, getUserAPI } from "querys/user"; +import { Prisma } from "@repo/db"; +import { useSession } from "next-auth/react"; +import { useAudioStore } from "_store/audioStore"; +import toast from "react-hot-toast"; + +export const SettingsBtn = () => { + const session = useSession(); + const { data: user } = useQuery({ + queryKey: ["user", session.data?.user.id], + queryFn: () => getUserAPI(session.data!.user.id), + }); + + const editUserMutation = useMutation({ + mutationFn: ({ user }: { user: Prisma.UserUpdateInput }) => + editUserAPI(session.data!.user.id, user), + }); + + const modalRef = useRef(null); + + const [inputDevices, setInputDevices] = useState([]); + const [selectedDevice, setSelectedDevice] = useState( + user?.settingsMicDevice || null, + ); + const [showIndication, setShowInducation] = useState(false); + const [micVol, setMicVol] = useState(1); + + const setMic = useAudioStore((state) => state.setMic); + + useEffect(() => { + if (user?.settingsMicDevice) { + setSelectedDevice(user.settingsMicDevice); + setMic(user.settingsMicDevice, user.settingsMicVolume || 1); + } + }, [user, setMic]); + + useEffect(() => { + navigator.mediaDevices.enumerateDevices().then((devices) => { + setInputDevices(devices.filter((d) => d.kind === "audioinput")); + }); + }, []); + + return ( +
+ + + +
+

+ Einstellungen +

+
+
+ +
+

+ Microfonlautstärke +

+
+ { + const value = parseFloat(e.target.value); + setMicVol(value); + // Hier kannst du den Lautstärkewert verwenden + setShowInducation(true); + console.log("Lautstärke:", value); + }} + value={micVol} + className="range range-xs range-accent w-full" + /> +
+ 0 + 100 + 200 + 300 +
+
+ {showIndication && ( + + )} + {/* */} + {/* + {/* FÜGE HIER BITTE DEN MIKROFONAUSSCHLAG EIN WIE IN DER V1 */} +
+
+

+ Ausgabelautstärke +

+
+ +
+ 0 + 25 + 50 + 75 + 100 +
+
+ +
+ + +
+
+
+
+ ); +}; + +export const Settings = () => { + return ( +
+ +
+ ); +}; diff --git a/apps/dispatch/app/_components/map/ContextMenu.tsx b/apps/dispatch/app/_components/map/ContextMenu.tsx index 2948f566..04a2a3de 100644 --- a/apps/dispatch/app/_components/map/ContextMenu.tsx +++ b/apps/dispatch/app/_components/map/ContextMenu.tsx @@ -128,6 +128,8 @@ export const ContextMenu = () => { setMissionFormValues({ ...parsed, state: "draft", + addressLat: contextMenu.lat, + addressLng: contextMenu.lng, addressOSMways: [closestToContext], }); diff --git a/apps/dispatch/app/_components/map/MissionMarkers.tsx b/apps/dispatch/app/_components/map/MissionMarkers.tsx index bed7c4f2..41d7f140 100644 --- a/apps/dispatch/app/_components/map/MissionMarkers.tsx +++ b/apps/dispatch/app/_components/map/MissionMarkers.tsx @@ -46,7 +46,6 @@ const MissionPopupContent = ({ const handleTabChange = useCallback( (tab: "home" | "details" | "patient" | "log") => { - console.log("handleTabChange", tab); setMissionMarker({ open: [ { @@ -284,10 +283,6 @@ const MissionMarker = ({ mission }: { mission: Mission }) => { mission: Mission, anchor: "topleft" | "topright" | "bottomleft" | "bottomright", ) => { - console.log( - HPGValidationRequired(mission.missionStationIds, aircrafts, mission.hpgMissionString), - ); - const markerColor = needsAction ? MISSION_STATUS_COLORS["attention"] : MISSION_STATUS_COLORS[mission.state]; diff --git a/apps/dispatch/app/_components/map/_components/MissionMarkerTabs.tsx b/apps/dispatch/app/_components/map/_components/MissionMarkerTabs.tsx index 94b84a0e..615f5050 100644 --- a/apps/dispatch/app/_components/map/_components/MissionMarkerTabs.tsx +++ b/apps/dispatch/app/_components/map/_components/MissionMarkerTabs.tsx @@ -21,6 +21,7 @@ import { } from "lucide-react"; import { getPublicUser, + HpgState, HpgValidationState, Mission, MissionLog, @@ -37,6 +38,7 @@ import { getStationsAPI } from "querys/stations"; import { useDispatchConnectionStore } from "_store/dispatch/connectionStore"; import { HPGValidationRequired } from "helpers/hpgValidationRequired"; import { getOsmAddress } from "querys/osm"; +import { hpgStateToFMSStatus } from "helpers/hpgStateToFmsStatus"; const Einsatzdetails = ({ mission, @@ -161,10 +163,11 @@ const Einsatzdetails = ({ {mission.addressStreet}

+ {mission.addressZip && mission.addressCity ? ( `${mission.addressZip} ${mission.addressCity}` ) : ( - PLZ Ort nicht angegeben + PLZ / Ort nicht angegeben )}

@@ -366,8 +369,15 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => { const sendAlertMutation = useMutation({ mutationKey: ["missions"], - mutationFn: ({ id, stationId }: { id: number; stationId?: number }) => - sendMissionAPI(id, { stationId }), + mutationFn: ({ + id, + stationId, + vehicleName, + }: { + id: number; + stationId?: number; + vehicleName?: "ambulance" | "police" | "firebrigade"; + }) => sendMissionAPI(id, { stationId, vehicleName }), onError: (error) => { console.error(error); toast.error("Fehler beim Alarmieren"); @@ -379,6 +389,25 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => { const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected"; + const HPGVehicle = ({ state, name }: { state: HpgState; name: string }) => ( +
  • + + {hpgStateToFMSStatus(state)} + + + +
    + {name} +
    +
    +
  • + ); + return (
    @@ -435,6 +464,11 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => { ); })} + {mission.hpgAmbulanceState && } + {mission.hpgFireEngineState && ( + + )} + {mission.hpgPoliceState && } {dispatcherConnected && (
    @@ -475,7 +509,10 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => { className="btn btn-sm btn-primary btn-outline" onClick={async () => { if (typeof selectedStation === "string") { - toast.error("Fahrzeuge werden aktuell nicht unterstützt"); + await sendAlertMutation.mutate({ + id: mission.id, + vehicleName: selectedStation, + }); } else { if (!selectedStation?.id) return; await updateMissionMutation.mutateAsync({ @@ -520,7 +557,6 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => { queryClient.invalidateQueries({ queryKey: ["missions"] }); }, }); - console.log(mission.missionLog); if (!session.data?.user) return null; return ( diff --git a/apps/dispatch/app/_store/audioStore.ts b/apps/dispatch/app/_store/audioStore.ts index ff524c40..488e241c 100644 --- a/apps/dispatch/app/_store/audioStore.ts +++ b/apps/dispatch/app/_store/audioStore.ts @@ -1,6 +1,4 @@ import { PublicUser } from "@repo/db"; -import { usePilotConnectionStore } from "_store/pilot/connectionStore"; -import { channel } from "diagnostics_channel"; import { dispatchSocket } from "dispatch/socket"; import { serverApi } from "helpers/axios"; import { @@ -17,6 +15,8 @@ import { create } from "zustand"; let interval: NodeJS.Timeout; type TalkState = { + micDeviceId: string | null; + micVolume: number; isTalking: boolean; source: string; state: "connecting" | "connected" | "disconnected" | "error"; @@ -24,7 +24,7 @@ type TalkState = { connectionQuality: ConnectionQuality; remoteParticipants: number; toggleTalking: () => void; - + setMic: (micDeviceId: string | null, volume: number) => void; connect: (roomName: string) => void; disconnect: () => void; room: Room | null; @@ -38,15 +38,23 @@ const getToken = async (roomName: string) => { export const useAudioStore = create((set, get) => ({ isTalking: false, message: null, + micDeviceId: null, + micVolume: 1, state: "disconnected", source: "", remoteParticipants: 0, connectionQuality: ConnectionQuality.Unknown, room: null, + setMic: (micDeviceId, micVolume) => { + set({ micDeviceId, micVolume }); + }, toggleTalking: () => { - const { room, isTalking } = get(); + const { room, isTalking, micDeviceId, micVolume } = get(); if (!room) return; - room.localParticipant.setMicrophoneEnabled(!isTalking); + // Todo: use micVolume + room.localParticipant.setMicrophoneEnabled(!isTalking, { + deviceId: micDeviceId ?? undefined, + }); if (!isTalking) { // If old status was not talking, we need to emit the PTT event @@ -94,23 +102,19 @@ export const useAudioStore = create((set, get) => ({ handleDisconnect(); }) - .on(RoomEvent.ConnectionQualityChanged, (connectionQuality) => - set({ connectionQuality }), - ) + .on(RoomEvent.ConnectionQualityChanged, (connectionQuality) => set({ connectionQuality })) // Track events .on(RoomEvent.TrackSubscribed, handleTrackSubscribed) .on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed) .on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakerChange) .on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnpublished); - await room.connect(url, token); - console.log(room); + await room.connect(url, token, {}); set({ room }); interval = setInterval(() => { set({ - remoteParticipants: - room.numParticipants === 0 ? 0 : room.numParticipants - 1, // Unreliable and delayed + remoteParticipants: room.numParticipants === 0 ? 0 : room.numParticipants - 1, // Unreliable and delayed }); }, 500); } catch (error: Error | unknown) { @@ -150,11 +154,7 @@ const handlePTT = (data: PTTData) => { } }; -const handleOtherPTT = (data: { - publicUser: PublicUser; - channel: string; - source: string; -}) => { +const handleOtherPTT = (data: { publicUser: PublicUser; channel: string; source: string }) => { const currentChannel = useAudioStore.getState().room?.name; console.log("Other PTT", data); if (data.channel === currentChannel) diff --git a/apps/dispatch/app/_store/dispatch/connectionStore.ts b/apps/dispatch/app/_store/dispatch/connectionStore.ts index 2219971c..d189027a 100644 --- a/apps/dispatch/app/_store/dispatch/connectionStore.ts +++ b/apps/dispatch/app/_store/dispatch/connectionStore.ts @@ -1,19 +1,13 @@ import { create } from "zustand"; import { dispatchSocket } from "../../dispatch/socket"; -import toast from "react-hot-toast"; -import { HPGnotificationToast } from "_components/customToasts/HPGnotification"; -import { NotificationPayload } from "@repo/db"; +import { useAudioStore } from "_store/audioStore"; interface ConnectionStore { status: "connected" | "disconnected" | "connecting" | "error"; message: string; selectedZone: string; logoffTime: string; - connect: ( - uid: string, - selectedZone: string, - logoffTime: string, - ) => Promise; + connect: (uid: string, selectedZone: string, logoffTime: string) => Promise; disconnect: () => void; } @@ -40,7 +34,7 @@ export const useDispatchConnectionStore = create((set) => ({ dispatchSocket.on("connect", () => { const { logoffTime, selectedZone } = useDispatchConnectionStore.getState(); - + useAudioStore.getInitialState().connect("LST_01"); dispatchSocket.emit("connect-dispatch", { logoffTime, selectedZone, diff --git a/apps/dispatch/app/_store/pilot/connectionStore.ts b/apps/dispatch/app/_store/pilot/connectionStore.ts index 71ff874e..101fca7c 100644 --- a/apps/dispatch/app/_store/pilot/connectionStore.ts +++ b/apps/dispatch/app/_store/pilot/connectionStore.ts @@ -1,17 +1,10 @@ import { create } from "zustand"; import { dispatchSocket } from "../../dispatch/socket"; -import { - ConnectedAircraft, - Mission, - MissionSdsLog, - NotificationPayload, - Station, - User, -} from "@repo/db"; +import { ConnectedAircraft, Mission, MissionSdsLog, Station, User } from "@repo/db"; import { pilotSocket } from "pilot/socket"; import { useDmeStore } from "_store/pilot/dmeStore"; import { useMrtStore } from "_store/pilot/MrtStore"; -import toast from "react-hot-toast"; +import { useAudioStore } from "_store/audioStore"; interface ConnectionStore { status: "connected" | "disconnected" | "connecting" | "error"; @@ -71,6 +64,7 @@ pilotSocket.on("connect", () => { usePilotConnectionStore.setState({ status: "connected", message: "" }); const { logoffTime, selectedStation } = usePilotConnectionStore.getState(); dispatchSocket.disconnect(); + useAudioStore.getInitialState().connect("LST_01"); pilotSocket.emit("connect-pilot", { logoffTime, diff --git a/apps/dispatch/app/api/user/route.ts b/apps/dispatch/app/api/user/route.ts new file mode 100644 index 00000000..bfa8f323 --- /dev/null +++ b/apps/dispatch/app/api/user/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@repo/db"; + +export async function GET(req: NextRequest): Promise { + try { + const { searchParams } = new URL(req.url); + const id = searchParams.get("id"); + + if (!id) { + return NextResponse.json({ error: "User id is required" }, { status: 400 }); + } + + const user = await prisma.user.findUnique({ + where: { id: id }, + }); + + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + return NextResponse.json(user, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: "Failed to fetch user" }, { status: 500 }); + } +} + +export async function POST(req: NextRequest): Promise { + try { + const { searchParams } = new URL(req.url); + const id = searchParams.get("id"); + + if (!id) { + return NextResponse.json({ error: "User id is required" }, { status: 400 }); + } + + const body = await req.json(); + + const updatedUser = await prisma.user.update({ + where: { id: id }, + data: body, + }); + + return NextResponse.json(updatedUser, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: "Failed to update user" }, { status: 500 }); + } +} diff --git a/apps/dispatch/app/dispatch/_components/navbar/Navbar.tsx b/apps/dispatch/app/dispatch/_components/navbar/Navbar.tsx index bd3a30dc..a0ab4768 100644 --- a/apps/dispatch/app/dispatch/_components/navbar/Navbar.tsx +++ b/apps/dispatch/app/dispatch/_components/navbar/Navbar.tsx @@ -6,7 +6,7 @@ import { Audio } from "../../../_components/Audio"; /* import { useState } from "react"; */ import { ExitIcon, ExternalLinkIcon } from "@radix-ui/react-icons"; import Link from "next/link"; -import { SettingsBtn } from "./_components/Settings"; +import { Settings } from "_components/Settings"; export default function Navbar() { /* const [isDark, setIsDark] = useState(false); @@ -35,7 +35,7 @@ export default function Navbar() {
    {/* */}
    - + { - const modalRef = useRef(null); - - return ( -
    - - - -
    -

    - Einstellungen -

    -
    -
    - -
    - {/* FÜGE HIER BITTE DEN MIKROFONAUSSCHLAG EIN WIE IN DER V1 */} -
    -
    -

    - Ausgabelautstärke -

    -
    - -
    - 0 - 25 - 50 - 75 - 100 -
    -
    - -
    - - -
    -
    -
    -
    - ); -}; - -export const Settings = () => { - return ( -
    - -
    - ); -}; diff --git a/apps/dispatch/app/dispatch/_components/pannel/MissionForm.tsx b/apps/dispatch/app/dispatch/_components/pannel/MissionForm.tsx index 6580d89b..4d0133a6 100644 --- a/apps/dispatch/app/dispatch/_components/pannel/MissionForm.tsx +++ b/apps/dispatch/app/dispatch/_components/pannel/MissionForm.tsx @@ -4,7 +4,7 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { BellRing, BookmarkPlus } from "lucide-react"; import { Select } from "_components/Select"; -import { KEYWORD_CATEGORY, missionType, Prisma } from "@repo/db"; +import { KEYWORD_CATEGORY, Mission, missionType, Prisma } from "@repo/db"; import { MissionOptionalDefaults, MissionOptionalDefaultsSchema } from "@repo/db/zod"; import { usePannelStore } from "_store/pannelStore"; import { useSession } from "next-auth/react"; @@ -21,6 +21,8 @@ import { getStationsAPI } from "querys/stations"; import { useMapStore } from "_store/mapStore"; import { getConnectedAircraftsAPI } from "querys/aircrafts"; import { HPGValidationRequired } from "helpers/hpgValidationRequired"; +import { selectRandomHPGMissionSzenery } from "helpers/selectRandomHPGMission"; +import { AxiosError } from "axios"; export const MissionForm = () => { const { isEditingMission, editingMissionId, setEditingMission } = usePannelStore(); @@ -93,7 +95,8 @@ export const MissionForm = () => { hpgAmbulanceState: null, hpgPoliceState: null, hpgMissionString: null, - + hpgSelectedMissionString: null, + hpg: null, missionLog: [], }) as Partial, [session.data?.user.id], @@ -132,6 +135,47 @@ export const MissionForm = () => { } }, [missionFormValues, form, defaultFormValues]); + const saveMission = async ( + mission: MissionOptionalDefaults, + { alertWhenValid = false, createNewMission = false } = {}, + ) => { + const [hpgSzenario, hpgSzenarioCode] = mission.hpgMissionString?.split(":") || []; + const szenarioCode = selectRandomHPGMissionSzenery(hpgSzenarioCode || ""); + let newMission: Mission; + if (createNewMission) { + newMission = await createMissionMutation.mutateAsync({ + ...(mission as unknown as Prisma.MissionCreateInput), + missionAdditionalInfo: + !mission.missionAdditionalInfo.length && hpgSzenario + ? `HPG-Szenario: ${hpgSzenario}` + : mission.missionAdditionalInfo, + hpgSelectedMissionString: szenarioCode, + }); + if (validationRequired) { + await startHpgValidation(newMission.id, { + alertWhenValid, + }); + } + return newMission; + } else { + newMission = await editMissionMutation.mutateAsync({ + id: Number(editingMissionId), + mission: { + ...(mission as unknown as Prisma.MissionCreateInput), + missionAdditionalInfo: + !mission.missionAdditionalInfo.length && hpgSzenario + ? `HPG-Szenario: ${hpgSzenario}` + : mission.missionAdditionalInfo, + hpgSelectedMissionString: szenarioCode, + }, + }); + } + if (validationRequired) { + await startHpgValidation(newMission.id, {}); + } + return newMission; + }; + return (
    {/* Koorinaten Section */} @@ -266,7 +310,7 @@ export const MissionForm = () => { ))} - {/* TODO: Nur anzeigen wenn eine Station mit HPG ausgewählt ist */} +
    - - - - - - - - {/* FÜGE HIER BITTE DEN MIKROFONAUSSCHLAG EIN WIE IN DER V1 */} -
    -
    -

    - Ausgabelautstärke -

    -
    - -
    - 0 - 25 - 50 - 75 - 100 -
    -
    - -
    - - -
    -
    - -
    - ); -}; - -export const Settings = () => { - return ( -
    - -
    - ); -}; diff --git a/apps/dispatch/app/pilot/_components/navbar/Connection.tsx b/apps/dispatch/app/pilot/_components/navbar/_components/Connection.tsx similarity index 100% rename from apps/dispatch/app/pilot/_components/navbar/Connection.tsx rename to apps/dispatch/app/pilot/_components/navbar/_components/Connection.tsx diff --git a/apps/dispatch/app/pilot/_components/navbar/ThemeSwap.tsx b/apps/dispatch/app/pilot/_components/navbar/_components/ThemeSwap.tsx similarity index 100% rename from apps/dispatch/app/pilot/_components/navbar/ThemeSwap.tsx rename to apps/dispatch/app/pilot/_components/navbar/_components/ThemeSwap.tsx diff --git a/apps/dispatch/app/querys/missions.ts b/apps/dispatch/app/querys/missions.ts index a4cff3e1..ce8afe8d 100644 --- a/apps/dispatch/app/querys/missions.ts +++ b/apps/dispatch/app/querys/missions.ts @@ -19,21 +19,12 @@ export const createMissionAPI = async (mission: Prisma.MissionCreateInput) => { return response.data; }; -export const editMissionAPI = async ( - id: number, - mission: Prisma.MissionUpdateInput, -) => { +export const editMissionAPI = async (id: number, mission: Prisma.MissionUpdateInput) => { const respone = await serverApi.patch(`/mission/${id}`, mission); return respone.data; }; -export const sendSdsMessageAPI = async ( - id: number, - sdsMessage: MissionSdsLog, -) => { - const respone = await serverApi.post( - `/mission/${id}/send-sds`, - sdsMessage, - ); +export const sendSdsMessageAPI = async (id: number, sdsMessage: MissionSdsLog) => { + const respone = await serverApi.post(`/mission/${id}/send-sds`, sdsMessage); return respone.data; }; @@ -43,10 +34,7 @@ export const startHpgValidation = async ( alertWhenValid?: boolean; }, ) => { - const respone = await serverApi.post( - `/mission/${id}/validate-hpg`, - config, - ); + const respone = await serverApi.post(`/mission/${id}/validate-hpg`, config); return respone.data; }; @@ -54,14 +42,17 @@ export const sendMissionAPI = async ( id: number, { stationId, + vehicleName, }: { stationId?: number; + vehicleName?: "ambulance" | "police" | "firebrigade"; }, ) => { const respone = await serverApi.post<{ message: string; }>(`/mission/${id}/send-alert`, { stationId, + vehicleName, }); return respone.data; }; diff --git a/apps/dispatch/app/querys/user.ts b/apps/dispatch/app/querys/user.ts new file mode 100644 index 00000000..6e4cb8cf --- /dev/null +++ b/apps/dispatch/app/querys/user.ts @@ -0,0 +1,12 @@ +import { Prisma, User } from "@repo/db"; +import axios from "axios"; + +export const editUserAPI = async (id: string, user: Prisma.UserUpdateInput) => { + const response = await axios.post(`/api/user?id=${id}`, user); + return response.data; +}; + +export const getUserAPI = async (id: string) => { + const response = await axios.get(`/api/user?id=${id}`); + return response.data; +}; diff --git a/apps/dispatch/package.json b/apps/dispatch/package.json index aef088a6..e40d738c 100644 --- a/apps/dispatch/package.json +++ b/apps/dispatch/package.json @@ -13,6 +13,7 @@ "dependencies": { "@livekit/components-react": "^2.8.1", "@livekit/components-styles": "^1.1.4", + "@livekit/track-processors": "^0.5.6", "@radix-ui/react-icons": "^1.3.2", "@repo/db": "*", "@repo/ui": "*", diff --git a/package-lock.json b/package-lock.json index d0f37310..344c7976 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "dependencies": { "@livekit/components-react": "^2.8.1", "@livekit/components-styles": "^1.1.4", + "@livekit/track-processors": "^0.5.6", "@radix-ui/react-icons": "^1.3.2", "@repo/db": "*", "@repo/ui": "*", @@ -1908,6 +1909,24 @@ "@bufbuild/protobuf": "^1.10.0" } }, + "node_modules/@livekit/track-processors": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@livekit/track-processors/-/track-processors-0.5.6.tgz", + "integrity": "sha512-TlzObrSlp2PKor4VXqg6iefLRFVEb2T1lXwddBFdkPod60XVgYoMOj7V5xJm+UTE2MEtlE0003vUli9PyQGB1g==", + "license": "Apache-2.0", + "dependencies": { + "@mediapipe/tasks-vision": "0.10.14" + }, + "peerDependencies": { + "livekit-client": "^1.12.0 || ^2.1.0" + } + }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.14.tgz", + "integrity": "sha512-vOifgZhkndgybdvoRITzRkIueWWSiCKuEUXXK6Q4FaJsFvRJuwgg++vqFUMlL0Uox62U5aEXFhHxlhV7Ja5e3Q==", + "license": "Apache-2.0" + }, "node_modules/@next-auth/prisma-adapter": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@next-auth/prisma-adapter/-/prisma-adapter-1.0.7.tgz", diff --git a/packages/database/prisma/schema/mission.prisma b/packages/database/prisma/schema/mission.prisma index 845137c1..39c9a252 100644 --- a/packages/database/prisma/schema/mission.prisma +++ b/packages/database/prisma/schema/mission.prisma @@ -19,6 +19,7 @@ model Mission { missionStationUserIds String[] @default([]) missionLog Json[] @default([]) hpgMissionString String? + hpgSelectedMissionString String? hpgAmbulanceState HpgState? hpgFireEngineState HpgState? hpgPoliceState HpgState? @@ -73,9 +74,9 @@ enum MissionState { } enum HpgState { - ready - arrived - onway + NOT_REQUESTED + ON_SCENE + DISPATCHED } enum HpgValidationState { diff --git a/packages/database/prisma/schema/user.prisma b/packages/database/prisma/schema/user.prisma index a3455a84..b64d73c3 100644 --- a/packages/database/prisma/schema/user.prisma +++ b/packages/database/prisma/schema/user.prisma @@ -31,7 +31,9 @@ model User { emailVerified DateTime? @map(name: "email_verified") // Settings: - settingsNtfyRoom String? @map(name: "settings_ntfy_room") + settingsNtfyRoom String? @map(name: "settings_ntfy_room") + settingsMicDevice String? @map(name: "settings_mic_device") + settingsMicVolume Int? @map(name: "settings_mic_volume") image String? badges BADGES[] @default([])