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

View File

@@ -46,31 +46,40 @@
$: selectedCountryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || ''; $: 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(); updateHolidays();
} }
// Reactive: when fixedDaysOff changes, update calculations // Reactive: when fixedDaysOff changes, update calculations
$: if (fixedDaysOff) { $: if (fixedDaysOff && year !== undefined && year) {
updateHolidays(); 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 // Reactive: when startDate or year changes, update excluded months visibility
$: if (year !== undefined && year && startDate) { $: if (year !== undefined && year && startDate) {
showExcludedMonths = !hasExcludedMonths(); showExcludedMonths = !hasExcludedMonths();
} }
$: if (daysOff) { $: if (daysOff !== undefined && typeof window !== 'undefined') {
localStorage.setItem('daysOff', daysOff.toString()); localStorage.setItem('daysOff', daysOff.toString());
} }
$: if (year) { $: if (year && typeof window !== 'undefined') {
localStorage.setItem('year', year.toString()); localStorage.setItem('year', year.toString());
} }
@@ -104,7 +113,6 @@
year = storedYear ? parseInt(storedYear, 10) : defaultYear; year = storedYear ? parseInt(storedYear, 10) : defaultYear;
selectedCountry = storedCountry || defaultCountry; selectedCountry = storedCountry || defaultCountry;
daysOff = storedDaysOff ? parseInt(storedDaysOff, 10) : defaultDaysOff;
// Load state per country // Load state per country
const countryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || ''; const countryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || '';
@@ -115,8 +123,39 @@
selectedState = ''; selectedState = '';
selectedStateCode = ''; selectedStateCode = '';
} }
// Get the current country's default days off
const currentDefaultDaysOff = ptoData[countryCode] || 0;
startDate = getStartDate(year); startDate = getStartDate(year);
loadFixedDaysOff(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 // showExcludedMonths will be set by reactive statement
updateHolidays(); updateHolidays();
}); });
@@ -166,7 +205,7 @@
} }
function updateHolidays() { function updateHolidays() {
if (selectedCountryCode) { if (selectedCountryCode && year !== undefined && year) {
updateStatesList(selectedCountryCode); updateStatesList(selectedCountryCode);
let allHolidays = getHolidaysForYear(selectedCountryCode, year, selectedStateCode); let allHolidays = getHolidaysForYear(selectedCountryCode, year, selectedStateCode);
holidays = allHolidays.map(holiday => ({ holidays = allHolidays.map(holiday => ({
@@ -175,7 +214,10 @@
hidden: isHolidayHidden(holiday) hidden: isHolidayHidden(holiday)
})); }));
const visibleHolidays = holidays.filter(h => !h.hidden); 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); consecutiveDaysOff = calculateConsecutiveDaysOff(visibleHolidays, optimizedDaysOff, year, weekendDays, startDate, fixedDaysOff);
} else { } else {
holidays = []; holidays = [];
@@ -189,21 +231,32 @@
const normalizedDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); const normalizedDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const dateKeyStr = dateKey(normalizedDate); 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 // Check if date is already in fixedDaysOff
const existingIndex = fixedDaysOff.findIndex(d => dateKey(d) === dateKeyStr); const existingIndex = fixedDaysOff.findIndex(d => dateKey(d) === dateKeyStr);
if (existingIndex >= 0) { if (existingIndex >= 0) {
// Remove if already exists // Remove if already exists - don't subtract from days off count
fixedDaysOff = fixedDaysOff.filter((_, i) => i !== existingIndex); fixedDaysOff = fixedDaysOff.filter((_, i) => i !== existingIndex);
} else { } else {
// Add if doesn't exist // Add if doesn't exist
fixedDaysOff = [...fixedDaysOff, normalizedDate]; 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 // Save to localStorage
saveFixedDaysOff(year); saveFixedDaysOff(year);
localStorage.setItem('daysOff', daysOff.toString());
// Update calculations // Update calculations (using baseDaysOff for optimization)
updateHolidays(); updateHolidays();
} }
@@ -243,7 +296,8 @@
break; break;
case 'ArrowDown': case 'ArrowDown':
event.preventDefault(); event.preventDefault();
if (daysOff > 0) { const minDaysOff = fixedDaysOff.length;
if (daysOff > minDaysOff) {
daysOff--; daysOff--;
updateHolidays(); updateHolidays();
} }
@@ -374,6 +428,13 @@
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`; 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 // Load fixed days off for a given year from localStorage
function loadFixedDaysOff(year: number) { function loadFixedDaysOff(year: number) {
try { try {
@@ -1115,7 +1176,7 @@
aria-label="Select country" /> aria-label="Select country" />
and have and have
<span class="arrow-controls"> <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> <span class="bold">{daysOff}</span>
<button on:click={() => { daysOff++; updateHolidays(); }} aria-label="Increase days off"></button> <button on:click={() => { daysOff++; updateHolidays(); }} aria-label="Increase days off"></button>
</span> </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
});
});
});