remove appointment from events
This commit is contained in:
@@ -23,15 +23,6 @@ const page = async () => {
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
Appointments: {
|
||||
include: {
|
||||
Participants: {
|
||||
where: {
|
||||
appointmentCancelled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -47,23 +38,13 @@ const page = async () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="col-span-full">
|
||||
<p className="text-xl font-semibold text-left flex items-center gap-2 mb-2 mt-5">
|
||||
<RocketIcon className="w-4 h-4" /> Laufende Events & Kurse
|
||||
<p className="mb-2 mt-5 flex items-center gap-2 text-left text-xl font-semibold">
|
||||
<RocketIcon className="h-4 w-4" /> Laufende Events & Kurse
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
{filteredEvents.map((event) => {
|
||||
return (
|
||||
<EventCard
|
||||
appointments={event.Appointments}
|
||||
selectedAppointments={event.Appointments.filter((a) =>
|
||||
a.Participants.find((p) => p.userId == user.id),
|
||||
)}
|
||||
user={user}
|
||||
event={event}
|
||||
key={event.id}
|
||||
/>
|
||||
);
|
||||
return <EventCard user={user} event={event} key={event.id} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,18 +20,18 @@ const PathsOptions = ({
|
||||
<div className="flex gap-6">
|
||||
{/* Disponent Card */}
|
||||
<div
|
||||
className={`cursor-pointer border rounded-lg p-6 w-80 transition-colors ${
|
||||
selected === "disponent" ? "border-info ring-2 ring-info" : "border-base-300"
|
||||
className={`w-80 cursor-pointer rounded-lg border p-6 transition-colors ${
|
||||
selected === "disponent" ? "border-info ring-info ring-2" : "border-base-300"
|
||||
}`}
|
||||
onClick={() => setSelected("disponent")}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={selected === "disponent"}
|
||||
>
|
||||
<h1 className="font-semibold text-lg mb-2 flex gap-2 justify-center items-center">
|
||||
<h1 className="mb-2 flex items-center justify-center gap-2 text-lg font-semibold">
|
||||
Disponent <Workflow />
|
||||
</h1>
|
||||
<div className="text-sm text-base-content/70">
|
||||
<div className="text-base-content/70 text-sm">
|
||||
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
|
||||
@@ -43,18 +43,18 @@ const PathsOptions = ({
|
||||
</div>
|
||||
{/* Pilot Card */}
|
||||
<div
|
||||
className={`cursor-pointer border rounded-lg p-6 w-80 transition-colors ${
|
||||
selected === "pilot" ? "border-info ring-2 ring-info" : "border-base-300"
|
||||
className={`w-80 cursor-pointer rounded-lg border p-6 transition-colors ${
|
||||
selected === "pilot" ? "border-info ring-info ring-2" : "border-base-300"
|
||||
}`}
|
||||
onClick={() => setSelected("pilot")}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={selected === "pilot"}
|
||||
>
|
||||
<h1 className="font-semibold text-lg mb-2 flex gap-2 justify-center items-center">
|
||||
<h1 className="mb-2 flex items-center justify-center gap-2 text-lg font-semibold">
|
||||
Pilot <Plane />
|
||||
</h1>
|
||||
<div className="text-sm text-base-content/70">
|
||||
<div className="text-base-content/70 text-sm">
|
||||
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.
|
||||
@@ -76,17 +76,7 @@ const EventSelect = ({ pathSelected }: { pathSelected: "disponent" | "pilot" })
|
||||
const user = useSession().data?.user;
|
||||
if (!user) return null;
|
||||
return events?.map((event) => {
|
||||
return (
|
||||
<EventCard
|
||||
appointments={event.Appointments}
|
||||
selectedAppointments={event.Appointments.filter((a) =>
|
||||
a.Participants.find((p) => p.userId == user.id),
|
||||
)}
|
||||
user={user}
|
||||
event={event}
|
||||
key={event.id}
|
||||
/>
|
||||
);
|
||||
return <EventCard user={user} event={event} key={event.id} />;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -107,14 +97,14 @@ export const FirstPath = () => {
|
||||
return (
|
||||
<dialog ref={modalRef} className="modal">
|
||||
<div className="modal-box w-11/12 max-w-5xl">
|
||||
<h3 className="flex items-center gap-2 text-lg font-bold mb-10">
|
||||
<h3 className="mb-10 flex items-center gap-2 text-lg font-bold">
|
||||
{session?.user.migratedFromV1
|
||||
? "Hallo, hier hat sich einiges geändert!"
|
||||
: "Wähle deinen Einstieg!"}
|
||||
</h3>
|
||||
<h2 className="text-2xl font-bold mb-4 text-center">Willkommen bei Virtual Air Rescue!</h2>
|
||||
<h2 className="mb-4 text-center text-2xl font-bold">Willkommen bei Virtual Air Rescue!</h2>
|
||||
{session?.user.migratedFromV1 ? (
|
||||
<p className="mb-8 text-base text-base-content/80 text-center">
|
||||
<p className="text-base-content/80 mb-8 text-center text-base">
|
||||
Dein Account wurde erfolgreich auf das neue System migriert. Herzlich Willkommen im
|
||||
neuen HUB! Um die Erfahrung für alle Nutzer zu steigern haben wir uns dazu entschlossen,
|
||||
dass alle Nutzer einen Test absolvieren müssen:{" "}
|
||||
@@ -129,12 +119,12 @@ export const FirstPath = () => {
|
||||
ausprobieren, wenn du möchtest.
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-col items-center justify-center m-20">
|
||||
<div className="m-20 flex flex-col items-center justify-center">
|
||||
{page === "path" && <PathsOptions selected={selected} setSelected={setSelected} />}
|
||||
{page === "event-select" && (
|
||||
<div className="flex flex-col gap-3 min-w-[800px]">
|
||||
<div className="flex min-w-[800px] flex-col gap-3">
|
||||
<div>
|
||||
<p className="text-left text-gray-400 text-sm">Wähle dein Einführungs-Event aus:</p>
|
||||
<p className="text-left text-sm text-gray-400">Wähle dein Einführungs-Event aus:</p>
|
||||
</div>
|
||||
<EventSelect pathSelected={selected!} />
|
||||
</div>
|
||||
|
||||
@@ -2,23 +2,17 @@ import Image from "next/image";
|
||||
import { DiscordLogoIcon, InstagramLogoIcon, ReaderIcon } from "@radix-ui/react-icons";
|
||||
import YoutubeSvg from "./youtube_wider.svg";
|
||||
import FacebookSvg from "./facebook.svg";
|
||||
import { ChangelogModalBtn } from "@repo/shared-components";
|
||||
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||
import { updateUser } from "(app)/settings/actions";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { ChangelogWrapper } from "(app)/_components/ChangelogWrapper";
|
||||
import { prisma } from "@repo/db";
|
||||
|
||||
export const Footer = async () => {
|
||||
const session = await getServerSession();
|
||||
const latestChangelog = await prisma.changelog.findFirst({
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
const autoOpen = !session?.user.changelogAck && !!latestChangelog;
|
||||
|
||||
return (
|
||||
<footer className="footer bg-base-200 mt-4 flex items-center justify-between rounded-lg p-4 shadow-md">
|
||||
{/* Left: Impressum & Datenschutz */}
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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">
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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 } });
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { DrawingPinFilledIcon } from "@radix-ui/react-icons";
|
||||
import { Event, Participant, EventAppointment, User } from "@repo/db";
|
||||
import { Event, Participant, User } from "@repo/db";
|
||||
import ModalBtn from "./Modal";
|
||||
import MDEditor from "@uiw/react-md-editor";
|
||||
import { Badge } from "@repo/shared-components";
|
||||
@@ -8,25 +8,18 @@ import { Badge } from "@repo/shared-components";
|
||||
export const EventCard = ({
|
||||
user,
|
||||
event,
|
||||
selectedAppointments,
|
||||
appointments,
|
||||
}: {
|
||||
user: User;
|
||||
event: Event & {
|
||||
Appointments: EventAppointment[];
|
||||
Participants: Participant[];
|
||||
};
|
||||
selectedAppointments: EventAppointment[];
|
||||
appointments: (EventAppointment & {
|
||||
Participants: { userId: string }[];
|
||||
})[];
|
||||
}) => {
|
||||
return (
|
||||
<div className="col-span-full">
|
||||
<div className="card bg-base-200 shadow-xl mb-4">
|
||||
<div className="card bg-base-200 mb-4 shadow-xl">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">{event.name}</h2>
|
||||
<div className="absolute top-0 right-0 m-4">
|
||||
<div className="absolute right-0 top-0 m-4">
|
||||
{event.type === "COURSE" && (
|
||||
<span className="badge badge-info badge-outline">Zusatzqualifikation</span>
|
||||
)}
|
||||
@@ -36,7 +29,7 @@ export const EventCard = ({
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<div className="col-span-4">
|
||||
<div className="text-left text-balance" data-color-mode="dark">
|
||||
<div className="text-balance text-left" data-color-mode="dark">
|
||||
<MDEditor.Markdown
|
||||
source={event.descriptionShort}
|
||||
style={{
|
||||
@@ -45,21 +38,21 @@ export const EventCard = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex col-span-2 justify-end">
|
||||
<div className="col-span-2 flex justify-end">
|
||||
{event.finishedBadges.map((b) => {
|
||||
return <Badge badge={b} key={b} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-actions flex justify-between items-center mt-5">
|
||||
<div className="card-actions mt-5 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-600 text-left flex items-center gap-2">
|
||||
<p className="flex items-center gap-2 text-left text-gray-600">
|
||||
<DrawingPinFilledIcon /> <b>Teilnahmevoraussetzungen: </b>
|
||||
{!event.requiredBadges.length && "Keine"}
|
||||
</p>
|
||||
{!!event.requiredBadges.length && (
|
||||
<div className="flex ml-6">
|
||||
<b className="text-gray-600 text-left mr-2">Abzeichen:</b>
|
||||
<div className="ml-6 flex">
|
||||
<b className="mr-2 text-left text-gray-600">Abzeichen:</b>
|
||||
<div className="flex gap-2">
|
||||
{event.requiredBadges.map((badge) => (
|
||||
<div className="badge badge-secondary badge-outline" key={badge}>
|
||||
@@ -71,11 +64,9 @@ export const EventCard = ({
|
||||
)}
|
||||
</div>
|
||||
<ModalBtn
|
||||
selectedAppointments={selectedAppointments}
|
||||
user={user}
|
||||
event={event}
|
||||
title={event.name}
|
||||
dates={appointments}
|
||||
participant={event.Participants[0]}
|
||||
modalId={`${event.name}_modal.${event.id}`}
|
||||
/>
|
||||
|
||||
@@ -1,56 +1,30 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import { CheckCircledIcon, EnterIcon, DrawingPinFilledIcon } from "@radix-ui/react-icons";
|
||||
import { Event, EventAppointment, Participant, User } from "@repo/db";
|
||||
import { Event, Participant, User } from "@repo/db";
|
||||
import { cn } from "@repo/shared-components";
|
||||
import { inscribeToMoodleCourse, upsertParticipant } from "../actions";
|
||||
import {
|
||||
BookCheck,
|
||||
Calendar,
|
||||
Check,
|
||||
CirclePlay,
|
||||
Clock10Icon,
|
||||
ExternalLink,
|
||||
EyeIcon,
|
||||
Info,
|
||||
TriangleAlert,
|
||||
} from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
InputJsonValueType,
|
||||
ParticipantOptionalDefaults,
|
||||
ParticipantOptionalDefaultsSchema,
|
||||
} from "@repo/db/zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Select } from "../../../_components/ui/Select";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { handleParticipantEnrolled } from "../../../../helper/events";
|
||||
import { eventCompleted } from "@repo/shared-components";
|
||||
import MDEditor from "@uiw/react-md-editor";
|
||||
import toast from "react-hot-toast";
|
||||
import { formatDate } from "date-fns";
|
||||
|
||||
interface ModalBtnProps {
|
||||
title: string;
|
||||
event: Event;
|
||||
dates: (EventAppointment & {
|
||||
Participants: { userId: string }[];
|
||||
})[];
|
||||
selectedAppointments: EventAppointment[];
|
||||
participant?: Participant;
|
||||
user: User;
|
||||
modalId: string;
|
||||
}
|
||||
|
||||
const ModalBtn = ({
|
||||
title,
|
||||
dates,
|
||||
modalId,
|
||||
participant,
|
||||
selectedAppointments,
|
||||
event,
|
||||
user,
|
||||
}: ModalBtnProps) => {
|
||||
const ModalBtn = ({ title, modalId, participant, event, user }: ModalBtnProps) => {
|
||||
useEffect(() => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||
const handleOpen = () => {
|
||||
@@ -66,12 +40,6 @@ const ModalBtn = ({
|
||||
modal?.removeEventListener("close", handleClose);
|
||||
};
|
||||
}, [modalId]);
|
||||
const router = useRouter();
|
||||
|
||||
const canSelectDate =
|
||||
event.hasPresenceEvents &&
|
||||
!participant?.attended &&
|
||||
(selectedAppointments.length === 0 || participant?.appointmentCancelled);
|
||||
|
||||
const openModal = () => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||
@@ -82,29 +50,6 @@ const ModalBtn = ({
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||
modal?.close();
|
||||
};
|
||||
const selectAppointmentForm = useForm<ParticipantOptionalDefaults>({
|
||||
resolver: zodResolver(ParticipantOptionalDefaultsSchema),
|
||||
defaultValues: {
|
||||
eventId: event.id,
|
||||
userId: user.id,
|
||||
...participant,
|
||||
},
|
||||
});
|
||||
const selectedAppointment = selectedAppointments[0];
|
||||
const selectedDate = dates.find(
|
||||
(date) =>
|
||||
date.id === selectAppointmentForm.watch("eventAppointmentId") || selectedAppointment?.id,
|
||||
);
|
||||
const ownIndexInParticipantList = selectedDate?.Participants?.findIndex(
|
||||
(p) => p.userId === user.id,
|
||||
);
|
||||
|
||||
const ownPlaceInParticipantList =
|
||||
typeof ownIndexInParticipantList === "number"
|
||||
? ownIndexInParticipantList === -1
|
||||
? (selectedDate?.Participants?.length ?? 0) + 1
|
||||
: ownIndexInParticipantList + 1
|
||||
: undefined;
|
||||
|
||||
const missingRequirements =
|
||||
event.requiredBadges?.length > 0 &&
|
||||
@@ -163,79 +108,6 @@ const ModalBtn = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{event.hasPresenceEvents && (
|
||||
<div className="bg-base-300 flex min-w-[300px] flex-1 flex-col gap-2 rounded-lg p-3 shadow">
|
||||
<h2 className="flex gap-2 text-lg font-bold">
|
||||
<Calendar /> Termine
|
||||
</h2>
|
||||
<div className="flex flex-1 flex-col items-center justify-center">
|
||||
{!!dates.length && !selectedDate && (
|
||||
<>
|
||||
<p className="text-info text-center">Melde dich zu einem Termin an</p>
|
||||
<Select
|
||||
form={selectAppointmentForm}
|
||||
options={dates.map((date) => ({
|
||||
label: `${formatDate(date.appointmentDate, "dd.MM.yyyy HH:mm")} - (${date.Participants.length}/${event.maxParticipants})`,
|
||||
value: date.id,
|
||||
}))}
|
||||
name="eventAppointmentId"
|
||||
label={""}
|
||||
placeholder="Wähle einen Termin"
|
||||
className="min-w-[250px]"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{selectedAppointment && !participant?.appointmentCancelled && (
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<span>Dein ausgewählter Termin (Deutsche Zeit)</span>
|
||||
<div>
|
||||
<button
|
||||
className="input input-border pointer-events-none min-w-[250px]"
|
||||
style={{ anchorName: "--rdp" } as React.CSSProperties}
|
||||
>
|
||||
{new Date(selectedAppointment.appointmentDate).toLocaleString("de-DE", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</button>
|
||||
</div>
|
||||
{participant?.attended ? (
|
||||
<p className="flex items-center justify-center gap-2 py-4">
|
||||
<CheckCircledIcon className="text-success" />
|
||||
Du hast an dem Presenztermin teilgenommen
|
||||
</p>
|
||||
) : (
|
||||
<p className="flex items-center justify-center gap-2 py-4">
|
||||
Bitte erscheine ~5 minuten vor dem Termin im Discord
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!dates.length && (
|
||||
<p className="text-error text-center">Aktuell sind keine Termine verfügbar</p>
|
||||
)}
|
||||
|
||||
{!!selectedDate &&
|
||||
!!event.maxParticipants &&
|
||||
!!ownPlaceInParticipantList &&
|
||||
!!(ownPlaceInParticipantList > event.maxParticipants) && (
|
||||
<p
|
||||
role="alert"
|
||||
className="alert alert-error alert-outline my-5 flex items-center justify-center gap-2 border py-4"
|
||||
>
|
||||
<TriangleAlert className="h-6 w-6 shrink-0 stroke-current" fill="none" />
|
||||
Dieser Termin ist ausgebucht, wahrscheinlich wirst du nicht teilnehmen
|
||||
können. (Listenplatz: {ownPlaceInParticipantList} , max.{" "}
|
||||
{event.maxParticipants})
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{event.finisherMoodleCourseId && (
|
||||
<div className="bg-base-300 flex min-w-[300px] flex-1 flex-col gap-2 rounded-lg p-3 shadow">
|
||||
<h2 className="flex gap-2 text-lg font-bold">
|
||||
@@ -261,64 +133,6 @@ const ModalBtn = ({
|
||||
{!event.requiredBadges.length && "Keine"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="modal-action">
|
||||
{!!canSelectDate && (
|
||||
<button
|
||||
className={cn(
|
||||
"btn btn-info btn-outline btn-wide",
|
||||
event.type === "COURSE" && "btn-secondary",
|
||||
)}
|
||||
onClick={async () => {
|
||||
const data = selectAppointmentForm.getValues();
|
||||
if (!data.eventAppointmentId) return;
|
||||
|
||||
const participant = await upsertParticipant({
|
||||
...data,
|
||||
enscriptionDate: new Date(),
|
||||
statusLog: data.statusLog?.filter((log) => log !== null),
|
||||
appointmentCancelled: false,
|
||||
});
|
||||
await handleParticipantEnrolled(participant.id.toString());
|
||||
|
||||
router.refresh();
|
||||
closeModal();
|
||||
}}
|
||||
disabled={!selectAppointmentForm.watch("eventAppointmentId")}
|
||||
>
|
||||
<EnterIcon /> Anmelden
|
||||
</button>
|
||||
)}
|
||||
{selectedAppointment &&
|
||||
!participant?.appointmentCancelled &&
|
||||
!participant?.attended && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
await upsertParticipant({
|
||||
eventId: event.id,
|
||||
userId: participant!.userId,
|
||||
appointmentCancelled: true,
|
||||
statusLog: [
|
||||
...(participant?.statusLog as unknown as InputJsonValueType[]),
|
||||
{
|
||||
data: {
|
||||
appointmentId: selectedAppointment.id,
|
||||
appointmentDate: selectedAppointment.appointmentDate,
|
||||
},
|
||||
user: `${user?.firstname} ${user?.lastname} - ${user?.publicId}`,
|
||||
event: "Termin abgesagt",
|
||||
timestamp: new Date(),
|
||||
},
|
||||
],
|
||||
});
|
||||
toast.success("Termin abgesagt");
|
||||
router.refresh();
|
||||
}}
|
||||
className="btn btn-error btn-outline btn-wide"
|
||||
>
|
||||
Termin Absagen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="modal-backdrop" onClick={closeModal}>
|
||||
@@ -335,7 +149,6 @@ const MoodleCourseIndicator = ({
|
||||
completed,
|
||||
moodleCourseId,
|
||||
event,
|
||||
participant,
|
||||
user,
|
||||
}: {
|
||||
user: User;
|
||||
@@ -345,13 +158,6 @@ const MoodleCourseIndicator = ({
|
||||
event: Event;
|
||||
}) => {
|
||||
const courseUrl = `${process.env.NEXT_PUBLIC_MOODLE_URL}/course/view.php?id=${moodleCourseId}`;
|
||||
if (event.hasPresenceEvents && !participant?.attended)
|
||||
return (
|
||||
<p className="flex items-center justify-center gap-2 py-4">
|
||||
<Clock10Icon className="text-error" />
|
||||
Abschlusstest erst nach Teilnahme verfügbar
|
||||
</p>
|
||||
);
|
||||
if (completed)
|
||||
return (
|
||||
<p className="flex items-center justify-center gap-2 py-4">
|
||||
|
||||
@@ -14,13 +14,6 @@ const page = async () => {
|
||||
hidden: false,
|
||||
},
|
||||
include: {
|
||||
Appointments: {
|
||||
where: {
|
||||
appointmentDate: {
|
||||
gte: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
Participants: {
|
||||
where: {
|
||||
userId: user.id,
|
||||
@@ -32,42 +25,6 @@ const page = async () => {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
const appointments = await prisma.eventAppointment.findMany({
|
||||
where: {
|
||||
appointmentDate: {
|
||||
gte: new Date(),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Participants: {
|
||||
select: {
|
||||
enscriptionDate: true,
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
where: {
|
||||
appointmentCancelled: false,
|
||||
},
|
||||
orderBy: {
|
||||
enscriptionDate: "asc",
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
Participants: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const userAppointments = await prisma.eventAppointment.findMany({
|
||||
where: {
|
||||
Participants: {
|
||||
some: {
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
@@ -78,15 +35,7 @@ const page = async () => {
|
||||
</div>
|
||||
|
||||
{events.map((event) => {
|
||||
return (
|
||||
<EventCard
|
||||
appointments={appointments}
|
||||
selectedAppointments={userAppointments}
|
||||
user={user}
|
||||
event={event}
|
||||
key={event.id}
|
||||
/>
|
||||
);
|
||||
return <EventCard user={user} event={event} key={event.id} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -24,15 +24,6 @@ export async function GET(request: Request): Promise<NextResponse> {
|
||||
userId: session.user.id,
|
||||
},
|
||||
},
|
||||
Appointments: {
|
||||
include: {
|
||||
Participants: {
|
||||
where: {
|
||||
appointmentCancelled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { Event, EventAppointment, Participant, Prisma } from "@repo/db";
|
||||
import { Event, Participant, Prisma } from "@repo/db";
|
||||
import axios from "axios";
|
||||
|
||||
export const getEvents = async (filter: Prisma.EventWhereInput) => {
|
||||
const { data } = await axios.get<
|
||||
(Event & {
|
||||
Appointments: (EventAppointment & {
|
||||
Appointments: EventAppointment[];
|
||||
Participants: Participant[];
|
||||
})[];
|
||||
Participants: Participant[];
|
||||
})[]
|
||||
>(`/api/event`, {
|
||||
|
||||
Reference in New Issue
Block a user