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:
@@ -263,6 +263,10 @@
|
|||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.details-chevron {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
details summary {
|
details summary {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
@@ -677,6 +681,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Bridge Days Recommendations -->
|
||||||
|
<details id="bridgeDaysContainer" class="mb-6 glass-card rounded-xl border border-cyan-600/30 hidden">
|
||||||
|
<summary class="cursor-pointer p-4 hover:bg-gray-700/30 rounded-t-xl transition-colors select-none">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<i data-lucide="lightbulb" class="w-5 h-5 text-cyan-400"></i>
|
||||||
|
<h3 class="text-sm font-semibold text-cyan-400 uppercase tracking-wide">Brückentags-Empfehlungen</h3>
|
||||||
|
<i data-lucide="chevron-down" class="w-4 h-4 text-gray-400 ml-auto details-chevron"></i>
|
||||||
|
</div>
|
||||||
|
</summary>
|
||||||
|
<div class="p-4 pt-2">
|
||||||
|
<div id="bridgeDaysList" class="space-y-2">
|
||||||
|
<!-- Will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<!-- Bulk Edit Actions Bar -->
|
<!-- Bulk Edit Actions Bar -->
|
||||||
<div id="bulkEditBar" class="hidden mb-6 bg-gray-800 rounded-lg shadow p-4 border border-gray-700">
|
<div id="bulkEditBar" class="hidden mb-6 bg-gray-800 rounded-lg shadow p-4 border border-gray-700">
|
||||||
<div class="flex items-center justify-between flex-wrap gap-3">
|
<div class="flex items-center justify-between flex-wrap gap-3">
|
||||||
@@ -878,6 +898,7 @@
|
|||||||
<script src="js/state.js"></script>
|
<script src="js/state.js"></script>
|
||||||
<script src="js/utils.js"></script>
|
<script src="js/utils.js"></script>
|
||||||
<script src="js/holidays.js"></script>
|
<script src="js/holidays.js"></script>
|
||||||
|
<script src="js/bridge-days.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
|
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -1282,6 +1282,7 @@ async function loadMonthlyView() {
|
|||||||
renderMonthlyView(entries);
|
renderMonthlyView(entries);
|
||||||
updateMonthDisplay();
|
updateMonthDisplay();
|
||||||
updateStatistics(entries);
|
updateStatistics(entries);
|
||||||
|
updateBridgeDaysDisplay();
|
||||||
|
|
||||||
// Show/hide PDF export button based on whether month is complete
|
// Show/hide PDF export button based on whether month is complete
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@@ -1531,6 +1532,116 @@ async function updateVacationStatistics() {
|
|||||||
document.getElementById('statVacationRemaining').textContent = `${vacationRemaining} / ${totalVacationDays}`;
|
document.getElementById('statVacationRemaining').textContent = `${vacationRemaining} / ${totalVacationDays}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update bridge days display for current month
|
||||||
|
*/
|
||||||
|
function updateBridgeDaysDisplay() {
|
||||||
|
const container = document.getElementById('bridgeDaysContainer');
|
||||||
|
const list = document.getElementById('bridgeDaysList');
|
||||||
|
|
||||||
|
// Calculate bridge days for current month
|
||||||
|
const recommendations = calculateBridgeDays(displayYear, displayMonth, currentBundesland);
|
||||||
|
|
||||||
|
if (recommendations.length === 0) {
|
||||||
|
container.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show container and populate list
|
||||||
|
container.classList.remove('hidden');
|
||||||
|
list.innerHTML = '';
|
||||||
|
|
||||||
|
recommendations.forEach((rec, index) => {
|
||||||
|
const startDate = new Date(rec.startDate);
|
||||||
|
const endDate = new Date(rec.endDate);
|
||||||
|
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'flex items-start gap-3 p-3 bg-gray-800/50 rounded-lg border border-cyan-600/20 hover:border-cyan-600/40 transition-colors';
|
||||||
|
|
||||||
|
// Format date range
|
||||||
|
const startStr = formatDateDisplay(rec.startDate);
|
||||||
|
const endStr = formatDateDisplay(rec.endDate);
|
||||||
|
|
||||||
|
// Create vacation days list
|
||||||
|
const vacDaysList = rec.vacationDays.map(d => {
|
||||||
|
const date = new Date(d);
|
||||||
|
return `${date.getDate()}.${String(date.getMonth() + 1).padStart(2, '0')}.`;
|
||||||
|
}).join(', ');
|
||||||
|
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-cyan-600/20 flex items-center justify-center text-cyan-400 font-bold text-sm">
|
||||||
|
${Math.round(rec.ratio)}x
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="text-sm font-medium text-gray-200">
|
||||||
|
${startStr} - ${endStr}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400 mt-1">
|
||||||
|
${rec.vacationDaysNeeded} Urlaubstag${rec.vacationDaysNeeded > 1 ? 'e' : ''} (${vacDaysList}) für ${rec.totalFreeDays} freie Tage
|
||||||
|
</div>
|
||||||
|
${rec.holidays.length > 0 ? `
|
||||||
|
<div class="text-xs text-cyan-400 mt-1">
|
||||||
|
${rec.holidays.join(', ')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
<button class="btn-add-bridge-days flex-shrink-0 px-3 py-1 bg-cyan-600 hover:bg-cyan-700 text-white text-xs rounded transition-colors"
|
||||||
|
data-days='${JSON.stringify(rec.vacationDays)}'
|
||||||
|
title="Als Urlaub eintragen">
|
||||||
|
<i data-lucide="calendar-plus" class="w-3 h-3"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-initialize icons
|
||||||
|
if (typeof lucide !== 'undefined' && lucide.createIcons) {
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners for quick add buttons
|
||||||
|
document.querySelectorAll('.btn-add-bridge-days').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const days = JSON.parse(btn.dataset.days);
|
||||||
|
await addBridgeDaysAsVacation(days);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add bridge days as vacation entries
|
||||||
|
*/
|
||||||
|
async function addBridgeDaysAsVacation(days) {
|
||||||
|
let created = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
for (const dateStr of days) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/entries', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
date: dateStr,
|
||||||
|
entryType: 'vacation'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
created++;
|
||||||
|
} else {
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating vacation entry:', error);
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await reloadView();
|
||||||
|
showNotification(`✓ ${created} Urlaubstag${created > 1 ? 'e' : ''} eingetragen${skipped > 0 ? `, ${skipped} übersprungen` : ''}`, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate balance from all previous months (starting from first month with entries)
|
* Calculate balance from all previous months (starting from first month with entries)
|
||||||
*/
|
*/
|
||||||
@@ -2696,7 +2807,65 @@ async function handleBundeslandChange(event) {
|
|||||||
const newBundesland = event.target.value;
|
const newBundesland = event.target.value;
|
||||||
const oldBundesland = currentBundesland;
|
const oldBundesland = currentBundesland;
|
||||||
|
|
||||||
// Show warning with backup recommendation
|
// Check for conflicts with existing entries first
|
||||||
|
const entries = await fetchEntries();
|
||||||
|
const conflicts = [];
|
||||||
|
|
||||||
|
// Get old and new holidays for comparison
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const years = [currentYear - 1, currentYear, currentYear + 1];
|
||||||
|
|
||||||
|
const oldHolidays = new Set();
|
||||||
|
const newHolidays = new Set();
|
||||||
|
|
||||||
|
years.forEach(year => {
|
||||||
|
getPublicHolidays(year, oldBundesland).forEach(h => {
|
||||||
|
// Convert to YYYY-MM-DD format for comparison
|
||||||
|
const year = h.date.getFullYear();
|
||||||
|
const month = String(h.date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(h.date.getDate()).padStart(2, '0');
|
||||||
|
oldHolidays.add(`${year}-${month}-${day}`);
|
||||||
|
});
|
||||||
|
getPublicHolidays(year, newBundesland).forEach(h => {
|
||||||
|
// Convert to YYYY-MM-DD format for comparison
|
||||||
|
const year = h.date.getFullYear();
|
||||||
|
const month = String(h.date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(h.date.getDate()).padStart(2, '0');
|
||||||
|
const dateKey = `${year}-${month}-${day}`;
|
||||||
|
newHolidays.add(dateKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a map of new holidays with their names
|
||||||
|
const newHolidayMap = new Map();
|
||||||
|
years.forEach(year => {
|
||||||
|
getPublicHolidays(year, newBundesland).forEach(h => {
|
||||||
|
const year = h.date.getFullYear();
|
||||||
|
const month = String(h.date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(h.date.getDate()).padStart(2, '0');
|
||||||
|
const dateKey = `${year}-${month}-${day}`;
|
||||||
|
newHolidayMap.set(dateKey, h.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find dates that are holidays in new state but not in old state, and have entries
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (newHolidays.has(entry.date) && !oldHolidays.has(entry.date)) {
|
||||||
|
conflicts.push({
|
||||||
|
date: entry.date,
|
||||||
|
displayDate: formatDateDisplay(entry.date),
|
||||||
|
holidayName: newHolidayMap.get(entry.date)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no conflicts, change directly without warning
|
||||||
|
if (conflicts.length === 0) {
|
||||||
|
await performBundeslandChange(newBundesland, oldBundesland, event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show warning with backup recommendation only if conflicts exist
|
||||||
const bundeslandNames = {
|
const bundeslandNames = {
|
||||||
'BW': 'Baden-Württemberg',
|
'BW': 'Baden-Württemberg',
|
||||||
'BY': 'Bayern',
|
'BY': 'Bayern',
|
||||||
@@ -2716,17 +2885,23 @@ async function handleBundeslandChange(event) {
|
|||||||
'TH': 'Thüringen'
|
'TH': 'Thüringen'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const conflictList = conflicts.map(c => `<li class="text-sm">• ${c.displayDate} (<span class="text-yellow-300">${c.holidayName}</span>)</li>`).join('');
|
||||||
|
|
||||||
// Create custom modal for bundesland change confirmation
|
// Create custom modal for bundesland change confirmation
|
||||||
const modalHTML = `
|
const modalHTML = `
|
||||||
<div id="bundeslandWarningModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" style="display: flex;">
|
<div id="bundeslandWarningModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" style="display: flex;">
|
||||||
<div class="bg-gray-800 rounded-xl shadow-2xl p-6 max-w-md w-full mx-4 border border-yellow-600">
|
<div class="bg-gray-800 rounded-xl shadow-2xl p-6 max-w-md w-full mx-4 border border-yellow-600">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<i data-lucide="alert-triangle" class="w-8 h-8 text-yellow-500"></i>
|
<i data-lucide="alert-triangle" class="w-8 h-8 text-yellow-500"></i>
|
||||||
<h3 class="text-xl font-bold text-yellow-500">Achtung: Bundesland ändern</h3>
|
<h3 class="text-xl font-bold text-yellow-500">Achtung: Konflikte gefunden</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-gray-300 mb-6 space-y-3">
|
<div class="text-gray-300 mb-6 space-y-3">
|
||||||
<p>Sie möchten das Bundesland von <strong>${bundeslandNames[oldBundesland]}</strong> auf <strong>${bundeslandNames[newBundesland]}</strong> ändern.</p>
|
<p>Sie möchten das Bundesland von <strong>${bundeslandNames[oldBundesland]}</strong> auf <strong>${bundeslandNames[newBundesland]}</strong> ändern.</p>
|
||||||
<p class="text-yellow-400"><strong>Warnung:</strong> Durch die Änderung der Feiertage können bestehende Einträge betroffen sein. An Tagen, die zu Feiertagen werden, bleiben Arbeitseinträge erhalten.</p>
|
<p class="text-yellow-400"><strong>Warnung:</strong> Die folgenden Tage werden zu Feiertagen und haben bereits Einträge:</p>
|
||||||
|
<ul class="bg-gray-900 rounded-lg p-3 max-h-40 overflow-y-auto">
|
||||||
|
${conflictList}
|
||||||
|
</ul>
|
||||||
|
<p class="text-sm text-gray-400">Die Einträge bleiben erhalten, aber die Tage werden als Feiertage markiert.</p>
|
||||||
<div class="bg-gray-900 border border-blue-600 rounded-lg p-3 mt-4">
|
<div class="bg-gray-900 border border-blue-600 rounded-lg p-3 mt-4">
|
||||||
<p class="text-blue-400 text-sm mb-2"><i data-lucide="info" class="w-4 h-4 inline mr-1"></i> Wir empfehlen ein Backup vor der Änderung:</p>
|
<p class="text-blue-400 text-sm mb-2"><i data-lucide="info" class="w-4 h-4 inline mr-1"></i> Wir empfehlen ein Backup vor der Änderung:</p>
|
||||||
<button id="quickBackupBtn" class="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors text-sm">
|
<button id="quickBackupBtn" class="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors text-sm">
|
||||||
@@ -2780,59 +2955,6 @@ async function handleBundeslandChange(event) {
|
|||||||
* Perform the actual bundesland change after confirmation
|
* Perform the actual bundesland change after confirmation
|
||||||
*/
|
*/
|
||||||
async function performBundeslandChange(newBundesland, oldBundesland, event) {
|
async function performBundeslandChange(newBundesland, oldBundesland, event) {
|
||||||
// Check for conflicts with existing entries
|
|
||||||
const entries = await fetchEntries();
|
|
||||||
const conflicts = [];
|
|
||||||
|
|
||||||
// Get old and new holidays for comparison
|
|
||||||
const currentYear = new Date().getFullYear();
|
|
||||||
const years = [currentYear - 1, currentYear, currentYear + 1];
|
|
||||||
|
|
||||||
const oldHolidays = new Set();
|
|
||||||
const newHolidays = new Set();
|
|
||||||
|
|
||||||
years.forEach(year => {
|
|
||||||
getPublicHolidays(year, oldBundesland).forEach(h => {
|
|
||||||
oldHolidays.add(h.date.toISOString().split('T')[0]);
|
|
||||||
});
|
|
||||||
getPublicHolidays(year, newBundesland).forEach(h => {
|
|
||||||
newHolidays.add(h.date.toISOString().split('T')[0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find dates that are holidays in new state but not in old state, and have entries
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (newHolidays.has(entry.date) && !oldHolidays.has(entry.date)) {
|
|
||||||
const dateObj = new Date(entry.date);
|
|
||||||
// Temporarily set to new bundesland to get holiday name
|
|
||||||
const tempBundesland = currentBundesland;
|
|
||||||
currentBundesland = newBundesland;
|
|
||||||
const holidayName = getHolidayName(dateObj);
|
|
||||||
currentBundesland = tempBundesland;
|
|
||||||
|
|
||||||
conflicts.push({
|
|
||||||
date: entry.date,
|
|
||||||
displayDate: formatDateDisplay(entry.date),
|
|
||||||
holidayName: holidayName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show conflict info if exists
|
|
||||||
if (conflicts.length > 0) {
|
|
||||||
const conflictList = conflicts.map(c => ` • ${c.displayDate} (${c.holidayName})`).join('\n');
|
|
||||||
const message = `Folgende Tage werden zu Feiertagen und haben bereits Einträge:\n\n${conflictList}\n\nDie Einträge bleiben erhalten.`;
|
|
||||||
showNotification(`⚠️ ${conflicts.length} Konflikt(e) gefunden`, 'warning');
|
|
||||||
console.info(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update state and save
|
|
||||||
currentBundesland = newBundesland;
|
|
||||||
await setSetting('bundesland', newBundesland);
|
|
||||||
|
|
||||||
// Reload view to show updated holidays
|
|
||||||
await reloadView();
|
|
||||||
|
|
||||||
const bundeslandNames = {
|
const bundeslandNames = {
|
||||||
'BW': 'Baden-Württemberg',
|
'BW': 'Baden-Württemberg',
|
||||||
'BY': 'Bayern',
|
'BY': 'Bayern',
|
||||||
@@ -2852,6 +2974,13 @@ async function performBundeslandChange(newBundesland, oldBundesland, event) {
|
|||||||
'TH': 'Thüringen'
|
'TH': 'Thüringen'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update state and save
|
||||||
|
currentBundesland = newBundesland;
|
||||||
|
await setSetting('bundesland', newBundesland);
|
||||||
|
|
||||||
|
// Reload view to show updated holidays
|
||||||
|
await reloadView();
|
||||||
|
|
||||||
showNotification(`✓ Bundesland auf ${bundeslandNames[newBundesland]} gesetzt`, 'success');
|
showNotification(`✓ Bundesland auf ${bundeslandNames[newBundesland]} gesetzt`, 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2915,6 +3044,34 @@ async function handleVacationDaysChange(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for conflicts: More vacation days already taken/planned than new value?
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const fromDate = `${currentYear}-01-01`;
|
||||||
|
const toDate = `${currentYear}-12-31`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const allEntries = await fetchEntries(fromDate, toDate);
|
||||||
|
const vacationCount = allEntries.filter(e => e.entryType === 'vacation').length;
|
||||||
|
|
||||||
|
if (vacationCount > newValue) {
|
||||||
|
// Show warning
|
||||||
|
const difference = vacationCount - newValue;
|
||||||
|
const confirmed = confirm(
|
||||||
|
`⚠️ Warnung: Konflikt erkannt!\n\n` +
|
||||||
|
`Sie haben bereits ${vacationCount} Urlaubstage für ${currentYear} eingetragen.\n` +
|
||||||
|
`Der neue Wert von ${newValue} Tag${newValue !== 1 ? 'en' : ''} ist ${difference} Tag${difference > 1 ? 'e' : ''} zu niedrig.\n\n` +
|
||||||
|
`Möchten Sie den Wert trotzdem auf ${newValue} setzen?`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
event.target.value = totalVacationDays;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking vacation conflicts:', error);
|
||||||
|
}
|
||||||
|
|
||||||
totalVacationDays = newValue;
|
totalVacationDays = newValue;
|
||||||
await setSetting('vacationDays', newValue.toString());
|
await setSetting('vacationDays', newValue.toString());
|
||||||
await updateVacationStatistics();
|
await updateVacationStatistics();
|
||||||
|
|||||||
Reference in New Issue
Block a user