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:
Nikolay Rademacher 2022-01-28 18:40:29 +01:00 committed by GitHub
parent 8d0861809c
commit 07b75dadbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 85 additions and 41 deletions

View file

@ -48,6 +48,7 @@ ${this.getWhat()}
${this.getWhen()} ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.calEvent.cancellationReason && this.getCancellationReason()}
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
} }
@ -95,6 +96,7 @@ ${this.getAdditionalNotes()}
${this.getWho()} ${this.getWho()}
${this.getLocation()} ${this.getLocation()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.calEvent.cancellationReason && this.getCancellationReason()}
</div> </div>
</td> </td>
</tr> </tr>
@ -126,4 +128,13 @@ ${this.getAdditionalNotes()}
</html> </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>`;
}
} }

View file

@ -57,6 +57,7 @@ ${this.getWhat()}
${this.getWhen()} ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.calEvent.cancellationReason && this.getCancellationReason()}
`.replace(/(<([^>]+)>)/gi, ""); `.replace(/(<([^>]+)>)/gi, "");
} }
@ -103,6 +104,7 @@ ${this.getAdditionalNotes()}
${this.getWho()} ${this.getWho()}
${this.getLocation()} ${this.getLocation()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
${this.calEvent.cancellationReason && this.getCancellationReason()}
</div> </div>
</td> </td>
</tr> </tr>
@ -134,4 +136,13 @@ ${this.getAdditionalNotes()}
</html> </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>`;
}
} }

View file

@ -52,6 +52,7 @@ export interface CalendarEvent {
videoCallData?: VideoCallData; videoCallData?: VideoCallData;
paymentInfo?: PaymentInfo | null; paymentInfo?: PaymentInfo | null;
destinationCalendar?: DestinationCalendar | null; destinationCalendar?: DestinationCalendar | null;
cancellationReason?: string | null;
} }
export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "externalId"> { export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "externalId"> {

View file

@ -25,6 +25,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
const uid = asStringOrNull(req.body.uid) || ""; const uid = asStringOrNull(req.body.uid) || "";
const cancellationReason = asStringOrNull(req.body.reason) || "";
const session = await getSession({ req: req }); const session = await getSession({ req: req });
const bookingToDelete = await prisma.booking.findUnique({ const bookingToDelete = await prisma.booking.findUnique({
@ -125,6 +126,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
uid: bookingToDelete?.uid, uid: bookingToDelete?.uid,
location: bookingToDelete?.location, location: bookingToDelete?.location,
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar, destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
cancellationReason: cancellationReason,
}; };
// Hook up the webhook logic here // Hook up the webhook logic here
@ -148,6 +150,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}, },
data: { data: {
status: BookingStatus.CANCELLED, status: BookingStatus.CANCELLED,
cancellationReason: cancellationReason,
}, },
}); });

View file

@ -12,6 +12,7 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/t
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import CustomBranding from "@components/CustomBranding"; import CustomBranding from "@components/CustomBranding";
import { TextField } from "@components/form/fields";
import { HeadSeo } from "@components/seo/head-seo"; import { HeadSeo } from "@components/seo/head-seo";
import { Button } from "@components/ui/Button"; import { Button } from "@components/ui/Button";
@ -25,6 +26,7 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
const [is24h] = useState(false); const [is24h] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(props.booking ? null : t("booking_already_cancelled")); const [error, setError] = useState<string | null>(props.booking ? null : t("booking_already_cancelled"));
const [cancellationReason, setCancellationReason] = useState<string>("");
const telemetry = useTelemetry(); const telemetry = useTelemetry();
return ( return (
@ -89,50 +91,60 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
</div> </div>
</div> </div>
{props.cancellationAllowed && ( {props.cancellationAllowed && (
<div className="mt-5 space-x-2 text-center sm:mt-6"> <div className="mt-5 sm:mt-6">
<Button <TextField
color="secondary" name={t("cancellation_reason")}
data-testid="cancel" placeholder={t("cancellation_reason_placeholder")}
onClick={async () => { value={cancellationReason}
setLoading(true); 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 = { const payload = {
uid: uid, uid: uid,
}; reason: cancellationReason,
};
telemetry.withJitsu((jitsu) => telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.bookingCancelled, collectPageParameters()) 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
}`
); );
} else {
setLoading(false); const res = await fetch("/api/cancel", {
setError( body: JSON.stringify(payload),
`${t("error_with_status_code_occured", { status: res.status })} ${t( headers: {
"please_try_again" "Content-Type": "application/json",
)}` },
); method: "DELETE",
} });
}}
loading={loading}> if (res.status >= 200 && res.status < 300) {
{t("cancel")} await router.push(
</Button> `/cancel/success?name=${props.profile.name}&title=${
<Button onClick={() => router.push("/reschedule/" + uid)}>{t("reschedule")}</Button> 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> </div>
)} )}
</> </>

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Booking" ADD COLUMN "cancellationReason" TEXT;

View file

@ -242,6 +242,7 @@ model Booking {
paid Boolean @default(false) paid Boolean @default(false)
payment Payment[] payment Payment[]
destinationCalendar DestinationCalendar? destinationCalendar DestinationCalendar?
cancellationReason String?
} }
model Schedule { model Schedule {

View file

@ -35,6 +35,7 @@ export const _BookingModel = z.object({
rejected: z.boolean(), rejected: z.boolean(),
status: z.nativeEnum(BookingStatus), status: z.nativeEnum(BookingStatus),
paid: z.boolean(), paid: z.boolean(),
cancellationReason: z.string().nullish(),
}); });
export interface CompleteBooking extends z.infer<typeof _BookingModel> { export interface CompleteBooking extends z.infer<typeof _BookingModel> {

View file

@ -13,6 +13,8 @@
"event_request_cancelled": "Your scheduled event was cancelled", "event_request_cancelled": "Your scheduled event was cancelled",
"organizer": "Organizer", "organizer": "Organizer",
"need_to_reschedule_or_cancel": "Need to reschedule or cancel?", "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", "manage_this_event": "Manage this event",
"your_event_has_been_scheduled": "Your event has been scheduled", "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}}'.", "accept_our_license": "Accept our license by changing the .env variable <1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.",