feat: enhance PDF generation and improve Lucide icon initialization

This commit is contained in:
Felix Schlusche
2025-10-24 14:41:10 +02:00
parent af23aa369c
commit 1cc8dc3b6c
3 changed files with 267 additions and 303 deletions

View File

@@ -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 = `<i data-lucide="plane" class="w-4 h-4"></i> 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');