diff --git a/apps/hub/app/(app)/admin/event/[id]/page.tsx b/apps/hub/app/(app)/admin/event/[id]/page.tsx index 9188846c..8583a4d5 100644 --- a/apps/hub/app/(app)/admin/event/[id]/page.tsx +++ b/apps/hub/app/(app)/admin/event/[id]/page.tsx @@ -8,6 +8,19 @@ export default async ({ params }: { params: Promise<{ id: string }> }) => { id: parseInt(id), }, }); + const users = await prisma.user.findMany({ + select: { + id: true, + firstname: true, + lastname: true, + publicId: true, + }, + }); + const appointments = await prisma.eventAppointment.findMany({ + where: { + eventId: parseInt(id), + }, + }); if (!event) return
Event not found
; - return
; + return ; }; diff --git a/apps/hub/app/(app)/admin/event/_components/Form.tsx b/apps/hub/app/(app)/admin/event/_components/Form.tsx index 47bae6f4..57375138 100644 --- a/apps/hub/app/(app)/admin/event/_components/Form.tsx +++ b/apps/hub/app/(app)/admin/event/_components/Form.tsx @@ -1,28 +1,97 @@ 'use client'; import { zodResolver } from '@hookform/resolvers/zod'; -import { EventOptionalDefaultsSchema } from '@repo/db/zod'; +import { + EventAppointmentOptionalDefaults, + EventAppointmentOptionalDefaultsSchema, + EventOptionalDefaults, + EventOptionalDefaultsSchema, + ParticipantOptionalDefaultsSchema, +} from '@repo/db/zod'; import { set, useForm } from 'react-hook-form'; -import { z } from 'zod'; -import { BosUse, Country, Event, prisma } from '@repo/db'; -import { FileText, LocateIcon, PlaneIcon, UserIcon } from 'lucide-react'; +import { BADGES, Event, EventAppointment, User } from '@repo/db'; +import { Bot, Calendar, FileText, UserIcon } from 'lucide-react'; import { Input } from '../../../../_components/ui/Input'; -import { useState } from 'react'; -import { deleteEvent, upsertEvent } from '../action'; +import { useRef, useState } from 'react'; +import { deleteEvent, upsertAppointment, upsertEvent } from '../action'; import { Button } from '../../../../_components/ui/Button'; -import { redirect } from 'next/navigation'; +import { redirect, useRouter } from 'next/navigation'; import { Switch } from '../../../../_components/ui/Switch'; -import { PaginatedTable } from '../../../../_components/PaginatedTable'; +import { + PaginatedTable, + PaginatedTableRef, +} from '../../../../_components/PaginatedTable'; +import { Select } from '../../../../_components/ui/Select'; +import { useSession } from 'next-auth/react'; -export const Form = ({ event }: { event?: Event }) => { - const form = useForm>({ +export const Form = ({ + event, + users, + appointments = [], +}: { + event?: Event; + users: { + id: string; + firstname: string; + lastname: string; + publicId: string; + }[]; + appointments?: EventAppointment[]; +}) => { + const { data: session } = useSession(); + const form = useForm({ resolver: zodResolver(EventOptionalDefaultsSchema), defaultValues: event, }); + const appointmentForm = useForm({ + resolver: zodResolver(EventAppointmentOptionalDefaultsSchema), + defaultValues: { + eventId: event?.id, + presenterId: session?.user?.id, + }, + }); + const appointmentsTableRef = useRef(null); const [loading, setLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); - console.log(form.formState.errors); + const addParticipantModal = useRef(null); return ( <> + +
+ + {/* if there is a button in form, it will close the modal */} + + +

Termin hinzufügen

+
{ + if (!event) return; + const createdAppointment = await upsertAppointment({ + appointmentDate: values.appointmentDate, + eventId: event?.id, + presenterId: values.presenterId, + }); + console.log(createdAppointment); + addParticipantModal.current?.close(); + appointmentsTableRef.current?.refresh(); + })} + className="flex flex-col" + > + +
+ +
+
+
+
{ setLoading(true); @@ -44,41 +113,134 @@ export const Form = ({ event }: { event?: Event }) => { name="description" className="input-sm" /> +

- Teilnehmer + Automation

+ + + ({ + label: value, + value: key, + }))} + /> + +
+
+
+
+
+

+ Termine +

+ +
+ new Date(date.appointmentDate).toLocaleDateString(), }, { - header: 'Nachname', - accessorKey: 'user.lastname', + header: 'Presenter', + accessorKey: 'presenter', + cell: ({ row }) => ( +
+ + {(row.original as any).Presenter.firstname}{' '} + {(row.original as any).Presenter.lastname} + +
+ ), }, { - header: 'VAR ID', - accessorKey: 'user.publicId', + header: 'Teilnehmer', + accessorKey: 'Participants', + cell: ({ row }) => ( +
+ + + {row.original.Participants.length} + +
+ ), }, { - header: 'Status', - accessorKey: 'status', + header: 'Aktionen', + cell: ({ row }) => { + return ( +
+ +
+ ); + }, }, ]} /> diff --git a/apps/hub/app/(app)/admin/event/action.ts b/apps/hub/app/(app)/admin/event/action.ts index 4e21c109..6554c78d 100644 --- a/apps/hub/app/(app)/admin/event/action.ts +++ b/apps/hub/app/(app)/admin/event/action.ts @@ -1,6 +1,6 @@ 'use server'; -import { prisma, Prisma, Event } from '@repo/db'; +import { prisma, Prisma, Event, Participant, EventAppointment } from '@repo/db'; export const upsertEvent = async ( event: Prisma.EventCreateInput, @@ -18,3 +18,48 @@ export const upsertEvent = async ( export const deleteEvent = async (id: Event['id']) => { await prisma.event.delete({ where: { id: id } }); }; + +export const upsertAppointment = async ( + eventAppointment: Prisma.XOR< + Prisma.EventAppointmentCreateInput, + Prisma.EventAppointmentUncheckedCreateInput + >, + id?: EventAppointment['id'] +) => { + const newEventAppointment = id + ? await prisma.eventAppointment.update({ + where: { id: id }, + data: eventAppointment, + }) + : await prisma.eventAppointment.create({ data: eventAppointment }); + return newEventAppointment; +}; + +export const deleteAppoinement = async (id: Event['id']) => { + await prisma.eventAppointment.delete({ where: { id: id } }); + prisma.eventAppointment.findMany({ + where: { + eventId: id, + }, + orderBy: { + // TODO: add order by in relation to table selected column + }, + }); +}; + +export const upsertParticipant = async ( + participant: Prisma.ParticipantCreateInput, + id?: Participant['id'] +) => { + const newParticipant = id + ? await prisma.participant.update({ + where: { id: id }, + data: participant, + }) + : await prisma.participant.create({ data: participant }); + return newParticipant; +}; + +export const deleteParticipant = async (id: Participant['id']) => { + await prisma.participant.delete({ where: { id: id } }); +}; diff --git a/apps/hub/app/(app)/admin/event/new/page.tsx b/apps/hub/app/(app)/admin/event/new/page.tsx index 0f421071..e942088b 100644 --- a/apps/hub/app/(app)/admin/event/new/page.tsx +++ b/apps/hub/app/(app)/admin/event/new/page.tsx @@ -1,5 +1,14 @@ +import { prisma } from '@repo/db'; import { Form } from '../_components/Form'; -export default () => { - return ; +export default async () => { + const users = await prisma.user.findMany({ + select: { + id: true, + firstname: true, + lastname: true, + publicId: true, + }, + }); + return ; }; diff --git a/apps/hub/app/(app)/admin/event/page.tsx b/apps/hub/app/(app)/admin/event/page.tsx index eab36849..f1ac44a5 100644 --- a/apps/hub/app/(app)/admin/event/page.tsx +++ b/apps/hub/app/(app)/admin/event/page.tsx @@ -5,16 +5,6 @@ import Link from 'next/link'; export default () => { return ( <> -

- - Events - - - - -

{ accessorKey: 'hidden', }, ]} + leftOfSearch={ + + Events + + } + rightOfSearch={ + + + + } /> ); diff --git a/apps/hub/app/(app)/admin/station/page.tsx b/apps/hub/app/(app)/admin/station/page.tsx index e525ebf6..39a98b33 100644 --- a/apps/hub/app/(app)/admin/station/page.tsx +++ b/apps/hub/app/(app)/admin/station/page.tsx @@ -1,42 +1,46 @@ -import { DatabaseBackupIcon } from "lucide-react"; -import { PaginatedTable } from "../../../_components/PaginatedTable"; -import Link from "next/link"; +import { DatabaseBackupIcon } from 'lucide-react'; +import { PaginatedTable } from '../../../_components/PaginatedTable'; +import Link from 'next/link'; export default () => { return ( <> -

- - Stationen - - - - -

+ Stationen + + } + rightOfSearch={ +

+ + + +

+ } /> ); diff --git a/apps/hub/app/(app)/admin/user/page.tsx b/apps/hub/app/(app)/admin/user/page.tsx index 02ee1736..d423e913 100644 --- a/apps/hub/app/(app)/admin/user/page.tsx +++ b/apps/hub/app/(app)/admin/user/page.tsx @@ -1,34 +1,36 @@ -import { User2 } from "lucide-react"; -import { PaginatedTable } from "../../../_components/PaginatedTable"; +import { User2 } from 'lucide-react'; +import { PaginatedTable } from '../../../_components/PaginatedTable'; export default async () => { return ( <> -

- Benutzer -

+ Benutzer +

+ } /> ); diff --git a/apps/hub/app/(app)/events/_components/item.tsx b/apps/hub/app/(app)/events/_components/item.tsx new file mode 100644 index 00000000..09168b7b --- /dev/null +++ b/apps/hub/app/(app)/events/_components/item.tsx @@ -0,0 +1,93 @@ +"use client"; +import { DrawingPinFilledIcon, EnterIcon } from "@radix-ui/react-icons"; +import { User } from "@repo/db"; +import ModalBtn from "./modalBtn"; + +export const KursItem = ({ + user, + title, + type, + beschreibung, + badge, + moodleReq, +}: { + user: User; + title: string; + type: string; + beschreibung: string; + badge: string; + moodleReq: boolean; +}) => { + return ( +
+
+
+

{title}

+
+ + Zusatzqualifikation + +
+
+
+

{beschreibung}

+
+
{badge}
+
+
+

+ Teilnahmevoraussetzungen: + + Moodle Kurs /MOODLEKURSTITLE\ + +

+ +
+
+
+
+ ); +}; + +export const PilotKurs = ({ user }: { user: User }) => { + { + /* STATISCH, DA FÜR ALLE NEUEN MITGLIEDER MANDATORY, WIRD AUSGEBLENDET WENN ABSOLVIERT */ + } + return ( +
+
+
+

Einsteigerkurs für Piloten

+
+ + Verpflichtend + +
+
+
+

+ In diesem Kurs lernen Piloten die Grundlagen der Luftrettung, + Einsatzverfahren, den Umgang mit dem BOS-Funk und einige + medizinische Basics. Der Kurs bietet eine ideale Vorbereitung + für alle Standard Operations bei Virtual Air Rescue. +

+
+
Badge
+
+
+

+ Teilnahmevoraussetzungen: Keine +

+ +
+
+
+
+ ); +}; diff --git a/apps/hub/app/(app)/events/_components/modalBtn.tsx b/apps/hub/app/(app)/events/_components/modalBtn.tsx new file mode 100644 index 00000000..54a05b43 --- /dev/null +++ b/apps/hub/app/(app)/events/_components/modalBtn.tsx @@ -0,0 +1,87 @@ +"use client"; +import { useEffect } from "react"; +import { + CheckCircledIcon, + CalendarIcon, + EnterIcon, +} from "@radix-ui/react-icons"; + +interface ModalBtnProps { + title: string; + dates: string[]; + modalId: string; +} + +const ModalBtn = ({ title, dates, modalId }: ModalBtnProps) => { + useEffect(() => { + const modal = document.getElementById(modalId) as HTMLDialogElement; + const handleOpen = () => { + document.body.classList.add("modal-open"); + }; + const handleClose = () => { + document.body.classList.remove("modal-open"); + }; + modal?.addEventListener("show", handleOpen); + modal?.addEventListener("close", handleClose); + return () => { + modal?.removeEventListener("show", handleOpen); + modal?.removeEventListener("close", handleClose); + }; + }, [modalId]); + + const openModal = () => { + const modal = document.getElementById(modalId) as HTMLDialogElement; + document.body.classList.add("modal-open"); + modal?.showModal(); + }; + + const closeModal = () => { + const modal = document.getElementById(modalId) as HTMLDialogElement; + document.body.classList.remove("modal-open"); + modal?.close(); + }; + + return ( + <> + + +
+

{title}

+

+ + Moodle Kurs abgeschlossen +

+
+
+ + +
+

+ Bitte finde dich an diesem Termin in unserem Discord ein. +

+
+
+ + +
+
+ +
+ + ); +}; + +export default ModalBtn; diff --git a/apps/hub/app/(app)/events/page.tsx b/apps/hub/app/(app)/events/page.tsx new file mode 100644 index 00000000..81d51f59 --- /dev/null +++ b/apps/hub/app/(app)/events/page.tsx @@ -0,0 +1,45 @@ +import { getServerSession } from "../../api/auth/[...nextauth]/auth"; +import { PrismaClient } from "@repo/db"; +import { PilotKurs, KursItem } from "./_components/item"; +import { + RocketIcon, + DrawingPinFilledIcon, + EnterIcon, +} from "@radix-ui/react-icons"; + +export default async () => { + const prisma = new PrismaClient(); + const session = await getServerSession(); + if (!session) return null; + const user = await prisma.user.findFirst({ + where: { + id: session.user.id, + }, + }); + if (!user) return null; + return ( +
+
+

+ Events & Kurse +

+
+ + +
+ ); +}; diff --git a/apps/hub/app/(app)/layout.tsx b/apps/hub/app/(app)/layout.tsx index d169595b..28c701a2 100644 --- a/apps/hub/app/(app)/layout.tsx +++ b/apps/hub/app/(app)/layout.tsx @@ -4,8 +4,11 @@ import { InstagramLogoIcon, ReaderIcon, } from "@radix-ui/react-icons"; -import { HorizontalNav, VerticalNav } from "../_components/ui/Nav"; +import { HorizontalNav, VerticalNav } from "../_components/Nav"; import { Toaster } from "react-hot-toast"; +import { redirect } from "next/navigation"; +import { getServerSession } from "../api/auth/[...nextauth]/auth"; +import { headers } from "next/headers"; export const metadata: Metadata = { title: "Create Next App", @@ -17,6 +20,10 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { + const session = await getServerSession(); + + if (!session) redirect(`/login`); + return (
{/* Hauptlayout: Sidebar + Content (nimmt Resthöhe ein) */} -
+
{/* Linke Sidebar */} {/* Scrollbarer Content-Bereich */} -
+
{children}
diff --git a/apps/hub/app/(app)/settings/_components/forms.tsx b/apps/hub/app/(app)/settings/_components/forms.tsx index 2f1fd89b..b11b1ad1 100644 --- a/apps/hub/app/(app)/settings/_components/forms.tsx +++ b/apps/hub/app/(app)/settings/_components/forms.tsx @@ -1,14 +1,14 @@ -'use client'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { DiscordAccount, User } from '@repo/db'; -import { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; -import { unlinkDiscord, updateUser, changePassword } from '../actions'; -import { Toaster, toast } from 'react-hot-toast'; -import { useSession } from 'next-auth/react'; -import { useRouter } from 'next/navigation'; -import { Button } from '../../../_components/ui/Button'; +"use client"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { DiscordAccount, User } from "@repo/db"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { unlinkDiscord, updateUser, changePassword } from "../actions"; +import { Toaster, toast } from "react-hot-toast"; +import { useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { Button } from "../../../_components/ui/Button"; import { PersonIcon, EnvelopeClosedIcon, @@ -20,14 +20,14 @@ import { LockClosedIcon, LockOpen2Icon, LockOpen1Icon, -} from '@radix-ui/react-icons'; +} from "@radix-ui/react-icons"; export const ProfileForm = ({ user }: { user: User }) => { const schema = z.object({ firstname: z.string().min(2).max(30), lastname: z.string().min(2).max(30), email: z.string().email({ - message: 'Bitte gebe eine gültige E-Mail Adresse ein', + message: "Bitte gebe eine gültige E-Mail Adresse ein", }), }); const [isLoading, setIsLoading] = useState(false); @@ -49,12 +49,10 @@ export const ProfileForm = ({ user }: { user: User }) => { await updateUser(values); form.reset(values); setIsLoading(false); - toast.success('Deine Änderungen wurden gespeichert!', { + toast.success("Deine Änderungen wurden gespeichert!", { style: { - background: - 'var(--fallback-b1, oklch(var(--b1) / var(--tw-bg-opacity, 1)))', - color: - 'var(--fallback-nc, oklch(var(--nc) / var(--tw-text-opacity, 1)))', + background: "var(--color-base-100)", + color: "var(--color-base-content)", }, }); })} @@ -62,15 +60,13 @@ export const ProfileForm = ({ user }: { user: User }) => {

Persönliche Informationen

-
-
-