Merge pull request #153 from VAR-Virtual-Air-Rescue/event-admin-redesign
Event admin redesign
This commit was merged in pull request #153.
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 */}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { prisma } from "@repo/db";
|
||||||
|
import { ParticipantForm } from "../../../_components/ParticipantForm";
|
||||||
|
import { Error } from "_components/Error";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { PersonIcon } from "@radix-ui/react-icons";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
|
||||||
|
export default async function Page({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string; participantId: string }>;
|
||||||
|
}) {
|
||||||
|
const { id: eventId, participantId } = await params;
|
||||||
|
console.log(eventId, participantId);
|
||||||
|
|
||||||
|
const event = await prisma.event.findUnique({
|
||||||
|
where: { id: parseInt(eventId) },
|
||||||
|
});
|
||||||
|
|
||||||
|
const participant = await prisma.participant.findUnique({
|
||||||
|
where: { id: parseInt(participantId) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!participant) {
|
||||||
|
return <Error title="Teilnehmer nicht gefunden" statusCode={404} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: participant?.userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!event) return <Error title="Event nicht gefunden" statusCode={404} />;
|
||||||
|
|
||||||
|
if (!participant || !user) {
|
||||||
|
return <Error title="Teilnehmer nicht gefunden" statusCode={404} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="my-3">
|
||||||
|
<div className="text-left">
|
||||||
|
<Link href={`/admin/event/${event.id}`} className="link-hover l-0 text-gray-500">
|
||||||
|
<ArrowLeft className="mb-1 mr-1 inline h-4 w-4" />
|
||||||
|
Zurück zum Event
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<p className="text-left text-2xl font-semibold">
|
||||||
|
<PersonIcon className="mr-2 inline h-5 w-5" /> Event-Übersicht für{" "}
|
||||||
|
{`${user.firstname} ${user.lastname} #${user.publicId}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ParticipantForm event={event} participant={participant} />
|
||||||
|
</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,70 +1,37 @@
|
|||||||
"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, Participant, PERMISSION, Prisma } from "@repo/db";
|
||||||
import {
|
import { EventOptionalDefaults, EventOptionalDefaultsSchema } from "@repo/db/zod";
|
||||||
EventAppointmentOptionalDefaults,
|
import { Bot, FileText, UserIcon } 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 { PaginatedTable } from "_components/PaginatedTable";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { User } from "next-auth";
|
||||||
|
|
||||||
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
|
||||||
? {
|
? {
|
||||||
...event,
|
...event,
|
||||||
discordRoleId: event.discordRoleId ?? undefined,
|
discordRoleId: event.discordRoleId ?? undefined,
|
||||||
maxParticipants: event.maxParticipants ?? undefined,
|
|
||||||
finisherMoodleCourseId: event.finisherMoodleCourseId ?? undefined,
|
finisherMoodleCourseId: event.finisherMoodleCourseId ?? undefined,
|
||||||
}
|
}
|
||||||
: 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);
|
||||||
@@ -139,224 +106,115 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
label="Discord Rolle für eingeschriebene Teilnehmer"
|
label="Discord Rolle für eingeschriebene Teilnehmer"
|
||||||
className="input-sm"
|
className="input-sm"
|
||||||
/>
|
/>
|
||||||
<Input
|
|
||||||
form={form}
|
|
||||||
label="Maximale Teilnehmer (Nur für live Events)"
|
|
||||||
className="input-sm"
|
|
||||||
{...form.register("maxParticipants", {
|
|
||||||
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 bg-base-200 col-span-6 shadow-xl">
|
<div className="card-body">
|
||||||
<div className="card-body">
|
{
|
||||||
<PaginatedTable
|
<PaginatedTable
|
||||||
ref={appointmentsTableRef}
|
|
||||||
prismaModel={"eventAppointment"}
|
|
||||||
getFilter={() =>
|
|
||||||
({
|
|
||||||
eventId: event?.id,
|
|
||||||
}) as Prisma.EventAppointmentWhereInput
|
|
||||||
}
|
|
||||||
include={{
|
|
||||||
Presenter: true,
|
|
||||||
Participants: true,
|
|
||||||
}}
|
|
||||||
leftOfSearch={
|
leftOfSearch={
|
||||||
<h2 className="card-title">
|
<h2 className="card-title">
|
||||||
<Calendar className="h-5 w-5" /> Termine
|
<UserIcon className="h-5 w-5" /> Teilnehmer
|
||||||
</h2>
|
</h2>
|
||||||
}
|
}
|
||||||
rightOfSearch={
|
prismaModel={"participant"}
|
||||||
event && (
|
showSearch
|
||||||
<button
|
getFilter={(searchTerm) =>
|
||||||
className="btn btn-primary btn-outline"
|
({
|
||||||
onClick={() => {
|
AND: [{ eventId: event?.id }],
|
||||||
appointmentModal.current?.showModal();
|
OR: [
|
||||||
appointmentForm.reset({
|
{
|
||||||
id: undefined,
|
User: {
|
||||||
eventId: event.id,
|
OR: [
|
||||||
presenterId: session?.user?.id,
|
{ firstname: { contains: searchTerm, mode: "insensitive" } },
|
||||||
});
|
{ lastname: { contains: searchTerm, mode: "insensitive" } },
|
||||||
}}
|
{ publicId: { contains: searchTerm, mode: "insensitive" } },
|
||||||
>
|
],
|
||||||
Hinzufügen
|
},
|
||||||
</button>
|
},
|
||||||
)
|
],
|
||||||
|
}) as Prisma.ParticipantWhereInput
|
||||||
}
|
}
|
||||||
|
include={{
|
||||||
|
User: true,
|
||||||
|
}}
|
||||||
|
supressQuery={!event}
|
||||||
columns={
|
columns={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
header: "Datum",
|
header: "Vorname",
|
||||||
accessorKey: "appointmentDate",
|
accessorKey: "User.firstname",
|
||||||
accessorFn: (date) => new Date(date.appointmentDate).toLocaleString(),
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className="hover:underline"
|
||||||
|
href={`/admin/user/${row.original.User.id}`}
|
||||||
|
>
|
||||||
|
{row.original.User.firstname}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Presenter",
|
header: "Nachname",
|
||||||
accessorKey: "presenter",
|
accessorKey: "User.lastname",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<div className="flex items-center">
|
return (
|
||||||
<span className="ml-2">
|
<Link
|
||||||
{row.original.Presenter.firstname} {row.original.Presenter.lastname}
|
className="hover:underline"
|
||||||
</span>
|
href={`/admin/user/${row.original.User.id}`}
|
||||||
</div>
|
>
|
||||||
),
|
{row.original.User.lastname}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Teilnehmer",
|
header: "VAR-Nummer",
|
||||||
accessorKey: "Participants",
|
accessorKey: "User.publicId",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<div className="flex items-center">
|
return (
|
||||||
<UserIcon className="h-5 w-5" />
|
<Link
|
||||||
<span className="ml-2">{row.original.Participants.length}</span>
|
className="hover:underline"
|
||||||
</div>
|
href={`/admin/user/${row.original.User.id}`}
|
||||||
),
|
>
|
||||||
|
{row.original.User.publicId}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Moodle Kurs abgeschlossen",
|
||||||
|
accessorKey: "finisherMoodleCurseCompleted",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Aktionen",
|
header: "Aktionen",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<Link
|
||||||
|
href={`/admin/event/${event?.id}/participant/${row.original.id}`}
|
||||||
|
className="flex gap-2"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onSubmit={() => false}
|
onSubmit={() => false}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
|
||||||
appointmentForm.reset(row.original);
|
|
||||||
appointmentModal.current?.showModal();
|
|
||||||
}}
|
|
||||||
className="btn btn-sm btn-outline"
|
className="btn btn-sm btn-outline"
|
||||||
>
|
>
|
||||||
Bearbeiten
|
Ansehen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</Link>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
] as ColumnDef<
|
] as ColumnDef<Participant & { User: User }>[]
|
||||||
EventAppointmentOptionalDefaults & {
|
|
||||||
Presenter: User;
|
|
||||||
Participants: Participant[];
|
|
||||||
}
|
|
||||||
>[]
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</div>
|
||||||
{!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">
|
||||||
|
|||||||
213
apps/hub/app/(app)/admin/event/_components/ParticipantForm.tsx
Normal file
213
apps/hub/app/(app)/admin/event/_components/ParticipantForm.tsx
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
"use client";
|
||||||
|
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";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Switch } from "_components/ui/Switch";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "_components/ui/Button";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { upsertParticipant } from "(app)/events/actions";
|
||||||
|
import { deleteParticipant } from "../action";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
interface ParticipantFormProps {
|
||||||
|
event: Event;
|
||||||
|
participant: Participant;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkEventCompleted = (participant: Participant, event: Event): boolean => {
|
||||||
|
return event.finisherMoodleCourseId ? participant.finisherMoodleCurseCompleted : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ParticipantForm = ({ event, participant }: ParticipantFormProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const upsertParticipantMutation = useMutation({
|
||||||
|
mutationKey: ["upsertParticipant"],
|
||||||
|
mutationFn: async (newData: Prisma.ParticipantUncheckedCreateInput) => {
|
||||||
|
const data = await upsertParticipant(newData);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["participants", event.id] });
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteParticipantMutation = useMutation({
|
||||||
|
mutationKey: ["deleteParticipant"],
|
||||||
|
mutationFn: async (participantId: number) => {
|
||||||
|
await deleteParticipant(participantId);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["participants", event.id] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventCompleted = checkEventCompleted(participant, event);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(ParticipantOptionalDefaultsSchema),
|
||||||
|
defaultValues: participant,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleEventFinished = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await upsertParticipantMutation.mutateAsync({
|
||||||
|
eventId: event.id,
|
||||||
|
userId: participant.userId,
|
||||||
|
});
|
||||||
|
toast.success("Event als beendet markiert");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckMoodle = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
toast.success("Moodle-Check durchgeführt");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(async (formData) => {
|
||||||
|
const data = await upsertParticipantMutation.mutateAsync({
|
||||||
|
...formData,
|
||||||
|
statusLog: participant.statusLog as unknown as Prisma.ParticipantCreatestatusLogInput,
|
||||||
|
eventId: event.id,
|
||||||
|
userId: participant.userId,
|
||||||
|
});
|
||||||
|
form.reset(data);
|
||||||
|
toast.success("Teilnehmer aktualisiert");
|
||||||
|
})}
|
||||||
|
className="bg-base-100 flex flex-wrap gap-6 p-6"
|
||||||
|
>
|
||||||
|
{/* Status Section */}
|
||||||
|
<div className="card bg-base-200 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title text-sm">
|
||||||
|
<Bug className="mr-2 inline h-5 w-5" />
|
||||||
|
Debug
|
||||||
|
</h3>
|
||||||
|
<Switch form={form} name={"attended"} label="Anwesend" />
|
||||||
|
<Switch form={form} name={"appointmentCancelled"} label="Termin Abgesagt" />
|
||||||
|
<Switch
|
||||||
|
form={form}
|
||||||
|
name={"finisherMoodleCurseCompleted"}
|
||||||
|
label="Moodle Kurs abgeschlossen"
|
||||||
|
/>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Info Card */}
|
||||||
|
<div className="card bg-base-200 min-w-[200px] flex-1 shadow-lg">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title text-lg">
|
||||||
|
<Users className="h-5 w-5" /> Informationen
|
||||||
|
</h2>
|
||||||
|
<div className="divider" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Kurs-status</p>
|
||||||
|
<p className="font-semibold">{eventCompleted ? "Abgeschlossen" : "In Bearbeitung"}</p>
|
||||||
|
<p className="text-sm text-gray-600">Einschreibedatum</p>
|
||||||
|
<p className="font-semibold">
|
||||||
|
{participant?.enscriptionDate
|
||||||
|
? new Date(participant.enscriptionDate).toLocaleDateString()
|
||||||
|
: "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divider" />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleEventFinished}
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="btn btn-sm btn-success"
|
||||||
|
>
|
||||||
|
Event beendet
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCheckMoodle}
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="btn btn-sm btn-info"
|
||||||
|
>
|
||||||
|
Moodle-Check
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Activity Log */}
|
||||||
|
<div className="card bg-base-200 min-w-[300px] flex-1 shadow-lg">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title text-lg">
|
||||||
|
<Activity className="h-5 w-5" /> Aktivitätslog
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="timeline timeline-vertical">
|
||||||
|
<table className="table-sm table w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Event</th>
|
||||||
|
<th>User</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{participant?.statusLog &&
|
||||||
|
Array.isArray(participant.statusLog) &&
|
||||||
|
(participant.statusLog as InputJsonValueType[])
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((log: InputJsonValueType, index: number) => {
|
||||||
|
const logEntry = log as unknown as ParticipantLog;
|
||||||
|
return (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>
|
||||||
|
{logEntry.timestamp
|
||||||
|
? new Date(logEntry.timestamp).toLocaleDateString()
|
||||||
|
: "-"}
|
||||||
|
</td>
|
||||||
|
<td>{logEntry.event || "-"}</td>
|
||||||
|
<td>{logEntry.user || "-"}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card bg-base-200 w-full shadow-xl">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="flex w-full gap-4">
|
||||||
|
<Button
|
||||||
|
disabled={!form.formState.isDirty}
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary flex-1"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
{event && (
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteParticipantMutation.mutateAsync(participant.id);
|
||||||
|
redirect(`/admin/event/${event.id}`);
|
||||||
|
}}
|
||||||
|
className="btn btn-error"
|
||||||
|
>
|
||||||
|
Austragen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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`, {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"node": ">=18",
|
"node": ">=18",
|
||||||
"pnpm": ">=10"
|
"pnpm": ">=10"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.28.0",
|
"packageManager": "pnpm@10.28.2",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"zod-prisma-types": "^3.2.4"
|
"zod-prisma-types": "^3.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.1.0",
|
||||||
"prisma": "^6.12.0"
|
"prisma": "^6.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,33 +5,17 @@ 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([])
|
||||||
eventId Int
|
eventId Int
|
||||||
// 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;
|
||||||
};
|
};
|
||||||
|
|||||||
94
pnpm-lock.yaml
generated
94
pnpm-lock.yaml
generated
@@ -130,7 +130,7 @@ importers:
|
|||||||
version: 0.5.8(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.15.3(@types/dom-mediacapture-record@1.0.22))
|
version: 0.5.8(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.15.3(@types/dom-mediacapture-record@1.0.22))
|
||||||
'@next-auth/prisma-adapter':
|
'@next-auth/prisma-adapter':
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(@prisma/client@6.12.0(prisma@6.12.0(typescript@5.8.3))(typescript@5.8.3))(next-auth@4.24.13(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
|
version: 1.0.7(@prisma/client@6.12.0(prisma@6.12.0(typescript@5.8.3))(typescript@5.8.3))(next-auth@4.24.13(next@16.1.1(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
|
||||||
'@radix-ui/react-icons':
|
'@radix-ui/react-icons':
|
||||||
specifier: ^1.3.2
|
specifier: ^1.3.2
|
||||||
version: 1.3.2(react@19.1.0)
|
version: 1.3.2(react@19.1.0)
|
||||||
@@ -211,10 +211,10 @@ importers:
|
|||||||
version: 0.525.0(react@19.1.0)
|
version: 0.525.0(react@19.1.0)
|
||||||
next:
|
next:
|
||||||
specifier: '>=15.4.9'
|
specifier: '>=15.4.9'
|
||||||
version: 16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 16.1.1(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
next-auth:
|
next-auth:
|
||||||
specifier: '>=4.24.12'
|
specifier: '>=4.24.12'
|
||||||
version: 4.24.13(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 4.24.13(next@16.1.1(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
npm:
|
npm:
|
||||||
specifier: ^11.4.2
|
specifier: ^11.4.2
|
||||||
version: 11.4.2
|
version: 11.4.2
|
||||||
@@ -588,6 +588,9 @@ importers:
|
|||||||
specifier: ^3.2.4
|
specifier: ^3.2.4
|
||||||
version: 3.2.4(@prisma/client@6.12.0(prisma@6.12.0(typescript@5.9.3))(typescript@5.9.3))(prisma@6.12.0(typescript@5.9.3))
|
version: 3.2.4(@prisma/client@6.12.0(prisma@6.12.0(typescript@5.9.3))(typescript@5.9.3))(prisma@6.12.0(typescript@5.9.3))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^25.1.0
|
||||||
|
version: 25.1.0
|
||||||
prisma:
|
prisma:
|
||||||
specifier: ^6.12.0
|
specifier: ^6.12.0
|
||||||
version: 6.12.0(typescript@5.9.3)
|
version: 6.12.0(typescript@5.9.3)
|
||||||
@@ -2161,6 +2164,9 @@ packages:
|
|||||||
'@types/node@22.15.34':
|
'@types/node@22.15.34':
|
||||||
resolution: {integrity: sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==}
|
resolution: {integrity: sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==}
|
||||||
|
|
||||||
|
'@types/node@25.1.0':
|
||||||
|
resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==}
|
||||||
|
|
||||||
'@types/nodemailer@6.4.17':
|
'@types/nodemailer@6.4.17':
|
||||||
resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==}
|
resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==}
|
||||||
|
|
||||||
@@ -5290,6 +5296,9 @@ packages:
|
|||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
|
undici-types@7.16.0:
|
||||||
|
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||||
|
|
||||||
undici@6.21.3:
|
undici@6.21.3:
|
||||||
resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
|
resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
|
||||||
engines: {node: '>=18.17'}
|
engines: {node: '>=18.17'}
|
||||||
@@ -5603,7 +5612,7 @@ snapshots:
|
|||||||
'@babel/parser': 7.27.7
|
'@babel/parser': 7.27.7
|
||||||
'@babel/template': 7.27.2
|
'@babel/template': 7.27.2
|
||||||
'@babel/types': 7.27.7
|
'@babel/types': 7.27.7
|
||||||
debug: 4.4.1
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
globals: 11.12.0
|
globals: 11.12.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -5852,7 +5861,7 @@ snapshots:
|
|||||||
'@eslint/eslintrc@3.3.1':
|
'@eslint/eslintrc@3.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
debug: 4.4.1
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
espree: 10.4.0
|
espree: 10.4.0
|
||||||
globals: 14.0.0
|
globals: 14.0.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
@@ -6091,11 +6100,6 @@ snapshots:
|
|||||||
'@prisma/client': 6.12.0(prisma@6.12.0(typescript@5.8.3))(typescript@5.8.3)
|
'@prisma/client': 6.12.0(prisma@6.12.0(typescript@5.8.3))(typescript@5.8.3)
|
||||||
next-auth: 4.24.13(next@16.1.1(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
next-auth: 4.24.13(next@16.1.1(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
|
||||||
'@next-auth/prisma-adapter@1.0.7(@prisma/client@6.12.0(prisma@6.12.0(typescript@5.8.3))(typescript@5.8.3))(next-auth@4.24.13(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))':
|
|
||||||
dependencies:
|
|
||||||
'@prisma/client': 6.12.0(prisma@6.12.0(typescript@5.8.3))(typescript@5.8.3)
|
|
||||||
next-auth: 4.24.13(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
|
||||||
|
|
||||||
'@next/env@16.1.1': {}
|
'@next/env@16.1.1': {}
|
||||||
|
|
||||||
'@next/eslint-plugin-next@15.4.2':
|
'@next/eslint-plugin-next@15.4.2':
|
||||||
@@ -7678,9 +7682,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
|
'@types/node@25.1.0':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.16.0
|
||||||
|
|
||||||
'@types/nodemailer@6.4.17':
|
'@types/nodemailer@6.4.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.29
|
'@types/node': 22.15.34
|
||||||
|
|
||||||
'@types/parse-json@4.0.2': {}
|
'@types/parse-json@4.0.2': {}
|
||||||
|
|
||||||
@@ -7749,7 +7757,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.37.0
|
'@typescript-eslint/types': 8.37.0
|
||||||
'@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.3)
|
'@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.37.0
|
'@typescript-eslint/visitor-keys': 8.37.0
|
||||||
debug: 4.4.1
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
eslint: 9.31.0(jiti@2.4.2)
|
eslint: 9.31.0(jiti@2.4.2)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -7759,7 +7767,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.37.0(typescript@5.8.3)
|
'@typescript-eslint/tsconfig-utils': 8.37.0(typescript@5.8.3)
|
||||||
'@typescript-eslint/types': 8.37.0
|
'@typescript-eslint/types': 8.37.0
|
||||||
debug: 4.4.1
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -7778,7 +7786,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.37.0
|
'@typescript-eslint/types': 8.37.0
|
||||||
'@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.3)
|
'@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.3)
|
||||||
'@typescript-eslint/utils': 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
|
'@typescript-eslint/utils': 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
|
||||||
debug: 4.4.1
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
eslint: 9.31.0(jiti@2.4.2)
|
eslint: 9.31.0(jiti@2.4.2)
|
||||||
ts-api-utils: 2.1.0(typescript@5.8.3)
|
ts-api-utils: 2.1.0(typescript@5.8.3)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
@@ -7793,7 +7801,7 @@ snapshots:
|
|||||||
'@typescript-eslint/tsconfig-utils': 8.37.0(typescript@5.8.3)
|
'@typescript-eslint/tsconfig-utils': 8.37.0(typescript@5.8.3)
|
||||||
'@typescript-eslint/types': 8.37.0
|
'@typescript-eslint/types': 8.37.0
|
||||||
'@typescript-eslint/visitor-keys': 8.37.0
|
'@typescript-eslint/visitor-keys': 8.37.0
|
||||||
debug: 4.4.1
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
@@ -8424,10 +8432,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
debug@4.4.1:
|
|
||||||
dependencies:
|
|
||||||
ms: 2.1.3
|
|
||||||
|
|
||||||
debug@4.4.1(supports-color@5.5.0):
|
debug@4.4.1(supports-color@5.5.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@@ -8770,7 +8774,7 @@ snapshots:
|
|||||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.31.0(jiti@2.4.2)):
|
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.31.0(jiti@2.4.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.4.1
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
eslint: 9.31.0(jiti@2.4.2)
|
eslint: 9.31.0(jiti@2.4.2)
|
||||||
get-tsconfig: 4.10.1
|
get-tsconfig: 4.10.1
|
||||||
is-bun-module: 2.0.0
|
is-bun-module: 2.0.0
|
||||||
@@ -10223,23 +10227,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
nodemailer: 7.0.11
|
nodemailer: 7.0.11
|
||||||
|
|
||||||
next-auth@4.24.13(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.27.6
|
|
||||||
'@panva/hkdf': 1.2.1
|
|
||||||
cookie: 0.7.2
|
|
||||||
jose: 4.15.9
|
|
||||||
next: 16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
|
||||||
oauth: 0.9.15
|
|
||||||
openid-client: 5.7.1
|
|
||||||
preact: 10.28.2
|
|
||||||
preact-render-to-string: 5.2.6(preact@10.28.2)
|
|
||||||
react: 19.1.0
|
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
|
||||||
uuid: 8.3.2
|
|
||||||
optionalDependencies:
|
|
||||||
nodemailer: 7.0.11
|
|
||||||
|
|
||||||
next-remove-imports@1.0.12(webpack@5.99.9):
|
next-remove-imports@1.0.12(webpack@5.99.9):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.27.7
|
'@babel/core': 7.27.7
|
||||||
@@ -10275,32 +10262,6 @@ snapshots:
|
|||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
|
|
||||||
next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
|
||||||
dependencies:
|
|
||||||
'@next/env': 16.1.1
|
|
||||||
'@swc/helpers': 0.5.15
|
|
||||||
baseline-browser-mapping: 2.9.14
|
|
||||||
caniuse-lite: 1.0.30001726
|
|
||||||
postcss: 8.4.31
|
|
||||||
react: 19.1.0
|
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
|
||||||
styled-jsx: 5.1.6(react@19.1.0)
|
|
||||||
optionalDependencies:
|
|
||||||
'@next/swc-darwin-arm64': 16.1.1
|
|
||||||
'@next/swc-darwin-x64': 16.1.1
|
|
||||||
'@next/swc-linux-arm64-gnu': 16.1.1
|
|
||||||
'@next/swc-linux-arm64-musl': 16.1.1
|
|
||||||
'@next/swc-linux-x64-gnu': 16.1.1
|
|
||||||
'@next/swc-linux-x64-musl': 16.1.1
|
|
||||||
'@next/swc-win32-arm64-msvc': 16.1.1
|
|
||||||
'@next/swc-win32-x64-msvc': 16.1.1
|
|
||||||
'@opentelemetry/api': 1.9.0
|
|
||||||
'@playwright/test': 1.52.0
|
|
||||||
sharp: 0.34.5
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@babel/core'
|
|
||||||
- babel-plugin-macros
|
|
||||||
|
|
||||||
node-cron@4.2.1: {}
|
node-cron@4.2.1: {}
|
||||||
|
|
||||||
node-releases@2.0.19: {}
|
node-releases@2.0.19: {}
|
||||||
@@ -11238,11 +11199,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@babel/core': 7.27.7
|
'@babel/core': 7.27.7
|
||||||
|
|
||||||
styled-jsx@5.1.6(react@19.1.0):
|
|
||||||
dependencies:
|
|
||||||
client-only: 0.0.1
|
|
||||||
react: 19.1.0
|
|
||||||
|
|
||||||
stylis@4.2.0: {}
|
stylis@4.2.0: {}
|
||||||
|
|
||||||
supports-color@5.5.0:
|
supports-color@5.5.0:
|
||||||
@@ -11463,6 +11419,8 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
|
undici-types@7.16.0: {}
|
||||||
|
|
||||||
undici@6.21.3: {}
|
undici@6.21.3: {}
|
||||||
|
|
||||||
unified@11.0.5:
|
unified@11.0.5:
|
||||||
|
|||||||
Reference in New Issue
Block a user