Add settings management with Bundesland selection and holiday calculations
This commit is contained in:
232
public/app.js
232
public/app.js
@@ -24,6 +24,9 @@ let displayMonth = new Date().getMonth(); // 0-11
|
||||
let bulkEditMode = false;
|
||||
let selectedEntries = new Set();
|
||||
|
||||
// Settings state
|
||||
let currentBundesland = 'BW'; // Default: Baden-Württemberg
|
||||
|
||||
// ============================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================
|
||||
@@ -169,58 +172,103 @@ function getEasterSunday(year) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all public holidays for Baden-Württemberg for a given year
|
||||
* Get all public holidays for a given year and Bundesland
|
||||
*/
|
||||
function getPublicHolidays(year) {
|
||||
function getPublicHolidays(year, bundesland = currentBundesland) {
|
||||
const holidays = [];
|
||||
|
||||
// Fixed holidays
|
||||
// Fixed holidays (all states)
|
||||
holidays.push({ date: new Date(year, 0, 1), name: 'Neujahr' });
|
||||
holidays.push({ date: new Date(year, 0, 6), name: 'Heilige Drei Könige' });
|
||||
holidays.push({ date: new Date(year, 4, 1), name: 'Tag der Arbeit' });
|
||||
holidays.push({ date: new Date(year, 9, 3), name: 'Tag der Deutschen Einheit' });
|
||||
holidays.push({ date: new Date(year, 10, 1), name: 'Allerheiligen' });
|
||||
holidays.push({ date: new Date(year, 11, 25), name: '1. Weihnachtstag' });
|
||||
holidays.push({ date: new Date(year, 11, 26), name: '2. Weihnachtstag' });
|
||||
|
||||
// Heilige Drei Könige (BW, BY, ST)
|
||||
if (['BW', 'BY', 'ST'].includes(bundesland)) {
|
||||
holidays.push({ date: new Date(year, 0, 6), name: 'Heilige Drei Könige' });
|
||||
}
|
||||
|
||||
// Internationaler Frauentag (BE, MV since 2023)
|
||||
if (['BE'].includes(bundesland) || (bundesland === 'MV' && year >= 2023)) {
|
||||
holidays.push({ date: new Date(year, 2, 8), name: 'Internationaler Frauentag' });
|
||||
}
|
||||
|
||||
// Weltkindertag (TH since 2019)
|
||||
if (bundesland === 'TH' && year >= 2019) {
|
||||
holidays.push({ date: new Date(year, 8, 20), name: 'Weltkindertag' });
|
||||
}
|
||||
|
||||
// Reformationstag (BB, MV, SN, ST, TH, + HB, HH, NI, SH since 2018)
|
||||
const reformationstagStates = ['BB', 'MV', 'SN', 'ST', 'TH'];
|
||||
if (year >= 2018) {
|
||||
reformationstagStates.push('HB', 'HH', 'NI', 'SH');
|
||||
}
|
||||
if (reformationstagStates.includes(bundesland)) {
|
||||
holidays.push({ date: new Date(year, 9, 31), name: 'Reformationstag' });
|
||||
}
|
||||
|
||||
// Allerheiligen (BW, BY, NW, RP, SL)
|
||||
if (['BW', 'BY', 'NW', 'RP', 'SL'].includes(bundesland)) {
|
||||
holidays.push({ date: new Date(year, 10, 1), name: 'Allerheiligen' });
|
||||
}
|
||||
|
||||
// Buß- und Bettag (only SN)
|
||||
if (bundesland === 'SN') {
|
||||
// Buß- und Bettag is the Wednesday before November 23
|
||||
let bussbettag = new Date(year, 10, 23);
|
||||
while (bussbettag.getDay() !== 3) { // 3 = Wednesday
|
||||
bussbettag.setDate(bussbettag.getDate() - 1);
|
||||
}
|
||||
bussbettag.setDate(bussbettag.getDate() - 7); // One week before
|
||||
holidays.push({ date: bussbettag, name: 'Buß- und Bettag' });
|
||||
}
|
||||
|
||||
// Easter-dependent holidays
|
||||
const easter = getEasterSunday(year);
|
||||
|
||||
// Karfreitag (Good Friday) - 2 days before Easter
|
||||
// Karfreitag (Good Friday) - 2 days before Easter (all states)
|
||||
const goodFriday = new Date(easter);
|
||||
goodFriday.setDate(easter.getDate() - 2);
|
||||
holidays.push({ date: goodFriday, name: 'Karfreitag' });
|
||||
|
||||
// Ostermontag (Easter Monday) - 1 day after Easter
|
||||
// Ostermontag (Easter Monday) - 1 day after Easter (all states)
|
||||
const easterMonday = new Date(easter);
|
||||
easterMonday.setDate(easter.getDate() + 1);
|
||||
holidays.push({ date: easterMonday, name: 'Ostermontag' });
|
||||
|
||||
// Christi Himmelfahrt (Ascension Day) - 39 days after Easter
|
||||
// Christi Himmelfahrt (Ascension Day) - 39 days after Easter (all states)
|
||||
const ascension = new Date(easter);
|
||||
ascension.setDate(easter.getDate() + 39);
|
||||
holidays.push({ date: ascension, name: 'Christi Himmelfahrt' });
|
||||
|
||||
// Pfingstmontag (Whit Monday) - 50 days after Easter
|
||||
// Pfingstmontag (Whit Monday) - 50 days after Easter (all states)
|
||||
const whitMonday = new Date(easter);
|
||||
whitMonday.setDate(easter.getDate() + 50);
|
||||
holidays.push({ date: whitMonday, name: 'Pfingstmontag' });
|
||||
|
||||
// Fronleichnam (Corpus Christi) - 60 days after Easter
|
||||
const corpusChristi = new Date(easter);
|
||||
corpusChristi.setDate(easter.getDate() + 60);
|
||||
holidays.push({ date: corpusChristi, name: 'Fronleichnam' });
|
||||
// Fronleichnam (Corpus Christi) - 60 days after Easter (BW, BY, HE, NW, RP, SL, + some communities in SN, TH)
|
||||
if (['BW', 'BY', 'HE', 'NW', 'RP', 'SL'].includes(bundesland)) {
|
||||
const corpusChristi = new Date(easter);
|
||||
corpusChristi.setDate(easter.getDate() + 60);
|
||||
holidays.push({ date: corpusChristi, name: 'Fronleichnam' });
|
||||
}
|
||||
|
||||
// Mariä Himmelfahrt (Assumption of Mary) - August 15 (BY in some communities, SL)
|
||||
if (['SL'].includes(bundesland)) {
|
||||
holidays.push({ date: new Date(year, 7, 15), name: 'Mariä Himmelfahrt' });
|
||||
}
|
||||
|
||||
return holidays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if date is a public holiday in Baden-Württemberg
|
||||
* Check if date is a public holiday
|
||||
* Returns the holiday name or null
|
||||
*/
|
||||
function getHolidayName(date) {
|
||||
const year = date.getFullYear();
|
||||
const holidays = getPublicHolidays(year);
|
||||
const holidays = getPublicHolidays(year, currentBundesland);
|
||||
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
const holiday = holidays.find(h => {
|
||||
@@ -610,6 +658,53 @@ async function deleteEntry(id) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a setting by key
|
||||
*/
|
||||
async function getSetting(key) {
|
||||
try {
|
||||
const response = await fetch(`/api/settings/${key}`);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return null; // Setting doesn't exist yet
|
||||
}
|
||||
throw new Error('Failed to get setting');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.value;
|
||||
} catch (error) {
|
||||
console.error('Error getting setting:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a setting
|
||||
*/
|
||||
async function setSetting(key, value) {
|
||||
try {
|
||||
const response = await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ key, value })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to set setting');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error setting setting:', error);
|
||||
showNotification('Fehler beim Speichern der Einstellung', 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export entries as CSV
|
||||
*/
|
||||
@@ -1502,6 +1597,107 @@ async function bulkDeleteEntries() {
|
||||
showNotification(`✓ ${deleted} Eintrag/Einträge gelöscht`, 'success');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// BUNDESLAND / SETTINGS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Handle Bundesland change
|
||||
*/
|
||||
async function handleBundeslandChange(event) {
|
||||
const newBundesland = event.target.value;
|
||||
const oldBundesland = currentBundesland;
|
||||
|
||||
// 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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Warn user if conflicts exist
|
||||
if (conflicts.length > 0) {
|
||||
const conflictList = conflicts.map(c => ` • ${c.displayDate} (${c.holidayName})`).join('\n');
|
||||
const message = `⚠️ Achtung!\n\nDie folgenden Tage werden zu Feiertagen und haben bereits Einträge:\n\n${conflictList}\n\nMöchten Sie fortfahren? Die Einträge bleiben erhalten, aber die Tage werden als Feiertage markiert.`;
|
||||
|
||||
if (!confirm(message)) {
|
||||
// Revert selection
|
||||
event.target.value = oldBundesland;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update state and save
|
||||
currentBundesland = newBundesland;
|
||||
await setSetting('bundesland', newBundesland);
|
||||
|
||||
// Reload view to show updated holidays
|
||||
await loadMonthlyView();
|
||||
|
||||
const bundeslandNames = {
|
||||
'BW': 'Baden-Württemberg',
|
||||
'BY': 'Bayern',
|
||||
'BE': 'Berlin',
|
||||
'BB': 'Brandenburg',
|
||||
'HB': 'Bremen',
|
||||
'HH': 'Hamburg',
|
||||
'HE': 'Hessen',
|
||||
'MV': 'Mecklenburg-Vorpommern',
|
||||
'NI': 'Niedersachsen',
|
||||
'NW': 'Nordrhein-Westfalen',
|
||||
'RP': 'Rheinland-Pfalz',
|
||||
'SL': 'Saarland',
|
||||
'SN': 'Sachsen',
|
||||
'ST': 'Sachsen-Anhalt',
|
||||
'SH': 'Schleswig-Holstein',
|
||||
'TH': 'Thüringen'
|
||||
};
|
||||
|
||||
showNotification(`✓ Bundesland auf ${bundeslandNames[newBundesland]} gesetzt`, 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load saved settings
|
||||
*/
|
||||
async function loadSettings() {
|
||||
const savedBundesland = await getSetting('bundesland');
|
||||
if (savedBundesland) {
|
||||
currentBundesland = savedBundesland;
|
||||
document.getElementById('bundeslandSelect').value = savedBundesland;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// INLINE EDITING
|
||||
// ============================================
|
||||
@@ -1862,6 +2058,9 @@ function initializeEventListeners() {
|
||||
document.getElementById('btnStartWork').addEventListener('click', startWork);
|
||||
document.getElementById('btnStopWork').addEventListener('click', stopWork);
|
||||
|
||||
// Bundesland selection
|
||||
document.getElementById('bundeslandSelect').addEventListener('change', handleBundeslandChange);
|
||||
|
||||
// Close modal when clicking outside
|
||||
document.getElementById('entryModal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'entryModal') {
|
||||
@@ -1879,9 +2078,10 @@ function initializeEventListeners() {
|
||||
// INITIALIZATION
|
||||
// ============================================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
initializeFlatpickr();
|
||||
initializeEventListeners();
|
||||
await loadSettings(); // Load saved settings first
|
||||
checkRunningTimer(); // Check if timer was running
|
||||
loadMonthlyView(); // Load monthly view by default
|
||||
});
|
||||
|
||||
@@ -193,6 +193,38 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Section (Collapsible) -->
|
||||
<details class="mt-4 pt-4 border-t border-gray-700">
|
||||
<summary class="cursor-pointer text-gray-300 hover:text-gray-100 font-medium text-sm flex items-center gap-2">
|
||||
<span class="text-lg">⚙️</span>
|
||||
<span>Einstellungen</span>
|
||||
</summary>
|
||||
<div class="mt-4 flex flex-wrap gap-4 items-center">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label for="bundeslandSelect" class="block text-sm font-medium text-gray-300 mb-1">Bundesland (Feiertage)</label>
|
||||
<select id="bundeslandSelect"
|
||||
class="w-full px-4 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||
<option value="BW">Baden-Württemberg</option>
|
||||
<option value="BY">Bayern</option>
|
||||
<option value="BE">Berlin</option>
|
||||
<option value="BB">Brandenburg</option>
|
||||
<option value="HB">Bremen</option>
|
||||
<option value="HH">Hamburg</option>
|
||||
<option value="HE">Hessen</option>
|
||||
<option value="MV">Mecklenburg-Vorpommern</option>
|
||||
<option value="NI">Niedersachsen</option>
|
||||
<option value="NW">Nordrhein-Westfalen</option>
|
||||
<option value="RP">Rheinland-Pfalz</option>
|
||||
<option value="SL">Saarland</option>
|
||||
<option value="SN">Sachsen</option>
|
||||
<option value="ST">Sachsen-Anhalt</option>
|
||||
<option value="SH">Schleswig-Holstein</option>
|
||||
<option value="TH">Thüringen</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
|
||||
Reference in New Issue
Block a user