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 = '
';
+ 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 = '';
+ }
+});
+
+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 = '';
+ }
+});
+
+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
✓
+
🗑 Leeren
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');
📋 Aufträge
+ 👥 Kunden
📑 Berichte
⚙️
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,