fix: [DHIS2-17544] enforce org unit opening/closing dates in Capture#4619
fix: [DHIS2-17544] enforce org unit opening/closing dates in Capture#4619karolinelien wants to merge 7 commits into
Conversation
Org unit opening/closing dates were never enforced in the Capture app, so users could enroll people and add/edit events with dates outside the selected org unit's open/close range. Enforce the range (both bounds inclusive, day granularity, calendar-safe) on the date fields that represent activity at an org unit, each validated against its own org unit: - Enrollment date (new + edit) - Event occurred date (new + edit) - Event scheduled/due date - Enrollment transfer: hard-block when the enrollment date falls outside the destination org unit's range Each constrained field disables out-of-range dates in the calendar and adds a blocking validator. Org units without opening/closing dates leave that side unconstrained. The incident date is intentionally not constrained, as it does not represent activity at the org unit (it may legitimately be a future or historical onset date). AI Assisted Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| }, | ||
| { | ||
| validator: getWithinOrgUnitDateRangeValidator(orgUnit), | ||
| errorMessage: '', |
There was a problem hiding this comment.
Something is not right here. I am guessing there should have been a i18n string for error message here as well if the date is outside range.
There was a problem hiding this comment.
Claude: "Good question — it's already internationalized, just not as a static container string. The message is built dynamically inside the validator (getWithinOrgUnitDateRangeValidator → buildErrorMessage), where every variant goes through i18n.t(...) and interpolates the org unit's opening/closing dates and label. That's why the validator container's static errorMessage is intentionally left ''.
The framework picks the dynamic message over the static one: getValidationError in dataEntryField.utils.ts uses result.errorMessage || validatorContainer.errorMessage, so the static '' is only a fallback for when a validator returns no message — and this one always returns a non-empty, translated message on failure. It's the same pattern validateNotExpired already uses. I've added a clarifying comment at the container so this isn't mistaken for a missing string. Let me know if you'd still prefer a static string as well."
- getOrgUnitClosingCalendarMax: replace reduce() (no initial value) with a sort to pick the earliest ISO bound (S6959, S7766) - getWithinOrgUnitDateRangeValidator: log caught date-parse errors instead of silently swallowing them (S2486) - test: use optional chaining (S6582) AI Assisted Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unit label Review fixes: - getOrgUnitCalendarBounds: normalize the opening date the same way as the closing date (strip time component) to avoid a timezone day-shift in the calendar's min bound - extract a shared useOrgUnitWithDates hook, removing the duplicated org-unit date-enrichment across the new-event, edit-event and schedule containers (also memoized to avoid feeding the form engine an unstable prop) - export a single OrgUnitDateRange type instead of declaring it three times - add unit tests for getOrgUnitCalendarBounds and a production calendar-id case Custom labels (DHIS2-16610): - the date-range error messages and the transfer block notice now use the program's configured "organisation unit" label when one is set, falling back to the default term otherwise AI Assisted Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fallback - getOrgUnitClosingCalendarMax: give sort() an explicit localeCompare compare function (SonarQube typescript:S2871) - document why the org-unit date-range validator containers use an empty static errorMessage (the message is built dynamically by the validator, interpolating the org unit's dates and label) — addressing review feedback AI Assisted Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the org unit opening/closing-date enforcement into the event-program "new event" flow (SingleEventRegistrationEntry), which was previously the only add/edit date flow left unenforced. The org unit is selected within the form, so the field value is enriched with its dates at the redux layer via useOrgUnitWithDates; the occurred-date field gets calendar bounds and the blocking within-range validator, matching the tracker flows. AI Assisted
- buildErrorMessage: truncate time component before calendar conversion, symmetric with isoToPlainDate / getOrgUnitCalendarBounds (no timezone day-shift) - getOrgUnitClosingCalendarMax: sort the local array in place (drop redundant copy) - ScheduleDate: simplify the org-unit range check (the !== true guard already narrows) - useCoreOrgUnit: document that openingDate/closedDate must stay in the fetched fields or cached org units fail open - tests: add the openingDate == closedDate one-day-window case and a non-Gregorian (nepali) case exercising the active-calendar input path AI Assisted
- getOrgUnitCalendarBounds: sort as a standalone statement (S4043) - single event DataEntry container: swap negated ternary for the error prop (S7735) AI Assisted
|



What & why
DHIS2-17544 — Org unit opening/closing dates were never enforced in the Capture app, so users could enroll people and add/edit events with dates outside the selected org unit's open/close range. (Verified the dhis2-core tracker importer doesn't enforce this either — see Follow-up below.)
What this does
Enforces the range — both bounds inclusive, day granularity, calendar-safe (handles non-Gregorian calendars) — on the date fields that represent activity at an org unit, each validated against its own org unit:
Each constrained field disables out-of-range dates in the calendar picker and adds a blocking validator (existing "no future date" limits are combined with the closing date). Org units with no opening/closing date leave that side unconstrained.
Key decisions
Implementation
getWithinOrgUnitDateRangeValidator+isIsoDateWithinOrgUnitRange(shared boundary core) in form validators.getOrgUnitCalendarBoundshelper for calendar min/max.calendarMinplumbed through the shared date component to@dhis2/uiCalendarInput.CoreOrgUnittype + both org-unit fetch paths now requestopeningDate,closedDate; event/transfer flows enrich their selected org unit viauseCoreOrgUnit.Testing
tsc --noEmit: clean. ESLint: clean (locallinebreak-stylenoise is the Windows CRLF checkout only).Known risks (deliberate)
Not in this PR
DateValidatororg-unit-range check) — the importer currently accepts out-of-range dates, so API/import/Android still bypass the rule. To be filed separately.AI Assisted