diff --git a/app.css b/app.css index fb732e1..2579ad6 100644 --- a/app.css +++ b/app.css @@ -439,6 +439,38 @@ body { .btn[disabled] { opacity: 0.5; cursor: not-allowed; } +/* Mini-Cards für Kundendetail (Aufträge/Rechnungen/Berichte) */ +.mini-card { + background: #2a2a30; + border-radius: 6px; + padding: 10px 12px; + margin-bottom: 6px; + cursor: pointer; + transition: background 0.15s; +} +.mini-card:active { background: #35353b; } +.mini-card .mini-ref { + font-weight: 600; + color: #7aa2f7; + font-size: 13px; +} +.mini-card .mini-meta { + font-size: 12px; + opacity: 0.7; + margin-top: 3px; +} + +.customer-actions { + display: flex; + gap: 8px; + margin-top: 12px; +} +.customer-actions a { + text-decoration: none; + text-align: center; + flex: 1; +} + /* Report-Page-Thumb mit Nummer */ .report-page-thumb { position: relative; } .report-page-thumb .page-num { @@ -453,6 +485,36 @@ body { } /* Unterschrift-Modal */ +.signature-modal .signature-meta { + padding: 12px 16px; + background: #1a1a1f; + border-bottom: 1px solid #333; + display: flex; + flex-direction: column; + gap: 8px; +} +.signature-modal .signature-meta input[type="text"] { + width: 100%; + padding: 12px 14px; + background: #2a2a30; + color: #fff; + border: 1px solid #444; + border-radius: 6px; + font-size: 15px; + box-sizing: border-box; +} +.signature-modal .sig-gps-label { + color: #e0e0e0; + font-size: 13px; + display: flex; + align-items: center; + gap: 8px; +} +.signature-modal .sig-legal { + color: #999; + font-size: 11px; + line-height: 1.4; +} .signature-modal .signature-toolbar { display: flex; align-items: center; diff --git a/app.js b/app.js index 5914ade..6fb676c 100644 --- a/app.js +++ b/app.js @@ -307,6 +307,135 @@ async function loadThumbs() { } } +router.on('/customers', async () => { + if (!(await ensureAuth())) return; + title('Kunden'); + setNav(true, 'customers'); + setBack(false); + showLoader('Lade Kunden…'); + + try { + const data = await api.listCustomers(); + if (!data.customers.length) { + main().innerHTML = '
👥
Keine Kunden
'; + return; + } + main().innerHTML = ` + +
${renderCustomerList(data.customers)}
+ `; + bindCustomerCards(); + document.getElementById('cust-search').addEventListener('input', async (e) => { + const d = await api.listCustomers({ q: e.target.value }); + document.getElementById('cust-list').innerHTML = renderCustomerList(d.customers); + bindCustomerCards(); + }); + } catch (e) { + main().innerHTML = '
⚠️
' + e.message + '
'; + } +}); + +function renderCustomerList(list) { + return list.map(c => ` +
+
${escapeHtml(c.name || '')}
+
${escapeHtml((c.zip || '') + ' ' + (c.town || '')).trim() || '—'}
+
+ ${c.phone ? '📞 ' + escapeHtml(c.phone) : ''} + ${c.bericht_count > 0 ? `📑 ${c.bericht_count}` : ''} +
+
+ `).join(''); +} + +function bindCustomerCards() { + document.querySelectorAll('.customer-card').forEach(c => { + c.addEventListener('click', () => router.go('#/customers/' + c.dataset.id)); + }); +} + +router.on('/customers/:id', async (args) => { + if (!(await ensureAuth())) return; + setNav(true, 'customers'); + setBack(true, '#/customers'); + showLoader('Lade Kunde…'); + + try { + const data = await api.getCustomer(args.id); + title(data.customer.name); + + const addr = [data.customer.address, (data.customer.zip || '') + ' ' + (data.customer.town || '')] + .map(s => (s || '').trim()).filter(Boolean).join('\n'); + + main().innerHTML = ` +
+

Stammdaten

+

${escapeHtml(data.customer.name)}

+ ${data.customer.code ? `

Kundennr: ${escapeHtml(data.customer.code)}

` : ''} + ${addr ? `

${escapeHtml(addr).replace(/\n/g, '
')}

` : ''} + ${data.customer.phone ? `

📞 ${escapeHtml(data.customer.phone)}

` : ''} + ${data.customer.email ? `

✉ ${escapeHtml(data.customer.email)}

` : ''} + ${data.customer.siret || data.customer.vat ? `

USt-ID: ${escapeHtml(data.customer.vat || '')}

` : ''} +
+ ${data.customer.address || data.customer.town ? `🗺 Route` : ''} +
+
+ + ${data.orders.length ? ` +
+

Aufträge (${data.orders.length})

+ ${data.orders.map(o => ` +
+
${escapeHtml(o.ref)}
+
${formatDate(o.date)} · ${formatEur(o.total)}${o.bericht_count > 0 ? ' · 📑 ' + o.bericht_count : ''}
+
+ `).join('')} +
` : ''} + + ${data.reports.length ? ` +
+

Berichte (${data.reports.length})

+ ${data.reports.map(r => ` +
+
${escapeHtml(r.ref)} ${r.status === 1 ? 'Final' : 'Entwurf'}
+
${escapeHtml(r.titel || '')} · ${formatDate(r.datec)}
+
+ `).join('')} +
` : ''} + + ${data.invoices.length ? ` +
+

Rechnungen (${data.invoices.length})

+ ${data.invoices.map(i => ` +
+
${escapeHtml(i.ref)}
+
${formatDate(i.date)} · ${formatEur(i.total)}
+
+ `).join('')} +
` : ''} + `; + + document.querySelectorAll('.mini-card[data-order-id]').forEach(el => { + el.addEventListener('click', () => router.go('#/orders/' + el.dataset.orderId)); + }); + document.querySelectorAll('.mini-card[data-report-id]').forEach(el => { + el.addEventListener('click', () => router.go('#/reports/' + el.dataset.reportId)); + }); + } catch (e) { + main().innerHTML = '
⚠️
' + e.message + '
'; + } +}); + +function formatDate(unix) { + if (!unix) return '—'; + const d = new Date(unix * 1000); + return d.toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' }); +} +function formatEur(v) { + if (v == null) return ''; + return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(v); +} + router.on('/reports', async () => { if (!(await ensureAuth())) return; title('Berichte'); @@ -538,6 +667,14 @@ function openSignatureModal(berichtId) {
✍️ Kunden-Unterschrift
+
+ + + +
Mit dem Finger unterschreiben @@ -592,11 +729,32 @@ function openSignatureModal(berichtId) { window.removeEventListener('resize', fitCanvas); modal.remove(); }; - modal.querySelector('#sig-save').onclick = () => { + modal.querySelector('#sig-save').onclick = async () => { + const signer = modal.querySelector('#sig-name').value.trim(); + if (!signer) { + showToast('Bitte Namen des Unterzeichners eingeben', 'error'); + modal.querySelector('#sig-name').focus(); + return; + } + // GPS holen wenn aktiviert + let gps = null; + if (modal.querySelector('#sig-gps').checked && 'geolocation' in navigator) { + showToast('Hole GPS-Position…'); + try { + const pos = await new Promise((res, rej) => { + navigator.geolocation.getCurrentPosition(res, rej, { timeout: 8000, enableHighAccuracy: true }); + }); + gps = { lat: pos.coords.latitude, lon: pos.coords.longitude }; + } catch (e) { + console.warn('GPS failed', e); + } + } showToast('Speichere Unterschrift…'); canvas.toBlob(async (blob) => { try { - await api.uploadSignature(berichtId, blob); + const opts = { signer_name: signer }; + if (gps) { opts.gps_lat = gps.lat; opts.gps_lon = gps.lon; } + await api.uploadSignature(berichtId, blob, opts); showToast('✓ Unterschrift hinzugefügt'); modal.remove(); router.navigate(); diff --git a/index.php b/index.php index d8208eb..81c28dd 100644 --- a/index.php +++ b/index.php @@ -51,6 +51,7 @@ header('Expires: 0'); diff --git a/lib/api.js b/lib/api.js index 8c24fe1..a746a5a 100644 --- a/lib/api.js +++ b/lib/api.js @@ -55,6 +55,17 @@ 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); @@ -121,9 +132,12 @@ }); } - async function uploadSignature(berichtId, pngBlob) { + 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, @@ -182,6 +196,7 @@ getToken, setToken, clearToken, login, logout, listOrders, getOrder, listOrderPhotos, uploadOrderPhoto, + listCustomers, getCustomer, getReport, listReports, finalizeReport, deletePhoto, uploadVoiceNote, uploadAnnotatedPhoto, getPhotoBlobUrl, clearPhotoCache,