Dispatch Router-Struktur; AddPenalty Layout gefixed
@@ -1,6 +1,6 @@
|
|||||||
import { Connection } from "./_components/Connection";
|
import { Connection } from "./_components/Connection";
|
||||||
/* import { ThemeSwap } from "./_components/ThemeSwap"; */
|
/* import { ThemeSwap } from "./_components/ThemeSwap"; */
|
||||||
import { Audio } from "../../../_components/Audio/Audio";
|
import { Audio } from "../../../../_components/Audio/Audio";
|
||||||
/* import { useState } from "react"; */
|
/* import { useState } from "react"; */
|
||||||
import { ExitIcon, ExternalLinkIcon } from "@radix-ui/react-icons";
|
import { ExitIcon, ExternalLinkIcon } from "@radix-ui/react-icons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useDispatchConnectionStore } from "../../../../_store/dispatch/connectionStore";
|
import { useDispatchConnectionStore } from "../../../../../_store/dispatch/connectionStore";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
27
apps/dispatch/app/(app)/dispatch/layout.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Navbar from "./_components/navbar/Navbar";
|
||||||
|
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||||
|
import { Error } from "_components/Error";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "VAR: Disponent",
|
||||||
|
description: "Die neue VAR Leitstelle.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
const session = await getServerSession();
|
||||||
|
|
||||||
|
if (!session?.user.permissions.includes("DISPO"))
|
||||||
|
return <Error title="Zugriff verweigert" statusCode={403} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navbar />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Pannel } from "dispatch/_components/pannel/Pannel";
|
import { Pannel } from "(app)/dispatch/_components/pannel/Pannel";
|
||||||
import { usePannelStore } from "_store/pannelStore";
|
import { usePannelStore } from "_store/pannelStore";
|
||||||
import { cn } from "@repo/shared-components";
|
import { cn } from "@repo/shared-components";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { Chat } from "../_components/left/Chat";
|
import { Chat } from "../../_components/left/Chat";
|
||||||
import { Report } from "../_components/left/Report";
|
import { Report } from "../../_components/left/Report";
|
||||||
import { SituationBoard } from "_components/left/SituationBoard";
|
import { SituationBoard } from "_components/left/SituationBoard";
|
||||||
|
|
||||||
const Map = dynamic(() => import("../_components/map/Map"), { ssr: false });
|
const Map = dynamic(() => import("../../_components/map/Map"), { ssr: false });
|
||||||
|
|
||||||
const DispatchPage = () => {
|
const DispatchPage = () => {
|
||||||
const { isOpen } = usePannelStore();
|
const { isOpen } = usePannelStore();
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Navbar from "./_components/navbar/Navbar";
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getServerSession } from "../api/auth/[...nextauth]/auth";
|
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||||
import { Error } from "_components/Error";
|
import { Error } from "_components/Error";
|
||||||
import { prisma } from "@repo/db";
|
import { prisma } from "@repo/db";
|
||||||
|
|
||||||
@@ -27,8 +26,9 @@ export default async function RootLayout({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!session || !session.user.firstname) {
|
if (!session) {
|
||||||
redirect("/login");
|
console.log(session);
|
||||||
|
return redirect("/logout");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (openPenaltys[0]) {
|
if (openPenaltys[0]) {
|
||||||
@@ -37,7 +37,7 @@ export default async function RootLayout({
|
|||||||
<Error
|
<Error
|
||||||
title="Du wurdest permanent ausgeschlossen"
|
title="Du wurdest permanent ausgeschlossen"
|
||||||
statusCode={403}
|
statusCode={403}
|
||||||
description={`Dein Fehlverhalten war so schwerwiegend, dass du dauerhaft von VirtualAirRescue ausgeschlossen wurdest.`}
|
description={`Dein Fehlverhalten war so schwerwiegend, dass du dauerhaft von VirtualAirRescue ausgeschlossen wurdest. Du kannst im Hub weitere Informationen finden.`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ export default async function RootLayout({
|
|||||||
<Error
|
<Error
|
||||||
title="Du hast eine aktive Strafe"
|
title="Du hast eine aktive Strafe"
|
||||||
statusCode={403}
|
statusCode={403}
|
||||||
description={`Du bist bis zum ${new Date(openPenaltys[0].until!).toLocaleString()} gesperrt.`}
|
description={`Du bist bis zum ${new Date(openPenaltys[0].until!).toLocaleString()} gesperrt. Du kannst im Hub weitere Informationen finden.`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,14 +53,5 @@ export default async function RootLayout({
|
|||||||
if (!session.user.emailVerified) {
|
if (!session.user.emailVerified) {
|
||||||
return <Error title="E-Mail-Adresse nicht verifiziert" statusCode={403} />;
|
return <Error title="E-Mail-Adresse nicht verifiziert" statusCode={403} />;
|
||||||
}
|
}
|
||||||
|
return <>{children}</>;
|
||||||
if (!session.user.permissions.includes("PILOT"))
|
|
||||||
return <Error title="Zugriff verweigert" statusCode={403} />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Navbar />
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 626 KiB After Width: | Height: | Size: 626 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 610 KiB After Width: | Height: | Size: 610 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 322 KiB After Width: | Height: | Size: 322 KiB |
@@ -1,7 +1,7 @@
|
|||||||
import { ConnectedAircraft, Prisma } from "@repo/db";
|
import { ConnectedAircraft, Prisma } from "@repo/db";
|
||||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||||
import { useMrtStore } from "_store/pilot/MrtStore";
|
import { useMrtStore } from "_store/pilot/MrtStore";
|
||||||
import { pilotSocket } from "pilot/socket";
|
import { pilotSocket } from "(app)/pilot/socket";
|
||||||
import { editConnectedAircraftAPI } from "_querys/aircrafts";
|
import { editConnectedAircraftAPI } from "_querys/aircrafts";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Connection } from "./_components/Connection";
|
import { Connection } from "./_components/Connection";
|
||||||
/* import { ThemeSwap } from "./ThemeSwap"; */
|
import { Audio } from "_components/Audio/Audio";
|
||||||
import { Audio } from "../../../_components/Audio/Audio";
|
|
||||||
/* import { useState } from "react"; */
|
|
||||||
import { ExitIcon, ExternalLinkIcon } from "@radix-ui/react-icons";
|
import { ExitIcon, ExternalLinkIcon } from "@radix-ui/react-icons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Settings } from "_components/navbar/Settings";
|
import { Settings } from "_components/navbar/Settings";
|
||||||
27
apps/dispatch/app/(app)/pilot/layout.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Navbar from "./_components/navbar/Navbar";
|
||||||
|
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||||
|
import { Error } from "_components/Error";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "VAR: Pilot",
|
||||||
|
description: "Die neue VAR Leitstelle.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
const session = await getServerSession();
|
||||||
|
|
||||||
|
if (!session?.user.permissions.includes("PILOT"))
|
||||||
|
return <Error title="Zugriff verweigert" statusCode={403} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navbar />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Mrt } from "pilot/_components/mrt/Mrt";
|
import { Mrt } from "(app)/pilot/_components/mrt/Mrt";
|
||||||
import { Chat } from "../_components/left/Chat";
|
import { Chat } from "../../_components/left/Chat";
|
||||||
import { Report } from "../_components/left/Report";
|
import { Report } from "../../_components/left/Report";
|
||||||
import { Dme } from "pilot/_components/dme/Dme";
|
import { Dme } from "(app)/pilot/_components/dme/Dme";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { ConnectedDispatcher } from "tracker/_components/ConnectedDispatcher";
|
import { ConnectedDispatcher } from "tracker/_components/ConnectedDispatcher";
|
||||||
|
|
||||||
const Map = dynamic(() => import("../_components/map/Map"), {
|
const Map = dynamic(() => import("_components/map/Map"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -7,11 +7,11 @@ import { Toaster } from "react-hot-toast";
|
|||||||
export const Login = () => {
|
export const Login = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const { data: session } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const navigate = useRouter();
|
const navigate = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session) {
|
if (status === "authenticated") {
|
||||||
navigate.push("/");
|
navigate.push("/");
|
||||||
}
|
}
|
||||||
}, [session, navigate]);
|
}, [session, navigate]);
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
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, useState } from "react";
|
||||||
import { dispatchSocket } from "dispatch/socket";
|
import { dispatchSocket } from "(app)/dispatch/socket";
|
||||||
import { Mission, NotificationPayload } from "@repo/db";
|
import { Mission, NotificationPayload } from "@repo/db";
|
||||||
import { HPGnotificationToast } from "_components/customToasts/HPGnotification";
|
import { HPGnotificationToast } from "_components/customToasts/HPGnotification";
|
||||||
import { useMapStore } from "_store/mapStore";
|
import { useMapStore } from "_store/mapStore";
|
||||||
import { AdminMessageToast } from "_components/customToasts/AdminMessage";
|
import { AdminMessageToast } from "_components/customToasts/AdminMessage";
|
||||||
import { pilotSocket } from "pilot/socket";
|
import { pilotSocket } from "(app)/pilot/socket";
|
||||||
|
|
||||||
export function QueryProvider({ children }: { children: ReactNode }) {
|
export function QueryProvider({ children }: { children: ReactNode }) {
|
||||||
const mapStore = useMapStore((s) => s);
|
const mapStore = useMapStore((s) => s);
|
||||||
|
|||||||
@@ -150,21 +150,29 @@ export default function AdminPanel() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="flex gap-2">
|
<td className="flex gap-2">
|
||||||
<PenaltyDropdown
|
<PenaltyDropdown
|
||||||
|
btnName="Verbindung trennen"
|
||||||
btnClassName="btn-warning"
|
btnClassName="btn-warning"
|
||||||
btnTip="Kick"
|
btnTip="Die Verbindung zur Leitstelle wird für diesesn Nutzer unterbrochen"
|
||||||
Icon={<RedoDot size={15} />}
|
Icon={<RedoDot size={15} />}
|
||||||
onClick={({ reason }) =>
|
onClick={({ reason }) =>
|
||||||
kickPilotMutation.mutate({ id: p.id, reason })
|
kickPilotMutation.mutate({ id: p.id, reason })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<PenaltyDropdown
|
<PenaltyDropdown
|
||||||
|
btnName="Kick + Berechtigungen entfernen"
|
||||||
btnClassName="btn-error tooltip-error"
|
btnClassName="btn-error tooltip-error"
|
||||||
btnTip="Kick + Berechtigungen entfernen"
|
btnTip="Dadurch wird sich der Pilot nicht mehr mit dem VAR verbinden können."
|
||||||
showDatePicker
|
showDatePicker
|
||||||
Icon={<LockKeyhole size={15} />}
|
Icon={<LockKeyhole size={15} />}
|
||||||
onClick={({ reason, until }) =>
|
onClick={({ reason, until }) => {
|
||||||
kickPilotMutation.mutate({ id: p.id, reason, bann: true, until })
|
if (!until) {
|
||||||
}
|
toast.error(
|
||||||
|
"Bitte wähle ein Datum aus. Ein permanenter Bann ist nur vom HUB aus möglich.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
kickPilotMutation.mutate({ id: p.id, reason, bann: true, until });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.userId}`}
|
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.userId}`}
|
||||||
@@ -203,16 +211,18 @@ export default function AdminPanel() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="flex gap-2">
|
<td className="flex gap-2">
|
||||||
<PenaltyDropdown
|
<PenaltyDropdown
|
||||||
|
btnName="Verbindung trennen"
|
||||||
btnClassName="btn-warning"
|
btnClassName="btn-warning"
|
||||||
btnTip="Kick"
|
btnTip="Die Verbindung zur Leitstelle wird für diesesn Nutzer unterbrochen"
|
||||||
Icon={<RedoDot size={15} />}
|
Icon={<RedoDot size={15} />}
|
||||||
onClick={({ reason }) =>
|
onClick={({ reason }) =>
|
||||||
kickDispatchMutation.mutate({ id: d.id, reason })
|
kickDispatchMutation.mutate({ id: d.id, reason })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<PenaltyDropdown
|
<PenaltyDropdown
|
||||||
|
btnName="Kick + Berechtigungen entfernen"
|
||||||
btnClassName="btn-error tooltip-error"
|
btnClassName="btn-error tooltip-error"
|
||||||
btnTip="Kick + Berechtigungen entfernen"
|
btnTip="Dadurch wird sich der Pilot nicht mehr mit dem VAR verbinden können."
|
||||||
showDatePicker
|
showDatePicker
|
||||||
Icon={<LockKeyhole size={15} />}
|
Icon={<LockKeyhole size={15} />}
|
||||||
onClick={({ reason, until }) =>
|
onClick={({ reason, until }) =>
|
||||||
@@ -264,12 +274,6 @@ export default function AdminPanel() {
|
|||||||
>
|
>
|
||||||
<RedoDot size={15} />
|
<RedoDot size={15} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
|
||||||
data-tip="Kick + Berechtigungen entfernen"
|
|
||||||
>
|
|
||||||
<LockKeyhole size={15} />
|
|
||||||
</button>
|
|
||||||
<a
|
<a
|
||||||
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.participant.attributes.userId}`}
|
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.participant.attributes.userId}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { dispatchSocket } from "dispatch/socket";
|
import { dispatchSocket } from "(app)/dispatch/socket";
|
||||||
import {
|
import {
|
||||||
handleDisconnect,
|
handleDisconnect,
|
||||||
handleLocalTrackUnpublished,
|
handleLocalTrackUnpublished,
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
handleTrackUnsubscribed,
|
handleTrackUnsubscribed,
|
||||||
} from "_helpers/liveKitEventHandler";
|
} from "_helpers/liveKitEventHandler";
|
||||||
import { ConnectionQuality, Participant, Room, RoomEvent, RpcInvocationData } from "livekit-client";
|
import { ConnectionQuality, Participant, Room, RoomEvent, RpcInvocationData } from "livekit-client";
|
||||||
import { pilotSocket } from "pilot/socket";
|
import { pilotSocket } from "(app)/pilot/socket";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { dispatchSocket } from "../../dispatch/socket";
|
import { dispatchSocket } from "../../(app)/dispatch/socket";
|
||||||
import { useAudioStore } from "_store/audioStore";
|
import { useAudioStore } from "_store/audioStore";
|
||||||
import { ConnectedDispatcher } from "@repo/db";
|
import { ConnectedDispatcher } from "@repo/db";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { ChatMessage } from "@repo/db";
|
import { ChatMessage } from "@repo/db";
|
||||||
import { dispatchSocket } from "dispatch/socket";
|
import { dispatchSocket } from "(app)/dispatch/socket";
|
||||||
import { pilotSocket } from "pilot/socket";
|
import { pilotSocket } from "(app)/pilot/socket";
|
||||||
|
|
||||||
interface ChatStore {
|
interface ChatStore {
|
||||||
situationTabOpen: boolean;
|
situationTabOpen: boolean;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { MissionSdsLog, Station } from "@repo/db";
|
import { MissionSdsLog, Station } from "@repo/db";
|
||||||
import { fmsStatusDescription } from "_data/fmsStatusDescription";
|
import { fmsStatusDescription } from "_data/fmsStatusDescription";
|
||||||
import { DisplayLineProps } from "pilot/_components/mrt/Mrt";
|
import { DisplayLineProps } from "(app)/pilot/_components/mrt/Mrt";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { syncTabs } from "zustand-sync-tabs";
|
import { syncTabs } from "zustand-sync-tabs";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { dispatchSocket } from "../../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 { pilotSocket } from "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";
|
||||||
import { useAudioStore } from "_store/audioStore";
|
import { useAudioStore } from "_store/audioStore";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Mission, Station, User } from "@repo/db";
|
import { Mission, Station, User } from "@repo/db";
|
||||||
import { DisplayLineProps } from "pilot/_components/dme/Dme";
|
import { DisplayLineProps } from "(app)/pilot/_components/dme/Dme";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { syncTabs } from "zustand-sync-tabs";
|
import { syncTabs } from "zustand-sync-tabs";
|
||||||
|
|
||||||
|
|||||||
@@ -76,15 +76,7 @@ export const options: AuthOptions = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!dbUser) {
|
if (!dbUser) {
|
||||||
return {
|
return null as any;
|
||||||
...session,
|
|
||||||
user: {
|
|
||||||
name: null,
|
|
||||||
email: null,
|
|
||||||
image: null,
|
|
||||||
},
|
|
||||||
expires: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...session,
|
...session,
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
import type { Metadata } from "next";
|
|
||||||
import Navbar from "./_components/navbar/Navbar";
|
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
import { getServerSession } from "../api/auth/[...nextauth]/auth";
|
|
||||||
import { Error } from "_components/Error";
|
|
||||||
import { prisma } from "@repo/db";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "VAR: Disponent",
|
|
||||||
description: "Die neue VAR Leitstelle.",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function RootLayout({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) {
|
|
||||||
const session = await getServerSession();
|
|
||||||
const openPenaltys = await prisma.penalty.findMany({
|
|
||||||
where: {
|
|
||||||
userId: session?.user.id,
|
|
||||||
until: {
|
|
||||||
gte: new Date(),
|
|
||||||
},
|
|
||||||
suspended: false,
|
|
||||||
|
|
||||||
type: { in: ["TIME_BAN", "BAN"] },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!session || !session.user) {
|
|
||||||
redirect("/login");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (openPenaltys[0]) {
|
|
||||||
if (openPenaltys[0].type === "BAN") {
|
|
||||||
return (
|
|
||||||
<Error
|
|
||||||
title="Du wurdest permanent ausgeschlossen"
|
|
||||||
statusCode={403}
|
|
||||||
description={`Dein Fehlverhalten war so schwerwiegend, dass du dauerhaft von VirtualAirRescue ausgeschlossen wurdest.`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Error
|
|
||||||
title="Du hast eine aktive Strafe"
|
|
||||||
statusCode={403}
|
|
||||||
description={`Du bist bis zum ${new Date(openPenaltys[0].until!).toLocaleString()} gesperrt.`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session.user.emailVerified)
|
|
||||||
return <Error title="E-Mail-Adresse nicht verifiziert" statusCode={403} />;
|
|
||||||
|
|
||||||
if (!session.user.permissions.includes("DISPO"))
|
|
||||||
return <Error title="Zugriff verweigert" statusCode={403} />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Navbar />
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -42,8 +42,9 @@ export default async function RootLayout({
|
|||||||
style: {
|
style: {
|
||||||
background: "var(--color-base-100)",
|
background: "var(--color-base-100)",
|
||||||
color: "var(--color-base-content)",
|
color: "var(--color-base-content)",
|
||||||
|
zIndex: 9999,
|
||||||
},
|
},
|
||||||
duration: 4000,
|
duration: 5000,
|
||||||
}}
|
}}
|
||||||
position="top-left"
|
position="top-left"
|
||||||
reverseOrder={false}
|
reverseOrder={false}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default () => {
|
|||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session.status !== "authenticated") {
|
if (session.status === "unauthenticated") {
|
||||||
router.replace("/login");
|
router.replace("/login");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
import ModeSwitchDropdown from "_components/navbar/ModeSwitchDropdown";
|
import ModeSwitchDropdown from "_components/navbar/ModeSwitchDropdown";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { ConnectedDispatcher } from "tracker/_components/ConnectedDispatcher";
|
import { ConnectedDispatcher } from "./_components/ConnectedDispatcher";
|
||||||
|
|
||||||
const Map = dynamic(() => import("../_components/map/Map"), {
|
const Map = dynamic(() => import("_components/map/Map"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export const FirstPath = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{page === "path" ? "Weiter" : "Pfad auswählen"}
|
{page === "path" ? "Weiter" : "Intro abschließen"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ export const DispoStats = async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="stat-title">Disponent Login Zeit</div>
|
<div className="stat-title">Disponent Login Zeit</div>
|
||||||
<div className="stat-value text-secondary">
|
<div className="stat-value text-secondary">
|
||||||
{hours}h {minutes}m
|
{hours}h {minutes}min
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import {
|
|||||||
ConnectedAircraft,
|
ConnectedAircraft,
|
||||||
ConnectedDispatcher,
|
ConnectedDispatcher,
|
||||||
DiscordAccount,
|
DiscordAccount,
|
||||||
Penalty,
|
|
||||||
PenaltyType,
|
|
||||||
PERMISSION,
|
PERMISSION,
|
||||||
Report,
|
|
||||||
Station,
|
Station,
|
||||||
User,
|
User,
|
||||||
} from "@repo/db";
|
} from "@repo/db";
|
||||||
@@ -57,7 +54,7 @@ import { Error } from "_components/Error";
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { setStandardName } from "../../../../../../helper/discord";
|
import { setStandardName } from "../../../../../../helper/discord";
|
||||||
import { penaltyColumns } from "(app)/admin/penalty/columns";
|
import { penaltyColumns } from "(app)/admin/penalty/columns";
|
||||||
import { addPenalty, editPenalty, editPenaltys } from "(app)/admin/penalty/actions";
|
import { addPenalty, editPenaltys } from "(app)/admin/penalty/actions";
|
||||||
import { reportColumns } from "(app)/admin/report/columns";
|
import { reportColumns } from "(app)/admin/report/columns";
|
||||||
|
|
||||||
interface ProfileFormProps {
|
interface ProfileFormProps {
|
||||||
@@ -123,26 +120,30 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormPro
|
|||||||
{form.formState.errors.lastname && (
|
{form.formState.errors.lastname && (
|
||||||
<p className="text-error">{form.formState.errors.lastname?.message}</p>
|
<p className="text-error">{form.formState.errors.lastname?.message}</p>
|
||||||
)}
|
)}
|
||||||
<label className="floating-label w-full">
|
{session.data?.user.permissions.includes("ADMIN_USER_ADVANCED") && (
|
||||||
<span className="text-lg flex items-center gap-2">
|
<>
|
||||||
<EnvelopeClosedIcon /> E-Mail
|
<label className="floating-label w-full">
|
||||||
</span>
|
<span className="text-lg flex items-center gap-2">
|
||||||
<input
|
<EnvelopeClosedIcon /> E-Mail
|
||||||
{...form.register("email")}
|
</span>
|
||||||
type="text"
|
<input
|
||||||
className="input input-bordered w-full mb-2"
|
{...form.register("email")}
|
||||||
defaultValue={user?.email}
|
type="text"
|
||||||
placeholder="E-Mail"
|
className="input input-bordered w-full mb-2"
|
||||||
/>
|
defaultValue={user?.email}
|
||||||
</label>
|
placeholder="E-Mail"
|
||||||
{form.formState.errors.email && (
|
/>
|
||||||
<p className="text-error">{form.formState.errors.email?.message}</p>
|
</label>
|
||||||
)}
|
{form.formState.errors.email && (
|
||||||
|
<p className="text-error">{form.formState.errors.email?.message}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<input type="checkbox" {...form.register("emailVerified")} className="checkbox" />
|
<input type="checkbox" {...form.register("emailVerified")} className="checkbox" />
|
||||||
Email bestätigt
|
Email bestätigt
|
||||||
</label>
|
</label>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Select
|
<Select
|
||||||
isMulti
|
isMulti
|
||||||
form={form}
|
form={form}
|
||||||
@@ -334,7 +335,8 @@ export const UserPenalties = ({ user }: { user: User }) => {
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<PenaltyDropdown
|
<PenaltyDropdown
|
||||||
Icon={<RedoDot size={15} />}
|
btnName="Zeitstrafe hinzufügen"
|
||||||
|
Icon={<Timer size={15} />}
|
||||||
onClick={async ({ reason, until }) => {
|
onClick={async ({ reason, until }) => {
|
||||||
if (!reason) return toast.error("Bitte gib einen Grund für die Strafe an.");
|
if (!reason) return toast.error("Bitte gib einen Grund für die Strafe an.");
|
||||||
if (!until) return toast.error("Bitte gib eine Dauer für die Strafe ein.");
|
if (!until) return toast.error("Bitte gib eine Dauer für die Strafe ein.");
|
||||||
@@ -350,12 +352,13 @@ export const UserPenalties = ({ user }: { user: User }) => {
|
|||||||
penaltyTable.current?.refresh();
|
penaltyTable.current?.refresh();
|
||||||
toast.success("Time-Ban wurde hinzugefügt!");
|
toast.success("Time-Ban wurde hinzugefügt!");
|
||||||
}}
|
}}
|
||||||
btnClassName="btn btn-outline btn-warning tooltip-warning"
|
btnClassName="btn-outline btn-warning tooltip-warning"
|
||||||
btnTip="Timeban hinzufügen"
|
btnTip="Der Nutzer wird für eine bestimmte Zeit gesperrt"
|
||||||
showDatePicker={true}
|
showDatePicker={true}
|
||||||
/>
|
/>
|
||||||
{session.data?.user.permissions.includes("ADMIN_USER_ADVANCED") && (
|
{session.data?.user.permissions.includes("ADMIN_USER_ADVANCED") && (
|
||||||
<PenaltyDropdown
|
<PenaltyDropdown
|
||||||
|
btnName="Bannen"
|
||||||
Icon={<LockKeyhole size={15} />}
|
Icon={<LockKeyhole size={15} />}
|
||||||
onClick={async ({ reason }) => {
|
onClick={async ({ reason }) => {
|
||||||
if (!reason) return toast.error("Bitte gib einen Grund für die Strafe an.");
|
if (!reason) return toast.error("Bitte gib einen Grund für die Strafe an.");
|
||||||
@@ -371,7 +374,7 @@ export const UserPenalties = ({ user }: { user: User }) => {
|
|||||||
penaltyTable.current?.refresh();
|
penaltyTable.current?.refresh();
|
||||||
toast.success("Ban wurde hinzugefügt!");
|
toast.success("Ban wurde hinzugefügt!");
|
||||||
}}
|
}}
|
||||||
btnClassName="btn btn-outline btn-error tooltip-error"
|
btnClassName="btn-outline btn-error tooltip-error"
|
||||||
btnTip="Nutzerkonto sperren"
|
btnTip="Nutzerkonto sperren"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -503,7 +506,7 @@ export const AdminForm = ({
|
|||||||
role="submit"
|
role="submit"
|
||||||
className="btn-sm flex-1 min-w-[250px] btn-outline btn-warning"
|
className="btn-sm flex-1 min-w-[250px] btn-outline btn-warning"
|
||||||
>
|
>
|
||||||
<HobbyKnifeIcon /> HUB zugang entsperren
|
<HobbyKnifeIcon /> Account entsperren
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{discordAccount && (
|
{discordAccount && (
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { User2 } from "lucide-react";
|
import { User2 } from "lucide-react";
|
||||||
import { PaginatedTable } from "../../../_components/PaginatedTable";
|
import { PaginatedTable } from "../../../_components/PaginatedTable";
|
||||||
|
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||||
|
|
||||||
const AdminUserPage = async () => {
|
const AdminUserPage = async () => {
|
||||||
|
const session = await getServerSession();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PaginatedTable
|
<PaginatedTable
|
||||||
@@ -27,10 +29,14 @@ const AdminUserPage = async () => {
|
|||||||
header: "Nachname",
|
header: "Nachname",
|
||||||
accessorKey: "lastname",
|
accessorKey: "lastname",
|
||||||
},
|
},
|
||||||
{
|
...(session?.user.permissions.includes("ADMIN_USER_ADVANCED")
|
||||||
header: "Email",
|
? [
|
||||||
accessorKey: "email",
|
{
|
||||||
},
|
header: "Email",
|
||||||
|
accessorKey: "email",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
]}
|
]}
|
||||||
leftOfSearch={
|
leftOfSearch={
|
||||||
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const Penalty = async () => {
|
|||||||
<div className="card-body text-base-300">
|
<div className="card-body text-base-300">
|
||||||
<h2 className="card-title text-3xl">
|
<h2 className="card-title text-3xl">
|
||||||
<TriangleAlert />
|
<TriangleAlert />
|
||||||
Du wurdest permanent von VirtualAirRescue ausgeschlossen.
|
Du wurdest permanent von Virtual Air Rescue ausgeschlossen.
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-left font-bold">
|
<p className="text-left font-bold">
|
||||||
Dein Fehlverhalten war so schwerwiegend, dass du dauerhaft von VirtualAirRescue
|
Dein Fehlverhalten war so schwerwiegend, dass du dauerhaft von VirtualAirRescue
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ export const PenaltyDropdown = ({
|
|||||||
btnClassName,
|
btnClassName,
|
||||||
showDatePicker,
|
showDatePicker,
|
||||||
btnTip,
|
btnTip,
|
||||||
|
btnName,
|
||||||
Icon,
|
Icon,
|
||||||
}: {
|
}: {
|
||||||
onClick: (data: { reason: string; until: Date | null }) => void;
|
onClick: (data: { reason: string; until: Date | null }) => void;
|
||||||
showDatePicker?: boolean;
|
showDatePicker?: boolean;
|
||||||
btnClassName?: string;
|
btnClassName?: string;
|
||||||
|
btnName: string;
|
||||||
btnTip?: string;
|
btnTip?: string;
|
||||||
Icon: ReactNode;
|
Icon: ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -20,22 +22,26 @@ export const PenaltyDropdown = ({
|
|||||||
return (
|
return (
|
||||||
<details className="dropdown dropdown-left dropdown-center">
|
<details className="dropdown dropdown-left dropdown-center">
|
||||||
<summary className={cn("btn btn-xs btn-square btn-soft", btnClassName)}>{Icon}</summary>
|
<summary className={cn("btn btn-xs btn-square btn-soft", btnClassName)}>{Icon}</summary>
|
||||||
<div className="dropdown-content flex gap-3 items-center bg-base-100 rounded-box z-1 p-2 mr-3 shadow-sm">
|
<div
|
||||||
<input
|
className="dropdown-content bg-base-100 rounded-box z-1 p-4 shadow-sm space-y-4 shadow-md"
|
||||||
|
style={{ minWidth: "500px", right: "40px" }}
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-blod text-center">{btnName}</h2>
|
||||||
|
<textarea
|
||||||
value={reason}
|
value={reason}
|
||||||
onChange={(e) => setReason(e.target.value)}
|
onChange={(e) => setReason(e.target.value)}
|
||||||
type="text"
|
className="input w-full"
|
||||||
className="input min-w-[250px]"
|
|
||||||
placeholder="Begründung"
|
placeholder="Begründung"
|
||||||
|
style={{ minHeight: "100px" }}
|
||||||
/>
|
/>
|
||||||
{showDatePicker && (
|
{showDatePicker && (
|
||||||
<select
|
<select
|
||||||
className="select min-w-[150px] select-bordered"
|
className="select w-full select-bordered"
|
||||||
value={until}
|
value={until}
|
||||||
onChange={(e) => setUntil(e.target.value)}
|
onChange={(e) => setUntil(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="default" disabled>
|
<option value="default" disabled>
|
||||||
Unbegrenzt
|
Keine
|
||||||
</option>
|
</option>
|
||||||
<option value="1h">1 Stunde</option>
|
<option value="1h">1 Stunde</option>
|
||||||
<option value="6h">6 Stunden</option>
|
<option value="6h">6 Stunden</option>
|
||||||
@@ -51,7 +57,7 @@ export const PenaltyDropdown = ({
|
|||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
className={cn("btn btn-square btn-soft tooltip tooltip-bottom", btnClassName)}
|
className={cn("btn w-full btn-square btn-soft tooltip tooltip-bottom", btnClassName)}
|
||||||
data-tip={btnTip}
|
data-tip={btnTip}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
let untilDate: Date | null = null;
|
let untilDate: Date | null = null;
|
||||||
@@ -98,7 +104,7 @@ export const PenaltyDropdown = ({
|
|||||||
onClick({ reason, until: untilDate });
|
onClick({ reason, until: untilDate });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Icon}
|
{Icon} {btnName}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||