fix: improve camp analytics data fetching
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 (
ignoreflag) 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 ofbuildFunnelData(): replaced per-patient consultations/vitals/prescriptions calls with queue status-based counting usinggetPatientStatus(). 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 frommedical_camp_idtocamp_idforgetCampAnalytics,getMedicinesAnalytics, andgetDoctorsAnalytics. Properly omit query string when campId is null. -
src/pages/admin/CampAnalyticsPage.tsx— Concurrent analytics + doctors fetch viaPromise.all;ignoreflag 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.tsx—waitingForDoctornow uses funnel data instead of hardcoded 0; removed 5s auto-refresh interval (parent now handles at 30s); memoizedsetStageValuesprevents unnecessary re-renders; D3 rendering keyed by string join for stable identity. -
src/types/api.ts— ChangedAnalyticsRequest.medical_camp_idtocamp_id. -
tests/lib/api.test.ts— Updated all analytics API test expectations frommedical_camp_idtocamp_id; fixedvolunteerSignupForCamptest 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_id→camp_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
- Pull this branch
- Run the development server:
bun dev - Navigate to Admin → Analytics page and select a camp
- 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 ( ignoreflag,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:
- The status endpoint records the actual queue flow a patient passed through
- It returns a single array per patient instead of requiring N separate API calls
- It naturally produces a decreasing funnel (patients who reach stage N necessarily passed through stages 0..N-1)