Skip to content

feat: GPS tab enhancements #32

Merged
akkaouim merged 3 commits intojjackson:labs-mainfrom
akkaouim:labs-mbw-v3
Mar 2, 2026
Merged

feat: GPS tab enhancements #32
akkaouim merged 3 commits intojjackson:labs-mainfrom
akkaouim:labs-mbw-v3

Conversation

@akkaouim
Copy link
Collaborator

@akkaouim akkaouim commented Mar 2, 2026

— sortable columns, aligned names, Dist. Ratio, aggregate map

  • Make all GPS tab columns sortable (With GPS, Unique Cases, Max Revisit Dist.)
  • Rename columns to match Overview tab (Avg Case Dist → Revisit Dist., Max Case Dist → Max Revisit Dist.)
  • Add cases_with_revisits denominator "(N)" to Revisit Dist. in both GPS and Overview tabs
  • Add Meter/Visit column to GPS tab (with color coding)
  • Add Dist. Ratio column to both GPS and Overview tabs (revisit dist / meter per visit)
  • Add collapsible aggregate map showing all FLW visits with per-user HSL color coding and MarkerCluster
  • Make GPS drill-down visit table sortable (Date, Form, Entity, Revisit Dist., Status)
  • Fix sticky header displacement when aggregate map or drill-down expands
  • Extract all_coordinates in views.py for aggregate map data
  • Update Guide tab, DOCUMENTATION.md, and DASHBOARD_GUIDE.md

Summary by CodeRabbit

  • New Features

    • Added Dist. Ratio and Meter/Visit metrics, plus per-worker Revisit denominator (cases with 2+ visits)
    • Aggregate Map of all visits (collapsible, color-coded pins, clustering) and sortable GPS columns across views
    • GPS drill-down shows Revisit Dist. and supports client-side sorting of detail rows
  • Documentation

    • Updated guides with new metrics, calculations, examples, renamed metrics (Revisit Dist. / Max Revisit Dist.) and red-flag updates
  • Bug Fixes

    • Improved GPS location fallback when primary GPS is missing

…atio, aggregate map

- Make all GPS tab columns sortable (With GPS, Unique Cases, Max Revisit Dist.)
- Rename columns to match Overview tab (Avg Case Dist → Revisit Dist., Max Case Dist → Max Revisit Dist.)
- Add cases_with_revisits denominator "(N)" to Revisit Dist. in both GPS and Overview tabs
- Add Meter/Visit column to GPS tab (with color coding)
- Add Dist. Ratio column to both GPS and Overview tabs (revisit dist / meter per visit)
- Add collapsible aggregate map showing all FLW visits with per-user HSL color coding and MarkerCluster
- Make GPS drill-down visit table sortable (Date, Form, Entity, Revisit Dist., Status)
- Fix sticky header displacement when aggregate map or drill-down expands
- Extract all_coordinates in views.py for aggregate map data
- Update Guide tab, DOCUMENTATION.md, and DASHBOARD_GUIDE.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

📝 Walkthrough

Walkthrough

Adds per‑FLW revisit counts, median meters‑per‑visit, a Dist. Ratio metric, renamed revisit distance fields, an aggregate "All Visits" GPS map, gps_location fallback logic, and wires these into analysis, views, serializers, templates, and docs.

Changes

Cohort / File(s) Summary
Documentation
commcare_connect/workflow/templates/mbw_monitoring/DASHBOARD_GUIDE.md, commcare_connect/workflow/templates/mbw_monitoring/DOCUMENTATION.md
Renamed distance metrics (Revisit Dist / Max Revisit Dist), added Dist. Ratio and Meter/Visit definitions, noted revisit denominator (N), documented Aggregate Map (all_coordinates) behavior, updated column counts and sortable column notes.
GPS analysis / Data model
commcare_connect/workflow/templates/mbw_monitoring/gps_analysis.py
Added cases_with_revisits: int to FLWSummary; compute and populate count of unique mother cases with distance_from_prev_case_visit != None in analysis/build functions.
Views / Backend assembly
commcare_connect/workflow/templates/mbw_monitoring/views.py
Compute and include per‑FLW cases_with_revisits and median_meters_per_visit; extract all_coordinates for aggregate map; add gps_location fallback to row location in cache/rebuild paths; merge new fields into gps_data and overview.
Serialization
commcare_connect/workflow/templates/mbw_monitoring/serializers.py
Expose new cases_with_revisits in serialized FLW summary output.
Frontend / Template
commcare_connect/workflow/templates/mbw_monitoring/template.py
Added UI state for gpsDetailSort, showAggregateMap, aggregateMapRef, aggregateMarkersRef; introduced dist_ratio column across Overview/GPS tables with sorting; render Aggregate GPS Map and per‑FLW legend; compute/display dist_ratio and adjust table/detail column spans and sorting behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Views as Views (views.py)
    participant GA as GPS Analysis (gps_analysis.py)
    participant Ser as Serializers (serializers.py)
    participant UI as Frontend (template.py)
    participant Map as Aggregate Map UI

    Views->>GA: send visits for analysis
    GA->>GA: compute per‑FLW metrics (revisit distances, median_meters_per_visit, cases_with_revisits)
    GA-->>Views: return FLWSummary objects with new fields
    Views->>Views: extract all_coordinates from visits
    Views->>Ser: hand off enhanced gps_data + flw_summaries
    Ser-->>UI: JSON payload (includes cases_with_revisits, median_meters_per_visit, all_coordinates)
    UI->>UI: compute dist_ratio for items, update gpsDetailSort state
    UI->>Map: render aggregate map using all_coordinates and per‑FLW legend
    Map->>Map: display color‑coded pins and clusters
    UI->>UI: render tables with new Dist. Ratio column and sorting
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped through lines of GPS code,

counting revisits down each road.
Pins cluster bright on the map I span,
Dist. Ratio hums — a careful plan.
Carrots and flags for every FLW hand!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'feat: GPS tab enhancements' is vague and generic, using a non-descriptive term 'enhancements' that doesn't convey the specific changes made to the GPS tab features. Consider a more specific title that highlights key changes, such as 'feat: Add sorting, new metrics, and aggregate map to GPS tab' or 'feat: Implement GPS columns sortability and Dist. Ratio metric'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@commcare_connect/workflow/templates/mbw_monitoring/template.py`:
- Around line 577-583: The effect in React.useEffect accesses
gpsData.all_coordinates without guarding for gpsData being undefined, which can
throw during refresh/loading transitions; update the React.useEffect block (and
the similar usages around the gpsData references at the other spots mentioned)
to first check gpsData exists (or use a safe default like const coords =
(gpsData && gpsData.all_coordinates) || []) before using it, and ensure any
other reads of gpsData (the ones near lines referencing gpsData at 623-625 and
648) use the same guard or optional chaining so aggregateMapRef.current and
rendering logic do not crash when gpsData is undefined.
- Around line 630-635: The popup currently interpolates unescaped values into
HTML in the marker.bindPopup call (flwName, c.e, c.d), creating an XSS risk; fix
by escaping these data fields before composing the HTML (e.g., add and use a
small helper like escapeHtml(value) and apply it to flwName, c.e and c.d) or
build the popup using DOM text nodes instead of raw string concatenation, then
pass the safe string/element to marker.bindPopup so only the intentional flagged
span remains unescaped.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5fe21d and 5147829.

📒 Files selected for processing (6)
  • commcare_connect/workflow/templates/mbw_monitoring/DASHBOARD_GUIDE.md
  • commcare_connect/workflow/templates/mbw_monitoring/DOCUMENTATION.md
  • commcare_connect/workflow/templates/mbw_monitoring/gps_analysis.py
  • commcare_connect/workflow/templates/mbw_monitoring/serializers.py
  • commcare_connect/workflow/templates/mbw_monitoring/template.py
  • commcare_connect/workflow/templates/mbw_monitoring/views.py

- Add null guard for gpsData.all_coordinates in aggregate map useEffect
  (prevents crash during refresh/loading transitions)
- Add escapeHtml helper and apply to all three bindPopup calls
  (aggregate map, drill-down visit markers, drill-down mother markers)
  to prevent XSS from entity names, dates, or FLW display names

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
commcare_connect/workflow/templates/mbw_monitoring/template.py (1)

578-648: ⚠️ Potential issue | 🟠 Major

Tear down/recreate the aggregate map when its container disappears.

Line 583 and Line 586 can return without clearing aggregateMapRef.current, and Line 648 does not track tab changes. This can leave Leaflet bound to a detached DOM node after refresh/tab-switch and break map rendering when returning.

💡 Suggested fix
 React.useEffect(function() {
-    if (!leafletReady || !showAggregateMap) {
+    var shouldShow = leafletReady && showAggregateMap && activeTab === 'gps';
+    if (!shouldShow) {
         if (aggregateMapRef.current) { aggregateMapRef.current.remove(); aggregateMapRef.current = null; }
+        aggregateMarkersRef.current = null;
         return;
     }
     var coords = (gpsData && gpsData.all_coordinates) || [];
-    if (coords.length === 0) return;
+    if (coords.length === 0) {
+        if (aggregateMapRef.current) { aggregateMapRef.current.remove(); aggregateMapRef.current = null; }
+        aggregateMarkersRef.current = null;
+        return;
+    }
 
     var mapDiv = document.getElementById('aggregate-gps-map');
-    if (!mapDiv) return;
+    if (!mapDiv) {
+        if (aggregateMapRef.current) { aggregateMapRef.current.remove(); aggregateMapRef.current = null; }
+        aggregateMarkersRef.current = null;
+        return;
+    }
@@
-}, [leafletReady, showAggregateMap, gpsData, filterFlws]);
+}, [leafletReady, showAggregateMap, activeTab, gpsData, filterFlws]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@commcare_connect/workflow/templates/mbw_monitoring/template.py` around lines
578 - 648, The effect can early-return without removing the Leaflet map, leaving
aggregateMapRef.current bound to a detached DOM node; before every early return
(the checks using leafletReady || showAggregateMap, coords.length === 0, and
!mapDiv) ensure you call the same teardown used at the end: if
(aggregateMarkersRef.current && aggregateMapRef.current) remove the layer and
set aggregateMarkersRef.current = null; if (aggregateMapRef.current) call
aggregateMapRef.current.remove() and set aggregateMapRef.current = null. Also
ensure the effect's dependency array includes the tab/state that controls
visibility (e.g., showAggregateMap or the parent tab identifier) so the cleanup
runs on tab changes; update the effect dependencies (currently [leafletReady,
showAggregateMap, gpsData, filterFlws]) to include any missing tab variable used
to show/hide the map.
🧹 Nitpick comments (1)
commcare_connect/workflow/templates/mbw_monitoring/template.py (1)

1663-1673: Avoid mutating state-derived rows while computing dist_ratio.

Line 1664 and Line 1669 mutate objects coming from dashData-derived arrays during render. Prefer immutable derivation to avoid side effects and stale cross-view behavior.

♻️ Suggested refactor
-// Compute dist_ratio for GPS and Overview (revisit_distance_km * 1000 / median_meters_per_visit)
-filteredGpsFlws.forEach(function(g) {
-    g.dist_ratio = (g.avg_case_distance_km != null && g.median_meters_per_visit != null && g.median_meters_per_visit > 0)
-        ? Math.round(g.avg_case_distance_km * 1000 / g.median_meters_per_visit * 10) / 10
-        : null;
-});
-filteredOverview.forEach(function(f) {
-    f.dist_ratio = (f.revisit_distance_km != null && f.median_meters_per_visit != null && f.median_meters_per_visit > 0)
-        ? Math.round(f.revisit_distance_km * 1000 / f.median_meters_per_visit * 10) / 10
-        : null;
-});
+// Compute dist_ratio for GPS and Overview (revisit_distance_km * 1000 / median_meters_per_visit)
+filteredGpsFlws = filteredGpsFlws.map(function(g) {
+    return Object.assign({}, g, {
+        dist_ratio: (g.avg_case_distance_km != null && g.median_meters_per_visit != null && g.median_meters_per_visit > 0)
+            ? Math.round(g.avg_case_distance_km * 1000 / g.median_meters_per_visit * 10) / 10
+            : null
+    });
+});
+filteredOverview = filteredOverview.map(function(f) {
+    return Object.assign({}, f, {
+        dist_ratio: (f.revisit_distance_km != null && f.median_meters_per_visit != null && f.median_meters_per_visit > 0)
+            ? Math.round(f.revisit_distance_km * 1000 / f.median_meters_per_visit * 10) / 10
+            : null
+    });
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@commcare_connect/workflow/templates/mbw_monitoring/template.py` around lines
1663 - 1673, The code mutates dashData-derived rows by assigning g.dist_ratio
and f.dist_ratio inside filteredGpsFlws.forEach and filteredOverview.forEach;
instead create new arrays (e.g., use filteredGpsFlws = filteredGpsFlws.map(g =>
({...g, dist_ratio: computedValue})) and similarly for filteredOverview) so you
derive dist_ratio immutably without mutating the original objects; keep the
exact computation (use avg_case_distance_km and revisit_distance_km with
median_meters_per_visit checks) but return new objects rather than assigning to
g.dist_ratio or f.dist_ratio.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@commcare_connect/workflow/templates/mbw_monitoring/template.py`:
- Around line 578-648: The effect can early-return without removing the Leaflet
map, leaving aggregateMapRef.current bound to a detached DOM node; before every
early return (the checks using leafletReady || showAggregateMap, coords.length
=== 0, and !mapDiv) ensure you call the same teardown used at the end: if
(aggregateMarkersRef.current && aggregateMapRef.current) remove the layer and
set aggregateMarkersRef.current = null; if (aggregateMapRef.current) call
aggregateMapRef.current.remove() and set aggregateMapRef.current = null. Also
ensure the effect's dependency array includes the tab/state that controls
visibility (e.g., showAggregateMap or the parent tab identifier) so the cleanup
runs on tab changes; update the effect dependencies (currently [leafletReady,
showAggregateMap, gpsData, filterFlws]) to include any missing tab variable used
to show/hide the map.

---

Nitpick comments:
In `@commcare_connect/workflow/templates/mbw_monitoring/template.py`:
- Around line 1663-1673: The code mutates dashData-derived rows by assigning
g.dist_ratio and f.dist_ratio inside filteredGpsFlws.forEach and
filteredOverview.forEach; instead create new arrays (e.g., use filteredGpsFlws =
filteredGpsFlws.map(g => ({...g, dist_ratio: computedValue})) and similarly for
filteredOverview) so you derive dist_ratio immutably without mutating the
original objects; keep the exact computation (use avg_case_distance_km and
revisit_distance_km with median_meters_per_visit checks) but return new objects
rather than assigning to g.dist_ratio or f.dist_ratio.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5147829 and d4e07a3.

📒 Files selected for processing (1)
  • commcare_connect/workflow/templates/mbw_monitoring/template.py

…t_ratio

- Extract teardown() helper in aggregate map useEffect and call it on all
  three early-return paths (leaflet not ready, no coordinates, no map div)
  to prevent detached DOM node leaks
- Add activeTab to aggregate map useEffect deps so cleanup runs on tab switch
- Use cleanup ref (aggregateMapRef.current) instead of local var in return fn
- Replace mutating forEach with immutable .map + Object.assign for dist_ratio
  computation on filteredGpsFlws and filteredOverview

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@commcare_connect/workflow/templates/mbw_monitoring/template.py`:
- Around line 628-635: The loop in activeCoords.forEach computes flwName by
scanning gpsFlws for each point (and similarly elsewhere), causing O(points ×
flws) cost; to fix, build a one-time lookup map from gpsFlws keyed by username
(e.g., flwLookup = {}; gpsFlws.forEach(g => flwLookup[g.username] =
g.display_name || g.username)) and then replace the inner linear scan in
activeCoords.forEach (and the other occurrence) to do a constant-time
flwLookup[c.u] lookup for flwName, preserving existing fallbacks and reusing
flwColorMap as before.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d4e07a3 and ca38a7c.

📒 Files selected for processing (1)
  • commcare_connect/workflow/templates/mbw_monitoring/template.py

@akkaouim akkaouim merged commit b3bd3d7 into jjackson:labs-main Mar 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant