Pfadauswahl hinzugefügt

This commit is contained in:
PxlLoewe
2025-06-25 21:06:03 -07:00
parent 7ad75bff63
commit 2bd8a455c8
14 changed files with 253 additions and 90 deletions

View File

@@ -1,6 +1,6 @@
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
import { prisma } from "@repo/db";
import { KursItem } from "../events/_components/item";
import { EventCard } from "../events/_components/item";
import { RocketIcon } from "lucide-react";
import { eventCompleted } from "../../../helper/events";
@@ -15,15 +15,15 @@ const page = async () => {
const events = await prisma.event.findMany({
where: {
type: "OBLIGATED_COURSE",
type: "EVENT",
},
include: {
participants: {
Participants: {
where: {
userId: user.id,
},
},
appointments: {
Appointments: {
include: {
Participants: {
where: {
@@ -36,12 +36,10 @@ const page = async () => {
});
const filteredEvents = events.filter((event) => {
const userParticipant = event.participants.find(
const userParticipant = event.Participants.find(
(participant) => participant.userId === user.id,
);
if (eventCompleted(event, userParticipant)) return false;
if (event.type === "OBLIGATED_COURSE" && !eventCompleted(event, event.participants[0]))
return true;
return false;
});
@@ -56,9 +54,9 @@ const page = async () => {
<div className="grid grid-cols-6 gap-4">
{filteredEvents.map((event) => {
return (
<KursItem
appointments={event.appointments}
selectedAppointments={event.appointments.filter((a) =>
<EventCard
appointments={event.Appointments}
selectedAppointments={event.Appointments.filter((a) =>
a.Participants.find((p) => p.userId == user.id),
)}
user={user}

View File

@@ -1,9 +1,100 @@
"use client";
import { editUser } from "(app)/admin/user/action";
import { Button } from "_components/ui/Button";
import { Plane, Workflow } from "lucide-react";
import { useSession } from "next-auth/react";
import { useRef, useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { getEvents } from "../../../helper/events";
import { EventCard } from "(app)/events/_components/item";
const PathsOptions = ({
selected,
setSelected,
}: {
selected: "disponent" | "pilot" | null;
setSelected: (value: "disponent" | "pilot") => void;
}) => {
return (
<>
<div className="flex gap-6">
{/* Disponent Card */}
<div
className={`cursor-pointer border rounded-lg p-6 w-80 transition-colors ${
selected === "disponent" ? "border-info ring-2 ring-info" : "border-base-300"
}`}
onClick={() => setSelected("disponent")}
role="button"
tabIndex={0}
aria-pressed={selected === "disponent"}
>
<h1 className="font-semibold text-lg mb-2 flex gap-2 justify-center items-center">
Disponent <Workflow />
</h1>
<div className="text-sm text-base-content/70">
Denkt sich realistische Einsatzszenarien aus, koordiniert deren Ablauf und ist die
zentrale Schnittstelle zwischen Piloten und bodengebundenen Rettungsmitteln. Er trägt
die Verantwortung für einen reibungslosen Ablauf und der erfolgreichen Durchführung der
Einsätze.
<div className="badge badge-sm badge-secondary mt-3">
Teilnahme an Einführungsevent Nötig
</div>
</div>
</div>
{/* Pilot Card */}
<div
className={`cursor-pointer border rounded-lg p-6 w-80 transition-colors ${
selected === "pilot" ? "border-info ring-2 ring-info" : "border-base-300"
}`}
onClick={() => setSelected("pilot")}
role="button"
tabIndex={0}
aria-pressed={selected === "pilot"}
>
<h1 className="font-semibold text-lg mb-2 flex gap-2 justify-center items-center">
Pilot <Plane />
</h1>
<div className="text-sm text-base-content/70">
Fliegt die vom Disponenten erstellten Einsätze und transportiert die Med-Crew sicher zum
Einsatzort. Er übernimmt die navigatorische Vorbereitung, achtet auf Wetterentwicklungen
und sorgt für die Sicherheit seiner Crew im Flug.
</div>
</div>
</div>
</>
);
};
const EventSelect = ({ pathSelected }: { pathSelected: "disponent" | "pilot" }) => {
const { data: events } = useQuery({
queryKey: ["events", "initial", pathSelected],
queryFn: () =>
getEvents({
type: pathSelected === "pilot" ? "PILOT_STARTER" : "DISPATCH_STARTER",
}),
});
const user = useSession().data?.user;
if (!user) return null;
return events?.map((event) => {
return (
<EventCard
appointments={event.Appointments}
selectedAppointments={event.Appointments.filter((a) =>
a.Participants.find((p) => p.userId == user.id),
)}
user={user}
event={event}
key={event.id}
/>
);
});
};
export const FirstPath = () => {
const modalRef = useRef<HTMLDialogElement>(null);
const { data: session } = useSession();
const [selected, setSelected] = useState<"disponent" | "pilot" | null>(null);
const [page, setPage] = useState<"path" | "event-select">("path");
useEffect(() => {
if (modalRef.current && !modalRef.current.open) {
@@ -17,59 +108,40 @@ export const FirstPath = () => {
<h3 className="flex items-center gap-2 text-lg font-bold mb-10">Wähle deinen Einstieg!</h3>
<p className="mb-8 text-base text-base-content/80 text-center">
Willkommen bei Virtual Air Rescue!
<br /> Wähle deinen ersten Pfad aus. Du kannst später jederzeit auch den anderen Pfad
<br /> Wie möchtest du bei uns starten? Du kannst später jederzeit auch den anderen Pfad
ausprobieren, wenn du möchtest.
</p>
<div className="flex flex-col items-center justify-center m-20">
<div className="flex gap-6">
{/* Disponent Card */}
<div
className={`cursor-pointer border rounded-lg p-6 w-80 transition-colors ${
selected === "disponent" ? "border-info ring-2 ring-info" : "border-base-300"
}`}
onClick={() => setSelected("disponent")}
role="button"
tabIndex={0}
aria-pressed={selected === "disponent"}
>
<div className="font-semibold text-lg mb-2">Disponent</div>
<div className="text-sm text-base-content/70">
Denkt sich realistische Einsatzszenarien aus, koordiniert deren Ablauf und ist die
zentrale Schnittstelle zwischen Piloten und bodengebundenen Rettungsmitteln. Er
trägt die Verantwortung für einen reibungslosen Ablauf und der erfolgreichen
Durchführung der Einsätze.
<div className="badge badge-sm badge-secondary mt-3">
Teilnahme an Einführungsevent Nötig
</div>
{page === "path" && <PathsOptions selected={selected} setSelected={setSelected} />}
{page === "event-select" && (
<div className="flex flex-col gap-3 min-w-[800px]">
<div>
<p className="text-left text-gray-400 text-sm">Wähle dein Einführungs-Event aus:</p>
</div>
<EventSelect pathSelected={selected!} />
</div>
{/* Pilot Card */}
<div
className={`cursor-pointer border rounded-lg p-6 w-80 transition-colors ${
selected === "pilot" ? "border-info ring-2 ring-info" : "border-base-300"
}`}
onClick={() => setSelected("pilot")}
role="button"
tabIndex={0}
aria-pressed={selected === "pilot"}
>
<div className="font-semibold text-lg mb-2">Pilot</div>
<div className="text-sm text-base-content/70">
Fliegt die vom Disponenten erstellten Einsätze und transportiert die Med-Crew sicher
zum Einsatzort. Er übernimmt die navigatorische Vorbereitung, achtet auf
Wetterentwicklungen und sorgt für die Sicherheit seiner Crew im Flug.
</div>
</div>
</div>
)}
</div>
<div className="modal-action">
<button
<button className="btn" disabled={page === "path"} onClick={() => setPage("path")}>
Zurück
</button>
<Button
className="btn btn-info"
disabled={!selected}
onClick={() => modalRef.current?.close()}
onClick={async () => {
if (page === "path") {
setPage("event-select");
} else if (session?.user.id) {
await editUser(session?.user.id, {
pathSelected: true,
});
modalRef.current?.close();
}
}}
>
Auswahl Bestätigen
</button>
{page === "path" ? "Weiter" : "Pfad auswählen"}
</Button>
</div>
</div>
</dialog>

View File

@@ -5,7 +5,7 @@ import ModalBtn from "./modalBtn";
import MDEditor from "@uiw/react-md-editor";
import { Badge } from "../../../_components/Badge/Badge";
export const KursItem = ({
export const EventCard = ({
user,
event,
selectedAppointments,
@@ -13,8 +13,8 @@ export const KursItem = ({
}: {
user: User;
event: Event & {
appointments: EventAppointment[];
participants: Participant[];
Appointments: EventAppointment[];
Participants: Participant[];
};
selectedAppointments: EventAppointment[];
appointments: EventAppointment[];
@@ -28,8 +28,8 @@ export const KursItem = ({
{event.type === "COURSE" && (
<span className="badge badge-info badge-outline">Zusatzqualifikation</span>
)}
{event.type === "OBLIGATED_COURSE" && (
<span className="badge badge-secondary badge-outline">Verpflichtend</span>
{event.type === "EVENT" && (
<span className="badge badge-secondary badge-outline">Event</span>
)}
</div>
<div className="grid grid-cols-6 gap-4">
@@ -75,7 +75,7 @@ export const KursItem = ({
event={event}
title={event.name}
dates={appointments}
participant={event.participants[0]}
participant={event.Participants[0]}
modalId={`${event.name}_modal.${event.id}`}
/>
</div>

View File

@@ -92,7 +92,7 @@ const ModalBtn = ({
<button
className={cn(
"btn btn-outline btn-info btn-wide",
event.type === "OBLIGATED_COURSE" && "btn-secondary",
event.type === "COURSE" && "btn-secondary",
eventCompleted(event, participant) && "btn-success",
)}
onClick={openModal}
@@ -219,7 +219,7 @@ const ModalBtn = ({
<button
className={cn(
"btn btn-info btn-outline btn-wide",
event.type === "OBLIGATED_COURSE" && "btn-secondary",
event.type === "COURSE" && "btn-secondary",
)}
onClick={async () => {
const data = selectAppointmentForm.getValues();

View File

@@ -1,6 +1,6 @@
import { prisma } from "@repo/db";
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
import { KursItem } from "./_components/item";
import { EventCard } from "./_components/item";
import { RocketIcon } from "@radix-ui/react-icons";
const page = async () => {
@@ -11,14 +11,14 @@ const page = async () => {
const events = await prisma.event.findMany({
include: {
appointments: {
Appointments: {
where: {
appointmentDate: {
gte: new Date(),
},
},
},
participants: {
Participants: {
where: {
userId: user.id,
},
@@ -69,7 +69,7 @@ const page = async () => {
{events.map((event) => {
return (
<KursItem
<EventCard
appointments={appointments}
selectedAppointments={userAppointments}
user={user}

View File

@@ -46,7 +46,7 @@ export default async function RootLayout({
<EmailVerification />
</div>
)}
<FirstPath />
{!session.user.pathSelected && <FirstPath />}
{children}
</div>
</div>

View File

@@ -0,0 +1,25 @@
// components/TanstackProvider.tsx
"use client";
import { toast } from "react-hot-toast";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactNode, useEffect, useState } from "react";
export function QueryProvider({ children }: { children: ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
mutations: {
onError: (error) => {
toast.error("An error occurred: " + (error as Error).message, {
position: "top-right",
});
},
},
},
}),
);
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}

View File

@@ -0,0 +1,46 @@
import { Prisma, prisma } from "@repo/db";
import { getServerSession } from "api/auth/[...nextauth]/auth";
import { NextResponse } from "next/server";
export async function GET(request: Request): Promise<NextResponse> {
try {
const session = await getServerSession();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const filter = JSON.parse(
new URL(request.url).searchParams.get("filter") || "{}",
) as Prisma.EventWhereInput;
const connectedAircraft = await prisma.event.findMany({
where: {
...filter, // Ensure filter is parsed correctly
},
include: {
Participants: {
where: {
userId: session.user.id,
},
},
Appointments: {
include: {
Participants: {
where: {
appointmentCancelled: false,
},
},
},
},
},
});
return NextResponse.json(connectedAircraft, {
status: 200,
});
} catch (error) {
console.error(error);
return NextResponse.json({ error: "Failed to fetch Aircrafts" }, { status: 500 });
}
}

View File

@@ -4,6 +4,7 @@ import { getServerSession } from "./api/auth/[...nextauth]/auth";
import { CustomErrorBoundary } from "_components/ErrorBoundary";
import { Toaster } from "react-hot-toast";
import "./globals.css";
import { QueryProvider } from "_components/QueryClient";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -33,7 +34,9 @@ const RootLayout = async ({
reverseOrder={false}
/>
</div>
<CustomErrorBoundary>{children}</CustomErrorBoundary>
<QueryProvider>
<CustomErrorBoundary>{children}</CustomErrorBoundary>
</QueryProvider>
</body>
</NextAuthSessionProvider>
</html>

View File

@@ -1,5 +1,23 @@
import { Event, Participant } from "@repo/db";
import { Event, EventAppointment, Participant, Prisma } from "@repo/db";
import axios from "axios";
import { da } from "date-fns/locale";
export const getEvents = async (filter: Prisma.EventWhereInput) => {
const { data } = await axios.get<
(Event & {
Appointments: (EventAppointment & {
Appointments: EventAppointment[];
Participants: Participant[];
})[];
Participants: Participant[];
})[]
>(`/api/event`, {
params: {
filter: JSON.stringify(filter),
},
});
return data;
};
export const eventCompleted = (event: Event, participant?: Participant) => {
if (!participant) return false;

View File

@@ -10,19 +10,29 @@
"lint": "next lint"
},
"dependencies": {
"@eslint/eslintrc": "^3",
"@hookform/resolvers": "^5.0.1",
"@next-auth/prisma-adapter": "^1.0.7",
"@radix-ui/react-icons": "^1.3.2",
"@repo/db": "workspace:*",
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@tanstack/react-query": "^5.79.0",
"@tailwindcss/postcss": "^4.1.8",
"@tanstack/react-query": "^5.79.2",
"@tanstack/react-table": "^8.21.3",
"@types/bcryptjs": "^3.0.0",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^22.15.29",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.5",
"@uiw/react-md-editor": "^4.0.7",
"axios": "^1.9.0",
"bcryptjs": "^3.0.2",
"clsx": "^2.1.1",
"daisyui": "^5.0.43",
"date-fns": "^4.1.0",
"eslint": "^9.15.0",
"eslint-config-next": "^15.3.3",
"i": "^0.3.7",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
@@ -31,6 +41,7 @@
"next-auth": "^4.24.11",
"next-remove-imports": "^1.0.12",
"npm": "^11.4.1",
"postcss": "^8.5.4",
"react": "^19.1.0",
"react-datepicker": "^8.4.0",
"react-day-picker": "^9.7.0",
@@ -40,20 +51,8 @@
"react-hot-toast": "^2.5.2",
"react-select": "^5.10.1",
"tailwind-merge": "^3.3.0",
"zod": "^3.25.46",
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4.1.8",
"@types/bcryptjs": "^3.0.0",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^22.15.29",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.5",
"daisyui": "^5.0.43",
"eslint": "^9.15.0",
"eslint-config-next": "^15.3.3",
"postcss": "^8.5.4",
"tailwindcss": "^4.1.8",
"typescript": "^5.8.3"
},
"devDependencies": {}
"typescript": "^5.8.3",
"zod": "^3.25.46"
}
}