Allow customisable Weekend days

This commit is contained in:
Zachary
2024-11-22 19:39:56 +01:00
parent 50fb4f399f
commit f9dc40a857
5 changed files with 360 additions and 198 deletions
+4 -6
View File
@@ -7,6 +7,7 @@
export let optimizedDaysOff: Date[];
export let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }>;
export let selectedCountryCode: string;
export let weekendDays: number[] = [6, 0];
// Function to determine the first day of the week based on locale
function getFirstDayOfWeek(locale: string): number {
@@ -80,11 +81,8 @@
});
}
function isWeekend(day: number): boolean {
const dayOfWeek = (adjustedFirstDay + day - 1) % 7;
const saturdayIndex = (6 - firstDayOfWeek + 7) % 7;
const sundayIndex = (7 - firstDayOfWeek + 7) % 7;
return dayOfWeek === saturdayIndex || dayOfWeek === sundayIndex;
function isWeekend(date: Date): boolean {
return weekendDays.includes(date.getDay());
}
const dayInitials = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
@@ -103,7 +101,7 @@
<div class="day"></div>
{/each}
{#each Array.from({ length: daysInMonth }, (_, i) => i + 1) as day}
<div class="day {isWeekend(day) ? 'weekend' : ''} {getHoliday(day) ? 'holiday' : ''} {isOptimizedDayOff(day) ? 'optimized' : ''} {isConsecutiveDayOff(day) ? 'consecutive-day' : ''}">
<div class="day {isWeekend(new Date(year, month, day)) ? 'weekend' : ''} {getHoliday(day) ? 'holiday' : ''} {isOptimizedDayOff(day) ? 'optimized' : ''} {isConsecutiveDayOff(day) ? 'consecutive-day' : ''}">
{day}
{#if getHoliday(day)}
<Tooltip text={getHoliday(day)?.name} />
+152 -39
View File
@@ -5,7 +5,7 @@ const MS_IN_A_DAY = 86400000;
const MAX_GAP_LENGTH = 5;
// Helper function to check if a date is a weekend
const isWeekend = (date: Date): boolean => date.getDay() === 0 || date.getDay() === 6;
const isWeekend = (date: Date, weekendDays: number[]): boolean => weekendDays.includes(date.getDay());
// Helper function to check if two dates are the same day
const isSameDay = (date1: Date, date2: Date): boolean =>
@@ -17,7 +17,7 @@ const isSameDay = (date1: Date, date2: Date): boolean =>
const dateKey = (date: Date): string => `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
// Helper function to check if a date is a holiday
const isHoliday = (date: Date, holidays: { date: Date }[]): boolean => holidays.some(h => isSameDay(h.date, date));
const isHoliday = (date: Date, holidays: { date: Date }[]): boolean => holidays.some(h => dateKey(h.date) === dateKey(date));
// Helper function to check if a date is a day off
const isDayOff = (date: Date, allDaysOffSet: Set<string>): boolean => allDaysOffSet.has(dateKey(date));
@@ -50,22 +50,49 @@ export function getHolidaysForYear(countryCode: string, year: number, stateCode?
}
// Optimize days off to create the longest possible chains
export function optimizeDaysOff(holidays: { date: Date }[], year: number, daysOff: number): Date[] {
export function optimizeDaysOff(
holidays: { date: Date }[],
year: number,
daysOff: number,
weekendDays: number[] = [0, 6]
): Date[] {
const currentYearHolidays = holidays.filter(h => h.date.getFullYear() === year);
const weekends = getWeekends(year);
const weekends = getWeekends(year, weekendDays);
const allDaysOffSet = new Set([
...currentYearHolidays.map(h => dateKey(h.date)),
...weekends.map(d => dateKey(d))
]);
let rankedGaps = rankGapsByEfficiency(findGaps(allDaysOffSet, year), allDaysOffSet);
let rankedGaps = rankGapsByEfficiency(
findGaps(allDaysOffSet, year, weekendDays),
allDaysOffSet,
weekendDays
);
return selectDaysOff(rankedGaps, daysOff, allDaysOffSet, year);
return selectDaysOff(rankedGaps, daysOff, allDaysOffSet, year, weekendDays);
}
// Calculate consecutive days off
export function calculateConsecutiveDaysOff(holidays: { date: Date }[], optimizedDaysOff: Date[], year: number): { startDate: Date; endDate: Date; usedDaysOff: number; totalDays: number }[] {
const allDaysOffSet = new Set([...holidays.map(h => dateKey(h.date)), ...optimizedDaysOff.map(d => dateKey(d))]);
export function calculateConsecutiveDaysOff(
holidays: Array<{ date: Date }>,
optimizedDaysOff: Date[],
year: number,
weekendDays: number[] = [0, 6]
) {
const allDaysOff = new Set([
...holidays.map(h => h.date.toISOString()),
...optimizedDaysOff.map(d => d.toISOString())
]);
// Add all weekend days for the year
const startDate = new Date(year, 0, 1);
const endDate = new Date(year, 11, 31);
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
if (weekendDays.includes(d.getDay())) {
allDaysOff.add(new Date(d).toISOString());
}
}
const consecutiveDaysOff: { startDate: Date; endDate: Date; usedDaysOff: number; totalDays: number }[] = [];
let currentGroup: Date[] = [];
@@ -74,39 +101,102 @@ export function calculateConsecutiveDaysOff(holidays: { date: Date }[], optimize
const date = new Date(year, month, day);
if (date.getMonth() !== month) break;
if (isWeekend(date) || isHoliday(date, holidays) || isDayOff(date, allDaysOffSet)) {
if (isWeekend(date, weekendDays) || isHoliday(date, holidays) || allDaysOff.has(date.toISOString())) {
currentGroup.push(date);
} else {
if (currentGroup.length > 2) {
addConsecutiveDaysOff(consecutiveDaysOff, currentGroup, optimizedDaysOff);
if (isValidPeriod(currentGroup, weekendDays, holidays, optimizedDaysOff)) {
const startDate = currentGroup[0];
const endDate = currentGroup[currentGroup.length - 1];
const totalDays = daysBetween(startDate, endDate) + 1;
const usedDaysOff = currentGroup.filter(d =>
optimizedDaysOff.some(od =>
od.getFullYear() === d.getFullYear() &&
od.getMonth() === d.getMonth() &&
od.getDate() === d.getDate()
)
).length;
consecutiveDaysOff.push({
startDate,
endDate,
usedDaysOff,
totalDays
});
}
currentGroup = [];
}
}
}
if (currentGroup.length > 2) {
addConsecutiveDaysOff(consecutiveDaysOff, currentGroup, optimizedDaysOff);
if (isValidPeriod(currentGroup, weekendDays, holidays, optimizedDaysOff)) {
const startDate = currentGroup[0];
const endDate = currentGroup[currentGroup.length - 1];
const totalDays = daysBetween(startDate, endDate) + 1;
const usedDaysOff = currentGroup.filter(d =>
optimizedDaysOff.some(od =>
od.getFullYear() === d.getFullYear() &&
od.getMonth() === d.getMonth() &&
od.getDate() === d.getDate()
)
).length;
consecutiveDaysOff.push({
startDate,
endDate,
usedDaysOff,
totalDays
});
}
return consecutiveDaysOff;
}
// Update the isValidPeriod function
function isValidPeriod(
group: Date[],
weekendDays: number[],
holidays: Array<{ date: Date }>,
optimizedDaysOff: Date[]
): boolean {
if (group.length < 2) return false;
// Count weekend days
const weekendDates = group.filter(date => weekendDays.includes(date.getDay()));
if (weekendDates.length === 0) return false; // Must have at least one weekend day
// Count non-weekend days that are either holidays or PTO days
const nonWeekendHolidayOrPTO = group.some(date => {
// Skip if it's a weekend day
if (weekendDays.includes(date.getDay())) return false;
// Check if it's either a holiday or PTO day
return isHoliday(date, holidays) ||
optimizedDaysOff.some(od =>
od.getFullYear() === date.getFullYear() &&
od.getMonth() === date.getMonth() &&
od.getDate() === date.getDate()
);
});
// Must have at least one weekend day AND one non-weekend holiday/PTO day
return nonWeekendHolidayOrPTO;
}
// Get all weekends for a specific year
function getWeekends(year: number): Date[] {
function getWeekends(year: number, weekendDays: number[]): Date[] {
const weekends: Date[] = [];
for (let month = 0; month < 12; month++) {
for (let day = 1; day <= 31; day++) {
const date = new Date(year, month, day);
if (date.getMonth() !== month) break;
if (isWeekend(date)) weekends.push(date);
if (isWeekend(date, weekendDays)) weekends.push(date);
}
}
return weekends;
}
// Find gaps between days off
function findGaps(allDaysOffSet: Set<string>, year: number): { start: Date; end: Date; gapLength: number }[] {
function findGaps(allDaysOffSet: Set<string>, year: number, weekendDays: number[]): { start: Date; end: Date; gapLength: number }[] {
const gaps: { start: Date; end: Date; gapLength: number }[] = [];
let currentGapStart: Date | null = null;
@@ -117,7 +207,7 @@ function findGaps(allDaysOffSet: Set<string>, year: number): { start: Date; end:
const isDayOff = allDaysOffSet.has(dateKey(date));
if (!isDayOff && !isWeekend(date)) {
if (!isDayOff && !isWeekend(date, weekendDays)) {
if (!currentGapStart) currentGapStart = date;
} else if (currentGapStart) {
const gapLength = daysBetween(currentGapStart, date);
@@ -141,19 +231,34 @@ function findGaps(allDaysOffSet: Set<string>, year: number): { start: Date; end:
}
// Rank gaps by efficiency
function rankGapsByEfficiency(gaps: { start: Date; end: Date; gapLength: number }[], allDaysOffSet: Set<string>): any[] {
function rankGapsByEfficiency(
gaps: { start: Date; end: Date; gapLength: number }[],
allDaysOffSet: Set<string>,
weekendDays: number[] = [0, 6]
): any[] {
return gaps.map(gap => {
const backward = calculateChain(gap.start, gap.gapLength, allDaysOffSet, 'backward');
const forward = calculateChain(gap.end, gap.gapLength, allDaysOffSet, 'forward');
const backward = calculateChain(gap.start, gap.gapLength, allDaysOffSet, 'backward', weekendDays);
const forward = calculateChain(gap.end, gap.gapLength, allDaysOffSet, 'forward', weekendDays);
return forward.chainLength > backward.chainLength || (forward.chainLength === backward.chainLength && forward.usedDaysOff <= backward.usedDaysOff)
return forward.chainLength > backward.chainLength ||
(forward.chainLength === backward.chainLength && forward.usedDaysOff <= backward.usedDaysOff)
? { ...gap, ...forward, fillFrom: 'end' }
: { ...gap, ...backward, fillFrom: 'start' };
}).sort((a, b) => a.gapLength - b.gapLength || b.chainLength - a.chainLength || a.usedDaysOff - b.usedDaysOff);
}).sort((a, b) =>
a.gapLength - b.gapLength ||
b.chainLength - a.chainLength ||
a.usedDaysOff - b.usedDaysOff
);
}
// Calculate potential chain length and days off used
function calculateChain(startDate: Date, gapLength: number, allDaysOffSet: Set<string>, direction: 'backward' | 'forward'): { chainLength: number; usedDaysOff: number } {
function calculateChain(
startDate: Date,
gapLength: number,
allDaysOffSet: Set<string>,
direction: 'backward' | 'forward',
weekendDays: number[] = [0, 6]
): { chainLength: number; usedDaysOff: number } {
let chainLength = gapLength;
let usedDaysOff = 0;
let currentDate = new Date(startDate);
@@ -161,7 +266,8 @@ function calculateChain(startDate: Date, gapLength: number, allDaysOffSet: Set<s
const increment = direction === 'backward' ? -1 : 1;
const boundaryCheck = direction === 'backward' ? -MS_IN_A_DAY : MS_IN_A_DAY;
while (allDaysOffSet.has(dateKey(new Date(currentDate.getTime() + boundaryCheck))) || isWeekend(new Date(currentDate.getTime() + boundaryCheck))) {
while (allDaysOffSet.has(dateKey(new Date(currentDate.getTime() + boundaryCheck))) ||
isWeekend(new Date(currentDate.getTime() + boundaryCheck), weekendDays)) {
chainLength++;
currentDate.setDate(currentDate.getDate() + increment);
}
@@ -169,7 +275,7 @@ function calculateChain(startDate: Date, gapLength: number, allDaysOffSet: Set<s
for (let i = 0; i < gapLength; i++) {
const potentialDayOff = new Date(startDate);
potentialDayOff.setDate(potentialDayOff.getDate() + (i * increment));
if (!allDaysOffSet.has(dateKey(potentialDayOff)) && !isWeekend(potentialDayOff)) {
if (!allDaysOffSet.has(dateKey(potentialDayOff)) && !isWeekend(potentialDayOff, weekendDays)) {
usedDaysOff++;
}
}
@@ -178,7 +284,7 @@ function calculateChain(startDate: Date, gapLength: number, allDaysOffSet: Set<s
}
// Select days off based on ranked gaps
function selectDaysOff(rankedGaps: any[], daysOff: number, allDaysOffSet: Set<string>, year: number): Date[] {
function selectDaysOff(rankedGaps: any[], daysOff: number, allDaysOffSet: Set<string>, year: number, weekendDays: number[]): Date[] {
const selectedDays: Date[] = [];
while (daysOff > 0 && rankedGaps.length > 0) {
@@ -191,33 +297,40 @@ function selectDaysOff(rankedGaps: any[], daysOff: number, allDaysOffSet: Set<st
const potentialDayOff = new Date(startDate);
potentialDayOff.setDate(potentialDayOff.getDate() + (i * increment));
if (!allDaysOffSet.has(dateKey(potentialDayOff)) && !isWeekend(potentialDayOff)) {
if (!allDaysOffSet.has(dateKey(potentialDayOff)) && !isWeekend(potentialDayOff, weekendDays)) {
selectedDays.push(potentialDayOff);
allDaysOffSet.add(dateKey(potentialDayOff));
daysOff--;
}
}
const newGaps = findGaps(allDaysOffSet, year);
rankedGaps = rankGapsByEfficiency(newGaps, allDaysOffSet);
const newGaps = findGaps(allDaysOffSet, year, weekendDays);
rankedGaps = rankGapsByEfficiency(newGaps, allDaysOffSet, weekendDays);
}
return selectedDays;
}
// Add consecutive days off to the list
function addConsecutiveDaysOff(consecutiveDaysOff: { startDate: Date; endDate: Date; usedDaysOff: number; totalDays: number }[], currentGroup: Date[], optimizedDaysOff: Date[]) {
function addConsecutiveDaysOff(
consecutiveDaysOff: { startDate: Date; endDate: Date; usedDaysOff: number; totalDays: number }[],
currentGroup: Date[],
optimizedDaysOff: Date[]
) {
const startDate = currentGroup[0];
const endDate = currentGroup[currentGroup.length - 1];
const totalDays = daysBetween(startDate, endDate) + 1;
const usedDaysOff = currentGroup.filter(d => isDayOff(d, new Set(optimizedDaysOff.map(d => dateKey(d))))).length;
// Create a Set of optimized days off for faster lookup
const optimizedDaysOffSet = new Set(optimizedDaysOff.map(d => dateKey(d)));
// Count only the days that were actually used from our PTO days
const usedDaysOff = currentGroup.filter(d => optimizedDaysOffSet.has(dateKey(d))).length;
if (totalDays > 2) {
consecutiveDaysOff.push({
startDate,
endDate,
usedDaysOff,
totalDays
});
}
consecutiveDaysOff.push({
startDate,
endDate,
usedDaysOff,
totalDays
});
}