remove appointment from events
This commit is contained in:
@@ -7,7 +7,6 @@ const router: Router = Router();
|
|||||||
export const eventCompleted = (event: Event, participant?: Participant) => {
|
export const eventCompleted = (event: Event, participant?: Participant) => {
|
||||||
if (!participant) return false;
|
if (!participant) return false;
|
||||||
if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted) return false;
|
if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted) return false;
|
||||||
if (event.hasPresenceEvents && !participant.attended) return false;
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,15 +23,6 @@ const page = async () => {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Appointments: {
|
|
||||||
include: {
|
|
||||||
Participants: {
|
|
||||||
where: {
|
|
||||||
appointmentCancelled: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,23 +38,13 @@ const page = async () => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="col-span-full">
|
<div className="col-span-full">
|
||||||
<p className="text-xl font-semibold text-left flex items-center gap-2 mb-2 mt-5">
|
<p className="mb-2 mt-5 flex items-center gap-2 text-left text-xl font-semibold">
|
||||||
<RocketIcon className="w-4 h-4" /> Laufende Events & Kurse
|
<RocketIcon className="h-4 w-4" /> Laufende Events & Kurse
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-6 gap-4">
|
<div className="grid grid-cols-6 gap-4">
|
||||||
{filteredEvents.map((event) => {
|
{filteredEvents.map((event) => {
|
||||||
return (
|
return <EventCard user={user} event={event} key={event.id} />;
|
||||||
<EventCard
|
|
||||||
appointments={event.Appointments}
|
|
||||||
selectedAppointments={event.Appointments.filter((a) =>
|
|
||||||
a.Participants.find((p) => p.userId == user.id),
|
|
||||||
)}
|
|
||||||
user={user}
|
|
||||||
event={event}
|
|
||||||
key={event.id}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,18 +20,18 @@ const PathsOptions = ({
|
|||||||
<div className="flex gap-6">
|
<div className="flex gap-6">
|
||||||
{/* Disponent Card */}
|
{/* Disponent Card */}
|
||||||
<div
|
<div
|
||||||
className={`cursor-pointer border rounded-lg p-6 w-80 transition-colors ${
|
className={`w-80 cursor-pointer rounded-lg border p-6 transition-colors ${
|
||||||
selected === "disponent" ? "border-info ring-2 ring-info" : "border-base-300"
|
selected === "disponent" ? "border-info ring-info ring-2" : "border-base-300"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setSelected("disponent")}
|
onClick={() => setSelected("disponent")}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-pressed={selected === "disponent"}
|
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 />
|
Disponent <Workflow />
|
||||||
</h1>
|
</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
|
Denkt sich realistische Einsatzszenarien aus, koordiniert deren Ablauf und ist die
|
||||||
zentrale Schnittstelle zwischen Piloten und bodengebundenen Rettungsmitteln. Er trägt
|
zentrale Schnittstelle zwischen Piloten und bodengebundenen Rettungsmitteln. Er trägt
|
||||||
die Verantwortung für einen reibungslosen Ablauf und der erfolgreichen Durchführung der
|
die Verantwortung für einen reibungslosen Ablauf und der erfolgreichen Durchführung der
|
||||||
@@ -43,18 +43,18 @@ const PathsOptions = ({
|
|||||||
</div>
|
</div>
|
||||||
{/* Pilot Card */}
|
{/* Pilot Card */}
|
||||||
<div
|
<div
|
||||||
className={`cursor-pointer border rounded-lg p-6 w-80 transition-colors ${
|
className={`w-80 cursor-pointer rounded-lg border p-6 transition-colors ${
|
||||||
selected === "pilot" ? "border-info ring-2 ring-info" : "border-base-300"
|
selected === "pilot" ? "border-info ring-info ring-2" : "border-base-300"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setSelected("pilot")}
|
onClick={() => setSelected("pilot")}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-pressed={selected === "pilot"}
|
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 />
|
Pilot <Plane />
|
||||||
</h1>
|
</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
|
Fliegt die vom Disponenten erstellten Einsätze und transportiert die Med-Crew sicher zum
|
||||||
Einsatzort. Er übernimmt die navigatorische Vorbereitung, achtet auf Wetterentwicklungen
|
Einsatzort. Er übernimmt die navigatorische Vorbereitung, achtet auf Wetterentwicklungen
|
||||||
und sorgt für die Sicherheit seiner Crew im Flug.
|
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;
|
const user = useSession().data?.user;
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
return events?.map((event) => {
|
return events?.map((event) => {
|
||||||
return (
|
return <EventCard user={user} event={event} key={event.id} />;
|
||||||
<EventCard
|
|
||||||
appointments={event.Appointments}
|
|
||||||
selectedAppointments={event.Appointments.filter((a) =>
|
|
||||||
a.Participants.find((p) => p.userId == user.id),
|
|
||||||
)}
|
|
||||||
user={user}
|
|
||||||
event={event}
|
|
||||||
key={event.id}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,14 +97,14 @@ export const FirstPath = () => {
|
|||||||
return (
|
return (
|
||||||
<dialog ref={modalRef} className="modal">
|
<dialog ref={modalRef} className="modal">
|
||||||
<div className="modal-box w-11/12 max-w-5xl">
|
<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
|
{session?.user.migratedFromV1
|
||||||
? "Hallo, hier hat sich einiges geändert!"
|
? "Hallo, hier hat sich einiges geändert!"
|
||||||
: "Wähle deinen Einstieg!"}
|
: "Wähle deinen Einstieg!"}
|
||||||
</h3>
|
</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 ? (
|
{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
|
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,
|
neuen HUB! Um die Erfahrung für alle Nutzer zu steigern haben wir uns dazu entschlossen,
|
||||||
dass alle Nutzer einen Test absolvieren müssen:{" "}
|
dass alle Nutzer einen Test absolvieren müssen:{" "}
|
||||||
@@ -129,12 +119,12 @@ export const FirstPath = () => {
|
|||||||
ausprobieren, wenn du möchtest.
|
ausprobieren, wenn du möchtest.
|
||||||
</p>
|
</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 === "path" && <PathsOptions selected={selected} setSelected={setSelected} />}
|
||||||
{page === "event-select" && (
|
{page === "event-select" && (
|
||||||
<div className="flex flex-col gap-3 min-w-[800px]">
|
<div className="flex min-w-[800px] flex-col gap-3">
|
||||||
<div>
|
<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>
|
</div>
|
||||||
<EventSelect pathSelected={selected!} />
|
<EventSelect pathSelected={selected!} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,23 +2,17 @@ import Image from "next/image";
|
|||||||
import { DiscordLogoIcon, InstagramLogoIcon, ReaderIcon } from "@radix-ui/react-icons";
|
import { DiscordLogoIcon, InstagramLogoIcon, ReaderIcon } from "@radix-ui/react-icons";
|
||||||
import YoutubeSvg from "./youtube_wider.svg";
|
import YoutubeSvg from "./youtube_wider.svg";
|
||||||
import FacebookSvg from "./facebook.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 { ChangelogWrapper } from "(app)/_components/ChangelogWrapper";
|
||||||
import { prisma } from "@repo/db";
|
import { prisma } from "@repo/db";
|
||||||
|
|
||||||
export const Footer = async () => {
|
export const Footer = async () => {
|
||||||
const session = await getServerSession();
|
|
||||||
const latestChangelog = await prisma.changelog.findFirst({
|
const latestChangelog = await prisma.changelog.findFirst({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: "desc",
|
createdAt: "desc",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const autoOpen = !session?.user.changelogAck && !!latestChangelog;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="footer bg-base-200 mt-4 flex items-center justify-between rounded-lg p-4 shadow-md">
|
<footer className="footer bg-base-200 mt-4 flex items-center justify-between rounded-lg p-4 shadow-md">
|
||||||
{/* Left: Impressum & Datenschutz */}
|
{/* 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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-left text-2xl font-semibold">
|
<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}`}
|
{`${user.firstname} ${user.lastname} #${user.publicId}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ParticipantForm event={event} user={user} participant={participant} />
|
<ParticipantForm event={event} participant={participant} />
|
||||||
</div>
|
</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";
|
"use client";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { BADGES, Event, EVENT_TYPE, Participant, PERMISSION, Prisma, User } from "@repo/db";
|
import { BADGES, Event, EVENT_TYPE, PERMISSION } from "@repo/db";
|
||||||
import {
|
import { EventOptionalDefaults, EventOptionalDefaultsSchema } from "@repo/db/zod";
|
||||||
EventAppointmentOptionalDefaults,
|
import { Bot, FileText } from "lucide-react";
|
||||||
EventAppointmentOptionalDefaultsSchema,
|
|
||||||
EventOptionalDefaults,
|
|
||||||
EventOptionalDefaultsSchema,
|
|
||||||
ParticipantSchema,
|
|
||||||
} from "@repo/db/zod";
|
|
||||||
import { Bot, Calendar, FileText, UserIcon } from "lucide-react";
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { useRef } from "react";
|
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { PaginatedTable, PaginatedTableRef } from "../../../../_components/PaginatedTable";
|
|
||||||
import { Button } from "../../../../_components/ui/Button";
|
import { Button } from "../../../../_components/ui/Button";
|
||||||
import { Input } from "../../../../_components/ui/Input";
|
import { Input } from "../../../../_components/ui/Input";
|
||||||
import { MarkdownEditor } from "../../../../_components/ui/MDEditor";
|
import { MarkdownEditor } from "../../../../_components/ui/MDEditor";
|
||||||
import { Select } from "../../../../_components/ui/Select";
|
import { Select } from "../../../../_components/ui/Select";
|
||||||
import { Switch } from "../../../../_components/ui/Switch";
|
import { Switch } from "../../../../_components/ui/Switch";
|
||||||
import { deleteEvent, upsertEvent } from "../action";
|
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 toast from "react-hot-toast";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export const Form = ({ event }: { event?: Event }) => {
|
export const Form = ({ event }: { event?: Event }) => {
|
||||||
const { data: session } = useSession();
|
|
||||||
const form = useForm<EventOptionalDefaults>({
|
const form = useForm<EventOptionalDefaults>({
|
||||||
resolver: zodResolver(EventOptionalDefaultsSchema),
|
resolver: zodResolver(EventOptionalDefaultsSchema),
|
||||||
defaultValues: event
|
defaultValues: event
|
||||||
@@ -40,31 +26,9 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppointmentModal
|
|
||||||
participantModal={participantModal}
|
|
||||||
participantForm={participantForm}
|
|
||||||
appointmentForm={appointmentForm}
|
|
||||||
ref={appointmentModal}
|
|
||||||
appointmentsTableRef={appointmentsTableRef}
|
|
||||||
event={event}
|
|
||||||
/>
|
|
||||||
<ParticipantModal participantForm={participantForm} ref={participantModal} />
|
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(async (values) => {
|
onSubmit={form.handleSubmit(async (values) => {
|
||||||
await upsertEvent(values, event?.id);
|
await upsertEvent(values, event?.id);
|
||||||
@@ -147,216 +111,11 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Switch form={form} name="hasPresenceEvents" label="Hat Live Event" />
|
|
||||||
<div className="divider w-full" />
|
<div className="divider w-full" />
|
||||||
|
|
||||||
<Switch form={form} name="hidden" label="Event verstecken" />
|
<Switch form={form} name="hidden" label="Event verstecken" />
|
||||||
</div>
|
</div>
|
||||||
</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 bg-base-200 col-span-6 shadow-xl">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="flex w-full gap-4">
|
<div className="flex w-full gap-4">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"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 { Users, Activity, Bug } from "lucide-react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { InputJsonValueType, ParticipantOptionalDefaultsSchema } from "@repo/db/zod";
|
import { InputJsonValueType, ParticipantOptionalDefaultsSchema } from "@repo/db/zod";
|
||||||
@@ -16,19 +16,13 @@ import { redirect } from "next/navigation";
|
|||||||
interface ParticipantFormProps {
|
interface ParticipantFormProps {
|
||||||
event: Event;
|
event: Event;
|
||||||
participant: Participant;
|
participant: Participant;
|
||||||
user: User;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkEventCompleted = (participant: Participant, event: Event): boolean => {
|
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;
|
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 [isLoading, setIsLoading] = useState(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -106,6 +100,7 @@ export const ParticipantForm = ({ event, participant, user }: ParticipantFormPro
|
|||||||
name={"finisherMoodleCurseCompleted"}
|
name={"finisherMoodleCurseCompleted"}
|
||||||
label="Moodle Kurs abgeschlossen"
|
label="Moodle Kurs abgeschlossen"
|
||||||
/>
|
/>
|
||||||
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Info Card */}
|
{/* Info Card */}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { prisma, Prisma, Event, Participant } from "@repo/db";
|
import { prisma, Prisma, Event, Participant } from "@repo/db";
|
||||||
|
|
||||||
|
//############# Event //#############
|
||||||
export const upsertEvent = async (event: Prisma.EventCreateInput, id?: Event["id"]) => {
|
export const upsertEvent = async (event: Prisma.EventCreateInput, id?: Event["id"]) => {
|
||||||
const newEvent = id
|
const newEvent = id
|
||||||
? await prisma.event.update({
|
? await prisma.event.update({
|
||||||
@@ -11,34 +12,22 @@ export const upsertEvent = async (event: Prisma.EventCreateInput, id?: Event["id
|
|||||||
: await prisma.event.create({ data: event });
|
: await prisma.event.create({ data: event });
|
||||||
return newEvent;
|
return newEvent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteEvent = async (id: Event["id"]) => {
|
export const deleteEvent = async (id: Event["id"]) => {
|
||||||
await prisma.event.delete({ where: { id: id } });
|
await prisma.event.delete({ where: { id: id } });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const upsertAppointment = async (
|
//############# Participant //#############
|
||||||
eventAppointment: Prisma.EventAppointmentUncheckedCreateInput,
|
|
||||||
) => {
|
export const upsertParticipant = async (participant: Prisma.ParticipantUncheckedCreateInput) => {
|
||||||
const newEventAppointment = eventAppointment.id
|
const newParticipant = participant.id
|
||||||
? await prisma.eventAppointment.update({
|
? await prisma.participant.update({
|
||||||
where: { id: eventAppointment.id },
|
where: { id: participant.id },
|
||||||
data: eventAppointment,
|
data: participant,
|
||||||
})
|
})
|
||||||
: await prisma.eventAppointment.create({ data: eventAppointment });
|
: await prisma.participant.create({ data: participant });
|
||||||
return newEventAppointment;
|
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"]) => {
|
export const deleteParticipant = async (id: Participant["id"]) => {
|
||||||
await prisma.participant.delete({ where: { id: id } });
|
await prisma.participant.delete({ where: { id: id } });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { DrawingPinFilledIcon } from "@radix-ui/react-icons";
|
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 ModalBtn from "./Modal";
|
||||||
import MDEditor from "@uiw/react-md-editor";
|
import MDEditor from "@uiw/react-md-editor";
|
||||||
import { Badge } from "@repo/shared-components";
|
import { Badge } from "@repo/shared-components";
|
||||||
@@ -8,25 +8,18 @@ import { Badge } from "@repo/shared-components";
|
|||||||
export const EventCard = ({
|
export const EventCard = ({
|
||||||
user,
|
user,
|
||||||
event,
|
event,
|
||||||
selectedAppointments,
|
|
||||||
appointments,
|
|
||||||
}: {
|
}: {
|
||||||
user: User;
|
user: User;
|
||||||
event: Event & {
|
event: Event & {
|
||||||
Appointments: EventAppointment[];
|
|
||||||
Participants: Participant[];
|
Participants: Participant[];
|
||||||
};
|
};
|
||||||
selectedAppointments: EventAppointment[];
|
|
||||||
appointments: (EventAppointment & {
|
|
||||||
Participants: { userId: string }[];
|
|
||||||
})[];
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="col-span-full">
|
<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">
|
<div className="card-body">
|
||||||
<h2 className="card-title">{event.name}</h2>
|
<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" && (
|
{event.type === "COURSE" && (
|
||||||
<span className="badge badge-info badge-outline">Zusatzqualifikation</span>
|
<span className="badge badge-info badge-outline">Zusatzqualifikation</span>
|
||||||
)}
|
)}
|
||||||
@@ -36,7 +29,7 @@ export const EventCard = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-6 gap-4">
|
<div className="grid grid-cols-6 gap-4">
|
||||||
<div className="col-span-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
|
<MDEditor.Markdown
|
||||||
source={event.descriptionShort}
|
source={event.descriptionShort}
|
||||||
style={{
|
style={{
|
||||||
@@ -45,21 +38,21 @@ export const EventCard = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex col-span-2 justify-end">
|
<div className="col-span-2 flex justify-end">
|
||||||
{event.finishedBadges.map((b) => {
|
{event.finishedBadges.map((b) => {
|
||||||
return <Badge badge={b} key={b} />;
|
return <Badge badge={b} key={b} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<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>
|
<DrawingPinFilledIcon /> <b>Teilnahmevoraussetzungen: </b>
|
||||||
{!event.requiredBadges.length && "Keine"}
|
{!event.requiredBadges.length && "Keine"}
|
||||||
</p>
|
</p>
|
||||||
{!!event.requiredBadges.length && (
|
{!!event.requiredBadges.length && (
|
||||||
<div className="flex ml-6">
|
<div className="ml-6 flex">
|
||||||
<b className="text-gray-600 text-left mr-2">Abzeichen:</b>
|
<b className="mr-2 text-left text-gray-600">Abzeichen:</b>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{event.requiredBadges.map((badge) => (
|
{event.requiredBadges.map((badge) => (
|
||||||
<div className="badge badge-secondary badge-outline" key={badge}>
|
<div className="badge badge-secondary badge-outline" key={badge}>
|
||||||
@@ -71,11 +64,9 @@ export const EventCard = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ModalBtn
|
<ModalBtn
|
||||||
selectedAppointments={selectedAppointments}
|
|
||||||
user={user}
|
user={user}
|
||||||
event={event}
|
event={event}
|
||||||
title={event.name}
|
title={event.name}
|
||||||
dates={appointments}
|
|
||||||
participant={event.Participants[0]}
|
participant={event.Participants[0]}
|
||||||
modalId={`${event.name}_modal.${event.id}`}
|
modalId={`${event.name}_modal.${event.id}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,56 +1,30 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { CheckCircledIcon, EnterIcon, DrawingPinFilledIcon } from "@radix-ui/react-icons";
|
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 { cn } from "@repo/shared-components";
|
||||||
import { inscribeToMoodleCourse, upsertParticipant } from "../actions";
|
import { inscribeToMoodleCourse, upsertParticipant } from "../actions";
|
||||||
import {
|
import {
|
||||||
BookCheck,
|
BookCheck,
|
||||||
Calendar,
|
|
||||||
Check,
|
Check,
|
||||||
CirclePlay,
|
CirclePlay,
|
||||||
Clock10Icon,
|
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
Info,
|
Info,
|
||||||
TriangleAlert,
|
TriangleAlert,
|
||||||
} from "lucide-react";
|
} 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 { eventCompleted } from "@repo/shared-components";
|
||||||
import MDEditor from "@uiw/react-md-editor";
|
import MDEditor from "@uiw/react-md-editor";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { formatDate } from "date-fns";
|
|
||||||
|
|
||||||
interface ModalBtnProps {
|
interface ModalBtnProps {
|
||||||
title: string;
|
title: string;
|
||||||
event: Event;
|
event: Event;
|
||||||
dates: (EventAppointment & {
|
|
||||||
Participants: { userId: string }[];
|
|
||||||
})[];
|
|
||||||
selectedAppointments: EventAppointment[];
|
|
||||||
participant?: Participant;
|
participant?: Participant;
|
||||||
user: User;
|
user: User;
|
||||||
modalId: string;
|
modalId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalBtn = ({
|
const ModalBtn = ({ title, modalId, participant, event, user }: ModalBtnProps) => {
|
||||||
title,
|
|
||||||
dates,
|
|
||||||
modalId,
|
|
||||||
participant,
|
|
||||||
selectedAppointments,
|
|
||||||
event,
|
|
||||||
user,
|
|
||||||
}: ModalBtnProps) => {
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||||
const handleOpen = () => {
|
const handleOpen = () => {
|
||||||
@@ -66,12 +40,6 @@ const ModalBtn = ({
|
|||||||
modal?.removeEventListener("close", handleClose);
|
modal?.removeEventListener("close", handleClose);
|
||||||
};
|
};
|
||||||
}, [modalId]);
|
}, [modalId]);
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const canSelectDate =
|
|
||||||
event.hasPresenceEvents &&
|
|
||||||
!participant?.attended &&
|
|
||||||
(selectedAppointments.length === 0 || participant?.appointmentCancelled);
|
|
||||||
|
|
||||||
const openModal = () => {
|
const openModal = () => {
|
||||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||||
@@ -82,29 +50,6 @@ const ModalBtn = ({
|
|||||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||||
modal?.close();
|
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 =
|
const missingRequirements =
|
||||||
event.requiredBadges?.length > 0 &&
|
event.requiredBadges?.length > 0 &&
|
||||||
@@ -163,79 +108,6 @@ const ModalBtn = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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 && (
|
{event.finisherMoodleCourseId && (
|
||||||
<div className="bg-base-300 flex min-w-[300px] flex-1 flex-col gap-2 rounded-lg p-3 shadow">
|
<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">
|
<h2 className="flex gap-2 text-lg font-bold">
|
||||||
@@ -261,64 +133,6 @@ const ModalBtn = ({
|
|||||||
{!event.requiredBadges.length && "Keine"}
|
{!event.requiredBadges.length && "Keine"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<button className="modal-backdrop" onClick={closeModal}>
|
<button className="modal-backdrop" onClick={closeModal}>
|
||||||
@@ -335,7 +149,6 @@ const MoodleCourseIndicator = ({
|
|||||||
completed,
|
completed,
|
||||||
moodleCourseId,
|
moodleCourseId,
|
||||||
event,
|
event,
|
||||||
participant,
|
|
||||||
user,
|
user,
|
||||||
}: {
|
}: {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -345,13 +158,6 @@ const MoodleCourseIndicator = ({
|
|||||||
event: Event;
|
event: Event;
|
||||||
}) => {
|
}) => {
|
||||||
const courseUrl = `${process.env.NEXT_PUBLIC_MOODLE_URL}/course/view.php?id=${moodleCourseId}`;
|
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)
|
if (completed)
|
||||||
return (
|
return (
|
||||||
<p className="flex items-center justify-center gap-2 py-4">
|
<p className="flex items-center justify-center gap-2 py-4">
|
||||||
|
|||||||
@@ -14,13 +14,6 @@ const page = async () => {
|
|||||||
hidden: false,
|
hidden: false,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
Appointments: {
|
|
||||||
where: {
|
|
||||||
appointmentDate: {
|
|
||||||
gte: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Participants: {
|
Participants: {
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -32,42 +25,6 @@ const page = async () => {
|
|||||||
id: "desc",
|
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 (
|
return (
|
||||||
<div className="grid grid-cols-6 gap-4">
|
<div className="grid grid-cols-6 gap-4">
|
||||||
@@ -78,15 +35,7 @@ const page = async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{events.map((event) => {
|
{events.map((event) => {
|
||||||
return (
|
return <EventCard user={user} event={event} key={event.id} />;
|
||||||
<EventCard
|
|
||||||
appointments={appointments}
|
|
||||||
selectedAppointments={userAppointments}
|
|
||||||
user={user}
|
|
||||||
event={event}
|
|
||||||
key={event.id}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,15 +24,6 @@ export async function GET(request: Request): Promise<NextResponse> {
|
|||||||
userId: session.user.id,
|
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";
|
import axios from "axios";
|
||||||
|
|
||||||
export const getEvents = async (filter: Prisma.EventWhereInput) => {
|
export const getEvents = async (filter: Prisma.EventWhereInput) => {
|
||||||
const { data } = await axios.get<
|
const { data } = await axios.get<
|
||||||
(Event & {
|
(Event & {
|
||||||
Appointments: (EventAppointment & {
|
|
||||||
Appointments: EventAppointment[];
|
|
||||||
Participants: Participant[];
|
|
||||||
})[];
|
|
||||||
Participants: Participant[];
|
Participants: Participant[];
|
||||||
})[]
|
})[]
|
||||||
>(`/api/event`, {
|
>(`/api/event`, {
|
||||||
|
|||||||
@@ -5,25 +5,10 @@ enum EVENT_TYPE {
|
|||||||
EVENT
|
EVENT
|
||||||
}
|
}
|
||||||
|
|
||||||
model EventAppointment {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
eventId Int
|
|
||||||
appointmentDate DateTime
|
|
||||||
presenterId String
|
|
||||||
// relations:
|
|
||||||
Users User[] @relation("EventAppointmentUser")
|
|
||||||
Participants Participant[]
|
|
||||||
Event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
|
||||||
Presenter User @relation(fields: [presenterId], references: [id])
|
|
||||||
}
|
|
||||||
|
|
||||||
model Participant {
|
model Participant {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
userId String @map(name: "user_id")
|
userId String @map(name: "user_id")
|
||||||
finisherMoodleCurseCompleted Boolean @default(false)
|
finisherMoodleCurseCompleted Boolean @default(false)
|
||||||
attended Boolean @default(false)
|
|
||||||
appointmentCancelled Boolean @default(false)
|
|
||||||
eventAppointmentId Int?
|
|
||||||
enscriptionDate DateTime @default(now())
|
enscriptionDate DateTime @default(now())
|
||||||
|
|
||||||
statusLog Json[] @default([])
|
statusLog Json[] @default([])
|
||||||
@@ -31,7 +16,6 @@ model Participant {
|
|||||||
// relations:
|
// relations:
|
||||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
Event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
Event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
||||||
EventAppointment EventAppointment? @relation(fields: [eventAppointmentId], references: [id])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Event {
|
model Event {
|
||||||
@@ -41,8 +25,6 @@ model Event {
|
|||||||
description String
|
description String
|
||||||
type EVENT_TYPE @default(EVENT)
|
type EVENT_TYPE @default(EVENT)
|
||||||
discordRoleId String? @default("")
|
discordRoleId String? @default("")
|
||||||
hasPresenceEvents Boolean @default(false)
|
|
||||||
maxParticipants Int? @default(0)
|
|
||||||
finisherMoodleCourseId String? @default("")
|
finisherMoodleCourseId String? @default("")
|
||||||
finishedBadges BADGES[] @default([])
|
finishedBadges BADGES[] @default([])
|
||||||
requiredBadges BADGES[] @default([])
|
requiredBadges BADGES[] @default([])
|
||||||
@@ -51,7 +33,6 @@ model Event {
|
|||||||
|
|
||||||
// relations:
|
// relations:
|
||||||
Participants Participant[]
|
Participants Participant[]
|
||||||
Appointments EventAppointment[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model File {
|
model File {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `appointmentCancelled` on the `Participant` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `attended` on the `Participant` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `eventAppointmentId` on the `Participant` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the `EventAppointment` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- You are about to drop the `_EventAppointmentUser` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "EventAppointment" DROP CONSTRAINT "EventAppointment_eventId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "EventAppointment" DROP CONSTRAINT "EventAppointment_presenterId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Participant" DROP CONSTRAINT "Participant_eventAppointmentId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "_EventAppointmentUser" DROP CONSTRAINT "_EventAppointmentUser_A_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "_EventAppointmentUser" DROP CONSTRAINT "_EventAppointmentUser_B_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Participant" DROP COLUMN "appointmentCancelled",
|
||||||
|
DROP COLUMN "attended",
|
||||||
|
DROP COLUMN "eventAppointmentId";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "EventAppointment";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "_EventAppointmentUser";
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `hasPresenceEvents` on the `Event` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Event" DROP COLUMN "hasPresenceEvents";
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `maxParticipants` on the `Event` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Event" DROP COLUMN "maxParticipants";
|
||||||
@@ -69,8 +69,6 @@ model User {
|
|||||||
// relations:
|
// relations:
|
||||||
oauthTokens OAuthToken[]
|
oauthTokens OAuthToken[]
|
||||||
participants Participant[]
|
participants Participant[]
|
||||||
EventAppointmentUser EventAppointment[] @relation("EventAppointmentUser")
|
|
||||||
EventAppointment EventAppointment[]
|
|
||||||
SentMessages ChatMessage[] @relation("SentMessages")
|
SentMessages ChatMessage[] @relation("SentMessages")
|
||||||
ReceivedMessages ChatMessage[] @relation("ReceivedMessages")
|
ReceivedMessages ChatMessage[] @relation("ReceivedMessages")
|
||||||
SentReports Report[] @relation("SentReports")
|
SentReports Report[] @relation("SentReports")
|
||||||
|
|||||||
@@ -3,6 +3,5 @@ import { Event, Participant } from "@repo/db";
|
|||||||
export const eventCompleted = (event: Event, participant?: Participant) => {
|
export const eventCompleted = (event: Event, participant?: Participant) => {
|
||||||
if (!participant) return false;
|
if (!participant) return false;
|
||||||
if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted) return false;
|
if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted) return false;
|
||||||
if (event.hasPresenceEvents && !participant.attended) return false;
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user