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
This commit is contained in:
		
							parent
							
								
									8d0861809c
								
							
						
					
					
						commit
						07b75dadbd
					
				
					 9 changed files with 85 additions and 41 deletions
				
			
		|  | @ -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()} | ||||
|                             </div> | ||||
|                           </td> | ||||
|                         </tr> | ||||
|  | @ -126,4 +128,13 @@ ${this.getAdditionalNotes()} | |||
|     </html> | ||||
|     `;
 | ||||
|   } | ||||
| 
 | ||||
|   protected getCancellationReason(): string { | ||||
|     return ` | ||||
|     <p style="height: 6px"></p> | ||||
|     <div style="line-height: 6px;"> | ||||
|       <p style="color: #494949;">${this.calEvent.attendees[0].language.translate("cancellation_reason")}</p> | ||||
|       <p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.cancellationReason}</p> | ||||
|     </div>`;
 | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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()} | ||||
|                             </div> | ||||
|                           </td> | ||||
|                         </tr> | ||||
|  | @ -134,4 +136,13 @@ ${this.getAdditionalNotes()} | |||
|     </html> | ||||
|     `;
 | ||||
|   } | ||||
| 
 | ||||
|   protected getCancellationReason(): string { | ||||
|     return ` | ||||
|     <p style="height: 6px"></p> | ||||
|     <div style="line-height: 6px;"> | ||||
|       <p style="color: #494949;">${this.calEvent.organizer.language.translate("cancellation_reason")}</p> | ||||
|       <p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.cancellationReason}</p> | ||||
|     </div>`;
 | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -52,6 +52,7 @@ export interface CalendarEvent { | |||
|   videoCallData?: VideoCallData; | ||||
|   paymentInfo?: PaymentInfo | null; | ||||
|   destinationCalendar?: DestinationCalendar | null; | ||||
|   cancellationReason?: string | null; | ||||
| } | ||||
| 
 | ||||
| export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "externalId"> { | ||||
|  |  | |||
|  | @ -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, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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<typeof getServerSideProps>) { | |||
|   const [is24h] = useState(false); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [error, setError] = useState<string | null>(props.booking ? null : t("booking_already_cancelled")); | ||||
|   const [cancellationReason, setCancellationReason] = useState<string>(""); | ||||
|   const telemetry = useTelemetry(); | ||||
| 
 | ||||
|   return ( | ||||
|  | @ -89,50 +91,60 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) { | |||
|                       </div> | ||||
|                     </div> | ||||
|                     {props.cancellationAllowed && ( | ||||
|                       <div className="mt-5 space-x-2 text-center sm:mt-6"> | ||||
|                         <Button | ||||
|                           color="secondary" | ||||
|                           data-testid="cancel" | ||||
|                           onClick={async () => { | ||||
|                             setLoading(true); | ||||
|                       <div className="mt-5 sm:mt-6"> | ||||
|                         <TextField | ||||
|                           name={t("cancellation_reason")} | ||||
|                           placeholder={t("cancellation_reason_placeholder")} | ||||
|                           value={cancellationReason} | ||||
|                           onChange={(e) => setCancellationReason(e.target.value)} | ||||
|                           className="mb-5 sm:mb-6" | ||||
|                         /> | ||||
|                         <div className="text-center space-x-2"> | ||||
|                           <Button | ||||
|                             color="secondary" | ||||
|                             data-testid="cancel" | ||||
|                             onClick={async () => { | ||||
|                               setLoading(true); | ||||
| 
 | ||||
|                             const payload = { | ||||
|                               uid: uid, | ||||
|                             }; | ||||
|                               const payload = { | ||||
|                                 uid: uid, | ||||
|                                 reason: cancellationReason, | ||||
|                               }; | ||||
| 
 | ||||
|                             telemetry.withJitsu((jitsu) => | ||||
|                               jitsu.track(telemetryEventTypes.bookingCancelled, collectPageParameters()) | ||||
|                             ); | ||||
| 
 | ||||
|                             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 | ||||
|                                 }` | ||||
|                               telemetry.withJitsu((jitsu) => | ||||
|                                 jitsu.track(telemetryEventTypes.bookingCancelled, collectPageParameters()) | ||||
|                               ); | ||||
|                             } else { | ||||
|                               setLoading(false); | ||||
|                               setError( | ||||
|                                 `${t("error_with_status_code_occured", { status: res.status })} ${t( | ||||
|                                   "please_try_again" | ||||
|                                 )}` | ||||
|                               ); | ||||
|                             } | ||||
|                           }} | ||||
|                           loading={loading}> | ||||
|                           {t("cancel")} | ||||
|                         </Button> | ||||
|                         <Button onClick={() => router.push("/reschedule/" + uid)}>{t("reschedule")}</Button> | ||||
| 
 | ||||
|                               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")} | ||||
|                           </Button> | ||||
|                           <Button onClick={() => router.push("/reschedule/" + uid)}>{t("reschedule")}</Button> | ||||
|                         </div> | ||||
|                       </div> | ||||
|                     )} | ||||
|                   </> | ||||
|  |  | |||
|  | @ -0,0 +1,2 @@ | |||
| -- AlterTable | ||||
| ALTER TABLE "Booking" ADD COLUMN     "cancellationReason" TEXT; | ||||
|  | @ -242,6 +242,7 @@ model Booking { | |||
|   paid                Boolean              @default(false) | ||||
|   payment             Payment[] | ||||
|   destinationCalendar DestinationCalendar? | ||||
|   cancellationReason  String? | ||||
| } | ||||
| 
 | ||||
| model Schedule { | ||||
|  |  | |||
|  | @ -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<typeof _BookingModel> { | ||||
|  |  | |||
|  | @ -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</1> to '{{agree}}'.", | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 Nikolay Rademacher
						Nikolay Rademacher