Skip to main content

Clinician Workspace — How it works

Overview

The providerportal Maven module is the internal clinician workspace — a read-aggregation BFF that fans out to platform engines over Feign and never imports core. Routes live under /api/v1/workspace/** (ApiV1Paths.WORKSPACE). Design: MILESTONE16_PLAN.md, CORE_PLAN.md §15.4 (provider-facing).

workforce is the structural reference for domain-module skeletons (Feign propagation, module descriptor, ArchUnit boundary). The workspace differs in one non-negotiable way: every outbound call carries the clinician's delegated token — never a service account.

M16-001 delivers the skeleton only. What exists today:

TicketDeliversStatus
M16-001Maven module, /api/v1/workspace namespace, status endpoint, Feign header propagation, ArchUnit fencedone
M16-002My patients / panel (GET /my-patients, paged, scope-filtered)done
M16-003Clinical inbox (results to sign, review tasks)done
M16-004Unified worklistsdone
M16-005Unified patient chart summarydone
M16-006Workspace preferences + saved viewsdone
M16-007Milestone gate + MILESTONE16.mddone

Frontend counterpart: FE-326 (workspace shell), FE-327 (my-patients panel), FE-328 (inbox), FE-329 (worklist), FE-330 (chart), FE-331 (preferences), FE-332 (Playwright gate). Route keys workspace.home, workspace.panel, workspace.inbox, workspace.worklist, workspace.chart, and workspace.preferences are registered in AuthorizationCatalog.

Module boundary

  • Package: com.zhenus.uhp.api.providerportal
  • Dependencies: common + exchange only — ArchUnit fails on any core import
  • Platform access: Feign clients in exchange.client.* only; caller Authorization and scope headers forwarded by ProviderPortalFeignContextPropagationConfig (no service-identity fallback)
  • Schema: providerportal (Liquibase master providerportal.db.changelog-master.yaml). M16-006 adds workspace_preference and saved_view; all clinical aggregation remains Feign-sourced.

Permissions

ProviderPortalModuleDescriptor registers workspace.* codes at startup:

CodeUsed by
workspace.status.readGET /api/v1/workspace/status
workspace.panel.readM16-002 panel
workspace.inbox.read / workspace.inbox.writeM16-003 inbox actions
workspace.worklist.read / workspace.worklist.writeM16-004 worklist actions
workspace.chart.readM16-005 chart summary
workspace.preferences.read / workspace.preferences.writeM16-006 preferences

Status endpoint

GET /api/v1/workspace/status returns module identity (moduleKey, displayName, version) and an empty capabilities list until aggregation surfaces ship. Gated by workspace.status.read.

My patients panel (M16-002)

GET /api/v1/workspace/my-patients returns a derived, paginated panel of patients the signed-in clinician may act on. Membership is computed from provider–facility assignments (M3-008), effective access scope, and recent encounters — never stored locally. Gated by workspace.panel.read.

Query parameters: standard Spring page / size / sort, plus publicDisplay=true to redact patient names server-side (shoulder-surfing / public-board mode).

Response (PanelPageDto): showPatientNames, assignedFacilityIds (scope indication), and a paged content list of PanelEntryDto rows (patientId, last encounter metadata, optional display name).

Feign additions: ProviderClient.listFacilityAssignments(providerId) for assignment lookup. Under ID-001 the provider id equals the caller's person id, so no separate provider-by-person lookup is required.

Clinical inbox (M16-003)

GET /api/v1/workspace/inbox merges the signed-in clinician's pending result reviews (M13-010) with assigned workflow tasks (M8 / M10-010 searchTasks), sorted abnormal-first then oldest-first. Gated by workspace.inbox.read.

When a Feign source fails, the response includes unavailableSources (RESULT_REVIEW and/or WORKFLOW_TASK) instead of silently dropping rows.

Action endpoints delegate to owning engines — the workspace never stores sign-offs locally:

Workspace routeDelegates to
POST /inbox/result-reviews/{id}/acknowledgePOST /clinical/result-reviews/{id}/acknowledge
POST /inbox/workflow-tasks/{id}/completePOST /workflow/workflow-tasks/{id}/complete

Write actions require workspace.inbox.write.

Feign additions: ResultReviewClient (list + acknowledge), WorkflowClient.searchTasks (cross-instance task board query returning PageDto<WorkflowTaskDto>).

Unified worklist (M16-004)

GET /api/v1/workspace/worklists merges queue items assigned to the caller, assigned workflow tasks, and open orders placed by the caller (PLACED / IN_PROGRESS). Queue rows preserve the queue engine's service order; workflow and order blocks follow their own timestamps. Gated by workspace.worklist.read.

Delegate write routes (require workspace.worklist.write):

Workspace routeDelegates to
POST /worklists/queue-items/{id}/status?status=PUT /queue/queue-items/{id}/status
POST /worklists/workflow-tasks/{id}/completePOST /workflow/workflow-tasks/{id}/complete

Feign addition: ClinicalOrderClient.searchOrders for open-order aggregation.

Patient chart summary (M16-005)

GET /api/v1/workspace/patients/{patientId}/chart assembles a one-call chart for an in-scope patient. ChartAccessGuard runs a single platform access decision and writes a PHI read audit row before any Feign fan-out; out-of-scope patients receive 404.

Six sections fan out in parallel with a 3-second per-section budget. Slow or failed sources appear in unavailableSections (CLINICAL_SUMMARY, SUMMARY_PANEL, ACTIVE_MEDICATIONS, OPEN_ORDERS, REFERRALS, APPOINTMENTS) — never as empty clinical data.

Allergy state is surfaced distinctly via allergyStatus (RECORDED / NONE_KNOWN / NOT_ASKED) from the summary panel when available.

Feign additions: ClinicalClient.getPatientSummaryPanel, ReferralClient.historyForPatient, SchedulingClient.findAppointments (optional patientId filter), existing medication and order clients.

Workspace preferences and saved views (M16-006)

Per-user layout keys and saved filter criteria live in the providerportal schema (workspace_preference, saved_view). Rows are keyed on the resolved user account plus tenant and facility from the session.

Saved views store criteria JSON only, never patient identifiers or result rows.

RoutePermission
GET /api/v1/workspace/preferencesworkspace.preferences.read
PUT /api/v1/workspace/preferences/{key}?preferenceValue=workspace.preferences.write
GET /api/v1/workspace/saved-viewsworkspace.preferences.read
POST /api/v1/workspace/saved-viewsworkspace.preferences.write
PUT /api/v1/workspace/saved-views/{id}workspace.preferences.write
DELETE /api/v1/workspace/saved-views/{id}workspace.preferences.write

Route key workspace.preferences is registered in AuthorizationCatalog (FE-331 backend half).

Caller resolution

CurrentClinicianResolver resolves the signed-in clinician's personId from the JWT personId claim or a local principal — the same pattern as workforce's CurrentStaffResolver, without importing core security types.

Standalone run

WORKSPACE_APP_PORT=8093 ./mvnw -pl providerportal spring-boot:run

When bundled in platform-app, the module is component-scanned alongside core; its standalone ProviderPortalApplication launcher is excluded from the platform scan (same pattern as workforce).

Verification

./mvnw -pl providerportal -am test -Dtest=ProviderPortal* -Dsurefire.failIfNoSpecifiedTests=false

Gate evidence: core/MILESTONE16.md and Milestone16ClinicianWorkspaceIntegrationTest.

Configuration & feature flags

SettingDefaultPurpose
WORKSPACE_APP_PORT8093Standalone listen port when run as its own Spring Boot app
WORKSPACE_SERVICE_URL${UHP_PLATFORM_URL}Feign loopback target after a future split

Chart section fan-out uses a 3-second per-section timeout (hard-coded in ChartSummaryServiceImpl); slow Feign sources surface in unavailableSections, not as empty clinical data.

  • Clinical — observations, orders, result reviews the inbox and chart aggregate
  • Workflow — assigned tasks in inbox and worklist
  • Queue — assigned queue items in the worklist
  • Workforce — structural reference for domain-module skeleton; staff portal is separate from this clinician BFF
  • Lab / Imaging — fulfiller modules whose results appear in inbox/chart

Why it is this way

Read-aggregation BFF, not a clinical store. Every clinical fact stays in the owning engine; providerportal only merges views and stores per-user layout. Writes on inbox/worklist routes delegate to clinical/workflow/queue APIs in the same request — the workspace never caches sign-offs or status locally.

The caller's token on every Feign hop. Unlike scheduled jobs that need SEC-018 service identity, the workspace always forwards the clinician's delegated Authorization and scope headers. A service account would bypass patient-access decisions the panel and chart rely on.

Access before chart fan-out (SEC-011 posture). ChartAccessGuard decides platform access and writes a PHI audit row before any Feign call. Out-of-scope patients answer 404, not 403, so the chart route does not become an existence oracle.

Saved views are criteria-only. Filter JSON must never carry patient ids or result rows — those belong in live queries against scoped engines, not in a reusable view another session could replay.

Traps

unavailableSources / unavailableSections mean upstream failure, not "empty". Inbox, worklist, and chart responses distinguish a Feign outage from legitimately zero rows. UI that treats a missing block as "nothing to do" hides partial platform outages.

Saved-view criteria reject patientId / personId keys (case-insensitive) — storing patient identifiers in criteria would let one clinician's filter leak another patient's scope when replayed.

JaCoCo gate on providerportal is ≥ 0.80 — preference and controller paths need explicit tests; bundle coverage failures blocked M16-006 CI until expanded.

Do not import core. ArchUnit fails on any com.zhenus.uhp.api.core dependency; reach platform engines only through exchange.client.* Feign interfaces.

Panel membership is derived, never stored. GET /my-patients recomputes from assignments and encounters each call — caching panel rows in providerportal would drift from access-scope changes.