Dynamic Links re-integrate with availability logic (#3687)
* -re init dynamic links * typing fix 001 * added missing div closing tag * added necessary DB column pull to satisfy Type errors * further type fixes * WIP * removed console logs * some revert * WIP * another approach * enabled dynamic links availability fetch * Added users to eventTypeObject for consistency * WIP: Moving user and changing map item name * Fix user list call * Removed explicit User type in map * modify default user attributes * adds explicit users to EventTypeObject in teams * Updated availability page * Updates Availability * Futher availability change * Remove explicit user type from slot router * more fixes * more fixes WIP * cleaning up more errors WIP * object assign used for typesafety * added check if dynamic booking is allowed by all users * cleaned up console logs * clean up * Improvement * resolving suggestions by alex * changes requested by Omar * Filter out empty usernames instead of accepting null Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
co-authored by
Peer Richelsen
kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
zomars
parent
870f70fc38
commit
ee02112a7c
@@ -21,6 +21,7 @@ import classNames from "@calcom/lib/classNames";
|
||||
import { CAL_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
@@ -97,13 +98,17 @@ const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
|
||||
|
||||
const useSlots = ({
|
||||
eventTypeId,
|
||||
eventTypeSlug,
|
||||
startTime,
|
||||
endTime,
|
||||
usernameList,
|
||||
timeZone,
|
||||
}: {
|
||||
eventTypeId: number;
|
||||
eventTypeSlug: string;
|
||||
startTime?: Dayjs;
|
||||
endTime?: Dayjs;
|
||||
usernameList: string[];
|
||||
timeZone?: string;
|
||||
}) => {
|
||||
const { data, isLoading, isIdle } = trpc.useQuery(
|
||||
@@ -111,6 +116,8 @@ const useSlots = ({
|
||||
"viewer.public.slots.getSchedule",
|
||||
{
|
||||
eventTypeId,
|
||||
eventTypeSlug,
|
||||
usernameList,
|
||||
startTime: startTime?.toISOString() || "",
|
||||
endTime: endTime?.toISOString() || "",
|
||||
timeZone,
|
||||
@@ -118,7 +125,6 @@ const useSlots = ({
|
||||
],
|
||||
{ enabled: !!startTime && !!endTime }
|
||||
);
|
||||
|
||||
const [cachedSlots, setCachedSlots] = useState<NonNullable<typeof data>["slots"]>({});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -136,6 +142,7 @@ const SlotPicker = ({
|
||||
timeFormat,
|
||||
timeZone,
|
||||
recurringEventCount,
|
||||
users,
|
||||
seatsPerTimeSlot,
|
||||
weekStart = 0,
|
||||
}: {
|
||||
@@ -144,6 +151,7 @@ const SlotPicker = ({
|
||||
timeZone?: string;
|
||||
seatsPerTimeSlot?: number;
|
||||
recurringEventCount?: number;
|
||||
users: string[];
|
||||
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
}) => {
|
||||
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
||||
@@ -176,12 +184,16 @@ const SlotPicker = ({
|
||||
const { i18n, isLocaleReady } = useLocale();
|
||||
const { slots: _1 } = useSlots({
|
||||
eventTypeId: eventType.id,
|
||||
eventTypeSlug: eventType.slug,
|
||||
usernameList: users,
|
||||
startTime: selectedDate?.startOf("day"),
|
||||
endTime: selectedDate?.endOf("day"),
|
||||
timeZone,
|
||||
});
|
||||
const { slots: _2, isLoading } = useSlots({
|
||||
eventTypeId: eventType.id,
|
||||
eventTypeSlug: eventType.slug,
|
||||
usernameList: users,
|
||||
startTime: browsingDate?.startOf("month"),
|
||||
endTime: browsingDate?.endOf("month"),
|
||||
timeZone,
|
||||
@@ -356,6 +368,8 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
}
|
||||
}, [telemetry]);
|
||||
|
||||
// get dynamic user list here
|
||||
const userList = eventType.users.map((user) => user.username).filter(notEmpty);
|
||||
// Recurring event sidebar requires more space
|
||||
const maxWidth = isAvailableTimesVisible
|
||||
? recurringEventCount
|
||||
@@ -373,7 +387,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
onChangeTimeZone={setTimeZone}
|
||||
/>
|
||||
),
|
||||
[timeZone]
|
||||
[timeZone, timeFormat]
|
||||
);
|
||||
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
||||
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
||||
@@ -694,6 +708,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
eventType={eventType}
|
||||
timeFormat={timeFormat}
|
||||
timeZone={timeZone}
|
||||
users={userList}
|
||||
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
||||
recurringEventCount={recurringEventCount}
|
||||
/>
|
||||
|
||||
+39
-39
@@ -54,47 +54,47 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const groupEventTypes =
|
||||
/* props.users.some((user) => !user.allowDynamicBooking) TODO: Re-enable after v1.7 launch */ true ? (
|
||||
<div className="space-y-6" data-testid="event-types">
|
||||
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
||||
<div className="p-8 text-center text-gray-400 dark:text-white">
|
||||
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white">{" " + t("unavailable")}</h2>
|
||||
<p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled") as string}</p>
|
||||
</div>
|
||||
const groupEventTypes = props.users.some((user) => !user.allowDynamicBooking) ? (
|
||||
<div className="space-y-6" data-testid="event-types">
|
||||
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
||||
<div className="p-8 text-center text-gray-400 dark:text-white">
|
||||
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white">{" " + t("unavailable")}</h2>
|
||||
<p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled") as string}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="space-y-3">
|
||||
{eventTypes.map((type, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="hover:border-brand group relative rounded-sm border border-neutral-200 bg-white dark:border-neutral-700 dark:bg-gray-800 dark:hover:border-neutral-600">
|
||||
<Icon.FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
||||
<Link href={getUsernameSlugLink({ users: props.users, slug: type.slug })}>
|
||||
<a className="flex justify-between px-6 py-4" data-testid="event-type-link">
|
||||
<div className="flex-shrink">
|
||||
<h2 className="font-cal font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
|
||||
<EventTypeDescription className="text-sm" eventType={type} />
|
||||
</div>
|
||||
<div className="mt-1 self-center">
|
||||
<AvatarGroup
|
||||
border="border-2 border-white"
|
||||
truncateAfter={4}
|
||||
className="flex flex-shrink-0"
|
||||
size={10}
|
||||
items={props.users.map((user) => ({
|
||||
alt: user.name || "",
|
||||
image: user.avatar || "",
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
</div>
|
||||
) : (
|
||||
<ul className="space-y-3">
|
||||
{eventTypes.map((type, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="hover:border-brand group relative rounded-sm border border-neutral-200 bg-white dark:border-neutral-700 dark:bg-gray-800 dark:hover:border-neutral-600">
|
||||
<Icon.FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
||||
<Link href={getUsernameSlugLink({ users: props.users, slug: type.slug })}>
|
||||
<a className="flex justify-between px-6 py-4" data-testid="event-type-link">
|
||||
<div className="flex-shrink">
|
||||
<h2 className="font-cal font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
|
||||
<EventTypeDescription className="text-sm" eventType={type} />
|
||||
</div>
|
||||
<div className="mt-1 self-center">
|
||||
<AvatarGroup
|
||||
border="border-2 border-white"
|
||||
truncateAfter={4}
|
||||
className="flex flex-shrink-0"
|
||||
size={10}
|
||||
items={props.users.map((user) => ({
|
||||
alt: user.name || "",
|
||||
image: user.avatar || "",
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
const isEmbed = useIsEmbed();
|
||||
const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
|
||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function Type(props: AvailabilityPageProps) {
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
) : props.isDynamic /* && !props.profile.allowDynamicBooking TODO: Re-enable after v1.7 launch */ ? (
|
||||
) : props.isDynamic && !props.profile.allowDynamicBooking ? (
|
||||
<div className="h-screen dark:bg-neutral-900">
|
||||
<main className="mx-auto max-w-3xl px-4 py-24">
|
||||
<div className="space-y-6" data-testid="event-types">
|
||||
@@ -175,6 +175,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
||||
hideBranding: user.hideBranding,
|
||||
plan: user.plan,
|
||||
timeZone: user.timeZone,
|
||||
allowDynamicBooking: false,
|
||||
weekStart: user.weekStart,
|
||||
brandColor: user.brandColor,
|
||||
darkBrandColor: user.darkBrandColor,
|
||||
|
||||
@@ -142,6 +142,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
...e,
|
||||
periodStartDate: e.periodStartDate?.toString() ?? null,
|
||||
periodEndDate: e.periodEndDate?.toString() ?? null,
|
||||
users: users.map((u) => ({
|
||||
id: u.id,
|
||||
name: u.name,
|
||||
username: u.username,
|
||||
avatar: u.avatar,
|
||||
image: u.avatar,
|
||||
slug: u.username,
|
||||
theme: u.theme,
|
||||
})),
|
||||
};
|
||||
})[0];
|
||||
|
||||
|
||||
@@ -258,6 +258,13 @@ async function handler(req: NextApiRequest) {
|
||||
...userSelect,
|
||||
})
|
||||
: eventType.users;
|
||||
const isDynamicAllowed = !users.some((user) => !user.allowDynamicBooking);
|
||||
if (!isDynamicAllowed) {
|
||||
throw new HttpError({
|
||||
message: "Some of the users in this group do not allow dynamic booking",
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
/* If this event was pre-relationship migration */
|
||||
if (!users.length && eventType.userId) {
|
||||
@@ -270,6 +277,9 @@ async function handler(req: NextApiRequest) {
|
||||
if (!eventTypeUser) throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
|
||||
users.push(eventTypeUser);
|
||||
}
|
||||
|
||||
if (!users) throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
|
||||
|
||||
const [organizerUser] = users;
|
||||
/**
|
||||
* @TODO: add a validation to check if organizerUser is found, otherwise it will throw error on user not found
|
||||
@@ -285,7 +295,6 @@ async function handler(req: NextApiRequest) {
|
||||
});
|
||||
|
||||
const tOrganizer = await getTranslation(organizer?.locale ?? "en", "common");
|
||||
|
||||
if (eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
|
||||
const bookingCounts = await getUserNameWithBookingCounts(
|
||||
eventTypeId,
|
||||
|
||||
@@ -99,7 +99,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
? (hashedLink.eventType.locations as LocationObject[])
|
||||
: [];
|
||||
|
||||
const [user] = users;
|
||||
const eventTypeObject = Object.assign({}, hashedLink.eventType, {
|
||||
metadata: {} as JSONObject,
|
||||
recurringEvent: parseRecurringEvent(hashedLink.eventType.recurringEvent),
|
||||
@@ -107,8 +106,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
periodEndDate: hashedLink.eventType.periodEndDate?.toString() ?? null,
|
||||
slug,
|
||||
locations: locationHiddenFilter(locations),
|
||||
users: users.map((u) => ({
|
||||
name: u.name,
|
||||
username: u.username,
|
||||
hideBranding: u.hideBranding,
|
||||
plan: u.plan,
|
||||
timeZone: u.timeZone,
|
||||
})),
|
||||
});
|
||||
|
||||
const [user] = users;
|
||||
|
||||
const schedule = {
|
||||
...user.schedules.filter(
|
||||
(schedule) => !user.defaultScheduleId || schedule.id === user.defaultScheduleId
|
||||
@@ -150,6 +158,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
trpcState: ssr.dehydrate(),
|
||||
previousPage: context.req.headers.referer ?? null,
|
||||
booking,
|
||||
users: [user.username],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -103,6 +103,18 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
...e,
|
||||
periodStartDate: e.periodStartDate?.toString() ?? null,
|
||||
periodEndDate: e.periodEndDate?.toString() ?? null,
|
||||
users: users.map((u) => ({
|
||||
id: u.id,
|
||||
name: u.name,
|
||||
username: u.username,
|
||||
avatar: u.avatar,
|
||||
image: u.avatar,
|
||||
slug: u.username,
|
||||
theme: u.theme,
|
||||
email: u.email,
|
||||
brandColor: u.brandColor,
|
||||
darkBrandColor: u.darkBrandColor,
|
||||
})),
|
||||
};
|
||||
})[0];
|
||||
|
||||
@@ -118,7 +130,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
|
||||
// Checking if number of recurring event ocurrances is valid against event type configuration
|
||||
const recurringEventCount =
|
||||
(eventTypeObject?.recurringEvent?.count &&
|
||||
(eventTypeObject.recurringEvent?.count &&
|
||||
recurringEventCountQuery &&
|
||||
(parseInt(recurringEventCountQuery) <= eventTypeObject.recurringEvent.count
|
||||
? parseInt(recurringEventCountQuery)
|
||||
|
||||
@@ -123,6 +123,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
periodEndDate: eventType.periodEndDate?.toString() ?? null,
|
||||
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
|
||||
locations: locationHiddenFilter(locations),
|
||||
users: eventType.users.map((user) => ({
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
hideBranding: user.hideBranding,
|
||||
plan: user.plan,
|
||||
timeZone: user.timeZone,
|
||||
})),
|
||||
});
|
||||
|
||||
eventTypeObject.availability = [];
|
||||
|
||||
@@ -96,6 +96,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
metadata: (eventType.metadata || {}) as JSONObject,
|
||||
periodStartDate: e.periodStartDate?.toString() ?? null,
|
||||
periodEndDate: e.periodEndDate?.toString() ?? null,
|
||||
users: eventType.users.map((u) => ({
|
||||
id: u.id,
|
||||
name: u.name,
|
||||
username: u.username,
|
||||
avatar: u.avatar,
|
||||
image: u.avatar,
|
||||
slug: u.username,
|
||||
})),
|
||||
};
|
||||
})[0];
|
||||
|
||||
|
||||
@@ -320,6 +320,7 @@ describe("getSchedule", () => {
|
||||
const scheduleOnCompletelyFreeDay = await getSchedule(
|
||||
{
|
||||
eventTypeId: eventType.id,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: "Asia/Kolkata",
|
||||
@@ -349,6 +350,7 @@ describe("getSchedule", () => {
|
||||
const scheduleForDayWithOneBooking = await getSchedule(
|
||||
{
|
||||
eventTypeId: eventType.id,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: "Asia/Kolkata", // GMT+5:30
|
||||
@@ -438,6 +440,7 @@ describe("getSchedule", () => {
|
||||
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule(
|
||||
{
|
||||
eventTypeId: eventType.id,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: "Asia/Kolkata",
|
||||
@@ -480,6 +483,7 @@ describe("getSchedule", () => {
|
||||
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${todayDateString}T18:30:00.000Z`,
|
||||
endTime: `${plus1DateString}T18:29:59.999Z`,
|
||||
timeZone: "Asia/Kolkata",
|
||||
@@ -509,6 +513,7 @@ describe("getSchedule", () => {
|
||||
const scheduleForTeamEventOnADayWithOneBooking = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: "Asia/Kolkata",
|
||||
@@ -557,6 +562,7 @@ describe("getSchedule", () => {
|
||||
const scheduleOfTeamEventHavingAUserWithBlockedTimeInAnotherEvent = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: "Asia/Kolkata",
|
||||
|
||||
@@ -24,6 +24,7 @@ const baseUser = {
|
||||
plan: UserPlan.PRO,
|
||||
avatar: "",
|
||||
hideBranding: true,
|
||||
allowDynamicBooking: true,
|
||||
};
|
||||
|
||||
it("can find lucky users", async () => {
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function getBusyTimes(params: {
|
||||
logger.silly(`Busy Time from Cal Bookings ${JSON.stringify(busyTimes)}`);
|
||||
const endPrismaBookingGet = performance.now();
|
||||
logger.debug(`prisma booking get took ${endPrismaBookingGet - startPrismaBookingGet}ms`);
|
||||
if (credentials.length > 0) {
|
||||
if (credentials?.length > 0) {
|
||||
const calendarBusyTimes = await getBusyCalendarTimes(credentials, startTime, endTime, selectedCalendars);
|
||||
|
||||
busyTimes.push(...calendarBusyTimes); /*
|
||||
|
||||
@@ -146,6 +146,7 @@ export async function getUserAvailability(
|
||||
|
||||
const timeZone = timezone || schedule?.timeZone || eventType?.timeZone || currentUser.timeZone;
|
||||
const startGetWorkingHours = performance.now();
|
||||
|
||||
const workingHours = getWorkingHours(
|
||||
{ timeZone },
|
||||
schedule.availability ||
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
import type { EventTypeCustomInput } from "@prisma/client";
|
||||
import { PeriodType, Prisma, SchedulingType, UserPlan } from "@prisma/client";
|
||||
|
||||
import { baseUserSelect } from "@calcom/prisma/selects";
|
||||
import { userSelect } from "@calcom/prisma/selects";
|
||||
|
||||
const userSelectData = Prisma.validator<Prisma.UserArgs>()({ select: baseUserSelect });
|
||||
type User = Prisma.UserGetPayload<typeof userSelectData>;
|
||||
|
||||
const availability = [
|
||||
{
|
||||
days: [1, 2, 3, 4, 5],
|
||||
startTime: new Date().getTime(),
|
||||
endTime: new Date().getTime(),
|
||||
date: new Date(),
|
||||
scheduleId: null,
|
||||
},
|
||||
];
|
||||
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||
|
||||
type UsernameSlugLinkProps = {
|
||||
users: {
|
||||
@@ -33,6 +22,31 @@ type UsernameSlugLinkProps = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
const user: User = {
|
||||
theme: null,
|
||||
credentials: [],
|
||||
username: "john.doe",
|
||||
timeZone: "",
|
||||
bufferTime: 0,
|
||||
availability: [],
|
||||
id: 0,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
selectedCalendars: [],
|
||||
schedules: [],
|
||||
defaultScheduleId: null,
|
||||
locale: "en",
|
||||
email: "john.doe@example.com",
|
||||
name: "John doe",
|
||||
avatar: "",
|
||||
destinationCalendar: null,
|
||||
plan: UserPlan.PRO,
|
||||
hideBranding: true,
|
||||
brandColor: "#797979",
|
||||
darkBrandColor: "#efefef",
|
||||
allowDynamicBooking: true,
|
||||
};
|
||||
|
||||
const customInputs: EventTypeCustomInput[] = [];
|
||||
|
||||
const commons = {
|
||||
@@ -52,6 +66,8 @@ const commons = {
|
||||
schedule: null,
|
||||
timeZone: null,
|
||||
successRedirectUrl: "",
|
||||
teamId: null,
|
||||
scheduleId: null,
|
||||
availability: [],
|
||||
price: 0,
|
||||
currency: "usd",
|
||||
@@ -70,31 +86,7 @@ const commons = {
|
||||
hidden: false,
|
||||
userId: 0,
|
||||
workflows: [],
|
||||
users: [
|
||||
{
|
||||
id: 0,
|
||||
plan: UserPlan.PRO,
|
||||
email: "jdoe@example.com",
|
||||
name: "John Doe",
|
||||
username: "jdoe",
|
||||
avatar: "",
|
||||
hideBranding: true,
|
||||
timeZone: "",
|
||||
destinationCalendar: null,
|
||||
credentials: [],
|
||||
bufferTime: 0,
|
||||
locale: "en",
|
||||
theme: null,
|
||||
brandColor: "#292929",
|
||||
darkBrandColor: "#fafafa",
|
||||
availability: [],
|
||||
selectedCalendars: [],
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
schedules: [],
|
||||
defaultScheduleId: null,
|
||||
} as User,
|
||||
],
|
||||
users: [user],
|
||||
};
|
||||
|
||||
const min15Event = {
|
||||
@@ -103,6 +95,7 @@ const min15Event = {
|
||||
title: "15min",
|
||||
eventName: "Dynamic Collective 15min Event",
|
||||
description: "Dynamic Collective 15min Event",
|
||||
position: 0,
|
||||
...commons,
|
||||
};
|
||||
const min30Event = {
|
||||
@@ -111,6 +104,7 @@ const min30Event = {
|
||||
title: "30min",
|
||||
eventName: "Dynamic Collective 30min Event",
|
||||
description: "Dynamic Collective 30min Event",
|
||||
position: 1,
|
||||
...commons,
|
||||
};
|
||||
const min60Event = {
|
||||
@@ -119,6 +113,7 @@ const min60Event = {
|
||||
title: "60min",
|
||||
eventName: "Dynamic Collective 60min Event",
|
||||
description: "Dynamic Collective 60min Event",
|
||||
position: 2,
|
||||
...commons,
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export const availabilityUserSelect = Prisma.validator<Prisma.UserSelect>()({
|
||||
availability: true,
|
||||
id: true,
|
||||
startTime: true,
|
||||
username: true,
|
||||
endTime: true,
|
||||
selectedCalendars: true,
|
||||
schedules: {
|
||||
@@ -22,7 +23,6 @@ export const availabilityUserSelect = Prisma.validator<Prisma.UserSelect>()({
|
||||
export const baseUserSelect = Prisma.validator<Prisma.UserSelect>()({
|
||||
email: true,
|
||||
name: true,
|
||||
username: true,
|
||||
destinationCalendar: true,
|
||||
locale: true,
|
||||
plan: true,
|
||||
@@ -38,7 +38,7 @@ export const userSelect = Prisma.validator<Prisma.UserArgs>()({
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
username: true,
|
||||
allowDynamicBooking: true,
|
||||
destinationCalendar: true,
|
||||
locale: true,
|
||||
plan: true,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { z } from "zod";
|
||||
import type { CurrentSeats } from "@calcom/core/getUserAvailability";
|
||||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import isOutOfBounds from "@calcom/lib/isOutOfBounds";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
@@ -23,7 +24,9 @@ const getScheduleSchema = z
|
||||
// endTime ISOString
|
||||
endTime: z.string(),
|
||||
// Event type ID
|
||||
eventTypeId: z.number().optional(),
|
||||
eventTypeId: z.number().int().optional(),
|
||||
// Event type slug
|
||||
eventTypeSlug: z.string(),
|
||||
// invitee timezone
|
||||
timeZone: z.string().optional(),
|
||||
// or list of users (for dynamic events)
|
||||
@@ -100,17 +103,7 @@ export const slotsRouter = createRouter().query("getSchedule", {
|
||||
},
|
||||
});
|
||||
|
||||
export async function getSchedule(
|
||||
input: {
|
||||
timeZone?: string | undefined;
|
||||
eventTypeId?: number | undefined;
|
||||
usernameList?: string[] | undefined;
|
||||
debug?: boolean | undefined;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
},
|
||||
ctx: { prisma: typeof prisma }
|
||||
) {
|
||||
export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx: { prisma: typeof prisma }) {
|
||||
if (input.debug === true) {
|
||||
logger.setSettings({ minLevel: "debug" });
|
||||
}
|
||||
@@ -118,7 +111,7 @@ export async function getSchedule(
|
||||
logger.setSettings({ minLevel: "silly" });
|
||||
}
|
||||
const startPrismaEventTypeGet = performance.now();
|
||||
const eventType = await ctx.prisma.eventType.findUnique({
|
||||
const eventTypeObject = await ctx.prisma.eventType.findUnique({
|
||||
where: {
|
||||
id: input.eventTypeId,
|
||||
},
|
||||
@@ -152,12 +145,42 @@ export async function getSchedule(
|
||||
},
|
||||
users: {
|
||||
select: {
|
||||
username: true,
|
||||
...availabilityUserSelect,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const isDynamicBooking = !input.eventTypeId;
|
||||
// For dynamic booking, we need to get and update user credentials, schedule and availability in the eventTypeObject as they're required in the new availability logic
|
||||
const dynamicEventType = getDefaultEvent(input.eventTypeSlug);
|
||||
let dynamicEventTypeObject = dynamicEventType;
|
||||
|
||||
if (isDynamicBooking) {
|
||||
const users = await ctx.prisma.user.findMany({
|
||||
where: {
|
||||
username: {
|
||||
in: input.usernameList,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
allowDynamicBooking: true,
|
||||
...availabilityUserSelect,
|
||||
},
|
||||
});
|
||||
const isDynamicAllowed = !users.some((user) => !user.allowDynamicBooking);
|
||||
if (!isDynamicAllowed) {
|
||||
throw new TRPCError({
|
||||
message: "Some of the users in this group do not allow dynamic booking",
|
||||
code: "UNAUTHORIZED",
|
||||
});
|
||||
}
|
||||
dynamicEventTypeObject = Object.assign({}, dynamicEventType, {
|
||||
users,
|
||||
});
|
||||
}
|
||||
const eventType = isDynamicBooking ? dynamicEventTypeObject : eventTypeObject;
|
||||
|
||||
const endPrismaEventTypeGet = performance.now();
|
||||
logger.debug(
|
||||
`Prisma eventType get took ${endPrismaEventTypeGet - startPrismaEventTypeGet}ms for event:${
|
||||
@@ -189,6 +212,7 @@ export async function getSchedule(
|
||||
} = await getUserAvailability(
|
||||
{
|
||||
userId: currentUser.id,
|
||||
username: currentUser.username || "",
|
||||
dateFrom: startTime.format(),
|
||||
dateTo: endTime.format(),
|
||||
eventTypeId: input.eventTypeId,
|
||||
|
||||
@@ -412,7 +412,6 @@ export const viewerTeamsRouter = createProtectedRouter()
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
...availabilityUserSelect,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user