added chron to hub server, removed starterEvent
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ node_modules
|
||||
.pnp.js
|
||||
|
||||
migrations
|
||||
mkcert
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
|
||||
@@ -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
|
||||
|
||||
2
apps/hub-server/.env.example
Normal file
2
apps/hub-server/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
MOODLE_TOKEN=
|
||||
MOODLE_URL=
|
||||
11
apps/hub-server/helper/event.ts
Normal file
11
apps/hub-server/helper/event.ts
Normal 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;
|
||||
};
|
||||
@@ -1,2 +1,5 @@
|
||||
import "dotenv/config";
|
||||
import "modules/chron";
|
||||
|
||||
// Add API eventually
|
||||
console.log("VAR hub Server started");
|
||||
|
||||
@@ -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,71 +31,84 @@ const syncMoodleIds = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const updateParticipantMoodleResults =
|
||||
(usage: "starter" | "finisher") => async () => {
|
||||
const participantsMoodlePending = await prisma.participant.findMany({
|
||||
where: {
|
||||
[usage === "starter"
|
||||
? "starterMoodleCourseCompleted"
|
||||
: "finisherMoodleCourseCompleted"]: false,
|
||||
Event: {
|
||||
[usage === "starter"
|
||||
? "starterMoodleCourseId"
|
||||
: "finisherMoodleCourseId"]: {
|
||||
not: null,
|
||||
},
|
||||
const updateParticipantMoodleResults = async () => {
|
||||
const participantsMoodlePending = await prisma.participant.findMany({
|
||||
where: {
|
||||
finisherMoodleCurseCompleted: false,
|
||||
Event: {
|
||||
finisherMoodleCourseId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
Event: true,
|
||||
User: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
include: {
|
||||
Event: true,
|
||||
User: true,
|
||||
},
|
||||
});
|
||||
await Promise.all(
|
||||
participantsMoodlePending.map(async (p) => {
|
||||
if (!p.User) return;
|
||||
if (!p.User.moodleId) return;
|
||||
|
||||
await Promise.all(
|
||||
participantsMoodlePending.map(async (p) => {
|
||||
if (!p.User) return;
|
||||
if (!p.User.moodleId) return;
|
||||
const quizzResult = await getMoodleCourseCompletionStatus(
|
||||
p.User.moodleId.toString(),
|
||||
p.Event.finisherMoodleCourseId!,
|
||||
);
|
||||
|
||||
const quizzResult = await getMoodleCourseCompletionStatus(
|
||||
p.User.moodleId.toString(),
|
||||
p.Event[
|
||||
usage === "starter"
|
||||
? "starterMoodleCourseId"
|
||||
: "finisherMoodleCourseId"
|
||||
]!,
|
||||
);
|
||||
p.Event.finisherMoodleCourseId;
|
||||
if (quizzResult?.completionstatus?.completed === true) {
|
||||
if (quizzResult?.completionstatus?.completed === true) {
|
||||
await prisma.participant.update({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
data: {
|
||||
finisherMoodleCurseCompleted: true,
|
||||
statusLog: {
|
||||
push: {
|
||||
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: {
|
||||
[usage === "starter"
|
||||
? "starterMoodleCurseCompleted"
|
||||
: "finisherMoodleCurseCompleted"]: true,
|
||||
statusLog: {
|
||||
push: {
|
||||
event: "Starter course completed",
|
||||
timestamp: new Date(),
|
||||
user: "system",
|
||||
},
|
||||
},
|
||||
finished: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
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;
|
||||
return completionStatus as {
|
||||
completionstatus: {
|
||||
completed: true;
|
||||
aggregation: number;
|
||||
};
|
||||
warnings: [];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,3 +6,5 @@ DISCORD_OAUTH_SECRET=
|
||||
DISCORD_BOT_TOKEN=
|
||||
NEXT_PUBLIC_DISCORD_URL=
|
||||
DISCORD_REDIRECT=
|
||||
MOODLE_TOKEN=
|
||||
NEXT_PUBLIC_MOODLE_URL=
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,87 +1,156 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
CheckCircledIcon,
|
||||
CalendarIcon,
|
||||
EnterIcon,
|
||||
CheckCircledIcon,
|
||||
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[];
|
||||
modalId: string;
|
||||
title: string;
|
||||
event: Event;
|
||||
dates: EventAppointment[];
|
||||
participant?: Participant;
|
||||
user: User;
|
||||
modalId: string;
|
||||
}
|
||||
|
||||
const ModalBtn = ({ title, dates, modalId }: ModalBtnProps) => {
|
||||
useEffect(() => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||
const handleOpen = () => {
|
||||
document.body.classList.add("modal-open");
|
||||
};
|
||||
const handleClose = () => {
|
||||
document.body.classList.remove("modal-open");
|
||||
};
|
||||
modal?.addEventListener("show", handleOpen);
|
||||
modal?.addEventListener("close", handleClose);
|
||||
return () => {
|
||||
modal?.removeEventListener("show", handleOpen);
|
||||
modal?.removeEventListener("close", handleClose);
|
||||
};
|
||||
}, [modalId]);
|
||||
const ModalBtn = ({
|
||||
title,
|
||||
dates,
|
||||
modalId,
|
||||
participant,
|
||||
event,
|
||||
user,
|
||||
}: ModalBtnProps) => {
|
||||
useEffect(() => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||
const handleOpen = () => {
|
||||
document.body.classList.add("modal-open");
|
||||
};
|
||||
const handleClose = () => {
|
||||
document.body.classList.remove("modal-open");
|
||||
};
|
||||
modal?.addEventListener("show", handleOpen);
|
||||
modal?.addEventListener("close", handleClose);
|
||||
return () => {
|
||||
modal?.removeEventListener("show", handleOpen);
|
||||
modal?.removeEventListener("close", handleClose);
|
||||
};
|
||||
}, [modalId]);
|
||||
|
||||
const openModal = () => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||
document.body.classList.add("modal-open");
|
||||
modal?.showModal();
|
||||
};
|
||||
const openModal = () => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||
document.body.classList.add("modal-open");
|
||||
modal?.showModal();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||
document.body.classList.remove("modal-open");
|
||||
modal?.close();
|
||||
};
|
||||
const closeModal = () => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||
document.body.classList.remove("modal-open");
|
||||
modal?.close();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button className="btn btn-outline btn-info btn-wide" 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>
|
||||
<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>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<p className="mt-3 text-center">
|
||||
Bitte finde dich an diesem Termin in unserem Discord ein.
|
||||
</p>
|
||||
</div>
|
||||
<div className="modal-action flex justify-between">
|
||||
<button className="btn" onClick={closeModal}>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button className="btn btn-info btn-outline btn-wide">
|
||||
<EnterIcon /> Anmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button className="modal-backdrop" onClick={closeModal}>
|
||||
Abbrechen
|
||||
</button>
|
||||
</dialog>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
{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.appointmentDate.toLocaleString()}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<p className="mt-3 text-center">
|
||||
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}>
|
||||
Abbrechen
|
||||
</button>
|
||||
</dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
26
apps/hub/app/(app)/events/actions.ts
Normal file
26
apps/hub/app/(app)/events/actions.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,27 +1,44 @@
|
||||
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();
|
||||
if (!user) return null;
|
||||
const prisma = new PrismaClient();
|
||||
const session = await getServerSession();
|
||||
if (!session) return null;
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
});
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<div className="col-span-full">
|
||||
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
||||
<RocketIcon className="w-5 h-5" /> Events & Kurse
|
||||
</p>
|
||||
</div>
|
||||
<PilotKurs user={user} />
|
||||
{events.map((event) => (
|
||||
<KursItem user={user} event={event} key={event.id} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
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">
|
||||
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
||||
<RocketIcon className="w-5 h-5" /> Events & Kurse
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,62 +1,103 @@
|
||||
'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';
|
||||
FieldValues,
|
||||
Path,
|
||||
RegisterOptions,
|
||||
UseFormReturn,
|
||||
} 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>;
|
||||
form: UseFormReturn<T>;
|
||||
formOptions?: RegisterOptions<T>;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
name: Path<T>;
|
||||
form: UseFormReturn<T>;
|
||||
formOptions?: RegisterOptions<T>;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
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,
|
||||
placeholder = label,
|
||||
form,
|
||||
formOptions,
|
||||
className,
|
||||
...inputProps
|
||||
name,
|
||||
label = name,
|
||||
placeholder = label,
|
||||
form,
|
||||
formOptions,
|
||||
className,
|
||||
...inputProps
|
||||
}: SelectProps<T>) => {
|
||||
return (
|
||||
<div>
|
||||
<span className="label-text text-lg flex items-center gap-2">
|
||||
{label}
|
||||
</span>
|
||||
<SelectTemplate
|
||||
{...form.register(name, formOptions)}
|
||||
onChange={(newValue: any) => {
|
||||
if ('value' in newValue) {
|
||||
form.setValue(name, newValue.value);
|
||||
} else {
|
||||
form.setValue(name, newValue);
|
||||
}
|
||||
form.trigger(name);
|
||||
}}
|
||||
className={cn('w-full placeholder:text-neutral-600', className)}
|
||||
placeholder={placeholder}
|
||||
{...inputProps}
|
||||
/>
|
||||
{form.formState.errors[name] && (
|
||||
<p className="text-error">
|
||||
{form.formState.errors[name].message as string}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<span className="label-text text-lg flex items-center gap-2">
|
||||
{label}
|
||||
</span>
|
||||
<SelectTemplate
|
||||
{...form.register(name, formOptions)}
|
||||
onChange={(newValue: any) => {
|
||||
if (Array.isArray(newValue)) {
|
||||
form.setValue(name, newValue.map((v: any) => v.value) as any);
|
||||
} else {
|
||||
form.setValue(name, newValue.value);
|
||||
}
|
||||
form.trigger(name);
|
||||
}}
|
||||
styles={customStyles}
|
||||
className={cn("w-full placeholder:text-neutral-600", className)}
|
||||
placeholder={placeholder}
|
||||
{...inputProps}
|
||||
/>
|
||||
{form.formState.errors[name]?.message && (
|
||||
<p className="text-error">
|
||||
{form.formState.errors[name].message as string}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectWrapper = (props: any) => <SelectCom {...props} />;
|
||||
|
||||
export const Select = dynamic(() => Promise.resolve(SelectWrapper), {
|
||||
ssr: false,
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
@@ -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
11
apps/hub/helper/event.ts
Normal 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
56
apps/hub/helper/moodle.ts
Normal 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
15
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -37,18 +32,18 @@ model Participant {
|
||||
}
|
||||
|
||||
model Event {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
description String
|
||||
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([])
|
||||
hidden Boolean @default(true)
|
||||
type EVENT_TYPE @default(EVENT)
|
||||
discordRoleId String? @default("")
|
||||
hasPresenceEvents Boolean @default(false)
|
||||
maxParticipants Int? @default(0)
|
||||
finisherMoodleCourseId String? @default("")
|
||||
finishedBadges BADGES[] @default([])
|
||||
requiredBadges BADGES[] @default([])
|
||||
finishedPermissions PERMISSION[] @default([])
|
||||
hidden Boolean @default(true)
|
||||
|
||||
// relations:
|
||||
participants Participant[]
|
||||
|
||||
Reference in New Issue
Block a user