diff --git a/packages/features/eventtypes/components/tabs/availability/EventAvailabilityTab.tsx b/packages/features/eventtypes/components/tabs/availability/EventAvailabilityTab.tsx index a6de6f10b8..6972155334 100644 --- a/packages/features/eventtypes/components/tabs/availability/EventAvailabilityTab.tsx +++ b/packages/features/eventtypes/components/tabs/availability/EventAvailabilityTab.tsx @@ -54,7 +54,6 @@ type HostSchedulesQueryType = type EventTypeTeamScheduleProps = { hostSchedulesQuery: HostSchedulesQueryType; - hosts?: Host[]; }; type TeamMember = Pick; @@ -392,13 +391,13 @@ const TeamMemberSchedule = ({ }; const TeamAvailability = ({ - hosts, teamMembers, hostSchedulesQuery, }: EventTypeTeamScheduleProps & { teamMembers: TeamMember[] }) => { const { t } = useLocale(); + const { watch } = useFormContext(); const [animationRef] = useAutoAnimate(); - + const hosts = watch("hosts"); return ( <>
@@ -439,42 +438,41 @@ const TeamAvailability = ({ ); }; +const useCommonScheduleState = (initialScheduleId: number | null) => { + const { setValue } = useFormContext(); + const [useHostSchedulesForTeamEvent, setUseHostSchedulesForTeamEvent] = useState(!initialScheduleId); + // Reset the main schedule + const clearMainSchedule = () => { + setValue("schedule", null, { shouldDirty: Boolean(initialScheduleId) }); + }; + // Toggle function + const toggleScheduleState = (checked: boolean) => { + const useHostSchedulesForTeamEvent = !checked; + setUseHostSchedulesForTeamEvent(useHostSchedulesForTeamEvent); + if (useHostSchedulesForTeamEvent) clearMainSchedule(); + }; + return { + useHostSchedulesForTeamEvent, + toggleScheduleState, + }; +}; + const UseCommonScheduleSettingsToggle = ({ eventType, ...rest }: EventTypeScheduleProps) => { const { t } = useLocale(); - const { setValue, resetField, getValues, watch } = useFormContext(); - - const [useHostSchedulesForTeamEvent, setUseHostSchedulesForTeamEvent] = useState( - !Boolean(eventType.schedule) - ); - - const watchHosts = watch("hosts"); - + const { useHostSchedulesForTeamEvent, toggleScheduleState } = useCommonScheduleState(eventType.schedule); return ( <> { - setUseHostSchedulesForTeamEvent(!checked); - if (checked) { - if (Boolean(eventType.schedule)) resetField("schedule"); - getValues("hosts").map((_, index) => { - setValue(`hosts.${index}.scheduleId`, null, { shouldDirty: true }); - }); - } else { - setValue("schedule", null, { shouldDirty: Boolean(eventType.schedule) }); - } - }} + onCheckedChange={toggleScheduleState} title={t("choose_common_schedule_team_event")} description={t("choose_common_schedule_team_event_description")}> + {/* handles the state for which 'schedule' ID is set, as it's unknown until the Select dropdown is loaded */} {useHostSchedulesForTeamEvent && (
- +
)} diff --git a/packages/features/eventtypes/lib/types.ts b/packages/features/eventtypes/lib/types.ts index d59d021e95..33e4050812 100644 --- a/packages/features/eventtypes/lib/types.ts +++ b/packages/features/eventtypes/lib/types.ts @@ -37,6 +37,33 @@ export type TeamMember = { defaultScheduleId: number | null; }; +type EventLocation = { + type: EventLocationType["type"]; + address?: string; + attendeeAddress?: string; + somewhereElse?: string; + link?: string; + hostPhoneNumber?: string; + displayLocationPublicly?: boolean; + phone?: string; + hostDefault?: string; + credentialId?: number; + teamName?: string; +}; + +type PhoneCallConfig = { + generalPrompt: string; + enabled: boolean; + beginMessage: string; + yourPhoneNumber: string; + numberToCall: string; + guestName?: string; + guestEmail?: string; + guestCompany?: string; + templateType: string; + schedulerName?: string; +}; + export type FormValues = { id: number; title: string; @@ -59,31 +86,8 @@ export type FormValues = { hideCalendarNotes: boolean; multiplePrivateLinks: string[] | undefined; eventTypeColor: z.infer; - locations: { - type: EventLocationType["type"]; - address?: string; - attendeeAddress?: string; - somewhereElse?: string; - link?: string; - hostPhoneNumber?: string; - displayLocationPublicly?: boolean; - phone?: string; - hostDefault?: string; - credentialId?: number; - teamName?: string; - }[]; - aiPhoneCallConfig: { - generalPrompt: string; - enabled: boolean; - beginMessage: string; - yourPhoneNumber: string; - numberToCall: string; - guestName?: string; - guestEmail?: string; - guestCompany?: string; - templateType: string; - schedulerName?: string; - }; + locations: EventLocation[]; + aiPhoneCallConfig: PhoneCallConfig; customInputs: CustomInputParsed[]; schedule: number | null; diff --git a/packages/trpc/server/routers/viewer/eventTypes/types.ts b/packages/trpc/server/routers/viewer/eventTypes/types.ts index ae68393fe4..dd4d0ff923 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/types.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/types.ts @@ -5,77 +5,84 @@ import { _DestinationCalendarModel, _EventTypeModel } from "@calcom/prisma/zod"; import { customInputSchema, EventTypeMetaDataSchema, stringOrNumber } from "@calcom/prisma/zod-utils"; import { eventTypeBookingFields } from "@calcom/prisma/zod-utils"; -export const EventTypeUpdateInput = _EventTypeModel - /** Optional fields */ +const aiPhoneCallConfig = z + .object({ + generalPrompt: z.string(), + enabled: z.boolean(), + beginMessage: z.string().nullable(), + yourPhoneNumber: z.string(), + numberToCall: z.string(), + guestName: z.string().nullable().optional(), + guestEmail: z.string().nullable().optional(), + guestCompany: z.string().nullable().optional(), + templateType: templateTypeEnum, + }) + .optional(); + +const hostSchema = z.object({ + userId: z.number(), + profileId: z.number().or(z.null()).optional(), + isFixed: z.boolean().optional(), + priority: z.number().min(0).max(4).optional().nullable(), + weight: z.number().min(0).optional().nullable(), + scheduleId: z.number().optional().nullable(), +}); + +const childSchema = z.object({ + owner: z.object({ + id: z.number(), + name: z.string(), + email: z.string(), + eventTypeSlugs: z.array(z.string()), + }), + hidden: z.boolean(), +}); + +/** Optional fields */ +const BaseEventTypeUpdateInput = _EventTypeModel .extend({ - isInstantEvent: z.boolean().optional(), - instantMeetingExpiryTimeOffsetInSeconds: z.number().optional(), - aiPhoneCallConfig: z - .object({ - generalPrompt: z.string(), - enabled: z.boolean(), - beginMessage: z.string().nullable(), - yourPhoneNumber: z.string().default(""), - numberToCall: z.string().default(""), - guestName: z - .string() - .nullable() - .optional() - .transform((val) => (!!val ? val : undefined)), - guestEmail: z.string().nullable().default(null), - guestCompany: z.string().nullable().default(null), - templateType: templateTypeEnum, - }) - .optional(), - calAiPhoneScript: z.string().optional(), - customInputs: z.array(customInputSchema).optional(), + isInstantEvent: z.boolean(), + instantMeetingExpiryTimeOffsetInSeconds: z.number(), + aiPhoneCallConfig, + calAiPhoneScript: z.string(), + customInputs: z.array(customInputSchema), destinationCalendar: _DestinationCalendarModel .pick({ integration: true, externalId: true, }) .nullable(), - users: z.array(stringOrNumber).optional(), - children: z - .array( - z.object({ - owner: z.object({ - id: z.number(), - name: z.string(), - email: z.string(), - eventTypeSlugs: z.array(z.string()), - }), - hidden: z.boolean(), - }) - ) - .optional(), - hosts: z - .array( - z.object({ - userId: z.number(), - profileId: z.number().or(z.null()).optional(), - isFixed: z.boolean().optional(), - priority: z.number().min(0).max(4).optional().nullable(), - weight: z.number().min(0).optional().nullable(), - scheduleId: z.number().optional().nullable(), - }) - ) - .optional(), - schedule: z.number().nullable().optional(), - instantMeetingSchedule: z.number().nullable().optional(), + users: z.array(stringOrNumber), + children: z.array(childSchema), + hosts: z.array(hostSchema), + schedule: z.number().nullable(), + instantMeetingSchedule: z.number().nullable(), multiplePrivateLinks: z.array(z.string()), - assignAllTeamMembers: z.boolean().optional(), - isRRWeightsEnabled: z.boolean().optional(), + assignAllTeamMembers: z.boolean(), + isRRWeightsEnabled: z.boolean(), + metadata: EventTypeMetaDataSchema, + bookingFields: eventTypeBookingFields, }) .partial() - .extend({ - metadata: EventTypeMetaDataSchema.optional(), - bookingFields: eventTypeBookingFields.optional(), - }) - .merge( - _EventTypeModel - /** Required fields */ - .pick({ - id: true, - }) - ); + .extend(_EventTypeModel.pick({ id: true }).shape); + +const ZUpdateInputSchema = BaseEventTypeUpdateInput.extend({ + aiPhoneCallConfig: aiPhoneCallConfig.refine( + (data) => { + if (!data) return true; + data.yourPhoneNumber = data.yourPhoneNumber || ""; + data.numberToCall = data.numberToCall || ""; + data.guestName = data.guestName ?? undefined; + data.guestEmail = data.guestEmail ?? null; + data.guestCompany = data.guestCompany ?? null; + return true; + }, + { + message: "Applying default values and transformations", + } + ), +}).strict(); +// only run infer over the simple type, excluding refines/transforms. +type TUpdateInputSchema = z.infer; + +export { ZUpdateInputSchema, type TUpdateInputSchema }; diff --git a/packages/trpc/server/routers/viewer/eventTypes/update.schema.ts b/packages/trpc/server/routers/viewer/eventTypes/update.schema.ts index 4e69d8d4be..e0a0cf6c11 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/update.schema.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/update.schema.ts @@ -1,7 +1,2 @@ -import type { z } from "zod"; - -import { EventTypeUpdateInput } from "./types"; - -export const ZUpdateInputSchema = EventTypeUpdateInput.strict(); - -export type TUpdateInputSchema = z.infer; +export { ZUpdateInputSchema } from "./types"; +export type { TUpdateInputSchema } from "./types"; diff --git a/packages/trpc/server/routers/viewer/eventTypes/util.ts b/packages/trpc/server/routers/viewer/eventTypes/util.ts index 0891163f8e..dd097aafbb 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/util.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/util.ts @@ -12,7 +12,7 @@ import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { TRPCError } from "@trpc/server"; import authedProcedure from "../../../procedures/authedProcedure"; -import type { EventTypeUpdateInput } from "./types"; +import type { TUpdateInputSchema } from "./types"; type EventType = Awaited>[number]; @@ -142,7 +142,7 @@ export function handleCustomInputs(customInputs: CustomInputSchema[], eventTypeI }; } -export function ensureUniqueBookingFields(fields: z.infer["bookingFields"]) { +export function ensureUniqueBookingFields(fields: TUpdateInputSchema["bookingFields"]) { if (!fields) { return; } @@ -161,9 +161,7 @@ export function ensureUniqueBookingFields(fields: z.infer); } -export function ensureEmailOrPhoneNumberIsPresent( - fields: z.infer["bookingFields"] -) { +export function ensureEmailOrPhoneNumberIsPresent(fields: TUpdateInputSchema["bookingFields"]) { if (!fields || fields.length === 0) { return; }