added event helpers to ui libary, added Badge component, reordered Dashboard Components, splitted Event Admin page

This commit is contained in:
PxlLoewe
2025-03-07 14:16:19 -07:00
parent c1f1ad47b7
commit 829e78a47d
25 changed files with 465 additions and 355 deletions

View File

@@ -0,0 +1,28 @@
import { Badge } from "@repo/ui";
import { Award } from "lucide-react";
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
export const Badges = async () => {
const session = await getServerSession();
if (!session) return null;
return (
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<div className="card-body">
<h2 className="card-title justify-between">
<span className="card-title">
<Award className="w-4 h-4" /> Verdiente Abzeichen
</span>
</h2>
{session.user.badges.map((badge) => {
return (
<div className="badge badge-primary badge-outline">
<Badge name={badge} />
</div>
);
})}
</div>
</div>
);
};

View File

@@ -1,6 +1,8 @@
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
import { PrismaClient } from "@repo/db";
import { KursItem } from "../events/_components/item";
import { RocketIcon } from "lucide-react";
import { eventCompleted } from "@repo/ui";
export default async () => {
const prisma = new PrismaClient();
@@ -39,18 +41,37 @@ export default async () => {
},
});
const filteredEvents = events.filter((event) => {
console.log;
if (eventCompleted(event, event.participants[0])) return false;
if (
event.type === "OBLIGATED_COURSE" &&
!eventCompleted(event, event.participants[0])
)
return true;
return false;
});
if (!filteredEvents.length) return null;
return (
<div className="grid grid-cols-6 gap-4">
{events.map((event) => {
return (
<KursItem
selectedAppointments={userAppointments}
user={user}
event={event}
key={event.id}
/>
);
})}
<div>
<div className="col-span-full">
<p className="text-xl font-semibold text-left flex items-center gap-2 mb-2 mt-5">
<RocketIcon className="w-4 h-4" /> Laufende Events & Kurse
</p>
</div>
<div className="grid grid-cols-6 gap-4">
{filteredEvents.map((event) => {
return (
<KursItem
selectedAppointments={userAppointments}
user={user}
event={event}
key={event.id}
/>
);
})}
</div>
</div>
);
};

View File

@@ -0,0 +1,203 @@
import { DateInput } from "../../../../_components/ui/DateInput";
import { zodResolver } from "@hookform/resolvers/zod";
import { Event, Participant, Prisma } from "@repo/db";
import {
EventAppointmentOptionalDefaults,
EventAppointmentOptionalDefaultsSchema,
ParticipantOptionalDefaultsSchema,
} from "@repo/db/zod";
import { useSession } from "next-auth/react";
import { Controller, useForm } from "react-hook-form";
import { deleteAppoinement, upsertAppointment } from "../action";
import { Button } from "../../../../_components/ui/Button";
import {
PaginatedTable,
PaginatedTableRef,
} from "../../../../_components/PaginatedTable";
import { Ref, RefObject, useRef } from "react";
import { CellContext } from "@tanstack/react-table";
import { upsertParticipant } from "../../../events/actions";
import { Switch } from "../../../../_components/ui/Switch";
interface AppointmentModalProps {
event?: Event;
ref: RefObject<HTMLDialogElement | null>;
appointmentsTableRef: React.RefObject<PaginatedTableRef>;
}
export const AppointmentModal = ({
event,
ref,
appointmentsTableRef,
}: AppointmentModalProps) => {
const { data: session } = useSession();
const appointmentForm = useForm<EventAppointmentOptionalDefaults>({
resolver: zodResolver(EventAppointmentOptionalDefaultsSchema),
defaultValues: {
eventId: event?.id,
presenterId: session?.user?.id,
},
});
const participantTableRef = useRef<PaginatedTableRef>(null);
const participantForm = useForm<Participant>({
resolver: zodResolver(ParticipantOptionalDefaultsSchema),
});
return (
<dialog ref={ref} className="modal">
<div className="modal-box">
<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">
</button>
</form>
<h3 className="font-bold text-lg">
Termin {appointmentForm.watch("id")}
</h3>
<form
onSubmit={appointmentForm.handleSubmit(async (values) => {
if (!event) return;
const createdAppointment = await upsertAppointment(values);
ref.current?.close();
appointmentsTableRef.current?.refresh();
})}
className="flex flex-col"
>
<DateInput control={appointmentForm.control} name="appointmentDate" />
{/* <Input
form={appointmentForm}
type="datetime-local"
label="Datum"
name="appointmentDate"
formOptions={{
valueAsDate: true,
}}
/> */}
<div>
{appointmentForm.watch("id") && (
<PaginatedTable
ref={participantTableRef}
columns={[
{
accessorKey: "User.firstname",
header: "Vorname",
},
{
accessorKey: "User.lastname",
header: "Nachname",
},
{
header: "Aktion",
cell: ({ row }: CellContext<Participant, any>) => {
return (
<>
<button
onClick={() => {
participantForm.reset(row.original);
}}
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: participantForm.watch("userId"),
attended: true,
});
participantTableRef.current?.refresh();
}}
className="btn btn-outline btn-info btn-sm"
>
Anwesend
</button>
)}
</>
);
},
},
]}
prismaModel={"participant"}
filter={{
eventAppointmentId: appointmentForm.watch("id"),
}}
include={{ User: true }}
leftOfPagination={
<div className="space-x-1">
<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>
<div className="modal-action"></div>
</form>
{participantForm.watch("id") && (
<form
onSubmit={participantForm.handleSubmit(async (data) => {
await upsertParticipant({
...data,
statusLog: data.statusLog as Prisma.InputJsonValue[],
});
participantTableRef.current?.refresh();
})}
className="space-y-1"
>
<h1 className="text-2xl">Teilnehmer bearbeiten</h1>
<Switch
form={participantForm}
name="appointmentCancelled"
label="Termin abgesagt"
/>
<Switch
form={participantForm}
name="finisherMoodleCurseCompleted"
label="Abschluss-Moodle kurs abgeschlossen"
/>
{event?.hasPresenceEvents && (
<Switch
form={participantForm}
name="attended"
label="An Presenstermin teilgenommen"
/>
)}
<div className="flex flex-col">
<h3 className="text-xl">Verlauf</h3>
{participantForm.watch("statusLog").map((s) => {
return (
<div className="flex justify-between">
<p>{(s as any).event}</p>
<p>{new Date((s as any).timestamp).toLocaleString()}</p>
</div>
);
})}
</div>
<Button>Speichern</Button>
</form>
)}
</div>
</dialog>
);
};

View File

@@ -38,10 +38,7 @@ import {
import { Select } from "../../../../_components/ui/Select";
import { useSession } from "next-auth/react";
import { MarkdownEditor } from "../../../../_components/ui/MDEditor";
import { CellContext } from "@tanstack/react-table";
import { upsertParticipant } from "../../../events/actions";
import { de } from "date-fns/locale";
registerLocale("de", de);
import { AppointmentModal } from "./AppointmentModal";
export const Form = ({ event }: { event?: Event }) => {
const { data: session } = useSession();
@@ -66,177 +63,10 @@ export const Form = ({ event }: { event?: Event }) => {
});
return (
<>
<dialog ref={appointmentModal} className="modal">
<div className="modal-box">
<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">
</button>
</form>
<h3 className="font-bold text-lg">
Termin {appointmentForm.watch("id")}
</h3>
<form
onSubmit={appointmentForm.handleSubmit(async (values) => {
if (!event) return;
const createdAppointment = await upsertAppointment(values);
appointmentModal.current?.close();
appointmentsTableRef.current?.refresh();
})}
className="flex flex-col"
>
<Controller
control={appointmentForm.control}
name="appointmentDate"
render={({ field }) => (
<DatePicker
locale={"de"}
showTimeCaption
showTimeInput
showTimeSelect
placeholderText="Select date"
onChange={(date) => field.onChange(date)}
selected={field.value}
/>
)}
/>
{/* <Input
form={appointmentForm}
type="datetime-local"
label="Datum"
name="appointmentDate"
formOptions={{
valueAsDate: true,
}}
/> */}
<div>
{appointmentForm.watch("id") && (
<PaginatedTable
ref={participantTableRef}
columns={[
{
accessorKey: "User.firstname",
header: "Vorname",
},
{
accessorKey: "User.lastname",
header: "Nachname",
},
{
header: "Aktion",
cell: ({ row }: CellContext<Participant, any>) => {
return (
<>
<button
onClick={() => {
participantForm.reset(row.original);
}}
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: participantForm.watch("userId"),
attended: true,
});
participantTableRef.current?.refresh();
}}
className="btn btn-outline btn-info btn-sm"
>
Anwesend
</button>
)}
</>
);
},
},
]}
prismaModel={"participant"}
filter={{
eventAppointmentId: appointmentForm.watch("id"),
}}
include={{ User: true }}
leftOfPagination={
<div className="space-x-1">
<Button type="submit" className="btn btn-primary">
Speichern
</Button>
{appointmentForm.watch("id") && (
<Button
type="button"
onSubmit={() => {}}
onClick={async () => {
await deleteAppoinement(
appointmentForm.watch("id")!,
);
appointmentModal.current?.close();
appointmentsTableRef.current?.refresh();
}}
className="btn btn-error btn-outline"
>
Löschen
</Button>
)}
</div>
}
/>
)}
</div>
<div className="modal-action"></div>
</form>
{participantForm.watch("id") && (
<form
onSubmit={participantForm.handleSubmit(async (data) => {
await upsertParticipant({
...data,
statusLog: data.statusLog as Prisma.InputJsonValue[],
});
participantTableRef.current?.refresh();
})}
className="space-y-1"
>
<h1 className="text-2xl">Teilnehmer bearbeiten</h1>
<Switch
form={participantForm}
name="appointmentCancelled"
label="Termin abgesagt"
/>
<Switch
form={participantForm}
name="finisherMoodleCurseCompleted"
label="Abschluss-Moodle kurs abgeschlossen"
/>
{event?.hasPresenceEvents && (
<Switch
form={participantForm}
name="attended"
label="An Presenstermin teilgenommen"
/>
)}
<div className="flex flex-col">
<h3 className="text-xl">Verlauf</h3>
{participantForm.watch("statusLog").map((s) => {
return (
<div className="flex justify-between">
<p>{(s as any).event}</p>
<p>{new Date((s as any).timestamp).toLocaleString()}</p>
</div>
);
})}
</div>
<Button>Speichern</Button>
</form>
)}
</div>
</dialog>
<AppointmentModal
ref={appointmentModal}
appointmentsTableRef={appointmentsTableRef}
/>
<form
onSubmit={form.handleSubmit(async (values) => {
setLoading(true);
@@ -322,92 +152,94 @@ export const Form = ({ event }: { event?: Event }) => {
/>
</div>
</div>
<div className="card bg-base-200 shadow-xl col-span-6">
<div className="card-body">
<div className="flex justify-between">
<h2 className="card-title">
<Calendar className="w-5 h-5" /> Termine
</h2>
{event && (
<button
className="btn btn-primary btn-outline"
onClick={() => {
appointmentModal.current?.showModal();
appointmentForm.reset({
id: undefined,
});
}}
>
Hinzufügen
</button>
)}
</div>
{form.watch("hasPresenceEvents") ? (
<div className="card bg-base-200 shadow-xl col-span-6">
<div className="card-body">
<div className="flex justify-between">
<h2 className="card-title">
<Calendar className="w-5 h-5" /> Termine
</h2>
{event && (
<button
className="btn btn-primary btn-outline"
onClick={() => {
appointmentModal.current?.showModal();
appointmentForm.reset({
id: undefined,
});
}}
>
Hinzufügen
</button>
)}
</div>
<PaginatedTable
ref={appointmentsTableRef}
prismaModel={"eventAppointment"}
filter={{
eventId: event?.id,
}}
include={{
Presenter: true,
Participants: true,
}}
columns={[
{
header: "Datum",
accessorKey: "appointmentDate",
accessorFn: (date) =>
new Date(date.appointmentDate).toLocaleDateString(),
},
{
header: "Presenter",
accessorKey: "presenter",
cell: ({ row }) => (
<div className="flex items-center">
<span className="ml-2">
{(row.original as any).Presenter.firstname}{" "}
{(row.original as any).Presenter.lastname}
</span>
</div>
),
},
{
header: "Teilnehmer",
accessorKey: "Participants",
cell: ({ row }) => (
<div className="flex items-center">
<UserIcon className="w-5 h-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>
);
<PaginatedTable
ref={appointmentsTableRef}
prismaModel={"eventAppointment"}
filter={{
eventId: event?.id,
}}
include={{
Presenter: true,
Participants: true,
}}
columns={[
{
header: "Datum",
accessorKey: "appointmentDate",
accessorFn: (date) =>
new Date(date.appointmentDate).toLocaleDateString(),
},
},
]}
/>
{
header: "Presenter",
accessorKey: "presenter",
cell: ({ row }) => (
<div className="flex items-center">
<span className="ml-2">
{(row.original as any).Presenter.firstname}{" "}
{(row.original as any).Presenter.lastname}
</span>
</div>
),
},
{
header: "Teilnehmer",
accessorKey: "Participants",
cell: ({ row }) => (
<div className="flex items-center">
<UserIcon className="w-5 h-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>
);
},
},
]}
/>
</div>
</div>
</div>
) : null}
<div className="card bg-base-200 shadow-xl col-span-6">
<div className="card-body ">
<div className="flex w-full gap-4">

View File

@@ -8,7 +8,7 @@ import {
import { Event, EventAppointment, Participant, prisma, User } from "@repo/db";
import { cn } from "../../../../helper/cn";
import { inscribeToMoodleCourse, upsertParticipant } from "../actions";
import { Clock10Icon, Cross } from "lucide-react";
import { Check, Clock10Icon, Cross, EyeIcon } from "lucide-react";
import { useForm } from "react-hook-form";
import {
EventAppointmentOptionalDefaults,
@@ -22,6 +22,7 @@ import { Select } from "../../../_components/ui/Select";
import toast from "react-hot-toast";
import { useRouter } from "next/navigation";
import { JsonArray } from "../../../../../../packages/database/generated/client/runtime/library";
import { eventCompleted } from "@repo/ui";
interface ModalBtnProps {
title: string;
@@ -90,10 +91,25 @@ const ModalBtn = ({
className={cn(
"btn btn-outline btn-info btn-wide",
event.type === "OBLIGATED_COURSE" && "btn-secondary",
eventCompleted(event, participant) && "btn-success",
)}
onClick={openModal}
>
<EnterIcon /> Anmelden
{participant && !eventCompleted(event, participant) && (
<>
<EyeIcon /> Anzeigen
</>
)}
{!participant && (
<>
<EnterIcon /> Anmelden
</>
)}
{eventCompleted(event, participant) && (
<>
<Check /> Abgeschlossen
</>
)}
</button>
<dialog id={modalId} className="modal">
<div className="modal-box">

View File

@@ -3,6 +3,7 @@ import { ArrowRight, NotebookText, Award, RocketIcon } from "lucide-react";
import Link from "next/link";
import Events from "./_components/Events";
import StatsClientWrapper from "./_components/StatsClientWrapper";
import { Badges } from "./_components/Badges";
/*
✔️ Einlog-Zeit
@@ -36,22 +37,7 @@ export default function Home() {
<Logbook />
</div>
</div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<div className="card-body">
<h2 className="card-title justify-between">
<span className="card-title">
<Award className="w-4 h-4" /> Verdiente Abzeichen
</span>
</h2>
Badges
</div>
</div>
</div>
<div className="col-span-full">
<p className="text-xl font-semibold text-left flex items-center gap-2 mb-2 mt-5">
<RocketIcon className="w-4 h-4" /> Laufende Events & Kurse
</p>
<Badges />
</div>
<Events />
</div>

View File

@@ -0,0 +1,37 @@
import DatePicker, { DatePickerProps, registerLocale } from "react-datepicker";
import {
Control,
Controller,
FieldValues,
Path,
PathValue,
} from "react-hook-form";
import { de } from "date-fns/locale";
registerLocale("de", de);
interface DateInputProps<T extends FieldValues>
extends Omit<DatePickerProps, "onChange" | "selected"> {
control: Control<T>;
name: Path<T>;
}
export const DateInput = <T extends FieldValues>({
control,
name,
...props
}: DateInputProps<T>) => {
return (
<Controller
control={control}
name={name}
render={({ field }) => (
<DatePicker
locale={"de"}
onChange={(date) => field.onChange(date)}
selected={field.value}
{...(props as any)}
/>
)}
/>
);
};