Availabilty consolitadion (#3010)
parent
1f1e364a30
commit
22d2bae46b
|
@ -826,22 +826,8 @@ const BookingPage = ({
|
|||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
{mutation.isError && (
|
||||
<div
|
||||
data-testid="booking-fail"
|
||||
className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ltr:ml-3 rtl:mr-3">
|
||||
<p className="text-sm text-yellow-700">
|
||||
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
|
||||
{(mutation.error as HttpError)?.message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{(mutation.isError || recurringMutation.isError) && (
|
||||
<ErrorMessage error={mutation.error || recurringMutation.error} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -853,3 +839,24 @@ const BookingPage = ({
|
|||
};
|
||||
|
||||
export default BookingPage;
|
||||
|
||||
function ErrorMessage({ error }: { error: unknown }) {
|
||||
const { t } = useLocale();
|
||||
const { query: { rescheduleUid } = {} } = useRouter();
|
||||
|
||||
return (
|
||||
<div data-testid="booking-fail" className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ltr:ml-3 rtl:mr-3">
|
||||
<p className="text-sm text-yellow-700">
|
||||
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
|
||||
{error instanceof HttpError || error instanceof Error ? error.message : "Unknown error"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||
import { CreateEventType, CreateEventTypeResponse } from "@lib/types/event-type";
|
||||
|
||||
/**
|
||||
* @deprecated Use `trpc.useMutation("viewer.eventTypes.create")` instead.
|
||||
*/
|
||||
const createEventType = async (data: CreateEventType) => {
|
||||
const response = await fetch.post<CreateEventType, CreateEventTypeResponse>(
|
||||
"/api/availability/eventtype",
|
||||
data
|
||||
);
|
||||
return response;
|
||||
};
|
||||
|
||||
export default createEventType;
|
|
@ -1,14 +0,0 @@
|
|||
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||
|
||||
/**
|
||||
* @deprecated Use `trpc.useMutation("viewer.eventTypes.delete")` instead.
|
||||
*/
|
||||
const deleteEventType = async (data: { id: number }) => {
|
||||
const response = await fetch.remove<{ id: number }, Record<string, never>>(
|
||||
"/api/availability/eventtype",
|
||||
data
|
||||
);
|
||||
return response;
|
||||
};
|
||||
|
||||
export default deleteEventType;
|
|
@ -1,18 +0,0 @@
|
|||
import { EventType } from "@prisma/client";
|
||||
|
||||
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||
import { EventTypeInput } from "@lib/types/event-type";
|
||||
|
||||
type EventTypeResponse = {
|
||||
eventType: EventType;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use `trpc.useMutation("viewer.eventTypes.update")` instead.
|
||||
*/
|
||||
const updateEventType = async (data: EventTypeInput) => {
|
||||
const response = await fetch.patch<EventTypeInput, EventTypeResponse>("/api/availability/eventtype", data);
|
||||
return response;
|
||||
};
|
||||
|
||||
export default updateEventType;
|
|
@ -1,87 +0,0 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getWorkingHours } from "@lib/availability";
|
||||
import getBusyTimes from "@lib/getBusyTimes";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export async function getUserAvailability(query: {
|
||||
username: string;
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
eventTypeId?: number;
|
||||
timezone?: string;
|
||||
}) {
|
||||
const username = asStringOrNull(query.username);
|
||||
const dateFrom = dayjs(asStringOrNull(query.dateFrom));
|
||||
const dateTo = dayjs(asStringOrNull(query.dateTo));
|
||||
|
||||
if (!username) throw new Error("Missing username");
|
||||
if (!dateFrom.isValid() || !dateTo.isValid()) throw new Error("Invalid time range given.");
|
||||
|
||||
const rawUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
username: username,
|
||||
},
|
||||
select: {
|
||||
credentials: true,
|
||||
timeZone: true,
|
||||
bufferTime: true,
|
||||
availability: true,
|
||||
id: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
selectedCalendars: true,
|
||||
},
|
||||
});
|
||||
|
||||
const getEventType = (id: number) =>
|
||||
prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
timeZone: true,
|
||||
availability: {
|
||||
select: {
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
days: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type EventType = Prisma.PromiseReturnType<typeof getEventType>;
|
||||
let eventType: EventType | null = null;
|
||||
if (query.eventTypeId) eventType = await getEventType(query.eventTypeId);
|
||||
|
||||
if (!rawUser) throw new Error("No user found");
|
||||
|
||||
const { selectedCalendars, ...currentUser } = rawUser;
|
||||
|
||||
const busyTimes = await getBusyTimes({
|
||||
credentials: currentUser.credentials,
|
||||
startTime: dateFrom.format(),
|
||||
endTime: dateTo.format(),
|
||||
eventTypeId: query.eventTypeId,
|
||||
userId: currentUser.id,
|
||||
selectedCalendars,
|
||||
});
|
||||
|
||||
const bufferedBusyTimes = busyTimes.map((a) => ({
|
||||
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
|
||||
end: dayjs(a.end).add(currentUser.bufferTime, "minute").toString(),
|
||||
}));
|
||||
|
||||
const timeZone = query.timezone || eventType?.timeZone || currentUser.timeZone;
|
||||
const workingHours = getWorkingHours(
|
||||
{ timeZone },
|
||||
eventType?.availability.length ? eventType.availability : currentUser.availability
|
||||
);
|
||||
|
||||
return {
|
||||
busy: bufferedBusyTimes,
|
||||
timeZone,
|
||||
workingHours,
|
||||
};
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import Stripe from "stripe";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
type Handle<T> = (req: NextApiRequest, res: NextApiResponse) => Promise<T>;
|
||||
|
||||
/** Allows us to get type inference from API handler responses */
|
||||
function defaultResponder<T>(f: Handle<T>) {
|
||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
const result = await f(req, res);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
res.statusCode = err.statusCode;
|
||||
res.json({ message: err.message });
|
||||
} else if (err instanceof Stripe.errors.StripeInvalidRequestError) {
|
||||
console.error("err", err);
|
||||
res.statusCode = err.statusCode || 500;
|
||||
res.json({ message: "Stripe error: " + err.message });
|
||||
} else if (err instanceof ZodError && err.name === "ZodError") {
|
||||
console.error("err", JSON.parse(err.message)[0].message);
|
||||
res.statusCode = 400;
|
||||
res.json({
|
||||
message: "Validation errors: " + err.issues.map((i) => `—'${i.path}' ${i.message}`).join(". "),
|
||||
});
|
||||
} else {
|
||||
console.error("err", err);
|
||||
res.statusCode = 500;
|
||||
res.json({ message: "Unknown error" });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultResponder;
|
|
@ -1,2 +0,0 @@
|
|||
export { default as defaultHandler } from "./defaultHandler";
|
||||
export { default as defaultResponder } from "./defaultResponder";
|
|
@ -1 +0,0 @@
|
|||
export * from "./api";
|
|
@ -1,141 +1,25 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import type { NextApiRequest } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getWorkingHours } from "@lib/availability";
|
||||
import getBusyTimes from "@lib/getBusyTimes";
|
||||
import prisma from "@lib/prisma";
|
||||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
const availabilitySchema = z.object({
|
||||
user: z.string(),
|
||||
dateFrom: z.string(),
|
||||
dateTo: z.string(),
|
||||
eventTypeId: stringOrNumber.optional(),
|
||||
});
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = asStringOrNull(req.query.user);
|
||||
const dateFrom = dayjs(asStringOrNull(req.query.dateFrom));
|
||||
const dateTo = dayjs(asStringOrNull(req.query.dateTo));
|
||||
const eventTypeId = typeof req.query.eventTypeId === "string" ? parseInt(req.query.eventTypeId) : undefined;
|
||||
|
||||
if (!dateFrom.isValid() || !dateTo.isValid()) {
|
||||
return res.status(400).json({ message: "Invalid time range given." });
|
||||
}
|
||||
|
||||
const rawUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
username: user as string,
|
||||
},
|
||||
select: {
|
||||
credentials: true,
|
||||
timeZone: true,
|
||||
bufferTime: true,
|
||||
availability: true,
|
||||
id: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
selectedCalendars: true,
|
||||
schedules: {
|
||||
select: {
|
||||
availability: true,
|
||||
timeZone: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
defaultScheduleId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const getEventType = (id: number) =>
|
||||
prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
seatsPerTimeSlot: true,
|
||||
timeZone: true,
|
||||
schedule: {
|
||||
select: {
|
||||
availability: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
availability: {
|
||||
select: {
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
days: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type EventType = Prisma.PromiseReturnType<typeof getEventType>;
|
||||
let eventType: EventType | null = null;
|
||||
if (eventTypeId) eventType = await getEventType(eventTypeId);
|
||||
|
||||
if (!rawUser) throw new Error("No user found");
|
||||
|
||||
const { selectedCalendars, ...currentUser } = rawUser;
|
||||
|
||||
const busyTimes = await getBusyTimes({
|
||||
credentials: currentUser.credentials,
|
||||
startTime: dateFrom.format(),
|
||||
endTime: dateTo.format(),
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { user: username, eventTypeId, dateTo, dateFrom } = availabilitySchema.parse(req.query);
|
||||
return getUserAvailability({
|
||||
username,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
eventTypeId,
|
||||
userId: currentUser.id,
|
||||
selectedCalendars,
|
||||
});
|
||||
|
||||
const bufferedBusyTimes = busyTimes.map((a) => ({
|
||||
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute"),
|
||||
end: dayjs(a.end).add(currentUser.bufferTime, "minute"),
|
||||
}));
|
||||
|
||||
const schedule = eventType?.schedule
|
||||
? { ...eventType?.schedule }
|
||||
: {
|
||||
...currentUser.schedules.filter(
|
||||
(schedule) => !currentUser.defaultScheduleId || schedule.id === currentUser.defaultScheduleId
|
||||
)[0],
|
||||
};
|
||||
|
||||
const timeZone = schedule.timeZone || eventType?.timeZone || currentUser.timeZone;
|
||||
|
||||
const workingHours = getWorkingHours(
|
||||
{
|
||||
timeZone,
|
||||
},
|
||||
schedule.availability ||
|
||||
(eventType?.availability.length ? eventType.availability : currentUser.availability)
|
||||
);
|
||||
|
||||
/* Current logic is if a booking is in a time slot mark it as busy, but seats can have more than one attendee so grab
|
||||
current bookings with a seats event type and display them on the calendar, even if they are full */
|
||||
let currentSeats;
|
||||
if (eventType?.seatsPerTimeSlot) {
|
||||
currentSeats = await prisma.booking.findMany({
|
||||
where: {
|
||||
eventTypeId: eventTypeId,
|
||||
startTime: {
|
||||
gte: dateFrom.format(),
|
||||
lte: dateTo.format(),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
uid: true,
|
||||
startTime: true,
|
||||
_count: {
|
||||
select: {
|
||||
attendees: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
busy: bufferedBusyTimes,
|
||||
timeZone,
|
||||
workingHours,
|
||||
currentSeats,
|
||||
});
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
|
|
|
@ -6,6 +6,7 @@ import EventManager from "@calcom/core/EventManager";
|
|||
import { sendDeclinedEmails, sendScheduledEmails } from "@calcom/emails";
|
||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
||||
import { refund } from "@ee/lib/stripe/server";
|
||||
|
@ -15,8 +16,6 @@ import { HttpError } from "@lib/core/http/error";
|
|||
|
||||
import { getTranslation } from "@server/lib/i18n";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "~/common";
|
||||
|
||||
const authorized = async (
|
||||
currentUser: Pick<User, "id">,
|
||||
booking: Pick<Booking, "eventTypeId" | "userId">
|
||||
|
|
|
@ -5,33 +5,35 @@ import dayjsBusinessTime from "dayjs-business-days2";
|
|||
import isBetween from "dayjs/plugin/isBetween";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import type { NextApiRequest } from "next";
|
||||
import rrule from "rrule";
|
||||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import EventManager from "@calcom/core/EventManager";
|
||||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||
import {
|
||||
sendAttendeeRequestEmail,
|
||||
sendOrganizerRequestEmail,
|
||||
sendRescheduledEmails,
|
||||
sendScheduledEmails,
|
||||
} from "@calcom/emails";
|
||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||
import { getLuckyUsers, isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma, { userSelect } from "@calcom/prisma";
|
||||
import { extendedBookingCreateBody } from "@calcom/prisma/zod-utils";
|
||||
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
|
||||
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
||||
import { handlePayment } from "@ee/lib/stripe/server";
|
||||
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import { ensureArray } from "@lib/ensureArray";
|
||||
import { getEventName } from "@lib/event";
|
||||
import getBusyTimes from "@lib/getBusyTimes";
|
||||
import isOutOfBounds from "@lib/isOutOfBounds";
|
||||
import prisma from "@lib/prisma";
|
||||
import { BookingCreateBody } from "@lib/types/booking";
|
||||
import sendPayload from "@lib/webhooks/sendPayload";
|
||||
import getSubscribers from "@lib/webhooks/subscriptions";
|
||||
|
||||
|
@ -81,45 +83,38 @@ function isAvailable(busyTimes: BufferedBusyTimes, time: dayjs.ConfigType, lengt
|
|||
// Check for conflicts
|
||||
let t = true;
|
||||
|
||||
if (Array.isArray(busyTimes) && busyTimes.length > 0) {
|
||||
busyTimes.forEach((busyTime) => {
|
||||
const startTime = dayjs(busyTime.start);
|
||||
const endTime = dayjs(busyTime.end);
|
||||
// Early return
|
||||
if (!Array.isArray(busyTimes) || busyTimes.length < 1) return t;
|
||||
|
||||
// Check if time is between start and end times
|
||||
if (dayjs(time).isBetween(startTime, endTime, null, "[)")) {
|
||||
t = false;
|
||||
}
|
||||
let i = 0;
|
||||
while (t === true && i < busyTimes.length) {
|
||||
const busyTime = busyTimes[i];
|
||||
i++;
|
||||
const startTime = dayjs(busyTime.start);
|
||||
const endTime = dayjs(busyTime.end);
|
||||
|
||||
// Check if slot end time is between start and end time
|
||||
if (dayjs(time).add(length, "minutes").isBetween(startTime, endTime)) {
|
||||
t = false;
|
||||
}
|
||||
// Check if time is between start and end times
|
||||
if (dayjs(time).isBetween(startTime, endTime, null, "[)")) {
|
||||
t = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if startTime is between slot
|
||||
if (startTime.isBetween(dayjs(time), dayjs(time).add(length, "minutes"))) {
|
||||
t = false;
|
||||
}
|
||||
});
|
||||
// Check if slot end time is between start and end time
|
||||
if (dayjs(time).add(length, "minutes").isBetween(startTime, endTime)) {
|
||||
t = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if startTime is between slot
|
||||
if (startTime.isBetween(dayjs(time), dayjs(time).add(length, "minutes"))) {
|
||||
t = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
const userSelect = Prisma.validator<Prisma.UserArgs>()({
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
username: true,
|
||||
timeZone: true,
|
||||
credentials: true,
|
||||
bufferTime: true,
|
||||
destinationCalendar: true,
|
||||
locale: true,
|
||||
},
|
||||
});
|
||||
|
||||
const getUserNameWithBookingCounts = async (eventTypeId: number, selectedUserNames: string[]) => {
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
|
@ -191,6 +186,20 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
|||
seatsPerTimeSlot: true,
|
||||
recurringEvent: true,
|
||||
locations: true,
|
||||
timeZone: true,
|
||||
schedule: {
|
||||
select: {
|
||||
availability: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
availability: {
|
||||
select: {
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
days: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -202,23 +211,15 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
|||
|
||||
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||
|
||||
type ExtendedBookingCreateBody = BookingCreateBody & {
|
||||
noEmail?: boolean;
|
||||
recurringCount?: number;
|
||||
rescheduleReason?: string;
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { recurringCount, noEmail, ...reqBody } = req.body as ExtendedBookingCreateBody;
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { recurringCount, noEmail, eventTypeSlug, eventTypeId, hasHashedBookingLink, language, ...reqBody } =
|
||||
extendedBookingCreateBody.parse(req.body);
|
||||
|
||||
// handle dynamic user
|
||||
const dynamicUserList = Array.isArray(reqBody.user)
|
||||
? getGroupName(req.body.user)
|
||||
: getUsernameList(reqBody.user as string);
|
||||
const hasHashedBookingLink = reqBody.hasHashedBookingLink;
|
||||
const eventTypeSlug = reqBody.eventTypeSlug;
|
||||
const eventTypeId = reqBody.eventTypeId;
|
||||
const tAttendees = await getTranslation(reqBody.language ?? "en", "common");
|
||||
? getGroupName(reqBody.user)
|
||||
: getUsernameList(reqBody.user);
|
||||
const tAttendees = await getTranslation(language ?? "en", "common");
|
||||
const tGuests = await getTranslation("en", "common");
|
||||
log.debug(`Booking eventType ${eventTypeId} started`);
|
||||
|
||||
|
@ -233,11 +234,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
};
|
||||
|
||||
log.error(`Booking ${eventTypeId} failed`, error);
|
||||
return res.status(400).json(error);
|
||||
throw new HttpError({ statusCode: 400, message: error.message });
|
||||
}
|
||||
|
||||
const eventType = !eventTypeId ? getDefaultEvent(eventTypeSlug) : await getEventTypesFromDB(eventTypeId);
|
||||
if (!eventType) return res.status(404).json({ message: "eventType.notFound" });
|
||||
if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" });
|
||||
|
||||
let users = !eventTypeId
|
||||
? await prisma.user.findMany({
|
||||
|
@ -258,7 +259,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
},
|
||||
...userSelect,
|
||||
});
|
||||
if (!eventTypeUser) return res.status(404).json({ message: "eventTypeUser.notFound" });
|
||||
if (!eventTypeUser) throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
|
||||
users.push(eventTypeUser);
|
||||
}
|
||||
const [organizerUser] = users;
|
||||
|
@ -287,23 +288,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
email: reqBody.email,
|
||||
name: reqBody.name,
|
||||
timeZone: reqBody.timeZone,
|
||||
language: { translate: tAttendees, locale: reqBody.language ?? "en" },
|
||||
language: { translate: tAttendees, locale: language ?? "en" },
|
||||
},
|
||||
];
|
||||
const guests = (reqBody.guests || []).map((guest) => {
|
||||
const g = {
|
||||
email: guest,
|
||||
name: "",
|
||||
timeZone: reqBody.timeZone,
|
||||
language: { translate: tGuests, locale: "en" },
|
||||
};
|
||||
return g;
|
||||
});
|
||||
const guests = (reqBody.guests || []).map((guest) => ({
|
||||
email: guest,
|
||||
name: "",
|
||||
timeZone: reqBody.timeZone,
|
||||
language: { translate: tGuests, locale: "en" },
|
||||
}));
|
||||
|
||||
// For seats, if the booking already exists then we want to add the new attendee to the existing booking
|
||||
if (reqBody.bookingUid) {
|
||||
if (!eventType.seatsPerTimeSlot)
|
||||
return res.status(404).json({ message: "Event type does not have seats" });
|
||||
throw new HttpError({ statusCode: 404, message: "Event type does not have seats" });
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
|
@ -313,13 +311,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
attendees: true,
|
||||
},
|
||||
});
|
||||
if (!booking) return res.status(404).json({ message: "Booking not found" });
|
||||
if (!booking) throw new HttpError({ statusCode: 404, message: "Booking not found" });
|
||||
|
||||
if (eventType.seatsPerTimeSlot <= booking.attendees.length)
|
||||
return res.status(409).json({ message: "Booking seats are full" });
|
||||
throw new HttpError({ statusCode: 409, message: "Booking seats are full" });
|
||||
|
||||
if (booking.attendees.some((attendee) => attendee.email === invitee[0].email))
|
||||
return res.status(409).json({ message: "Already signed up for time slot" });
|
||||
throw new HttpError({ statusCode: 409, message: "Already signed up for time slot" });
|
||||
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
|
@ -336,7 +334,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
},
|
||||
},
|
||||
});
|
||||
return res.status(201).json(booking);
|
||||
req.statusCode = 201;
|
||||
return booking;
|
||||
}
|
||||
|
||||
const teamMemberPromises =
|
||||
|
@ -358,7 +357,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
const attendeesList = [...invitee, ...guests, ...teamMembers];
|
||||
|
||||
const seed = `${organizerUser.username}:${dayjs(req.body.start).utc().format()}:${new Date().getTime()}`;
|
||||
const seed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}:${new Date().getTime()}`;
|
||||
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
|
||||
|
||||
const location = !!eventType.locations ? (eventType.locations as Array<{ type: string }>)[0] : "";
|
||||
|
@ -456,8 +455,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
async function createBooking() {
|
||||
// @TODO: check as metadata
|
||||
if (req.body.web3Details) {
|
||||
const { web3Details } = req.body;
|
||||
if (reqBody.web3Details) {
|
||||
const { web3Details } = reqBody;
|
||||
await verifyAccount(web3Details.userSignature, web3Details.userWallet);
|
||||
}
|
||||
|
||||
|
@ -477,7 +476,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
const dynamicEventSlugRef = !eventTypeId ? eventTypeSlug : null;
|
||||
const dynamicGroupSlugRef = !eventTypeId ? (reqBody.user as string).toLowerCase() : null;
|
||||
|
||||
const isConfirmedByDefault = (!eventType.requiresConfirmation && !eventType.price) || !!rescheduleUid;
|
||||
const newBookingData: Prisma.BookingCreateInput = {
|
||||
uid,
|
||||
|
@ -548,25 +546,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
}
|
||||
|
||||
/* Validate if there is any stripe_payment credential for this user */
|
||||
const stripePaymentCredential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
type: "stripe_payment",
|
||||
userId: organizerUser.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
/** eventType doesn’t require payment then we create a booking
|
||||
* OR
|
||||
* stripePaymentCredential is found and price is higher than 0 then we create a booking
|
||||
*/
|
||||
if (!eventType.price || (stripePaymentCredential && eventType.price > 0)) {
|
||||
return prisma.booking.create(createBookingObj);
|
||||
if (typeof eventType.price === "number" && eventType.price > 0) {
|
||||
/* Validate if there is any stripe_payment credential for this user */
|
||||
await prisma.credential.findFirst({
|
||||
rejectOnNotFound(err) {
|
||||
throw new HttpError({ statusCode: 400, message: "Missing stripe credentials", cause: err });
|
||||
},
|
||||
where: {
|
||||
type: "stripe_payment",
|
||||
userId: organizerUser.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
// stripePaymentCredential not found and eventType requires payment we return null
|
||||
return null;
|
||||
|
||||
return prisma.booking.create(createBookingObj);
|
||||
}
|
||||
|
||||
let results: EventResult[] = [];
|
||||
|
@ -577,31 +573,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
for (const currentUser of users) {
|
||||
if (!currentUser) {
|
||||
console.error(`currentUser not found`);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
if (!user) user = currentUser;
|
||||
|
||||
const selectedCalendars = await prisma.selectedCalendar.findMany({
|
||||
where: {
|
||||
const { busy: bufferedBusyTimes } = await getUserAvailability(
|
||||
{
|
||||
userId: currentUser.id,
|
||||
dateFrom: reqBody.start,
|
||||
dateTo: reqBody.end,
|
||||
eventTypeId,
|
||||
},
|
||||
});
|
||||
{ user, eventType }
|
||||
);
|
||||
|
||||
const busyTimes = await getBusyTimes({
|
||||
credentials: currentUser.credentials,
|
||||
startTime: reqBody.start,
|
||||
endTime: reqBody.end,
|
||||
eventTypeId,
|
||||
userId: currentUser.id,
|
||||
selectedCalendars,
|
||||
});
|
||||
|
||||
console.log("calendarBusyTimes==>>>", busyTimes);
|
||||
|
||||
const bufferedBusyTimes = busyTimes.map((a) => ({
|
||||
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
|
||||
end: dayjs(a.end).add(currentUser.bufferTime, "minute").toString(),
|
||||
}));
|
||||
console.log("calendarBusyTimes==>>>", bufferedBusyTimes);
|
||||
|
||||
let isAvailableToBeBooked = true;
|
||||
try {
|
||||
|
@ -609,9 +595,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
const recurringEvent = parseRecurringEvent(eventType.recurringEvent);
|
||||
const allBookingDates = new rrule({ dtstart: new Date(reqBody.start), ...recurringEvent }).all();
|
||||
// Go through each date for the recurring event and check if each one's availability
|
||||
isAvailableToBeBooked = allBookingDates
|
||||
.map((aDate) => isAvailable(bufferedBusyTimes, aDate, eventType.length)) // <-- array of booleans
|
||||
.reduce((acc, value) => acc && value, true); // <-- checks boolean array applying "AND" to each value and the current one, starting in true
|
||||
// DONE: Decreased computational complexity from O(2^n) to O(n) by refactoring this loop to stop
|
||||
// running at the first unavailable time.
|
||||
let i = 0;
|
||||
while (isAvailableToBeBooked === true && i < allBookingDates.length) {
|
||||
const aDate = allBookingDates[i];
|
||||
i++;
|
||||
isAvailableToBeBooked = isAvailable(bufferedBusyTimes, aDate, eventType.length);
|
||||
/* We bail at the first false, we don't need to keep checking */
|
||||
if (!isAvailableToBeBooked) break;
|
||||
}
|
||||
} else {
|
||||
isAvailableToBeBooked = isAvailable(bufferedBusyTimes, reqBody.start, eventType.length);
|
||||
}
|
||||
|
@ -628,8 +621,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
};
|
||||
|
||||
log.debug(`Booking ${currentUser.name} failed`, error);
|
||||
res.status(409).json(error);
|
||||
return;
|
||||
throw new HttpError({ statusCode: 409, message: error.message });
|
||||
}
|
||||
|
||||
let timeOutOfBounds = false;
|
||||
|
@ -655,8 +647,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
};
|
||||
|
||||
log.debug(`Booking ${currentUser.name} failed`, error);
|
||||
res.status(400).json(error);
|
||||
return;
|
||||
throw new HttpError({ statusCode: 409, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -669,14 +660,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
const err = getErrorFromUnknown(_err);
|
||||
log.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", err.message);
|
||||
if (err.code === "P2002") {
|
||||
res.status(409).json({ message: "booking.conflict" });
|
||||
return;
|
||||
throw new HttpError({ statusCode: 409, message: "booking.conflict" });
|
||||
}
|
||||
res.status(500).end();
|
||||
return;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!user) throw Error("Can't continue, user not found.");
|
||||
if (!user) throw new HttpError({ statusCode: 404, message: "Can't continue, user not found." });
|
||||
|
||||
// After polling videoBusyTimes, credentials might have been changed due to refreshment, so query them again.
|
||||
const credentials = await refreshCredentials(user.credentials);
|
||||
|
@ -777,21 +766,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
!originalRescheduledBooking?.paid &&
|
||||
!!booking
|
||||
) {
|
||||
try {
|
||||
const [firstStripeCredential] = user.credentials.filter((cred) => cred.type == "stripe_payment");
|
||||
const [firstStripeCredential] = user.credentials.filter((cred) => cred.type == "stripe_payment");
|
||||
|
||||
if (!firstStripeCredential) return res.status(500).json({ message: "Missing payment credentials" });
|
||||
if (!firstStripeCredential)
|
||||
throw new HttpError({ statusCode: 400, message: "Missing payment credentials" });
|
||||
|
||||
if (!booking.user) booking.user = user;
|
||||
const payment = await handlePayment(evt, eventType, firstStripeCredential, booking);
|
||||
if (!booking.user) booking.user = user;
|
||||
const payment = await handlePayment(evt, eventType, firstStripeCredential, booking);
|
||||
|
||||
res.status(201).json({ ...booking, message: "Payment required", paymentUid: payment.uid });
|
||||
return;
|
||||
} catch (e) {
|
||||
log.error(`Creating payment failed`, e);
|
||||
res.status(500).json({ message: "Payment Failed" });
|
||||
return;
|
||||
}
|
||||
req.statusCode = 201;
|
||||
return { ...booking, message: "Payment required", paymentUid: payment.uid };
|
||||
}
|
||||
|
||||
log.debug(`Booking ${user.username} completed`);
|
||||
|
@ -821,7 +805,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
await Promise.all(promises);
|
||||
// Avoid passing referencesToCreate with id unique constrain values
|
||||
// refresh hashed link if used
|
||||
const urlSeed = `${organizerUser.username}:${dayjs(req.body.start).utc().format()}`;
|
||||
const urlSeed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}`;
|
||||
const hashedUid = translator.fromUUID(uuidv5(urlSeed, uuidv5.URL));
|
||||
|
||||
if (hasHashedBookingLink) {
|
||||
|
@ -834,32 +818,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
},
|
||||
});
|
||||
}
|
||||
if (booking) {
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: booking.uid,
|
||||
},
|
||||
data: {
|
||||
references: {
|
||||
createMany: {
|
||||
data: referencesToCreate,
|
||||
},
|
||||
if (!booking) throw new HttpError({ statusCode: 400, message: "Booking failed" });
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: booking.uid,
|
||||
},
|
||||
data: {
|
||||
references: {
|
||||
createMany: {
|
||||
data: referencesToCreate,
|
||||
},
|
||||
},
|
||||
});
|
||||
// booking successful
|
||||
return res.status(201).json(booking);
|
||||
}
|
||||
return res.status(400).json({ message: "There is not a stripe_payment credential" });
|
||||
},
|
||||
});
|
||||
// booking successful
|
||||
req.statusCode = 201;
|
||||
return booking;
|
||||
}
|
||||
|
||||
export function getLuckyUsers(
|
||||
users: User[],
|
||||
bookingCounts: Prisma.PromiseReturnType<typeof getUserNameWithBookingCounts>
|
||||
) {
|
||||
if (!bookingCounts.length) users.slice(0, 1);
|
||||
|
||||
const [firstMostAvailableUser] = bookingCounts.sort((a, b) => (a.bookingCount > b.bookingCount ? 1 : -1));
|
||||
const luckyUser = users.find((user) => user.username === firstMostAvailableUser?.username);
|
||||
return luckyUser ? [luckyUser] : users;
|
||||
}
|
||||
export default defaultResponder(handler);
|
||||
|
|
|
@ -12,7 +12,7 @@ import { NextPageContext } from "next";
|
|||
import { useSession } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect, useRef, useState, useCallback } from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
|
@ -120,21 +120,7 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
|||
[props.user.id]
|
||||
);
|
||||
|
||||
const createEventType = async (data: Prisma.EventTypeCreateInput) => {
|
||||
const res = await fetch(`/api/availability/eventtype`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error((await res.json()).message);
|
||||
}
|
||||
const responseData = await res.json();
|
||||
return responseData.data;
|
||||
};
|
||||
const createEventType = trpc.useMutation("viewer.eventTypes.create");
|
||||
|
||||
const createSchedule = trpc.useMutation("viewer.availability.schedule.create", {
|
||||
onError: (err) => {
|
||||
|
@ -229,7 +215,7 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
|||
if (eventTypes.length === 0) {
|
||||
await Promise.all(
|
||||
DEFAULT_EVENT_TYPES.map(async (event) => {
|
||||
return await createEventType(event);
|
||||
return createEventType.mutate(event);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ import { MembershipRole, Prisma, UserPlan } from "@prisma/client";
|
|||
import { randomBytes } from "crypto";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||
import { sendTeamInviteEmail } from "@calcom/emails";
|
||||
import { availabilityUserSelect } from "@calcom/prisma";
|
||||
import {
|
||||
addSeat,
|
||||
downgradeTeamMembers,
|
||||
|
@ -13,7 +15,6 @@ import {
|
|||
} from "@calcom/stripe/team-billing";
|
||||
|
||||
import { BASE_URL, HOSTED_CAL_FEATURES } from "@lib/config/constants";
|
||||
import { getUserAvailability } from "@lib/queries/availability";
|
||||
import { getTeamWithMembers, isTeamAdmin, isTeamOwner } from "@lib/queries/teams";
|
||||
import slugify from "@lib/slugify";
|
||||
|
||||
|
@ -408,7 +409,14 @@ export const viewerTeamsRouter = createProtectedRouter()
|
|||
// verify member is in team
|
||||
const members = await ctx.prisma.membership.findMany({
|
||||
where: { teamId: input.teamId },
|
||||
include: { user: true },
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
...availabilityUserSelect,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const member = members?.find((m) => m.userId === input.memberId);
|
||||
if (!member) throw new TRPCError({ code: "NOT_FOUND", message: "Member not found" });
|
||||
|
@ -416,12 +424,15 @@ export const viewerTeamsRouter = createProtectedRouter()
|
|||
throw new TRPCError({ code: "BAD_REQUEST", message: "Member doesn't have a username" });
|
||||
|
||||
// get availability for this member
|
||||
return await getUserAvailability({
|
||||
username: member.user.username,
|
||||
timezone: input.timezone,
|
||||
dateFrom: input.dateFrom,
|
||||
dateTo: input.dateTo,
|
||||
});
|
||||
return await getUserAvailability(
|
||||
{
|
||||
username: member.user.username,
|
||||
timezone: input.timezone,
|
||||
dateFrom: input.dateFrom,
|
||||
dateTo: input.dateTo,
|
||||
},
|
||||
{ user: member.user }
|
||||
);
|
||||
},
|
||||
})
|
||||
.mutation("upgradeTeam", {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { UserPlan } from "@prisma/client";
|
||||
|
||||
import { getLuckyUsers } from "../../pages/api/book/event";
|
||||
import { getLuckyUsers } from "@calcom/lib";
|
||||
|
||||
const baseUser = {
|
||||
id: 0,
|
||||
|
|
|
@ -3,11 +3,10 @@ import { BookingStatus, Credential, SelectedCalendar } from "@prisma/client";
|
|||
import { getBusyCalendarTimes } from "@calcom/core/CalendarManager";
|
||||
import { getBusyVideoTimes } from "@calcom/core/videoClient";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { EventBusyDate } from "@calcom/types/Calendar";
|
||||
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
async function getBusyTimes(params: {
|
||||
export async function getBusyTimes(params: {
|
||||
credentials: Credential[];
|
||||
userId: number;
|
||||
eventTypeId?: number;
|
||||
|
@ -32,7 +31,7 @@ async function getBusyTimes(params: {
|
|||
endTime: true,
|
||||
},
|
||||
})
|
||||
.then((bookings) => bookings.map((booking) => ({ end: booking.endTime, start: booking.startTime })));
|
||||
.then((bookings) => bookings.map(({ startTime, endTime }) => ({ end: endTime, start: startTime })));
|
||||
|
||||
if (credentials) {
|
||||
const calendarBusyTimes = await getBusyCalendarTimes(credentials, startTime, endTime, selectedCalendars);
|
|
@ -0,0 +1,145 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getWorkingHours } from "@calcom/lib/availability";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma, { availabilityUserSelect } from "@calcom/prisma";
|
||||
import { stringToDayjs } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { getBusyTimes } from "./getBusyTimes";
|
||||
|
||||
const availabilitySchema = z
|
||||
.object({
|
||||
dateFrom: stringToDayjs,
|
||||
dateTo: stringToDayjs,
|
||||
eventTypeId: z.number().optional(),
|
||||
timezone: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
userId: z.number().optional(),
|
||||
})
|
||||
.refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
|
||||
|
||||
const getEventType = (id: number) =>
|
||||
prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
seatsPerTimeSlot: true,
|
||||
timeZone: true,
|
||||
schedule: {
|
||||
select: {
|
||||
availability: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
availability: {
|
||||
select: {
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
days: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type EventType = Awaited<ReturnType<typeof getEventType>>;
|
||||
|
||||
const getUser = (where: Prisma.UserWhereUniqueInput) =>
|
||||
prisma.user.findUnique({
|
||||
where,
|
||||
select: availabilityUserSelect,
|
||||
});
|
||||
|
||||
type User = Awaited<ReturnType<typeof getUser>>;
|
||||
|
||||
export async function getUserAvailability(
|
||||
query: ({ username: string } | { userId: number }) & {
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
eventTypeId?: number;
|
||||
timezone?: string;
|
||||
},
|
||||
initialData?: {
|
||||
user?: User;
|
||||
eventType?: EventType;
|
||||
}
|
||||
) {
|
||||
const { username, userId, dateFrom, dateTo, eventTypeId, timezone } = availabilitySchema.parse(query);
|
||||
|
||||
if (!dateFrom.isValid() || !dateTo.isValid())
|
||||
throw new HttpError({ statusCode: 400, message: "Invalid time range given." });
|
||||
|
||||
const where: Prisma.UserWhereUniqueInput = {};
|
||||
if (username) where.username = username;
|
||||
if (userId) where.id = userId;
|
||||
|
||||
let user: User | null = initialData?.user || null;
|
||||
if (!user) user = await getUser(where);
|
||||
if (!user) throw new HttpError({ statusCode: 404, message: "No user found" });
|
||||
|
||||
let eventType: EventType | null = initialData?.eventType || null;
|
||||
if (!eventType && eventTypeId) eventType = await getEventType(eventTypeId);
|
||||
|
||||
const { selectedCalendars, ...currentUser } = user;
|
||||
|
||||
const busyTimes = await getBusyTimes({
|
||||
credentials: currentUser.credentials,
|
||||
startTime: dateFrom.toISOString(),
|
||||
endTime: dateTo.toISOString(),
|
||||
eventTypeId,
|
||||
userId: currentUser.id,
|
||||
selectedCalendars,
|
||||
});
|
||||
|
||||
const bufferedBusyTimes = busyTimes.map((a) => ({
|
||||
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toISOString(),
|
||||
end: dayjs(a.end).add(currentUser.bufferTime, "minute").toISOString(),
|
||||
}));
|
||||
|
||||
const timeZone = timezone || eventType?.timeZone || currentUser.timeZone;
|
||||
|
||||
const schedule = eventType?.schedule
|
||||
? { ...eventType?.schedule }
|
||||
: {
|
||||
...currentUser.schedules.filter(
|
||||
(schedule) => !currentUser.defaultScheduleId || schedule.id === currentUser.defaultScheduleId
|
||||
)[0],
|
||||
};
|
||||
|
||||
const workingHours = getWorkingHours(
|
||||
{ timeZone },
|
||||
schedule.availability ||
|
||||
(eventType?.availability.length ? eventType.availability : currentUser.availability)
|
||||
);
|
||||
|
||||
/* Current logic is if a booking is in a time slot mark it as busy, but seats can have more than one attendee so grab
|
||||
current bookings with a seats event type and display them on the calendar, even if they are full */
|
||||
let currentSeats;
|
||||
if (eventType?.seatsPerTimeSlot) {
|
||||
currentSeats = await prisma.booking.findMany({
|
||||
where: {
|
||||
eventTypeId: eventTypeId,
|
||||
startTime: {
|
||||
gte: dateFrom.format(),
|
||||
lte: dateTo.format(),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
uid: true,
|
||||
startTime: true,
|
||||
_count: {
|
||||
select: {
|
||||
attendees: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
busy: bufferedBusyTimes,
|
||||
timeZone,
|
||||
workingHours,
|
||||
currentSeats,
|
||||
};
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./CalendarManager";
|
||||
export * from "./EventManager";
|
||||
export { default as getBusyTimes } from "./getBusyTimes";
|
||||
export * from "./videoClient";
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import type { EventTypeCustomInput } from "@prisma/client";
|
||||
import { PeriodType, SchedulingType, UserPlan } from "@prisma/client";
|
||||
import { PeriodType, Prisma, SchedulingType, UserPlan } from "@prisma/client";
|
||||
|
||||
import { baseUserSelect } from "@calcom/prisma/selects";
|
||||
|
||||
const userSelectData = Prisma.validator<Prisma.UserArgs>()({ select: baseUserSelect });
|
||||
type User = Prisma.UserGetPayload<typeof userSelectData>;
|
||||
|
||||
const availability = [
|
||||
{
|
||||
|
@ -81,7 +86,13 @@ const commons = {
|
|||
theme: null,
|
||||
brandColor: "#292929",
|
||||
darkBrandColor: "#fafafa",
|
||||
},
|
||||
availability: [],
|
||||
selectedCalendars: [],
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
schedules: [],
|
||||
defaultScheduleId: null,
|
||||
} as User,
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { userSelect } from "@calcom/prisma";
|
||||
|
||||
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||
|
||||
export function getLuckyUsers(
|
||||
users: User[],
|
||||
bookingCounts: {
|
||||
username: string | null;
|
||||
bookingCount: number;
|
||||
}[]
|
||||
) {
|
||||
if (!bookingCounts.length) users.slice(0, 1);
|
||||
|
||||
const [firstMostAvailableUser] = bookingCounts.sort((a, b) => (a.bookingCount > b.bookingCount ? 1 : -1));
|
||||
const luckyUser = users.find((user) => user.username === firstMostAvailableUser?.username);
|
||||
return luckyUser ? [luckyUser] : users;
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export { getLuckyUsers } from "./getLuckyUsers";
|
||||
export { default as isPrismaObj, isPrismaObjOrUndefined } from "./isPrismaObj";
|
||||
export * from "./isRecurringEvent";
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { performance } from "perf_hooks";
|
||||
|
||||
import { perfObserver } from ".";
|
||||
import { getServerErrorFromUnkown } from "./getServerErrorFromUnkown";
|
||||
|
||||
type Handle<T> = (req: NextApiRequest, res: NextApiResponse) => Promise<T>;
|
||||
|
||||
perfObserver.observe({ entryTypes: ["measure"], buffered: true });
|
||||
|
||||
/** Allows us to get type inference from API handler responses */
|
||||
function defaultResponder<T>(f: Handle<T>) {
|
||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
performance.mark("Start");
|
||||
const result = await f(req, res);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
const error = getServerErrorFromUnkown(err);
|
||||
res.statusCode = error.statusCode;
|
||||
res.json({ message: error.message });
|
||||
} finally {
|
||||
performance.mark("End");
|
||||
performance.measure("Measuring endpoint: " + req.url, "Start", "End");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultResponder;
|
|
@ -0,0 +1,29 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import Stripe from "stripe";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { HttpError } from "../http-error";
|
||||
|
||||
export function getServerErrorFromUnkown(cause: unknown): HttpError {
|
||||
if (cause instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
return new HttpError({ statusCode: 400, message: cause.message, cause });
|
||||
}
|
||||
if (cause instanceof Error) {
|
||||
return new HttpError({ statusCode: 500, message: cause.message, cause });
|
||||
}
|
||||
if (cause instanceof HttpError) {
|
||||
return cause;
|
||||
}
|
||||
if (cause instanceof Stripe.errors.StripeInvalidRequestError) {
|
||||
return new HttpError({ statusCode: 400, message: cause.message, cause });
|
||||
}
|
||||
if (cause instanceof ZodError) {
|
||||
return new HttpError({ statusCode: 400, message: cause.message, cause });
|
||||
}
|
||||
if (typeof cause === "string") {
|
||||
// @ts-expect-error https://github.com/tc39/proposal-error-cause
|
||||
return new Error(cause, { cause });
|
||||
}
|
||||
|
||||
return new HttpError({ statusCode: 500, message: `Unhandled error of type '${typeof cause}'` });
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export { default as defaultHandler } from "./defaultHandler";
|
||||
export { default as defaultResponder } from "./defaultResponder";
|
||||
export { getServerErrorFromUnkown } from "./getServerErrorFromUnkown";
|
||||
export { getTranslation } from "./i18n";
|
||||
export { default as perfObserver } from "./perfObserver";
|
|
@ -0,0 +1,20 @@
|
|||
import { PerformanceObserver } from "perf_hooks";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var perfObserver: PerformanceObserver | undefined;
|
||||
}
|
||||
|
||||
export const perfObserver =
|
||||
globalThis.perfObserver ||
|
||||
new PerformanceObserver((items) => {
|
||||
items.getEntries().forEach((entry) => {
|
||||
console.log(entry); // fake call to our custom logging solution
|
||||
});
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalThis.perfObserver = perfObserver;
|
||||
}
|
||||
|
||||
export default perfObserver;
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./booking";
|
||||
export * from "./event-types";
|
||||
export * from "./user";
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export const availabilityUserSelect = Prisma.validator<Prisma.UserSelect>()({
|
||||
credentials: true,
|
||||
timeZone: true,
|
||||
bufferTime: true,
|
||||
availability: true,
|
||||
id: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
selectedCalendars: true,
|
||||
schedules: {
|
||||
select: {
|
||||
availability: true,
|
||||
timeZone: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
defaultScheduleId: true,
|
||||
});
|
||||
|
||||
export const baseUserSelect = Prisma.validator<Prisma.UserSelect>()({
|
||||
email: true,
|
||||
name: true,
|
||||
username: true,
|
||||
destinationCalendar: true,
|
||||
locale: true,
|
||||
plan: true,
|
||||
avatar: true,
|
||||
hideBranding: true,
|
||||
theme: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
...availabilityUserSelect,
|
||||
});
|
||||
|
||||
export const userSelect = Prisma.validator<Prisma.UserArgs>()({
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
username: true,
|
||||
destinationCalendar: true,
|
||||
locale: true,
|
||||
plan: true,
|
||||
avatar: true,
|
||||
hideBranding: true,
|
||||
theme: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
...availabilityUserSelect,
|
||||
},
|
||||
});
|
|
@ -56,3 +56,39 @@ export const stringOrNumber = z.union([
|
|||
]);
|
||||
|
||||
export const stringToDayjs = z.string().transform((val) => dayjs(val));
|
||||
|
||||
export const bookingCreateBodySchema = z.object({
|
||||
email: z.string(),
|
||||
end: z.string(),
|
||||
web3Details: z
|
||||
.object({
|
||||
userWallet: z.string(),
|
||||
userSignature: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
eventTypeId: z.number(),
|
||||
eventTypeSlug: z.string(),
|
||||
guests: z.array(z.string()).optional(),
|
||||
location: z.string(),
|
||||
name: z.string(),
|
||||
notes: z.string().optional(),
|
||||
rescheduleUid: z.string().optional(),
|
||||
recurringEventId: z.string().optional(),
|
||||
start: z.string(),
|
||||
timeZone: z.string(),
|
||||
user: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
language: z.string(),
|
||||
bookingUid: z.string().optional(),
|
||||
customInputs: z.array(z.object({ label: z.string(), value: z.union([z.string(), z.boolean()]) })),
|
||||
metadata: z.record(z.string()),
|
||||
hasHashedBookingLink: z.boolean(),
|
||||
hashedLink: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const extendedBookingCreateBody = bookingCreateBodySchema.merge(
|
||||
z.object({
|
||||
noEmail: z.boolean().optional(),
|
||||
recurringCount: z.number().optional(),
|
||||
rescheduleReason: z.string().optional(),
|
||||
})
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue