Skip to content

feat(patient): care hub, visit queue, access log and payment receipts - #138

Open
classikdev wants to merge 6 commits into
DocForum:mainfrom
classikdev:feat/patient-home-as-a-care-hub-what-needs-my
Open

classikdev wants to merge 6 commits into
DocForum:mainfrom
classikdev:feat/patient-home-as-a-care-hub-what-needs-my

Conversation

@classikdev

@classikdev classikdev commented Oct 16, 2026 •

Copy link
Copy Markdown

Closes #33
Closes #32
Closes #34
Closes #37

Four patient-side issues in one PR. #33 is the hub; #32, #34 and #37 are the three screens it now links to.

Correction to the first two commits of this PR. The description I pushed with 1e1d093 said #32, #34 and #37 "stay unimplemented" because their backend routes do not exist yet, and that Coming up had no card source. Both calls were wrong. This repo already ships three Phase W2 screens (#18, #19, #21) built against documented assumed core contracts, and the same route applies here: build the UI, name the assumption, and make a 404 say "not available for this deployment yet" instead of pretending the patient has nothing. Those paragraphs are rewritten below, and the ROADMAP changelog entry that recorded the wrong verdict is annotated as corrected rather than quietly edited out.

What changed

#33 — patient home as a care hub

PatientDashboardPage was a placeholder with two links. It is now an action-first hub that answers what do I need to do?

src/features/care-hub/ranking.ts — the ranking model, pure and UI-free: a CareCard is {id, section, priority, title, detail, to, badge?}, sections are action | upcoming | recent, and priorities are a named scale (blockedNow 10 → needsYou 20 → goodToKnow 30 → recent 40). rankCards sorts by priority then title and does not mutate its input; groupBySection splits the ranked list. Keeping this a plain module is what makes the ordering testable without a DOM.

src/features/care-hub/hooks/useCareHub.ts — fans out to the sources in parallel: useDependents(), useMyAppointments(), plus one useHealthEntries(section) per section. Independent queries, no waterfall, each with its own status. It derives cards from what came back: a visit today (needsYou, badged Today — it leads Action needed because check-in is open right now), a later visit (goodToKnow, under Coming up), an expired handover invite (blockedNow), a dependent eligible for handover (needsYou), a health profile that is genuinely empty (goodToKnow), and health entries updated in the last 30 days under Recent.

src/pages/PatientDashboardPage.tsx — renders the three sections, each aria-labelledby its own heading, cards linking to the detail flow they belong to, and nav links to the two new screens (/payments, /account/access-log). Coming up and Recent render only when they have cards, so an unbuilt feature is absent rather than an empty shell.

#32 — visit check-in with a live queue position

src/services/appointments-service.ts — listMyAppointments() reads the real GET /appointments/mine (a bare array, newest-first, with availabilitySlot included). The queue routes are assumed (below).

src/features/appointments/slot.ts — pure date logic. "On the day" is the patient's local calendar day, not a UTC midnight, because the slot instants come from Postgres. isUpcoming keeps a visit in Coming up while it is ahead of you or happening today and drops older ones without a server-side filter; shouldPollQueue only ever polls a scheduled visit that is today.

src/features/appointments/queue.ts — the alert rules: NOTIFY_AT_OR_BELOW = 2 per the issue's "1–2 patients ahead", being_seen always alerts, and queueSignature (phase:peopleAhead) makes two identical reads produce one alert while 3 → 2 → 1 produces three. canCheckIn treats the window as the backend's call: waiting/being_seen is always open, a not_open whose checkInOpensAt has already passed is offered (the projection just has not recomputed), and a missing window is not treated as a closed one.

src/features/appointments/hooks/useAppointmentQueue.ts — QUEUE_POLL_INTERVAL_MS = 15_000 with gcTime: 0, so a returning patient never sees a stale position; useCheckIn POSTs and invalidates; useQueueNotification fires once per distinct state.

src/hooks/useBrowserNotification.ts — permission is requested from the click on "Notify me", never at page load, asked for once per session, and stays silent when permission is denied or Notification is undefined (Firefox on Android, and any insecure origin).

src/pages/AppointmentDetailPage.tsx at /patient/appointments/:appointmentId — the id is matched against the patient's own /appointments/mine list before the queue route is ever called, so a forwarded or guessed id cannot probe someone else's visit, and the page shows a not-found state instead. The panel itself renders position, people ahead, the wait estimate labelled as an estimate, and the checked-in timestamp.

#34 — who viewed my records

src/services/access-log-service.ts — GET /patients/me/access-log?from&to&recordType&cursor (assumed, core#45).

src/features/account/copy.ts — the wording layer for a privacy screen: roleLabel (doctor → Clinician, patient → You, admin → DocForum staff), recordLabel, reasonCopy, each with a fallback string so a reason core adds before this repo catches up renders as "Opened as part of your care." rather than undefined. formatWhen shows the raw value if the timestamp does not parse. reportConcernHref() returns null unless VITE_SUPPORT_EMAIL is set — the page then says plainly that no support channel is configured for this deployment, because pointing a worried patient at an address that does not exist is worse than admitting the gap.

src/pages/AccessLogPage.tsx + AccessLogFilters / AccessLogList / AccessLogError — from/to/type filters with an explicit Apply (typing a date must not fire four requests), useInfiniteQuery with a "Show older entries" button, and a per-row who · what · when · why.

#37 — payment history and receipts

src/services/payment-history-service.ts — GET /payments/mine and GET /payments/mine/:intentId (assumed, core#90). The real /payments/intents... routes are admin-only, which is why these are mine-scoped rather than a filter on the existing list.

src/features/payments/stroops.ts — stroops → XLM entirely in BigInt and string manipulation. amountStroops arrives as a string because the column is a BigInt; routing it through a JS number would silently round anything over 2^53. Values that are not a plain non-negative integer string return null and the caller shows "<raw> stroops" — an amount this helper cannot parse is still visible, never replaced by a plausible-looking number.

src/features/payments/status.ts — a sentence per escrow state in the payer's terms ("DocForum is holding this money in escrow. The facility has not been paid yet — that is the protection escrow exists for."), refundDeadlineCopy (absent deadline renders nothing; refundDeadlineAt is an assumed column), and historyErrorCopy, which distinguishes a 404 ("not available for this deployment yet") from a failed request from an actually-empty history.

src/features/payments/explorer.ts — one place for the testnet explorer base (VITE_STELLAR_EXPLORER_BASE_URL), used for both the transaction and the escrow contract links.

PaymentHistoryList / PaymentReceipt / PaymentHistoryPage / PaymentDetailPage — a cursor-paged list at /payments, and a detail page whose receipt prints cleanly (@media print drops the app chrome; PaymentReceipt restates itself in black on white).

Design decisions and trade-offs

  • A source that cannot be queried is a third state, not "loading". React Query leaves a disabled query isPending forever, so the first cut of the hub spun on "Loading your care summary…" indefinitely and never showed the cards it did have. Found by rendering it, not by reading it. SourceStatus now carries isLoading | failed | unavailable, and only the first two are ever reported to the patient.
  • "Empty" is a claim that requires an answer. The prompt to add allergies/conditions/medications appears only when every health section resolved successfully. Same reasoning drives every new screen: an empty list and a list that could not be fetched are different statements about the patient's data, and for [Phase W2] Payment history and receipts for patients #37 and [Phase W2] 'Who viewed my records' access log for patients #34 they are statements about their money and their PHI.
  • Everything new is me-scoped. middleware/auth.ts exposes only {id, role}, so the frontend cannot know its own patientProfileId. Every route added here is named from the session (/patients/me/access-log, /payments/mine) rather than taking an id the client would have to guess.
  • CareCard is presentation-only. It never crosses the wire in either direction, so hard rule 2 (mirror docforum-core shapes, no parallel frontend entities) holds; every field on it comes from a real Dependent, Appointment or HealthEntry. Same for the derived AccessLogEvent and PaymentHistoryEntry types — they mirror the assumed core rows, they do not invent a frontend model.
  • Polling is bounded by the domain, not by a timer. 15s is only ever running for a scheduled appointment on the patient's local today. A cancelled or past visit cannot keep a background poll alive.
  • The greeting dropped the email (from the first cut of [Phase W2] Patient home as a care hub: what needs my attention now #33): User has no display name, so interpolating the address into the H1 read badly and overflowed at 390px.
  • Route placement follows the issue text, not the nesting convenience. [Phase W2] 'Who viewed my records' access log for patients #34 names /account/access-log and [Phase W2] Payment history and receipts for patients #37 names /payments, so those stay at the literal paths, guarded to patient, instead of being folded under /patient where the hub's nested routes live. [Phase W2] Check-in and live queue position on appointment day #32's detail page has no path in the issue, so it nests.

Known assumptions

Full text in ARCHITECTURE_ESSENTIALS.md → "Known assumptions — Phase W2 patient follow-ons". Summary:

Issue Assumed route Blocked on Frontend behaviour until it lands
#32 GET /appointments/:appointmentId/queue, POST /appointments/:appointmentId/check-in, AppointmentQueue docforum-core#129 (open) hub cards work today off the real /appointments/mine; the queue panel reports "not available for this deployment yet"
#34 GET /patients/me/access-log?from&to&recordType&cursor, AccessLogEvent docforum-core#45 (open) page, filters and paging are exercised in tests against fixtures
#37 GET /payments/mine, GET /payments/mine/:intentId, plus facilityName / refundDeadlineAt on the row docforum-core#90 (open) list and receipt render from fixtures; a 404 never renders as "you have no payments"
all three cursor envelope {items, nextCursor} core has no pagination helper getNextPageParam treats a null cursor as the end

One gap worth a core issue on its own: GET /appointments/mine includes availabilitySlot but no clinician or facility name, so the hub and the appointment page can say when a visit is but not with whom. The UI does not invent a name for it.

Screenshots

Captured at 1440 and 390 CSS px. Method, so nobody misreads these: the app was built against a local stub of docforum-core serving fixture appointments, queue states, access-log entries and payment intents, then rendered in iframes pinned to those exact widths. Not the Render preview backend, and the fixture names are not real accounts. The states behind core#129/#45/#90 are only reviewable this way — that is the point of committing the images.

Screen Desktop 1440 Mobile 390
Care hub (#33) care hub care hub
Check-in + queue (#32) queue queue
Access log (#34) access log access log
Payment history (#37) payments payments
Receipt (#37) receipt receipt

The mobile receipt is the one image that changed code: at 390px the two-column label/value grid starved the value column and the 64-character transaction hash was unreadable, so PaymentReceipt.module.css collapses to one column below 760px. The #33 screenshots from the earlier commits are still in docs/screenshots/2026-10-16-patient-care-hub/, including the no-care-profile state that only a real session reaches today — useProfileStore is never populated by production code, so that remains a docforum-core API gap, not something to paper over with profileId === userId.

Commands run, with output

$ npm run typecheck        # tsc -b --noEmit
(no output)
TYPECHECK_EXIT=0

$ npx vitest run
 Test Files  25 passed (25)
      Tests  156 passed (156)
   Duration  11.15s
TEST_EXIT=0

$ npm run build            # tsc -b && vite build
dist/index.html                   2.90 kB │ gzip:   1.32 kB
dist/assets/index-Y0s0EwON.css   25.23 kB │ gzip:   4.73 kB
dist/assets/index-Dtczvj1E.js   444.48 kB │ gzip: 144.03 kB
✓ built in 3.99s
BUILD_EXIT=0

Node 24.21.0 locally; CI pins Node 20. The only CI gate I cannot reproduce is the runner itself.

Baseline, measured not remembered: origin/main is 11 files / 44 tests, all green (checked in a throwaway worktree of the untouched commit, node_modules symlinked). This PR is 25 files / 156 tests, so +14 files and +112 tests are attributable to this diff:

Issue New test files (count)
#33 ranking.test.ts (5), PatientDashboardPage.test.tsx (13, +5 for appointment-sourced cards)
#32 queue.test.ts (13), slot.test.ts (6), useAppointmentQueue.test.tsx (4), QueuePanel.test.tsx (11), AppointmentDetailPage.test.tsx (6), useBrowserNotification.test.ts (7)
#34 AccessLogPage.test.tsx (9)
#37 stroops.test.ts (6), status.test.ts (11), explorer.test.ts (2), PaymentHistoryPage.test.tsx (8), PaymentDetailPage.test.tsx (11)

Cases a reviewer should check rather than trust: queue.test.ts asserts two identical reads produce one notification and 3 → 2 → 1 produces three; AppointmentDetailPage.test.tsx asserts the queue route is never called for an id that is not the patient's own; PaymentHistoryPage.test.tsx and AccessLogPage.test.tsx each assert a 404 renders "not available for this deployment yet" and not an empty state; PaymentDetailPage.test.tsx asserts an unauthenticated visit never calls the service at all; PatientDashboardPage.test.tsx asserts a failed appointments request still renders the dependent and health cards.

Four act(...) warnings surfaced while writing these tests — permission resolution in useBrowserNotification.test.ts, the poll timer in useAppointmentQueue.test.tsx — are fixed by wrapping the awaited state update in act. The one warning that remains, in src/components/RequireAuth.test.tsx, predates this PR and is on a file this diff does not touch, so it is left alone rather than quietly improved.

Which ROADMAP.md item does this close/progress?

Phase W2. The care-hub line (#33) was checked off in 1e1d093; #32, #34 and #37 are now checked off with their issue links, each naming the unlanded core PR it is waiting on. The W2 header line names all of them. Changelog entries were added per issue, and the earlier entry recording the "stays unimplemented" verdict is annotated as corrected by the entries that follow it.

Deliberately left out

PRD flow

Implements the patient-side entry point of PRD §4 in docforum-core — the step that sequences book → attend → follow-up for one patient — plus the two accountability surfaces the PRD's escrow and privacy sections promise the patient: a receipt for money that left them, and a log of who read their record. No screen outside the patient's own flows changed.

Docs updated in this PR

  • ROADMAP.md
  • ARCHITECTURE.md
  • ARCHITECTURE_ESSENTIALS.md
  • PRD.md
  • N/A for this change

Ranks the dependents and health-profile data already available into Action needed / Coming up / Recent, fetches every source in parallel, and degrades one failing source to its own notice instead of blanking the hub. A source that cannot be queried yet (no care profile selected) reports unavailable rather than loading, so the screen no longer spins forever, and is never read as an empty profile. Sections for unbuilt features stay hidden; Coming up waits on DocForum#3.

Closes DocForum#33
…ocForum#33

Captured at 1440 and 390 CSS px against a local stub of docforum-core seeded with fixture dependents and health entries. The third image is the state reachable in a real session today, with no care-profile id to query health with.
@drips-wave

drips-wave Bot commented Oct 16, 2026

Copy link
Copy Markdown

@classikdev Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

…payment history with receipts

Three patient follow-ons to the care hub, each built against a documented assumed core contract so the UI lands ahead of the backend. A route that is not deployed yet reads as "not available for this deployment yet", never as an empty list or a zero.

DocForum#32: GET /appointments/mine (real) feeds the hub's Coming up section and the new /patient/appointments/:appointmentId detail page, which polls the assumed queue route every 15s and posts check-in. The id is matched against the patient's own appointments before the queue route is ever called.

DocForum#34: /account/access-log lists who opened what, when and why, with from/to/recordType filters and cursor paging, against the assumed GET /patients/me/access-log.

DocForum#37: /payments and /payments/:intentId read the assumed patient-scoped GET /payments/mine routes (the existing intents routes are admin-only), format stroops as XLM, word each escrow status, link the testnet explorer, and render a printable receipt.

All three names are me-scoped because middleware/auth.ts exposes only {id, role}, so the frontend cannot know its own patientProfileId.

Closes DocForum#32
Closes DocForum#34
Closes DocForum#37
…sktop and mobile widths

Captured at 1440 and 390 CSS px against a local stub of docforum-core seeded with fixture appointments, queue positions, access-log entries and payment intents, so the states behind core#129, core#45 and core#90 are reviewable before those backends land. The receipt shot is the reason the receipt's two-column grid collapses below 760px: at 390px the value column was starved and the transaction hash was unreadable.
@classikdev classikdev changed the title feat(patient): patient home as a care hub: what needs my attention now feat(patient): care hub, visit queue, access log and payment receipts Oct 16, 2026
@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@classikdev fix conflicts

@classikdev
classikdev marked this pull request as ready for review October 17, 2026 05:40
@classikdev

Copy link
Copy Markdown
Author

done @afeezorobsco-cyber

@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@classikdev

This PR has merge conflicts with main, which has moved on since this branch was created. Please rebase onto the latest main (or merge main into your branch), resolve the conflicts, and push. CI will then re-run, and the PR can be merged once it's conflict-free, CI is green and it's been reviewed. Thanks!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants