feat: add 40-minute duration option for multiple duration event types (#27032)
- Add 40 minutes to the multipleDurationOptions array in EventSetupTab - Add comprehensive tests for 40-minute slot generation including: - Full day slot generation - Correct time intervals - Default duration handling - Same day booking with time constraints - Multiple date ranges Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: ali@cal.com <ali@cal.com>
This commit is contained in:
co-authored by
ali@cal.com
parent
c60addc528
commit
1e0b0d949f
@@ -1,18 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
|
||||
import type { MultiValue } from "react-select";
|
||||
|
||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { LocationCustomClassNames } from "../../locations/types";
|
||||
import type {
|
||||
EventTypeSetupProps,
|
||||
FormValues,
|
||||
InputClassNames,
|
||||
LocationFormValues,
|
||||
SelectClassNames,
|
||||
SettingsToggleClassNames,
|
||||
} from "@calcom/features/eventtypes/lib/types";
|
||||
import type { FormValues, LocationFormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { MAX_EVENT_DURATION_MINUTES, MIN_EVENT_DURATION_MINUTES } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
@@ -20,14 +15,14 @@ import { slugify } from "@calcom/lib/slugify";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Editor } from "@calcom/ui/components/editor";
|
||||
import { TextAreaField } from "@calcom/ui/components/form";
|
||||
import { Label } from "@calcom/ui/components/form";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { Select } from "@calcom/ui/components/form";
|
||||
import { SettingsToggle } from "@calcom/ui/components/form";
|
||||
import { Label, Select, SettingsToggle, TextAreaField, TextField } from "@calcom/ui/components/form";
|
||||
import { Skeleton } from "@calcom/ui/components/skeleton";
|
||||
|
||||
import Locations from "@calcom/web/modules/event-types/components/locations/Locations";
|
||||
import { useState } from "react";
|
||||
import type { Control, FormState, UseFormGetValues, UseFormSetValue } from "react-hook-form";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { MultiValue } from "react-select";
|
||||
import type { LocationCustomClassNames } from "../../locations/types";
|
||||
|
||||
export type EventSetupTabCustomClassNames = {
|
||||
wrapper?: string;
|
||||
@@ -80,7 +75,7 @@ export const EventSetupTab = (
|
||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||
|
||||
const multipleDurationOptions = [
|
||||
5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 150, 180, 240, 300, 360, 420, 480,
|
||||
5, 10, 15, 20, 25, 30, 40, 45, 50, 60, 75, 80, 90, 120, 150, 180, 240, 300, 360, 420, 480,
|
||||
].map((mins) => ({
|
||||
value: mins,
|
||||
label: t("multiple_duration_mins", { count: mins }),
|
||||
@@ -109,7 +104,7 @@ export const EventSetupTab = (
|
||||
<div className={classNames("stack-y-4", customClassNames?.wrapper)}>
|
||||
<div
|
||||
className={classNames(
|
||||
"border-subtle stack-y-6 rounded-lg border p-6",
|
||||
"stack-y-6 rounded-lg border border-subtle p-6",
|
||||
customClassNames?.titleSection?.container
|
||||
)}>
|
||||
<TextField
|
||||
@@ -169,7 +164,7 @@ export const EventSetupTab = (
|
||||
className={classNames("pl-0", customClassNames?.titleSection?.urlInput?.input)}
|
||||
addOnLeading={
|
||||
isPlatform ? undefined : (
|
||||
<span className="max-w-24 md:max-w-56 inline-block min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className="inline-block min-w-0 max-w-24 overflow-hidden text-ellipsis whitespace-nowrap md:max-w-56">
|
||||
{urlPrefix}/
|
||||
{!isManagedEventType
|
||||
? team
|
||||
@@ -187,7 +182,7 @@ export const EventSetupTab = (
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"border-subtle rounded-lg border p-6",
|
||||
"rounded-lg border border-subtle p-6",
|
||||
customClassNames?.durationSection?.container
|
||||
)}>
|
||||
{multipleDuration ? (
|
||||
@@ -212,7 +207,7 @@ export const EventSetupTab = (
|
||||
isSearchable={false}
|
||||
isDisabled={lengthLockedProps.disabled}
|
||||
className={classNames(
|
||||
"min-h-[36px]! h-auto text-sm",
|
||||
"h-auto min-h-[36px]! text-sm",
|
||||
customClassNames?.durationSection?.multipleDuration?.availableDurationsSelect?.select
|
||||
)}
|
||||
innerClassNames={
|
||||
@@ -306,7 +301,7 @@ export const EventSetupTab = (
|
||||
message: t("duration_max_error", { max: MAX_EVENT_DURATION_MINUTES }),
|
||||
},
|
||||
})}
|
||||
addOnSuffix={<>{t("minutes")}</>}
|
||||
addOnSuffix={t("minutes")}
|
||||
min={MIN_EVENT_DURATION_MINUTES}
|
||||
max={MAX_EVENT_DURATION_MINUTES}
|
||||
/>
|
||||
@@ -341,7 +336,7 @@ export const EventSetupTab = (
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"border-subtle rounded-lg border p-6",
|
||||
"rounded-lg border border-subtle p-6",
|
||||
customClassNames?.locationSection?.container
|
||||
)}>
|
||||
<div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, expect, it, beforeAll, vi } from "vitest";
|
||||
|
||||
import process from "node:process";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { DateRange } from "@calcom/features/schedules/lib/date-ranges";
|
||||
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import getSlots from "./slots";
|
||||
|
||||
let dateRangesNextDay: DateRange[];
|
||||
@@ -886,7 +885,7 @@ describe("Tests the date-range slot logic with showOptimizedSlots", () => {
|
||||
});
|
||||
|
||||
expect(day2Slots.length).toBeGreaterThan(0);
|
||||
|
||||
|
||||
// Day 2 slots should be marked as away
|
||||
day2Slots.forEach((slot) => {
|
||||
expect(slot.away).toBe(true);
|
||||
@@ -1055,3 +1054,134 @@ describe("Tests the date-range slot logic with showOptimizedSlots", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests 40-minute duration slot generation", () => {
|
||||
beforeAll(() => {
|
||||
vi.setSystemTime(dayjs.utc("2021-06-20T11:59:59Z").toDate());
|
||||
});
|
||||
|
||||
it("generates correct number of 40-minute slots for a full day", async () => {
|
||||
const nextDay = dayjs.utc().add(1, "day").startOf("day");
|
||||
const dateRanges = [
|
||||
{
|
||||
start: nextDay,
|
||||
end: nextDay.endOf("day"),
|
||||
},
|
||||
];
|
||||
|
||||
const slots = getSlots({
|
||||
inviteeDate: nextDay,
|
||||
frequency: 40,
|
||||
minimumBookingNotice: 0,
|
||||
dateRanges: dateRanges,
|
||||
eventLength: 40,
|
||||
offsetStart: 0,
|
||||
});
|
||||
|
||||
// 24 hours = 1440 minutes, 1440 / 40 = 36 slots
|
||||
expect(slots).toHaveLength(36);
|
||||
});
|
||||
|
||||
it("generates 40-minute slots with correct time intervals", async () => {
|
||||
const nextDay = dayjs.utc().add(1, "day").startOf("day");
|
||||
const dateRanges = [
|
||||
{
|
||||
start: nextDay.hour(9),
|
||||
end: nextDay.hour(11),
|
||||
},
|
||||
];
|
||||
|
||||
const slots = getSlots({
|
||||
inviteeDate: nextDay,
|
||||
frequency: 40,
|
||||
minimumBookingNotice: 0,
|
||||
dateRanges: dateRanges,
|
||||
eventLength: 40,
|
||||
offsetStart: 0,
|
||||
});
|
||||
|
||||
// 2 hours = 120 minutes, 120 / 40 = 3 slots (9:00, 9:40, 10:20)
|
||||
expect(slots).toHaveLength(3);
|
||||
expect(slots[0].time.format("HH:mm")).toBe("09:00");
|
||||
expect(slots[1].time.format("HH:mm")).toBe("09:40");
|
||||
expect(slots[2].time.format("HH:mm")).toBe("10:20");
|
||||
});
|
||||
|
||||
it("handles 40-minute slots as default duration in variable length event", async () => {
|
||||
const nextDay = dayjs.utc().add(1, "day").startOf("day");
|
||||
const dateRanges = [
|
||||
{
|
||||
start: nextDay.hour(10),
|
||||
end: nextDay.hour(12),
|
||||
},
|
||||
];
|
||||
|
||||
const slots = getSlots({
|
||||
inviteeDate: nextDay,
|
||||
frequency: 40,
|
||||
minimumBookingNotice: 0,
|
||||
dateRanges: dateRanges,
|
||||
eventLength: 40,
|
||||
offsetStart: 0,
|
||||
});
|
||||
|
||||
// 2 hours = 120 minutes, 120 / 40 = 3 slots
|
||||
expect(slots).toHaveLength(3);
|
||||
// Verify each slot has correct 40-minute spacing
|
||||
for (let i = 1; i < slots.length; i++) {
|
||||
const diff = slots[i].time.diff(slots[i - 1].time, "minute");
|
||||
expect(diff).toBe(40);
|
||||
}
|
||||
});
|
||||
|
||||
it("generates 40-minute slots with minimum booking notice on same day", async () => {
|
||||
// System time is set to 2021-06-20T11:59:59Z (almost noon)
|
||||
const today = dayjs.utc().startOf("day");
|
||||
const dateRanges = [
|
||||
{
|
||||
start: today,
|
||||
end: today.endOf("day"),
|
||||
},
|
||||
];
|
||||
|
||||
const slots = getSlots({
|
||||
inviteeDate: today,
|
||||
frequency: 40,
|
||||
minimumBookingNotice: 0,
|
||||
dateRanges: dateRanges,
|
||||
eventLength: 40,
|
||||
offsetStart: 0,
|
||||
});
|
||||
|
||||
// System time is ~12:00, so only slots from 12:00 onwards should be available
|
||||
// From 12:00 to 24:00 = 12 hours = 720 minutes, 720 / 40 = 18 slots
|
||||
expect(slots).toHaveLength(18);
|
||||
});
|
||||
|
||||
it("handles 40-minute slots across multiple date ranges", async () => {
|
||||
const nextDay = dayjs.utc().add(1, "day").startOf("day");
|
||||
const dateRanges = [
|
||||
{
|
||||
start: nextDay.hour(9),
|
||||
end: nextDay.hour(10).minute(20),
|
||||
},
|
||||
{
|
||||
start: nextDay.hour(14),
|
||||
end: nextDay.hour(15).minute(20),
|
||||
},
|
||||
];
|
||||
|
||||
const slots = getSlots({
|
||||
inviteeDate: nextDay,
|
||||
frequency: 40,
|
||||
minimumBookingNotice: 0,
|
||||
dateRanges: dateRanges,
|
||||
eventLength: 40,
|
||||
offsetStart: 0,
|
||||
});
|
||||
|
||||
// First range: 9:00-10:20 = 80 minutes = 2 slots (9:00, 9:40)
|
||||
// Second range: 14:00-15:20 = 80 minutes = 2 slots (14:00, 14:40)
|
||||
expect(slots).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user