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

@@ -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 (
<KursItem
appointments={appointments}
selectedAppointments={userAppointments}
appointments={event.appointments}
selectedAppointments={event.appointments.filter((a) =>
a.Participants.find((p) => p.userId == user.id),
)}
user={user}
event={event}
key={event.id}

View File

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

View File

@@ -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<Participant>;
@@ -30,6 +33,27 @@ export const ParticipantModal = ({ participantForm, ref }: ParticipantModalProps
})}
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="appointmentCancelled" label="Termin abgesagt" />
<Switch

View File

@@ -4,6 +4,7 @@ import {
BADGES,
ConnectedAircraft,
ConnectedDispatcher,
DiscordAccount,
PERMISSION,
Report,
Station,
@@ -22,6 +23,7 @@ import {
LockOpen1Icon,
HobbyKnifeIcon,
ExclamationTriangleIcon,
DiscordLogoIcon,
} from "@radix-ui/react-icons";
import { Button } from "../../../../../_components/ui/Button";
import { Select } from "../../../../../_components/ui/Select";
@@ -34,6 +36,7 @@ import Link from "next/link";
import { ColumnDef } from "@tanstack/react-table";
import { Error } from "_components/Error";
import { useSession } from "next-auth/react";
import { setStandardName } from "../../../../../../helper/discord";
interface ProfileFormProps {
user: User;
@@ -362,6 +365,7 @@ export const UserReports = ({ user }: { user: User }) => {
};
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
<HobbyKnifeIcon /> User Entperren
</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>
<h2 className="card-title">

View File

@@ -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 <Error statusCode={404} title="User not found" />;
return (
<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} />
</div>
<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 className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-6">
<UserReports user={user} />

View File

@@ -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 = ({
<h2 className="card-title">{event.name}</h2>
<div className="absolute top-0 right-0 m-4">
{event.type === "COURSE" && (
<span className="badge badge-info badge-outline">
Zusatzqualifikation
</span>
<span className="badge badge-info badge-outline">Zusatzqualifikation</span>
)}
{event.type === "OBLIGATED_COURSE" && (
<span className="badge badge-secondary badge-outline">
Verpflichtend
</span>
<span className="badge badge-secondary badge-outline">Verpflichtend</span>
)}
</div>
<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>
<div className="flex gap-2">
{event.requiredBadges.map((badge) => (
<div
className="badge badge-secondary badge-outline"
key={badge}
>
<div className="badge badge-secondary badge-outline" key={badge}>
{badge}
</div>
))}

View File

@@ -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 = ({
<div className="flex items-center gap-2 justify-center">
<CalendarIcon />
{!!dates.length && (
// TODO: Prevent users from selecting an appointment that is full
<Select
form={selectAppointmentForm as any}
form={selectAppointmentForm}
options={dates.map((date) => ({
label: `${new Date(
date.appointmentDate,
).toLocaleString()} - (${(date as any)._count.Participants}/${event.maxParticipants})`,
).toLocaleString()} - (${(date as any).Participants.length}/${event.maxParticipants})`,
value: date.id,
}))}
name="eventAppointmentId"
@@ -226,12 +225,14 @@ const ModalBtn = ({
const data = selectAppointmentForm.getValues();
if (!data.eventAppointmentId) return;
await upsertParticipant({
const participant = await upsertParticipant({
...data,
enscriptionDate: new Date(),
statusLog: data.statusLog?.filter((log) => log !== null),
appointmentCancelled: false,
});
await handleParticipantEnrolled(participant.id.toString());
router.refresh();
closeModal();
}}

View File

@@ -33,7 +33,11 @@ export default async function RootLayout({
>
<div className="hero-overlay bg-opacity-30"></div>
<div>
<Toaster position="top-center" reverseOrder={false} />
<Toaster
containerStyle={{ zIndex: "999999999" }}
position="top-center"
reverseOrder={false}
/>
</div>
{/* Card */}
<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 { Badges } from "./_components/Badges";
import { RecentFlights } from "(app)/_components/RecentFlights";