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)}
+
+
+
`).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 = `
+
+
+ ${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,
};
})();