diff --git a/src/backend/package.json b/src/backend/package.json index 92ec84d984..698e42e182 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -22,6 +22,7 @@ "concat-stream": "^2.0.0", "cookie-parser": "^1.4.5", "cors": "^2.8.5", + "dayjs": "^1.11.18", "decimal.js": "^10.4.3", "dotenv": "^16.0.1", "express": "^5.0.0", diff --git a/src/backend/src/services/work-packages.services.ts b/src/backend/src/services/work-packages.services.ts index ef14628a30..3239eb7d01 100644 --- a/src/backend/src/services/work-packages.services.ts +++ b/src/backend/src/services/work-packages.services.ts @@ -13,7 +13,8 @@ import { WorkPackagePreview, WorkPackageStage, User, - WorkPackageSelection + WorkPackageSelection, + toUtcMidnight } from 'shared'; import prisma from '../prisma/prisma'; import { @@ -208,9 +209,7 @@ export default class WorkPackagesService { .map((element) => element.wbsElement.workPackageNumber) .reduce((prev, curr) => Math.max(prev, curr), 0) + 1; - // make the date object but add 12 hours so that the time isn't 00:00 to avoid timezone problems - const date = new Date(startDate.split('T')[0]); - date.setTime(date.getTime() + 12 * 60 * 60 * 1000); + const date = toUtcMidnight(startDate); const changesToCreate = crId ? [ @@ -255,7 +254,7 @@ export default class WorkPackagesService { null, stage, null, - new Date(startDate), + date, null, duration, [], @@ -342,13 +341,14 @@ export default class WorkPackagesService { const blockedByElems = await validateBlockedBys(blockedBy, organization.organizationId); + const normalizedEdit = toUtcMidnight(startDate); const changes = await getWorkPackageChanges( originalWorkPackage.wbsElement.name, name, originalWorkPackage.stage, stage, originalWorkPackage.startDate, - new Date(startDate), + normalizedEdit, originalWorkPackage.duration, duration, originalWorkPackage.blockedBy, @@ -363,10 +363,8 @@ export default class WorkPackagesService { wbsElementId, userId ); - - // make the date object but add 12 hours so that the time isn't 00:00 to avoid timezone problems - const date = new Date(startDate); - date.setTime(date.getTime() + 12 * 60 * 60 * 1000); + // Store at 00:00 UTC (canonical) + const date = normalizedEdit; // set the status of the wbs element to active if an edit is made to a completed version const status = diff --git a/src/frontend/package.json b/src/frontend/package.json index bdf0864a26..501c1ab792 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -24,7 +24,7 @@ "classnames": "^2.3.1", "customize-cra": "^1.0.0", "date-fns": "^4.1.0", - "dayjs": "^1.11.10", + "dayjs": "^1.11.18", "file-saver": "^2.0.5", "google-auth-library": "^9.15.0", "pdf-lib": "^1.17.1", diff --git a/src/frontend/src/apis/work-packages.api.ts b/src/frontend/src/apis/work-packages.api.ts index 55b38ae550..bac2c0c0d1 100644 --- a/src/frontend/src/apis/work-packages.api.ts +++ b/src/frontend/src/apis/work-packages.api.ts @@ -54,9 +54,7 @@ export const getSingleWorkPackage = (wbsNum: WbsNumber) => { * @param payload Payload containing all the necessary data to create a work package. */ export const createSingleWorkPackage = (payload: WorkPackageCreateArgs) => { - return axios.post(apiUrls.workPackagesCreate(), { - ...payload - }); + return axios.post(apiUrls.workPackagesCreate(), payload); }; /** @@ -66,9 +64,7 @@ export const createSingleWorkPackage = (payload: WorkPackageCreateArgs) => { * @returns Promise that will resolve to either a success status code or a fail status code. */ export const editWorkPackage = (payload: WorkPackageEditArgs) => { - return axios.post<{ message: string }>(apiUrls.workPackagesEdit(), { - ...payload - }); + return axios.post<{ message: string }>(apiUrls.workPackagesEdit(), payload); }; /** diff --git a/src/frontend/src/pages/FinancePage/FinanceDashboard/AdminFinanceDashboard.tsx b/src/frontend/src/pages/FinancePage/FinanceDashboard/AdminFinanceDashboard.tsx index 52fdce6039..180ba36f42 100644 --- a/src/frontend/src/pages/FinancePage/FinanceDashboard/AdminFinanceDashboard.tsx +++ b/src/frontend/src/pages/FinancePage/FinanceDashboard/AdminFinanceDashboard.tsx @@ -19,7 +19,7 @@ import TotalAmountSpentModal from '../FinanceComponents/TotalAmountSpentModal'; import { DatePicker } from '@mui/x-date-pickers'; import ListAltIcon from '@mui/icons-material/ListAlt'; import WorkIcon from '@mui/icons-material/Work'; -import { isAdmin } from 'shared'; +import { isAdmin, dateToUtcMidnight } from 'shared'; import { useGetAllCars } from '../../../hooks/cars.hooks'; import NERAutocomplete from '../../../components/NERAutocomplete'; @@ -232,7 +232,7 @@ const AdminFinanceDashboard: React.FC = ({ startDate }, field: { clearable: true } }} - onChange={(newValue: Date | null) => setStartDateState(newValue ?? undefined)} + onChange={(newValue: Date | null) => setStartDateState(newValue ? dateToUtcMidnight(newValue) : undefined)} /> @@ -251,7 +251,7 @@ const AdminFinanceDashboard: React.FC = ({ startDate }, field: { clearable: true } }} - onChange={(newValue: Date | null) => setEndDateState(newValue ?? undefined)} + onChange={(newValue: Date | null) => setEndDateState(newValue ? dateToUtcMidnight(newValue) : undefined)} /> = ({ start }, field: { clearable: true } }} - onChange={(newValue: Date | null) => setStartDateState(newValue ?? undefined)} + onChange={(newValue: Date | null) => setStartDateState(newValue ? dateToUtcMidnight(newValue) : undefined)} /> @@ -175,7 +176,7 @@ const GeneralFinanceDashboard: React.FC = ({ start }, field: { clearable: true } }} - onChange={(newValue: Date | null) => setEndDateState(newValue ?? undefined)} + onChange={(newValue: Date | null) => setEndDateState(newValue ? dateToUtcMidnight(newValue) : undefined)} /> ); diff --git a/src/frontend/src/utils/datetime.utils.ts b/src/frontend/src/utils/datetime.utils.ts index ea7c37442b..d0d1292967 100644 --- a/src/frontend/src/utils/datetime.utils.ts +++ b/src/frontend/src/utils/datetime.utils.ts @@ -19,10 +19,13 @@ export const dateFormatMonthDate = (date: Date) => { return dayjs(date).format('MMM D'); }; +/** + * Transforms a Date object to a YYYY-MM-DD format using Day.js + * @param date the Date object to transform + * @returns the date string in YYYY-MM-DD format representing the user's calendar date + */ export const transformDate = (date: Date) => { - const month = date.getMonth() + 1 < 10 ? `0${date.getMonth() + 1}` : (date.getMonth() + 1).toString(); - const day = date.getDate() < 10 ? `0${date.getDate()}` : date.getDate().toString(); - return `${date.getFullYear().toString()}/${month}/${day}`; + return dayjs(date).format('YYYY-MM-DD'); }; export const formatDate = (date: Date) => { diff --git a/src/shared/src/date-utils.ts b/src/shared/src/date-utils.ts index a7980552c3..4352743723 100644 --- a/src/shared/src/date-utils.ts +++ b/src/shared/src/date-utils.ts @@ -3,8 +3,55 @@ * See the LICENSE file in the repository root folder for details. */ +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; import { Availability } from './types/user-types'; +dayjs.extend(utc); + +/** + * @returns Date object representing the user's local midnight converted to UTC + */ +export const toUtcMidnight = (input: string): Date => { + return dayjs(input).startOf('day').utc().toDate(); +}; + +/** + * Converts a Date object to UTC midnight, preserving the calendar day in the user's timezone + * @param date - The Date object from DatePicker (in user's local timezone) + * @returns Date object representing the selected day at UTC midnight + */ +export const dateToUtcMidnight = (date: Date): Date => { + const year = date.getFullYear(); + const month = date.getMonth(); + const day = date.getDate(); + return dayjs.utc().year(year).month(month).date(day).startOf('day').toDate(); +}; + +/** + * @param utcDate - The UTC date from the database + * @returns Date string in local timezone (YYYY-MM-DD format) + */ +export const fromUtcMidnight = (utcDate: Date): string => { + return dayjs.utc(utcDate).local().format('YYYY-MM-DD'); +}; + +/** + * @returns Date object representing today at UTC midnight + */ +export const getCurrentUtcMidnight = (): Date => { + return dayjs().startOf('day').utc().toDate(); +}; + +/** + * @param date1 - First date to compare + * @param date2 - Second date to compare + * @returns true if both dates represent the same calendar day + */ +export const isSameCalendarDay = (date1: Date, date2: Date): boolean => { + return dayjs(date1).format('YYYY-MM-DD') === dayjs(date2).format('YYYY-MM-DD'); +}; + /** * Add the given number of weeks to the given date and return the outcome. * @param start the start date diff --git a/yarn.lock b/yarn.lock index 6e5ea4207e..c12ffde6ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7903,6 +7903,7 @@ __metadata: concat-stream: ^2.0.0 cookie-parser: ^1.4.5 cors: ^2.8.5 + dayjs: ^1.11.18 decimal.js: ^10.4.3 dotenv: ^16.0.1 express: ^5.0.0 @@ -9558,7 +9559,7 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:^1.11.10": +"dayjs@npm:^1.11.18": version: 1.11.18 resolution: "dayjs@npm:1.11.18" checksum: cc90054bad30ab011417a7a474b2ffa70e7a28ca6f834d7e86fe53a408a40a14c174f26155072628670e9eda4c48c4ed0d847d2edf83d47c0bfb78be15bbf2dd @@ -11975,7 +11976,7 @@ __metadata: classnames: ^2.3.1 customize-cra: ^1.0.0 date-fns: ^4.1.0 - dayjs: ^1.11.10 + dayjs: ^1.11.18 file-saver: ^2.0.5 google-auth-library: ^9.15.0 jest-fail-on-console: ^3.0.2