feat: preference to show/hide available seats count in events (#11109)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
Shubham Singh 2023-09-08 21:07:26 +05:30 committed by GitHub
parent 7e2ad3cea9
commit 9c3cbee363
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 117 additions and 12 deletions

View File

@ -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,

View File

@ -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'

View File

@ -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

View File

@ -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>
)}
/>

View File

@ -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<

View File

@ -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,
});

View File

@ -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",

View File

@ -262,6 +262,7 @@ export default class EventManager {
select: {
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
},
},
},

View File

@ -327,6 +327,7 @@ const BookerComponent = ({
prefetchNextMonth={prefetchNextMonth}
monthCount={monthCount}
seatsPerTimeSlot={event.data?.seatsPerTimeSlot}
showAvailableSeatsCount={event.data?.seatsShowAvailabilityCount}
/>
</BookerSection>
</AnimatePresence>

View File

@ -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")

View File

@ -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>

View File

@ -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 });

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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,
};

View File

@ -263,6 +263,7 @@ const EmailEmbed = ({ eventType, username }: { eventType?: EventType; username:
}
onTimeSelect={onTimeSelect}
slots={slots}
showAvailableSeatsCount={eventType.seatsShowAvailabilityCount}
/>
</div>
) : null}

View File

@ -39,6 +39,7 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
price: true,
currency: true,
seatsPerTimeSlot: true,
seatsShowAvailabilityCount: true,
bookingFields: true,
team: {
select: {

View File

@ -79,6 +79,7 @@ const commons = {
schedulingType: SchedulingType.COLLECTIVE,
seatsPerTimeSlot: null,
seatsShowAttendees: null,
seatsShowAvailabilityCount: null,
id: 0,
hideCalendarNotes: false,
recurringEvent: null,

View File

@ -174,6 +174,7 @@ export default async function getEventTypeById({
destinationCalendar: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
webhooks: {
select: {
id: true,

View File

@ -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,

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EventType" ADD COLUMN "seatsShowAvailabilityCount" BOOLEAN DEFAULT true;

View File

@ -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?

View File

@ -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,

View File

@ -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,

View File

@ -191,6 +191,7 @@ async function getBookings({
currency: true,
metadata: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
team: {
select: {
id: true,

View File

@ -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;