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