diff --git a/app.css b/app.css index 4dd944a..0598a24 100644 --- a/app.css +++ b/app.css @@ -203,6 +203,15 @@ body { height: 100%; object-fit: cover; } +.photo-grid .thumb-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + opacity: 0.5; +} /* ----- Toast ----- */ #toast-container { diff --git a/app.js b/app.js index 2a4d0a7..9358a48 100644 --- a/app.js +++ b/app.js @@ -147,11 +147,13 @@ router.on('/orders/:id', async (args) => {

Hochgeladene Fotos (${photos.photos.length})

- ${photos.photos.map(p => `
`).join('')} + ${photos.photos.map(p => `
`).join('')}
`; + loadThumbs(); + const camInput = document.getElementById('camera-input'); const galInput = document.getElementById('gallery-input'); document.getElementById('btn-take-photo').onclick = () => camInput.click(); @@ -165,7 +167,8 @@ router.on('/orders/:id', async (args) => { try { const np = await api.listOrderPhotos(args.id); document.getElementById('photo-grid').innerHTML = - np.photos.map(p => `
`).join(''); + np.photos.map(p => `
`).join(''); + loadThumbs(); } catch (e) {} } camInput.addEventListener('change', () => handleFiles(camInput.files)); @@ -211,11 +214,28 @@ async function resizeImage(file, maxSide) { }); } -function getPhotoUrl(relpath) { - // Wir haben keinen direkten "Read-Photo"-Endpoint im Bericht-Modul, - // nutzen stattdessen page_image.php der Anhänge — funktioniert nur via document.php - // Vereinfacht: Dolibarr's document.php mit modulepart=ecm - return window.location.origin + '/document.php?modulepart=commande&file=' + encodeURIComponent(relpath.replace(/^commande\//, '')); +/** + * Lädt alle sichtbaren Thumbnails im aktuellen Photo-Grid. + * Nutzt /api/photo.php mit JWT (Header kann nicht schicken, + * deshalb Blob-URLs). + */ +async function loadThumbs() { + const thumbs = document.querySelectorAll('.photo-grid .thumb[data-relpath]'); + for (const t of thumbs) { + const rel = t.dataset.relpath; + try { + // Erst Thumbnail versuchen (_small), bei Misserfolg das Original + let url = await api.getPhotoBlobUrl(rel, 'small'); + if (!url) url = await api.getPhotoBlobUrl(rel); + if (url) { + t.innerHTML = ''; + } else { + t.innerHTML = '
'; + } + } catch (e) { + t.innerHTML = '
'; + } + } } router.on('/reports', async () => { diff --git a/lib/api.js b/lib/api.js index 880901f..a003e04 100644 --- a/lib/api.js +++ b/lib/api.js @@ -84,9 +84,39 @@ return request('/reports.php?id=' + id); } + /** + * Lädt eine Bild-Datei von der API als Blob-URL (inkl. JWT). + * Wird benötigt weil keine Authorization-Header mitschickt. + */ + const blobUrlCache = new Map(); + async function getPhotoBlobUrl(relpath, size) { + const key = (size || 'full') + '|' + relpath; + if (blobUrlCache.has(key)) return blobUrlCache.get(key); + + const t = await getToken(); + if (!t) return null; + const params = new URLSearchParams({ relpath }); + if (size) params.set('size', size); + + const r = await fetch(API_BASE + '/photo.php?' + params.toString(), { + headers: { Authorization: 'Bearer ' + t }, + }); + if (!r.ok) return null; + const blob = await r.blob(); + const url = URL.createObjectURL(blob); + blobUrlCache.set(key, url); + return url; + } + + function clearPhotoCache() { + for (const url of blobUrlCache.values()) URL.revokeObjectURL(url); + blobUrlCache.clear(); + } + window.api = { getToken, setToken, clearToken, login, logout, listOrders, getOrder, listOrderPhotos, uploadOrderPhoto, getReport, + getPhotoBlobUrl, clearPhotoCache, }; })();