fix(contact-logs): correct timezone handling on Contact_Date save/edit#63
Merged
Conversation
Customer reported Contact Log entries saving at the wrong date/time and each
edit shifting the date back another day. Root cause was a double conversion:
the form appended `T00:00:00.000Z` to the date string, and ContactLogService
ran `new Date(...).getFullYear()` on the result — producing the SQL string in
the Node server's local timezone, not the MP domain's. Editing re-applied the
same transform to the already-shifted date.
Ministry Platform stores datetimes as wall-clock values in the domain's
configured time zone (exposed via `getDomainInfo().TimeZoneName`), not UTC.
Changes:
- Add DomainTimezoneService (`src/services/domainTimezoneService.ts`) — a
singleton wrapping `MPHelper.getDomainInfo()` with cached IANA zone
resolution. Includes a Windows→IANA mapping table since MP's API typically
returns Windows zone names ("Eastern Standard Time") and `Intl` requires
IANA ("America/New_York"). Public API: `getMpTimezone()`,
`toMpSqlDatetime(value)`, `parseMpDatetime(value)`.
- Add shared server action `getMpTimezone()` in `shared-actions/domain.ts`
for client-side display.
- Fix ContactLogService — `Contact_Date` now flows through
`toMpSqlDatetime()`; Zod still validates the rest of the record. The
`new Date(...).getFullYear()` round-trip is gone.
- Fix `contact-logs.tsx` — stop appending UTC marker on submit; thread
`mpTimezone` prop through page → ContactLookupDetails → ContactLogs;
`formatDateTime` uses `Intl.DateTimeFormat({ timeZone })` so MP wall-clock
values render correctly regardless of the user's browser zone.
- Upgrade Add/Edit form to `<input type="datetime-local">` defaulting to
current moment in MP-TZ wall-clock. Fresh "now" is seeded each time the
Add Log dialog opens. Edit pre-fill preserves both date and time.
- Add reference doc `.claude/references/ministryplatform.datetimehandling.md`
covering why MP isn't UTC, the service API, write/read recipes, the
anti-patterns this commit removes, and test mocking guidance.
- Update `CLAUDE.md` with Key Development Practice #10 and a Reference
Documents link so future MP date work picks this up.
Tests:
- 16 new tests for `DomainTimezoneService`, isolated via `mockReset()` and
passing under both `TZ=UTC` and `TZ=America/Los_Angeles`.
- Rewrote `contactLogService.test.ts` cases that previously asserted the
broken Zod-rejection behavior; added a 3-edit round-trip regression that
proves the date no longer drifts.
- Full suite: 258/258 passing; lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue is fixed and documented; the TODO's contents are now captured in the commit that landed the fix and in `.claude/references/ministryplatform.datetimehandling.md`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
T00:00:00.000Zto the date string, andContactLogServicethen rannew Date(...).getFullYear()on the result — producing the SQL string in the Node server's local timezone, not the MP domain's. Editing re-applied the same transform.getDomainInfo().TimeZoneName), not UTC. This PR adds a reusableDomainTimezoneService, threads MP timezone through the contact-log read/write paths, and upgrades the Add/Edit form to a<input type="datetime-local">defaulting to the current moment in MP-TZ..claude/references/ministryplatform.datetimehandling.mddocuments the rule, the service API, write/read recipes, anti-patterns, the Windows↔IANA mapping, and test mocking guidance.CLAUDE.mdgains a Key Development Practice (Cleaning Up and adding Claude #10) and a Reference Documents link so future MP datetime work picks this up.What changed
New
src/services/domainTimezoneService.ts— singleton withgetMpTimezone()/toMpSqlDatetime()/parseMpDatetime(), Windows→IANA mapping, cached per process.src/services/domainTimezoneService.test.ts— 16 tests; isolation viamockReset()(queue-leak gotcha documented in the reference doc).src/components/shared-actions/domain.ts—getMpTimezone()server action for client display..claude/references/ministryplatform.datetimehandling.md— the rule, recipes, anti-patterns, testing guidance.Fixed
src/components/contact-logs/contact-logs.tsx— stopped appendingT00:00:00.000Zon create/update; threadedmpTimezoneprop;formatDateTimeusesIntl.DateTimeFormat({ timeZone }); form upgraded todatetime-localdefaulting to MP-TZ "now"; fresh "now" seeded each open.src/services/contactLogService.ts— removed thenew Date(x).getFullYear()round-trip;Contact_Dateflows throughDomainTimezoneService.toMpSqlDatetime(); Zod still validates the rest of the record.src/components/contact-lookup-details/contact-lookup-details.tsx+ page — passmpTimezonedown.src/services/contactLogService.test.ts— rewrote cases that previously asserted the broken Zod-rejection behavior; added a 3-edit round-trip regression that proves no drift.Docs
CLAUDE.md— Key Development Practice Cleaning Up and adding Claude #10 + Reference Documents link.Test plan
npm run test:run— 258/258 passingnpm run lint— cleannpx tsc --noEmit— no new errors (7 pre-existing inhelper.test.tsunrelated)TZ=UTCandTZ=America/Los_Angeles/contactlookup:🤖 Generated with Claude Code