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.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>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"> {
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Booking" ADD COLUMN "cancellationReason" TEXT;
|
|
@ -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 {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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}}'.",
|
||||||
|
|
Loading…
Reference in a new issue