* WIP-already-reschedule-success-emails-missing * WIP now saving bookingSeatsReferences and identifyin on reschedule/book page * Remove logs and created test * WIP saving progress * Select second slot to pass test * Delete attendee from event * Clean up * Update with main changes * Fix emails not being sent * Changed test end url from success to booking * Remove unused pkg * Fix new booking reschedule * remove log * Renable test * remove unused pkg * rename table name * review changes * Fix and and other test to reschedule with seats * Fix api for cancel booking * Typings * Update [uid].tsx * Abstracted common pattern into maybeGetBookingUidFromSeat * Reverts * Nitpicks * Update handleCancelBooking.ts * Adds missing cascades * Improve booking seats changes (#6858) * Create sendCancelledSeatEmails * Draft attendee cancelled seat email * Send no longer attendee email to attendee * Send email to organizer when attendee cancels * Pass cloned event data to emails * Send booked email for first seat * Add seat reference uid from email link * Query for seatReferenceUId and add to cancel & reschedule links * WIP * Display proper attendee when rescheduling seats * Remove console.logs * Only check for already invited when not rescheduling * WIP sending reschedule email to just single attendee and owner * Merge branch 'main' into send-email-on-seats-attendee-changes * Remove console.logs * Add cloned event to seat emails * Do not show manage link for calendar event * First seat, have both attendees on calendar * WIP refactor booking seats reschedule logic * WIP Refactor handleSeats * Change relation of attendee & seat reference to a one-to-one * Migration with relationship change * Booking page handling unique seat references * Abstract to handleSeats * Remove console.logs and clean up * New migration file, delete on cascade * Check if attendee is already a part of the booking * Move deleting booking logic to `handleSeats` * When owner reschedule, move whole booking * Prevent owner from rescheduling if not enough seats * Add owner reschedule * Send reschedule email when moving to new timeslot * Add event data to reschedule email for seats * Remove DB changes from event manager * When a booking has no attendees then delete * Update calendar when merging bookings * Move both attendees and seat references when merging * Remove guest list from seats booking page * Update original booking when moving an attendee * Delete calendar and video events if no more attendees * Update or delete integrations when attendees cancel * Show no longer attendee if a single attendee cancels * Change booking to accepted if an attendee books on an empty booking * If booking in same slot then just return the booking * Clean up * Clean up * Remove booking select * Typos --------- Co-authored-by: zomars <zomars@me.com> * Fix migration table name * Add missing trpc import * Rename bookingSeatReferences to bookingSeat * Change relationship between Attendee & BookingSeat to one to one * Fix some merge conflicts * Minor merge artifact fixup * Add the right 'Person' type * Check on email, less (although still) editable than name * Removed calEvent.attendeeUniqueId * rename referenceUId -> referenceUid * Squashes migrations * Run cached installs Should still be faster. Ensures prisma client is up to date. * Solve attendee form on booking page * Remove unused code * Some type fixes * Squash migrations * Type fixes * Fix for reschedule/[uid] redirect * Fix e2e test * Solve double declaration of host * Solve lint errors * Drop constraint only if exists * Renamed UId to Uid * Explicit vs. implicit * Attempt to work around text flakiness by adding a little break between animations * Various bugfixes * Persistently apply seatReferenceUid (#7545) * Persistently apply seatReferenceUid * Small ts fix * Setup guards correctly * Type fixes * Fix render 0 in conditional * Test refactoring * Fix type on handleSeats * Fix handle seats conditional * Fix type inference * Update packages/features/bookings/lib/handleNewBooking.ts * Update apps/web/components/booking/pages/BookingPage.tsx * Fix type and missing logic for reschedule * Fix delete of calendar event and booking * Add handleSeats return type * Fix seats booking creation * Fall through normal booking for initial booking, handleSeats for secondary/reschedule * Simplification of fetching booking * Enable seats for round-robin events * A lot harder than I expected * ignore-owner-if-seat-reference-given * Return seatReferenceUid when second seat * negate userIsOwner * Fix booking seats with a link without bookingUid * Needed a time check otherwise the attendee will be in the older booking * Can't open dialog twice in test.. * Allow passing the booking ID from the server * Fixed isCancelled check, fixed test * Delete through cascade instead of multiple deletes --------- Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com> Co-authored-by: Peer Richelsen <peer@cal.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Efraín Rochín <roae.85@gmail.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>
204 lines
6.7 KiB
TypeScript
204 lines
6.7 KiB
TypeScript
import type { EventType } from "@prisma/client";
|
|
import dynamic from "next/dynamic";
|
|
import { useRouter } from "next/router";
|
|
import { useEffect, useState } from "react";
|
|
import type { z } from "zod";
|
|
|
|
import type { Dayjs } from "@calcom/dayjs";
|
|
import dayjs from "@calcom/dayjs";
|
|
import DatePicker from "@calcom/features/calendars/DatePicker";
|
|
import classNames from "@calcom/lib/classNames";
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import type { TimeFormat } from "@calcom/lib/timeFormat";
|
|
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
|
import { trpc } from "@calcom/trpc/react";
|
|
|
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
|
|
|
const AvailableTimes = dynamic(() => import("@components/booking/AvailableTimes"));
|
|
|
|
const getRefetchInterval = (refetchCount: number): number => {
|
|
const intervals = [3000, 3000, 5000, 10000, 20000, 30000] as const;
|
|
return intervals[refetchCount] || intervals[intervals.length - 1];
|
|
};
|
|
|
|
const useSlots = ({
|
|
eventTypeId,
|
|
eventTypeSlug,
|
|
startTime,
|
|
endTime,
|
|
usernameList,
|
|
timeZone,
|
|
duration,
|
|
enabled = true,
|
|
}: {
|
|
eventTypeId: number;
|
|
eventTypeSlug: string;
|
|
startTime?: Dayjs;
|
|
endTime?: Dayjs;
|
|
usernameList: string[];
|
|
timeZone?: string;
|
|
duration?: string;
|
|
enabled?: boolean;
|
|
}) => {
|
|
const [refetchCount, setRefetchCount] = useState(0);
|
|
const refetchInterval = getRefetchInterval(refetchCount);
|
|
const { data, isLoading, isPaused, fetchStatus } = trpc.viewer.public.slots.getSchedule.useQuery(
|
|
{
|
|
eventTypeId,
|
|
eventTypeSlug,
|
|
usernameList,
|
|
startTime: startTime?.toISOString() || "",
|
|
endTime: endTime?.toISOString() || "",
|
|
timeZone,
|
|
duration,
|
|
},
|
|
{
|
|
enabled: !!startTime && !!endTime && enabled,
|
|
refetchInterval,
|
|
trpc: { context: { skipBatch: true } },
|
|
}
|
|
);
|
|
useEffect(() => {
|
|
if (!!data && fetchStatus === "idle") {
|
|
setRefetchCount(refetchCount + 1);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [fetchStatus, data]);
|
|
|
|
// The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state.
|
|
return { slots: data?.slots || {}, isLoading: isLoading || isPaused };
|
|
};
|
|
|
|
export const SlotPicker = ({
|
|
eventType,
|
|
timeFormat,
|
|
onTimeFormatChange,
|
|
timeZone,
|
|
recurringEventCount,
|
|
users,
|
|
seatsPerTimeSlot,
|
|
bookingAttendees,
|
|
weekStart = 0,
|
|
ethSignature,
|
|
}: {
|
|
eventType: Pick<
|
|
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
|
|
"id" | "schedulingType" | "slug" | "length" | "metadata"
|
|
>;
|
|
timeFormat: TimeFormat;
|
|
onTimeFormatChange: (is24Hour: boolean) => void;
|
|
timeZone?: string;
|
|
seatsPerTimeSlot?: number;
|
|
bookingAttendees?: number;
|
|
recurringEventCount?: number;
|
|
users: string[];
|
|
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
ethSignature?: string;
|
|
}) => {
|
|
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
|
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
|
let { duration = eventType.length.toString() } = useRouterQuery("duration");
|
|
const { date, setQuery: setDate } = useRouterQuery("date");
|
|
const { month, setQuery: setMonth } = useRouterQuery("month");
|
|
const router = useRouter();
|
|
|
|
if (!eventType.metadata?.multipleDuration) {
|
|
duration = eventType.length.toString();
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!router.isReady) return;
|
|
|
|
// Etc/GMT is not actually a timeZone, so handle this select option explicitly to prevent a hard crash.
|
|
if (timeZone === "Etc/GMT") {
|
|
setBrowsingDate(dayjs.utc(month).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0));
|
|
if (date) {
|
|
setSelectedDate(dayjs.utc(date));
|
|
}
|
|
} else {
|
|
// Set the start of the month without shifting time like startOf() may do.
|
|
setBrowsingDate(
|
|
dayjs.tz(month, timeZone).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0)
|
|
);
|
|
if (date) {
|
|
// It's important to set the date immediately to the timeZone, dayjs(date) will convert to browsertime.
|
|
setSelectedDate(dayjs.tz(date, timeZone));
|
|
}
|
|
}
|
|
}, [router.isReady, month, date, duration, timeZone]);
|
|
|
|
const { i18n, isLocaleReady } = useLocale();
|
|
const { slots: monthSlots, isLoading } = useSlots({
|
|
eventTypeId: eventType.id,
|
|
eventTypeSlug: eventType.slug,
|
|
usernameList: users,
|
|
startTime:
|
|
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
|
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
|
: browsingDate?.startOf("month"),
|
|
endTime: browsingDate?.endOf("month"),
|
|
timeZone,
|
|
duration,
|
|
});
|
|
const { slots: selectedDateSlots, isLoading: _isLoadingSelectedDateSlots } = useSlots({
|
|
eventTypeId: eventType.id,
|
|
eventTypeSlug: eventType.slug,
|
|
usernameList: users,
|
|
startTime: selectedDate?.startOf("day"),
|
|
endTime: selectedDate?.endOf("day"),
|
|
timeZone,
|
|
duration,
|
|
/** Prevent refetching is we already have this data from month slots */
|
|
enabled: !!selectedDate,
|
|
});
|
|
|
|
/** Hide skeleton if we have the slot loaded in the month query */
|
|
const isLoadingSelectedDateSlots = (() => {
|
|
if (!selectedDate) return _isLoadingSelectedDateSlots;
|
|
if (!!selectedDateSlots[selectedDate.format("YYYY-MM-DD")]) return false;
|
|
if (!!monthSlots[selectedDate.format("YYYY-MM-DD")]) return false;
|
|
return false;
|
|
})();
|
|
|
|
return (
|
|
<>
|
|
<DatePicker
|
|
isLoading={isLoading}
|
|
className={classNames(
|
|
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-4 lg:min-w-[455px]",
|
|
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
|
|
)}
|
|
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
|
|
locale={isLocaleReady ? i18n.language : "en"}
|
|
selected={selectedDate}
|
|
onChange={(newDate) => {
|
|
setDate(newDate.format("YYYY-MM-DD"));
|
|
}}
|
|
onMonthChange={(newMonth) => {
|
|
setMonth(newMonth.format("YYYY-MM"));
|
|
}}
|
|
browsingDate={browsingDate}
|
|
weekStart={weekStart}
|
|
/>
|
|
<AvailableTimes
|
|
isLoading={isLoadingSelectedDateSlots}
|
|
slots={
|
|
selectedDate &&
|
|
(selectedDateSlots[selectedDate.format("YYYY-MM-DD")] ||
|
|
monthSlots[selectedDate.format("YYYY-MM-DD")])
|
|
}
|
|
date={selectedDate}
|
|
timeFormat={timeFormat}
|
|
onTimeFormatChange={onTimeFormatChange}
|
|
eventTypeId={eventType.id}
|
|
eventTypeSlug={eventType.slug}
|
|
seatsPerTimeSlot={seatsPerTimeSlot}
|
|
bookingAttendees={bookingAttendees}
|
|
recurringCount={recurringEventCount}
|
|
ethSignature={ethSignature}
|
|
/>
|
|
</>
|
|
);
|
|
};
|