diff --git a/src/controls/calendar/hooks/useCalendar.ts b/src/controls/calendar/hooks/useCalendar.ts index 260908331..59aab2594 100644 --- a/src/controls/calendar/hooks/useCalendar.ts +++ b/src/controls/calendar/hooks/useCalendar.ts @@ -1,4 +1,5 @@ /* eslint-disable no-unmodified-loop-condition */ +import { format } from 'date-fns'; import { IEvent } from '../models/IEvents'; import { useCallback, } from 'react'; import { v4 as uuidv4 } from 'uuid'; // Use UUID for generating unique IDs @@ -39,9 +40,10 @@ export const useCalendar = (timezone: string): IUseCalendar => { // Memoized helper for timezone handling const toLocalDate = useCallback( (dateString: string): Date => { - return new Date( - new Date(dateString).toLocaleString(undefined, { timeZone: timezone }) + const localDate = new Date( + new Date(dateString).toLocaleString('en-US', { timeZone: timezone }) ); + return localDate }, [timezone] ); @@ -59,7 +61,7 @@ export const useCalendar = (timezone: string): IUseCalendar => { for (let day = 1; day <= daysInMonth; day++) { const date = new Date(year, month, day); - const dateString = date.toISOString().split('T')[0]; + const dateString = format(date, 'yyyy-MM-dd'); calendarEventsByDay[dateString] = []; } @@ -69,7 +71,7 @@ export const useCalendar = (timezone: string): IUseCalendar => { const currentDate = new Date(eventStart); while (currentDate <= eventEnd) { - const dateString = currentDate.toISOString().split('T')[0]; + const dateString = format(currentDate, 'yyyy-MM-dd'); if (calendarEventsByDay[dateString]) { calendarEventsByDay[dateString].push(event); } @@ -91,7 +93,7 @@ export const useCalendar = (timezone: string): IUseCalendar => { for (let i = 0; i < 7; i++) { const currentDate = new Date(start); currentDate.setDate(start.getDate() + i); - const dateString = currentDate.toISOString().split('T')[0]; + const dateString = format(currentDate, 'yyyy-MM-dd'); const dayTimeSlots: TimeSlot[] = Array.from( { length: 48 }, @@ -111,8 +113,8 @@ export const useCalendar = (timezone: string): IUseCalendar => { if (event.isFullDay) { if ( - eventStart.toISOString().split('T')[0] <= dateString && - eventEnd.toISOString().split('T')[0] >= dateString + format(eventStart, 'yyyy-MM-dd') <= dateString && + format(eventEnd, 'yyyy-MM-dd') >= dateString ) { fullDayEvents.push(event); } @@ -120,12 +122,12 @@ export const useCalendar = (timezone: string): IUseCalendar => { } if ( - eventStart.toISOString().split('T')[0] <= dateString && - eventEnd.toISOString().split('T')[0] >= dateString + format(eventStart, 'yyyy-MM-dd') <= dateString && + format(eventEnd, 'yyyy-MM-dd') >= dateString ) { const currentSlot = new Date(eventStart); while (currentSlot <= eventEnd) { - const slotDateString = currentSlot.toISOString().split('T')[0]; + const slotDateString = format(currentSlot, 'yyyy-MM-dd'); if (slotDateString === dateString) { const slotIndex = currentSlot.getHours() * 2 + diff --git a/src/webparts/controlsTest/components/TestCalendarControl.tsx b/src/webparts/controlsTest/components/TestCalendarControl.tsx index b99ab5f5b..fee0bd8b1 100644 --- a/src/webparts/controlsTest/components/TestCalendarControl.tsx +++ b/src/webparts/controlsTest/components/TestCalendarControl.tsx @@ -193,6 +193,87 @@ export const mockEvents: IEvent[] = [ imageUrl: "https://via.placeholder.com/150", color: "magenta", }, + // midnight event — UTC+10 users: 00:30 local + { + id: "11", + title: "Midnight Deployment", + start: "2026-06-14T00:30:00", + end: "2026-06-14T01:00:00", + category: "Task", + importance: "High", + description: "Edge case: early morning event (00:30 local). Fails to render in month view without the date key fix for UTC+ timezones.", + enableOnHover: true, + color: "steel", + webLink: null, + }, + // 02:00 local — UTC+4 users: 02:00 local + { + id: "12", + title: "Early Morning Sync", + start: "2026-06-15T02:00:00", + end: "2026-06-15T02:30:00", + category: "Meeting", + importance: "Medium", + description: "Edge case: 02:00 local. Misplaced in month view for UTC+4 users without the date key fix.", + enableOnHover: true, + color: "lavender", + webLink: null, + }, + // event spanning midnight + { + id: "13", + title: "Overnight On-call", + start: "2026-06-17T23:00:00", + end: "2026-06-18T01:00:00", + category: "Task", + importance: "High", + description: "Edge case: spans midnight. Should appear on both Jun 15 and Jun 16.", + enableOnHover: true, + color: "cranberry", + webLink: null, + }, + // negative offset (UTC-5, New York) + { + id: "14", + title: "New York All-Hands (UTC-5)", + start: "2026-06-19T14:00:00-05:00", + end: "2026-06-19T15:00:00-05:00", + category: "Meeting", + location: "New York HQ", + importance: "High", + description: "Timezone test: negative offset UTC-5. Stored as 19:00 UTC — each user sees it in their local time.", + enableOnHover: true, + color: "navy", + webLink: "https://outlook.com", + }, + // zero offset (UTC) + { + id: "15", + title: "London Sprint Review (UTC+0)", + start: "2026-06-20T09:00:00Z", + end: "2026-06-20T10:00:00Z", + category: "Meeting", + location: "London Office", + importance: "Medium", + description: "Timezone test: UTC (Z suffix). Each user sees it in their local time.", + enableOnHover: true, + color: "anchor", + webLink: "https://outlook.com", + }, + // positive offset (UTC+5:30, India) + { + id: "16", + title: "Mumbai Design Review (UTC+5:30)", + start: "2026-06-20T15:30:00+05:30", + end: "2026-06-20T16:30:00+05:30", + category: "Meeting", + location: "Mumbai Office", + importance: "Medium", + description: "Timezone test: positive offset UTC+5:30. Stored as 10:00 UTC — each user sees it in their local time.", + enableOnHover: true, + color: "brass", + webLink: "https://outlook.com", + }, ]; export const TestCalendarControl: React.FunctionComponent = (