Doctor Portal — Clinical Workflow UI
Audiences: doctor, developer, clinical-buyer
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, howPortalNavigation 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:
// 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:
// 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)
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 fromlib/portal/nav-configs.BackendStatusProviderwraps the entire shell (shellscope) 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
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js ^16.1.6 + React ^19.2.4 + Tailwind ^4.1.18 | Server-first; suspense streaming for slow data; semantic tokens. |
| RBAC gate | requireRoleAccess('DOCTOR_ONLY', locale) on every page | Doctor is the privileged write role; gate must be near the top of every page. |
| Layout factory | createRoleLayout(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 status | needsBackendStatus + scope: 'shell' | Doctor must see API health (degraded mode blocks Rx / chart save). |
| Tagline | taglineFormatter: (user) => Dr. ${user.name}`` | Doctor-honorific shown in the header. |
| AI integration | TUCAN orchestrator + Mastra codebase | Sessions pre-fetched server-side on dashboard (dashboard/page.tsx:24). |
| Voice | TranscriptionLocalePrompt extra element | Once-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
- Doctor logs in → JWT carries
role: DOCTOR+organizationId+fhirPractitionerId. - Browser hits
/<locale>/portal/doctor/dashboard. proxy.ts:248-319— locale prefix, session-cookie gate, trigram →X-Clinic-ID.- Bare parent layout passes children through (
portal/doctor/layout.tsx:22-26). (with-nav)/layout.tsxruns the factory:requireRoleAccess('DOCTOR_ONLY', locale)returns the doctor user, organization is fetched, full shell +DoctorNavmount,<TranscriptionLocalePrompt>mounted after shell, all wrapped inBackendStatusProvider.dashboard/page.tsx:19-35— server-siderequireRoleAccess, prefetch recent AI sessions viabackendFetch<AgentSession[]>('agent/sessions?limit=5'), render<DashboardClient userId={user.id} fhirPractitionerId={user.fhirPractitionerId} initialSessions={...}>.- Patient list page (
(with-nav)/patients/page.tsx:29-65) awaits bothparamsandsearchParams(Next.js 16), checks role, delegates to<PatientListClient>with server-resolved query inputs. - The Doctor portal subscribes to SSE for live updates via
<SSEBootstrap userId={user.id}>mounted bycreateRoleLayout.
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;DoctorNavdefined inline;TranscriptionLocalePromptas extra element.ddx-web/src/lib/portal/role-configs.ts:167-181—DOCTOR_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:35—DOCTOR: DOCTOR_PERMISSIONSregistration.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 atportal/doctor/layout.tsx:1-16.TranscriptionLocalePromptruns once per session — added viaextraElements: [TranscriptionLocalePrompt]in the doctor config. Mounting it twice (e.g., also inside a child) double-prompts the user.backendStatusScope: 'shell'— the entire chrome lives insideBackendStatusProvider; 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 viaPageContainer'smx-2 md:mx-4). Mixing shell-level padding withPageContainerproduces visible double-gutters.fhirPractitionerIdis 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 links —
Link href={\/${locale}/portal/doctor/patients/${id}`}; always interpolate the locale (next-intllocalePrefix: 'always'`).
Related Topics
- Multi-Portal Architecture — the eleven-portal shell.
- i18n — Multi-Lingual Interface —
doctor.*namespace. - Patient Portal — clinician's mirror image (read mostly, no Rx).
- Nurse Portal — pre-clinic handoff (vitals, intake) feeding the doctor's encounter.
- TUCAN UI — agent surface embedded throughout the doctor portal.
- TUCAN Context Builder — how the doctor's current patient + visit feeds agent context (W3 wiring).
- AI Assistant ↔ Visit Wiring — how dictation, transcription, and visit notes thread through this portal (W4 wiring).