Skip to content

[ENG-831] feat: add Patient Medications and Diagnostic Reports pages with navigation links - #16612

Open
abhimanyurajeesh wants to merge 25 commits into
developfrom
ENG-831
Open

[ENG-831] feat: add Patient Medications and Diagnostic Reports pages with navigation links#16612
abhimanyurajeesh wants to merge 25 commits into
developfrom
ENG-831

Conversation

@abhimanyurajeesh

@abhimanyurajeesh abhimanyurajeesh commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Proposed Changes

Fixes ENG-831

Screenshoot

Mobile
image image image

if multiple users
image

image image

Desktop
image

Tagging: @ohcnetwork/care-fe-code-reviewers

Merge Checklist

  • Add specs that demonstrate the bug or test the new feature.
  • Update product documentation.
  • Ensure that UI text is placed in I18n files.
  • Prepare a screenshot or demo video for the changelog entry and attach it to the issue.
  • Request peer reviews.
  • Complete QA on mobile devices.
  • Complete QA on desktop devices.
  • Add or update Playwright tests for related changes

Summary by CodeRabbit

  • New Features
    • Delivered a redesigned patient portal: home, visits (upcoming/history), records hub (prescriptions & diagnostic reports), plus prescription and diagnostic report detail pages with flagged indicators.
    • Added patient profile selection/settings (including language) and an in-app patient switcher.
    • Introduced step-based appointment booking and an enhanced appointment success experience (calendar/share).
  • Bug Fixes
    • Improved patient deep-link and legacy records navigation to route users to the correct patient login and apply compatibility redirects.
    • Ensured sign-out cleanup clears the saved selected patient, and selection now persists reliably across sessions.

@abhimanyurajeesh
abhimanyurajeesh requested review from a team and Copilot July 30, 2026 11:03
@abhimanyurajeesh
abhimanyurajeesh requested a review from a team as a code owner July 30, 2026 11:03
@github-actions

Copy link
Copy Markdown

⚠️ Merge Checklist Incomplete

Thank you for your contribution! To help us review your PR efficiently, please complete the merge checklist in your PR description.

Your PR will be reviewed once you have marked the appropriate checklist items.

To update the checklist:

  • Change - [ ] to - [x] for completed items
  • Only check items that are relevant to your PR
  • Leave items unchecked if they don't apply

The checklist helps ensure code quality, testing coverage, and documentation are properly addressed.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Changes

Patient portal and booking experience

Layer / File(s) Summary
Patient authentication and entry routing
src/hooks/usePatientOtpLogin.ts, src/pages/PublicAppointments/auth/*, src/Routers/PublicRouter.tsx, src/components/Auth/Login.tsx
Patient OTP login is centralized, configurable, and routed through /patient/login; staff login remains in the shared login component.
Patient context and profile switching
src/Providers/PatientUserProvider.tsx, src/components/Patient/*, src/pages/Patient/SelectProfile.tsx, src/pages/Patient/PatientProfileSettings.tsx
Patient selection is persisted and derived from fetched profiles, with profile selection, switching, settings, and shared patient-shell navigation added.
Patient portal data and clinical records
src/types/emr/patientPortal/*, src/hooks/usePatientPortalData.ts, src/pages/Patient/*, src/pages/Patient/records/*
Patient-scoped appointments, prescriptions, and diagnostic reports are fetched, partitioned, summarized, and rendered across home, visits, records, and detail pages.
Step-based appointment booking
src/pages/PublicAppointments/BookingStepLayout.tsx, src/pages/PublicAppointments/BookFacility.tsx, src/pages/PublicAppointments/BookPractitioner.tsx, src/pages/PublicAppointments/Schedule.tsx
Booking uses shared progress layout, facility and practitioner selection, date/slot selection, reason entry, patient switching, and updated create/reschedule handling.
Registration and appointment completion
src/pages/PublicAppointments/PatientRegistration.tsx, src/pages/PublicAppointments/Success.tsx
Patient registration supports profile-flow entry and required organization depth, while appointment success adds calendar download, sharing, appointment details, and home navigation.
Environment and shared labels
.env, care.config.ts, src/vite-env.d.ts, public/locale/en.json
API configuration, OTP and support settings, and English translations for patient authentication, booking, profiles, visits, records, and shared labels are added or updated.

Possibly related PRs

Suggested reviewers: amjithtitus09, jacobjeevan

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: adding patient medications and diagnostic reports pages with navigation links.
Description check ✅ Passed The description includes the issue, screenshots, reviewer tag, and merge checklist, with UI text and QA items documented.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ENG-831

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 30, 2026

Copy link
Copy Markdown

Deploying care-preview with  Cloudflare Pages  Cloudflare Pages

Latest commit: 90fd169
Status: ✅  Deploy successful!
Preview URL: https://100439f2.care-preview-a7w.pages.dev
Branch Preview URL: https://eng-831.care-preview-a7w.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pages/Patient/PatientMedications.tsx`:
- Around line 39-46: Update the Collapsible state and detail queries in
src/pages/Patient/PatientMedications.tsx#L39-L46 and
src/pages/Patient/PatientDiagnosticReports.tsx#L40-L47: track each card’s isOpen
state, pass it to the corresponding Collapsible, and set the
getPrescription/getDiagnosticReport query enabled condition to isOpen && !!token
so details are fetched only after expansion.
- Around line 30-36: Replace the inline props type in PrescriptionCard with a
named PrescriptionCardProps interface in
src/pages/Patient/PatientMedications.tsx:30-36, and use that interface in the
component signature. Apply the same change to DiagnosticReportCard by extracting
and using a DiagnosticReportCardProps interface in
src/pages/Patient/PatientDiagnosticReports.tsx:31-37.
- Around line 39-90: Handle query errors explicitly in the affected render
paths: in src/pages/Patient/PatientMedications.tsx lines 39-90, show a
user-friendly detail error instead of an empty MedicationsTable; in lines
108-144, show a list error instead of “no prescriptions.” In
src/pages/Patient/PatientDiagnosticReports.tsx lines 40-92, show a detail error
instead of “no observations”; in lines 110-148, show a list error instead of “no
reports.” Use each query’s error state alongside loading and data checks, while
preserving existing empty-state behavior for successful empty responses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: deac220b-a84e-4284-a6f1-1f67e1529aac

📥 Commits

Reviewing files that changed from the base of the PR and between c560786 and 79ad661.

📒 Files selected for processing (6)
  • public/locale/en.json
  • src/Routers/PatientRouter.tsx
  • src/components/ui/sidebar/patient-nav.tsx
  • src/pages/Patient/PatientDiagnosticReports.tsx
  • src/pages/Patient/PatientMedications.tsx
  • src/types/emr/patientPortal/patientPortalApi.ts

Comment thread src/pages/Patient/PatientMedications.tsx Outdated
Comment thread src/pages/Patient/PatientMedications.tsx Outdated
Comment thread src/pages/Patient/PatientMedications.tsx Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds patient-portal pages for viewing medications (prescriptions) and diagnostic reports, wires them into the PatientRouter, and exposes navigation links in the patient sidebar.

Changes:

  • Added OTP patient-portal API routes for listing/retrieving prescriptions and diagnostic reports.
  • Introduced new patient pages to render prescriptions/medications and diagnostic reports with collapsible card layouts.
  • Added patient sidebar navigation entries and an English i18n label for “Diagnostic Reports”.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/types/emr/patientPortal/patientPortalApi.ts Adds OTP API route definitions for prescriptions and diagnostic reports.
src/Routers/PatientRouter.tsx Registers new patient-portal routes for medications and diagnostic reports.
src/pages/Patient/PatientMedications.tsx New patient-portal page rendering prescription list + per-prescription medication details.
src/pages/Patient/PatientDiagnosticReports.tsx New patient-portal page rendering diagnostic report list + results display.
src/components/ui/sidebar/patient-nav.tsx Adds sidebar links to the new patient-portal pages.
public/locale/en.json Adds diagnostic_reports translation key.

Comment on lines +39 to +46
const { data: detail, isLoading } = useQuery({
queryKey: ["portal-prescription", prescription.id],
queryFn: query(patientPortalApi.getPrescription, {
pathParams: { id: prescription.id },
headers: { Authorization: `Bearer ${token}` },
}),
enabled: !!token,
});
Comment on lines +31 to +47
function DiagnosticReportCard({
report,
token,
}: {
report: DiagnosticReportRead;
token?: string;
}) {
const { t } = useTranslation();

const { data: detail, isLoading } = useQuery({
queryKey: ["portal-diagnostic-report", report.id],
queryFn: query(patientPortalApi.getDiagnosticReport, {
pathParams: { id: report.id },
headers: { Authorization: `Bearer ${token}` },
}),
enabled: !!token,
});
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

🎭 Playwright Test Results

Status: ✅ Passed
Test Shards: 3

Metric Count
Total Tests 337
✅ Passed 337
❌ Failed 0
⏭️ Skipped 0

📊 Detailed results are available in the playwright-final-report artifact.

Run: #10602

Entire-Checkpoint: bc6a6d40b35a
Copilot AI review requested due to automatic review settings July 30, 2026 14:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 40 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (5)

.env:12

  • This PR changes the default REACT_CARE_API_URL in .env to https://develop-api.ohc.network, which is unrelated to the patient portal feature and conflicts with the README’s documented fallback (https://careapi.ohc.network). This will change the default backend used in local/dev setups for anyone pulling the branch.
# Care API URL without the /api prefix
REACT_CARE_API_URL=https://develop-api.ohc.network

src/Routers/PublicRouter.tsx:67

  • Comment typo: "deep-linking int" looks truncated/unfinished, which makes the intent unclear for future readers.
  // A signed-out visitor deep-linking int

src/pages/PublicAppointments/auth/PatientLogin.tsx:64

  • This introduces a new patient OTP login UX and routing entrypoint (/patient/login) but there’s no Playwright coverage for the patient login flow (send OTP → verify OTP → profile selection) or for the new patient portal navigation. Given existing Playwright coverage for staff login (tests/auth/login.spec.ts), adding at least a basic patient sign-in/navigation spec would help prevent regressions.
    src/pages/PublicAppointments/PatientRegistration.tsx:54
  • isGeoOrganizationComplete duplicates the geo-organization depth/leaf validation logic that already exists in src/components/Patient/PatientRegistration.tsx (isGeoOrganizationValid). Keeping two copies increases the risk of the public booking registration and staff registration drifting in behavior when the rules change.
    src/components/Auth/Login.tsx:77
  • The mode=patient query param is still read/persisted, but the /login screen no longer renders patient login. Visiting /login?mode=patient currently shows the staff login without redirecting, which breaks existing deep links/bookmarks and also stores an unsupported preference in localStorage.
  // Remember the last login mode
  useEffect(() => {
    localStorage.setItem(LocalStorageKeys.loginPreference, mode);
  }, [mode]);

Comment thread care.config.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 30

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Providers/PatientUserProvider.tsx (1)

43-53: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not render a failed patient lookup as an empty profile list.

After a query error, isLoading becomes false and userData?.results ?? [] becomes []; SelectProfile then tells the user no profiles are linked and offers profile creation. Expose and render a patient-query error state separately from a legitimate empty result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Providers/PatientUserProvider.tsx` around lines 43 - 53, Expose the error
state from the useQuery call in PatientUserProvider and pass it through the
provider’s rendering flow separately from patients. Update the SelectProfile
conditional so failed patient lookups render an error state rather than treating
userData?.results ?? [] as a legitimate empty profile list or offering profile
creation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@public/locale/en.json`:
- Around line 33-41: Move the newly introduced blood-group keys and later
patient-related keys in the English locale to the end of the locale object,
preserving their existing key names and translations. Keep all pre-existing
entries in their current order and ensure the new keys are appended rather than
inserted among existing translations.

In `@src/components/Patient/CancelAppointmentButton.tsx`:
- Around line 70-91: Replace the fixed format call on the `start` timestamp in
`CancelAppointmentButton` with locale-aware date/time formatting using the
active `i18n.language`, preserving medium date and short time styles before
passing `datetime` to the cancellation confirmation translation.

In `@src/components/Patient/PatientAppShell.tsx`:
- Around line 25-40: Update the TABS label handling and its usage in
PatientAppShell so translation calls use statically detectable literal keys
instead of passing tab.label to t(). Preserve each tab’s existing translation
key by using explicit t("home"), t("visits"), t("records"), and t("profile")
call sites or equivalent per-tab label accessors.
- Around line 245-252: Update the confirmation notice rendered by
PatientAppShell around switchedTo to use an appropriate ARIA live-region role,
such as status, so assistive technologies announce the “now showing” message
when the profile switch takes effect. Preserve the existing conditional
rendering and auto-dismiss behavior.

In `@src/components/Patient/PatientProfileCard.tsx`:
- Around line 14-16: Update the JSDoc for PatientProfileCard in
src/components/Patient/PatientProfileCard.tsx:14-16 to document its patient
profile-selection medical use case and selection semantics for assistive
technology. Also update the component documentation in
src/components/Patient/CancelAppointmentButton.tsx:30 to describe appointment
cancellation and the confirmation dialog’s accessibility behavior.
- Around line 21-31: Update the blood-group translation logic in the patient
display formatter to avoid constructing the t() key dynamically. Use a static
BloodGroupChoices-to-translation-key map or explicit branches so every
BLOOD_GROUP__* key is discoverable, while preserving the existing exclusion of
unknown or missing blood groups.

In `@src/hooks/usePatientOtpLogin.ts`:
- Around line 80-117: Update the error extraction in the verifyOtpRequest
mutation’s onError handler to read validation errors from the thrown response’s
established error-data properties: use error.errors for the manually thrown OTP
response and error.data for HTTP failures from mutate(), while retaining
rawMessage as the fallback. Remove the cause-based lookup and preserve the
existing OTP-specific selection and "invalid_otp" default behavior.

In `@src/hooks/usePatientPortalData.ts`:
- Around line 42-46: Update the appointment query key in usePatientPortalData to
use only the phoneNumber, since getAppointments returns the same phone-scoped
payload for every profile. Keep selectedPatient-based filtering in the existing
useMemo logic so profile switches reuse cached data without refetching.
- Around line 78-89: Expose isError from the useQuery result in the prescription
hook alongside isLoading, while retaining silent: true to suppress global
toasts. Apply the same change to usePatientDiagnosticReports so both hooks
return their query error state for pages to render retry/error states instead of
treating failures as empty data.

In `@src/pages/Patient/DiagnosticReportDetail.tsx`:
- Around line 92-113: Replace the observation results’ unassociated div/span
structure in the observations.map block with a semantic table, using table,
thead, tbody, tr, th, and td elements while preserving the existing grid styling
and displayed Test/Result/Reference content. Associate each data cell with its
corresponding column header and retain the current flagged, reference,
interpretation, and observation.id behavior.

In `@src/pages/Patient/index.tsx`:
- Around line 47-64: Update the Patient page’s QUICK_ACTIONS label resolution so
each translation key is exposed through explicit static t() calls rather than
dynamic t(action.key). Define the resolved labels or a labelKey mapping inside
the component with literal calls for book_appointment, prescriptions,
diagnostic_reports, and visits, then use those values when rendering each quick
action.

In `@src/pages/Patient/PatientRecords.tsx`:
- Around line 194-196: In the shownReports mapping, replace the
readyReports.includes(report) identity check with a direct status check using
the exported READY_REPORT_STATUSES symbol. Update isReady in the relevant
component flow so readiness is derived from report.status without relying on
shared object references or scanning readyReports.

In `@src/pages/Patient/PrescriptionDetail.tsx`:
- Around line 47-51: Update src/pages/Patient/PrescriptionDetail.tsx around the
useQuery call and loading branch to destructure isError, keeping the skeleton
only while loading and rendering a not-found/retry message once the query
settles without prescription; apply the same change to
src/pages/Patient/DiagnosticReportDetail.tsx around its report query and
rendering branch, using isError and the settled-without-report state.
- Around line 18-25: Extract the duplicated MetaField component into a shared
component with an optional value-size or className prop, preserving the default
styling. In src/pages/Patient/PrescriptionDetail.tsx#L18-L25 and
src/pages/Patient/VisitSummary.tsx#L32-L39, remove the local declarations and
import the shared component; in
src/pages/Patient/DiagnosticReportDetail.tsx#L28-L35, do the same and pass the
smaller value styling through the prop.

In `@src/pages/Patient/records/reportUtils.ts`:
- Around line 140-143: Update observationValueLabel so the fallback and final
placeholder decision use nullish checks rather than truthiness, preserving
numeric 0 as a displayed value while still returning "-" for null or undefined
results.
- Around line 61-67: Update the reference_range matching predicate in the
surrounding report utility to require at least one parseable bound before
accepting a range. Preserve the existing inclusive min/max comparisons and
undefined-bound behavior for ranges where either min or max is valid, while
ensuring entries with both bounds undefined do not match any value.

In `@src/pages/PublicAppointments/BookFacility.tsx`:
- Line 22: Centralize the booking wizard constants beside BookingStepLayout.tsx
and export TOTAL_STEPS, SLOT_STEP, and REASON_STEP from that shared module. In
src/pages/PublicAppointments/BookFacility.tsx lines 22-22 and
src/pages/PublicAppointments/BookPractitioner.tsx lines 16-16, remove the local
TOTAL_STEPS declarations and import the shared constant; in
src/pages/PublicAppointments/Schedule.tsx lines 43-45, move all three local
constants to the shared module and import them.
- Around line 28-39: Update districtOrganization to begin traversal at
patient?.geo_organization rather than its parent, and walk ancestors until the
level_cache === 1 district is found. Return that district-level organization,
while preserving undefined when no qualifying organization exists.

In `@src/pages/PublicAppointments/BookingStepLayout.tsx`:
- Around line 63-69: Add an accessible name to the progressbar element in
BookingStepLayout by providing an aria-label that describes the tracked booking
step; optionally add aria-valuetext conveying the current step and total steps
(for example, “Step 3 of 5”) while preserving the existing aria-valuenow,
aria-valuemin, and aria-valuemax attributes.

In `@src/pages/PublicAppointments/BookPractitioner.tsx`:
- Line 52: Update the back action in BookPractitioner to navigate to the
facility-list step route that renders BookFacility instead of
/nearby_facilities. Also update Schedule’s back action to return to
/patient/book/${facilityId} instead of goBack("/facility/${facilityId}"),
preserving the wizard’s step-by-step flow at both affected sites:
src/pages/PublicAppointments/BookPractitioner.tsx:52-52 and
src/pages/PublicAppointments/Schedule.tsx:326-326.

In `@src/pages/PublicAppointments/PatientRegistration.tsx`:
- Around line 373-382: Align the age validation boundary used by the schema’s
superRefine with the UI condition in the PatientRegistration age display: update
the schema to reject age 0 as well as negative values, matching the existing
enteredAge check for <= 0.
- Around line 172-177: Align token handling in PatientRegistration with the
patient context contract: update the createPatient mutation and the other
tokenData.token access near the submit flow to consistently either require a
guaranteed patientUserContext and remove optional access, or guard both token
reads when the context may be absent. Preserve the existing selected-patient
behavior while ensuring no optional tokenData is dereferenced unconditionally.
- Around line 56-64: Replace the PatientRegistrationProps object type alias with
an interface while preserving the existing optional facilityId and staffId
properties and their documentation.
- Around line 476-478: Update the submit Button in PatientRegistration so it
remains disabled through the entire patient registration and appointment-booking
flow, not just while isCreatingPatient is true. Include the booking-in-progress
state used by createAppointment in the disabled condition, preserving the
existing register_patient label and preventing a second submission while the
form remains mounted.

In `@src/pages/PublicAppointments/Schedule.tsx`:
- Around line 333-337: The hardcoded English date/time format must be replaced
with the repository’s localized formatter across all appointment timestamp
displays. Update Schedule.tsx lines 333-337, 240-242, and 417, plus Success.tsx
lines 159-161 and 104, using the active locale so weekday/month names and
12/24-hour conventions are localized; preserve the existing fallback behavior.
- Around line 185-215: Update the rescheduling flow around cancelAppointment’s
onSuccess and createAppointment so creation failures are handled explicitly:
show a distinct error explaining that the original appointment was cancelled and
the user must choose another slot, then keep the user on the slot-selection step
instead of advancing. Add the necessary onError handling for the create
operation while preserving normal cancellation and creation behavior.

In `@src/pages/PublicAppointments/Success.tsx`:
- Around line 70-93: Handle the missing-auth case explicitly in the component
before the appointment lookup/not-found rendering: when tokenData?.token is
absent, render the established patient login prompt or redirect behavior instead
of “appointment not found.” Keep the existing query and appointment rendering
unchanged for authenticated users.
- Around line 113-123: Update the clipboard fallback in handleShare to guard
navigator.clipboard availability and catch writeText failures, showing an
appropriate translated error toast instead of allowing an unhandled rejection.
Reuse an existing translation key if available; otherwise add the required error
key to public/locale/en.json and use it in the failure path.

In `@src/Routers/PatientRouter.tsx`:
- Around line 114-117: Update the appointmentPages branch in the PatientRouter
route rendering logic to wrap appointmentPages with the same ErrorBoundary used
by the pages branch, while preserving the existing PatientUserProvider wrapper
and fallback configuration.

In `@src/Routers/PublicRouter.tsx`:
- Around line 65-75: Update the isPatientPath check in PublicRouter to match
only the exact "/patient" path or paths beginning with "/patient/"; keep
excluding "/patient/login" and preserve the existing redirect behavior for valid
patient deep links.

---

Outside diff comments:
In `@src/Providers/PatientUserProvider.tsx`:
- Around line 43-53: Expose the error state from the useQuery call in
PatientUserProvider and pass it through the provider’s rendering flow separately
from patients. Update the SelectProfile conditional so failed patient lookups
render an error state rather than treating userData?.results ?? [] as a
legitimate empty profile list or offering profile creation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 59613ba8-df4a-48a2-9e2d-ab420cd86177

📥 Commits

Reviewing files that changed from the base of the PR and between 79ad661 and 605f469.

📒 Files selected for processing (38)
  • .env
  • care.config.ts
  • public/locale/en.json
  • src/Providers/AuthUserProvider.tsx
  • src/Providers/PatientUserProvider.tsx
  • src/Routers/PatientRouter.tsx
  • src/Routers/PublicRouter.tsx
  • src/common/constants.tsx
  • src/components/Auth/Login.tsx
  • src/components/Patient/CancelAppointmentButton.tsx
  • src/components/Patient/PatientAppShell.tsx
  • src/components/Patient/PatientProfileCard.tsx
  • src/components/Patient/PatientSwitcherSheet.tsx
  • src/hooks/usePatientOtpLogin.ts
  • src/hooks/usePatientPortalData.ts
  • src/pages/Landing/LandingPage.tsx
  • src/pages/Organization/components/GovtOrganizationSelector.tsx
  • src/pages/Patient/DiagnosticReportDetail.tsx
  • src/pages/Patient/PatientProfileSettings.tsx
  • src/pages/Patient/PatientRecords.tsx
  • src/pages/Patient/PatientVisits.tsx
  • src/pages/Patient/PrescriptionDetail.tsx
  • src/pages/Patient/SelectProfile.tsx
  • src/pages/Patient/VisitSummary.tsx
  • src/pages/Patient/components/AppointmentDialog.tsx
  • src/pages/Patient/index.tsx
  • src/pages/Patient/records/reportUtils.ts
  • src/pages/PublicAppointments/BookFacility.tsx
  • src/pages/PublicAppointments/BookPractitioner.tsx
  • src/pages/PublicAppointments/BookingStepLayout.tsx
  • src/pages/PublicAppointments/PatientRegistration.tsx
  • src/pages/PublicAppointments/PatientSelect.tsx
  • src/pages/PublicAppointments/Schedule.tsx
  • src/pages/PublicAppointments/Success.tsx
  • src/pages/PublicAppointments/auth/PatientAuthLayout.tsx
  • src/pages/PublicAppointments/auth/PatientLogin.tsx
  • src/types/otp/otp.ts
  • src/vite-env.d.ts
💤 Files with no reviewable changes (2)
  • src/pages/PublicAppointments/PatientSelect.tsx
  • src/pages/Patient/components/AppointmentDialog.tsx

Comment thread public/locale/en.json
Comment on lines +70 to +91
const start = dayjs(appointment.token_slot.start_datetime);

return (
<>
<Button
variant="outline"
size={size}
className={className}
onClick={() => setOpen(true)}
>
{t("cancel_appointment")}
</Button>

<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent className="max-w-[440px]">
<AlertDialogHeader>
<AlertDialogTitle>{t("cancel_appointment")}</AlertDialogTitle>
<AlertDialogDescription>
{t("patient_visits__cancel_confirmation", {
name: formatScheduleResourceName(appointment),
datetime: start.format("ddd, D MMM YYYY · h:mm A"),
})}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use locale-aware formatting for the appointment timestamp.

The fixed Day.js format hard-codes ordering and a 12-hour clock. Format the value with the active locale, such as Intl.DateTimeFormat(i18n.language, { dateStyle: "medium", timeStyle: "short" }).

As per coding guidelines, “localized medical timestamps must use localized date/time formatting.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Patient/CancelAppointmentButton.tsx` around lines 70 - 91,
Replace the fixed format call on the `start` timestamp in
`CancelAppointmentButton` with locale-aware date/time formatting using the
active `i18n.language`, preserving medium date and short time styles before
passing `datetime` to the cancellation confirmation translation.

Source: Coding guidelines

Comment on lines +25 to +40
const TABS = [
{ key: "home", href: "/patient/home", icon: Home, label: "home" },
{
key: "visits",
href: "/patient/visits",
icon: CalendarDays,
label: "visits",
},
{
key: "records",
href: "/patient/records",
icon: FolderClosed,
label: "records",
},
{ key: "profile", href: "/patient/profile", icon: User, label: "profile" },
] as const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

i18n keys passed via variable aren't statically detectable.

t(tab.label) resolves keys from TABS at runtime, so the repo's remove-unused-i18n.js cleanup can't see home/visits/records/profile and may prune them. Store the full call site statically, e.g. put a label: () => t("home")-style accessor or inline literal t() calls per tab.

Based on learnings: to ensure the remove-unused-i18n.js cleanup script detects i18n keys, avoid passing computed keys directly to t(); prefer explicit literal calls or a precomputed shared key.

Also applies to: 193-193

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Patient/PatientAppShell.tsx` around lines 25 - 40, Update the
TABS label handling and its usage in PatientAppShell so translation calls use
statically detectable literal keys instead of passing tab.label to t(). Preserve
each tab’s existing translation key by using explicit t("home"), t("visits"),
t("records"), and t("profile") call sites or equivalent per-tab label accessors.

Source: Learnings

Comment thread src/components/Patient/PatientAppShell.tsx Outdated
Comment on lines +14 to +16
/**
* `34 yrs · Female · O+`, dropping the blood group when it is not recorded.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Document medical use and accessibility considerations for both patient UI components.

  • src/components/Patient/PatientProfileCard.tsx#L14-L16: document its profile-selection use case and selection semantics for assistive technology.
  • src/components/Patient/CancelAppointmentButton.tsx#L30-L30: document its appointment-cancellation use case and confirmation-dialog accessibility behavior.

As per coding guidelines, “Document components with their medical use cases and accessibility considerations.”

📍 Affects 2 files
  • src/components/Patient/PatientProfileCard.tsx#L14-L16 (this comment)
  • src/components/Patient/CancelAppointmentButton.tsx#L30-L30
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Patient/PatientProfileCard.tsx` around lines 14 - 16, Update
the JSDoc for PatientProfileCard in
src/components/Patient/PatientProfileCard.tsx:14-16 to document its patient
profile-selection medical use case and selection semantics for assistive
technology. Also update the component documentation in
src/components/Patient/CancelAppointmentButton.tsx:30 to describe appointment
cancellation and the confirmation dialog’s accessibility behavior.

Source: Coding guidelines

Comment on lines +333 to +337
? dayjs(selectedSlot.start_datetime).format(
"ddd D MMM · h:mm A",
)
: "-"}
</span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Appointment timestamps use hardcoded English dayjs patterns. "ddd D MMM · h:mm A" bypasses locale-aware weekday/month names and 12/24-hour conventions, so these clinical timestamps stay English under every non-English locale. Use the repo's localized date/time formatting helper (or dayjs with the active i18n locale loaded).

  • src/pages/PublicAppointments/Schedule.tsx#L333-L337: format the selected-slot summary via the localized formatter (same for the reason-step footer at Lines 240-242 and the slot time at Line 417).
  • src/pages/PublicAppointments/Success.tsx#L159-L161: format the appointment date/time via the localized formatter (same for the shared text at Line 104).

As per coding guidelines, "localized medical timestamps must use localized date/time formatting".

📍 Affects 2 files
  • src/pages/PublicAppointments/Schedule.tsx#L333-L337 (this comment)
  • src/pages/PublicAppointments/Success.tsx#L159-L161
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/PublicAppointments/Schedule.tsx` around lines 333 - 337, The
hardcoded English date/time format must be replaced with the repository’s
localized formatter across all appointment timestamp displays. Update
Schedule.tsx lines 333-337, 240-242, and 417, plus Success.tsx lines 159-161 and
104, using the active locale so weekday/month names and 12/24-hour conventions
are localized; preserve the existing fallback behavior.

Source: Coding guidelines

Comment thread src/pages/PublicAppointments/Success.tsx
Comment on lines +113 to +123
if (navigator.share) {
try {
await navigator.share({ title: summaryTitle, text });
} catch {
// Dismissing the native share sheet is not an error worth surfacing.
}
return;
}

await navigator.clipboard.writeText(text);
toast.success(t("copied_to_clipboard"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Clipboard fallback can throw unhandled.

navigator.clipboard is undefined in non-secure contexts and writeText rejects when permission is denied; handleShare is an unguarded async click handler, so the failure surfaces as an unhandled rejection with no user feedback.

🛡️ Proposed fix
-    await navigator.clipboard.writeText(text);
-    toast.success(t("copied_to_clipboard"));
+    try {
+      await navigator.clipboard.writeText(text);
+      toast.success(t("copied_to_clipboard"));
+    } catch {
+      toast.error(t("copy_to_clipboard_failed"));
+    }

Confirm the error key exists in public/locale/en.json (add it if not).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (navigator.share) {
try {
await navigator.share({ title: summaryTitle, text });
} catch {
// Dismissing the native share sheet is not an error worth surfacing.
}
return;
}
await navigator.clipboard.writeText(text);
toast.success(t("copied_to_clipboard"));
if (navigator.share) {
try {
await navigator.share({ title: summaryTitle, text });
} catch {
// Dismissing the native share sheet is not an error worth surfacing.
}
return;
}
try {
await navigator.clipboard.writeText(text);
toast.success(t("copied_to_clipboard"));
} catch {
toast.error(t("copy_to_clipboard_failed"));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/PublicAppointments/Success.tsx` around lines 113 - 123, Update the
clipboard fallback in handleShare to guard navigator.clipboard availability and
catch writeText failures, showing an appropriate translated error toast instead
of allowing an unhandled rejection. Reuse an existing translation key if
available; otherwise add the required error key to public/locale/en.json and use
it in the failure path.

Comment thread src/Routers/PatientRouter.tsx
Comment on lines +65 to +75
const path = usePath();

// A signed-out visitor deep-linking int
const isPatientPath =
!!path && path.startsWith("/patient") && path !== "/patient/login";

return (
<>
<BrowserWarning />
{routeResult || <Login />}
{routeResult ||
(isPatientPath ? <Redirect to="/patient/login" /> : <Login />)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

startsWith("/patient") can false-positive match unrelated paths.

Without a trailing slash, any unmatched path beginning with the literal string "/patient" (e.g. /patients, /patient-registration) is treated as a patient deep link and redirected to /patient/login, even if it isn't actually under the /patient/... namespace.

🐛 Proposed fix
-  const isPatientPath =
-    !!path && path.startsWith("/patient") && path !== "/patient/login";
+  const isPatientPath =
+    !!path &&
+    (path === "/patient" || path.startsWith("/patient/")) &&
+    path !== "/patient/login";
#!/bin/bash
rg -n '"/patient' src/Routers -g '*.tsx' -g '*.ts'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Routers/PublicRouter.tsx` around lines 65 - 75, Update the isPatientPath
check in PublicRouter to match only the exact "/patient" path or paths beginning
with "/patient/"; keep excluding "/patient/login" and preserve the existing
redirect behavior for valid patient deep links.

bodhish added 4 commits July 30, 2026 21:05
… a scale

Replaced 90 arbitrary font sizes (53 of them sub-pixel: 12.5px, 13.5px,
10.5px, 14.5px, 11.5px, 9.5px) with Tailwind scale classes. A single
screen rendered 8-11 distinct sizes with no ratio between them.

Scale is now 10 / xs(12) / sm(14) / base(16) / lg(18) plus three
display sizes. Max visual change is 1.5px.

Entire-Checkpoint: ee2aa8184c79
The patient portal is mobile-first but several primary controls were
well under the 44px tap minimum: 'See all' at 20px (below even the
WCAG 2.2 AA 24px floor), Records/Visits header tabs at 34px, filter
chips at 33px, the patient switcher chip at 42px, and the logo home
link at 28px.

All now min-h-11. Filter chips lose their vertical padding in favour
of the min-height so the pill keeps its proportions.

Entire-Checkpoint: fd291f30c9fd
View Details, Reschedule and Cancel Appointment used Button size="sm"
(h-8, 32px). These are the primary actions on the upcoming appointment
card, so they get a min-h-11 floor. Same for the cancel dialog's
confirm/dismiss pair.

Entire-Checkpoint: 007b8d19f32c
Entire-Checkpoint: c316d7900a98
Copilot AI review requested due to automatic review settings July 30, 2026 17:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 55 out of 57 changed files in this pull request and generated no new comments.

Suppressed comments (6)

src/pages/PublicAppointments/PatientRegistration.tsx:193

  • reason comes from query params and can be undefined when the URL has no reason parameter; calling .trim() will throw and break the post-registration appointment creation. Use optional chaining and default to an empty string instead (also removes the redundant ?? "" after trim()).
    src/pages/PublicAppointments/auth/PatientAuthLayout.tsx:63
  • Clickable <img> elements are not keyboard-accessible and don’t announce as an interactive control to assistive tech. Wrap the logo in a <button> (or a <Link>) so it’s focusable and has proper semantics.
    src/pages/PublicAppointments/Success.tsx:84
  • navigator.clipboard.writeText can fail (permissions, insecure context, unsupported browser) and currently any failure will result in an unhandled rejection and no user feedback. Handle errors and show a fallback toast.
    .env:12
  • This PR is focused on patient portal pages/navigation, but it also changes the committed default REACT_CARE_API_URL. Since .env is tracked (and not gitignored), this alters the default API target for anyone pulling the branch and can unintentionally affect builds/deployments. If this isn’t intentional, revert the value or move it to a non-committed env file (e.g. .env.local).
REACT_CARE_API_URL=https://develop-api.ohc.network

src/pages/PublicAppointments/auth/PatientLogin.tsx:51

  • This introduces a new end-to-end patient OTP login flow (consent → send OTP → verify → profile selection) and new booking/records/visits pages, but there are no Playwright specs covering /patient/login or the patient portal routes. Adding at least one Playwright test for successful sign-in + profile select and a smoke test for records/visits navigation would help prevent regressions.
    src/Routers/PatientRouter.tsx:117
  • BrowserWarning is rendered for dashboard routes but not for AppointmentRoutes (booking / registration pages). This makes unsupported-browser handling inconsistent and can hide the warning on some patient flows. Render <BrowserWarning /> in the appointmentPages branch as well.
        <PatientUserProvider>
          <ErrorBoundary fallback={<ErrorPage forError="PAGE_LOAD_ERROR" />}>
            {appointmentPages}
          </ErrorBoundary>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grumpy Review 🔥

The architecture here is actually reasonable — extracting usePatientOtpLogin, building out a proper PatientAppShell with a shared switcher, and wiring up the localStorage-backed patient selection properly. Begrudgingly: it's a decent refactor.

That said, there are issues you need to address:

  1. The .env API URL change is the most alarming thing in this PR. Fix it before merge.
  2. path.startsWith tab detection — works today, breaks silently later. Add the trailing slash guard.
  3. BookingStepLayout double space — run Prettier.
  4. extractOtpErrorMessage type-casting — not wrong enough to block, but it's the kind of code that bites you six months from now.
  5. PatientUserProvider double navigation — not a hard bug but worth tidying up.

This is also a draft PR, so you presumably know it's not ready. When you are ready: write those Playwright tests mentioned in the checklist. "Add specs" being unchecked on a feature this size is not an acceptable final state.

Generated by Grumpy PR Reviewer for issue #16612 · 71.1 AIC · ⌖ 6.22 AIC · ⊞ 6.3K

import mutate from "@/Utils/request/mutate";

const extractOtpErrorMessage = (error: unknown, fallback: string) => {
const cause = (error as { cause?: unknown })?.cause;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole function is a tower of as casts on unknown. It works, barely, but it's fragile — if the backend error shape changes, this silently falls through and you get the generic fallback with no indication why. At least use typeof x === "object" && x !== null type guards so TypeScript can actually help you here.

Comment thread .env Outdated

# Care API URL without the /api prefix
REACT_CARE_API_URL=https://careapi.ohc.network
REACT_CARE_API_URL=https://develop-api.ohc.network

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 You're committing develop-api.ohc.network as the default API URL. That was careapi.ohc.network before. Anyone who clones this repo now gets pointed at the dev API by default. This is almost certainly a "works on my machine" leftover. Revert this before it bites someone in production.

const tabBar = (
<>
{TABS.map((tab) => {
const isActive = path?.startsWith(tab.href);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path?.startsWith("/patient/home") is a trap. If someone ever adds a route like /patient/homepage or /patient/home-visit, the Home tab will light up incorrectly. Use path === tab.href || path?.startsWith(tab.href + "/") — or at minimum add a trailing-slash guard for the home route specifically.

<div className=" xl:w-2/3 xl:mx-auto">{headerExtra}</div>
</header>

<div className="flex flex-1 flex-col xl:w-2/3 xl:mx-auto">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double space in the className: "flex flex-1 flex-col xl:w-2/3 xl:mx-auto". Harmless, but embarrassing. Prettier should have caught this.

// unauthenticated (or signed-out) visitor to the patient login rather than
// the public landing page.
useEffect(() => {
if (!tokenData) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have navigate("/patient/login") inside a useEffect AND if (!tokenData) { return null } at line 100. The effect fires asynchronously so there's a render gap where this component returns null and the navigate hasn't resolved yet — flickers are possible. Pick one: either return null and navigate synchronously in the render body (before hooks, which is a no-no) or use only the effect. The effect is fine; just make sure the return null below doesn't cause a blank flash while raviger processes the navigation.

Copilot AI review requested due to automatic review settings August 4, 2026 10:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 56 out of 59 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/pages/PublicAppointments/PatientRegistration.tsx:193

  • reason comes from query params and can be undefined; calling reason.trim() will throw at runtime and break the booking flow after patient creation. Guard it before trimming.
    src/pages/PublicAppointments/Success.tsx:85
  • If navigator.share is unavailable, navigator.clipboard.writeText can throw (e.g. insecure context, permission denied, unsupported API), which would create an unhandled rejection and no user feedback. Wrap clipboard access in try/catch and surface a generic error toast on failure.
    src/Providers/PatientUserProvider.tsx:95
  • This redirect triggers whenever patients.length === 0 and the query is no longer loading. If the patients query fails (network/API error), userData stays undefined and patients becomes [], so the user will be redirected to the profile picker as if they had no linked profiles. Gate this redirect on having received userData successfully.
    if (
      tokenData &&
      !isLoading &&
      patients.length === 0 &&
      !isProfileSetupPath

.env:12

  • This PR changes the default REACT_CARE_API_URL in the committed .env. That alters the default backend target for anyone running the repo without a custom .env.local, and it isn’t mentioned in the PR description. If this isn’t intentional, revert it (or move it to a documented sample env file).
REACT_CARE_API_URL=https://develop-api.ohc.network

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grumpy but fair assessment

The patient portal redesign is architecturally solid — the PatientUserProvider refactor with proper localStorage persistence, the usePortalInfiniteList abstraction for server-paged data, and separating data-fetching into usePatientPortalData.ts are all good decisions.

But I found 5 things that need fixing before this ships:

  1. .env API URL — committed develop-api.ohc.network as the default. Revert immediately.
  2. usePatientAppointments — all appointments fetched for the phone number then client-side filtered by patient ID. The queryKey also doesn't include selectedPatient?.id, meaning cached data from one patient bleeds into another. Fix the queryKey and preferably server-filter.
  3. DiagnosticReportDetail — no isError handling. Perpetual skeleton on failure.
  4. PrescriptionDetail — no isLoading or isError destructured from useQuery. Flashes empty state while loading; silent failure on error.
  5. PrescritionList typo — it exists upstream, yes. You don't have to fix it everywhere today, but don't add more references to a misspelled type name.

The rest of it is... fine. Good, even. The SelectProfile auto-skip for single-patient accounts is a nice touch.

Generated by Grumpy PR Reviewer for issue #16612 · 91 AIC · ⌖ 6.27 AIC · ⊞ 6.3K

Comment thread .env Outdated

# Care API URL without the /api prefix
REACT_CARE_API_URL=https://careapi.ohc.network
REACT_CARE_API_URL=https://develop-api.ohc.network

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 Seriously? You're committing a change to the default API URL pointing at develop-api.ohc.network in a feature PR? This will redirect every developer who clones this repo away from the production API. Revert this to careapi.ohc.network. Dev-only URL overrides belong in .env.local, not in the committed .env.


return useMemo(() => {
const appointments = (data?.results ?? [])
.filter((appointment) => appointment.patient.id === selectedPatient?.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The queryKey is ["appointment", phoneNumber], but you filter the results by selectedPatient?.id client-side. You're fetching every appointment for the entire phone number and filtering in JS. Worse, the cache key doesn't include the patient ID — when a user switches profiles, React Query serves the same stale list until a refetch. Either pass patient as a query param to the server (like you correctly do for prescriptions and reports), or at minimum include selectedPatient?.id in the queryKey so stale data isn't served for the wrong patient.

const { t } = useTranslation();
const { tokenData } = usePatientContext();

const { data: report, isLoading } = useQuery({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You destructure isLoading from useQuery but never destructure isError. When this request fails (404, 500, network error — take your pick), the user is stuck staring at skeletons forever. Add isError to the destructuring and render an error state. Medical reports that silently fail to load are a usability nightmare.

const { t } = useTranslation();
const { tokenData } = usePatientContext();

const { data: prescription } = useQuery({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only destructure { data: prescription } — no isLoading, no isError. While the page fetches, it renders with empty medications and falls through to the empty state. The user sees "no medications found" for a second before the data loads. And if the request errors out? Same empty state, no indication that something went wrong. Destructure isLoading and isError and handle them properly.

import { DiagnosticReportRead } from "@/types/emr/diagnosticReport/diagnosticReport";
import {
PrescriptionRead,
PrescritionList,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PrescritionList — missing the 'c' in Prescription. Yes, this typo originates in prescription.ts (line 26), but this is your chance to fix it. You're creating a new API file right now. An exported interface named PrescritionList that's already used in 3+ places is technical debt that compounds with every new reference you add here.

@abhimanyurajeesh
abhimanyurajeesh marked this pull request as ready for review August 4, 2026 11:08
Copilot AI review requested due to automatic review settings August 4, 2026 11:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 56 out of 59 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/pages/PublicAppointments/PatientRegistration.tsx:193

  • reason comes from useQueryParams() and can be undefined; calling reason.trim() will throw at runtime, breaking patient registration after a successful create. Use optional chaining (and only trim when present).
    src/Utils/observationRange.ts:85
  • formatRangeBounds treats empty strings as present bounds (e.g. min === "" renders as "> "), but earlier in this file toNumber() explicitly treats "" as missing. If the API sends "" for absent bounds (as the comment suggests), this will render incorrect range text.
  const hasMin = min !== null && min !== undefined;
  const hasMax = max !== null && max !== undefined;

src/pages/Patient/PatientVisits.tsx:127

  • The History tab’s empty-state copy still uses the Upcoming-tab wording/key. You already added patient_visits__no_history to i18n; use it here so the UI matches the selected tab.
    .env:13
  • The committed .env now points REACT_CARE_API_URL to develop-api.ohc.network, but both README.md and .example.env document https://careapi.ohc.network as the default fallback. This mismatch is likely to confuse local development and onboarding; prefer keeping .env aligned with docs (and use .env.local for developer-specific overrides).
# Care API URL without the /api prefix
REACT_CARE_API_URL=https://develop-api.ohc.network
REACT_SBOM_BASE_URL=https://sbom.ohc.network

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

The patient portal redesign is architecturally sound — the move from a sidebar-driven layout to a mobile-first app shell, consolidating sign-out into AuthUserProvider, and the new usePatientPortalData hook are all steps in the right direction. That said, there are a handful of issues that need addressing before this merges:

Must fix:

  • .env API URLREACT_CARE_API_URL points to develop-api.ohc.network. That's the wrong default for a default config file that ships in the repo.
  • QR code encodes raw patient UUID — Use the booking reference or token number instead; the internal DB ID shouldn't be baked into a shareable QR code.
  • PatientShellContext is never provided — The context is created and a value object is assembled, but <PatientShellContext.Provider> is missing from the JSX. Consumers will silently get the default (no-op) value.

Should fix:

  • usePatientOtpLogin error strings — The hook sets error state with bare i18n keys ("phone_number_validation_error", "invalid_otp", etc.) without translating them. Verify consumers call t() on these, or translate at the source.
  • Auth guard in PatientUserProvider — Navigating on every tokenData === null render, before the auth context has finished hydrating, risks a redirect loop. Add a loading guard.
  • Client-side appointment filtering — Fetching all appointments then filtering by patient ID in JS wastes bandwidth on multi-patient accounts. Pass the patient ID to the API.

Grumpy but not hopeless — the bones are good, fix the sharp edges.

Generated by Grumpy PR Reviewer for issue #16612 · 120.7 AIC · ⌖ 6.38 AIC · ⊞ 6.3K

Comments that could not be inline-anchored

.env:12

Oh, fantastic. The default REACT_CARE_API_URL has been quietly swapped from the production API (careapi.ohc.network) to the dev API (develop-api.ohc.network). Anyone cloning this repo — or any CI build that sources .env — now points at the dev backend by default. That's not a configuration, that's a footgun. Revert this or at least add a screaming comment explaining why develop is the new default.

src/components/Patient/AppointmentTokenPass.tsx:98

You're encoding the patient's internal UUID directly into a QR code that's rendered on-screen and potentially screenshotted/shared. That UUID can be used to correlate the patient across API calls. If the QR is meant to be scanned at a counter, use the booking reference ID (appointment.id.slice(0, 8)) or the token number — something that's already shown in the same card. Don't give the raw database ID to scanners who may not need it.

src/hooks/usePatientOtpLogin.ts:116

setPhoneError(&quot;phone_number_validation_error&quot;) — you're passing a bare i18n key as the error string, but is anyone calling t() on it before rendering? If the error is rendered directly as {phoneError}, users see the raw key. Check that every consumer of phoneError and otpError passes them through t(). If not, either translate here or document the contract. Same applies to the hardcoded &quot;send_otp_error&quot; and &quot;invalid_otp&quot; strings below.

src/Providers/PatientUserProvider.tsx:78

Navigating inside a useEffect that fires whenever tokenData changes is asking for trouble. If PatientUserProvider is mounted before the auth context has fully initialised (e.g. during the very first render), tokenData will be null and you'll immediately redirect to /patient/login — which likely triggers a re-render, which re-fires this effect. At minimum, wait until the auth context has finished loading before deciding to redirect. Add a loading guard here.

src/hooks/usePatientPortalData.ts:70

You're fetching every appointment for the phone number and then filtering by selectedPatient?.id on the client. When a phone has multiple linked patients with many appointments, that's a lot of data transferred to silently throw most of it away. Pass the patient ID as a query parameter to the API so the server does the filtering — your bandwidth bill and your users on slow mobile connections will thank you.

src/components/Patient/PatientAppShell.tsx:1152

PatientShellContext is created here but I don't see it exported or provided anywhere in the tree below. PatientAppShell creates the context value but never wraps children in a &lt;PatientShellContext.Provider&gt;, so any descendant calling useContext(PatientShellContext) will always get the default { openSwitcher: () =&gt; {}, canSwitch: false }. Either export and provide it, or remove the context entirely and pass props directly.

Copilot AI review requested due to automatic review settings August 4, 2026 11:34

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grumpy ReviewTM

This is a substantial feature — new portal pages, hooks, routing, shell component. The architecture is generally sound. But there are a handful of real problems worth fixing before merge:

Must fix:

  • .env API URL — you committed a dev-API URL as the new default. This breaks any environment that sources this file expecting to hit production. Revert it.
  • PrescriptionDetail has no error state — permanent skeletons on query failure. DiagnosticReportDetail has the same issue (it does destructure isLoading but no error handling).
  • PatientShellContext is dead code — context is provided but never consumed. Either wire it up with an exported hook or remove it.

Should fix:

  • All three new files import dayjs directly instead of from "@/Utils/dayjs". This is an inconsistency that will bite someone when they add a plugin-dependent call.
  • queryKey: unknown[] in usePortalInfiniteList is lazy typing in a file that otherwise has good type coverage.
  • The usePatientAppointments client-side filter vs cache-key design needs a comment explaining the intent, otherwise the next reader will "fix" it and break the patient-switcher performance.

The first name extraction (name.trim().split(/\s+/)[0]) is duplicated between PatientRecords.tsx and PatientAppShell.tsx's abbreviateName. Minor, but worth a shared utility.

Otherwise: the routing redirects for legacy paths, the infinite scroll integration, and the overall component structure are... fine. Begrudgingly fine.

Generated by Grumpy PR Reviewer for issue #16612 · 73.2 AIC · ⌖ 6.38 AIC · ⊞ 6.3K

Comment thread .env Outdated

# Care API URL without the /api prefix
REACT_CARE_API_URL=https://careapi.ohc.network
REACT_CARE_API_URL=https://develop-api.ohc.network

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh fantastic, you committed the dev API URL to the shared .env. The old value pointed at the production API; this now silently redirects anyone who pulls this branch—or any CI job that sources .env—to the dev backend. Either leave .env alone or add a comment making it clear this is a local-dev-only override. Changing the default in the committed file is how you break everyone else's environment.

@@ -0,0 +1,236 @@
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import dayjs from "dayjs";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entire project uses import dayjs from "@/Utils/dayjs" — the plugin-extended instance that has relativeTime and other plugins pre-loaded. Bare import dayjs from "dayjs" skips that setup. Right now .isAfter() doesn't need a plugin, but the moment someone calls .fromNow() or similar in this file, it silently returns Invalid Date. Use the project's shared instance.

queryParams,
enabled,
}: {
queryKey: unknown[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

queryKey: unknown[] is too loose. You're basically telling TypeScript "don't help me here." The factory function already knows the shape — use a typed tuple or at least readonly (string | number | undefined)[]. unknown[] defeats the purpose of having TypeScript.

const { token, phoneNumber, headers } = useAuthHeaders();

const { data, isLoading } = useQuery({
queryKey: ["appointment", phoneNumber],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

queryKey is ["appointment", phoneNumber] but the results are filtered by selectedPatient?.id. If the user switches profiles, the query key doesn't change, so TanStack Query returns cached data instantly and filters it client-side — fine. But this means the whole appointment list for the phone number is always fetched regardless of which patient is selected. At least document this intent; right now it looks like a missing cache-key dependency.

const SWITCH_NOTICE_MS = 3000;

/** Lets descendant screens open the shell's shared patient switcher sheet. */
const PatientShellContext = createContext<{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PatientShellContext is created and provided but never consumed anywhere — no exported hook, no useContext call outside this file. The context value (openSwitcher, canSwitch) is dead code. Either export a usePatientShell() hook so descendant screens can actually open the switcher, or delete the context and just wire things up via props/callbacks directly. As written it does nothing.

@@ -0,0 +1,130 @@
import { useQuery } from "@tanstack/react-query";
import dayjs from "dayjs";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same bare dayjs import problem as in usePatientPortalData.ts — use "@/Utils/dayjs". Also: import dayjs from "dayjs" here (line 2) vs import { useTranslation } in line 3 violates the project's import ordering convention (3rd-party alphabetical, then internal). Run npm run format before committing.

const { t } = useTranslation();
const { tokenData } = usePatientContext();

const { data: prescription } = useQuery({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useQuery is called without destructuring isLoading. When the query is loading or has errored, prescription is undefined, so the skeleton renders forever on a network error. The user stares at spinning bones with no way to retry. Destructure isLoading and isError, render an error state, and add retry: 1 or similar. Medical data pages need proper error handling.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 57 out of 60 changed files in this pull request and generated no new comments.

Suppressed comments (6)

src/pages/PublicAppointments/PatientRegistration.tsx:193

  • reason comes from query params and can be undefined; calling reason.trim() will throw and block patient registration. Use optional chaining (and the nullish fallback becomes meaningful).
    src/Utils/observationRange.ts:89
  • formatRangeBounds treats empty-string bounds ("") as present, which can render placeholders like "> "/"< ". The module already treats "" as absent in toNumber, so the formatter should normalize empty strings too.
  const hasMin = min !== null && min !== undefined;
  const hasMax = max !== null && max !== undefined;
  if (hasMin && hasMax) return `${min} - ${max}`;
  if (hasMin) return `> ${min}`;
  if (hasMax) return `< ${max}`;

src/Routers/PatientRouter.tsx:64

  • <Redirect> from raviger doesn’t support a query prop, so these redirects will drop tab=... and always land on the default records tab. Include the query string in the to URL instead.
  "/patient/medications": () => (
    <Redirect to="/patient/records" query={{ tab: "prescriptions" }} />
  ),
  "/patient/diagnostic_reports": () => (
    <Redirect to="/patient/records" query={{ tab: "reports" }} />

src/pages/PublicAppointments/auth/PatientAuthLayout.tsx:37

  • An <img> with onClick is not keyboard-accessible and won’t be announced as an interactive control. Wrap the logo in a <button>/link so it’s focusable and has an accessible name.
    src/components/Common/LoginHeader.tsx:50
  • The dropdown trigger <button> has no accessible name (its contents are icons marked/treated as decorative), so screen readers will announce it poorly. Add an aria-label (or a visually-hidden text label).
            <button
              type="button"
              className="flex items-center gap-2 rounded-full border border-gray-200 bg-gray-50 py-1.5 pl-1.5 pr-3 hover:border-gray-300"
            >

.env:12

  • Changing the committed default REACT_CARE_API_URL to develop-api will silently change the backend used by anyone relying on this repo’s .env. If this was only needed for a local/testing setup, it’s safer to keep the previous default (or move the override to .env.local, which shouldn’t be committed).
# Care API URL without the /api prefix
REACT_CARE_API_URL=https://develop-api.ohc.network

Copilot AI review requested due to automatic review settings August 4, 2026 12:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 56 out of 59 changed files in this pull request and generated no new comments.

Suppressed comments (7)

src/pages/PublicAppointments/PatientRegistration.tsx:193

  • reason comes from query params and can be undefined; calling reason.trim() will throw at runtime during patient creation. Use a null-safe trim so booking without a reason (or without the param) doesn’t crash the flow.
    src/pages/PublicAppointments/auth/PatientAuthLayout.tsx:38
  • The logo is made clickable via onClick on an <img>, which isn’t keyboard-accessible and doesn’t expose an interactive role to assistive tech. Wrap it in a <button> (or <Link>) so it’s reachable and operable via keyboard.
    src/components/Patient/PatientSwitcherSheet.tsx:50
  • max-w-120 is not part of Tailwind’s default scale and isn’t added in tailwind.config.js, so the sheet width constraint will be ignored. Use an arbitrary value (or an existing max-w token) to ensure the layout matches the intended 480px column.
        className="mx-auto max-h-[85dvh] max-w-120 overflow-y-auto rounded-t-3xl px-5 pb-6 pt-3 [&>button:first-of-type]:hidden"

src/components/Patient/VisitCard.tsx:86

  • w-13 is not a default Tailwind width token and isn’t defined in tailwind.config.js, so the date tile width won’t apply. Use an arbitrary width (or an existing spacing token) to keep the layout consistent.
          <div className="w-13 shrink-0 rounded-xl border border-primary-200 bg-white py-1.5 text-center">

src/pages/Patient/SelectProfile.tsx:51

  • h-19 is not a default Tailwind height token and isn’t defined in tailwind.config.js, so these skeleton rows won’t have the intended height. Consider using an arbitrary height (or a standard token) to match the design.
    src/components/Patient/PrescriptionRow.tsx:62
  • size-4.25 is not a default Tailwind size token and isn’t defined in tailwind.config.js, so the chevron size won’t apply. Use an arbitrary size-[…] value or a standard size token.
        className="size-4.25 shrink-0 text-gray-600"

src/pages/Patient/VisitSummary.tsx:231

  • h-15.5 is not a default Tailwind spacing token and isn’t defined in tailwind.config.js, so the sentinel placeholder height will be ignored. Use an arbitrary height (or a standard token) so the list doesn’t jump when fetching more rows.

Copilot AI review requested due to automatic review settings August 4, 2026 12:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 56 out of 59 changed files in this pull request and generated no new comments.

Suppressed comments (3)

src/pages/PublicAppointments/PatientRegistration.tsx:193

  • reason comes from query params and can be undefined; calling reason.trim() will throw and prevent appointment creation. Use optional chaining (or a fallback string) before trimming.
    src/pages/Patient/PatientVisits.tsx:128
  • The empty state for the History tab uses the upcoming-appointments description key, which is misleading. There is already a dedicated patient_visits__no_history string in en.json.
    src/pages/PublicAppointments/auth/PatientAuthLayout.tsx:37
  • The logo is implemented as a clickable <img>, which is not keyboard-accessible and doesn’t convey button/link semantics to assistive tech. Wrap it in a <button> (or a link) and keep the image purely presentational inside.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grumpy Review: Patient Portal Redesign

Big feature, generally clean. The pattern of centralising all portal data fetching in usePatientPortalData.ts is the right call, and the useMemo for selectedPatient resolution in PatientUserProvider with the localStorage persistence is done correctly.

That said, four things need addressing before this ships:

  1. Appointment pagination (usePatientPortalData.ts L97) — the appointments hook doesn't paginate. The prescriptions and reports hooks do. This is a latent data-completeness bug.
  2. Client-side appointment filtering (L103) — inconsistent with the server-side patient scoping everywhere else. Fix or document why it's intentional here.
  3. PrescritionList typo (patientPortalApi.ts L11) — propagating a misspelling. Fix it at the source.
  4. Fragile Tailwind DOM hack (PatientSwitcherSheet.tsx L56) — [&>button:first-of-type]:hidden will break silently on any shadcn upgrade.
  5. Non-localized date formats (DiagnosticReportDetail.tsx L73/78) — hard-coded "DD MMM, h:mm A" in a multi-locale healthcare app is a bad look.

The rest is... fine, I guess. The infinite-scroll abstraction is reusable, the profile-selection redirect logic is solid, and removing AppointmentDialog.tsx in favour of the new flows is the right call.

Generated by Grumpy PR Reviewer for issue #16612 · 56.6 AIC · ⌖ 7.54 AIC · ⊞ 6.3K

export default {
listPrescriptions: {
path: "/api/v1/otp/medication_prescription/",
method: HttpMethod.GET,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the imported type name. PrescritionList is missing a 'p' — it should be PrescriptionList. This pre-existing typo in the source type is now spreading to every consumer of this API file. Fix it at the source before it metastasizes further.

<MetaField
label={t("reported")}
value={dayjs(report.created_date).format("DD MMM, h:mm A")}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded date format, not localized. "DD MMM, h:mm A" is baked in on lines 73 and 78. This is a patient-facing healthcare app deployed in multiple locales — dates should be formatted with dayjs locale-aware helpers or at minimum routed through a shared utility. Same issue on line 78.

aria-hidden
className="mx-auto mb-4 block h-1 w-10 rounded-full bg-gray-300"
/>
<SheetHeader className="space-y-1 text-left">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[&>button:first-of-type]:hidden is a fragile DOM-structure hack. You're reaching into shadcn's SheetContent internals to hide the default close button. If the component library ever changes its internal structure (it has before), this silently stops working and the close button reappears. Use a SheetClose without a trigger, or pass hideClose / a similar controlled prop to your SheetContent wrapper instead.


return useMemo(() => {
const appointments = (data?.results ?? [])
.filter((appointment) => appointment.patient.id === selectedPatient?.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No pagination here. getAppointments almost certainly returns a paginated response, but you're using a plain useQuery with no limit/offset handling. If a patient has more than whatever the default page size is, history and upcoming counts will be silently wrong. The prescriptions and diagnostic reports correctly use usePortalInfiniteList — appointments deserve the same treatment.

new Date(a.token_slot.start_datetime).getTime() -
new Date(b.token_slot.start_datetime).getTime(),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Client-side patient filtering is inconsistent with the rest of the portal. Prescriptions and diagnostic reports pass patient: selectedPatient?.id as a server-side query param. Appointments fetch all patients' data for the phone number and filter client-side. That's fine for the Success page (intentional), but for the Visits page you're shipping extra data over the wire and doing unnecessary work on the client. Pass patient_id (or whatever the backend accepts) as a query param here if the API supports it.

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

Labels

needs review needs testing Type Changes Contains changes in typescript types

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants