Seats events send email to attendees and organizer (#3206)
* Add attendees to email notfications * New seat title * Change organizer email * Clean console.log * Fix type errors * Update packages/emails/src/templates/OrganizerScheduledEmail.tsx Co-authored-by: Omar López <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Omar López
kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
parent
833791bc9a
commit
2cbf46a323
@@ -13,6 +13,7 @@ import {
|
||||
sendOrganizerRequestEmail,
|
||||
sendRescheduledEmails,
|
||||
sendScheduledEmails,
|
||||
sendScheduledSeatsEmails,
|
||||
} from "@calcom/emails";
|
||||
import { getLuckyUsers, isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
@@ -291,6 +292,67 @@ async function handler(req: NextApiRequest) {
|
||||
language: { translate: tGuests, locale: "en" },
|
||||
}));
|
||||
|
||||
const seed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}:${new Date().getTime()}`;
|
||||
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
|
||||
|
||||
const location = !!eventType.locations ? (eventType.locations as Array<{ type: string }>)[0] : "";
|
||||
const locationType = !!location && location.type ? location.type : "";
|
||||
|
||||
const customInputs = {} as NonNullable<CalendarEvent["customInputs"]>;
|
||||
|
||||
const teamMemberPromises =
|
||||
eventType.schedulingType === SchedulingType.COLLECTIVE
|
||||
? users.slice(1).map(async function (user) {
|
||||
return {
|
||||
email: user.email || "",
|
||||
name: user.name || "",
|
||||
timeZone: user.timeZone,
|
||||
language: {
|
||||
translate: await getTranslation(user.locale ?? "en", "common"),
|
||||
locale: user.locale ?? "en",
|
||||
},
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
const teamMembers = await Promise.all(teamMemberPromises);
|
||||
|
||||
const attendeesList = [...invitee, ...guests, ...teamMembers];
|
||||
|
||||
const eventNameObject = {
|
||||
attendeeName: reqBody.name || "Nameless",
|
||||
eventType: eventType.title,
|
||||
eventName: eventType.eventName,
|
||||
host: organizerUser.name || "Nameless",
|
||||
location: locationType,
|
||||
t: tOrganizer,
|
||||
};
|
||||
|
||||
const additionalNotes = reqBody.notes;
|
||||
|
||||
let evt: CalendarEvent = {
|
||||
type: eventType.title,
|
||||
title: getEventName(eventNameObject), //this needs to be either forced in english, or fetched for each attendee and organizer separately
|
||||
description: eventType.description,
|
||||
additionalNotes,
|
||||
customInputs,
|
||||
startTime: reqBody.start,
|
||||
endTime: reqBody.end,
|
||||
organizer: {
|
||||
name: organizerUser.name || "Nameless",
|
||||
email: organizerUser.email || "Email-less",
|
||||
timeZone: organizerUser.timeZone,
|
||||
language: { translate: tOrganizer, locale: organizer?.locale ?? "en" },
|
||||
},
|
||||
attendees: attendeesList,
|
||||
location: reqBody.location, // Will be processed by the EventManager later.
|
||||
/** For team events & dynamic collective events, we will need to handle each member destinationCalendar eventually */
|
||||
destinationCalendar: eventType.destinationCalendar || organizerUser.destinationCalendar,
|
||||
hideCalendarNotes: eventType.hideCalendarNotes,
|
||||
requiresConfirmation: eventType.requiresConfirmation ?? false,
|
||||
eventTypeId: eventType.id,
|
||||
};
|
||||
|
||||
// For seats, if the booking already exists then we want to add the new attendee to the existing booking
|
||||
if (reqBody.bookingUid) {
|
||||
if (!eventType.seatsPerTimeSlot)
|
||||
@@ -306,6 +368,13 @@ async function handler(req: NextApiRequest) {
|
||||
});
|
||||
if (!booking) throw new HttpError({ statusCode: 404, message: "Booking not found" });
|
||||
|
||||
// Need to add translation for attendees to pass type checks. Since these values are never written to the db we can just use the new attendee language
|
||||
const bookingAttendees = booking.attendees.map((attendee) => {
|
||||
return { ...attendee, language: { translate: tAttendees, locale: language ?? "en" } };
|
||||
});
|
||||
|
||||
evt = { ...evt, attendees: [...bookingAttendees, invitee[0]] };
|
||||
|
||||
if (eventType.seatsPerTimeSlot <= booking.attendees.length)
|
||||
throw new HttpError({ statusCode: 409, message: "Booking seats are full" });
|
||||
|
||||
@@ -327,76 +396,21 @@ async function handler(req: NextApiRequest) {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newSeat = booking.attendees.length !== 0;
|
||||
|
||||
await sendScheduledSeatsEmails(evt, invitee[0], newSeat);
|
||||
|
||||
req.statusCode = 201;
|
||||
return booking;
|
||||
}
|
||||
|
||||
const teamMemberPromises =
|
||||
eventType.schedulingType === SchedulingType.COLLECTIVE
|
||||
? users.slice(1).map(async function (user) {
|
||||
return {
|
||||
email: user.email || "",
|
||||
name: user.name || "",
|
||||
timeZone: user.timeZone,
|
||||
language: {
|
||||
translate: await getTranslation(user.locale ?? "en", "common"),
|
||||
locale: user.locale ?? "en",
|
||||
},
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
const teamMembers = await Promise.all(teamMemberPromises);
|
||||
|
||||
const attendeesList = [...invitee, ...guests, ...teamMembers];
|
||||
|
||||
const seed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}:${new Date().getTime()}`;
|
||||
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
|
||||
|
||||
const location = !!eventType.locations ? (eventType.locations as Array<{ type: string }>)[0] : "";
|
||||
const locationType = !!location && location.type ? location.type : "";
|
||||
const eventNameObject = {
|
||||
attendeeName: reqBody.name || "Nameless",
|
||||
eventType: eventType.title,
|
||||
eventName: eventType.eventName,
|
||||
host: organizerUser.name || "Nameless",
|
||||
location: locationType,
|
||||
t: tOrganizer,
|
||||
};
|
||||
|
||||
const additionalNotes = reqBody.notes;
|
||||
|
||||
const customInputs = {} as NonNullable<CalendarEvent["customInputs"]>;
|
||||
|
||||
if (reqBody.customInputs.length > 0) {
|
||||
reqBody.customInputs.forEach(({ label, value }) => {
|
||||
customInputs[label] = value;
|
||||
});
|
||||
}
|
||||
|
||||
const evt: CalendarEvent = {
|
||||
type: eventType.title,
|
||||
title: getEventName(eventNameObject), //this needs to be either forced in english, or fetched for each attendee and organizer separately
|
||||
description: eventType.description,
|
||||
additionalNotes,
|
||||
customInputs,
|
||||
startTime: reqBody.start,
|
||||
endTime: reqBody.end,
|
||||
organizer: {
|
||||
name: organizerUser.name || "Nameless",
|
||||
email: organizerUser.email || "Email-less",
|
||||
timeZone: organizerUser.timeZone,
|
||||
language: { translate: tOrganizer, locale: organizer?.locale ?? "en" },
|
||||
},
|
||||
attendees: attendeesList,
|
||||
location: reqBody.location, // Will be processed by the EventManager later.
|
||||
/** For team events & dynamic collective events, we will need to handle each member destinationCalendar eventually */
|
||||
destinationCalendar: eventType.destinationCalendar || organizerUser.destinationCalendar,
|
||||
hideCalendarNotes: eventType.hideCalendarNotes,
|
||||
requiresConfirmation: eventType.requiresConfirmation ?? false,
|
||||
eventTypeId: eventType.id,
|
||||
};
|
||||
|
||||
if (eventType.schedulingType === SchedulingType.COLLECTIVE) {
|
||||
evt.team = {
|
||||
members: users.map((user) => user.name || user.username || "Nameless"),
|
||||
|
||||
@@ -923,5 +923,7 @@
|
||||
"attendee_name": "Attendee's name",
|
||||
"broken_integration": "Broken integration",
|
||||
"problem_adding_video_link": "There was a problem adding a video link",
|
||||
"problem_updating_calendar": "There was a problem updating your calendar"
|
||||
"problem_updating_calendar": "There was a problem updating your calendar",
|
||||
"new_seat_subject": "New Attendee {{name}} on {{eventType}} at {{date}}",
|
||||
"new_seat_title": "Someone has added themselves to an event"
|
||||
}
|
||||
|
||||
@@ -81,6 +81,38 @@ export const sendRescheduledEmails = async (calEvent: CalendarEvent) => {
|
||||
await Promise.all(emailsToSend);
|
||||
};
|
||||
|
||||
export const sendScheduledSeatsEmails = async (
|
||||
calEvent: CalendarEvent,
|
||||
invitee: Person,
|
||||
newSeat: boolean
|
||||
) => {
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
emailsToSend.push(
|
||||
new Promise((resolve, reject) => {
|
||||
try {
|
||||
const scheduledEmail = new AttendeeScheduledEmail(calEvent, invitee);
|
||||
resolve(scheduledEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("AttendeeRescheduledEmail.sendEmail failed", e));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
emailsToSend.push(
|
||||
new Promise((resolve, reject) => {
|
||||
try {
|
||||
const scheduledEmail = new OrganizerScheduledEmail(calEvent, newSeat);
|
||||
resolve(scheduledEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("OrganizerScheduledEmail.sendEmail failed", e));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(emailsToSend);
|
||||
};
|
||||
|
||||
export const sendOrganizerRequestEmail = async (calEvent: CalendarEvent) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
try {
|
||||
|
||||
@@ -6,17 +6,33 @@ export const OrganizerScheduledEmail = (
|
||||
props: {
|
||||
calEvent: CalendarEvent;
|
||||
attendee: Person;
|
||||
newSeat?: boolean;
|
||||
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
|
||||
) => {
|
||||
let subject;
|
||||
let title;
|
||||
|
||||
if (props.newSeat) {
|
||||
subject = "new_seat_subject";
|
||||
} else {
|
||||
subject = "confirmed_event_type_subject";
|
||||
}
|
||||
|
||||
if (props.calEvent.recurringEvent?.count) {
|
||||
title = "new_event_scheduled_recurring";
|
||||
} else if (props.newSeat) {
|
||||
title = "new_seat_title";
|
||||
} else {
|
||||
title = "new_event_scheduled";
|
||||
}
|
||||
|
||||
const t = props.calEvent.organizer.language.translate;
|
||||
return (
|
||||
<BaseScheduledEmail
|
||||
timeZone={props.calEvent.organizer.timeZone}
|
||||
t={t}
|
||||
subject={t("confirmed_event_type_subject")}
|
||||
title={t(
|
||||
props.calEvent.recurringEvent?.count ? "new_event_scheduled_recurring" : "new_event_scheduled"
|
||||
)}
|
||||
subject={t(subject)}
|
||||
title={t(title)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -12,12 +12,14 @@ import BaseEmail from "./_base-email";
|
||||
export default class OrganizerScheduledEmail extends BaseEmail {
|
||||
calEvent: CalendarEvent;
|
||||
t: TFunction;
|
||||
newSeat?: boolean;
|
||||
|
||||
constructor(calEvent: CalendarEvent) {
|
||||
constructor(calEvent: CalendarEvent, newSeat?: boolean) {
|
||||
super();
|
||||
this.name = "SEND_BOOKING_CONFIRMATION";
|
||||
this.calEvent = calEvent;
|
||||
this.t = this.calEvent.organizer.language.translate;
|
||||
this.newSeat = newSeat;
|
||||
}
|
||||
|
||||
protected getiCalEventAsString(): string | undefined {
|
||||
@@ -66,6 +68,13 @@ export default class OrganizerScheduledEmail extends BaseEmail {
|
||||
});
|
||||
}
|
||||
|
||||
let subject;
|
||||
if (this.newSeat) {
|
||||
subject = "new_seat_subject";
|
||||
} else {
|
||||
subject = "confirmed_event_type_subject";
|
||||
}
|
||||
|
||||
return {
|
||||
icalEvent: {
|
||||
filename: "event.ics",
|
||||
@@ -73,7 +82,7 @@ export default class OrganizerScheduledEmail extends BaseEmail {
|
||||
},
|
||||
from: `Cal.com <${this.getMailerOptions().from}>`,
|
||||
to: toAddresses.join(","),
|
||||
subject: `${this.t("confirmed_event_type_subject", {
|
||||
subject: `${this.t(subject, {
|
||||
eventType: this.calEvent.type,
|
||||
name: this.calEvent.attendees[0].name,
|
||||
date: this.getFormattedDate(),
|
||||
@@ -81,6 +90,7 @@ export default class OrganizerScheduledEmail extends BaseEmail {
|
||||
html: renderEmail("OrganizerScheduledEmail", {
|
||||
calEvent: this.calEvent,
|
||||
attendee: this.calEvent.organizer,
|
||||
newSeat: this.newSeat,
|
||||
}),
|
||||
text: this.getTextBody(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user