feat: PDF-Export mit Kachel-Auswahl und hellem Drucklayout
- Neuer PDF-Export-Dialog mit Checkboxen: Lagebild, Quellen, Faktencheck, Karte, Timeline - Helles, schlichtes Drucklayout (weiss, Serifenlos, A4-optimiert) - Oeffnet neues Fenster mit sauberem HTML fuer Drucken/PDF-Speichern - Ersetzt alte window.print() Funktion die das dunkle Theme exportierte - Quellenübersicht als Tabelle + Artikelliste mit Links - Faktencheck mit farbcodierten Status-Badges
Dieser Commit ist enthalten in:
@@ -4231,6 +4231,33 @@ select:focus-visible, textarea:focus-visible,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* === Print Styles === */
|
/* === Print Styles === */
|
||||||
|
|
||||||
|
/* === PDF Export Dialog === */
|
||||||
|
.pdf-tile-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.pdf-tile-option:hover {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
.pdf-tile-option input[type="checkbox"] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.pdf-tile-option input[type="checkbox"]:checked + span {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
.sidebar,
|
.sidebar,
|
||||||
.header,
|
.header,
|
||||||
|
|||||||
@@ -240,7 +240,7 @@
|
|||||||
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('md','full')">Vollexport (Markdown)</button>
|
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('md','full')">Vollexport (Markdown)</button>
|
||||||
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('json','full')">Vollexport (JSON)</button>
|
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('json','full')">Vollexport (JSON)</button>
|
||||||
<hr class="export-dropdown-divider" role="separator">
|
<hr class="export-dropdown-divider" role="separator">
|
||||||
<button class="export-dropdown-item" role="menuitem" onclick="App.printIncident()">Drucken / PDF</button>
|
<button class="export-dropdown-item" role="menuitem" onclick="App.openPdfExportDialog()">PDF exportieren...</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary btn-small" id="archive-incident-btn">Archivieren</button>
|
<button class="btn btn-secondary btn-small" id="archive-incident-btn">Archivieren</button>
|
||||||
@@ -781,5 +781,30 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="map-fullscreen-container" id="map-fullscreen-container"></div>
|
<div class="map-fullscreen-container" id="map-fullscreen-container"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- PDF Export Dialog -->
|
||||||
|
<div class="modal-overlay" id="modal-pdf-export" role="dialog" aria-modal="true" aria-labelledby="pdf-export-title">
|
||||||
|
<div class="modal" style="max-width:420px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="pdf-export-title">PDF exportieren</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal('modal-pdf-export')" aria-label="Schliessen">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="padding:20px;">
|
||||||
|
<p style="margin:0 0 16px;font-size:13px;color:var(--text-secondary);">Kacheln fuer den Export auswaehlen:</p>
|
||||||
|
<div id="pdf-export-tiles" style="display:flex;flex-direction:column;gap:10px;">
|
||||||
|
<label class="pdf-tile-option"><input type="checkbox" value="lagebild" checked><span>Lagebild</span></label>
|
||||||
|
<label class="pdf-tile-option"><input type="checkbox" value="quellen" checked><span>Quellenübersicht</span></label>
|
||||||
|
<label class="pdf-tile-option"><input type="checkbox" value="faktencheck" checked><span>Faktencheck</span></label>
|
||||||
|
<label class="pdf-tile-option"><input type="checkbox" value="karte" checked><span>Karte</span></label>
|
||||||
|
<label class="pdf-tile-option"><input type="checkbox" value="timeline"><span>Ereignis-Timeline</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer" style="padding:12px 20px;display:flex;justify-content:flex-end;gap:8px;border-top:1px solid var(--border);">
|
||||||
|
<button class="btn btn-secondary" onclick="closeModal('modal-pdf-export')">Abbrechen</button>
|
||||||
|
<button class="btn btn-primary" onclick="App.executePdfExport()">Exportieren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -2132,6 +2132,229 @@ const App = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
openPdfExportDialog() {
|
||||||
|
this._closeExportDropdown();
|
||||||
|
if (!this.currentIncidentId) return;
|
||||||
|
openModal('modal-pdf-export');
|
||||||
|
},
|
||||||
|
|
||||||
|
executePdfExport() {
|
||||||
|
closeModal('modal-pdf-export');
|
||||||
|
const checked = [...document.querySelectorAll('#pdf-export-tiles input:checked')].map(c => c.value);
|
||||||
|
if (!checked.length) { UI.showToast('Keine Kacheln ausgewählt', 'warning'); return; }
|
||||||
|
this._generatePdf(checked);
|
||||||
|
},
|
||||||
|
|
||||||
|
_generatePdf(tiles) {
|
||||||
|
const title = document.getElementById('incident-title')?.textContent || 'Export';
|
||||||
|
const now = new Date().toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||||
|
|
||||||
|
let sections = '';
|
||||||
|
const esc = (s) => s ? s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"') : '';
|
||||||
|
|
||||||
|
// === Lagebild ===
|
||||||
|
if (tiles.includes('lagebild')) {
|
||||||
|
const summaryEl = document.getElementById('summary-text');
|
||||||
|
const timestamp = document.getElementById('lagebild-timestamp')?.textContent || '';
|
||||||
|
if (summaryEl && summaryEl.innerHTML.trim()) {
|
||||||
|
sections += '<div class="pdf-section">'
|
||||||
|
+ '<h2>Lagebild</h2>'
|
||||||
|
+ (timestamp ? '<p class="pdf-meta">' + esc(timestamp) + '</p>' : '')
|
||||||
|
+ '<div class="pdf-content">' + summaryEl.innerHTML + '</div>'
|
||||||
|
+ '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Quellen ===
|
||||||
|
if (tiles.includes('quellen')) {
|
||||||
|
const articles = this._currentArticles || [];
|
||||||
|
if (articles.length) {
|
||||||
|
const sourceMap = {};
|
||||||
|
articles.forEach(a => {
|
||||||
|
const name = a.source || 'Unbekannt';
|
||||||
|
if (!sourceMap[name]) sourceMap[name] = [];
|
||||||
|
sourceMap[name].push(a);
|
||||||
|
});
|
||||||
|
const sources = Object.entries(sourceMap).sort((a,b) => b[1].length - a[1].length);
|
||||||
|
let s = '<p class="pdf-meta">' + articles.length + ' Artikel aus ' + sources.length + ' Quellen</p>';
|
||||||
|
s += '<table class="pdf-table"><thead><tr><th>Quelle</th><th>Artikel</th><th>Sprache</th></tr></thead><tbody>';
|
||||||
|
sources.forEach(([name, arts]) => {
|
||||||
|
const langs = [...new Set(arts.map(a => (a.language || 'de').toUpperCase()))].join(', ');
|
||||||
|
s += '<tr><td><strong>' + esc(name) + '</strong></td><td>' + arts.length + '</td><td>' + langs + '</td></tr>';
|
||||||
|
});
|
||||||
|
s += '</tbody></table>';
|
||||||
|
s += '<div class="pdf-article-list">';
|
||||||
|
sources.forEach(([name, arts]) => {
|
||||||
|
s += '<h4>' + esc(name) + ' (' + arts.length + ')</h4>';
|
||||||
|
arts.forEach(a => {
|
||||||
|
const hl = esc(a.headline_de || a.headline || 'Ohne Titel');
|
||||||
|
const url = a.source_url || '';
|
||||||
|
const dateStr = a.published_at ? new Date(a.published_at).toLocaleDateString('de-DE') : '';
|
||||||
|
s += '<div class="pdf-article-item">';
|
||||||
|
s += url ? '<a href="' + esc(url) + '">' + hl + '</a>' : '<span>' + hl + '</span>';
|
||||||
|
if (dateStr) s += ' <span class="pdf-date">(' + dateStr + ')</span>';
|
||||||
|
s += '</div>';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
s += '</div>';
|
||||||
|
sections += '<div class="pdf-section"><h2>Quellenübersicht</h2>' + s + '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Faktencheck ===
|
||||||
|
if (tiles.includes('faktencheck')) {
|
||||||
|
const fcItems = document.querySelectorAll('#factcheck-list .factcheck-item');
|
||||||
|
if (fcItems.length) {
|
||||||
|
let s = '<div class="pdf-fc-list">';
|
||||||
|
fcItems.forEach(item => {
|
||||||
|
const status = item.dataset.fcStatus || '';
|
||||||
|
const statusEl = item.querySelector('.fc-status-text, .factcheck-status');
|
||||||
|
const claimEl = item.querySelector('.fc-claim-text, .factcheck-claim');
|
||||||
|
const evidenceEls = item.querySelectorAll('.fc-evidence-chip, .evidence-chip');
|
||||||
|
const statusText = statusEl ? statusEl.textContent.trim() : status;
|
||||||
|
const claim = claimEl ? claimEl.textContent.trim() : '';
|
||||||
|
const statusClass = (status.includes('confirmed') || status.includes('verified')) ? 'confirmed'
|
||||||
|
: (status.includes('refuted') || status.includes('disputed')) ? 'refuted'
|
||||||
|
: 'unverified';
|
||||||
|
s += '<div class="pdf-fc-item">'
|
||||||
|
+ '<span class="pdf-fc-badge pdf-fc-' + statusClass + '">' + esc(statusText) + '</span>'
|
||||||
|
+ '<div class="pdf-fc-claim">' + esc(claim) + '</div>';
|
||||||
|
if (evidenceEls.length) {
|
||||||
|
s += '<div class="pdf-fc-evidence">';
|
||||||
|
evidenceEls.forEach(ev => {
|
||||||
|
const link = ev.closest('a');
|
||||||
|
const href = link ? link.href : '';
|
||||||
|
const text = ev.textContent.trim();
|
||||||
|
s += href
|
||||||
|
? '<a href="' + esc(href) + '" class="pdf-fc-ev-link">' + esc(text) + '</a> '
|
||||||
|
: '<span class="pdf-fc-ev-tag">' + esc(text) + '</span> ';
|
||||||
|
});
|
||||||
|
s += '</div>';
|
||||||
|
}
|
||||||
|
s += '</div>';
|
||||||
|
});
|
||||||
|
s += '</div>';
|
||||||
|
sections += '<div class="pdf-section"><h2>Faktencheck</h2>' + s + '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Karte ===
|
||||||
|
if (tiles.includes('karte')) {
|
||||||
|
const mapContainer = document.getElementById('map-container');
|
||||||
|
const mapEmpty = document.getElementById('map-empty');
|
||||||
|
if (mapContainer && (!mapEmpty || mapEmpty.style.display === 'none')) {
|
||||||
|
const stats = document.getElementById('map-stats')?.textContent || '';
|
||||||
|
// Try to capture map as canvas image
|
||||||
|
let mapImg = '';
|
||||||
|
try {
|
||||||
|
const leafletPane = mapContainer.querySelector('.leaflet-container');
|
||||||
|
if (leafletPane && typeof leafletPane.toDataURL === 'function') {
|
||||||
|
mapImg = '<img src="' + leafletPane.toDataURL() + '" style="max-width:100%;">';
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
if (!mapImg) {
|
||||||
|
// Leaflet container is not a canvas, so we use a placeholder
|
||||||
|
mapImg = '<div class="pdf-map-placeholder">Die interaktive Karte kann nicht direkt in PDF exportiert werden. Nutzen Sie die Screenshot-Funktion Ihres Browsers (z.B. Strg+Shift+S in Firefox).</div>';
|
||||||
|
}
|
||||||
|
sections += '<div class="pdf-section"><h2>Geografische Verteilung</h2>'
|
||||||
|
+ (stats ? '<p class="pdf-meta">' + esc(stats) + '</p>' : '')
|
||||||
|
+ mapImg + '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Timeline ===
|
||||||
|
if (tiles.includes('timeline')) {
|
||||||
|
const buckets = document.querySelectorAll('#timeline .ht-bucket');
|
||||||
|
if (buckets.length) {
|
||||||
|
let s = '<div class="pdf-timeline">';
|
||||||
|
buckets.forEach(bucket => {
|
||||||
|
const label = bucket.querySelector('.ht-bucket-label');
|
||||||
|
const items = bucket.querySelectorAll('.ht-item');
|
||||||
|
if (label) s += '<h4>' + esc(label.textContent.trim()) + '</h4>';
|
||||||
|
items.forEach(item => {
|
||||||
|
const time = item.querySelector('.ht-item-time');
|
||||||
|
const ttl = item.querySelector('.ht-item-title');
|
||||||
|
const src = item.querySelector('.ht-item-source');
|
||||||
|
s += '<div class="pdf-tl-item">';
|
||||||
|
if (time) s += '<span class="pdf-tl-time">' + esc(time.textContent.trim()) + '</span> ';
|
||||||
|
if (ttl) s += '<span class="pdf-tl-title">' + esc(ttl.textContent.trim()) + '</span>';
|
||||||
|
if (src) s += ' <span class="pdf-tl-source">' + esc(src.textContent.trim()) + '</span>';
|
||||||
|
s += '</div>';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
s += '</div>';
|
||||||
|
sections += '<div class="pdf-section"><h2>Ereignis-Timeline</h2>' + s + '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sections) { UI.showToast('Keine Inhalte zum Exportieren', 'warning'); return; }
|
||||||
|
|
||||||
|
const printHtml = '<!DOCTYPE html>'
|
||||||
|
+ '\n<html lang="de">'
|
||||||
|
+ '\n<head>'
|
||||||
|
+ '\n<meta charset="utf-8">'
|
||||||
|
+ '\n<title>' + esc(title) + ' \u2014 AegisSight Export</title>'
|
||||||
|
+ '\n<style>'
|
||||||
|
+ '\n@page { margin: 20mm 15mm; size: A4; }'
|
||||||
|
+ '\n* { box-sizing: border-box; margin: 0; padding: 0; }'
|
||||||
|
+ "\nbody { font-family: -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 11pt; line-height: 1.5; color: #1a1a1a; background: #fff; }"
|
||||||
|
+ '\n.pdf-header { border-bottom: 2px solid #2c3e50; padding-bottom: 12px; margin-bottom: 24px; }'
|
||||||
|
+ '\n.pdf-header h1 { font-size: 20pt; font-weight: 700; color: #2c3e50; margin-bottom: 4px; }'
|
||||||
|
+ '\n.pdf-header .pdf-subtitle { font-size: 9pt; color: #666; }'
|
||||||
|
+ '\n.pdf-section { margin-bottom: 28px; page-break-inside: avoid; }'
|
||||||
|
+ '\n.pdf-section h2 { font-size: 14pt; font-weight: 600; color: #2c3e50; border-bottom: 1px solid #ddd; padding-bottom: 6px; margin-bottom: 12px; }'
|
||||||
|
+ '\n.pdf-section h4 { font-size: 10pt; font-weight: 600; color: #444; margin: 12px 0 4px; }'
|
||||||
|
+ '\n.pdf-meta { font-size: 9pt; color: #888; margin-bottom: 10px; }'
|
||||||
|
+ '\n.pdf-content { font-size: 11pt; line-height: 1.6; }'
|
||||||
|
+ '\n.pdf-content h3 { font-size: 12pt; font-weight: 600; color: #2c3e50; margin: 14px 0 6px; }'
|
||||||
|
+ '\n.pdf-content strong { font-weight: 600; }'
|
||||||
|
+ '\n.pdf-content ul { margin: 6px 0 6px 20px; }'
|
||||||
|
+ '\n.pdf-content li { margin-bottom: 2px; }'
|
||||||
|
+ '\n.pdf-content a, .pdf-content .citation { color: #2c3e50; font-weight: 600; text-decoration: none; }'
|
||||||
|
+ '\n.pdf-table { width: 100%; border-collapse: collapse; font-size: 10pt; margin-bottom: 16px; }'
|
||||||
|
+ '\n.pdf-table th { background: #f0f0f0; text-align: left; padding: 6px 10px; border: 1px solid #ddd; font-weight: 600; font-size: 9pt; text-transform: uppercase; color: #555; }'
|
||||||
|
+ '\n.pdf-table td { padding: 5px 10px; border: 1px solid #ddd; }'
|
||||||
|
+ '\n.pdf-table tr:nth-child(even) { background: #fafafa; }'
|
||||||
|
+ '\n.pdf-article-list { font-size: 10pt; }'
|
||||||
|
+ '\n.pdf-article-item { padding: 2px 0; }'
|
||||||
|
+ '\n.pdf-article-item a { color: #2c3e50; text-decoration: none; }'
|
||||||
|
+ '\n.pdf-date { color: #888; font-size: 9pt; }'
|
||||||
|
+ '\n.pdf-fc-list { display: flex; flex-direction: column; gap: 12px; }'
|
||||||
|
+ '\n.pdf-fc-item { border: 1px solid #ddd; border-radius: 6px; padding: 10px 14px; page-break-inside: avoid; }'
|
||||||
|
+ '\n.pdf-fc-badge { display: inline-block; font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; padding: 2px 8px; border-radius: 3px; margin-bottom: 4px; }'
|
||||||
|
+ '\n.pdf-fc-confirmed { background: #d4edda; color: #155724; }'
|
||||||
|
+ '\n.pdf-fc-refuted { background: #f8d7da; color: #721c24; }'
|
||||||
|
+ '\n.pdf-fc-unverified { background: #fff3cd; color: #856404; }'
|
||||||
|
+ '\n.pdf-fc-claim { font-size: 11pt; margin-top: 4px; }'
|
||||||
|
+ '\n.pdf-fc-evidence { margin-top: 6px; font-size: 9pt; }'
|
||||||
|
+ '\n.pdf-fc-ev-link { color: #2c3e50; text-decoration: underline; margin-right: 6px; }'
|
||||||
|
+ '\n.pdf-fc-ev-tag { background: #eee; padding: 1px 6px; border-radius: 3px; margin-right: 4px; }'
|
||||||
|
+ '\n.pdf-map-placeholder { padding: 30px; text-align: center; color: #888; background: #f9f9f9; border: 1px dashed #ccc; border-radius: 6px; font-size: 10pt; }'
|
||||||
|
+ '\n.pdf-timeline h4 { color: #2c3e50; border-bottom: 1px solid #eee; padding-bottom: 3px; margin-top: 10px; }'
|
||||||
|
+ '\n.pdf-tl-item { padding: 2px 0; font-size: 10pt; }'
|
||||||
|
+ '\n.pdf-tl-time { color: #888; font-size: 9pt; min-width: 40px; display: inline-block; }'
|
||||||
|
+ '\n.pdf-tl-source { color: #888; font-size: 9pt; }'
|
||||||
|
+ '\n.pdf-footer { margin-top: 30px; padding-top: 10px; border-top: 1px solid #ddd; font-size: 8pt; color: #999; text-align: center; }'
|
||||||
|
+ '\n</style>'
|
||||||
|
+ '\n</head>'
|
||||||
|
+ '\n<body>'
|
||||||
|
+ '\n<div class="pdf-header">'
|
||||||
|
+ '\n <h1>' + esc(title) + '</h1>'
|
||||||
|
+ '\n <div class="pdf-subtitle">AegisSight Monitor \u2014 Exportiert am ' + esc(now) + '</div>'
|
||||||
|
+ '\n</div>'
|
||||||
|
+ sections
|
||||||
|
+ '\n<div class="pdf-footer">Erstellt mit AegisSight Monitor \u2014 aegis-sight.de</div>'
|
||||||
|
+ '\n</body></html>';
|
||||||
|
|
||||||
|
const printWin = window.open('', '_blank', 'width=800,height=600');
|
||||||
|
if (!printWin) { UI.showToast('Popup blockiert \u2014 bitte Popup-Blocker deaktivieren', 'error'); return; }
|
||||||
|
printWin.document.write(printHtml);
|
||||||
|
printWin.document.close();
|
||||||
|
printWin.onload = function() { printWin.focus(); printWin.print(); };
|
||||||
|
setTimeout(function() { try { printWin.focus(); printWin.print(); } catch(e) {} }, 500);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
async exportIncident(format, scope) {
|
async exportIncident(format, scope) {
|
||||||
this._closeExportDropdown();
|
this._closeExportDropdown();
|
||||||
if (!this.currentIncidentId) return;
|
if (!this.currentIncidentId) return;
|
||||||
@@ -2160,10 +2383,7 @@ const App = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
printIncident() {
|
|
||||||
this._closeExportDropdown();
|
|
||||||
window.print();
|
|
||||||
},
|
|
||||||
|
|
||||||
// === Sidebar-Stats ===
|
// === Sidebar-Stats ===
|
||||||
|
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren