CHanged Event admin layout
This commit is contained in:
@@ -1,2 +1,6 @@
|
||||
MOODLE_TOKEN=
|
||||
MOODLE_URL=
|
||||
MOODLE_URL=
|
||||
MAIL_SERVER=
|
||||
MAIL_USER=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_PORT=
|
||||
@@ -2,6 +2,8 @@ import { getMoodleCourseCompletionStatus, getMoodleUserById } from "./moodle";
|
||||
import { CronJob } from "cron";
|
||||
import { prisma } from "@repo/db";
|
||||
import { eventCompleted } from "@repo/ui/helper";
|
||||
import { sendCourseCompletedEmail } from "modules/mail";
|
||||
import { handleParticipantFinished } from "modules/event";
|
||||
|
||||
const syncMoodleIds = async () => {
|
||||
try {
|
||||
@@ -57,7 +59,7 @@ const updateParticipantMoodleResults = async () => {
|
||||
);
|
||||
|
||||
if (quizzResult?.completionstatus?.completed === true) {
|
||||
await prisma.participant.update({
|
||||
return prisma.participant.update({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
@@ -65,7 +67,7 @@ const updateParticipantMoodleResults = async () => {
|
||||
finisherMoodleCurseCompleted: true,
|
||||
statusLog: {
|
||||
push: {
|
||||
event: "Finisher course completed",
|
||||
event: "Moodle-Kurs abgeschlossen",
|
||||
timestamp: new Date(),
|
||||
user: "system",
|
||||
},
|
||||
@@ -77,33 +79,40 @@ const updateParticipantMoodleResults = async () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const checkedFinishedParticipants = async () => {
|
||||
export const checkFinishedParticipants = async () => {
|
||||
console.log("Checking finished participants");
|
||||
const participantsPending = await prisma.participant.findMany({
|
||||
where: {
|
||||
finished: false,
|
||||
completetionWorkflowFinished: false,
|
||||
},
|
||||
include: {
|
||||
Event: true,
|
||||
User: true,
|
||||
},
|
||||
});
|
||||
|
||||
participantsPending.forEach(async (p) => {
|
||||
if (!p.User) return;
|
||||
if (!p.User.moodleId) return;
|
||||
|
||||
const completed = eventCompleted(p.Event, p);
|
||||
|
||||
if (!completed) return;
|
||||
handleParticipantFinished(p.Event, p, p.User);
|
||||
});
|
||||
};
|
||||
|
||||
CronJob.from({ cronTime: "0 * * * *", onTick: syncMoodleIds, start: true });
|
||||
CronJob.from({
|
||||
cronTime: "*/5 * * * *",
|
||||
cronTime: "*/1 * * * *",
|
||||
onTick: async () => {
|
||||
console.log("Updating participant moodle results");
|
||||
await updateParticipantMoodleResults();
|
||||
await checkFinishedParticipants();
|
||||
},
|
||||
start: true,
|
||||
});
|
||||
|
||||
updateParticipantMoodleResults();
|
||||
const debug = async () => {
|
||||
await updateParticipantMoodleResults();
|
||||
await checkFinishedParticipants();
|
||||
};
|
||||
|
||||
debug();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Event, Participant, prisma, User } from "@repo/db";
|
||||
import { sendCourseCompletedEmail } from "modules/mail";
|
||||
|
||||
export const handleParticipantFinished = async (
|
||||
event: Event,
|
||||
@@ -11,9 +12,6 @@ export const handleParticipantFinished = async (
|
||||
},
|
||||
});
|
||||
|
||||
//TODO: Send Discord Message
|
||||
//TODO: Send Email
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
@@ -22,19 +20,24 @@ export const handleParticipantFinished = async (
|
||||
badges: {
|
||||
push: event.finishedBadges,
|
||||
},
|
||||
permissions: event.finishedPermissions,
|
||||
permissions: {
|
||||
push: event.finishedPermissions,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
//TODO: Send Discord Message
|
||||
await sendCourseCompletedEmail(user.email, user, event);
|
||||
|
||||
await prisma.participant.update({
|
||||
where: {
|
||||
id: participant.id,
|
||||
},
|
||||
data: {
|
||||
finished: true,
|
||||
completetionWorkflowFinished: true,
|
||||
statusLog: {
|
||||
push: {
|
||||
event: "Event finished",
|
||||
event: "Berechtigungen und Badges vergeben",
|
||||
timestamp: new Date(),
|
||||
user: "system",
|
||||
},
|
||||
|
||||
21
apps/hub-server/modules/mail-templates/CourseCompleted.tsx
Normal file
21
apps/hub-server/modules/mail-templates/CourseCompleted.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
import { Event, User } from "@repo/db";
|
||||
|
||||
import { Html, Button, render } from "@react-email/components";
|
||||
|
||||
const Template = ({ event, user }: { user: User; event: Event }) => (
|
||||
<Html lang="en">
|
||||
<p>You completed the Course {event.name}</p>
|
||||
<p>Congratulation</p>
|
||||
</Html>
|
||||
);
|
||||
|
||||
export function renderCourseCompleted({
|
||||
user,
|
||||
event,
|
||||
}: {
|
||||
user: User;
|
||||
event: Event;
|
||||
}) {
|
||||
return render(<Template event={event} user={user} />);
|
||||
}
|
||||
59
apps/hub-server/modules/mail.ts
Normal file
59
apps/hub-server/modules/mail.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Event, User } from "@repo/db";
|
||||
import nodemailer from "nodemailer";
|
||||
import { renderCourseCompleted } from "./mail-templates/CourseCompleted";
|
||||
|
||||
let transporter: nodemailer.Transporter | null = null;
|
||||
|
||||
const initTransporter = () => {
|
||||
if (!process.env.MAIL_SERVER)
|
||||
return console.error("MAIL_SERVER is not defined");
|
||||
if (!process.env.MAIL_PORT) return console.error("MAIL_PORT is not defined");
|
||||
if (!process.env.MAIL_USER) return console.error("MAIL_USER is not defined");
|
||||
if (!process.env.MAIL_PASSWORD)
|
||||
return console.error("MAIL_PASSWORD is not defined");
|
||||
|
||||
transporter = nodemailer.createTransport({
|
||||
host: process.env.MAIL_SERVER,
|
||||
port: parseInt(process.env.MAIL_PORT),
|
||||
secure: true, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: process.env.MAIL_USER,
|
||||
pass: process.env.MAIL_PASSWORD,
|
||||
},
|
||||
});
|
||||
transporter.on("error", (err) => {
|
||||
console.error("Mail occurred:", err);
|
||||
});
|
||||
transporter.on("idle", () => {
|
||||
console.log("Mail Idle");
|
||||
});
|
||||
};
|
||||
|
||||
initTransporter();
|
||||
|
||||
export const sendCourseCompletedEmail = async (
|
||||
to: string,
|
||||
user: User,
|
||||
event: Event,
|
||||
) => {
|
||||
const emailHtml = await renderCourseCompleted({ user, event });
|
||||
|
||||
if (!transporter) {
|
||||
console.error("Transporter is not initialized");
|
||||
return;
|
||||
}
|
||||
transporter.sendMail(
|
||||
{
|
||||
from: process.env.MAIL_USER,
|
||||
to,
|
||||
subject: `Congratulations ${user.firstname} on completing ${event.name}`,
|
||||
html: emailHtml,
|
||||
},
|
||||
(error, info) => {
|
||||
if (error) {
|
||||
console.error("Error sending email:", error);
|
||||
} else {
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -11,12 +11,16 @@
|
||||
"@repo/db": "*",
|
||||
"@repo/typescript-config": "*",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"concurrently": "^9.1.2",
|
||||
"typescript": "latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "^0.0.33",
|
||||
"axios": "^1.7.9",
|
||||
"cron": "^4.1.0",
|
||||
"dotenv": "^16.4.7"
|
||||
"dotenv": "^16.4.7",
|
||||
"nodemailer": "^6.10.0",
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"allowImportingTsExtensions": false,
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["**/*.ts", "./index.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"allowImportingTsExtensions": false,
|
||||
"baseUrl": ".",
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": ["**/*.ts", "./index.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -22,33 +22,37 @@ import { Switch } from "../../../../_components/ui/Switch";
|
||||
interface AppointmentModalProps {
|
||||
event?: Event;
|
||||
ref: RefObject<HTMLDialogElement | null>;
|
||||
participantModal: RefObject<HTMLDialogElement | null>;
|
||||
appointmentsTableRef: React.RefObject<PaginatedTableRef>;
|
||||
appointmentForm: UseFormReturn<
|
||||
EventAppointmentOptionalDefaults,
|
||||
any,
|
||||
undefined
|
||||
>;
|
||||
participantForm: UseFormReturn<Participant, any, undefined>;
|
||||
}
|
||||
|
||||
export const AppointmentModal = ({
|
||||
event,
|
||||
ref,
|
||||
participantModal,
|
||||
appointmentsTableRef,
|
||||
appointmentForm,
|
||||
participantForm,
|
||||
}: AppointmentModalProps) => {
|
||||
const { data: session } = useSession();
|
||||
|
||||
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
|
||||
className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
||||
onClick={() => ref.current?.close()}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</form>
|
||||
@@ -57,6 +61,7 @@ export const AppointmentModal = ({
|
||||
</h3>
|
||||
<form
|
||||
onSubmit={appointmentForm.handleSubmit(async (values) => {
|
||||
console.log(values);
|
||||
if (!event) return;
|
||||
const createdAppointment = await upsertAppointment(values);
|
||||
ref.current?.close();
|
||||
@@ -75,131 +80,83 @@ export const AppointmentModal = ({
|
||||
}}
|
||||
/> */}
|
||||
<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 (
|
||||
<>
|
||||
<PaginatedTable
|
||||
hide={appointmentForm.watch("id") === undefined}
|
||||
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);
|
||||
participantModal.current?.showModal();
|
||||
}}
|
||||
className="btn btn-outline btn-sm"
|
||||
>
|
||||
anzeigen
|
||||
</button>
|
||||
{!row.original.attended && event?.hasPresenceEvents && (
|
||||
<button
|
||||
onClick={() => {
|
||||
participantForm.reset(row.original);
|
||||
type="button"
|
||||
onSubmit={() => {}}
|
||||
onClick={async () => {
|
||||
await upsertParticipant({
|
||||
eventId: event!.id,
|
||||
userId: participantForm.watch("userId"),
|
||||
attended: true,
|
||||
});
|
||||
participantTableRef.current?.refresh();
|
||||
}}
|
||||
className="btn btn-outline btn-sm"
|
||||
className="btn btn-outline btn-info btn-sm"
|
||||
>
|
||||
anzeigen
|
||||
Anwesend
|
||||
</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="flex gap-2">
|
||||
<Button type="submit" className="btn btn-primary">
|
||||
Speichern
|
||||
},
|
||||
]}
|
||||
prismaModel={"participant"}
|
||||
filter={{
|
||||
eventAppointmentId: appointmentForm.watch("id"),
|
||||
}}
|
||||
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>
|
||||
{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>
|
||||
<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"
|
||||
key={(s as any).timestamp}
|
||||
>
|
||||
<p>{(s as any).event}</p>
|
||||
<p>{new Date((s as any).timestamp).toLocaleString()}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button>Speichern</Button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
prisma,
|
||||
Prisma,
|
||||
} from "@repo/db";
|
||||
import { Bot, Calendar, FileText, UserIcon } from "lucide-react";
|
||||
import { Bot, Calendar, FileText, User, UserIcon } from "lucide-react";
|
||||
import { Input } from "../../../../_components/ui/Input";
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
@@ -39,6 +39,7 @@ import { Select } from "../../../../_components/ui/Select";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { MarkdownEditor } from "../../../../_components/ui/MDEditor";
|
||||
import { AppointmentModal } from "./AppointmentModal";
|
||||
import { ParticipantModal } from "./ParticipantModal";
|
||||
|
||||
export const Form = ({ event }: { event?: Event }) => {
|
||||
const { data: session } = useSession();
|
||||
@@ -53,17 +54,28 @@ export const Form = ({ event }: { event?: Event }) => {
|
||||
presenterId: session?.user?.id,
|
||||
},
|
||||
});
|
||||
const participantForm = useForm<Participant>({
|
||||
resolver: zodResolver(ParticipantOptionalDefaultsSchema),
|
||||
});
|
||||
const appointmentsTableRef = useRef<PaginatedTableRef>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const appointmentModal = useRef<HTMLDialogElement>(null);
|
||||
const participantModal = useRef<HTMLDialogElement>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppointmentModal
|
||||
participantModal={participantModal}
|
||||
participantForm={participantForm}
|
||||
appointmentForm={appointmentForm}
|
||||
ref={appointmentModal}
|
||||
appointmentsTableRef={appointmentsTableRef}
|
||||
event={event}
|
||||
/>
|
||||
<ParticipantModal
|
||||
participantForm={participantForm}
|
||||
ref={participantModal}
|
||||
/>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(async (values) => {
|
||||
@@ -164,6 +176,8 @@ export const Form = ({ event }: { event?: Event }) => {
|
||||
appointmentModal.current?.showModal();
|
||||
appointmentForm.reset({
|
||||
id: undefined,
|
||||
eventId: event.id,
|
||||
presenterId: session?.user?.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -238,6 +252,62 @@ export const Form = ({ event }: { event?: Event }) => {
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{!form.watch("hasPresenceEvents") ? (
|
||||
<div className="card bg-base-200 shadow-xl col-span-6">
|
||||
<div className="card-body">
|
||||
<PaginatedTable
|
||||
leftOfSearch={
|
||||
<h2 className="card-title">
|
||||
<Calendar className="w-5 h-5" /> Teilnehmer
|
||||
</h2>
|
||||
}
|
||||
searchFields={["User.firstname", "User.lastname"]}
|
||||
ref={appointmentsTableRef}
|
||||
prismaModel={"participant"}
|
||||
filter={{
|
||||
eventId: event?.id,
|
||||
}}
|
||||
include={{
|
||||
User: true,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
header: "Vorname",
|
||||
accessorKey: "User.firstname",
|
||||
},
|
||||
{
|
||||
header: "Nachname",
|
||||
accessorKey: "User.lastname",
|
||||
},
|
||||
{
|
||||
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>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</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">
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Switch } from "../../../../_components/ui/Switch";
|
||||
import { Button } from "../../../../_components/ui/Button";
|
||||
import { Participant, Prisma } from "@repo/db";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import { upsertParticipant } from "../../../events/actions";
|
||||
import { RefObject } from "react";
|
||||
|
||||
interface ParticipantModalProps {
|
||||
participantForm: UseFormReturn<Participant>;
|
||||
ref: RefObject<HTMLDialogElement | null>;
|
||||
}
|
||||
|
||||
export const ParticipantModal = ({
|
||||
participantForm,
|
||||
ref,
|
||||
}: ParticipantModalProps) => {
|
||||
return (
|
||||
<dialog className="modal" ref={ref}>
|
||||
<div className="modal-box">
|
||||
<form method="dialog">
|
||||
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
|
||||
✕
|
||||
</button>
|
||||
</form>
|
||||
<h1 className="text-2xl">Teilnehmer bearbeiten</h1>
|
||||
<form
|
||||
onSubmit={participantForm.handleSubmit(async (data) => {
|
||||
await upsertParticipant({
|
||||
...data,
|
||||
statusLog: data.statusLog as Prisma.InputJsonValue[],
|
||||
});
|
||||
participantForm.reset();
|
||||
ref.current?.close();
|
||||
})}
|
||||
className="space-y-1"
|
||||
>
|
||||
<Switch
|
||||
form={participantForm}
|
||||
name="appointmentCancelled"
|
||||
label="Termin abgesagt"
|
||||
/>
|
||||
<Switch
|
||||
form={participantForm}
|
||||
name="finisherMoodleCurseCompleted"
|
||||
label="Abschluss-Moodle-Kurs abgeschlossen"
|
||||
/>
|
||||
<Switch
|
||||
form={participantForm}
|
||||
name="completetionWorkflowFinished"
|
||||
label="Abgeschlossen (E-Mail-Benachrichtigung senden)"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-xl">Verlauf</h3>
|
||||
{participantForm.watch("statusLog")?.map((s) => (
|
||||
<div className="flex justify-between" key={(s as any).timestamp}>
|
||||
<p>{(s as any).event}</p>
|
||||
<p>{new Date((s as any).timestamp).toLocaleString()}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button type="submit">Speichern</Button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
};
|
||||
@@ -120,6 +120,7 @@ const ModalBtn = ({
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<CalendarIcon />
|
||||
{!!dates.length && (
|
||||
// TODO: Prevent users from selecting an appointment that is full
|
||||
<Select
|
||||
form={selectAppointmentForm as any}
|
||||
options={dates.map((date) => ({
|
||||
@@ -180,10 +181,10 @@ const ModalBtn = ({
|
||||
>
|
||||
absagen
|
||||
</button>
|
||||
<p className="mt-3 text-center">
|
||||
Bitte finde dich an diesem Termin in unserem Discord ein.
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-3 text-center">
|
||||
Bitte finde dich an diesem Termin in unserem Discord ein.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -251,7 +252,7 @@ const MoodleCourseIndicator = ({
|
||||
event: Event;
|
||||
}) => {
|
||||
const courseUrl = `${process.env.NEXT_PUBLIC_MOODLE_URL}/course/view.php?id=${moodleCourseId}`;
|
||||
if (!participant || (event.hasPresenceEvents && !participant?.attended))
|
||||
if (event.hasPresenceEvents && !participant?.attended)
|
||||
return (
|
||||
<p className="py-4 flex items-center gap-2 justify-center">
|
||||
<Clock10Icon className="text-error" />
|
||||
|
||||
@@ -25,6 +25,7 @@ interface PaginatedTableProps<TData>
|
||||
leftOfSearch?: React.ReactNode;
|
||||
rightOfSearch?: React.ReactNode;
|
||||
leftOfPagination?: React.ReactNode;
|
||||
hide?: boolean;
|
||||
ref?: Ref<PaginatedTableRef>;
|
||||
}
|
||||
|
||||
@@ -39,6 +40,7 @@ export function PaginatedTable<TData>({
|
||||
leftOfSearch,
|
||||
rightOfSearch,
|
||||
leftOfPagination,
|
||||
hide,
|
||||
...restProps
|
||||
}: PaginatedTableProps<TData>) {
|
||||
const [data, setData] = useState<TData[]>([]);
|
||||
@@ -111,19 +113,23 @@ export function PaginatedTable<TData>({
|
||||
)}
|
||||
<div className="flex justify-center">{rightOfSearch}</div>
|
||||
</div>
|
||||
<SortableTable
|
||||
data={data}
|
||||
prismaModel={prismaModel}
|
||||
showEditButton={showEditButton}
|
||||
{...restProps}
|
||||
/>
|
||||
{!hide && (
|
||||
<SortableTable
|
||||
data={data}
|
||||
prismaModel={prismaModel}
|
||||
showEditButton={showEditButton}
|
||||
{...restProps}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-between">
|
||||
{leftOfPagination}
|
||||
<Pagination
|
||||
totalPages={Math.ceil(total / rowsPerPage)}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
/>
|
||||
{!hide && (
|
||||
<Pagination
|
||||
totalPages={Math.ceil(total / rowsPerPage)}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,10 +23,11 @@ export const GET = async (req: NextRequest) => {
|
||||
});
|
||||
if (!user)
|
||||
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||
|
||||
setTimeout(async () => {
|
||||
console.log("getting moodle ID");
|
||||
const moodleUser = await getMoodleUserById(user.id);
|
||||
prisma.user.update({
|
||||
console.log("got moodle ID", moodleUser.id);
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
@@ -35,8 +36,6 @@ export const GET = async (req: NextRequest) => {
|
||||
},
|
||||
});
|
||||
|
||||
if (user.moodleId) return;
|
||||
|
||||
const participatingEvents = await prisma.participant.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@repo/db": "*",
|
||||
"@repo/ui": "*",
|
||||
"@tanstack/react-query": "^5.67.2",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@uiw/react-md-editor": "^4.0.5",
|
||||
"axios": "^1.7.9",
|
||||
|
||||
Reference in New Issue
Block a user