Merge pull request #20 from zachd/feat-calendar-start-date-c6a9d
Add start dates
This commit is contained in:
1236
package-lock.json
generated
1236
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,16 +7,21 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:ui": "vitest --ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@vitest/ui": "^4.0.8",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.3"
|
||||
"vite": "^5.0.3",
|
||||
"vitest": "^4.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"date-holidays": "^3.23.12",
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
export let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }>;
|
||||
export let selectedCountryCode: string;
|
||||
export let weekendDays: number[] = [6, 0];
|
||||
export let startDate: Date = new Date(year, 0, 1);
|
||||
export let isActive: boolean = true;
|
||||
|
||||
// Function to determine the first day of the week based on locale
|
||||
function getFirstDayOfWeek(locale: string): number {
|
||||
@@ -84,12 +86,19 @@
|
||||
return weekendDays.includes(date.getDay());
|
||||
}
|
||||
|
||||
function isPastDate(day: number): boolean {
|
||||
const date = new Date(year, month, day);
|
||||
// Normalize startDate to current year for comparison
|
||||
const startDateInYear = new Date(year, startDate.getMonth(), startDate.getDate());
|
||||
return date < startDateInYear;
|
||||
}
|
||||
|
||||
const dayInitials = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
||||
|
||||
$: orderedDayInitials = dayInitials.slice(firstDayOfWeek).concat(dayInitials.slice(0, firstDayOfWeek));
|
||||
</script>
|
||||
|
||||
<div class="calendar">
|
||||
<div class="calendar {isActive ? '' : 'excluded-month'}">
|
||||
<div class="month-name">{new Date(year, month).toLocaleString('default', { month: 'long' })}</div>
|
||||
|
||||
{#each orderedDayInitials as dayInitial}
|
||||
@@ -101,7 +110,8 @@
|
||||
{/each}
|
||||
{#each Array.from({ length: daysInMonth }, (_, i) => i + 1) as day}
|
||||
{@const holiday = getHoliday(day)}
|
||||
<div class="day {isWeekend(new Date(year, month, day)) ? 'weekend' : ''} {holiday ? 'holiday' : ''} {isOptimizedDayOff(day) ? 'optimized' : ''} {isConsecutiveDayOff(day) ? 'consecutive-day' : ''}">
|
||||
{@const pastDate = isPastDate(day)}
|
||||
<div class="day {isWeekend(new Date(year, month, day)) ? 'weekend' : ''} {holiday ? 'holiday' : ''} {isOptimizedDayOff(day) ? 'optimized' : ''} {isConsecutiveDayOff(day) ? 'consecutive-day' : ''} {pastDate ? 'past-date' : ''}">
|
||||
<span class={holiday?.hidden ? 'strikethrough' : ''}>{day}</span>
|
||||
{#if holiday}
|
||||
<Tooltip text={holiday.name} />
|
||||
@@ -137,6 +147,14 @@
|
||||
color: #c5c6c7;
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
.excluded-month .month-name {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.excluded-month .day-initial {
|
||||
color: #666;
|
||||
}
|
||||
.day {
|
||||
aspect-ratio: 1;
|
||||
text-align: center;
|
||||
@@ -202,4 +220,12 @@
|
||||
text-decoration: line-through;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.past-date {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.past-date span {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
</style>
|
||||
781
src/lib/holidayUtils.test.ts
Normal file
781
src/lib/holidayUtils.test.ts
Normal file
@@ -0,0 +1,781 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { getHolidaysForYear, optimizeDaysOff, calculateConsecutiveDaysOff } from './holidayUtils';
|
||||
|
||||
// Test constants
|
||||
const TEST_YEAR = 2024;
|
||||
const DEFAULT_WEEKENDS = [0, 6]; // Sunday, Saturday
|
||||
const CUSTOM_WEEKENDS = [5, 6]; // Friday, Saturday
|
||||
|
||||
// Mock browser APIs
|
||||
const mockNavigator = {
|
||||
languages: ['en', 'en-US']
|
||||
};
|
||||
|
||||
const mockIntlDateTimeFormat = vi.fn(() => ({
|
||||
resolvedOptions: () => ({ timeZone: 'America/New_York' })
|
||||
}));
|
||||
|
||||
describe('holidayUtils', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.stubGlobal('navigator', mockNavigator);
|
||||
vi.stubGlobal('Intl', {
|
||||
...Intl,
|
||||
DateTimeFormat: mockIntlDateTimeFormat
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHolidaysForYear', () => {
|
||||
it('should return holidays for a given year and country', () => {
|
||||
const holidays = getHolidaysForYear('US', TEST_YEAR);
|
||||
expect(holidays).toBeDefined();
|
||||
expect(Array.isArray(holidays)).toBe(true);
|
||||
expect(holidays.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should filter only public holidays', () => {
|
||||
const holidays = getHolidaysForYear('US', TEST_YEAR);
|
||||
holidays.forEach(holiday => {
|
||||
expect(holiday).toHaveProperty('date');
|
||||
expect(holiday).toHaveProperty('name');
|
||||
expect(holiday.date).toBeInstanceOf(Date);
|
||||
expect(typeof holiday.name).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle state codes', () => {
|
||||
const holidays = getHolidaysForYear('US', TEST_YEAR, 'CA');
|
||||
expect(holidays).toBeDefined();
|
||||
expect(Array.isArray(holidays)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return holidays sorted by date', () => {
|
||||
const holidays = getHolidaysForYear('US', TEST_YEAR);
|
||||
for (let i = 1; i < holidays.length; i++) {
|
||||
const prev = holidays[i - 1].date.getTime();
|
||||
const curr = holidays[i].date.getTime();
|
||||
expect(curr).toBeGreaterThanOrEqual(prev);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle different countries', () => {
|
||||
const usHolidays = getHolidaysForYear('US', TEST_YEAR);
|
||||
const gbHolidays = getHolidaysForYear('GB', TEST_YEAR);
|
||||
|
||||
expect(usHolidays.length).toBeGreaterThan(0);
|
||||
expect(gbHolidays.length).toBeGreaterThan(0);
|
||||
expect(usHolidays.length).not.toBe(gbHolidays.length);
|
||||
});
|
||||
|
||||
it('should expand multi-day holidays correctly', () => {
|
||||
const holidays = getHolidaysForYear('US', TEST_YEAR);
|
||||
const dateKeys = new Set(holidays.map(h =>
|
||||
`${h.date.getFullYear()}-${h.date.getMonth()}-${h.date.getDate()}`
|
||||
));
|
||||
expect(holidays.length).toBeGreaterThanOrEqual(dateKeys.size);
|
||||
});
|
||||
|
||||
it('should sort holidays by date first, then by name', () => {
|
||||
const holidays = getHolidaysForYear('US', TEST_YEAR);
|
||||
for (let i = 1; i < holidays.length; i++) {
|
||||
const prev = holidays[i - 1];
|
||||
const curr = holidays[i];
|
||||
const prevTime = prev.date.getTime();
|
||||
const currTime = curr.date.getTime();
|
||||
|
||||
if (prevTime === currTime) {
|
||||
expect(curr.name.localeCompare(prev.name)).toBeGreaterThanOrEqual(0);
|
||||
} else {
|
||||
expect(currTime).toBeGreaterThan(prevTime);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('optimizeDaysOff', () => {
|
||||
const mockHolidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'New Year' },
|
||||
{ date: new Date(TEST_YEAR, 6, 4), name: 'Independence Day' },
|
||||
];
|
||||
|
||||
describe('basic functionality', () => {
|
||||
it('should return an array of dates', () => {
|
||||
const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 5);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
result.forEach(date => {
|
||||
expect(date).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return at most the requested number of days', () => {
|
||||
const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 5);
|
||||
expect(result.length).toBeLessThanOrEqual(5);
|
||||
});
|
||||
|
||||
it('should handle zero days off', () => {
|
||||
const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 0);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle more days off than available gaps', () => {
|
||||
const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 1000);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
expect(result.length).toBeLessThan(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exclusion rules', () => {
|
||||
it('should not include weekends in optimized days', () => {
|
||||
const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 10);
|
||||
result.forEach(date => {
|
||||
expect(DEFAULT_WEEKENDS).not.toContain(date.getDay());
|
||||
});
|
||||
});
|
||||
|
||||
it('should not include holidays in optimized days', () => {
|
||||
const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 10);
|
||||
const holidayKeys = new Set(mockHolidays.map(h =>
|
||||
`${h.date.getFullYear()}-${h.date.getMonth()}-${h.date.getDate()}`
|
||||
));
|
||||
result.forEach(date => {
|
||||
const dateKey = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
||||
expect(holidayKeys.has(dateKey)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not select days that are already holidays or weekends', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' },
|
||||
{ date: new Date(TEST_YEAR, 0, 3), name: 'Holiday' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 10);
|
||||
const holidaySet = new Set(holidays.map(h =>
|
||||
`${h.date.getFullYear()}-${h.date.getMonth()}-${h.date.getDate()}`
|
||||
));
|
||||
result.forEach(date => {
|
||||
const dateKey = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
||||
expect(holidaySet.has(dateKey)).toBe(false);
|
||||
expect(DEFAULT_WEEKENDS).not.toContain(date.getDay());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parameters', () => {
|
||||
it('should respect startDate parameter', () => {
|
||||
const startDate = new Date(TEST_YEAR, 5, 1);
|
||||
const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate);
|
||||
result.forEach(date => {
|
||||
expect(date.getTime()).toBeGreaterThanOrEqual(startDate.getTime());
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle custom weekend days', () => {
|
||||
const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 5, CUSTOM_WEEKENDS);
|
||||
result.forEach(date => {
|
||||
expect(CUSTOM_WEEKENDS).not.toContain(date.getDay());
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter holidays by year and startDate', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(2023, 11, 31), name: 'Old Year' },
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'New Year' },
|
||||
{ date: new Date(TEST_YEAR, 5, 15), name: 'Mid Year' },
|
||||
];
|
||||
const startDate = new Date(TEST_YEAR, 2, 1);
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gap finding and prioritization', () => {
|
||||
it('should only find gaps of MAX_GAP_LENGTH (5) days or less', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'Mon' },
|
||||
{ date: new Date(TEST_YEAR, 0, 3), name: 'Wed' },
|
||||
{ date: new Date(TEST_YEAR, 0, 10), name: 'Wed' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 10);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
result.forEach(date => {
|
||||
expect(DEFAULT_WEEKENDS).not.toContain(date.getDay());
|
||||
});
|
||||
});
|
||||
|
||||
it('should find and fill gaps of 1-5 days', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' },
|
||||
{ date: new Date(TEST_YEAR, 0, 3), name: 'Wed Holiday' },
|
||||
{ date: new Date(TEST_YEAR, 0, 11), name: 'Thu Holiday' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 10);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
result.forEach(date => {
|
||||
expect(DEFAULT_WEEKENDS).not.toContain(date.getDay());
|
||||
});
|
||||
});
|
||||
|
||||
it('should prioritize gaps that create longer consecutive periods', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 4), name: 'Thursday Holiday' },
|
||||
{ date: new Date(TEST_YEAR, 0, 8), name: 'Monday Holiday' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 1);
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should prioritize smaller gaps when they create longer chains', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 4), name: 'Thu Holiday' },
|
||||
{ date: new Date(TEST_YEAR, 0, 9), name: 'Tue Holiday' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 1);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].getDate()).toBe(5);
|
||||
});
|
||||
|
||||
it('should handle multiple gaps and select most efficient ones first', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'Mon' },
|
||||
{ date: new Date(TEST_YEAR, 0, 4), name: 'Thu' },
|
||||
{ date: new Date(TEST_YEAR, 0, 8), name: 'Mon' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 3);
|
||||
expect(result.length).toBeLessThanOrEqual(3);
|
||||
result.forEach(date => {
|
||||
expect(DEFAULT_WEEKENDS).not.toContain(date.getDay());
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle backward vs forward chain calculation', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 5), name: 'Friday Holiday' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 1);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('should optimize to create longer consecutive periods', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 4), name: 'Holiday' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 1);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle partial gap filling when daysOff is less than gap length', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'Mon' },
|
||||
{ date: new Date(TEST_YEAR, 0, 8), name: 'Mon' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 2);
|
||||
expect(result.length).toBe(2);
|
||||
result.forEach(date => {
|
||||
expect(date.getDate()).toBeGreaterThanOrEqual(2);
|
||||
expect(date.getDate()).toBeLessThanOrEqual(6);
|
||||
expect(DEFAULT_WEEKENDS).not.toContain(date.getDay());
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple gaps when daysOff exceeds single gap capacity', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'Mon' },
|
||||
{ date: new Date(TEST_YEAR, 0, 3), name: 'Wed' },
|
||||
{ date: new Date(TEST_YEAR, 0, 5), name: 'Fri' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 3);
|
||||
expect(result.length).toBeGreaterThanOrEqual(2);
|
||||
expect(result.length).toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('should handle optimization with no available gaps', () => {
|
||||
const holidays = Array.from({ length: 365 }, (_, i) => {
|
||||
const date = new Date(TEST_YEAR, 0, 1);
|
||||
date.setDate(date.getDate() + i);
|
||||
if (date.getDay() !== 0 && date.getDay() !== 6) {
|
||||
return { date, name: `Holiday ${i}` };
|
||||
}
|
||||
return null;
|
||||
}).filter(Boolean) as Array<{ date: Date; name: string }>;
|
||||
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 5);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle gaps at the start of the year', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 5), name: 'Holiday' },
|
||||
];
|
||||
const startDate = new Date(TEST_YEAR, 0, 1);
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle gaps at the end of the year', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 11, 25), name: 'Christmas' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 5);
|
||||
result.forEach(date => {
|
||||
expect(date.getFullYear()).toBe(TEST_YEAR);
|
||||
expect(date.getMonth()).toBeLessThanOrEqual(11);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle gaps that span year boundaries correctly', () => {
|
||||
const startDate = new Date(TEST_YEAR, 11, 20);
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 11, 25), name: 'Christmas' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate);
|
||||
result.forEach(date => {
|
||||
expect(date.getFullYear()).toBe(TEST_YEAR);
|
||||
expect(date.getTime()).toBeGreaterThanOrEqual(startDate.getTime());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateConsecutiveDaysOff', () => {
|
||||
const mockHolidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'New Year' },
|
||||
{ date: new Date(TEST_YEAR, 0, 15), name: 'Holiday' },
|
||||
];
|
||||
|
||||
describe('basic functionality', () => {
|
||||
it('should return an array of periods', () => {
|
||||
const optimizedDays = [new Date(TEST_YEAR, 0, 2)];
|
||||
const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('should calculate periods with correct structure', () => {
|
||||
const optimizedDays = [new Date(TEST_YEAR, 0, 2)];
|
||||
const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR);
|
||||
result.forEach(period => {
|
||||
expect(period).toHaveProperty('startDate');
|
||||
expect(period).toHaveProperty('endDate');
|
||||
expect(period).toHaveProperty('totalDays');
|
||||
expect(period).toHaveProperty('usedDaysOff');
|
||||
expect(period.startDate).toBeInstanceOf(Date);
|
||||
expect(period.endDate).toBeInstanceOf(Date);
|
||||
expect(typeof period.totalDays).toBe('number');
|
||||
expect(typeof period.usedDaysOff).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
it('should include holidays in consecutive periods', () => {
|
||||
const optimizedDays = [new Date(TEST_YEAR, 0, 2)];
|
||||
const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR);
|
||||
const hasPeriodWithHoliday = result.some(period => {
|
||||
const holidayDate = mockHolidays[0].date;
|
||||
return period.startDate <= holidayDate && period.endDate >= holidayDate;
|
||||
});
|
||||
expect(hasPeriodWithHoliday).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculations', () => {
|
||||
it('should calculate totalDays correctly', () => {
|
||||
const optimizedDays = [new Date(TEST_YEAR, 0, 2)];
|
||||
const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR);
|
||||
result.forEach(period => {
|
||||
const calculatedDays = Math.round(
|
||||
(period.endDate.getTime() - period.startDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||
) + 1;
|
||||
expect(period.totalDays).toBe(calculatedDays);
|
||||
});
|
||||
});
|
||||
|
||||
it('should count usedDaysOff correctly', () => {
|
||||
const optimizedDays = [
|
||||
new Date(TEST_YEAR, 0, 2),
|
||||
new Date(TEST_YEAR, 0, 3),
|
||||
];
|
||||
const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR);
|
||||
result.forEach(period => {
|
||||
expect(period.usedDaysOff).toBeGreaterThanOrEqual(0);
|
||||
const daysInPeriod = optimizedDays.filter(day =>
|
||||
day >= period.startDate && day <= period.endDate
|
||||
).length;
|
||||
expect(period.usedDaysOff).toBeLessThanOrEqual(daysInPeriod);
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly count usedDaysOff in periods', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' },
|
||||
];
|
||||
const optimizedDays = [
|
||||
new Date(TEST_YEAR, 0, 2),
|
||||
new Date(TEST_YEAR, 0, 3),
|
||||
];
|
||||
const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR);
|
||||
const periodWithOptimized = result.find(period =>
|
||||
optimizedDays.some(day => day >= period.startDate && day <= period.endDate)
|
||||
);
|
||||
if (periodWithOptimized) {
|
||||
const daysInPeriod = optimizedDays.filter(day =>
|
||||
day >= periodWithOptimized.startDate && day <= periodWithOptimized.endDate
|
||||
).length;
|
||||
expect(periodWithOptimized.usedDaysOff).toBe(daysInPeriod);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation rules', () => {
|
||||
it('should not include periods that are only weekends', () => {
|
||||
const optimizedDays: Date[] = [];
|
||||
const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR);
|
||||
result.forEach(period => {
|
||||
let allWeekends = true;
|
||||
for (let d = new Date(period.startDate); d <= period.endDate; d.setDate(d.getDate() + 1)) {
|
||||
const dayOfWeek = d.getDay();
|
||||
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
|
||||
allWeekends = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(allWeekends).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should exclude single-day periods', () => {
|
||||
const holidays: Array<{ date: Date; name: string }> = [];
|
||||
const optimizedDays: Date[] = [];
|
||||
const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR);
|
||||
result.forEach(period => {
|
||||
expect(period.totalDays).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle groups that are only weekends correctly', () => {
|
||||
const holidays: Array<{ date: Date; name: string }> = [];
|
||||
const optimizedDays: Date[] = [];
|
||||
const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR);
|
||||
result.forEach(period => {
|
||||
let hasNonWeekend = false;
|
||||
for (let d = new Date(period.startDate); d <= period.endDate; d.setDate(d.getDate() + 1)) {
|
||||
const dayOfWeek = d.getDay();
|
||||
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
|
||||
hasNonWeekend = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(hasNonWeekend).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parameters', () => {
|
||||
it('should respect startDate parameter', () => {
|
||||
const startDate = new Date(TEST_YEAR, 5, 1);
|
||||
const optimizedDays = [new Date(TEST_YEAR, 5, 2)];
|
||||
const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR, DEFAULT_WEEKENDS, startDate);
|
||||
result.forEach(period => {
|
||||
expect(period.startDate.getTime()).toBeGreaterThanOrEqual(startDate.getTime());
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle custom weekend days', () => {
|
||||
const optimizedDays = [new Date(TEST_YEAR, 0, 2)];
|
||||
const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR, CUSTOM_WEEKENDS);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle empty optimized days', () => {
|
||||
const result = calculateConsecutiveDaysOff(mockHolidays, [], TEST_YEAR);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle periods spanning multiple months', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 31), name: 'End of Jan' },
|
||||
];
|
||||
const optimizedDays = [new Date(TEST_YEAR, 1, 1)];
|
||||
const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle periods that start exactly at startDate', () => {
|
||||
const startDate = new Date(TEST_YEAR, 5, 1);
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 5, 1), name: 'Start Holiday' },
|
||||
];
|
||||
const optimizedDays = [new Date(TEST_YEAR, 5, 3)];
|
||||
const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR, DEFAULT_WEEKENDS, startDate);
|
||||
if (result.length > 0) {
|
||||
expect(result[0].startDate.getTime()).toBeGreaterThanOrEqual(startDate.getTime());
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle periods that end exactly at year end (Dec 31)', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 11, 30), name: 'Dec 30' },
|
||||
];
|
||||
const optimizedDays = [new Date(TEST_YEAR, 11, 31)];
|
||||
const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR);
|
||||
const periodAtYearEnd = result.find(period =>
|
||||
period.endDate.getMonth() === 11 && period.endDate.getDate() === 31
|
||||
);
|
||||
if (periodAtYearEnd) {
|
||||
expect(periodAtYearEnd.endDate.getFullYear()).toBe(TEST_YEAR);
|
||||
}
|
||||
});
|
||||
|
||||
it('should correctly handle overlapping optimized days and holidays', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' },
|
||||
];
|
||||
const optimizedDays = [
|
||||
new Date(TEST_YEAR, 0, 2),
|
||||
new Date(TEST_YEAR, 0, 3),
|
||||
];
|
||||
const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR);
|
||||
const period = result.find(p =>
|
||||
p.startDate <= holidays[0].date && p.endDate >= optimizedDays[1]
|
||||
);
|
||||
if (period) {
|
||||
expect(period.usedDaysOff).toBe(2);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle consecutive periods separated by work days', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'Mon' },
|
||||
{ date: new Date(TEST_YEAR, 0, 4), name: 'Thu' },
|
||||
];
|
||||
const optimizedDays = [
|
||||
new Date(TEST_YEAR, 0, 2),
|
||||
new Date(TEST_YEAR, 0, 5),
|
||||
];
|
||||
const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR);
|
||||
expect(result.length).toBeGreaterThanOrEqual(1);
|
||||
result.forEach(period => {
|
||||
expect(period.totalDays).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration tests', () => {
|
||||
it('should work together: get holidays, optimize, and calculate periods', () => {
|
||||
const holidays = getHolidaysForYear('US', TEST_YEAR);
|
||||
const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 10);
|
||||
const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR);
|
||||
|
||||
expect(holidays.length).toBeGreaterThan(0);
|
||||
expect(optimizedDays.length).toBeLessThanOrEqual(10);
|
||||
expect(Array.isArray(periods)).toBe(true);
|
||||
|
||||
periods.forEach(period => {
|
||||
expect(period.totalDays).toBeGreaterThanOrEqual(2);
|
||||
expect(period.usedDaysOff).toBeGreaterThanOrEqual(0);
|
||||
expect(period.startDate <= period.endDate).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should optimize efficiently to maximize consecutive days', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 4), name: 'Holiday' },
|
||||
];
|
||||
const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 1);
|
||||
const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR);
|
||||
|
||||
if (periods.length > 0) {
|
||||
const hasOptimizedDay = periods.some(period =>
|
||||
optimizedDays.some(day =>
|
||||
day >= period.startDate && day <= period.endDate
|
||||
)
|
||||
);
|
||||
expect(hasOptimizedDay).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle edge case: all days are holidays or weekends', () => {
|
||||
const holidays = Array.from({ length: 50 }, (_, i) => ({
|
||||
date: new Date(TEST_YEAR, 0, i + 1),
|
||||
name: `Holiday ${i + 1}`
|
||||
}));
|
||||
const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 5);
|
||||
const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR);
|
||||
|
||||
expect(Array.isArray(optimizedDays)).toBe(true);
|
||||
expect(Array.isArray(periods)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases and error handling', () => {
|
||||
it('should handle year with no holidays gracefully', () => {
|
||||
const result = optimizeDaysOff([], TEST_YEAR, 5);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle invalid country codes gracefully', () => {
|
||||
try {
|
||||
const holidays = getHolidaysForYear('XX', TEST_YEAR);
|
||||
expect(Array.isArray(holidays)).toBe(true);
|
||||
} catch (e) {
|
||||
expect(e).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle dates at year boundaries', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 11, 31), name: 'New Year Eve' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 5);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle startDate at end of year', () => {
|
||||
const startDate = new Date(TEST_YEAR, 11, 15);
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 11, 25), name: 'Christmas' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate);
|
||||
result.forEach(date => {
|
||||
expect(date.getTime()).toBeGreaterThanOrEqual(startDate.getTime());
|
||||
expect(date.getFullYear()).toBe(TEST_YEAR);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle holidays from previous year correctly', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(2023, 11, 31), name: 'Old Year' },
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'New Year' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 5);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle leap year correctly', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 1, 29), name: 'Leap Day' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 5);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Private function behavior (tested indirectly)', () => {
|
||||
it('should correctly identify weekend days', () => {
|
||||
const holidays: Array<{ date: Date; name: string }> = [];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 10);
|
||||
result.forEach(date => {
|
||||
expect(DEFAULT_WEEKENDS).not.toContain(date.getDay());
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly calculate days between dates', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'Start' },
|
||||
];
|
||||
const optimizedDays = [new Date(TEST_YEAR, 0, 5)];
|
||||
const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR);
|
||||
result.forEach(period => {
|
||||
const calculated = Math.round(
|
||||
(period.endDate.getTime() - period.startDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||
) + 1;
|
||||
expect(period.totalDays).toBe(calculated);
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate consistent date keys', () => {
|
||||
const date1 = new Date(TEST_YEAR, 0, 15);
|
||||
const date2 = new Date(TEST_YEAR, 0, 15);
|
||||
const holidays = [
|
||||
{ date: date1, name: 'Holiday' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 5);
|
||||
const hasDate2 = result.some(d =>
|
||||
d.getFullYear() === date2.getFullYear() &&
|
||||
d.getMonth() === date2.getMonth() &&
|
||||
d.getDate() === date2.getDate()
|
||||
);
|
||||
expect(hasDate2).toBe(false);
|
||||
});
|
||||
|
||||
it('should correctly identify holidays using dateKey', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 15, 10, 30), name: 'Holiday' },
|
||||
];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 5);
|
||||
const hasHolidayDate = result.some(d =>
|
||||
d.getFullYear() === TEST_YEAR &&
|
||||
d.getMonth() === 0 &&
|
||||
d.getDate() === 15
|
||||
);
|
||||
expect(hasHolidayDate).toBe(false);
|
||||
});
|
||||
|
||||
it('should correctly get weekends for the year with startDate', () => {
|
||||
const startDate = new Date(TEST_YEAR, 5, 1);
|
||||
const holidays: Array<{ date: Date; name: string }> = [];
|
||||
const result = optimizeDaysOff(holidays, TEST_YEAR, 10, DEFAULT_WEEKENDS, startDate);
|
||||
result.forEach(date => {
|
||||
expect(date.getTime()).toBeGreaterThanOrEqual(startDate.getTime());
|
||||
expect(DEFAULT_WEEKENDS).not.toContain(date.getDay());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex scenarios and real-world cases', () => {
|
||||
it('should handle a typical year with multiple holidays and weekends', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'New Year' },
|
||||
{ date: new Date(TEST_YEAR, 4, 27), name: 'Memorial Day' },
|
||||
{ date: new Date(TEST_YEAR, 6, 4), name: 'Independence Day' },
|
||||
{ date: new Date(TEST_YEAR, 8, 2), name: 'Labor Day' },
|
||||
{ date: new Date(TEST_YEAR, 10, 28), name: 'Thanksgiving' },
|
||||
{ date: new Date(TEST_YEAR, 11, 25), name: 'Christmas' },
|
||||
];
|
||||
const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 10);
|
||||
const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR);
|
||||
|
||||
expect(optimizedDays.length).toBeLessThanOrEqual(10);
|
||||
expect(periods.length).toBeGreaterThan(0);
|
||||
|
||||
periods.forEach(period => {
|
||||
expect(period.totalDays).toBeGreaterThanOrEqual(2);
|
||||
expect(period.startDate <= period.endDate).toBe(true);
|
||||
expect(period.usedDaysOff).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should maximize consecutive days off efficiently', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 4), name: 'Thu' },
|
||||
{ date: new Date(TEST_YEAR, 0, 8), name: 'Mon' },
|
||||
];
|
||||
const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 1);
|
||||
const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR);
|
||||
|
||||
const periodWithOptimized = periods.find(p =>
|
||||
optimizedDays.some(day => day >= p.startDate && day <= p.endDate)
|
||||
);
|
||||
expect(periodWithOptimized).toBeDefined();
|
||||
if (periodWithOptimized) {
|
||||
expect(periodWithOptimized.totalDays).toBeGreaterThanOrEqual(4);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle non-standard weekend configurations', () => {
|
||||
const holidays = [
|
||||
{ date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' },
|
||||
];
|
||||
const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 5, CUSTOM_WEEKENDS);
|
||||
const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR, CUSTOM_WEEKENDS);
|
||||
|
||||
optimizedDays.forEach(date => {
|
||||
expect(CUSTOM_WEEKENDS).not.toContain(date.getDay());
|
||||
});
|
||||
|
||||
expect(Array.isArray(periods)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -31,49 +31,56 @@ export function getHolidaysForYear(countryCode: string, year: number, stateCode?
|
||||
}
|
||||
|
||||
// Find optimal placement of PTO days to maximize consecutive time off
|
||||
export function optimizeDaysOff(holidays: { date: Date }[], year: number, daysOff: number, weekendDays: number[] = [0, 6]): Date[] {
|
||||
export function optimizeDaysOff(holidays: { date: Date }[], year: number, daysOff: number, weekendDays: number[] = [0, 6], startDate?: Date): Date[] {
|
||||
const effectiveStartDate = startDate || new Date(year, 0, 1);
|
||||
const filteredHolidays = holidays.filter(h => h.date.getFullYear() === year && h.date >= effectiveStartDate);
|
||||
const allDaysOff = new Set([
|
||||
...holidays.filter(h => h.date.getFullYear() === year).map(h => dateKey(h.date)),
|
||||
...getWeekends(year, weekendDays).map(d => dateKey(d))
|
||||
...filteredHolidays.map(h => dateKey(h.date)),
|
||||
...getWeekends(year, weekendDays, effectiveStartDate).map(d => dateKey(d))
|
||||
]);
|
||||
|
||||
const gaps = findGaps(allDaysOff, year, weekendDays);
|
||||
const gaps = findGaps(allDaysOff, year, weekendDays, effectiveStartDate);
|
||||
return selectDaysOff(rankGapsByEfficiency(gaps, allDaysOff, weekendDays), daysOff, allDaysOff, weekendDays);
|
||||
}
|
||||
|
||||
// Calculate periods of consecutive days off (weekends + holidays + PTO)
|
||||
export function calculateConsecutiveDaysOff(holidays: { date: Date }[], optimizedDaysOff: Date[], year: number, weekendDays: number[] = [0, 6]) {
|
||||
export function calculateConsecutiveDaysOff(holidays: { date: Date }[], optimizedDaysOff: Date[], year: number, weekendDays: number[] = [0, 6], startDate?: Date) {
|
||||
const effectiveStartDate = startDate || new Date(year, 0, 1);
|
||||
const filteredHolidays = holidays.filter(h => h.date >= effectiveStartDate);
|
||||
const filteredOptimizedDaysOff = optimizedDaysOff.filter(d => d >= effectiveStartDate);
|
||||
|
||||
const allDaysOff = new Set([
|
||||
...holidays.map(h => dateKey(h.date)),
|
||||
...optimizedDaysOff.map(d => dateKey(d)),
|
||||
...getWeekends(year, weekendDays).map(d => dateKey(d))
|
||||
...filteredHolidays.map(h => dateKey(h.date)),
|
||||
...filteredOptimizedDaysOff.map(d => dateKey(d)),
|
||||
...getWeekends(year, weekendDays, effectiveStartDate).map(d => dateKey(d))
|
||||
]);
|
||||
|
||||
const consecutiveDaysOff = [];
|
||||
let currentGroup = [];
|
||||
|
||||
for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
|
||||
if (isWeekend(d, weekendDays) || isHoliday(d, holidays) || allDaysOff.has(dateKey(d))) {
|
||||
for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
|
||||
if (isWeekend(d, weekendDays) || isHoliday(d, filteredHolidays) || allDaysOff.has(dateKey(d))) {
|
||||
currentGroup.push(new Date(d));
|
||||
} else if (currentGroup.length > 0) {
|
||||
if (isValidConsecutiveGroup(currentGroup, weekendDays)) {
|
||||
consecutiveDaysOff.push(createPeriod(currentGroup, optimizedDaysOff));
|
||||
consecutiveDaysOff.push(createPeriod(currentGroup, filteredOptimizedDaysOff));
|
||||
}
|
||||
currentGroup = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (currentGroup.length > 0 && isValidConsecutiveGroup(currentGroup, weekendDays)) {
|
||||
consecutiveDaysOff.push(createPeriod(currentGroup, optimizedDaysOff));
|
||||
consecutiveDaysOff.push(createPeriod(currentGroup, filteredOptimizedDaysOff));
|
||||
}
|
||||
|
||||
return consecutiveDaysOff;
|
||||
}
|
||||
|
||||
// Get all weekend days for a year
|
||||
function getWeekends(year: number, weekendDays: number[]): Date[] {
|
||||
function getWeekends(year: number, weekendDays: number[], startDate?: Date): Date[] {
|
||||
const effectiveStartDate = startDate || new Date(year, 0, 1);
|
||||
const weekends = [];
|
||||
for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
|
||||
for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
|
||||
if (d.getMonth() === d.getMonth() && isWeekend(d, weekendDays)) {
|
||||
weekends.push(new Date(d));
|
||||
}
|
||||
@@ -82,11 +89,12 @@ function getWeekends(year: number, weekendDays: number[]): Date[] {
|
||||
}
|
||||
|
||||
// Find gaps between days off that could be filled with PTO
|
||||
function findGaps(allDaysOff: Set<string>, year: number, weekendDays: number[]) {
|
||||
function findGaps(allDaysOff: Set<string>, year: number, weekendDays: number[], startDate?: Date) {
|
||||
const effectiveStartDate = startDate || new Date(year, 0, 1);
|
||||
const gaps = [];
|
||||
let gapStart = null;
|
||||
|
||||
for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
|
||||
for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
|
||||
if (!allDaysOff.has(dateKey(d)) && !isWeekend(d, weekendDays)) {
|
||||
if (!gapStart) gapStart = new Date(d);
|
||||
} else if (gapStart) {
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
let daysOff: number = 0;
|
||||
let optimizedDaysOff: Date[] = [];
|
||||
let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }> = [];
|
||||
let showExcludedMonths: boolean = true;
|
||||
let visibleMonths: number[] = [];
|
||||
let countriesInput: HTMLInputElement | null = null;
|
||||
let statesInput: HTMLInputElement | null = null;
|
||||
let showHowItWorks: boolean = false;
|
||||
@@ -35,12 +37,27 @@
|
||||
let showWeekendSettings: boolean = false;
|
||||
let weekendDays: number[] = [];
|
||||
|
||||
// Start date state
|
||||
let startDate: Date = new Date(new Date().getFullYear(), 0, 1);
|
||||
let showDatePicker: boolean = false;
|
||||
let datePickerValue: string = '';
|
||||
|
||||
$: selectedCountryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || '';
|
||||
|
||||
$: if (selectedCountryCode || selectedStateCode || daysOff || year) {
|
||||
$: if (selectedCountryCode || selectedStateCode || daysOff || year || startDate) {
|
||||
updateHolidays();
|
||||
}
|
||||
|
||||
// Reactive: when year changes, load start date for that year
|
||||
$: if (year !== undefined && year) {
|
||||
startDate = getStartDate(year);
|
||||
}
|
||||
|
||||
// Reactive: when startDate or year changes, update excluded months visibility
|
||||
$: if (year !== undefined && year && startDate) {
|
||||
showExcludedMonths = !hasExcludedMonths();
|
||||
}
|
||||
|
||||
$: if (daysOff) {
|
||||
localStorage.setItem('daysOff', daysOff.toString());
|
||||
}
|
||||
@@ -59,8 +76,11 @@
|
||||
const stateName = target.value;
|
||||
selectedStateCode = Object.keys(statesList).find(code => statesList[code] === stateName) || '';
|
||||
selectedState = stateName;
|
||||
localStorage.setItem('selectedState', selectedState);
|
||||
localStorage.setItem('selectedStateCode', selectedStateCode);
|
||||
// Save state per country
|
||||
if (selectedCountryCode) {
|
||||
localStorage.setItem(`selectedState_${selectedCountryCode}`, selectedState);
|
||||
localStorage.setItem(`selectedStateCode_${selectedCountryCode}`, selectedStateCode);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -73,14 +93,22 @@
|
||||
const storedYear = localStorage.getItem('year');
|
||||
const storedCountry = localStorage.getItem('selectedCountry');
|
||||
const storedDaysOff = localStorage.getItem('daysOff');
|
||||
const storedState = localStorage.getItem('selectedState');
|
||||
const storedStateCode = localStorage.getItem('selectedStateCode');
|
||||
|
||||
year = storedYear ? parseInt(storedYear, 10) : defaultYear;
|
||||
selectedCountry = storedCountry || defaultCountry;
|
||||
daysOff = storedDaysOff ? parseInt(storedDaysOff, 10) : defaultDaysOff;
|
||||
selectedState = storedState || '';
|
||||
selectedStateCode = storedStateCode || '';
|
||||
|
||||
// Load state per country
|
||||
const countryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || '';
|
||||
if (countryCode) {
|
||||
selectedState = localStorage.getItem(`selectedState_${countryCode}`) || '';
|
||||
selectedStateCode = localStorage.getItem(`selectedStateCode_${countryCode}`) || '';
|
||||
} else {
|
||||
selectedState = '';
|
||||
selectedStateCode = '';
|
||||
}
|
||||
startDate = getStartDate(year);
|
||||
// showExcludedMonths will be set by reactive statement
|
||||
updateHolidays();
|
||||
});
|
||||
|
||||
@@ -113,14 +141,17 @@
|
||||
function handleCountryChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const fullValue = target.value;
|
||||
if (selectedCountryCode) {
|
||||
daysOff = ptoData[selectedCountryCode] || 0;
|
||||
selectedState = ''; // Reset state
|
||||
selectedStateCode = ''; // Reset state code
|
||||
updateStatesList(selectedCountryCode); // Update states list for the new country
|
||||
selectedCountry = fullValue;
|
||||
// Get the country code for the new country (selectedCountryCode will update reactively)
|
||||
const newCountryCode = Object.keys(countriesList).find(code => countriesList[code] === fullValue) || '';
|
||||
if (newCountryCode) {
|
||||
// Update days off to the new country's default
|
||||
daysOff = ptoData[newCountryCode] || 0;
|
||||
// Load state for the new country
|
||||
selectedState = localStorage.getItem(`selectedState_${newCountryCode}`) || '';
|
||||
selectedStateCode = localStorage.getItem(`selectedStateCode_${newCountryCode}`) || '';
|
||||
// updateStatesList and updateHolidays will be called by reactive statements
|
||||
localStorage.setItem('selectedCountry', selectedCountry);
|
||||
localStorage.setItem('selectedState', selectedState);
|
||||
localStorage.setItem('selectedStateCode', selectedStateCode);
|
||||
localStorage.setItem('daysOff', daysOff.toString());
|
||||
}
|
||||
}
|
||||
@@ -135,8 +166,8 @@
|
||||
hidden: isHolidayHidden(holiday)
|
||||
}));
|
||||
const visibleHolidays = holidays.filter(h => !h.hidden);
|
||||
optimizedDaysOff = optimizeDaysOff(visibleHolidays, year, daysOff, weekendDays);
|
||||
consecutiveDaysOff = calculateConsecutiveDaysOff(visibleHolidays, optimizedDaysOff, year, weekendDays);
|
||||
optimizedDaysOff = optimizeDaysOff(visibleHolidays, year, daysOff, weekendDays, startDate);
|
||||
consecutiveDaysOff = calculateConsecutiveDaysOff(visibleHolidays, optimizedDaysOff, year, weekendDays, startDate);
|
||||
} else {
|
||||
holidays = [];
|
||||
optimizedDaysOff = [];
|
||||
@@ -147,14 +178,18 @@
|
||||
function resetToDefault() {
|
||||
year = defaultYear;
|
||||
selectedCountry = defaultCountry;
|
||||
selectedState = '';
|
||||
selectedStateCode = '';
|
||||
daysOff = defaultDaysOff;
|
||||
const defaultCountryCode = Object.keys(countriesList).find(code => countriesList[code] === defaultCountry) || '';
|
||||
// Load state for default country
|
||||
if (defaultCountryCode) {
|
||||
selectedState = localStorage.getItem(`selectedState_${defaultCountryCode}`) || '';
|
||||
selectedStateCode = localStorage.getItem(`selectedStateCode_${defaultCountryCode}`) || '';
|
||||
} else {
|
||||
selectedState = '';
|
||||
selectedStateCode = '';
|
||||
}
|
||||
// Keep current daysOff value, don't reset it
|
||||
localStorage.setItem('year', year.toString());
|
||||
localStorage.setItem('selectedCountry', selectedCountry);
|
||||
localStorage.setItem('selectedState', selectedState);
|
||||
localStorage.setItem('selectedStateCode', selectedStateCode);
|
||||
localStorage.setItem('daysOff', daysOff.toString());
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
@@ -252,6 +287,156 @@
|
||||
|
||||
$: visibleHolidaysCount = holidays.filter(h => !h.hidden).length;
|
||||
|
||||
// Get start date for a given year from localStorage
|
||||
function getStartDate(year: number): Date {
|
||||
try {
|
||||
const stored = localStorage.getItem('startDates');
|
||||
if (stored) {
|
||||
const startDates: string[] = JSON.parse(stored);
|
||||
// Find date that matches the year (extract year from date string)
|
||||
const dateStr = startDates.find(date => {
|
||||
const dateYear = parseInt(date.split('-')[0] || date.split('T')[0].split('-')[0]);
|
||||
return dateYear === year;
|
||||
});
|
||||
if (dateStr) {
|
||||
// Parse date string - handle both YYYY-MM-DD and ISO format
|
||||
const parsed = dateStr.includes('T') ? dateStr.split('T')[0] : dateStr;
|
||||
const [y, m, d] = parsed.split('-').map(Number);
|
||||
return new Date(y, m - 1, d);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading start date:', e);
|
||||
}
|
||||
return new Date(year, 0, 1); // Default to Jan 1st
|
||||
}
|
||||
|
||||
// Save start date for a given year to localStorage
|
||||
function saveStartDate(year: number, date: Date) {
|
||||
try {
|
||||
const stored = localStorage.getItem('startDates');
|
||||
let startDates: string[] = stored ? JSON.parse(stored) : [];
|
||||
// Remove existing date for this year (extract year from date string)
|
||||
startDates = startDates.filter(dateStr => {
|
||||
const dateYear = parseInt(dateStr.split('-')[0] || dateStr.split('T')[0].split('-')[0]);
|
||||
return dateYear !== year;
|
||||
});
|
||||
// Add new date
|
||||
startDates.push(formatDateForInput(date));
|
||||
localStorage.setItem('startDates', JSON.stringify(startDates));
|
||||
} catch (e) {
|
||||
console.error('Error saving start date:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Format date as YYYY-MM-DD for date input (no timezone conversion)
|
||||
function formatDateForInput(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
// Format start date for display
|
||||
function formatStartDate(date: Date): string {
|
||||
const today = new Date();
|
||||
if (date.getTime() === new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime() && date.getFullYear() === year) {
|
||||
return 'Today';
|
||||
}
|
||||
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
const day = date.getDate();
|
||||
const suffix = getDaySuffix(day);
|
||||
return `${monthNames[date.getMonth()]} ${day}${suffix}`;
|
||||
}
|
||||
|
||||
// Get day suffix (st, nd, rd, th)
|
||||
function getDaySuffix(day: number): string {
|
||||
if (day > 3 && day < 21) return 'th';
|
||||
switch (day % 10) {
|
||||
case 1: return 'st';
|
||||
case 2: return 'nd';
|
||||
case 3: return 'rd';
|
||||
default: return 'th';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle start date change
|
||||
function handleStartDateChange(newDate: Date) {
|
||||
startDate = newDate;
|
||||
saveStartDate(year, newDate);
|
||||
updateHolidays();
|
||||
// showExcludedMonths will be updated by reactive statement
|
||||
}
|
||||
|
||||
// Handle date picker input change (auto-save)
|
||||
function handleDatePickerChange() {
|
||||
if (datePickerValue) {
|
||||
// Parse YYYY-MM-DD format in local time (consistent with getStartDate)
|
||||
const [y, m, d] = datePickerValue.split('-').map(Number);
|
||||
const newDate = new Date(y, m - 1, d);
|
||||
handleStartDateChange(newDate);
|
||||
}
|
||||
}
|
||||
|
||||
// Set start date to today
|
||||
function setStartDateToToday() {
|
||||
const today = new Date();
|
||||
if (today.getFullYear() === year) {
|
||||
handleStartDateChange(new Date(today.getFullYear(), today.getMonth(), today.getDate()));
|
||||
showDatePicker = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset start date to Jan 1st
|
||||
function resetStartDateToJan1() {
|
||||
const jan1st = new Date(year, 0, 1);
|
||||
handleStartDateChange(jan1st);
|
||||
showDatePicker = false;
|
||||
}
|
||||
|
||||
// Check if today is in the current year
|
||||
function isTodayInYear(): boolean {
|
||||
const today = new Date();
|
||||
return today.getFullYear() === year;
|
||||
}
|
||||
|
||||
// Helper: Get start date normalized to the current year
|
||||
function getStartDateInYear(): Date {
|
||||
return new Date(year, startDate.getMonth(), startDate.getDate());
|
||||
}
|
||||
|
||||
// Check if a month is active (not entirely before the start date)
|
||||
function isMonthActive(monthIndex: number): boolean {
|
||||
const startDateInYear = getStartDateInYear();
|
||||
const startMonth = startDateInYear.getMonth();
|
||||
|
||||
// Month is active if:
|
||||
// 1. The start date falls within this month (same month), OR
|
||||
// 2. The month starts on or after the start date (later month)
|
||||
// This means only months entirely before the start date's month are excluded
|
||||
return monthIndex >= startMonth;
|
||||
}
|
||||
|
||||
// Check if start date is Jan 1st
|
||||
function isStartDateJan1st(): boolean {
|
||||
const startDateInYear = getStartDateInYear();
|
||||
return startDateInYear.getTime() === new Date(year, 0, 1).getTime();
|
||||
}
|
||||
|
||||
// Check if there are any excluded months (months entirely before the start date)
|
||||
function hasExcludedMonths(): boolean {
|
||||
return months.some(month => !isMonthActive(month));
|
||||
}
|
||||
|
||||
// Filter months based on showExcludedMonths setting
|
||||
// Explicitly depend on startDate, year, and showExcludedMonths to ensure proper reactivity
|
||||
// Use a computed value that depends on all relevant variables
|
||||
$: visibleMonths = (startDate && year !== undefined && year)
|
||||
? (showExcludedMonths
|
||||
? months
|
||||
: months.filter(month => isMonthActive(month)))
|
||||
: [];
|
||||
|
||||
function toggleWeekendDay(dayNumber: number) {
|
||||
if (weekendDays.includes(dayNumber)) {
|
||||
weekendDays = weekendDays.filter(d => d !== dayNumber);
|
||||
@@ -318,7 +503,7 @@
|
||||
|
||||
.content-box p {
|
||||
text-align: center;
|
||||
line-height: 2;
|
||||
line-height: 3;
|
||||
}
|
||||
|
||||
input {
|
||||
@@ -365,6 +550,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-excluded-months-container {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.toggle-excluded-months {
|
||||
padding: 8px 16px;
|
||||
background-color: #333;
|
||||
border: 1px solid #555;
|
||||
border-radius: 5px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: background-color 0.3s;
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.toggle-excluded-months:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
.calendar-container {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
@@ -557,7 +765,6 @@
|
||||
}
|
||||
|
||||
.content-box button {
|
||||
margin-left: 10px;
|
||||
background-color: #444;
|
||||
border: none;
|
||||
color: #fff;
|
||||
@@ -641,6 +848,161 @@
|
||||
margin-bottom: 15px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.start-date-link {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.start-date-link:hover {
|
||||
text-decoration-style: solid;
|
||||
}
|
||||
|
||||
.date-picker-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.date-picker-modal {
|
||||
background-color: #222;
|
||||
border-radius: 10px;
|
||||
padding: 25px;
|
||||
max-width: min(400px, calc(100vw - 40px));
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.date-picker-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.date-picker-close:hover,
|
||||
.date-picker-close:active,
|
||||
.date-picker-close:focus {
|
||||
background: transparent;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.date-picker-modal h3 {
|
||||
margin: 0 0 10px 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.date-picker-modal p {
|
||||
margin: 0 0 20px 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.date-picker-controls {
|
||||
margin: 0 0 20px 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 1em;
|
||||
background-color: #222;
|
||||
border: 1px solid #555;
|
||||
border-radius: 5px;
|
||||
color: #fff;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.date-input:focus {
|
||||
outline: 2px solid #61dafb;
|
||||
border-color: #61dafb;
|
||||
}
|
||||
|
||||
/* Make calendar icon white */
|
||||
.date-input::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.date-input::-webkit-inner-spin-button,
|
||||
.date-input::-webkit-clear-button {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.date-picker-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
margin: 0 0 15px 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.date-picker-button,
|
||||
.date-picker-save {
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.date-picker-button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 20px;
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.date-picker-button:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.date-picker-save {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
background-color: #61dafb;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.date-picker-save:hover {
|
||||
background-color: #4fa8c5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<main>
|
||||
@@ -689,15 +1051,27 @@
|
||||
<span class="bold">{daysOff}</span>
|
||||
<button on:click={() => { daysOff++; updateHolidays(); }} aria-label="Increase days off">▲</button>
|
||||
</span>
|
||||
days off in
|
||||
days off from
|
||||
<a href="#" on:click|preventDefault={() => { showDatePicker = true; datePickerValue = formatDateForInput(startDate); }} class="bold start-date-link">
|
||||
{@html formatStartDate(startDate)}
|
||||
</a>
|
||||
until the end of
|
||||
<span class="arrow-controls">
|
||||
<button on:click={() => { year--; updateHolidays(); }} aria-label="Previous year">◀</button>
|
||||
<span class="bold">{year}</span>
|
||||
<button on:click={() => { year++; updateHolidays(); }} aria-label="Next year">▶</button>
|
||||
</span>
|
||||
</p>
|
||||
{#if year !== defaultYear || selectedCountry !== defaultCountry || daysOff !== defaultDaysOff}
|
||||
<a href="#" on:click|preventDefault={resetToDefault} class="reset-link">Reset to my country</a>
|
||||
{#if year !== defaultYear || selectedCountry !== defaultCountry}
|
||||
{@const yearDifferent = year !== defaultYear}
|
||||
{@const countryDifferent = selectedCountry !== defaultCountry}
|
||||
<a href="#" on:click|preventDefault={resetToDefault} class="reset-link">
|
||||
{yearDifferent && countryDifferent
|
||||
? 'Reset to current country and year'
|
||||
: yearDifferent
|
||||
? 'Reset to current year'
|
||||
: 'Reset to current country'}
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<datalist id="countries">
|
||||
@@ -781,13 +1155,27 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if hasExcludedMonths()}
|
||||
<div class="toggle-excluded-months-container">
|
||||
<button
|
||||
class="toggle-excluded-months"
|
||||
on:click={() => showExcludedMonths = !showExcludedMonths}
|
||||
aria-label={showExcludedMonths ? 'Hide excluded months' : 'Show excluded months'}
|
||||
>
|
||||
{showExcludedMonths ? 'Hide' : 'Show'} excluded months
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="calendar-grid">
|
||||
{#each months as month}
|
||||
{#each visibleMonths as month}
|
||||
<div class="calendar-container">
|
||||
<CalendarMonth
|
||||
year={year}
|
||||
month={month}
|
||||
holidays={holidays}
|
||||
startDate={startDate}
|
||||
isActive={isMonthActive(month)}
|
||||
optimizedDaysOff={optimizedDaysOff}
|
||||
consecutiveDaysOff={consecutiveDaysOff}
|
||||
selectedCountryCode={selectedCountryCode}
|
||||
@@ -798,6 +1186,41 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showDatePicker}
|
||||
<div class="date-picker-overlay" on:click|self={() => showDatePicker = false}>
|
||||
<div class="date-picker-modal" on:click|stopPropagation>
|
||||
<button class="date-picker-close" on:click={() => showDatePicker = false} aria-label="Close">×</button>
|
||||
<h3>Set Start Date</h3>
|
||||
<p>Choose when your time off period begins for {year}</p>
|
||||
<div class="date-picker-controls">
|
||||
<input
|
||||
type="date"
|
||||
bind:value={datePickerValue}
|
||||
on:change={handleDatePickerChange}
|
||||
class="date-input"
|
||||
min={formatDateForInput(new Date(year, 0, 1))}
|
||||
max={formatDateForInput(new Date(year, 11, 31))}
|
||||
/>
|
||||
</div>
|
||||
<div class="date-picker-buttons">
|
||||
{#if isTodayInYear()}
|
||||
<button on:click={setStartDateToToday} class="date-picker-button">
|
||||
Set to today
|
||||
</button>
|
||||
{/if}
|
||||
{#if !isStartDateJan1st()}
|
||||
<button on:click={resetStartDateToJan1} class="date-picker-button">
|
||||
Reset to Jan 1st
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<button class="date-picker-button date-picker-save" on:click={() => showDatePicker = false}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button type="button" class="toggle-text" on:click={toggleHowItWorks}>
|
||||
{showHowItWorks ? 'Hide Details' : 'How does this work?'}
|
||||
</button>
|
||||
|
||||
@@ -2,5 +2,10 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
plugins: [sveltekit()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user