Skip to content

Booking and Queue

c0dewhacker edited this page Apr 25, 2026 · 2 revisions

Booking and Queue

This page describes what happens when a user books an asset, what "joining the queue" actually does, and all the rules that gate it.

The floor plan view

Navigate to any floor via Sidebar → Buildings → building → floor. Roomer renders the floor plan image with every bookable asset overlaid as a draggable-shaped marker.

Date picker

The top of the floor page has a date picker. The availability calculation is always relative to a specific date — by default, today. Users can pick any date up to a configurable ceiling (ADVANCE_BOOKING_DAYS, defaults to 14).

Asset marker colours and statuses

Each marker is coloured by its primary zone. Hovering (or tapping on mobile) shows a tooltip with the asset's status for the selected date:

Status Marker appearance Meaning
available Normal zone colour You can book it.
mine Green border You already have a confirmed booking covering part of the day.
booked Faded + "booked" badge Someone else has a confirmed booking covering part of the day.
queued Orange "waitlist" badge You're on the waitlist for this asset.
promoted Orange "claim now!" badge A slot opened for you. You have 2 hours to claim it.
restricted Padlock icon RESTRICTED asset, and you're not on its allow list.
assigned "assigned to X" label ASSIGNED asset. You can't book unless an availability window covers your slot.
disabled Greyed, X icon DISABLED asset. Not bookable by anyone.
zone_conflict Red "in-use" badge You already have a booking on this floor for an asset in the same zone group overlapping the same time.

The computation lives in GET /api/v1/floors/:id/availability?date=YYYY-MM-DD.

Creating a booking

  1. Click an available asset.
  2. A dialog opens showing the asset name, zone, amenities and a time picker pre-filled with the selected date's working hours.
  3. Adjust the start and end time. Defaults to the working-hour block.
  4. Optionally add a note (e.g. "training session, keep chairs"). The note is visible to admins via the booking report and to the booker on My Bookings.
  5. Click Book.

On success the marker flips to mine, a booking-confirmation email goes out via the notification queue (see Email Notifications), and the entry appears under Sidebar → My Bookings.

Rules the API enforces

POST /api/v1/bookings rejects the request if any of the following apply:

Check Error code
Asset doesn't exist NOT_FOUND
Asset has isBookable = false ASSET_NOT_BOOKABLE
Asset bookingStatus = DISABLED ASSET_DISABLED
Asset bookingStatus = RESTRICTED and user is not on the allow list NOT_ON_ALLOW_LIST
Asset bookingStatus = ASSIGNED to someone else and no availability window covers the slot ASSET_ASSIGNED
The user's group access doesn't include the building or floor GROUP_ACCESS_DENIED
Another confirmed booking already overlaps this slot ASSET_CONFLICT
The user already has a booking in the same zone group at this time ZONE_GROUP_CONFLICT

SUPER_ADMINs bypass the allow-list, assignment, and group-access checks — they can still hit ASSET_CONFLICT and ZONE_GROUP_CONFLICT.

Modifying a booking

PATCH /api/v1/bookings/:id lets the owner (or a SUPER_ADMIN) change the startsAt, endsAt, or notes of a confirmed booking. The same conflict checks re-run. A booking that is already CANCELLED or COMPLETED cannot be edited.

Cancelling a booking

From My Bookings, click Cancel. From an asset's detail page (as admin or floor manager), click the booking then Cancel.

When a booking is cancelled:

  1. status flips to CANCELLED.
  2. The booker receives a cancellation email (BOOKING_CANCELLED if they cancelled themselves, BOOKING_CANCELLED_BY_ADMIN if someone with floor- manager or admin rights cancelled on their behalf).
  3. If a queue entry is waiting for an overlapping slot on the same asset, it is promoted (see below).

Floor managers can cancel any booking on their floors. SUPER_ADMINs can cancel any booking system-wide. Regular users can only cancel their own.

My Bookings

Sidebar → My Bookings. Filters:

  • Upcoming (default) — status = CONFIRMED and endsAt >= now.
  • Past — endsAt < now.
  • All — every booking ever.

The queue (waitlist)

When an asset is already booked for the time you want, the booking dialog changes from Book to Join Queue. This records a QueueEntry with:

  • The asset and the wanted time range.
  • An expiresAt — how long you're willing to wait. Past this point the entry auto-expires.
  • A position — your spot in line for that asset + overlapping slot. Lower position claims first.

Joining the queue

  1. Click a booked asset.
  2. The dialog shows the existing booker (admins) or just "currently booked" (regular users), with a Join Queue section.
  3. Pick how long you want to wait — the expiresAt. Useful values: "until the booking ends", "until end of day", "until end of week".
  4. Submit. You receive a Queue joined email and the marker flips to queued.

You can only have one active queue entry per overlapping slot per asset. The API returns ALREADY_QUEUED if you try to stack.

Promotion

When the blocking booking is cancelled (or when a PROMOTED slot ahead of you expires without being claimed):

  1. Roomer finds the lowest-position WAITING entry for the asset that overlaps the freed slot.
  2. Flips it to PROMOTED and stamps claimDeadline = now + 2h.
  3. Sends a QUEUE_PROMOTED email.

Under My Queue the entry now shows a big Claim now button and a countdown. You have exactly 2 hours.

Claiming

Click Claim on the promoted entry. Under the hood:

  1. The API re-checks that nothing has snuck in front of you (e.g., a SUPER_ADMIN bypassing the queue). If so, returns ASSET_CONFLICT — the entry is left PROMOTED and you can try again.
  2. A new Booking is created with the queued time range.
  3. The queue entry flips to CLAIMED.
  4. You get a booking-confirmation email.

After claiming, the booking behaves like any normal booking — modifiable, cancellable, visible under My Bookings.

Letting a claim lapse

If you do nothing for 2 hours after promotion, a background worker (expire-claim-deadlines, runs every 5 minutes) will:

  • Flip your entry to EXPIRED and send a QUEUE_EXPIRED email.
  • Promote the next WAITING entry for the slot, starting a fresh 2-hour clock for them.

Expiring the queue entry itself

A second worker (expire-queue-entries, runs every 15 minutes) handles expiresAt — the ceiling you set when joining. Any WAITING entry whose expiresAt is in the past is flipped to EXPIRED with a QUEUE_EXPIRED email, and removed from promotion consideration.

Leaving the queue

From My Queue, click Cancel on any WAITING or PROMOTED entry. The entry status flips to CANCELLED. The next overlapping waiter is not auto-promoted in this case — promotion only happens when a booking is cancelled or a claim lapses.

Queue entry lifecycle at a glance

WAITING ──(booking cancelled)──▶ PROMOTED ──(claim within 2h)──▶ CLAIMED
   │                                │
   │                                └──(2h passes)──▶ EXPIRED
   │
   ├──(expiresAt passes)──▶ EXPIRED
   │
   └──(user cancels)───────▶ CANCELLED

Zone-group conflicts (the "no double-booking" rule)

Roomer stops a single user from holding two overlapping bookings in the same zone group. The check is done per-booking on create/modify, and surfaces in the floor view as the zone_conflict marker status.

  • Same asset, overlapping times — always blocked (ASSET_CONFLICT).
  • Different assets, same zone group, overlapping times — blocked (ZONE_GROUP_CONFLICT).
  • Different assets, different zone groups, overlapping times — allowed.
  • Different assets in the same zone (no zone group configured) — allowed.

If you want to prevent users from booking two desks concurrently in the same zone, set a zone group on the zone. See Zones and Assets for setup.

"Book on behalf" — admin-only

There is no separate "book for someone else" UI. A SUPER_ADMIN can achieve this today by creating the booking directly against the user via POST /api/v1/bookings with the user's session — or by temporarily elevating a service account. If you need this workflow routinely, open an issue — it's a common request that isn't implemented as a first-class flow.

Working-hours and advance limits

Two env vars control the booking horizon:

Variable Default Effect
WORK_DAY_START_HOUR 9 Default start of the time-picker (24h clock).
WORK_DAY_END_HOUR 17 Default end of the time-picker.
ADVANCE_BOOKING_DAYS 14 How many days ahead the date picker allows.

Users can still book outside working hours by adjusting the picker — these are defaults, not hard bounds.

Floor availability subscriptions

Users can subscribe to a floor (and optionally specific zones within it) to receive a FLOOR_AVAILABLE email whenever a desk on that floor becomes free on a given date. This is distinct from the per-asset queue: subscriptions are floor-wide, not tied to a specific booking or time slot.

Subscribing

Open a floor plan and click Subscribe (bell icon in the floor header).

  • Floor-only — get notified whenever any desk on the floor is freed.
  • Zone-filtered — optionally pick one or more zones; notifications are only sent when a desk in one of those zones becomes free.

A 30-minute cooldown applies per subscription: if multiple desks free up in a short burst, you receive at most one email per 30 minutes. This prevents notification storms when, for example, a block booking is cancelled.

When notifications fire

FLOOR_AVAILABLE is enqueued when:

  • A booking is cancelled (user-initiated or admin-initiated) and the freed slot is on the same date as the cancellation.
  • A promoted queue entry expires without being claimed — the now-free slot fans out to floor subscribers.
  • An asset is explicitly made available by an admin (POST /api/v1/assets/:id/make-available).

The fan-out respects zone filters: if your subscription includes zones, you only get notified when the freed asset is in one of those zones. If your subscription has no zone filter, any freed desk on the floor triggers a notification.

Managing subscriptions

  • Sidebar → My Subscriptions lists all your active floor subscriptions. You can remove any of them here.
  • Deleting a floor or zone removes related subscriptions automatically (cascade delete).

API endpoints

Method & path Purpose
GET /api/v1/subscriptions List the current user's floor subscriptions.
POST /api/v1/subscriptions Subscribe to a floor ({ floorId, zoneIds? }).
DELETE /api/v1/subscriptions/:id Unsubscribe.

API summary

Method & path Purpose
GET /api/v1/floors/:id/availability?date=YYYY-MM-DD Render the floor view.
POST /api/v1/bookings Create a booking.
GET /api/v1/bookings List current user's bookings (`status=upcoming
GET /api/v1/bookings/:id Single booking (owner or admin).
PATCH /api/v1/bookings/:id Modify a confirmed booking.
DELETE /api/v1/bookings/:id Cancel a booking. Triggers queue promotion.
GET /api/v1/queue Current user's WAITING and PROMOTED entries.
POST /api/v1/queue Join the queue for an asset/slot.
DELETE /api/v1/queue/:id Leave the queue.
POST /api/v1/queue/:id/claim Claim a promoted asset (becomes a booking).
GET /api/v1/subscriptions List floor subscriptions.
POST /api/v1/subscriptions Subscribe to a floor.
DELETE /api/v1/subscriptions/:id Unsubscribe from a floor.
GET /api/v1/bookings/report Admin report (see Reports and Leases).

All endpoints enforce JWT authentication; floor availability is public to any authenticated user but filtered by group access.

Clone this wiki locally