Skip to content

Admin UI Guide

Multi-tenant clinic management with RBAC authentication

Version: 4.2.1 Updated: 2026-03-07


Overview

The VitaraVox Admin UI is a React 18 single-page application providing self-service clinic management, EMR integration, onboarding, clinical control, support ticketing, and system administration. It supports two distinct user roles with separate dashboards and navigation.

Stack: React 18.2 + TypeScript + Vite 5 + Tailwind CSS 3.4 + React Router 6 + Lucide Icons + shadcn/ui patterns


User Roles

Role Prefix Access Pages
Clinic Manager /clinic/* Own clinic only 12 pages
Vitara Admin /admin/* All clinics, system settings 8 pages

Authentication

JWT-Based Authentication

  • Access tokens: 1 hour expiry (HS256)
  • Refresh tokens: 7 days expiry
  • Password hashing: bcrypt with cost factor 12
  • Password max-length: 128 characters (bcrypt DoS prevention)
  • Account lockout: 5 failed attempts = 15 minute lock
  • Token storage: In-memory via React Context (useAuth hook)
  • Auto-refresh: Transparent token renewal on 401 responses

Login Flow

  1. User submits email/password to POST /api/auth/login
  2. Server validates against users table (bcrypt compare)
  3. Returns JWT access token + refresh token + user profile
  4. useAuth() React Context stores tokens and user state
  5. All API requests include Authorization: Bearer {token}
  6. Role-based redirect: vitara_admin -> /admin, clinic_manager -> /clinic
JWT LOGIN FLOW

┌──────────┐     POST /api/auth/login     ┌──────────────┐
│  Browser │ ────{email, password}────────►│   Express    │
│  (React) │                               │   Server     │
│          │◄────{accessToken,─────────────│              │
│          │      refreshToken,            │  bcrypt      │
│          │      user}                    │  compare     │
└────┬─────┘                               └──────────────┘
┌──────────────────────────────────────┐
│ useAuth() React Context              │
│  ├─ Store tokens in memory           │
│  ├─ Set Authorization header         │
│  └─ Role-based redirect:             │
│      vitara_admin  → /admin           │
│      clinic_manager → /clinic         │
└────┬─────────────────────────────────┘
     ▼ (on 401)
┌──────────────────────────────────────┐
│ Auto-refresh: POST /api/auth/refresh │
│  ├─ Send refreshToken                │
│  ├─ Receive new accessToken          │
│  └─ Retry original request           │
└──────────────────────────────────────┘

Clinic Manager Features

Dashboard (/clinic)

  • Stats cards: Total calls, appointments booked, patients registered, active waitlist
  • Recent calls: Last 5 call log entries with intent/outcome badges
  • Setup banner: "Complete your clinic setup" CTA for clinics with incomplete onboarding

Onboarding Wizard (/clinic/onboarding)

4-step guided setup for new clinics:

Step Page Fields
1. Clinic Info Name, phone, address, city, province, postal code, timezone Auto-saves on blur
2. EMR Connection EMR type selector (OSCAR / TELUS PS Suite / Accuro / Other with "coming soon" badges), OSCAR credential form (URL, username, password), "Test Connection" button with auto-pull of OSCAR config on success. Non-OSCAR EMR types display "Contact support for integration" message Live connection feedback
3. Business Hours Explanatory text ("Typical clinic hours are Mon-Fri 8 AM - 5 PM"), 7-day grid with open/close time selectors, toggle per day Visual schedule grid
4. Validation 6 parallel integration checks run on entry: EMR Connection, Provider Roster, Schedule Codes, Appointment Types, Sample Availability, Patient Search. EMR Connection and Provider Roster are required (block completion). Others are informational (shown but do not block). Expandable data panels for each check. "Complete Onboarding" button triggers admin notification via POST /api/clinic/onboarding/complete Real-time check status

Pre-launch checklist (10 checks -- 7 blocking + 3 informational):

Blocking (must pass before go-live):

  1. Clinic info complete (name, phone, address, timezone)
  2. Business hours configured (at least 1 day open)
  3. At least 1 active provider with OSCAR ID
  4. EMR connection verified
  5. Vapi phone number assigned
  6. Privacy officer designated
  7. Credentials encrypted

Informational (do not block go-live):

  1. Test call passed (recommended)
  2. Schedule data flow (adapter returns well-shaped slots)
  3. OSCAR config synced (pulled within 7 days)
CLINIC ONBOARDING FLOW (4 steps with validation gates)

┌──────────┐    ┌──────────────┐    ┌──────────────┐    ┌─────────────────┐
│ Step 1   │───►│ Step 2       │───►│ Step 3       │───►│ Step 4          │
│ Clinic   │    │ EMR          │    │ Business     │    │ Validation      │
│ Info     │    │ Connection   │    │ Hours        │    │ (6 checks)      │
│          │    │              │    │              │    │                 │
│ name     │    │ OSCAR  ✓     │    │ 7-day grid   │    │ BLOCKING:       │
│ phone    │    │ TELUS  ⏳    │    │ open/close   │    │ ✓ EMR connected │
│ address  │    │ Accuro ⏳    │    │ per day      │    │ ✓ Providers set │
│ timezone │    │ Other  ⏳    │    │              │    │                 │
│          │    │              │    │              │    │ INFORMATIONAL:  │
│ auto-save│    │ Test Conn.   │    │              │    │ ○ Schedule codes│
│ on blur  │    │ button       │    │              │    │ ○ Appt types    │
│          │    │              │    │              │    │ ○ Sample avail. │
└──────────┘    └──────────────┘    └──────────────┘    │ ○ Patient srch  │
                                                        ├─────────────────┤
                                                        │ [Complete       │
                                                        │  Onboarding]    │
                                                        │  ↓              │
                                                        │ Admin notified  │
                                                        │ → Provisioning  │
                                                        └─────────────────┘

ADMIN PROVISIONING:
┌────────────────────────────────────────┐
│ Pending Activation Card (admin dash)   │
│  ├─ Assign Vapi phone number           │
│  ├─ Link squad (vapiSquadId)           │
│  └─ [Activate Clinic] → status=active  │
└────────────────────────────────────────┘

Settings Page (/clinic/settings)

Redesigned in v4.2.1 into 4 organized cards:

Card Contents
General Clinic name, phone, address, timezone
Voice & Booking VoiceBookingConfig component -- visit mode, visit reason, new patient default, preferred time, booking buffer, max daily bookings, double booking toggle, schedule intelligence toggle
OSCAR Configuration OSCAR URL, credentials, sync status, template codes, appointment types
Privacy & Compliance Privacy officer, BAA status, retention periods

Sub-pages

Sub-page Route Features
Schedule Settings /clinic/settings/schedule Business hours grid, holiday management, advance booking limits
Provider Settings /clinic/settings/providers Provider list, EMR sync, edit details, status management
EMR Configuration /clinic/settings/emr OSCAR connection, credentials, test connection, sync status
Clinical Settings /clinic/settings/clinical Custom greeting (EN/ZH), default provider, pharmacy, appointment type mappings
OSCAR Config /clinic/settings/oscar-config Schedule codes, appointment types, config pull, sync status

Call History (/clinic/calls)

  • Paginated call log table with date range filter
  • Intent and outcome badge columns
  • Duration, language, transferred status
  • Click-through to call detail

Waitlist (/clinic/waitlist)

  • Patient waitlist table with status management
  • Bulk operations (contact, register)
  • Add new entries manually
  • Status workflow: pending -> contacted -> registered

Analytics (/clinic/analytics)

  • Call volume over time (chart)
  • Intent distribution
  • Outcome breakdown
  • Language split

Support (/clinic/support)

  • Ticket list: Status and priority filter pills, pagination
  • Create ticket: Modal with title, description, priority dropdown
  • Ticket detail: Threaded message view with reply input
  • Route: /clinic/support (list) and /clinic/support/:id (detail)

Vitara Admin Features

Dashboard (/admin)

  • System statistics: Total clinics, active clinics, total users, total calls
  • Clinics Pending Activation: Amber card listing clinics with status=pending AND onboardingProgress.completedAt set. Shows clinic name, completion date, phone status badge, and "Review" button linking to /admin/clinics/:id
  • Quick actions: Links to clinic management, support tickets
  • System health: Database, OSCAR Bridge, Vapi connectivity status

Clinic Management (/admin/clinics)

Feature Description
List Clinics Paginated table with search and status filter
Create Clinic Modal: clinic name, manager email, manager password. Transaction creates Clinic + User + ClinicConfig + OnboardingProgress + 7 ClinicHours
Clinic Detail Status, EMR config, Vapi mapping, provider list. When status=pending AND onboarding is completed, shows Admin Provisioning Checklist: Phone number assigned (yes/no), Squad linked (yes/no). "Activate Clinic" button enabled when both phone and squad are assigned. Calls PUT /api/admin/clinics/:id/activate

Support Tickets (/admin/support)

Feature Description
All tickets System-wide ticket list with status + priority filters
Ticket detail Full message thread with reply
Status controls Dropdown to change ticket status and priority
Internal notes Toggle checkbox with Lock icon. Internal messages highlighted in amber and labeled "Internal Note". Hidden from clinic managers
Assignment Assign ticket to specific admin user

Phone Number Management (/admin/phones)

Centralized management of Vapi phone numbers and their clinic assignments.

Stat Cards:

Card Description
Total Phone Numbers Count of all Vapi phone numbers
Assigned Phone numbers currently linked to a clinic
Available Phone numbers not yet assigned
Clinics Without Phone Clinics that have no Vapi phone number assigned

Clinic Assignments Table:

Column Description
Clinic Name Name of the clinic
Assigned Phone Vapi phone number linked to this clinic (or "No phone assigned")
Actions Assign (if no phone) or Unassign (if phone linked)

Vapi Phone Numbers Table:

Column Description
Phone Number E.164 formatted number
Name Display name from Vapi
Provider Telephony provider (e.g., Telnyx)
Assigned Clinic Clinic name if assigned, or "Unassigned"
Created Date the number was provisioned

Assign Modal:

  • Triggered by "Assign" button on a clinic row
  • Dropdown lists only unassigned Vapi phone numbers
  • On confirm, links the selected phone number to the clinic
  • Updates both tables immediately on success

Placeholder Pages

The following admin pages exist with placeholder content (future implementation):

  • Users (/admin/users) -- User management
  • System Settings (/admin/settings) -- Global configuration
  • Audit Logs (/admin/audit) -- Audit log viewer
  • API Keys (/admin/api-keys) -- API key management

Layout Structure

+--------------------------------------------------------------+
| Header                                                        |
|  [Hamburger]           [NotificationBell] [Role Label] [Out] |
+----------+---------------------------------------------------+
| Sidebar  |  Page Content (<Outlet />)                         |
|          |                                                    |
| Logo     |  Renders the active route's page component         |
| Nav:     |                                                    |
| Dashboard|                                                    |
| Settings>|                                                    |
|  Schedule|                                                    |
|  Provider|                                                    |
|  EMR     |                                                    |
|  Clinical|                                                    |
|  OSCAR   |                                                    |
| Calls    |                                                    |
| Waitlist |                                                    |
| Analytics|                                                    |
| Support  |                                                    |
|          |                                                    |
| [User]   |                                                    |
+----------+---------------------------------------------------+
  • Sidebar: Collapsible on mobile (hamburger toggle), always visible on lg: breakpoint
  • Settings submenu: Expandable chevron with 5 children (Schedule, Providers, EMR, Clinical, OSCAR Config)
  • Notification bell: Unread count badge (red), dropdown with notifications, 30-second polling, mark all read
  • Responsive: Mobile-friendly with sm: / md: / lg: breakpoints throughout

Notification System

The notification bell in the header provides real-time awareness of support ticket activity.

Feature Implementation
Bell icon Lucide Bell, always visible in header
Unread badge Red circle, shows count (capped at "9+")
Dropdown Click to open, shows recent notifications with relative timestamps
Click-through Each notification links to relevant page (e.g., ticket detail)
Mark all read Button at top of dropdown
Polling Fetches GET /api/notifications every 30 seconds

Auto-creation triggers:

Event Who Gets Notified
Clinic creates ticket All admin users
Admin replies to ticket Ticket creator
Clinic replies to ticket Assigned admin (or all admins)
Status change Ticket creator

API Integration

Client API Service

The api.ts service provides a typed ApiClient class with 50+ methods:

// Authentication
api.login(email, password)
api.logout()
api.refreshToken()
api.getMe()

// Clinic Manager
api.getClinicMe()
api.getClinicStats()
api.updateClinicalSettings(data)
api.getProviders()
api.syncProvidersFromEmr()
api.getWaitlist(page, pageSize)
api.getCallHistory(page, pageSize, filters)
api.getOnboardingProgress()
api.updateOnboardingStep(step, data)
api.validatePreLaunch()
api.goLive()

// Support
api.createTicket(data)
api.getTickets(params)
api.getTicket(id)
api.addTicketMessage(ticketId, data)

// Admin
api.getAdminTickets(params)
api.getAdminTicket(id)
api.updateAdminTicket(id, data)
api.addAdminTicketMessage(ticketId, data)

// Vapi Phone Management
api.getVapiPhoneNumbers()
api.assignPhoneToClinic(clinicId, phoneData)
api.unassignPhoneFromClinic(clinicId)

// Onboarding validation
api.testScheduleSlots()
api.testPatientSearch()
api.completeOnboarding()
api.getOscarConfig()
api.pullOscarConfig()

// Admin provisioning
api.getProvisioningStatus(clinicId)
api.activateClinic(clinicId)
api.getPendingActivationClinics()

// Notifications
api.getNotifications()
api.markNotificationRead(id)
api.markAllNotificationsRead()

All methods automatically:

  • Include Authorization: Bearer {token} header
  • Parse JSON responses
  • Throw on non-2xx status codes

Auth Context

React Context-based auth provider (contexts/AuthContext.tsx) providing:

  • user -- Current user profile
  • isAuthenticated -- Login state
  • isLoading -- Initial auth check
  • login(email, password) -- Authenticate and store tokens
  • logout() -- Clear tokens and redirect
  • Auto-refresh on app mount

Environment Variables

Variable Default Description
JWT_SECRET (required) Access token signing key
JWT_REFRESH_SECRET (required) Refresh token signing key
ENCRYPTION_KEY (required in prod) AES-256-GCM for credential encryption
DATABASE_URL (required) PostgreSQL connection string
PORT 3002 Server port

Fax Intelligence (/clinic/fax)

AI-powered fax document processing and patient matching. Added in v4.1.0.

Controls Bar

Control Action
Polling toggle Start/stop automated inbox polling (green dot = running)
Upload Fax Manual PDF/image upload via file dialog
Seed Fax (Demo) Inject a fixture fax into mock inbox
Reset Inbox (Demo) Reload all fixtures as unread

Detail Panel

When a fax is selected, shows:

  • Extraction results: Document type, urgency, patient info (name, PHN, DOB with confidence scores), sender/recipient, clinical summary, flags
  • OSCAR match card: Matched patient name + demographic ID, confidence percentage, match method (PHN exact, name+DOB, name-only)
  • Actions: Confirm & File, Wrong Patient (manual correction), Skip

History Table

Paginated table of all fax documents with columns:

Column Description
Received Timestamp
Source upload or mock-srfax
Type AI-detected document type
Patient Extracted patient name
Confidence Match confidence bar (0-100%)
Status Badge: Pending (gray), Processing (amber), Matched (blue), Verified (green), Error (red)
Actions View detail, verify match

Auto-refreshes every 5 seconds when poller is running.

AI Providers

Configured via FAX_AI_PROVIDER environment variable:

Provider Model Default
anthropic Claude (configurable via FAX_ANTHROPIC_MODEL, default: claude-sonnet-4-6) Yes
openai GPT-4o-mini No
google Gemini 2.0 Flash No

File Processing

Input Format Processing Temp Files
PDF pdftoppm -png -r 200 → PNG pages Yes (deleted after extraction)
TIFF/TIF sharp multi-page → PNG pages Yes (deleted after extraction)
PNG/JPG/JPEG Direct to AI vision No (original preserved)

Max 5 pages sent to AI. Dynamic MIME type detection for all image formats.


Security Features

Feature Implementation
Password hashing bcrypt with cost factor 12
JWT tokens HS256 algorithm, short-lived (1h access, 7d refresh)
Account lockout DB fields exist (failedLoginAttempts, lockedUntil) — middleware enforcement pending
Audit logging All POST/PUT/DELETE auto-logged via middleware
RBAC requireRole() and requireClinicAccess middleware
Input validation 23 Zod schemas (8 .strict() + 15 .strip()) on all endpoints
Credential encryption AES-256-GCM for OSCAR secrets
Rate limiting 5/min auth, 100/min API, 300/min webhooks
Security headers Helmet (CSP, HSTS, X-Frame-Options)
Internal notes isInternal enforced server-side (not client trust)
Data retention Automated transcript/log purge (configurable per clinic)