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:
Syed Ali Shahbaz
2022-08-12 19:29:29 +00:00
committed by GitHub
co-authored by Peer Richelsen kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> zomars
parent 870f70fc38
commit ee02112a7c
17 changed files with 197 additions and 101 deletions
@@ -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
View File
@@ -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";
+2 -1
View File
@@ -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,
+9
View File
@@ -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];
+10 -1
View File
@@ -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,
+10 -1
View File
@@ -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],
},
};
};
+13 -1
View File
@@ -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)
+7
View File
@@ -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 = [];
+8
View File
@@ -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];
+6
View File
@@ -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 () => {
+1 -1
View File
@@ -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); /*
+1
View File
@@ -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 ||
+33 -38
View File
@@ -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,
};
+2 -2
View File
@@ -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,
+38 -14
View File
@@ -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,
},
},