remove appointment from events

This commit is contained in:
PxlLoewe
2026-01-18 01:09:39 +01:00
parent 606379d151
commit 9129652912
22 changed files with 105 additions and 1133 deletions

View File

@@ -1,34 +0,0 @@
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 <div>Event nicht gefunden</div>;
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 <div>Termin nicht gefunden</div>;
}
return (
<AppointmentForm event={event} initialAppointment={appointment} appointmentId={appointmentId} />
);
}

View File

@@ -40,11 +40,11 @@ export default async function Page({
</Link>
</div>
<p className="text-left text-2xl font-semibold">
<PersonIcon className="mr-2 inline h-5 w-5" /> Event-übersicht für{" "}
<PersonIcon className="mr-2 inline h-5 w-5" /> Event-Übersicht für{" "}
{`${user.firstname} ${user.lastname} #${user.publicId}`}
</p>
</div>
<ParticipantForm event={event} user={user} participant={participant} />
<ParticipantForm event={event} participant={participant} />
</div>
);
}

View File

@@ -1,260 +0,0 @@
"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<PaginatedTableRef>(null);
const form = useForm<EventAppointmentOptionalDefaults>({
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 (
<div className="bg-base-100 min-h-screen p-6">
<div className="mx-auto max-w-6xl">
{/* Header */}
<div className="mb-6">
<Link href={`/admin/event/${event.id}`} className="btn btn-ghost btn-sm mb-4">
<ArrowLeft className="h-4 w-4" /> Zurück
</Link>
<h1 className="text-3xl font-bold">
{appointmentId === "new" ? "Neuer Termin" : "Termin bearbeiten"}
</h1>
</div>
{/* Main Content */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Form Card */}
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 lg:col-span-1">
<div className="card bg-base-200 shadow-lg">
<div className="card-body">
<h2 className="card-title text-lg">
<Calendar className="h-5 w-5" /> Termin Details
</h2>
<div className="form-control">
<label className="label">
<span className="label-text font-semibold">Datum & Zeit</span>
</label>
<DateInput
value={new Date(form.watch("appointmentDate") || Date.now())}
onChange={(date) => form.setValue("appointmentDate", date)}
/>
{form.formState.errors.appointmentDate && (
<span className="text-error mt-1 text-sm">
{form.formState.errors.appointmentDate.message}
</span>
)}
</div>
<div className="divider" />
<div className="flex gap-2 pt-2">
<Button
type="submit"
isLoading={form.formState.isSubmitting}
className="btn btn-primary flex-1"
>
Speichern
</Button>
{appointmentId !== "new" && (
<Button
type="button"
onClick={handleDelete}
isLoading={form.formState.isSubmitting}
className="btn btn-error"
>
Löschen
</Button>
)}
</div>
</div>
</div>
</form>
{/* Participants Table */}
<div className="lg:col-span-2">
<div className="card bg-base-200 shadow-lg">
<div className="card-body">
<h2 className="card-title text-lg">
<Users className="h-5 w-5" /> Teilnehmer
</h2>
{appointmentId !== "new" ? (
<PaginatedTable
ref={participantTableRef}
prismaModel={"participant"}
getFilter={() =>
({
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 (
<span>{new Date(row.original.enscriptionDate).toLocaleString()}</span>
);
},
},
{
header: "Status",
cell: ({ row }) => {
if (row.original.attended) {
return <span className="badge badge-success">Anwesend</span>;
} else if (row.original.appointmentCancelled) {
return <span className="badge badge-error">Abgesagt</span>;
} else {
return <span className="badge badge-ghost">Offen</span>;
}
},
},
{
header: "Aktionen",
cell: ({ row }) => {
return (
<div className="flex gap-2">
<Link
href={`/admin/event/${event.id}/participant/${row.original.id}`}
className="btn btn-sm btn-outline"
>
Anzeigen
</Link>
{!row.original.attended && (
<button
type="button"
onClick={async () => {
await upsertParticipant({
eventId: event.id,
userId: row.original.userId,
attended: true,
appointmentCancelled: false,
});
participantTableRef.current?.refresh();
}}
className="btn btn-sm btn-info btn-outline"
>
</button>
)}
{!row.original.appointmentCancelled && (
<button
type="button"
onClick={async () => {
await upsertParticipant({
eventId: event.id,
userId: row.original.userId,
attended: false,
appointmentCancelled: true,
statusLog: [
...(row.original.statusLog as InputJsonValueType[]),
{
event: "Gefehlt an Event",
timestamp: new Date().toISOString(),
user: "Admin",
},
],
});
participantTableRef.current?.refresh();
}}
className="btn btn-sm btn-error btn-outline"
>
</button>
)}
</div>
);
},
},
] as ColumnDef<Participant>[]
}
/>
) : (
<div className="py-8 text-center text-gray-500">
Speichern Sie den Termin um Teilnehmer hinzuzufügen
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -1,203 +0,0 @@
import { Event, Participant, Prisma } from "@repo/db";
import { EventAppointmentOptionalDefaults, InputJsonValueType } from "@repo/db/zod";
import { ColumnDef } from "@tanstack/react-table";
import { useSession } from "next-auth/react";
import { RefObject, useRef } from "react";
import { UseFormReturn } from "react-hook-form";
import { PaginatedTable, PaginatedTableRef } from "../../../../_components/PaginatedTable";
import { Button } from "../../../../_components/ui/Button";
import { DateInput } from "../../../../_components/ui/DateInput";
import { upsertParticipant } from "../../../events/actions";
import { deleteAppoinement, upsertAppointment } from "../action";
import { handleParticipantFinished } from "../../../../../helper/events";
import toast from "react-hot-toast";
interface AppointmentModalProps {
event?: Event;
ref: RefObject<HTMLDialogElement | null>;
participantModal: RefObject<HTMLDialogElement | null>;
appointmentsTableRef: React.RefObject<PaginatedTableRef | null>;
appointmentForm: UseFormReturn<EventAppointmentOptionalDefaults>;
participantForm: UseFormReturn<Participant>;
}
export const AppointmentModal = ({
event,
ref,
participantModal,
appointmentsTableRef,
appointmentForm,
participantForm,
}: AppointmentModalProps) => {
const { data: session } = useSession();
const participantTableRef = useRef<PaginatedTableRef>(null);
return (
<dialog ref={ref} className="modal">
<div className="modal-box min-h-[500px] min-w-[900px]">
<form method="dialog">
{/* if there is a button in form, it will close the modal */}
<button
className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
onClick={() => ref.current?.close()}
>
</button>
</form>
<form
onSubmit={appointmentForm.handleSubmit(async (values) => {
if (!event) return;
await upsertAppointment(values);
ref.current?.close();
appointmentsTableRef.current?.refresh();
})}
className="flex flex-col"
>
<div className="mr-7 flex justify-between">
<h3 className="text-lg font-bold">Termin {appointmentForm.watch("id")}</h3>
<DateInput
value={new Date(appointmentForm.watch("appointmentDate") || Date.now())}
onChange={(date) => appointmentForm.setValue("appointmentDate", date)}
/>
</div>
<div>
<PaginatedTable
supressQuery={appointmentForm.watch("id") === undefined}
ref={participantTableRef}
columns={
[
{
accessorKey: "User.firstname",
header: "Vorname",
},
{
accessorKey: "User.lastname",
header: "Nachname",
},
{
accessorKey: "enscriptionDate",
header: "Einschreibedatum",
cell: ({ row }) => {
return <span>{new Date(row.original.enscriptionDate).toLocaleString()}</span>;
},
},
{
header: "Anwesend",
cell: ({ row }) => {
if (row.original.attended) {
return <span className="text-green-500">Ja</span>;
} else if (row.original.appointmentCancelled) {
return <span className="text-red-500">Nein (Termin abgesagt)</span>;
} else {
return <span>?</span>;
}
},
},
{
header: "Aktion",
cell: ({ row }) => {
return (
<div className="space-x-2">
<button
onClick={() => {
participantForm.reset(row.original);
participantModal.current?.showModal();
}}
className="btn btn-outline btn-sm"
>
anzeigen
</button>
{!row.original.attended && event?.hasPresenceEvents && (
<button
type="button"
onSubmit={() => {}}
onClick={async () => {
await upsertParticipant({
eventId: event!.id,
userId: row.original.userId,
attended: true,
appointmentCancelled: false,
});
if (!event.finisherMoodleCourseId?.length) {
toast(
"Teilnehmer hat das event abgeschlossen, workflow ausgeführt",
);
await handleParticipantFinished(row.original.id.toString());
}
participantTableRef.current?.refresh();
}}
className="btn btn-outline btn-info btn-sm"
>
Anwesend
</button>
)}
{!row.original.appointmentCancelled && event?.hasPresenceEvents && (
<button
type="button"
onSubmit={() => {}}
onClick={async () => {
await upsertParticipant({
eventId: event!.id,
userId: row.original.userId,
attended: false,
appointmentCancelled: true,
statusLog: [
...(row.original.statusLog as InputJsonValueType[]),
{
event: "Gefehlt an Event",
timestamp: new Date().toISOString(),
user: `${session?.user?.firstname} ${session?.user?.lastname} - ${session?.user?.publicId}`,
},
],
});
participantTableRef.current?.refresh();
}}
className="btn btn-outline btn-error btn-sm"
>
abwesend
</button>
)}
</div>
);
},
},
] as ColumnDef<Participant>[]
}
prismaModel={"participant"}
getFilter={() =>
({
eventAppointmentId: appointmentForm.watch("id")!,
}) as Prisma.ParticipantWhereInput
}
include={{ User: true }}
leftOfPagination={
<div className="flex gap-2">
<Button type="submit" className="btn btn-primary">
Speichern
</Button>
{appointmentForm.watch("id") && (
<Button
type="button"
onSubmit={() => {}}
onClick={async () => {
await deleteAppoinement(appointmentForm.watch("id")!);
ref.current?.close();
appointmentsTableRef.current?.refresh();
}}
className="btn btn-error btn-outline"
>
Löschen
</Button>
)}
</div>
}
/>
</div>
</form>
</div>
</dialog>
);
};

View File

@@ -1,34 +1,20 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { BADGES, Event, EVENT_TYPE, Participant, PERMISSION, Prisma, User } from "@repo/db";
import {
EventAppointmentOptionalDefaults,
EventAppointmentOptionalDefaultsSchema,
EventOptionalDefaults,
EventOptionalDefaultsSchema,
ParticipantSchema,
} from "@repo/db/zod";
import { Bot, Calendar, FileText, UserIcon } from "lucide-react";
import { useSession } from "next-auth/react";
import { BADGES, Event, EVENT_TYPE, PERMISSION } from "@repo/db";
import { EventOptionalDefaults, EventOptionalDefaultsSchema } from "@repo/db/zod";
import { Bot, FileText } from "lucide-react";
import { redirect } from "next/navigation";
import { useRef } from "react";
import "react-datepicker/dist/react-datepicker.css";
import { useForm } from "react-hook-form";
import { PaginatedTable, PaginatedTableRef } from "../../../../_components/PaginatedTable";
import { Button } from "../../../../_components/ui/Button";
import { Input } from "../../../../_components/ui/Input";
import { MarkdownEditor } from "../../../../_components/ui/MDEditor";
import { Select } from "../../../../_components/ui/Select";
import { Switch } from "../../../../_components/ui/Switch";
import { deleteEvent, upsertEvent } from "../action";
import { AppointmentModal } from "./AppointmentModal";
import { ParticipantModal } from "./ParticipantModal";
import { ColumnDef } from "@tanstack/react-table";
import toast from "react-hot-toast";
import Link from "next/link";
export const Form = ({ event }: { event?: Event }) => {
const { data: session } = useSession();
const form = useForm<EventOptionalDefaults>({
resolver: zodResolver(EventOptionalDefaultsSchema),
defaultValues: event
@@ -40,31 +26,9 @@ export const Form = ({ event }: { event?: Event }) => {
}
: undefined,
});
const appointmentForm = useForm<EventAppointmentOptionalDefaults>({
resolver: zodResolver(EventAppointmentOptionalDefaultsSchema),
defaultValues: {
eventId: event?.id,
presenterId: session?.user?.id,
},
});
const participantForm = useForm<Participant>({
resolver: zodResolver(ParticipantSchema),
});
const appointmentsTableRef = useRef<PaginatedTableRef>(null);
const appointmentModal = useRef<HTMLDialogElement>(null);
const participantModal = useRef<HTMLDialogElement>(null);
return (
<>
<AppointmentModal
participantModal={participantModal}
participantForm={participantForm}
appointmentForm={appointmentForm}
ref={appointmentModal}
appointmentsTableRef={appointmentsTableRef}
event={event}
/>
<ParticipantModal participantForm={participantForm} ref={participantModal} />
<form
onSubmit={form.handleSubmit(async (values) => {
await upsertEvent(values, event?.id);
@@ -147,216 +111,11 @@ export const Form = ({ event }: { event?: Event }) => {
valueAsNumber: true,
})}
/>
<Switch form={form} name="hasPresenceEvents" label="Hat Live Event" />
<div className="divider w-full" />
<Switch form={form} name="hidden" label="Event verstecken" />
</div>
</div>
{form.watch("hasPresenceEvents") ? (
<div className="card bg-base-200 col-span-6 shadow-xl">
<div className="card-body">
<PaginatedTable
ref={appointmentsTableRef}
prismaModel={"eventAppointment"}
getFilter={() =>
({
eventId: event?.id,
}) as Prisma.EventAppointmentWhereInput
}
include={{
Presenter: true,
Participants: true,
}}
leftOfSearch={
<h2 className="card-title">
<Calendar className="h-5 w-5" /> Termine
</h2>
}
rightOfSearch={
event && (
<button
className="btn btn-primary btn-outline"
onClick={() => {
appointmentModal.current?.showModal();
appointmentForm.reset({
id: undefined,
eventId: event.id,
presenterId: session?.user?.id,
});
}}
>
Hinzufügen
</button>
)
}
columns={
[
{
header: "Datum",
accessorKey: "appointmentDate",
accessorFn: (date) => new Date(date.appointmentDate).toLocaleString(),
},
{
header: "Presenter",
accessorKey: "presenter",
cell: ({ row }) => (
<div className="flex items-center">
<span className="ml-2">
{row.original.Presenter.firstname} {row.original.Presenter.lastname}
</span>
</div>
),
},
{
header: "Teilnehmer",
accessorKey: "Participants",
cell: ({ row }) => (
<div className="flex items-center">
<UserIcon className="h-5 w-5" />
<span className="ml-2">{row.original.Participants.length}</span>
</div>
),
},
{
header: "Aktionen",
cell: ({ row }) => {
return (
<div className="flex gap-2">
<button
onSubmit={() => false}
type="button"
onClick={() => {
appointmentForm.reset(row.original);
appointmentModal.current?.showModal();
}}
className="btn btn-sm btn-outline"
>
Bearbeiten
</button>
</div>
);
},
},
] as ColumnDef<
EventAppointmentOptionalDefaults & {
Presenter: User;
Participants: Participant[];
}
>[]
}
/>
</div>
</div>
) : null}
{!form.watch("hasPresenceEvents") ? (
<div className="card bg-base-200 col-span-6 shadow-xl">
<div className="card-body">
{
<PaginatedTable
leftOfSearch={
<h2 className="card-title">
<UserIcon className="h-5 w-5" /> Teilnehmer
</h2>
}
ref={appointmentsTableRef}
prismaModel={"participant"}
showSearch
getFilter={(searchTerm) =>
({
AND: [{ eventId: event?.id }],
OR: [
{
User: {
OR: [
{ firstname: { contains: searchTerm, mode: "insensitive" } },
{ lastname: { contains: searchTerm, mode: "insensitive" } },
{ publicId: { contains: searchTerm, mode: "insensitive" } },
],
},
},
],
}) as Prisma.ParticipantWhereInput
}
include={{
User: true,
}}
supressQuery={!event}
columns={
[
{
header: "Vorname",
accessorKey: "User.firstname",
cell: ({ row }) => {
return (
<Link
className="hover:underline"
href={`/admin/user/${row.original.User.id}`}
>
{row.original.User.firstname}
</Link>
);
},
},
{
header: "Nachname",
accessorKey: "User.lastname",
cell: ({ row }) => {
return (
<Link
className="hover:underline"
href={`/admin/user/${row.original.User.id}`}
>
{row.original.User.lastname}
</Link>
);
},
},
{
header: "VAR-Nummer",
accessorKey: "User.publicId",
cell: ({ row }) => {
return (
<Link
className="hover:underline"
href={`/admin/user/${row.original.User.id}`}
>
{row.original.User.publicId}
</Link>
);
},
},
{
header: "Moodle Kurs abgeschlossen",
accessorKey: "finisherMoodleCurseCompleted",
},
{
header: "Aktionen",
cell: ({ row }) => {
return (
<div className="flex gap-2">
<button
onSubmit={() => false}
type="button"
onClick={() => {
participantForm.reset(row.original);
participantModal.current?.showModal();
}}
className="btn btn-sm btn-outline"
>
Bearbeiten
</button>
</div>
);
},
},
] as ColumnDef<Participant & { User: User }>[]
}
/>
}
</div>
</div>
) : null}
<div className="card bg-base-200 col-span-6 shadow-xl">
<div className="card-body">
<div className="flex w-full gap-4">

View File

@@ -1,5 +1,5 @@
"use client";
import { Participant, Event, User, ParticipantLog, Prisma } from "@repo/db";
import { Participant, Event, 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";
@@ -16,19 +16,13 @@ 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) => {
export const ParticipantForm = ({ event, participant }: ParticipantFormProps) => {
const [isLoading, setIsLoading] = useState(false);
const queryClient = useQueryClient();
@@ -106,6 +100,7 @@ export const ParticipantForm = ({ event, participant, user }: ParticipantFormPro
name={"finisherMoodleCurseCompleted"}
label="Moodle Kurs abgeschlossen"
/>
<div></div>
</div>
</div>
{/* Info Card */}

View File

@@ -2,6 +2,7 @@
import { prisma, Prisma, Event, Participant } from "@repo/db";
//############# Event //#############
export const upsertEvent = async (event: Prisma.EventCreateInput, id?: Event["id"]) => {
const newEvent = id
? await prisma.event.update({
@@ -11,34 +12,22 @@ export const upsertEvent = async (event: Prisma.EventCreateInput, id?: Event["id
: await prisma.event.create({ data: event });
return newEvent;
};
export const deleteEvent = async (id: Event["id"]) => {
await prisma.event.delete({ where: { id: id } });
};
export const upsertAppointment = async (
eventAppointment: Prisma.EventAppointmentUncheckedCreateInput,
) => {
const newEventAppointment = eventAppointment.id
? await prisma.eventAppointment.update({
where: { id: eventAppointment.id },
data: eventAppointment,
//############# Participant //#############
export const upsertParticipant = async (participant: Prisma.ParticipantUncheckedCreateInput) => {
const newParticipant = participant.id
? await prisma.participant.update({
where: { id: participant.id },
data: participant,
})
: await prisma.eventAppointment.create({ data: eventAppointment });
return newEventAppointment;
: await prisma.participant.create({ data: participant });
return newParticipant;
};
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 deleteParticipant = async (id: Participant["id"]) => {
await prisma.participant.delete({ where: { id: id } });
};