added Badges to Dashboard, warning for full events
@@ -80,7 +80,6 @@ const updateParticipantMoodleResults = async () => {
|
||||
};
|
||||
|
||||
export const checkFinishedParticipants = async () => {
|
||||
console.log("Checking finished participants");
|
||||
const participantsPending = await prisma.participant.findMany({
|
||||
where: {
|
||||
completetionWorkflowFinished: false,
|
||||
@@ -95,6 +94,9 @@ export const checkFinishedParticipants = async () => {
|
||||
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);
|
||||
});
|
||||
};
|
||||
@@ -103,7 +105,6 @@ CronJob.from({ cronTime: "0 * * * *", onTick: syncMoodleIds, start: true });
|
||||
CronJob.from({
|
||||
cronTime: "*/1 * * * *",
|
||||
onTick: async () => {
|
||||
console.log("Updating participant moodle results");
|
||||
await updateParticipantMoodleResults();
|
||||
await checkFinishedParticipants();
|
||||
},
|
||||
|
||||
@@ -12,16 +12,23 @@ export const handleParticipantFinished = async (
|
||||
},
|
||||
});
|
||||
|
||||
const badgedToAdd = event.finishedBadges.filter((badge) => {
|
||||
return !user.badges.includes(badge);
|
||||
});
|
||||
const permissionsToAdd = event.finishedPermissions.filter((permission) => {
|
||||
return !user.permissions.includes(permission);
|
||||
});
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
badges: {
|
||||
push: event.finishedBadges,
|
||||
push: badgedToAdd,
|
||||
},
|
||||
permissions: {
|
||||
push: event.finishedPermissions,
|
||||
push: permissionsToAdd,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Badge } from "@repo/ui";
|
||||
|
||||
import { Award } from "lucide-react";
|
||||
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
|
||||
import { Badge } from "../../_components/Badge/Badge";
|
||||
|
||||
export const Badges = async () => {
|
||||
const session = await getServerSession();
|
||||
@@ -15,14 +14,12 @@ export const Badges = async () => {
|
||||
<Award className="w-4 h-4" /> Verdiente Abzeichen
|
||||
</span>
|
||||
</h2>
|
||||
{session.user.badges.map((badge) => {
|
||||
return (
|
||||
<div className="badge badge-primary badge-outline">
|
||||
<Badge name={badge} />
|
||||
</div>
|
||||
);
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{session.user.badges.map((badge, i) => {
|
||||
return <Badge name={badge} key={`${badge} - ${i}`} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -41,6 +41,26 @@ export default async () => {
|
||||
},
|
||||
});
|
||||
|
||||
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) => {
|
||||
console.log;
|
||||
if (eventCompleted(event, event.participants[0])) return false;
|
||||
@@ -64,6 +84,7 @@ export default async () => {
|
||||
{filteredEvents.map((event) => {
|
||||
return (
|
||||
<KursItem
|
||||
appointments={appointments}
|
||||
selectedAppointments={userAppointments}
|
||||
user={user}
|
||||
event={event}
|
||||
|
||||
@@ -69,7 +69,13 @@ export const AppointmentModal = ({
|
||||
})}
|
||||
className="flex flex-col"
|
||||
>
|
||||
<DateInput control={appointmentForm.control} name="appointmentDate" />
|
||||
<DateInput
|
||||
control={appointmentForm.control}
|
||||
name="appointmentDate"
|
||||
showTimeInput
|
||||
timeCaption="Uhrzeit"
|
||||
showTimeCaption
|
||||
/>
|
||||
{/* <Input
|
||||
form={appointmentForm}
|
||||
type="datetime-local"
|
||||
|
||||
@@ -81,7 +81,7 @@ export const Form = ({ event }: { event?: Event }) => {
|
||||
onSubmit={form.handleSubmit(async (values) => {
|
||||
setLoading(true);
|
||||
|
||||
const createdEvent = await upsertEvent(values, event?.id);
|
||||
await upsertEvent(values, event?.id);
|
||||
setLoading(false);
|
||||
if (!event) redirect(`/admin/event`);
|
||||
})}
|
||||
@@ -201,7 +201,7 @@ export const Form = ({ event }: { event?: Event }) => {
|
||||
header: "Datum",
|
||||
accessorKey: "appointmentDate",
|
||||
accessorFn: (date) =>
|
||||
new Date(date.appointmentDate).toLocaleDateString(),
|
||||
new Date(date.appointmentDate).toLocaleString(),
|
||||
},
|
||||
{
|
||||
header: "Presenter",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Participant, Prisma } from "@repo/db";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import { upsertParticipant } from "../../../events/actions";
|
||||
import { RefObject } from "react";
|
||||
import { deleteParticipant } from "../action";
|
||||
|
||||
interface ParticipantModalProps {
|
||||
participantForm: UseFormReturn<Participant>;
|
||||
@@ -59,6 +60,17 @@ export const ParticipantModal = ({
|
||||
))}
|
||||
</div>
|
||||
<Button type="submit">Speichern</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onSubmit={() => false}
|
||||
onClick={() => {
|
||||
deleteParticipant(participantForm.watch("id"));
|
||||
participantForm.reset();
|
||||
ref.current?.close();
|
||||
}}
|
||||
>
|
||||
Löschen
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -3,11 +3,13 @@ import { DrawingPinFilledIcon, EnterIcon } from "@radix-ui/react-icons";
|
||||
import { Event, Participant, EventAppointment, User } from "@repo/db";
|
||||
import ModalBtn from "./modalBtn";
|
||||
import MDEditor from "@uiw/react-md-editor";
|
||||
import { Badge } from "../../../_components/Badge/Badge";
|
||||
|
||||
export const KursItem = ({
|
||||
user,
|
||||
event,
|
||||
selectedAppointments,
|
||||
appointments,
|
||||
}: {
|
||||
user: User;
|
||||
event: Event & {
|
||||
@@ -15,6 +17,7 @@ export const KursItem = ({
|
||||
participants: Participant[];
|
||||
};
|
||||
selectedAppointments: EventAppointment[];
|
||||
appointments: EventAppointment[];
|
||||
}) => {
|
||||
return (
|
||||
<div className="col-span-full">
|
||||
@@ -45,7 +48,11 @@ export const KursItem = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">{event.finishedBadges}</div>
|
||||
<div className="col-span-2">
|
||||
{event.finishedBadges.map((b) => {
|
||||
return <Badge name={b} key={b} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-actions flex justify-between items-center mt-5">
|
||||
<div>
|
||||
@@ -55,10 +62,13 @@ export const KursItem = ({
|
||||
</p>
|
||||
{!!event.requiredBadges.length && (
|
||||
<div className="flex ml-6">
|
||||
<b className="text-gray-600 text-left">Abzeichen:</b>
|
||||
<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">
|
||||
<div
|
||||
className="badge badge-secondary badge-outline"
|
||||
key={badge}
|
||||
>
|
||||
{badge}
|
||||
</div>
|
||||
))}
|
||||
@@ -71,7 +81,7 @@ export const KursItem = ({
|
||||
user={user}
|
||||
event={event}
|
||||
title={event.name}
|
||||
dates={event.appointments}
|
||||
dates={appointments}
|
||||
participant={event.participants[0]}
|
||||
modalId={`${event.name}_modal.${event.id}`}
|
||||
/>
|
||||
|
||||
@@ -8,20 +8,23 @@ import {
|
||||
import { Event, EventAppointment, Participant, prisma, User } from "@repo/db";
|
||||
import { cn } from "../../../../helper/cn";
|
||||
import { inscribeToMoodleCourse, upsertParticipant } from "../actions";
|
||||
import { Check, Clock10Icon, Cross, EyeIcon } from "lucide-react";
|
||||
import {
|
||||
Check,
|
||||
Clock10Icon,
|
||||
Cross,
|
||||
EyeIcon,
|
||||
MessageCircleWarning,
|
||||
TriangleAlert,
|
||||
} from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
EventAppointmentOptionalDefaults,
|
||||
EventAppointmentSchema,
|
||||
ParticipantOptionalDefaults,
|
||||
ParticipantOptionalDefaultsSchema,
|
||||
ParticipantSchema,
|
||||
} from "@repo/db/zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Select } from "../../../_components/ui/Select";
|
||||
import toast from "react-hot-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { JsonArray } from "../../../../../../packages/database/generated/client/runtime/library";
|
||||
import { eventCompleted } from "@repo/ui";
|
||||
|
||||
interface ModalBtnProps {
|
||||
@@ -85,6 +88,11 @@ const ModalBtn = ({
|
||||
},
|
||||
});
|
||||
const selectedAppointment = selectedAppointments[0];
|
||||
const selectedDate = dates.find(
|
||||
(date) =>
|
||||
date.id === selectAppointmentForm.watch("eventAppointmentId") ||
|
||||
selectedAppointment?.id,
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
@@ -124,7 +132,9 @@ const ModalBtn = ({
|
||||
<Select
|
||||
form={selectAppointmentForm as any}
|
||||
options={dates.map((date) => ({
|
||||
label: new Date(date.appointmentDate).toLocaleString(),
|
||||
label: `${new Date(
|
||||
date.appointmentDate,
|
||||
).toLocaleString()} - (${(date as any)._count.Participants}/${event.maxParticipants})`,
|
||||
value: date.id,
|
||||
}))}
|
||||
name="eventAppointmentId"
|
||||
@@ -132,6 +142,7 @@ const ModalBtn = ({
|
||||
className="min-w-[200px]"
|
||||
/>
|
||||
)}
|
||||
{}
|
||||
{!dates.length && (
|
||||
<p className="text-center text-info">
|
||||
Keine Termine verfügbar
|
||||
@@ -145,6 +156,22 @@ const ModalBtn = ({
|
||||
Du hast an dem Presenztermin teilgenommen
|
||||
</p>
|
||||
)}
|
||||
{selectedDate &&
|
||||
event.maxParticipants !== null &&
|
||||
(selectedDate as any)._count.Participants >=
|
||||
event.maxParticipants && (
|
||||
<p
|
||||
role="alert"
|
||||
className="py-4 my-5 flex items-center gap-2 justify-center border alert alert-error alert-outline"
|
||||
>
|
||||
<TriangleAlert
|
||||
className="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
/>
|
||||
Dieser Termin ist ausgebucht, wahrscheinlich wirst du nicht
|
||||
teilnehmen können
|
||||
</p>
|
||||
)}
|
||||
{selectedAppointment && !participant?.appointmentCancelled && (
|
||||
<>
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
@@ -182,6 +209,7 @@ const ModalBtn = ({
|
||||
absagen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-center">
|
||||
Bitte finde dich an diesem Termin in unserem Discord ein.
|
||||
</p>
|
||||
|
||||
@@ -30,6 +30,25 @@ export default async () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const appointments = await prisma.eventAppointment.findMany({
|
||||
where: {
|
||||
appointmentDate: {
|
||||
gte: new Date(),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Participants: {
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
Participants: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const userAppointments = await prisma.eventAppointment.findMany({
|
||||
where: {
|
||||
Participants: {
|
||||
@@ -51,6 +70,7 @@ export default async () => {
|
||||
{events.map((event) => {
|
||||
return (
|
||||
<KursItem
|
||||
appointments={appointments}
|
||||
selectedAppointments={userAppointments}
|
||||
user={user}
|
||||
event={event}
|
||||
|
||||
@@ -6,6 +6,7 @@ import D1 from "./d-1.png";
|
||||
import D2 from "./d-2.png";
|
||||
import D3 from "./d-3.png";
|
||||
import DAY1 from "./day-1-member.png";
|
||||
import { cn } from "../../../helper/cn";
|
||||
|
||||
const BadgeImage = {
|
||||
[BADGES.P1]: P1,
|
||||
@@ -17,12 +18,20 @@ const BadgeImage = {
|
||||
[BADGES.DAY1]: DAY1,
|
||||
};
|
||||
|
||||
export const Badge = ({ name }: { name: BADGES }) => {
|
||||
export const Badge = ({
|
||||
name,
|
||||
className,
|
||||
}: {
|
||||
name: BADGES;
|
||||
className?: string;
|
||||
}) => {
|
||||
const image = BadgeImage[name];
|
||||
|
||||
return (
|
||||
<span className="badge">
|
||||
<img src={image} />
|
||||
<span
|
||||
className={cn("badge badge-secondary badge-outline h-fit p-1", className)}
|
||||
>
|
||||
<img src={image.src} alt={name} width={100} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
@@ -27,7 +27,7 @@ model User {
|
||||
moodleId Int? @map(name: "moodle_id")
|
||||
emailVerified DateTime? @map(name: "email_verified")
|
||||
image String?
|
||||
badges BADGES[] @default([])
|
||||
badges BADGES[] @unique @default([])
|
||||
permissions PERMISSION[] @default([])
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from "./Badge/Badge";
|
||||
export * from "./helper/event";
|
||||
|
||||