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. -
-
-
+ )}
- + + {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 = ({
)} - + {!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} + diff --git a/apps/hub/helper/events.ts b/apps/hub/helper/events.ts index 3668de49..bee9495e 100644 --- a/apps/hub/helper/events.ts +++ b/apps/hub/helper/events.ts @@ -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; diff --git a/apps/hub/package.json b/apps/hub/package.json index fb41a300..5bfd9484 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -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" + } } diff --git a/packages/database/prisma/schema/event.prisma b/packages/database/prisma/schema/event.prisma index 2262d6a7..6b232040 100644 --- a/packages/database/prisma/schema/event.prisma +++ b/packages/database/prisma/schema/event.prisma @@ -1,6 +1,7 @@ enum EVENT_TYPE { COURSE - OBLIGATED_COURSE + PILOT_STARTER + DISPATCH_STARTER EVENT } @@ -12,7 +13,7 @@ model EventAppointment { // relations: Users User[] @relation("EventAppointmentUser") Participants Participant[] - Event Event @relation(fields: [eventId], references: [id]) + Event Event @relation(fields: [eventId], references: [id], onDelete: Cascade) Presenter User @relation(fields: [presenterId], references: [id]) } @@ -29,7 +30,7 @@ model Participant { eventId Int // relations: User User @relation(fields: [userId], references: [id], onDelete: Cascade) - Event Event @relation(fields: [eventId], references: [id]) + Event Event @relation(fields: [eventId], references: [id], onDelete: Cascade) EventAppointment EventAppointment? @relation(fields: [eventAppointmentId], references: [id]) } @@ -48,8 +49,8 @@ model Event { hidden Boolean @default(true) // relations: - participants Participant[] - appointments EventAppointment[] + Participants Participant[] + Appointments EventAppointment[] } model File { diff --git a/packages/database/prisma/schema/user.prisma b/packages/database/prisma/schema/user.prisma index 9d55a584..7fbc6b6c 100644 --- a/packages/database/prisma/schema/user.prisma +++ b/packages/database/prisma/schema/user.prisma @@ -33,6 +33,7 @@ model User { moodleId Int? @map(name: "moodle_id") // Settings: + pathSelected Boolean @default(false) settingsNtfyRoom String? @map(name: "settings_ntfy_room") settingsMicDevice String? @map(name: "settings_mic_device") settingsMicVolume Float? @map(name: "settings_mic_volume") diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16ed81e9..0848b006 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -342,7 +342,7 @@ importers: specifier: ^4.1.8 version: 4.1.8 '@tanstack/react-query': - specifier: ^5.79.0 + specifier: ^5.79.2 version: 5.79.2(react@19.1.0) '@tanstack/react-table': specifier: ^8.21.3