/** * Main Application * Manages UI interactions and coordinates between components */ class DienstplanApp { constructor() { this.storage = new DataStorage(); this.holidayProvider = new HolidayProvider(); this.calculator = new BonusCalculator(this.holidayProvider); this.currentMonth = new Date().getMonth() + 1; this.currentYear = new Date().getFullYear(); this.init(); } init() { this.setupEventListeners(); this.populateYearSelects(); this.setCurrentMonthYear(); this.loadEmployeeSelects(); this.loadEmployeeList(); this.switchTab('duties'); } /** * Setup all event listeners */ setupEventListeners() { // Tab switching document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', (e) => { this.switchTab(e.target.dataset.tab); }); }); // Employee management document.getElementById('add-employee-btn').addEventListener('click', () => this.addEmployee()); document.getElementById('new-employee-name').addEventListener('keypress', (e) => { if (e.key === 'Enter') this.addEmployee(); }); // Duty management document.getElementById('add-duty-btn').addEventListener('click', () => this.addDuty()); document.getElementById('employee-select-duty').addEventListener('change', () => this.loadDutiesForSelectedEmployee()); document.getElementById('month-select').addEventListener('change', () => this.loadDutiesForSelectedEmployee()); document.getElementById('year-select').addEventListener('change', () => this.loadDutiesForSelectedEmployee()); // Calculation document.getElementById('calculate-btn').addEventListener('click', () => this.calculateBonuses()); // Settings document.getElementById('export-csv-btn').addEventListener('click', () => this.exportCSV()); document.getElementById('export-btn').addEventListener('click', () => this.exportData()); document.getElementById('import-btn').addEventListener('click', () => this.importData()); document.getElementById('clear-all-btn').addEventListener('click', () => this.clearAllData()); } /** * Populate year select dropdowns */ populateYearSelects() { const currentYear = new Date().getFullYear(); const years = []; for (let year = currentYear - 1; year <= currentYear + 5; year++) { years.push(year); } const yearSelects = ['year-select', 'calc-year-select']; yearSelects.forEach(selectId => { const select = document.getElementById(selectId); select.innerHTML = ''; years.forEach(year => { const option = document.createElement('option'); option.value = year; option.textContent = year; if (year === currentYear) option.selected = true; select.appendChild(option); }); }); } /** * Set current month and year in selects */ setCurrentMonthYear() { const currentMonth = new Date().getMonth() + 1; const currentYear = new Date().getFullYear(); document.getElementById('month-select').value = currentMonth; document.getElementById('year-select').value = currentYear; document.getElementById('calc-month-select').value = currentMonth; document.getElementById('calc-year-select').value = currentYear; // Set date input to today const today = new Date().toISOString().split('T')[0]; document.getElementById('duty-date').value = today; } /** * Switch between tabs */ switchTab(tabName) { // Update tab buttons document.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.remove('active'); if (btn.dataset.tab === tabName) { btn.classList.add('active'); } }); // Update tab content document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.getElementById(`tab-${tabName}`).classList.add('active'); // Refresh data when switching to certain tabs if (tabName === 'employees') { this.loadEmployeeList(); } else if (tabName === 'duties') { this.loadDutiesForSelectedEmployee(); } } /** * Load employee select dropdowns */ loadEmployeeSelects() { const employees = this.storage.getEmployees(); const selects = ['employee-select-duty']; selects.forEach(selectId => { const select = document.getElementById(selectId); const currentValue = select.value; select.innerHTML = ''; employees.forEach(employee => { const option = document.createElement('option'); option.value = employee; option.textContent = employee; select.appendChild(option); }); // Restore previous selection if still valid if (employees.includes(currentValue)) { select.value = currentValue; } }); } /** * Add a new employee */ addEmployee() { const input = document.getElementById('new-employee-name'); const name = input.value.trim(); if (!name) { this.showToast('Bitte geben Sie einen Namen ein.', 'error'); return; } const success = this.storage.addEmployee(name); if (success) { this.showToast(`Mitarbeiter "${name}" wurde hinzugefügt.`, 'success'); input.value = ''; this.loadEmployeeList(); this.loadEmployeeSelects(); } else { this.showToast(`Mitarbeiter "${name}" existiert bereits.`, 'error'); } } /** * Remove an employee */ removeEmployee(employeeName) { if (!confirm(`Möchten Sie "${employeeName}" wirklich löschen? Alle Dienste werden ebenfalls gelöscht.`)) { return; } this.storage.removeEmployee(employeeName); this.showToast(`Mitarbeiter "${employeeName}" wurde gelöscht.`, 'success'); this.loadEmployeeList(); this.loadEmployeeSelects(); this.loadDutiesForSelectedEmployee(); } /** * Load and display employee list */ loadEmployeeList() { const employees = this.storage.getEmployees(); const container = document.getElementById('employee-list-display'); if (employees.length === 0) { container.innerHTML = '

Keine Mitarbeiter vorhanden.

'; return; } container.innerHTML = ''; employees.forEach(employee => { const item = document.createElement('div'); item.className = 'employee-item'; item.innerHTML = ` ${employee} `; container.appendChild(item); }); } /** * Add a duty */ addDuty() { const employeeSelect = document.getElementById('employee-select-duty'); const dateInput = document.getElementById('duty-date'); const shareSelect = document.getElementById('duty-share'); const employeeName = employeeSelect.value; const dateStr = dateInput.value; const share = parseFloat(shareSelect.value); if (!employeeName) { this.showToast('Bitte wählen Sie einen Mitarbeiter aus.', 'error'); return; } if (!dateStr) { this.showToast('Bitte wählen Sie ein Datum aus.', 'error'); return; } const date = new Date(dateStr + 'T12:00:00'); // Add time to avoid timezone issues const year = date.getFullYear(); const month = date.getMonth() + 1; this.storage.addDuty(employeeName, year, month, date, share); this.showToast('Dienst wurde hinzugefügt.', 'success'); this.loadDutiesForSelectedEmployee(); // Update month/year selects to match the added duty document.getElementById('month-select').value = month; document.getElementById('year-select').value = year; } /** * Remove a duty */ removeDuty(employeeName, year, month, date) { this.storage.removeDuty(employeeName, year, month, date); this.showToast('Dienst wurde gelöscht.', 'success'); this.loadDutiesForSelectedEmployee(); } /** * Load duties for the selected employee and month */ loadDutiesForSelectedEmployee() { const employeeSelect = document.getElementById('employee-select-duty'); const monthSelect = document.getElementById('month-select'); const yearSelect = document.getElementById('year-select'); const container = document.getElementById('duties-display'); const employeeName = employeeSelect.value; const month = parseInt(monthSelect.value); const year = parseInt(yearSelect.value); if (!employeeName) { container.innerHTML = '

Wählen Sie einen Mitarbeiter aus, um Dienste anzuzeigen.

'; return; } const duties = this.storage.getDutiesForMonth(employeeName, year, month); if (duties.length === 0) { const monthNames = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; container.innerHTML = `

Keine Dienste für ${monthNames[month - 1]} ${year}.

`; return; } container.innerHTML = ''; duties.forEach(duty => { const isQualifying = this.calculator.isQualifyingDay(duty.date); const dayType = this.calculator.getDayTypeLabel(duty.date); const dateStr = duty.date.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric' }); const item = document.createElement('div'); item.className = `duty-item ${isQualifying ? 'qualifying' : ''}`; item.innerHTML = `
${dateStr}
${dayType} ${isQualifying ? 'WE/Feiertag' : 'Normal'}
${duty.share === 1 ? 'Ganzer Dienst' : 'Halber Dienst'}
`; container.appendChild(item); }); } /** * Calculate bonuses for all employees */ calculateBonuses() { const monthSelect = document.getElementById('calc-month-select'); const yearSelect = document.getElementById('calc-year-select'); const resultsContainer = document.getElementById('calculation-results'); const month = parseInt(monthSelect.value); const year = parseInt(yearSelect.value); const employeeDuties = this.storage.getAllEmployeeDutiesForMonth(year, month); const results = this.calculator.calculateAllEmployees(employeeDuties); const monthNames = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; resultsContainer.innerHTML = `

Ergebnisse für ${monthNames[month - 1]} ${year}

`; const employees = Object.keys(results); if (employees.length === 0) { resultsContainer.innerHTML += '

Keine Daten verfügbar.

'; return; } employees.forEach(employeeName => { const result = results[employeeName]; const resultCard = this.createResultCard(employeeName, result); resultsContainer.appendChild(resultCard); }); this.showToast('Berechnung abgeschlossen.', 'success'); } /** * Create a result card for an employee */ createResultCard(employeeName, result) { const card = document.createElement('div'); card.className = 'result-card'; let content = `

${employeeName}

`; if (!result.thresholdReached) { content += `

Schwellenwert nicht erreicht

Es wurden nur ${result.qualifyingDays.toFixed(1)} qualifizierende Tage gearbeitet. Mindestens ${this.calculator.MIN_QUALIFYING_DAYS} Tage erforderlich.

Keine Bonuszahlung

`; } else { content += `
Normale Tage
${result.normalDays.toFixed(1)}
WE/Feiertag Tage
${result.qualifyingDays.toFixed(1)}
Abzug
-${result.qualifyingDaysDeducted.toFixed(1)}
Normale Tage (bezahlt)
${result.normalDaysPaid.toFixed(1)}
WE/Feiertag (bezahlt)
${result.qualifyingDaysPaid.toFixed(1)}
Normale Tage (250€)
${this.calculator.formatCurrency(result.bonusNormalDays)}
WE/Feiertag (450€)
${this.calculator.formatCurrency(result.bonusQualifyingDays)}

Gesamtbonus

${this.calculator.formatCurrency(result.totalBonus)}
`; } card.innerHTML = content; return card; } /** * Export data as JSON */ exportData() { const data = this.storage.exportData(); const blob = new Blob([data], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `dienstplan-export-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.showToast('Daten wurden exportiert.', 'success'); } /** * Export data as CSV (Excel-compatible) - Beginner-friendly format * Exports all duties and monthly summary for the selected month */ exportCSV() { const monthNames = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']; const month = parseInt(document.getElementById('calc-month-select').value); const year = parseInt(document.getElementById('calc-year-select').value); // Helper function to escape CSV values (handles semicolons, quotes, newlines) const escapeCSV = (value) => { const str = String(value); if (str.includes(';') || str.includes('"') || str.includes('\n')) { return '"' + str.replace(/"/g, '""') + '"'; } return str; }; // Build CSV content with BOM for Excel UTF-8 support let csv = '\uFEFF'; // UTF-8 BOM for Excel // === Sheet 1: Dienste (All Duties for the month) === csv += `DIENSTE ${monthNames[month - 1]} ${year}\n`; csv += 'Datum;Wochentag;Mitarbeiter;Anteil;Tagestyp\n'; const employees = this.storage.getEmployees(); const allDuties = []; // Collect all duties for the selected month from all employees employees.forEach(employee => { const duties = this.storage.getDutiesForMonth(employee, year, month); duties.forEach(duty => { allDuties.push({ ...duty, employee: employee }); }); }); // Sort by date allDuties.sort((a, b) => a.date - b.date); allDuties.forEach(duty => { const isQual = this.calculator.isQualifyingDay(duty.date); const dateStr = duty.date.toLocaleDateString('de-DE'); const weekday = weekdays[duty.date.getDay()]; const dayType = isQual ? 'WE-Tag' : 'Werktag (WT)'; csv += `${dateStr};${weekday};${escapeCSV(duty.employee)};${duty.share.toFixed(1).replace('.', ',')};${dayType}\n`; }); csv += '\n\n'; // === Sheet 2: Monatliche Auswertung === csv += `AUSWERTUNG ${monthNames[month - 1]} ${year}\n`; csv += 'Mitarbeiter;Normale Tage;WE/Feiertag Tage;Abzug;Normale Tage (bezahlt);WE/Feiertag (bezahlt);Schwelle erreicht;Bonus Normal;Bonus WE;Gesamtbonus (EUR)\n'; const employeeDuties = this.storage.getAllEmployeeDutiesForMonth(year, month); const results = this.calculator.calculateAllEmployees(employeeDuties); let totalBonus = 0; for (const [employeeName, result] of Object.entries(results)) { const threshold = result.thresholdReached ? 'JA' : 'NEIN'; totalBonus += result.totalBonus; csv += `${escapeCSV(employeeName)};`; csv += `${result.normalDays.toFixed(1).replace('.', ',')};`; csv += `${result.qualifyingDays.toFixed(1).replace('.', ',')};`; csv += `${result.qualifyingDaysDeducted.toFixed(1).replace('.', ',')};`; csv += `${result.normalDaysPaid.toFixed(1).replace('.', ',')};`; csv += `${result.qualifyingDaysPaid.toFixed(1).replace('.', ',')};`; csv += `${threshold};`; csv += `${result.bonusNormalDays.toFixed(2).replace('.', ',')};`; csv += `${result.bonusQualifyingDays.toFixed(2).replace('.', ',')};`; csv += `${result.totalBonus.toFixed(2).replace('.', ',')}\n`; } csv += `\nGESAMT;;;;;;;;;${totalBonus.toFixed(2).replace('.', ',')}\n`; csv += '\n\n'; csv += 'LEGENDE\n'; csv += 'Normale Tage;Montag-Donnerstag ohne Feiertag/Vortag\n'; csv += 'WE/Feiertag Tage;"Freitag, Samstag, Sonntag, Feiertag oder Tag vor Feiertag"\n'; csv += 'Schwelle;"Mindestens 2,0 WE-Einheiten für Bonuszahlung erforderlich"\n'; csv += 'Sätze;"Normale Tage = 250 EUR/Einheit, WE/Feiertag = 450 EUR/Einheit"\n'; csv += 'Abzug;"Bei Erreichen der Schwelle wird 1,0 WE-Einheit abgezogen"\n'; // Download CSV file const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `Dienstplan_${year}_${String(month).padStart(2, '0')}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.showToast('CSV wurde exportiert. Öffnen Sie die Datei mit Excel oder LibreOffice.', 'success'); } /** * Import data from JSON file */ importData() { const fileInput = document.getElementById('import-file'); const file = fileInput.files[0]; if (!file) { this.showToast('Bitte wählen Sie eine Datei aus.', 'error'); return; } const reader = new FileReader(); reader.onload = (e) => { const success = this.storage.importData(e.target.result); if (success) { this.showToast('Daten wurden erfolgreich importiert.', 'success'); this.loadEmployeeList(); this.loadEmployeeSelects(); this.loadDutiesForSelectedEmployee(); } else { this.showToast('Import fehlgeschlagen. Bitte überprüfen Sie die Datei.', 'error'); } }; reader.readAsText(file); fileInput.value = ''; // Reset file input } /** * Clear all data */ clearAllData() { if (!confirm('Möchten Sie wirklich ALLE Daten löschen? Diese Aktion kann nicht rückgängig gemacht werden!')) { return; } this.storage.clearAll(); this.showToast('Alle Daten wurden gelöscht.', 'info'); this.loadEmployeeList(); this.loadEmployeeSelects(); this.loadDutiesForSelectedEmployee(); } /** * Show toast notification */ showToast(message, type = 'info') { const toast = document.getElementById('toast'); toast.textContent = message; toast.className = `toast ${type}`; setTimeout(() => { toast.classList.add('show'); }, 100); setTimeout(() => { toast.classList.remove('show'); }, 3000); } } // Initialize app when DOM is ready let app; document.addEventListener('DOMContentLoaded', () => { app = new DienstplanApp(); });