From 606379d151c1445c956c60721b72724ae68cb5d2 Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Sun, 18 Jan 2026 01:01:15 +0100 Subject: [PATCH] Remove Event-Appointment --- .../[id]/appointment/[appointmentId]/page.tsx | 34 +++ .../[id]/participant/[participantId]/page.tsx | 50 ++++ .../event/_components/AppointmentForm.tsx | 260 ++++++++++++++++++ .../event/_components/ParticipantForm.tsx | 218 +++++++++++++++ 4 files changed, 562 insertions(+) create mode 100644 apps/hub/app/(app)/admin/event/[id]/appointment/[appointmentId]/page.tsx create mode 100644 apps/hub/app/(app)/admin/event/[id]/participant/[participantId]/page.tsx create mode 100644 apps/hub/app/(app)/admin/event/_components/AppointmentForm.tsx create mode 100644 apps/hub/app/(app)/admin/event/_components/ParticipantForm.tsx diff --git a/apps/hub/app/(app)/admin/event/[id]/appointment/[appointmentId]/page.tsx b/apps/hub/app/(app)/admin/event/[id]/appointment/[appointmentId]/page.tsx new file mode 100644 index 00000000..b3373878 --- /dev/null +++ b/apps/hub/app/(app)/admin/event/[id]/appointment/[appointmentId]/page.tsx @@ -0,0 +1,34 @@ +import { AppointmentForm } from "(app)/admin/event/_components/AppointmentForm"; +import { prisma } from "@repo/db"; + +export default async function Page({ + params, +}: { + params: Promise<{ id: string; appointmentId: string }>; +}) { + const { id: eventId, appointmentId } = await params; + + const event = await prisma.event.findUnique({ + where: { id: parseInt(eventId) }, + }); + + if (!event) return
Event nicht gefunden
; + + let appointment = null; + if (appointmentId !== "new") { + appointment = await prisma.eventAppointment.findUnique({ + where: { id: parseInt(appointmentId) }, + include: { + Presenter: true, + Participants: { + include: { User: true }, + }, + }, + }); + if (!appointment) return
Termin nicht gefunden
; + } + + return ( + + ); +} diff --git a/apps/hub/app/(app)/admin/event/[id]/participant/[participantId]/page.tsx b/apps/hub/app/(app)/admin/event/[id]/participant/[participantId]/page.tsx new file mode 100644 index 00000000..1cfe0d6c --- /dev/null +++ b/apps/hub/app/(app)/admin/event/[id]/participant/[participantId]/page.tsx @@ -0,0 +1,50 @@ +import { prisma } from "@repo/db"; +import { ParticipantForm } from "../../../_components/ParticipantForm"; +import { Error } from "_components/Error"; +import Link from "next/link"; +import { PersonIcon } from "@radix-ui/react-icons"; +import { ArrowLeft } from "lucide-react"; + +export default async function Page({ + params, +}: { + params: Promise<{ id: string; participantId: string }>; +}) { + const { id: eventId, participantId } = await params; + + const event = await prisma.event.findUnique({ + where: { id: parseInt(eventId) }, + }); + + const participant = await prisma.participant.findUnique({ + where: { id: parseInt(participantId) }, + }); + + const user = await prisma.user.findUnique({ + where: { id: participant?.userId }, + }); + + if (!event) return
Event nicht gefunden
; + + if (!participant || !user) { + return ; + } + + return ( +
+
+
+ + + Zurück zum Event + +
+

+ Event-übersicht für{" "} + {`${user.firstname} ${user.lastname} #${user.publicId}`} +

+
+ +
+ ); +} diff --git a/apps/hub/app/(app)/admin/event/_components/AppointmentForm.tsx b/apps/hub/app/(app)/admin/event/_components/AppointmentForm.tsx new file mode 100644 index 00000000..cae6f8c3 --- /dev/null +++ b/apps/hub/app/(app)/admin/event/_components/AppointmentForm.tsx @@ -0,0 +1,260 @@ +"use client"; +import { + EventAppointmentOptionalDefaults, + EventAppointmentOptionalDefaultsSchema, +} from "@repo/db/zod"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRef } from "react"; +import { ColumnDef } from "@tanstack/react-table"; +import { Event, EventAppointment, Participant, Prisma } from "@repo/db"; +import { ArrowLeft, Calendar, Users } from "lucide-react"; +import toast from "react-hot-toast"; +import Link from "next/link"; +import { PaginatedTable, PaginatedTableRef } from "../../../_components/PaginatedTable"; +import { Button } from "../../../_components/ui/Button"; +import { DateInput } from "../../../_components/ui/DateInput"; +import { upsertParticipant } from "../../events/actions"; +import { upsertAppointment, deleteAppoinement } from "../action"; +import { InputJsonValueType } from "@repo/db/zod"; + +interface AppointmentFormProps { + event: Event; + initialAppointment: + | (EventAppointment & { + Presenter?: any; + Participants?: (Participant & { User?: any })[]; + }) + | null; + appointmentId: string; +} + +export const AppointmentForm = ({ + event, + initialAppointment, + appointmentId, +}: AppointmentFormProps) => { + const router = useRouter(); + const participantTableRef = useRef(null); + + const form = useForm({ + resolver: zodResolver(EventAppointmentOptionalDefaultsSchema), + defaultValues: initialAppointment + ? { + ...initialAppointment, + eventId: event.id, + presenterId: initialAppointment.presenterId, + } + : undefined, + }); + + const onSubmit = async (values: EventAppointmentOptionalDefaults) => { + try { + await upsertAppointment({ + ...values, + eventId: event.id, + }); + toast.success("Termin erfolgreich gespeichert"); + router.back(); + } catch { + toast.error("Fehler beim Speichern des Termins"); + } + }; + + const handleDelete = async () => { + if (!confirm("Wirklich löschen?")) return; + try { + await deleteAppoinement(parseInt(appointmentId)); + toast.success("Termin gelöscht"); + router.back(); + } catch { + toast.error("Fehler beim Löschen"); + } + }; + + return ( +
+
+ {/* Header */} +
+ + Zurück + +

+ {appointmentId === "new" ? "Neuer Termin" : "Termin bearbeiten"} +

+
+ + {/* Main Content */} +
+ {/* Form Card */} +
+
+
+

+ Termin Details +

+ +
+ + form.setValue("appointmentDate", date)} + /> + {form.formState.errors.appointmentDate && ( + + {form.formState.errors.appointmentDate.message} + + )} +
+ +
+ +
+ + {appointmentId !== "new" && ( + + )} +
+
+
+ + + {/* Participants Table */} +
+
+
+

+ Teilnehmer +

+ + {appointmentId !== "new" ? ( + + ({ + eventAppointmentId: appointmentId, + }) as Prisma.ParticipantWhereInput + } + include={{ User: true }} + columns={ + [ + { + accessorKey: "User.firstname", + header: "Vorname", + }, + { + accessorKey: "User.lastname", + header: "Nachname", + }, + { + accessorKey: "enscriptionDate", + header: "Einschreibedatum", + cell: ({ row }) => { + return ( + {new Date(row.original.enscriptionDate).toLocaleString()} + ); + }, + }, + { + header: "Status", + cell: ({ row }) => { + if (row.original.attended) { + return Anwesend; + } else if (row.original.appointmentCancelled) { + return Abgesagt; + } else { + return Offen; + } + }, + }, + { + header: "Aktionen", + cell: ({ row }) => { + return ( +
+ + Anzeigen + + {!row.original.attended && ( + + )} + {!row.original.appointmentCancelled && ( + + )} +
+ ); + }, + }, + ] as ColumnDef[] + } + /> + ) : ( +
+ Speichern Sie den Termin um Teilnehmer hinzuzufügen +
+ )} +
+
+
+
+
+
+ ); +}; diff --git a/apps/hub/app/(app)/admin/event/_components/ParticipantForm.tsx b/apps/hub/app/(app)/admin/event/_components/ParticipantForm.tsx new file mode 100644 index 00000000..39dc62e3 --- /dev/null +++ b/apps/hub/app/(app)/admin/event/_components/ParticipantForm.tsx @@ -0,0 +1,218 @@ +"use client"; +import { Participant, Event, User, ParticipantLog, Prisma } from "@repo/db"; +import { Users, Activity, Bug } from "lucide-react"; +import toast from "react-hot-toast"; +import { InputJsonValueType, ParticipantOptionalDefaultsSchema } from "@repo/db/zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Switch } from "_components/ui/Switch"; +import { useState } from "react"; +import { Button } from "_components/ui/Button"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { upsertParticipant } from "(app)/events/actions"; +import { deleteParticipant } from "../action"; +import { redirect } from "next/navigation"; + +interface ParticipantFormProps { + event: Event; + participant: Participant; + user: User; +} + +const checkEventCompleted = (participant: Participant, event: Event): boolean => { + if (event.hasPresenceEvents) { + return event.finisherMoodleCourseId + ? participant.finisherMoodleCurseCompleted && participant.attended + : !!participant.attended; + } + return event.finisherMoodleCourseId ? participant.finisherMoodleCurseCompleted : false; +}; + +export const ParticipantForm = ({ event, participant, user }: ParticipantFormProps) => { + const [isLoading, setIsLoading] = useState(false); + const queryClient = useQueryClient(); + + const upsertParticipantMutation = useMutation({ + mutationKey: ["upsertParticipant"], + mutationFn: async (newData: Prisma.ParticipantUncheckedCreateInput) => { + const data = await upsertParticipant(newData); + await queryClient.invalidateQueries({ queryKey: ["participants", event.id] }); + return data; + }, + }); + + const deleteParticipantMutation = useMutation({ + mutationKey: ["deleteParticipant"], + mutationFn: async (participantId: number) => { + await deleteParticipant(participantId); + await queryClient.invalidateQueries({ queryKey: ["participants", event.id] }); + }, + }); + + const eventCompleted = checkEventCompleted(participant, event); + + const form = useForm({ + resolver: zodResolver(ParticipantOptionalDefaultsSchema), + defaultValues: participant, + }); + + const handleEventFinished = async () => { + setIsLoading(true); + try { + await upsertParticipantMutation.mutateAsync({ + eventId: event.id, + userId: participant.userId, + }); + toast.success("Event als beendet markiert"); + } finally { + setIsLoading(false); + } + }; + + const handleCheckMoodle = async () => { + setIsLoading(true); + try { + toast.success("Moodle-Check durchgeführt"); + } finally { + setIsLoading(false); + } + }; + + return ( +
{ + const data = await upsertParticipantMutation.mutateAsync({ + ...formData, + statusLog: participant.statusLog as unknown as Prisma.ParticipantCreatestatusLogInput, + eventId: event.id, + userId: participant.userId, + }); + form.reset(data); + toast.success("Teilnehmer aktualisiert"); + })} + className="bg-base-100 flex flex-wrap gap-6 p-6" + > + {/* Status Section */} +
+
+

+ + Debug +

+ + + +
+
+ {/* Info Card */} +
+
+

+ Informationen +

+
+
+

Kurs-status

+

{eventCompleted ? "Abgeschlossen" : "In Bearbeitung"}

+

Einschreibedatum

+

+ {participant?.enscriptionDate + ? new Date(participant.enscriptionDate).toLocaleDateString() + : "-"} +

+
+ +
+ +
+ + +
+
+
+ + {/* Activity Log */} +
+
+

+ Aktivitätslog +

+ +
+ + + + + + + + + + {participant?.statusLog && + Array.isArray(participant.statusLog) && + (participant.statusLog as InputJsonValueType[]) + .slice() + .reverse() + .map((log: InputJsonValueType, index: number) => { + const logEntry = log as unknown as ParticipantLog; + return ( + + + + + + ); + })} + +
DatumEventUser
+ {logEntry.timestamp + ? new Date(logEntry.timestamp).toLocaleDateString() + : "-"} + {logEntry.event || "-"}{logEntry.user || "-"}
+
+
+
+
+
+
+ + {event && ( + + )} +
+
+
+ + ); +};