08-portals-and-i18nwave: W5filled18 citations

Doctor Portal — Clinical Workflow UI

Audiences: doctor, developer, clinical-buyer

Doctor Portal — Clinical Workflow UI

The clinician's primary workspace: patient roster, visit timeline, AI-assisted note capture, prescriptions, document upload, diagnosis engine, deep search, and TUCAN assistant — all under portal/doctor/(with-nav)/.

Business Purpose

The doctor portal is the product surface where the clinic's most expensive minute is spent. Every interaction has to either save the clinician seconds, make a clinical decision safer, or reduce after-hours documentation. The portal collects: today's schedule, the active patient roster, a per-patient timeline (encounters, labs, imaging, notes), AI tooling (visit dictation, TUCAN assistant, document summarization, diagnosis engine, deep search across notes and literature), prescribing, and document workflow.

It is the demo surface for clinical buyers, the integration surface for partner devices (LiveKit voice, OCR), and the canonical consumer of the TUCAN agent stack. If a TUCAN tool ships and the doctor portal can't drive it, it does not exist for the customer.

Audiences

  • Doctor: the daily workspace — appointments → patient card → visit → notes → prescription → next patient. Every minute saved compounds.
  • Investor: the AI-augmented clinical workflow story — voice dictation, TUCAN assistant, diagnosis engine, document generation. The doctor portal is where the Mastra-codebase agents become a UX.
  • Clinical buyer (doctor/nurse/receptionist): shows clinical-buyer-specific surfaces (patient list, schedule, prescription) and how AI is opt-in per workflow, not a blocking gate.
  • Developer/partner: explains the (with-nav)/ route-group pitfall, how PortalNavigation roleKey="doctor" resolves the sidebar, and where doctor-specific components live (components/portal/doctor/).

Architecture

Route group: (with-nav)/

Doctor is the only portal that hosts chrome-free sibling routes alongside the full sidebar shell. To accommodate this, the parent layout is intentionally bare:

typescript
// ddx-web/src/app/[locale]/portal/doctor/layout.tsx (lines 22-26)
export default function DoctorPortalLayout({ children }: LayoutProps): React.JSX.Element {
  return <>{children}</>;
}

The full chrome lives inside the route group:

typescript
// ddx-web/src/app/[locale]/portal/doctor/(with-nav)/layout.tsx (line 29)
export default createRoleLayout(doctorConfig, DoctorNav);

Routes that need the sidebar (appointments/, patients/, dashboard/, calendar/, flowagent/, forms/, etc.) live under (with-nav)/. Sibling routes that render chrome-free (help/, cms/help/) live at the doctor-portal root. Adding a new doctor page directly under portal/doctor/ and not under (with-nav)/ is the canonical regression — the page renders without sidebar or header.

Routes (selected)

diagram
portal/doctor/
├── layout.tsx                  → bare passthrough
├── help/                       → focus-mode help center (chrome-free)
├── cms/help/                   → help-CMS admin (chrome-free)
├── activity/, assessments/,    → flat siblings, currently chrome-free; verify ownership
│   drugs/, favorites/, imaging/,
│   messages/, video/, voxiate/
└── (with-nav)/                 → ALL chrome-equipped routes belong here
    ├── dashboard/              → today + recent AI sessions
    ├── appointments/           → scheduling surface
    ├── calendar/               → calendar grid
    ├── patients/               → roster + patient detail by id
    │   ├── new/                → new patient form
    │   └── [id]/               → command-center layout (sidebar + tabs)
    ├── assistant/              → TUCAN assistant entry
    ├── deep-search/            → semantic search over notes + literature
    ├── diagnosis-engine/       → AI differential support
    ├── document-generation/    → letter / referral / certificate generation
    ├── document-store/         → bulk upload + status
    ├── document-summarization-templates/ → admin for summarization presets
    ├── flowagent/              → conversational form engine
    ├── forms/                  → form catalog + builder
    ├── hr/, intake/,           → admin/auxiliary surfaces
    │   ki-tools/, medications/
    ├── messenger/, communications/,
    │   kommunikation/
    └── availability/           → schedule availability editor

Note: a small number of doctor pages render outside (with-nav)/. Some are intentional (focus shells); others may need verification — when adding new pages, default to (with-nav)/.

Layout composition

createRoleLayout(doctorConfig, DoctorNav) at ddx-web/src/app/[locale]/portal/doctor/(with-nav)/layout.tsx:14-29 composes:

  • DOCTOR_CONFIG (ddx-web/src/lib/portal/role-configs.ts:167-181): authRequirement: 'DOCTOR_ONLY', roleBadge.icon: HeartIcon, variant: 'primary', brandNameKey: 'portals.doctor', taglineFormatter: (user) => Dr. ${user.name}``, needsBackendStatus: true, backendStatusScope: 'shell', shellNoPadding: true.
  • extraElements: [TranscriptionLocalePrompt] — voice-transcription locale picker mounted after the shell ((with-nav)/layout.tsx:24-27).
  • DoctorNav = PortalNavigation roleKey="doctor" — sectioned config from lib/portal/nav-configs.
  • BackendStatusProvider wraps the entire shell (shell scope) so a doctor sees a banner when the ddx-api is degraded.

Command-center patient detail

The patient detail page uses a "command-center" layout (sidebar + tab nav + tab content): DDXPatientSidebar (identity + clinical context + actions) + ParentTabNavPathBased (DDXTabs underline variant) + PatientDetailWrapper (grid orchestrator). i18n namespace: doctor.commandCenter.*. See ddx-web/CLAUDE.md:389-414 for the layout spec.

Tech Stack & Choices

LayerChoiceWhy
FrameworkNext.js ^16.1.6 + React ^19.2.4 + Tailwind ^4.1.18Server-first; suspense streaming for slow data; semantic tokens.
RBAC gaterequireRoleAccess('DOCTOR_ONLY', locale) on every pageDoctor is the privileged write role; gate must be near the top of every page.
Layout factorycreateRoleLayout(DOCTOR_CONFIG, DoctorNav)One factory, one config; lets the eleven-portal architecture stay symmetric.
Route group(with-nav)/Lets chrome-free siblings (help/, cms/help/) coexist; documented at portal/doctor/layout.tsx:1-16.
Backend statusneedsBackendStatus + scope: 'shell'Doctor must see API health (degraded mode blocks Rx / chart save).
TaglinetaglineFormatter: (user) => Dr. ${user.name}``Doctor-honorific shown in the header.
AI integrationTUCAN orchestrator + Mastra codebaseSessions pre-fetched server-side on dashboard (dashboard/page.tsx:24).
VoiceTranscriptionLocalePrompt extra elementOnce-per-session locale picker for dictation.

Rejected: a separate "AI doctor portal" (would fragment the workspace and hide TUCAN usage from the rest of the practice), client-side RBAC (would shift authority out of the server), middleware.ts for role gating (file is proxy.ts per project convention).

Data Flow

  1. Doctor logs in → JWT carries role: DOCTOR + organizationId + fhirPractitionerId.
  2. Browser hits /<locale>/portal/doctor/dashboard.
  3. proxy.ts:248-319 — locale prefix, session-cookie gate, trigram → X-Clinic-ID.
  4. Bare parent layout passes children through (portal/doctor/layout.tsx:22-26).
  5. (with-nav)/layout.tsx runs the factory: requireRoleAccess('DOCTOR_ONLY', locale) returns the doctor user, organization is fetched, full shell + DoctorNav mount, <TranscriptionLocalePrompt> mounted after shell, all wrapped in BackendStatusProvider.
  6. dashboard/page.tsx:19-35 — server-side requireRoleAccess, prefetch recent AI sessions via backendFetch<AgentSession[]>('agent/sessions?limit=5'), render <DashboardClient userId={user.id} fhirPractitionerId={user.fhirPractitionerId} initialSessions={...}>.
  7. Patient list page ((with-nav)/patients/page.tsx:29-65) awaits both params and searchParams (Next.js 16), checks role, delegates to <PatientListClient> with server-resolved query inputs.
  8. The Doctor portal subscribes to SSE for live updates via <SSEBootstrap userId={user.id}> mounted by createRoleLayout.

Implicated Code

  • ddx-web/src/app/[locale]/portal/doctor/layout.tsx:1-26 — intentional bare passthrough; comments explain why.
  • ddx-web/src/app/[locale]/portal/doctor/(with-nav)/layout.tsx:14-29 — full shell composition; DoctorNav defined inline; TranscriptionLocalePrompt as extra element.
  • ddx-web/src/lib/portal/role-configs.ts:167-181DOCTOR_CONFIG (auth, badge, taglineFormatter, backend-status, no-padding).
  • ddx-web/src/app/[locale]/portal/doctor/(with-nav)/dashboard/page.tsx:19-35 — doctor dashboard page (RBAC + AI sessions prefetch + delegate to client).
  • ddx-web/src/app/[locale]/portal/doctor/(with-nav)/patients/page.tsx:29-65 — patient list page (RBAC + searchParams handling + PageContainer + PatientListClient).
  • ddx-web/src/lib/api/rbac-config.ts:35DOCTOR: DOCTOR_PERMISSIONS registration.
  • ddx-web/CLAUDE.md:389-414 — Command Center layout pattern reference for patient detail.

Operational Notes

  • (with-nav)/ is mandatory for chrome — new doctor pages outside the group render without sidebar or header. Verify by visiting the route in dev; a chrome-free rendering is the symptom. Documented at portal/doctor/layout.tsx:1-16.
  • TranscriptionLocalePrompt runs once per session — added via extraElements: [TranscriptionLocalePrompt] in the doctor config. Mounting it twice (e.g., also inside a child) double-prompts the user.
  • backendStatusScope: 'shell' — the entire chrome lives inside BackendStatusProvider; a degraded ddx-api shows a header banner. Switching this to 'children' would hide the banner from the header — verify before changing.
  • shellNoPadding: true — doctor pages own their own padding (typically via PageContainer's mx-2 md:mx-4). Mixing shell-level padding with PageContainer produces visible double-gutters.
  • fhirPractitionerId is JWT-claim derived — never client-derived. The dashboard passes it down to <DashboardClient> so TUCAN tools can scope FHIR queries to the current clinician.
  • Doctor → patient deep linksLink href={\/${locale}/portal/doctor/patients/${id}`}; always interpolate the locale (next-intl localePrefix: 'always'`).
    Doctor Portal — Clinical Workflow UI — Dudoxx Docs | Dudoxx