Feature: Reserve slots currently being booked (#6909)

* Reserving slot picked up on cache

* change memory-cache to database table to block slots while reservation completes

* remove memory-cache

* update realeaseAt field when same user change te selected Slot

* Change default time to book

Co-authored-by: alannnc <alannnc@gmail.com>

* remove ip field and renews the session when the user remains in the booking form

* Remove duplicate router

* types fixes

* nit picks

* Update turbo.json

* Revert unrelated change

* Uses constant

* Constant already has a fallback

* Update slots.ts

* Unit test fixes

* slot reservation on user level and support seats

* types fixes and reserve slots on click

* Fix nit var name

---------

Co-authored-by: Efraín Rochín <roae.85@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
authored and GitHub committed 2023-04-13 19:55:26 +00:00
1 parent 7c9012738a
commit e478a46358
11 files changed
+227 -8

No files matched your search

@@ -10,6 +10,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { nameOfDay } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
@@ -28,6 +29,7 @@ type AvailableTimesProps = {
slots?: Slot[];
isLoading: boolean;
ethSignature?: string;
duration: number;
};
const AvailableTimes: FC<AvailableTimesProps> = ({
@@ -42,7 +44,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
seatsPerTimeSlot,
bookingAttendees,
ethSignature,
duration,
}) => {
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
const { t, i18n } = useLocale();
const router = useRouter();
@@ -63,6 +67,14 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
[isMobile]
);
const reserveSlot = (slot: Slot) => {
reserveSlotMutation.mutate({
slotUtcStartDate: slot.time,
eventTypeId,
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
});
};
return (
<div ref={slotPickerRef}>
{!!date ? (
@@ -150,6 +162,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
" bg-default dark:bg-muted border-default hover:bg-subtle hover:border-brand-default text-emphasis mb-2 block rounded-md border py-2 text-sm font-medium",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}
onClick={() => reserveSlot(slot)}
data-testid="time">
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && (
@@ -194,6 +194,7 @@ export const SlotPicker = ({
bookingAttendees={bookingAttendees}
recurringCount={recurringEventCount}
ethSignature={ethSignature}
duration={parseInt(duration)}
/>
</>
);
@@ -32,7 +32,7 @@ import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocati
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder";
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
import classNames from "@calcom/lib/classNames";
import { APP_NAME } from "@calcom/lib/constants";
import { APP_NAME, MINUTES_TO_BOOK } from "@calcom/lib/constants";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
@@ -41,6 +41,7 @@ import { HttpError } from "@calcom/lib/http-error";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { trpc } from "@calcom/trpc";
import { Button, Form, Tooltip, useCalcomTheme } from "@calcom/ui";
import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/icon";
@@ -210,8 +211,11 @@ const BookingPage = ({
hashedLink,
...restProps
}: BookingPageProps) => {
const removeSelectedSlotMarkMutation = trpc.viewer.public.slots.removeSelectedSlotMark.useMutation();
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const { t, i18n } = useLocale();
const { duration: queryDuration } = useRouterQuery("duration");
const { date: queryDate } = useRouterQuery("date");
const isEmbed = useIsEmbed(restProps.isEmbed);
const embedUiConfig = useEmbedUiConfig();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
@@ -227,6 +231,15 @@ const BookingPage = ({
}),
{}
);
const reserveSlot = () => {
if (queryDuration) {
reserveSlotMutation.mutate({
eventTypeId: eventType.id,
slotUtcStartDate: dayjs(queryDate).utc().format(),
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
});
}
};
// Define duration now that we support multiple duration eventTypes
let duration = eventType.length;
if (
@@ -246,6 +259,12 @@ const BookingPage = ({
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
);
}
reserveSlot();
const interval = setInterval(reserveSlot, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);
return () => {
clearInterval(interval);
removeSelectedSlotMarkMutation.mutate();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -649,9 +668,9 @@ const BookingPage = ({
</Button>
</div>
</Form>
{(mutation.isError || recurringMutation.isError) && (
{mutation.isError || recurringMutation.isError ? (
<ErrorMessage error={mutation.error || recurringMutation.error} />
)}
) : null}
</div>
</div>
</div>
+2 -1
View File
@@ -23,6 +23,7 @@ import { prismaMock, CalendarManagerMock } from "../../../../tests/config/single
// TODO: Mock properly
prismaMock.eventType.findUnique.mockResolvedValue(null);
prismaMock.user.findMany.mockResolvedValue([]);
prismaMock.selectedSlots.findMany.mockResolvedValue([]);
jest.mock("@calcom/lib/constants", () => ({
IS_PRODUCTION: true,
@@ -152,7 +153,7 @@ const TestData = {
};
const ctx = {
prisma,
prisma: prismaMock,
};
type App = {