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
|
.pnp.js
|
||||||
|
|
||||||
migrations
|
migrations
|
||||||
|
mkcert
|
||||||
|
|
||||||
# Local env files
|
# Local env files
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -99,3 +99,5 @@ LocalMachine RemoteSigned
|
|||||||
4. http://localhost:8081/admin/category.php?category=authsettings -> Guest login button -> Hide
|
4. http://localhost:8081/admin/category.php?category=authsettings -> Guest login button -> Hide
|
||||||
5. http://localhost:8081/admin/settings.php?section=sitepolicies -> emailchangeconfirmation -> False
|
5. http://localhost:8081/admin/settings.php?section=sitepolicies -> emailchangeconfirmation -> False
|
||||||
6. Beim anlegen des Auth-Services Require Email verification deaktivieren
|
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";
|
import "modules/chron";
|
||||||
|
|
||||||
|
// Add API eventually
|
||||||
console.log("VAR hub Server started");
|
console.log("VAR hub Server started");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getMoodleCourseCompletionStatus, getMoodleUserById } from "./moodle";
|
import { getMoodleCourseCompletionStatus, getMoodleUserById } from "./moodle";
|
||||||
import { CronJob } from "cron";
|
import { CronJob } from "cron";
|
||||||
import { prisma } from "@repo/db";
|
import { prisma } from "@repo/db";
|
||||||
|
import { participantCompleted } from "helper/event";
|
||||||
|
|
||||||
const syncMoodleIds = async () => {
|
const syncMoodleIds = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -30,28 +31,21 @@ const syncMoodleIds = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateParticipantMoodleResults =
|
const updateParticipantMoodleResults = async () => {
|
||||||
(usage: "starter" | "finisher") => async () => {
|
|
||||||
const participantsMoodlePending = await prisma.participant.findMany({
|
const participantsMoodlePending = await prisma.participant.findMany({
|
||||||
where: {
|
where: {
|
||||||
[usage === "starter"
|
finisherMoodleCurseCompleted: false,
|
||||||
? "starterMoodleCourseCompleted"
|
|
||||||
: "finisherMoodleCourseCompleted"]: false,
|
|
||||||
Event: {
|
Event: {
|
||||||
[usage === "starter"
|
finisherMoodleCourseId: {
|
||||||
? "starterMoodleCourseId"
|
|
||||||
: "finisherMoodleCourseId"]: {
|
|
||||||
not: null,
|
not: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
select: {
|
include: {
|
||||||
id: true,
|
|
||||||
Event: true,
|
Event: true,
|
||||||
User: true,
|
User: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
participantsMoodlePending.map(async (p) => {
|
participantsMoodlePending.map(async (p) => {
|
||||||
if (!p.User) return;
|
if (!p.User) return;
|
||||||
@@ -59,31 +53,49 @@ const updateParticipantMoodleResults =
|
|||||||
|
|
||||||
const quizzResult = await getMoodleCourseCompletionStatus(
|
const quizzResult = await getMoodleCourseCompletionStatus(
|
||||||
p.User.moodleId.toString(),
|
p.User.moodleId.toString(),
|
||||||
p.Event[
|
p.Event.finisherMoodleCourseId!,
|
||||||
usage === "starter"
|
|
||||||
? "starterMoodleCourseId"
|
|
||||||
: "finisherMoodleCourseId"
|
|
||||||
]!,
|
|
||||||
);
|
);
|
||||||
p.Event.finisherMoodleCourseId;
|
|
||||||
if (quizzResult?.completionstatus?.completed === true) {
|
if (quizzResult?.completionstatus?.completed === true) {
|
||||||
await prisma.participant.update({
|
await prisma.participant.update({
|
||||||
where: {
|
where: {
|
||||||
id: p.id,
|
id: p.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
[usage === "starter"
|
finisherMoodleCurseCompleted: true,
|
||||||
? "starterMoodleCurseCompleted"
|
|
||||||
: "finisherMoodleCurseCompleted"]: true,
|
|
||||||
statusLog: {
|
statusLog: {
|
||||||
push: {
|
push: {
|
||||||
event: "Starter course completed",
|
event: "Finisher course completed",
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
user: "system",
|
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({
|
CronJob.from({
|
||||||
cronTime: "*/5 * * * *",
|
cronTime: "*/5 * * * *",
|
||||||
onTick: async () => {
|
onTick: async () => {
|
||||||
await updateParticipantMoodleResults("starter");
|
console.log("Updating participant moodle results");
|
||||||
await updateParticipantMoodleResults("finisher");
|
await updateParticipantMoodleResults();
|
||||||
},
|
},
|
||||||
start: true,
|
start: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateParticipantMoodleResults();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import axios from "axios";
|
|||||||
|
|
||||||
export const getMoodleUserById = async (id: string) => {
|
export const getMoodleUserById = async (id: string) => {
|
||||||
const { data: user } = await axios.get(
|
const { data: user } = await axios.get(
|
||||||
"https://moodle.virtualairrescue.com/webservice/rest/server.php",
|
`${process.env.MOODLE_URL}/webservice/rest/server.php`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
wstoken: process.env.MOODLE_TOKEN,
|
wstoken: process.env.MOODLE_TOKEN,
|
||||||
@@ -31,7 +31,7 @@ export const getMoodleUserById = async (id: string) => {
|
|||||||
|
|
||||||
export const getMoodleQuizResult = async (userId: string, quizId: string) => {
|
export const getMoodleQuizResult = async (userId: string, quizId: string) => {
|
||||||
const { data: quizzes } = await axios.get(
|
const { data: quizzes } = await axios.get(
|
||||||
"https://moodle.virtualairrescue.com/webservice/rest/server.php",
|
`${process.env.MOODLE_URL}/webservice/rest/server.php`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
wstoken: process.env.MOODLE_TOKEN,
|
wstoken: process.env.MOODLE_TOKEN,
|
||||||
@@ -50,7 +50,7 @@ export const getMoodleCourseCompletionStatus = async (
|
|||||||
courseId: string,
|
courseId: string,
|
||||||
) => {
|
) => {
|
||||||
const { data: completionStatus } = await axios.get(
|
const { data: completionStatus } = await axios.get(
|
||||||
"https://moodle.virtualairrescue.com/webservice/rest/server.php",
|
`${process.env.MOODLE_URL}/webservice/rest/server.php`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
wstoken: process.env.MOODLE_TOKEN,
|
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;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "hub-server",
|
"name": "hub-server",
|
||||||
|
"exports": {
|
||||||
|
"helpers": "./helper"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon",
|
"dev": "nodemon",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/db": "*",
|
"@repo/db": "*",
|
||||||
|
"@repo/hub": "*",
|
||||||
"@repo/typescript-config": "*",
|
"@repo/typescript-config": "*",
|
||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.13.5",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
@@ -13,6 +17,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
"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=
|
DISCORD_BOT_TOKEN=
|
||||||
NEXT_PUBLIC_DISCORD_URL=
|
NEXT_PUBLIC_DISCORD_URL=
|
||||||
DISCORD_REDIRECT=
|
DISCORD_REDIRECT=
|
||||||
|
MOODLE_TOKEN=
|
||||||
|
NEXT_PUBLIC_MOODLE_URL=
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
ParticipantOptionalDefaultsSchema,
|
ParticipantOptionalDefaultsSchema,
|
||||||
} from "@repo/db/zod";
|
} from "@repo/db/zod";
|
||||||
import { set, useForm } from "react-hook-form";
|
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 { Bot, Calendar, FileText, UserIcon } from "lucide-react";
|
||||||
import { Input } from "../../../../_components/ui/Input";
|
import { Input } from "../../../../_components/ui/Input";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
@@ -60,7 +60,6 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
eventId: event?.id,
|
eventId: event?.id,
|
||||||
presenterId: values.presenterId,
|
presenterId: values.presenterId,
|
||||||
});
|
});
|
||||||
console.log(createdAppointment);
|
|
||||||
addParticipantModal.current?.close();
|
addParticipantModal.current?.close();
|
||||||
appointmentsTableRef.current?.refresh();
|
appointmentsTableRef.current?.refresh();
|
||||||
})}
|
})}
|
||||||
@@ -83,6 +82,7 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(async (values) => {
|
onSubmit={form.handleSubmit(async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const createdEvent = await upsertEvent(values, event?.id);
|
const createdEvent = await upsertEvent(values, event?.id);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
if (!event) redirect(`/admin/event`);
|
if (!event) redirect(`/admin/event`);
|
||||||
@@ -105,6 +105,15 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Switch form={form} name="hidden" label="Versteckt" />
|
<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>
|
</div>
|
||||||
<div className="card bg-base-200 shadow-xl col-span-3 max-xl:col-span-6">
|
<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">
|
<h2 className="card-title">
|
||||||
<Bot className="w-5 h-5" /> Automation
|
<Bot className="w-5 h-5" /> Automation
|
||||||
</h2>
|
</h2>
|
||||||
<Input
|
|
||||||
form={form}
|
|
||||||
name="starterMoodleCourseId"
|
|
||||||
label="Moodle Anmelde Kurs ID"
|
|
||||||
className="input-sm"
|
|
||||||
/>
|
|
||||||
<Input
|
<Input
|
||||||
name="finisherMoodleCourseId"
|
name="finisherMoodleCourseId"
|
||||||
form={form}
|
form={form}
|
||||||
@@ -144,6 +147,16 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
value: key,
|
value: key,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
<Select
|
||||||
|
isMulti
|
||||||
|
form={form}
|
||||||
|
name="finishedPermissions"
|
||||||
|
label="Berechtigungen bei Abschluss"
|
||||||
|
options={Object.entries(PERMISSION).map(([key, value]) => ({
|
||||||
|
label: value,
|
||||||
|
value: key,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
form={form}
|
form={form}
|
||||||
name="hasPresenceEvents"
|
name="hasPresenceEvents"
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { DrawingPinFilledIcon, EnterIcon } from "@radix-ui/react-icons";
|
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 ModalBtn from "./modalBtn";
|
||||||
import MDEditor from "@uiw/react-md-editor";
|
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 (
|
return (
|
||||||
<div className="col-span-full">
|
<div className="col-span-full">
|
||||||
<div className="card bg-base-200 shadow-xl mb-4">
|
<div className="card bg-base-200 shadow-xl mb-4">
|
||||||
@@ -56,8 +65,10 @@ export const KursItem = ({ user, event }: { user: User; event: Event }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ModalBtn
|
<ModalBtn
|
||||||
|
user={user}
|
||||||
|
event={event}
|
||||||
title={event.name}
|
title={event.name}
|
||||||
dates={["Dienstag, 25 Februar 2025", "Mittwoch, 26 Februar 2025"]}
|
dates={event.appointments}
|
||||||
modalId={`${event.name}_modal.${event.id}`}
|
modalId={`${event.name}_modal.${event.id}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 */
|
/* 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">
|
<p className="text-gray-600 text-left flex items-center gap-2">
|
||||||
<DrawingPinFilledIcon /> <b>Teilnahmevoraussetzungen:</b> Keine
|
<DrawingPinFilledIcon /> <b>Teilnahmevoraussetzungen:</b> Keine
|
||||||
</p>
|
</p>
|
||||||
<button className="btn btn-outline btn-secondary btn-wide">
|
<ModalBtn
|
||||||
<EnterIcon /> Zum Moodle Kurs
|
user={user}
|
||||||
</button>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,14 +5,28 @@ import {
|
|||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
EnterIcon,
|
EnterIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} 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 {
|
interface ModalBtnProps {
|
||||||
title: string;
|
title: string;
|
||||||
dates: string[];
|
event: Event;
|
||||||
|
dates: EventAppointment[];
|
||||||
|
participant?: Participant;
|
||||||
|
user: User;
|
||||||
modalId: string;
|
modalId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalBtn = ({ title, dates, modalId }: ModalBtnProps) => {
|
const ModalBtn = ({
|
||||||
|
title,
|
||||||
|
dates,
|
||||||
|
modalId,
|
||||||
|
participant,
|
||||||
|
event,
|
||||||
|
user,
|
||||||
|
}: ModalBtnProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||||
const handleOpen = () => {
|
const handleOpen = () => {
|
||||||
@@ -43,23 +57,28 @@ const ModalBtn = ({ title, dates, modalId }: ModalBtnProps) => {
|
|||||||
|
|
||||||
return (
|
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
|
<EnterIcon /> Anmelden
|
||||||
</button>
|
</button>
|
||||||
<dialog id={modalId} className="modal">
|
<dialog id={modalId} className="modal">
|
||||||
<div className="modal-box">
|
<div className="modal-box">
|
||||||
<h3 className="font-bold text-lg">{title}</h3>
|
<h3 className="font-bold text-lg">{title}</h3>
|
||||||
<p className="py-4 flex items-center gap-2 justify-center">
|
{event.hasPresenceEvents && (
|
||||||
<CheckCircledIcon className="text-success" />
|
|
||||||
Moodle Kurs abgeschlossen
|
|
||||||
</p>
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 justify-center">
|
<div className="flex items-center gap-2 justify-center">
|
||||||
<CalendarIcon />
|
<CalendarIcon />
|
||||||
<select className="select w-full max-w-xs" defaultValue={0}>
|
<select className="select w-full max-w-xs" defaultValue={0}>
|
||||||
<option disabled>Bitte wähle einen Termin aus</option>
|
<option disabled>Bitte wähle einen Termin aus</option>
|
||||||
{dates.map((date, index) => (
|
{dates.map((date, index) => (
|
||||||
<option key={index}>{date}</option>
|
<option key={index}>
|
||||||
|
{date.appointmentDate.toLocaleString()}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,13 +86,24 @@ const ModalBtn = ({ title, dates, modalId }: ModalBtnProps) => {
|
|||||||
Bitte finde dich an diesem Termin in unserem Discord ein.
|
Bitte finde dich an diesem Termin in unserem Discord ein.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{event.finisherMoodleCourseId && (
|
||||||
|
<MoodleCourseIndicator
|
||||||
|
user={user}
|
||||||
|
moodleCourseId={event.finisherMoodleCourseId}
|
||||||
|
completed={participant?.finisherMoodleCurseCompleted}
|
||||||
|
eventId={event.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="modal-action flex justify-between">
|
<div className="modal-action flex justify-between">
|
||||||
<button className="btn" onClick={closeModal}>
|
<button className="btn" onClick={closeModal}>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
|
{event.hasPresenceEvents && (
|
||||||
<button className="btn btn-info btn-outline btn-wide">
|
<button className="btn btn-info btn-outline btn-wide">
|
||||||
<EnterIcon /> Anmelden
|
<EnterIcon /> Anmelden
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="modal-backdrop" onClick={closeModal}>
|
<button className="modal-backdrop" onClick={closeModal}>
|
||||||
@@ -85,3 +115,42 @@ const ModalBtn = ({ title, dates, modalId }: ModalBtnProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ModalBtn;
|
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,16 +1,30 @@
|
|||||||
import { getServerSession } from '../../api/auth/[...nextauth]/auth';
|
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
|
||||||
import { PrismaClient } from '@repo/db';
|
import { PrismaClient } from "@repo/db";
|
||||||
import { PilotKurs, KursItem } from './_components/item';
|
import { ObligatedEvent, KursItem } from "./_components/item";
|
||||||
import { RocketIcon } from '@radix-ui/react-icons';
|
import { RocketIcon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
const session = await getServerSession();
|
const session = await getServerSession();
|
||||||
if (!session) return null;
|
if (!session) return null;
|
||||||
const user = session.user;
|
const user = await prisma.user.findUnique({
|
||||||
const events = await prisma.event.findMany();
|
where: {
|
||||||
|
id: session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
|
const events = await prisma.event.findMany({
|
||||||
|
include: {
|
||||||
|
appointments: true,
|
||||||
|
participants: {
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-6 gap-4">
|
<div className="grid grid-cols-6 gap-4">
|
||||||
<div className="col-span-full">
|
<div className="col-span-full">
|
||||||
@@ -18,10 +32,13 @@ export default async () => {
|
|||||||
<RocketIcon className="w-5 h-5" /> Events & Kurse
|
<RocketIcon className="w-5 h-5" /> Events & Kurse
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<PilotKurs user={user} />
|
|
||||||
{events.map((event) => (
|
{events.map((event) => {
|
||||||
<KursItem user={user} event={event} key={event.id} />
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import {
|
import {
|
||||||
FieldValues,
|
FieldValues,
|
||||||
Path,
|
Path,
|
||||||
RegisterOptions,
|
RegisterOptions,
|
||||||
UseFormReturn,
|
UseFormReturn,
|
||||||
} from 'react-hook-form';
|
} from "react-hook-form";
|
||||||
import SelectTemplate, { Props as SelectTemplateProps } from 'react-select';
|
import SelectTemplate, {
|
||||||
import { cn } from '../../../helper/cn';
|
Props as SelectTemplateProps,
|
||||||
import dynamic from 'next/dynamic';
|
StylesConfig,
|
||||||
|
} from "react-select";
|
||||||
|
import { cn } from "../../../helper/cn";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
interface SelectProps<T extends FieldValues> {
|
interface SelectProps<T extends FieldValues> {
|
||||||
name: Path<T>;
|
name: Path<T>;
|
||||||
@@ -18,6 +21,43 @@ interface SelectProps<T extends FieldValues> {
|
|||||||
className?: 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>({
|
const SelectCom = <T extends FieldValues>({
|
||||||
name,
|
name,
|
||||||
label = name,
|
label = name,
|
||||||
@@ -35,18 +75,19 @@ const SelectCom = <T extends FieldValues>({
|
|||||||
<SelectTemplate
|
<SelectTemplate
|
||||||
{...form.register(name, formOptions)}
|
{...form.register(name, formOptions)}
|
||||||
onChange={(newValue: any) => {
|
onChange={(newValue: any) => {
|
||||||
if ('value' in newValue) {
|
if (Array.isArray(newValue)) {
|
||||||
form.setValue(name, newValue.value);
|
form.setValue(name, newValue.map((v: any) => v.value) as any);
|
||||||
} else {
|
} else {
|
||||||
form.setValue(name, newValue);
|
form.setValue(name, newValue.value);
|
||||||
}
|
}
|
||||||
form.trigger(name);
|
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}
|
placeholder={placeholder}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
{form.formState.errors[name] && (
|
{form.formState.errors[name]?.message && (
|
||||||
<p className="text-error">
|
<p className="text-error">
|
||||||
{form.formState.errors[name].message as string}
|
{form.formState.errors[name].message as string}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,26 +1,8 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getServerSession } from "../auth/[...nextauth]/auth";
|
|
||||||
import { prisma } from "@repo/db";
|
import { prisma } from "@repo/db";
|
||||||
import { generateToken } from "../../(auth)/oauth/_components/action";
|
import { verify } from "jsonwebtoken";
|
||||||
import { decode, verify } from "jsonwebtoken";
|
import { getMoodleUserById } from "../../../helper/moodle";
|
||||||
|
import { inscribeToMoodleCourse } from "../../(app)/events/actions";
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GET = async (req: NextRequest) => {
|
export const GET = async (req: NextRequest) => {
|
||||||
// This route is only used by Moodle, so NextAuth is not used here
|
// 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,
|
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({
|
return NextResponse.json({
|
||||||
...user,
|
...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": {
|
"apps/hub-server": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"cron": "^4.1.0"
|
"cron": "^4.1.0",
|
||||||
|
"dotenv": "^16.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/db": "*",
|
"@repo/db": "*",
|
||||||
@@ -156,6 +157,18 @@
|
|||||||
"undici-types": "~6.20.0"
|
"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": {
|
"apps/hub-server/node_modules/typescript": {
|
||||||
"version": "5.7.3",
|
"version": "5.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
enum PARTICIPANT_STATUS {
|
enum EVENT_TYPE {
|
||||||
WAITING_FOR_ENTRY_TEST
|
COURSE
|
||||||
ENTRY_TEST_FAILED
|
OBLIGATED_COURSE
|
||||||
READY_FOR_EVENT
|
EVENT
|
||||||
PARTICIPATED
|
|
||||||
WAITING_FOR_EXIT_TEST
|
|
||||||
EXIT_TEST_FAILED
|
|
||||||
WAITING_FOR_PERMISISONS
|
|
||||||
FINISHED
|
|
||||||
WAVED
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model EventAppointment {
|
model EventAppointment {
|
||||||
@@ -25,8 +19,9 @@ model EventAppointment {
|
|||||||
model Participant {
|
model Participant {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
userId String @map(name: "user_id")
|
userId String @map(name: "user_id")
|
||||||
starterMoodleCurseCompleted Boolean @default(false)
|
|
||||||
finisherMoodleCurseCompleted Boolean @default(false)
|
finisherMoodleCurseCompleted Boolean @default(false)
|
||||||
|
attended Boolean @default(false)
|
||||||
|
finished Boolean @default(false)
|
||||||
eventAppointmentId Int?
|
eventAppointmentId Int?
|
||||||
statusLog Json[]
|
statusLog Json[]
|
||||||
eventId Int
|
eventId Int
|
||||||
@@ -40,14 +35,14 @@ model Event {
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
description String
|
description String
|
||||||
|
type EVENT_TYPE @default(EVENT)
|
||||||
discordRoleId String? @default("")
|
discordRoleId String? @default("")
|
||||||
hasPresenceEvents Boolean @default(false)
|
hasPresenceEvents Boolean @default(false)
|
||||||
maxParticipants Int? @default(0)
|
maxParticipants Int? @default(0)
|
||||||
starterMoodleCourseId String? @default("")
|
|
||||||
finisherMoodleCourseId String? @default("")
|
finisherMoodleCourseId String? @default("")
|
||||||
finishedBadges String[] @default([])
|
finishedBadges BADGES[] @default([])
|
||||||
requiredBadges String[] @default([])
|
requiredBadges BADGES[] @default([])
|
||||||
finishedPermissions String[] @default([])
|
finishedPermissions PERMISSION[] @default([])
|
||||||
hidden Boolean @default(true)
|
hidden Boolean @default(true)
|
||||||
|
|
||||||
// relations:
|
// relations:
|
||||||
|
|||||||
Reference in New Issue
Block a user