Dispatch Router-Struktur; AddPenalty Layout gefixed

This commit is contained in:
PxlLoewe
2025-06-27 15:52:28 -07:00
parent 782ba7669d
commit 91ed14ac08
47 changed files with 168 additions and 179 deletions

View File

@@ -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";

View File

@@ -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";

View 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}
</>
);
}

View File

@@ -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();

View File

@@ -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}
</>
);
} }

View File

Before

Width:  |  Height:  |  Size: 626 KiB

After

Width:  |  Height:  |  Size: 626 KiB

View File

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 167 KiB

View File

Before

Width:  |  Height:  |  Size: 610 KiB

After

Width:  |  Height:  |  Size: 610 KiB

View File

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 194 KiB

View File

Before

Width:  |  Height:  |  Size: 322 KiB

After

Width:  |  Height:  |  Size: 322 KiB

View File

@@ -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";

View File

@@ -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";

View 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}
</>
);
}

View File

@@ -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,
}); });

View File

@@ -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]);

View File

@@ -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);

View File

@@ -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"

View File

@@ -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";

View File

@@ -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";

View File

@@ -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;

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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,

View File

@@ -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}
</>
);
}

View File

@@ -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}

View File

@@ -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;
} }

View File

@@ -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,
}); });

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 && (

View File

@@ -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">

View File

@@ -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

View File

@@ -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>