diff --git a/apps/discord-server/routes/helper.ts b/apps/discord-server/routes/helper.ts new file mode 100644 index 00000000..f2018d1b --- /dev/null +++ b/apps/discord-server/routes/helper.ts @@ -0,0 +1,57 @@ +import { DISCORD_ROLES, Event, getPublicUser, Participant, prisma } from "@repo/db"; +import { Router } from "express"; +import { changeMemberRoles, getMember } from "routes/member"; + +const router: Router = Router(); + +export const eventCompleted = (event: Event, participant?: Participant) => { + if (!participant) return false; + if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted) return false; + if (event.hasPresenceEvents && !participant.attended) return false; + return true; +}; + +router.post("/set-standard-name", async (req, res) => { + const { memberId, userId } = req.body; + + const user = await prisma.user.findUnique({ + where: { + id: userId, + }, + }); + if (!user) { + res.status(404).json({ error: "User not found" }); + return; + } + const participant = await prisma.participant.findMany({ + where: { + userId: user.id, + }, + include: { + Event: true, + }, + }); + + let eventRoles: string[] = []; + + participant.forEach(async (p) => { + if (!p.Event.discordRoleId) return; + if (eventCompleted(p.Event, p)) { + await changeMemberRoles(memberId, [p.Event.discordRoleId], "remove"); + } else { + await changeMemberRoles(memberId, [p.Event.discordRoleId], "add"); + } + }); + + const publicUser = getPublicUser(user); + const member = await getMember(memberId); + + await member.setNickname(`${publicUser.fullName} (${user.publicId})`); + const isPilot = user.permissions.includes("PILOT"); + const isDispatcher = user.permissions.includes("DISPO"); + + await changeMemberRoles(memberId, [DISCORD_ROLES.PILOT], isPilot ? "add" : "remove"); + await changeMemberRoles(memberId, [DISCORD_ROLES.DISPATCHER], isDispatcher ? "add" : "remove"); +}); + +export default router; diff --git a/apps/discord-server/routes/member.ts b/apps/discord-server/routes/member.ts index f6278887..3625daef 100644 --- a/apps/discord-server/routes/member.ts +++ b/apps/discord-server/routes/member.ts @@ -8,7 +8,7 @@ if (!GUILD_ID) { const router: Router = Router(); -const getMember = async (memberId: string) => { +export const getMember = async (memberId: string) => { const guild = client.guilds.cache.get(GUILD_ID); if (!guild) throw new Error("Guild not found"); try { @@ -36,6 +36,27 @@ router.post("/rename", async (req: Request, res: Response) => { } }); +export const changeMemberRoles = async ( + memberId: string, + roleIds: string[], + action: "add" | "remove", +) => { + const member = await getMember(memberId); + + const currentRoleIds = member.roles.cache.map((role) => role.id); + const filteredRoleIds = + action === "add" + ? roleIds.filter((id: string) => !currentRoleIds.includes(id)) + : roleIds.filter((id: string) => currentRoleIds.includes(id)); + + if (filteredRoleIds.length === 0) { + return { message: `No roles to ${action}` }; + } + + await member.roles[action](filteredRoleIds); + return { message: `Roles ${action}ed successfully` }; +}; + const handleRoleChange = (action: "add" | "remove") => async (req: Request, res: Response) => { const { roleIds, memberId } = req.body; if (!Array.isArray(roleIds) || !memberId) { @@ -43,26 +64,8 @@ const handleRoleChange = (action: "add" | "remove") => async (req: Request, res: return; } try { - const member = await getMember(memberId); - - const currentRoleIds = member.roles.cache.map((role) => role.id); - const filteredRoleIds = - action === "add" - ? roleIds.filter((id: string) => !currentRoleIds.includes(id)) - : roleIds.filter((id: string) => currentRoleIds.includes(id)); - - console.log( - `Attempting to ${action} roles: ${filteredRoleIds.join(", ")} to member ${member.nickname || member.user.username}`, - ); - // Option to skip if no roles to add/remove - if (filteredRoleIds.length === 0) { - console.log(`No roles to ${action}`); - res.status(200).json({ message: `No roles to ${action}` }); - return; - } - - await member.roles[action](roleIds); - res.status(200).json({ message: `Roles ${action}ed successfully` }); + const result = await changeMemberRoles(memberId, roleIds, action); + res.status(200).json(result); } catch (error) { console.error(`Error ${action}ing roles:`, error); res.status(500).json({ error: `Failed to ${action} roles` }); diff --git a/apps/discord-server/routes/router.ts b/apps/discord-server/routes/router.ts index f65c1b0a..ea5ee59b 100644 --- a/apps/discord-server/routes/router.ts +++ b/apps/discord-server/routes/router.ts @@ -1,8 +1,10 @@ import { Router } from "express"; import memberRouter from "./member"; +import helperRouter from "./helper"; const router: Router = Router(); router.use("/member", memberRouter); +router.use("/helper", helperRouter); export default router; diff --git a/apps/hub-server/modules/chron.ts b/apps/hub-server/modules/chron.ts index 2caf14df..a833d265 100644 --- a/apps/hub-server/modules/chron.ts +++ b/apps/hub-server/modules/chron.ts @@ -1,11 +1,7 @@ import { getMoodleCourseCompletionStatus, getMoodleUserById } from "./moodle"; import { CronJob } from "cron"; -import { DISCORD_ROLES, ParticipantLog, prisma } from "@repo/db"; -import { sendCourseCompletedEmail } from "modules/mail"; +import { prisma } from "@repo/db"; import { handleParticipantFinished } from "modules/event"; -import { eventCompleted } from "helper/events"; -import { addRolesToMember, removeRolesFromMember } from "modules/discord"; -import { JsonValueType } from "@repo/db/zod"; const syncMoodleIds = async () => { try { @@ -61,6 +57,8 @@ const updateParticipantMoodleResults = async () => { ); if (quizzResult?.completionstatus?.completed === true) { + await handleParticipantFinished(p.Event, p, p.User); + return prisma.participant.update({ where: { id: p.id, @@ -81,127 +79,11 @@ const updateParticipantMoodleResults = async () => { ); }; -export const checkFinishedParticipants = async () => { - const participantsPending = await prisma.participant.findMany({ - where: { - completetionWorkflowFinished: false, - }, - include: { - Event: true, - User: true, - }, - }); - participantsPending.forEach(async (p) => { - if (!p.User) return; - const completed = eventCompleted(p.Event, p); - - if (!completed) return; - console.log( - `User ${p.User.firstname} ${p.User.lastname} - ${p.User.publicId} finished event ${p.Event.name}`, - ); - handleParticipantFinished(p.Event, p, p.User); - }); -}; - -const checkUnfinishedParticipants = async () => { - const participantsPending = await prisma.participant.findMany({ - where: { - completetionWorkflowFinished: false, - }, - include: { - Event: true, - User: { - include: { - discordAccounts: true, - }, - }, - }, - }); - participantsPending.forEach(async (p) => { - if (!p.User) return; - const completed = eventCompleted(p.Event, p); - - if (completed) return; - - if (!p.Event.discordRoleId) { - await prisma.participant.update({ - where: { - id: p.id, - }, - data: { - inscriptionWorkflowCompleted: true, - }, - }); - return; - } - console.log( - `User ${p.User.firstname} ${p.User.lastname} - ${p.User.publicId} did not finish event ${p.Event.name}`, - ); - if (p.User.discordAccounts[0] && p.Event.discordRoleId) { - await addRolesToMember(p.User.discordAccounts[0].discordId, [p.Event.discordRoleId]); - await prisma.participant.update({ - where: { - id: p.id, - }, - data: { - inscriptionWorkflowCompleted: true, - statusLog: { - push: { - event: "Discord-Rolle hinzugefügt", - timestamp: new Date(), - user: "system", - } as ParticipantLog as any, - }, - }, - }); - } - }); -}; - -const checkDiscordRoles = async () => { - const user = await prisma.user.findMany({ - where: { - discordAccounts: { - some: {}, - }, - }, - include: { - discordAccounts: true, - }, - }); - - for (const u of user) { - // Here ony member Roles regarding their rights are checked - if (!u.discordAccounts[0]) continue; - const discordAccount = u.discordAccounts[0]; - - // For Pilot - if (u.permissions.includes("PILOT")) { - await addRolesToMember(discordAccount.discordId, [DISCORD_ROLES.PILOT]); // ONLINE_PILOT - } else { - await removeRolesFromMember(discordAccount.discordId, [DISCORD_ROLES.PILOT]); // ONLINE_PILOT - } - // for Dispatcher - if (u.permissions.includes("DISPO")) { - await addRolesToMember(discordAccount.discordId, [DISCORD_ROLES.DISPATCHER]); // ONLINE_DISPATCHER - } else { - await removeRolesFromMember(discordAccount.discordId, [DISCORD_ROLES.DISPATCHER]); // ONLINE_DISPATCHER - } - } -}; - CronJob.from({ cronTime: "0 * * * *", onTick: syncMoodleIds, start: true }); CronJob.from({ cronTime: "*/1 * * * *", onTick: async () => { await updateParticipantMoodleResults(); - await checkFinishedParticipants(); - await checkUnfinishedParticipants(); }, start: true, }); -CronJob.from({ - cronTime: "0 * * * *", - onTick: checkDiscordRoles, - start: true, -}); diff --git a/apps/hub-server/modules/discord.ts b/apps/hub-server/modules/discord.ts index 94317358..248c53ea 100644 --- a/apps/hub-server/modules/discord.ts +++ b/apps/hub-server/modules/discord.ts @@ -36,3 +36,19 @@ export const removeRolesFromMember = async (memberId: string, roleIds: string[]) console.error("Error removing roles from member:", error); }); }; +export const setStandardName = async ({ + memberId, + userId, +}: { + memberId: string; + userId: string; +}) => { + discordAxiosClient + .post("/helper/set-standard-name", { + memberId, + userId, + }) + .catch((error) => { + console.error("Error removing roles from member:", error); + }); +}; diff --git a/apps/hub-server/modules/event.ts b/apps/hub-server/modules/event.ts index fe8d11f2..45107118 100644 --- a/apps/hub-server/modules/event.ts +++ b/apps/hub-server/modules/event.ts @@ -1,5 +1,5 @@ import { Event, Participant, prisma, User } from "@repo/db"; -import { removeRolesFromMember } from "modules/discord"; +import { addRolesToMember, removeRolesFromMember, setStandardName } from "modules/discord"; import { sendCourseCompletedEmail } from "modules/mail"; export const handleParticipantFinished = async ( @@ -35,7 +35,10 @@ export const handleParticipantFinished = async ( }); if (event.discordRoleId && discordAccount) { - await removeRolesFromMember(discordAccount.discordId, [event.discordRoleId]); + await setStandardName({ + memberId: discordAccount.discordId, + userId: user.id, + }); } await sendCourseCompletedEmail(user.email, user, event); @@ -55,3 +58,18 @@ export const handleParticipantFinished = async ( }, }); }; + +export const handleParticipantEnrolled = async ( + event: Event, + participant: Participant, + user: User, +) => { + const discordAccount = await prisma.discordAccount.findFirst({ + where: { + userId: user.id, + }, + }); + if (event.discordRoleId && discordAccount) { + await addRolesToMember(discordAccount.discordId, [event.discordRoleId]); + } +}; diff --git a/apps/hub-server/routes/event.ts b/apps/hub-server/routes/event.ts new file mode 100644 index 00000000..28d9c522 --- /dev/null +++ b/apps/hub-server/routes/event.ts @@ -0,0 +1,72 @@ +import { prisma } from "@repo/db"; +import { Router } from "express"; +import { eventCompleted } from "helper/events"; +import { handleParticipantEnrolled, handleParticipantFinished } from "modules/event"; + +const router: Router = Router(); + +router.post("/handle-participant-finished", async (req, res) => { + const { participantId } = req.body; + if (!participantId) { + res.status(400).json({ error: "Participant ID is required" }); + return; + } + const participant = await prisma.participant.findUnique({ + where: { + id: typeof participantId == "string" ? parseInt(participantId) : participantId, + }, + include: { + Event: true, + User: { + include: { + discordAccounts: true, + }, + }, + }, + }); + if (!participant) { + res.status(404).json({ error: "Participant not found" }); + return; + } + const completed = eventCompleted(participant.Event, participant); + if (!completed) { + res.status(400).json({ error: "Event not completed" }); + return; + } + await handleParticipantFinished(participant.Event, participant, participant.User); + res.status(200).json({ message: "Participant finished successfully" }); +}); + +router.post("/handle-participant-enrolled", async (req, res) => { + const { participantId } = req.body; + if (!participantId) { + res.status(400).json({ error: "Participant ID is required" }); + return; + } + const participant = await prisma.participant.findUnique({ + where: { + id: typeof participantId == "string" ? parseInt(participantId) : participantId, + }, + include: { + Event: true, + User: { + include: { + discordAccounts: true, + }, + }, + }, + }); + if (!participant) { + res.status(404).json({ error: "Participant not found" }); + return; + } + const completed = eventCompleted(participant.Event, participant); + if (completed) { + res.status(400).json({ error: "Event already completed" }); + return; + } + await handleParticipantEnrolled(participant.Event, participant, participant.User); + res.status(200).json({ message: "Participant enrolled successfully" }); +}); + +export default router; diff --git a/apps/hub-server/routes/router.ts b/apps/hub-server/routes/router.ts index 49dd5d77..c98b35f7 100644 --- a/apps/hub-server/routes/router.ts +++ b/apps/hub-server/routes/router.ts @@ -1,8 +1,10 @@ import { Router } from "express"; import mailRouter from "./mail"; +import eventRouter from "./event"; const router: Router = Router(); router.use("/mail", mailRouter); +router.use("/event", eventRouter); export default router; diff --git a/apps/hub/app/(app)/_components/Events.tsx b/apps/hub/app/(app)/_components/FeaturedEvents.tsx similarity index 67% rename from apps/hub/app/(app)/_components/Events.tsx rename to apps/hub/app/(app)/_components/FeaturedEvents.tsx index 0b9f0b2b..f7233595 100644 --- a/apps/hub/app/(app)/_components/Events.tsx +++ b/apps/hub/app/(app)/_components/FeaturedEvents.tsx @@ -14,53 +14,32 @@ const page = async () => { if (!user) return null; const events = await prisma.event.findMany({ + where: { + type: "OBLIGATED_COURSE", + }, include: { - appointments: { - where: { - appointmentDate: { - gte: new Date(), - }, - }, - }, participants: { where: { userId: user.id, }, }, - }, - }); - const userAppointments = await prisma.eventAppointment.findMany({ - where: { - Participants: { - some: { - userId: user.id, - }, - }, - }, - }); - - const appointments = await prisma.eventAppointment.findMany({ - where: { - appointmentDate: { - gte: new Date(), - }, - }, - include: { - Participants: { - where: { - userId: user.id, - }, - }, - _count: { - select: { - Participants: true, + appointments: { + include: { + Participants: { + where: { + appointmentCancelled: false, + }, + }, }, }, }, }); const filteredEvents = events.filter((event) => { - if (eventCompleted(event, event.participants[0])) return false; + const userParticipant = event.participants.find( + (participant) => participant.userId === user.id, + ); + if (eventCompleted(event, userParticipant)) return false; if (event.type === "OBLIGATED_COURSE" && !eventCompleted(event, event.participants[0])) return true; return false; @@ -78,8 +57,10 @@ const page = async () => { {filteredEvents.map((event) => { return ( + a.Participants.find((p) => p.userId == user.id), + )} user={user} event={event} key={event.id} diff --git a/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx b/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx index 869ce82e..78f00643 100644 --- a/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx +++ b/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx @@ -9,6 +9,7 @@ import { Button } from "../../../../_components/ui/Button"; import { DateInput } from "../../../../_components/ui/DateInput"; import { upsertParticipant } from "../../../events/actions"; import { deleteAppoinement, upsertAppointment } from "../action"; +import { handleParticipantFinished } from "../../../../../helper/events"; interface AppointmentModalProps { event?: Event; @@ -127,6 +128,9 @@ export const AppointmentModal = ({ attended: true, appointmentCancelled: false, }); + if (!event.finisherMoodleCourseId) { + await handleParticipantFinished(row.original.id.toString()); + } participantTableRef.current?.refresh(); }} className="btn btn-outline btn-info btn-sm" diff --git a/apps/hub/app/(app)/admin/event/_components/ParticipantModal.tsx b/apps/hub/app/(app)/admin/event/_components/ParticipantModal.tsx index 3217ed2a..b8f5ae7f 100644 --- a/apps/hub/app/(app)/admin/event/_components/ParticipantModal.tsx +++ b/apps/hub/app/(app)/admin/event/_components/ParticipantModal.tsx @@ -5,6 +5,9 @@ import { UseFormReturn } from "react-hook-form"; import { upsertParticipant } from "../../../events/actions"; import { RefObject } from "react"; import { deleteParticipant } from "../action"; +import { handleParticipantFinished } from "../../../../../helper/events"; +import { AxiosError } from "axios"; +import toast from "react-hot-toast"; interface ParticipantModalProps { participantForm: UseFormReturn; @@ -30,6 +33,27 @@ export const ParticipantModal = ({ participantForm, ref }: ParticipantModalProps })} className="space-y-1" > + { }; interface AdminFormProps { + discordAccount?: DiscordAccount; user: User; dispoTime: { hours: number; @@ -380,7 +384,13 @@ interface AdminFormProps { }; } -export const AdminForm = ({ user, dispoTime, pilotTime, reports }: AdminFormProps) => { +export const AdminForm = ({ + user, + dispoTime, + pilotTime, + reports, + discordAccount, +}: AdminFormProps) => { const router = useRouter(); return ( @@ -444,6 +454,27 @@ export const AdminForm = ({ user, dispoTime, pilotTime, reports }: AdminFormProp User Entperren )} + {discordAccount && ( +
+ +
+ )}

diff --git a/apps/hub/app/(app)/admin/user/[id]/page.tsx b/apps/hub/app/(app)/admin/user/[id]/page.tsx index 2009e314..88007af3 100644 --- a/apps/hub/app/(app)/admin/user/[id]/page.tsx +++ b/apps/hub/app/(app)/admin/user/[id]/page.tsx @@ -1,15 +1,18 @@ import { PersonIcon } from "@radix-ui/react-icons"; -import { prisma, User } from "@repo/db"; +import { prisma } from "@repo/db"; import { AdminForm, ConnectionHistory, ProfileForm, UserReports } from "./_components/forms"; import { Error } from "../../../../_components/Error"; export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; - const user: User | null = await prisma.user.findUnique({ + const user = await prisma.user.findUnique({ where: { id: id, }, + include: { + discordAccounts: true, + }, }); const dispoSessions = await prisma.connectedDispatcher.findMany({ @@ -88,7 +91,6 @@ export default async function Page({ params }: { params: Promise<{ id: string }> open: totalReportsOpen, total60Days: totalReports60Days, }; - if (!user) return ; return (
@@ -102,7 +104,13 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
- +
diff --git a/apps/hub/app/(app)/events/_components/item.tsx b/apps/hub/app/(app)/events/_components/item.tsx index 71d754de..8592d421 100644 --- a/apps/hub/app/(app)/events/_components/item.tsx +++ b/apps/hub/app/(app)/events/_components/item.tsx @@ -1,5 +1,5 @@ "use client"; -import { DrawingPinFilledIcon, EnterIcon } from "@radix-ui/react-icons"; +import { DrawingPinFilledIcon } from "@radix-ui/react-icons"; import { Event, Participant, EventAppointment, User } from "@repo/db"; import ModalBtn from "./modalBtn"; import MDEditor from "@uiw/react-md-editor"; @@ -26,14 +26,10 @@ export const KursItem = ({

{event.name}

{event.type === "COURSE" && ( - - Zusatzqualifikation - + Zusatzqualifikation )} {event.type === "OBLIGATED_COURSE" && ( - - Verpflichtend - + Verpflichtend )}
@@ -65,10 +61,7 @@ export const KursItem = ({ Abzeichen:
{event.requiredBadges.map((badge) => ( -
+
{badge}
))} diff --git a/apps/hub/app/(app)/events/_components/modalBtn.tsx b/apps/hub/app/(app)/events/_components/modalBtn.tsx index 230b81fa..a5748141 100644 --- a/apps/hub/app/(app)/events/_components/modalBtn.tsx +++ b/apps/hub/app/(app)/events/_components/modalBtn.tsx @@ -11,7 +11,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Select } from "../../../_components/ui/Select"; import toast from "react-hot-toast"; import { useRouter } from "next/navigation"; -import { eventCompleted } from "../../../../helper/events"; +import { eventCompleted, handleParticipantEnrolled } from "../../../../helper/events"; interface ModalBtnProps { title: string; @@ -123,13 +123,12 @@ const ModalBtn = ({
{!!dates.length && ( - // TODO: Prevent users from selecting an appointment that is full