diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index f22c2c06d4..c7039de7be 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -44,7 +44,7 @@ "event_request_cancelled": "Your scheduled event was canceled", "organizer": "Organizer", "need_to_reschedule_or_cancel": "Need to reschedule or cancel?", - "you_can_view_booking_details_with_this_url":"You can view the booking details from this url {{url}} and add the event to your calendar", + "you_can_view_booking_details_with_this_url": "You can view the booking details from this url {{url}} and add the event to your calendar", "no_options_available": "No options available", "cancellation_reason": "Reason for cancellation (optional)", "cancellation_reason_placeholder": "Why are you cancelling?", @@ -601,7 +601,7 @@ "enter_address": "Enter address", "in_person_attendee_address": "In Person (Attendee Address)", "somewhere_else": "Somewhere Else", - "custom_attendee_location":"Custom attendee location", + "custom_attendee_location": "Custom attendee location", "any_location": "Any location", "yes": "Yes", "no": "No", @@ -1702,7 +1702,7 @@ "invalid_credential_action": "Reinstall app", "reschedule_reason": "Reschedule reason", "choose_common_schedule_team_event": "Choose a common schedule", - "choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule.", + "choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule or their choosen schedule.", "reason": "Reason", "sender_id": "Sender ID", "sender_id_error_message": "Only letters, numbers and spaces allowed (max. 11 characters)", @@ -2484,7 +2484,7 @@ "add_emails": "Add Emails", "add_email_description": "Add an email address to replace your primary or to use as an alternative email on your event types.", "confirm_email": "Confirm your email", - "confirming_your_booking_sms":"$t(hey_there) {{name}}, confirming your booking on {{date}}.", + "confirming_your_booking_sms": "$t(hey_there) {{name}}, confirming your booking on {{date}}.", "scheduler_first_name": "The first name of the person booking", "scheduler_last_name": "The last name of the person booking", "scheduler_name": "Scheduler Name", @@ -2592,19 +2592,19 @@ "event_expired": "This event is expired", "skip_contact_creation": "Skip creating contacts if they do not exist in {{appName}} ", "skip_writing_to_calendar_note": "If your ICS link is read-only (e.g., Proton Calendar), check the box above to avoid errors. You'll also need to manually update your calendar for changes.", - "attributes":"Attributes", + "attributes": "Attributes", "new_attribute": "New attribute", "add_attributes": "Add attributes", "add_attributes_description": "Add attributes to your team members", "new_option": "New option", - "update_profile":"Update member", + "update_profile": "Update member", "attribute_updated_successfully": "Attribute updated successfully", "attributes_edited_successfully": "Attributes edited successfully", "attribute_meta_description": "Manage attributes for your team members", "attributes_edit_description": "Edit attributes for your team members", "back_to_attributes": "Back to attributes", "delete_attribute": "Are you sure you want to delete this option?", - "delete_attribute_description":"This option is assigned to {{numberOfUsers}} members. Deleting it will remove it from their profile.", + "delete_attribute_description": "This option is assigned to {{numberOfUsers}} members. Deleting it will remove it from their profile.", "disable_all_emails_to_attendees": "Disable standard emails to attendees related to this event type", "disable_all_emails_description": "Disables standard email communication related to this event type, including booking confirmations, reminders, and cancellations.", "disable_all_emails_to_hosts": "Disable standard emails to hosts related to this event type", @@ -2627,7 +2627,7 @@ "stripe_connect_atom_loading_label": "Checking Stripe credentials", "booking_question_response_variables": "Booking question response variables", "managed_by_teamAdmins": "Managed by {{teamAdmins}}", - "number_of_options":"{{count}} options", + "number_of_options": "{{count}} options", "reschedule_with_same_round_robin_host_title": "Reschedule with same Round-Robin host", "reschedule_with_same_round_robin_host_description": "Rescheduled events will be assigned to the same host as initially scheduled", "disable_input_if_prefilled": "Disable input if the URL identifier is prefilled", @@ -2654,5 +2654,8 @@ "salesforce_book_directly_with_attendee_owner": "If attendee exists in Salesforce, book directly with the owner", "salesforce_check_owner_of": "Entity to check owner of to book directly", "account": "Account", + "choose_hosts_schedule": "Choose host schedules", + "hosts_schedule_description": "You can select a different schedule for each host. Each host's default schedule is chosen by default.", + "no_hosts_description": "No hosts assigned, add hosts in assignment tab.", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/test/lib/CheckForEmptyAssignment.test.ts b/apps/web/test/lib/CheckForEmptyAssignment.test.ts index 4c968416bd..ee4698ecfb 100644 --- a/apps/web/test/lib/CheckForEmptyAssignment.test.ts +++ b/apps/web/test/lib/CheckForEmptyAssignment.test.ts @@ -8,7 +8,9 @@ describe("Tests to Check if Event Types have empty Assignment", () => { checkForEmptyAssignment({ assignedUsers: [], assignAllTeamMembers: false, - hosts: [{ userId: 101, isFixed: false, priority: 2, weight: 100, weightAdjustment: 0 }], + hosts: [ + { userId: 101, isFixed: false, priority: 2, weight: 100, weightAdjustment: 0, scheduleId: null }, + ], isManagedEventType: true, }) ).toBe(true); @@ -61,7 +63,9 @@ describe("Tests to Check if Event Types have empty Assignment", () => { checkForEmptyAssignment({ assignedUsers: [], assignAllTeamMembers: false, - hosts: [{ userId: 101, isFixed: false, priority: 2, weight: 100, weightAdjustment: 0 }], + hosts: [ + { userId: 101, isFixed: false, priority: 2, weight: 100, weightAdjustment: 0, scheduleId: null }, + ], isManagedEventType: false, }) ).toBe(false); diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index 14f2387f58..80a6493049 100644 --- a/apps/web/test/lib/getSchedule.test.ts +++ b/apps/web/test/lib/getSchedule.test.ts @@ -1896,4 +1896,141 @@ describe("getSchedule", () => { ); }); }); + + describe("Round robin event", async () => { + test("correctly get slots for event with round robin hosts choosen schedule (non-default)", async () => { + vi.setSystemTime("2024-05-21T00:00:13Z"); + + const plus1DateString = "2024-05-22"; + const plus2DateString = "2024-05-23"; + + // An event with no common schedule and hosts choosen schedule (non-default schedule) + await createBookingScenario({ + eventTypes: [ + { + id: 1, + slotInterval: 60, + length: 60, + hosts: [ + { + userId: 101, + isFixed: false, + scheduleId: 2, + }, + { + userId: 102, + isFixed: false, + scheduleId: 3, + }, + ], + schedulingType: "ROUND_ROBIN", + schedule: null, + }, + ], + users: [ + { + ...TestData.users.example, + email: "example@example.com", + id: 101, + schedules: [TestData.schedules.IstMorningShift, TestData.schedules.IstEveningShift], + defaultScheduleId: 1, + }, + { + ...TestData.users.example, + email: "example1@example.com", + id: 102, + schedules: [TestData.schedules.IstEveningShift, TestData.schedules.IstMorningShift], + defaultScheduleId: 4, + }, + ], + bookings: [], + }); + + const scheduleWithHostChoosenSch = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + startTime: `${plus1DateString}T18:30:00.000Z`, + endTime: `${plus2DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: true, + teamMemberEmail: "test@test.com", + }, + }); + + // expect only slots of IstEveningShift , as this is the choosen schedule for hosts + expect(scheduleWithHostChoosenSch).toHaveTimeSlots( + [`11:30:00.000Z`, `12:30:00.000Z`, `13:30:00.000Z`, `14:30:00.000Z`, `15:30:00.000Z`], + { + dateString: plus2DateString, + } + ); + }); + test("correctly get slots for event with commom schedule", async () => { + vi.setSystemTime("2024-05-21T00:00:13Z"); + + const plus1DateString = "2024-05-22"; + const plus2DateString = "2024-05-23"; + + // An event with common schedule + await createBookingScenario({ + eventTypes: [ + { + id: 1, + slotInterval: 60, + length: 60, + hosts: [ + { + userId: 101, + isFixed: false, + }, + { + userId: 102, + isFixed: false, + }, + ], + schedule: TestData.schedules.IstEveningShift, + schedulingType: "ROUND_ROBIN", + }, + ], + users: [ + { + ...TestData.users.example, + email: "example@example.com", + id: 101, + schedules: [TestData.schedules.IstMorningShift], + defaultScheduleId: 1, + }, + { + ...TestData.users.example, + email: "example1@example.com", + id: 102, + schedules: [TestData.schedules.IstMidShift], + defaultScheduleId: 2, + }, + ], + bookings: [], + }); + + const scheduleWithEventCommonSch = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + startTime: `${plus1DateString}T18:30:00.000Z`, + endTime: `${plus2DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: true, + teamMemberEmail: "test@test.com", + }, + }); + + // expect only slots of IstEveningShift , as this is the common schedule of event + expect(scheduleWithEventCommonSch).toHaveTimeSlots( + [`11:30:00.000Z`, `12:30:00.000Z`, `13:30:00.000Z`, `14:30:00.000Z`, `15:30:00.000Z`], + { + dateString: plus2DateString, + } + ); + }); + }); }); diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index bb5362bd06..4461f1c8f9 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -78,6 +78,7 @@ type InputWorkflowReminder = { type InputHost = { userId: number; isFixed?: boolean; + scheduleId?: number | null; }; /** * Data to be mocked @@ -165,7 +166,7 @@ export type InputEventType = { }; requiresConfirmation?: boolean; destinationCalendar?: Prisma.DestinationCalendarCreateInput; - schedule?: InputUser["schedules"][number]; + schedule?: InputUser["schedules"][number] | null; bookingLimits?: IntervalLimit; durationLimits?: IntervalLimit; owner?: number; @@ -222,6 +223,13 @@ async function addHostsToDb(eventTypes: InputEventType[]) { id: host.userId, }, }, + schedule: host.scheduleId + ? { + connect: { + id: host.scheduleId, + }, + } + : undefined, }; await prismock.host.create({ diff --git a/packages/app-store/routing-forms/components/SingleForm.tsx b/packages/app-store/routing-forms/components/SingleForm.tsx index f0627897d6..8fd876f1cd 100644 --- a/packages/app-store/routing-forms/components/SingleForm.tsx +++ b/packages/app-store/routing-forms/components/SingleForm.tsx @@ -482,6 +482,7 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF avatar: member.avatarUrl || "", email: member.email, isFixed: true, + defaultScheduleId: member.defaultScheduleId, }))} value={sendUpdatesTo.map((userId) => ({ isFixed: true, @@ -489,6 +490,7 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF priority: 2, weight: 100, weightAdjustment: 0, + scheduleId: 1, }))} onChange={(value) => { hookForm.setValue( diff --git a/packages/app-store/routing-forms/lib/getSerializableForm.ts b/packages/app-store/routing-forms/lib/getSerializableForm.ts index 5ccacbd909..e6513ade4d 100644 --- a/packages/app-store/routing-forms/lib/getSerializableForm.ts +++ b/packages/app-store/routing-forms/lib/getSerializableForm.ts @@ -81,6 +81,7 @@ export async function getSerializableForm({ name: true, email: true, avatarUrl: true, + defaultScheduleId: true, }, }); } diff --git a/packages/app-store/routing-forms/types/types.d.ts b/packages/app-store/routing-forms/types/types.d.ts index 061c72d1f0..2ffc92450a 100644 --- a/packages/app-store/routing-forms/types/types.d.ts +++ b/packages/app-store/routing-forms/types/types.d.ts @@ -31,6 +31,7 @@ export type SerializableFormTeamMembers = { name: string | null; email: string; avatarUrl: string | null; + defaultScheduleId: number | null; }; export type SerializableForm = Omit< T, diff --git a/packages/core/getUserAvailability.ts b/packages/core/getUserAvailability.ts index 4562bb9008..8823c726a0 100644 --- a/packages/core/getUserAvailability.ts +++ b/packages/core/getUserAvailability.ts @@ -76,6 +76,21 @@ const _getEventType = async (id: number) => { user: { select: { email: true, + id: true, + }, + }, + schedule: { + select: { + availability: { + select: { + date: true, + startTime: true, + endTime: true, + days: true, + }, + }, + timeZone: true, + id: true, }, }, }, @@ -281,7 +296,9 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA (schedule) => !user?.defaultScheduleId || schedule.id === user?.defaultScheduleId )[0]; - const schedule = eventType?.schedule ? eventType.schedule : userSchedule; + const hostSchedule = eventType?.hosts?.find((host) => host.user.id === user.id)?.schedule; + + const schedule = eventType?.schedule ? eventType.schedule : hostSchedule ? hostSchedule : userSchedule; const timeZone = schedule?.timeZone || eventType?.timeZone || user.timeZone; @@ -356,7 +373,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA ...busyTimesFromTeamLimits, ]; - const isDefaultSchedule = userSchedule && userSchedule.id === schedule.id; + const isDefaultSchedule = userSchedule && userSchedule.id === schedule?.id; log.debug( "Using schedule:", @@ -364,6 +381,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA chosenSchedule: schedule, eventTypeSchedule: eventType?.schedule, userSchedule: userSchedule, + hostSchedule: hostSchedule, }) ); diff --git a/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts b/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts index 68b9f31bf2..ae3445a3d1 100644 --- a/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts +++ b/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts @@ -119,6 +119,20 @@ export const getEventTypesFromDB = async (eventTypeId: number) => { ...userSelect.select, }, }, + schedule: { + select: { + availability: { + select: { + date: true, + startTime: true, + endTime: true, + days: true, + }, + }, + timeZone: true, + id: true, + }, + }, }, }, availability: { diff --git a/packages/features/ee/round-robin/roundRobinReassignment.ts b/packages/features/ee/round-robin/roundRobinReassignment.ts index d3c2a21054..5b389dfd48 100644 --- a/packages/features/ee/round-robin/roundRobinReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinReassignment.ts @@ -101,6 +101,7 @@ export const roundRobinReassignment = async ({ priority: 2, weight: 100, weightAdjustment: 0, + schedule: null, })); const roundRobinHosts = eventType.hosts.filter((host) => !host.isFixed); diff --git a/packages/features/eventtypes/components/AddMembersWithSwitch.tsx b/packages/features/eventtypes/components/AddMembersWithSwitch.tsx index f273770eeb..bfb238aaed 100644 --- a/packages/features/eventtypes/components/AddMembersWithSwitch.tsx +++ b/packages/features/eventtypes/components/AddMembersWithSwitch.tsx @@ -16,16 +16,18 @@ interface IUserToValue { username: string | null; avatar: string; email: string; + defaultScheduleId: number | null; } export const mapUserToValue = ( - { id, name, username, avatar, email }: IUserToValue, + { id, name, username, avatar, email, defaultScheduleId }: IUserToValue, pendingString: string ) => ({ value: `${id || ""}`, label: `${name || email || ""}${!username ? ` (${pendingString})` : ""}`, avatar, email, + defaultScheduleId, }); const sortByLabel = (a: ReturnType, b: ReturnType) => { @@ -73,6 +75,7 @@ const CheckedHostField = ({ priority: option.priority ?? 2, weight: option.weight ?? 100, weightAdjustment: option.weightAdjustment ?? 0, + scheduleId: option.defaultScheduleId, })) ); }} diff --git a/packages/features/eventtypes/components/CheckedTeamSelect.tsx b/packages/features/eventtypes/components/CheckedTeamSelect.tsx index 094b086dac..690798d8a3 100644 --- a/packages/features/eventtypes/components/CheckedTeamSelect.tsx +++ b/packages/features/eventtypes/components/CheckedTeamSelect.tsx @@ -17,6 +17,7 @@ export type CheckedSelectOption = { weightAdjustment?: number; isFixed?: boolean; disabled?: boolean; + defaultScheduleId?: number | null; }; export const CheckedTeamSelect = ({ diff --git a/packages/features/eventtypes/components/EventType.tsx b/packages/features/eventtypes/components/EventType.tsx index c32ab3d0f3..552298c5e0 100644 --- a/packages/features/eventtypes/components/EventType.tsx +++ b/packages/features/eventtypes/components/EventType.tsx @@ -25,6 +25,7 @@ export type Host = { priority: number; weight: number; weightAdjustment: number; + scheduleId?: number | null; }; export type CustomInputParsed = typeof customInputSchema._output; @@ -46,6 +47,7 @@ const tabs = [ export type EventTypeSetup = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"]; export type EventTypeAssignedUsers = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"]["children"]; export type EventTypeHosts = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"]["hosts"]; +export type TeamMembers = RouterOutputs["viewer"]["eventTypes"]["get"]["teamMembers"]; export type EventTypeComponentProps = EventTypeSetupProps & { allActiveWorkflows?: Workflow[]; diff --git a/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx b/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx index 2c025529d6..4552170a44 100644 --- a/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx +++ b/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx @@ -132,19 +132,25 @@ const FixedHosts = ({ setAssignAllTeamMembers={setAssignAllTeamMembers} automaticAddAllEnabled={!isRoundRobinEvent} isFixed={true} - onActive={() => + onActive={() => { + const currentHosts = getValues("hosts"); setValue( "hosts", - teamMembers.map((teamMember) => ({ - isFixed: true, - userId: parseInt(teamMember.value, 10), - priority: 2, - weight: 100, - weightAdjustment: 0, - })), + teamMembers.map((teamMember) => { + const host = currentHosts.find((host) => host.userId === parseInt(teamMember.value, 10)); + return { + isFixed: true, + userId: parseInt(teamMember.value, 10), + priority: 2, + weight: 100, + weightAdjustment: 0, + // if host was already added, retain scheduleId + scheduleId: host?.scheduleId || teamMember.defaultScheduleId, + }; + }), { shouldDirty: true } - ) - } + ); + }} /> @@ -175,19 +181,25 @@ const FixedHosts = ({ setAssignAllTeamMembers={setAssignAllTeamMembers} automaticAddAllEnabled={!isRoundRobinEvent} isFixed={true} - onActive={() => + onActive={() => { + const currentHosts = getValues("hosts"); setValue( "hosts", - teamMembers.map((teamMember) => ({ - isFixed: true, - userId: parseInt(teamMember.value, 10), - priority: 2, - weight: 100, - weightAdjustment: 0, - })), + teamMembers.map((teamMember) => { + const host = currentHosts.find((host) => host.userId === parseInt(teamMember.value, 10)); + return { + isFixed: true, + userId: parseInt(teamMember.value, 10), + priority: 2, + weight: 100, + weightAdjustment: 0, + // if host was already added, retain scheduleId + scheduleId: host?.scheduleId || teamMember.defaultScheduleId, + }; + }), { shouldDirty: true } - ) - } + ); + }} /> @@ -255,15 +267,21 @@ const RoundRobinHosts = ({ isFixed={false} containerClassName={assignAllTeamMembers ? "-mt-4" : ""} onActive={() => { + const currentHosts = getValues("hosts"); setValue( "hosts", - teamMembers.map((teamMember) => ({ - isFixed: false, - userId: parseInt(teamMember.value, 10), - priority: 2, - weight: 100, - weightAdjustment: 0, - })), + teamMembers.map((teamMember) => { + const host = currentHosts.find((host) => host.userId === parseInt(teamMember.value, 10)); + return { + isFixed: false, + userId: parseInt(teamMember.value, 10), + priority: 2, + weight: 100, + weightAdjustment: 0, + // if host was already added, retain scheduleId + scheduleId: host?.scheduleId || teamMember.defaultScheduleId, + }; + }), { shouldDirty: true } ); setValue("isRRWeightsEnabled", false); @@ -346,6 +364,21 @@ const Hosts = ({ ); }, [schedulingType, setValue, getValues, submitCount]); + // To ensure existing host do not loose its scheduleId property, whenever a new host of same type is added. + // This is because the host is created from list option in CheckedHostField component. + const updatedHosts = (changedHosts: Host[]) => { + const existingHosts = getValues("hosts"); + return changedHosts.map((newValue) => { + const existingHost = existingHosts.find((host: Host) => host.userId === newValue.userId); + return existingHost + ? { + ...newValue, + scheduleId: existingHost.scheduleId, + } + : newValue; + }); + }; + return ( name="hosts" @@ -355,7 +388,9 @@ const Hosts = ({ { + onChange([...updatedHosts(changeValue)]); + }} assignAllTeamMembers={assignAllTeamMembers} setAssignAllTeamMembers={setAssignAllTeamMembers} /> @@ -366,7 +401,7 @@ const Hosts = ({ teamMembers={teamMembers} value={value} onChange={(changeValue) => { - onChange([...value.filter((host: Host) => !host.isFixed), ...changeValue]); + onChange([...value.filter((host: Host) => !host.isFixed), ...updatedHosts(changeValue)]); }} assignAllTeamMembers={assignAllTeamMembers} setAssignAllTeamMembers={setAssignAllTeamMembers} @@ -376,7 +411,7 @@ const Hosts = ({ teamMembers={teamMembers} value={value} onChange={(changeValue) => { - const hosts = [...value.filter((host: Host) => host.isFixed), ...changeValue]; + const hosts = [...value.filter((host: Host) => host.isFixed), ...updatedHosts(changeValue)]; onChange(hosts); }} assignAllTeamMembers={assignAllTeamMembers} diff --git a/packages/features/eventtypes/components/tabs/availability/EventAvailabilityTab.tsx b/packages/features/eventtypes/components/tabs/availability/EventAvailabilityTab.tsx index 9b9c723054..10cc891fe8 100644 --- a/packages/features/eventtypes/components/tabs/availability/EventAvailabilityTab.tsx +++ b/packages/features/eventtypes/components/tabs/availability/EventAvailabilityTab.tsx @@ -1,20 +1,31 @@ +import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useState, memo, useEffect } from "react"; import { Controller, useFormContext } from "react-hook-form"; import type { OptionProps, SingleValueProps } from "react-select"; import { components } from "react-select"; -import type { EventAvailabilityTabWebWrapperProps } from "@calcom/atoms/event-types/wrappers/EventAvailabilityTabWebWrapper"; +import type { + EventAvailabilityTabWebWrapperProps, + GetAllSchedulesByUserIdQueryType, +} from "@calcom/atoms/event-types/wrappers/EventAvailabilityTabWebWrapper"; import dayjs from "@calcom/dayjs"; import { SelectSkeletonLoader } from "@calcom/features/availability/components/SkeletonLoader"; import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager"; -import type { AvailabilityOption, FormValues, EventTypeSetup } from "@calcom/features/eventtypes/lib/types"; +import type { TeamMembers } from "@calcom/features/eventtypes/components/EventType"; +import type { + AvailabilityOption, + FormValues, + EventTypeSetup, + Host, +} from "@calcom/features/eventtypes/lib/types"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { weekdayNames } from "@calcom/lib/weekday"; import { weekStartNum } from "@calcom/lib/weekstart"; import { SchedulingType } from "@calcom/prisma/enums"; import type { RouterOutputs } from "@calcom/trpc/react"; -import { Badge, Button, Icon, Select, SettingsToggle, SkeletonText } from "@calcom/ui"; +import { Avatar, Badge, Button, Icon, Label, Select, SettingsToggle, SkeletonText } from "@calcom/ui"; +import { Spinner } from "@calcom/ui/components/icon/Spinner"; type EventTypeScheduleDetailsProps = { scheduleQueryData?: RouterOutputs["viewer"]["availability"]["schedule"]["get"]; @@ -23,11 +34,17 @@ type EventTypeScheduleDetailsProps = { editAvailabilityRedirectUrl?: string; }; +type EventTypeTeamScheduleProps = { + hostSchedulesQuery: GetAllSchedulesByUserIdQueryType; + hosts?: Host[]; +}; + type EventTypeScheduleProps = { availabilityQueryData?: RouterOutputs["viewer"]["availability"]["list"]; isAvailabilityPending?: boolean; eventType: EventTypeSetup; -} & EventTypeScheduleDetailsProps; +} & EventTypeScheduleDetailsProps & + EventTypeTeamScheduleProps; type EventAvailabilityTabProps = EventAvailabilityTabWebWrapperProps & EventTypeScheduleProps; @@ -270,32 +287,172 @@ const EventTypeSchedule = ({ ); }; +const TeamMemberSchedule = ({ + host, + index, + teamMembers, + hostScheduleQuery, +}: { + host: Host; + index: number; + teamMembers: TeamMembers; + hostScheduleQuery: GetAllSchedulesByUserIdQueryType; +}) => { + const { t } = useLocale(); + + const formMethods = useFormContext(); + const { getValues } = formMethods; + + const { data, isPending } = hostScheduleQuery({ + userId: host.userId, + }); + + const schedules = data?.schedules; + const options = schedules?.map((schedule) => ({ + value: schedule.id, + label: schedule.name, + isDefault: schedule.isDefault, + isManaged: false, + })); + + //Set to defaultSchedule if Host Schedule is not previously selected + const scheduleId = getValues(`hosts.${index}.scheduleId`); + const value = options?.find((option) => + scheduleId + ? option.value === scheduleId + : option.value === schedules?.find((schedule) => schedule.isDefault)?.id + ); + + const member = teamMembers.find((mem) => mem.id === host.userId); + const avatar = member?.avatar; + const label = member?.name; + + return ( + <> +
+ +

{label}

+
+
+ {isPending ? ( + + ) : ( + { + return ( +