/* API-Client für die Bericht-REST-API */ (function () { // API-Base wird aus der aktuellen Origin gebaut. // PWA läuft unter /baustelle/, API unter /custom/bericht/api/ const API_BASE = window.location.origin + '/custom/bericht/api'; let cachedToken = null; async function getToken() { if (cachedToken) return cachedToken; cachedToken = await idb.get('jwt'); return cachedToken; } async function setToken(t) { cachedToken = t; await idb.set('jwt', t); } async function clearToken() { cachedToken = null; await idb.del('jwt'); await idb.del('user'); } async function request(path, opts = {}) { const t = await getToken(); const headers = opts.headers || {}; if (t && !headers.Authorization) headers.Authorization = 'Bearer ' + t; if (!headers['Content-Type'] && !(opts.body instanceof FormData)) { headers['Content-Type'] = 'application/json'; } const r = await fetch(API_BASE + path, { ...opts, headers }); if (r.status === 401) { await clearToken(); window.location.hash = '#/login'; throw new Error('Nicht authentifiziert'); } const data = await r.json().catch(() => ({})); if (!r.ok) throw new Error(data.error || 'API-Fehler'); return data; } async function login(loginName, password) { const r = await request('/auth.php', { method: 'POST', body: JSON.stringify({ login: loginName, password }), }); await setToken(r.token); await idb.set('user', r.user); return r; } async function logout() { await clearToken(); } async function listCustomers(opts = {}) { const params = new URLSearchParams(); if (opts.q) params.set('q', opts.q); const qs = params.toString(); return request('/customers.php' + (qs ? '?' + qs : '')); } async function getCustomer(id) { return request('/customers.php?id=' + id); } async function listOrders(opts = {}) { const params = new URLSearchParams(); if (opts.q) params.set('q', opts.q); if (opts.open) params.set('open', '1'); const qs = params.toString(); return request('/orders.php' + (qs ? '?' + qs : '')); } async function getOrder(id) { return request('/orders.php?id=' + id); } async function listOrderPhotos(id) { return request('/orders.php?id=' + id + '&action=photos'); } async function uploadOrderPhoto(orderId, fileBlob, filename) { const fd = new FormData(); fd.append('file', fileBlob, filename || 'photo.jpg'); return request('/orders.php?id=' + orderId + '&action=upload_photo', { method: 'POST', body: fd, }); } async function createOrder(payload) { return request('/orders.php?action=create', { method: 'POST', body: JSON.stringify(payload), }); } async function getReport(id) { return request('/reports.php?id=' + id); } async function listReports() { return request('/reports.php'); } async function createReport(opts) { return request('/reports.php?action=create', { method: 'POST', body: JSON.stringify(opts), }); } async function listTemplates() { return request('/templates.php'); } async function listOdtTemplates() { return request('/odt_templates.php'); } async function finalizeReport(id) { return request('/reports.php?id=' + id + '&action=finalize', { method: 'POST' }); } async function deleteReport(id) { return request('/reports.php?id=' + id, { method: 'DELETE' }); } async function deletePhoto(relpath) { return request('/delete_photo.php', { method: 'POST', body: JSON.stringify({ relpath }), }); } async function uploadVoiceNote(orderId, audioBlob, filename) { const fd = new FormData(); fd.append('file', audioBlob, filename || 'voice.webm'); return request('/voice.php?order_id=' + orderId, { method: 'POST', body: fd }); } async function transcribeAudio(relpath) { return request('/transcribe.php', { method: 'POST', body: JSON.stringify({ relpath }), }); } async function listMaterials(elementType, elementId) { return request('/materials.php?element_type=' + elementType + '&element_id=' + elementId); } async function addMaterial(elementType, elementId, data) { return request('/materials.php?element_type=' + elementType + '&element_id=' + elementId, { method: 'POST', body: JSON.stringify(data), }); } async function deleteMaterial(id) { return request('/materials.php?id=' + id + '&delete=1', { method: 'POST' }); } async function uploadAnnotatedPhoto(orderId, fileBlob, filename) { // Wie uploadOrderPhoto — Skizze ist schon ins Bild eingebrannt return uploadOrderPhoto(orderId, fileBlob, filename); } async function deletePage(pageId) { return request('/pages.php?id=' + pageId, { method: 'DELETE' }); } async function updatePageNote(pageId, note) { return request('/pages.php?id=' + pageId, { method: 'POST', body: JSON.stringify({ note }), }); } async function uploadSignature(berichtId, pngBlob, opts) { const fd = new FormData(); fd.append('file', pngBlob, 'signature.png'); if (opts && opts.signer_name) fd.append('signer_name', opts.signer_name); if (opts && opts.gps_lat != null) fd.append('gps_lat', opts.gps_lat); if (opts && opts.gps_lon != null) fd.append('gps_lon', opts.gps_lon); return request('/pages.php?action=signature&bericht_id=' + berichtId, { method: 'POST', body: fd, }); } async function reorderPages(pageIds) { return request('/pages.php?action=reorder', { method: 'POST', body: JSON.stringify({ order: pageIds }), }); } async function getPdfBlobUrl(berichtId) { const t = await getToken(); if (!t) return null; const params = new URLSearchParams({ id: berichtId, jwt: t }); const r = await fetch(API_BASE + '/pdf.php?' + params.toString()); if (!r.ok) return null; const blob = await r.blob(); return URL.createObjectURL(blob); } /** * 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; // JWT als Query-Param, weil Apache auf prod den Authorization-Header filtert const params = new URLSearchParams({ relpath, jwt: t }); if (size) params.set('size', size); const r = await fetch(API_BASE + '/photo.php?' + params.toString()); if (!r.ok) { const body = await r.text().catch(() => ''); console.warn('photo.php failed', r.status, relpath, body); return null; } const ct = r.headers.get('Content-Type') || ''; if (!ct.startsWith('image/')) { const body = await r.text().catch(() => ''); console.warn('photo.php not an image', ct, body); 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(); } // 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, getToken, setToken, clearToken, login, logout, listOrders, getOrder, listOrderPhotos, uploadOrderPhoto, createOrder, listCustomers, getCustomer, getReport, listReports, createReport, listTemplates, listOdtTemplates, finalizeReport, deleteReport, deletePhoto, uploadVoiceNote, transcribeAudio, uploadAnnotatedPhoto, listMaterials, addMaterial, deleteMaterial, getPhotoBlobUrl, clearPhotoCache, getFileBlobUrl, deletePage, updatePageNote, uploadSignature, getPdfBlobUrl, reorderPages, }; })();