baustelle-pwa/lib/api.js
Eduard Wisch 503d36bd09
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 0s
feat: Kundenkarten-Tab + Unterschriften-Härtung im Signature-Modal
Kundenkarten:
- Neuer Bottom-Nav Eintrag '👥 Kunden'
- /customers Route: Liste mit Suche, gefiltert über api/customers.php
- /customers/:id Route: Stammdaten mit Click-to-Call, Click-to-Mail,
  Google-Maps-Route-Button, Listen für Aufträge/Berichte/Rechnungen
  mit Mini-Card-Layout
- Mini-Cards navigieren zum jeweiligen Auftrag/Bericht
- formatDate + formatEur Helper (de-DE Locale)

Unterschriften-Härtung (Signature-Modal):
- Pflicht-Input für Signer-Name
- GPS-Checkbox (default an) — fragt navigator.geolocation ab
- Rechtstext als Hinweis
- Beim Save: Name + GPS werden an api.uploadSignature übergeben
- Server brennt Metadaten, Hash und GPS in die PNG ein

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
2026-04-09 08:06:22 +02:00

205 lines
6.8 KiB
JavaScript

/* 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 getReport(id) {
return request('/reports.php?id=' + id);
}
async function listReports() {
return request('/reports.php');
}
async function finalizeReport(id) {
return request('/reports.php?id=' + id + '&action=finalize', { method: 'POST' });
}
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 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 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 <img src> 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();
}
window.api = {
getToken, setToken, clearToken,
login, logout,
listOrders, getOrder, listOrderPhotos, uploadOrderPhoto,
listCustomers, getCustomer,
getReport, listReports, finalizeReport,
deletePhoto, uploadVoiceNote, uploadAnnotatedPhoto,
getPhotoBlobUrl, clearPhotoCache,
deletePage, updatePageNote, uploadSignature, getPdfBlobUrl,
};
})();