Skip to content

fix: improve camp analytics data fetching

Suma Pullaiahgari requested to merge fix/camp-analytics into develop

Overview

Improves data fetching for camp analytics by fixing race conditions, correcting API query parameters, rewriting the funnel data computation to use queue status as the single source of truth, and optimizing concurrent data fetching patterns.

What does this MR do and why?

The camp analytics page had several data integrity and performance issues: analytics stat cards showed potentially stale/incorrect values from the backend /api/v1/analytics endpoint that didn't match patient-level data, the funnel chart data was computed by making N+1 calls to consultations/vitals/prescriptions per patient, API query parameters used the wrong key (medical_camp_id instead of camp_id), and component unmounts could trigger state updates on unmounted components.

This MR addresses all these issues by:

  • Rewriting buildFunnelData() to use the queue status endpoint (GET /api/v1/patients/{book_no}/status) as the single authoritative source, with concurrency-limited batch fetching
  • Adding proper cleanup logic (ignore flag) to prevent state updates after unmount
  • Fixing API query parameter keys across all analytics endpoints
  • Running analytics and doctors analytics fetches concurrently
  • Prioritizing funnel-computed values over backend analytics in stat cards

Changes Made

  • src/utils/funnel.ts — Complete rewrite of buildFunnelData(): replaced per-patient consultations/vitals/prescriptions calls with queue status-based counting using getPatientStatus(). Added all 8 funnel stages with proper status lists, concurrency-limited batch fetching (5 at a time), and correct handling of patients with no queue status.
  • src/lib/api.ts — Fixed query parameter key from medical_camp_id to camp_id for getCampAnalytics, getMedicinesAnalytics, and getDoctorsAnalytics. Properly omit query string when campId is null.
  • src/pages/admin/CampAnalyticsPage.tsx — Concurrent analytics + doctors fetch via Promise.all; ignore flag cleanup pattern for both analytics and funnel data fetches; funnel data prioritized in all stat cards and CSV report; medicines cache cleared per-camp; 30s auto-refresh moved from funnel chart to analytics page; null safety and camp ID validation.
  • src/components/CampFunnelChart.tsxwaitingForDoctor now uses funnel data instead of hardcoded 0; removed 5s auto-refresh interval (parent now handles at 30s); memoized setStageValues prevents unnecessary re-renders; D3 rendering keyed by string join for stable identity.
  • src/types/api.ts — Changed AnalyticsRequest.medical_camp_id to camp_id.
  • tests/lib/api.test.ts — Updated all analytics API test expectations from medical_camp_id to camp_id; fixed volunteerSignupForCamp test parameter shape.

Technical Details

Root cause of funnel data mismatch: The previous buildFunnelData() fetched consultations, vitals, and prescriptions for each patient individually, making up to 3N API calls. This was slow, fragile, and used incorrect heuristics (e.g., a consultation existing does not mean the patient actually reached the consultation stage in queue). The new implementation fetches the patient's queue status array from the backend, which contains ALL statuses the patient has passed through (e.g., ['vitals_waiting', 'vitals', 'waiting']). Each funnel stage then checks for membership in a predefined list of statuses that indicate the patient has "reached" that stage — giving a true decreasing funnel.

Race condition fix: Previously, fast camp switching could trigger state updates on unmounted components. Added the ignore flag pattern to both analytics and funnel data useEffect hooks, and a separate useEffect for clearing medicines cache on camp change.

Concurrent fetching: getCampAnalytics and getDoctorsAnalyticsWithNames now run in parallel rather than sequentially, reducing total load time.

API query fix: Backend expects camp_id but the code was sending medical_camp_id, which would cause the server to ignore the parameter and return data for all camps.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • Performance improvement
  • ️ Refactor (no functional changes)

Related Issues / References

Closes issues with:

  • Analytics stat cards showing incorrect values
  • Funnel data mismatch with backend queue status
  • API query parameter key mismatch (medical_camp_idcamp_id)
  • Race conditions on camp switching
  • N+1 API call pattern in funnel data computation

Screenshots or Screen Recordings

N/A — data integrity changes, no UI layout changes.

How to Set Up and Validate Locally

  1. Pull this branch
  2. Run the development server:
    bun dev
  3. Navigate to Admin → Analytics page and select a camp
  4. Verify:
    • Funnel chart numbers match backend queue statuses per patient
    • Stat cards show funnel-prioritized values (close to real-time)
    • CSV download includes correct funnel values
    • Switching camps quickly doesn't cause stale data or console errors
    • Analytics data refreshes every 30 seconds

Testing Done

  • Manual testing completed
  • Unit tests updated

Test Cases Covered:

Scenario Expected Result Status
Analytics API calls use camp_id param Tests verify query parameter
Null/undefined campId omits query string Tests verify no ? suffix
getDoctorsAnalyticsWithNames merges correctly Ensures all registered doctors appear
buildFunnelData returns correct shape All 9 funnel fields populated
Camp switching doesn't trigger stale state ignore flag prevents unmounted updates

Code Quality Checklist

Code Standards

  • Code follows project conventions (naming, structure, formatting)
  • No console.log() or debugger statements left in code (console.warn/debug retained for diagnostics)
  • No unused imports, variables, or functions
  • No duplicate code and use of existing components for reusability
  • TypeScript types are properly defined
  • ESLint and Prettier checks pass

React Best Practices

  • Cleanup patterns used (ignore flag, clearInterval)
  • State management is appropriate (local state)
  • No unnecessary re-renders (memoized setStageValues, string-keyed D3 effect)

API & Data Fetching

  • Loading and error states handled
  • API types defined in src/types/api.ts
  • Axios interceptors handle auth tokens correctly
  • Concurrent data fetching via Promise.all

Error Handling

  • Errors are caught and handled gracefully
  • Console warnings for camp_id mismatches
  • Null-safety guards for API responses

Known Limitations / Technical Debt

  • Funnel data still requires O(N/5) sequential batches (5 concurrent per batch); for camps with 500+ patients, the initial load may take several seconds. The 30-second refresh mitigates this for subsequent views.
  • Queue status as source of truth assumes the backend correctly records all status transitions — any gaps in the status array would undercount the funnel.

Additional Notes

The key design decision was to use GET /api/v1/patients/{book_no}/status as the single source of truth instead of combining data from consultations, vitals, and prescriptions. This is more reliable because:

  1. The status endpoint records the actual queue flow a patient passed through
  2. It returns a single array per patient instead of requiring N separate API calls
  3. It naturally produces a decreasing funnel (patients who reach stage N necessarily passed through stages 0..N-1)

Merge request reports

Loading