removed event-chronjobs, used Events in hub-app insteand, added admin Btn to set Discord-User and run Event-completed-workflow. Fixed Bug of wrong participants-count in Event-Modal

This commit is contained in:
PxlLoewe
2025-06-05 23:02:34 -07:00
parent 91d811e289
commit 587884dfd9
21 changed files with 341 additions and 232 deletions

View File

@@ -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;

View File

@@ -8,7 +8,7 @@ if (!GUILD_ID) {
const router: Router = Router(); const router: Router = Router();
const getMember = async (memberId: string) => { export const getMember = async (memberId: string) => {
const guild = client.guilds.cache.get(GUILD_ID); const guild = client.guilds.cache.get(GUILD_ID);
if (!guild) throw new Error("Guild not found"); if (!guild) throw new Error("Guild not found");
try { 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 handleRoleChange = (action: "add" | "remove") => async (req: Request, res: Response) => {
const { roleIds, memberId } = req.body; const { roleIds, memberId } = req.body;
if (!Array.isArray(roleIds) || !memberId) { if (!Array.isArray(roleIds) || !memberId) {
@@ -43,26 +64,8 @@ const handleRoleChange = (action: "add" | "remove") => async (req: Request, res:
return; return;
} }
try { try {
const member = await getMember(memberId); const result = await changeMemberRoles(memberId, roleIds, action);
res.status(200).json(result);
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` });
} catch (error) { } catch (error) {
console.error(`Error ${action}ing roles:`, error); console.error(`Error ${action}ing roles:`, error);
res.status(500).json({ error: `Failed to ${action} roles` }); res.status(500).json({ error: `Failed to ${action} roles` });

View File

@@ -1,8 +1,10 @@
import { Router } from "express"; import { Router } from "express";
import memberRouter from "./member"; import memberRouter from "./member";
import helperRouter from "./helper";
const router: Router = Router(); const router: Router = Router();
router.use("/member", memberRouter); router.use("/member", memberRouter);
router.use("/helper", helperRouter);
export default router; export default router;

View File

@@ -1,11 +1,7 @@
import { getMoodleCourseCompletionStatus, getMoodleUserById } from "./moodle"; import { getMoodleCourseCompletionStatus, getMoodleUserById } from "./moodle";
import { CronJob } from "cron"; import { CronJob } from "cron";
import { DISCORD_ROLES, ParticipantLog, prisma } from "@repo/db"; import { prisma } from "@repo/db";
import { sendCourseCompletedEmail } from "modules/mail";
import { handleParticipantFinished } from "modules/event"; 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 () => { const syncMoodleIds = async () => {
try { try {
@@ -61,6 +57,8 @@ const updateParticipantMoodleResults = async () => {
); );
if (quizzResult?.completionstatus?.completed === true) { if (quizzResult?.completionstatus?.completed === true) {
await handleParticipantFinished(p.Event, p, p.User);
return prisma.participant.update({ return prisma.participant.update({
where: { where: {
id: p.id, 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: "0 * * * *", onTick: syncMoodleIds, start: true });
CronJob.from({ CronJob.from({
cronTime: "*/1 * * * *", cronTime: "*/1 * * * *",
onTick: async () => { onTick: async () => {
await updateParticipantMoodleResults(); await updateParticipantMoodleResults();
await checkFinishedParticipants();
await checkUnfinishedParticipants();
}, },
start: true, start: true,
}); });
CronJob.from({
cronTime: "0 * * * *",
onTick: checkDiscordRoles,
start: true,
});

View File

@@ -36,3 +36,19 @@ export const removeRolesFromMember = async (memberId: string, roleIds: string[])
console.error("Error removing roles from member:", error); 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);
});
};

View File

@@ -1,5 +1,5 @@
import { Event, Participant, prisma, User } from "@repo/db"; 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"; import { sendCourseCompletedEmail } from "modules/mail";
export const handleParticipantFinished = async ( export const handleParticipantFinished = async (
@@ -35,7 +35,10 @@ export const handleParticipantFinished = async (
}); });
if (event.discordRoleId && discordAccount) { if (event.discordRoleId && discordAccount) {
await removeRolesFromMember(discordAccount.discordId, [event.discordRoleId]); await setStandardName({
memberId: discordAccount.discordId,
userId: user.id,
});
} }
await sendCourseCompletedEmail(user.email, user, event); 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]);
}
};

View File

@@ -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;

View File

@@ -1,8 +1,10 @@
import { Router } from "express"; import { Router } from "express";
import mailRouter from "./mail"; import mailRouter from "./mail";
import eventRouter from "./event";
const router: Router = Router(); const router: Router = Router();
router.use("/mail", mailRouter); router.use("/mail", mailRouter);
router.use("/event", eventRouter);
export default router; export default router;

View File

@@ -14,53 +14,32 @@ const page = async () => {
if (!user) return null; if (!user) return null;
const events = await prisma.event.findMany({ const events = await prisma.event.findMany({
where: {
type: "OBLIGATED_COURSE",
},
include: { include: {
appointments: {
where: {
appointmentDate: {
gte: new Date(),
},
},
},
participants: { participants: {
where: { where: {
userId: user.id, userId: user.id,
}, },
}, },
}, appointments: {
}); include: {
const userAppointments = await prisma.eventAppointment.findMany({ Participants: {
where: { where: {
Participants: { appointmentCancelled: false,
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,
}, },
}, },
}, },
}); });
const filteredEvents = events.filter((event) => { 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])) if (event.type === "OBLIGATED_COURSE" && !eventCompleted(event, event.participants[0]))
return true; return true;
return false; return false;
@@ -78,8 +57,10 @@ const page = async () => {
{filteredEvents.map((event) => { {filteredEvents.map((event) => {
return ( return (
<KursItem <KursItem
appointments={appointments} appointments={event.appointments}
selectedAppointments={userAppointments} selectedAppointments={event.appointments.filter((a) =>
a.Participants.find((p) => p.userId == user.id),
)}
user={user} user={user}
event={event} event={event}
key={event.id} key={event.id}

View File

@@ -9,6 +9,7 @@ import { Button } from "../../../../_components/ui/Button";
import { DateInput } from "../../../../_components/ui/DateInput"; import { DateInput } from "../../../../_components/ui/DateInput";
import { upsertParticipant } from "../../../events/actions"; import { upsertParticipant } from "../../../events/actions";
import { deleteAppoinement, upsertAppointment } from "../action"; import { deleteAppoinement, upsertAppointment } from "../action";
import { handleParticipantFinished } from "../../../../../helper/events";
interface AppointmentModalProps { interface AppointmentModalProps {
event?: Event; event?: Event;
@@ -127,6 +128,9 @@ export const AppointmentModal = ({
attended: true, attended: true,
appointmentCancelled: false, appointmentCancelled: false,
}); });
if (!event.finisherMoodleCourseId) {
await handleParticipantFinished(row.original.id.toString());
}
participantTableRef.current?.refresh(); participantTableRef.current?.refresh();
}} }}
className="btn btn-outline btn-info btn-sm" className="btn btn-outline btn-info btn-sm"

View File

@@ -5,6 +5,9 @@ import { UseFormReturn } from "react-hook-form";
import { upsertParticipant } from "../../../events/actions"; import { upsertParticipant } from "../../../events/actions";
import { RefObject } from "react"; import { RefObject } from "react";
import { deleteParticipant } from "../action"; import { deleteParticipant } from "../action";
import { handleParticipantFinished } from "../../../../../helper/events";
import { AxiosError } from "axios";
import toast from "react-hot-toast";
interface ParticipantModalProps { interface ParticipantModalProps {
participantForm: UseFormReturn<Participant>; participantForm: UseFormReturn<Participant>;
@@ -30,6 +33,27 @@ export const ParticipantModal = ({ participantForm, ref }: ParticipantModalProps
})} })}
className="space-y-1" className="space-y-1"
> >
<Button
onClick={async () => {
if (!participantForm.watch("id")) return;
const participant = participantForm.getValues();
await handleParticipantFinished(participant.id.toString()).catch((e) => {
const error = e as AxiosError;
if (
error.response?.data &&
typeof error.response.data === "object" &&
"error" in error.response.data
) {
toast.error(`Fehler: ${error.response.data.error}`);
} else {
toast.error("Unbekannter Fehler beim ausführen des Workflows");
}
});
}}
>
Event-abgeschlossen Workflow ausführen
</Button>
<Switch form={participantForm} name="attended" label="Termin Teilgenommen" /> <Switch form={participantForm} name="attended" label="Termin Teilgenommen" />
<Switch form={participantForm} name="appointmentCancelled" label="Termin abgesagt" /> <Switch form={participantForm} name="appointmentCancelled" label="Termin abgesagt" />
<Switch <Switch

View File

@@ -4,6 +4,7 @@ import {
BADGES, BADGES,
ConnectedAircraft, ConnectedAircraft,
ConnectedDispatcher, ConnectedDispatcher,
DiscordAccount,
PERMISSION, PERMISSION,
Report, Report,
Station, Station,
@@ -22,6 +23,7 @@ import {
LockOpen1Icon, LockOpen1Icon,
HobbyKnifeIcon, HobbyKnifeIcon,
ExclamationTriangleIcon, ExclamationTriangleIcon,
DiscordLogoIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { Button } from "../../../../../_components/ui/Button"; import { Button } from "../../../../../_components/ui/Button";
import { Select } from "../../../../../_components/ui/Select"; import { Select } from "../../../../../_components/ui/Select";
@@ -34,6 +36,7 @@ import Link from "next/link";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { Error } from "_components/Error"; import { Error } from "_components/Error";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { setStandardName } from "../../../../../../helper/discord";
interface ProfileFormProps { interface ProfileFormProps {
user: User; user: User;
@@ -362,6 +365,7 @@ export const UserReports = ({ user }: { user: User }) => {
}; };
interface AdminFormProps { interface AdminFormProps {
discordAccount?: DiscordAccount;
user: User; user: User;
dispoTime: { dispoTime: {
hours: number; 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(); const router = useRouter();
return ( return (
@@ -444,6 +454,27 @@ export const AdminForm = ({ user, dispoTime, pilotTime, reports }: AdminFormProp
<HobbyKnifeIcon /> User Entperren <HobbyKnifeIcon /> User Entperren
</Button> </Button>
)} )}
{discordAccount && (
<div className="tooltip flex-1" data-tip={`Name: ${discordAccount.username}`}>
<Button
onClick={async () => {
await setStandardName({
memberId: discordAccount.discordId,
userId: user.id,
});
toast.success("Standard Name wurde gesetzt!", {
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
});
}}
className="btn-sm w-full btn-outline btn-info"
>
<DiscordLogoIcon /> Name und Berechtigungen setzen
</Button>
</div>
)}
</div> </div>
</div> </div>
<h2 className="card-title"> <h2 className="card-title">

View File

@@ -1,15 +1,18 @@
import { PersonIcon } from "@radix-ui/react-icons"; 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 { AdminForm, ConnectionHistory, ProfileForm, UserReports } from "./_components/forms";
import { Error } from "../../../../_components/Error"; import { Error } from "../../../../_components/Error";
export default async function Page({ params }: { params: Promise<{ id: string }> }) { export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params; const { id } = await params;
const user: User | null = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
id: id, id: id,
}, },
include: {
discordAccounts: true,
},
}); });
const dispoSessions = await prisma.connectedDispatcher.findMany({ const dispoSessions = await prisma.connectedDispatcher.findMany({
@@ -88,7 +91,6 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
open: totalReportsOpen, open: totalReportsOpen,
total60Days: totalReports60Days, total60Days: totalReports60Days,
}; };
if (!user) return <Error statusCode={404} title="User not found" />; if (!user) return <Error statusCode={404} title="User not found" />;
return ( return (
<div className="grid grid-cols-6 gap-4"> <div className="grid grid-cols-6 gap-4">
@@ -102,7 +104,13 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
<ProfileForm user={user} /> <ProfileForm user={user} />
</div> </div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3"> <div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<AdminForm user={user} dispoTime={dispoTime} pilotTime={pilotTime} reports={reports} /> <AdminForm
user={user}
dispoTime={dispoTime}
pilotTime={pilotTime}
reports={reports}
discordAccount={user.discordAccounts[0]}
/>
</div> </div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-6"> <div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-6">
<UserReports user={user} /> <UserReports user={user} />

View File

@@ -1,5 +1,5 @@
"use client"; "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 { 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";
@@ -26,14 +26,10 @@ export const KursItem = ({
<h2 className="card-title">{event.name}</h2> <h2 className="card-title">{event.name}</h2>
<div className="absolute top-0 right-0 m-4"> <div className="absolute top-0 right-0 m-4">
{event.type === "COURSE" && ( {event.type === "COURSE" && (
<span className="badge badge-info badge-outline"> <span className="badge badge-info badge-outline">Zusatzqualifikation</span>
Zusatzqualifikation
</span>
)} )}
{event.type === "OBLIGATED_COURSE" && ( {event.type === "OBLIGATED_COURSE" && (
<span className="badge badge-secondary badge-outline"> <span className="badge badge-secondary badge-outline">Verpflichtend</span>
Verpflichtend
</span>
)} )}
</div> </div>
<div className="grid grid-cols-6 gap-4"> <div className="grid grid-cols-6 gap-4">
@@ -65,10 +61,7 @@ export const KursItem = ({
<b className="text-gray-600 text-left mr-2">Abzeichen:</b> <b className="text-gray-600 text-left mr-2">Abzeichen:</b>
<div className="flex gap-2"> <div className="flex gap-2">
{event.requiredBadges.map((badge) => ( {event.requiredBadges.map((badge) => (
<div <div className="badge badge-secondary badge-outline" key={badge}>
className="badge badge-secondary badge-outline"
key={badge}
>
{badge} {badge}
</div> </div>
))} ))}

View File

@@ -11,7 +11,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { Select } from "../../../_components/ui/Select"; import { Select } from "../../../_components/ui/Select";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { eventCompleted } from "../../../../helper/events"; import { eventCompleted, handleParticipantEnrolled } from "../../../../helper/events";
interface ModalBtnProps { interface ModalBtnProps {
title: string; title: string;
@@ -123,13 +123,12 @@ const ModalBtn = ({
<div className="flex items-center gap-2 justify-center"> <div className="flex items-center gap-2 justify-center">
<CalendarIcon /> <CalendarIcon />
{!!dates.length && ( {!!dates.length && (
// TODO: Prevent users from selecting an appointment that is full
<Select <Select
form={selectAppointmentForm as any} form={selectAppointmentForm}
options={dates.map((date) => ({ options={dates.map((date) => ({
label: `${new Date( label: `${new Date(
date.appointmentDate, date.appointmentDate,
).toLocaleString()} - (${(date as any)._count.Participants}/${event.maxParticipants})`, ).toLocaleString()} - (${(date as any).Participants.length}/${event.maxParticipants})`,
value: date.id, value: date.id,
}))} }))}
name="eventAppointmentId" name="eventAppointmentId"
@@ -226,12 +225,14 @@ const ModalBtn = ({
const data = selectAppointmentForm.getValues(); const data = selectAppointmentForm.getValues();
if (!data.eventAppointmentId) return; if (!data.eventAppointmentId) return;
await upsertParticipant({ const participant = await upsertParticipant({
...data, ...data,
enscriptionDate: new Date(), enscriptionDate: new Date(),
statusLog: data.statusLog?.filter((log) => log !== null), statusLog: data.statusLog?.filter((log) => log !== null),
appointmentCancelled: false, appointmentCancelled: false,
}); });
await handleParticipantEnrolled(participant.id.toString());
router.refresh(); router.refresh();
closeModal(); closeModal();
}} }}

View File

@@ -33,7 +33,11 @@ export default async function RootLayout({
> >
<div className="hero-overlay bg-opacity-30"></div> <div className="hero-overlay bg-opacity-30"></div>
<div> <div>
<Toaster position="top-center" reverseOrder={false} /> <Toaster
containerStyle={{ zIndex: "999999999" }}
position="top-center"
reverseOrder={false}
/>
</div> </div>
{/* Card */} {/* Card */}
<div className="hero-content text-neutral-content text-center w-full max-w-full h-full m-10"> <div className="hero-content text-neutral-content text-center w-full max-w-full h-full m-10">

View File

@@ -1,4 +1,4 @@
import Events from "./_components/Events"; import Events from "./_components/FeaturedEvents";
import { Stats } from "./_components/Stats"; import { Stats } from "./_components/Stats";
import { Badges } from "./_components/Badges"; import { Badges } from "./_components/Badges";
import { RecentFlights } from "(app)/_components/RecentFlights"; import { RecentFlights } from "(app)/_components/RecentFlights";

View File

@@ -1,18 +1,10 @@
import { import { ButtonHTMLAttributes, DetailedHTMLProps, useEffect, useState } from "react";
ButtonHTMLAttributes,
DetailedHTMLProps,
useEffect,
useState,
} from "react";
import { cn } from "../../../helper/cn"; import { cn } from "../../../helper/cn";
export const Button = ({ export const Button = ({
isLoading, isLoading,
...props ...props
}: DetailedHTMLProps< }: DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> & {
isLoading?: boolean; isLoading?: boolean;
}) => { }) => {
const [isLoadingState, setIsLoadingState] = useState(isLoading); const [isLoadingState, setIsLoadingState] = useState(isLoading);
@@ -34,9 +26,7 @@ export const Button = ({
} }
}} }}
> >
{isLoadingState && ( {isLoadingState && <span className="loading loading-spinner loading-sm"></span>}
<span className="loading loading-spinner loading-sm"></span>
)}
{props.children as any} {props.children as any}
</button> </button>
); );

View File

@@ -1,15 +1,15 @@
import axios, { AxiosError } from "axios"; import axios from "axios";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { DISCORD_ROLES, DiscordAccount, getPublicUser, prisma, PrismaClient } from "@repo/db"; import { DiscordAccount, prisma } from "@repo/db";
import { getServerSession } from "../auth/[...nextauth]/auth"; import { getServerSession } from "../auth/[...nextauth]/auth";
import { addRolesToMember, removeRolesFromMember, renameMember } from "../../../helper/discord"; import { setStandardName } from "../../../helper/discord";
export const GET = async (req: NextRequest) => { export const GET = async (req: NextRequest) => {
const session = await getServerSession(); const session = await getServerSession();
const code = req.nextUrl.searchParams.get("code"); const code = req.nextUrl.searchParams.get("code");
if (!session) { if (!session) {
return NextResponse.redirect(`${process.env.NEXTAUTH_URL}/login`); return NextResponse.redirect(`${process.env.NEXT_PUBLIC_HUB_URL}/login`);
} }
if ( if (
@@ -72,17 +72,10 @@ export const GET = async (req: NextRequest) => {
where: { id: session.user.id }, where: { id: session.user.id },
}); });
if (user) { if (user) {
await renameMember(discordUser.id, `${getPublicUser(user).fullName} - ${user?.publicId}`); await setStandardName({
} memberId: discordUser.id,
if (user?.permissions.includes("PILOT")) { userId: user.id,
await addRolesToMember(discordUser.id, [DISCORD_ROLES.PILOT]); });
} else {
await removeRolesFromMember(discordUser.id, [DISCORD_ROLES.PILOT]);
}
if (user?.permissions.includes("DISPO")) {
await addRolesToMember(discordUser.id, [DISCORD_ROLES.ONLINE_DISPATCHER]);
} else {
await removeRolesFromMember(discordUser.id, [DISCORD_ROLES.PILOT]);
} }
return NextResponse.redirect(`${process.env.NEXT_PUBLIC_HUB_URL}/settings`); return NextResponse.redirect(`${process.env.NEXT_PUBLIC_HUB_URL}/settings`);

View File

@@ -37,3 +37,20 @@ export const removeRolesFromMember = async (memberId: string, roleIds: string[])
console.error("Error removing roles from member:", error); 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);
});
};

View File

@@ -1,4 +1,5 @@
import { Event, Participant } from "@repo/db"; import { Event, Participant } from "@repo/db";
import axios from "axios";
export const eventCompleted = (event: Event, participant?: Participant) => { export const eventCompleted = (event: Event, participant?: Participant) => {
if (!participant) return false; if (!participant) return false;
@@ -6,3 +7,13 @@ export const eventCompleted = (event: Event, participant?: Participant) => {
if (event.hasPresenceEvents && !participant.attended) return false; if (event.hasPresenceEvents && !participant.attended) return false;
return true; return true;
}; };
export const handleParticipantFinished = async (participantId: string) =>
axios.post(`${process.env.NEXT_PUBLIC_HUB_SERVER_URL}/event/handle-participant-finished`, {
participantId,
});
export const handleParticipantEnrolled = async (participantId: string) =>
axios.post(`${process.env.NEXT_PUBLIC_HUB_SERVER_URL}/event/handle-participant-enrolled`, {
participantId,
});