From 2bd8a455c8587e100c48afc44c3388e238846e43 Mon Sep 17 00:00:00 2001
From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com>
Date: Wed, 25 Jun 2025 21:06:03 -0700
Subject: [PATCH] =?UTF-8?q?Pfadauswahl=20hinzugef=C3=BCgt?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../app/(app)/_components/FeaturedEvents.tsx | 18 +-
apps/hub/app/(app)/_components/FirstPath.tsx | 158 +++++++++++++-----
.../hub/app/(app)/events/_components/item.tsx | 12 +-
.../app/(app)/events/_components/modalBtn.tsx | 4 +-
apps/hub/app/(app)/events/page.tsx | 8 +-
apps/hub/app/(app)/layout.tsx | 2 +-
apps/hub/app/_components/QueryClient.tsx | 25 +++
apps/hub/app/api/event/route.ts | 46 +++++
apps/hub/app/layout.tsx | 5 +-
apps/hub/helper/events.ts | 20 ++-
apps/hub/package.json | 31 ++--
packages/database/prisma/schema/event.prisma | 11 +-
packages/database/prisma/schema/user.prisma | 1 +
pnpm-lock.yaml | 2 +-
14 files changed, 253 insertions(+), 90 deletions(-)
create mode 100644 apps/hub/app/_components/QueryClient.tsx
create mode 100644 apps/hub/app/api/event/route.ts
diff --git a/apps/hub/app/(app)/_components/FeaturedEvents.tsx b/apps/hub/app/(app)/_components/FeaturedEvents.tsx
index f7233595..5297c264 100644
--- a/apps/hub/app/(app)/_components/FeaturedEvents.tsx
+++ b/apps/hub/app/(app)/_components/FeaturedEvents.tsx
@@ -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 () => {
{filteredEvents.map((event) => {
return (
-
+
a.Participants.find((p) => p.userId == user.id),
)}
user={user}
diff --git a/apps/hub/app/(app)/_components/FirstPath.tsx b/apps/hub/app/(app)/_components/FirstPath.tsx
index e0aa0542..05aa2bb4 100644
--- a/apps/hub/app/(app)/_components/FirstPath.tsx
+++ b/apps/hub/app/(app)/_components/FirstPath.tsx
@@ -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 (
+ <>
+
+ {/* Disponent Card */}
+
setSelected("disponent")}
+ role="button"
+ tabIndex={0}
+ aria-pressed={selected === "disponent"}
+ >
+
+ Disponent
+
+
+ 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.
+
+ Teilnahme an Einführungsevent Nötig
+
+
+
+ {/* Pilot Card */}
+
setSelected("pilot")}
+ role="button"
+ tabIndex={0}
+ aria-pressed={selected === "pilot"}
+ >
+
+ Pilot
+
+
+ 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.
+
+
+
+ >
+ );
+};
+
+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 (
+
+ a.Participants.find((p) => p.userId == user.id),
+ )}
+ user={user}
+ event={event}
+ key={event.id}
+ />
+ );
+ });
+};
export const FirstPath = () => {
const modalRef = useRef(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 = () => {
Wähle deinen Einstieg!
Willkommen bei Virtual Air Rescue!
- Wähle deinen ersten Pfad aus. Du kannst später jederzeit auch den anderen Pfad
+ Wie möchtest du bei uns starten? Du kannst später jederzeit auch den anderen Pfad
ausprobieren, wenn du möchtest.
-
- {/* Disponent Card */}
-
setSelected("disponent")}
- role="button"
- tabIndex={0}
- aria-pressed={selected === "disponent"}
- >
-
Disponent
-
- 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.
-
- Teilnahme an Einführungsevent Nötig
-
+ {page === "path" &&
}
+ {page === "event-select" && (
+
+
+
Wähle dein Einführungs-Event aus:
+
- {/* Pilot Card */}
-
setSelected("pilot")}
- role="button"
- tabIndex={0}
- aria-pressed={selected === "pilot"}
- >
-
Pilot
-
- 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.
-
-
-
+ )}
- setPage("path")}>
+ Zurück
+
+ 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
-
+ {page === "path" ? "Weiter" : "Pfad auswählen"}
+
diff --git a/apps/hub/app/(app)/events/_components/item.tsx b/apps/hub/app/(app)/events/_components/item.tsx
index 8592d421..1267a2dd 100644
--- a/apps/hub/app/(app)/events/_components/item.tsx
+++ b/apps/hub/app/(app)/events/_components/item.tsx
@@ -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" && (
Zusatzqualifikation
)}
- {event.type === "OBLIGATED_COURSE" && (
-
Verpflichtend
+ {event.type === "EVENT" && (
+
Event
)}
@@ -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}`}
/>
diff --git a/apps/hub/app/(app)/events/_components/modalBtn.tsx b/apps/hub/app/(app)/events/_components/modalBtn.tsx
index a5748141..9abb65bf 100644
--- a/apps/hub/app/(app)/events/_components/modalBtn.tsx
+++ b/apps/hub/app/(app)/events/_components/modalBtn.tsx
@@ -92,7 +92,7 @@ const ModalBtn = ({
{
const data = selectAppointmentForm.getValues();
diff --git a/apps/hub/app/(app)/events/page.tsx b/apps/hub/app/(app)/events/page.tsx
index 9c18c206..eaa753f2 100644
--- a/apps/hub/app/(app)/events/page.tsx
+++ b/apps/hub/app/(app)/events/page.tsx
@@ -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 (
-
)}
-
+ {!session.user.pathSelected && }
{children}
diff --git a/apps/hub/app/_components/QueryClient.tsx b/apps/hub/app/_components/QueryClient.tsx
new file mode 100644
index 00000000..6f16d695
--- /dev/null
+++ b/apps/hub/app/_components/QueryClient.tsx
@@ -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 {children} ;
+}
diff --git a/apps/hub/app/api/event/route.ts b/apps/hub/app/api/event/route.ts
new file mode 100644
index 00000000..704842e6
--- /dev/null
+++ b/apps/hub/app/api/event/route.ts
@@ -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 {
+ 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 });
+ }
+}
diff --git a/apps/hub/app/layout.tsx b/apps/hub/app/layout.tsx
index 768c6a09..24f089f5 100644
--- a/apps/hub/app/layout.tsx
+++ b/apps/hub/app/layout.tsx
@@ -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}
/>
- {children}
+
+ {children}
+