Dispo-Option, die HPG validierung nicht zu nutzen

This commit is contained in:
PxlLoewe
2025-11-27 22:21:27 +01:00
parent 6a739f4871
commit b9e871ae01
17 changed files with 138 additions and 39 deletions

View File

View File

@@ -3,16 +3,17 @@ import { useEffect, useRef, useState } from "react";
import { GearIcon } from "@radix-ui/react-icons"; import { GearIcon } from "@radix-ui/react-icons";
import { SettingsIcon, Volume2 } from "lucide-react"; import { SettingsIcon, Volume2 } from "lucide-react";
import MicVolumeBar from "_components/MicVolumeIndication"; import MicVolumeBar from "_components/MicVolumeIndication";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { editUserAPI, getUserAPI } from "_querys/user"; import { editUserAPI, getUserAPI } from "_querys/user";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { useAudioStore } from "_store/audioStore"; import { useAudioStore } from "_store/audioStore";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useMapStore } from "_store/mapStore"; import { useMapStore } from "_store/mapStore";
import { set } from "date-fns"; import { Button } from "@repo/shared-components";
export const SettingsBtn = () => { export const SettingsBtn = () => {
const session = useSession(); const session = useSession();
const queryClient = useQueryClient();
const [inputDevices, setInputDevices] = useState<MediaDeviceInfo[]>([]); const [inputDevices, setInputDevices] = useState<MediaDeviceInfo[]>([]);
const { data: user } = useQuery({ const { data: user } = useQuery({
@@ -23,6 +24,10 @@ export const SettingsBtn = () => {
const editUserMutation = useMutation({ const editUserMutation = useMutation({
mutationFn: editUserAPI, mutationFn: editUserAPI,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["user", session.data?.user.id] });
},
}); });
useEffect(() => { useEffect(() => {
@@ -40,6 +45,7 @@ export const SettingsBtn = () => {
micVolume: user?.settingsMicVolume || 1, micVolume: user?.settingsMicVolume || 1,
radioVolume: user?.settingsRadioVolume || 0.8, radioVolume: user?.settingsRadioVolume || 0.8,
autoCloseMapPopup: user?.settingsAutoCloseMapPopup || false, autoCloseMapPopup: user?.settingsAutoCloseMapPopup || false,
useHPGAsDispatcher: user?.settingsUseHPGAsDispatcher || false,
}); });
const { setSettings: setAudioSettings } = useAudioStore((state) => state); const { setSettings: setAudioSettings } = useAudioStore((state) => state);
@@ -57,7 +63,8 @@ export const SettingsBtn = () => {
micDeviceId: user.settingsMicDevice, micDeviceId: user.settingsMicDevice,
micVolume: user.settingsMicVolume || 1, micVolume: user.settingsMicVolume || 1,
radioVolume: user.settingsRadioVolume || 0.8, radioVolume: user.settingsRadioVolume || 0.8,
autoCloseMapPopup: user.settingsAutoCloseMapPopup || false, autoCloseMapPopup: user.settingsAutoCloseMapPopup,
useHPGAsDispatcher: user.settingsUseHPGAsDispatcher,
}); });
setUserSettings({ setUserSettings({
settingsAutoCloseMapPopup: user.settingsAutoCloseMapPopup || false, settingsAutoCloseMapPopup: user.settingsAutoCloseMapPopup || false,
@@ -198,6 +205,17 @@ export const SettingsBtn = () => {
/> />
Popups automatisch schließen Popups automatisch schließen
</div> </div>
<div className="mt-2 flex w-full items-center gap-2">
<input
type="checkbox"
className="toggle"
checked={settings.useHPGAsDispatcher}
onChange={(e) => {
setSettingsPartial({ useHPGAsDispatcher: e.target.checked });
}}
/>
HPG als Disponent verwenden
</div>
<div className="modal-action flex justify-between"> <div className="modal-action flex justify-between">
<button <button
@@ -211,7 +229,7 @@ export const SettingsBtn = () => {
> >
Schließen Schließen
</button> </button>
<button <Button
className="btn btn-soft btn-success" className="btn btn-soft btn-success"
type="submit" type="submit"
onSubmit={() => false} onSubmit={() => false}
@@ -224,6 +242,7 @@ export const SettingsBtn = () => {
settingsMicVolume: settings.micVolume, settingsMicVolume: settings.micVolume,
settingsRadioVolume: settings.radioVolume, settingsRadioVolume: settings.radioVolume,
settingsAutoCloseMapPopup: settings.autoCloseMapPopup, settingsAutoCloseMapPopup: settings.autoCloseMapPopup,
settingsUseHPGAsDispatcher: settings.useHPGAsDispatcher,
}, },
}); });
setAudioSettings({ setAudioSettings({
@@ -239,7 +258,7 @@ export const SettingsBtn = () => {
}} }}
> >
Speichern Speichern
</button> </Button>
</div> </div>
</div> </div>
</dialog> </dialog>

View File

@@ -28,8 +28,11 @@ import { selectRandomHPGMissionSzenery } from "_helpers/selectRandomHPGMission";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { cn } from "@repo/shared-components"; import { cn } from "@repo/shared-components";
import { StationsSelect } from "(app)/dispatch/_components/StationSelect"; import { StationsSelect } from "(app)/dispatch/_components/StationSelect";
import { getUserAPI } from "_querys/user";
export const MissionForm = () => { export const MissionForm = () => {
const session = useSession();
const { editingMissionId, setEditingMission } = usePannelStore(); const { editingMissionId, setEditingMission } = usePannelStore();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { setSearchElements, searchElements, setContextMenu } = useMapStore((s) => s); const { setSearchElements, searchElements, setContextMenu } = useMapStore((s) => s);
@@ -44,6 +47,10 @@ export const MissionForm = () => {
queryFn: () => getConnectedAircraftsAPI(), queryFn: () => getConnectedAircraftsAPI(),
refetchInterval: 10000, refetchInterval: 10000,
}); });
const { data: user } = useQuery({
queryKey: ["user", session.data?.user.id],
queryFn: () => getUserAPI(session.data!.user.id),
});
const createMissionMutation = useMutation({ const createMissionMutation = useMutation({
mutationFn: createMissionAPI, mutationFn: createMissionAPI,
@@ -81,7 +88,6 @@ export const MissionForm = () => {
}, },
}); });
const session = useSession();
const defaultFormValues = React.useMemo( const defaultFormValues = React.useMemo(
() => () =>
({ ({
@@ -124,7 +130,9 @@ export const MissionForm = () => {
form.watch("missionStationIds"), form.watch("missionStationIds"),
aircrafts, aircrafts,
form.watch("hpgMissionString"), form.watch("hpgMissionString"),
) && !form.watch("hpgMissionString")?.startsWith("kein Szenario"); ) &&
!form.watch("hpgMissionString")?.startsWith("kein Szenario") &&
user?.settingsUseHPGAsDispatcher;
useEffect(() => { useEffect(() => {
if (session.data?.user.id) { if (session.data?.user.id) {

View File

@@ -3,15 +3,17 @@ import { useEffect, useRef, useState } from "react";
import { GearIcon } from "@radix-ui/react-icons"; import { GearIcon } from "@radix-ui/react-icons";
import { Bell, SettingsIcon, Volume2 } from "lucide-react"; import { Bell, SettingsIcon, Volume2 } from "lucide-react";
import MicVolumeBar from "_components/MicVolumeIndication"; import MicVolumeBar from "_components/MicVolumeIndication";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { editUserAPI, getUserAPI } from "_querys/user"; import { editUserAPI, getUserAPI } from "_querys/user";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { useAudioStore } from "_store/audioStore"; import { useAudioStore } from "_store/audioStore";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import Link from "next/link"; import Link from "next/link";
import { Button } from "@repo/shared-components";
export const SettingsBtn = () => { export const SettingsBtn = () => {
const session = useSession(); const session = useSession();
const queryClient = useQueryClient();
const [inputDevices, setInputDevices] = useState<MediaDeviceInfo[]>([]); const [inputDevices, setInputDevices] = useState<MediaDeviceInfo[]>([]);
const { data: user } = useQuery({ const { data: user } = useQuery({
@@ -22,6 +24,10 @@ export const SettingsBtn = () => {
const editUserMutation = useMutation({ const editUserMutation = useMutation({
mutationFn: editUserAPI, mutationFn: editUserAPI,
mutationKey: ["user", session.data?.user.id],
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["user", session.data?.user.id] });
},
}); });
useEffect(() => { useEffect(() => {
@@ -248,7 +254,7 @@ export const SettingsBtn = () => {
> >
Schließen Schließen
</button> </button>
<button <Button
className="btn btn-soft btn-success" className="btn btn-soft btn-success"
type="submit" type="submit"
onSubmit={() => false} onSubmit={() => false}
@@ -275,7 +281,7 @@ export const SettingsBtn = () => {
}} }}
> >
Speichern Speichern
</button> </Button>
</div> </div>
</div> </div>
</dialog> </dialog>

View File

@@ -23,7 +23,7 @@ const Map = dynamic(() => import("_components/map/Map"), {
}); });
const PilotPage = () => { const PilotPage = () => {
const { connectedAircraft, status } = usePilotConnectionStore((state) => state); const { connectedAircraft, status, } = usePilotConnectionStore((state) => state);
const { latestMission } = useDmeStore((state) => state); const { latestMission } = useDmeStore((state) => state);
// Query will be cached anyway, due to this, displayed Markers are in sync with own Aircraft connection-warning // Query will be cached anyway, due to this, displayed Markers are in sync with own Aircraft connection-warning
const { data: aircrafts } = useQuery({ const { data: aircrafts } = useQuery({

View File

@@ -10,7 +10,7 @@ export default () => {
}, []); }, []);
return ( return (
<div className="card-body"> <div className="card-body">
<h1 className="text-5xl">logging out...</h1> <h1 className="text-5xl">ausloggen...</h1>
</div> </div>
); );
}; };

View File

@@ -3,7 +3,7 @@
import { toast } from "react-hot-toast"; 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, useRef, useState } from "react";
import { dispatchSocket } from "(app)/dispatch/socket"; import { dispatchSocket } from "(app)/dispatch/socket";
import { NotificationPayload } from "@repo/db"; import { NotificationPayload } from "@repo/db";
import { HPGnotificationToast } from "_components/customToasts/HPGnotification"; import { HPGnotificationToast } from "_components/customToasts/HPGnotification";
@@ -15,6 +15,7 @@ import { MissionAutoCloseToast } from "_components/customToasts/MissionAutoClose
export function QueryProvider({ children }: { children: ReactNode }) { export function QueryProvider({ children }: { children: ReactNode }) {
const mapStore = useMapStore((s) => s); const mapStore = useMapStore((s) => s);
const notificationSound = useRef<HTMLAudioElement | null>(null);
const [queryClient] = useState( const [queryClient] = useState(
() => () =>
@@ -30,6 +31,9 @@ export function QueryProvider({ children }: { children: ReactNode }) {
}, },
}), }),
); );
useEffect(() => {
notificationSound.current = new Audio("/sounds/notification.mp3");
}, []);
useEffect(() => { useEffect(() => {
const invalidateMission = () => { const invalidateMission = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
@@ -59,8 +63,18 @@ export function QueryProvider({ children }: { children: ReactNode }) {
}; };
const handleNotification = (notification: NotificationPayload) => { const handleNotification = (notification: NotificationPayload) => {
const playNotificationSound = () => {
if (notificationSound.current) {
notificationSound.current.currentTime = 0;
notificationSound.current
.play()
.catch((e) => console.error("Notification sound error:", e));
}
}
switch (notification.type) { switch (notification.type) {
case "hpg-validation": case "hpg-validation":
playNotificationSound();
toast.custom( toast.custom(
(t) => <HPGnotificationToast event={notification} mapStore={mapStore} t={t} />, (t) => <HPGnotificationToast event={notification} mapStore={mapStore} t={t} />,
{ {
@@ -70,6 +84,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
break; break;
case "admin-message": case "admin-message":
playNotificationSound();
toast.custom((t) => <AdminMessageToast event={notification} t={t} />, { toast.custom((t) => <AdminMessageToast event={notification} t={t} />, {
duration: 999999, duration: 999999,
}); });
@@ -81,6 +96,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
}); });
break; break;
case "mission-auto-close": case "mission-auto-close":
playNotificationSound();
toast.custom( toast.custom(
(t) => <MissionAutoCloseToast event={notification} t={t} mapStore={mapStore} />, (t) => <MissionAutoCloseToast event={notification} t={t} mapStore={mapStore} />,
{ {
@@ -90,6 +106,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
break; break;
case "mission-closed": case "mission-closed":
toast("Dein aktueller Einsatz wurde geschlossen."); toast("Dein aktueller Einsatz wurde geschlossen.");
break; break;
default: default:
toast("unbekanntes Notification-Event"); toast("unbekanntes Notification-Event");

View File

@@ -0,0 +1,24 @@
import { BaseNotification } from "_components/customToasts/BaseNotification"
import { TriangleAlert } from "lucide-react"
import toast, { Toast } from "react-hot-toast"
export const HPGnotValidatedToast = ({_toast}: {_toast: Toast}) => {
return <BaseNotification icon={<TriangleAlert />} className="flex flex-row">
<div className="flex-1">
<h1 className="font-bold text-red-600">Einsatz nicht HPG-validiert</h1>
<p className="text-sm">Vergleiche die Position des Einsatzes mit der HPG-Position in Hubschrauber</p>
</div>
<div className="ml-11">
<button className="btn" onClick={() => toast.dismiss(_toast.id)}>
schließen
</button>
</div>
</BaseNotification>
}
export const showToast = () => {
toast.custom((t) => {
return (<HPGnotValidatedToast _toast={t} />);
}, {duration: 1000 * 60 * 10}); // 10 minutes
}

View File

@@ -14,11 +14,14 @@ export const changeDispatcherAPI = async (
}; };
export const getConnectedDispatcherAPI = async (filter?: Prisma.ConnectedDispatcherWhereInput) => { export const getConnectedDispatcherAPI = async (filter?: Prisma.ConnectedDispatcherWhereInput) => {
const res = await axios.get<ConnectedDispatcher[]>("/api/dispatcher", { const res = await axios.get<(ConnectedDispatcher & { settingsUseHPGAsDispatcher: boolean })[]>(
"/api/dispatcher",
{
params: { params: {
filter: JSON.stringify(filter), filter: JSON.stringify(filter),
}, },
}); },
);
if (res.status !== 200) { if (res.status !== 200) {
throw new Error("Failed to fetch Connected Dispatcher"); throw new Error("Failed to fetch Connected Dispatcher");
} }

View File

@@ -1,6 +1,7 @@
import { create } from "zustand"; import { create } from "zustand";
import { dispatchSocket } from "../../(app)/dispatch/socket"; import { dispatchSocket } from "../../(app)/dispatch/socket";
import { ConnectedAircraft, Mission, MissionSdsLog, Station, User } from "@repo/db"; import { ConnectedAircraft, Mission, MissionSdsLog, Station, User } from "@repo/db";
import { showToast } from "../../_components/customToasts/HPGnotValidated";
import { pilotSocket } from "(app)/pilot/socket"; import { pilotSocket } from "(app)/pilot/socket";
import { useDmeStore } from "_store/pilot/dmeStore"; import { useDmeStore } from "_store/pilot/dmeStore";
import { useMrtStore } from "_store/pilot/MrtStore"; import { useMrtStore } from "_store/pilot/MrtStore";
@@ -132,6 +133,12 @@ pilotSocket.on("mission-alert", (data: Mission & { Stations: Station[] }) => {
useDmeStore.getState().setPage({ useDmeStore.getState().setPage({
page: "new-mission", page: "new-mission",
}); });
if (
data.hpgValidationState === "NOT_VALIDATED" &&
usePilotConnectionStore.getState().connectedAircraft?.posH145active
) {
showToast();
}
}); });
pilotSocket.on("sds-message", (sdsMessage: MissionSdsLog) => { pilotSocket.on("sds-message", (sdsMessage: MissionSdsLog) => {

View File

@@ -24,6 +24,7 @@ export async function GET(request: Request): Promise<NextResponse> {
...d, ...d,
user: undefined, user: undefined,
publicUser: getPublicUser(d.user), publicUser: getPublicUser(d.user),
settingsUseHPGAsDispatcher: d.user.settingsUseHPGAsDispatcher,
}; };
}), }),
{ {

View File

@@ -78,6 +78,15 @@ export const ConnectedDispatcher = () => {
<div>{asPublicUser(d.publicUser).fullName}</div> <div>{asPublicUser(d.publicUser).fullName}</div>
<div className="text-xs font-semibold uppercase opacity-60">{d.zone}</div> <div className="text-xs font-semibold uppercase opacity-60">{d.zone}</div>
</div> </div>
<div className="mr-2 flex flex-col justify-center">
{d.settingsUseHPGAsDispatcher ? (
<span className="badge badge-sm badge-success badge-outline">HPG aktiv</span>
) : (
<span className="badge badge-sm badge-info badge-outline">
HPG deaktiviert
</span>
)}
</div>
<div> <div>
{(() => { {(() => {
const badges = (d.publicUser as unknown as PublicUser).badges const badges = (d.publicUser as unknown as PublicUser).badges

Binary file not shown.

View File

@@ -1,16 +1,16 @@
'use client'; "use client";
import { signOut } from 'next-auth/react'; import { signOut } from "next-auth/react";
import { useEffect } from 'react'; import { useEffect } from "react";
export default () => { export default () => {
useEffect(() => { useEffect(() => {
signOut({ signOut({
callbackUrl: '/login', callbackUrl: "/login",
}); });
}, []); }, []);
return ( return (
<div className="card-body"> <div className="card-body">
<h1 className="text-5xl">logging out...</h1> <h1 className="text-5xl">ausloggen...</h1>
</div> </div>
); );
}; };

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "settings_use_hpg_as_dispatcher" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ALTER COLUMN "settings_use_hpg_as_dispatcher" SET DEFAULT true;

View File

@@ -47,6 +47,7 @@ model User {
settingsRadioVolume Float? @map(name: "settings_funk_volume") settingsRadioVolume Float? @map(name: "settings_funk_volume")
settingsHideLastname Boolean @default(false) @map(name: "settings_hide_lastname") settingsHideLastname Boolean @default(false) @map(name: "settings_hide_lastname")
settingsAutoCloseMapPopup Boolean @default(false) @map(name: "settings_auto_close_map_popup") settingsAutoCloseMapPopup Boolean @default(false) @map(name: "settings_auto_close_map_popup")
settingsUseHPGAsDispatcher Boolean @default(true) @map(name: "settings_use_hpg_as_dispatcher")
// email Verification: // email Verification:
emailVerificationToken String? @map(name: "email_verification_token") emailVerificationToken String? @map(name: "email_verification_token")