Skip to content

Mobile Doctor Dashboard

Overview

The mobile doctor home was redesigned in May 2026 around three ideas: see every patient at a glance, prep for the patients you're seeing today, and drill into a single patient's signals without leaving the dashboard. The new dashboard surfaces compliance grades on every patient row, lets the therapist mark patients as "seeing today," exposes a dedicated patient detail screen with a Quick Analysis view, and adds a one-tap re-onboarding flow for patients who never finished signing up.

Compliance grades and per-patient activity stats come from the same backend service that powers the admin Daily Review tool — see internal/daily-review.md for the grading rubric and payload shape.

Dashboard Layout

components/doctor/DoctorDashboard.tsx is one list under two pills, Today's patients and Patients (the first pill and everything about today's list follow the todaysPatients tenant flag, on unless the clinic set it to false; with it off the screen is the directory alone, without pills, and the Scheduling cards move above it; app builds from before 2026-10-29 ignore the flag):

  1. Today's patients — patients the therapist has added to today's caseload via useInClinicPatients(). Each row shows a small indicator dot and a dismiss button. While someone is on the list, a search field above it filters today's patients as you type (since 2026-10-06); when nobody matches, the list says so ("Nobody on today's list matches…") and offers Search all patients, which opens the directory search with the same words. Switching pills clears it.
  2. Patients — the directory: the therapist's own patients or, with the allActivePatientsDashboard flag, a scope chooser at the top of the view that switches between My patients and All clinic patients, with the count beside it ({n} patients).

The header also holds a search icon that opens PatientSearchModal (see Patient search), an "Add patient seeing today" button that opens TodayPickerModal, and an "Onboard new patient" button that opens OnboardPatientModal. (The greeting-and-date line and the scope toggle in the header of the first design are gone: the scope lives inside Patients.)

Skeleton rows (PatientRowSkeleton) appear while usePatientsSummary is loading.

The dashboard opens on Today's patients only when someone is on today's list; once that list finishes its first load and it's empty, it opens on Patients instead. A tap on either pill wins from then on.

Sample patient (internal/sample-patient.md): "My patients" asks for the doctor's sample with includeSample=1. While the sample is the doctor's only patient, a card at the top of the Patients view offers "Explore Alex, your sample patient" and "Onboard your first patient" in place of the empty state. The sample row carries a Sample badge and always sorts below real patients.

Patient Row

components/doctor/PatientRow.tsx is the workhorse cell. Each row shows:

  • Avatar with initials
  • Patient name + case (most recent injury / operation, or "No case")
  • A "Not onboarded" pill when the patient hasn't completed onboarding
  • A compliance grade chip (ComplianceGrade component, see below)
  • A last-visit chip with relative time ("Today", "Yesterday", "3 days ago", "2 weeks ago", …) and a stale indicator after 14 days
  • Flag chips for elevated pain/exertion, long inactivity, excessive exercises, and never-started states (FlagChip component)
  • A small "today" dot on the left rail when the patient is in today's list, plus a dismiss button on the right

Compliance Grade Chip

components/doctor/ComplianceGrade.tsx renders a single letter (A+, A, B, C, D, F, or a dash for NONE) with palette-tuned background and text colors:

Grade Color family
A+, A Green
B Lime
C Amber
D Orange
F Red
NONE Gray

Sizes (sm, md, lg) and variants (chip background vs bare letter) let the same component fit in row cells, header summaries, and the full-bleed Quick Analysis card.

Seeing Today

Since 2026-10-29 the whole of this follows the todaysPatients tenant flag (default on): with it set to false, the dashboard shows no pills and no today list, the patient screen has no Seeing today chip and no add/remove action, and useInClinicPatients({ enabled: false }) keeps the list empty without reading the server. The server keeps the list; turning the flag back on shows it again.

useInClinicPatients() manages a rolling per-doctor list of "patients I'm seeing today" through the backend at /v2/doctor/in-clinic-patients. The backend stores the active list by tenant, therapist, and patient, and writes an audit event whenever a patient is added or removed. The hook still keeps a per-user AsyncStorage cache for instant display/offline fallback and migrates the legacy local-only list once when the backend list is empty.

TodayPickerModal shows the doctor's full caseload with checkboxes; the modal preloads the full patient list via usePatientsSummary().loadPickerPatients() so it can list patients outside the current scope. Adding a patient surfaces a success toast and the row immediately appears in the Today's patients section at the top of the dashboard.

Dismissing a patient from today's list is a one-tap action on the row (no confirmation) — it only removes them from the today list, not from the caseload.

When the doctor has no patients at all, the picker says so and offers Onboard New Patient (which closes the picker and opens OnboardPatientModal), instead of the "All your patients are already added" message.

PatientSearchModal (opened from the header's search icon, or from the Today tab's Search all patients) searches with one matcher, lib/patientSearch.ts, used by every patient search box in the doctor's app, the Today picker and the Today tab's field included: every word of the query has to appear in the name, the email or the case, in any order and in any case ("lee anna" finds Anna Lee; ё reads as е, so "Семенов" finds "Семёнов"), and a query of three or more digits, written with spaces, brackets, dashes, dots or a plus, also matches the phone number.

It searches the current scope's patients first. Since 2026-10-06 it also looks further: usePatientsSummary().loadSearchPools() fetches the clinic list and the archived list when the search opens ("Checking the rest of the clinic…" while they load; the archive is read again after a pull-to-refresh or a scope change), and the modal lists:

  • matches among other therapists' patients under Not your patients ("Other therapists' patients in your clinic"), only with clinic-wide access (allActivePatientsDashboard; otherPatients is empty otherwise);
  • matches among archived patients (GET /v2/doctor/patients/archived, doctorPatientsService.getArchivedPatients()) under Archived ("Tap a patient to restore them"). Tapping one asks, over the open search, to restore them ("{name} will come back to your patients with their records." → Restore and open patient; Cancel leaves the search as it was), restores them and opens their screen. A patient the clinic list marks as the doctor's own (isConnectedToMe) never shows under Not your patients, even while the doctor's own list is still loading, and opening the search again while it is still looking joins that look instead of starting another.

Patient Detail Screen

app/(doctor)/patient/[id].tsx is the per-patient hub, reachable by tapping any row. It shows:

  • Name, age, and case
  • Care team — the connected therapists, with (you) next to the viewer
  • An "Assign to me" CTA when the viewer is browsing a patient outside their caseload (only with therapistPatientAssignment enabled)
  • A "Patient hasn't onboarded yet" banner with a Re-onboard patient button when applicable
  • For a patient who already set up their account, a Send sign-in link quick action (see Sign-in link)
  • Stat tiles: last visit, next session, completion %
  • Action grid (filtered by feature flags):
    • Start test — clinical movement test recording
    • Record exercise video — gated by therapistPersonalizedExerciseVideo
    • Manage HEP — gated by therapistMobileHepManagement (see mobile-doctor-hep-management.md)
    • View videos — opens the personalized video library for this patient (see personalized-exercise-videos.md)
  • A Quick Analysis link that opens patient-analysis/[id].tsx
  • Two list sections:
    • Movement tests — recent clinical assessments (PatientTestRow), with a "See all tests" link
    • Personalized videos — recent personalized videos
  • For a sample patient: a banner ("made-up data, only visible to you") with Reset sample data and Remove sample; the care team, re-onboard, personalized-video and Screening Lab entries are hidden. Opening Quick Analysis, saving the HEP and replying in the sample thread check off the sample checklist.
  • A pair of Section Info modals (SectionInfoModal) that explain what movement tests and personalized videos are, opened from each section header — useful for therapists trying these features for the first time

Clinician RTM time

When the clinic uses RTM billing (rtmBillingDashboard), a real patient's working screens add the clinician's active time to that patient's monthly RTM total, silently, as on the web: there is no timer on screen and nothing to start or stop. The patient screen, Quick Analysis, session history and review (including notes), HEP editing, assessment results/report and conversations with exactly one active patient count; one screen counts at a time, so opening a related screen never doubles time. Recording pauses in the background and 30 seconds after the last tap, scroll or typing, and starts again on the next input.

General patient lists, live visits, camera capture and sample patients never accrue time. Unsent time waits on the device (for up to 7 days) and is sent under the same clinician. Totals, review and approval are in the web billing dashboard. See RTM billing and the implementation audit.

Quick Analysis Screen

app/(doctor)/patient-analysis/[id].tsx calls a doctor-scoped endpoint that returns the same DailyReviewPatient payload shape as the admin daily review (one patient). It renders:

  • Compliance grade (lg variant) with the reason string
  • Flags section — chips for any active flags (long inactivity, elevated pain count, elevated exertion count, excessive exercises, has personalized videos, never started). Pain and exertion counts are derived from the last completed session
  • Notes section — the suggestedNotes[] array from the service, rendered as a checklist
  • Next up — upcoming session exercises with planned sets/reps/duration. Exercises with a personalized video are marked
  • Last completed — last completed session exercises with elevated pain/exertion highlighted
  • Footer stats: streak, sessions in last 7 days, sessions in last 14 days
  • A Manage HEP quick action and a View all completed sessions link to patient-sessions/[id].tsx

All Sessions Screen

app/(doctor)/patient-sessions/[id].tsx paginates through the patient's completed workouts, each session expandable to show its exercise list, with elevated pain/exertion exposed inline. The same SessionPreviewCard is used here and on the Quick Analysis "next up" / "last completed" cards.

Patient Re-onboarding

When a patient has been invited but never completed onboarding, the patient detail screen shows a Re-onboard patient CTA. Tapping it opens ReonboardPatientModal (components/doctor/ReonboardPatientModal.tsx):

  1. Loading — the modal asks patientInviteService.generateInviteLink(patientId) for a fresh magic-link URL.
  2. DOB required — if the patient has no DOB on file the backend returns requiresDob: true; the modal collects DOB via DOBFieldInput and retries.
  3. Ready — shows the magic link with three actions:
  4. Send invite email — emails the patient (patientInviteService.sendInviteEmail) when they have an email on file
  5. Copy link — copies the URL to the clipboard
  6. QR code — a react-native-qrcode-svg block under the link for in-clinic scanning
  7. The modal also exposes Generate a new link which calls the same endpoint with a regeneration flag, invalidating any prior token for the patient.

This flow reuses the same /v2/user/generate-invite-link/:patientId and /v2/user/send-invite-email/:patientId endpoints used by web — see internal/patient-onboarding.md for the full token lifecycle.

Once the patient has set up their account the re-onboard button is gone, and a patient who can't get back in needs something else. The patient screen's quick actions then include Send sign-in link (for an active patient with an email; never the sample patient). It opens SendSignInLinkSheet (components/doctor/SendSignInLinkSheet.tsx): the patient's email, what the link does, and Send link. The sheet says when the email is out, and explains a refusal: a patient who never finished the invite ("send the invite again instead"), a missing email, a switched-off account, a patient the therapist can no longer open, or too many links in an hour. Two quick taps send one email, and the sheet stays open while the email is on its way.

The email takes the patient to the web page behind the link, which shows their email, takes their password, or lets them set a new one right there and signs them in. Same endpoint as the web dashboard's action: POST /v2/user/send-sign-in-link/:patientId (patientInviteService.sendSignInLink); the token lifecycle is in internal/patient-onboarding.md.

Translations

~140 new keys were added under these namespaces in Okta-Mobile/locales/{en,ru}.json:

Namespace Coverage
doctorHome Header, scope chooser, today/all sections, the Today search, the search's section headings, search hints, empty states
patientDetail Patient hub strings, action grid labels, care team, re-onboard modal copy, section info modals
quickAnalysis Flags, notes, next-up / last-completed labels, relative counts
inClinic Today picker buttons, dismiss confirmations
personalizedVideo Library picker additions (chooseFromLibrary, videoTooLongMessage, …)

All strings are present in both English and Russian.

Backend Tie-in

The doctor controller (OktaPT-API/src/routes/doctor/controller.ts) imports gradeFromActivity and getPatientSummaryStats from src/services/dailyReviewService.ts so the getMyPatients and getAllTenantPatients endpoints return:

  • Compliance grade and reason
  • Last completed date
  • Next upcoming session date
  • Completed and incomplete workout counts
  • Care team (when therapist assignment is enabled)

usePatientsSummary consumes both endpoints with separate caches, and the patient detail screen calls a single-patient summary endpoint to refresh stats when returning to the screen. Since 2026-10-06 every /v2/doctor route requires the DOCTOR role (requireDoctor), as the archived list already did (see therapist-patient-assignment.md).

Key Files

File Role
components/doctor/DoctorDashboard.tsx Top-level dashboard list, scope toggle, modals
components/doctor/PatientRow.tsx Patient row cell
components/doctor/PatientRowSkeleton.tsx Loading state
components/doctor/ComplianceGrade.tsx Grade chip / letter
components/doctor/FlagChip.tsx Status flag chip
components/doctor/PatientSearchModal.tsx Patient search: the current scope, other therapists' patients (clinic-wide access), archived patients
lib/patientSearch.ts The one matcher for every patient search box (name in any order, email, case, phone digits)
components/doctor/TodayPickerModal.tsx "Seeing today" picker
components/doctor/ReonboardPatientModal.tsx Re-onboarding flow
components/doctor/SendSignInLinkSheet.tsx Sign-in link for a patient who already set up their account
components/doctor/SectionInfoModal.tsx Explanatory popovers
components/doctor/SessionPreviewCard.tsx Shared session/exercise card
components/doctor/PatientTestRow.tsx Clinical assessment row
app/(doctor)/patient/[id].tsx Patient detail hub
app/(doctor)/patient-analysis/[id].tsx Quick Analysis
app/(doctor)/patient-sessions/[id].tsx All completed sessions
hooks/usePatientsSummary.ts Caseload loader with scope + caching, plus the search pools (other therapists' and archived patients)
hooks/useInClinicPatients.ts Backend-backed today's-list hook with AsyncStorage cache
services/doctorPatientsService.ts getMyPatients, getAllTenantPatients, getArchivedPatients, getPatientSummary
services/patientAnalysisService.ts Quick Analysis payload
services/patientInviteService.ts Re-onboarding endpoints