Skip to contentVibraUI
Dashboards

Clinic desk actions

The clinic desk's two server actions — check a patient in and cancel a visit — shared by every Clinic page that changes a visit.

A "use server" module the Clinic overview and the Appointments page both import. Each action looks the visit up by id and trusts only the stored row: only a visit that is still booked can be cancelled, and only one still booked for REFERENCE_DATE's own day can be checked in — a visit on a later day is "not due until" it; one under way, done or already cancelled is refused with a sentence that names the patient. What comes back is the visit trimmed to what a page prints — no email — because a server action's return value travels to the browser; a refusal carries the visit as the store holds it too.

Install

npx shadcn@latest add @vibra/actions-clinic

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

Props

PropTypeDefaultDescription
checkInAppointment(id: string) => Promise<VisitAnswer>—Marks a visit booked for today as arrived; refuses another day, and any other status.
cancelAppointment(id: string) => Promise<VisitAnswer>—Takes a booked visit out of the book; refuses one under way, done or already cancelled.
VisitChange{ id, patient, doctor, status }—The visit after the action.
VisitAnswer{ ok: true, data: VisitChange } | { ok: false, error, current?: VisitChange }—A Result whose refusal also carries the visit as it stands — absent only when no appointment has the id.

Dependencies

Source

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

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

/**
 * The clinic desk's two verbs, shared by every Clinic page that changes a
 * visit: the overview's upcoming book and the appointments table both call
 * these, so a check-in means the same thing wherever it is pressed.
 *
 * Each action looks the visit up by id and trusts only that row. What comes
 * back is the visit trimmed to what a page prints — a server action's return
 * value is serialised to the browser, so it carries no email and nothing the
 * page did not ask for. A refusal carries the visit 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 visit after a desk action: enough to name the patient and the new state. */
export type VisitChange = {
  id: string
  patient: string
  doctor: string
  status: Appointment["status"]
}

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

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

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

const change = ({ id, patient, doctor, status }: Appointment): VisitChange => ({ id, patient, doctor, status })

const refuse = (appointment: Appointment, message: string): VisitAnswer => ({
  ok: false,
  error: { code: "conflict", message },
  current: change(appointment),
})

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

async function move(appointment: Appointment, status: Appointment["status"]): Promise<VisitAnswer> {
  const updated = await db.appointments.update(appointment.id, { status })
  if (!updated.ok) return updated
  return { ok: true, data: change(updated.data) }
}

/**
 * Marks a patient as arrived. Only a visit still booked, on today's own day,
 * can be checked in: a patient is not seen a week early, and a visit whose
 * day has gone by is not an arrival.
 */
export async function checkInAppointment(id: string): Promise<VisitAnswer> {
  const appointment = await db.appointments.get(id)
  if (!appointment) return missing(id)

  if (appointment.status !== "scheduled") {
    const state = {
      checked_in: "already checked in",
      done: "already done",
      cancelled: "cancelled",
    }[appointment.status]
    return refuse(appointment, `${appointment.patient}'s visit is ${state}.`)
  }
  const day = utcDay(appointment.at)
  const today = utcDay(REFERENCE_DATE)
  if (day > today) return refuse(appointment, `${appointment.patient}'s visit is not due until ${DUE.format(appointment.at)}.`)
  if (day < today) return refuse(appointment, `${appointment.patient}'s visit was due on ${DUE.format(appointment.at)}.`)

  return move(appointment, "checked_in")
}

/**
 * Takes a visit out of the book. Only a visit that is still booked can be
 * cancelled: a patient who has arrived is being seen, and a visit that is
 * done is history.
 */
export async function cancelAppointment(id: string): Promise<VisitAnswer> {
  const appointment = await db.appointments.get(id)
  if (!appointment) return missing(id)

  switch (appointment.status) {
    case "checked_in":
      return refuse(appointment, `${appointment.patient} is already checked in — the visit is under way.`)
    case "done":
      return refuse(appointment, `${appointment.patient}'s visit is already done.`)
    case "cancelled":
      return refuse(appointment, `${appointment.patient}'s visit is already cancelled.`)
  }

  return move(appointment, "cancelled")
}