Hotfix: Ensure fixed days are counted in days off

This commit is contained in:
zachd
2025-11-10 01:29:54 +01:00
parent 3d1ef520d2
commit d3119d7486
3 changed files with 397 additions and 19 deletions

View File

@@ -78,7 +78,7 @@
} else if (isOptimizedDayOff(day)) {
return 'Day off (calculated)';
} else {
return 'Tap to select fixed day off';
return 'Tap to add fixed day off';
}
}
@@ -228,7 +228,7 @@
cursor: pointer;
}
.clickable:hover {
opacity: 0.8;
opacity: 0.7;
transform: scale(1.05);
transition: transform 0.1s, opacity 0.1s;
}

View File

@@ -46,31 +46,40 @@
$: selectedCountryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || '';
$: if (selectedCountryCode || selectedStateCode || daysOff || year || startDate) {
// Reactive: when year changes, load start date and fixed days off for that year
$: if (year !== undefined && year && typeof window !== 'undefined') {
startDate = getStartDate(year);
loadFixedDaysOff(year);
// Adjust daysOff to include fixed days off if they exist
// Calculate base days off (total - fixed days)
const baseDaysOff = Math.max(0, daysOff - fixedDaysOff.length);
// If we have fixed days but base is 0, get the country default and add fixed days
if (fixedDaysOff.length > 0 && baseDaysOff === 0 && daysOff < fixedDaysOff.length) {
const countryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || '';
const currentDefaultDaysOff = ptoData[countryCode] || 0;
daysOff = currentDefaultDaysOff + fixedDaysOff.length;
}
}
$: if (selectedCountryCode && year !== undefined && year) {
updateHolidays();
}
// Reactive: when fixedDaysOff changes, update calculations
$: if (fixedDaysOff) {
$: if (fixedDaysOff && year !== undefined && year) {
updateHolidays();
}
// Reactive: when year changes, load start date and fixed days off for that year
$: if (year !== undefined && year) {
startDate = getStartDate(year);
loadFixedDaysOff(year);
}
// Reactive: when startDate or year changes, update excluded months visibility
$: if (year !== undefined && year && startDate) {
showExcludedMonths = !hasExcludedMonths();
}
$: if (daysOff) {
$: if (daysOff !== undefined && typeof window !== 'undefined') {
localStorage.setItem('daysOff', daysOff.toString());
}
$: if (year) {
$: if (year && typeof window !== 'undefined') {
localStorage.setItem('year', year.toString());
}
@@ -104,7 +113,6 @@
year = storedYear ? parseInt(storedYear, 10) : defaultYear;
selectedCountry = storedCountry || defaultCountry;
daysOff = storedDaysOff ? parseInt(storedDaysOff, 10) : defaultDaysOff;
// Load state per country
const countryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || '';
@@ -115,8 +123,39 @@
selectedState = '';
selectedStateCode = '';
}
// Get the current country's default days off
const currentDefaultDaysOff = ptoData[countryCode] || 0;
startDate = getStartDate(year);
loadFixedDaysOff(year);
// Initialize daysOff: use stored value if it exists, otherwise use country default
// Then add fixed days off to it
if (storedDaysOff !== null && storedDaysOff !== '') {
const storedValue = parseInt(storedDaysOff, 10);
// If stored value is 0 and there are no fixed days, use default instead
if (storedValue === 0 && fixedDaysOff.length === 0) {
daysOff = currentDefaultDaysOff;
} else {
daysOff = storedValue;
}
} else {
// No stored value, use country default
daysOff = currentDefaultDaysOff;
}
// Add fixed days off to the base days off
if (fixedDaysOff.length > 0) {
// Calculate base: if daysOff is less than fixed days, base is 0, otherwise subtract fixed days
const baseDaysOff = Math.max(0, daysOff - fixedDaysOff.length);
// If base is 0 and we have fixed days, set base to default and add fixed days
if (baseDaysOff === 0 && daysOff < fixedDaysOff.length) {
daysOff = currentDefaultDaysOff + fixedDaysOff.length;
} else {
daysOff = baseDaysOff + fixedDaysOff.length;
}
}
// showExcludedMonths will be set by reactive statement
updateHolidays();
});
@@ -166,7 +205,7 @@
}
function updateHolidays() {
if (selectedCountryCode) {
if (selectedCountryCode && year !== undefined && year) {
updateStatesList(selectedCountryCode);
let allHolidays = getHolidaysForYear(selectedCountryCode, year, selectedStateCode);
holidays = allHolidays.map(holiday => ({
@@ -175,7 +214,10 @@
hidden: isHolidayHidden(holiday)
}));
const visibleHolidays = holidays.filter(h => !h.hidden);
optimizedDaysOff = optimizeDaysOff(visibleHolidays, year, daysOff, weekendDays, startDate, fixedDaysOff);
// Use baseDaysOff for optimization (not including fixed days in the budget)
// Calculate it here to ensure it's always defined
const budgetDaysOff = Math.max(0, (daysOff || 0) - (fixedDaysOff?.length || 0));
optimizedDaysOff = optimizeDaysOff(visibleHolidays, year, budgetDaysOff, weekendDays, startDate, fixedDaysOff);
consecutiveDaysOff = calculateConsecutiveDaysOff(visibleHolidays, optimizedDaysOff, year, weekendDays, startDate, fixedDaysOff);
} else {
holidays = [];
@@ -189,21 +231,32 @@
const normalizedDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const dateKeyStr = dateKey(normalizedDate);
// Check if this day is already a day off for any reason (holiday, weekend, or optimized)
const isWeekendDay = weekendDays.includes(normalizedDate.getDay());
const isHolidayDay = holidays.some(h => datesMatch(h.date, normalizedDate));
const isOptimizedDay = optimizedDaysOff.some(d => datesMatch(d, normalizedDate));
const isAlreadyDayOff = isWeekendDay || isHolidayDay || isOptimizedDay;
// Check if date is already in fixedDaysOff
const existingIndex = fixedDaysOff.findIndex(d => dateKey(d) === dateKeyStr);
if (existingIndex >= 0) {
// Remove if already exists
// Remove if already exists - don't subtract from days off count
fixedDaysOff = fixedDaysOff.filter((_, i) => i !== existingIndex);
} else {
// Add if doesn't exist
fixedDaysOff = [...fixedDaysOff, normalizedDate];
// Only increase days off if this day isn't already a day off for another reason
if (!isAlreadyDayOff) {
daysOff++;
}
}
// Save to localStorage
saveFixedDaysOff(year);
localStorage.setItem('daysOff', daysOff.toString());
// Update calculations
// Update calculations (using baseDaysOff for optimization)
updateHolidays();
}
@@ -243,7 +296,8 @@
break;
case 'ArrowDown':
event.preventDefault();
if (daysOff > 0) {
const minDaysOff = fixedDaysOff.length;
if (daysOff > minDaysOff) {
daysOff--;
updateHolidays();
}
@@ -374,6 +428,13 @@
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
}
// Helper function to check if a date matches another date (ignoring time)
function datesMatch(date1: Date, date2: Date): boolean {
return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate();
}
// Load fixed days off for a given year from localStorage
function loadFixedDaysOff(year: number) {
try {
@@ -1115,7 +1176,7 @@
aria-label="Select country" />
and have
<span class="arrow-controls">
<button on:click={() => { if (daysOff > 0) { daysOff--; updateHolidays(); } }} aria-label="Decrease days off"></button>
<button on:click={() => { const minDaysOff = fixedDaysOff.length; if (daysOff > minDaysOff) { daysOff--; updateHolidays(); } }} aria-label="Decrease days off"></button>
<span class="bold">{daysOff}</span>
<button on:click={() => { daysOff++; updateHolidays(); }} aria-label="Increase days off"></button>
</span>

317
src/routes/page.test.ts Normal file
View File

@@ -0,0 +1,317 @@
import { describe, it, expect, beforeEach } from 'vitest';
// Helper function to create a date key (same as in +page.svelte)
function dateKey(date: Date): string {
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
}
// Helper function to check if a day is already a day off
function isAlreadyDayOff(
date: Date,
weekendDays: number[],
holidays: Array<{ date: Date; name: string }>,
optimizedDaysOff: Date[]
): boolean {
const isWeekendDay = weekendDays.includes(date.getDay());
const isHolidayDay = holidays.some(h =>
h.date.getFullYear() === date.getFullYear() &&
h.date.getMonth() === date.getMonth() &&
h.date.getDate() === date.getDate()
);
const isOptimizedDay = optimizedDaysOff.some(d =>
d.getFullYear() === date.getFullYear() &&
d.getMonth() === date.getMonth() &&
d.getDate() === date.getDate()
);
return isWeekendDay || isHolidayDay || isOptimizedDay;
}
// Simulate toggleFixedDayOff logic
function simulateToggleFixedDayOff(
date: Date,
fixedDaysOff: Date[],
daysOff: number,
weekendDays: number[],
holidays: Array<{ date: Date; name: string }>,
optimizedDaysOff: Date[]
): { fixedDaysOff: Date[]; daysOff: number } {
const normalizedDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const dateKeyStr = dateKey(normalizedDate);
const isAlreadyDayOffDay = isAlreadyDayOff(normalizedDate, weekendDays, holidays, optimizedDaysOff);
const existingIndex = fixedDaysOff.findIndex(d => dateKey(d) === dateKeyStr);
let newFixedDaysOff: Date[];
let newDaysOff = daysOff;
if (existingIndex >= 0) {
// Remove if already exists - don't subtract from days off count
newFixedDaysOff = fixedDaysOff.filter((_, i) => i !== existingIndex);
} else {
// Add if doesn't exist
newFixedDaysOff = [...fixedDaysOff, normalizedDate];
// Only increase days off if this day isn't already a day off for another reason
if (!isAlreadyDayOffDay) {
newDaysOff = daysOff + 1;
}
}
return { fixedDaysOff: newFixedDaysOff, daysOff: newDaysOff };
}
// Helper to check minimum days off validation
function canDecreaseDaysOff(daysOff: number, fixedDaysOffCount: number): boolean {
return daysOff > fixedDaysOffCount;
}
describe('Fixed Days Off Logic', () => {
const TEST_YEAR = 2024;
const WEEKEND_DAYS = [0, 6]; // Sunday, Saturday
describe('toggleFixedDayOff behavior', () => {
it('should increase days off when adding a fixed day off to a regular day', () => {
const regularDay = new Date(TEST_YEAR, 0, 15); // Monday, Jan 15
const fixedDaysOff: Date[] = [];
const daysOff = 10;
const holidays: Array<{ date: Date; name: string }> = [];
const optimizedDaysOff: Date[] = [];
const result = simulateToggleFixedDayOff(
regularDay,
fixedDaysOff,
daysOff,
WEEKEND_DAYS,
holidays,
optimizedDaysOff
);
expect(result.fixedDaysOff.length).toBe(1);
expect(result.daysOff).toBe(11); // Increased by 1
});
it('should NOT increase days off when adding a fixed day off to a weekend', () => {
const weekendDay = new Date(TEST_YEAR, 0, 13); // Saturday, Jan 13
const fixedDaysOff: Date[] = [];
const daysOff = 10;
const holidays: Array<{ date: Date; name: string }> = [];
const optimizedDaysOff: Date[] = [];
const result = simulateToggleFixedDayOff(
weekendDay,
fixedDaysOff,
daysOff,
WEEKEND_DAYS,
holidays,
optimizedDaysOff
);
expect(result.fixedDaysOff.length).toBe(1);
expect(result.daysOff).toBe(10); // Not increased
});
it('should NOT increase days off when adding a fixed day off to a holiday', () => {
const holidayDate = new Date(TEST_YEAR, 0, 1); // New Year's Day
const fixedDaysOff: Date[] = [];
const daysOff = 10;
const holidays: Array<{ date: Date; name: string }> = [
{ date: new Date(TEST_YEAR, 0, 1), name: 'New Year\'s Day' }
];
const optimizedDaysOff: Date[] = [];
const result = simulateToggleFixedDayOff(
holidayDate,
fixedDaysOff,
daysOff,
WEEKEND_DAYS,
holidays,
optimizedDaysOff
);
expect(result.fixedDaysOff.length).toBe(1);
expect(result.daysOff).toBe(10); // Not increased
});
it('should NOT increase days off when adding a fixed day off to an optimized day', () => {
const optimizedDay = new Date(TEST_YEAR, 0, 15); // Monday, Jan 15
const fixedDaysOff: Date[] = [];
const daysOff = 10;
const holidays: Array<{ date: Date; name: string }> = [];
const optimizedDaysOff: Date[] = [new Date(TEST_YEAR, 0, 15)];
const result = simulateToggleFixedDayOff(
optimizedDay,
fixedDaysOff,
daysOff,
WEEKEND_DAYS,
holidays,
optimizedDaysOff
);
expect(result.fixedDaysOff.length).toBe(1);
expect(result.daysOff).toBe(10); // Not increased
});
it('should NOT decrease days off when removing a fixed day off', () => {
const fixedDay = new Date(TEST_YEAR, 0, 15);
const fixedDaysOff: Date[] = [fixedDay];
const daysOff = 11;
const holidays: Array<{ date: Date; name: string }> = [];
const optimizedDaysOff: Date[] = [];
const result = simulateToggleFixedDayOff(
fixedDay,
fixedDaysOff,
daysOff,
WEEKEND_DAYS,
holidays,
optimizedDaysOff
);
expect(result.fixedDaysOff.length).toBe(0);
expect(result.daysOff).toBe(11); // Not decreased
});
it('should NOT decrease days off when removing a fixed day off that was on a weekend', () => {
const weekendDay = new Date(TEST_YEAR, 0, 13); // Saturday
const fixedDaysOff: Date[] = [weekendDay];
const daysOff = 10;
const holidays: Array<{ date: Date; name: string }> = [];
const optimizedDaysOff: Date[] = [];
const result = simulateToggleFixedDayOff(
weekendDay,
fixedDaysOff,
daysOff,
WEEKEND_DAYS,
holidays,
optimizedDaysOff
);
expect(result.fixedDaysOff.length).toBe(0);
expect(result.daysOff).toBe(10); // Not decreased
});
it('should handle multiple fixed days off correctly', () => {
const day1 = new Date(TEST_YEAR, 0, 15);
const day2 = new Date(TEST_YEAR, 0, 16);
const day3 = new Date(TEST_YEAR, 0, 17);
let fixedDaysOff: Date[] = [];
let daysOff = 10;
const holidays: Array<{ date: Date; name: string }> = [];
const optimizedDaysOff: Date[] = [];
// Add first day
let result = simulateToggleFixedDayOff(day1, fixedDaysOff, daysOff, WEEKEND_DAYS, holidays, optimizedDaysOff);
fixedDaysOff = result.fixedDaysOff;
daysOff = result.daysOff;
expect(daysOff).toBe(11);
// Add second day
result = simulateToggleFixedDayOff(day2, fixedDaysOff, daysOff, WEEKEND_DAYS, holidays, optimizedDaysOff);
fixedDaysOff = result.fixedDaysOff;
daysOff = result.daysOff;
expect(daysOff).toBe(12);
// Add third day
result = simulateToggleFixedDayOff(day3, fixedDaysOff, daysOff, WEEKEND_DAYS, holidays, optimizedDaysOff);
fixedDaysOff = result.fixedDaysOff;
daysOff = result.daysOff;
expect(daysOff).toBe(13);
// Remove one day - count should stay the same
result = simulateToggleFixedDayOff(day2, fixedDaysOff, daysOff, WEEKEND_DAYS, holidays, optimizedDaysOff);
expect(result.fixedDaysOff.length).toBe(2);
expect(result.daysOff).toBe(13); // Not decreased
});
});
describe('Minimum days off validation', () => {
it('should allow decreasing when days off is greater than fixed days off count', () => {
const daysOff = 15;
const fixedDaysOffCount = 5;
expect(canDecreaseDaysOff(daysOff, fixedDaysOffCount)).toBe(true);
});
it('should NOT allow decreasing when days off equals fixed days off count', () => {
const daysOff = 5;
const fixedDaysOffCount = 5;
expect(canDecreaseDaysOff(daysOff, fixedDaysOffCount)).toBe(false);
});
it('should NOT allow decreasing when days off is less than fixed days off count', () => {
const daysOff = 3;
const fixedDaysOffCount = 5;
expect(canDecreaseDaysOff(daysOff, fixedDaysOffCount)).toBe(false);
});
it('should allow decreasing when there are no fixed days off', () => {
const daysOff = 10;
const fixedDaysOffCount = 0;
expect(canDecreaseDaysOff(daysOff, fixedDaysOffCount)).toBe(true);
});
it('should NOT allow decreasing when days off is 0 and there are no fixed days off', () => {
const daysOff = 0;
const fixedDaysOffCount = 0;
expect(canDecreaseDaysOff(daysOff, fixedDaysOffCount)).toBe(false);
});
});
describe('Edge cases', () => {
it('should handle adding and removing the same day multiple times', () => {
const day = new Date(TEST_YEAR, 0, 15);
let fixedDaysOff: Date[] = [];
let daysOff = 10;
const holidays: Array<{ date: Date; name: string }> = [];
const optimizedDaysOff: Date[] = [];
// Add
let result = simulateToggleFixedDayOff(day, fixedDaysOff, daysOff, WEEKEND_DAYS, holidays, optimizedDaysOff);
fixedDaysOff = result.fixedDaysOff;
daysOff = result.daysOff;
expect(fixedDaysOff.length).toBe(1);
expect(daysOff).toBe(11);
// Remove
result = simulateToggleFixedDayOff(day, fixedDaysOff, daysOff, WEEKEND_DAYS, holidays, optimizedDaysOff);
fixedDaysOff = result.fixedDaysOff;
daysOff = result.daysOff;
expect(fixedDaysOff.length).toBe(0);
expect(daysOff).toBe(11); // Not decreased
// Add again
result = simulateToggleFixedDayOff(day, fixedDaysOff, daysOff, WEEKEND_DAYS, holidays, optimizedDaysOff);
expect(result.fixedDaysOff.length).toBe(1);
expect(result.daysOff).toBe(12); // Increased again
});
it('should handle fixed day off on a day that becomes optimized later', () => {
const day = new Date(TEST_YEAR, 0, 15);
const fixedDaysOff: Date[] = [day];
const daysOff = 10;
const holidays: Array<{ date: Date; name: string }> = [];
// Day is now optimized
const optimizedDaysOff: Date[] = [day];
// Removing fixed day off should not decrease count since it's now optimized
const result = simulateToggleFixedDayOff(
day,
fixedDaysOff,
daysOff,
WEEKEND_DAYS,
holidays,
optimizedDaysOff
);
expect(result.fixedDaysOff.length).toBe(0);
expect(result.daysOff).toBe(10); // Not decreased
});
});
});