feat: add bridge days recommendations feature with display and calculation logic
All checks were successful
Build and Push Docker Image / build (push) Successful in 35s
All checks were successful
Build and Push Docker Image / build (push) Successful in 35s
This commit is contained in:
219
public/js/bridge-days.js
Normal file
219
public/js/bridge-days.js
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Bridge Days Calculator
|
||||
* Calculates optimal vacation days based on public holidays
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculate bridge days and optimal vacation periods for a month
|
||||
* @param {number} year - The year to calculate for
|
||||
* @param {number} month - The month (0-11)
|
||||
* @param {string} bundesland - The German state code
|
||||
* @returns {Array} Array of bridge day recommendations
|
||||
*/
|
||||
function calculateBridgeDays(year, month, bundesland) {
|
||||
const recommendations = [];
|
||||
|
||||
// Get all holidays for the year
|
||||
const holidays = getPublicHolidays(year, bundesland);
|
||||
|
||||
// Create a calendar map for the entire year
|
||||
const calendar = createYearCalendar(year, holidays);
|
||||
|
||||
// Find all work day blocks (consecutive work days between weekends/holidays)
|
||||
const workBlocks = findWorkDayBlocks(calendar);
|
||||
|
||||
// Evaluate each block and calculate benefit
|
||||
workBlocks.forEach(block => {
|
||||
const benefit = evaluateBlock(block, calendar);
|
||||
if (benefit.ratio >= 2.0) { // Only show if at least 2x benefit
|
||||
recommendations.push(benefit);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by benefit ratio (best deals first)
|
||||
recommendations.sort((a, b) => b.ratio - a.ratio);
|
||||
|
||||
// Filter for the specific month
|
||||
const monthRecommendations = recommendations.filter(rec => {
|
||||
const startDate = new Date(rec.startDate);
|
||||
return startDate.getMonth() === month && startDate.getFullYear() === year;
|
||||
});
|
||||
|
||||
return monthRecommendations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a calendar map for the entire year
|
||||
* @param {number} year - The year
|
||||
* @param {Array} holidays - Array of holiday objects
|
||||
* @returns {Map} Map of date strings to day types
|
||||
*/
|
||||
function createYearCalendar(year, holidays) {
|
||||
const calendar = new Map();
|
||||
|
||||
// Create holiday map for fast lookup
|
||||
const holidayMap = new Map();
|
||||
holidays.forEach(h => {
|
||||
const dateStr = formatDateKey(h.date);
|
||||
holidayMap.set(dateStr, h.name);
|
||||
});
|
||||
|
||||
// Process each day of the year
|
||||
for (let month = 0; month < 12; month++) {
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const date = new Date(year, month, day);
|
||||
const dateStr = formatDateKey(date);
|
||||
const dayOfWeek = date.getDay();
|
||||
|
||||
let type;
|
||||
if (holidayMap.has(dateStr)) {
|
||||
type = { status: 'HOLIDAY', name: holidayMap.get(dateStr) };
|
||||
} else if (dayOfWeek === 0 || dayOfWeek === 6) {
|
||||
type = { status: 'WEEKEND' };
|
||||
} else {
|
||||
type = { status: 'WORKDAY' };
|
||||
}
|
||||
|
||||
calendar.set(dateStr, type);
|
||||
}
|
||||
}
|
||||
|
||||
return calendar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date as YYYY-MM-DD
|
||||
*/
|
||||
function formatDateKey(date) {
|
||||
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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all work day blocks in the calendar
|
||||
* @param {Map} calendar - The calendar map
|
||||
* @returns {Array} Array of work day blocks
|
||||
*/
|
||||
function findWorkDayBlocks(calendar) {
|
||||
const blocks = [];
|
||||
let currentBlock = null;
|
||||
|
||||
// Sort dates for sequential processing
|
||||
const sortedDates = Array.from(calendar.keys()).sort();
|
||||
|
||||
sortedDates.forEach(dateStr => {
|
||||
const dayType = calendar.get(dateStr);
|
||||
|
||||
if (dayType.status === 'WORKDAY') {
|
||||
if (!currentBlock) {
|
||||
currentBlock = {
|
||||
startDate: dateStr,
|
||||
endDate: dateStr,
|
||||
days: [dateStr]
|
||||
};
|
||||
} else {
|
||||
currentBlock.endDate = dateStr;
|
||||
currentBlock.days.push(dateStr);
|
||||
}
|
||||
} else {
|
||||
if (currentBlock) {
|
||||
blocks.push(currentBlock);
|
||||
currentBlock = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Don't forget the last block
|
||||
if (currentBlock) {
|
||||
blocks.push(currentBlock);
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a work day block and calculate benefit
|
||||
* @param {Object} block - The work day block
|
||||
* @param {Map} calendar - The calendar map
|
||||
* @returns {Object} Benefit information
|
||||
*/
|
||||
function evaluateBlock(block, calendar) {
|
||||
const vacationDaysNeeded = block.days.length;
|
||||
|
||||
// Find the extended free period (including surrounding weekends/holidays)
|
||||
let startDate = new Date(block.startDate);
|
||||
let endDate = new Date(block.endDate);
|
||||
|
||||
// Extend backwards to include preceding weekends/holidays
|
||||
let currentDate = new Date(startDate);
|
||||
currentDate.setDate(currentDate.getDate() - 1);
|
||||
while (true) {
|
||||
const dateStr = formatDateKey(currentDate);
|
||||
const dayType = calendar.get(dateStr);
|
||||
if (!dayType || dayType.status === 'WORKDAY') break;
|
||||
startDate = new Date(currentDate);
|
||||
currentDate.setDate(currentDate.getDate() - 1);
|
||||
}
|
||||
|
||||
// Extend forwards to include following weekends/holidays
|
||||
currentDate = new Date(endDate);
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
while (true) {
|
||||
const dateStr = formatDateKey(currentDate);
|
||||
const dayType = calendar.get(dateStr);
|
||||
if (!dayType || dayType.status === 'WORKDAY') break;
|
||||
endDate = new Date(currentDate);
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
|
||||
// Calculate total free days
|
||||
const totalFreeDays = Math.floor((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1;
|
||||
|
||||
// Calculate benefit ratio
|
||||
const ratio = totalFreeDays / vacationDaysNeeded;
|
||||
|
||||
// Find holidays in the period for description
|
||||
const holidaysInPeriod = [];
|
||||
currentDate = new Date(startDate);
|
||||
while (currentDate <= endDate) {
|
||||
const dateStr = formatDateKey(currentDate);
|
||||
const dayType = calendar.get(dateStr);
|
||||
if (dayType && dayType.status === 'HOLIDAY') {
|
||||
holidaysInPeriod.push(dayType.name);
|
||||
}
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
|
||||
return {
|
||||
startDate: formatDateKey(startDate),
|
||||
endDate: formatDateKey(endDate),
|
||||
vacationDays: block.days,
|
||||
vacationDaysNeeded: vacationDaysNeeded,
|
||||
totalFreeDays: totalFreeDays,
|
||||
ratio: ratio,
|
||||
holidays: holidaysInPeriod
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable description for a bridge day recommendation
|
||||
* @param {Object} recommendation - The recommendation object
|
||||
* @returns {string} Description text
|
||||
*/
|
||||
function getBridgeDayDescription(recommendation) {
|
||||
const { vacationDaysNeeded, totalFreeDays, ratio, holidays } = recommendation;
|
||||
|
||||
let description = `${vacationDaysNeeded} Urlaubstag${vacationDaysNeeded > 1 ? 'e' : ''} für ${totalFreeDays} freie Tage`;
|
||||
|
||||
if (holidays.length > 0) {
|
||||
description += ` (inkl. ${holidays.join(', ')})`;
|
||||
}
|
||||
|
||||
description += ` - ${ratio.toFixed(1)}x Ertrag`;
|
||||
|
||||
return description;
|
||||
}
|
||||
Reference in New Issue
Block a user