feat: Disable booking emails to guests (#25217)
* feat: disable SMS org setting * chore: undo * fix: cal evnet * feat: add more settings * tests: add email manager unit tests * fix: update * fix: type error * fix: test * refactor: UI * refactor: UI * perf: fetch only once * perf: fetch only once * chore: use org * fix: test * test: add unit tests * refactor: email manager * fix: add confirmation dialog for individual checkbox * fix: sms * chore: common.json
This commit is contained in:
+4
@@ -91,6 +91,10 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
|
||||
href: "/settings/organizations/general",
|
||||
trackingMetadata: { section: "organization", page: "general" },
|
||||
},
|
||||
{
|
||||
name: "guest_notifications",
|
||||
href: "/settings/organizations/guest-notifications",
|
||||
},
|
||||
...(orgBranding
|
||||
? [
|
||||
{
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import GuestNotificationsView from "@calcom/features/ee/organizations/pages/settings/guest-notifications";
|
||||
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { validateUserHasOrg } from "../../actions/validateUserHasOrg";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("guest_notifications"),
|
||||
(t) => t("guest_notifications_description"),
|
||||
undefined,
|
||||
undefined,
|
||||
"/settings/organizations/guest-notifications"
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
const session = await validateUserHasOrg();
|
||||
const t = await getTranslate();
|
||||
|
||||
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
|
||||
return redirect("/settings/profile");
|
||||
}
|
||||
|
||||
const { canRead, canEdit } = await getResourcePermissions({
|
||||
userId: session.user.id,
|
||||
teamId: session.user.profile.organizationId,
|
||||
resource: Resource.Organization,
|
||||
userRole: session.user.org.role,
|
||||
fallbackRoles: {
|
||||
read: {
|
||||
roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
update: {
|
||||
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!canRead) {
|
||||
return redirect("/settings/profile");
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsHeader title={t("guest_notifications")} description={t("guest_notifications_description")}>
|
||||
<GuestNotificationsView permissions={{ canRead, canEdit }} />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -308,6 +308,8 @@
|
||||
"guest": "Guest",
|
||||
"web_conferencing_details_to_follow": "Web conferencing details to follow in the confirmation email.",
|
||||
"confirmation": "Confirmation",
|
||||
"cancellation": "Cancellation",
|
||||
"request": "Request",
|
||||
"what_booker_should_provide": "What your booker should provide to receive confirmations",
|
||||
"404_the_user": "The username",
|
||||
"username": "Username",
|
||||
@@ -3932,6 +3934,37 @@
|
||||
"no_description_provided": "No description provided",
|
||||
"organization_blocklist": "Organization Blocklist",
|
||||
"manage_blocked_emails_and_domains": "Manage blocked emails and domains for your organization",
|
||||
"guest_notifications": "Guest Notifications",
|
||||
"guest_notifications_description": "Manage guest booking emails across all event types in your organization",
|
||||
"guest_booking_email_notifications": "Guest Booking Email Notifications",
|
||||
"guest_booking_email_notifications_description": "Manage which booking emails are sent to guests across all event types in your organization",
|
||||
"disable_guest_emails_warning": "These settings control important booking emails to keep your guests updated. They apply to all event types in your organization. If you disable emails, we recommend setting up org-wide workflows instead.",
|
||||
"disable_all_booking_emails_to_guests": "Disable all booking emails to guests",
|
||||
"disable_all": "Disable All",
|
||||
"enable_all": "Enable All",
|
||||
"disable_all_booking_emails_to_guests_description": "When enabled, guests will not receive any booking-related emails. This does not affect Workflow emails.",
|
||||
"disable_all_guest_booking_emails_confirm_title": "Are you sure you want to disable all booking emails?",
|
||||
"disable_all_guest_booking_emails_confirm_description": "This will prevent guests from receiving important booking updates across all event types in your organization. We recommend setting up custom org-wide workflow.",
|
||||
"enable_all_guest_booking_emails_confirm_title": "Are you sure you want to enable all booking emails?",
|
||||
"enable_all_guest_booking_emails_confirm_description": "This will allow guests to receive all booking-related email notifications across all event types in your organization.",
|
||||
"disable_individual_guest_email_confirm_title": "Are you sure you want to disable {{emailType}} emails?",
|
||||
"disable_individual_guest_email_confirm_description": "This will prevent guests from receiving {{emailType}} emails across all event types in your organization. We recommend setting up a custom org-wide Workflow for this.",
|
||||
"email_type": "Email Type",
|
||||
"guest_confirmation_email_description": "Email sent when booking is created",
|
||||
"guest_cancellation_email_description": "Email sent when booking is cancelled",
|
||||
"guest_rescheduled_email_description": "Email sent when booking is rescheduled",
|
||||
"guest_request_email_description": "Email sent when booking is requested",
|
||||
"attendee_request_email_description": "Email sent when booking is requested or declined",
|
||||
"attendee_reassigned_email_description": "Email sent when host is reassigned for the booking",
|
||||
"attendee_awaiting_payment_email_description": "Email sent when payment is pending",
|
||||
"attendee_reschedule_request_email_description": "Email sent when organizer requests attendee to reschedule",
|
||||
"attendee_location_change_email_description": "Email sent when meeting location is updated",
|
||||
"attendee_new_event_email_description": "Email sent to existing attendees when new guests are added",
|
||||
"host_reassignment": "Host Reassignment",
|
||||
"awaiting_payment": "Awaiting Payment",
|
||||
"reschedule_request": "Reschedule Request",
|
||||
"location_change": "Location Change",
|
||||
"guest_added": "Guest Added",
|
||||
"invalid_domain_format": "Invalid domain format. Example: example.com",
|
||||
"invalid_email_address": "Invalid email address. Example: user@example.com",
|
||||
"reason_for_adding_to_blocklist": "Reason for adding to blocklist",
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
|
||||
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { shouldSkipAttendeeEmailWithSettings, fetchOrganizationEmailSettings } from "./email-manager";
|
||||
|
||||
const mockGetEmailSettings = vi.fn();
|
||||
|
||||
vi.mock("@calcom/features/organizations/repositories/OrganizationSettingsRepository", () => ({
|
||||
OrganizationSettingsRepository: vi.fn().mockImplementation(() => ({
|
||||
getEmailSettings: mockGetEmailSettings,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/prisma", () => ({
|
||||
prisma: {},
|
||||
}));
|
||||
|
||||
describe("shouldSkipAttendeeEmailWithSettings", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
["confirmation", "disableAttendeeConfirmationEmail"],
|
||||
["cancellation", "disableAttendeeCancellationEmail"],
|
||||
["rescheduled", "disableAttendeeRescheduledEmail"],
|
||||
["request", "disableAttendeeRequestEmail"],
|
||||
["reassigned", "disableAttendeeReassignedEmail"],
|
||||
["awaiting_payment", "disableAttendeeAwaitingPaymentEmail"],
|
||||
["reschedule_request", "disableAttendeeRescheduleRequestEmail"],
|
||||
["location_change", "disableAttendeeLocationChangeEmail"],
|
||||
["new_event", "disableAttendeeNewEventEmail"],
|
||||
] as const)("Email type: %s", (emailType, settingKey) => {
|
||||
it(`should skip email when organization has ${settingKey} enabled`, async () => {
|
||||
const orgSettings = {
|
||||
disableAttendeeConfirmationEmail: settingKey === "disableAttendeeConfirmationEmail",
|
||||
disableAttendeeCancellationEmail: settingKey === "disableAttendeeCancellationEmail",
|
||||
disableAttendeeRescheduledEmail: settingKey === "disableAttendeeRescheduledEmail",
|
||||
disableAttendeeRequestEmail: settingKey === "disableAttendeeRequestEmail",
|
||||
disableAttendeeReassignedEmail: settingKey === "disableAttendeeReassignedEmail",
|
||||
disableAttendeeAwaitingPaymentEmail: settingKey === "disableAttendeeAwaitingPaymentEmail",
|
||||
disableAttendeeRescheduleRequestEmail: settingKey === "disableAttendeeRescheduleRequestEmail",
|
||||
disableAttendeeLocationChangeEmail: settingKey === "disableAttendeeLocationChangeEmail",
|
||||
disableAttendeeNewEventEmail: settingKey === "disableAttendeeNewEventEmail",
|
||||
};
|
||||
|
||||
const result = shouldSkipAttendeeEmailWithSettings(undefined, orgSettings, emailType);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it(`should send email when organization has ${settingKey} disabled`, async () => {
|
||||
const orgSettings = {
|
||||
disableAttendeeConfirmationEmail: false,
|
||||
disableAttendeeCancellationEmail: false,
|
||||
disableAttendeeRescheduledEmail: false,
|
||||
disableAttendeeRequestEmail: false,
|
||||
disableAttendeeReassignedEmail: false,
|
||||
disableAttendeeAwaitingPaymentEmail: false,
|
||||
disableAttendeeRescheduleRequestEmail: false,
|
||||
disableAttendeeLocationChangeEmail: false,
|
||||
disableAttendeeNewEventEmail: false,
|
||||
};
|
||||
|
||||
const result = shouldSkipAttendeeEmailWithSettings(undefined, orgSettings, emailType);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Metadata fallback", () => {
|
||||
it("should skip email when metadata has disableStandardEmails.all.attendee enabled", () => {
|
||||
const metadata: EventTypeMetadata = {
|
||||
disableStandardEmails: {
|
||||
all: {
|
||||
attendee: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = shouldSkipAttendeeEmailWithSettings(metadata, null, "confirmation");
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Priority: organization settings override metadata", () => {
|
||||
it("should skip email when org setting is enabled even if metadata allows", () => {
|
||||
const orgSettings = {
|
||||
disableAttendeeConfirmationEmail: true,
|
||||
disableAttendeeCancellationEmail: false,
|
||||
disableAttendeeRescheduledEmail: false,
|
||||
disableAttendeeRequestEmail: false,
|
||||
disableAttendeeReassignedEmail: false,
|
||||
disableAttendeeAwaitingPaymentEmail: false,
|
||||
disableAttendeeRescheduleRequestEmail: false,
|
||||
disableAttendeeLocationChangeEmail: false,
|
||||
disableAttendeeNewEventEmail: false,
|
||||
};
|
||||
|
||||
const metadata: EventTypeMetadata = {
|
||||
disableStandardEmails: {
|
||||
confirmation: {
|
||||
attendee: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = shouldSkipAttendeeEmailWithSettings(metadata, orgSettings, "confirmation");
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge cases", () => {
|
||||
it("should send email when organizationSettings is null", () => {
|
||||
const result = shouldSkipAttendeeEmailWithSettings(undefined, null, "confirmation");
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should send email when emailType is undefined", () => {
|
||||
const orgSettings = {
|
||||
disableAttendeeConfirmationEmail: true,
|
||||
disableAttendeeCancellationEmail: false,
|
||||
disableAttendeeRescheduledEmail: false,
|
||||
disableAttendeeRequestEmail: false,
|
||||
disableAttendeeReassignedEmail: false,
|
||||
disableAttendeeAwaitingPaymentEmail: false,
|
||||
disableAttendeeRescheduleRequestEmail: false,
|
||||
disableAttendeeLocationChangeEmail: false,
|
||||
disableAttendeeNewEventEmail: false,
|
||||
};
|
||||
|
||||
const result = shouldSkipAttendeeEmailWithSettings(undefined, orgSettings, undefined);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should send email when metadata is undefined and org settings are disabled", () => {
|
||||
const orgSettings = {
|
||||
disableAttendeeConfirmationEmail: false,
|
||||
disableAttendeeCancellationEmail: false,
|
||||
disableAttendeeRescheduledEmail: false,
|
||||
disableAttendeeRequestEmail: false,
|
||||
disableAttendeeReassignedEmail: false,
|
||||
disableAttendeeAwaitingPaymentEmail: false,
|
||||
disableAttendeeRescheduleRequestEmail: false,
|
||||
disableAttendeeLocationChangeEmail: false,
|
||||
disableAttendeeNewEventEmail: false,
|
||||
};
|
||||
|
||||
const result = shouldSkipAttendeeEmailWithSettings(undefined, orgSettings, "confirmation");
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,10 +5,12 @@ import dayjs from "@calcom/dayjs";
|
||||
import type BaseEmail from "@calcom/emails/templates/_base-email";
|
||||
import type { EventNameObjectType } from "@calcom/features/eventtypes/lib/eventNaming";
|
||||
import { getEventName } from "@calcom/features/eventtypes/lib/eventNaming";
|
||||
import { OrganizationSettingsRepository } from "@calcom/features/organizations/repositories/OrganizationSettingsRepository";
|
||||
import { formatCalEvent } from "@calcom/lib/formatCalendarEvent";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { withReporting } from "@calcom/lib/sentryWrapper";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
@@ -21,6 +23,7 @@ import EventRequestSMS from "../sms/attendee/event-request-sms";
|
||||
import EventRequestToRescheduleSMS from "../sms/attendee/event-request-to-reschedule-sms";
|
||||
import EventSuccessfullyReScheduledSMS from "../sms/attendee/event-rescheduled-sms";
|
||||
import EventSuccessfullyScheduledSMS from "../sms/attendee/event-scheduled-sms";
|
||||
import { EmailType } from "./email-types";
|
||||
import AttendeeAddGuestsEmail from "./templates/attendee-add-guests-email";
|
||||
import AttendeeAwaitingPaymentEmail from "./templates/attendee-awaiting-payment-email";
|
||||
import AttendeeCancelledEmail from "./templates/attendee-cancelled-email";
|
||||
@@ -56,7 +59,48 @@ const sendEmail = (prepare: () => BaseEmail) => {
|
||||
});
|
||||
};
|
||||
|
||||
const eventTypeDisableAttendeeEmail = (metadata?: EventTypeMetadata) => {
|
||||
export const fetchOrganizationEmailSettings = async (organizationId?: number | null | undefined) => {
|
||||
if (!organizationId) return null;
|
||||
const repo = new OrganizationSettingsRepository(prisma);
|
||||
return await repo.getEmailSettings(organizationId);
|
||||
};
|
||||
|
||||
export const shouldSkipAttendeeEmailWithSettings = (
|
||||
metadata: EventTypeMetadata | undefined,
|
||||
organizationSettings: Awaited<ReturnType<typeof fetchOrganizationEmailSettings>>,
|
||||
emailType?: EmailType
|
||||
): boolean => {
|
||||
if (organizationSettings && emailType) {
|
||||
switch (emailType) {
|
||||
case EmailType.CONFIRMATION:
|
||||
if (organizationSettings.disableAttendeeConfirmationEmail) return true;
|
||||
break;
|
||||
case EmailType.CANCELLATION:
|
||||
if (organizationSettings.disableAttendeeCancellationEmail) return true;
|
||||
break;
|
||||
case EmailType.RESCHEDULED:
|
||||
if (organizationSettings.disableAttendeeRescheduledEmail) return true;
|
||||
break;
|
||||
case EmailType.REQUEST:
|
||||
if (organizationSettings.disableAttendeeRequestEmail) return true;
|
||||
break;
|
||||
case EmailType.REASSIGNED:
|
||||
if (organizationSettings.disableAttendeeReassignedEmail) return true;
|
||||
break;
|
||||
case EmailType.AWAITING_PAYMENT:
|
||||
if (organizationSettings.disableAttendeeAwaitingPaymentEmail) return true;
|
||||
break;
|
||||
case EmailType.RESCHEDULE_REQUEST:
|
||||
if (organizationSettings.disableAttendeeRescheduleRequestEmail) return true;
|
||||
break;
|
||||
case EmailType.LOCATION_CHANGE:
|
||||
if (organizationSettings.disableAttendeeLocationChangeEmail) return true;
|
||||
break;
|
||||
case EmailType.NEW_EVENT:
|
||||
if (organizationSettings.disableAttendeeNewEventEmail) return true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return !!metadata?.disableStandardEmails?.all?.attendee;
|
||||
};
|
||||
|
||||
@@ -73,6 +117,7 @@ const _sendScheduledEmailsAndSMS = async (
|
||||
) => {
|
||||
const formattedCalEvent = formatCalEvent(calEvent);
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
const organizationSettings = await fetchOrganizationEmailSettings(calEvent.organizationId);
|
||||
|
||||
if (!hostEmailDisabled && !eventTypeDisableHostEmail(eventTypeMetadata)) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent: formattedCalEvent })));
|
||||
@@ -86,7 +131,10 @@ const _sendScheduledEmailsAndSMS = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (!attendeeEmailDisabled && !eventTypeDisableAttendeeEmail(eventTypeMetadata)) {
|
||||
if (
|
||||
!attendeeEmailDisabled &&
|
||||
!shouldSkipAttendeeEmailWithSettings(eventTypeMetadata, organizationSettings, EmailType.CONFIRMATION)
|
||||
) {
|
||||
emailsToSend.push(
|
||||
...formattedCalEvent.attendees.map((attendee) => {
|
||||
return sendEmail(
|
||||
@@ -154,13 +202,16 @@ export const sendRoundRobinRescheduledEmailsAndSMS = async (
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
const emailsAndSMSToSend: Promise<unknown>[] = [];
|
||||
const successfullyReScheduledSMS = new EventSuccessfullyReScheduledSMS(calEvent);
|
||||
const organizationSettings = await fetchOrganizationEmailSettings(calEvent.organizationId);
|
||||
|
||||
for (const person of teamMembersAndAttendees) {
|
||||
const isAttendee = calendarEvent.attendees.some((attendee) => attendee.email === person.email);
|
||||
const isTeamMember = !!calendarEvent.team?.members.some((member) => member.email === person.email);
|
||||
|
||||
if (isAttendee && !isTeamMember) {
|
||||
if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) {
|
||||
if (
|
||||
!shouldSkipAttendeeEmailWithSettings(eventTypeMetadata, organizationSettings, EmailType.RESCHEDULED)
|
||||
) {
|
||||
emailsAndSMSToSend.push(sendEmail(() => new AttendeeRescheduledEmail(calendarEvent, person)));
|
||||
if (person.phoneNumber) {
|
||||
emailsAndSMSToSend.push(successfullyReScheduledSMS.sendSMSToAttendee(person));
|
||||
@@ -186,7 +237,9 @@ export const sendReassignedUpdatedEmailsAndSMS = async ({
|
||||
calEvent: CalendarEvent;
|
||||
eventTypeMetadata?: EventTypeMetadata;
|
||||
}) => {
|
||||
if (eventTypeDisableAttendeeEmail(eventTypeMetadata)) return;
|
||||
const organizationSettings = await fetchOrganizationEmailSettings(calEvent.organizationId);
|
||||
if (shouldSkipAttendeeEmailWithSettings(eventTypeMetadata, organizationSettings, EmailType.REASSIGNED))
|
||||
return;
|
||||
|
||||
const emailsToSend = calEvent.attendees.map((attendee) =>
|
||||
sendEmail(() => new AttendeeUpdatedEmail(calEvent, attendee))
|
||||
@@ -252,6 +305,7 @@ const _sendRescheduledEmailsAndSMS = async (
|
||||
) => {
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
const organizationSettings = await fetchOrganizationEmailSettings(calEvent.organizationId);
|
||||
|
||||
if (!eventTypeDisableHostEmail(eventTypeMetadata)) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRescheduledEmail({ calEvent: calendarEvent })));
|
||||
@@ -265,7 +319,7 @@ const _sendRescheduledEmailsAndSMS = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) {
|
||||
if (!shouldSkipAttendeeEmailWithSettings(eventTypeMetadata, organizationSettings, EmailType.RESCHEDULED)) {
|
||||
emailsToSend.push(
|
||||
...calendarEvent.attendees.map((attendee) => {
|
||||
return sendEmail(() => new AttendeeRescheduledEmail(calendarEvent, attendee));
|
||||
@@ -291,10 +345,11 @@ export const sendRescheduledSeatEmailAndSMS = async (
|
||||
|
||||
const clonedCalEvent = cloneDeep(calendarEvent);
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
const organizationSettings = await fetchOrganizationEmailSettings(calEvent.organizationId);
|
||||
|
||||
if (!eventTypeDisableHostEmail(eventTypeMetadata))
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRescheduledEmail({ calEvent: calendarEvent })));
|
||||
if (!eventTypeDisableAttendeeEmail(eventTypeMetadata))
|
||||
if (!shouldSkipAttendeeEmailWithSettings(eventTypeMetadata, organizationSettings, EmailType.RESCHEDULED))
|
||||
emailsToSend.push(sendEmail(() => new AttendeeRescheduledEmail(clonedCalEvent, attendee)));
|
||||
|
||||
const successfullyReScheduledSMS = new EventSuccessfullyReScheduledSMS(calEvent);
|
||||
@@ -315,6 +370,7 @@ export const sendScheduledSeatsEmailsAndSMS = async (
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
const organizationSettings = await fetchOrganizationEmailSettings(calEvent.organizationId);
|
||||
|
||||
if (!hostEmailDisabled && !eventTypeDisableHostEmail(eventTypeMetadata)) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent: calendarEvent, newSeat })));
|
||||
@@ -328,7 +384,10 @@ export const sendScheduledSeatsEmailsAndSMS = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (!attendeeEmailDisabled && !eventTypeDisableAttendeeEmail(eventTypeMetadata)) {
|
||||
if (
|
||||
!attendeeEmailDisabled &&
|
||||
!shouldSkipAttendeeEmailWithSettings(eventTypeMetadata, organizationSettings, EmailType.CONFIRMATION)
|
||||
) {
|
||||
emailsToSend.push(
|
||||
sendEmail(
|
||||
() =>
|
||||
@@ -356,8 +415,9 @@ export const sendCancelledSeatEmailsAndSMS = async (
|
||||
const formattedCalEvent = formatCalEvent(calEvent);
|
||||
const clonedCalEvent = cloneDeep(formattedCalEvent);
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
const organizationSettings = await fetchOrganizationEmailSettings(calEvent.organizationId);
|
||||
|
||||
if (!eventTypeDisableAttendeeEmail(eventTypeMetadata))
|
||||
if (!shouldSkipAttendeeEmailWithSettings(eventTypeMetadata, organizationSettings, EmailType.CANCELLATION))
|
||||
emailsToSend.push(sendEmail(() => new AttendeeCancelledSeatEmail(clonedCalEvent, cancelledAttendee)));
|
||||
if (!eventTypeDisableHostEmail(eventTypeMetadata))
|
||||
emailsToSend.push(
|
||||
@@ -402,7 +462,8 @@ const _sendAttendeeRequestEmailAndSMS = async (
|
||||
attendee: Person,
|
||||
eventTypeMetadata?: EventTypeMetadata
|
||||
) => {
|
||||
if (eventTypeDisableAttendeeEmail(eventTypeMetadata)) return;
|
||||
const organizationSettings = await fetchOrganizationEmailSettings(calEvent.organizationId);
|
||||
if (shouldSkipAttendeeEmailWithSettings(eventTypeMetadata, organizationSettings, EmailType.REQUEST)) return;
|
||||
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
await sendEmail(() => new AttendeeRequestEmail(calendarEvent, attendee));
|
||||
@@ -419,7 +480,8 @@ export const sendDeclinedEmailsAndSMS = async (
|
||||
calEvent: CalendarEvent,
|
||||
eventTypeMetadata?: EventTypeMetadata
|
||||
) => {
|
||||
if (eventTypeDisableAttendeeEmail(eventTypeMetadata)) return;
|
||||
const organizationSettings = await fetchOrganizationEmailSettings(calEvent.organizationId);
|
||||
if (shouldSkipAttendeeEmailWithSettings(eventTypeMetadata, organizationSettings, EmailType.REQUEST)) return;
|
||||
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
@@ -444,6 +506,7 @@ export const sendCancelledEmailsAndSMS = async (
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
const calEventLength = calendarEvent.length;
|
||||
const eventDuration = dayjs(calEvent.endTime).diff(calEvent.startTime, "minutes");
|
||||
const organizationSettings = await fetchOrganizationEmailSettings(calEvent.organizationId);
|
||||
|
||||
if (typeof calEventLength !== "number") {
|
||||
logger.error(
|
||||
@@ -464,7 +527,7 @@ export const sendCancelledEmailsAndSMS = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) {
|
||||
if (!shouldSkipAttendeeEmailWithSettings(eventTypeMetadata, organizationSettings, EmailType.CANCELLATION)) {
|
||||
emailsToSend.push(
|
||||
...calendarEvent.attendees.map((attendee) => {
|
||||
return sendEmail(
|
||||
@@ -519,20 +582,24 @@ export const sendAwaitingPaymentEmailAndSMS = async (
|
||||
calEvent: CalendarEvent,
|
||||
eventTypeMetadata?: EventTypeMetadata
|
||||
) => {
|
||||
if (eventTypeDisableAttendeeEmail(eventTypeMetadata)) return;
|
||||
const organizationSettings = await fetchOrganizationEmailSettings(calEvent.organizationId);
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
emailsToSend.push(
|
||||
...calEvent.attendees.map((attendee) => {
|
||||
return sendEmail(() => new AttendeeAwaitingPaymentEmail(calEvent, attendee));
|
||||
})
|
||||
);
|
||||
if (
|
||||
!shouldSkipAttendeeEmailWithSettings(eventTypeMetadata, organizationSettings, EmailType.AWAITING_PAYMENT)
|
||||
) {
|
||||
emailsToSend.push(
|
||||
...calEvent.attendees.map((attendee) => {
|
||||
return sendEmail(() => new AttendeeAwaitingPaymentEmail(calEvent, attendee));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(emailsToSend);
|
||||
const awaitingPaymentSMS = new AwaitingPaymentSMS(calEvent);
|
||||
await awaitingPaymentSMS.sendSMSToAttendees();
|
||||
};
|
||||
|
||||
|
||||
export const sendRequestRescheduleEmailAndSMS = async (
|
||||
calEvent: CalendarEvent,
|
||||
metadata: { rescheduleLink: string },
|
||||
@@ -540,11 +607,18 @@ export const sendRequestRescheduleEmailAndSMS = async (
|
||||
) => {
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
const organizationSettings = await fetchOrganizationEmailSettings(calEvent.organizationId);
|
||||
|
||||
if (!eventTypeDisableHostEmail(eventTypeMetadata)) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerRequestedToRescheduleEmail(calendarEvent, metadata)));
|
||||
}
|
||||
if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) {
|
||||
if (
|
||||
!shouldSkipAttendeeEmailWithSettings(
|
||||
eventTypeMetadata,
|
||||
organizationSettings,
|
||||
EmailType.RESCHEDULE_REQUEST
|
||||
)
|
||||
) {
|
||||
emailsToSend.push(sendEmail(() => new AttendeeWasRequestedToRescheduleEmail(calendarEvent, metadata)));
|
||||
}
|
||||
|
||||
@@ -560,6 +634,7 @@ export const sendLocationChangeEmailsAndSMS = async (
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
const organizationSettings = await fetchOrganizationEmailSettings(calEvent.organizationId);
|
||||
|
||||
if (!eventTypeDisableHostEmail(eventTypeMetadata)) {
|
||||
emailsToSend.push(sendEmail(() => new OrganizerLocationChangeEmail({ calEvent: calendarEvent })));
|
||||
@@ -573,7 +648,9 @@ export const sendLocationChangeEmailsAndSMS = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) {
|
||||
if (
|
||||
!shouldSkipAttendeeEmailWithSettings(eventTypeMetadata, organizationSettings, EmailType.LOCATION_CHANGE)
|
||||
) {
|
||||
emailsToSend.push(
|
||||
...calendarEvent.attendees.map((attendee) => {
|
||||
return sendEmail(() => new AttendeeLocationChangeEmail(calendarEvent, attendee));
|
||||
@@ -634,17 +711,25 @@ export const sendAddGuestsEmailsAndSMS = async (args: {
|
||||
}
|
||||
}
|
||||
|
||||
if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) {
|
||||
const eventScheduledSMS = new EventSuccessfullyScheduledSMS(calEvent);
|
||||
const eventScheduledSMS = new EventSuccessfullyScheduledSMS(calEvent);
|
||||
const organizationSettings = await fetchOrganizationEmailSettings(calEvent.organizationId);
|
||||
|
||||
for (const attendee of calendarEvent.attendees) {
|
||||
if (newGuests.includes(attendee.email)) {
|
||||
for (const attendee of calendarEvent.attendees) {
|
||||
if (newGuests.includes(attendee.email)) {
|
||||
// New guests get confirmation emails
|
||||
if (
|
||||
!shouldSkipAttendeeEmailWithSettings(eventTypeMetadata, organizationSettings, EmailType.CONFIRMATION)
|
||||
) {
|
||||
emailsAndSMSToSend.push(sendEmail(() => new AttendeeScheduledEmail(calendarEvent, attendee)));
|
||||
|
||||
if (attendee.phoneNumber) {
|
||||
emailsAndSMSToSend.push(eventScheduledSMS.sendSMSToAttendee(attendee));
|
||||
}
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!shouldSkipAttendeeEmailWithSettings(eventTypeMetadata, organizationSettings, EmailType.NEW_EVENT)
|
||||
) {
|
||||
emailsAndSMSToSend.push(sendEmail(() => new AttendeeAddGuestsEmail(calendarEvent, attendee)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export enum EmailType {
|
||||
CONFIRMATION = "confirmation",
|
||||
CANCELLATION = "cancellation",
|
||||
RESCHEDULED = "rescheduled",
|
||||
REQUEST = "request",
|
||||
REASSIGNED = "reassigned",
|
||||
AWAITING_PAYMENT = "awaiting_payment",
|
||||
RESCHEDULE_REQUEST = "reschedule_request",
|
||||
LOCATION_CHANGE = "location_change",
|
||||
NEW_EVENT = "new_event",
|
||||
}
|
||||
@@ -184,7 +184,8 @@ export class CalendarEventBuilder {
|
||||
})
|
||||
.withRecurring(recurring)
|
||||
.withUid(uid)
|
||||
.withOneTimePassword(oneTimePassword);
|
||||
.withOneTimePassword(oneTimePassword)
|
||||
.withOrganization(organizationId);
|
||||
|
||||
// Seats
|
||||
if (seatsReferences?.length && bookingResponses) {
|
||||
@@ -499,6 +500,14 @@ export class CalendarEventBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
withOrganization(organizationId?: number | null) {
|
||||
this.event = {
|
||||
...this.event,
|
||||
organizationId,
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
withHashedLink(hashedLink?: string | null) {
|
||||
this.event = {
|
||||
...this.event,
|
||||
|
||||
@@ -23,6 +23,11 @@ export async function getBookingToDelete(id: number | undefined, uid: string | u
|
||||
name: true,
|
||||
destinationCalendar: true,
|
||||
locale: true,
|
||||
profiles: {
|
||||
select: {
|
||||
organizationId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
location: true,
|
||||
|
||||
@@ -313,6 +313,7 @@ async function handler(input: CancelBookingInput) {
|
||||
hideOrganizerEmail: bookingToDelete.eventType?.hideOrganizerEmail,
|
||||
platformBookingUrl,
|
||||
customReplyToEmail: bookingToDelete.eventType?.customReplyToEmail,
|
||||
organizationId: ownerProfile?.organizationId ?? null,
|
||||
};
|
||||
|
||||
const dataForWebhooks = { evt, webhooks, eventTypeInfo };
|
||||
|
||||
@@ -31,6 +31,9 @@ export const handleNoShowFee = async ({
|
||||
name?: string | null;
|
||||
locale: string | null;
|
||||
timeZone: string;
|
||||
profiles: {
|
||||
organizationId: number | null;
|
||||
}[];
|
||||
} | null;
|
||||
eventType: {
|
||||
title: string;
|
||||
@@ -97,6 +100,7 @@ export const handleNoShowFee = async ({
|
||||
currency: payment.currency,
|
||||
paymentOption: payment.paymentOption,
|
||||
},
|
||||
organizationId: booking.user?.profiles?.[0]?.organizationId ?? null,
|
||||
};
|
||||
|
||||
if (teamId) {
|
||||
|
||||
@@ -1394,6 +1394,7 @@ async function handler(
|
||||
platformCancelUrl,
|
||||
platformBookingUrl,
|
||||
})
|
||||
.withOrganization(organizerOrganizationId)
|
||||
.withHashedLink(hasHashedBookingLink ? reqBody.hashedLink ?? null : null)
|
||||
.build();
|
||||
|
||||
|
||||
@@ -1383,6 +1383,19 @@ export class BookingRepository {
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
timeZone: true,
|
||||
locale: true,
|
||||
profiles: {
|
||||
select: {
|
||||
organizationId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
payment: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -1425,6 +1438,11 @@ export class BookingRepository {
|
||||
include: {
|
||||
destinationCalendar: true,
|
||||
credentials: true,
|
||||
profiles: {
|
||||
select: {
|
||||
organizationId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -236,6 +236,11 @@ const handleDeleteCredential = async ({
|
||||
name: true,
|
||||
destinationCalendar: true,
|
||||
locale: true,
|
||||
profiles: {
|
||||
select: {
|
||||
organizationId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
location: true,
|
||||
@@ -357,6 +362,7 @@ const handleDeleteCredential = async ({
|
||||
members: [],
|
||||
}
|
||||
: undefined,
|
||||
organizationId: booking.user?.profiles?.[0]?.organizationId ?? null,
|
||||
},
|
||||
{
|
||||
eventName: booking?.eventType?.eventName,
|
||||
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { EmailType } from "@calcom/emails/email-types";
|
||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Alert } from "@calcom/ui/components/alert";
|
||||
import { ConfirmationDialogContent } from "@calcom/ui/components/dialog";
|
||||
import { Checkbox, SettingsToggle } from "@calcom/ui/components/form";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
const EMAIL_TYPE_TO_SETTING_KEY = {
|
||||
[EmailType.CONFIRMATION]: "disableAttendeeConfirmationEmail",
|
||||
[EmailType.CANCELLATION]: "disableAttendeeCancellationEmail",
|
||||
[EmailType.RESCHEDULED]: "disableAttendeeRescheduledEmail",
|
||||
[EmailType.REQUEST]: "disableAttendeeRequestEmail",
|
||||
[EmailType.REASSIGNED]: "disableAttendeeReassignedEmail",
|
||||
[EmailType.AWAITING_PAYMENT]: "disableAttendeeAwaitingPaymentEmail",
|
||||
[EmailType.RESCHEDULE_REQUEST]: "disableAttendeeRescheduleRequestEmail",
|
||||
[EmailType.LOCATION_CHANGE]: "disableAttendeeLocationChangeEmail",
|
||||
[EmailType.NEW_EVENT]: "disableAttendeeNewEventEmail",
|
||||
} as const;
|
||||
|
||||
type EmailSettings = Record<EmailType, boolean>;
|
||||
|
||||
interface EmailRowProps {
|
||||
emailType: EmailType;
|
||||
labelKey: string;
|
||||
descriptionKey: string;
|
||||
isDisabled: boolean;
|
||||
onToggle: (type: EmailType, disabled: boolean) => void;
|
||||
testId: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const EmailRow = ({
|
||||
emailType,
|
||||
labelKey,
|
||||
descriptionKey,
|
||||
isDisabled,
|
||||
onToggle,
|
||||
testId,
|
||||
disabled,
|
||||
}: EmailRowProps) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className="text-default px-6 py-4 text-sm font-medium">{t(labelKey)}</td>
|
||||
<td className="text-default px-6 py-4 text-sm">{t(descriptionKey)}</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<Checkbox
|
||||
checked={!isDisabled}
|
||||
onCheckedChange={(checked) => onToggle(emailType, !checked)}
|
||||
data-testid={testId}
|
||||
disabled={disabled}
|
||||
aria-label={t(labelKey)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
interface IDisableGuestBookingEmailsSettingProps {
|
||||
orgId: number;
|
||||
settings: {
|
||||
disableAttendeeConfirmationEmail: boolean;
|
||||
disableAttendeeCancellationEmail: boolean;
|
||||
disableAttendeeRescheduledEmail: boolean;
|
||||
disableAttendeeRequestEmail: boolean;
|
||||
disableAttendeeReassignedEmail: boolean;
|
||||
disableAttendeeAwaitingPaymentEmail: boolean;
|
||||
disableAttendeeRescheduleRequestEmail: boolean;
|
||||
disableAttendeeLocationChangeEmail: boolean;
|
||||
disableAttendeeNewEventEmail: boolean;
|
||||
};
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
const EMAIL_TYPE_LABELS: Record<EmailType, string> = {
|
||||
[EmailType.CONFIRMATION]: "confirmation",
|
||||
[EmailType.CANCELLATION]: "cancellation",
|
||||
[EmailType.RESCHEDULED]: "rescheduled",
|
||||
[EmailType.REQUEST]: "request",
|
||||
[EmailType.REASSIGNED]: "host_reassignment",
|
||||
[EmailType.AWAITING_PAYMENT]: "awaiting_payment",
|
||||
[EmailType.RESCHEDULE_REQUEST]: "reschedule_request",
|
||||
[EmailType.LOCATION_CHANGE]: "location_change",
|
||||
[EmailType.NEW_EVENT]: "guest_added",
|
||||
};
|
||||
|
||||
const DisableGuestBookingEmailsSetting = (props: IDisableGuestBookingEmailsSettingProps) => {
|
||||
const { readOnly = false } = props;
|
||||
const utils = trpc.useUtils();
|
||||
const { t } = useLocale();
|
||||
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
const [dialogAction, setDialogAction] = useState<"enable" | "disable">("disable");
|
||||
const [dialogMode, setDialogMode] = useState<"all" | "individual">("all");
|
||||
const [pendingEmailType, setPendingEmailType] = useState<EmailType | null>(null);
|
||||
|
||||
const [emailSettings, setEmailSettings] = useState<EmailSettings>({
|
||||
[EmailType.CONFIRMATION]: props.settings.disableAttendeeConfirmationEmail,
|
||||
[EmailType.CANCELLATION]: props.settings.disableAttendeeCancellationEmail,
|
||||
[EmailType.RESCHEDULED]: props.settings.disableAttendeeRescheduledEmail,
|
||||
[EmailType.REQUEST]: props.settings.disableAttendeeRequestEmail,
|
||||
[EmailType.REASSIGNED]: props.settings.disableAttendeeReassignedEmail,
|
||||
[EmailType.AWAITING_PAYMENT]: props.settings.disableAttendeeAwaitingPaymentEmail,
|
||||
[EmailType.RESCHEDULE_REQUEST]: props.settings.disableAttendeeRescheduleRequestEmail,
|
||||
[EmailType.LOCATION_CHANGE]: props.settings.disableAttendeeLocationChangeEmail,
|
||||
[EmailType.NEW_EVENT]: props.settings.disableAttendeeNewEventEmail,
|
||||
});
|
||||
|
||||
const allDisabled = Object.values(emailSettings).every(Boolean);
|
||||
|
||||
const mutation = trpc.viewer.organizations.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
showToast(t("your_org_updated_successfully"), "success");
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
},
|
||||
onSettled: () => {
|
||||
utils.viewer.organizations.listCurrent.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const handleDisableAll = (disable: boolean) => {
|
||||
const apiPayload = Object.fromEntries(
|
||||
Object.values(EMAIL_TYPE_TO_SETTING_KEY).map((key) => [key, disable])
|
||||
);
|
||||
mutation.mutate(apiPayload);
|
||||
|
||||
setEmailSettings(
|
||||
Object.fromEntries(Object.values(EmailType).map((type) => [type, disable])) as EmailSettings
|
||||
);
|
||||
};
|
||||
|
||||
const handleIndividualToggle = (type: EmailType, disabled: boolean) => {
|
||||
if (disabled) {
|
||||
setPendingEmailType(type);
|
||||
setDialogAction("disable");
|
||||
setDialogMode("individual");
|
||||
setShowConfirmDialog(true);
|
||||
} else {
|
||||
const settingKey = EMAIL_TYPE_TO_SETTING_KEY[type];
|
||||
mutation.mutate({ [settingKey]: false });
|
||||
setEmailSettings((prev) => ({ ...prev, [type]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const confirmIndividualToggle = () => {
|
||||
if (pendingEmailType === null) return;
|
||||
const settingKey = EMAIL_TYPE_TO_SETTING_KEY[pendingEmailType];
|
||||
const shouldDisable = dialogAction === "disable";
|
||||
mutation.mutate({ [settingKey]: shouldDisable });
|
||||
setEmailSettings((prev) => ({ ...prev, [pendingEmailType]: shouldDisable }));
|
||||
setPendingEmailType(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||
<ConfirmationDialogContent
|
||||
variety={dialogAction === "disable" ? "danger" : "warning"}
|
||||
title={
|
||||
dialogMode === "all"
|
||||
? t(
|
||||
dialogAction === "disable"
|
||||
? "disable_all_guest_booking_emails_confirm_title"
|
||||
: "enable_all_guest_booking_emails_confirm_title"
|
||||
)
|
||||
: t("disable_individual_guest_email_confirm_title", {
|
||||
emailType: pendingEmailType ? t(EMAIL_TYPE_LABELS[pendingEmailType]) : "",
|
||||
})
|
||||
}
|
||||
confirmBtnText={t(
|
||||
dialogMode === "all"
|
||||
? dialogAction === "disable"
|
||||
? "disable_all"
|
||||
: "enable_all"
|
||||
: "disable_email"
|
||||
)}
|
||||
onConfirm={() => {
|
||||
if (dialogMode === "all") {
|
||||
handleDisableAll(dialogAction === "disable");
|
||||
} else {
|
||||
confirmIndividualToggle();
|
||||
}
|
||||
setShowConfirmDialog(false);
|
||||
}}>
|
||||
<p className="mt-2">
|
||||
{dialogMode === "all"
|
||||
? t(
|
||||
dialogAction === "disable"
|
||||
? "disable_all_guest_booking_emails_confirm_description"
|
||||
: "enable_all_guest_booking_emails_confirm_description"
|
||||
)
|
||||
: t("disable_individual_guest_email_confirm_description", {
|
||||
emailType: pendingEmailType ? t(EMAIL_TYPE_LABELS[pendingEmailType]) : "",
|
||||
})}
|
||||
</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Alert severity="warning" title={t("disable_guest_emails_warning")} />
|
||||
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
checked={allDisabled}
|
||||
title={t("disable_all_booking_emails_to_guests")}
|
||||
labelClassName="text-sm font-semibold"
|
||||
description={t("disable_all_booking_emails_to_guests_description")}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
data-testid="disable-all-guest-emails"
|
||||
disabled={readOnly}
|
||||
onCheckedChange={(checked) => {
|
||||
setDialogAction(checked ? "disable" : "enable");
|
||||
setDialogMode("all");
|
||||
setShowConfirmDialog(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="border-subtle overflow-hidden rounded-lg border">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-muted border-subtle border-b">
|
||||
<th className="text-emphasis px-6 py-3 text-left text-sm font-medium">{t("email_type")}</th>
|
||||
<th className="text-emphasis px-6 py-3 text-left text-sm font-medium">{t("description")}</th>
|
||||
<th className="text-emphasis px-6 py-3 text-right text-sm font-medium">{t("enabled")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-subtle divide-y">
|
||||
<EmailRow
|
||||
emailType={EmailType.CONFIRMATION}
|
||||
labelKey="confirmation"
|
||||
descriptionKey="guest_confirmation_email_description"
|
||||
isDisabled={emailSettings[EmailType.CONFIRMATION]}
|
||||
onToggle={handleIndividualToggle}
|
||||
testId="disable-guest-confirmation-email"
|
||||
disabled={readOnly || allDisabled}
|
||||
/>
|
||||
<EmailRow
|
||||
emailType={EmailType.CANCELLATION}
|
||||
labelKey="cancellation"
|
||||
descriptionKey="guest_cancellation_email_description"
|
||||
isDisabled={emailSettings[EmailType.CANCELLATION]}
|
||||
onToggle={handleIndividualToggle}
|
||||
testId="disable-guest-cancellation-email"
|
||||
disabled={readOnly || allDisabled}
|
||||
/>
|
||||
<EmailRow
|
||||
emailType={EmailType.RESCHEDULED}
|
||||
labelKey="rescheduled"
|
||||
descriptionKey="guest_rescheduled_email_description"
|
||||
isDisabled={emailSettings[EmailType.RESCHEDULED]}
|
||||
onToggle={handleIndividualToggle}
|
||||
testId="disable-guest-rescheduled-email"
|
||||
disabled={readOnly || allDisabled}
|
||||
/>
|
||||
<EmailRow
|
||||
emailType={EmailType.REQUEST}
|
||||
labelKey="request"
|
||||
descriptionKey="attendee_request_email_description"
|
||||
isDisabled={emailSettings[EmailType.REQUEST]}
|
||||
onToggle={handleIndividualToggle}
|
||||
testId="disable-attendee-request-email"
|
||||
disabled={readOnly || allDisabled}
|
||||
/>
|
||||
<EmailRow
|
||||
emailType={EmailType.REASSIGNED}
|
||||
labelKey="host_reassignment"
|
||||
descriptionKey="attendee_reassigned_email_description"
|
||||
isDisabled={emailSettings[EmailType.REASSIGNED]}
|
||||
onToggle={handleIndividualToggle}
|
||||
testId="disable-attendee-reassigned-email"
|
||||
disabled={readOnly || allDisabled}
|
||||
/>
|
||||
<EmailRow
|
||||
emailType={EmailType.AWAITING_PAYMENT}
|
||||
labelKey="awaiting_payment"
|
||||
descriptionKey="attendee_awaiting_payment_email_description"
|
||||
isDisabled={emailSettings[EmailType.AWAITING_PAYMENT]}
|
||||
onToggle={handleIndividualToggle}
|
||||
testId="disable-attendee-awaiting-payment-email"
|
||||
disabled={readOnly || allDisabled}
|
||||
/>
|
||||
<EmailRow
|
||||
emailType={EmailType.RESCHEDULE_REQUEST}
|
||||
labelKey="reschedule_request"
|
||||
descriptionKey="attendee_reschedule_request_email_description"
|
||||
isDisabled={emailSettings[EmailType.RESCHEDULE_REQUEST]}
|
||||
onToggle={handleIndividualToggle}
|
||||
testId="disable-attendee-reschedule-request-email"
|
||||
disabled={readOnly || allDisabled}
|
||||
/>
|
||||
<EmailRow
|
||||
emailType={EmailType.LOCATION_CHANGE}
|
||||
labelKey="location_change"
|
||||
descriptionKey="attendee_location_change_email_description"
|
||||
isDisabled={emailSettings[EmailType.LOCATION_CHANGE]}
|
||||
onToggle={handleIndividualToggle}
|
||||
testId="disable-attendee-location-change-email"
|
||||
disabled={readOnly || allDisabled}
|
||||
/>
|
||||
<EmailRow
|
||||
emailType={EmailType.NEW_EVENT}
|
||||
labelKey="guest_added"
|
||||
descriptionKey="attendee_new_event_email_description"
|
||||
isDisabled={emailSettings[EmailType.NEW_EVENT]}
|
||||
onToggle={handleIndividualToggle}
|
||||
testId="disable-attendee-new-event-email"
|
||||
disabled={readOnly || allDisabled}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisableGuestBookingEmailsSetting;
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { SkeletonContainer, SkeletonText, SkeletonButton } from "@calcom/ui/components/skeleton";
|
||||
|
||||
import DisableGuestBookingEmailsSetting from "../components/DisableGuestBookingEmailsSetting";
|
||||
|
||||
const SkeletonLoader = () => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<div className="mb-8 mt-6 space-y-6">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const GuestNotificationsView = ({ permissions }: { permissions: { canRead: boolean; canEdit: boolean } }) => {
|
||||
const { data: currentOrg, isPending } = trpc.viewer.organizations.listCurrent.useQuery();
|
||||
const isInviteOpen = !currentOrg?.user.accepted;
|
||||
const isDisabled = !permissions.canEdit || isInviteOpen;
|
||||
|
||||
if (isPending) return <SkeletonLoader />;
|
||||
|
||||
if (!currentOrg) return null;
|
||||
|
||||
if (!currentOrg.organizationSettings) return null;
|
||||
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<div className="space-y-8">
|
||||
<DisableGuestBookingEmailsSetting
|
||||
orgId={currentOrg.id}
|
||||
settings={{
|
||||
disableAttendeeConfirmationEmail:
|
||||
currentOrg.organizationSettings.disableAttendeeConfirmationEmail ?? false,
|
||||
disableAttendeeCancellationEmail:
|
||||
currentOrg.organizationSettings.disableAttendeeCancellationEmail ?? false,
|
||||
disableAttendeeRescheduledEmail:
|
||||
currentOrg.organizationSettings.disableAttendeeRescheduledEmail ?? false,
|
||||
disableAttendeeRequestEmail: currentOrg.organizationSettings.disableAttendeeRequestEmail ?? false,
|
||||
disableAttendeeReassignedEmail:
|
||||
currentOrg.organizationSettings.disableAttendeeReassignedEmail ?? false,
|
||||
disableAttendeeAwaitingPaymentEmail:
|
||||
currentOrg.organizationSettings.disableAttendeeAwaitingPaymentEmail ?? false,
|
||||
disableAttendeeRescheduleRequestEmail:
|
||||
currentOrg.organizationSettings.disableAttendeeRescheduleRequestEmail ?? false,
|
||||
disableAttendeeLocationChangeEmail:
|
||||
currentOrg.organizationSettings.disableAttendeeLocationChangeEmail ?? false,
|
||||
disableAttendeeNewEventEmail:
|
||||
currentOrg.organizationSettings.disableAttendeeNewEventEmail ?? false,
|
||||
}}
|
||||
readOnly={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
</LicenseRequired>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuestNotificationsView;
|
||||
@@ -289,6 +289,15 @@ export class OrganizationRepository {
|
||||
disablePhoneOnlySMSNotifications: true,
|
||||
disableAutofillOnBookingPage: true,
|
||||
orgAutoJoinOnSignup: true,
|
||||
disableAttendeeConfirmationEmail: true,
|
||||
disableAttendeeCancellationEmail: true,
|
||||
disableAttendeeRescheduledEmail: true,
|
||||
disableAttendeeRequestEmail: true,
|
||||
disableAttendeeReassignedEmail: true,
|
||||
disableAttendeeAwaitingPaymentEmail: true,
|
||||
disableAttendeeRescheduleRequestEmail: true,
|
||||
disableAttendeeLocationChangeEmail: true,
|
||||
disableAttendeeNewEventEmail: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -309,6 +318,15 @@ export class OrganizationRepository {
|
||||
disablePhoneOnlySMSNotifications: organizationSettings?.disablePhoneOnlySMSNotifications,
|
||||
disableAutofillOnBookingPage: organizationSettings?.disableAutofillOnBookingPage,
|
||||
orgAutoJoinOnSignup: organizationSettings?.orgAutoJoinOnSignup,
|
||||
disableAttendeeConfirmationEmail: organizationSettings?.disableAttendeeConfirmationEmail,
|
||||
disableAttendeeCancellationEmail: organizationSettings?.disableAttendeeCancellationEmail,
|
||||
disableAttendeeRescheduledEmail: organizationSettings?.disableAttendeeRescheduledEmail,
|
||||
disableAttendeeRequestEmail: organizationSettings?.disableAttendeeRequestEmail,
|
||||
disableAttendeeReassignedEmail: organizationSettings?.disableAttendeeReassignedEmail,
|
||||
disableAttendeeAwaitingPaymentEmail: organizationSettings?.disableAttendeeAwaitingPaymentEmail,
|
||||
disableAttendeeRescheduleRequestEmail: organizationSettings?.disableAttendeeRescheduleRequestEmail,
|
||||
disableAttendeeLocationChangeEmail: organizationSettings?.disableAttendeeLocationChangeEmail,
|
||||
disableAttendeeNewEventEmail: organizationSettings?.disableAttendeeNewEventEmail,
|
||||
},
|
||||
user: {
|
||||
role: membership?.role,
|
||||
|
||||
@@ -313,6 +313,7 @@ export const roundRobinManualReassignment = async ({
|
||||
location: bookingLocation,
|
||||
...(platformClientParams ? platformClientParams : {}),
|
||||
conferenceCredentialId: conferenceCredentialId ?? undefined,
|
||||
organizationId: orgId,
|
||||
};
|
||||
|
||||
if (hasOrganizerChanged) {
|
||||
|
||||
@@ -340,6 +340,7 @@ export const roundRobinReassignment = async ({
|
||||
customReplyToEmail: eventType?.customReplyToEmail,
|
||||
location: bookingLocation,
|
||||
...(platformClientParams ? platformClientParams : {}),
|
||||
organizationId: orgId,
|
||||
};
|
||||
|
||||
if (hasOrganizerChanged) {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
export class OrganizationSettingsRepository {
|
||||
constructor(private prismaClient: PrismaClient) {}
|
||||
|
||||
async getEmailSettings(organizationId: number) {
|
||||
return await this.prismaClient.organizationSettings.findUnique({
|
||||
where: { organizationId },
|
||||
select: {
|
||||
disableAttendeeConfirmationEmail: true,
|
||||
disableAttendeeCancellationEmail: true,
|
||||
disableAttendeeRescheduledEmail: true,
|
||||
disableAttendeeRequestEmail: true,
|
||||
disableAttendeeReassignedEmail: true,
|
||||
disableAttendeeAwaitingPaymentEmail: true,
|
||||
disableAttendeeRescheduleRequestEmail: true,
|
||||
disableAttendeeLocationChangeEmail: true,
|
||||
disableAttendeeNewEventEmail: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,7 @@ describe("buildCalEventFromBooking", () => {
|
||||
organizer,
|
||||
location,
|
||||
conferenceCredentialId,
|
||||
organizationId: null,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -127,6 +128,7 @@ describe("buildCalEventFromBooking", () => {
|
||||
hideOrganizerEmail: undefined,
|
||||
iCalSequence: 0,
|
||||
iCalUID: booking.iCalUID,
|
||||
organizationId: null,
|
||||
});
|
||||
|
||||
expect(parseRecurringEvent).toHaveBeenCalledWith(booking.eventType?.recurringEvent);
|
||||
@@ -154,6 +156,7 @@ describe("buildCalEventFromBooking", () => {
|
||||
organizer,
|
||||
location,
|
||||
conferenceCredentialId,
|
||||
organizationId: null,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -182,6 +185,7 @@ describe("buildCalEventFromBooking", () => {
|
||||
hideOrganizerEmail: undefined,
|
||||
iCalSequence: 0,
|
||||
iCalUID: "icaluid",
|
||||
organizationId: null,
|
||||
});
|
||||
|
||||
// @ts-expect-error - locale is set in mock
|
||||
|
||||
@@ -63,11 +63,13 @@ export const buildCalEventFromBooking = async ({
|
||||
organizer,
|
||||
location,
|
||||
conferenceCredentialId,
|
||||
organizationId,
|
||||
}: {
|
||||
booking: Booking;
|
||||
organizer: Organizer;
|
||||
location: string;
|
||||
conferenceCredentialId: number | null;
|
||||
organizationId: number | null;
|
||||
}) => {
|
||||
const attendeesList = await Promise.all(
|
||||
booking.attendees.map(async (attendee) => {
|
||||
@@ -113,5 +115,6 @@ export const buildCalEventFromBooking = async ({
|
||||
customReplyToEmail: booking.eventType?.customReplyToEmail,
|
||||
iCalUID: booking.iCalUID ?? booking.uid,
|
||||
iCalSequence: booking.iCalSequence ?? 0,
|
||||
organizationId,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ class CalendarEventClass implements CalendarEvent {
|
||||
recurrence?: string;
|
||||
iCalUID?: string | null;
|
||||
customReplyToEmail?: string | null;
|
||||
organizationId?: number | null;
|
||||
|
||||
constructor(initProps?: CalendarEvent) {
|
||||
// If more parameters are given we update this
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."OrganizationSettings" ADD COLUMN "disableAttendeeAwaitingPaymentEmail" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "disableAttendeeCancellationEmail" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "disableAttendeeConfirmationEmail" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "disableAttendeeLocationChangeEmail" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "disableAttendeeNewEventEmail" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "disableAttendeeReassignedEmail" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "disableAttendeeRequestEmail" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "disableAttendeeRescheduleRequestEmail" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "disableAttendeeRescheduledEmail" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -1,4 +1,4 @@
|
||||
// This is your Prisma Schema file
|
||||
// This is your Prisma Schema file
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
datasource db {
|
||||
@@ -688,6 +688,16 @@ model OrganizationSettings {
|
||||
disablePhoneOnlySMSNotifications Boolean @default(false)
|
||||
disableAutofillOnBookingPage Boolean @default(false)
|
||||
orgAutoJoinOnSignup Boolean @default(true)
|
||||
|
||||
disableAttendeeConfirmationEmail Boolean @default(false)
|
||||
disableAttendeeCancellationEmail Boolean @default(false)
|
||||
disableAttendeeRescheduledEmail Boolean @default(false)
|
||||
disableAttendeeRequestEmail Boolean @default(false)
|
||||
disableAttendeeReassignedEmail Boolean @default(false)
|
||||
disableAttendeeAwaitingPaymentEmail Boolean @default(false)
|
||||
disableAttendeeRescheduleRequestEmail Boolean @default(false)
|
||||
disableAttendeeLocationChangeEmail Boolean @default(false)
|
||||
disableAttendeeNewEventEmail Boolean @default(false)
|
||||
}
|
||||
|
||||
enum MembershipRole {
|
||||
|
||||
@@ -213,6 +213,7 @@ export const Handler = async ({ ctx, input }: Options) => {
|
||||
eventTypeId: eventType?.id,
|
||||
videoCallData,
|
||||
customReplyToEmail: eventType?.customReplyToEmail,
|
||||
organizationId: user?.organizationId ?? null,
|
||||
team: updatedBooking.eventType?.team
|
||||
? {
|
||||
name: updatedBooking.eventType.team.name,
|
||||
|
||||
@@ -291,6 +291,7 @@ async function buildCalendarEvent(
|
||||
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
|
||||
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
|
||||
customReplyToEmail: booking.eventType?.customReplyToEmail,
|
||||
organizationId: booking.user?.profiles?.[0]?.organizationId ?? null,
|
||||
};
|
||||
|
||||
if (videoCallReference) {
|
||||
|
||||
@@ -253,6 +253,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
|
||||
}
|
||||
: undefined,
|
||||
...(platformClientParams ? platformClientParams : {}),
|
||||
organizationId: organizerOrganizationId ?? booking.eventType?.team?.parentId ?? null,
|
||||
additionalNotes: booking.description,
|
||||
};
|
||||
|
||||
|
||||
@@ -262,6 +262,7 @@ export async function editLocationHandler({ ctx, input }: EditLocationOptions) {
|
||||
const { booking, user: loggedInUser } = ctx;
|
||||
|
||||
const organizer = await new UserRepository(prisma).findByIdOrThrow({ id: booking.userId || 0 });
|
||||
const organizationId = booking.user?.profiles?.[0]?.organizationId ?? null;
|
||||
|
||||
const newLocationInEvtFormat = await getLocationInEvtFormatOrThrow({
|
||||
location: newLocation,
|
||||
@@ -274,6 +275,7 @@ export async function editLocationHandler({ ctx, input }: EditLocationOptions) {
|
||||
organizer,
|
||||
location: newLocationInEvtFormat,
|
||||
conferenceCredentialId,
|
||||
organizationId,
|
||||
});
|
||||
|
||||
const eventManager = new EventManager({
|
||||
|
||||
@@ -40,6 +40,11 @@ export const bookingsProcedure = authedProcedure
|
||||
include: {
|
||||
destinationCalendar: true,
|
||||
credentials: true,
|
||||
profiles: {
|
||||
select: {
|
||||
organizationId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -110,6 +115,7 @@ export type BookingsProcedureContext = {
|
||||
| (User & {
|
||||
destinationCalendar: DestinationCalendar | null;
|
||||
credentials: Credential[];
|
||||
profiles: { organizationId: number }[];
|
||||
})
|
||||
| null;
|
||||
references: BookingReference[];
|
||||
|
||||
@@ -99,6 +99,51 @@ const updateOrganizationSettings = async ({
|
||||
data.orgAutoJoinOnSignup = input.orgAutoJoinOnSignup;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (input.hasOwnProperty("disableAttendeeConfirmationEmail")) {
|
||||
data.disableAttendeeConfirmationEmail = input.disableAttendeeConfirmationEmail;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (input.hasOwnProperty("disableAttendeeCancellationEmail")) {
|
||||
data.disableAttendeeCancellationEmail = input.disableAttendeeCancellationEmail;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (input.hasOwnProperty("disableAttendeeRescheduledEmail")) {
|
||||
data.disableAttendeeRescheduledEmail = input.disableAttendeeRescheduledEmail;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (input.hasOwnProperty("disableAttendeeRequestEmail")) {
|
||||
data.disableAttendeeRequestEmail = input.disableAttendeeRequestEmail;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (input.hasOwnProperty("disableAttendeeReassignedEmail")) {
|
||||
data.disableAttendeeReassignedEmail = input.disableAttendeeReassignedEmail;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (input.hasOwnProperty("disableAttendeeAwaitingPaymentEmail")) {
|
||||
data.disableAttendeeAwaitingPaymentEmail = input.disableAttendeeAwaitingPaymentEmail;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (input.hasOwnProperty("disableAttendeeRescheduleRequestEmail")) {
|
||||
data.disableAttendeeRescheduleRequestEmail = input.disableAttendeeRescheduleRequestEmail;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (input.hasOwnProperty("disableAttendeeLocationChangeEmail")) {
|
||||
data.disableAttendeeLocationChangeEmail = input.disableAttendeeLocationChangeEmail;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (input.hasOwnProperty("disableAttendeeNewEventEmail")) {
|
||||
data.disableAttendeeNewEventEmail = input.disableAttendeeNewEventEmail;
|
||||
}
|
||||
|
||||
// If no settings values have changed lets skip this update
|
||||
if (Object.keys(data).length === 0) return;
|
||||
|
||||
|
||||
@@ -37,6 +37,15 @@ export const ZUpdateInputSchema = z.object({
|
||||
disablePhoneOnlySMSNotifications: z.boolean().optional(),
|
||||
disableAutofillOnBookingPage: z.boolean().optional(),
|
||||
orgAutoJoinOnSignup: z.boolean().optional(),
|
||||
disableAttendeeConfirmationEmail: z.boolean().optional(),
|
||||
disableAttendeeCancellationEmail: z.boolean().optional(),
|
||||
disableAttendeeRescheduledEmail: z.boolean().optional(),
|
||||
disableAttendeeRequestEmail: z.boolean().optional(),
|
||||
disableAttendeeReassignedEmail: z.boolean().optional(),
|
||||
disableAttendeeAwaitingPaymentEmail: z.boolean().optional(),
|
||||
disableAttendeeRescheduleRequestEmail: z.boolean().optional(),
|
||||
disableAttendeeLocationChangeEmail: z.boolean().optional(),
|
||||
disableAttendeeNewEventEmail: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type TUpdateInputSchema = z.infer<typeof ZUpdateInputSchema>;
|
||||
|
||||
Vendored
+1
@@ -223,6 +223,7 @@ export interface CalendarEvent {
|
||||
domainWideDelegationCredentialId?: string | null;
|
||||
customReplyToEmail?: string | null;
|
||||
rescheduledBy?: string;
|
||||
organizationId?: number | null;
|
||||
hasOrganizerChanged?: boolean;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user