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
pull/1642/head^2
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.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>`;
}
}

View File

@ -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>`;
}
}

View File

@ -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"> {

View File

@ -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,
},
});

View File

@ -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>
)}
</>

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)
payment Payment[]
destinationCalendar DestinationCalendar?
cancellationReason String?
}
model Schedule {

View File

@ -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> {

View File

@ -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}}'.",