From 36206c85f8c57cc901dbf2b1e2f78e3fe0ecc3a9 Mon Sep 17 00:00:00 2001 From: jasonk55 Date: Wed, 24 Sep 2025 00:17:28 -0400 Subject: [PATCH 01/16] working fix for start date bug --- .../controllers/work-packages.controllers.ts | 22 +++- .../src/services/work-packages.services.ts | 108 ++++++++++++++++-- src/frontend/src/apis/work-packages.api.ts | 12 +- 3 files changed, 125 insertions(+), 17 deletions(-) diff --git a/src/backend/src/controllers/work-packages.controllers.ts b/src/backend/src/controllers/work-packages.controllers.ts index 09981f66ac..f0174cdf1e 100644 --- a/src/backend/src/controllers/work-packages.controllers.ts +++ b/src/backend/src/controllers/work-packages.controllers.ts @@ -44,7 +44,8 @@ export default class WorkPackagesController { // Create a work package with the given details static async createWorkPackage(req: Request, res: Response, next: NextFunction) { try { - const { name, crId, startDate, duration, blockedBy, descriptionBullets, projectWbsNum } = req.body; + const { name, crId, startDate, duration, blockedBy, descriptionBullets, projectWbsNum, clientOffsetMinutes } = + req.body; let { stage } = req.body; if (stage === 'NONE') { stage = null; @@ -59,7 +60,8 @@ export default class WorkPackagesController { blockedBy, descriptionBullets, projectWbsNum, - req.organization + req.organization, + typeof clientOffsetMinutes === 'number' ? clientOffsetMinutes : undefined ); res.status(200).json(workPackage); @@ -71,7 +73,18 @@ export default class WorkPackagesController { // Edit a work package to the given specifications static async editWorkPackage(req: Request, res: Response, next: NextFunction) { try { - const { workPackageId, name, crId, startDate, duration, blockedBy, descriptionBullets, leadId, managerId } = req.body; + const { + workPackageId, + name, + crId, + startDate, + duration, + blockedBy, + descriptionBullets, + leadId, + managerId, + clientOffsetMinutes + } = req.body; let { stage } = req.body; if (stage === 'NONE') { stage = null; @@ -88,7 +101,8 @@ export default class WorkPackagesController { descriptionBullets, leadId, managerId, - req.organization + req.organization, + typeof clientOffsetMinutes === 'number' ? clientOffsetMinutes : undefined ); res.status(200).json({ message: 'Work package updated successfully' }); } catch (error: unknown) { diff --git a/src/backend/src/services/work-packages.services.ts b/src/backend/src/services/work-packages.services.ts index c804088b06..bdfa931a34 100644 --- a/src/backend/src/services/work-packages.services.ts +++ b/src/backend/src/services/work-packages.services.ts @@ -37,6 +37,73 @@ import { userHasPermission } from '../utils/users.utils'; /** Service layer containing logic for work package controller functions. */ export default class WorkPackagesService { + /** Lightweight structured logger for date normalization during create/edit. */ + private static logDateNormalization( + context: 'create' | 'edit', + details: { + userId: string; + organizationId: string; + rawInput: string; + clientOffsetMinutes?: number; + normalized: Date; + workPackageId?: string; + projectRef?: string; // e.g., "car.project.wp#" + } + ) { + try { + const { userId, organizationId, rawInput, clientOffsetMinutes, normalized, workPackageId, projectRef } = details; + // Keep it concise but useful for debugging timezone issues + console.log( + `WP ${context} date normalization`, + { + userId, + organizationId, + rawInput, + clientOffsetMinutes, + normalizedISO: normalized.toISOString(), + normalizedLocal: normalized.toString(), + workPackageId, + projectRef + } + ); + } catch { + // never let logging break the flow + } + } + /** + * Normalize an input date string to the user's local midnight converted to UTC. + * - If clientOffsetMinutes is provided (minutes from UTC, same sign as Date.getTimezoneOffset), we interpret day boundaries in that local timezone. + * - For date-only inputs (YYYY-MM-DD or YYYY/MM/DD): use 00:00 local on that date, then convert to the corresponding UTC instant. + * - For timestamp inputs: derive the local calendar day of that instant in user's timezone, then normalize to that day's 00:00 local converted to UTC. + * - If clientOffsetMinutes is not provided, default to UTC day boundaries (00:00:00.000Z) for backwards compatibility. + */ + private static toUtcMidnight(input: string, clientOffsetMinutes?: number): Date { + const dateOnly = input.match(/^(\d{4})[-/](\d{2})[-/](\d{2})$/); + const offsetMs = (clientOffsetMinutes ?? 0) * 60 * 1000; + + // Helper: build a Date for local midnight converted to UTC using the offset + const toUtcFromLocalMidnight = (y: number, mZeroIdx: number, d: number) => + new Date(Date.UTC(y, mZeroIdx, d, 0, 0, 0, 0) + offsetMs); + + if (dateOnly) { + const y = Number(dateOnly[1]); + const m = Number(dateOnly[2]); + const d = Number(dateOnly[3]); + const normalized = toUtcFromLocalMidnight(y, m - 1, d); + return normalized; + } + + const parsed = new Date(input); + if (isNaN(parsed.getTime())) throw new Error(`Invalid date input: ${input}`); + + // Derive user's local calendar date by shifting the instant by the offset + const shifted = new Date(parsed.getTime() - offsetMs); + const y = shifted.getUTCFullYear(); + const m = shifted.getUTCMonth(); + const d = shifted.getUTCDate(); + const normalized = toUtcFromLocalMidnight(y, m, d); + return normalized; + } /** * Retrieve all work packages, optionally filtered by query parameters. * @@ -157,7 +224,8 @@ export default class WorkPackagesService { blockedBy: WbsNumber[], descriptionBullets: DescriptionBulletPreview[], projectWbsNum: WbsNumber, - organization: Organization + organization: Organization, + clientOffsetMinutes?: number ): Promise { if (await userHasPermission(user.userId, organization.organizationId, isGuest)) throw new AccessDeniedGuestException('create work packages'); @@ -205,9 +273,17 @@ 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); + // Normalize incoming startDate using user's local midnight converted to UTC when offset provided + const date = WorkPackagesService.toUtcMidnight(startDate, clientOffsetMinutes); + // Log normalization context for troubleshooting + WorkPackagesService.logDateNormalization('create', { + userId: user.userId, + organizationId: organization.organizationId, + rawInput: startDate, + clientOffsetMinutes, + normalized: date, + projectRef: `${carNumber}.${projectNumber}.${newWorkPackageNumber}` + }); const changesToCreate = crId ? [ @@ -252,7 +328,7 @@ export default class WorkPackagesService { null, stage, null, - new Date(startDate), + date, null, duration, [], @@ -307,7 +383,8 @@ export default class WorkPackagesService { descriptionBullets: DescriptionBulletPreview[], leadId: string | null, managerId: string | null, - organization: Organization + organization: Organization, + clientOffsetMinutes?: number ): Promise { const { userId } = user; // verify user is allowed to edit work packages @@ -339,13 +416,24 @@ export default class WorkPackagesService { const blockedByElems = await validateBlockedBys(blockedBy, organization.organizationId); + // Normalize new startDate using user's local midnight converted to UTC when offset provided + const normalizedEdit = WorkPackagesService.toUtcMidnight(startDate, clientOffsetMinutes); + // Log normalization context for troubleshooting + WorkPackagesService.logDateNormalization('edit', { + userId, + organizationId: organization.organizationId, + rawInput: startDate, + clientOffsetMinutes, + normalized: normalizedEdit, + workPackageId + }); const changes = await getWorkPackageChanges( originalWorkPackage.wbsElement.name, name, originalWorkPackage.stage, stage, originalWorkPackage.startDate, - new Date(startDate), + normalizedEdit, originalWorkPackage.duration, duration, originalWorkPackage.blockedBy, @@ -360,10 +448,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/src/apis/work-packages.api.ts b/src/frontend/src/apis/work-packages.api.ts index 390aca23b1..1d5ed68441 100644 --- a/src/frontend/src/apis/work-packages.api.ts +++ b/src/frontend/src/apis/work-packages.api.ts @@ -17,6 +17,8 @@ export interface WorkPackageApiInputs { crId?: string; blockedBy: WbsNumber[]; descriptionBullets: DescriptionBulletPreview[]; + /** Optional client timezone offset in minutes (same sign as Date.getTimezoneOffset). */ + clientOffsetMinutes?: number; } export interface WorkPackageCreateArgs extends WorkPackageApiInputs { @@ -53,8 +55,11 @@ export const getSingleWorkPackage = (wbsNum: WbsNumber) => { * @param payload Payload containing all the necessary data to create a work package. */ export const createSingleWorkPackage = (payload: WorkPackageCreateArgs) => { + const clientOffsetMinutes = + typeof payload.clientOffsetMinutes === 'number' ? payload.clientOffsetMinutes : new Date().getTimezoneOffset(); return axios.post(apiUrls.workPackagesCreate(), { - ...payload + ...payload, + clientOffsetMinutes }); }; @@ -65,8 +70,11 @@ 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) => { + const clientOffsetMinutes = + typeof payload.clientOffsetMinutes === 'number' ? payload.clientOffsetMinutes : new Date().getTimezoneOffset(); return axios.post<{ message: string }>(apiUrls.workPackagesEdit(), { - ...payload + ...payload, + clientOffsetMinutes }); }; From c3f8e64eb920ce18bff2b253a18f05aa23c48226 Mon Sep 17 00:00:00 2001 From: jasonk55 Date: Wed, 24 Sep 2025 00:24:18 -0400 Subject: [PATCH 02/16] #1192 fixed file versions --- .../src/services/work-packages.services.ts | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/src/backend/src/services/work-packages.services.ts b/src/backend/src/services/work-packages.services.ts index bdfa931a34..6e9c1c9164 100644 --- a/src/backend/src/services/work-packages.services.ts +++ b/src/backend/src/services/work-packages.services.ts @@ -37,39 +37,6 @@ import { userHasPermission } from '../utils/users.utils'; /** Service layer containing logic for work package controller functions. */ export default class WorkPackagesService { - /** Lightweight structured logger for date normalization during create/edit. */ - private static logDateNormalization( - context: 'create' | 'edit', - details: { - userId: string; - organizationId: string; - rawInput: string; - clientOffsetMinutes?: number; - normalized: Date; - workPackageId?: string; - projectRef?: string; // e.g., "car.project.wp#" - } - ) { - try { - const { userId, organizationId, rawInput, clientOffsetMinutes, normalized, workPackageId, projectRef } = details; - // Keep it concise but useful for debugging timezone issues - console.log( - `WP ${context} date normalization`, - { - userId, - organizationId, - rawInput, - clientOffsetMinutes, - normalizedISO: normalized.toISOString(), - normalizedLocal: normalized.toString(), - workPackageId, - projectRef - } - ); - } catch { - // never let logging break the flow - } - } /** * Normalize an input date string to the user's local midnight converted to UTC. * - If clientOffsetMinutes is provided (minutes from UTC, same sign as Date.getTimezoneOffset), we interpret day boundaries in that local timezone. @@ -275,15 +242,6 @@ export default class WorkPackagesService { // Normalize incoming startDate using user's local midnight converted to UTC when offset provided const date = WorkPackagesService.toUtcMidnight(startDate, clientOffsetMinutes); - // Log normalization context for troubleshooting - WorkPackagesService.logDateNormalization('create', { - userId: user.userId, - organizationId: organization.organizationId, - rawInput: startDate, - clientOffsetMinutes, - normalized: date, - projectRef: `${carNumber}.${projectNumber}.${newWorkPackageNumber}` - }); const changesToCreate = crId ? [ @@ -418,15 +376,6 @@ export default class WorkPackagesService { // Normalize new startDate using user's local midnight converted to UTC when offset provided const normalizedEdit = WorkPackagesService.toUtcMidnight(startDate, clientOffsetMinutes); - // Log normalization context for troubleshooting - WorkPackagesService.logDateNormalization('edit', { - userId, - organizationId: organization.organizationId, - rawInput: startDate, - clientOffsetMinutes, - normalized: normalizedEdit, - workPackageId - }); const changes = await getWorkPackageChanges( originalWorkPackage.wbsElement.name, name, From af942d8edf0268d17803054423e5e26838fbc8d8 Mon Sep 17 00:00:00 2001 From: jasonk55 Date: Wed, 24 Sep 2025 21:25:38 -0400 Subject: [PATCH 03/16] 1192 Date is the same anywhere --- .../src/services/work-packages.services.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/backend/src/services/work-packages.services.ts b/src/backend/src/services/work-packages.services.ts index 6e9c1c9164..230a497bc1 100644 --- a/src/backend/src/services/work-packages.services.ts +++ b/src/backend/src/services/work-packages.services.ts @@ -40,28 +40,17 @@ export default class WorkPackagesService { /** * Normalize an input date string to the user's local midnight converted to UTC. * - If clientOffsetMinutes is provided (minutes from UTC, same sign as Date.getTimezoneOffset), we interpret day boundaries in that local timezone. - * - For date-only inputs (YYYY-MM-DD or YYYY/MM/DD): use 00:00 local on that date, then convert to the corresponding UTC instant. - * - For timestamp inputs: derive the local calendar day of that instant in user's timezone, then normalize to that day's 00:00 local converted to UTC. + * - Derive the local calendar day of that instant in user's timezone, then normalize to that day's 00:00 local converted to UTC. * - If clientOffsetMinutes is not provided, default to UTC day boundaries (00:00:00.000Z) for backwards compatibility. */ private static toUtcMidnight(input: string, clientOffsetMinutes?: number): Date { - const dateOnly = input.match(/^(\d{4})[-/](\d{2})[-/](\d{2})$/); const offsetMs = (clientOffsetMinutes ?? 0) * 60 * 1000; - // Helper: build a Date for local midnight converted to UTC using the offset + // Build a Date for local midnight converted to UTC using the offset const toUtcFromLocalMidnight = (y: number, mZeroIdx: number, d: number) => new Date(Date.UTC(y, mZeroIdx, d, 0, 0, 0, 0) + offsetMs); - if (dateOnly) { - const y = Number(dateOnly[1]); - const m = Number(dateOnly[2]); - const d = Number(dateOnly[3]); - const normalized = toUtcFromLocalMidnight(y, m - 1, d); - return normalized; - } - const parsed = new Date(input); - if (isNaN(parsed.getTime())) throw new Error(`Invalid date input: ${input}`); // Derive user's local calendar date by shifting the instant by the offset const shifted = new Date(parsed.getTime() - offsetMs); From e53ee7dde4e45eb6c33be9f2ea309ac14215b9dd Mon Sep 17 00:00:00 2001 From: jasonk55 Date: Wed, 24 Sep 2025 21:25:38 -0400 Subject: [PATCH 04/16] #1192 Date stays the same in all timezones --- .../src/services/work-packages.services.ts | 49 ++++++++----------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/backend/src/services/work-packages.services.ts b/src/backend/src/services/work-packages.services.ts index 6e9c1c9164..0c69973c66 100644 --- a/src/backend/src/services/work-packages.services.ts +++ b/src/backend/src/services/work-packages.services.ts @@ -38,38 +38,29 @@ import { userHasPermission } from '../utils/users.utils'; /** Service layer containing logic for work package controller functions. */ export default class WorkPackagesService { /** - * Normalize an input date string to the user's local midnight converted to UTC. - * - If clientOffsetMinutes is provided (minutes from UTC, same sign as Date.getTimezoneOffset), we interpret day boundaries in that local timezone. - * - For date-only inputs (YYYY-MM-DD or YYYY/MM/DD): use 00:00 local on that date, then convert to the corresponding UTC instant. - * - For timestamp inputs: derive the local calendar day of that instant in user's timezone, then normalize to that day's 00:00 local converted to UTC. - * - If clientOffsetMinutes is not provided, default to UTC day boundaries (00:00:00.000Z) for backwards compatibility. + * Normalize an input date string to the canonical calendar day at UTC midnight. + * + * Behavior: + * - Treat the provided startDate as a calendar date (the date the user selected), not a moment-in-time. + * - Persist that calendar date at 00:00:00.000Z (UTC midnight) regardless of the user's timezone. + * - When viewed in a different timezone, this may render as the previous/next local day (expected by design). + * + * Notes: + * - clientOffsetMinutes is accepted for backward compatibility but intentionally ignored. + * - Supports common formats like YYYY-MM-DD (preferred), YYYY/MM/DD, and M/D/(YY|YYYY). */ - private static toUtcMidnight(input: string, clientOffsetMinutes?: number): Date { - const dateOnly = input.match(/^(\d{4})[-/](\d{2})[-/](\d{2})$/); - const offsetMs = (clientOffsetMinutes ?? 0) * 60 * 1000; - - // Helper: build a Date for local midnight converted to UTC using the offset - const toUtcFromLocalMidnight = (y: number, mZeroIdx: number, d: number) => - new Date(Date.UTC(y, mZeroIdx, d, 0, 0, 0, 0) + offsetMs); - - if (dateOnly) { - const y = Number(dateOnly[1]); - const m = Number(dateOnly[2]); - const d = Number(dateOnly[3]); - const normalized = toUtcFromLocalMidnight(y, m - 1, d); - return normalized; - } + private static toUtcMidnight(input: string, _clientOffsetMinutes?: number): Date { + // Helpers + const toUtcMidnight = (y: number, mZeroIdx: number, d: number) => new Date(Date.UTC(y, mZeroIdx, d, 0, 0, 0, 0)); const parsed = new Date(input); - if (isNaN(parsed.getTime())) throw new Error(`Invalid date input: ${input}`); - - // Derive user's local calendar date by shifting the instant by the offset - const shifted = new Date(parsed.getTime() - offsetMs); - const y = shifted.getUTCFullYear(); - const m = shifted.getUTCMonth(); - const d = shifted.getUTCDate(); - const normalized = toUtcFromLocalMidnight(y, m, d); - return normalized; + if (!isNaN(parsed.getTime())) { + return toUtcMidnight(parsed.getUTCFullYear(), parsed.getUTCMonth(), parsed.getUTCDate()); + } + + // 6) If all parsing fails, default to "today" at UTC midnight + const now = new Date(); + return toUtcMidnight(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()); } /** * Retrieve all work packages, optionally filtered by query parameters. From ce10efba398ea70ef0be32057b1403ad1a38cec1 Mon Sep 17 00:00:00 2001 From: jasonk55 Date: Wed, 24 Sep 2025 16:42:57 -0400 Subject: [PATCH 05/16] #1192 Cleaned up controller methods --- .../src/services/work-packages.services.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/backend/src/services/work-packages.services.ts b/src/backend/src/services/work-packages.services.ts index 230a497bc1..8f1107efcd 100644 --- a/src/backend/src/services/work-packages.services.ts +++ b/src/backend/src/services/work-packages.services.ts @@ -47,19 +47,16 @@ export default class WorkPackagesService { const offsetMs = (clientOffsetMinutes ?? 0) * 60 * 1000; // Build a Date for local midnight converted to UTC using the offset - const toUtcFromLocalMidnight = (y: number, mZeroIdx: number, d: number) => - new Date(Date.UTC(y, mZeroIdx, d, 0, 0, 0, 0) + offsetMs); + const toUtcFromLocalMidnight = (y: number, m: number, d: number) => new Date(Date.UTC(y, m, d, 0, 0, 0, 0) + offsetMs); const parsed = new Date(input); // Derive user's local calendar date by shifting the instant by the offset const shifted = new Date(parsed.getTime() - offsetMs); - const y = shifted.getUTCFullYear(); - const m = shifted.getUTCMonth(); - const d = shifted.getUTCDate(); - const normalized = toUtcFromLocalMidnight(y, m, d); + const normalized = toUtcFromLocalMidnight(shifted.getUTCFullYear(), shifted.getUTCMonth(), shifted.getUTCDate()); return normalized; } + /** * Retrieve all work packages, optionally filtered by query parameters. * @@ -230,7 +227,10 @@ export default class WorkPackagesService { .reduce((prev, curr) => Math.max(prev, curr), 0) + 1; // Normalize incoming startDate using user's local midnight converted to UTC when offset provided - const date = WorkPackagesService.toUtcMidnight(startDate, clientOffsetMinutes); + const date = WorkPackagesService.toUtcMidnight( + startDate, + typeof clientOffsetMinutes === 'number' ? clientOffsetMinutes : undefined + ); const changesToCreate = crId ? [ @@ -364,7 +364,10 @@ export default class WorkPackagesService { const blockedByElems = await validateBlockedBys(blockedBy, organization.organizationId); // Normalize new startDate using user's local midnight converted to UTC when offset provided - const normalizedEdit = WorkPackagesService.toUtcMidnight(startDate, clientOffsetMinutes); + const normalizedEdit = WorkPackagesService.toUtcMidnight( + startDate, + typeof clientOffsetMinutes === 'number' ? clientOffsetMinutes : undefined + ); const changes = await getWorkPackageChanges( originalWorkPackage.wbsElement.name, name, From 57e484032bb4647aefbc0a0e6983376f20ab6294 Mon Sep 17 00:00:00 2001 From: jasonk55 Date: Wed, 24 Sep 2025 19:01:07 -0400 Subject: [PATCH 06/16] #1192 Fixed controllers file --- src/backend/src/controllers/work-packages.controllers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/src/controllers/work-packages.controllers.ts b/src/backend/src/controllers/work-packages.controllers.ts index f0174cdf1e..2f4cde41d1 100644 --- a/src/backend/src/controllers/work-packages.controllers.ts +++ b/src/backend/src/controllers/work-packages.controllers.ts @@ -61,7 +61,7 @@ export default class WorkPackagesController { descriptionBullets, projectWbsNum, req.organization, - typeof clientOffsetMinutes === 'number' ? clientOffsetMinutes : undefined + clientOffsetMinutes ); res.status(200).json(workPackage); @@ -102,7 +102,7 @@ export default class WorkPackagesController { leadId, managerId, req.organization, - typeof clientOffsetMinutes === 'number' ? clientOffsetMinutes : undefined + clientOffsetMinutes ); res.status(200).json({ message: 'Work package updated successfully' }); } catch (error: unknown) { From 1a9095c3b200c5717246521f236a2b1501d6ec64 Mon Sep 17 00:00:00 2001 From: harish Date: Thu, 16 Oct 2025 19:38:15 -0400 Subject: [PATCH 07/16] dayjs --- src/backend/package.json | 1 + .../src/services/work-packages.services.ts | 30 +++++++++++-------- yarn.lock | 8 +++++ 3 files changed, 27 insertions(+), 12 deletions(-) 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 8f1107efcd..a9fb6b68c5 100644 --- a/src/backend/src/services/work-packages.services.ts +++ b/src/backend/src/services/work-packages.services.ts @@ -26,6 +26,8 @@ import workPackageTransformer from '../transformers/work-packages.transformer'; import { updateBlocking, validateChangeRequestAccepted } from '../utils/change-requests.utils'; import { sendSlackUpcomingDeadlineNotification } from '../utils/slack.utils'; import { getWorkPackageChanges } from '../utils/changes.utils'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; import { DescriptionBulletDestination, addRawDescriptionBullets, @@ -35,26 +37,30 @@ import { getBlockingWorkPackages, validateBlockedBys } from '../utils/work-packa import { getDescriptionBulletQueryArgs } from '../prisma-query-args/description-bullets.query-args'; import { userHasPermission } from '../utils/users.utils'; +// Initialize Day.js plugins for timezone handling +dayjs.extend(utc); + /** Service layer containing logic for work package controller functions. */ export default class WorkPackagesService { /** - * Normalize an input date string to the user's local midnight converted to UTC. - * - If clientOffsetMinutes is provided (minutes from UTC, same sign as Date.getTimezoneOffset), we interpret day boundaries in that local timezone. - * - Derive the local calendar day of that instant in user's timezone, then normalize to that day's 00:00 local converted to UTC. - * - If clientOffsetMinutes is not provided, default to UTC day boundaries (00:00:00.000Z) for backwards compatibility. + * Normalize an input date string to the user's local midnight converted to UTC + * + * @param input - The date string to normalize + * @param clientOffsetMinutes - client timezone offset in minutes + * @returns date representing midnight */ private static toUtcMidnight(input: string, clientOffsetMinutes?: number): Date { - const offsetMs = (clientOffsetMinutes ?? 0) * 60 * 1000; + if (clientOffsetMinutes === undefined) { + return dayjs(input).utc().startOf('day').toDate(); + } - // Build a Date for local midnight converted to UTC using the offset - const toUtcFromLocalMidnight = (y: number, m: number, d: number) => new Date(Date.UTC(y, m, d, 0, 0, 0, 0) + offsetMs); + const inputDate = dayjs(input); + const userLocalDate = inputDate.subtract(clientOffsetMinutes, 'minutes'); - const parsed = new Date(input); + const userMidnight = userLocalDate.startOf('day'); + const utcMidnight = userMidnight.add(clientOffsetMinutes, 'minutes'); - // Derive user's local calendar date by shifting the instant by the offset - const shifted = new Date(parsed.getTime() - offsetMs); - const normalized = toUtcFromLocalMidnight(shifted.getUTCFullYear(), shifted.getUTCMonth(), shifted.getUTCDate()); - return normalized; + return utcMidnight.toDate(); } /** diff --git a/yarn.lock b/yarn.lock index 3052d16cf3..c93d2b252e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6911,6 +6911,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 @@ -8543,6 +8544,13 @@ __metadata: languageName: node linkType: hard +"dayjs@npm:^1.11.18": + version: 1.11.18 + resolution: "dayjs@npm:1.11.18" + checksum: cc90054bad30ab011417a7a474b2ffa70e7a28ca6f834d7e86fe53a408a40a14c174f26155072628670e9eda4c48c4ed0d847d2edf83d47c0bfb78be15bbf2dd + languageName: node + linkType: hard + "debug@npm:2.6.9, debug@npm:^2.6.0, debug@npm:^2.6.1": version: 2.6.9 resolution: "debug@npm:2.6.9" From fe522fa83f02d0719ed2f769da721a830bb2548c Mon Sep 17 00:00:00 2001 From: harish Date: Thu, 16 Oct 2025 19:39:06 -0400 Subject: [PATCH 08/16] prettier --- src/backend/src/services/work-packages.services.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/src/services/work-packages.services.ts b/src/backend/src/services/work-packages.services.ts index a9fb6b68c5..d4037b0e54 100644 --- a/src/backend/src/services/work-packages.services.ts +++ b/src/backend/src/services/work-packages.services.ts @@ -37,7 +37,6 @@ import { getBlockingWorkPackages, validateBlockedBys } from '../utils/work-packa import { getDescriptionBulletQueryArgs } from '../prisma-query-args/description-bullets.query-args'; import { userHasPermission } from '../utils/users.utils'; -// Initialize Day.js plugins for timezone handling dayjs.extend(utc); /** Service layer containing logic for work package controller functions. */ From bbe0d3c4143fc35b2f07dffd343c3c1a61a712a6 Mon Sep 17 00:00:00 2001 From: harish Date: Thu, 16 Oct 2025 20:06:41 -0400 Subject: [PATCH 09/16] getting rid of the offset --- .../controllers/work-packages.controllers.ts | 22 ++-------- .../src/services/work-packages.services.ts | 40 +++++-------------- src/frontend/package.json | 2 +- src/frontend/src/apis/work-packages.api.ts | 16 +------- src/frontend/src/utils/datetime.utils.ts | 10 +++-- yarn.lock | 9 +---- 6 files changed, 25 insertions(+), 74 deletions(-) diff --git a/src/backend/src/controllers/work-packages.controllers.ts b/src/backend/src/controllers/work-packages.controllers.ts index 2f4cde41d1..09981f66ac 100644 --- a/src/backend/src/controllers/work-packages.controllers.ts +++ b/src/backend/src/controllers/work-packages.controllers.ts @@ -44,8 +44,7 @@ export default class WorkPackagesController { // Create a work package with the given details static async createWorkPackage(req: Request, res: Response, next: NextFunction) { try { - const { name, crId, startDate, duration, blockedBy, descriptionBullets, projectWbsNum, clientOffsetMinutes } = - req.body; + const { name, crId, startDate, duration, blockedBy, descriptionBullets, projectWbsNum } = req.body; let { stage } = req.body; if (stage === 'NONE') { stage = null; @@ -60,8 +59,7 @@ export default class WorkPackagesController { blockedBy, descriptionBullets, projectWbsNum, - req.organization, - clientOffsetMinutes + req.organization ); res.status(200).json(workPackage); @@ -73,18 +71,7 @@ export default class WorkPackagesController { // Edit a work package to the given specifications static async editWorkPackage(req: Request, res: Response, next: NextFunction) { try { - const { - workPackageId, - name, - crId, - startDate, - duration, - blockedBy, - descriptionBullets, - leadId, - managerId, - clientOffsetMinutes - } = req.body; + const { workPackageId, name, crId, startDate, duration, blockedBy, descriptionBullets, leadId, managerId } = req.body; let { stage } = req.body; if (stage === 'NONE') { stage = null; @@ -101,8 +88,7 @@ export default class WorkPackagesController { descriptionBullets, leadId, managerId, - req.organization, - clientOffsetMinutes + req.organization ); res.status(200).json({ message: 'Work package updated successfully' }); } catch (error: unknown) { diff --git a/src/backend/src/services/work-packages.services.ts b/src/backend/src/services/work-packages.services.ts index d4037b0e54..499f95d812 100644 --- a/src/backend/src/services/work-packages.services.ts +++ b/src/backend/src/services/work-packages.services.ts @@ -43,23 +43,11 @@ dayjs.extend(utc); export default class WorkPackagesService { /** * Normalize an input date string to the user's local midnight converted to UTC - * - * @param input - The date string to normalize - * @param clientOffsetMinutes - client timezone offset in minutes - * @returns date representing midnight + * @param input - The date string to normalize + * @returns date object representing midnight UTC */ - private static toUtcMidnight(input: string, clientOffsetMinutes?: number): Date { - if (clientOffsetMinutes === undefined) { - return dayjs(input).utc().startOf('day').toDate(); - } - - const inputDate = dayjs(input); - const userLocalDate = inputDate.subtract(clientOffsetMinutes, 'minutes'); - - const userMidnight = userLocalDate.startOf('day'); - const utcMidnight = userMidnight.add(clientOffsetMinutes, 'minutes'); - - return utcMidnight.toDate(); + private static toUtcMidnight(input: string): Date { + return dayjs(input).utc().startOf('day').toDate(); } /** @@ -182,8 +170,7 @@ export default class WorkPackagesService { blockedBy: WbsNumber[], descriptionBullets: DescriptionBulletPreview[], projectWbsNum: WbsNumber, - organization: Organization, - clientOffsetMinutes?: number + organization: Organization ): Promise { if (await userHasPermission(user.userId, organization.organizationId, isGuest)) throw new AccessDeniedGuestException('create work packages'); @@ -231,11 +218,8 @@ export default class WorkPackagesService { .map((element) => element.wbsElement.workPackageNumber) .reduce((prev, curr) => Math.max(prev, curr), 0) + 1; - // Normalize incoming startDate using user's local midnight converted to UTC when offset provided - const date = WorkPackagesService.toUtcMidnight( - startDate, - typeof clientOffsetMinutes === 'number' ? clientOffsetMinutes : undefined - ); + // Parse the startDate to UTC midnight (frontend handles timezone formatting) + const date = WorkPackagesService.toUtcMidnight(startDate); const changesToCreate = crId ? [ @@ -335,8 +319,7 @@ export default class WorkPackagesService { descriptionBullets: DescriptionBulletPreview[], leadId: string | null, managerId: string | null, - organization: Organization, - clientOffsetMinutes?: number + organization: Organization ): Promise { const { userId } = user; // verify user is allowed to edit work packages @@ -368,11 +351,8 @@ export default class WorkPackagesService { const blockedByElems = await validateBlockedBys(blockedBy, organization.organizationId); - // Normalize new startDate using user's local midnight converted to UTC when offset provided - const normalizedEdit = WorkPackagesService.toUtcMidnight( - startDate, - typeof clientOffsetMinutes === 'number' ? clientOffsetMinutes : undefined - ); + // Parse new startDate to UTC midnight (frontend handles timezone formatting) + const normalizedEdit = WorkPackagesService.toUtcMidnight(startDate); const changes = await getWorkPackageChanges( originalWorkPackage.wbsElement.name, name, 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 1d5ed68441..02e164f099 100644 --- a/src/frontend/src/apis/work-packages.api.ts +++ b/src/frontend/src/apis/work-packages.api.ts @@ -17,8 +17,6 @@ export interface WorkPackageApiInputs { crId?: string; blockedBy: WbsNumber[]; descriptionBullets: DescriptionBulletPreview[]; - /** Optional client timezone offset in minutes (same sign as Date.getTimezoneOffset). */ - clientOffsetMinutes?: number; } export interface WorkPackageCreateArgs extends WorkPackageApiInputs { @@ -55,12 +53,7 @@ export const getSingleWorkPackage = (wbsNum: WbsNumber) => { * @param payload Payload containing all the necessary data to create a work package. */ export const createSingleWorkPackage = (payload: WorkPackageCreateArgs) => { - const clientOffsetMinutes = - typeof payload.clientOffsetMinutes === 'number' ? payload.clientOffsetMinutes : new Date().getTimezoneOffset(); - return axios.post(apiUrls.workPackagesCreate(), { - ...payload, - clientOffsetMinutes - }); + return axios.post(apiUrls.workPackagesCreate(), payload); }; /** @@ -70,12 +63,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) => { - const clientOffsetMinutes = - typeof payload.clientOffsetMinutes === 'number' ? payload.clientOffsetMinutes : new Date().getTimezoneOffset(); - return axios.post<{ message: string }>(apiUrls.workPackagesEdit(), { - ...payload, - clientOffsetMinutes - }); + return axios.post<{ message: string }>(apiUrls.workPackagesEdit(), payload); }; /** diff --git a/src/frontend/src/utils/datetime.utils.ts b/src/frontend/src/utils/datetime.utils.ts index ea7c37442b..8eb7cd564a 100644 --- a/src/frontend/src/utils/datetime.utils.ts +++ b/src/frontend/src/utils/datetime.utils.ts @@ -19,10 +19,14 @@ export const dateFormatMonthDate = (date: Date) => { return dayjs(date).format('MMM D'); }; +/** + * Transforms a Date object to a string in YYYY/MM/DD format using the user's local date + * This avoids timezone offset issues by using Day.js to format the date as-is + * @param date the Date object to transform + * @returns the date string in YYYY/MM/DD format + */ 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/yarn.lock b/yarn.lock index c93d2b252e..696a216a1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8537,13 +8537,6 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:^1.11.10": - version: 1.11.13 - resolution: "dayjs@npm:1.11.13" - checksum: f388db88a6aa93956c1f6121644e783391c7b738b73dbc54485578736565c8931bdfba4bb94e9b1535c6e509c97d5deb918bbe1ae6b34358d994de735055cca9 - languageName: node - linkType: hard - "dayjs@npm:^1.11.18": version: 1.11.18 resolution: "dayjs@npm:1.11.18" @@ -10947,7 +10940,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 From 0b3b8aebf8f980a5d88e206d9b84c83569339785 Mon Sep 17 00:00:00 2001 From: harish Date: Thu, 16 Oct 2025 20:11:52 -0400 Subject: [PATCH 10/16] prettier --- src/backend/src/services/work-packages.services.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/src/services/work-packages.services.ts b/src/backend/src/services/work-packages.services.ts index 499f95d812..dd408a6f6a 100644 --- a/src/backend/src/services/work-packages.services.ts +++ b/src/backend/src/services/work-packages.services.ts @@ -43,7 +43,7 @@ dayjs.extend(utc); export default class WorkPackagesService { /** * Normalize an input date string to the user's local midnight converted to UTC - * @param input - The date string to normalize + * @param input - The date string to normalize * @returns date object representing midnight UTC */ private static toUtcMidnight(input: string): Date { From f2f77034f574a9c02807b5e299ab543a34eef484 Mon Sep 17 00:00:00 2001 From: harish Date: Thu, 23 Oct 2025 17:29:03 -0400 Subject: [PATCH 11/16] syntax error --- src/backend/src/services/work-packages.services.ts | 4 ++-- src/frontend/src/utils/datetime.utils.ts | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/backend/src/services/work-packages.services.ts b/src/backend/src/services/work-packages.services.ts index dd408a6f6a..b4a596e409 100644 --- a/src/backend/src/services/work-packages.services.ts +++ b/src/backend/src/services/work-packages.services.ts @@ -43,11 +43,11 @@ dayjs.extend(utc); export default class WorkPackagesService { /** * Normalize an input date string to the user's local midnight converted to UTC - * @param input - The date string to normalize + * @param input - The date string from the frontend * @returns date object representing midnight UTC */ private static toUtcMidnight(input: string): Date { - return dayjs(input).utc().startOf('day').toDate(); + return dayjs.utc(input).startOf('day').toDate(); } /** diff --git a/src/frontend/src/utils/datetime.utils.ts b/src/frontend/src/utils/datetime.utils.ts index 8eb7cd564a..d0d1292967 100644 --- a/src/frontend/src/utils/datetime.utils.ts +++ b/src/frontend/src/utils/datetime.utils.ts @@ -20,13 +20,12 @@ export const dateFormatMonthDate = (date: Date) => { }; /** - * Transforms a Date object to a string in YYYY/MM/DD format using the user's local date - * This avoids timezone offset issues by using Day.js to format the date as-is + * 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 + * @returns the date string in YYYY-MM-DD format representing the user's calendar date */ export const transformDate = (date: Date) => { - return dayjs(date).format('YYYY/MM/DD'); + return dayjs(date).format('YYYY-MM-DD'); }; export const formatDate = (date: Date) => { From 73dc4a7a70823efc0dc7e43333280e0cfc36d097 Mon Sep 17 00:00:00 2001 From: harish Date: Wed, 5 Nov 2025 18:47:32 -0500 Subject: [PATCH 12/16] new date utils --- .../src/services/work-packages.services.ts | 19 +++------- src/shared/src/date-utils.ts | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/backend/src/services/work-packages.services.ts b/src/backend/src/services/work-packages.services.ts index bc0f305d95..b62d86465d 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 { @@ -29,8 +30,6 @@ import workPackageTransformer, { workPackagePreviewTransformer } from '../transf import { updateBlocking, validateChangeRequestAccepted } from '../utils/change-requests.utils'; import { sendSlackUpcomingDeadlineNotification } from '../utils/slack.utils'; import { getWorkPackageChanges } from '../utils/changes.utils'; -import dayjs from 'dayjs'; -import utc from 'dayjs/plugin/utc'; import { DescriptionBulletDestination, addRawDescriptionBullets, @@ -41,18 +40,8 @@ import { getDescriptionBulletQueryArgs } from '../prisma-query-args/description- import { userHasPermission } from '../utils/users.utils'; import { getUserPreviewQueryArgs } from '../prisma-query-args/user.query-args'; -dayjs.extend(utc); - /** Service layer containing logic for work package controller functions. */ export default class WorkPackagesService { - /** - * Normalize an input date string to the user's local midnight converted to UTC - * @param input - The date string from the frontend - * @returns date object representing midnight UTC - */ - private static toUtcMidnight(input: string): Date { - return dayjs.utc(input).startOf('day').toDate(); - } /** * Retrieve all work packages, optionally filtered by query parameters. @@ -222,7 +211,7 @@ export default class WorkPackagesService { .reduce((prev, curr) => Math.max(prev, curr), 0) + 1; // Parse the startDate to UTC midnight (frontend handles timezone formatting) - const date = WorkPackagesService.toUtcMidnight(startDate); + const date = toUtcMidnight(startDate); const changesToCreate = crId ? [ @@ -355,7 +344,7 @@ export default class WorkPackagesService { const blockedByElems = await validateBlockedBys(blockedBy, organization.organizationId); // Parse new startDate to UTC midnight (frontend handles timezone formatting) - const normalizedEdit = WorkPackagesService.toUtcMidnight(startDate); + const normalizedEdit = toUtcMidnight(startDate); const changes = await getWorkPackageChanges( originalWorkPackage.wbsElement.name, name, diff --git a/src/shared/src/date-utils.ts b/src/shared/src/date-utils.ts index a7980552c3..1f98184e1d 100644 --- a/src/shared/src/date-utils.ts +++ b/src/shared/src/date-utils.ts @@ -3,8 +3,43 @@ * 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(); +}; + + /** + * @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 From c0a625dcb5ea5bde754cf5edd5a98358badbbb84 Mon Sep 17 00:00:00 2001 From: harish Date: Thu, 20 Nov 2025 13:55:41 -0500 Subject: [PATCH 13/16] #1192 prettier --- src/backend/src/services/work-packages.services.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/src/services/work-packages.services.ts b/src/backend/src/services/work-packages.services.ts index b62d86465d..3bb8874f2e 100644 --- a/src/backend/src/services/work-packages.services.ts +++ b/src/backend/src/services/work-packages.services.ts @@ -42,7 +42,6 @@ import { getUserPreviewQueryArgs } from '../prisma-query-args/user.query-args'; /** Service layer containing logic for work package controller functions. */ export default class WorkPackagesService { - /** * Retrieve all work packages, optionally filtered by query parameters. * From bf2a3b31beebb144928218777875a0f3b1f64d5e Mon Sep 17 00:00:00 2001 From: harish Date: Thu, 20 Nov 2025 13:57:15 -0500 Subject: [PATCH 14/16] #1192 prettier --- src/shared/src/date-utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shared/src/date-utils.ts b/src/shared/src/date-utils.ts index 1f98184e1d..1550d46a2a 100644 --- a/src/shared/src/date-utils.ts +++ b/src/shared/src/date-utils.ts @@ -9,14 +9,14 @@ 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(); }; - /** +/** * @param utcDate - The UTC date from the database * @returns Date string in local timezone (YYYY-MM-DD format) */ @@ -24,14 +24,14 @@ 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 From c3829c642ff3105614f01328e032902e9e6627a5 Mon Sep 17 00:00:00 2001 From: harish Date: Mon, 22 Dec 2025 13:25:44 -0500 Subject: [PATCH 15/16] #1192 applying date method in finance --- .../FinanceDashboard/AdminFinanceDashboard.tsx | 6 +++--- .../FinanceDashboard/GeneralFinanceDashboard.tsx | 5 +++-- src/shared/src/date-utils.ts | 12 ++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) 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/shared/src/date-utils.ts b/src/shared/src/date-utils.ts index 1550d46a2a..4352743723 100644 --- a/src/shared/src/date-utils.ts +++ b/src/shared/src/date-utils.ts @@ -16,6 +16,18 @@ 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) From b04d683f23ae5c5777bc7d516433ef21ff58db6f Mon Sep 17 00:00:00 2001 From: harish Date: Mon, 22 Dec 2025 14:37:49 -0500 Subject: [PATCH 16/16] #1192 prettier --- src/backend/src/services/work-packages.services.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backend/src/services/work-packages.services.ts b/src/backend/src/services/work-packages.services.ts index 3bb8874f2e..3239eb7d01 100644 --- a/src/backend/src/services/work-packages.services.ts +++ b/src/backend/src/services/work-packages.services.ts @@ -209,7 +209,6 @@ export default class WorkPackagesService { .map((element) => element.wbsElement.workPackageNumber) .reduce((prev, curr) => Math.max(prev, curr), 0) + 1; - // Parse the startDate to UTC midnight (frontend handles timezone formatting) const date = toUtcMidnight(startDate); const changesToCreate = crId @@ -342,7 +341,6 @@ export default class WorkPackagesService { const blockedByElems = await validateBlockedBys(blockedBy, organization.organizationId); - // Parse new startDate to UTC midnight (frontend handles timezone formatting) const normalizedEdit = toUtcMidnight(startDate); const changes = await getWorkPackageChanges( originalWorkPackage.wbsElement.name,