Skip to contentVibraUI
Dashboards

Hotel desk actions

The front desk's three server actions — check in, check out and cancel — shared by every Hotel page that changes a stay.

A "use server" module the Front desk, Bookings and Rooms pages all import, so a check-in means the same thing wherever it is pressed. Each action looks the booking up by id and trusts only the stored row: a check-in wants a confirmed booking whose first night has come and whose nights have not all gone by (one that ended without the guest is a no-show, and is refused as one), a check-out a guest in the house, a cancellation a booking not yet arrived — anything else is refused with a sentence that names the guest and the reason. What comes back is the stay trimmed to what a page prints, never the whole row, because a server action's return value travels to the browser; a refusal carries the stay as the store holds it too, so the page that asked can show it rather than the state that was refused.

Install

npx shadcn@latest add @vibra/actions-hotel

Needs the @vibra registry in your components.json — set it up once.

Props

PropTypeDefaultDescription
checkIn(id: string) => Promise<StayAnswer>—Puts a guest in their room; refuses a booking that is not confirmed, not due until a later day, or whose nights have all gone by.
checkOut(id: string) => Promise<StayAnswer>—Settles an in-house stay; refuses a guest who is not in the house.
cancelBooking(id: string) => Promise<StayAnswer>—Gives the room back; refuses a stay that is under way, over, or already cancelled.
StayChange{ id, reference, guestName, room, status }—The stay after the action — the door number, not the room id, so a status line can name it.
StayAnswer{ ok: true, data: StayChange } | { ok: false, error, current?: StayChange }—A Result whose refusal also carries the stay as it stands — absent only when no booking has the id.

Dependencies

Source

lib/dashboards/hotel/actions.ts
"use server"

import { REFERENCE_DATE, ROOMS, db, type Booking } from "@/lib/sample-data"

/**
 * The front desk's three verbs, shared by every Hotel page that changes a
 * stay: the overview's guest lists, the bookings table and the room rack all
 * call these, so a check-in means the same thing wherever it is pressed.
 *
 * Each action looks the booking up by id and trusts only that row: whether a
 * guest can be checked in is a question about the stored status and the
 * stored dates, never about what the page that asked believed. What comes
 * back is the stay trimmed to what a page prints — a server action's return
 * value is serialised to the browser, so it carries no rates, no contact and
 * nothing the page did not ask for. A refusal carries the stay as the store
 * now holds it as well, so the page that asked can stop showing the state
 * the desk just found out was wrong.
 */

/** A stay after a desk action: enough to name the guest, the room and the new state. */
export type StayChange = {
  id: string
  reference: string
  guestName: string
  /** The number on the door. */
  room: string
  status: Booking["status"]
}

/** What a desk action answers: the stay it moved, or why it would not — with the stay as it stands, when there is one. */
export type StayAnswer =
  | { ok: true; data: StayChange }
  | { ok: false; error: { code: string; message: string }; current?: StayChange }

const DOOR = new Map(ROOMS.map((room) => [room.id, room.number]))

const DUE = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" })

/** Midnight UTC on the day `date` falls on — a night is a calendar day. */
const utcDay = (date: Date) => Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())

const change = (booking: Booking): StayChange => ({
  id: booking.id,
  reference: booking.reference,
  guestName: booking.guestName,
  room: DOOR.get(booking.roomId) ?? "—",
  status: booking.status,
})

const refuse = (booking: Booking, message: string): StayAnswer => ({
  ok: false,
  error: { code: "conflict", message },
  current: change(booking),
})

const missing = (id: string): StayAnswer => ({
  ok: false,
  error: { code: "not_found", message: `No booking with id "${id}".` },
})

async function move(booking: Booking, status: Booking["status"]): Promise<StayAnswer> {
  const updated = await db.bookings.update(booking.id, { status })
  if (!updated.ok) return updated
  return { ok: true, data: change(updated.data) }
}

/**
 * Puts a guest in their room. Only a confirmed booking whose stay is under
 * way today can be checked in: a guest is not handed a key a week early, and a
 * stay whose nights have all gone by without them is a no-show, not an arrival.
 */
export async function checkIn(id: string): Promise<StayAnswer> {
  const booking = await db.bookings.get(id)
  if (!booking) return missing(id)

  switch (booking.status) {
    case "checked_in":
      return refuse(booking, `${booking.guestName} is already checked in.`)
    case "checked_out":
      return refuse(booking, `${booking.guestName} has already checked out of room ${DOOR.get(booking.roomId) ?? "—"}.`)
    case "cancelled":
      return refuse(booking, `${booking.reference} was cancelled — there is no stay to check in.`)
    case "no_show":
      return refuse(booking, `${booking.reference} was marked a no-show.`)
  }
  const today = utcDay(REFERENCE_DATE)
  if (utcDay(booking.checkIn) > today) {
    return refuse(booking, `${booking.guestName} is not due until ${DUE.format(booking.checkIn)}.`)
  }
  if (utcDay(booking.checkOut) <= today) {
    return refuse(booking, `${booking.reference} ended on ${DUE.format(booking.checkOut)} — mark it a no-show.`)
  }

  return move(booking, "checked_in")
}

/** Settles a stay. Only a guest who is in the house can leave it. */
export async function checkOut(id: string): Promise<StayAnswer> {
  const booking = await db.bookings.get(id)
  if (!booking) return missing(id)

  if (booking.status !== "checked_in") {
    const state = {
      confirmed: "still due in",
      checked_out: "already checked out",
      cancelled: "cancelled",
      no_show: "a no-show",
    }[booking.status]
    return refuse(booking, `${booking.guestName} is not in the house — ${booking.reference} is ${state}.`)
  }

  return move(booking, "checked_out")
}

/**
 * Gives the room back. Only a booking still to be arrived can be cancelled: a
 * guest in the house is checked out, and a stay that is over is history.
 */
export async function cancelBooking(id: string): Promise<StayAnswer> {
  const booking = await db.bookings.get(id)
  if (!booking) return missing(id)

  switch (booking.status) {
    case "checked_in":
      return refuse(booking, `${booking.guestName} is in the house — check them out rather than cancel.`)
    case "checked_out":
      return refuse(booking, `${booking.guestName} has already stayed; ${booking.reference} cannot be cancelled.`)
    case "cancelled":
      return refuse(booking, `${booking.reference} is already cancelled.`)
    case "no_show":
      return refuse(booking, `${booking.reference} was marked a no-show.`)
  }

  return move(booking, "cancelled")
}