diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 77fc94a77c..7be8c20963 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -39,10 +39,14 @@
"email_not_sent": "Error occurred while sending email",
"event_declined_subject": "Declined: {{title}} at {{date}}",
"event_cancelled_subject": "Canceled: {{title}} at {{date}}",
+ "event_reassigned_subject": "Reassigned: {{title}} at {{date}}",
"event_request_declined": "Your event request has been declined",
"event_request_declined_recurring": "Your recurring event request has been declined",
"event_request_cancelled": "Your scheduled event was canceled",
+ "event_request_reassigned": "Your scheduled event was reassigned",
+ "event_reassigned_subtitle": "You will no longer have the event on your calendar and your round robin likelihood will not be negatively impacted",
"organizer": "Organizer",
+ "reassigned_to": "Reassigned to",
"need_to_reschedule_or_cancel": "Need to reschedule or cancel?",
"you_can_view_booking_details_with_this_url": "You can view the booking details from this url {{url}} and add the event to your calendar",
"no_options_available": "No options available",
@@ -2654,10 +2658,10 @@
"no_columns_found": "No columns found",
"salesforce_create_record_as": "On booking, add events on and new attendees as:",
"salesforce_lead": "Lead",
- "salesforce_contact_under_account": "Contact under an account",
+ "salesforce_contact_under_account": "Contact under an account",
"salesforce_skip_entry_creation": "Skip creating {{entry}} record if they do not exist in Salesforce",
"salesforce_if_account_does_not_exist": "If the contact does not exist under an account, create new lead from attendee",
- "salesforce_create_new_contact_under_account": "Create a new contact under an account based on email domain of attendee and existing contacts",
+ "salesforce_create_new_contact_under_account": "Create a new contact under an account based on email domain of attendee and existing contacts",
"mass_assign_attributes": "Mass assign attributes",
"reroute_preview_custom_message": "It results in showing custom message. Try changing the response to route to an event",
"reroute_preview_external_redirect": "It results in redirecting to {{externalUrl}}. Try changing the response to route to an event",
diff --git a/packages/emails/email-manager.ts b/packages/emails/email-manager.ts
index 226be2c67b..96a16f49aa 100644
--- a/packages/emails/email-manager.ts
+++ b/packages/emails/email-manager.ts
@@ -65,6 +65,7 @@ import OrganizerDailyVideoDownloadRecordingEmail from "./templates/organizer-dai
import OrganizerDailyVideoDownloadTranscriptEmail from "./templates/organizer-daily-video-download-transcript-email";
import OrganizerLocationChangeEmail from "./templates/organizer-location-change-email";
import OrganizerPaymentRefundFailedEmail from "./templates/organizer-payment-refund-failed-email";
+import OrganizerReassignedEmail from "./templates/organizer-reassigned-email";
import OrganizerRequestEmail from "./templates/organizer-request-email";
import OrganizerRequestReminderEmail from "./templates/organizer-request-reminder-email";
import OrganizerRequestedToRescheduleEmail from "./templates/organizer-requested-to-reschedule-email";
@@ -191,7 +192,8 @@ export const sendRoundRobinRescheduledEmailsAndSMS = async (
export const sendRoundRobinCancelledEmailsAndSMS = async (
calEvent: CalendarEvent,
members: Person[],
- eventTypeMetadata?: EventTypeMetadata
+ eventTypeMetadata?: EventTypeMetadata,
+ reassignedTo?: { name: string | null; email: string }
) => {
if (eventTypeDisableHostEmail(eventTypeMetadata)) return;
const calendarEvent = formatCalEvent(calEvent);
@@ -199,9 +201,16 @@ export const sendRoundRobinCancelledEmailsAndSMS = async (
const successfullyReScheduledSMS = new EventCancelledSMS(calEvent);
for (const teamMember of members) {
- emailsAndSMSToSend.push(
- sendEmail(() => new OrganizerCancelledEmail({ calEvent: calendarEvent, teamMember }))
- );
+ if (!reassignedTo) {
+ emailsAndSMSToSend.push(
+ sendEmail(() => new OrganizerCancelledEmail({ calEvent: calendarEvent, teamMember }))
+ );
+ } else {
+ emailsAndSMSToSend.push(
+ sendEmail(() => new OrganizerReassignedEmail({ calEvent: calendarEvent, teamMember, reassignedTo }))
+ );
+ }
+
if (teamMember.phoneNumber) {
emailsAndSMSToSend.push(successfullyReScheduledSMS.sendSMSToAttendee(teamMember));
}
diff --git a/packages/emails/src/components/WhoInfo.tsx b/packages/emails/src/components/WhoInfo.tsx
index ec852d9120..da3023513a 100644
--- a/packages/emails/src/components/WhoInfo.tsx
+++ b/packages/emails/src/components/WhoInfo.tsx
@@ -5,7 +5,7 @@ import type { CalendarEvent } from "@calcom/types/Calendar";
import { Info } from "./Info";
-const PersonInfo = ({ name = "", email = "", role = "", phoneNumber = "" }) => (
+export const PersonInfo = ({ name = "", email = "", role = "", phoneNumber = "" }) => (
{name} - {role} {phoneNumber}
{!isSmsCalEmail(email) && (
diff --git a/packages/emails/src/templates/BaseScheduledEmail.tsx b/packages/emails/src/templates/BaseScheduledEmail.tsx
index 50f3d024e6..aa3e01bebe 100644
--- a/packages/emails/src/templates/BaseScheduledEmail.tsx
+++ b/packages/emails/src/templates/BaseScheduledEmail.tsx
@@ -15,6 +15,7 @@ import {
AppsStatus,
UserFieldsResponses,
} from "../components";
+import { PersonInfo } from "../components/WhoInfo";
export const BaseScheduledEmail = (
props: {
@@ -26,6 +27,7 @@ export const BaseScheduledEmail = (
locale: string;
timeFormat: TimeFormat | undefined;
isOrganizer?: boolean;
+ reassignedTo?: { name: string | null; email: string };
} & Partial>
) => {
const { t, timeZone, locale, timeFormat: timeFormat_ } = props;
@@ -79,7 +81,15 @@ export const BaseScheduledEmail = (
withSpacer
/>
)}
-
+ {props.reassignedTo && (
+
+ }
+ withSpacer
+ />
+ )}
diff --git a/packages/emails/src/templates/OrganizerReassignedEmail.tsx b/packages/emails/src/templates/OrganizerReassignedEmail.tsx
new file mode 100644
index 0000000000..b46ef76a0e
--- /dev/null
+++ b/packages/emails/src/templates/OrganizerReassignedEmail.tsx
@@ -0,0 +1,16 @@
+import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
+
+export const OrganizerReassignedEmail = (props: React.ComponentProps) => {
+ const t = props.teamMember?.language.translate || props.calEvent.organizer.language.translate;
+ return (
+ {t("event_reassigned_subtitle")}>}
+ callToAction={null}
+ reassignedTo={props.reassignedTo}
+ {...props}
+ />
+ );
+};
diff --git a/packages/emails/src/templates/OrganizerScheduledEmail.tsx b/packages/emails/src/templates/OrganizerScheduledEmail.tsx
index 50b7612568..523089c7a8 100644
--- a/packages/emails/src/templates/OrganizerScheduledEmail.tsx
+++ b/packages/emails/src/templates/OrganizerScheduledEmail.tsx
@@ -10,6 +10,7 @@ export const OrganizerScheduledEmail = (
newSeat?: boolean;
attendeeCancelled?: boolean;
teamMember?: Person;
+ reassignedTo?: { email: string; name: string | null };
} & Partial>
) => {
let subject;
@@ -49,12 +50,17 @@ export const OrganizerScheduledEmail = (
timeFormat={timeFormat}
isOrganizer
subtitle={
- <>
- {props.attendeeCancelled
- ? t("attendee_no_longer_attending_subtitle", { name: props.attendee.name })
- : ""}
- >
+ props.subtitle ? (
+ props.subtitle
+ ) : (
+ <>
+ {props.attendeeCancelled
+ ? t("attendee_no_longer_attending_subtitle", { name: props.attendee.name })
+ : ""}
+ >
+ )
}
+ reassignedTo={props.reassignedTo}
{...props}
attendee={attendee}
/>
diff --git a/packages/emails/src/templates/index.ts b/packages/emails/src/templates/index.ts
index a1e91e8e59..c5df13bc2f 100644
--- a/packages/emails/src/templates/index.ts
+++ b/packages/emails/src/templates/index.ts
@@ -12,6 +12,7 @@ export { SlugReplacementEmail } from "./SlugReplacementEmail";
export { FeedbackEmail } from "./FeedbackEmail";
export { ForgotPasswordEmail } from "./ForgotPasswordEmail";
export { OrganizerCancelledEmail } from "./OrganizerCancelledEmail";
+export { OrganizerReassignedEmail } from "./OrganizerReassignedEmail";
export { OrganizerLocationChangeEmail } from "./OrganizerLocationChangeEmail";
export { OrganizerPaymentRefundFailedEmail } from "./OrganizerPaymentRefundFailedEmail";
export { OrganizerRequestEmail } from "./OrganizerRequestEmail";
diff --git a/packages/emails/templates/organizer-reassigned-email.ts b/packages/emails/templates/organizer-reassigned-email.ts
new file mode 100644
index 0000000000..f509fe00be
--- /dev/null
+++ b/packages/emails/templates/organizer-reassigned-email.ts
@@ -0,0 +1,31 @@
+import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
+
+import { renderEmail } from "../";
+import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
+import OrganizerScheduledEmail from "./organizer-scheduled-email";
+
+export default class OrganizerReassignedEmail extends OrganizerScheduledEmail {
+ protected async getNodeMailerPayload(): Promise> {
+ const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
+
+ return {
+ icalEvent: generateIcsFile({
+ calEvent: this.calEvent,
+ status: "CANCELLED",
+ role: GenerateIcsRole.ORGANIZER,
+ }),
+ from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
+ to: toAddresses.join(","),
+ subject: `${this.t("event_reassigned_subject", {
+ title: this.calEvent.title,
+ date: this.getFormattedDate(),
+ })}`,
+ html: await renderEmail("OrganizerReassignedEmail", {
+ attendee: this.calEvent.organizer,
+ calEvent: this.calEvent,
+ reassignedTo: this.reassignedTo,
+ }),
+ text: this.getTextBody("event_request_reassigned"),
+ };
+ }
+}
diff --git a/packages/emails/templates/organizer-scheduled-email.ts b/packages/emails/templates/organizer-scheduled-email.ts
index d55fd76048..3fe48e0af8 100644
--- a/packages/emails/templates/organizer-scheduled-email.ts
+++ b/packages/emails/templates/organizer-scheduled-email.ts
@@ -17,14 +17,21 @@ export default class OrganizerScheduledEmail extends BaseEmail {
t: TFunction;
newSeat?: boolean;
teamMember?: Person;
+ reassignedTo?: { name: string | null; email: string };
- constructor(input: { calEvent: CalendarEvent; newSeat?: boolean; teamMember?: Person }) {
+ constructor(input: {
+ calEvent: CalendarEvent;
+ newSeat?: boolean;
+ teamMember?: Person;
+ reassignedTo?: { email: string; name: string | null };
+ }) {
super();
this.name = "SEND_BOOKING_CONFIRMATION";
this.calEvent = input.calEvent;
this.t = this.calEvent.organizer.language.translate;
this.newSeat = input.newSeat;
this.teamMember = input.teamMember;
+ this.reassignedTo = input.reassignedTo;
}
protected async getNodeMailerPayload(): Promise> {
diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.ts
index 7117dbe693..fa8f452ee2 100644
--- a/packages/features/ee/round-robin/roundRobinManualReassignment.ts
+++ b/packages/features/ee/round-robin/roundRobinManualReassignment.ts
@@ -244,7 +244,6 @@ export const roundRobinManualReassignment = async ({
bookingFields: eventType.bookingFields ?? null,
booking,
}),
- cancellationReason: "Manually re-assigned",
};
const credentials = await prisma.credential.findMany({
@@ -287,7 +286,7 @@ export const roundRobinManualReassignment = async ({
},
]);
- // Send cancellation email to original organizer
+ // Send cancellation email to previous RR host
const cancelledEvt = cloneDeep(evt);
cancelledEvt.organizer = {
email: originalOrganizer.email,
@@ -308,7 +307,8 @@ export const roundRobinManualReassignment = async ({
language: { translate: previousRRHostT, locale: previousRRHost.locale || "en" },
},
],
- eventType?.metadata as EventTypeMetadata
+ eventType?.metadata as EventTypeMetadata,
+ { name: newUser.name, email: newUser.email }
);
}
diff --git a/packages/features/ee/round-robin/roundRobinReassignment.ts b/packages/features/ee/round-robin/roundRobinReassignment.ts
index b4fdc71eb4..9e402ec3d3 100644
--- a/packages/features/ee/round-robin/roundRobinReassignment.ts
+++ b/packages/features/ee/round-robin/roundRobinReassignment.ts
@@ -375,7 +375,8 @@ export const roundRobinReassignment = async ({
language: { translate: previousRRHostT, locale: previousRRHost.locale || "en" },
},
],
- eventType?.metadata as EventTypeMetadata
+ eventType?.metadata as EventTypeMetadata,
+ { name: reassignedRRHost.name, email: reassignedRRHost.email }
);
}