Receptionist Portal — Appointment and Waiting Room Management
Audiences: clinical-buyer, developer
Front-desk workspace: today's dashboard with appointment + waiting-room stats, appointment creation/rescheduling (including recurring), patient check-in, calendar, patient registration, communications, and document handling — gated to
RECEPTIONIST_ONLY.
Business Purpose
The receptionist is the orchestrator of the clinic day. Every appointment moves through their hands: booking, rescheduling, the call when someone runs late, the check-in when the patient arrives, the handoff to the waiting room, and the billing intake once the visit ends. The receptionist portal collapses what used to be a phone + paper + sticky note workflow into one surface that drives the same state machine the nurse and doctor see.
For the clinic, it is the throughput lever — a receptionist who can check in five patients per minute saves the practice an FTE. For the patient, it's the difference between waiting 20 minutes at the desk and walking straight into a triage room. For the doctor, it's confidence that the schedule reflects reality.
Audiences
- Clinical buyer (clinic admin): shows the front-desk productivity surface — single pane for appointments + check-in + waiting room + recurring scheduling + patient registration.
- Doctor + nurse: confidence that the visible schedule mirrors what the desk is actually doing (same source of truth, real-time SSE).
- Developer/partner: explains the receptionist routes, the
RECEPTIONIST_ONLYgate, theBackendStatusProvider(children scope), and the Trusted Gateway data pattern used by the dashboard and check-in pages. - Internal (ops/support): where to look when "today's count is wrong" or "check-in says 0 patients" reports come in.
Architecture
Routes
portal/receptionist/ ├── layout.tsx → createRoleLayout(RECEPTIONIST_CONFIG, ReceptionistNav) ├── page.tsx → root entry ├── dashboard/ → today's appointments + waiting-room stats ├── appointments/ → list, new, by id │ ├── new/ → create appointment form │ └── [id]/ → appointment detail ├── recurring-appointments/ → repeating schedule editor ├── calendar/ → calendar grid view ├── check-in/ → today's SCHEDULED/CONFIRMED/ARRIVED list ├── waiting-room/ → waiting-room triage (read-only view; nurse owns transitions) ├── patients/ → roster + registration ├── communications/, messages/, → outbound + inbox │ messenger/ ├── notes/ → desk notes ├── documents/ → upload/download surface ├── forms/ → fillable forms inbox ├── ai/ → AI tooling entry ├── flowagent/ → conversational flows ├── vivoxx/ → voice (LiveKit) surface ├── settings/ → preferences ├── profile/ → identity └── help/ → help center
Layout composition
ddx-web/src/app/[locale]/portal/receptionist/layout.tsx:15-19:
function ReceptionistNav() { return <PortalNavigation roleKey="receptionist" />; }
export default createRoleLayout(RECEPTIONIST_CONFIG, ReceptionistNav);
RECEPTIONIST_CONFIG (ddx-web/src/lib/portal/role-configs.ts:134-146):
authRequirement: 'RECEPTIONIST_ONLY', roleBadge.icon: ClipboardTextIcon,
variant: 'info', brandNameKey: 'portals.receptionist',
needsBackendStatus: true, backendStatusScope: 'children'.
The children scope is distinctive: the BackendStatusProvider wraps children
inside the shell, not the entire shell (which doctor uses). Practical effect: a
degraded backend shows the alert within the page area, not in the header banner.
Wired at ddx-web/src/components/portal/createRoleLayout.tsx:95-98 (when
backendStatusScope === 'children', innerChildren is wrapped via
wrapBackendStatus).
Trusted Gateway data fetches
Receptionist pages follow the BFF (Trusted Gateway) pattern strictly — every backend
call is a server-side backendGet / backendFetch from @/lib/api/backend-client,
never a browser fetch. The dashboard at
ddx-web/src/app/[locale]/portal/receptionist/dashboard/page.tsx:37-58 is the canonical
example:
const [dashRes, statsRes] = await Promise.all([
backendGet<DayDashboardResponse>(`appointments/dashboard/day?date=${today}`).catch(() => null),
backendGet<WaitingRoomStatsResponse>('waiting-room/stats').catch(() => null),
]);
The check-in page (check-in/page.tsx, 88 lines) follows the same pattern for today's
SCHEDULED/CONFIRMED/ARRIVED appointments.
Per-page role gate
Unlike the nurse portal, receptionist pages are consistent about per-page
requireRoleAccess('RECEPTIONIST_ONLY', locale) in addition to the layout gate
(e.g. dashboard/page.tsx:39, check-in/page.tsx). This defense-in-depth pattern
catches any future layout misconfig at the page level.
Tech Stack & Choices
| Layer | Choice | Why |
|---|---|---|
| Layout factory | createRoleLayout(RECEPTIONIST_CONFIG, ReceptionistNav) | Symmetric eleven-portal shell. |
| Navigation | PortalNavigation roleKey="receptionist" | Shared component; reception-specific nav config in lib/portal/nav-configs. |
| Role gate | RECEPTIONIST_ONLY at layout and at each page | Defense-in-depth; catches layout regressions. |
| Backend status | needsBackendStatus: true, scope: 'children' | A degraded backend should show inline, not in the header — receptionist surfaces are heavy on live data. |
| Brand icon | ClipboardTextIcon (info variant) | Front-desk clipboard semantic; distinct from clinical icons. |
| Data fetching | backendGet / backendFetch server-side | Trusted Gateway Pattern (BFF only); never a browser fetch. |
| Date formatting | date-fns format(today, 'yyyy-MM-dd') | Predictable query params for the dashboard endpoint. |
| i18n namespace | receptionist (+ sub-namespaces receptionist.pageTitles.*) | One namespace per role. |
Rejected: client-side polling for live counts (would multiply backend load and break under degraded mode), shared dashboard with the doctor (different data shapes; shared component would be over-coupled).
Data Flow
- Receptionist logs in → JWT carries
role: RECEPTIONIST. - Browser hits
/<locale>/portal/receptionist/dashboard. proxy.ts:248-319— locale + session gate.portal/receptionist/layout.tsx:19runscreateRoleLayout(RECEPTIONIST_CONFIG, ReceptionistNav):requireRoleAccess('RECEPTIONIST_ONLY', locale)returns the user, organization is fetched, the shell mounts with the children-scopedBackendStatusProviderwrapping the page area.dashboard/page.tsx:37-58callsrequireRoleAccess('RECEPTIONIST_ONLY', locale)again (page-level defense), then runs the twobackendGetcalls in parallel (Promise.allforappointments/dashboard/dayandwaiting-room/stats), tolerating failure with.catch(() => null). The result is shaped into aDashboardStatsobject and handed to<ReceptionistDashboardClient locale userName stats>.- Check-in page fetches today's
SCHEDULED/CONFIRMED/ARRIVEDappointments server-side, hands them to<ReceptionistCheckInClient>for click-to-arrive UX. - Once a patient is marked arrived, the appointment moves to status
ARRIVED; the nurse portal's waiting-room view picks it up via SSE.
Implicated Code
ddx-web/src/app/[locale]/portal/receptionist/layout.tsx:15-19—ReceptionistNav+createRoleLayout.ddx-web/src/lib/portal/role-configs.ts:134-146—RECEPTIONIST_CONFIG(needsBackendStatus,backendStatusScope: 'children').ddx-web/src/components/portal/createRoleLayout.tsx:95-98—children-scopeBackendStatusProviderwrapping logic.ddx-web/src/app/[locale]/portal/receptionist/dashboard/page.tsx:37-58— Trusted GatewayPromise.alldata fetch + client delegation.ddx-web/src/app/[locale]/portal/receptionist/check-in/page.tsx:1-40— check-in Server Component,CheckInAppointmentDTO mirrors the backend enrichment shape.ddx-web/src/lib/api/rbac-config.ts:37—RECEPTIONIST: RECEPTIONIST_PERMISSIONSregistration.ddx-web/src/app/[locale]/portal/receptionist/calendar/page.tsx(89 lines) — calendar grid Server Component.
Operational Notes
backendStatusScope: 'children'is intentional — the alert renders inline in the page area, not in the header. Switching to'shell'would change UX and is a reviewer-flag for the design-system owner..catch(() => null)on dashboard fetches — bothappointments/dashboard/dayandwaiting-room/statstolerate failure so the dashboard renders zeros instead of a crash. If the receptionist reports "always shows 0", check the network response in the BFF logs — the page hides backend errors by design.- Page-level role gate is the standard — receptionist pages call
requireRoleAccess('RECEPTIONIST_ONLY', locale)explicitly. Keep this when adding new pages. - Status transitions — receptionist marks
ARRIVED; nurse advances through the waiting-room state machine. The receptionist surface should be read-mostly for post-arrival states. Cross-role write would break the handoff invariant. - Date queries — always
format(date, 'yyyy-MM-dd')fromdate-fns(no rawtoISOString().split('T')[0]— timezone surprises). The dashboard pins toEurope/Berlinvia the globalnext-intlconfig (ddx-web/src/lib/i18n/config.ts:157). - Recurring appointments —
recurring-appointments/is a distinct route fromappointments/. Editing recurrence rules from the single-appointment route is not supported; deep-link from the recurring view instead.
Related Topics
- Multi-Portal Architecture — shell + RBAC + BFF rules.
- i18n — Multi-Lingual Interface —
receptionist.*namespace. - Doctor Portal — downstream consumer of scheduled appointments.
- Nurse Portal — owns the waiting-room state machine the receptionist hands off into.
- Patient Portal — patient-side mirror of the same appointment state.