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 (
useAuthhook) - Auto-refresh: Transparent token renewal on 401 responses
Login Flow¶
- User submits email/password to
POST /api/auth/login - Server validates against
userstable (bcrypt compare) - Returns JWT access token + refresh token + user profile
useAuth()React Context stores tokens and user state- All API requests include
Authorization: Bearer {token} - 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):
- Clinic info complete (name, phone, address, timezone)
- Business hours configured (at least 1 day open)
- At least 1 active provider with OSCAR ID
- EMR connection verified
- Vapi phone number assigned
- Privacy officer designated
- Credentials encrypted
Informational (do not block go-live):
- Test call passed (recommended)
- Schedule data flow (adapter returns well-shaped slots)
- 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=pendingANDonboardingProgress.completedAtset. 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 profileisAuthenticated-- Login stateisLoading-- Initial auth checklogin(email, password)-- Authenticate and store tokenslogout()-- 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 |
|---|---|---|
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) |