feat: to enable optimized slots (#16579)
* feature to enable optimized slots * type check (was not caught running locally) * update for typechecks * update for type checks * initialize form * update slots test for showOptimizedSlots * added some more tests * handled edge case * added test for edge case * generalized condition * refactored instead of ternary and added comments * update condition * Update apps/web/public/static/locales/en/common.json Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * moved showOptimizedSlots setting to advanced tab * updated to always move to next 15min if not possible to next interval * correct after merge * chore * fix typecheck after latest merges * remove workinghours when dateRanges input * refactor: extract to fn * chore * remove unrelated changes * remove unrealted change * updated tests * unrelated format changes no-verify * unrelated format changes no-verify * handle current day booking cases, to show roundedoff minutes * remove unrelated format changes in openapi.json * add test for new condition * remove unrelated auto-format changes * remove unrelated auto-format changes * update after merge * For current day bookings, normalizing the seconds to zero to avoid issues with time calculations * Update packages/platform/atoms/event-types/hooks/useEventTypeForm.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * update for typecheck due to latest changes * resolve merge conflicts * Smaller description * Minor style change to prefer early return --------- Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com> Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com> Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
co-authored by
Carina Wollendorfer
coderabbitai[bot]
Anik Dhabal Babu
CarinaWolli
Devanshu Sharma
Volnei Munhoz
Keith Williams
Alex van Andel
parent
bfa23ddb80
commit
0bcb81bedd
@@ -3511,6 +3511,8 @@
|
||||
"file_upload_instructions": "Accepted types: {{acceptedTypes}}; Max file size: {{maxSize}}",
|
||||
"license_key_saved": "License key saved successfully",
|
||||
"timezone_mismatch_tooltip": "You are viewing the report based on your profile timezone ({{userTimezone}}), while your browser is set to timezone ({{browserTimezone}})",
|
||||
"show_optimized_slots": "Optimized slots",
|
||||
"show_optimized_slots_description": "Arrange time slots to optimize availability.",
|
||||
"failed_bookings_by_field": "Failed Bookings By Field",
|
||||
"failed_bookings": "Failed bookings",
|
||||
"event_type_no_hosts": "No hosts are assigned to event type",
|
||||
|
||||
@@ -121,6 +121,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
autoTranslateDescriptionEnabled,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
showOptimizedSlots,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
id: 123,
|
||||
@@ -182,6 +183,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
assignRRMembersUsingSegment,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
showOptimizedSlots,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
@@ -292,6 +294,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
assignRRMembersUsingSegment,
|
||||
showOptimizedSlots,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
id: 123,
|
||||
@@ -356,6 +359,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
assignRRMembersUsingSegment,
|
||||
rrSegmentQueryValue,
|
||||
useEventLevelSelectedCalendars,
|
||||
showOptimizedSlots,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
@@ -421,6 +425,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
assignRRMembersUsingSegment,
|
||||
showOptimizedSlots,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
@@ -444,6 +449,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
requiresBookerEmailVerification: false,
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
useEventTypeDestinationCalendarEmail: false,
|
||||
showOptimizedSlots: false,
|
||||
workflows: [],
|
||||
parentId: 1,
|
||||
locations: [],
|
||||
|
||||
@@ -522,6 +522,8 @@ export const EventAdvancedTab = ({
|
||||
eventType.allowReschedulingCancelledBookings ?? false
|
||||
);
|
||||
|
||||
const showOptimizedSlotsLocked = shouldLockDisableProps("showOptimizedSlots");
|
||||
|
||||
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||
|
||||
const [isEventTypeColorChecked, setIsEventTypeColorChecked] = useState(!!eventType.eventTypeColor);
|
||||
@@ -1264,6 +1266,29 @@ export const EventAdvancedTab = ({
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="showOptimizedSlots"
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = value;
|
||||
return (
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
labelClassName="text-sm"
|
||||
title={t("show_optimized_slots")}
|
||||
description={t("show_optimized_slots_description")}
|
||||
checked={isChecked}
|
||||
{...showOptimizedSlotsLocked}
|
||||
onCheckedChange={(active) => {
|
||||
onChange(active ?? false);
|
||||
}}
|
||||
switchContainerClassName={classNames(
|
||||
"border-subtle rounded-lg border py-6 px-4 sm:px-6",
|
||||
isChecked && "rounded-b-none"
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{isRoundRobinEventType && (
|
||||
<Controller
|
||||
name="rescheduleWithSameRoundRobinHost"
|
||||
|
||||
@@ -146,6 +146,7 @@ export type FormValues = {
|
||||
durationLimits?: IntervalLimit;
|
||||
bookingLimits?: IntervalLimit;
|
||||
onlyShowFirstAvailableSlot: boolean;
|
||||
showOptimizedSlots: boolean;
|
||||
children: ChildrenEventType[];
|
||||
hosts: Host[];
|
||||
hostGroups: {
|
||||
|
||||
@@ -94,6 +94,7 @@ const commons = {
|
||||
onlyShowFirstAvailableSlot: false,
|
||||
allowReschedulingPastBookings: false,
|
||||
hideOrganizerEmail: false,
|
||||
showOptimizedSlots: false,
|
||||
id: 0,
|
||||
hideCalendarNotes: false,
|
||||
hideCalendarEventDetails: false,
|
||||
|
||||
@@ -48,6 +48,7 @@ export const eventTypeSelect = {
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
allowReschedulingPastBookings: true,
|
||||
hideOrganizerEmail: true,
|
||||
showOptimizedSlots: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
scheduleId: true,
|
||||
|
||||
@@ -577,6 +577,7 @@ export class EventTypeRepository {
|
||||
eventTypeColor: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
showOptimizedSlots: true,
|
||||
durationLimits: true,
|
||||
maxActiveBookingsPerBooker: true,
|
||||
maxActiveBookingPerBookerOfferReschedule: true,
|
||||
@@ -873,6 +874,7 @@ export class EventTypeRepository {
|
||||
eventTypeColor: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
showOptimizedSlots: true,
|
||||
durationLimits: true,
|
||||
maxActiveBookingsPerBooker: true,
|
||||
maxActiveBookingPerBookerOfferReschedule: true,
|
||||
@@ -1198,6 +1200,7 @@ export class EventTypeRepository {
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
allowReschedulingPastBookings: true,
|
||||
hideOrganizerEmail: true,
|
||||
showOptimizedSlots: true,
|
||||
periodCountCalendarDays: true,
|
||||
rescheduleWithSameRoundRobinHost: true,
|
||||
periodDays: true,
|
||||
|
||||
@@ -399,3 +399,445 @@ describe("Tests the slots function performance", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests the date-range slot logic with showOptimizedSlots", () => {
|
||||
it("should respect start of the hour for 60minutes slots for availabilities like 09:30-17:00, when showOptimizedSlots set to true", async () => {
|
||||
const slots = getSlots({
|
||||
showOptimizedSlots: true,
|
||||
inviteeDate: dayjs.tz("2024-09-12T00:00:00.000-07:00", "America/Los_Angeles"),
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
// equivalent dateRanges in UTC-7
|
||||
// start:09:30, end:17:00
|
||||
// with 09:00 - 09:30 busy time
|
||||
dateRanges: [{ start: dayjs("2024-09-12T16:30:00.000Z"), end: dayjs("2024-09-13T00:00:00.000Z") }],
|
||||
minimumBookingNotice: 0,
|
||||
frequency: 60,
|
||||
});
|
||||
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2024-09-12T10:00:00-07:00",
|
||||
"2024-09-12T11:00:00-07:00",
|
||||
"2024-09-12T12:00:00-07:00",
|
||||
"2024-09-12T13:00:00-07:00",
|
||||
"2024-09-12T14:00:00-07:00",
|
||||
"2024-09-12T15:00:00-07:00",
|
||||
"2024-09-12T16:00:00-07:00",
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows 4 60minutes slots for availability like 08:00-12:30 with 10:00-10:10 busytime, when showOptimizedSlots set to true", async () => {
|
||||
const slots = getSlots({
|
||||
showOptimizedSlots: true,
|
||||
inviteeDate: dayjs.tz("2024-09-12T00:00:00.000-07:00", "America/Los_Angeles"),
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
// equivalent dateRanges in UTC-7
|
||||
// start:08:00, end:10:00
|
||||
// start:10:10, end:12:30
|
||||
// with 10:00 - 10:10 busy time
|
||||
dateRanges: [
|
||||
{ start: dayjs("2024-09-12T15:00:00.000Z"), end: dayjs("2024-09-12T17:00:00.000Z") },
|
||||
{ start: dayjs("2024-09-12T17:10:00.000Z"), end: dayjs("2024-09-12T19:30:00.000Z") },
|
||||
],
|
||||
minimumBookingNotice: 0,
|
||||
frequency: 60,
|
||||
});
|
||||
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2024-09-12T08:00:00-07:00",
|
||||
"2024-09-12T09:00:00-07:00",
|
||||
"2024-09-12T10:15:00-07:00",
|
||||
"2024-09-12T11:15:00-07:00",
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows 3 60minutes slots for availability like 08:00-12:30 with 10:00-10:10 busytime, when showOptimizedSlots set to false", async () => {
|
||||
const slots = getSlots({
|
||||
showOptimizedSlots: false,
|
||||
inviteeDate: dayjs.tz("2024-09-12T00:00:00.000-07:00", "America/Los_Angeles"),
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
// equivalent dateRanges in UTC-7
|
||||
// start:08:00, end:10:00
|
||||
// start:10:10, end:12:30
|
||||
// with 10:00 - 10:10 busy time
|
||||
dateRanges: [
|
||||
{ start: dayjs("2024-09-12T15:00:00.000Z"), end: dayjs("2024-09-12T17:00:00.000Z") },
|
||||
{ start: dayjs("2024-09-12T17:10:00.000Z"), end: dayjs("2024-09-12T19:30:00.000Z") },
|
||||
],
|
||||
minimumBookingNotice: 0,
|
||||
frequency: 60,
|
||||
});
|
||||
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2024-09-12T08:00:00-07:00",
|
||||
"2024-09-12T09:00:00-07:00",
|
||||
"2024-09-12T11:00:00-07:00",
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows 3 60minutes slots for availability like 08:00-12:00 with 10:00-10:10 busytime, when showOptimizedSlots set to true", async () => {
|
||||
const slots = getSlots({
|
||||
showOptimizedSlots: true,
|
||||
inviteeDate: dayjs.tz("2024-09-12T00:00:00.000-07:00", "America/Los_Angeles"),
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
// equivalent dateRanges in UTC-7
|
||||
// start:08:00, end:10:00
|
||||
// start:10:10, end:12:00
|
||||
// with 10:00 - 10:10 busy time
|
||||
dateRanges: [
|
||||
{ start: dayjs("2024-09-12T15:00:00.000Z"), end: dayjs("2024-09-12T17:00:00.000Z") },
|
||||
{ start: dayjs("2024-09-12T17:10:00.000Z"), end: dayjs("2024-09-12T19:00:00.000Z") },
|
||||
],
|
||||
minimumBookingNotice: 0,
|
||||
frequency: 60,
|
||||
});
|
||||
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2024-09-12T08:00:00-07:00",
|
||||
"2024-09-12T09:00:00-07:00",
|
||||
"2024-09-12T11:00:00-07:00",
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows 3 60minutes slots for availability like 08:00-12:00 with 10:00-10:10 busytime, when showOptimizedSlots set to false", async () => {
|
||||
const slots = getSlots({
|
||||
showOptimizedSlots: false,
|
||||
inviteeDate: dayjs.tz("2024-09-12T00:00:00.000-07:00", "America/Los_Angeles"),
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
// equivalent dateRanges in UTC-7
|
||||
// start:08:00, end:10:00
|
||||
// start:10:10, end:12:00
|
||||
// with 10:00 - 10:10 busy time
|
||||
dateRanges: [
|
||||
{ start: dayjs("2024-09-12T15:00:00.000Z"), end: dayjs("2024-09-12T17:00:00.000Z") },
|
||||
{ start: dayjs("2024-09-12T17:10:00.000Z"), end: dayjs("2024-09-12T19:00:00.000Z") },
|
||||
],
|
||||
minimumBookingNotice: 0,
|
||||
frequency: 60,
|
||||
});
|
||||
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2024-09-12T08:00:00-07:00",
|
||||
"2024-09-12T09:00:00-07:00",
|
||||
"2024-09-12T11:00:00-07:00",
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows 9 30minutes slots for availability like 08:00-12:45 with 10:00-10:10 busytime, when showOptimizedSlots set to true", async () => {
|
||||
const slots = getSlots({
|
||||
showOptimizedSlots: true,
|
||||
inviteeDate: dayjs.tz("2024-09-12T00:00:00.000-07:00", "America/Los_Angeles"),
|
||||
eventLength: 30,
|
||||
offsetStart: 0,
|
||||
// equivalent dateRanges in UTC-7
|
||||
// start:08:00, end:10:00
|
||||
// start:10:10, end:12:45
|
||||
// with 10:00 - 10:10 busy time
|
||||
dateRanges: [
|
||||
{ start: dayjs("2024-09-12T15:00:00.000Z"), end: dayjs("2024-09-12T17:00:00.000Z") },
|
||||
{ start: dayjs("2024-09-12T17:10:00.000Z"), end: dayjs("2024-09-12T19:45:00.000Z") },
|
||||
],
|
||||
minimumBookingNotice: 0,
|
||||
frequency: 30,
|
||||
});
|
||||
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2024-09-12T08:00:00-07:00",
|
||||
"2024-09-12T08:30:00-07:00",
|
||||
"2024-09-12T09:00:00-07:00",
|
||||
"2024-09-12T09:30:00-07:00",
|
||||
"2024-09-12T10:15:00-07:00",
|
||||
"2024-09-12T10:45:00-07:00",
|
||||
"2024-09-12T11:15:00-07:00",
|
||||
"2024-09-12T11:45:00-07:00",
|
||||
"2024-09-12T12:15:00-07:00",
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows 8 30minutes slots for availability like 08:00-12:45 with 10:00-10:10 busytime, when showOptimizedSlots set to false", async () => {
|
||||
const slots = getSlots({
|
||||
showOptimizedSlots: false,
|
||||
inviteeDate: dayjs.tz("2024-09-12T00:00:00.000-07:00", "America/Los_Angeles"),
|
||||
eventLength: 30,
|
||||
offsetStart: 0,
|
||||
// equivalent dateRanges in UTC-7
|
||||
// start:08:00, end:10:00
|
||||
// start:10:10, end:12:45
|
||||
// with 10:00 - 10:10 busy time
|
||||
dateRanges: [
|
||||
{ start: dayjs("2024-09-12T15:00:00.000Z"), end: dayjs("2024-09-12T17:00:00.000Z") },
|
||||
{ start: dayjs("2024-09-12T17:10:00.000Z"), end: dayjs("2024-09-12T19:45:00.000Z") },
|
||||
],
|
||||
minimumBookingNotice: 0,
|
||||
frequency: 30,
|
||||
});
|
||||
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2024-09-12T08:00:00-07:00",
|
||||
"2024-09-12T08:30:00-07:00",
|
||||
"2024-09-12T09:00:00-07:00",
|
||||
"2024-09-12T09:30:00-07:00",
|
||||
"2024-09-12T10:30:00-07:00",
|
||||
"2024-09-12T11:00:00-07:00",
|
||||
"2024-09-12T11:30:00-07:00",
|
||||
"2024-09-12T12:00:00-07:00",
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows 13 20minutes slots for availability like 08:00-12:30 with 10:00-10:10 busytime, when showOptimizedSlots set to true", async () => {
|
||||
const slots = getSlots({
|
||||
showOptimizedSlots: true,
|
||||
inviteeDate: dayjs.tz("2024-09-12T00:00:00.000-07:00", "America/Los_Angeles"),
|
||||
eventLength: 20,
|
||||
offsetStart: 0,
|
||||
// equivalent dateRanges in UTC-7
|
||||
// start:08:00, end:10:00
|
||||
// start:10:10, end:12:30
|
||||
// with 10:00 - 10:10 busy time
|
||||
dateRanges: [
|
||||
{ start: dayjs("2024-09-12T15:00:00.000Z"), end: dayjs("2024-09-12T17:00:00.000Z") },
|
||||
{ start: dayjs("2024-09-12T17:10:00.000Z"), end: dayjs("2024-09-12T19:30:00.000Z") },
|
||||
],
|
||||
minimumBookingNotice: 0,
|
||||
frequency: 20,
|
||||
});
|
||||
|
||||
expect(slots.length).toStrictEqual(13);
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2024-09-12T08:00:00-07:00",
|
||||
"2024-09-12T08:20:00-07:00",
|
||||
"2024-09-12T08:40:00-07:00",
|
||||
"2024-09-12T09:00:00-07:00",
|
||||
"2024-09-12T09:20:00-07:00",
|
||||
"2024-09-12T09:40:00-07:00",
|
||||
"2024-09-12T10:10:00-07:00",
|
||||
"2024-09-12T10:30:00-07:00",
|
||||
"2024-09-12T10:50:00-07:00",
|
||||
"2024-09-12T11:10:00-07:00",
|
||||
"2024-09-12T11:30:00-07:00",
|
||||
"2024-09-12T11:50:00-07:00",
|
||||
"2024-09-12T12:10:00-07:00",
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows 12 20minutes slots for availability like 08:00-12:30 with 10:00-10:10 busytime, when showOptimizedSlots set to false", async () => {
|
||||
const slots = getSlots({
|
||||
showOptimizedSlots: false,
|
||||
inviteeDate: dayjs.tz("2024-09-12T00:00:00.000-07:00", "America/Los_Angeles"),
|
||||
eventLength: 20,
|
||||
offsetStart: 0,
|
||||
// equivalent dateRanges in UTC-7
|
||||
// start:08:00, end:10:00
|
||||
// start:10:10, end:12:30
|
||||
// with 10:00 - 10:10 busy time
|
||||
dateRanges: [
|
||||
{ start: dayjs("2024-09-12T15:00:00.000Z"), end: dayjs("2024-09-12T17:00:00.000Z") },
|
||||
{ start: dayjs("2024-09-12T17:10:00.000Z"), end: dayjs("2024-09-12T19:30:00.000Z") },
|
||||
],
|
||||
minimumBookingNotice: 0,
|
||||
frequency: 20,
|
||||
});
|
||||
|
||||
expect(slots.length).toStrictEqual(12);
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2024-09-12T08:00:00-07:00",
|
||||
"2024-09-12T08:20:00-07:00",
|
||||
"2024-09-12T08:40:00-07:00",
|
||||
"2024-09-12T09:00:00-07:00",
|
||||
"2024-09-12T09:20:00-07:00",
|
||||
"2024-09-12T09:40:00-07:00",
|
||||
"2024-09-12T10:20:00-07:00",
|
||||
"2024-09-12T10:40:00-07:00",
|
||||
"2024-09-12T11:00:00-07:00",
|
||||
"2024-09-12T11:20:00-07:00",
|
||||
"2024-09-12T11:40:00-07:00",
|
||||
"2024-09-12T12:00:00-07:00",
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows 8 60minutes slots for availability like 09:30-17:30, when showOptimizedSlots set to true", async () => {
|
||||
const slots = getSlots({
|
||||
showOptimizedSlots: true,
|
||||
inviteeDate: dayjs.tz("2024-09-12T00:00:00.000-07:00", "America/Los_Angeles"),
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
// equivalent dateRanges in UTC-7
|
||||
// start:09:30, end:17:30
|
||||
dateRanges: [{ start: dayjs("2024-09-12T16:30:00.000Z"), end: dayjs("2024-09-13T00:30:00.000Z") }],
|
||||
minimumBookingNotice: 0,
|
||||
frequency: 60,
|
||||
});
|
||||
|
||||
expect(slots.length).toStrictEqual(8);
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2024-09-12T09:30:00-07:00",
|
||||
"2024-09-12T10:30:00-07:00",
|
||||
"2024-09-12T11:30:00-07:00",
|
||||
"2024-09-12T12:30:00-07:00",
|
||||
"2024-09-12T13:30:00-07:00",
|
||||
"2024-09-12T14:30:00-07:00",
|
||||
"2024-09-12T15:30:00-07:00",
|
||||
"2024-09-12T16:30:00-07:00",
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows 7 60minutes slots for availability like 09:30-17:30, when showOptimizedSlots set to false", async () => {
|
||||
const slots = getSlots({
|
||||
showOptimizedSlots: false,
|
||||
inviteeDate: dayjs.tz("2024-09-12T00:00:00.000-07:00", "America/Los_Angeles"),
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
// equivalent dateRanges in UTC-7
|
||||
// start:09:30, end:17:30
|
||||
dateRanges: [{ start: dayjs("2024-09-12T16:30:00.000Z"), end: dayjs("2024-09-13T00:30:00.000Z") }],
|
||||
minimumBookingNotice: 0,
|
||||
frequency: 60,
|
||||
});
|
||||
|
||||
expect(slots.length).toStrictEqual(7);
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2024-09-12T10:00:00-07:00",
|
||||
"2024-09-12T11:00:00-07:00",
|
||||
"2024-09-12T12:00:00-07:00",
|
||||
"2024-09-12T13:00:00-07:00",
|
||||
"2024-09-12T14:00:00-07:00",
|
||||
"2024-09-12T15:00:00-07:00",
|
||||
"2024-09-12T16:00:00-07:00",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should respect start of the hour for 60minutes slots for availabilities like 07:45-15:30, even when showOptimizedSlots set to true", async () => {
|
||||
const slots = getSlots({
|
||||
showOptimizedSlots: true,
|
||||
inviteeDate: dayjs.tz("2024-09-12T00:00:00.000-07:00", "America/Los_Angeles"),
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
// equivalent dateRanges in UTC-7
|
||||
// start:07:45, end:15:30
|
||||
dateRanges: [{ start: dayjs("2024-09-12T14:45:00.000Z"), end: dayjs("2024-09-12T22:30:00.000Z") }],
|
||||
minimumBookingNotice: 0,
|
||||
frequency: 60,
|
||||
});
|
||||
|
||||
expect(slots.length).toStrictEqual(7);
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2024-09-12T08:00:00-07:00",
|
||||
"2024-09-12T09:00:00-07:00",
|
||||
"2024-09-12T10:00:00-07:00",
|
||||
"2024-09-12T11:00:00-07:00",
|
||||
"2024-09-12T12:00:00-07:00",
|
||||
"2024-09-12T13:00:00-07:00",
|
||||
"2024-09-12T14:00:00-07:00",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should respect start of the hour for 60minutes slots for availabilities like 07:45-15:30, when showOptimizedSlots set to false", async () => {
|
||||
const slots = getSlots({
|
||||
showOptimizedSlots: false,
|
||||
inviteeDate: dayjs.tz("2024-09-12T00:00:00.000-07:00", "America/Los_Angeles"),
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
// equivalent dateRanges in UTC-7
|
||||
// start:07:45, end:15:30
|
||||
dateRanges: [{ start: dayjs("2024-09-12T14:45:00.000Z"), end: dayjs("2024-09-12T22:30:00.000Z") }],
|
||||
minimumBookingNotice: 0,
|
||||
frequency: 60,
|
||||
});
|
||||
|
||||
expect(slots.length).toStrictEqual(7);
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2024-09-12T08:00:00-07:00",
|
||||
"2024-09-12T09:00:00-07:00",
|
||||
"2024-09-12T10:00:00-07:00",
|
||||
"2024-09-12T11:00:00-07:00",
|
||||
"2024-09-12T12:00:00-07:00",
|
||||
"2024-09-12T13:00:00-07:00",
|
||||
"2024-09-12T14:00:00-07:00",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should respect start of the hour for 60minutes slots for availabilities like 09:05-12:55, even when showOptimizedSlots set to true", async () => {
|
||||
const slots = getSlots({
|
||||
showOptimizedSlots: true,
|
||||
inviteeDate: dayjs.tz("2024-09-12T00:00:00.000-07:00", "America/Los_Angeles"),
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
// equivalent dateRanges in UTC-7
|
||||
// start:09:05, end:12:55
|
||||
dateRanges: [{ start: dayjs("2024-09-12T16:05:00.000Z"), end: dayjs("2024-09-12T19:55:00.000Z") }],
|
||||
minimumBookingNotice: 0,
|
||||
frequency: 60,
|
||||
});
|
||||
|
||||
expect(slots.length).toStrictEqual(3);
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2024-09-12T09:15:00-07:00",
|
||||
"2024-09-12T10:15:00-07:00",
|
||||
"2024-09-12T11:15:00-07:00",
|
||||
]);
|
||||
});
|
||||
|
||||
it("adjusts slot start time to next 5-minute boundary for current day bookings with non-aligned start time", async () => {
|
||||
// Set system time to 2021-06-20 11:20:00 UTC
|
||||
vi.setSystemTime(dayjs.utc("2021-06-20T11:20:00.000Z").toDate());
|
||||
|
||||
const slots = getSlots({
|
||||
inviteeDate: dayjs.tz("2021-06-20T00:00:00.000Z", "America/Los_Angeles"),
|
||||
frequency: 15,
|
||||
minimumBookingNotice: 0,
|
||||
eventLength: 15,
|
||||
dateRanges: [
|
||||
{
|
||||
start: dayjs.tz("2021-06-20T11:22:00.000", "America/Los_Angeles"),
|
||||
end: dayjs.tz("2021-06-20T12:55:00.000", "America/Los_Angeles"),
|
||||
},
|
||||
],
|
||||
offsetStart: 0,
|
||||
showOptimizedSlots: true,
|
||||
});
|
||||
|
||||
expect(slots.length).toStrictEqual(6);
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2021-06-20T11:25:00-07:00",
|
||||
"2021-06-20T11:40:00-07:00",
|
||||
"2021-06-20T11:55:00-07:00",
|
||||
"2021-06-20T12:10:00-07:00",
|
||||
"2021-06-20T12:25:00-07:00",
|
||||
"2021-06-20T12:40:00-07:00",
|
||||
]);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should respect start of the hour for current day bookings with non-aligned start time", async () => {
|
||||
// Set system time to 2021-06-20 11:20:00 UTC
|
||||
vi.setSystemTime(dayjs.utc("2021-06-20T11:20:00.000Z").toDate());
|
||||
|
||||
const slots = getSlots({
|
||||
inviteeDate: dayjs.tz("2021-06-20T00:00:00.000Z", "America/Los_Angeles"),
|
||||
frequency: 60,
|
||||
minimumBookingNotice: 0,
|
||||
eventLength: 60,
|
||||
dateRanges: [
|
||||
{
|
||||
start: dayjs.tz("2021-06-20T11:22:36.234", "America/Los_Angeles"), //with seconds
|
||||
end: dayjs.tz("2021-06-20T14:00:00.000", "America/Los_Angeles"),
|
||||
},
|
||||
],
|
||||
offsetStart: 0,
|
||||
showOptimizedSlots: true,
|
||||
});
|
||||
|
||||
expect(slots.length).toStrictEqual(2);
|
||||
expect(slots.map((slot) => slot.time.format())).toStrictEqual([
|
||||
"2021-06-20T12:00:00-07:00",
|
||||
"2021-06-20T13:00:00-07:00",
|
||||
]);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
+60
-4
@@ -14,11 +14,56 @@ export type GetSlots = {
|
||||
eventLength: number;
|
||||
offsetStart?: number;
|
||||
datesOutOfOffice?: IOutOfOfficeData;
|
||||
showOptimizedSlots?: boolean | null;
|
||||
};
|
||||
export type TimeFrame = { userIds?: number[]; startTime: number; endTime: number };
|
||||
|
||||
const minimumOfOne = (input: number) => (input < 1 ? 1 : input);
|
||||
|
||||
function getCorrectedSlotStartTime({
|
||||
slotStartTime,
|
||||
range,
|
||||
showOptimizedSlots,
|
||||
interval,
|
||||
}: {
|
||||
showOptimizedSlots: boolean | null | undefined;
|
||||
interval: number;
|
||||
slotStartTime: Dayjs;
|
||||
range: DateRange;
|
||||
}) {
|
||||
if (showOptimizedSlots) {
|
||||
let correctedSlotStartTime = slotStartTime;
|
||||
// if showOptimizedSlots option is selected, the slotStartTime should not be modified,
|
||||
// so that maximum possible slots are shown.
|
||||
// The below logic in this entire `if branch` only tries to add an increment if sufficient minutes are available (after max possible slots are consumed),
|
||||
// so that slots are shown respecting the 'Start of the Hour'.
|
||||
const minutesRequiredToMoveToNextSlot = interval - (slotStartTime.minute() % interval);
|
||||
const minutesRequiredToMoveTo15MinSlot = 15 - (slotStartTime.minute() % 15);
|
||||
const minutesRequiredToMoveTo5MinSlot = 5 - (slotStartTime.minute() % 5);
|
||||
const extraMinutesAvailable = range.end.diff(slotStartTime, "minutes") % interval;
|
||||
|
||||
if (extraMinutesAvailable >= minutesRequiredToMoveToNextSlot) {
|
||||
// For cases like, Availability -> 9:05 - 12:00, 60Min EventTypes.
|
||||
// Total available minutes are 175, so only 2 60Min slots can be provided max
|
||||
// And still 175-120 = 55mins are available, hence 'slotStartTime' is pushed to 10:00 to respect 'Start of the Hour'.
|
||||
// Slots will be shown as '10:00, 11:00' instead of '09:05, 10:05'
|
||||
correctedSlotStartTime = slotStartTime.add(minutesRequiredToMoveToNextSlot, "minute");
|
||||
} else if (extraMinutesAvailable >= minutesRequiredToMoveTo15MinSlot) {
|
||||
// For cases like, Availability -> 9:05 - 11:55, 60Min EventTypes.
|
||||
// Total available minutes are 170, so only 2 60Min slots can be provided max
|
||||
// And still 175-120 = 50mins are available, but it is less 55mins which is required to push to 10:00
|
||||
// so slotStartTime is pushed to next 15Min slot 09:15, instead of showing slots like 9:05,10:05 now slots will be 9:15,10:15
|
||||
correctedSlotStartTime = slotStartTime.add(minutesRequiredToMoveTo15MinSlot, "minute");
|
||||
} else if (extraMinutesAvailable >= minutesRequiredToMoveTo5MinSlot) {
|
||||
// so slotStartTime is pushed to next 5Min, instead of showing slots like 11:22,11:37 now slots will be 11:25,11:40
|
||||
correctedSlotStartTime = slotStartTime.add(minutesRequiredToMoveTo5MinSlot, "minute");
|
||||
}
|
||||
return correctedSlotStartTime;
|
||||
}
|
||||
|
||||
return slotStartTime.startOf("hour").add(Math.ceil(slotStartTime.minute() / interval) * interval, "minute");
|
||||
}
|
||||
|
||||
function buildSlotsWithDateRanges({
|
||||
dateRanges,
|
||||
frequency,
|
||||
@@ -27,6 +72,7 @@ function buildSlotsWithDateRanges({
|
||||
minimumBookingNotice,
|
||||
offsetStart,
|
||||
datesOutOfOffice,
|
||||
showOptimizedSlots,
|
||||
}: {
|
||||
dateRanges: DateRange[];
|
||||
frequency: number;
|
||||
@@ -35,6 +81,7 @@ function buildSlotsWithDateRanges({
|
||||
minimumBookingNotice: number;
|
||||
offsetStart?: number;
|
||||
datesOutOfOffice?: IOutOfOfficeData;
|
||||
showOptimizedSlots?: boolean | null;
|
||||
}) {
|
||||
// keep the old safeguards in; may be needed.
|
||||
frequency = minimumOfOne(frequency);
|
||||
@@ -78,10 +125,17 @@ function buildSlotsWithDateRanges({
|
||||
? range.start
|
||||
: startTimeWithMinNotice;
|
||||
|
||||
slotStartTime =
|
||||
slotStartTime.minute() % interval !== 0
|
||||
? slotStartTime.startOf("hour").add(Math.ceil(slotStartTime.minute() / interval) * interval, "minute")
|
||||
: slotStartTime;
|
||||
// For current day bookings, normalizing the seconds to zero to avoid issues with time calculations
|
||||
slotStartTime = slotStartTime.set("second", 0).set("millisecond", 0);
|
||||
|
||||
if (slotStartTime.minute() % interval !== 0) {
|
||||
slotStartTime = getCorrectedSlotStartTime({
|
||||
showOptimizedSlots,
|
||||
interval,
|
||||
slotStartTime,
|
||||
range,
|
||||
});
|
||||
}
|
||||
|
||||
slotStartTime = slotStartTime.add(offsetStart ?? 0, "minutes").tz(timeZone);
|
||||
|
||||
@@ -163,6 +217,7 @@ const getSlots = ({
|
||||
eventLength,
|
||||
offsetStart = 0,
|
||||
datesOutOfOffice,
|
||||
showOptimizedSlots,
|
||||
}: GetSlots): {
|
||||
time: Dayjs;
|
||||
userIds?: number[];
|
||||
@@ -180,6 +235,7 @@ const getSlots = ({
|
||||
minimumBookingNotice,
|
||||
offsetStart,
|
||||
datesOutOfOffice,
|
||||
showOptimizedSlots,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
|
||||
beforeEventBuffer: 0,
|
||||
afterEventBuffer: 0,
|
||||
onlyShowFirstAvailableSlot: false,
|
||||
showOptimizedSlots: false,
|
||||
seatsPerTimeSlot: null,
|
||||
seatsShowAttendees: null,
|
||||
disableCancelling: false,
|
||||
|
||||
@@ -145,6 +145,7 @@ export const useEventTypeForm = ({
|
||||
calVideoSettings: eventType.calVideoSettings,
|
||||
maxActiveBookingsPerBooker: eventType.maxActiveBookingsPerBooker || null,
|
||||
maxActiveBookingPerBookerOfferReschedule: eventType.maxActiveBookingPerBookerOfferReschedule,
|
||||
showOptimizedSlots: eventType.showOptimizedSlots ?? false,
|
||||
};
|
||||
}, [eventType, periodDates]);
|
||||
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "EventType" ADD COLUMN "showOptimizedSlots" BOOLEAN DEFAULT false;
|
||||
@@ -168,6 +168,7 @@ model EventType {
|
||||
afterEventBuffer Int @default(0)
|
||||
seatsPerTimeSlot Int?
|
||||
onlyShowFirstAvailableSlot Boolean @default(false)
|
||||
showOptimizedSlots Boolean? @default(false)
|
||||
disableCancelling Boolean? @default(false)
|
||||
disableRescheduling Boolean? @default(false)
|
||||
seatsShowAttendees Boolean? @default(false)
|
||||
|
||||
@@ -1155,6 +1155,7 @@ export class AvailableSlotsService {
|
||||
minimumBookingNotice: eventType.minimumBookingNotice,
|
||||
frequency: eventType.slotInterval || input.duration || eventType.length,
|
||||
datesOutOfOffice: !isTeamEvent ? allUsersAvailability[0]?.datesOutOfOffice : undefined,
|
||||
showOptimizedSlots: eventType.showOptimizedSlots,
|
||||
});
|
||||
|
||||
let availableTimeSlots: typeof timeSlots = [];
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
"NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE_NEW",
|
||||
"NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL",
|
||||
"NEXT_PUBLIC_WEBSITE_TERMS_URL",
|
||||
"NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL",
|
||||
"NEXT_RUNTIME",
|
||||
"NEXTAUTH_COOKIE_DOMAIN",
|
||||
"NEXTAUTH_SECRET",
|
||||
|
||||
Reference in New Issue
Block a user