Add seats to event types (#2485)
* Add seatsPerTimeSlot to event type schema * Add seats per time slot to event type form * Book event and render seats * Pass booking uid for seats * Disable requires confirmation if seats are enabled * Fix type errors * Update submodules * Fix type errors * Fix type errors * Fix duplicate string * Fix duplicate string * Fix schema and migration file * Fix render seats * Fix bookinguid typos * Remove console.log * Fix type error * Fix mobile formatting * Update apps/web/lib/hooks/useSlots.ts Co-authored-by: Omar López <zomars@me.com> * Update apps/web/lib/hooks/useSlots.ts Co-authored-by: Omar López <zomars@me.com> * Added translation for seats available text Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: alannnc <alannnc@gmail.com>
This commit is contained in:
parent
cc0ea19420
commit
c8d6c0dbdd
|
@ -29,6 +29,7 @@ type AvailableTimesProps = {
|
|||
username: string | null;
|
||||
}[];
|
||||
schedulingType: SchedulingType | null;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
};
|
||||
|
||||
const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
|
@ -44,6 +45,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||
schedulingType,
|
||||
beforeBufferTime,
|
||||
afterBufferTime,
|
||||
seatsPerTimeSlot,
|
||||
}) => {
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
|
@ -105,18 +107,48 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||
bookingUrl.query.user = slot.users;
|
||||
}
|
||||
|
||||
// If event already has an attendee add booking id
|
||||
if (slot.bookingUid) {
|
||||
bookingUrl.query.bookingUid = slot.bookingUid;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={slot.time.format()}>
|
||||
<Link href={bookingUrl}>
|
||||
<a
|
||||
{/* Current there is no way to disable Next.js Links */}
|
||||
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
||||
<div
|
||||
className={classNames(
|
||||
"text-bookingdarker hover:bg-brand hover:text-brandcontrast dark:hover:bg-darkmodebrand dark:hover:text-darkmodebrandcontrast mb-2 block rounded-sm border bg-white py-4 font-medium hover:text-white dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 dark:hover:border-black",
|
||||
"text-primary-500 mb-2 block rounded-sm border bg-white py-4 font-medium opacity-25 dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 ",
|
||||
brand === "#fff" || brand === "#ffffff" ? "border-brandcontrast" : "border-brand"
|
||||
)}
|
||||
data-testid="time">
|
||||
{dayjs.tz(slot.time, timeZone()).format(timeFormat)}
|
||||
</a>
|
||||
</Link>
|
||||
)}>
|
||||
{slot.time.format(timeFormat)}
|
||||
{seatsPerTimeSlot && <p className={`text-sm`}>{t("booking_full")}</p>}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={bookingUrl}>
|
||||
<a
|
||||
className={classNames(
|
||||
"text-primary-500 hover:bg-brand hover:text-brandcontrast dark:hover:bg-darkmodebrand dark:hover:text-darkmodebrandcontrast mb-2 block rounded-sm border bg-white py-4 font-medium hover:text-white dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 dark:hover:border-black",
|
||||
brand === "#fff" || brand === "#ffffff" ? "border-brandcontrast" : "border-brand"
|
||||
)}
|
||||
data-testid="time">
|
||||
{dayjs.tz(slot.time, timeZone()).format(timeFormat)}
|
||||
{seatsPerTimeSlot && (
|
||||
<p
|
||||
className={`${
|
||||
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
||||
? "text-rose-600"
|
||||
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-emerald-400"
|
||||
} text-sm`}>
|
||||
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
||||
{seatsPerTimeSlot} {t("seats_available")}
|
||||
</p>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -398,6 +398,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
|||
schedulingType={eventType.schedulingType ?? null}
|
||||
beforeBufferTime={eventType.beforeEventBuffer}
|
||||
afterBufferTime={eventType.afterEventBuffer}
|
||||
seatsPerTimeSlot={eventType.seatsPerTimeSlot}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -395,6 +395,7 @@ const BookingPage = ({
|
|||
timeZone: timeZone(),
|
||||
language: i18n.language,
|
||||
rescheduleUid,
|
||||
bookingUid: router.query.bookingUid as string,
|
||||
user: router.query.user,
|
||||
location: getLocationValue(
|
||||
booking.locationType ? booking : { ...booking, locationType: selectedLocation }
|
||||
|
@ -468,6 +469,21 @@ const BookingPage = ({
|
|||
<h1 className="text-bookingdark mb-4 text-xl font-semibold dark:text-white">
|
||||
{eventType.title}
|
||||
</h1>
|
||||
{eventType.seatsPerTimeSlot && (
|
||||
<p
|
||||
className={`${
|
||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
? "text-rose-600"
|
||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-emerald-400"
|
||||
} mb-2`}>
|
||||
{booking
|
||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
||||
: eventType.seatsPerTimeSlot}{" "}
|
||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||
</p>
|
||||
)}
|
||||
{eventType?.description && (
|
||||
<p className="text-bookinglight mb-2 dark:text-white">
|
||||
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
|
|
|
@ -30,6 +30,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
|||
<div className="flex h-5 items-center">
|
||||
<input
|
||||
{...rest}
|
||||
disabled={rest.disabled}
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300"
|
||||
|
|
|
@ -6,7 +6,7 @@ import { stringify } from "querystring";
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import getSlots from "@lib/slots";
|
||||
import { TimeRange, WorkingHours } from "@lib/types/schedule";
|
||||
import { CurrentSeats, TimeRange, WorkingHours } from "@lib/types/schedule";
|
||||
|
||||
dayjs.extend(isBetween);
|
||||
dayjs.extend(utc);
|
||||
|
@ -15,11 +15,14 @@ type AvailabilityUserResponse = {
|
|||
busy: TimeRange[];
|
||||
timeZone: string;
|
||||
workingHours: WorkingHours[];
|
||||
currentSeats?: CurrentSeats[];
|
||||
};
|
||||
|
||||
type Slot = {
|
||||
time: Dayjs;
|
||||
users?: string[];
|
||||
bookingUid?: string;
|
||||
attendees?: number;
|
||||
};
|
||||
|
||||
type UseSlotsProps = {
|
||||
|
@ -40,10 +43,11 @@ type getFilteredTimesProps = {
|
|||
eventLength: number;
|
||||
beforeBufferTime: number;
|
||||
afterBufferTime: number;
|
||||
currentSeats?: CurrentSeats[];
|
||||
};
|
||||
|
||||
export const getFilteredTimes = (props: getFilteredTimesProps) => {
|
||||
const { times, busy, eventLength, beforeBufferTime, afterBufferTime } = props;
|
||||
const { times, busy, eventLength, beforeBufferTime, afterBufferTime, currentSeats } = props;
|
||||
const finalizationTime = times[times.length - 1]?.add(eventLength, "minutes");
|
||||
// Check for conflicts
|
||||
for (let i = times.length - 1; i >= 0; i -= 1) {
|
||||
|
@ -56,6 +60,10 @@ export const getFilteredTimes = (props: getFilteredTimesProps) => {
|
|||
const slotStartTime = times[i];
|
||||
const slotEndTime = times[i].add(eventLength, "minutes");
|
||||
const slotStartTimeWithBeforeBuffer = times[i].subtract(beforeBufferTime, "minutes");
|
||||
// If the event has seats then see if there is already a booking (want to show full bookings as well)
|
||||
if (currentSeats?.some((booking) => booking.startTime === slotStartTime.toISOString())) {
|
||||
break;
|
||||
}
|
||||
busy.every((busyTime): boolean => {
|
||||
const startTime = dayjs(busyTime.start);
|
||||
const endTime = dayjs(busyTime.end);
|
||||
|
@ -124,19 +132,21 @@ export const useSlots = (props: UseSlotsProps) => {
|
|||
|
||||
const handleAvailableSlots = async (res: Response) => {
|
||||
const responseBody: AvailabilityUserResponse = await res.json();
|
||||
const { workingHours, busy, currentSeats } = responseBody;
|
||||
const times = getSlots({
|
||||
frequency: slotInterval || eventLength,
|
||||
inviteeDate: date,
|
||||
workingHours: responseBody.workingHours,
|
||||
workingHours,
|
||||
minimumBookingNotice,
|
||||
eventLength,
|
||||
});
|
||||
const filterTimeProps = {
|
||||
times,
|
||||
busy: responseBody.busy,
|
||||
busy,
|
||||
eventLength,
|
||||
beforeBufferTime,
|
||||
afterBufferTime,
|
||||
currentSeats,
|
||||
};
|
||||
const filteredTimes = getFilteredTimes(filterTimeProps);
|
||||
// temporary
|
||||
|
@ -144,6 +154,14 @@ export const useSlots = (props: UseSlotsProps) => {
|
|||
return filteredTimes.map((time) => ({
|
||||
time,
|
||||
users: [user],
|
||||
// Conditionally add the attendees and booking id to slots object if there is already a booking during that time
|
||||
...(currentSeats?.some((booking) => booking.startTime === time.toISOString()) && {
|
||||
attendees:
|
||||
currentSeats[currentSeats.findIndex((booking) => booking.startTime === time.toISOString())]._count
|
||||
.attendees,
|
||||
bookingUid:
|
||||
currentSeats[currentSeats.findIndex((booking) => booking.startTime === time.toISOString())].uid,
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import isToday from "dayjs/plugin/isToday";
|
|||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
import { getWorkingHours } from "./availability";
|
||||
import { WorkingHours } from "./types/schedule";
|
||||
import { WorkingHours, CurrentSeats } from "./types/schedule";
|
||||
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(utc);
|
||||
|
@ -16,6 +16,7 @@ export type GetSlots = {
|
|||
workingHours: WorkingHours[];
|
||||
minimumBookingNotice: number;
|
||||
eventLength: number;
|
||||
currentSeats?: CurrentSeats[];
|
||||
};
|
||||
export type WorkingHoursTimeFrame = { startTime: number; endTime: number };
|
||||
|
||||
|
@ -42,7 +43,14 @@ const splitAvailableTime = (
|
|||
return result;
|
||||
};
|
||||
|
||||
const getSlots = ({ inviteeDate, frequency, minimumBookingNotice, workingHours, eventLength }: GetSlots) => {
|
||||
const getSlots = ({
|
||||
inviteeDate,
|
||||
frequency,
|
||||
minimumBookingNotice,
|
||||
workingHours,
|
||||
eventLength,
|
||||
currentSeats,
|
||||
}: GetSlots) => {
|
||||
// current date in invitee tz
|
||||
const startDate = dayjs().add(minimumBookingNotice, "minute");
|
||||
const startOfDay = dayjs.utc().startOf("day");
|
||||
|
|
|
@ -24,6 +24,7 @@ export type BookingCreateBody = {
|
|||
timeZone: string;
|
||||
user?: string | string[];
|
||||
language: string;
|
||||
bookingUid?: string;
|
||||
customInputs: { label: string; value: string | boolean }[];
|
||||
metadata: {
|
||||
[key: string]: string;
|
||||
|
|
|
@ -24,6 +24,7 @@ export type AdvancedOptions = {
|
|||
availability?: { openingHours: WorkingHours[]; dateOverrides: WorkingHours[] };
|
||||
customInputs?: EventTypeCustomInput[];
|
||||
timeZone?: string;
|
||||
seatsPerTimeSlot: number;
|
||||
destinationCalendar?: {
|
||||
userId?: number;
|
||||
eventTypeId?: number;
|
||||
|
|
|
@ -16,3 +16,11 @@ export type WorkingHours = {
|
|||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
|
||||
export type CurrentSeats = {
|
||||
uid: string;
|
||||
startTime: string;
|
||||
_count: {
|
||||
attendees: number;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -100,6 +100,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
timeZone: true,
|
||||
metadata: true,
|
||||
slotInterval: true,
|
||||
seatsPerTimeSlot: true,
|
||||
users: {
|
||||
select: {
|
||||
avatar: true,
|
||||
|
|
|
@ -126,6 +126,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
price: true,
|
||||
currency: true,
|
||||
disableGuests: true,
|
||||
seatsPerTimeSlot: true,
|
||||
users: {
|
||||
select: {
|
||||
id: true,
|
||||
|
@ -176,8 +177,13 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
})[0];
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (context.query.rescheduleUid) {
|
||||
booking = await getBooking(prisma, context.query.rescheduleUid as string);
|
||||
if (context.query.rescheduleUid || context.query.bookingUid) {
|
||||
booking = await getBooking(
|
||||
prisma,
|
||||
context.query.rescheduleUid
|
||||
? (context.query.rescheduleUid as string)
|
||||
: (context.query.bookingUid as string)
|
||||
);
|
||||
}
|
||||
|
||||
const dynamicNames = isDynamicGroupBooking
|
||||
|
|
|
@ -50,6 +50,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
seatsPerTimeSlot: true,
|
||||
timeZone: true,
|
||||
schedule: {
|
||||
select: {
|
||||
|
@ -107,9 +108,34 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
(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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -188,6 +188,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
|||
metadata: true,
|
||||
destinationCalendar: true,
|
||||
hideCalendarNotes: true,
|
||||
seatsPerTimeSlot: true,
|
||||
recurringEvent: true,
|
||||
},
|
||||
});
|
||||
|
@ -294,6 +295,45 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
return g;
|
||||
});
|
||||
|
||||
// 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" });
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
uid: reqBody.bookingUid,
|
||||
},
|
||||
include: {
|
||||
attendees: true,
|
||||
},
|
||||
});
|
||||
if (!booking) return res.status(404).json({ message: "Booking not found" });
|
||||
|
||||
if (eventType.seatsPerTimeSlot <= booking.attendees.length)
|
||||
return res.status(409).json({ 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" });
|
||||
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: reqBody.bookingUid,
|
||||
},
|
||||
data: {
|
||||
attendees: {
|
||||
create: {
|
||||
email: invitee[0].email,
|
||||
name: invitee[0].name,
|
||||
timeZone: invitee[0].timeZone,
|
||||
locale: invitee[0].language.locale,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return res.status(201).json(booking);
|
||||
}
|
||||
|
||||
const teamMemberPromises =
|
||||
eventType.schedulingType === SchedulingType.COLLECTIVE
|
||||
? users.slice(1).map(async function (user) {
|
||||
|
|
|
@ -41,6 +41,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
periodCountCalendarDays: true,
|
||||
recurringEvent: true,
|
||||
schedulingType: true,
|
||||
seatsPerTimeSlot: true,
|
||||
userId: true,
|
||||
schedule: {
|
||||
select: {
|
||||
|
|
|
@ -46,6 +46,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
periodEndDate: true,
|
||||
metadata: true,
|
||||
periodCountCalendarDays: true,
|
||||
seatsPerTimeSlot: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
disableGuests: true,
|
||||
|
|
|
@ -126,6 +126,7 @@ export type FormValues = {
|
|||
periodDays: number;
|
||||
periodCountCalendarDays: "1" | "0";
|
||||
periodDates: { startDate: Date; endDate: Date };
|
||||
seatsPerTimeSlot: number | null;
|
||||
minimumBookingNotice: number;
|
||||
beforeBufferTime: number;
|
||||
afterBufferTime: number;
|
||||
|
@ -316,6 +317,11 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
);
|
||||
const [tokensList, setTokensList] = useState<Array<Token>>([]);
|
||||
|
||||
const defaultSeats = 2;
|
||||
const defaultSeatsInput = 6;
|
||||
const [enableSeats, setEnableSeats] = useState(!!eventType.seatsPerTimeSlot);
|
||||
const [inputSeatNumber, setInputSeatNumber] = useState(eventType.seatsPerTimeSlot! >= defaultSeatsInput);
|
||||
|
||||
const periodType =
|
||||
PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
|
||||
PERIOD_TYPES.find((s) => s.type === "UNLIMITED");
|
||||
|
@ -1006,6 +1012,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
giphyThankYouPage,
|
||||
beforeBufferTime,
|
||||
afterBufferTime,
|
||||
seatsPerTimeSlot,
|
||||
recurringEvent,
|
||||
locations,
|
||||
...input
|
||||
|
@ -1021,6 +1028,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
id: eventType.id,
|
||||
beforeEventBuffer: beforeBufferTime,
|
||||
afterEventBuffer: afterBufferTime,
|
||||
seatsPerTimeSlot,
|
||||
metadata: {
|
||||
...(smartContractAddress ? { smartContractAddress } : {}),
|
||||
...(giphyThankYouPage ? { giphyThankYouPage } : {}),
|
||||
|
@ -1410,6 +1418,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
label={t("opt_in_booking")}
|
||||
description={t("opt_in_booking_description")}
|
||||
defaultChecked={eventType.requiresConfirmation}
|
||||
disabled={enableSeats}
|
||||
checked={formMethods.watch("disableGuests")}
|
||||
onChange={(e) => {
|
||||
formMethods.setValue("requiresConfirmation", e?.target.checked);
|
||||
}}
|
||||
|
@ -1436,6 +1446,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
label={t("disable_guests")}
|
||||
description={t("disable_guests_description")}
|
||||
defaultChecked={eventType.disableGuests}
|
||||
// If we have seats per booking then we need to disable guests
|
||||
disabled={enableSeats}
|
||||
checked={formMethods.watch("disableGuests")}
|
||||
onChange={(e) => {
|
||||
formMethods.setValue("disableGuests", e?.target.checked);
|
||||
}}
|
||||
|
@ -1739,6 +1752,130 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<hr className="border-neutral-200" />
|
||||
<div className="block flex-col sm:flex">
|
||||
<Controller
|
||||
name="seatsPerTimeSlot"
|
||||
control={formMethods.control}
|
||||
render={() => (
|
||||
<CheckboxField
|
||||
id="seats"
|
||||
name="seats"
|
||||
label={t("offer_seats")}
|
||||
description={t("offer_seats_description")}
|
||||
defaultChecked={!!eventType.seatsPerTimeSlot}
|
||||
onChange={(e) => {
|
||||
if (e?.target.checked) {
|
||||
setEnableSeats(true);
|
||||
// Want to disable individuals from taking multiple seats
|
||||
formMethods.setValue("seatsPerTimeSlot", defaultSeats);
|
||||
formMethods.setValue("disableGuests", true);
|
||||
formMethods.setValue("requiresConfirmation", false);
|
||||
} else {
|
||||
setEnableSeats(false);
|
||||
formMethods.setValue("seatsPerTimeSlot", null);
|
||||
formMethods.setValue(
|
||||
"requiresConfirmation",
|
||||
eventType.requiresConfirmation
|
||||
);
|
||||
formMethods.setValue("disableGuests", eventType.disableGuests);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{enableSeats && (
|
||||
<div className="block sm:flex">
|
||||
<div className="mt-2 inline-flex w-full space-x-2 md:ml-48">
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
name="seatsPerTimeSlot"
|
||||
control={formMethods.control}
|
||||
render={() => {
|
||||
const selectSeatsPerTimeSlotOptions = [
|
||||
{ value: 2, label: "2" },
|
||||
{ value: 3, label: "3" },
|
||||
{ value: 4, label: "4" },
|
||||
{ value: 5, label: "5" },
|
||||
{
|
||||
value: -1,
|
||||
isDisabled: !eventType.users.some((user) => user.plan === "PRO"),
|
||||
label: (
|
||||
<div className="flex flex-row justify-between">
|
||||
<span>6 +</span>
|
||||
<Badge variant="default">PRO</Badge>
|
||||
</div>
|
||||
) as unknown as string,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<div className="block sm:flex">
|
||||
<div className="flex-auto">
|
||||
<label
|
||||
htmlFor="beforeBufferTime"
|
||||
className="mb-2 flex text-sm font-medium text-neutral-700">
|
||||
Number of seats per booking
|
||||
</label>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
classNamePrefix="react-select"
|
||||
className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-auto rounded-sm border border-gray-300 sm:text-sm "
|
||||
onChange={(val) => {
|
||||
if (val!.value === -1) {
|
||||
formMethods.setValue(
|
||||
"seatsPerTimeSlot",
|
||||
defaultSeatsInput
|
||||
);
|
||||
setInputSeatNumber(true);
|
||||
} else {
|
||||
setInputSeatNumber(false);
|
||||
formMethods.setValue("seatsPerTimeSlot", val!.value);
|
||||
}
|
||||
}}
|
||||
defaultValue={{
|
||||
value: eventType.seatsPerTimeSlot || defaultSeats,
|
||||
label: `${eventType.seatsPerTimeSlot || defaultSeats}`,
|
||||
}}
|
||||
options={selectSeatsPerTimeSlotOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{inputSeatNumber && (
|
||||
<div className="flex-auto md:ml-5">
|
||||
<label
|
||||
htmlFor="beforeBufferTime"
|
||||
className="mb-2 flex text-sm font-medium text-neutral-700">
|
||||
Enter number of seats
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className="focus:border-primary-500 focus:ring-primary-500 py- block w-20 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
|
||||
placeholder={`${defaultSeatsInput}`}
|
||||
{...formMethods.register("seatsPerTimeSlot", {
|
||||
valueAsNumber: true,
|
||||
min: defaultSeatsInput,
|
||||
})}
|
||||
defaultValue={defaultSeatsInput}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<hr className="border-neutral-200" />
|
||||
</>
|
||||
|
||||
<SuccessRedirectEdit<typeof formMethods>
|
||||
formMethods={formMethods}
|
||||
eventType={eventType}></SuccessRedirectEdit>
|
||||
|
@ -2219,6 +2356,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
price: true,
|
||||
currency: true,
|
||||
destinationCalendar: true,
|
||||
seatsPerTimeSlot: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
timeZone: true,
|
||||
slotInterval: true,
|
||||
metadata: true,
|
||||
seatsPerTimeSlot: true,
|
||||
schedule: {
|
||||
select: {
|
||||
timeZone: true,
|
||||
|
|
|
@ -50,6 +50,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
price: true,
|
||||
currency: true,
|
||||
metadata: true,
|
||||
seatsPerTimeSlot: true,
|
||||
team: {
|
||||
select: {
|
||||
slug: true,
|
||||
|
|
|
@ -767,7 +767,11 @@
|
|||
"external_redirect_url": "https://example.com/redirect-to-my-success-page",
|
||||
"redirect_url_upgrade_description": "In order to use this feature, you need to upgrade to a Pro account.",
|
||||
"duplicate": "Duplicate",
|
||||
"offer_seats": "Offer seats",
|
||||
"offer_seats_description": "Offer seats to bookings (This disables guests)",
|
||||
"seats_available": "Seats available",
|
||||
"you_can_manage_your_schedules": "You can manage your schedules on the Availability page.",
|
||||
"booking_full": "No more seats available",
|
||||
"api_keys": "API Keys",
|
||||
"api_key_modal_subtitle": "API keys allow you to make API calls for your own account.",
|
||||
"api_keys_subtitle": "Generate API keys to use for accessing your own account.",
|
||||
|
|
|
@ -49,6 +49,7 @@ const commons = {
|
|||
price: 0,
|
||||
currency: "usd",
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
seatsPerTimeSlot: null,
|
||||
id: 0,
|
||||
metadata: {
|
||||
smartContractAddress: "",
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "EventType" ADD COLUMN "seatsPerTimeSlot" INTEGER;
|
||||
|
|
@ -66,6 +66,7 @@ model EventType {
|
|||
minimumBookingNotice Int @default(120)
|
||||
beforeEventBuffer Int @default(0)
|
||||
afterEventBuffer Int @default(0)
|
||||
seatsPerTimeSlot Int?
|
||||
schedulingType SchedulingType?
|
||||
schedule Schedule?
|
||||
price Int @default(0)
|
||||
|
|
Loading…
Reference in New Issue
Block a user