PWA: Weitere Dokumente anzeigen + öffnen (PDF-Viewer, Download) [deploy]
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 6s
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 6s
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
46f680039d
commit
a538dbb744
3 changed files with 153 additions and 2 deletions
52
app.css
52
app.css
|
|
@ -553,6 +553,58 @@ body {
|
||||||
|
|
||||||
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }
|
.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-modal .pin-body {
|
.pin-modal .pin-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
||||||
86
app.js
86
app.js
|
|
@ -431,7 +431,16 @@ router.on('/orders/:id', async (args) => {
|
||||||
${otherDocs.length ? `
|
${otherDocs.length ? `
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h3>Weitere Dokumente (${otherDocs.length})</h3>
|
<h3>Weitere Dokumente (${otherDocs.length})</h3>
|
||||||
${otherDocs.map(p => `<p>📄 ${escapeHtml(p.filename)}</p>`).join('')}
|
<div class="doc-list">
|
||||||
|
${otherDocs.map(p => `<div class="doc-item" data-relpath="${escapeHtml(p.relpath)}" data-mime="${escapeHtml(p.mime || '')}" data-filename="${escapeHtml(p.filename)}">
|
||||||
|
<span class="doc-icon">${docIconFor(p.filename, p.mime)}</span>
|
||||||
|
<div class="doc-meta">
|
||||||
|
<div class="doc-name">${escapeHtml(p.filename)}</div>
|
||||||
|
<div class="doc-sub">${formatFileSize(p.size)}${p.date ? ' · ' + formatShortDate(p.date) : ''}</div>
|
||||||
|
</div>
|
||||||
|
<button class="doc-open" title="Öffnen">↗</button>
|
||||||
|
</div>`).join('')}
|
||||||
|
</div>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -440,6 +449,19 @@ router.on('/orders/:id', async (args) => {
|
||||||
t.addEventListener('click', () => openPhotoModal(args.id, t.dataset.relpath));
|
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
|
// Sprachnotiz
|
||||||
document.getElementById('btn-voice').onclick = () => openVoiceModal(args.id);
|
document.getElementById('btn-voice').onclick = () => openVoiceModal(args.id);
|
||||||
|
|
||||||
|
|
@ -1746,6 +1768,68 @@ function escapeHtml(s) {
|
||||||
return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
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 = `
|
||||||
|
<div class="fs-header">
|
||||||
|
<button class="icon-btn" id="dv-close">✕</button>
|
||||||
|
<div class="fs-title">${escapeHtml(filename || '')}</div>
|
||||||
|
<a class="icon-btn" id="dv-download" title="Herunterladen" download="${escapeHtml(filename || 'download')}" href="${url}">⬇</a>
|
||||||
|
</div>
|
||||||
|
<div class="dv-body">
|
||||||
|
${isPdf
|
||||||
|
? `<iframe src="${url}" style="width:100%;height:100%;border:0;background:#fff;"></iframe>`
|
||||||
|
: `<img src="${url}" style="max-width:100%;max-height:100%;object-fit:contain;">`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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
|
* PHOTO VOLLBILD MODAL MIT ZOOM + SWIPE + SKIZZEN-EDITOR
|
||||||
* ============================================================ */
|
* ============================================================ */
|
||||||
|
|
|
||||||
17
lib/api.js
17
lib/api.js
|
|
@ -247,6 +247,21 @@
|
||||||
blobUrlCache.clear();
|
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
|
// Low-level request-Funktion auch exposen für Spezialfälle
|
||||||
window.api = {
|
window.api = {
|
||||||
request,
|
request,
|
||||||
|
|
@ -257,7 +272,7 @@
|
||||||
getReport, listReports, createReport, listTemplates, listOdtTemplates, finalizeReport, deleteReport,
|
getReport, listReports, createReport, listTemplates, listOdtTemplates, finalizeReport, deleteReport,
|
||||||
deletePhoto, uploadVoiceNote, transcribeAudio, uploadAnnotatedPhoto,
|
deletePhoto, uploadVoiceNote, transcribeAudio, uploadAnnotatedPhoto,
|
||||||
listMaterials, addMaterial, deleteMaterial,
|
listMaterials, addMaterial, deleteMaterial,
|
||||||
getPhotoBlobUrl, clearPhotoCache,
|
getPhotoBlobUrl, clearPhotoCache, getFileBlobUrl,
|
||||||
deletePage, updatePageNote, uploadSignature, getPdfBlobUrl, reorderPages,
|
deletePage, updatePageNote, uploadSignature, getPdfBlobUrl, reorderPages,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue