Skip to content

#1192 start date bug#3615

Open
jasonk55 wants to merge 28 commits intodevelopfrom
#1192-start-date-bug
Open

#1192 start date bug#3615
jasonk55 wants to merge 28 commits intodevelopfrom
#1192-start-date-bug

Conversation

@jasonk55
Copy link
Contributor

@jasonk55 jasonk55 commented Sep 24, 2025

Changes

Fix start date off-by-one bug by normalizing client-provided dates to the user’s local midnight converted to UTC before persisting.

Notes

This change is timezone agnostic, so if someone in another timezone chooses a day (e.g. 9/24/25) for the start date, this is the date for everyone else.

To Do

Any remaining things that need to get done

  • Make the frontend render the date correctly according to user's timezone

Checklist

It can be helpful to check the Checks and Files changed tabs.
Please review the contributor guide and reach out to your Tech Lead if anything is unclear.
Please request reviewers and ping on slack only after you've gone through this whole checklist.

  • All commits are tagged with the ticket number
  • No linting errors / newline at end of file warnings
  • All code follows repository-configured prettier formatting
  • No merge conflicts
  • All checks passing
  • Screenshots of UI changes (see Screenshots section)
  • Remove any non-applicable sections of this template
  • Assign the PR to yourself
  • No yarn.lock changes (unless dependencies have changed)
  • Request reviewers & ping on Slack
  • PR is linked to the ticket (fill in the closes line below)

Closes #1192

@jasonk55 jasonk55 self-assigned this Sep 24, 2025
@gcooper407 gcooper407 self-requested a review September 29, 2025 17:49
gcooper407
gcooper407 previously approved these changes Sep 29, 2025
Copy link
Contributor

@gcooper407 gcooper407 left a comment

Choose a reason for hiding this comment

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

thanks for making those fixes, looks good!

@superhvarn superhvarn self-assigned this Oct 23, 2025
Copy link
Member

@walker-sean walker-sean left a comment

Choose a reason for hiding this comment

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

Can we make this standard for how we handle dates in the frontend and backend to avoid this issue in other places

@cielbellerose cielbellerose self-assigned this Feb 28, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes the “start date shifts to next day” timezone bug by standardizing how date-only selections are normalized and persisted (moving toward storing/transporting dates as UTC-midnight for the intended calendar day).

Changes:

  • Added shared UTC-midnight date helpers and started applying them across frontend date flows.
  • Removed backend “+12 hours” workaround and now parses incoming start dates directly.
  • Updated frontend date formatting/Day.js usage and bumped Day.js versions/deps to support the new utilities.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
yarn.lock Updates lockfile for Day.js range changes across workspaces.
src/shared/src/date-utils.ts Adds UTC-midnight/date normalization helpers (used by frontend).
src/shared/package.json Adds Day.js dependency to shared package.
src/frontend/src/utils/pipes.ts Adjusts datePipe to avoid previous Date normalization approach.
src/frontend/src/utils/datetime.utils.ts Switches transformDate to Day.js formatting.
src/frontend/src/pages/WorkPackageForm/WorkPackageFormView.tsx Normalizes submitted work package start date.
src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskCard.tsx Normalizes task deadline/start date during edit.
src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/TaskFormModal.tsx Normalizes task form dates and adjusts DatePicker defaults.
src/frontend/src/pages/FinancePage/FinanceDashboard/GeneralFinanceDashboard.tsx Normalizes DatePicker-selected finance filter dates.
src/frontend/src/pages/FinancePage/FinanceDashboard/AdminFinanceDashboard.tsx Normalizes DatePicker-selected finance filter dates.
src/frontend/src/pages/CalendarPage/UpcomingMeetingsCard.tsx Normalizes displayed meeting date.
src/frontend/src/pages/CalendarPage/CalendarPage.tsx Normalizes event bucketing and removes prior timezone-offset hacks.
src/frontend/src/apis/work-packages.api.ts Simplifies axios payload posting.
src/frontend/package.json Bumps Day.js version in frontend.
src/backend/src/services/work-packages.services.ts Removes “+12 hours” hack and parses start date directly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +61 to +62
deadline: deadline ? dateToUtcMidnight(deadline) : undefined,
startDate: startDate ? dateToUtcMidnight(startDate) : undefined,
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

handleEditTask normalizes deadline/startDate with dateToUtcMidnight, but TaskFormModal already normalizes these fields before calling onSubmit. Normalizing twice is not idempotent across timezones (a UTC-midnight Date can appear as the previous day locally, so the second normalization can shift the day). Ensure the UTC-midnight conversion happens exactly once (either here or in the modal).

Suggested change
deadline: deadline ? dateToUtcMidnight(deadline) : undefined,
startDate: startDate ? dateToUtcMidnight(startDate) : undefined,
deadline: deadline ?? undefined,
startDate: startDate ?? undefined,

Copilot uses AI. Check for mistakes.
Comment on lines 167 to 180
<DatePicker
label="End Date"
value={endDateState}
minDate={startDateState || undefined}
shouldDisableDate={(date) => (startDateState ? date < startDateState : false)}
slotProps={{
textField: {
size: 'small',
sx: datePickerStyle
},
field: { clearable: true }
}}
onChange={(newValue: Date | null) => setEndDateState(newValue ?? undefined)}
onChange={(newValue: Date | null) => setEndDateState(newValue ? dateToUtcMidnight(newValue) : undefined)}
/>
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

Same issue for the end date: persisting dateToUtcMidnight(newValue) into endDateState can cause the DatePicker to render an off-by-one day in non-UTC timezones. Keep local DatePicker dates in state (and normalize only when constructing API params), or re-wrap the UTC date into a local calendar-day Date for the picker.

Copilot uses AI. Check for mistakes.
Comment on lines 223 to 236
<DatePicker
label="Start Date"
value={startDateState}
maxDate={endDateState || undefined}
shouldDisableDate={(date) => (endDateState ? date > endDateState : false)}
slotProps={{
textField: {
size: 'small',
sx: datePickerStyle
},
field: { clearable: true }
}}
onChange={(newValue: Date | null) => setStartDateState(newValue ?? undefined)}
onChange={(newValue: Date | null) => setStartDateState(newValue ? dateToUtcMidnight(newValue) : undefined)}
/>
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

Storing dateToUtcMidnight(newValue) directly into the DatePicker value can make the picker display the wrong day for users not in UTC, since the DatePicker renders using local time. Prefer keeping the local DatePicker selection in state and converting to UTC midnight only when building the API/query params (or convert back to a local calendar-day Date before passing into value).

Copilot uses AI. Check for mistakes.
field: { clearable: true }
}}
onChange={(newValue: Date | null) => setEndDateState(newValue ?? undefined)}
onChange={(newValue: Date | null) => setEndDateState(newValue ? dateToUtcMidnight(newValue) : undefined)}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

Same issue for the end date: setting endDateState to dateToUtcMidnight(newValue) can cause the DatePicker to show an off-by-one day depending on timezone. Keep local DatePicker values in state and normalize at the request boundary, or re-wrap UTC dates into local calendar-day Dates for display.

Suggested change
onChange={(newValue: Date | null) => setEndDateState(newValue ? dateToUtcMidnight(newValue) : undefined)}
onChange={(newValue: Date | null) => setEndDateState(newValue ?? undefined)}

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +110
handleSubmit((data) => {
const transformedData: EditTaskFormInput = {
...data,
startDate: data.startDate ? dateToUtcMidnight(data.startDate) : undefined,
deadline: data.deadline ? dateToUtcMidnight(data.deadline) : undefined
};
onSubmit(transformedData);
})(e);
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The modal is converting startDate/deadline to UTC-midnight before calling onSubmit. That changes the meaning of the form values for callers that expect local DatePicker dates (e.g. task creation currently formats the returned Date into a date-only string). It also causes double-normalization in the edit flow (TaskCard normalizes again), which can shift the day backwards in some timezones. Prefer keeping the form values as local Dates and performing UTC normalization at the API boundary (or ensure normalization happens exactly once).

Copilot uses AI. Check for mistakes.
Comment on lines 148 to 161
<DatePicker
label="Start Date"
value={startDateState}
maxDate={endDateState || undefined}
shouldDisableDate={(date) => (endDateState ? date > endDateState : false)}
slotProps={{
textField: {
size: 'small',
sx: datePickerStyle
},
field: { clearable: true }
}}
onChange={(newValue: Date | null) => setStartDateState(newValue ?? undefined)}
onChange={(newValue: Date | null) => setStartDateState(newValue ? dateToUtcMidnight(newValue) : undefined)}
/>
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

Storing dateToUtcMidnight(newValue) directly into the DatePicker value can make the picker display the previous/next day for users not in UTC, because the picker renders based on the local timezone of the Date object. Consider keeping startDateState as the local DatePicker value and only converting to UTC midnight when building request/query parameters (or convert back to a local “calendar day” Date before passing into value).

Copilot uses AI. Check for mistakes.
datePipe(new Date(cardDate.getTime() + cardDate.getTimezoneOffset() * 60000))
) ?? []
}
tasks={taskDict.get(datePipe(cardDate)) ?? []}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The tasks lookup uses the same datePipe(cardDate) key as events/dayOfWeek. If the card key should represent the user’s selected calendar day (timezone-agnostic), normalize cardDate to UTC midnight (preserving local calendar day) before calling datePipe, otherwise tasks can appear under the wrong day in some timezones.

Copilot uses AI. Check for mistakes.
* @returns Date object representing today at UTC midnight
*/
export const getCurrentUtcMidnight = (): Date => {
return dayjs().startOf('day').utc().toDate();
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

getCurrentUtcMidnight does not currently return UTC midnight: dayjs().startOf('day').utc() computes local midnight and then converts that instant to UTC, which can be a non-midnight UTC time. If the intent is “today at 00:00 UTC”, build it in UTC (e.g., start from dayjs.utc() / call .utc() before .startOf('day')).

Suggested change
return dayjs().startOf('day').utc().toDate();
return dayjs.utc().startOf('day').toDate();

Copilot uses AI. Check for mistakes.
@@ -76,8 +76,8 @@ export const emDashPipe = (str: string) => {
*/
export const datePipe = (date?: Date, includeYear = true) => {
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

datePipe’s parameter is typed as Date, but the implementation explicitly handles string values (typeof date === 'string'). This will either be dead code (if TS prevents string callers) or cause type errors where datePipe is called with a string. Consider updating the signature to accept Date | string (or removing the string branch) so the type matches actual usage.

Suggested change
export const datePipe = (date?: Date, includeYear = true) => {
export const datePipe = (date?: Date | string, includeYear = true) => {

Copilot uses AI. Check for mistakes.
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.

Backend - Monday Start Date Changes to Tuesday after a certain time of the day in EST

6 participants