feat: preference to show/hide available seats count in events (#11109)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
parent
7e2ad3cea9
commit
9c3cbee363
|
@ -70,6 +70,7 @@ const schemaEventTypeCreateParams = z
|
|||
recurringEvent: recurringEventInputSchema.optional(),
|
||||
seatsPerTimeSlot: z.number().optional(),
|
||||
seatsShowAttendees: z.boolean().optional(),
|
||||
seatsShowAvailabilityCount: z.boolean().optional(),
|
||||
bookingFields: eventTypeBookingFields.optional(),
|
||||
scheduleId: z.number().optional(),
|
||||
})
|
||||
|
@ -89,6 +90,7 @@ const schemaEventTypeEditParams = z
|
|||
length: z.number().int().optional(),
|
||||
seatsPerTimeSlot: z.number().optional(),
|
||||
seatsShowAttendees: z.boolean().optional(),
|
||||
seatsShowAvailabilityCount: z.boolean().optional(),
|
||||
bookingFields: eventTypeBookingFields.optional(),
|
||||
scheduleId: z.number().optional(),
|
||||
})
|
||||
|
@ -129,6 +131,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
|||
metadata: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
bookingFields: true,
|
||||
bookingLimits: true,
|
||||
durationLimits: true,
|
||||
|
|
|
@ -94,6 +94,9 @@ import { defaultResponder } from "@calcom/lib/server";
|
|||
* seatsShowAttendees:
|
||||
* type: boolean
|
||||
* description: 'Share Attendee information in seats'
|
||||
* seatsShowAvailabilityCount:
|
||||
* type: boolean
|
||||
* description: 'Show the number of available seats'
|
||||
* smsReminderNumber:
|
||||
* type: number
|
||||
* description: 'SMS reminder number'
|
||||
|
|
|
@ -146,6 +146,9 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
|
|||
* seatsShowAttendees:
|
||||
* type: boolean
|
||||
* description: 'Share Attendee information in seats'
|
||||
* seatsShowAvailabilityCount:
|
||||
* type: boolean
|
||||
* description: 'Show the number of available seats'
|
||||
* locations:
|
||||
* type: array
|
||||
* description: A list of all available locations for the event type
|
||||
|
|
|
@ -377,6 +377,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||
defaultChecked={!!eventType.seatsShowAttendees}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<CheckboxField
|
||||
description={t("show_available_seats_count")}
|
||||
disabled={seatsLocked.disabled}
|
||||
onChange={(e) => formMethods.setValue("seatsShowAvailabilityCount", e.target.checked)}
|
||||
defaultChecked={!!eventType.seatsShowAvailabilityCount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -917,6 +917,7 @@ const getEventTypesFromDB = async (id: number) => {
|
|||
metadata: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
},
|
||||
|
@ -940,6 +941,7 @@ const handleSeatsEventTypeOnBooking = async (
|
|||
eventType: {
|
||||
seatsPerTimeSlot?: number | null;
|
||||
seatsShowAttendees: boolean | null;
|
||||
seatsShowAvailabilityCount: boolean | null;
|
||||
[x: string | number | symbol]: unknown;
|
||||
},
|
||||
bookingInfo: Partial<
|
||||
|
|
|
@ -113,6 +113,7 @@ export type FormValues = {
|
|||
periodDates: { startDate: Date; endDate: Date };
|
||||
seatsPerTimeSlot: number | null;
|
||||
seatsShowAttendees: boolean | null;
|
||||
seatsShowAvailabilityCount: boolean | null;
|
||||
seatsPerTimeSlotEnabled: boolean;
|
||||
minimumBookingNotice: number;
|
||||
minimumBookingNoticeInDurationType: number;
|
||||
|
@ -360,6 +361,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
afterBufferTime,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
seatsShowAvailabilityCount,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
recurringEvent,
|
||||
|
@ -426,6 +428,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
durationLimits,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
seatsShowAvailabilityCount,
|
||||
metadata,
|
||||
customInputs,
|
||||
children,
|
||||
|
@ -460,6 +463,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
afterBufferTime,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
seatsShowAvailabilityCount,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
recurringEvent,
|
||||
|
@ -516,6 +520,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
durationLimits,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
seatsShowAvailabilityCount,
|
||||
metadata,
|
||||
customInputs,
|
||||
});
|
||||
|
|
|
@ -974,6 +974,8 @@
|
|||
"offer_seats_description": "Offer seats for booking. This automatically disables guest & opt-in bookings.",
|
||||
"seats_available_one": "Seat available",
|
||||
"seats_available_other": "Seats available",
|
||||
"seats_nearly_full": "Seats almost full",
|
||||
"seats_half_full": "Seats filling fast",
|
||||
"number_of_seats": "Number of seats per booking",
|
||||
"enter_number_of_seats": "Enter number of seats",
|
||||
"you_can_manage_your_schedules": "You can manage your schedules on the Availability page.",
|
||||
|
@ -1455,6 +1457,7 @@
|
|||
"add_limit": "Add Limit",
|
||||
"team_name_required": "Team name required",
|
||||
"show_attendees": "Share attendee information between guests",
|
||||
"show_available_seats_count": "Show the number of available seats",
|
||||
"how_booking_questions_as_variables": "How to use booking questions as variables?",
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Use uppercase for all letters",
|
||||
|
|
|
@ -262,6 +262,7 @@ export default class EventManager {
|
|||
select: {
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -327,6 +327,7 @@ const BookerComponent = ({
|
|||
prefetchNextMonth={prefetchNextMonth}
|
||||
monthCount={monthCount}
|
||||
seatsPerTimeSlot={event.data?.seatsPerTimeSlot}
|
||||
showAvailableSeatsCount={event.data?.seatsShowAvailabilityCount}
|
||||
/>
|
||||
</BookerSection>
|
||||
</AnimatePresence>
|
||||
|
|
|
@ -19,6 +19,7 @@ type AvailableTimeSlotsProps = {
|
|||
prefetchNextMonth: boolean;
|
||||
monthCount: number | undefined;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
showAvailableSeatsCount?: boolean | null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -32,6 +33,7 @@ export const AvailableTimeSlots = ({
|
|||
extraDays,
|
||||
limitHeight,
|
||||
seatsPerTimeSlot,
|
||||
showAvailableSeatsCount,
|
||||
prefetchNextMonth,
|
||||
monthCount,
|
||||
}: AvailableTimeSlotsProps) => {
|
||||
|
@ -60,6 +62,7 @@ export const AvailableTimeSlots = ({
|
|||
seatsPerTimeSlot,
|
||||
attendees,
|
||||
bookingUid,
|
||||
showAvailableSeatsCount,
|
||||
});
|
||||
|
||||
if (seatsPerTimeSlot && seatsPerTimeSlot - attendees > 1) {
|
||||
|
@ -116,6 +119,7 @@ export const AvailableTimeSlots = ({
|
|||
date={dayjs(slots.date)}
|
||||
slots={slots.slots}
|
||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||
showAvailableSeatsCount={showAvailableSeatsCount}
|
||||
availableMonth={
|
||||
dayjs(selectedDate).format("MM") !== dayjs(slots.date).format("MM")
|
||||
? dayjs(slots.date).format("MMM")
|
||||
|
|
|
@ -4,6 +4,7 @@ import { shallow } from "zustand/shallow";
|
|||
|
||||
import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings";
|
||||
import { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText";
|
||||
import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
@ -130,13 +131,12 @@ export const EventMeta = () => {
|
|||
<EventMetaBlock icon={User} className={`${colorClass}`}>
|
||||
<div className="text-bookinghighlight flex items-start text-sm">
|
||||
<p>
|
||||
{bookingSeatAttendeesQty ? eventTotalSeats - bookingSeatAttendeesQty : eventTotalSeats} /{" "}
|
||||
{eventTotalSeats}{" "}
|
||||
{t("seats_available", {
|
||||
count: bookingSeatAttendeesQty
|
||||
? eventTotalSeats - bookingSeatAttendeesQty
|
||||
: eventTotalSeats,
|
||||
})}
|
||||
<SeatsAvailabilityText
|
||||
showExact={!!seatedEventData.showAvailableSeatsCount}
|
||||
totalSeats={eventTotalSeats}
|
||||
bookedSeats={bookingSeatAttendeesQty || 0}
|
||||
variant="fraction"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</EventMetaBlock>
|
||||
|
|
|
@ -33,6 +33,7 @@ type SeatedEventData = {
|
|||
seatsPerTimeSlot?: number | null;
|
||||
attendees?: number;
|
||||
bookingUid?: string;
|
||||
showAvailableSeatsCount?: boolean | null;
|
||||
};
|
||||
|
||||
export type BookerStore = {
|
||||
|
@ -206,6 +207,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
|
|||
seatsPerTimeSlot: undefined,
|
||||
attendees: undefined,
|
||||
bookingUid: undefined,
|
||||
showAvailableSeatsCount: true,
|
||||
},
|
||||
setSeatedEventData: (seatedEventData: SeatedEventData) => {
|
||||
set({ seatedEventData });
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Button, SkeletonText } from "@calcom/ui";
|
|||
|
||||
import { useBookerStore } from "../Booker/store";
|
||||
import { useTimePreferences } from "../lib";
|
||||
import { SeatsAvailabilityText } from "./SeatsAvailabilityText";
|
||||
import { TimeFormatToggle } from "./TimeFormatToggle";
|
||||
|
||||
type AvailableTimesProps = {
|
||||
|
@ -24,6 +25,7 @@ type AvailableTimesProps = {
|
|||
bookingUid?: string
|
||||
) => void;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
showAvailableSeatsCount?: boolean | null;
|
||||
showTimeFormatToggle?: boolean;
|
||||
className?: string;
|
||||
availableMonth?: string | undefined;
|
||||
|
@ -35,6 +37,7 @@ export const AvailableTimes = ({
|
|||
slots,
|
||||
onTimeSelect,
|
||||
seatsPerTimeSlot,
|
||||
showAvailableSeatsCount,
|
||||
showTimeFormatToggle = true,
|
||||
className,
|
||||
availableMonth,
|
||||
|
@ -110,15 +113,16 @@ export const AvailableTimes = ({
|
|||
{dayjs.utc(slot.time).tz(timezone).format(timeFormat)}
|
||||
{bookingFull && <p className="text-sm">{t("booking_full")}</p>}
|
||||
{hasTimeSlots && !bookingFull && (
|
||||
<p className="flex items-center text-sm lowercase">
|
||||
<p className="flex items-center text-sm">
|
||||
<span
|
||||
className={classNames(colorClass, "mr-1 inline-block h-2 w-2 rounded-full")}
|
||||
aria-hidden
|
||||
/>
|
||||
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot}{" "}
|
||||
{t("seats_available", {
|
||||
count: slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot,
|
||||
})}
|
||||
<SeatsAvailabilityText
|
||||
showExact={!!showAvailableSeatsCount}
|
||||
totalSeats={seatsPerTimeSlot}
|
||||
bookedSeats={slot.attendees || 0}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</Button>
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Whether to show the exact number of seats available or not
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
showExact: boolean;
|
||||
/**
|
||||
* Shows available seats count as either whole number or fraction.
|
||||
*
|
||||
* Applies only when `showExact` is `true`
|
||||
*
|
||||
* @default "whole"
|
||||
*/
|
||||
variant?: "whole" | "fraction";
|
||||
/** Number of seats booked in the event */
|
||||
bookedSeats: number;
|
||||
/** Total number of seats in the event */
|
||||
totalSeats: number;
|
||||
};
|
||||
|
||||
export const SeatsAvailabilityText = ({
|
||||
showExact = true,
|
||||
bookedSeats,
|
||||
totalSeats,
|
||||
variant = "whole",
|
||||
}: Props) => {
|
||||
const { t } = useLocale();
|
||||
const availableSeats = totalSeats - bookedSeats;
|
||||
const isHalfFull = bookedSeats / totalSeats >= 0.5;
|
||||
const isNearlyFull = bookedSeats / totalSeats >= 0.83;
|
||||
|
||||
return (
|
||||
<span className={classNames(showExact && "lowercase")}>
|
||||
{showExact
|
||||
? `${availableSeats}${variant === "fraction" ? ` / ${totalSeats}` : ""} ${t("seats_available", {
|
||||
count: availableSeats,
|
||||
})}`
|
||||
: isNearlyFull
|
||||
? t("seats_nearly_full")
|
||||
: isHalfFull
|
||||
? t("seats_half_full")
|
||||
: t("seats_available", {
|
||||
count: availableSeats,
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
|
@ -275,6 +275,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
|||
seatsPerTimeSlot: true,
|
||||
recurringEvent: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
bookingLimits: true,
|
||||
durationLimits: true,
|
||||
parentId: true,
|
||||
|
@ -1071,6 +1072,7 @@ async function handler(
|
|||
// if seats are not enabled we should default true
|
||||
seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true,
|
||||
seatsPerTimeSlot: eventType.seatsPerTimeSlot,
|
||||
seatsShowAvailabilityCount: eventType.seatsPerTimeSlot ? eventType.seatsShowAvailabilityCount : true,
|
||||
schedulingType: eventType.schedulingType,
|
||||
};
|
||||
|
||||
|
|
|
@ -263,6 +263,7 @@ const EmailEmbed = ({ eventType, username }: { eventType?: EventType; username:
|
|||
}
|
||||
onTimeSelect={onTimeSelect}
|
||||
slots={slots}
|
||||
showAvailableSeatsCount={eventType.seatsShowAvailabilityCount}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
@ -39,6 +39,7 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
|||
price: true,
|
||||
currency: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
bookingFields: true,
|
||||
team: {
|
||||
select: {
|
||||
|
|
|
@ -79,6 +79,7 @@ const commons = {
|
|||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
seatsPerTimeSlot: null,
|
||||
seatsShowAttendees: null,
|
||||
seatsShowAvailabilityCount: null,
|
||||
id: 0,
|
||||
hideCalendarNotes: false,
|
||||
recurringEvent: null,
|
||||
|
|
|
@ -174,6 +174,7 @@ export default async function getEventTypeById({
|
|||
destinationCalendar: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
webhooks: {
|
||||
select: {
|
||||
id: true,
|
||||
|
|
|
@ -93,6 +93,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
|
|||
afterEventBuffer: 0,
|
||||
seatsPerTimeSlot: null,
|
||||
seatsShowAttendees: null,
|
||||
seatsShowAvailabilityCount: null,
|
||||
schedulingType: null,
|
||||
scheduleId: null,
|
||||
bookingLimits: null,
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "EventType" ADD COLUMN "seatsShowAvailabilityCount" BOOLEAN DEFAULT true;
|
|
@ -98,6 +98,7 @@ model EventType {
|
|||
afterEventBuffer Int @default(0)
|
||||
seatsPerTimeSlot Int?
|
||||
seatsShowAttendees Boolean? @default(false)
|
||||
seatsShowAvailabilityCount Boolean? @default(true)
|
||||
schedulingType SchedulingType?
|
||||
schedule Schedule? @relation(fields: [scheduleId], references: [id])
|
||||
scheduleId Int?
|
||||
|
|
|
@ -575,6 +575,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect
|
|||
successRedirectUrl: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
periodType: true,
|
||||
hashedLink: true,
|
||||
webhooks: true,
|
||||
|
|
|
@ -206,6 +206,7 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp
|
|||
bookingFields: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
eventName: true,
|
||||
},
|
||||
},
|
||||
|
@ -295,6 +296,7 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp
|
|||
cancellationReason: "Payment method removed by organizer",
|
||||
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
|
||||
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
|
||||
seatsShowAvailabilityCount: booking.eventType?.seatsShowAvailabilityCount,
|
||||
},
|
||||
{
|
||||
eventName: booking?.eventType?.eventName,
|
||||
|
|
|
@ -191,6 +191,7 @@ async function getBookings({
|
|||
currency: true,
|
||||
metadata: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
|
|
|
@ -177,6 +177,7 @@ export interface CalendarEvent {
|
|||
eventTypeId?: number | null;
|
||||
appsStatus?: AppsStatus[];
|
||||
seatsShowAttendees?: boolean | null;
|
||||
seatsShowAvailabilityCount?: boolean | null;
|
||||
attendeeSeatId?: string;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
schedulingType?: SchedulingType | null;
|
||||
|
|
Loading…
Reference in New Issue
Block a user