diff --git a/apps/dispatch-server/routes/settings.ts b/apps/dispatch-server/routes/settings.ts new file mode 100644 index 00000000..e69de29b diff --git a/apps/dispatch/app/(app)/dispatch/_components/navbar/_components/Settings.tsx b/apps/dispatch/app/(app)/dispatch/_components/navbar/_components/Settings.tsx index c83288b8..d2a7402d 100644 --- a/apps/dispatch/app/(app)/dispatch/_components/navbar/_components/Settings.tsx +++ b/apps/dispatch/app/(app)/dispatch/_components/navbar/_components/Settings.tsx @@ -3,16 +3,17 @@ 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 { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { editUserAPI, getUserAPI } from "_querys/user"; import { useSession } from "next-auth/react"; import { useAudioStore } from "_store/audioStore"; import toast from "react-hot-toast"; import { useMapStore } from "_store/mapStore"; -import { set } from "date-fns"; +import { Button } from "@repo/shared-components"; export const SettingsBtn = () => { const session = useSession(); + const queryClient = useQueryClient(); const [inputDevices, setInputDevices] = useState([]); const { data: user } = useQuery({ @@ -23,6 +24,10 @@ export const SettingsBtn = () => { const editUserMutation = useMutation({ mutationFn: editUserAPI, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["user", session.data?.user.id] }); + }, + }); useEffect(() => { @@ -40,6 +45,7 @@ export const SettingsBtn = () => { micVolume: user?.settingsMicVolume || 1, radioVolume: user?.settingsRadioVolume || 0.8, autoCloseMapPopup: user?.settingsAutoCloseMapPopup || false, + useHPGAsDispatcher: user?.settingsUseHPGAsDispatcher || false, }); const { setSettings: setAudioSettings } = useAudioStore((state) => state); @@ -57,7 +63,8 @@ export const SettingsBtn = () => { micDeviceId: user.settingsMicDevice, micVolume: user.settingsMicVolume || 1, radioVolume: user.settingsRadioVolume || 0.8, - autoCloseMapPopup: user.settingsAutoCloseMapPopup || false, + autoCloseMapPopup: user.settingsAutoCloseMapPopup, + useHPGAsDispatcher: user.settingsUseHPGAsDispatcher, }); setUserSettings({ settingsAutoCloseMapPopup: user.settingsAutoCloseMapPopup || false, @@ -198,6 +205,17 @@ export const SettingsBtn = () => { /> Popups automatisch schließen +
+ { + setSettingsPartial({ useHPGAsDispatcher: e.target.checked }); + }} + /> + HPG als Disponent verwenden +
- +
diff --git a/apps/dispatch/app/(app)/dispatch/_components/pannel/MissionForm.tsx b/apps/dispatch/app/(app)/dispatch/_components/pannel/MissionForm.tsx index af1d8738..49279607 100644 --- a/apps/dispatch/app/(app)/dispatch/_components/pannel/MissionForm.tsx +++ b/apps/dispatch/app/(app)/dispatch/_components/pannel/MissionForm.tsx @@ -28,8 +28,11 @@ import { selectRandomHPGMissionSzenery } from "_helpers/selectRandomHPGMission"; import { AxiosError } from "axios"; import { cn } from "@repo/shared-components"; import { StationsSelect } from "(app)/dispatch/_components/StationSelect"; +import { getUserAPI } from "_querys/user"; export const MissionForm = () => { + const session = useSession(); + const { editingMissionId, setEditingMission } = usePannelStore(); const queryClient = useQueryClient(); const { setSearchElements, searchElements, setContextMenu } = useMapStore((s) => s); @@ -44,6 +47,10 @@ export const MissionForm = () => { queryFn: () => getConnectedAircraftsAPI(), refetchInterval: 10000, }); + const { data: user } = useQuery({ + queryKey: ["user", session.data?.user.id], + queryFn: () => getUserAPI(session.data!.user.id), + }); const createMissionMutation = useMutation({ mutationFn: createMissionAPI, @@ -81,7 +88,6 @@ export const MissionForm = () => { }, }); - const session = useSession(); const defaultFormValues = React.useMemo( () => ({ @@ -124,7 +130,9 @@ export const MissionForm = () => { form.watch("missionStationIds"), aircrafts, form.watch("hpgMissionString"), - ) && !form.watch("hpgMissionString")?.startsWith("kein Szenario"); + ) && + !form.watch("hpgMissionString")?.startsWith("kein Szenario") && + user?.settingsUseHPGAsDispatcher; useEffect(() => { if (session.data?.user.id) { diff --git a/apps/dispatch/app/(app)/pilot/_components/navbar/_components/Settings.tsx b/apps/dispatch/app/(app)/pilot/_components/navbar/_components/Settings.tsx index 9d1f23d8..d2d7ffd7 100644 --- a/apps/dispatch/app/(app)/pilot/_components/navbar/_components/Settings.tsx +++ b/apps/dispatch/app/(app)/pilot/_components/navbar/_components/Settings.tsx @@ -3,15 +3,17 @@ import { useEffect, useRef, useState } from "react"; import { GearIcon } from "@radix-ui/react-icons"; import { Bell, SettingsIcon, Volume2 } from "lucide-react"; 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 { useSession } from "next-auth/react"; import { useAudioStore } from "_store/audioStore"; import toast from "react-hot-toast"; import Link from "next/link"; +import { Button } from "@repo/shared-components"; export const SettingsBtn = () => { const session = useSession(); + const queryClient = useQueryClient(); const [inputDevices, setInputDevices] = useState([]); const { data: user } = useQuery({ @@ -22,6 +24,10 @@ export const SettingsBtn = () => { const editUserMutation = useMutation({ mutationFn: editUserAPI, + mutationKey: ["user", session.data?.user.id], + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["user", session.data?.user.id] }); + }, }); useEffect(() => { @@ -248,7 +254,7 @@ export const SettingsBtn = () => { > Schließen - + diff --git a/apps/dispatch/app/(app)/pilot/page.tsx b/apps/dispatch/app/(app)/pilot/page.tsx index 87322e21..5ec04be4 100644 --- a/apps/dispatch/app/(app)/pilot/page.tsx +++ b/apps/dispatch/app/(app)/pilot/page.tsx @@ -23,7 +23,7 @@ const Map = dynamic(() => import("_components/map/Map"), { }); const PilotPage = () => { - const { connectedAircraft, status } = usePilotConnectionStore((state) => state); + const { connectedAircraft, status, } = usePilotConnectionStore((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 const { data: aircrafts } = useQuery({ diff --git a/apps/dispatch/app/(auth)/logout/page.tsx b/apps/dispatch/app/(auth)/logout/page.tsx index 8090a370..5732aff0 100644 --- a/apps/dispatch/app/(auth)/logout/page.tsx +++ b/apps/dispatch/app/(auth)/logout/page.tsx @@ -10,7 +10,7 @@ export default () => { }, []); return (
-

logging out...

+

ausloggen...

); }; diff --git a/apps/dispatch/app/_components/QueryProvider.tsx b/apps/dispatch/app/_components/QueryProvider.tsx index 41bb2ce1..d5b2799a 100644 --- a/apps/dispatch/app/_components/QueryProvider.tsx +++ b/apps/dispatch/app/_components/QueryProvider.tsx @@ -3,7 +3,7 @@ import { toast } from "react-hot-toast"; 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 { NotificationPayload } from "@repo/db"; import { HPGnotificationToast } from "_components/customToasts/HPGnotification"; @@ -15,6 +15,7 @@ import { MissionAutoCloseToast } from "_components/customToasts/MissionAutoClose export function QueryProvider({ children }: { children: ReactNode }) { const mapStore = useMapStore((s) => s); + const notificationSound = useRef(null); const [queryClient] = useState( () => @@ -30,6 +31,9 @@ export function QueryProvider({ children }: { children: ReactNode }) { }, }), ); + useEffect(() => { + notificationSound.current = new Audio("/sounds/notification.mp3"); + }, []); useEffect(() => { const invalidateMission = () => { queryClient.invalidateQueries({ @@ -59,8 +63,18 @@ export function QueryProvider({ children }: { children: ReactNode }) { }; 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) { case "hpg-validation": + playNotificationSound(); toast.custom( (t) => , { @@ -70,6 +84,7 @@ export function QueryProvider({ children }: { children: ReactNode }) { break; case "admin-message": + playNotificationSound(); toast.custom((t) => , { duration: 999999, }); @@ -81,6 +96,7 @@ export function QueryProvider({ children }: { children: ReactNode }) { }); break; case "mission-auto-close": + playNotificationSound(); toast.custom( (t) => , { @@ -90,6 +106,7 @@ export function QueryProvider({ children }: { children: ReactNode }) { break; case "mission-closed": toast("Dein aktueller Einsatz wurde geschlossen."); + break; default: toast("unbekanntes Notification-Event"); diff --git a/apps/dispatch/app/_components/customToasts/HPGnotValidated.tsx b/apps/dispatch/app/_components/customToasts/HPGnotValidated.tsx new file mode 100644 index 00000000..4b6e6e4e --- /dev/null +++ b/apps/dispatch/app/_components/customToasts/HPGnotValidated.tsx @@ -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 } className="flex flex-row"> +
+

Einsatz nicht HPG-validiert

+

Vergleiche die Position des Einsatzes mit der HPG-Position in Hubschrauber

+
+
+ +
+
+} + +export const showToast = () => { + toast.custom((t) => { + return (); + }, {duration: 1000 * 60 * 10}); // 10 minutes +} \ No newline at end of file diff --git a/apps/dispatch/app/_querys/dispatcher.ts b/apps/dispatch/app/_querys/dispatcher.ts index fda3ed36..16ae94e0 100644 --- a/apps/dispatch/app/_querys/dispatcher.ts +++ b/apps/dispatch/app/_querys/dispatcher.ts @@ -14,11 +14,14 @@ export const changeDispatcherAPI = async ( }; export const getConnectedDispatcherAPI = async (filter?: Prisma.ConnectedDispatcherWhereInput) => { - const res = await axios.get("/api/dispatcher", { - params: { - filter: JSON.stringify(filter), + const res = await axios.get<(ConnectedDispatcher & { settingsUseHPGAsDispatcher: boolean })[]>( + "/api/dispatcher", + { + params: { + filter: JSON.stringify(filter), + }, }, - }); + ); if (res.status !== 200) { throw new Error("Failed to fetch Connected Dispatcher"); } diff --git a/apps/dispatch/app/_store/pilot/connectionStore.ts b/apps/dispatch/app/_store/pilot/connectionStore.ts index c7acd60c..e9fc29f8 100644 --- a/apps/dispatch/app/_store/pilot/connectionStore.ts +++ b/apps/dispatch/app/_store/pilot/connectionStore.ts @@ -1,6 +1,7 @@ import { create } from "zustand"; import { dispatchSocket } from "../../(app)/dispatch/socket"; import { ConnectedAircraft, Mission, MissionSdsLog, Station, User } from "@repo/db"; +import { showToast } from "../../_components/customToasts/HPGnotValidated"; import { pilotSocket } from "(app)/pilot/socket"; import { useDmeStore } from "_store/pilot/dmeStore"; import { useMrtStore } from "_store/pilot/MrtStore"; @@ -132,6 +133,12 @@ pilotSocket.on("mission-alert", (data: Mission & { Stations: Station[] }) => { useDmeStore.getState().setPage({ page: "new-mission", }); + if ( + data.hpgValidationState === "NOT_VALIDATED" && + usePilotConnectionStore.getState().connectedAircraft?.posH145active + ) { + showToast(); + } }); pilotSocket.on("sds-message", (sdsMessage: MissionSdsLog) => { diff --git a/apps/dispatch/app/api/dispatcher/route.ts b/apps/dispatch/app/api/dispatcher/route.ts index 63761deb..7fd5f069 100644 --- a/apps/dispatch/app/api/dispatcher/route.ts +++ b/apps/dispatch/app/api/dispatcher/route.ts @@ -24,6 +24,7 @@ export async function GET(request: Request): Promise { ...d, user: undefined, publicUser: getPublicUser(d.user), + settingsUseHPGAsDispatcher: d.user.settingsUseHPGAsDispatcher, }; }), { diff --git a/apps/dispatch/app/tracker/_components/ConnectedDispatcher.tsx b/apps/dispatch/app/tracker/_components/ConnectedDispatcher.tsx index 423c6f2d..9850db50 100644 --- a/apps/dispatch/app/tracker/_components/ConnectedDispatcher.tsx +++ b/apps/dispatch/app/tracker/_components/ConnectedDispatcher.tsx @@ -78,6 +78,15 @@ export const ConnectedDispatcher = () => {
{asPublicUser(d.publicUser).fullName}
{d.zone}
+
+ {d.settingsUseHPGAsDispatcher ? ( + HPG aktiv + ) : ( + + HPG deaktiviert + + )} +
{(() => { const badges = (d.publicUser as unknown as PublicUser).badges diff --git a/apps/dispatch/public/sounds/notification.mp3 b/apps/dispatch/public/sounds/notification.mp3 new file mode 100644 index 00000000..57f5dddc Binary files /dev/null and b/apps/dispatch/public/sounds/notification.mp3 differ diff --git a/apps/hub/app/(auth)/logout/page.tsx b/apps/hub/app/(auth)/logout/page.tsx index 8214e355..5732aff0 100644 --- a/apps/hub/app/(auth)/logout/page.tsx +++ b/apps/hub/app/(auth)/logout/page.tsx @@ -1,16 +1,16 @@ -'use client'; -import { signOut } from 'next-auth/react'; -import { useEffect } from 'react'; +"use client"; +import { signOut } from "next-auth/react"; +import { useEffect } from "react"; export default () => { - useEffect(() => { - signOut({ - callbackUrl: '/login', - }); - }, []); - return ( -
-

logging out...

-
- ); + useEffect(() => { + signOut({ + callbackUrl: "/login", + }); + }, []); + return ( +
+

ausloggen...

+
+ ); }; diff --git a/packages/database/prisma/schema/migrations/20251118221802_hpg_settings_as_dispatcher/migration.sql b/packages/database/prisma/schema/migrations/20251118221802_hpg_settings_as_dispatcher/migration.sql new file mode 100644 index 00000000..236c8279 --- /dev/null +++ b/packages/database/prisma/schema/migrations/20251118221802_hpg_settings_as_dispatcher/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "settings_use_hpg_as_dispatcher" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/database/prisma/schema/migrations/20251118222421_rename_hpg_settings/migration.sql b/packages/database/prisma/schema/migrations/20251118222421_rename_hpg_settings/migration.sql new file mode 100644 index 00000000..8bc8be66 --- /dev/null +++ b/packages/database/prisma/schema/migrations/20251118222421_rename_hpg_settings/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ALTER COLUMN "settings_use_hpg_as_dispatcher" SET DEFAULT true; diff --git a/packages/database/prisma/schema/user.prisma b/packages/database/prisma/schema/user.prisma index 5fc59e06..1f09f3f0 100644 --- a/packages/database/prisma/schema/user.prisma +++ b/packages/database/prisma/schema/user.prisma @@ -38,15 +38,16 @@ model User { changelogAck Boolean @default(false) // Settings: - pathSelected Boolean @default(false) - migratedFromV1 Boolean @default(false) - settingsNtfyRoom String? @map(name: "settings_ntfy_room") - settingsMicDevice String? @map(name: "settings_mic_device") - settingsMicVolume Float? @map(name: "settings_mic_volume") - settingsDmeVolume Float? @map(name: "settings_dme_volume") - settingsRadioVolume Float? @map(name: "settings_funk_volume") - settingsHideLastname Boolean @default(false) @map(name: "settings_hide_lastname") - settingsAutoCloseMapPopup Boolean @default(false) @map(name: "settings_auto_close_map_popup") + pathSelected Boolean @default(false) + migratedFromV1 Boolean @default(false) + settingsNtfyRoom String? @map(name: "settings_ntfy_room") + settingsMicDevice String? @map(name: "settings_mic_device") + settingsMicVolume Float? @map(name: "settings_mic_volume") + settingsDmeVolume Float? @map(name: "settings_dme_volume") + settingsRadioVolume Float? @map(name: "settings_funk_volume") + settingsHideLastname Boolean @default(false) @map(name: "settings_hide_lastname") + settingsAutoCloseMapPopup Boolean @default(false) @map(name: "settings_auto_close_map_popup") + settingsUseHPGAsDispatcher Boolean @default(true) @map(name: "settings_use_hpg_as_dispatcher") // email Verification: emailVerificationToken String? @map(name: "email_verification_token")