diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..a256fd5 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/index.html b/public/index.html index e9d4435..8a6369e 100644 --- a/public/index.html +++ b/public/index.html @@ -5,12 +5,12 @@ Zeiterfassung + + + - - - @@ -833,6 +833,11 @@ + + + @@ -842,7 +847,26 @@ diff --git a/public/js/main.js b/public/js/main.js index abccf5b..5915f55 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -934,7 +934,7 @@ function renderEntries(entries) { }); // Reinitialize Lucide icons - if (typeof lucide !== 'undefined') { + if (typeof lucide !== 'undefined' && lucide.createIcons) { lucide.createIcons(); } @@ -1103,7 +1103,10 @@ function renderMonthlyView(entries) { const holidayName = getHolidayName(dateObj); const displayText = holidayName || 'Kein Eintrag'; - let emptyRowClass = weekend ? 'hover:bg-gray-700 bg-gray-700' : 'hover:bg-gray-700 bg-red-950/40'; + // Don't mark future days as red, only past workdays without entries + const isFutureDay = dateObj > today; + let emptyRowClass = weekend ? 'hover:bg-gray-700 bg-gray-700' : + isFutureDay ? 'hover:bg-gray-700' : 'hover:bg-gray-700 bg-red-950/40'; row.className = emptyRowClass; @@ -1151,7 +1154,7 @@ function renderMonthlyView(entries) { } // Reinitialize Lucide icons - if (typeof lucide !== 'undefined') { + if (typeof lucide !== 'undefined' && lucide.createIcons) { lucide.createIcons(); } @@ -1467,7 +1470,7 @@ async function updateVacationStatistics() { // Update UI with dynamic year const vacationLabel = document.getElementById('vacationYearLabel'); vacationLabel.innerHTML = ` Urlaub ${currentYear}`; - if (typeof lucide !== 'undefined') { + if (typeof lucide !== 'undefined' && lucide.createIcons) { lucide.createIcons(); } document.getElementById('statVacationTaken').textContent = vacationTaken; @@ -1649,7 +1652,7 @@ function updateLocationButtons(location) { } // Reinitialize Lucide icons after DOM update - if (typeof lucide !== 'undefined') { + if (typeof lucide !== 'undefined' && lucide.createIcons) { lucide.createIcons(); } } @@ -2029,6 +2032,182 @@ async function bulkDeleteEntries() { toggleBulkEditMode(); // Close bulk edit mode } +/** + * Generate PDF with common template + * @param {Object} options - PDF generation options + */ +async function generatePDF(options) { + const { + title = 'Zeiterfassung', + subtitle, + tableData, + statistics, + additionalInfo = {}, + fileName + } = options; + + const { targetHours, totalNetHours, balance } = statistics; + const { vacationDays = 0, flextimeDays = 0 } = additionalInfo; + + // Get employee data from settings + const employeeName = await getSetting('employeeName') || ''; + const employeeId = await getSetting('employeeId') || ''; + + // Get jsPDF from window + const { jsPDF } = window.jspdf; + + // Create PDF + const doc = new jsPDF('p', 'mm', 'a4'); + + // Header with statistics + doc.setFillColor(15, 23, 42); + doc.rect(0, 0, 210, 35, 'F'); + + // Title and subtitle + doc.setTextColor(255, 255, 255); + doc.setFontSize(16); + doc.setFont(undefined, 'bold'); + doc.text(title, 15, 12, { align: 'left' }); + + doc.setFontSize(11); + doc.setFont(undefined, 'normal'); + doc.text(subtitle, 195, 12, { align: 'right' }); + + // Employee info in second line + if (employeeName || employeeId) { + doc.setFontSize(8); + doc.setTextColor(200, 200, 200); + let employeeInfo = ''; + if (employeeName) employeeInfo += employeeName; + if (employeeId) { + if (employeeInfo) employeeInfo += ' | '; + employeeInfo += `Personal-Nr. ${employeeId}`; + } + doc.text(employeeInfo, 15, 19, { align: 'left' }); + } + + // Statistics - three columns in one line + const statsY = employeeName || employeeId ? 28 : 22; + + // Soll + doc.setTextColor(180, 180, 180); + doc.setFontSize(7); + doc.text('SOLL-STUNDEN', 40, statsY - 3, { align: 'center' }); + doc.setTextColor(255, 255, 255); + doc.setFontSize(11); + doc.setFont(undefined, 'bold'); + doc.text(`${targetHours.toFixed(1)}h`, 40, statsY + 3, { align: 'center' }); + + // Ist + doc.setTextColor(180, 180, 180); + doc.setFontSize(7); + doc.setFont(undefined, 'normal'); + doc.text('IST-STUNDEN', 105, statsY - 3, { align: 'center' }); + doc.setTextColor(255, 255, 255); + doc.setFontSize(11); + doc.setFont(undefined, 'bold'); + doc.text(`${totalNetHours.toFixed(1)}h`, 105, statsY + 3, { align: 'center' }); + + // Saldo + doc.setTextColor(180, 180, 180); + doc.setFontSize(7); + doc.setFont(undefined, 'normal'); + doc.text('SALDO', 170, statsY - 3, { align: 'center' }); + if (balance >= 0) { + doc.setTextColor(34, 197, 94); + } else { + doc.setTextColor(239, 68, 68); + } + doc.setFontSize(11); + doc.setFont(undefined, 'bold'); + doc.text(`${balance >= 0 ? '+' : ''}${balance.toFixed(1)}h`, 170, statsY + 3, { align: 'center' }); + + // Additional info if needed (small, far right) + if (vacationDays > 0 || flextimeDays > 0) { + doc.setTextColor(150, 150, 150); + doc.setFontSize(7); + doc.setFont(undefined, 'normal'); + let infoText = ''; + if (vacationDays > 0) infoText += `Urlaub: ${vacationDays}`; + if (flextimeDays > 0) { + if (infoText) infoText += ' '; + infoText += `Gleitzeit: ${flextimeDays}`; + } + doc.text(infoText, 195, statsY + 3, { align: 'right' }); + } + + // Table starts after header + let yPos = 37; + + // Generate table + doc.autoTable({ + startY: yPos, + head: [['Datum', 'Tag', 'Beginn', 'Ende', 'Pause', 'Typ', 'Netto', 'Abw.']], + body: tableData, + theme: 'grid', + tableWidth: 'auto', + headStyles: { + fillColor: [30, 41, 59], + textColor: [255, 255, 255], + fontSize: 9, + fontStyle: 'bold', + halign: 'center', + cellPadding: 2.5, + minCellHeight: 7 + }, + bodyStyles: { + fillColor: [248, 250, 252], + textColor: [15, 23, 42], + fontSize: 8, + cellPadding: 2, + minCellHeight: 6 + }, + alternateRowStyles: { + fillColor: [241, 245, 249] + }, + columnStyles: { + 0: { halign: 'center', cellWidth: 24 }, // Datum + 1: { halign: 'center', cellWidth: 14 }, // Tag + 2: { halign: 'center', cellWidth: 18 }, // Beginn + 3: { halign: 'center', cellWidth: 18 }, // Ende + 4: { halign: 'center', cellWidth: 18 }, // Pause + 5: { halign: 'center', cellWidth: 26 }, // Ort + 6: { halign: 'center', cellWidth: 18 }, // Netto + 7: { halign: 'center', cellWidth: 18 } // Abweichung + }, + didParseCell: function(data) { + if (data.column.index === 7 && data.section === 'body') { + const value = data.cell.raw; + if (value.startsWith('+')) { + data.cell.styles.textColor = [34, 197, 94]; + data.cell.styles.fontStyle = 'bold'; + } else if (value.startsWith('-') && value !== '-') { + data.cell.styles.textColor = [239, 68, 68]; + data.cell.styles.fontStyle = 'bold'; + } + } + }, + margin: { left: 20, right: 20 } // Smaller margins for wider table + }); + + // Footer + const finalY = doc.lastAutoTable.finalY || yPos + 50; + if (finalY < 270) { + doc.setTextColor(156, 163, 175); + doc.setFontSize(8); + doc.text(`Erstellt am: ${new Date().toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + })}`, 105, 285, { align: 'center' }); + } + + // Save PDF + doc.save(fileName); +} + /** * Bulk export selected entries as PDF */ @@ -2143,82 +2322,7 @@ async function bulkExportPDF() { const targetHours = workdaysPassed * 8; const balance = totalNetHours - targetHours; - // Create PDF - const doc = new jsPDF('p', 'mm', 'a4'); - - // Header with gradient effect - doc.setFillColor(15, 23, 42); - doc.rect(0, 0, 210, 35, 'F'); - - doc.setTextColor(255, 255, 255); - doc.setFontSize(22); - doc.setFont(undefined, 'bold'); - doc.text('Zeiterfassung', 105, 18, { align: 'center' }); - - doc.setFontSize(13); - doc.setFont(undefined, 'normal'); - doc.text(dateRange, 105, 27, { align: 'center' }); - - // Statistics box - centered and styled - let yPos = 43; - doc.setFillColor(30, 41, 59); - doc.roundedRect(20, yPos, 170, 38, 3, 3, 'F'); - - doc.setTextColor(156, 163, 175); - doc.setFontSize(9); - - // Employee info (if available) - if (employeeName || employeeId) { - let employeeInfo = ''; - if (employeeName) employeeInfo += `Mitarbeiter: ${employeeName}`; - if (employeeId) { - if (employeeInfo) employeeInfo += ' | '; - employeeInfo += `Personal-Nr.: ${employeeId}`; - } - doc.text(employeeInfo, 105, yPos + 8, { align: 'center' }); - yPos += 5; - } - - // Statistics - centered layout with three columns - const col1X = 45; - const col2X = 105; - const col3X = 165; - - doc.text('Soll-Stunden', col1X, yPos + 12, { align: 'center' }); - doc.text('Ist-Stunden', col2X, yPos + 12, { align: 'center' }); - doc.text('Saldo', col3X, yPos + 12, { align: 'center' }); - - doc.setTextColor(255, 255, 255); - doc.setFontSize(16); - doc.setFont(undefined, 'bold'); - doc.text(`${targetHours.toFixed(1)}h`, col1X, yPos + 22, { align: 'center' }); - doc.text(`${totalNetHours.toFixed(1)}h`, col2X, yPos + 22, { align: 'center' }); - - if (balance >= 0) { - doc.setTextColor(34, 197, 94); - } else { - doc.setTextColor(239, 68, 68); - } - doc.text(`${balance >= 0 ? '+' : ''}${balance.toFixed(1)}h`, col3X, yPos + 22, { align: 'center' }); - - // Additional info - if (vacationDays > 0 || flextimeDays > 0) { - yPos += 30; - doc.setTextColor(156, 163, 175); - doc.setFontSize(8); - let infoText = ''; - if (vacationDays > 0) infoText += `Urlaubstage: ${vacationDays}`; - if (flextimeDays > 0) { - if (infoText) infoText += ' | '; - infoText += `Gleittage: ${flextimeDays}`; - } - doc.text(infoText, 105, yPos + 8, { align: 'center' }); - yPos += 13; - } else { - yPos += 43; - } - - // Table data - include all days in range (including weekends/holidays) + // Build table data const allDaysData = []; const entriesMap = new Map(selectedEntriesData.map(e => [e.date, e])); @@ -2258,7 +2362,7 @@ async function bulkExportPDF() { endTime = '-'; pauseText = '-'; } else { - locationText = entry.location === 'home' ? 'Home' : 'Büro'; + locationText = entry.location === 'home' ? 'Home' : 'Office'; } allDaysData.push([ @@ -2300,74 +2404,16 @@ async function bulkExportPDF() { currentDate.setDate(currentDate.getDate() + 1); } - const tableData = allDaysData; - - doc.autoTable({ - startY: yPos, - head: [['Datum', 'Tag', 'Beginn', 'Ende', 'Pause', 'Ort', 'Netto', 'Abw.']], - body: tableData, - theme: 'grid', - headStyles: { - fillColor: [30, 41, 59], - textColor: [255, 255, 255], - fontSize: 9, - fontStyle: 'bold', - halign: 'center', - cellPadding: 3 - }, - bodyStyles: { - fillColor: [248, 250, 252], - textColor: [15, 23, 42], - fontSize: 8, - cellPadding: 2.5 - }, - alternateRowStyles: { - fillColor: [241, 245, 249] - }, - columnStyles: { - 0: { halign: 'center', cellWidth: 24 }, // Datum - 1: { halign: 'center', cellWidth: 14 }, // Wochentag - 2: { halign: 'center', cellWidth: 18 }, // Beginn - 3: { halign: 'center', cellWidth: 18 }, // Ende - 4: { halign: 'center', cellWidth: 18 }, // Pause - 5: { halign: 'center', cellWidth: 28 }, // Ort - 6: { halign: 'center', cellWidth: 18 }, // Netto - 7: { halign: 'center', cellWidth: 18 } // Abweichung - }, - didParseCell: function(data) { - if (data.column.index === 7 && data.section === 'body') { - const value = data.cell.raw; - if (value.startsWith('+')) { - data.cell.styles.textColor = [34, 197, 94]; - data.cell.styles.fontStyle = 'bold'; - } else if (value.startsWith('-') && value !== '-') { - data.cell.styles.textColor = [239, 68, 68]; - data.cell.styles.fontStyle = 'bold'; - } - } - }, - // Calculate margins to center the table - // Total column width: 156mm, page width: 210mm, so we need (210-156)/2 = 27mm margins - margin: { left: 27, right: 27 } - }); - - // Footer - const finalY = doc.lastAutoTable.finalY || yPos + 50; - if (finalY < 270) { - doc.setTextColor(156, 163, 175); - doc.setFontSize(8); - doc.text(`Erstellt am: ${new Date().toLocaleDateString('de-DE', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - })}`, 105, 285, { align: 'center' }); - } - - // Save PDF + // Generate PDF using common template const fileName = `Zeiterfassung_${selectedEntriesData[0].date}_${selectedEntriesData[selectedEntriesData.length - 1].date}.pdf`; - doc.save(fileName); + await generatePDF({ + title: 'Zeiterfassung', + subtitle: dateRange, + tableData: allDaysData, + statistics: { targetHours, totalNetHours, balance }, + additionalInfo: { vacationDays, flextimeDays }, + fileName + }); showNotification(`PDF mit ${allDaysData.length} Tag(en) erstellt`, 'success'); } catch (error) { @@ -3072,87 +3118,7 @@ async function handleExportPDF() { const previousBalance = await calculatePreviousBalance(year, month); const totalBalance = monthBalance + previousBalance; - // Create PDF - const doc = new jsPDF('p', 'mm', 'a4'); - - // Get employee data from settings - const employeeName = await getSetting('employeeName') || ''; - const employeeId = await getSetting('employeeId') || ''; - - // Header with gradient effect - doc.setFillColor(15, 23, 42); - doc.rect(0, 0, 210, 35, 'F'); - - doc.setTextColor(255, 255, 255); - doc.setFontSize(22); - doc.setFont(undefined, 'bold'); - doc.text('Zeiterfassung', 105, 18, { align: 'center' }); - - doc.setFontSize(13); - doc.setFont(undefined, 'normal'); - doc.text(monthName, 105, 27, { align: 'center' }); - - // Statistics box - centered and styled - let yPos = 43; - doc.setFillColor(30, 41, 59); - doc.roundedRect(20, yPos, 170, 38, 3, 3, 'F'); - - doc.setTextColor(156, 163, 175); - doc.setFontSize(9); - - // Employee info (if available) - if (employeeName || employeeId) { - let employeeInfo = ''; - if (employeeName) employeeInfo += `Mitarbeiter: ${employeeName}`; - if (employeeId) { - if (employeeInfo) employeeInfo += ' | '; - employeeInfo += `Personal-Nr.: ${employeeId}`; - } - doc.text(employeeInfo, 105, yPos + 8, { align: 'center' }); - yPos += 5; - } - - // Statistics - centered layout with three columns - const col1X = 45; - const col2X = 105; - const col3X = 165; - - doc.text('Soll-Stunden', col1X, yPos + 12, { align: 'center' }); - doc.text('Ist-Stunden', col2X, yPos + 12, { align: 'center' }); - doc.text('Saldo', col3X, yPos + 12, { align: 'center' }); - - doc.setTextColor(255, 255, 255); - doc.setFontSize(16); - doc.setFont(undefined, 'bold'); - doc.text(`${targetHours.toFixed(1)}h`, col1X, yPos + 22, { align: 'center' }); - doc.text(`${totalNetHours.toFixed(1)}h`, col2X, yPos + 22, { align: 'center' }); - - if (monthBalance >= 0) { - doc.setTextColor(34, 197, 94); - } else { - doc.setTextColor(239, 68, 68); - } - doc.text(`${monthBalance >= 0 ? '+' : ''}${monthBalance.toFixed(1)}h`, col3X, yPos + 22, { align: 'center' }); - - // Additional info if vacation or flextime days exist - if (vacationDays > 0 || flextimeDays > 0) { - yPos += 30; - doc.setTextColor(156, 163, 175); - doc.setFontSize(8); - let infoText = ''; - if (vacationDays > 0) infoText += `Urlaubstage: ${vacationDays}`; - if (flextimeDays > 0) { - if (infoText) infoText += ' | '; - infoText += `Gleittage: ${flextimeDays}`; - } - doc.text(infoText, 105, yPos + 8, { align: 'center' }); - yPos += 13; - } else { - yPos += 43; - } - - // Table with entries - // Create a complete list of all days in the month including weekends/holidays + // Build table data const allDaysData = []; const entriesMap = new Map(entries.map(e => [e.date, e])); @@ -3192,7 +3158,7 @@ async function handleExportPDF() { endTime = '-'; pauseText = '-'; } else { - locationText = entry.location === 'home' ? 'Home' : 'Büro'; + locationText = entry.location === 'home' ? 'Home' : 'Office'; } allDaysData.push([ @@ -3228,78 +3194,21 @@ async function handleExportPDF() { '-' ]); } - // Skip regular workdays without entries (not shown in PDF) + // Skip regular workdays without entries } - const tableData = allDaysData; - - doc.autoTable({ - startY: yPos, - head: [['Datum', 'Tag', 'Beginn', 'Ende', 'Pause', 'Ort', 'Netto', 'Abw.']], - body: tableData, - theme: 'grid', - headStyles: { - fillColor: [30, 41, 59], - textColor: [255, 255, 255], - fontSize: 9, - fontStyle: 'bold', - halign: 'center', - cellPadding: 3 - }, - bodyStyles: { - fillColor: [248, 250, 252], - textColor: [15, 23, 42], - fontSize: 8, - cellPadding: 2.5 - }, - alternateRowStyles: { - fillColor: [241, 245, 249] - }, - columnStyles: { - 0: { halign: 'center', cellWidth: 24 }, // Datum - 1: { halign: 'center', cellWidth: 14 }, // Wochentag - 2: { halign: 'center', cellWidth: 18 }, // Beginn - 3: { halign: 'center', cellWidth: 18 }, // Ende - 4: { halign: 'center', cellWidth: 18 }, // Pause - 5: { halign: 'center', cellWidth: 28 }, // Ort - 6: { halign: 'center', cellWidth: 18 }, // Netto - 7: { halign: 'center', cellWidth: 18 } // Abweichung - }, - didParseCell: function(data) { - // Color code deviations in the last column - if (data.column.index === 7 && data.section === 'body') { - const value = data.cell.raw; - if (value.startsWith('+')) { - data.cell.styles.textColor = [34, 197, 94]; // green - data.cell.styles.fontStyle = 'bold'; - } else if (value.startsWith('-') && value !== '-') { - data.cell.styles.textColor = [239, 68, 68]; // red - data.cell.styles.fontStyle = 'bold'; - } - } - }, - margin: { left: 15, right: 15 } + // Generate PDF using common template + const fileName = `Zeiterfassung_${monthName.replace(' ', '_')}.pdf`; + await generatePDF({ + title: 'Zeiterfassung', + subtitle: monthName, + tableData: allDaysData, + statistics: { targetHours, totalNetHours, balance: monthBalance }, + additionalInfo: { vacationDays, flextimeDays }, + fileName }); - // Footer with generation date - const finalY = doc.lastAutoTable.finalY || yPos + 50; - if (finalY < 270) { - doc.setTextColor(156, 163, 175); - doc.setFontSize(8); - doc.text(`Erstellt am: ${new Date().toLocaleDateString('de-DE', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - })}`, 105, 285, { align: 'center' }); - } - - // Save PDF - const fileName = `Zeiterfassung_${monthName.replace(' ', '_')}.pdf`; - doc.save(fileName); - - showNotification('PDF erfolgreich erstellt', 'success'); + showNotification(`PDF für ${monthName} erstellt`, 'success'); } catch (error) { console.error('Error exporting PDF:', error); showNotification('Fehler beim PDF-Export', 'error');