MediRec docs Index Appointment lifecycle Gap analysis Migration plan Clinic questions

Operating manual

Appointment lifecycle expected flow

How a clinic appointment travels from creation to closure: what each role does, where it happens, and which component owns it. Written for receptionists, nurses, providers and admins — and as the integration map for engineers extending the flow.

The map

A single Appointment row whose status advances through a state machine. Three ways in, one path through the clinic.

Three entry points

Internal scheduling receptionist or provider SCHEDULED default on save Public request form returning patient, unauthenticated REQUESTED may be unmatched APPROVED doctor assigned SCHEDULED Walk-in reception board CTA CHECKING_IN skips approval all three converge on the in-clinic flow below

Internal scheduling and walk-ins skip approval entirely. Only public requests pass through the queue.

The in-clinic flow, by role

  • Reception
  • Nurse
  • Provider
  • Terminal
Reception Nurse Provider Terminal SCHEDULED or CONFIRMED CHECKING_IN READY_FOR _CHECKOUT COMPLETED READY_FOR _MEDICAL_EXAM IN_MEDICAL _EXAM READY_FOR _APPOINTMENT IN_APPOINTMENT NO_SHOW MISSED CANCELLED skip the exam · NUTRITION and LAB visits only nurse takes vitals provider sees patient back to reception

Lane changes are hand-offs. The patient physically moves between desks at each one, which is why the flow board is split into three role-specific views rather than one shared board.

Auto-advance from the exam form

When a nurse saves a fresh medical exam report, the patient's active appointment auto-walks READY_FOR_MEDICAL_EXAM → IN_MEDICAL_EXAM → READY_FOR_APPOINTMENT so the board matches reality without a second click. Editing an existing report does not retrigger it.

Off-path states

Drawn separately because they are reachable from nearly everywhere — wiring every edge would make the map unreadable.

CANCEL

CANCELLED

Terminal

Reachable from any non-terminal state. The cancel-with-reason dialog lives on the calendar day flyout; the flow board cancels through the state machine without capturing a reason.

MISS

MISSED

Terminal

Set explicitly during the day on a patient who checked in but left before being seen. Reachable from any state before IN_APPOINTMENT — deliberately not offered once the patient is in the room.

NOSHOW

NO_SHOW

Terminal

Decided after the fact on an appointment that never arrived. Set from the calendar, not the flow board. Distinct from MISSED: the patient never checked in at all.

1 · Creation

Three routes in, each with different gating.

1a · Internal scheduling Reception

Page/appointments/create
ComponentAppointmentForm
RolesAdmin, receptionist, and all provider roles
Pre-conditionsThe matched patient exists; the chosen provider has working hours configured, or the slot list comes back empty
ResultStatus defaults to SCHEDULED; create hook writes an audit row

Slots come from AppointmentSlotService::availableSlots, which respects working hours, lunch breaks, time off, and existing non-cancelled appointments. The doctor dropdown is filtered by AppointmentType::eligibleProviderRoles().

1b · Public returning-patient request

Page/appointments/request — public, outside the auth middleware group
ComponentVolt appointments.public-request
RequiredExpediente ID or DUI, plus date of birth, visit type and slot
ResultMatched → patient_id set, status REQUESTED. Unmatched → request_payload JSON retained, patient_id null, resolved by reception

The next-day-noon lock is enforced twice — in slot generation and again as a server-side guard on submit — so a hand-crafted POST cannot bypass it.

1c · Walk-in Reception

Reception board CTA, gated to receptionist and admin only. starts_at rounds up to the next 5-minute boundary, duration defaults to 30 minutes, and the appointment persists directly as CHECKING_IN with notes = '[walk-in]'.

Slot collision is soft-checked. A conflict surfaces a warning listing the clashing appointments, but does not block the save — the receptionist owns the call. This is deliberate; don't harden it into a block.

2 · Approval

Only public requests pass through here. Internal bookings and walk-ins skip it entirely.

Reception resolves unmatched patients by linking the row to a real patient record, then either approves — assigning a doctor filtered to those who can host the visit type and have the slot free — or cancels with a reason. Approved appointments appear on the calendar and in the Reception board's "Expected today" lane on the day they fall.

Every action method re-checks canApproveAppointments() server-side, not just on mount.

3 · Pre-arrival — calendar housekeeping

Anyone with canViewAppointments(). Providers default to their own; admin and reception see all.

Staff use the calendar to see what's scheduled, reschedule through the day flyout, or move statuses before arrival: SCHEDULED → CONFIRMED, and CONFIRMED → COMPLETED | CANCELLED | NO_SHOW. Cancellation captures a free-text reason. Every transition passes through AppointmentStatus::canTransitionTo() and is audit-logged.

4 · Day of visit — the Patient Flow board

The core operational surface. One URL — /appointments/flow — resolves to the right board for the current user's role.

The anchor date defaults to today and persists via ?anchor=YYYY-MM-DD. Every status change stamps stage_entered_at, which drives the "in stage X minutes" display on each card.

Reception and admin

LaneHoldsActions available
Expected todaySCHEDULED, CONFIRMEDCheck in · Mark as missed
Checking inCHECKING_INSend to nurse · Send to doctor (skip-exam types) · Mark as missed
In visitAll four mid-visit statesNone — informational only
Ready for checkoutREADY_FOR_CHECKOUTCheck out → COMPLETED

Pharmacists land on this same board in read-only mode — same view, no CTA, no action buttons.

Nurse

Two lanes: READY_FOR_MEDICAL_EXAM and IN_MEDICAL_EXAM. Skip-exam visit types are filtered out of the query entirely, so they never appear here.

Nurses deliberately do not have canManageAppointments() — they cannot create or edit appointment records — but canAdvanceVisitStages() authorises them to move stages on this board. That split is the whole reason the helper exists.

Provider

Two lanes: READY_FOR_APPOINTMENT and IN_APPOINTMENT. The query is hard-filtered to doctor_id = Auth::id(), and a defence-in-depth canActOn() check rejects any transition against another provider's row.

The provider's actual work happens off this board. Consultations, prescriptions and exam reports are created from the patient's profile. The flow board is the sequencing layer, not the documentation layer.

5 · Closure and follow-up

  • ClosureReception checks out the READY_FOR_CHECKOUT row. Status becomes COMPLETED and the card leaves the board on next refresh.
  • Audit trailEvery status change writes an audit row with the change diff, acting user, IP and user agent.
  • Follow-up careLives in consultation and exam-report records linked to the patient, not to the appointment. The appointment record ends at COMPLETED.
  • RemindersThe reminder_email_sent_at and reminder_whatsapp_sent_at columns exist but nothing writes them — no notification, mailable or job references either one. Schema is ready; the feature is not built.

Page reference

Quick index of every surface in the lifecycle.

PageRoutePrimary roles
Appointment list/appointmentsAll canViewAppointments
Calendar/appointments/calendarAll canViewAppointments
Patient Flow board/appointments/flowRole-resolved — reception / nurse / provider
New / edit appointment/appointments/create, /appointments/{id}/editcanManageAppointments
Provider schedule editor/users/{user}/schedulecanManageOwnSchedule (own) or canManageAllSchedules (any)
All-provider schedules/appointments/schedulescanManageAllSchedules
Public request form/appointments/requestPublic
Approval queue/appointments/approval-queuecanApproveAppointments

Permission matrix

Which roles hold which capability. Derived from App\Enums\UserRole — the helpers, never role strings.

Capability Admin Recep. Doctor Pediat. Gynae. Physio Gen. cons. Nutri. Nurse Pharm. Lab tech
canViewAppointments()
canManageAppointments()
canApproveAppointments()
canAdvanceVisitStages()
canManageOwnSchedule()
canManageAllSchedules()

The one asymmetry worth remembering: nurses can advance visit stages but cannot create or edit appointment records. That is what canAdvanceVisitStages() exists to express.

Notes for engineers

01

The state machine is the source of truth

Every UI button checks canTransitionTo() before rendering, and every action handler re-checks it server-side. To add a stage: extend the enum, then the color() and label() matches, the canTransitionTo arm, and the relevant board's nextActionsFor() map.

02

Keep the skip-exam policy out of the enum

The CHECKING_IN → READY_FOR_APPOINTMENT shortcut is allowed unconditionally by the state machine; the per-call guard in FlowBoard::advanceTo enforces requiresMedicalExam(). That's a visit-type concern, not a state-machine one.

03

The stage timer only tracks status

stage_entered_at is bumped only when status is dirty. A notes-only edit doesn't reset it — intentional, and covered by a test. Don't switch the trigger to updated_at.

04

Livewire reserves transition()

It's taken on Component for view-transition support. The state-machine method is called advanceTo for that reason — keep any new one similarly renamed.

05

Bilingual keys are mandatory

Lane labels, transition buttons, status labels and empty states all live under appointments.flow.* and appointments.status.* in both resources/lang/en/main.php and resources/lang/es/main.php. Add to both in the same change.