From 07b75dadbd6f171339b94ff7cbb43d16fe9e5b8b Mon Sep 17 00:00:00 2001 From: Nikolay Rademacher Date: Fri, 28 Jan 2022 18:40:29 +0100 Subject: [PATCH] feat: add option to provide cancellation reason for email (#1587) * feat: add option to provide cancellation reason for email * chore: move pos of getCancellationReason method in classes * fix: only show cancellation reason if given --- .../templates/attendee-cancelled-email.ts | 11 +++ .../templates/organizer-cancelled-email.ts | 11 +++ .../calendar/interfaces/Calendar.ts | 1 + pages/api/cancel.ts | 3 + pages/cancel/[uid].tsx | 94 +++++++++++-------- .../migration.sql | 2 + prisma/schema.prisma | 1 + prisma/zod/booking.ts | 1 + public/static/locales/en/common.json | 2 + 9 files changed, 85 insertions(+), 41 deletions(-) create mode 100644 prisma/migrations/20220121210720_add_cancellation_reason/migration.sql diff --git a/lib/emails/templates/attendee-cancelled-email.ts b/lib/emails/templates/attendee-cancelled-email.ts index 9e60566f..25762e24 100644 --- a/lib/emails/templates/attendee-cancelled-email.ts +++ b/lib/emails/templates/attendee-cancelled-email.ts @@ -48,6 +48,7 @@ ${this.getWhat()} ${this.getWhen()} ${this.getLocation()} ${this.getAdditionalNotes()} +${this.calEvent.cancellationReason && this.getCancellationReason()} `.replace(/(<([^>]+)>)/gi, ""); } @@ -95,6 +96,7 @@ ${this.getAdditionalNotes()} ${this.getWho()} ${this.getLocation()} ${this.getAdditionalNotes()} + ${this.calEvent.cancellationReason && this.getCancellationReason()} @@ -126,4 +128,13 @@ ${this.getAdditionalNotes()} `; } + + protected getCancellationReason(): string { + return ` +

+
+

${this.calEvent.attendees[0].language.translate("cancellation_reason")}

+

${this.calEvent.cancellationReason}

+
`; + } } diff --git a/lib/emails/templates/organizer-cancelled-email.ts b/lib/emails/templates/organizer-cancelled-email.ts index d6517118..639e4a8f 100644 --- a/lib/emails/templates/organizer-cancelled-email.ts +++ b/lib/emails/templates/organizer-cancelled-email.ts @@ -57,6 +57,7 @@ ${this.getWhat()} ${this.getWhen()} ${this.getLocation()} ${this.getAdditionalNotes()} +${this.calEvent.cancellationReason && this.getCancellationReason()} `.replace(/(<([^>]+)>)/gi, ""); } @@ -103,6 +104,7 @@ ${this.getAdditionalNotes()} ${this.getWho()} ${this.getLocation()} ${this.getAdditionalNotes()} + ${this.calEvent.cancellationReason && this.getCancellationReason()} @@ -134,4 +136,13 @@ ${this.getAdditionalNotes()} `; } + + protected getCancellationReason(): string { + return ` +

+
+

${this.calEvent.organizer.language.translate("cancellation_reason")}

+

${this.calEvent.cancellationReason}

+
`; + } } diff --git a/lib/integrations/calendar/interfaces/Calendar.ts b/lib/integrations/calendar/interfaces/Calendar.ts index 53960c1b..0b6c1b61 100644 --- a/lib/integrations/calendar/interfaces/Calendar.ts +++ b/lib/integrations/calendar/interfaces/Calendar.ts @@ -52,6 +52,7 @@ export interface CalendarEvent { videoCallData?: VideoCallData; paymentInfo?: PaymentInfo | null; destinationCalendar?: DestinationCalendar | null; + cancellationReason?: string | null; } export interface IntegrationCalendar extends Ensure, "externalId"> { diff --git a/pages/api/cancel.ts b/pages/api/cancel.ts index 89106951..8e86b2e4 100644 --- a/pages/api/cancel.ts +++ b/pages/api/cancel.ts @@ -25,6 +25,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } const uid = asStringOrNull(req.body.uid) || ""; + const cancellationReason = asStringOrNull(req.body.reason) || ""; const session = await getSession({ req: req }); const bookingToDelete = await prisma.booking.findUnique({ @@ -125,6 +126,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) uid: bookingToDelete?.uid, location: bookingToDelete?.location, destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar, + cancellationReason: cancellationReason, }; // Hook up the webhook logic here @@ -148,6 +150,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, data: { status: BookingStatus.CANCELLED, + cancellationReason: cancellationReason, }, }); diff --git a/pages/cancel/[uid].tsx b/pages/cancel/[uid].tsx index 2d605776..0d2d89f3 100644 --- a/pages/cancel/[uid].tsx +++ b/pages/cancel/[uid].tsx @@ -12,6 +12,7 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/t import { inferSSRProps } from "@lib/types/inferSSRProps"; import CustomBranding from "@components/CustomBranding"; +import { TextField } from "@components/form/fields"; import { HeadSeo } from "@components/seo/head-seo"; import { Button } from "@components/ui/Button"; @@ -25,6 +26,7 @@ export default function Type(props: inferSSRProps) { const [is24h] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(props.booking ? null : t("booking_already_cancelled")); + const [cancellationReason, setCancellationReason] = useState(""); const telemetry = useTelemetry(); return ( @@ -89,50 +91,60 @@ export default function Type(props: inferSSRProps) { {props.cancellationAllowed && ( -
- - + + const res = await fetch("/api/cancel", { + body: JSON.stringify(payload), + headers: { + "Content-Type": "application/json", + }, + method: "DELETE", + }); + + if (res.status >= 200 && res.status < 300) { + await router.push( + `/cancel/success?name=${props.profile.name}&title=${ + props.booking.title + }&eventPage=${props.profile.slug}&team=${ + props.booking.eventType?.team ? 1 : 0 + }` + ); + } else { + setLoading(false); + setError( + `${t("error_with_status_code_occured", { status: res.status })} ${t( + "please_try_again" + )}` + ); + } + }} + loading={loading}> + {t("cancel")} + + +
)} diff --git a/prisma/migrations/20220121210720_add_cancellation_reason/migration.sql b/prisma/migrations/20220121210720_add_cancellation_reason/migration.sql new file mode 100644 index 00000000..f23fe7ad --- /dev/null +++ b/prisma/migrations/20220121210720_add_cancellation_reason/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Booking" ADD COLUMN "cancellationReason" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c75c4767..0e149331 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -242,6 +242,7 @@ model Booking { paid Boolean @default(false) payment Payment[] destinationCalendar DestinationCalendar? + cancellationReason String? } model Schedule { diff --git a/prisma/zod/booking.ts b/prisma/zod/booking.ts index 96e9c983..4402bb48 100644 --- a/prisma/zod/booking.ts +++ b/prisma/zod/booking.ts @@ -35,6 +35,7 @@ export const _BookingModel = z.object({ rejected: z.boolean(), status: z.nativeEnum(BookingStatus), paid: z.boolean(), + cancellationReason: z.string().nullish(), }); export interface CompleteBooking extends z.infer { diff --git a/public/static/locales/en/common.json b/public/static/locales/en/common.json index 1a8a14b0..66ae463b 100644 --- a/public/static/locales/en/common.json +++ b/public/static/locales/en/common.json @@ -13,6 +13,8 @@ "event_request_cancelled": "Your scheduled event was cancelled", "organizer": "Organizer", "need_to_reschedule_or_cancel": "Need to reschedule or cancel?", + "cancellation_reason": "Reason for cancellation", + "cancellation_reason_placeholder": "Why are you cancelling? (optional)", "manage_this_event": "Manage this event", "your_event_has_been_scheduled": "Your event has been scheduled", "accept_our_license": "Accept our license by changing the .env variable <1>NEXT_PUBLIC_LICENSE_CONSENT to '{{agree}}'.",