From a538dbb744979da646fded749cedd71c1b50c767 Mon Sep 17 00:00:00 2001 From: Eddy Date: Fri, 17 Apr 2026 12:49:15 +0200 Subject: [PATCH] =?UTF-8?q?PWA:=20Weitere=20Dokumente=20anzeigen=20+=20?= =?UTF-8?q?=C3=B6ffnen=20(PDF-Viewer,=20Download)=20[deploy]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - otherDocs-Sektion mit gestylten doc-items (Icon, Name, Größe, Datum) - getFileBlobUrl() in api.js ohne Mime-Filter - PDF/Bilder: Fullscreen-Modal mit iframe/img + Download-Button - Andere Dateitypen: direkter Download - CSS analog zu .audio-item Co-Authored-By: Claude Haiku 4.5 --- app.css | 52 +++++++++++++++++++++++++++++++++ app.js | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++- lib/api.js | 17 ++++++++++- 3 files changed, 153 insertions(+), 2 deletions(-) diff --git a/app.css b/app.css index aaaa0b5..56efd80 100644 --- a/app.css +++ b/app.css @@ -553,6 +553,58 @@ body { .btn[disabled] { opacity: 0.5; cursor: not-allowed; } +/* Weitere Dokumente (Auftrags-Detail) */ +.doc-list { display: flex; flex-direction: column; gap: 8px; } +.doc-item { + background: #2a2a30; + border-radius: 6px; + padding: 10px 12px; + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + transition: background 0.15s; +} +.doc-item:hover { background: #33333a; } +.doc-item.loading { opacity: 0.6; pointer-events: none; } +.doc-item .doc-icon { font-size: 22px; flex-shrink: 0; } +.doc-item .doc-meta { flex: 1; min-width: 0; } +.doc-item .doc-name { + font-size: 14px; + color: #e0e0e0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.doc-item .doc-sub { font-size: 11px; color: #888; margin-top: 2px; } +.doc-item .doc-open { + background: #337ab7; + color: #fff; + border: none; + border-radius: 50%; + width: 32px; + height: 32px; + font-size: 14px; + cursor: pointer; + flex-shrink: 0; +} + +/* Doc-Viewer (PDF/Image inline) */ +.doc-viewer-modal .dv-body { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + background: #1a1a1f; + overflow: hidden; +} +.doc-viewer-modal .fs-header a.icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; +} + /* PIN-Modal */ .pin-modal .pin-body { flex: 1; diff --git a/app.js b/app.js index 768f18f..b86962e 100644 --- a/app.js +++ b/app.js @@ -431,7 +431,16 @@ router.on('/orders/:id', async (args) => { ${otherDocs.length ? `

Weitere Dokumente (${otherDocs.length})

- ${otherDocs.map(p => `

📄 ${escapeHtml(p.filename)}

`).join('')} +
+ ${otherDocs.map(p => `
+ ${docIconFor(p.filename, p.mime)} +
+
${escapeHtml(p.filename)}
+
${formatFileSize(p.size)}${p.date ? ' · ' + formatShortDate(p.date) : ''}
+
+ +
`).join('')} +
` : ''} `; @@ -440,6 +449,19 @@ router.on('/orders/:id', async (args) => { t.addEventListener('click', () => openPhotoModal(args.id, t.dataset.relpath)); }); + // Weitere Dokumente: Tap = Öffnen (PDF inline, sonst Download) + document.querySelectorAll('.doc-list .doc-item').forEach(el => { + el.addEventListener('click', async () => { + const rel = el.dataset.relpath; + const filename = el.dataset.filename; + el.classList.add('loading'); + const res = await api.getFileBlobUrl(rel); + el.classList.remove('loading'); + if (!res) { showToast('Datei konnte nicht geladen werden', 'error'); return; } + openFileViewer(res, filename); + }); + }); + // Sprachnotiz document.getElementById('btn-voice').onclick = () => openVoiceModal(args.id); @@ -1746,6 +1768,68 @@ function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } +function docIconFor(filename, mime) { + const ext = (filename || '').split('.').pop().toLowerCase(); + if ((mime || '').includes('pdf') || ext === 'pdf') return '📕'; + if (['doc','docx','odt','rtf'].includes(ext)) return '📝'; + if (['xls','xlsx','ods','csv'].includes(ext)) return '📊'; + if (['ppt','pptx','odp'].includes(ext)) return '📽'; + if (['zip','rar','7z','tar','gz'].includes(ext)) return '🗜'; + if (['txt','log','md'].includes(ext)) return '📃'; + return '📄'; +} + +function formatFileSize(bytes) { + const n = Number(bytes) || 0; + if (n < 1024) return n + ' B'; + if (n < 1024 * 1024) return (n / 1024).toFixed(0) + ' KB'; + return (n / 1024 / 1024).toFixed(1) + ' MB'; +} + +function formatShortDate(ts) { + const d = new Date(Number(ts) * 1000); + if (isNaN(d.getTime())) return ''; + return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit' }); +} + +function openFileViewer({ url, blob, mime }, filename) { + const isPdf = (mime || '').includes('pdf') || /\.pdf$/i.test(filename); + const isImage = (mime || '').startsWith('image/'); + + if (!isPdf && !isImage) { + // Nicht inline anzeigbar → Download anstoßen + const a = document.createElement('a'); + a.href = url; + a.download = filename || 'download'; + document.body.appendChild(a); + a.click(); + a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 10000); + return; + } + + const modal = document.createElement('div'); + modal.className = 'fullscreen-modal doc-viewer-modal'; + modal.innerHTML = ` +
+ +
${escapeHtml(filename || '')}
+ +
+
+ ${isPdf + ? `` + : ``} +
+ `; + document.body.appendChild(modal); + const cleanup = () => { + URL.revokeObjectURL(url); + modal.remove(); + }; + modal.querySelector('#dv-close').onclick = cleanup; +} + /* ============================================================ * PHOTO VOLLBILD MODAL MIT ZOOM + SWIPE + SKIZZEN-EDITOR * ============================================================ */ diff --git a/lib/api.js b/lib/api.js index 260ef96..848613b 100644 --- a/lib/api.js +++ b/lib/api.js @@ -247,6 +247,21 @@ blobUrlCache.clear(); } + // Lädt eine beliebige Datei (PDF, DOCX, ...) als Blob-URL inkl. JWT. + // Ohne Mime-Filter — Aufrufer entscheidet selbst, was damit passiert. + async function getFileBlobUrl(relpath) { + const t = await getToken(); + if (!t) return null; + const params = new URLSearchParams({ relpath, jwt: t }); + const r = await fetch(API_BASE + '/photo.php?' + params.toString()); + if (!r.ok) { + console.warn('getFileBlobUrl failed', r.status, relpath); + return null; + } + const blob = await r.blob(); + return { url: URL.createObjectURL(blob), blob, mime: r.headers.get('Content-Type') || '' }; + } + // Low-level request-Funktion auch exposen für Spezialfälle window.api = { request, @@ -257,7 +272,7 @@ getReport, listReports, createReport, listTemplates, listOdtTemplates, finalizeReport, deleteReport, deletePhoto, uploadVoiceNote, transcribeAudio, uploadAnnotatedPhoto, listMaterials, addMaterial, deleteMaterial, - getPhotoBlobUrl, clearPhotoCache, + getPhotoBlobUrl, clearPhotoCache, getFileBlobUrl, deletePage, updatePageNote, uploadSignature, getPdfBlobUrl, reorderPages, }; })();