From 9c3cbee3632933592448f2f742ab2fbcd8907592 Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Fri, 8 Sep 2023 21:07:26 +0530 Subject: [PATCH] feat: preference to show/hide available seats count in events (#11109) Co-authored-by: Peer Richelsen --- apps/api/lib/validations/event-type.ts | 3 ++ apps/api/pages/api/bookings/_post.ts | 3 ++ apps/api/pages/api/event-types/[id]/_patch.ts | 3 ++ .../components/eventtype/EventAdvancedTab.tsx | 8 +++ apps/web/pages/booking/[uid].tsx | 2 + apps/web/pages/event-types/[type]/index.tsx | 5 ++ apps/web/public/static/locales/en/common.json | 3 ++ packages/core/EventManager.ts | 1 + packages/features/bookings/Booker/Booker.tsx | 1 + .../Booker/components/AvailableTimeSlots.tsx | 4 ++ .../bookings/Booker/components/EventMeta.tsx | 14 ++--- packages/features/bookings/Booker/store.ts | 2 + .../bookings/components/AvailableTimes.tsx | 14 +++-- .../components/SeatsAvailabilityText.tsx | 51 +++++++++++++++++++ .../features/bookings/lib/handleNewBooking.ts | 2 + packages/features/embed/Embed.tsx | 1 + .../features/eventtypes/lib/getPublicEvent.ts | 1 + packages/lib/defaultEvents.ts | 1 + packages/lib/getEventTypeById.ts | 1 + packages/lib/test/builder.ts | 1 + .../migration.sql | 2 + packages/prisma/schema.prisma | 1 + packages/prisma/zod-utils.ts | 1 + .../deleteCredential.handler.ts | 2 + .../routers/viewer/bookings/get.handler.ts | 1 + packages/types/Calendar.d.ts | 1 + 26 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 packages/features/bookings/components/SeatsAvailabilityText.tsx create mode 100644 packages/prisma/migrations/20230902163155_add_seats_show_availability_count_field/migration.sql 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