diff --git a/apps/web/modules/bookings/components/BookEventForm/BookingFields.tsx b/apps/web/modules/bookings/components/BookEventForm/BookingFields.tsx index a64b3b771806a0..31a311f4173510 100644 --- a/apps/web/modules/bookings/components/BookEventForm/BookingFields.tsx +++ b/apps/web/modules/bookings/components/BookEventForm/BookingFields.tsx @@ -1,20 +1,18 @@ -import { useMemo, useRef } from "react"; -import { useFormContext } from "react-hook-form"; -import { z } from "zod"; - import type { LocationObject } from "@calcom/app-store/locations"; -import { getOrganizerInputLocationTypes } from "@calcom/app-store/locations"; -import { DefaultEventLocationTypeEnum } from "@calcom/app-store/locations"; +import { DefaultEventLocationTypeEnum, getOrganizerInputLocationTypes } from "@calcom/app-store/locations"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking"; import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect"; import { FormBuilderField } from "@calcom/features/form-builder/FormBuilderField"; -import { fieldTypesConfigMap } from "@calcom/features/form-builder/fieldTypes"; import { fieldsThatSupportLabelAsSafeHtml } from "@calcom/features/form-builder/fieldsThatSupportLabelAsSafeHtml"; +import { fieldTypesConfigMap } from "@calcom/features/form-builder/fieldTypes"; import { SystemField } from "@calcom/lib/bookings/SystemField"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import type { RouterOutputs } from "@calcom/trpc/react"; +import { useMemo, useRef } from "react"; +import { useFormContext } from "react-hook-form"; +import { z } from "zod"; type TouchedFields = { responses?: Record; @@ -159,16 +157,12 @@ export const BookingFields = ({ readOnly = false; } - if (field.name === SystemField.Enum.smsReminderNumber) { - // `smsReminderNumber` and location.optionValue when location.value===phone are the same data point. We should solve it in a better way in the Form Builder itself. - // I think we should have a way to connect 2 fields together and have them share the same value in Form Builder - if (locationResponse?.value === "phone") { - setValue(`responses.${SystemField.Enum.smsReminderNumber}`, locationResponse?.optionValue); - // Just don't render the field now, as the value is already connected to attendee phone location - return null; - } - // `smsReminderNumber` can be edited during reschedule even though it's a system field - readOnly = false; + // Skip duplicate/legacy phone fields (we only want attendeePhoneNumber now) + if ( + field.name === SystemField.Enum.smsReminderNumber || + field.name === SystemField.Enum.aiAgentCallPhoneNumber + ) { + return null; } if (field.name === SystemField.Enum.guests) { diff --git a/packages/features/bookings/lib/getBookingFields.ts b/packages/features/bookings/lib/getBookingFields.ts index b572cb797c37df..edaeb337fed2dd 100644 --- a/packages/features/bookings/lib/getBookingFields.ts +++ b/packages/features/bookings/lib/getBookingFields.ts @@ -1,19 +1,19 @@ -import type { z } from "zod"; - +import process from "node:process"; import type { Workflow } from "@calcom/features/ee/workflows/lib/types"; import { fieldsThatSupportLabelAsSafeHtml } from "@calcom/features/form-builder/fieldsThatSupportLabelAsSafeHtml"; import { getFieldIdentifier } from "@calcom/features/form-builder/utils/getFieldIdentifier"; -import { SMS_REMINDER_NUMBER_FIELD, CAL_AI_AGENT_PHONE_NUMBER_FIELD } from "@calcom/lib/bookings/SystemField"; +import { CAL_AI_AGENT_PHONE_NUMBER_FIELD, SMS_REMINDER_NUMBER_FIELD } from "@calcom/lib/bookings/SystemField"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import slugify from "@calcom/lib/slugify"; -import type { EventTypeCustomInput, EventType } from "@calcom/prisma/client"; +import type { EventType, EventTypeCustomInput } from "@calcom/prisma/client"; import { EventTypeCustomInputType } from "@calcom/prisma/enums"; import { BookingFieldTypeEnum, customInputSchema, - eventTypeBookingFields, EventTypeMetaDataSchema, + eventTypeBookingFields, } from "@calcom/prisma/zod-utils"; +import type { z } from "zod"; export type Fields = z.infer; @@ -36,7 +36,7 @@ export const getSmsReminderNumberField = () => defaultLabel: "number_text_notifications", defaultPlaceholder: "enter_phone_number", editable: "system", - } as const); + }) as const; export const getSmsReminderNumberSource = ({ workflowId, @@ -59,7 +59,7 @@ export const getAIAgentCallPhoneNumberField = () => defaultLabel: "phone_number_for_ai_call", defaultPlaceholder: "enter_phone_number", editable: "system", - } as const); + }) as const; export const getAIAgentCallPhoneNumberSource = ({ workflowId, @@ -115,7 +115,6 @@ export const getBookingFieldsWithSystemFields = ({ export const ensureBookingInputsHaveSystemFields = ({ bookingFields, disableGuests, - isOrgTeamEvent, disableBookingTitle, additionalNotesRequired, customInputs, @@ -143,17 +142,35 @@ export const ensureBookingInputsHaveSystemFields = ({ [EventTypeCustomInputType.PHONE]: BookingFieldTypeEnum.phone, }; - const smsNumberSources = [] as NonNullable<(typeof bookingFields)[number]["sources"]>; + const phoneNumberSources = [] as NonNullable<(typeof bookingFields)[number]["sources"]>; + let isPhoneNumberRequired = false; + workflows.forEach((workflow) => { workflow.workflow.steps.forEach((step) => { if (step.action === "SMS_ATTENDEE" || step.action === "WHATSAPP_ATTENDEE") { const workflowId = workflow.workflow.id; - smsNumberSources.push( + phoneNumberSources.push( getSmsReminderNumberSource({ workflowId, isSmsReminderNumberRequired: !!step.numberRequired, }) ); + if (step.numberRequired) { + isPhoneNumberRequired = true; + } + } + // Check for AI agent call workflows that require phone numbers + if (step.action === "CAL_AI_PHONE_CALL") { + const workflowId = workflow.workflow.id; + phoneNumberSources.push( + getAIAgentCallPhoneNumberSource({ + workflowId, + isAIAgentCallPhoneNumberRequired: !!step.numberRequired, + }) + ); + if (step.numberRequired) { + isPhoneNumberRequired = true; + } } }); }); @@ -333,21 +350,46 @@ export const ensureBookingInputsHaveSystemFields = ({ bookingFields = missingSystemBeforeFields.concat(bookingFields); - // Backward Compatibility for SMS Reminder Number - // Note: We still need workflows in `getBookingFields` due to Backward Compatibility. If we do a one time entry for all event-types, we can remove workflows from `getBookingFields` - // Also, note that even if Workflows don't explicitly add smsReminderNumber field to bookingFields, it would be added as a side effect of this backward compatibility logic - if ( - smsNumberSources.length && - !bookingFields.find((f) => getFieldIdentifier(f.name) !== getFieldIdentifier(SMS_REMINDER_NUMBER_FIELD)) - ) { - const indexForLocation = bookingFields.findIndex( - (f) => getFieldIdentifier(f.name) === getFieldIdentifier("location") + // Consolidated Phone Number Logic + // If any workflow requires a phone number, make the attendeePhoneNumber field visible and add sources + if (phoneNumberSources.length) { + const attendeePhoneNumberIndex = bookingFields.findIndex( + (f) => getFieldIdentifier(f.name) === getFieldIdentifier("attendeePhoneNumber") ); - // Add the SMS Reminder Number field after `location` field always - bookingFields.splice(indexForLocation + 1, 0, { - ...getSmsReminderNumberField(), - sources: smsNumberSources, - }); + + if (attendeePhoneNumberIndex !== -1) { + // Update the existing attendeePhoneNumber field to be visible and add sources + bookingFields[attendeePhoneNumberIndex] = { + ...bookingFields[attendeePhoneNumberIndex], + hidden: false, + required: bookingFields[attendeePhoneNumberIndex].required || isPhoneNumberRequired, + sources: [...(bookingFields[attendeePhoneNumberIndex].sources || []), ...phoneNumberSources], + }; + } + } + + // Consolidation Logic: If there are existing separate phone fields, make attendeePhoneNumber visible + // This ensures users see only one phone input instead of multiple + const hasExistingSmsField = bookingFields.find( + (f) => getFieldIdentifier(f.name) === getFieldIdentifier(SMS_REMINDER_NUMBER_FIELD) + ); + const hasExistingAiField = bookingFields.find( + (f) => getFieldIdentifier(f.name) === getFieldIdentifier(CAL_AI_AGENT_PHONE_NUMBER_FIELD) + ); + + if (hasExistingSmsField || hasExistingAiField) { + // Make sure attendeePhoneNumber is visible to consolidate the phone inputs + const attendeePhoneNumberIndex = bookingFields.findIndex( + (f) => getFieldIdentifier(f.name) === getFieldIdentifier("attendeePhoneNumber") + ); + + if (attendeePhoneNumberIndex !== -1) { + bookingFields[attendeePhoneNumberIndex] = { + ...bookingFields[attendeePhoneNumberIndex], + hidden: false, + required: bookingFields[attendeePhoneNumberIndex].required || isPhoneNumberRequired, + }; + } } // Backward Compatibility: If we are migrating from old system, we need to map `customInputs` to `bookingFields` diff --git a/packages/features/ee/workflows/lib/reminders/aiPhoneCallManager.ts b/packages/features/ee/workflows/lib/reminders/aiPhoneCallManager.ts index 4f6bf5a4e10d7a..d54b765459cea2 100644 --- a/packages/features/ee/workflows/lib/reminders/aiPhoneCallManager.ts +++ b/packages/features/ee/workflows/lib/reminders/aiPhoneCallManager.ts @@ -20,12 +20,6 @@ type timeUnitLowerCase = "day" | "hour" | "minute"; function extractPhoneNumber(responses: BookingInfo["responses"]): string | undefined { if (!responses) return undefined; - // Priority 1: CAL_AI_AGENT_PHONE_NUMBER_FIELD first - const aiAgentPhoneResponse = responses[CAL_AI_AGENT_PHONE_NUMBER_FIELD]; - if (aiAgentPhoneResponse && typeof aiAgentPhoneResponse === "object" && "value" in aiAgentPhoneResponse) { - return aiAgentPhoneResponse.value as string; - } - // Priority 2: attendeePhoneNumber as fallback const attendeePhoneResponse = responses.attendeePhoneNumber; if (