Conversation
There was a problem hiding this comment.
Pull request overview
Updates date handling across shared/frontend/backend by introducing centralized date formatting utilities and migrating several Prisma DateTime fields intended to be “date-only” to @db.Date, reducing timezone-related off-by-one issues.
Changes:
- Add
dayjs-backed shared formatting helpers for date-only vs timestamp rendering and for Slack formatting. - Migrate multiple Prisma models’ date-only fields to
@db.Dateand add a SQL migration to cast existing data. - Refactor frontend/backend usages to use shared date formatting /
toDateStringand remove legacy date transformation helpers.
Reviewed changes
Copilot reviewed 38 out of 39 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Adds/updates lock entries for dayjs across workspaces. |
| src/shared/src/date-format.ts | New shared date formatting utilities (date-only, timestamps, Slack) using dayjs. |
| src/shared/package.json | Adds dayjs as a shared dependency. |
| src/shared/index.ts | Exports the new date-format module from shared. |
| src/frontend/src/utils/pipes.ts | Switches UI pipes to shared formatting helpers and simplifies date logic. |
| src/frontend/src/utils/part.utils.ts | Uses shared date-only formatting for part history display. |
| src/frontend/src/utils/datetime.utils.ts | Removes several formatting helpers, keeping only remaining date utilities. |
| src/frontend/src/pages/WorkPackageForm/WorkPackageFormView.tsx | Uses shared toDateString for start date payloads. |
| src/frontend/src/pages/SettingsPage/UserScheduleSettings/UserScheduleSettingsView.tsx | Uses shared formatting for scheduled date display in modal title. |
| src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/TaskList/v2/TaskColumn.tsx | Uses shared toDateString for task create payload date-only fields. |
| src/frontend/src/pages/HomePage/components/TimelineSection.tsx | Uses shared date-only formatting for milestone display. |
| src/frontend/src/pages/HomePage/components/TeamTaskCard.tsx | Uses shared date-only formatting for task deadlines. |
| src/frontend/src/pages/HomePage/components/TaskDetailCard.tsx | Uses shared date-only formatting for task deadlines. |
| src/frontend/src/pages/HomePage/components/EventCard.tsx | Refactors event date display to distinguish date-only vs datetime. |
| src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttToolTip.tsx | Uses shared date-only formatting in tooltip. |
| src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBar.tsx | Replaces dateToString with shared toDateString for comparisons. |
| src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttChartTimeline.tsx | Replaces dateToString with shared toDateString for “current week” logic. |
| src/frontend/src/pages/GanttPage/GanttChart/GanttChart.tsx | Replaces dateToString with shared toDateString for “current week” column. |
| src/frontend/src/pages/FinancePage/FinanceComponents/GenerateReceiptsModal.tsx | Uses shared startOfDay for date filtering comparisons. |
| src/frontend/src/pages/CalendarPage/UpcomingMeetingsCard.tsx | Uses shared formatEventTime for meeting time display. |
| src/frontend/src/pages/CalendarPage/TaskClickPopup.tsx | Uses shared date-only formatting for task deadline display. |
| src/frontend/src/pages/CalendarPage/EventPartialInfoView.tsx | Uses shared formatEventTime for event time ranges. |
| src/frontend/src/pages/CalendarPage/EventClickPopup.tsx | Uses shared formatEventTime for event time ranges. |
| src/frontend/src/pages/CalendarPage/Components/ScheduleEventModal.tsx | Uses shared formatEventTime for time range display. |
| src/frontend/src/pages/CalendarPage/CalendarPage.tsx | Removes manual timezone adjustments when keying day dictionaries. |
| src/frontend/src/pages/AdminToolsPage/RecruitmentConfig/MilestoneTable.tsx | Uses shared date-only formatting for milestone date display. |
| src/frontend/src/pages/AdminToolsPage/AdminToolsSlackUpcomingDeadlines.tsx | Sends deadline Date directly instead of reconstructing via toDateString(). |
| src/backend/src/utils/slack.utils.ts | Switches scheduled-event Slack formatting to shared Slack formatters. |
| src/backend/src/utils/google-integration.utils.ts | Uses shared toDateString when creating all-day Google Calendar events. |
| src/backend/src/utils/datetime.utils.ts | Removes backend transformDate helper (now relying on shared). |
| src/backend/src/utils/changes.utils.ts | Uses shared date-only formatting for change details. |
| src/backend/src/utils/change-requests.utils.ts | Uses shared toDateString and date-only formatting in change request application/details. |
| src/backend/src/services/work-packages.services.ts | Removes “add 12 hours” workaround and uses new Date(startDate) for date-only storage. |
| src/backend/src/services/reimbursement-requests.services.ts | Removes “add 12 hours” workaround for reimbursements (date-only handling). |
| src/backend/src/services/notifications.services.ts | Uses shared Slack time formatting for event notification messages. |
| src/backend/src/prisma/seed.ts | Uses shared toDateString for seeded date-only strings. |
| src/backend/src/prisma/schema.prisma | Migrates multiple fields to @db.Date (date-only semantics). |
| src/backend/src/prisma/migrations/20260228000000_change_date_only_fields_to_db_date/migration.sql | SQL migration casting timestamp/date-time columns to date. |
| src/backend/package.json | Adds dayjs dependency to backend workspace. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 48 out of 49 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBar.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 54 out of 55 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (2)
src/frontend/src/pages/CalendarPage/Components/EventAvailabilityPage.tsx:132
displayDatecan becomeundefinedwhen there is nodatequery param andevent.initialDateScheduledis missing. It is then used as aDate(e.g., passed intogetMostRecentAvailabilities,toLocaleDateString, and modals), which will throw at runtime. Restore a safe fallback (e.g.,event?.initialDateScheduled ?? new Date()), and ifinitialDateScheduledis a date-only field, normalize it (e.g.,dbDateToLocalDate(...)) before using it for local date arithmetic.
const displayDate = useMemo(() => {
if (dateParam) {
return new Date(dateParam);
}
return event?.initialDateScheduled ?? new Date();
src/backend/src/utils/google-integration.utils.ts:246
- Same issue for the
enddate: switching from UTC (toISOString().split('T')[0]) to local formatting can shift the all-day end date depending on server timezone. Use a UTC-based conversion for Google all-day events, or a dedicated helper that makes the intended timezone explicit.
end: slot.allDay
? { date: slot.endTime ? toDateString(slot.endTime) : undefined }
: {
dateTime: slot.endTime ? slot.endTime.toISOString() : undefined,
timeZone: 'America/New_York'
},
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/frontend/src/pages/CalendarPage/Components/EventAvailabilityPage.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 51 out of 52 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * Format a date-only value (deadlines, start dates stored as @db.Date). | ||
| * Uses UTC because @db.Date columns return midnight UTC, and we want the UTC date (which IS the stored date). | ||
| */ | ||
| export const formatDateOnly = (date: Date, format = 'MM/DD/YYYY'): string => dayjs.utc(date).format(format); |
There was a problem hiding this comment.
formatDateOnly currently formats with dayjs.utc(date). That only works correctly if the input Date represents midnight UTC (as returned directly from a Postgres DATE / Prisma @db.Date). In the frontend you’re now normalizing @db.Date values to local-midnight via dbDateToLocalDate, and passing those into formatDateOnly; in positive-offset timezones this will format the previous calendar day. Consider either (a) changing formatDateOnly to format in local time and updating the docstring, or (b) splitting into two helpers (e.g., formatDbDateOnly vs formatLocalDateOnly, or a utc flag) so call sites can be explicit and consistent.
| const calculateEndDate = (start: Date, weeks: number) => { | ||
| const end = new Date(start); | ||
| end.setDate(start.getDate() + weeks * 7); | ||
| end.setUTCDate(start.getUTCDate() + weeks * 7); | ||
| return end; |
There was a problem hiding this comment.
calculateEndDate now uses getUTCDate/setUTCDate. This function is exported from shared and is used in the frontend (e.g., to display work package end dates) where startDate/endDate are now normalized to local-midnight via dbDateToLocalDate. For users in UTC+ timezones, UTC date arithmetic on a local-midnight Date can shift by one day and produce an incorrect end date. Consider adding a utc/dbDate flag (similar to getMonday) or reverting to local getDate/setDate for the default, and only using UTC arithmetic in backend-only paths.
| // Task deadlines are date-only values (@db.Date, midnight UTC). | ||
| // Extract UTC components from deadline, local components from weekDay, and compare. | ||
| const tasksByDay: CalendarTask[][] = weekDays.map((day) => | ||
| tasks.filter((t) => { | ||
| if (!t.deadline) return false; | ||
| return isSameDay(new Date(t.deadline), day); | ||
| const d = new Date(t.deadline); | ||
| return ( | ||
| d.getUTCFullYear() === day.getFullYear() && d.getUTCMonth() === day.getMonth() && d.getUTCDate() === day.getDate() | ||
| ); |
There was a problem hiding this comment.
tasksByDay compares the task deadline using UTC getters against day using local getters. However, filteredTasks are keyed in CalendarPage via datePipe(new Date(task.deadline)) (local formatting), and task deadlines are now transformed to local-midnight via dbDateToLocalDate. With the current UTC/local mix, tasks can land on the wrong day (especially in non-UTC timezones). Use a consistent comparison basis (e.g., isSameDay(d, day) or compare local Y/M/D on both sides) that matches how deadlines are normalized and displayed elsewhere in the calendar.
| start.getUTCFullYear() === day.getFullYear() && | ||
| start.getUTCMonth() === day.getMonth() && | ||
| start.getUTCDate() === day.getDate() |
There was a problem hiding this comment.
For single-point all-day events (start.getTime() === end.getTime()), the code compares UTC components from start against local components from day. This mixes time bases and can misclassify events on the wrong day around timezone boundaries. Either compare on local time for both (e.g., isSameDay(start, day)) or compare UTC components on both sides (day.getUTC*).
| start.getUTCFullYear() === day.getFullYear() && | |
| start.getUTCMonth() === day.getMonth() && | |
| start.getUTCDate() === day.getDate() | |
| start.getUTCFullYear() === day.getUTCFullYear() && | |
| start.getUTCMonth() === day.getUTCMonth() && | |
| start.getUTCDate() === day.getUTCDate() |
| const firstScheduledDate = event.initialDateScheduled || event.scheduledTimes[0]?.startTime; | ||
| const timezoneAdjustedDate = firstScheduledDate ? timezoneOffset(firstScheduledDate) : new Date(); | ||
| const displayDate = firstScheduledDate ? new Date(firstScheduledDate) : new Date(); | ||
| const formattedDate = formatDateOnly(displayDate); | ||
|
|
There was a problem hiding this comment.
formattedDate is derived with formatDateOnly(displayDate), but displayDate comes from initialDateScheduled / scheduledTimes[0].startTime (a full timestamp). Formatting a timestamp with a date-only UTC formatter can disagree with getWeekday(displayDate) and with the displayed meeting time in some timezones. Consider using a timestamp-aware formatter here (e.g., formatEventDate(displayDate) / formatTimestamp) so the weekday/date/time are all derived in the same timezone basis.
| changeRequestProject1Id, | ||
| WorkPackageStage.Design, | ||
| weeksAgo(12).toISOString().split('T')[0], | ||
| toDateString(weeksAgo(12)), |
There was a problem hiding this comment.
toDateString(weeksAgo(...)) formats using local timezone rules, but weeksAgo() retains the current time-of-day. That means the resulting YYYY-MM-DD string can vary depending on the machine timezone and the time the seed is run (e.g., around midnight), making seed data non-deterministic. For seed data, consider using a UTC-based conversion (like the previous toISOString().split('T')[0]) or add a dedicated UTC date-string helper for date-only fields.
| toDateString(weeksAgo(12)), | |
| weeksAgo(12).toISOString().split('T')[0], |
Changes
Handle dates properly by splitting datetimes and dates in db and logic