Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/components/Holidays/CalendarView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,19 @@ const mockHolidays: Holiday[] = [
},
]


export const Default: Story = {
render: () => {
const [currentDate, setCurrentDate] = useState(new Date(2025, 0, 1)) // January 2025
const today = TimeUtil.toUtcMidnight(new Date(2025, 0, 1)) // Server's "today"

const correctedCurrentDate = TimeUtil.toUtcMidnight(currentDate)
return (
<CalendarView
currentDate={correctedCurrentDate}
setCurrentDate={setCurrentDate}
holidays={mockHolidays}
today={today}
/>
)
},
Expand Down
40 changes: 26 additions & 14 deletions src/components/Holidays/CalendarView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface CalendarViewProps {
currentDate: Date
setCurrentDate: (date: Date) => void
holidays: Holiday[]
today: Date // Server-determined "today" to avoid hydration mismatch
}

interface Holiday {
Expand All @@ -24,18 +25,25 @@ interface Holiday {
leaveType: 'Full Day' | 'Morning' | 'Afternoon'
}

export function CalendarView({ currentDate, setCurrentDate, holidays }: CalendarViewProps) {
export function CalendarView({ currentDate, setCurrentDate, holidays, today }: CalendarViewProps) {
const [selectedDay, setSelectedDay] = useState<Date | null>(null)

// Helper to compare dates by their UTC year/month/day values
const isSameUtcDay = (a: Date, b: Date) =>
a.getUTCFullYear() === b.getUTCFullYear() &&
a.getUTCMonth() === b.getUTCMonth() &&
a.getUTCDate() === b.getUTCDate()

const firstDayOfMonth =
(new Date(currentDate.getFullYear(), currentDate.getMonth(), 1).getDay() + 6) % 7
(new Date(Date.UTC(currentDate.getUTCFullYear(), currentDate.getUTCMonth(), 1)).getUTCDay() + 6) % 7

const days = Array.from({ length: 42 }, (_, i) => {
const day = new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
i - firstDayOfMonth + 1,
currentDate.getHours(),
Date.UTC(
currentDate.getUTCFullYear(),
currentDate.getUTCMonth(),
i - firstDayOfMonth + 1,
),
)
const filteredHolidays = holidays.filter(
(h) =>
Expand All @@ -44,28 +52,32 @@ export function CalendarView({ currentDate, setCurrentDate, holidays }: Calendar
)
return {
date: day,
isCurrentMonth: day.getMonth() === currentDate.getMonth(),
isToday: day.toDateString() === new Date().toDateString(),
isCurrentMonth: day.getUTCMonth() === currentDate.getUTCMonth(),
isToday: isSameUtcDay(day, today),
holidays: filteredHolidays,
}
})

const prevMonth = () => {
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1))
setCurrentDate(new Date(Date.UTC(currentDate.getUTCFullYear(), currentDate.getUTCMonth() - 1, 1)))
}
const nextMonth = () => {
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1))
setCurrentDate(new Date(Date.UTC(currentDate.getUTCFullYear(), currentDate.getUTCMonth() + 1, 1)))
}
const setToday = () => {
setCurrentDate(new Date())
setCurrentDate(TimeUtil.toUtcMidnight(new Date()))
}

// Format month/year using UTC values to avoid hydration mismatch
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
const monthYearDisplay = `${months[currentDate.getUTCMonth()]} ${currentDate.getUTCFullYear()}`

return (
<div className="lg:flex lg:h-full lg:flex-col">
<header className="flex items-center justify-between border-b border-gray-200 px-6 py-4 lg:flex-none">
<h1 className="text-base font-semibold text-foreground">
<time dateTime={currentDate.toISOString()}>
{currentDate.toLocaleString('default', { month: 'long', year: 'numeric' })}
{monthYearDisplay}
</time>
</h1>
<div className="flex items-center">
Expand Down Expand Up @@ -139,7 +151,7 @@ export function CalendarView({ currentDate, setCurrentDate, holidays }: Calendar
day.isToday && 'bg-indigo-600 font-semibold text-white',
)}
>
{day.date.getDate()}
{day.date.getUTCDate()}
</time>
{day.holidays.length > 0 && (
<ol className="mt-2 space-y-1 overflow-visible">
Expand Down Expand Up @@ -189,7 +201,7 @@ export function CalendarView({ currentDate, setCurrentDate, holidays }: Calendar
'flex h-6 w-6 items-center justify-center rounded-full bg-gray-900',
)}
>
{day.date.getDate()}
{day.date.getUTCDate()}
</time>
<span className="sr-only">{day.holidays.length} holidays</span>
{day.holidays.length > 0 && (
Expand Down
1 change: 1 addition & 0 deletions src/components/Holidays/HolidayTracker.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const Default: Story = {
args: {
// Mock holidays data
currentDate: format(new Date(2025, 0, 2), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx"), // January 2025
today: format(new Date(2025, 0, 2), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx"), // Today's date for highlighting
holidays: [
{
id: '1',
Expand Down
6 changes: 5 additions & 1 deletion src/components/Holidays/HolidayTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ interface HolidayTrackerProps {
holidays: Holiday[]
leaveApprovals: LeaveRequest[]
employees: Employee[]
currentDate: string // ISO 8601 string
currentDate: string // ISO 8601 string - the date being viewed
today: string // ISO 8601 string - server's current date for "today" highlighting
currentUser: { grade: string; remainingLeaveDays: number }
submitLeaveRequest?: (formData: FormData) => Promise<{ success: boolean; message: string }>
approveLeave: (ids: string[]) => Promise<{ success: boolean; message: string }>
Expand All @@ -34,6 +35,7 @@ export function HolidayTracker({
holidays,
currentUser,
currentDate,
today,
leaveApprovals,
employees,
submitLeaveRequest,
Expand All @@ -46,6 +48,7 @@ export function HolidayTracker({
const isLoading = false

const parsedCurrentDate = TimeUtil.toUtcMidnight(parseISO(currentDate))
const parsedToday = TimeUtil.toUtcMidnight(parseISO(today))

const setCurrentDate = async (date: Date) => {
const formattedDate = format(date, 'dd-MM-yyyy')
Expand Down Expand Up @@ -147,6 +150,7 @@ export function HolidayTracker({
currentDate={parsedCurrentDate}
setCurrentDate={setCurrentDate}
holidays={holidays}
today={parsedToday}
/>
)}
{currentTab === 'Request Leave' && (
Expand Down
48 changes: 39 additions & 9 deletions src/components/Holidays/RequestLeave.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,34 @@ const ButtonGroup: React.FC<ButtonGroupProps> = ({ options, value, onChange, dis
}

export function RequestLeave({ remainingDays, submitLeaveRequest }: RequestLeaveProps) {
const [startDate, setStartDate] = useState<Date | undefined>(new Date())
const [endDate, setEndDate] = useState<Date | undefined>(new Date())
// Helper to create a UTC midnight date from the user's local date (year, month, day)
// This preserves the user's selected day regardless of their timezone
const setToMidnightUTC = (date: Date) => {
return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0))
}

// Format date using UTC values to avoid hydration mismatch between server/client timezones
const formatDateUTC = (date: Date) => {
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
const day = date.getUTCDate()
const month = months[date.getUTCMonth()]
const year = date.getUTCFullYear()
const suffix = day === 1 || day === 21 || day === 31 ? 'st' : day === 2 || day === 22 ? 'nd' : day === 3 || day === 23 ? 'rd' : 'th'
return `${month} ${day}${suffix}, ${year}`
}

const [startDate, setStartDate] = useState<Date | undefined>(undefined)
const [endDate, setEndDate] = useState<Date | undefined>(undefined)

// Set initial dates on client only to avoid hydration mismatch
useEffect(() => {
if (startDate === undefined) setStartDate(setToMidnightUTC(new Date()))
if (endDate === undefined) setEndDate(setToMidnightUTC(new Date()))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const [leaveType, setLeaveType] = useState('Full Day')
const [isMultipleDays, setIsMultipleDays] = useState(false)
const [totalDays, setTotalDays] = useState(1)
const [totalDays, setTotalDays] = useState(0)
const router = useRouter()

useEffect(() => {
Expand Down Expand Up @@ -129,17 +152,20 @@ export function RequestLeave({ remainingDays, submitLeaveRequest }: RequestLeave
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{startDate ? format(startDate, 'PPP') : <span>Pick a date</span>}
{startDate ? formatDateUTC(startDate) : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={startDate}
onSelect={(date) => {
setStartDate(date)
if (date && (!endDate || date > endDate)) {
setEndDate(date)
if (date) {
const dateMidnight = setToMidnightUTC(date)
setStartDate(dateMidnight)
if (!endDate || date > endDate) {
setEndDate(dateMidnight)
}
}
}}
initialFocus
Expand All @@ -159,14 +185,18 @@ export function RequestLeave({ remainingDays, submitLeaveRequest }: RequestLeave
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{endDate ? format(endDate, 'PPP') : <span>Pick a date</span>}
{endDate ? formatDateUTC(endDate) : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={endDate}
onSelect={setEndDate}
onSelect={(date) => {
if (date) {
setEndDate(setToMidnightUTC(date))
}
}}
disabled={(date) => (startDate ? date < startDate : false)}
initialFocus
/>
Expand Down
24 changes: 17 additions & 7 deletions src/utils/DaysUtil.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { bankHolidays } from './BankHolidays'

export const isDayOff = (date: Date) => {
const isSunday = date.getDay() === 0
const isSaturday = date.getDay() === 6
const isBankHoliday = bankHolidays.some((h) => h.toISOString() === date.toISOString())
const isSunday = date.getUTCDay() === 0
const isSaturday = date.getUTCDay() === 6
const isBankHoliday = bankHolidays.some(
(h) =>
h.getFullYear() === date.getUTCFullYear() &&
h.getMonth() === date.getUTCMonth() &&
h.getDate() === date.getUTCDate()
)

return isSunday || isSaturday || isBankHoliday
}

export const getTotalDaysBetween = (startDate: Date, endDate: Date, isHalfDay = false) => {
const start = new Date(startDate)
const end = new Date(endDate)
// Work with UTC day values to avoid timezone issues
let start = new Date(Date.UTC(startDate.getUTCFullYear(), startDate.getUTCMonth(), startDate.getUTCDate()))
const end = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate()))

let count = 0

Expand All @@ -23,7 +29,7 @@ export const getTotalDaysBetween = (startDate: Date, endDate: Date, isHalfDay =
count++
}

start.setDate(start.getDate() + 1)
start = new Date(start.getTime() + 24 * 60 * 60 * 1000) // Add 1 day in milliseconds
}

return count
Expand All @@ -34,6 +40,10 @@ export const getIsMultipleDays = (startDate?: Date, endDate?: Date) => {
return false
}

const isMultipleDays = startDate.toDateString() !== endDate.toDateString()
// Compare UTC dates to avoid timezone issues
const isMultipleDays =
startDate.getUTCFullYear() !== endDate.getUTCFullYear() ||
startDate.getUTCMonth() !== endDate.getUTCMonth() ||
startDate.getUTCDate() !== endDate.getUTCDate()
return isMultipleDays
}
Loading