Adds eventTypeId as a parameter (#1217)
This commit is contained in:
parent
8c1b69cc0f
commit
22aa083883
6 changed files with 116 additions and 59 deletions
|
@ -19,6 +19,7 @@ import { createPaymentLink } from "@ee/lib/stripe/client";
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
|
import { ensureArray } from "@lib/ensureArray";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
import useTheme from "@lib/hooks/useTheme";
|
||||||
import { LocationType } from "@lib/location";
|
import { LocationType } from "@lib/location";
|
||||||
|
@ -137,19 +138,12 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// can be shortened using .filter(), except TypeScript doesn't know what to make of the types.
|
|
||||||
const guests = router.query.guest
|
|
||||||
? Array.isArray(router.query.guest)
|
|
||||||
? router.query.guest
|
|
||||||
: [router.query.guest]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const bookingForm = useForm<BookingFormValues>({
|
const bookingForm = useForm<BookingFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: (router.query.name as string) || "",
|
name: (router.query.name as string) || "",
|
||||||
email: (router.query.email as string) || "",
|
email: (router.query.email as string) || "",
|
||||||
notes: (router.query.notes as string) || "",
|
notes: (router.query.notes as string) || "",
|
||||||
guests,
|
guests: ensureArray(router.query.guest),
|
||||||
customInputs: props.eventType.customInputs.reduce(
|
customInputs: props.eventType.customInputs.reduce(
|
||||||
(customInputs, input) => ({
|
(customInputs, input) => ({
|
||||||
...customInputs,
|
...customInputs,
|
||||||
|
@ -213,7 +207,7 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
timeZone: timeZone(),
|
timeZone: timeZone(),
|
||||||
language: i18n.language,
|
language: i18n.language,
|
||||||
rescheduleUid,
|
rescheduleUid,
|
||||||
user: router.query.user as string,
|
user: router.query.user,
|
||||||
location: getLocationValue(booking),
|
location: getLocationValue(booking),
|
||||||
metadata,
|
metadata,
|
||||||
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
||||||
|
|
|
@ -14,6 +14,7 @@ const config: Config.InitialOptions = {
|
||||||
"^@components(.*)$": "<rootDir>/components$1",
|
"^@components(.*)$": "<rootDir>/components$1",
|
||||||
"^@lib(.*)$": "<rootDir>/lib$1",
|
"^@lib(.*)$": "<rootDir>/lib$1",
|
||||||
"^@server(.*)$": "<rootDir>/server$1",
|
"^@server(.*)$": "<rootDir>/server$1",
|
||||||
|
"^@ee(.*)$": "<rootDir>/ee$1",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
9
lib/ensureArray.ts
Normal file
9
lib/ensureArray.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export function ensureArray<T>(val: unknown): T[] {
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
if (typeof val === "undefined") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [val] as T[];
|
||||||
|
}
|
|
@ -16,8 +16,7 @@ export type BookingCreateBody = {
|
||||||
rescheduleUid?: string;
|
rescheduleUid?: string;
|
||||||
start: string;
|
start: string;
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
users?: string[];
|
user?: string | string[];
|
||||||
user?: string;
|
|
||||||
language: string;
|
language: string;
|
||||||
customInputs: { label: string; value: string }[];
|
customInputs: { label: string; value: string }[];
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
sendRescheduledEmails,
|
sendRescheduledEmails,
|
||||||
sendOrganizerRequestEmail,
|
sendOrganizerRequestEmail,
|
||||||
} from "@lib/emails/email-manager";
|
} from "@lib/emails/email-manager";
|
||||||
|
import { ensureArray } from "@lib/ensureArray";
|
||||||
import { getErrorFromUnknown } from "@lib/errors";
|
import { getErrorFromUnknown } from "@lib/errors";
|
||||||
import { getEventName } from "@lib/event";
|
import { getEventName } from "@lib/event";
|
||||||
import EventManager, { EventResult, PartialReference } from "@lib/events/EventManager";
|
import EventManager, { EventResult, PartialReference } from "@lib/events/EventManager";
|
||||||
|
@ -123,6 +124,59 @@ function isOutOfBounds(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userSelect = Prisma.validator<Prisma.UserArgs>()({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
username: true,
|
||||||
|
timeZone: true,
|
||||||
|
credentials: true,
|
||||||
|
bufferTime: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getUserNameWithBookingCounts = async (eventTypeId: number, selectedUserNames: string[]) => {
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
username: { in: selectedUserNames },
|
||||||
|
bookings: {
|
||||||
|
some: {
|
||||||
|
startTime: {
|
||||||
|
gt: new Date(),
|
||||||
|
},
|
||||||
|
eventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const userNamesWithBookingCounts = await Promise.all(
|
||||||
|
users.map(async (user) => ({
|
||||||
|
username: user.username,
|
||||||
|
bookingCount: await prisma.booking.count({
|
||||||
|
where: {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
startTime: {
|
||||||
|
gt: new Date(),
|
||||||
|
},
|
||||||
|
eventTypeId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return userNamesWithBookingCounts;
|
||||||
|
};
|
||||||
|
|
||||||
|
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const reqBody = req.body as BookingCreateBody;
|
const reqBody = req.body as BookingCreateBody;
|
||||||
const eventTypeId = reqBody.eventTypeId;
|
const eventTypeId = reqBody.eventTypeId;
|
||||||
|
@ -144,28 +198,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
return res.status(400).json(error);
|
return res.status(400).json(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userSelect = Prisma.validator<Prisma.UserSelect>()({
|
|
||||||
id: true,
|
|
||||||
email: true,
|
|
||||||
name: true,
|
|
||||||
username: true,
|
|
||||||
timeZone: true,
|
|
||||||
credentials: true,
|
|
||||||
bufferTime: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const userData = Prisma.validator<Prisma.UserArgs>()({
|
|
||||||
select: userSelect,
|
|
||||||
});
|
|
||||||
|
|
||||||
const eventType = await prisma.eventType.findUnique({
|
const eventType = await prisma.eventType.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: eventTypeId,
|
id: eventTypeId,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
users: {
|
users: userSelect,
|
||||||
select: userSelect,
|
|
||||||
},
|
|
||||||
team: {
|
team: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
@ -198,43 +236,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
where: {
|
where: {
|
||||||
id: eventType.userId,
|
id: eventType.userId,
|
||||||
},
|
},
|
||||||
select: userSelect,
|
...userSelect,
|
||||||
});
|
});
|
||||||
if (!eventTypeUser) return res.status(404).json({ message: "eventTypeUser.notFound" });
|
if (!eventTypeUser) return res.status(404).json({ message: "eventTypeUser.notFound" });
|
||||||
users.push(eventTypeUser);
|
users.push(eventTypeUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
|
if (eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
|
||||||
const selectedUsers = reqBody.users || [];
|
const bookingCounts = await getUserNameWithBookingCounts(
|
||||||
const selectedUsersDataWithBookingsCount = await prisma.user.findMany({
|
eventTypeId,
|
||||||
where: {
|
ensureArray(reqBody.user) || users.map((user) => user.username)
|
||||||
username: { in: selectedUsers },
|
);
|
||||||
bookings: {
|
|
||||||
every: {
|
|
||||||
startTime: {
|
|
||||||
gt: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
username: true,
|
|
||||||
_count: {
|
|
||||||
select: { bookings: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const bookingCounts = selectedUsersDataWithBookingsCount.map((userData) => ({
|
users = getLuckyUsers(users, bookingCounts);
|
||||||
username: userData.username,
|
|
||||||
bookingCount: userData._count?.bookings || 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
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);
|
|
||||||
users = luckyUser ? [luckyUser] : users;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const invitee = [{ email: reqBody.email, name: reqBody.name, timeZone: reqBody.timeZone }];
|
const invitee = [{ email: reqBody.email, name: reqBody.name, timeZone: reqBody.timeZone }];
|
||||||
|
@ -354,7 +368,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
|
|
||||||
let results: EventResult[] = [];
|
let results: EventResult[] = [];
|
||||||
let referencesToCreate: PartialReference[] = [];
|
let referencesToCreate: PartialReference[] = [];
|
||||||
type User = Prisma.UserGetPayload<typeof userData>;
|
|
||||||
let user: User | null = null;
|
let user: User | null = null;
|
||||||
|
|
||||||
for (const currentUser of users) {
|
for (const currentUser of users) {
|
||||||
|
@ -555,3 +568,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
// booking successful
|
// booking successful
|
||||||
return res.status(201).json(booking);
|
return res.status(201).json(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;
|
||||||
|
}
|
||||||
|
|
30
test/lib/team-event-types.test.ts
Normal file
30
test/lib/team-event-types.test.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { getLuckyUsers } from "../../pages/api/book/event";
|
||||||
|
|
||||||
|
it("can find lucky users", async () => {
|
||||||
|
const users = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
username: "test",
|
||||||
|
name: "Test User",
|
||||||
|
credentials: [],
|
||||||
|
timeZone: "GMT",
|
||||||
|
bufferTime: 0,
|
||||||
|
email: "test@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
username: "test2",
|
||||||
|
name: "Test 2 User",
|
||||||
|
credentials: [],
|
||||||
|
timeZone: "GMT",
|
||||||
|
bufferTime: 0,
|
||||||
|
email: "test2@example.com",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(
|
||||||
|
getLuckyUsers(users, [
|
||||||
|
{ username: "test", bookingCount: 2 },
|
||||||
|
{ username: "test2", bookingCount: 0 },
|
||||||
|
])
|
||||||
|
).toStrictEqual([users[1]]);
|
||||||
|
});
|
Loading…
Reference in a new issue