feat: enhance PDF generation and improve Lucide icon initialization
This commit is contained in:
31
public/favicon.svg
Normal file
31
public/favicon.svg
Normal 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 |
@@ -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>
|
||||||
|
// 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();
|
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>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user