From b7aea4322c0ad39617945de0d3674d268a7ab76d Mon Sep 17 00:00:00 2001 From: Henrik Skog Date: Fri, 3 Nov 2023 15:29:10 +0100 Subject: [PATCH] Add event extras system with management in dashboard (#680) --- .../app/(dashboard)/event/[id]/edit-card.tsx | 6 +- .../(dashboard)/event/[id]/extras-page.tsx | 61 +++++++++ .../src/app/(dashboard)/event/[id]/page.tsx | 9 +- apps/dashboard/src/app/ModalProvider.tsx | 4 + .../molecules/ActionSelect/ActionSelect.tsx | 44 +++++++ .../molecules/extras-form/ExtrasForm.tsx | 118 ++++++++++++++++++ .../molecules/extras-form/templates.ts | 15 +++ .../modals/create-event-extras-modal.tsx | 52 ++++++++ .../event/modals/edit-event-extras-modal.tsx | 63 ++++++++++ .../mutations/use-create-extras-mutation.ts | 31 +++++ .../use-edit-event-mutation-comittees.ts | 29 +++++ apps/db-migrator/src/fixtures/attendee.ts | 20 +++ apps/db-migrator/src/fixtures/event.ts | 39 ++++++ apps/db-migrator/src/fixtures/user.ts | 5 + .../event/__test__/event-service.spec.ts | 1 + .../modules/event/attendance-repository.ts | 12 ++ .../src/modules/event/attendance-service.ts | 14 ++- .../modules/event/event-committee-service.ts | 14 ++- .../src/modules/event/event-repository.ts | 14 ++- .../core/src/modules/event/event-service.ts | 18 ++- packages/db/src/db.generated.d.ts | 59 +++++---- .../src/migrations/0019_add_event_extras.ts | 11 ++ .../src/modules/event/attendance-router.ts | 13 ++ .../src/modules/event/event-router.ts | 17 ++- packages/types/src/event.ts | 42 +++++++ 25 files changed, 664 insertions(+), 47 deletions(-) create mode 100644 apps/dashboard/src/app/(dashboard)/event/[id]/extras-page.tsx create mode 100644 apps/dashboard/src/components/molecules/ActionSelect/ActionSelect.tsx create mode 100644 apps/dashboard/src/components/molecules/extras-form/ExtrasForm.tsx create mode 100644 apps/dashboard/src/components/molecules/extras-form/templates.ts create mode 100644 apps/dashboard/src/modules/event/modals/create-event-extras-modal.tsx create mode 100644 apps/dashboard/src/modules/event/modals/edit-event-extras-modal.tsx create mode 100644 apps/dashboard/src/modules/event/mutations/use-create-extras-mutation.ts create mode 100644 apps/dashboard/src/modules/event/mutations/use-edit-event-mutation-comittees.ts create mode 100644 packages/db/src/migrations/0019_add_event_extras.ts diff --git a/apps/dashboard/src/app/(dashboard)/event/[id]/edit-card.tsx b/apps/dashboard/src/app/(dashboard)/event/[id]/edit-card.tsx index 1a40708c6..95c4c5c5f 100644 --- a/apps/dashboard/src/app/(dashboard)/event/[id]/edit-card.tsx +++ b/apps/dashboard/src/app/(dashboard)/event/[id]/edit-card.tsx @@ -1,12 +1,12 @@ import { type FC } from "react" import { useCommitteeAllQuery } from "src/modules/committee/queries/use-committee-all-query" import { useEventDetailsContext } from "./provider" -import { useEditEventMutation } from "../../../../modules/event/mutations/use-edit-event-mutation" +import { useEditEventWithCommitteesMutation } from "../../../../modules/event/mutations/use-edit-event-mutation-comittees" import { useEventEditForm } from "../edit-form" export const EventEditCard: FC = () => { const { event, eventCommittees } = useEventDetailsContext() - const edit = useEditEventMutation() + const edit = useEditEventWithCommitteesMutation() const { committees } = useCommitteeAllQuery() const FormComponent = useEventEditForm({ label: "Oppdater arrangement", @@ -16,7 +16,7 @@ export const EventEditCard: FC = () => { edit.mutate({ id: data.id, event, - committeeIds, + committees: committeeIds, }) }, defaultValues: { diff --git a/apps/dashboard/src/app/(dashboard)/event/[id]/extras-page.tsx b/apps/dashboard/src/app/(dashboard)/event/[id]/extras-page.tsx new file mode 100644 index 000000000..04d9d4ebf --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/event/[id]/extras-page.tsx @@ -0,0 +1,61 @@ +import { Icon } from "@iconify/react" +import { ActionIcon, Box, Button, Paper, Title } from "@mantine/core" +import { type FC } from "react" +import { useEventDetailsContext } from "./provider" +import { useCreateEventExtrasModal } from "../../../../modules/event/modals/create-event-extras-modal" +import { useEditEventExtrasModal } from "../../../../modules/event/modals/edit-event-extras-modal" +import { useEditEventMutation } from "../../../../modules/event/mutations/use-edit-event-mutation" + +export const ExtrasPage: FC = () => { + const { event } = useEventDetailsContext() + + const openCreate = useCreateEventExtrasModal({ + event, + }) + + const openEdit = useEditEventExtrasModal({ + event, + }) + + const edit = useEditEventMutation() + + const deleteAlternative = (id: string) => { + const newChoices = event.extras?.filter((alt) => alt.id !== id) + edit.mutate({ + id: event.id, + event: { + ...event, + extras: newChoices ?? [], + }, + }) + } + + return ( + + Valg + {!event.extras?.length &&

Ingen valg er lagt til

} + + {event.extras?.map((extra) => ( + + openEdit(extra)} mr="md"> + + + deleteAlternative(extra.id)} color="red"> + + +

{extra.name}

+ {extra.choices.map((choice) => ( +
+

{choice.name}

+
+ ))} +
+ ))} +
+ + +
+ ) +} diff --git a/apps/dashboard/src/app/(dashboard)/event/[id]/page.tsx b/apps/dashboard/src/app/(dashboard)/event/[id]/page.tsx index d08c69409..b2e730162 100644 --- a/apps/dashboard/src/app/(dashboard)/event/[id]/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/event/[id]/page.tsx @@ -4,10 +4,11 @@ import { Icon } from "@iconify/react" import { Box, CloseButton, Group, Tabs, Title } from "@mantine/core" import { useRouter } from "next/navigation" import { type FC } from "react" +import { EventAttendancePage } from "./attendance-page" import { EventCompaniesPage } from "./companies-page" import { EventEditCard } from "./edit-card" +import { ExtrasPage } from "./extras-page" import { useEventDetailsContext } from "./provider" -import { EventAttendancePage } from "./attendance-page" const EventDetailsCompanies: FC = () =>

Bedrifter

@@ -44,6 +45,12 @@ const SIDEBAR_LINKS = [ slug: "attendance", component: EventAttendancePage, }, + { + icon: "tabler:calendar-event", + label: "Valg", + slug: "extras", + component: ExtrasPage, + }, ] export default function EventDetailsPage() { diff --git a/apps/dashboard/src/app/ModalProvider.tsx b/apps/dashboard/src/app/ModalProvider.tsx index 1a61720b5..7766135ca 100644 --- a/apps/dashboard/src/app/ModalProvider.tsx +++ b/apps/dashboard/src/app/ModalProvider.tsx @@ -5,11 +5,15 @@ import { type FC, type PropsWithChildren } from "react" import { CreateCompanyModal } from "src/modules/company/modals/create-company-modal" import { CreateEventModal } from "src/modules/event/modals/create-event-modal" import { CreateJobListingModal } from "src/modules/job-listing/modals/create-job-listing-modal" +import { CreateEventExtrasModal } from "../modules/event/modals/create-event-extras-modal" +import { UpdateEventExtrasModal } from "../modules/event/modals/edit-event-extras-modal" const modals = { "event/create": CreateEventModal, "jobListing/create": CreateJobListingModal, "company/create": CreateCompanyModal, + "extras/create": CreateEventExtrasModal, + "extras/update": UpdateEventExtrasModal, } as const export const ModalProvider: FC = ({ children }) => ( diff --git a/apps/dashboard/src/components/molecules/ActionSelect/ActionSelect.tsx b/apps/dashboard/src/components/molecules/ActionSelect/ActionSelect.tsx new file mode 100644 index 000000000..da6314d29 --- /dev/null +++ b/apps/dashboard/src/components/molecules/ActionSelect/ActionSelect.tsx @@ -0,0 +1,44 @@ +import { Button, type ButtonProps, Combobox, type ComboboxProps, useCombobox } from "@mantine/core" +import { type FC } from "react" + +interface ActionSelectProps extends ComboboxProps { + data: { value: string; label: string }[] + onChange?(value: string): void + buttonProps?: ButtonProps +} + +export const ActionSelect: FC = ({ data, onChange, buttonProps, ...comboBoxProps }) => { + const combobox = useCombobox({ + onDropdownClose: () => combobox.resetSelectedOption(), + }) + + const options = data.map((item) => ( + + {item.label} + + )) + + return ( + { + combobox.closeDropdown() + if (onChange) { + onChange(val) + } + }} + > + + + + + + {options} + + + ) +} diff --git a/apps/dashboard/src/components/molecules/extras-form/ExtrasForm.tsx b/apps/dashboard/src/components/molecules/extras-form/ExtrasForm.tsx new file mode 100644 index 000000000..14875e4ee --- /dev/null +++ b/apps/dashboard/src/components/molecules/extras-form/ExtrasForm.tsx @@ -0,0 +1,118 @@ +import { Box, Button, Flex, InputLabel, Text, TextInput } from "@mantine/core" +import { type FC } from "react" +import { useFieldArray, useForm } from "react-hook-form" +import { Icon } from "@iconify/react" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { templates } from "./templates" +import { ActionSelect } from "../../../components/molecules/ActionSelect/ActionSelect" + +type TemplateKey = keyof typeof templates + +const FormValuesSchema = z.object({ + question: z.string(), + alternatives: z.array(z.object({ value: z.string().min(1, "Dette feltet er påkrevd") })), +}) + +export type ExtrasFormValues = z.infer + +interface Props { + onSubmit(data: ExtrasFormValues): void + defaultAlternatives: ExtrasFormValues +} + +const templateChoices: { value: TemplateKey; label: TemplateKey }[] = Object.keys(templates).map((key) => ({ + value: key, + label: key, +})) + +export const ExtrasForm: FC = ({ onSubmit, defaultAlternatives }) => { + const { + register, + control, + handleSubmit, + setValue, + formState: { errors }, + } = useForm({ + defaultValues: defaultAlternatives, + mode: "onSubmit", + resolver: zodResolver(FormValuesSchema), + }) + + const { fields, append, remove } = useFieldArray({ + name: "alternatives", + control, + }) + + return ( + + { + const template = templates[value] + setValue("question", template.question) + setValue("alternatives", template.alternatives) + }} + /> + +
+ + Spørsmål + + + + Svaralternativer + {fields.map((field, index) => ( + + + + + + {errors.alternatives?.[index]?.value && ( + + {errors.alternatives[index]?.value?.message ?? "Ukjent feil"} + + )} + + ))} + + + + + + +
+
+
+ ) +} diff --git a/apps/dashboard/src/components/molecules/extras-form/templates.ts b/apps/dashboard/src/components/molecules/extras-form/templates.ts new file mode 100644 index 000000000..1611582c8 --- /dev/null +++ b/apps/dashboard/src/components/molecules/extras-form/templates.ts @@ -0,0 +1,15 @@ +import { type ExtrasFormValues } from "./ExtrasForm" + +export const templates: Record = { + "Pizza / sushi": { + question: "Hvilken mat vil du ha?", + alternatives: [ + { + value: "Pizza", + }, + { + value: "Sushi", + }, + ], + }, +} diff --git a/apps/dashboard/src/modules/event/modals/create-event-extras-modal.tsx b/apps/dashboard/src/modules/event/modals/create-event-extras-modal.tsx new file mode 100644 index 000000000..1b081dd4f --- /dev/null +++ b/apps/dashboard/src/modules/event/modals/create-event-extras-modal.tsx @@ -0,0 +1,52 @@ +import { type Event } from "@dotkomonline/types" +import { type ContextModalProps, modals } from "@mantine/modals" +import { type FC } from "react" +import { useEditEventMutation } from "../mutations/use-edit-event-mutation" +import { ExtrasForm, type ExtrasFormValues } from "../../../components/molecules/extras-form/ExtrasForm" + +export const CreateEventExtrasModal: FC> = ({ context, id, innerProps }) => { + const editEvent = useEditEventMutation() + const allExtras = innerProps.event.extras || [] + + const defaultAlternatives: ExtrasFormValues = { + question: "", + alternatives: [{ value: "" }], + } + + const onSubmit = (data: ExtrasFormValues) => { + const newExtras = [ + ...allExtras, + { + id: `${allExtras.length - 1}`, + name: data.question, + choices: data.alternatives.map((alternative, i) => ({ + id: `${i}`, + name: alternative.value, + })), + }, + ] + + editEvent.mutate({ + id: innerProps.event.id, + event: { + ...innerProps.event, + extras: newExtras, + }, + }) + + context.closeModal(id) + } + + return +} + +export const useCreateEventExtrasModal = + ({ event }: { event: Event }) => + () => + modals.openContextModal({ + modal: "extras/create", + title: "Legg til nytt deltakervalg", + innerProps: { + event, + }, + }) diff --git a/apps/dashboard/src/modules/event/modals/edit-event-extras-modal.tsx b/apps/dashboard/src/modules/event/modals/edit-event-extras-modal.tsx new file mode 100644 index 000000000..209033668 --- /dev/null +++ b/apps/dashboard/src/modules/event/modals/edit-event-extras-modal.tsx @@ -0,0 +1,63 @@ +import { type Event, type EventExtra } from "@dotkomonline/types" +import { type ContextModalProps, modals } from "@mantine/modals" +import { type FC } from "react" +import { useEditEventMutation } from "../mutations/use-edit-event-mutation" +import { ExtrasForm, type ExtrasFormValues } from "../../../components/molecules/extras-form/ExtrasForm" + +export const UpdateEventExtrasModal: FC> = ({ + context, + id, + innerProps, +}) => { + const editEvent = useEditEventMutation() + + const allExtras = innerProps.event.extras || [] + const existingExtra = innerProps.existingExtra + + const defaultAlternatives = { + question: existingExtra.name, + alternatives: existingExtra.choices.map((choice) => ({ + value: choice.name, + })), + } + + const onSubmit = (data: ExtrasFormValues) => { + const newExtras = allExtras.map((extra) => { + if (extra.id === existingExtra.id) { + return { + id: extra.id, + name: data.question, + choices: data.alternatives.map((alternative, i) => ({ + id: `${i}`, + name: alternative.value, + })), + } + } + return extra + }) + + editEvent.mutate({ + id: innerProps.event.id, + event: { + ...innerProps.event, + extras: newExtras, + }, + }) + + context.closeModal(id) + } + + return +} + +export const useEditEventExtrasModal = + ({ event }: { event: Event }) => + (existingExtra: EventExtra) => + modals.openContextModal({ + modal: "extras/update", + title: "Endre extra", + innerProps: { + event, + existingExtra, + }, + }) diff --git a/apps/dashboard/src/modules/event/mutations/use-create-extras-mutation.ts b/apps/dashboard/src/modules/event/mutations/use-create-extras-mutation.ts new file mode 100644 index 000000000..1037d2be3 --- /dev/null +++ b/apps/dashboard/src/modules/event/mutations/use-create-extras-mutation.ts @@ -0,0 +1,31 @@ +import { useRouter } from "next/navigation" +import { useQueryNotification } from "../../../app/notifications" +import { trpc } from "../../../utils/trpc" + +export const useCreateEventMutation = () => { + const utils = trpc.useContext() + const router = useRouter() + const notification = useQueryNotification() + return trpc.event.create.useMutation({ + onMutate: () => { + notification.loading({ + title: "Oppretter arrangement...", + message: "Arrangementet blir opprettet, og du vil bli videresendt til arrangementsiden.", + }) + }, + onSuccess: (data) => { + notification.complete({ + title: "Arrangement opprettet", + message: `Arrangementet "${data.title}" har blitt opprettet.`, + }) + utils.event.all.invalidate() + router.push(`/event/${data.id}`) + }, + onError: (err) => { + notification.fail({ + title: "Feil oppsto", + message: `En feil oppsto under opprettelse av arrangementet: ${err.toString()}.`, + }) + }, + }) +} diff --git a/apps/dashboard/src/modules/event/mutations/use-edit-event-mutation-comittees.ts b/apps/dashboard/src/modules/event/mutations/use-edit-event-mutation-comittees.ts new file mode 100644 index 000000000..ba9ab3ead --- /dev/null +++ b/apps/dashboard/src/modules/event/mutations/use-edit-event-mutation-comittees.ts @@ -0,0 +1,29 @@ +import { useQueryNotification } from "../../../app/notifications" +import { trpc } from "../../../utils/trpc" + +export const useEditEventWithCommitteesMutation = () => { + const notification = useQueryNotification() + const utils = trpc.useContext() + return trpc.event.editWithCommittees.useMutation({ + onMutate: () => { + notification.loading({ + title: "Oppdaterer arrangement...", + message: "Arrangementet blir oppdatert.", + }) + }, + onSuccess: (data) => { + notification.complete({ + title: "Arrangement oppdatert", + message: `Arrangementet "${data.title}" har blitt oppdatert.`, + }) + utils.event.all.invalidate() + utils.event.get.invalidate() + }, + onError: (err) => { + notification.fail({ + title: "Feil oppsto", + message: `En feil oppsto under oppdatering av arrangementet: ${err.toString()}.`, + }) + }, + }) +} diff --git a/apps/db-migrator/src/fixtures/attendee.ts b/apps/db-migrator/src/fixtures/attendee.ts index ab023c169..7857d0c0c 100644 --- a/apps/db-migrator/src/fixtures/attendee.ts +++ b/apps/db-migrator/src/fixtures/attendee.ts @@ -8,6 +8,16 @@ export const attendees: Insertable[] = [ updatedAt: new Date("2023-02-22 13:30:04.713+00"), userId: "01HB64XF7WZZZZZZZZZZZZZZZZ", attendanceId: "01HB64JAPWJBMZN3HN6RF5GPVF", + extrasChoices: JSON.stringify([ + { + id: "1", + choice: "1", + }, + { + id: "2", + choice: "3", + }, + ]), }, { id: "01HB64JAPXD30K1WYK53HYFXR2", @@ -15,6 +25,16 @@ export const attendees: Insertable[] = [ updatedAt: new Date("2023-02-22 13:30:04.713+00"), userId: "01HB64XF7WXBPGVQKFKFGJBH4D", attendanceId: "01HB64JAPWJBMZN3HN6RF5GPVF", + extrasChoices: JSON.stringify([ + { + id: "1", + choice: "2", + }, + { + id: "2", + choice: "3", + }, + ]), }, { id: "01HB64JAPW4Q0XR46MK831NTB2", diff --git a/apps/db-migrator/src/fixtures/event.ts b/apps/db-migrator/src/fixtures/event.ts index bb15fd763..ffa15a0f8 100644 --- a/apps/db-migrator/src/fixtures/event.ts +++ b/apps/db-migrator/src/fixtures/event.ts @@ -18,6 +18,44 @@ export const events: Insertable[] = [ "https://online.ntnu.no/_next/image?url=https%3A%2F%2Fonlineweb4-prod.s3.eu-north-1.amazonaws.com%2Fmedia%2Fimages%2Fresponsive%2Flg%2Fdf32b932-f4c4-4a49-9129-a8ab528b1e33.jpeg&w=1200&q=75", location: "Hovedbygget", waitlist: null, + extras: JSON.stringify([ + { + id: "0", + name: "Hva vil du ha til mat?", + choices: [ + { + id: "0", + name: "Pizza", + }, + { + id: "1", + name: "Burger", + }, + { + id: "2", + name: "Salad", + }, + ], + }, + { + id: "1", + name: "Når vil du ha mat?", + choices: [ + { + id: "0", + name: "Når jeg kommer", + }, + { + id: "1", + name: "Halvveis i arrangementet", + }, + { + id: "2", + name: "Til slutt", + }, + ], + }, + ]), }, { id: "01HB64TWZK1N8ABMH8JAE12101", @@ -35,5 +73,6 @@ export const events: Insertable[] = [ "https://online.ntnu.no/_next/image?url=https%3A%2F%2Fonlineweb4-prod.s3.eu-north-1.amazonaws.com%2Fmedia%2Fimages%2Fresponsive%2Flg%2Fdf32b932-f4c4-4a49-9129-a8ab528b1e33.jpeg&w=1200&q=75", location: "Åre, Sverige", waitlist: null, + extras: null, }, ] diff --git a/apps/db-migrator/src/fixtures/user.ts b/apps/db-migrator/src/fixtures/user.ts index d66292078..afd0061a6 100644 --- a/apps/db-migrator/src/fixtures/user.ts +++ b/apps/db-migrator/src/fixtures/user.ts @@ -20,4 +20,9 @@ export const users: Insertable[] = [ cognitoSub: "dddddddd-c376-4e5e-b5fb-4db9bf6cd417", studyYear: 0, }, + { + createdAt: new Date("2023-04-30 21:22:17.627253+00"), + cognitoSub: "7ba93bf4-2e11-40a9-bcd7-9078ee9090bb", + studyYear: 3, + }, ] diff --git a/packages/core/src/modules/event/__test__/event-service.spec.ts b/packages/core/src/modules/event/__test__/event-service.spec.ts index 04b91a8df..655242740 100644 --- a/packages/core/src/modules/event/__test__/event-service.spec.ts +++ b/packages/core/src/modules/event/__test__/event-service.spec.ts @@ -22,6 +22,7 @@ export const eventPayload: Omit = { status: "PUBLIC", type: "COMPANY", waitlist: null, + extras: null, } describe("EventService", () => { diff --git a/packages/core/src/modules/event/attendance-repository.ts b/packages/core/src/modules/event/attendance-repository.ts index 660234127..68fc503f3 100644 --- a/packages/core/src/modules/event/attendance-repository.ts +++ b/packages/core/src/modules/event/attendance-repository.ts @@ -20,6 +20,7 @@ export interface AttendanceRepository { updateAttendee(attendeeWrite: AttendeeWrite, userId: string, attendanceId: string): Promise getByEventId(eventId: EventId): Promise getByAttendanceId(id: AttendanceId): Promise + addChoice(eventId: EventId, attendanceId: AttendanceId, questionId: string, choiceId: string): Promise } export class AttendanceRepositoryImpl implements AttendanceRepository { @@ -109,4 +110,15 @@ export class AttendanceRepositoryImpl implements AttendanceRepository { .executeTakeFirst() return res ? AttendanceSchema.parse(res) : undefined } + + async addChoice(eventId: EventId, attendanceId: AttendanceId, questionId: string, choiceId: string) { + const res = await this.db + .updateTable("attendee") + .set({ extrasChoices: JSON.stringify([{ id: questionId, choice: choiceId }]) }) + .where("userId", "=", eventId) + .where("attendanceId", "=", attendanceId) + .returningAll() + .executeTakeFirstOrThrow() + return AttendeeSchema.parse(res) + } } diff --git a/packages/core/src/modules/event/attendance-service.ts b/packages/core/src/modules/event/attendance-service.ts index ec37dfd5b..6e12f7d27 100644 --- a/packages/core/src/modules/event/attendance-service.ts +++ b/packages/core/src/modules/event/attendance-service.ts @@ -4,8 +4,9 @@ import { type AttendanceRepository } from "./attendance-repository" export interface AttendanceService { canAttend(eventId: EventId): Promise registerForEvent(userId: UserId, eventId: EventId): Promise - deregisterAttendee(userId: UserId, attendanceId: AttendanceId): Promise - registerForAttendance(userId: UserId, attendanceId: AttendanceId, attended: boolean): Promise + deregisterForEvent(userId: UserId, eventId: EventId): Promise + registerForAttendance(userId: UserId, attendanceId: string, attended: boolean): Promise + addChoice(eventId: string, attendanceId: string, questionId: string, choiceId: string): Promise } export class AttendanceServiceImpl implements AttendanceService { @@ -38,4 +39,13 @@ export class AttendanceServiceImpl implements AttendanceService { ) return attendedAttendee } + + async addChoice(eventId: string, attendanceId: string, questionId: string, choiceId: string) { + const attendee = await this.attendanceRepository.getAttendeeByIds(eventId, attendanceId) + if (!attendee) { + throw new Error("Attendee not found") + } + const choice = await this.attendanceRepository.addChoice(eventId, attendanceId, questionId, choiceId) + return choice + } } diff --git a/packages/core/src/modules/event/event-committee-service.ts b/packages/core/src/modules/event/event-committee-service.ts index 7b9e95481..73867c224 100644 --- a/packages/core/src/modules/event/event-committee-service.ts +++ b/packages/core/src/modules/event/event-committee-service.ts @@ -3,6 +3,8 @@ import { type EventCommitteeRepositoryImpl } from "./event-committee-repository" export interface EventCommitteeService { getCommitteesForEvent(eventId: EventId): Promise + getEventCommitteesForEvent(eventId: EventId): Promise + setEventCommittees(eventId: EventId, committees: CommitteeId[]): Promise } export class EventCommitteeServiceImpl implements EventCommitteeService { @@ -18,7 +20,7 @@ export class EventCommitteeServiceImpl implements EventCommitteeService { return eventCommittees } - async setEventCommittees(eventId: EventId, committees: CommitteeId[]): Promise { + async setEventCommittees(eventId: EventId, committees: CommitteeId[]): Promise { // Fetch all committees associated with the event const eventCommittees = await this.committeeOrganizerRepository.getAllEventCommittees(eventId) const currentCommitteeIds = eventCommittees.map((committee) => committee.committeeId) @@ -37,5 +39,15 @@ export class EventCommitteeServiceImpl implements EventCommitteeService { // Execute all promises in parallel await Promise.all([...removePromises, ...addPromises]) + + // After removal and addition, we can identify the remaining committees + const remainingCommittees = currentCommitteeIds + .filter((committeeId) => !committeesToRemove.includes(committeeId)) // Remove the committees to remove + .concat(committeesToAdd) // Add the committees to add + + return remainingCommittees.map((committeeId) => ({ + eventId, + committeeId, + })) } } diff --git a/packages/core/src/modules/event/event-repository.ts b/packages/core/src/modules/event/event-repository.ts index 9528fa50c..7f0d62419 100644 --- a/packages/core/src/modules/event/event-repository.ts +++ b/packages/core/src/modules/event/event-repository.ts @@ -1,13 +1,15 @@ import { type Database } from "@dotkomonline/db" -import { type Event, type EventId, EventSchema, type EventWrite } from "@dotkomonline/types" -import { type Kysely, type Selectable } from "kysely" +import { type Event, type EventId, EventSchema } from "@dotkomonline/types" +import { type Insertable, type Kysely, type Selectable } from "kysely" import { type Cursor, orderedQuery } from "../../utils/db-utils" export const mapToEvent = (data: Selectable) => EventSchema.parse(data) +export type EventInsert = Insertable + export interface EventRepository { - create(data: EventWrite): Promise - update(id: EventId, data: Omit): Promise + create(data: EventInsert): Promise + update(id: EventId, data: EventInsert): Promise getAll(take: number, cursor?: Cursor): Promise getAllByCommitteeId(committeeId: string, take: number, cursor?: Cursor): Promise getById(id: string): Promise @@ -16,12 +18,12 @@ export interface EventRepository { export class EventRepositoryImpl implements EventRepository { constructor(private readonly db: Kysely) {} - async create(data: EventWrite): Promise { + async create(data: EventInsert): Promise { const event = await this.db.insertInto("event").values(data).returningAll().executeTakeFirstOrThrow() return mapToEvent(event) } - async update(id: EventId, data: Omit): Promise { + async update(id: EventId, data: EventInsert): Promise { const event = await this.db .updateTable("event") .set(data) diff --git a/packages/core/src/modules/event/event-service.ts b/packages/core/src/modules/event/event-service.ts index db3372494..728a181bc 100644 --- a/packages/core/src/modules/event/event-service.ts +++ b/packages/core/src/modules/event/event-service.ts @@ -1,8 +1,9 @@ import { type Attendance, type AttendanceWrite, type Event, type EventId, type EventWrite } from "@dotkomonline/types" import { type AttendanceRepository } from "./attendance-repository.js" +import { type EventInsert } from "./event-repository" import { type EventRepository } from "./event-repository.js" -import { NotFoundError } from "../../errors/errors" import { type Cursor } from "../../utils/db-utils" +import { NotFoundError } from "../../errors/errors" export interface EventService { createEvent(eventCreate: EventWrite): Promise @@ -22,10 +23,12 @@ export class EventServiceImpl implements EventService { ) {} async createEvent(eventCreate: EventWrite): Promise { - const event = await this.eventRepository.create(eventCreate) - if (!event) { - throw new Error("Failed to create event") + const toInsert: EventInsert = { + ...eventCreate, + extras: JSON.stringify(eventCreate.extras), } + + const event = await this.eventRepository.create(toInsert) return event } @@ -48,7 +51,11 @@ export class EventServiceImpl implements EventService { } async updateEvent(id: EventId, eventUpdate: Omit): Promise { - const event = await this.eventRepository.update(id, eventUpdate) + const toInsert: EventInsert = { + ...eventUpdate, + extras: JSON.stringify(eventUpdate.extras), + } + const event = await this.eventRepository.update(id, toInsert) return event } @@ -82,6 +89,7 @@ export class EventServiceImpl implements EventService { await this.eventRepository.update(eventId, { ...event, waitlist: waitlist.id, + extras: JSON.stringify(event.extras), }) return waitlist } diff --git a/packages/db/src/db.generated.d.ts b/packages/db/src/db.generated.d.ts index feebc9ee3..6303c1460 100644 --- a/packages/db/src/db.generated.d.ts +++ b/packages/db/src/db.generated.d.ts @@ -8,7 +8,17 @@ export type Generated = T extends ColumnType ? ColumnType : ColumnType -export type Int8 = ColumnType +export type Json = ColumnType + +export type JsonArray = JsonValue[] + +export type JsonObject = { + [K in string]?: JsonValue +} + +export type JsonPrimitive = boolean | number | string | null + +export type JsonValue = JsonArray | JsonObject | JsonPrimitive export type PaymentProvider = "STRIPE" @@ -37,6 +47,7 @@ export interface Attendee { attendanceId: string | null attended: Generated createdAt: Generated + extrasChoices: Json | null id: Generated updatedAt: Generated userId: string | null @@ -64,16 +75,11 @@ export interface Company { website: string } -export interface DrizzleDrizzleMigrations { - id: Generated - hash: string - createdAt: Int8 | null -} - export interface Event { createdAt: Generated description: string | null end: Timestamp + extras: Json | null id: Generated imageUrl: string | null location: string | null @@ -217,24 +223,23 @@ export interface RefundRequest { } export interface DB { - "attendance": Attendance - "attendee": Attendee - "committee": Committee - "company": Company - "drizzle.DrizzleMigrations": DrizzleDrizzleMigrations - "event": Event - "eventCommittee": EventCommittee - "eventCompany": EventCompany - "jobListing": JobListing - "jobListingLocation": JobListingLocation - "jobListingLocationLink": JobListingLocationLink - "mark": Mark - "notificationPermissions": NotificationPermissions - "owUser": OwUser - "payment": Payment - "personalMark": PersonalMark - "privacyPermissions": PrivacyPermissions - "product": Product - "productPaymentProvider": ProductPaymentProvider - "refundRequest": RefundRequest + attendance: Attendance + attendee: Attendee + committee: Committee + company: Company + event: Event + eventCommittee: EventCommittee + eventCompany: EventCompany + jobListing: JobListing + jobListingLocation: JobListingLocation + jobListingLocationLink: JobListingLocationLink + mark: Mark + notificationPermissions: NotificationPermissions + owUser: OwUser + payment: Payment + personalMark: PersonalMark + privacyPermissions: PrivacyPermissions + product: Product + productPaymentProvider: ProductPaymentProvider + refundRequest: RefundRequest } diff --git a/packages/db/src/migrations/0019_add_event_extras.ts b/packages/db/src/migrations/0019_add_event_extras.ts new file mode 100644 index 000000000..a1b6eb471 --- /dev/null +++ b/packages/db/src/migrations/0019_add_event_extras.ts @@ -0,0 +1,11 @@ +import { type Kysely } from "kysely" + +export async function up(db: Kysely) { + await db.schema.alterTable("event").addColumn("extras", "json").execute() + await db.schema.alterTable("attendee").addColumn("extras_choices", "json").execute() +} + +export async function down(db: Kysely) { + await db.schema.alterTable("attendee").dropColumn("extras_choices").execute() + await db.schema.alterTable("event").dropColumn("extras").execute() +} diff --git a/packages/gateway-trpc/src/modules/event/attendance-router.ts b/packages/gateway-trpc/src/modules/event/attendance-router.ts index c958fc308..2a3fe6729 100644 --- a/packages/gateway-trpc/src/modules/event/attendance-router.ts +++ b/packages/gateway-trpc/src/modules/event/attendance-router.ts @@ -69,4 +69,17 @@ export const attendanceRouter = t.router({ }) ) .mutation(async ({ input, ctx }) => await ctx.eventService.createWaitlist(input.eventId)), + addChoice: protectedProcedure + .input( + z.object({ + eventId: EventSchema.shape.id, + attendanceId: z.string(), + questionId: z.string(), + choiceId: z.string(), + }) + ) + .mutation( + async ({ input, ctx }) => + await ctx.attendanceService.addChoice(input.eventId, input.attendanceId, input.questionId, input.choiceId) + ), }) diff --git a/packages/gateway-trpc/src/modules/event/event-router.ts b/packages/gateway-trpc/src/modules/event/event-router.ts index 5f2edb3f8..84ecfa5d8 100644 --- a/packages/gateway-trpc/src/modules/event/event-router.ts +++ b/packages/gateway-trpc/src/modules/event/event-router.ts @@ -26,14 +26,27 @@ export const eventRouter = t.router({ z.object({ id: EventSchema.shape.id, event: EventWriteSchema, - committeeIds: z.array(EventCommitteeSchema.shape.committeeId), }) ) .mutation(async ({ input, ctx }) => { const event = await ctx.eventService.updateEvent(input.id, input.event) - await ctx.eventCommitteeService.setEventCommittees(input.id, input.committeeIds) return event }), + + editWithCommittees: protectedProcedure + .input( + z.object({ + id: EventSchema.shape.id, + event: EventWriteSchema, + committees: z.array(EventCommitteeSchema.shape.committeeId), + }) + ) + .mutation(async ({ input, ctx }) => { + const event = await ctx.eventService.updateEvent(input.id, input.event) + await ctx.eventCommitteeService.setEventCommittees(input.id, input.committees) + return event + }), + all: publicProcedure.input(PaginateInputSchema).query(async ({ input, ctx }) => { const events = await ctx.eventService.getEvents(input.take, input.cursor) const committees = events.map(async (e) => ctx.eventCommitteeService.getEventCommitteesForEvent(e.id)) diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index 4629dc59a..5ca43d39d 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -1,5 +1,18 @@ import { z } from "zod" +const EventExtraSchema = z.object({ + id: z.string(), + name: z.string(), + choices: z.array( + z.object({ + id: z.string(), + name: z.string(), + }) + ), +}) + +export type EventExtra = z.infer + export const EventSchema = z.object({ id: z.string().ulid(), createdAt: z.date(), @@ -15,6 +28,7 @@ export const EventSchema = z.object({ imageUrl: z.string().nullable(), location: z.string().nullable(), waitlist: z.string().ulid().nullable(), + extras: z.array(EventExtraSchema).nullable(), }) export type EventId = Event["id"] @@ -28,6 +42,11 @@ export const EventWriteSchema = EventSchema.partial({ export type EventWrite = z.infer +export const AttendeeextrasSchema = z.object({ + id: z.string(), + choice: z.string(), +}) + export const AttendeeSchema = z.object({ id: z.string(), attendanceId: z.string().ulid(), @@ -35,6 +54,26 @@ export const AttendeeSchema = z.object({ createdAt: z.coerce.date(), updatedAt: z.coerce.date(), attended: z.boolean(), + extras: z + .array( + z.object({ + id: z.string(), + choice: z.string(), + }) + ) + .nullable() + .optional(), +}) + +export const AttendanceExtrasSchema = z.object({ + id: z.string(), + name: z.string(), + choices: z.array( + z.object({ + id: z.string(), + name: z.string(), + }) + ), }) export const AttendanceSchema = z.object({ @@ -49,6 +88,7 @@ export const AttendanceSchema = z.object({ attendees: z.array(AttendeeSchema), min: z.number().min(0).max(5), max: z.number().min(0).max(5), + extras: z.array(AttendanceExtrasSchema).nullable().optional(), }) export type AttendanceId = Attendance["id"] @@ -62,12 +102,14 @@ export const AttendanceWriteSchema = AttendanceSchema.partial({ createdAt: true, updatedAt: true, attendees: true, + extras: true, }) export const AttendeeWriteSchema = AttendeeSchema.omit({ id: true, createdAt: true, updatedAt: true, + extras: true, }) export type AttendanceWrite = z.infer