Skip to content

My Dashboard Due checklist card missing on 18.0; shows a real count on 17.x #2102

Description

@ombhardwajj

Summary

For organisations with showDueChecklistOnDashboard enabled, the Due checklist section is missing from My Dashboard on 18.0. On 17.x it showed a real count.

It fails silently — no error and no empty card, the row simply is not rendered — which is why it went unnoticed for months.

Raised during code review of #2022. It is not caused by that card's fix; it is an independent #1865 regression that the #2022 work made fully visible. Filed separately because #2022's acceptance criteria do not cover it (see Why this is not #2022) and because it needs a different fix.


What must be true when this is done

  1. An org with showDueChecklistOnDashboard enabled sees the Due checklist section on My Dashboard with a correct count, on 18.0, for the same data 17.x counted.
  2. Tapping it opens the checklist listing with the matching subjects.
  3. An org without the setting still sees no Due checklist section, and pays nothing for it.
  4. It holds across the My Dashboard counts blank after navigating away and back; app restart restores them #2022 navigation: leaving My Dashboard and coming back does not change or blank the number.
  5. The count honours the same filters as the other cards — subject type, location, program, and a My Dashboard custom filter.
  6. Verified on Realm and on SQLite.
  7. The card never displays a count it cannot open a listing for.

The regression

80085652a (#1865 | Lazy-load MyDashboard entity lists on card tap, 1 Apr 2026) made the entity lists lazy to remove a 40s white screen, and gave the six visit/registration cards real count queries (countScheduledVisits and friends — SELECT COUNT on SQLite, .length on Realm).

The due checklist got no query. It got a placeholder:

dueChecklist: 0,

and MyDashboardView.renderableVisits (MyDashboardView.js:110) drops any row whose count is 0:

if (visit.visits.dueChecklist) return visit.visits.dueChecklist.count !== 0

so the section stops rendering entirely.

Before #1865 every card took its number the same way — _.mapValues(queryResult, v => v && v.length || 0) — so the due checklist number was simply the length of the list dueChecklistForDefaultDashboard returned.

Measured, not inferred

Driving MyDashboardActions.onLoad against a stubbed IndividualService whose dueChecklistForDefaultDashboard returns three due subjects, across first load / re-entry / manual refresh:

Build auto-refresh on auto-refresh off
pre-#1865 (beb37c350~1, 17.x) real count 3 real count 3
18.0 before the #2022 fix (dd201d5ee) 0 — row hidden undefined — row rendered with a blank number; 0 after a manual refresh
18.0 after the #2022 fix (35825dbeb) 0 — row hidden 0 — row hidden

The card was already gone before the #2022 fix landed, in the configuration #2022 specifies for testing — auto-refresh on, the default, and the setting its reproduction used (anjali@jss_uat, recorded as disableAutoRefresh: false). The #2022 fix changed only the auto-refresh-off column, normalising an undefined count to 0, so a row that rendered with a blank number became a hidden row: a change between two already-broken states, not the cause.

Why this is not #2022

#2022's acceptance criteria concern the six cards in its recorded run — Scheduled, Overdue, Registrations, Enrolments, Visits, Total. Its test org (nupoork@ntest) has no checklists, so Due checklist appears in none of its columns.

Its AC 7 reads "The same steps on a 17.1 build and an 18.0 build produce the same numbers", and "the same steps" is anchored by that observed-run table. Reading it to cover a card absent from those steps stretches it past what was measured on either build.


Why this cannot be a count-only query

The obvious fix — give the due checklist a countDueChecklist alongside the other six — is not available. Two independent reasons:

1. "Due" is not stored; it is computed. dueChecklists runs a rule per checklist item:

let applicableState = items.calculateApplicableState();
if (applicableState.status && applicableState.status.state === "Due") { ... }

Nothing in the database records that an item is due — it is derived at read time from the item's definition and the subject's data. There is no SELECT COUNT to write, and obtaining the count means loading the enrolments and evaluating every item. Counting costs exactly what building the list costs.

2. This card does not rebuild its list on tap. Every other card dispatches ON_LIST_LOAD. This one does not — StatusCountRow.js:36 hands the already-built list straight to ChecklistListingView:

if (title === "dueChecklist") {
    setTimeout(() => TypedTransition.from(this).with({
        ...
        results: this.props.dueChecklist,

so the hydrated list has to be in reducer state or the drill-down opens on nothing.

Together these rule the approach out: a count-only query would cost the same as the list and produce a tappable card leading to an empty listing — worse than the card being hidden. Hence AC 7 above.

A constraint worth knowing, not addressed here

dueChecklists hardcodes the program name:

.filtered('voided = false AND individual.voided = false AND program.name = $0', 'Child')

The card only ever considers enrolments in a program literally named Child. This is long-standing — it predates #1865 and is untouched by this card. It has two consequences: it is what bounds the scan, and an org whose checklist programme is named anything else gets nothing from this card. Changing it would change what the card counts, which is a product decision rather than a regression fix, so it belongs in its own card if it matters.


The fix

9d5e00eb9 on 18.0.

The due-checklist list is loaded alongside the other counts on a refetching load, and the number is derived from it.

Situation Behaviour
Org without the setting dueChecklistForDefaultDashboard returns empty before it queries → count 0 → row hidden → no cost
Org with the setting Scan Child enrolments, evaluate the rule, keep the list in state, count distinct subjects
Non-refetching load (plain re-entry) Count derived from the list in state, never from the cached card
Custom filter applied dueChecklistFilter: 'individual.uuid' narrows it like every other card, chunked at 500

Two deliberate choices:

  • It counts people, not rows, per #1865 regression: dashboard cards count rows, not people — four cards diverge from the list behind them #2024: a subject with two due Child enrolments is one person. 17.x used the raw list length and double-counted them. This is a knowing deviation from strict 17.x parity.
  • The number never comes from the cached card. If it did, a restart with auto-refresh off would show a cached non-zero count with no list behind it, and the card would open an empty listing (AC 7). Deriving it from the list in hand means no list ⇒ count 0 ⇒ row hidden.

The cost #1865 removed is reintroduced only for orgs that have the feature on, bounded to Child enrolments, and only on refetching loads — app start, sync, date toggle, filter change, manual refresh. Plain re-entry still serves the cache, so #2022's fix is untouched.

Tests (MyDashboardOnLoadTest.js): the count is people not rows and the list is carried for the drill-down; the row stays hidden for an org without checklists; the number survives re-entry; a due count is never shown without a list behind it; the custom filter restricts it.

Still to verify on a device: AC 1, 2, 3 and 6, against an org that actually uses checklists, on Realm and on SQLite. The tests stub IndividualService, so they prove the wiring, not the query.

Out of scope

Activity

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

  • Status
    Code Review with Comments

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions