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,
};
})();