diff --git a/apps/hub-server/package.json b/apps/hub-server/package.json index d51f30fa..f3994044 100644 --- a/apps/hub-server/package.json +++ b/apps/hub-server/package.json @@ -9,7 +9,6 @@ }, "devDependencies": { "@repo/db": "*", - "@repo/hub": "*", "@repo/typescript-config": "*", "@types/node": "^22.13.5", "concurrently": "^9.1.2", diff --git a/apps/hub/app/(app)/admin/event/_components/Form.tsx b/apps/hub/app/(app)/admin/event/_components/Form.tsx index 59c97def..f2ed0bd8 100644 --- a/apps/hub/app/(app)/admin/event/_components/Form.tsx +++ b/apps/hub/app/(app)/admin/event/_components/Form.tsx @@ -7,15 +7,30 @@ import { EventOptionalDefaultsSchema, ParticipantOptionalDefaultsSchema, } from "@repo/db/zod"; -import { set, useForm } from "react-hook-form"; -import { BADGES, Event, EVENT_TYPE, PERMISSION } from "@repo/db"; +import { Controller, set, useForm } from "react-hook-form"; +import { + BADGES, + Event, + EVENT_TYPE, + Participant, + PERMISSION, + prisma, + Prisma, +} from "@repo/db"; import { Bot, Calendar, FileText, UserIcon } from "lucide-react"; import { Input } from "../../../../_components/ui/Input"; import { useRef, useState } from "react"; -import { deleteEvent, upsertAppointment, upsertEvent } from "../action"; +import { + deleteAppoinement, + deleteEvent, + upsertAppointment, + upsertEvent, +} from "../action"; import { Button } from "../../../../_components/ui/Button"; import { redirect, useRouter } from "next/navigation"; import { Switch } from "../../../../_components/ui/Switch"; +import DatePicker, { registerLocale } from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; import { PaginatedTable, PaginatedTableRef, @@ -23,6 +38,10 @@ import { import { Select } from "../../../../_components/ui/Select"; import { useSession } from "next-auth/react"; import { MarkdownEditor } from "../../../../_components/ui/MDEditor"; +import { CellContext } from "@tanstack/react-table"; +import { upsertParticipant } from "../../../events/actions"; +import { de } from "date-fns/locale"; +registerLocale("de", de); export const Form = ({ event }: { event?: Event }) => { const { data: session } = useSession(); @@ -38,12 +57,16 @@ export const Form = ({ event }: { event?: Event }) => { }, }); const appointmentsTableRef = useRef(null); + const participantTableRef = useRef(null); const [loading, setLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); - const addParticipantModal = useRef(null); + const appointmentModal = useRef(null); + const participantForm = useForm({ + resolver: zodResolver(ParticipantOptionalDefaultsSchema), + }); return ( <> - +
{/* if there is a button in form, it will close the modal */} @@ -51,32 +74,167 @@ export const Form = ({ event }: { event?: Event }) => { ✕
-

Termin hinzufügen

+

+ Termin {appointmentForm.watch("id")} +

{ if (!event) return; - const createdAppointment = await upsertAppointment({ - appointmentDate: values.appointmentDate, - eventId: event?.id, - presenterId: values.presenterId, - }); - addParticipantModal.current?.close(); + const createdAppointment = await upsertAppointment(values); + appointmentModal.current?.close(); appointmentsTableRef.current?.refresh(); })} className="flex flex-col" > - ( + field.onChange(date)} + selected={field.value} + /> + )} + /> + {/* -
- + formOptions={{ + valueAsDate: true, + }} + /> */} +
+ {appointmentForm.watch("id") && ( + ) => { + return ( + <> + + {!row.original.attended && + event?.hasPresenceEvents && ( + + )} + + ); + }, + }, + ]} + prismaModel={"participant"} + filter={{ + eventAppointmentId: appointmentForm.watch("id"), + }} + include={{ User: true }} + leftOfPagination={ +
+ + {appointmentForm.watch("id") && ( + + )} +
+ } + /> + )}
+
+ {participantForm.watch("id") && ( +
{ + await upsertParticipant({ + ...data, + statusLog: data.statusLog as Prisma.InputJsonValue[], + }); + participantTableRef.current?.refresh(); + })} + className="space-y-1" + > +

Teilnehmer bearbeiten

+ + + {event?.hasPresenceEvents && ( + + )} +
+

Verlauf

+ {participantForm.watch("statusLog").map((s) => { + return ( +
+

{(s as any).event}

+

{new Date((s as any).timestamp).toLocaleString()}

+
+ ); + })} +
+ + + )}
{ {event && ( @@ -230,8 +393,8 @@ export const Form = ({ event }: { event?: Event }) => { onSubmit={() => false} type="button" onClick={() => { - console.log(row.original); - // TODO: open modal to edit appointment + appointmentForm.reset(row.original); + appointmentModal.current?.showModal(); }} className="btn btn-sm btn-outline" > diff --git a/apps/hub/app/(app)/admin/event/action.ts b/apps/hub/app/(app)/admin/event/action.ts index 6554c78d..93a84183 100644 --- a/apps/hub/app/(app)/admin/event/action.ts +++ b/apps/hub/app/(app)/admin/event/action.ts @@ -1,65 +1,50 @@ -'use server'; +"use server"; -import { prisma, Prisma, Event, Participant, EventAppointment } from '@repo/db'; +import { prisma, Prisma, Event, Participant, EventAppointment } from "@repo/db"; export const upsertEvent = async ( - event: Prisma.EventCreateInput, - id?: Event['id'] + event: Prisma.EventCreateInput, + id?: Event["id"], ) => { - const newEvent = id - ? await prisma.event.update({ - where: { id: id }, - data: event, - }) - : await prisma.event.create({ data: event }); - return newEvent; + const newEvent = id + ? await prisma.event.update({ + where: { id: id }, + data: event, + }) + : await prisma.event.create({ data: event }); + return newEvent; }; -export const deleteEvent = async (id: Event['id']) => { - await prisma.event.delete({ where: { id: id } }); +export const deleteEvent = async (id: Event["id"]) => { + await prisma.event.delete({ where: { id: id } }); }; export const upsertAppointment = async ( - eventAppointment: Prisma.XOR< - Prisma.EventAppointmentCreateInput, - Prisma.EventAppointmentUncheckedCreateInput - >, - id?: EventAppointment['id'] + eventAppointment: Prisma.XOR< + Prisma.EventAppointmentCreateInput, + Prisma.EventAppointmentUncheckedCreateInput + >, ) => { - const newEventAppointment = id - ? await prisma.eventAppointment.update({ - where: { id: id }, - data: eventAppointment, - }) - : await prisma.eventAppointment.create({ data: eventAppointment }); - return newEventAppointment; + const newEventAppointment = eventAppointment.id + ? await prisma.eventAppointment.update({ + where: { id: eventAppointment.id }, + data: eventAppointment, + }) + : await prisma.eventAppointment.create({ data: eventAppointment }); + return newEventAppointment; }; -export const deleteAppoinement = async (id: Event['id']) => { - await prisma.eventAppointment.delete({ where: { id: id } }); - prisma.eventAppointment.findMany({ - where: { - eventId: id, - }, - orderBy: { - // TODO: add order by in relation to table selected column - }, - }); +export const deleteAppoinement = async (id: Event["id"]) => { + await prisma.eventAppointment.delete({ where: { id: id } }); + prisma.eventAppointment.findMany({ + where: { + eventId: id, + }, + orderBy: { + // TODO: add order by in relation to table selected column + }, + }); }; - -export const upsertParticipant = async ( - participant: Prisma.ParticipantCreateInput, - id?: Participant['id'] -) => { - const newParticipant = id - ? await prisma.participant.update({ - where: { id: id }, - data: participant, - }) - : await prisma.participant.create({ data: participant }); - return newParticipant; -}; - -export const deleteParticipant = async (id: Participant['id']) => { - await prisma.participant.delete({ where: { id: id } }); +export const deleteParticipant = async (id: Participant["id"]) => { + await prisma.participant.delete({ where: { id: id } }); }; diff --git a/apps/hub/app/(app)/events/_components/modalBtn.tsx b/apps/hub/app/(app)/events/_components/modalBtn.tsx index 758408b6..57cb0ac5 100644 --- a/apps/hub/app/(app)/events/_components/modalBtn.tsx +++ b/apps/hub/app/(app)/events/_components/modalBtn.tsx @@ -5,10 +5,9 @@ import { CalendarIcon, EnterIcon, } from "@radix-ui/react-icons"; -import { Event, EventAppointment, Participant, User } from "@repo/db"; +import { Event, EventAppointment, Participant, prisma, User } from "@repo/db"; import { cn } from "../../../../helper/cn"; import { inscribeToMoodleCourse, upsertParticipant } from "../actions"; -import { useSession } from "next-auth/react"; import { Clock10Icon, Cross } from "lucide-react"; import { useForm } from "react-hook-form"; import { @@ -22,6 +21,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 { JsonArray } from "../../../../../../packages/database/generated/client/runtime/library"; interface ModalBtnProps { title: string; @@ -122,50 +122,54 @@ const ModalBtn = ({ )} )} - {selectedAppointment && !participant?.appointmentCancelled && ( -
-

Dein Ausgewähler Termin

- -

- {new Date( - selectedAppointment.appointmentDate, - ).toLocaleString()} -

- -
- )} - {!!dates.length && ( -

- Bitte finde dich an diesem Termin in unserem Discord ein. + {!canSelectDate && participant?.attended && ( +

+ + Du hast an dem Presenztermin teilgenommen

)} + {selectedAppointment && !participant?.appointmentCancelled && ( + <> +
+

Dein Ausgewähler Termin

+ +

+ {new Date( + selectedAppointment.appointmentDate, + ).toLocaleString()} +

+ +

+ Bitte finde dich an diesem Termin in unserem Discord ein. +

+
+ + )} )} {event.finisherMoodleCourseId && ( diff --git a/apps/hub/app/_components/PaginatedTable.tsx b/apps/hub/app/_components/PaginatedTable.tsx index 995184d0..49522d65 100644 --- a/apps/hub/app/_components/PaginatedTable.tsx +++ b/apps/hub/app/_components/PaginatedTable.tsx @@ -24,6 +24,7 @@ interface PaginatedTableProps include?: Record; leftOfSearch?: React.ReactNode; rightOfSearch?: React.ReactNode; + leftOfPagination?: React.ReactNode; ref?: Ref; } @@ -37,6 +38,7 @@ export function PaginatedTable({ ref, leftOfSearch, rightOfSearch, + leftOfPagination, ...restProps }: PaginatedTableProps) { const [data, setData] = useState([]); @@ -62,6 +64,10 @@ export function PaginatedTable({ }); }; + useEffect(() => { + RefreshTableData(); + }, [filter]); + useImperativeHandle(ref, () => ({ refresh: () => { RefreshTableData(); @@ -111,11 +117,14 @@ export function PaginatedTable({ showEditButton={showEditButton} {...restProps} /> - +
+ {leftOfPagination} + +
); } diff --git a/apps/hub/app/_components/ui/Input.tsx b/apps/hub/app/_components/ui/Input.tsx index 84a4e1fa..58809865 100644 --- a/apps/hub/app/_components/ui/Input.tsx +++ b/apps/hub/app/_components/ui/Input.tsx @@ -1,48 +1,47 @@ import React from "react"; import { - FieldValues, - Path, - RegisterOptions, - UseFormReturn, + FieldValues, + Path, + RegisterOptions, + UseFormReturn, } from "react-hook-form"; import { cn } from "../../../helper/cn"; interface InputProps - extends Omit, "form"> { - name: Path; - form: UseFormReturn; - formOptions?: RegisterOptions; - label?: string; - placeholder?: string; + extends Omit, "form"> { + name: Path; + form: UseFormReturn; + formOptions?: RegisterOptions; + label?: string; + placeholder?: string; } export const Input = ({ - name, - label = name, - placeholder = label, - form, - formOptions, - className, - ...inputProps + name, + label = name, + placeholder = label, + form, + formOptions, + className, + ...inputProps }: InputProps) => { - return ( - - ); + return ( + + ); }; diff --git a/apps/hub/app/_components/ui/Select.tsx b/apps/hub/app/_components/ui/Select.tsx index 65be10a0..972372f7 100644 --- a/apps/hub/app/_components/ui/Select.tsx +++ b/apps/hub/app/_components/ui/Select.tsx @@ -17,7 +17,7 @@ interface SelectProps extends Omit { label?: any; name: Path; - form: UseFormReturn; + form: UseFormReturn | any; formOptions?: RegisterOptions; // eslint-disable-next-line @typescript-eslint/no-explicit-any } diff --git a/apps/hub/package.json b/apps/hub/package.json index 66c7f175..74fc33d9 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -18,6 +18,7 @@ "axios": "^1.7.9", "bcryptjs": "^2.4.3", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "i": "^0.3.7", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", @@ -27,6 +28,7 @@ "next-remove-imports": "^1.0.12", "npm": "^11.1.0", "react": "^19.0.0", + "react-datepicker": "^8.1.0", "react-day-picker": "^9.5.1", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", diff --git a/apps/hub/types/prisma.d.ts b/apps/hub/types/prisma.d.ts new file mode 100644 index 00000000..a97b5f14 --- /dev/null +++ b/apps/hub/types/prisma.d.ts @@ -0,0 +1,11 @@ +import { Prisma } from "@prisma/client"; + +declare module "@prisma/client" { + export type InputJsonValue = + | string + | number + | boolean + | null + | JsonObject + | JsonArray; +} diff --git a/package-lock.json b/package-lock.json index d4bbf257..f89101d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,6 +100,7 @@ "axios": "^1.7.9", "bcryptjs": "^2.4.3", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "i": "^0.3.7", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", @@ -109,6 +110,7 @@ "next-remove-imports": "^1.0.12", "npm": "^11.1.0", "react": "^19.0.0", + "react-datepicker": "^8.1.0", "react-day-picker": "^9.5.1", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", @@ -880,6 +882,34 @@ "@floating-ui/utils": "^0.2.9" } }, + "node_modules/@floating-ui/react": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.5.tgz", + "integrity": "sha512-BX3jKxo39Ba05pflcQmqPPwc0qdNsdNi/eweAFtoIdrJWNen2sVEWMEac3i6jU55Qfx+lOcdMNKYn2CtWmlnOQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.9", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/utils": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", @@ -4067,6 +4097,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -12515,6 +12546,21 @@ "node": ">=0.10.0" } }, + "node_modules/react-datepicker": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-8.1.0.tgz", + "integrity": "sha512-11gIOrBGK1MOvl4+wxGv4YxTqXf+uoRPtKstYhb/P1cBdRdOP1sL26VE31apmDnvw8wSYfJe9AWwWbKqmM9tzw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.3", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/react-day-picker": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.5.1.tgz", @@ -13872,6 +13918,12 @@ "upper-case": "^1.1.1" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",