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