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 { ThemeSwap } from "./_components/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 Link from "next/link";

View File

@@ -1,6 +1,6 @@
"use client";
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 { toast } from "react-hot-toast";
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";
import { Pannel } from "dispatch/_components/pannel/Pannel";
import { Pannel } from "(app)/dispatch/_components/pannel/Pannel";
import { usePannelStore } from "_store/pannelStore";
import { cn } from "@repo/shared-components";
import dynamic from "next/dynamic";
import { Chat } from "../_components/left/Chat";
import { Report } from "../_components/left/Report";
import { Chat } from "../../_components/left/Chat";
import { Report } from "../../_components/left/Report";
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 { isOpen } = usePannelStore();

View File

@@ -1,7 +1,6 @@
import type { Metadata } from "next";
import Navbar from "./_components/navbar/Navbar";
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 { prisma } from "@repo/db";
@@ -27,8 +26,9 @@ export default async function RootLayout({
},
});
if (!session || !session.user.firstname) {
redirect("/login");
if (!session) {
console.log(session);
return redirect("/logout");
}
if (openPenaltys[0]) {
@@ -37,7 +37,7 @@ export default async function RootLayout({
<Error
title="Du wurdest permanent ausgeschlossen"
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
title="Du hast eine aktive Strafe"
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) {
return <Error title="E-Mail-Adresse nicht verifiziert" statusCode={403} />;
}
if (!session.user.permissions.includes("PILOT"))
return <Error title="Zugriff verweigert" statusCode={403} />;
return (
<>
<Navbar />
{children}
</>
);
return <>{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 { usePilotConnectionStore } from "_store/pilot/connectionStore";
import { useMrtStore } from "_store/pilot/MrtStore";
import { pilotSocket } from "pilot/socket";
import { pilotSocket } from "(app)/pilot/socket";
import { editConnectedAircraftAPI } from "_querys/aircrafts";
import { useEffect } from "react";
import { useMutation } from "@tanstack/react-query";

View File

@@ -1,9 +1,7 @@
"use client";
import { Connection } from "./_components/Connection";
/* import { ThemeSwap } from "./ThemeSwap"; */
import { Audio } from "../../../_components/Audio/Audio";
/* import { useState } from "react"; */
import { Audio } from "_components/Audio/Audio";
import { ExitIcon, ExternalLinkIcon } from "@radix-ui/react-icons";
import Link from "next/link";
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";
import { Mrt } from "pilot/_components/mrt/Mrt";
import { Chat } from "../_components/left/Chat";
import { Report } from "../_components/left/Report";
import { Dme } from "pilot/_components/dme/Dme";
import { Mrt } from "(app)/pilot/_components/mrt/Mrt";
import { Chat } from "../../_components/left/Chat";
import { Report } from "../../_components/left/Report";
import { Dme } from "(app)/pilot/_components/dme/Dme";
import dynamic from "next/dynamic";
import { ConnectedDispatcher } from "tracker/_components/ConnectedDispatcher";
const Map = dynamic(() => import("../_components/map/Map"), {
const Map = dynamic(() => import("_components/map/Map"), {
ssr: false,
});

View File

@@ -7,11 +7,11 @@ import { Toaster } from "react-hot-toast";
export const Login = () => {
const [isLoading, setIsLoading] = useState(false);
const searchParams = useSearchParams();
const { data: session } = useSession();
const { data: session, status } = useSession();
const navigate = useRouter();
useEffect(() => {
if (session) {
if (status === "authenticated") {
navigate.push("/");
}
}, [session, navigate]);

View File

@@ -4,12 +4,12 @@
import { toast } from "react-hot-toast";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactNode, useEffect, useState } from "react";
import { dispatchSocket } from "dispatch/socket";
import { dispatchSocket } from "(app)/dispatch/socket";
import { Mission, NotificationPayload } from "@repo/db";
import { HPGnotificationToast } from "_components/customToasts/HPGnotification";
import { useMapStore } from "_store/mapStore";
import { AdminMessageToast } from "_components/customToasts/AdminMessage";
import { pilotSocket } from "pilot/socket";
import { pilotSocket } from "(app)/pilot/socket";
export function QueryProvider({ children }: { children: ReactNode }) {
const mapStore = useMapStore((s) => s);

View File

@@ -150,21 +150,29 @@ export default function AdminPanel() {
</td>
<td className="flex gap-2">
<PenaltyDropdown
btnName="Verbindung trennen"
btnClassName="btn-warning"
btnTip="Kick"
btnTip="Die Verbindung zur Leitstelle wird für diesesn Nutzer unterbrochen"
Icon={<RedoDot size={15} />}
onClick={({ reason }) =>
kickPilotMutation.mutate({ id: p.id, reason })
}
/>
<PenaltyDropdown
btnName="Kick + Berechtigungen entfernen"
btnClassName="btn-error tooltip-error"
btnTip="Kick + Berechtigungen entfernen"
btnTip="Dadurch wird sich der Pilot nicht mehr mit dem VAR verbinden können."
showDatePicker
Icon={<LockKeyhole size={15} />}
onClick={({ reason, until }) =>
kickPilotMutation.mutate({ id: p.id, reason, bann: true, until })
onClick={({ reason, 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
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.userId}`}
@@ -203,16 +211,18 @@ export default function AdminPanel() {
</td>
<td className="flex gap-2">
<PenaltyDropdown
btnName="Verbindung trennen"
btnClassName="btn-warning"
btnTip="Kick"
btnTip="Die Verbindung zur Leitstelle wird für diesesn Nutzer unterbrochen"
Icon={<RedoDot size={15} />}
onClick={({ reason }) =>
kickDispatchMutation.mutate({ id: d.id, reason })
}
/>
<PenaltyDropdown
btnName="Kick + Berechtigungen entfernen"
btnClassName="btn-error tooltip-error"
btnTip="Kick + Berechtigungen entfernen"
btnTip="Dadurch wird sich der Pilot nicht mehr mit dem VAR verbinden können."
showDatePicker
Icon={<LockKeyhole size={15} />}
onClick={({ reason, until }) =>
@@ -264,12 +274,6 @@ export default function AdminPanel() {
>
<RedoDot size={15} />
</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
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.participant.attributes.userId}`}
target="_blank"

View File

@@ -1,4 +1,4 @@
import { dispatchSocket } from "dispatch/socket";
import { dispatchSocket } from "(app)/dispatch/socket";
import {
handleDisconnect,
handleLocalTrackUnpublished,
@@ -6,7 +6,7 @@ import {
handleTrackUnsubscribed,
} from "_helpers/liveKitEventHandler";
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 axios from "axios";
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";

View File

@@ -1,5 +1,5 @@
import { create } from "zustand";
import { dispatchSocket } from "../../dispatch/socket";
import { dispatchSocket } from "../../(app)/dispatch/socket";
import { useAudioStore } from "_store/audioStore";
import { ConnectedDispatcher } from "@repo/db";

View File

@@ -1,7 +1,7 @@
import { create } from "zustand";
import { ChatMessage } from "@repo/db";
import { dispatchSocket } from "dispatch/socket";
import { pilotSocket } from "pilot/socket";
import { dispatchSocket } from "(app)/dispatch/socket";
import { pilotSocket } from "(app)/pilot/socket";
interface ChatStore {
situationTabOpen: boolean;

View File

@@ -1,6 +1,6 @@
import { MissionSdsLog, Station } from "@repo/db";
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 { syncTabs } from "zustand-sync-tabs";

View File

@@ -1,7 +1,7 @@
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 { pilotSocket } from "pilot/socket";
import { pilotSocket } from "(app)/pilot/socket";
import { useDmeStore } from "_store/pilot/dmeStore";
import { useMrtStore } from "_store/pilot/MrtStore";
import { useAudioStore } from "_store/audioStore";

View File

@@ -1,5 +1,5 @@
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 { syncTabs } from "zustand-sync-tabs";

View File

@@ -76,15 +76,7 @@ export const options: AuthOptions = {
},
});
if (!dbUser) {
return {
...session,
user: {
name: null,
email: null,
image: null,
},
expires: new Date().toISOString(),
};
return null as any;
}
return {
...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: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
zIndex: 9999,
},
duration: 4000,
duration: 5000,
}}
position="top-left"
reverseOrder={false}

View File

@@ -8,7 +8,7 @@ export default () => {
const session = useSession();
useEffect(() => {
if (session.status !== "authenticated") {
if (session.status === "unauthenticated") {
router.replace("/login");
return;
}

View File

@@ -2,9 +2,9 @@
import ModeSwitchDropdown from "_components/navbar/ModeSwitchDropdown";
import { useSession } from "next-auth/react";
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,
});

View File

@@ -158,7 +158,7 @@ export const FirstPath = () => {
}
}}
>
{page === "path" ? "Weiter" : "Pfad auswählen"}
{page === "path" ? "Weiter" : "Intro abschließen"}
</Button>
</div>
</div>

View File

@@ -256,7 +256,7 @@ export const DispoStats = async () => {
</div>
<div className="stat-title">Disponent Login Zeit</div>
<div className="stat-value text-secondary">
{hours}h {minutes}m
{hours}h {minutes}min
</div>
</div>

View File

@@ -6,10 +6,7 @@ import {
ConnectedAircraft,
ConnectedDispatcher,
DiscordAccount,
Penalty,
PenaltyType,
PERMISSION,
Report,
Station,
User,
} from "@repo/db";
@@ -57,7 +54,7 @@ import { Error } from "_components/Error";
import { useSession } from "next-auth/react";
import { setStandardName } from "../../../../../../helper/discord";
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";
interface ProfileFormProps {
@@ -123,6 +120,8 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormPro
{form.formState.errors.lastname && (
<p className="text-error">{form.formState.errors.lastname?.message}</p>
)}
{session.data?.user.permissions.includes("ADMIN_USER_ADVANCED") && (
<>
<label className="floating-label w-full">
<span className="text-lg flex items-center gap-2">
<EnvelopeClosedIcon /> E-Mail
@@ -143,6 +142,8 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormPro
<input type="checkbox" {...form.register("emailVerified")} className="checkbox" />
Email bestätigt
</label>
</>
)}
<Select
isMulti
form={form}
@@ -334,7 +335,8 @@ export const UserPenalties = ({ user }: { user: User }) => {
</span>
<div className="flex gap-2">
<PenaltyDropdown
Icon={<RedoDot size={15} />}
btnName="Zeitstrafe hinzufügen"
Icon={<Timer size={15} />}
onClick={async ({ reason, until }) => {
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.");
@@ -350,12 +352,13 @@ export const UserPenalties = ({ user }: { user: User }) => {
penaltyTable.current?.refresh();
toast.success("Time-Ban wurde hinzugefügt!");
}}
btnClassName="btn btn-outline btn-warning tooltip-warning"
btnTip="Timeban hinzufügen"
btnClassName="btn-outline btn-warning tooltip-warning"
btnTip="Der Nutzer wird für eine bestimmte Zeit gesperrt"
showDatePicker={true}
/>
{session.data?.user.permissions.includes("ADMIN_USER_ADVANCED") && (
<PenaltyDropdown
btnName="Bannen"
Icon={<LockKeyhole size={15} />}
onClick={async ({ reason }) => {
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();
toast.success("Ban wurde hinzugefügt!");
}}
btnClassName="btn btn-outline btn-error tooltip-error"
btnClassName="btn-outline btn-error tooltip-error"
btnTip="Nutzerkonto sperren"
/>
)}
@@ -503,7 +506,7 @@ export const AdminForm = ({
role="submit"
className="btn-sm flex-1 min-w-[250px] btn-outline btn-warning"
>
<HobbyKnifeIcon /> HUB zugang entsperren
<HobbyKnifeIcon /> Account entsperren
</Button>
)}
{discordAccount && (

View File

@@ -1,7 +1,9 @@
import { User2 } from "lucide-react";
import { PaginatedTable } from "../../../_components/PaginatedTable";
import { getServerSession } from "api/auth/[...nextauth]/auth";
const AdminUserPage = async () => {
const session = await getServerSession();
return (
<>
<PaginatedTable
@@ -27,10 +29,14 @@ const AdminUserPage = async () => {
header: "Nachname",
accessorKey: "lastname",
},
...(session?.user.permissions.includes("ADMIN_USER_ADVANCED")
? [
{
header: "Email",
accessorKey: "email",
},
]
: []),
]}
leftOfSearch={
<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">
<h2 className="card-title text-3xl">
<TriangleAlert />
Du wurdest permanent von VirtualAirRescue ausgeschlossen.
Du wurdest permanent von Virtual Air Rescue ausgeschlossen.
</h2>
<p className="text-left font-bold">
Dein Fehlverhalten war so schwerwiegend, dass du dauerhaft von VirtualAirRescue

View File

@@ -7,11 +7,13 @@ export const PenaltyDropdown = ({
btnClassName,
showDatePicker,
btnTip,
btnName,
Icon,
}: {
onClick: (data: { reason: string; until: Date | null }) => void;
showDatePicker?: boolean;
btnClassName?: string;
btnName: string;
btnTip?: string;
Icon: ReactNode;
}) => {
@@ -20,22 +22,26 @@ export const PenaltyDropdown = ({
return (
<details className="dropdown dropdown-left dropdown-center">
<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">
<input
<div
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}
onChange={(e) => setReason(e.target.value)}
type="text"
className="input min-w-[250px]"
className="input w-full"
placeholder="Begründung"
style={{ minHeight: "100px" }}
/>
{showDatePicker && (
<select
className="select min-w-[150px] select-bordered"
className="select w-full select-bordered"
value={until}
onChange={(e) => setUntil(e.target.value)}
>
<option value="default" disabled>
Unbegrenzt
Keine
</option>
<option value="1h">1 Stunde</option>
<option value="6h">6 Stunden</option>
@@ -51,7 +57,7 @@ export const PenaltyDropdown = ({
</select>
)}
<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}
onClick={() => {
let untilDate: Date | null = null;
@@ -98,7 +104,7 @@ export const PenaltyDropdown = ({
onClick({ reason, until: untilDate });
}}
>
{Icon}
{Icon} {btnName}
</button>
</div>
</details>