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