added chron to hub server, removed starterEvent

This commit is contained in:
PxlLoewe
2025-02-28 07:21:07 +01:00
parent bbcde2eb4a
commit a477b65c2f
20 changed files with 584 additions and 281 deletions

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ node_modules
.pnp.js
migrations
mkcert
# Local env files
.env

View File

@@ -99,3 +99,5 @@ LocalMachine RemoteSigned
4. http://localhost:8081/admin/category.php?category=authsettings -> Guest login button -> Hide
5. http://localhost:8081/admin/settings.php?section=sitepolicies -> emailchangeconfirmation -> False
6. Beim anlegen des Auth-Services Require Email verification deaktivieren
7. Beim erstellen der Role für API user: permission moodle/site:viewuseridentity geben
8. API user zu moodle kursen hinzufügen, um andere Nutzer hinzuzufügen

View File

@@ -0,0 +1,2 @@
MOODLE_TOKEN=
MOODLE_URL=

View File

@@ -0,0 +1,11 @@
import { Event, Participant } from "@repo/db";
export const participantCompleted = (
event: Event,
participant: Participant,
) => {
if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted)
return false;
if (event.hasPresenceEvents && !participant.attended) return false;
return true;
};

View File

@@ -1,2 +1,5 @@
import "dotenv/config";
import "modules/chron";
// Add API eventually
console.log("VAR hub Server started");

View File

@@ -1,6 +1,7 @@
import { getMoodleCourseCompletionStatus, getMoodleUserById } from "./moodle";
import { CronJob } from "cron";
import { prisma } from "@repo/db";
import { participantCompleted } from "helper/event";
const syncMoodleIds = async () => {
try {
@@ -30,28 +31,21 @@ const syncMoodleIds = async () => {
}
};
const updateParticipantMoodleResults =
(usage: "starter" | "finisher") => async () => {
const updateParticipantMoodleResults = async () => {
const participantsMoodlePending = await prisma.participant.findMany({
where: {
[usage === "starter"
? "starterMoodleCourseCompleted"
: "finisherMoodleCourseCompleted"]: false,
finisherMoodleCurseCompleted: false,
Event: {
[usage === "starter"
? "starterMoodleCourseId"
: "finisherMoodleCourseId"]: {
finisherMoodleCourseId: {
not: null,
},
},
},
select: {
id: true,
include: {
Event: true,
User: true,
},
});
await Promise.all(
participantsMoodlePending.map(async (p) => {
if (!p.User) return;
@@ -59,31 +53,49 @@ const updateParticipantMoodleResults =
const quizzResult = await getMoodleCourseCompletionStatus(
p.User.moodleId.toString(),
p.Event[
usage === "starter"
? "starterMoodleCourseId"
: "finisherMoodleCourseId"
]!,
p.Event.finisherMoodleCourseId!,
);
p.Event.finisherMoodleCourseId;
if (quizzResult?.completionstatus?.completed === true) {
await prisma.participant.update({
where: {
id: p.id,
},
data: {
[usage === "starter"
? "starterMoodleCurseCompleted"
: "finisherMoodleCurseCompleted"]: true,
finisherMoodleCurseCompleted: true,
statusLog: {
push: {
event: "Starter course completed",
event: "Finisher course completed",
timestamp: new Date(),
user: "system",
},
},
},
});
if (participantCompleted(p.Event, p)) {
// Event is completed, give relating permissions
await prisma.user.update({
where: {
id: p.userId,
},
data: {
permissions: {
push: p.Event.finishedPermissions,
},
badges: {
push: p.Event.finishedBadges,
},
},
});
await prisma.participant.update({
where: {
id: p.id,
},
data: {
finished: true,
},
});
}
}
}),
);
@@ -93,8 +105,10 @@ CronJob.from({ cronTime: "0 * * * *", onTick: syncMoodleIds, start: true });
CronJob.from({
cronTime: "*/5 * * * *",
onTick: async () => {
await updateParticipantMoodleResults("starter");
await updateParticipantMoodleResults("finisher");
console.log("Updating participant moodle results");
await updateParticipantMoodleResults();
},
start: true,
});
updateParticipantMoodleResults();

View File

@@ -2,7 +2,7 @@ import axios from "axios";
export const getMoodleUserById = async (id: string) => {
const { data: user } = await axios.get(
"https://moodle.virtualairrescue.com/webservice/rest/server.php",
`${process.env.MOODLE_URL}/webservice/rest/server.php`,
{
params: {
wstoken: process.env.MOODLE_TOKEN,
@@ -31,7 +31,7 @@ export const getMoodleUserById = async (id: string) => {
export const getMoodleQuizResult = async (userId: string, quizId: string) => {
const { data: quizzes } = await axios.get(
"https://moodle.virtualairrescue.com/webservice/rest/server.php",
`${process.env.MOODLE_URL}/webservice/rest/server.php`,
{
params: {
wstoken: process.env.MOODLE_TOKEN,
@@ -50,7 +50,7 @@ export const getMoodleCourseCompletionStatus = async (
courseId: string,
) => {
const { data: completionStatus } = await axios.get(
"https://moodle.virtualairrescue.com/webservice/rest/server.php",
`${process.env.MOODLE_URL}/webservice/rest/server.php`,
{
params: {
wstoken: process.env.MOODLE_TOKEN,
@@ -61,29 +61,11 @@ export const getMoodleCourseCompletionStatus = async (
},
},
);
return completionStatus;
return completionStatus as {
completionstatus: {
completed: true;
aggregation: number;
};
warnings: [];
};
export const enrollUserInCourse = async (
courseid: number | string,
userid: number | string,
) => {
const { data: enrollmentResponse } = await axios.get(
"https://moodle.virtualairrescue.com/webservice/rest/server.php",
{
params: {
wstoken: process.env.MOODLE_TOKEN,
wsfunction: "enrol_manual_enrol_users",
moodlewsrestformat: "json",
enrolments: [
{
roleid: 5,
userid,
courseid,
},
],
},
},
);
return enrollmentResponse;
};

View File

@@ -1,11 +1,15 @@
{
"name": "hub-server",
"exports": {
"helpers": "./helper"
},
"scripts": {
"dev": "nodemon",
"build": "tsc"
},
"devDependencies": {
"@repo/db": "*",
"@repo/hub": "*",
"@repo/typescript-config": "*",
"@types/node": "^22.13.5",
"concurrently": "^9.1.2",
@@ -13,6 +17,7 @@
},
"dependencies": {
"axios": "^1.7.9",
"cron": "^4.1.0"
"cron": "^4.1.0",
"dotenv": "^16.4.7"
}
}

View File

@@ -6,3 +6,5 @@ DISCORD_OAUTH_SECRET=
DISCORD_BOT_TOKEN=
NEXT_PUBLIC_DISCORD_URL=
DISCORD_REDIRECT=
MOODLE_TOKEN=
NEXT_PUBLIC_MOODLE_URL=

View File

@@ -8,7 +8,7 @@ import {
ParticipantOptionalDefaultsSchema,
} from "@repo/db/zod";
import { set, useForm } from "react-hook-form";
import { BADGES, Event, EventAppointment, User } from "@repo/db";
import { BADGES, Event, EVENT_TYPE, PERMISSION } from "@repo/db";
import { Bot, Calendar, FileText, UserIcon } from "lucide-react";
import { Input } from "../../../../_components/ui/Input";
import { useRef, useState } from "react";
@@ -60,7 +60,6 @@ export const Form = ({ event }: { event?: Event }) => {
eventId: event?.id,
presenterId: values.presenterId,
});
console.log(createdAppointment);
addParticipantModal.current?.close();
appointmentsTableRef.current?.refresh();
})}
@@ -83,6 +82,7 @@ export const Form = ({ event }: { event?: Event }) => {
<form
onSubmit={form.handleSubmit(async (values) => {
setLoading(true);
const createdEvent = await upsertEvent(values, event?.id);
setLoading(false);
if (!event) redirect(`/admin/event`);
@@ -105,6 +105,15 @@ export const Form = ({ event }: { event?: Event }) => {
})}
/>
<Switch form={form} name="hidden" label="Versteckt" />
<Select
form={form}
name="type"
label="Typ"
options={Object.entries(EVENT_TYPE).map(([key, value]) => ({
label: key,
value: value,
}))}
/>
</div>
</div>
<div className="card bg-base-200 shadow-xl col-span-3 max-xl:col-span-6">
@@ -112,12 +121,6 @@ export const Form = ({ event }: { event?: Event }) => {
<h2 className="card-title">
<Bot className="w-5 h-5" /> Automation
</h2>
<Input
form={form}
name="starterMoodleCourseId"
label="Moodle Anmelde Kurs ID"
className="input-sm"
/>
<Input
name="finisherMoodleCourseId"
form={form}
@@ -144,6 +147,16 @@ export const Form = ({ event }: { event?: Event }) => {
value: key,
}))}
/>
<Select
isMulti
form={form}
name="finishedPermissions"
label="Berechtigungen bei Abschluss"
options={Object.entries(PERMISSION).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
<Switch
form={form}
name="hasPresenceEvents"

View File

@@ -1,10 +1,19 @@
"use client";
import { DrawingPinFilledIcon, EnterIcon } from "@radix-ui/react-icons";
import { Event, User } from "@repo/db";
import { Event, Participant, EventAppointment, User } from "@repo/db";
import ModalBtn from "./modalBtn";
import MDEditor from "@uiw/react-md-editor";
export const KursItem = ({ user, event }: { user: User; event: Event }) => {
export const KursItem = ({
user,
event,
}: {
user: User;
event: Event & {
appointments: EventAppointment[];
participants: Participant[];
};
}) => {
return (
<div className="col-span-full">
<div className="card bg-base-200 shadow-xl mb-4">
@@ -56,8 +65,10 @@ export const KursItem = ({ user, event }: { user: User; event: Event }) => {
)}
</div>
<ModalBtn
user={user}
event={event}
title={event.name}
dates={["Dienstag, 25 Februar 2025", "Mittwoch, 26 Februar 2025"]}
dates={event.appointments}
modalId={`${event.name}_modal.${event.id}`}
/>
</div>
@@ -67,7 +78,13 @@ export const KursItem = ({ user, event }: { user: User; event: Event }) => {
);
};
export const PilotKurs = ({ user }: { user: User }) => {
export const ObligatedEvent = ({
event,
user,
}: {
event: Event;
user: User;
}) => {
{
/* STATISCH, DA FÜR ALLE NEUEN MITGLIEDER MANDATORY, WIRD AUSGEBLENDET WENN ABSOLVIERT */
}
@@ -96,9 +113,14 @@ export const PilotKurs = ({ user }: { user: User }) => {
<p className="text-gray-600 text-left flex items-center gap-2">
<DrawingPinFilledIcon /> <b>Teilnahmevoraussetzungen:</b> Keine
</p>
<button className="btn btn-outline btn-secondary btn-wide">
<EnterIcon /> Zum Moodle Kurs
</button>
<ModalBtn
user={user}
event={event}
title={event.name}
dates={(event as any).appointments}
participant={(event as any).participants[0]}
modalId={`${event.name}_modal.${event.id}`}
/>
</div>
</div>
</div>

View File

@@ -5,14 +5,28 @@ import {
CalendarIcon,
EnterIcon,
} from "@radix-ui/react-icons";
import { Event, EventAppointment, Participant, User } from "@repo/db";
import { cn } from "../../../../helper/cn";
import { addParticipant, inscribeToMoodleCourse } from "../actions";
import { useSession } from "next-auth/react";
interface ModalBtnProps {
title: string;
dates: string[];
event: Event;
dates: EventAppointment[];
participant?: Participant;
user: User;
modalId: string;
}
const ModalBtn = ({ title, dates, modalId }: ModalBtnProps) => {
const ModalBtn = ({
title,
dates,
modalId,
participant,
event,
user,
}: ModalBtnProps) => {
useEffect(() => {
const modal = document.getElementById(modalId) as HTMLDialogElement;
const handleOpen = () => {
@@ -43,23 +57,28 @@ const ModalBtn = ({ title, dates, modalId }: ModalBtnProps) => {
return (
<>
<button className="btn btn-outline btn-info btn-wide" onClick={openModal}>
<button
className={cn(
"btn btn-outline btn-info btn-wide",
event.type === "OBLIGATED_COURSE" && "btn-secondary",
)}
onClick={openModal}
>
<EnterIcon /> Anmelden
</button>
<dialog id={modalId} className="modal">
<div className="modal-box">
<h3 className="font-bold text-lg">{title}</h3>
<p className="py-4 flex items-center gap-2 justify-center">
<CheckCircledIcon className="text-success" />
Moodle Kurs abgeschlossen
</p>
{event.hasPresenceEvents && (
<div>
<div className="flex items-center gap-2 justify-center">
<CalendarIcon />
<select className="select w-full max-w-xs" defaultValue={0}>
<option disabled>Bitte wähle einen Termin aus</option>
{dates.map((date, index) => (
<option key={index}>{date}</option>
<option key={index}>
{date.appointmentDate.toLocaleString()}
</option>
))}
</select>
</div>
@@ -67,13 +86,24 @@ const ModalBtn = ({ title, dates, modalId }: ModalBtnProps) => {
Bitte finde dich an diesem Termin in unserem Discord ein.
</p>
</div>
)}
{event.finisherMoodleCourseId && (
<MoodleCourseIndicator
user={user}
moodleCourseId={event.finisherMoodleCourseId}
completed={participant?.finisherMoodleCurseCompleted}
eventId={event.id}
/>
)}
<div className="modal-action flex justify-between">
<button className="btn" onClick={closeModal}>
Abbrechen
</button>
{event.hasPresenceEvents && (
<button className="btn btn-info btn-outline btn-wide">
<EnterIcon /> Anmelden
</button>
)}
</div>
</div>
<button className="modal-backdrop" onClick={closeModal}>
@@ -85,3 +115,42 @@ const ModalBtn = ({ title, dates, modalId }: ModalBtnProps) => {
};
export default ModalBtn;
const MoodleCourseIndicator = ({
completed,
moodleCourseId,
eventId,
user,
}: {
completed?: boolean;
moodleCourseId: string;
eventId: number;
user: User;
}) => {
const courseUrl = `${process.env.NEXT_PUBLIC_MOODLE_URL}/course/view.php?id=${moodleCourseId}`;
if (completed)
return (
<p className="py-4 flex items-center gap-2 justify-center">
<CheckCircledIcon className="text-success" />
Moodle Kurs abgeschlossen
</p>
);
return (
<p className="py-4 flex items-center gap-2 justify-center">
Moodle-Kurs Benötigt
<button
className="btn btn-xs btn-info ml-2"
onClick={async () => {
await addParticipant(eventId, user.id);
if (user.moodleId) {
await inscribeToMoodleCourse(moodleCourseId, user.moodleId);
}
window.open(courseUrl, "_blank");
}}
>
Zum Moodle Kurs
</button>
</p>
);
};

View File

@@ -0,0 +1,26 @@
"use server";
import { enrollUserInCourse } from "../../../helper/moodle";
import { prisma } from "@repo/db";
export const inscribeToMoodleCourse = async (
moodleCourseId: string | number,
userMoodleId: string | number,
) => {
await enrollUserInCourse(moodleCourseId, userMoodleId);
};
export const addParticipant = async (eventId: number, userId: string) => {
const participant = await prisma.participant.findFirst({
where: {
userId: userId,
},
});
if (!participant) {
await prisma.participant.create({
data: {
userId: userId,
eventId,
},
});
}
};

View File

@@ -1,16 +1,30 @@
import { getServerSession } from '../../api/auth/[...nextauth]/auth';
import { PrismaClient } from '@repo/db';
import { PilotKurs, KursItem } from './_components/item';
import { RocketIcon } from '@radix-ui/react-icons';
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
import { PrismaClient } from "@repo/db";
import { ObligatedEvent, KursItem } from "./_components/item";
import { RocketIcon } from "@radix-ui/react-icons";
export default async () => {
const prisma = new PrismaClient();
const session = await getServerSession();
if (!session) return null;
const user = session.user;
const events = await prisma.event.findMany();
const user = await prisma.user.findUnique({
where: {
id: session.user.id,
},
});
if (!user) return null;
const events = await prisma.event.findMany({
include: {
appointments: true,
participants: {
where: {
userId: user.id,
},
},
},
});
return (
<div className="grid grid-cols-6 gap-4">
<div className="col-span-full">
@@ -18,10 +32,13 @@ export default async () => {
<RocketIcon className="w-5 h-5" /> Events & Kurse
</p>
</div>
<PilotKurs user={user} />
{events.map((event) => (
<KursItem user={user} event={event} key={event.id} />
))}
{events.map((event) => {
if (event.type === "OBLIGATED_COURSE")
return <ObligatedEvent user={user} event={event} key={event.id} />;
if (event.type === "COURSE")
return <KursItem user={user} event={event} key={event.id} />;
})}
</div>
);
};

View File

@@ -1,13 +1,16 @@
'use client';
"use client";
import {
FieldValues,
Path,
RegisterOptions,
UseFormReturn,
} from 'react-hook-form';
import SelectTemplate, { Props as SelectTemplateProps } from 'react-select';
import { cn } from '../../../helper/cn';
import dynamic from 'next/dynamic';
} from "react-hook-form";
import SelectTemplate, {
Props as SelectTemplateProps,
StylesConfig,
} from "react-select";
import { cn } from "../../../helper/cn";
import dynamic from "next/dynamic";
interface SelectProps<T extends FieldValues> {
name: Path<T>;
@@ -18,6 +21,43 @@ interface SelectProps<T extends FieldValues> {
className?: string;
}
const customStyles: StylesConfig<any, false> = {
control: (provided) => ({
...provided,
backgroundColor: "var(--color-base-100)",
borderColor: "color-mix(in oklab, var(--color-base-content) 20%, #0000);",
borderRadius: "0.5rem",
padding: "0.25rem",
boxShadow: "none",
"&:hover": {
borderColor: "color-mix(in oklab, var(--color-base-content) 20%, #0000);",
},
}),
option: (provided, state) => ({
...provided,
backgroundColor: state.isSelected ? "hsl(var(--p))" : "hsl(var(--b1))",
color: "var(--color-primary-content)",
"&:hover": { backgroundColor: "var(--color-base-200)" }, // DaisyUI secondary color
}),
multiValueLabel: (provided) => ({
...provided,
color: "var(--color-primary-content)",
}),
singleValue: (provided) => ({
...provided,
color: "var(--color-primary-content)",
}),
multiValue: (provided) => ({
...provided,
backgroundColor: "var(--color-base-300)",
}),
menu: (provided) => ({
...provided,
backgroundColor: "var(--color-base-100)",
borderRadius: "0.5rem",
}),
};
const SelectCom = <T extends FieldValues>({
name,
label = name,
@@ -35,18 +75,19 @@ const SelectCom = <T extends FieldValues>({
<SelectTemplate
{...form.register(name, formOptions)}
onChange={(newValue: any) => {
if ('value' in newValue) {
form.setValue(name, newValue.value);
if (Array.isArray(newValue)) {
form.setValue(name, newValue.map((v: any) => v.value) as any);
} else {
form.setValue(name, newValue);
form.setValue(name, newValue.value);
}
form.trigger(name);
}}
className={cn('w-full placeholder:text-neutral-600', className)}
styles={customStyles}
className={cn("w-full placeholder:text-neutral-600", className)}
placeholder={placeholder}
{...inputProps}
/>
{form.formState.errors[name] && (
{form.formState.errors[name]?.message && (
<p className="text-error">
{form.formState.errors[name].message as string}
</p>

View File

@@ -1,26 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "../auth/[...nextauth]/auth";
import { prisma } from "@repo/db";
import { generateToken } from "../../(auth)/oauth/_components/action";
import { decode, verify } from "jsonwebtoken";
export async function middleware(req: NextRequest) {
const authHeader = req.headers.get("authorization");
if (authHeader?.startsWith("Bearer ")) {
const token = authHeader.split(" ")[1];
if (token) {
// Hier kannst du den Token validieren (optional)
req.headers.set("x-next-auth-token", token);
}
// Falls NextAuth keine Session hat, erstellen wir eine Fake-Session
}
return NextResponse.next();
}
import { verify } from "jsonwebtoken";
import { getMoodleUserById } from "../../../helper/moodle";
import { inscribeToMoodleCourse } from "../../(app)/events/actions";
export const GET = async (req: NextRequest) => {
// This route is only used by Moodle, so NextAuth is not used here
@@ -39,6 +21,42 @@ export const GET = async (req: NextRequest) => {
id: decoded.id,
},
});
if (!user)
return NextResponse.json({ error: "User not found" }, { status: 404 });
setTimeout(async () => {
const moodleUser = await getMoodleUserById(user.id);
prisma.user.update({
where: {
id: user.id,
},
data: {
moodleId: moodleUser?.id,
},
});
if (user.moodleId) return;
const participatingEvents = await prisma.participant.findMany({
where: {
userId: user.id,
Event: {
finisherMoodleCourseId: {
not: null,
},
},
},
include: {
Event: true,
},
});
participatingEvents.forEach(async (p) => {
await inscribeToMoodleCourse(
p.Event.finisherMoodleCourseId!,
moodleUser?.id,
);
});
}, 1000);
return NextResponse.json({
...user,

11
apps/hub/helper/event.ts Normal file
View File

@@ -0,0 +1,11 @@
import { Event, Participant } from "@repo/db";
export const participantCompleted = (
event: Event,
participant: Participant,
) => {
if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted)
return false;
if (event.hasPresenceEvents && !participant.attended) return false;
return true;
};

56
apps/hub/helper/moodle.ts Normal file
View File

@@ -0,0 +1,56 @@
import axios from "axios";
export const enrollUserInCourse = async (
courseid: number | string,
userid: number | string,
) => {
const { data: enrollmentResponse } = await axios.get(
`${process.env.NEXT_PUBLIC_MOODLE_URL}/webservice/rest/server.php`,
{
params: {
wstoken: process.env.MOODLE_TOKEN,
wsfunction: "enrol_manual_enrol_users",
moodlewsrestformat: "json",
enrolments: [
{
roleid: 5,
userid: typeof userid === "string" ? parseInt(userid) : userid,
courseid:
typeof courseid === "string" ? parseInt(courseid) : courseid,
},
],
},
},
);
if (enrollmentResponse !== null) console.error(enrollmentResponse);
return enrollmentResponse;
};
export const getMoodleUserById = async (id: string) => {
const { data: user } = await axios.get(
`${process.env.NEXT_PUBLIC_MOODLE_URL}/webservice/rest/server.php`,
{
params: {
wstoken: process.env.MOODLE_TOKEN,
wsfunction: "core_user_get_users_by_field",
moodlewsrestformat: "json",
field: "idnumber",
values: [id],
},
paramsSerializer: {
indexes: true, // use brackets with indexes
},
},
);
const u = user[0];
return (
(u as {
id: number;
username: string;
firstname: string;
lastname: string;
fullname: string;
email: string;
}) || null
);
};

15
package-lock.json generated
View File

@@ -136,7 +136,8 @@
"apps/hub-server": {
"dependencies": {
"axios": "^1.7.9",
"cron": "^4.1.0"
"cron": "^4.1.0",
"dotenv": "^16.4.7"
},
"devDependencies": {
"@repo/db": "*",
@@ -156,6 +157,18 @@
"undici-types": "~6.20.0"
}
},
"apps/hub-server/node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"apps/hub-server/node_modules/typescript": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",

View File

@@ -1,13 +1,7 @@
enum PARTICIPANT_STATUS {
WAITING_FOR_ENTRY_TEST
ENTRY_TEST_FAILED
READY_FOR_EVENT
PARTICIPATED
WAITING_FOR_EXIT_TEST
EXIT_TEST_FAILED
WAITING_FOR_PERMISISONS
FINISHED
WAVED
enum EVENT_TYPE {
COURSE
OBLIGATED_COURSE
EVENT
}
model EventAppointment {
@@ -25,8 +19,9 @@ model EventAppointment {
model Participant {
id Int @id @default(autoincrement())
userId String @map(name: "user_id")
starterMoodleCurseCompleted Boolean @default(false)
finisherMoodleCurseCompleted Boolean @default(false)
attended Boolean @default(false)
finished Boolean @default(false)
eventAppointmentId Int?
statusLog Json[]
eventId Int
@@ -40,14 +35,14 @@ model Event {
id Int @id @default(autoincrement())
name String
description String
type EVENT_TYPE @default(EVENT)
discordRoleId String? @default("")
hasPresenceEvents Boolean @default(false)
maxParticipants Int? @default(0)
starterMoodleCourseId String? @default("")
finisherMoodleCourseId String? @default("")
finishedBadges String[] @default([])
requiredBadges String[] @default([])
finishedPermissions String[] @default([])
finishedBadges BADGES[] @default([])
requiredBadges BADGES[] @default([])
finishedPermissions PERMISSION[] @default([])
hidden Boolean @default(true)
// relations: