* add feature to change host schedule * update for type-check:ci * Update SingleForm.tsx for linterr * update as per design review * update for rerendering * migration files * On Delete set null for schedule if schedule is deleted, no need to delete the host * updated schema * added testcases * checking permissions before * updated for comments * update for initial value * updated and cleanup * set initial value * update after merge * updated nit style * update for console warning and added comments * nit/chore * chore * update for typechecks * update for scheduleId param after merge * update after merge or new changes in metadata.config * update after weights functionality * updated description text and column name as per review suggestion * resolved merge conflicts * fix typecheck due to changes in getScheduleSchema * update after merge having refactor * update refactored EventTypeWebWrapper after merge * updated props * updated trpc err code and added log * update due to refactor in EventAvailabilityTab * perf: Optimise TS type + select only the data that's needed for a specific function --------- Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com> Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
co-authored by
Amit Sharma
Syed Ali Shahbaz
Alex van Andel
parent
c21f0caecf
commit
a48d190af3
@@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -81,6 +81,7 @@ export async function getSerializableForm<TForm extends App_RoutingForms_Form>({
|
||||
name: true,
|
||||
email: true,
|
||||
avatarUrl: true,
|
||||
defaultScheduleId: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export type SerializableFormTeamMembers = {
|
||||
name: string | null;
|
||||
email: string;
|
||||
avatarUrl: string | null;
|
||||
defaultScheduleId: number | null;
|
||||
};
|
||||
export type SerializableForm<T extends App_RoutingForms_Form> = Omit<
|
||||
T,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -101,6 +101,7 @@ export const roundRobinReassignment = async ({
|
||||
priority: 2,
|
||||
weight: 100,
|
||||
weightAdjustment: 0,
|
||||
schedule: null,
|
||||
}));
|
||||
|
||||
const roundRobinHosts = eventType.hosts.filter((host) => !host.isFixed);
|
||||
|
||||
@@ -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<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
|
||||
@@ -73,6 +75,7 @@ const CheckedHostField = ({
|
||||
priority: option.priority ?? 2,
|
||||
weight: option.weight ?? 100,
|
||||
weightAdjustment: option.weightAdjustment ?? 0,
|
||||
scheduleId: option.defaultScheduleId,
|
||||
}))
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -17,6 +17,7 @@ export type CheckedSelectOption = {
|
||||
weightAdjustment?: number;
|
||||
isFixed?: boolean;
|
||||
disabled?: boolean;
|
||||
defaultScheduleId?: number | null;
|
||||
};
|
||||
|
||||
export const CheckedTeamSelect = ({
|
||||
|
||||
@@ -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[];
|
||||
|
||||
+65
-30
@@ -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 }
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
@@ -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 (
|
||||
<Controller<FormValues>
|
||||
name="hosts"
|
||||
@@ -355,7 +388,9 @@ const Hosts = ({
|
||||
<FixedHosts
|
||||
teamMembers={teamMembers}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onChange={(changeValue) => {
|
||||
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}
|
||||
|
||||
+177
-20
@@ -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<FormValues>();
|
||||
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 (
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<Avatar size="sm" imageSrc={avatar} alt={label || ""} />
|
||||
<p className="text-emphasis my-auto ms-3 text-sm">{label}</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-col pt-2 ">
|
||||
{isPending ? (
|
||||
<Spinner className="mt-2 h-6 w-6" />
|
||||
) : (
|
||||
<Controller
|
||||
name={`hosts.${index}.scheduleId`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Select
|
||||
placeholder={t("select")}
|
||||
options={options}
|
||||
isSearchable={false}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
}}
|
||||
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||
value={value as AvailabilityOption}
|
||||
components={{ Option, SingleValue }}
|
||||
isMulti={false}
|
||||
isDisabled={isPending}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const TeamAvailability = ({
|
||||
hosts,
|
||||
teamMembers,
|
||||
hostSchedulesQuery,
|
||||
}: EventTypeTeamScheduleProps & { teamMembers: TeamMembers }) => {
|
||||
const { t } = useLocale();
|
||||
const [animationRef] = useAutoAnimate<HTMLUListElement>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-subtle flex flex-col rounded-md">
|
||||
<div className="border-subtle mt-5 rounded-t-md border p-6 pb-5">
|
||||
<Label className="mb-1 text-sm font-semibold">{t("choose_hosts_schedule")}</Label>
|
||||
<p className="text-subtle max-w-full break-words text-sm leading-tight">
|
||||
{t("hosts_schedule_description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-subtle rounded-b-md border border-t-0 p-6">
|
||||
{hosts && hosts.length > 0 ? (
|
||||
<ul
|
||||
className={classNames("mb-4 mt-3 rounded-md", hosts.length >= 1 && "border-subtle border")}
|
||||
ref={animationRef}>
|
||||
{hosts?.map((host, index) => (
|
||||
<li
|
||||
key={host.userId}
|
||||
className={`flex flex-col px-3 py-2 ${
|
||||
index === hosts.length - 1 ? "" : "border-subtle border-b"
|
||||
}`}>
|
||||
<TeamMemberSchedule
|
||||
host={host}
|
||||
index={index}
|
||||
teamMembers={teamMembers}
|
||||
hostScheduleQuery={hostSchedulesQuery}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-subtle max-w-full break-words text-sm leading-tight">
|
||||
{t("no_hosts_description")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UseCommonScheduleSettingsToggle = ({
|
||||
eventType,
|
||||
...rest
|
||||
}: Omit<EventAvailabilityTabProps, "isTeamEvent">) => {
|
||||
const { t } = useLocale();
|
||||
const { setValue, resetField, getFieldState, getValues } = useFormContext<FormValues>();
|
||||
const { setValue, resetField, getFieldState, getValues, watch } = useFormContext<FormValues>();
|
||||
|
||||
const [useHostSchedulesForTeamEvent, setUseHostSchedulesForTeamEvent] = useState(
|
||||
Boolean(getFieldState("schedule").isDirty ? getValues("schedule") : eventType.schedule)
|
||||
!Boolean(getFieldState("schedule").isDirty ? getValues("schedule") : eventType.schedule)
|
||||
);
|
||||
|
||||
const watchHosts = watch("hosts");
|
||||
|
||||
return (
|
||||
<SettingsToggle
|
||||
checked={useHostSchedulesForTeamEvent}
|
||||
onCheckedChange={(checked) => {
|
||||
setUseHostSchedulesForTeamEvent(checked);
|
||||
if (checked) {
|
||||
if (Boolean(eventType.schedule)) resetField("schedule");
|
||||
} else {
|
||||
setValue("schedule", null, { shouldDirty: Boolean(eventType.schedule) });
|
||||
}
|
||||
}}
|
||||
title={t("choose_common_schedule_team_event")}
|
||||
description={t("choose_common_schedule_team_event_description")}>
|
||||
<EventTypeSchedule eventType={eventType} {...rest} />
|
||||
</SettingsToggle>
|
||||
<>
|
||||
<SettingsToggle
|
||||
checked={!useHostSchedulesForTeamEvent}
|
||||
onCheckedChange={(checked) => {
|
||||
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) });
|
||||
}
|
||||
}}
|
||||
title={t("choose_common_schedule_team_event")}
|
||||
description={t("choose_common_schedule_team_event_description")}>
|
||||
<EventTypeSchedule eventType={eventType} {...rest} />
|
||||
</SettingsToggle>
|
||||
{useHostSchedulesForTeamEvent && (
|
||||
<div className="lg:ml-14">
|
||||
<TeamAvailability
|
||||
hosts={watchHosts}
|
||||
teamMembers={rest.teamMembers}
|
||||
hostSchedulesQuery={rest.hostSchedulesQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -27,12 +27,14 @@ export type Host = {
|
||||
priority: number;
|
||||
weight: number;
|
||||
weightAdjustment: number;
|
||||
scheduleId?: number | null;
|
||||
};
|
||||
export type TeamMember = {
|
||||
value: string;
|
||||
label: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
defaultScheduleId: number | null;
|
||||
};
|
||||
|
||||
export type FormValues = {
|
||||
|
||||
@@ -535,6 +535,7 @@ export class EventTypeRepository {
|
||||
priority: true,
|
||||
weight: true,
|
||||
weightAdjustment: true,
|
||||
scheduleId: true,
|
||||
},
|
||||
},
|
||||
userId: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { TeamMembers } from "@calcom/features/eventtypes/components/EventType";
|
||||
import { EventAvailabilityTab } from "@calcom/features/eventtypes/components/tabs/availability/EventAvailabilityTab";
|
||||
import type { EventTypeSetup, FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -11,8 +12,12 @@ export type EventAvailabilityTabWebWrapperProps = {
|
||||
eventType: EventTypeSetup;
|
||||
isTeamEvent: boolean;
|
||||
loggedInUser?: RouterOutputs["viewer"]["me"];
|
||||
teamMembers: TeamMembers;
|
||||
};
|
||||
|
||||
export type GetAllSchedulesByUserIdQueryType =
|
||||
typeof trpc.viewer.availability.schedule.getAllSchedulesByUserId.useQuery;
|
||||
|
||||
const EventAvailabilityTabWebWrapper = (props: EventAvailabilityTabWebWrapperProps) => {
|
||||
const { t } = useLocale();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
@@ -36,6 +41,8 @@ const EventAvailabilityTabWebWrapper = (props: EventAvailabilityTabWebWrapperPro
|
||||
const { data: availabilityQueryData, isPending: isAvailabilityPending } =
|
||||
trpc.viewer.availability.list.useQuery(undefined);
|
||||
|
||||
const hostSchedulesQuery = trpc.viewer.availability.schedule.getAllSchedulesByUserId.useQuery;
|
||||
|
||||
return (
|
||||
<EventAvailabilityTab
|
||||
{...props}
|
||||
@@ -44,6 +51,7 @@ const EventAvailabilityTabWebWrapper = (props: EventAvailabilityTabWebWrapperPro
|
||||
isSchedulePending={isSchedulePending}
|
||||
scheduleQueryData={scheduleQueryData}
|
||||
editAvailabilityRedirectUrl={`/availability/${scheduleQueryData?.id}`}
|
||||
hostSchedulesQuery={hostSchedulesQuery}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -242,7 +242,12 @@ const EventTypeWeb = ({
|
||||
/>
|
||||
),
|
||||
availability: (
|
||||
<EventAvailabilityTab eventType={eventType} isTeamEvent={!!team} loggedInUser={loggedInUser} />
|
||||
<EventAvailabilityTab
|
||||
eventType={eventType}
|
||||
isTeamEvent={!!team}
|
||||
loggedInUser={loggedInUser}
|
||||
teamMembers={teamMembers}
|
||||
/>
|
||||
),
|
||||
team: <EventTeamAssignmentTab teamMembers={teamMembers} team={team} eventType={eventType} />,
|
||||
limits: <EventLimitsTab eventType={eventType} />,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Host" ADD COLUMN "scheduleId" INTEGER;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Host_scheduleId_idx" ON "Host"("scheduleId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Host" ADD CONSTRAINT "Host_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "Schedule"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -52,10 +52,13 @@ model Host {
|
||||
weight Int?
|
||||
// amount of fake bookings to be added when calculating the weight (for new hosts, OOO, etc.)
|
||||
weightAdjustment Int?
|
||||
schedule Schedule? @relation(fields: [scheduleId], references: [id])
|
||||
scheduleId Int?
|
||||
|
||||
@@id([userId, eventTypeId])
|
||||
@@index([userId])
|
||||
@@index([eventTypeId])
|
||||
@@index([scheduleId])
|
||||
}
|
||||
|
||||
model EventType {
|
||||
@@ -645,6 +648,7 @@ model Schedule {
|
||||
name String
|
||||
timeZone String?
|
||||
availability Availability[]
|
||||
Host Host[]
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ZCreateInputSchema } from "./create.schema";
|
||||
import { ZDeleteInputSchema } from "./delete.schema";
|
||||
import { ZScheduleDuplicateSchema } from "./duplicate.schema";
|
||||
import { ZGetInputSchema } from "./get.schema";
|
||||
import { ZGetAllByUserIdInputSchema } from "./getAllSchedulesByUserId.schema";
|
||||
import { ZGetByEventSlugInputSchema } from "./getScheduleByEventTypeSlug.schema";
|
||||
import { ZGetByUserIdInputSchema } from "./getScheduleByUserId.schema";
|
||||
import { ZUpdateInputSchema } from "./update.schema";
|
||||
@@ -16,6 +17,7 @@ type ScheduleRouterHandlerCache = {
|
||||
update?: typeof import("./update.handler").updateHandler;
|
||||
duplicate?: typeof import("./duplicate.handler").duplicateHandler;
|
||||
getScheduleByUserId?: typeof import("./getScheduleByUserId.handler").getScheduleByUserIdHandler;
|
||||
getAllSchedulesByUserId?: typeof import("./getAllSchedulesByUserId.handler").getAllSchedulesByUserIdHandler;
|
||||
getScheduleByEventSlug?: typeof import("./getScheduleByEventTypeSlug.handler").getScheduleByEventSlugHandler;
|
||||
bulkUpdateToDefaultAvailability?: typeof import("./bulkUpdateDefaultAvailability.handler").bulkUpdateToDefaultAvailabilityHandler;
|
||||
};
|
||||
@@ -122,6 +124,25 @@ export const scheduleRouter = router({
|
||||
input,
|
||||
});
|
||||
}),
|
||||
|
||||
getAllSchedulesByUserId: authedProcedure.input(ZGetAllByUserIdInputSchema).query(async ({ input, ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.getAllSchedulesByUserId) {
|
||||
UNSTABLE_HANDLER_CACHE.getAllSchedulesByUserId = await import("./getAllSchedulesByUserId.handler").then(
|
||||
(mod) => mod.getAllSchedulesByUserIdHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.getAllSchedulesByUserId) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.getAllSchedulesByUserId({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
}),
|
||||
|
||||
getScheduleByEventSlug: authedProcedure.input(ZGetByEventSlugInputSchema).query(async ({ input, ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.getScheduleByEventSlug) {
|
||||
UNSTABLE_HANDLER_CACHE.getScheduleByEventSlug = await import(
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { hasReadPermissionsForUserId } from "@calcom/lib/hasEditPermissionForUser";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../../trpc";
|
||||
import { getDefaultScheduleId } from "../util";
|
||||
import type { TGetAllByUserIdInputSchema } from "./getAllSchedulesByUserId.schema";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["getAllSchedulesByUserIdHandler"] });
|
||||
|
||||
type GetOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
input: TGetAllByUserIdInputSchema;
|
||||
};
|
||||
|
||||
export const getAllSchedulesByUserIdHandler = async ({ ctx, input }: GetOptions) => {
|
||||
const { user } = ctx;
|
||||
|
||||
const isCurrentUserPartOfTeam = hasReadPermissionsForUserId({ memberId: input?.userId, userId: user.id });
|
||||
|
||||
const isCurrentUserOwner = input?.userId === user.id;
|
||||
|
||||
if (!isCurrentUserPartOfTeam && !isCurrentUserOwner) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
});
|
||||
}
|
||||
|
||||
const schedules = await prisma.schedule.findMany({
|
||||
where: {
|
||||
userId: input.userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!schedules) {
|
||||
log.error(`No Schedules found for userId:${input.userId}`);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
});
|
||||
}
|
||||
|
||||
const defaultScheduleId = await getDefaultScheduleId(input.userId, prisma);
|
||||
|
||||
return {
|
||||
schedules: schedules.map((schedule) => {
|
||||
return {
|
||||
...schedule,
|
||||
isDefault: schedule.id === defaultScheduleId,
|
||||
readOnly: schedule.userId !== user.id,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZGetAllByUserIdInputSchema = z.object({
|
||||
userId: z.number(),
|
||||
});
|
||||
|
||||
export type TGetAllByUserIdInputSchema = z.infer<typeof ZGetAllByUserIdInputSchema>;
|
||||
@@ -57,6 +57,7 @@ export const EventTypeUpdateInput = _EventTypeModel
|
||||
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(),
|
||||
|
||||
@@ -191,6 +191,7 @@ type Host = {
|
||||
isFixed?: boolean | undefined;
|
||||
priority?: number | null | undefined;
|
||||
weight?: number | null | undefined;
|
||||
scheduleId?: number | null | undefined;
|
||||
};
|
||||
|
||||
type User = {
|
||||
@@ -230,6 +231,7 @@ export async function addWeightAdjustmentToNewHosts({
|
||||
weightAdjustment: true,
|
||||
priority: true,
|
||||
weight: true,
|
||||
scheduleId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -249,6 +251,7 @@ export async function addWeightAdjustmentToNewHosts({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
},
|
||||
scheduleId: hostData?.scheduleId ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -263,6 +266,7 @@ export async function addWeightAdjustmentToNewHosts({
|
||||
priority: host.priority,
|
||||
weight: host.weight,
|
||||
weightAdjustment: host.weightAdjustment,
|
||||
scheduleId: host.scheduleId,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -303,6 +307,7 @@ export async function addWeightAdjustmentToNewHosts({
|
||||
priority: host.priority,
|
||||
weight: host.weight,
|
||||
weightAdjustment: weightAdjustment > 0 ? Math.floor(weightAdjustment) : 0,
|
||||
scheduleId: host.scheduleId,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -228,6 +228,20 @@ export async function getEventType(
|
||||
...availabilityUserSelect,
|
||||
},
|
||||
},
|
||||
schedule: {
|
||||
select: {
|
||||
availability: {
|
||||
select: {
|
||||
date: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
days: true,
|
||||
},
|
||||
},
|
||||
timeZone: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
@@ -444,9 +458,17 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro
|
||||
}
|
||||
let currentSeats: CurrentSeats | undefined;
|
||||
|
||||
const eventHosts =
|
||||
const eventHosts: {
|
||||
isFixed: boolean;
|
||||
email: string;
|
||||
user: (typeof eventType.hosts)[number]["user"];
|
||||
}[] =
|
||||
eventType.hosts?.length && eventType.schedulingType
|
||||
? eventType.hosts
|
||||
? eventType.hosts.map((host) => ({
|
||||
isFixed: host.isFixed,
|
||||
email: host.user.email,
|
||||
user: host.user,
|
||||
}))
|
||||
: eventType.users.map((user) => {
|
||||
return {
|
||||
isFixed: !eventType.schedulingType || eventType.schedulingType === SchedulingType.COLLECTIVE,
|
||||
|
||||
Reference in New Issue
Block a user