diff --git a/apps/api/lib/validations/event-type.ts b/apps/api/lib/validations/event-type.ts
index 3c6c839fc1..295884ab46 100644
--- a/apps/api/lib/validations/event-type.ts
+++ b/apps/api/lib/validations/event-type.ts
@@ -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,
diff --git a/apps/api/pages/api/bookings/_post.ts b/apps/api/pages/api/bookings/_post.ts
index 39e9fc69a5..aae99a031b 100644
--- a/apps/api/pages/api/bookings/_post.ts
+++ b/apps/api/pages/api/bookings/_post.ts
@@ -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'
diff --git a/apps/api/pages/api/event-types/[id]/_patch.ts b/apps/api/pages/api/event-types/[id]/_patch.ts
index 19f50db189..f70bd28407 100644
--- a/apps/api/pages/api/event-types/[id]/_patch.ts
+++ b/apps/api/pages/api/event-types/[id]/_patch.ts
@@ -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
diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx
index 540021f92a..c95d28a4f9 100644
--- a/apps/web/components/eventtype/EventAdvancedTab.tsx
+++ b/apps/web/components/eventtype/EventAdvancedTab.tsx
@@ -377,6 +377,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick
+
+ formMethods.setValue("seatsShowAvailabilityCount", e.target.checked)}
+ defaultChecked={!!eventType.seatsShowAvailabilityCount}
+ />
+
)}
/>
diff --git a/apps/web/pages/booking/[uid].tsx b/apps/web/pages/booking/[uid].tsx
index 0e5072778e..a77dc76965 100644
--- a/apps/web/pages/booking/[uid].tsx
+++ b/apps/web/pages/booking/[uid].tsx
@@ -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<
diff --git a/apps/web/pages/event-types/[type]/index.tsx b/apps/web/pages/event-types/[type]/index.tsx
index 8b54d23b62..91b3901e0e 100644
--- a/apps/web/pages/event-types/[type]/index.tsx
+++ b/apps/web/pages/event-types/[type]/index.tsx
@@ -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,
});
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 90bd57c0e2..716c477f69 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -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",
diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts
index a56648bcab..b244278efb 100644
--- a/packages/core/EventManager.ts
+++ b/packages/core/EventManager.ts
@@ -262,6 +262,7 @@ export default class EventManager {
select: {
seatsPerTimeSlot: true,
seatsShowAttendees: true,
+ seatsShowAvailabilityCount: true,
},
},
},
diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx
index 7459b4d599..fc9d1dde34 100644
--- a/packages/features/bookings/Booker/Booker.tsx
+++ b/packages/features/bookings/Booker/Booker.tsx
@@ -327,6 +327,7 @@ const BookerComponent = ({
prefetchNextMonth={prefetchNextMonth}
monthCount={monthCount}
seatsPerTimeSlot={event.data?.seatsPerTimeSlot}
+ showAvailableSeatsCount={event.data?.seatsShowAvailabilityCount}
/>
diff --git a/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx b/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx
index 8f1e01b773..3243d1e5e2 100644
--- a/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx
+++ b/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx
@@ -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")
diff --git a/packages/features/bookings/Booker/components/EventMeta.tsx b/packages/features/bookings/Booker/components/EventMeta.tsx
index 2f0f4cbb05..92b117c689 100644
--- a/packages/features/bookings/Booker/components/EventMeta.tsx
+++ b/packages/features/bookings/Booker/components/EventMeta.tsx
@@ -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 = () => {
- {bookingSeatAttendeesQty ? eventTotalSeats - bookingSeatAttendeesQty : eventTotalSeats} /{" "}
- {eventTotalSeats}{" "}
- {t("seats_available", {
- count: bookingSeatAttendeesQty
- ? eventTotalSeats - bookingSeatAttendeesQty
- : eventTotalSeats,
- })}
+
diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts
index ef5bf203d5..04a995edca 100644
--- a/packages/features/bookings/Booker/store.ts
+++ b/packages/features/bookings/Booker/store.ts
@@ -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((set, get) => ({
seatsPerTimeSlot: undefined,
attendees: undefined,
bookingUid: undefined,
+ showAvailableSeatsCount: true,
},
setSeatedEventData: (seatedEventData: SeatedEventData) => {
set({ seatedEventData });
diff --git a/packages/features/bookings/components/AvailableTimes.tsx b/packages/features/bookings/components/AvailableTimes.tsx
index a97e5fb95d..0808394202 100644
--- a/packages/features/bookings/components/AvailableTimes.tsx
+++ b/packages/features/bookings/components/AvailableTimes.tsx
@@ -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 && {t("booking_full")}
}
{hasTimeSlots && !bookingFull && (
-
+
- {slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot}{" "}
- {t("seats_available", {
- count: slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot,
- })}
+
)}
diff --git a/packages/features/bookings/components/SeatsAvailabilityText.tsx b/packages/features/bookings/components/SeatsAvailabilityText.tsx
new file mode 100644
index 0000000000..3616c37a88
--- /dev/null
+++ b/packages/features/bookings/components/SeatsAvailabilityText.tsx
@@ -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 (
+
+ {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,
+ })}
+
+ );
+};
diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts
index fc344fa536..f7ad9e8ede 100644
--- a/packages/features/bookings/lib/handleNewBooking.ts
+++ b/packages/features/bookings/lib/handleNewBooking.ts
@@ -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,
};
diff --git a/packages/features/embed/Embed.tsx b/packages/features/embed/Embed.tsx
index 7648bd64e7..d561f951e0 100644
--- a/packages/features/embed/Embed.tsx
+++ b/packages/features/embed/Embed.tsx
@@ -263,6 +263,7 @@ const EmailEmbed = ({ eventType, username }: { eventType?: EventType; username:
}
onTimeSelect={onTimeSelect}
slots={slots}
+ showAvailableSeatsCount={eventType.seatsShowAvailabilityCount}
/>
) : null}
diff --git a/packages/features/eventtypes/lib/getPublicEvent.ts b/packages/features/eventtypes/lib/getPublicEvent.ts
index 9132b96fac..ced065b262 100644
--- a/packages/features/eventtypes/lib/getPublicEvent.ts
+++ b/packages/features/eventtypes/lib/getPublicEvent.ts
@@ -39,6 +39,7 @@ const publicEventSelect = Prisma.validator()({
price: true,
currency: true,
seatsPerTimeSlot: true,
+ seatsShowAvailabilityCount: true,
bookingFields: true,
team: {
select: {
diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts
index c0871fb983..8b0c0cbd8e 100644
--- a/packages/lib/defaultEvents.ts
+++ b/packages/lib/defaultEvents.ts
@@ -79,6 +79,7 @@ const commons = {
schedulingType: SchedulingType.COLLECTIVE,
seatsPerTimeSlot: null,
seatsShowAttendees: null,
+ seatsShowAvailabilityCount: null,
id: 0,
hideCalendarNotes: false,
recurringEvent: null,
diff --git a/packages/lib/getEventTypeById.ts b/packages/lib/getEventTypeById.ts
index 094a236186..592b74643a 100644
--- a/packages/lib/getEventTypeById.ts
+++ b/packages/lib/getEventTypeById.ts
@@ -174,6 +174,7 @@ export default async function getEventTypeById({
destinationCalendar: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
+ seatsShowAvailabilityCount: true,
webhooks: {
select: {
id: true,
diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts
index 43c25e4077..550b90175e 100644
--- a/packages/lib/test/builder.ts
+++ b/packages/lib/test/builder.ts
@@ -93,6 +93,7 @@ export const buildEventType = (eventType?: Partial): EventType => {
afterEventBuffer: 0,
seatsPerTimeSlot: null,
seatsShowAttendees: null,
+ seatsShowAvailabilityCount: null,
schedulingType: null,
scheduleId: null,
bookingLimits: null,
diff --git a/packages/prisma/migrations/20230902163155_add_seats_show_availability_count_field/migration.sql b/packages/prisma/migrations/20230902163155_add_seats_show_availability_count_field/migration.sql
new file mode 100644
index 0000000000..5f66dd5b82
--- /dev/null
+++ b/packages/prisma/migrations/20230902163155_add_seats_show_availability_count_field/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "EventType" ADD COLUMN "seatsShowAvailabilityCount" BOOLEAN DEFAULT true;
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index c6bf3208d4..199c2ea2af 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -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?
diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts
index 1b62e2e1d3..fd05ce23bb 100644
--- a/packages/prisma/zod-utils.ts
+++ b/packages/prisma/zod-utils.ts
@@ -575,6 +575,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit