← Back to landing

vcflow eCOA Slice 1 — v2 Design Doc

Design: vcflow eCOA Slice 1 — Clinician eCOA Per-Participant View (v2 — review feedback incorporated)

Generated by /office-hours on 2026-05-22 Branch: main Repo: validcare/vcflow Status: APPROVED (2026-05-22, Scott — /office-hours feedback-incorporation session) Mode: Startup (intrapreneurship) Supersedes: scott-main-design-20260521-230737.md

What this revision is. v1 (the file above) produced the strawman + four-fixture mockup. Scott reviewed it on 2026-05-22 and returned 8 feedback points as the final input for the wireframe phase — the wireframes are reworked once against this doc, then spec authoring begins; there is no further wireframe re-review. This v2 carries v1 forward unchanged where v1 was not contested, and reworks the layout, zones, and edge states against the 8 points. The new content is concentrated in §Review Feedback Incorporation, the reworked §Recommended Approach, and §Wireframe Rework Checklist.

Naming note (carried from v1). The handoff calls this the "Participant Dashboard," which is ambiguous — it can read as the participant's own dashboard (ePRO). This surface is the clinician's dashboard about one participant (eCOA). The spec should be titled "Clinician eCOA — Per-Participant View."


Review Feedback Incorporation

Scott's triage instruction: every one of the 8 points gets an explicit "Slice 1 / Slice 2+" call with recorded rationale — no point silently dropped.

Triage table

# Feedback Call Where it lands
1 Action bar — no duplicate action paths; recommend, don't gate Slice 1 Now banner reworked (D1)
2 ePRO data visible on this screen Slice 1 (display) / Slice 2+ (affirmation form) new ePRO zone (D2)
3 Journey box date semantics Slice 1 rail + step-summary date rework
4 PI sign-off fast summary view Slice 2+ — different surface logged to /signing (Slice 3 Task 17)
5 External lab results Slice 1 (display + journey attachment) / Slice 2+ (PDF intake) new Lab Results zone
6 Out-of-window / override handling Slice 1 override affordances (D3)
7 Pre-submit confirmation Slice 1 (visit-level) / separate (per-form field) confirmation pattern
8 CRA query / clarification flow — in-context Slice 1 role-aware raise-query affordance (D2/D4)

The resolved decisions (this session)

Triage rationale for the points NOT made into D-questions


Problem Statement

Design (not implement) Slice 1 of the vcflow clinician eCOA surface: the per-participant view a study coordinator opens to see where one enrolled participant is in their trial journey and what to do next with them. The deliverable is a strawman narrative + an interactive mockup for internal review. Implementation follows in a later slice, gated on mockup approval.

Today this surface (/participants/:id, apps/staff/src/app/features/participants/participant-detail.component.ts, ~285 lines) is a tabbed flat table of form responses ("Visit Matrix" / "Timeline" tabs). It makes the coordinator choose a view rather than see the next task.

Demand Evidence

Demand for the capability is committed, not speculative: the eCOA umbrella tracker (work-vcflow-ecoa.md) classifies it as "Day-1-required clinical capability — the clinician-side data-collection backbone," targeted for the first commercial study. The open question is not whether to build a clinician per-participant surface but what shape it takes. The 2026 industry direction corroborates the chosen shape: clinical dashboards are moving from static data displays to predictive prioritization — classify into urgency tiers, clinicians act rather than sort.

Status Quo

Validcare's legacy platform is a two-system split:

The wedge is to collapse the two-system split. vcflow natively owns forms, data, the XState journey engine, and the eCOA workflow in one platform. The dashboard's authoritative, live, journey-driven state is a correctness + trust argument vcpi structurally cannot make.

Target User & Narrowest Wedge

Primary persona: the study coordinator. Highest-frequency user; minutes per participant, not hours; does visit execution and clinician-completed eCOA data capture across a caseload. The default layout is optimized for the coordinator.

Secondary persona, now first-class on this surface: the monitor / CRA. v1 treated the surface as coordinator-only. Point 8 pulls the CRA in — not as a separate screen but as a role-scoped view of the same surface: the monitor gains a raise-query affordance and loses the coordinator's data-capture buttons (see Zone 3 and Open Question 8). PI/investigator e-signature remains secondary — surfaced, not centered.

Narrowest wedge: one participant, one screen, one glanceable next action — a journey-engine-driven view that ranks rather than lists.

Constraints

Premises (carried from v1, unchanged)

Tier-derivation rule (carried from v1, reworked for D3)

The rail separates two things v1 had fused: journey position (the single moving frontier — the state machine) and per-visit data completion (tracked independently). That split is what lets a missed visit's data be collected late without two concurrent journey states.

Journey-config-driven behavior

The journey config (study.json + the XState machine) is the single source of truth for override and sequencing — the dashboard reads and renders, it does not bake journey logic into the UI. OQ5 was investigated against the real engine (machine.factory.ts, guard.registry.ts, types.ts); the resolved model:

The only hard gates on this surface are data integrity (an open query blocks visit signature) and hard-window non-collectability. Everything else the banner recommends.

Data completion is already separate from journey state — confirmed in the engine: FormResponse rows persist on form save, independent of the XState machine; a form for a past visit can be filled after the frontier has advanced, and nothing blocks it. D3's core bet (collect late data without two concurrent journey states) needs no new separation — it is how the platform already works. The dashboard computes a visit's completeness from its FormResponse rows (all forms in the visitRef group present), not from journey state — that computation drives the Done-tier "Data outstanding → Complete" flip.

Cross-Model Perspective

From the v1 session (not re-run for this feedback-incorporation pass — the v2 decisions are feedback-driven, not premise-driven). Two independent cold reads validated the journey-engine wedge and the two-pane structure. Key v1 contributions still load-bearing here:

Approaches Considered (decided in v1 — not reopened)

Approach B was selected by Scott in v1 for its long-term scaling to Slice 2+ panels. This revision does not reopen the approach choice — the 8 points are all within Approach B.


A persistent participant identity header spans the top (study ID, arm, site, enrollment state — no PHI beyond study ID). Below it, two panes in a fixed 304px 1fr grid.

Left — Journey Rail (fixed 304px, net-new ParticipantJourneyRail)

Three tiers per the Tier-derivation rule. Date semantics (point 3, OQ4 resolved): there is no scheduled-appointment date in vcflow — each rail row shows the computed visit window as a calendar date range and a countdown to window close. A visit with no window guard shows just its name.

Right — Work Surface

All zones are step-scoped — they render the rail-selected step (default: the Now step). There are eight zones, numbered 1–8. v2 inserts Zone 6 (Lab Results) and renumbers the v1 Signature and Audit zones to 7 and 8; v1's zone order is otherwise preserved.

Zone 1 — Now recommendation banner (reworked, D1). The banner is a pointer, not an executor. It carries the "Next action" tag, a plain-language headline, and a one-line "why now." It has no primary button of its own. Its mechanic is fixed: the banner draws a static highlight border on the zone that owns the recommended action, and clicking the banner scrolls that zone into view. There is no scroll-on-load — the banner sits at the top and the owning zone is already near it.

Zone 2 — Step summary. Visit/step name, window range (computed from the anchor + offset), window status, one-line plain-language summary.

Zone 3 — Open Items. Queries + edit-check failures + signature blockers for the selected step, must-resolve-first order. Compact list (net-new; reuses query types/badges only). Quiet empty state when clear.

Zone 4 — ePRO since last visit (new, D2 / point 2). A read-only summary of participant-reported ePRO entries (e.g. "3 ePRO responses since last visit · last May 18 · Review"). "Review" opens the ePRO detail. Collapses to a one-line summary; quiet empty state when the participant has entered nothing. When a Done step is selected, the zone shows the ePRO entries that fell within that visit's window — the "since last visit" framing is relative to the current visit only; for a Soon step the zone is empty. The Part 11 clinician-affirmation form ("I reviewed this participant's ePRO data") is Slice 2+ — designed later, gated on work-vcflow-epro shipping real data. The zone is laid out now so ePRO is a planned surface, not a later bolt-on.

Zone 5 — Forms for this visit. Compact rows, status dot, one button per row — Start / Continue / View. This is the single executor for form actions (D1); the recommended row is highlighted by Zone 1 but is otherwise an ordinary row. The visit's forms (the visitRef group's states) render in the engine's chain order; all rows stay actionable regardless of order (recommend, don't gate). When a Done-tier "Data outstanding" visit is selected, its form rows stay actionable so the late data can be collected. Monitor role: each row carries the Zone-3 "Raise query" affordance and the Start / Continue button is hidden (see the Zone 3 design boundary).

Zone 6 — Lab Results (new, point 5). External lab results attached to this visit / the participant journey. Display only in Slice 1: result rows, source label, collected date, and a link to the source document (e.g. the lab PDF). When a Done step is selected, shows that visit's labs; empty for Soon steps. The display + journey-attachment model is Slice 1; the PDF upload/intake mechanism is Slice 2+ (the umbrella's interim path before Lab Connect). Quiet empty state when no external labs exist for the visit.

Zone 7 — Investigator signature. Wrapped pi-sig-banner; renders not-ready / pending-PI states with coordinator-first copy. Routing a completed visit to signature triggers the pre-submit confirmation (point 7) — see below.

Zone 8 — Audit trail. Step-scoped who-did-what-when, collapsed by default, present on the page (UX principle 5). "View full participant history" link opens the complete participant-wide audit (a separate view).

v1's "Coordinator job list" table (the 8-task → zone mapping) is superseded by the per-zone descriptions above and §Success Criteria's "every panel maps to a task" assertion — deliberately folded in, not silently dropped (P2).

Out-of-window handling (D3 / D8 / point 6)

A missed or overdue visit never blocks the journey — the next visit opens on its own window schedule; the dashboard does not gate it on the prior visit being complete. The prior visit drops into the Done tier, and what the clinician can do with it is dictated by the journey config's window policy:

The pre-submit / pre-commit confirmation pattern (point 7)

A single consistent confirmation pattern applied to the three dashboard transitions that each write an immutable audit or deviation event — the confirmation is a genuine last check before a permanent write, not a courtesy, and its copy says so. Each shows a summary of what is about to happen and a confirm button — and for the two deviation actions it captures a structured reason (D8):

  1. Route a completed visit to PI signature (Zone 7) — summary = forms + key values.
  2. Collect a soft-window visit's data out of window — summary + deviation-reason capture.
  3. Record the deviation reason for a hard-window missed visit.

Per-form, field-level confirm-before-submit is not in this pattern — it belongs to the SurveyJS forms renderer and is separate work.

The 9 strawman decisions (v1 table, amended)

Decision Resolution
Information hierarchy Identity header → Now banner → step detail; rail is persistent orientation.
Layout pattern Two-pane: journey rail + work surface, fixed 304px 1fr.
"Next action" surfacing Now banner = a pointer that highlights the owning zone (D1).
Visit + step rendering Tiered Now / Soon / Done in the rail; a visit = a meta.visitRef group of states; a visit's forms render co-equal and all actionable (engine chains them; dashboard does not gate).
Lock semantics No hard task-order gates — sequencing is recommended, not locked (point 1). Soon steps dimmed + hollow dot. The only hard gates: data-integrity (query blocks signature) and hard-window non-collectability. No vcpi-style "Requires:" tags.
Sign workflow Signature zone (Zone 7); route-to-signature triggers pre-submit confirmation; cross-participant PI summary is /signing, not here (point 4).
Participant selector Match /casebook's drill-down — a ← Casebook back link.
Empty / first-visit First-visit fixture: rail Now = first visit, Done · 0, banner points to the first form.
Density Desktop-first, generous whitespace, ~1180px content width. Tablet deferred to the spec.

Design decisions carried from v1 (unchanged)

Degraded & edge states (v1 table, amended)

State Treatment In mockup?
Out of window / overdue Now visit past its window; amber "Overdue" on rail row + step summary + Now banner. A soft-window visit stays collectable. Yes — fixture 4.
Data outstanding (collected late) A soft-window visit the frontier has passed, still collectable; Done tier, amber "Data outstanding," forms actionable; collecting opens deviation-reason capture (D8). New — fixture 5.
Hard-window missed A hard-window visit whose window closed with no data; auto-flagged "Missed" in Done, read-only; clinician records the deviation reason. New — fixture 5 (paired).
Query-blocked Forms complete but an open query blocks completion/signature; Zone 7 stays "not ready." Partially (fixture 2). Pure "forms done, query blocks" deferred to spec.
CRA query raise Monitor-role view; coordinator action buttons hidden; "Raise query" affordance on Forms rows + Open Items. New — fixture 6.
ePRO present Zone 4 shows the ePRO summary; "Review" opens detail. New — show populated in fixtures 2/3.
External lab attached Zone 6 shows lab result rows + source-document link. New — show populated where a visit has labs.
Withdrawn / discontinued Journey ended early; surface read-only; banner = neutral status. Designed; deferred from the mockup.
Unclassifiable journey state Fallback: render in Now, neutral "Review participant status," surface raw state. Deferred to spec.

Wireframe Rework Checklist

The single concrete punch list for the wireframe-rework session. Rework once against this, then author the spec.

Final fixture roster (v1 had 4; v2 reworks all 4 and adds 2): (1) newly-enrolled · (2) visit-in-progress · (3) awaiting-PI-signature · (4) visit-overdue · (5) out-of-window handling — a soft-window "Data outstanding" visit in the Done tier (collect-late + deviation-reason capture) and a hard-window "Missed" visit · (6) monitor-role view (CRA raise-query). Fixtures 5 and 6 may be interactive states layered on an existing fixture rather than standalone frames, at the rework session's discretion — but both must be walkable.

  1. Zone 1 — Now banner: remove the primary button from all fixtures; convert to a pointer — static highlight border on the owning zone + click-to-scroll. Verify the Forms-zone row is the sole executor.
  2. Rail + Zone 2 dates: replace "Due [date]" with the computed window range + countdown-to-close (OQ4 resolved — there is no scheduled-appointment date in vcflow; do not mock one). A non-date-gated visit shows just its name.
  3. Zone 4 — ePRO: add the read-only ePRO-summary zone; populate it in fixtures 2 and 3; quiet empty state in fixtures 1, 4, 5, and 6.
  4. Zone 6 — Lab Results: add the lab-results display zone; populate it in at least one fixture (a visit that has external labs).
  5. Out-of-window handling (D3 / D8): build fixture 5 — a soft-window visit in the Done tier flagged "Data outstanding" with its forms still actionable and the collect-late deviation-reason confirmation, plus a hard-window "Missed" visit (read-only, reason recorded). Show the Done tier holding all three row-states (Complete / Data outstanding / Missed) — and decide whether to rename the "Done" tier label, since it now holds not-yet-complete rows (see §Tier-derivation rule).
  6. Pre-submit confirmation: add the confirmation dialog for route-to-signature and the two deviation actions (collect-out-of-window, record-missed-reason), with reason capture.
  7. CRA query (D4): build fixture 6 — the monitor-role view showing the "Raise query" affordance, the prefilled in-context composer, and the coordinator action buttons hidden.
  8. Triage record: keep §Review Feedback Incorporation visible in the strawman narrative so the review can see every point was explicitly placed.

Open Questions & Resolutions (spec/plan inputs)

  1. Tier-derivation compute location — client-side from the journey config, or API-served pre-classified.
  2. Per-step aggregate API — the largest implementation cost (see Dependencies).
  3. "Start first visit" flow — the highest-risk dependency (see Dependencies).
  4. [RESOLVED — OQ4] Scheduled-appointment date. Investigated 2026-05-22 against the codebase: vcflow has no scheduled/appointment-date concept. Visits carry only relative windows (dateRange guards: an anchor + minDays/maxDays); anchors (enrollment, firstTreatment) are per-participant ISO dates in context.anchors; windows are computed at runtime in guards and are not exposed by any API today. Resolution: point 3 shows the computed window range + countdown-to-close, no scheduled-date headline. The aggregate API must compute the window per visit (Dependencies #1). Key file refs: apps/api/src/modules/journey/registry/types.ts:33-47 (JourneyContext.anchors), types.ts:59-65 (DateRangeGuardConfig), apps/api/src/modules/journey/registry/guard.registry.ts:107-143 (the dateRange window computation), studies/PILOT-001/study.json:191-208 (example dateRange guards — isInV2Window, isInV3Window), apps/api/src/modules/forms/entities/form-response.entity.ts:47 (visitRef column — string only, no date field).
  5. [RESOLVED — OQ5] Journey-config schema for D3. Investigated 2026-05-22 against machine.factory.ts, guard.registry.ts, types.ts. Findings: (a) window policysoft is exactly today's behavior (a dateRange guard failure records a deviation and the transition proceeds); hard is a small net-new per-state flag windowPolicy: 'hard' | 'soft' (default 'soft'). (b) next-step structure — the engine models one form per state, chains forms sequentially, and groups them into a visit by meta.visitRef; there is no unordered-set construct, and the dashboard does not need one (order-agnostic by design — see §Journey-config-driven behavior). (c) data-completion separation — already real: FormResponse rows persist independently of the XState machine; a past visit's form can be filled after the frontier advances. Resolution: the only net-new config is the windowPolicy flag; everything else D3 needs already exists. Key file refs: apps/api/src/modules/journey/registry/machine.factory.ts:88-149 (createJourneyMachine), machine.factory.ts:42-75 (transformStates), machine.factory.ts:22-32 + :62-65 (TransitionConfig + array-transitions for guarded branching), apps/api/src/modules/journey/registry/types.ts:162-178 (StateMeta / StateDefinition — no windowPolicy today; this is where the field gets added), apps/api/src/modules/journey/registry/guard.registry.ts:107-143 + :149 (dateRange + formCompleted guards), apps/api/src/modules/journey/entities/journey-state.entity.ts:40-44 (currentState + snapshot), apps/api/src/modules/journey/activities/journey.activities.ts:69 (getPersistedSnapshot), apps/api/src/modules/forms/entities/form-response.entity.ts:16-103 (FormResponse — independent of journey), apps/api/src/modules/forms/services/form-journey-bridge.service.ts:87-93 (onFormCompleted → signal journey), apps/api/src/modules/journey/services/participant-timeline.service.ts:108-120 (form responses + state history merged separately in the timeline), studies/EXAMPLE-001/study.json:76-135 and studies/PILOT-001/study.json (visit/state definitions, meta.visitRef grouping, transition arrays with recordWindowDeviation fallthrough).
  6. CRA query attach point — value-level queries need a field reference; designing the in-form field picker depends on forms gaining a monitor-review mode. Slice 1 designs the dashboard-side affordance only.
  7. Tablet collapse — responsive two-pane → single-pane, co-designed with the Now banner.
  8. Monitor-role zone-level permissions — the CRA view is not just an added affordance; a monitor must not execute clinical data capture. Which buttons and actions each zone hides or disables for the monitor role needs an explicit per-zone permission map. Spec input.

Success Criteria

Distribution Plan

Not applicable — the deliverable is a route inside the already-deployed apps/staff Angular app (Cloudflare Pages), covered by the existing CD pipeline.

Dependencies

Implementation prerequisites, ordered by magnitude — spec/plan inputs, not Slice 1 design work:

  1. Per-step aggregate API — the largest single implementation cost. All work-surface zones key off visit/step; v2 adds ePRO and lab-result data to that aggregate. No endpoint today serves forms + queries + edit-check failures
  2. "Start first visit" flow — the highest-risk dependency. Current form navigation requires an existing formResponseId (participant-detail.component.ts:282); a freshly-enrolled participant has none. First-form creation is net-new client + backend work.
  3. D3 — windowPolicy flag + deviation recording. Reduced after OQ5: the only net-new journey-config is a per-state windowPolicy: 'hard' | 'soft' flag (default 'soft' = current behavior), plus hard-window enforcement (block the transition when a hard window has closed). Recording a protocol deviation + structured reason on out-of-window collection or a missed visit is also net-new; the deviation-reason picklist taxonomy needs a source (protocol-defined or a standard deviation-reason list). Per-visit data-completion tracking is not net-new — FormResponse already persists independently of the journey (OQ5c). Starting points: apps/api/src/modules/journey/registry/guard.registry.ts:107-143 (current soft-window behavior — the place to extend with hard-window enforcement); studies/PILOT-001/study.json:191-208 + transition arrays (the recordWindowDeviation fallthrough pattern — the place to wire the windowPolicy: 'hard' branch); apps/api/src/modules/journey/registry/types.ts:162-178 (StateMeta / StateDefinition — where the windowPolicy field gets added).
  4. ePRO data source (D2). Zone 4 depends on work-vcflow-epro exposing queryable participant ePRO entries. If ePRO data does not exist when Slice 1 ships, Zone 4 renders its empty state.
  5. External lab data model (point 5). Zone 6 depends on a model for lab results attached to a participant/visit, plus document storage for the source PDF. The intake/upload path is Slice 2+.
  6. Monitor-role query-raise (D4). The query backend already exists (2026-05-06-queries-and-soft-checks); the net-new work is the in-context raise affordance + prefilled composer, and (Open Question 6) a forms monitor-review mode for value-level queries.
  7. Net-new components: ParticipantJourneyRail, a compact open-items list, an ePRO-summary zone, a lab-results zone, a coordinator-first pi-sig-banner wrapper, and the confirmation-dialog pattern.

Adjacent surfaces that must not collide: /casebook (visit-matrix.component), /signing + /signing/:id (point 4 feeds this), /dashboard.

The eventual spec must be authored on a feature branch (per ~/code/ branching policy).

Out of Scope for Slice 1

Tools panel, Reports panel, Unscheduled-Visit catch-all, per-test status grid — all Slice 2+. Cross-participant triage stays in /casebook. The cross-participant PI sign-off summary is /signing (point 4). The ePRO clinician-affirmation form, the lab-PDF intake mechanism, and per-form field-level pre-submit confirmation are all Slice 2+ / separate work. The deviation exclusion-review surface — where the PI / medical monitor / sponsor judges whether a recorded deviation is exclusionary — is a separate downstream surface, not this dashboard. libs/shared-ui polish is a separate task.

The Assignment

Take the reworked mockup to the wireframe-rework checkpoint and walk it against the §Wireframe Rework Checklist — confirm each of the 8 points landed where this doc says. The state-to-state shape change (newly-enrolled → in-progress → awaiting-signature → overdue → out-of-window handling → monitor-role) is what sells the journey-engine wedge; a static frame does not. On approval, the next concrete action is authoring the Rule-A spec on a feature branch.

What I noticed about how you think