import type { GetServerSidePropsContext } from "next"; import { z } from "zod"; import type { LocationObject } from "@calcom/app-store/locations"; import { privacyFilteredLocations } from "@calcom/app-store/locations"; import { getAppFromSlug } from "@calcom/app-store/utils"; import dayjs from "@calcom/dayjs"; import getBooking from "@calcom/features/bookings/lib/get-booking"; import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; import { parseRecurringEvent } from "@calcom/lib"; import { getDefaultEvent, getDynamicEventName, getGroupName, getUsernameList, } from "@calcom/lib/defaultEvents"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import prisma, { bookEventTypeSelect } from "@calcom/prisma"; import { customInputSchema, EventTypeMetaDataSchema, userMetadata as userMetadataSchema, } from "@calcom/prisma/zod-utils"; import type { inferSSRProps } from "@lib/types/inferSSRProps"; import PageWrapper from "@components/PageWrapper"; import BookingPage from "@components/booking/pages/BookingPage"; import { ssrInit } from "@server/lib/ssr"; export type BookPageProps = inferSSRProps; export default function Book(props: BookPageProps) { const { t } = useLocale(); return props.away ? (

😴{" " + t("user_away")}

{t("user_away_description")}

) : props.isDynamicGroupBooking && !props.profile.allowDynamicBooking ? (

{" " + t("unavailable")}

{t("user_dynamic_booking_disabled")}

) : ( ); } Book.isBookingPage = true; Book.PageWrapper = PageWrapper; const querySchema = z.object({ bookingUid: z.string().optional(), count: z.coerce.number().optional(), embed: z.string().optional(), rescheduleUid: z.string().optional(), slug: z.string().optional(), /** This is the event "type" ID */ type: z.coerce.number().optional(), user: z.string(), seatReferenceUid: z.string().optional(), date: z.string().optional(), duration: z.coerce.number().optional(), }); export async function getServerSideProps(context: GetServerSidePropsContext) { const ssr = await ssrInit(context); const query = querySchema.parse(context.query); const usernameList = getUsernameList(query.user); const eventTypeSlug = query.slug; const recurringEventCountQuery = query.count; const users = await prisma.user.findMany({ where: { username: { in: usernameList, }, }, select: { id: true, username: true, name: true, email: true, bio: true, avatar: true, theme: true, brandColor: true, darkBrandColor: true, allowDynamicBooking: true, away: true, metadata: true, }, }); if (!users.length) return { notFound: true }; const [user] = users; const isDynamicGroupBooking = users.length > 1 && !!eventTypeSlug; // Dynamic Group link doesn't need a type but it must have a slug if ((!isDynamicGroupBooking && !query.type) || (users.length > 1 && !eventTypeSlug)) { return { notFound: true }; } const eventTypeRaw = isDynamicGroupBooking ? getDefaultEvent(eventTypeSlug) : await prisma.eventType.findUnique({ where: { id: query.type, }, select: { ...bookEventTypeSelect, }, }); if (!eventTypeRaw) return { notFound: true }; const eventType = { ...eventTypeRaw, metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}), bookingFields: getBookingFieldsWithSystemFields(eventTypeRaw), recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent), }; const getLocations = () => { let locations = eventTypeRaw.locations || []; if (!isDynamicGroupBooking) return locations; let sortedUsers: typeof users = []; // sort and be in the same order as usernameList so first user is the first user in the list if (users.length > 1) { sortedUsers = users.sort((a, b) => { const aIndex = (a.username && usernameList.indexOf(a.username)) || 0; const bIndex = (b.username && usernameList.indexOf(b.username)) || 0; return aIndex - bIndex; }); } // Get the prefered location type from the first user const firstUsersMetadata = userMetadataSchema.parse(sortedUsers[0].metadata || {}); const preferedLocationType = firstUsersMetadata?.defaultConferencingApp; if (preferedLocationType?.appSlug) { const foundApp = getAppFromSlug(preferedLocationType.appSlug); const appType = foundApp?.appData?.location?.type; if (appType) { // Replace the location with the prefered location type // This will still be default to daily if the app is not found locations = [{ type: appType, link: preferedLocationType.appLink }] as LocationObject[]; } } return locations; }; const eventTypeObject = [eventType].map((e) => { let locations = getLocations(); locations = privacyFilteredLocations(locations as LocationObject[]); return { ...e, locations: locations, periodStartDate: e.periodStartDate?.toString() ?? null, periodEndDate: e.periodEndDate?.toString() ?? null, schedulingType: null, customInputs: customInputSchema.array().parse(e.customInputs || []), 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, })), descriptionAsSafeHTML: markdownToSafeHTML(eventType.description), }; })[0]; // If rescheduleUid and event has seats lets convert Uid to bookingUid let rescheduleUid = query.rescheduleUid; const rescheduleEventTypeHasSeats = query.rescheduleUid && eventTypeRaw.seatsPerTimeSlot; let attendeeEmail: string; let bookingUidWithSeats: string | null = null; if (rescheduleEventTypeHasSeats) { const bookingSeat = await prisma.bookingSeat.findFirst({ where: { referenceUid: query.rescheduleUid, }, select: { id: true, attendee: true, booking: { select: { uid: true, }, }, }, }); if (bookingSeat) { rescheduleUid = bookingSeat.booking.uid; attendeeEmail = bookingSeat.attendee.email; } } if (query.duration) { // If it's not reschedule but event Type has seats we should obtain // the bookingUid regardless and use it to get the booking const currentSeats = await prisma.booking.findFirst({ where: { eventTypeId: eventTypeRaw.id, startTime: dayjs(query.date).toISOString(), endTime: dayjs(query.date).add(query.duration, "minutes").toISOString(), }, select: { uid: true, }, }); if (currentSeats && currentSeats) { bookingUidWithSeats = currentSeats.uid; } } let booking: GetBookingType | null = null; if (rescheduleUid || query.bookingUid || bookingUidWithSeats) { booking = await getBooking(prisma, rescheduleUid || query.bookingUid || bookingUidWithSeats || ""); } if (rescheduleEventTypeHasSeats && booking?.attendees && booking?.attendees.length > 0) { const currentAttendee = booking?.attendees.find((attendee) => { return attendee.email === attendeeEmail; }); if (currentAttendee) { booking.attendees = [currentAttendee] || []; } } const dynamicNames = isDynamicGroupBooking ? users.map((user) => user.name || "") : []; const profile = isDynamicGroupBooking ? { name: getGroupName(dynamicNames), image: null, slug: eventTypeSlug, theme: null, brandColor: "", darkBrandColor: "", allowDynamicBooking: !users.some((user) => !user.allowDynamicBooking), eventName: getDynamicEventName(dynamicNames, eventTypeSlug), } : { name: user.name || user.username, image: user.avatar, slug: user.username, theme: user.theme, brandColor: user.brandColor, darkBrandColor: user.darkBrandColor, eventName: null, }; // Checking if number of recurring event ocurrances is valid against event type configuration const recurringEventCount = (eventType.recurringEvent?.count && recurringEventCountQuery && (recurringEventCountQuery <= eventType.recurringEvent.count ? recurringEventCountQuery : eventType.recurringEvent.count)) || null; const currentSlotBooking = await getBooking(prisma, bookingUidWithSeats || ""); return { props: { away: user.away, profile, // Dynamic group has no theme preference right now. It uses system theme. themeBasis: isDynamicGroupBooking ? null : user.username, eventType: eventTypeObject, booking, currentSlotBooking: currentSlotBooking, recurringEventCount, trpcState: ssr.dehydrate(), isDynamicGroupBooking, hasHashedBookingLink: false, hashedLink: null, isEmbed: !!query.embed, }, }; }