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