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

31
public/favicon.svg Normal file
View File

@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<!-- Background gradient -->
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1e40af;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Rounded square background -->
<rect width="100" height="100" rx="20" fill="url(#grad)"/>
<!-- Clock circle -->
<circle cx="50" cy="50" r="30" fill="none" stroke="white" stroke-width="3"/>
<!-- Clock hands -->
<!-- Hour hand (pointing to 10) -->
<line x1="50" y1="50" x2="50" y2="32" stroke="white" stroke-width="4" stroke-linecap="round"/>
<!-- Minute hand (pointing to 2) -->
<line x1="50" y1="50" x2="64" y2="42" stroke="white" stroke-width="3" stroke-linecap="round"/>
<!-- Center dot -->
<circle cx="50" cy="50" r="3" fill="white"/>
<!-- Clock markers (12, 3, 6, 9) -->
<circle cx="50" cy="23" r="2" fill="white"/>
<circle cx="77" cy="50" r="2" fill="white"/>
<circle cx="50" cy="77" r="2" fill="white"/>
<circle cx="23" cy="50" r="2" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -5,12 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zeiterfassung</title> <title>Zeiterfassung</title>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<!-- Tailwind CSS via CDN --> <!-- Tailwind CSS via CDN -->
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<!-- Flatpickr CSS --> <!-- Flatpickr CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
@@ -833,6 +833,11 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script>
<!-- Lucide Icons - Try multiple CDN sources -->
<script src="https://unpkg.com/lucide@0.294.0/dist/umd/lucide.js"
onerror="console.error('Failed to load Lucide from unpkg'); this.onerror=null; this.src='https://cdn.jsdelivr.net/npm/lucide@0.294.0/dist/umd/lucide.js'">
</script>
<!-- App Logic - Load in correct order --> <!-- App Logic - Load in correct order -->
<script src="js/state.js"></script> <script src="js/state.js"></script>
<script src="js/utils.js"></script> <script src="js/utils.js"></script>
@@ -842,7 +847,26 @@
<!-- Initialize Lucide Icons --> <!-- Initialize Lucide Icons -->
<script> <script>
lucide.createIcons(); // Use a more robust initialization
function initLucide() {
console.log('Checking lucide...', typeof lucide, window.lucide);
if (typeof lucide !== 'undefined' && lucide.createIcons) {
console.log('Initializing Lucide icons...');
lucide.createIcons();
} else if (window.lucide && window.lucide.createIcons) {
console.log('Initializing Lucide icons from window...');
window.lucide.createIcons();
} else {
console.error('Lucide still not available after all attempts');
}
}
// Try multiple times
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initLucide);
} else {
setTimeout(initLucide, 100);
}
</script> </script>
</body> </body>
</html> </html>

View File

@@ -934,7 +934,7 @@ function renderEntries(entries) {
}); });
// Reinitialize Lucide icons // Reinitialize Lucide icons
if (typeof lucide !== 'undefined') { if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons(); lucide.createIcons();
} }
@@ -1103,7 +1103,10 @@ function renderMonthlyView(entries) {
const holidayName = getHolidayName(dateObj); const holidayName = getHolidayName(dateObj);
const displayText = holidayName || 'Kein Eintrag'; 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; row.className = emptyRowClass;
@@ -1151,7 +1154,7 @@ function renderMonthlyView(entries) {
} }
// Reinitialize Lucide icons // Reinitialize Lucide icons
if (typeof lucide !== 'undefined') { if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons(); lucide.createIcons();
} }
@@ -1467,7 +1470,7 @@ async function updateVacationStatistics() {
// Update UI with dynamic year // Update UI with dynamic year
const vacationLabel = document.getElementById('vacationYearLabel'); const vacationLabel = document.getElementById('vacationYearLabel');
vacationLabel.innerHTML = `<i data-lucide="plane" class="w-4 h-4"></i> Urlaub ${currentYear}`; 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(); lucide.createIcons();
} }
document.getElementById('statVacationTaken').textContent = vacationTaken; document.getElementById('statVacationTaken').textContent = vacationTaken;
@@ -1649,7 +1652,7 @@ function updateLocationButtons(location) {
} }
// Reinitialize Lucide icons after DOM update // Reinitialize Lucide icons after DOM update
if (typeof lucide !== 'undefined') { if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons(); lucide.createIcons();
} }
} }
@@ -2029,6 +2032,182 @@ async function bulkDeleteEntries() {
toggleBulkEditMode(); // Close bulk edit mode 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 * Bulk export selected entries as PDF
*/ */
@@ -2143,82 +2322,7 @@ async function bulkExportPDF() {
const targetHours = workdaysPassed * 8; const targetHours = workdaysPassed * 8;
const balance = totalNetHours - targetHours; const balance = totalNetHours - targetHours;
// Create PDF // Build table data
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)
const allDaysData = []; const allDaysData = [];
const entriesMap = new Map(selectedEntriesData.map(e => [e.date, e])); const entriesMap = new Map(selectedEntriesData.map(e => [e.date, e]));
@@ -2258,7 +2362,7 @@ async function bulkExportPDF() {
endTime = '-'; endTime = '-';
pauseText = '-'; pauseText = '-';
} else { } else {
locationText = entry.location === 'home' ? 'Home' : 'Büro'; locationText = entry.location === 'home' ? 'Home' : 'Office';
} }
allDaysData.push([ allDaysData.push([
@@ -2300,74 +2404,16 @@ async function bulkExportPDF() {
currentDate.setDate(currentDate.getDate() + 1); currentDate.setDate(currentDate.getDate() + 1);
} }
const tableData = allDaysData; // Generate PDF using common template
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
const fileName = `Zeiterfassung_${selectedEntriesData[0].date}_${selectedEntriesData[selectedEntriesData.length - 1].date}.pdf`; 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'); showNotification(`PDF mit ${allDaysData.length} Tag(en) erstellt`, 'success');
} catch (error) { } catch (error) {
@@ -3072,87 +3118,7 @@ async function handleExportPDF() {
const previousBalance = await calculatePreviousBalance(year, month); const previousBalance = await calculatePreviousBalance(year, month);
const totalBalance = monthBalance + previousBalance; const totalBalance = monthBalance + previousBalance;
// Create PDF // Build table data
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
const allDaysData = []; const allDaysData = [];
const entriesMap = new Map(entries.map(e => [e.date, e])); const entriesMap = new Map(entries.map(e => [e.date, e]));
@@ -3192,7 +3158,7 @@ async function handleExportPDF() {
endTime = '-'; endTime = '-';
pauseText = '-'; pauseText = '-';
} else { } else {
locationText = entry.location === 'home' ? 'Home' : 'Büro'; locationText = entry.location === 'home' ? 'Home' : 'Office';
} }
allDaysData.push([ 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; // Generate PDF using common template
const fileName = `Zeiterfassung_${monthName.replace(' ', '_')}.pdf`;
doc.autoTable({ await generatePDF({
startY: yPos, title: 'Zeiterfassung',
head: [['Datum', 'Tag', 'Beginn', 'Ende', 'Pause', 'Ort', 'Netto', 'Abw.']], subtitle: monthName,
body: tableData, tableData: allDaysData,
theme: 'grid', statistics: { targetHours, totalNetHours, balance: monthBalance },
headStyles: { additionalInfo: { vacationDays, flextimeDays },
fillColor: [30, 41, 59], fileName
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 }
}); });
// Footer with generation date showNotification(`PDF für ${monthName} erstellt`, 'success');
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');
} catch (error) { } catch (error) {
console.error('Error exporting PDF:', error); console.error('Error exporting PDF:', error);
showNotification('Fehler beim PDF-Export', 'error'); showNotification('Fehler beim PDF-Export', 'error');