Seat Confirmation Roster
Host drawer lists for a class that needs a yes for each person.
Use this inside the existing booking drawer when an event type has both Requires confirmation and Seats. The Unconfirmed list stays one row per session and does not say how many people are waiting. Opening that row is the drawer.
A normal meeting that needs confirmation stays booking-level Confirm / Reject. A class that does not need confirmation keeps today's attendees list and Remove seats.
import {
SeatConfirmationRoster,
SeatConfirmationRosterActions,
} from "@calcom/features/bookings/components/seat-confirmation-roster";Ownership
Feature โ not @coss/ui/shared. It knows about accepted seats, waiting people, and the seat limit. Shared pieces stay generic: SelectionList, Checkbox, Button, Badge, Alert.
The roster does not decide booking status. Once anyone has been accepted, the booking stays confirmed even if the room is later emptied. That rule stays in the caller. Waiting is everyone who is not confirmed: pending and rejected.
When to use it
Two in the room and three waiting. Accept on a waiting person sends that one id. Check several waiting people, then Accept or Reject that selection from the footer. The row Accept hides once anyone is checked, so the footer is the only commit. Remove on a person in the room sends that one id. The footer appears only once someone waiting is checked, and it shows how many are selected. Rejected people show a Rejected badge next to their name. A full room hides the checkboxes and the row Accept. A line under In the room says to remove someone to free a seat. Search filters the waiting list only, so the room stays visible.
2 of 4 seats taken
In the room
Waiting
Everyone waiting, with none of the four seats taken.
0 of 4 seats taken
Waiting
Each person can carry a disclosure under their row. The drawer puts that seat's booking responses there, in both lists, so a host reads a waiting applicant's answers before accepting or rejecting them. A person with no answers gets no disclosure.
2 of 4 seats taken
In the room
Waiting
The room is full. Waiting people stay visible, without checkboxes, until someone in the room is removed.
4 of 4 seats taken
In the room
Remove someone to free a seat.
Waiting
Twelve people already in the room, sixteen waiting, and no seats left. Search sits above both lists and filters the waiting list only. Each list stops at a fixed height and scrolls on its own. The waiting list stays read-only until a seat is free.
12 of 12 seats taken
In the room
Remove someone to free a seat.
Waiting
Copy into the drawer
Lists go in the drawer body, in place of the single attendees list. Buttons go in the existing DrawerFooter. The product fills labels with t(). The design-system app has no i18n provider, so the demos above use the English defaults.
const labels = {
inTheRoom: t("seat_confirmation_in_the_room"),
waiting: t("seat_confirmation_waiting"),
seatsTaken: (taken, total) => t("seat_confirmation_seats_taken", { taken, total }),
reject: t("reject"),
accept: t("accept"),
remove: t("remove"),
removePerson: (name) => t("seat_confirmation_remove_person", { name }),
acceptPerson: (name) => t("seat_confirmation_accept_person", { name }),
freeSeat: t("seat_confirmation_free_seat"),
empty: t("seat_confirmation_empty"),
search: t("seat_confirmation_search"),
noMatches: t("seat_confirmation_no_matches"),
unselectAll: t("seat_confirmation_unselect_all"),
rejected: t("seat_confirmation_rejected"),
selected: (count) =>
t(count === 1 ? "seat_confirmation_selected_one" : "seat_confirmation_selected_other", { count }),
};
<SeatConfirmationRoster
seatsPerTimeSlot={4}
people={people}
selectedIds={selectedIds}
onSelectedIdsChange={setSelectedIds}
onAccept={acceptPeople}
onReject={rejectPeople}
labels={labels}
/>
<SeatConfirmationRosterActions
seatsPerTimeSlot={4}
people={people}
selectedIds={selectedIds}
onSelectedIdsChange={setSelectedIds}
onAccept={acceptPeople}
onReject={rejectPeople}
labels={labels}
/>people is { id, name, email, group: "accepted" | "pending" | "rejected" }. id is the seat id the later mutation will send. Pending and rejected people share the waiting list. Pass the same labels to SeatConfirmationRosterActions. The actions render nothing until a waiting person is checked.
API
| Prop / part | Type | Notes |
|---|---|---|
SeatConfirmationRoster | component | The two lists, the per-row Remove, and a per-row Accept while a seat is free and nobody is checked. |
SeatConfirmationRosterActions | component | Footer Reject and Accept, rendered only once a waiting person is checked. The row shows how many are selected and Unselect all. Accept stays off when the selection does not fit. |
seatsPerTimeSlot | number | Seat limit. Seats left = this minus accepted people. |
people | SeatConfirmationRosterPerson[] | Accepted people are in the room. Pending and rejected people are waiting. |
selectedIds | string[] | Checked waiting people. |
onSelectedIdsChange | (ids: string[]) => void | Fired when a check is added or removed, and when Unselect all clears the selection. Unchecked rows stay disabled once every free seat is already selected. A full room shows those rows without a checkbox. |
onAccept | (ids: string[]) => void | One id from a waiting row, or the checked waiting people from the footer. The row button hides once anyone is checked. |
onReject | (ids: string[]) => void | The selection, or one id from a row in the room. The room button is labeled Remove. |
renderPersonDetails | (person) => ReactNode | Optional. Rendered under the row, for both lists. The drawer puts that seat's booking responses here so a host can read a waiting applicant's answers before deciding. Return null for a person with nothing to show and the row stays a single line. |
labels | SeatConfirmationRosterLabels | Optional. Defaults are English. |
| Search | text field | Filters the waiting list by name or email. The room stays visible. Seat counts stay on the full list. A checked person who is filtered out stays checked, and Unselect all in the footer clears that selection without clearing the search. |
Selection rules live in packages/features/bookings/lib/seat-confirmation-selection.ts. Checking another person when none are left does not add them.
The lists scroll. There is no Previous / Next.
Internationalization
The component does not call t(). Demos use the English defaults. Add these keys to packages/i18n/locales/en/common.json when the drawer is wired, and pass them through labels. Accept, Reject, and Remove reuse accept, reject, and remove.
| Key | English |
|---|---|
seat_confirmation_in_the_room | In the room |
seat_confirmation_waiting | Waiting |
seat_confirmation_seats_taken | {{taken}} of {{total}} seats taken |
seat_confirmation_remove_person | Remove {{name}} |
seat_confirmation_accept_person | Accept {{name}} |
seat_confirmation_free_seat | Remove someone to free a seat. |
seat_confirmation_empty | No one is in this class. |
seat_confirmation_search | Search by name or email |
seat_confirmation_no_matches | No one waiting matches that search. |
seat_confirmation_unselect_all | Unselect all |
seat_confirmation_rejected | Rejected |
seat_confirmation_selected_one | {{count}} selected |
seat_confirmation_selected_other | {{count}} selected |
Call sites
| File | Notes |
|---|---|
apps/web/modules/bookings/components/booking-details-drawer.tsx | Fills labels with t() and passes renderPersonDetails so each seat can open its booking responses. |
Example URLs
Not in the product yet. The live demos on this page are the component.
| Path | Notes |
|---|---|
/docs/composites/seat-confirmation-roster | The three fixtures above |