diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 875fa641eb..c2b1894322 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -115,14 +115,14 @@ const BookingPage = ({ const mutation = useMutation(createBooking, { onSuccess: async (responseData) => { - const { id, attendees, paymentUid } = responseData; + const { id, paymentUid } = responseData; if (paymentUid) { return await router.push( createPaymentLink({ paymentUid, date, - name: attendees[0].name, - email: attendees[0].email, + name: bookingForm.getValues("name"), + email: bookingForm.getValues("email"), absolute: false, }) ); @@ -136,8 +136,8 @@ const BookingPage = ({ eventSlug: eventType.slug, username: profile.slug, reschedule: !!rescheduleUid, - name: attendees[0].name, - email: attendees[0].email, + name: bookingForm.getValues("name"), + email: bookingForm.getValues("email"), location: responseData.location, eventName: profile.eventName || "", bookingId: id, @@ -523,7 +523,15 @@ const BookingPage = ({ )} {!!eventType.seatsPerTimeSlot && (
- + = 0.5 + ? "text-rose-600" + : booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33 + ? "text-yellow-500" + : "text-bookinghighlight" + }`} + />

= 0.5 diff --git a/apps/web/components/v2/eventtype/EventAdvancedTab.tsx b/apps/web/components/v2/eventtype/EventAdvancedTab.tsx index 85b7641e8e..09f515d94a 100644 --- a/apps/web/components/v2/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/v2/eventtype/EventAdvancedTab.tsx @@ -24,6 +24,7 @@ import { TextField, Tooltip, } from "@calcom/ui/v2"; +import CheckboxField from "@calcom/ui/v2/core/form/Checkbox"; import CustomInputTypeForm from "@components/v2/eventtype/CustomInputTypeForm"; @@ -429,6 +430,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick +

+ formMethods.setValue("seatsShowAttendees", e.target.checked)} + defaultChecked={!!eventType.seatsShowAttendees} + /> +
)} /> diff --git a/apps/web/pages/api/email.ts b/apps/web/pages/api/email.ts index 57c81afa61..5b6b01da87 100644 --- a/apps/web/pages/api/email.ts +++ b/apps/web/pages/api/email.ts @@ -32,6 +32,18 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { timeZone: "America/Chihuahua", language, }, + { + email: "pro@example.com", + name: "pro@example.com", + timeZone: "America/Chihuahua", + language, + }, + { + email: "pro@example.com", + name: "pro@example.com", + timeZone: "America/Chihuahua", + language, + }, ], location: "Zoom video", destinationCalendar: null, @@ -48,7 +60,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { res.setHeader("Content-Type", "text/html"); res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate"); res.write( - renderEmail("OrganizerRequestReminderEmail", { + renderEmail("AttendeeScheduledEmail", { attendee: evt.attendees[0], calEvent: evt, }) diff --git a/apps/web/pages/event-types/[type]/index.tsx b/apps/web/pages/event-types/[type]/index.tsx index 810bd62a74..c8eadd7481 100644 --- a/apps/web/pages/event-types/[type]/index.tsx +++ b/apps/web/pages/event-types/[type]/index.tsx @@ -69,6 +69,7 @@ export type FormValues = { periodCountCalendarDays: "1" | "0"; periodDates: { startDate: Date; endDate: Date }; seatsPerTimeSlot: number | null; + seatsShowAttendees: boolean | null; seatsPerTimeSlotEnabled: boolean; minimumBookingNotice: number; beforeBufferTime: number; @@ -232,6 +233,7 @@ const EventTypePage = (props: inferSSRProps) => { beforeBufferTime, afterBufferTime, seatsPerTimeSlot, + seatsShowAttendees, bookingLimits, recurringEvent, locations, @@ -259,6 +261,7 @@ const EventTypePage = (props: inferSSRProps) => { afterEventBuffer: afterBufferTime, bookingLimits, seatsPerTimeSlot, + seatsShowAttendees, metadata, }); }}> @@ -391,6 +394,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => price: true, destinationCalendar: true, seatsPerTimeSlot: true, + seatsShowAttendees: true, workflows: { include: { workflow: { diff --git a/apps/web/pages/success.tsx b/apps/web/pages/success.tsx index 907bdc77ce..1d63301bc8 100644 --- a/apps/web/pages/success.tsx +++ b/apps/web/pages/success.tsx @@ -141,7 +141,15 @@ type SuccessProps = inferSSRProps; export default function Success(props: SuccessProps) { const { t } = useLocale(); const router = useRouter(); - const { location: _location, name, reschedule, listingStatus, status, isSuccessBookingPage } = router.query; + const { + location: _location, + name, + email, + reschedule, + listingStatus, + status, + isSuccessBookingPage, + } = router.query; const location: ReturnType = Array.isArray(_location) ? _location[0] || "" : _location || ""; @@ -357,14 +365,23 @@ export default function Success(props: SuccessProps) {

{bookingInfo.user.email}

)} - {bookingInfo?.attendees.map((attendee, index) => ( -
-

{attendee.name}

-

{attendee.email}

-
- ))} + {!!eventType.seatsShowAttendees + ? bookingInfo?.attendees + .filter((attendee) => attendee.email === email) + .map((attendee) => ( +
+

{attendee.name}

+

{attendee.email}

+
+ )) + : bookingInfo?.attendees.map((attendee, index) => ( +
+

{attendee.name}

+

{attendee.email}

+
+ ))} )} @@ -725,6 +742,7 @@ const getEventTypesFromDB = async (id: number) => { }, }, metadata: true, + seatsShowAttendees: true, }, }); diff --git a/apps/web/playwright/integrations.e2e.ts-snapshots/webhookResponse--calcom-web.txt b/apps/web/playwright/integrations.e2e.ts-snapshots/webhookResponse--calcom-web.txt index 66acf4008b..4ba223d0b0 100644 --- a/apps/web/playwright/integrations.e2e.ts-snapshots/webhookResponse--calcom-web.txt +++ b/apps/web/playwright/integrations.e2e.ts-snapshots/webhookResponse--calcom-web.txt @@ -1 +1 @@ -{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","uid":"[redacted/dynamic]","bookingId":"[redacted/dynamic]","metadata":{},"additionalInformation":"[redacted/dynamic]"}} +{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","seatsShowAttendees":false,"uid":"[redacted/dynamic]","bookingId":"[redacted/dynamic]","metadata":{},"additionalInformation":"[redacted/dynamic]"}} diff --git a/apps/web/playwright/webhook.e2e.ts-snapshots/webhookResponse--calcom-web.txt b/apps/web/playwright/webhook.e2e.ts-snapshots/webhookResponse--calcom-web.txt index 97234902d2..04144019b5 100644 --- a/apps/web/playwright/webhook.e2e.ts-snapshots/webhookResponse--calcom-web.txt +++ b/apps/web/playwright/webhook.e2e.ts-snapshots/webhookResponse--calcom-web.txt @@ -1 +1 @@ -{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","uid":"[redacted/dynamic]","eventTitle":"30 min","eventDescription":null,"price":0,"currency":"usd","length":30,"bookingId":"[redacted/dynamic]","metadata":{},"status":"ACCEPTED","additionalInformation":"[redacted/dynamic]"}} \ No newline at end of file +{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","seatsShowAttendees":false,"uid":"[redacted/dynamic]","eventTitle":"30 min","eventDescription":null,"price":0,"currency":"usd","length":30,"bookingId":"[redacted/dynamic]","metadata":{},"status":"ACCEPTED","additionalInformation":"[redacted/dynamic]"}} \ No newline at end of file diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 2556d8be0d..1474809f58 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1278,6 +1278,7 @@ "limit_booking_frequency_description":"Limit how many times this event can be booked", "add_limit":"Add Limit", "team_name_required": "Team name required", + "show_attendees": "Share attendee information between guests", "how_additional_inputs_as_variables": "How to use Additional Inputs as Variables", "format": "Format", "uppercase_for_letters": "Use uppercase for all letters", diff --git a/packages/app-store/closecomothercalendar/test/lib/CalendarService.test.ts b/packages/app-store/closecomothercalendar/test/lib/CalendarService.test.ts index 04d9781cdc..da4e316a6d 100644 --- a/packages/app-store/closecomothercalendar/test/lib/CalendarService.test.ts +++ b/packages/app-store/closecomothercalendar/test/lib/CalendarService.test.ts @@ -55,7 +55,7 @@ test("retrieve contact IDs: all exist", async () => { const event = { attendees, - } as CalendarEvent; + } as { attendees: { email: string; name: string | null; id: string }[] }; CloseCom.prototype.contact = { search: () => ({ data: attendees }), @@ -225,7 +225,7 @@ test("prepare data to create custom activity type instance: one attendees, with attendees, startTime: now.toISOString(), additionalNotes: "Some comment!", - } as CalendarEvent; + } as any; CloseCom.prototype.activity = { type: { diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index e943c9fcaf..d1ff65166c 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -82,12 +82,22 @@ export default class GoogleCalendarService implements Calendar { timeZone: calEventRaw.organizer.timeZone, }, attendees: [ - { ...calEventRaw.organizer, organizer: true, responseStatus: "accepted" }, - ...calEventRaw.attendees.map((attendee) => ({ ...attendee, responseStatus: "accepted" })), + { + ...calEventRaw.organizer, + id: String(calEventRaw.organizer.id), + organizer: true, + responseStatus: "accepted", + }, + // eslint-disable-next-line + ...calEventRaw.attendees.map(({ id, ...rest }) => ({ + rest, + responseStatus: "accepted", + })), ], reminders: { useDefault: true, }, + guestsCanSeeOtherGuests: calEventRaw.seatsShowAttendees, }; if (calEventRaw.location) { @@ -160,10 +170,23 @@ export default class GoogleCalendarService implements Calendar { dateTime: event.endTime, timeZone: event.organizer.timeZone, }, - attendees: [{ ...event.organizer, organizer: true, responseStatus: "accepted" }, ...event.attendees], + attendees: [ + { + ...event.organizer, + id: String(event.organizer.id), + organizer: true, + responseStatus: "accepted", + }, + // eslint-disable-next-line + ...event.attendees.map(({ id, ...rest }) => ({ + rest, + responseStatus: "accepted", + })), + ], reminders: { useDefault: true, }, + guestsCanSeeOtherGuests: event.seatsShowAttendees, }; if (event.location) { diff --git a/packages/emails/email-manager.ts b/packages/emails/email-manager.ts index 75e4b43844..e112d40932 100644 --- a/packages/emails/email-manager.ts +++ b/packages/emails/email-manager.ts @@ -84,17 +84,18 @@ export const sendRescheduledEmails = async (calEvent: CalendarEvent) => { export const sendScheduledSeatsEmails = async ( calEvent: CalendarEvent, invitee: Person, - newSeat: boolean + newSeat: boolean, + showAttendees: boolean ) => { const emailsToSend: Promise[] = []; emailsToSend.push( new Promise((resolve, reject) => { try { - const scheduledEmail = new AttendeeScheduledEmail(calEvent, invitee); + const scheduledEmail = new AttendeeScheduledEmail(calEvent, invitee, showAttendees); resolve(scheduledEmail.sendEmail()); } catch (e) { - reject(console.error("AttendeeRescheduledEmail.sendEmail failed", e)); + reject(console.error("AttendeeScheduledEmail.sendEmail failed", e)); } }) ); diff --git a/packages/emails/templates/attendee-scheduled-email.ts b/packages/emails/templates/attendee-scheduled-email.ts index 8a86a0393b..c0897daadf 100644 --- a/packages/emails/templates/attendee-scheduled-email.ts +++ b/packages/emails/templates/attendee-scheduled-email.ts @@ -12,14 +12,24 @@ import BaseEmail from "./_base-email"; export default class AttendeeScheduledEmail extends BaseEmail { calEvent: CalendarEvent; attendee: Person; + showAttendees: boolean | undefined; t: TFunction; - constructor(calEvent: CalendarEvent, attendee: Person) { + constructor(calEvent: CalendarEvent, attendee: Person, showAttendees?: boolean | undefined) { super(); this.name = "SEND_BOOKING_CONFIRMATION"; this.calEvent = calEvent; this.attendee = attendee; + this.showAttendees = showAttendees; this.t = attendee.language.translate; + + if (!this.showAttendees) { + this.calEvent.attendees = [ + { + ...this.attendee, + }, + ]; + } } protected getiCalEventAsString(): string | undefined { diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 4f9d594c47..51dee551c5 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -153,6 +153,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => { hideCalendarNotes: true, seatsPerTimeSlot: true, recurringEvent: true, + seatsShowAttendees: true, bookingLimits: true, workflows: { include: { @@ -442,6 +443,7 @@ async function handler(req: NextApiRequest & { userId?: number }) { hideCalendarNotes: eventType.hideCalendarNotes, requiresConfirmation: eventType.requiresConfirmation ?? false, eventTypeId: eventType.id, + seatsShowAttendees: !!eventType.seatsShowAttendees, }; // For seats, if the booking already exists then we want to add the new attendee to the existing booking @@ -502,7 +504,7 @@ async function handler(req: NextApiRequest & { userId?: number }) { const newSeat = booking.attendees.length !== 0; - await sendScheduledSeatsEmails(evt, invitee[0], newSeat); + await sendScheduledSeatsEmails(evt, invitee[0], newSeat, !!eventType.seatsShowAttendees); const credentials = await refreshCredentials(organizerUser.credentials); const eventManager = new EventManager({ ...organizerUser, credentials }); diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index 50c086fd8c..1968bc2c4c 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -76,6 +76,7 @@ const commons = { currency: "usd", schedulingType: SchedulingType.COLLECTIVE, seatsPerTimeSlot: null, + seatsShowAttendees: null, id: 0, hideCalendarNotes: false, recurringEvent: null, diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index 8678ab1712..d4daa0dc51 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -19,7 +19,7 @@ export const buildPerson = (person?: Partial): Person => { email: faker.internet.email(), timeZone: faker.address.timeZone(), username: faker.internet.userName(), - id: faker.datatype.uuid(), + id: faker.datatype.number(), language: { locale: faker.random.locale(), translate: (key: string) => key, @@ -85,6 +85,7 @@ export const buildEventType = (eventType?: Partial): EventType => { beforeEventBuffer: 0, afterEventBuffer: 0, seatsPerTimeSlot: null, + seatsShowAttendees: null, schedulingType: null, scheduleId: null, bookingLimits: null, diff --git a/packages/prisma/migrations/20220929132137_add_seats_hide_attendees/migration.sql b/packages/prisma/migrations/20220929132137_add_seats_hide_attendees/migration.sql new file mode 100644 index 0000000000..8d53957011 --- /dev/null +++ b/packages/prisma/migrations/20220929132137_add_seats_hide_attendees/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "EventType" ADD COLUMN "seatsShowAttendees" BOOLEAN; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 00b53dc320..cd96601c1c 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -68,6 +68,7 @@ model EventType { beforeEventBuffer Int @default(0) afterEventBuffer Int @default(0) seatsPerTimeSlot Int? + seatsShowAttendees Boolean? schedulingType SchedulingType? schedule Schedule? @relation(fields: [scheduleId], references: [id]) scheduleId Int? diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts index ad7aab9506..5e003f1c48 100644 --- a/packages/types/Calendar.d.ts +++ b/packages/types/Calendar.d.ts @@ -22,7 +22,9 @@ export type Person = { timeZone: string; language: { translate: TFunction; locale: string }; username?: string; - id?: string; + id?: number; + bookingId?: number; + locale?: string; }; export type EventBusyDate = { @@ -141,6 +143,7 @@ export interface CalendarEvent { recurrence?: string; recurringEvent?: RecurringEvent | null; eventTypeId?: number | null; + seatsShowAttendees?: boolean | null; } export interface EntryPoint {