PDF Export Design an neues Design angepasst und PDF größe verringert

This commit is contained in:
Erik committed 2026-08-12 15:06:45 +02:00
1 parent 004003cd17
commit c055091dc7
12 files changed
+93 -43

No files matched your search

+89 -39
View File
@@ -2246,9 +2246,9 @@ async function exportPDF(){
container.style.width = '1200px';
container.style.background = '#fff';
container.style.padding = '20px';
container.style.fontFamily = 'Arial, sans-serif';
container.style.fontFamily = '-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
container.style.fontSize = '10px';
container.style.color = '#1a1a1a';
container.style.color = 'var(--text)';
document.body.appendChild(container);
const grouped = peopleByRole(w);
@@ -2283,16 +2283,16 @@ async function exportPDF(){
cover.style.flexDirection = 'column';
cover.style.justifyContent = 'center';
cover.style.textAlign = 'center';
cover.style.fontFamily = 'Arial, sans-serif';
cover.style.fontFamily = '-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
const logoHtml = logoDataUrl
? `<div style="margin-bottom:28px;"><img src="${logoDataUrl}" alt="Logo" style="width:200px; height:200px; object-fit:contain;"></div>`
: `<div style="font-size:80px; margin-bottom:28px;">⚡</div>`;
cover.innerHTML = `
${logoHtml}
<h1 style="font-size:32px; margin:0 0 16px 0; color:#1e3a5f;">Projektübersicht ${DEPARTMENTS[currentDepartment].label}</h1>
<div style="font-size:18px; color:#4a5568; margin-bottom:48px;">${esc(w.label)}</div>
${w.date ? `<div style="font-size:14px; color:#718096; margin-bottom:12px;">Datum: ${esc(formatWeekDate(w.date))}</div>` : ''}
<div style="font-size:12px; color:#a0aec0; margin-top:60px;">Erstellt am ${new Date().toLocaleDateString('de-DE')}</div>
<h1 style="font-size:32px; margin:0 0 16px 0; color:#1d1d1f;">Projektübersicht ${DEPARTMENTS[currentDepartment].label}</h1>
<div style="font-size:18px; color:#6e6e73; margin-bottom:48px;">${esc(w.label)}</div>
${w.date ? `<div style="font-size:14px; color:#6e6e73; margin-bottom:12px;">Datum: ${esc(formatWeekDate(w.date))}</div>` : ''}
<div style="font-size:12px; color:#6e6e73; margin-top:60px;">Erstellt am ${new Date().toLocaleDateString('de-DE')}</div>
`;
container.appendChild(cover);
@@ -2313,11 +2313,14 @@ async function exportPDF(){
const landscapePageW = pageHeightPx - marginPx * 2;
const landscapePageH = pageWidthPx - marginPx * 2;
// JPEG quality (0.1 - 1.0) - lower = smaller file
const JPEG_QUALITY = 1;
async function addImageToLandscapePage(canvas){
const imgW = landscapePageW;
const imgH = (canvas.height * imgW) / canvas.width;
pdf.addPage([pageHeightPx, pageWidthPx], 'landscape');
pdf.addImage(canvas.toDataURL('image/png'), 'PNG', marginPx, marginPx, imgW, imgH);
pdf.addImage(canvas.toDataURL('image/jpeg', JPEG_QUALITY), 'JPEG', marginPx, marginPx, imgW, imgH);
}
async function drawBlockOnLandscapePage(blockHTML){
@@ -2328,7 +2331,7 @@ async function exportPDF(){
wrapper.style.background = '#fff';
wrapper.innerHTML = blockHTML;
container.appendChild(wrapper);
const canvas = await html2canvas(wrapper, { scale: 2, useCORS: true, backgroundColor: '#ffffff', width: 1123 });
const canvas = await html2canvas(wrapper, { scale: 1.5, useCORS: true, backgroundColor: '#ffffff', width: 1123 });
const imgH = (canvas.height * landscapePageW) / canvas.width;
wrapper.remove();
return { canvas, imgH };
@@ -2336,12 +2339,12 @@ async function exportPDF(){
try {
// Deckblatt (Hochkant) zeichnen
const coverCanvas = await html2canvas(cover, { scale: 2, useCORS: true, backgroundColor: '#ffffff', width: 595, height: 842 });
const coverCanvas = await html2canvas(cover, { scale: 1.5, useCORS: true, backgroundColor: '#ffffff', width: 595, height: 842 });
if(pdf.internal.getNumberOfPages() > 0) pdf.addPage();
const coverW = pageWidthPx - marginPx * 2;
const coverH = (coverCanvas.height * coverW) / coverCanvas.width;
const coverX = marginPx + (coverW - (coverCanvas.width * coverW / coverCanvas.width)) / 2;
pdf.addImage(coverCanvas.toDataURL('image/png'), 'PNG', marginPx, marginPx + ((pageHeightPx - marginPx * 2 - coverH) / 2), coverW, coverH);
pdf.addImage(coverCanvas.toDataURL('image/jpeg', JPEG_QUALITY), 'JPEG', marginPx, marginPx + ((pageHeightPx - marginPx * 2 - coverH) / 2), coverW, coverH);
const headerRows = buildPdfTableHeaderRows(allPeople);
@@ -2356,7 +2359,7 @@ async function exportPDF(){
// Hilfsfunktion: misst Höhe einer Tabelle mit gegebenen Zeilen
async function measureBlockWithRows(titleRow, bodyRows){
const wrapper = renderSinglePdfBlock(container, headerRows, titleRow, bodyRows);
const canvas = await html2canvas(wrapper, { scale: 2, useCORS: true, backgroundColor: '#ffffff', width: 1123 });
const canvas = await html2canvas(wrapper, { scale: 1.5, useCORS: true, backgroundColor: '#ffffff', width: 1123 });
const imgH = (canvas.height * landscapePageW) / canvas.width;
wrapper.remove();
return { canvas, imgH };
@@ -2375,7 +2378,10 @@ async function exportPDF(){
for(let i = 0; i < projects.length; i++){
const rowHTML = buildPdfProjectRow(projects[i], allPeople);
const testRows = currentRows + rowHTML;
// Alternating row background to match project board design
const rowBg = i % 2 === 0 ? ' style="background:#fafafc"' : '';
const rowHTMLWithBg = rowHTML.replace('<tr>', `<tr${rowBg}>`);
const testRows = currentRows + rowHTMLWithBg;
const { imgH: testImgH } = await measureBlockWithRows(titleRow, testRows);
// Wenn die aktuelle Seite überschritten würde und wir bereits Zeilen haben,
@@ -2396,7 +2402,7 @@ async function exportPDF(){
// Projektboard-Blöcke zeichnen
for(const block of groupBlocks){
const wrapper = renderSinglePdfBlock(container, headerRows, block.titleRow, block.bodyRows);
const canvas = await html2canvas(wrapper, { scale: 2, useCORS: true, backgroundColor: '#ffffff', width: 1123 });
const canvas = await html2canvas(wrapper, { scale: 1.5, useCORS: true, backgroundColor: '#ffffff', width: 1123 });
await addImageToLandscapePage(canvas);
wrapper.remove();
}
@@ -2424,25 +2430,36 @@ async function exportPDF(){
// buildPdfTableHeaderRows: baut die Kopfzeilen der Projektübersichts-Tabelle als HTML-String (zwei Zeilen).
function buildPdfTableHeaderRows(allPeople){
let h1 = `<tr><td rowspan="2" style="border:1px solid #4a5568;background:#1e3a5f;color:#fff;padding:6px;font-weight:bold;">Pr.-Nr.</td><td rowspan="2" style="border:1px solid #4a5568;background:#1e3a5f;color:#fff;padding:6px;font-weight:bold;">Bezeichnung</td><td rowspan="2" style="border:1px solid #4a5568;background:#1e3a5f;color:#fff;padding:6px;font-weight:bold;">Status</td>`;
let h1 = `<tr><td rowspan="2" style="border:1px solid #e8e8ed;background:#f0f0f2;color:#1d1d1f;padding:6px;font-weight:bold;">Pr.-Nr.</td><td rowspan="2" style="border:1px solid #e8e8ed;background:#f0f0f2;color:#1d1d1f;padding:6px;font-weight:bold;">Bezeichnung</td><td rowspan="2" style="border:1px solid #e8e8ed;background:#f0f0f2;color:#1d1d1f;padding:6px;font-weight:bold;">Status</td>`;
const grouped = peopleByRole(currentWeek());
ROLES.forEach(r => {
const count = Math.max(1, grouped[r].length);
h1 += `<td class="role-header role-${r.toLowerCase()}" colspan="${count}" style="border:1px solid #4a5568;background:#1e3a5f;color:#fff;padding:6px;font-weight:bold;">${r}</td>`;
// Rollen-spezifische Hintergrundfarben aus dem Projektboard
let roleBg;
if(r === 'Planung') roleBg = 'rgb(212, 245, 224)';
else if(r === 'Projektleitung') roleBg = 'rgb(255, 219, 219)';
else if(r === 'Bauüberwachung') roleBg = 'rgb(203, 238, 255)';
else roleBg = '#f0f0f2';
h1 += `<td class="role-header role-${r.toLowerCase()}" colspan="${count}" style="border:1px solid #e8e8ed;background:${roleBg};color:#1d1d1f;padding:6px;font-weight:bold;">${r}</td>`;
});
h1 += `<td colspan="${LPHS.length}" style="border:1px solid #4a5568;background:#1e3a5f;color:#fff;padding:6px;font-weight:bold;">Leistungsphasen (%)</td></tr>`;
h1 += `<td colspan="${LPHS.length}" style="border:1px solid #e8e8ed;background:#f0f0f2;color:#1d1d1f;padding:6px;font-weight:bold;">Leistungsphasen (%)</td></tr>`;
let h2 = `<tr>`;
ROLES.forEach(r => {
let roleBg;
if(r === 'Planung') roleBg = 'rgb(212, 245, 224)';
else if(r === 'Projektleitung') roleBg = 'rgb(255, 219, 219)';
else if(r === 'Bauüberwachung') roleBg = 'rgb(203, 238, 255)';
else roleBg = '#fafafc';
if(grouped[r].length === 0){
h2 += `<td style="border:1px solid #4a5568;background:#cbd5e0;padding:4px;font-weight:bold;">–</td>`;
h2 += `<td style="border:1px solid #e8e8ed;background:${roleBg};padding:4px;font-weight:bold;">–</td>`;
} else {
grouped[r].forEach(entry => {
h2 += `<td style="border:1px solid #4a5568;background:#cbd5e0;padding:4px;font-weight:bold;">${esc(entry.person.name)}</td>`;
h2 += `<td style="border:1px solid #e8e8ed;background:${roleBg};padding:4px;font-weight:bold;">${esc(entry.person.name)}</td>`;
});
}
});
LPHS.forEach(l => h2 += `<td style="border:1px solid #4a5568;background:#cbd5e0;padding:4px;font-weight:bold;">${l}</td>`);
LPHS.forEach(l => h2 += `<td style="border:1px solid #e8e8ed;background:#fafafc;padding:4px;font-weight:bold;">${l}</td>`);
h2 += `</tr>`;
return h1 + h2;
}
@@ -2450,28 +2467,61 @@ function buildPdfTableHeaderRows(allPeople){
// buildPdfProjectRow: baut eine einzelne Projektzeile als HTML-String.
function buildPdfProjectRow(p, allPeople){
let body = `<tr>`;
body += `<td style="border:1px solid #a0aec0;padding:4px;white-space:nowrap;">${esc(p.nr)}</td>`;
body += `<td style="border:1px solid #a0aec0;padding:4px;">${esc(p.name)}</td>`;
body += `<td style="border:1px solid #e8e8ed;padding:4px;white-space:nowrap;">${esc(p.nr)}</td>`;
body += `<td style="border:1px solid #e8e8ed;padding:4px;">${esc(p.name)}</td>`;
const statusText = pdfStatusText(p.status);
body += `<td style="border:1px solid #a0aec0;padding:4px;">${statusText}</td>`;
body += `<td style="border:1px solid #e8e8ed;padding:4px;">${statusText}</td>`;
allPeople.forEach(entry => {
const s = p.assign[assignKey(entry.person.id, entry.role)] || 0;
const txt = s === 1 ? 'X' : s === 2 ? '(X)' : '';
const bg = s === 1 ? '#9ae6b4' : s === 2 ? '#fbd38d' : '#fff';
body += `<td style="border:1px solid #a0aec0;padding:4px;text-align:center;background:${bg};">${txt}</td>`;
let roleBg = '#fff';
if(entry.role === 'Planung') roleBg = 'rgb(212, 245, 224)';
else if(entry.role === 'Projektleitung') roleBg = 'rgb(255, 219, 219)';
else if(entry.role === 'Bauüberwachung') roleBg = 'rgb(203, 238, 255)';
const textColor = (s === 1 || s === 2) ? '#fff' : '#1d1d1f';
const cellBg = s === 1 ? '#248a3d' : s === 2 ? '#d18b00' : roleBg;
body += `<td style="border:1px solid #e8e8ed;padding:4px;text-align:center;background:${cellBg};color:${textColor};">${txt}</td>`;
});
LPHS.forEach(l => {
const v = (p.lph[l] !== undefined && p.lph[l] !== '') ? String(p.lph[l]).replace('%','') : '';
body += `<td style="border:1px solid #a0aec0;padding:4px;text-align:center;">${esc(v ? v + '%' : '')}</td>`;
body += `<td style="border:1px solid #e8e8ed;padding:4px;text-align:center;">${esc(v ? v + '%' : '')}</td>`;
});
body += `</tr>`;
return body;
}
// Build PDF group body rows with alternating background colors
function buildPdfGroupBodyRows(projectRows, allPeople) {
let html = '';
projectRows.forEach((pr, i) => {
const s = pr.assign ? Object.values(pr.assign)[0] || 0 : 0;
const isEven = i % 2 === 0;
const rowBg = isEven ? '#fafafc' : '#fff';
html += `<tr style="background:${rowBg}">`;
html += `<td style="border:1px solid #e8e8ed;padding:4px;white-space:nowrap;">${esc(pr.nr)}</td>`;
html += `<td style="border:1px solid #e8e8ed;padding:4px;">${esc(pr.name)}</td>`;
const statusText = pdfStatusText(pr.status);
html += `<td style="border:1px solid #e8e8ed;padding:4px;">${statusText}</td>`;
allPeople.forEach(entry => {
const assignKey = `${entry.person.id}-${entry.role}`;
const assign = pr.assign ? pr.assign[assignKey] : 0;
const txt = assign === 1 ? 'X' : assign === 2 ? '(X)' : '';
const bg = assign === 1 ? '#248a3d' : assign === 2 ? '#d18b00' : '#fff';
html += `<td style="border:1px solid #e8e8ed;padding:4px;text-align:center;background:${bg};color:${assign===1||assign===2?'#fff':'#1d1d1f'}";">${txt}</td>`;
});
LPHS.forEach(l => {
const v = (pr.lph && pr.lph[l] !== undefined && pr.lph[l] !== '') ? String(pr.lph[l]).replace('%','') : '';
html += `<td style="border:1px solid #e8e8ed;padding:4px;text-align:center;">${esc(v ? v + '%' : '')}</td>`;
});
html += `</tr>`;
});
return html;
}
// buildPdfGroupTitleRow: baut die Zeile mit dem Gruppentitel.
function buildPdfGroupTitleRow(groupName, allPeople){
const colCount = 3 + allPeople.length + LPHS.length;
return `<tr><td colspan="${colCount}" style="border:1px solid #4a5568;background:#edf2f7;font-weight:bold;padding:6px;">${esc(groupName)}</td></tr>`;
return `<tr><td colspan="${colCount}" style="border:1px solid #e8e8ed;background:#f0f0f2;font-weight:bold;padding:6px;color:#1d1d1f;">${esc(groupName)}</td></tr>`;
}
// renderSinglePdfBlock: erzeugt einen DOM-Wrapper mit einer einzelnen Tabelle
@@ -2490,7 +2540,7 @@ function renderSinglePdfBlock(container, headerRows, titleRow, bodyRows){
// buildPdfAttendanceBlock: baut den Anwesenheits-Block als HTML-String.
function buildPdfAttendanceBlock(presentList, absentList){
return `<div class="pdf-block" style="margin-bottom:16px;">
<div style="margin-bottom:8px; font-weight:bold; color:#1e3a5f; font-size:12px;">Anwesenheit</div>
<div style="margin-bottom:8px; font-weight:bold; color:#1d1d1f; font-size:12px;">Anwesenheit</div>
<div style="margin-bottom:4px;"><b>Anwesend:</b> ${presentList || '—'}</div>
<div><b>Abwesend:</b> ${absentList || '—'}</div>
</div>`;
@@ -2545,33 +2595,33 @@ function buildPdfLoadSection(allPeople){
if(l > maxLoad) maxLoad = l;
});
let html = `<div class="pdf-block"><div style="margin-top:24px; margin-bottom:8px; font-weight:bold; color:#1e3a5f; font-size:12px;">Auslastung der Projektbeteiligten (aktuelle Sitzung)</div>`;
html += '<table style="border-collapse:collapse;width:100%;"><tbody><tr><td style="border:1px solid #4a5568;background:#1e3a5f;color:#fff;padding:6px;font-weight:bold;">Rolle</td><td style="border:1px solid #4a5568;background:#1e3a5f;color:#fff;padding:6px;font-weight:bold;">Kürzel</td><td style="border:1px solid #4a5568;background:#1e3a5f;color:#fff;padding:6px;font-weight:bold;">Punkte</td><td style="border:1px solid #4a5568;background:#1e3a5f;color:#fff;padding:6px;font-weight:bold;">Balken</td><td style="border:1px solid #4a5568;background:#1e3a5f;color:#fff;padding:6px;font-weight:bold;">Projektdetails</td></tr>';
let html = `<div class="pdf-block"><div style="margin-top:24px; margin-bottom:8px; font-weight:bold; color:#1d1d1f; font-size:12px;">Auslastung der Projektbeteiligten (aktuelle Sitzung)</div>`;
html += '<table style="border-collapse:collapse;width:100%;"><tbody><tr><td style="border:1px solid #e8e8ed;background:#f0f0f2;color:#1d1d1f;padding:6px;font-weight:bold;">Rolle</td><td style="border:1px solid #e8e8ed;background:#f0f0f2;color:#1d1d1f;padding:6px;font-weight:bold;">Kürzel</td><td style="border:1px solid #e8e8ed;background:#f0f0f2;color:#1d1d1f;padding:6px;font-weight:bold;">Punkte</td><td style="border:1px solid #e8e8ed;background:#f0f0f2;color:#1d1d1f;padding:6px;font-weight:bold;">Balken</td><td style="border:1px solid #e8e8ed;background:#f0f0f2;color:#1d1d1f;padding:6px;font-weight:bold;">Projektdetails</td></tr>';
ROLES.forEach(r => {
grouped[r].forEach(entry => {
const s = stats[assignKey(entry.person.id, r)];
const load = s.main + s.opt * 0.5 + s.kleine * 0.3;
const pct = Math.round(load / maxLoad * 100);
const color = pct > 75 ? '#c53030' : pct > 45 ? '#d69e2e' : '#2f855a';
const color = pct > 75 ? '#d70015' : pct > 45 ? '#b25000' : '#248a3d';
const detail = Object.keys(s.projects).map(k => `${esc(k)} (${s.projects[k]}×)`).join(', ') || '—';
html += `<tr>
<td style="border:1px solid #a0aec0;padding:4px;">${r}</td>
<td style="border:1px solid #a0aec0;padding:4px;font-weight:bold;">${esc(entry.person.name)}</td>
<td style="border:1px solid #a0aec0;padding:4px;text-align:center;">${load.toString().replace('.',',')}</td>
<td style="border:1px solid #a0aec0;padding:4px;">
<div style="background:#e2e8f0;height:10px;border-radius:3px;overflow:hidden;">
<td style="border:1px solid #e8e8ed;padding:4px;">${r}</td>
<td style="border:1px solid #e8e8ed;padding:4px;font-weight:bold;">${esc(entry.person.name)}</td>
<td style="border:1px solid #e8e8ed;padding:4px;text-align:center;">${load.toString().replace('.',',')}</td>
<td style="border:1px solid #e8e8ed;padding:4px;">
<div style="background:#f0f0f0;height:10px;border-radius:3px;overflow:hidden;">
<div style="width:${pct}%;background:${color};height:100%;"></div>
</div>
<div style="font-size:9px;color:#666;text-align:right;">${pct}%</div>
<div style="font-size:9px;color:#6e6e73;text-align:right;">${pct}%</div>
</td>
<td style="border:1px solid #a0aec0;padding:4px;font-size:9px;">${detail}${s.anbahnung ? `<br><span style="color:#3182ce;">In Anbahnung: ${s.anbahnung}</span>` : ''}</td>
<td style="border:1px solid #e8e8ed;padding:4px;font-size:9px;">${detail}${s.anbahnung ? `<br><span style="color:#0071e3;">In Anbahnung: ${s.anbahnung}</span>` : ''}</td>
</tr>`;
});
});
html += '</tbody></table>';
html += '<div style="margin-top:4px;font-size:9px;color:#666;">Haupt=1 · Optional=0,5 · Kleine Projekte=0,3 · In Anbahnung wird nicht in der Punktzahl gezählt.</div>';
html += '<div style="margin-top:4px;font-size:9px;color:#6e6e73;">Haupt=1 · Optional=0,5 · Kleine Projekte=0,3 · In Anbahnung wird nicht in der Punktzahl gezählt.</div>';
html += '</div>';
return html;
}