From d4e4d09366fee6c48e50103849ce230dc477d049 Mon Sep 17 00:00:00 2001 From: Eddy Date: Thu, 16 Apr 2026 17:42:40 +0200 Subject: [PATCH] [deploy] Schnell-Auftrag-Erfassen via FAB + Login-Persistenz-Fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FAB unten rechts (halbtransparent) öffnet Fullscreen-Modal zur Auftrags-Anlage: Kundensuche mit Debounce und "Zuletzt verwendet"- Quick-Pick aus IndexedDB → Anzeige der übernommenen Defaults → Titel + optional Kunden-Ref → direkter Sprung auf #/orders/ wo die Fotos aufgenommen werden können. - appBoot() preloadet das JWT aktiv aus IndexedDB bevor der Router läuft — fixt "nach App-Neustart immer wieder einloggen müssen". - setNav() steuert zusätzlich die FAB-Sichtbarkeit mit. - api.createOrder() als neuer Wrapper für POST /orders.php?action=create. Co-Authored-By: Claude Opus 4.6 (1M context) --- app.css | 65 ++++++++++++++++++ app.js | 197 +++++++++++++++++++++++++++++++++++++++++++++++++++++ index.php | 2 + lib/api.js | 9 ++- 4 files changed, 272 insertions(+), 1 deletion(-) diff --git a/app.css b/app.css index fe3d8de..a0cefb0 100644 --- a/app.css +++ b/app.css @@ -72,6 +72,71 @@ body { } #bottom-nav button.active { color: #7aa2f7; } +/* ----- FAB (Neuer Auftrag) ----- */ +.fab { + position: fixed; + right: 18px; + bottom: calc(68px + env(safe-area-inset-bottom, 0px)); + width: 56px; + height: 56px; + border-radius: 50%; + border: none; + font-size: 30px; + line-height: 1; + font-weight: 300; + background: rgba(122, 162, 247, 0.85); + color: #fff; + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + z-index: 50; + cursor: pointer; + transition: transform 0.1s ease, background 0.15s ease; +} +.fab:active { transform: scale(0.92); background: rgba(122, 162, 247, 1); } +.fab[hidden] { display: none; } + +/* ----- Neuer Auftrag Modal (nutzt .fullscreen-modal) ----- */ +.new-order-customer-row { + display: flex; + gap: 10px; + padding: 10px 12px; + background: #2a2a30; + border: 1px solid #3a3a42; + border-radius: 8px; + margin-bottom: 10px; + align-items: center; +} +.new-order-customer-row .nc-name { font-weight: 600; flex: 1; } +.new-order-customer-row .nc-meta { color: #888; font-size: 12px; } +.new-order-customer-row .nc-change { + background: transparent; border: 1px solid #555; + color: #7aa2f7; border-radius: 6px; padding: 4px 10px; cursor: pointer; +} +.new-order-defaults { + font-size: 12px; color: #aaa; + padding: 6px 12px; margin-bottom: 10px; + border-left: 2px solid #7aa2f7; +} +.new-order-customer-search { + display: block; width: 100%; + padding: 12px; border-radius: 8px; + border: 1px solid #444; background: #2a2a30; color: #eee; + font-size: 15px; margin-bottom: 8px; +} +.new-order-customer-list { + max-height: 45vh; overflow-y: auto; + border: 1px solid #333; border-radius: 8px; background: #222228; +} +.new-order-customer-list .nc-item { + padding: 12px; border-bottom: 1px solid #2a2a30; cursor: pointer; +} +.new-order-customer-list .nc-item:last-child { border-bottom: none; } +.new-order-customer-list .nc-item:active { background: #2e2e35; } +.new-order-customer-list .nc-item .nc-sub { color: #888; font-size: 12px; } +.new-order-customer-list .nc-item.recent { background: rgba(122,162,247,0.06); } +.new-order-empty { padding: 16px; color: #888; text-align: center; } + /* ----- Login ----- */ .login-form { max-width: 360px; diff --git a/app.js b/app.js index e44c64b..768f18f 100644 --- a/app.js +++ b/app.js @@ -21,6 +21,9 @@ function setNav(visible, active) { nav.querySelectorAll('button').forEach(b => { b.classList.toggle('active', b.dataset.route === active); }); + // FAB folgt der Nav-Sichtbarkeit (auf Login verstecken, sonst überall erreichbar) + const fab = document.getElementById('fab-new-order'); + if (fab) fab.hidden = !visible; } function setBack(visible, hash) { @@ -173,6 +176,23 @@ async function promptNewPin() { * Startet die App — fragt ggf. PIN ab bevor router läuft. */ window.appBoot = async function appBoot() { + // JWT aktiv aus IndexedDB preloaden, bevor eine Route rennt. + // Verhindert Race-Conditions, in denen ensureAuth() zu früh ein `null` bekommt + // und nach Login-Screen redirectet, obwohl ein gültiges Token in IDB liegt. + try { + const t = await api.getToken(); + console.log('[boot] jwt vorhanden:', !!t); + } catch (e) { + console.warn('[boot] Token-Preload fehlgeschlagen', e); + } + + // FAB: Click öffnet das Schnell-Auftrag-Modal + const fab = document.getElementById('fab-new-order'); + if (fab && !fab.dataset.bound) { + fab.dataset.bound = '1'; + fab.addEventListener('click', () => { openNewOrderModal(); }); + } + const pinSet = await idb.get('pin_hash'); if (!pinSet) return; // kein PIN-Schutz aktiv // Lockscreen: vollständiger Overlay bis richtige PIN @@ -1241,6 +1261,183 @@ async function openNewReportModal(orderId) { }; } +/* ============================================================ + * NEUER AUFTRAG MODAL (Schnell-Erfassung vom FAB) + * ============================================================ */ +async function openNewOrderModal() { + if (!(await ensureAuth())) return; + + const modal = document.createElement('div'); + modal.className = 'fullscreen-modal'; + modal.innerHTML = ` +
+ +
➕ Neuer Auftrag
+ +
+
+
+ + +
+
+ + +
+ `; + document.body.appendChild(modal); + + const closeBtn = modal.querySelector('#no-close'); + const saveBtn = modal.querySelector('#no-save'); + const searchIn = modal.querySelector('#no-customer-search'); + const listEl = modal.querySelector('#no-customer-list'); + const stepCust = modal.querySelector('#no-step-customer'); + const stepDet = modal.querySelector('#no-step-details'); + const socName = modal.querySelector('#no-soc-name'); + const socMeta = modal.querySelector('#no-soc-meta'); + const defaultsEl = modal.querySelector('#no-defaults'); + const titleIn = modal.querySelector('#no-title'); + const refIn = modal.querySelector('#no-refclient'); + const changeBtn = modal.querySelector('#no-change-customer'); + + let selectedCustomer = null; + + closeBtn.onclick = () => modal.remove(); + + /* ---- Letzte Kunden als Quick-Pick ---- */ + async function renderRecent() { + const recent = (await idb.get('recent_customers')) || []; + if (!recent.length) { + listEl.innerHTML = '
Tippe zum Suchen…
'; + return; + } + listEl.innerHTML = '
Zuletzt verwendet
' + + recent.map(c => ` +
+
${escapeHtml(c.name)}
+
${escapeHtml((c.zip || '') + ' ' + (c.town || ''))}
+
+ `).join(''); + } + await renderRecent(); + + /* ---- Suche mit Debounce ---- */ + let searchTimer = null; + searchIn.addEventListener('input', () => { + clearTimeout(searchTimer); + const q = searchIn.value.trim(); + if (!q) { renderRecent(); return; } + searchTimer = setTimeout(async () => { + try { + const data = await api.listCustomers({ q }); + const items = (data.customers || []).slice(0, 30); + if (!items.length) { + listEl.innerHTML = '
Keine Treffer
'; + return; + } + listEl.innerHTML = items.map(c => ` +
+
${escapeHtml(c.name)}
+
${escapeHtml((c.zip || '') + ' ' + (c.town || ''))}
+
+ `).join(''); + } catch (e) { + listEl.innerHTML = '
Fehler: ' + escapeHtml(e.message) + '
'; + } + }, 300); + }); + + /* ---- Kunden-Klick: Details laden, Wechsel zum zweiten Schritt ---- */ + listEl.addEventListener('click', async (e) => { + const item = e.target.closest('.nc-item'); + if (!item) return; + const id = parseInt(item.dataset.id, 10); + if (!id) return; + + showToast('Lade Kundendaten…'); + try { + const res = await api.getCustomer(id); + const c = res.customer || res; + selectedCustomer = c; + + socName.textContent = c.name || ''; + socMeta.textContent = [c.address, ((c.zip || '') + ' ' + (c.town || '')).trim()].filter(Boolean).join(' · '); + + const defs = []; + if (c.cond_reglement_label) defs.push('💳 ' + c.cond_reglement_label); + if (c.mode_reglement_label) defs.push('🏦 ' + c.mode_reglement_label); + if (!defs.length) defs.push('Keine speziellen Defaults hinterlegt'); + defaultsEl.innerHTML = 'Übernommen: ' + escapeHtml(defs.join(' · ')); + + stepCust.hidden = true; + stepDet.hidden = false; + saveBtn.disabled = false; + titleIn.focus(); + } catch (err) { + showToast('Fehler: ' + err.message, 'error'); + } + }); + + changeBtn.onclick = () => { + selectedCustomer = null; + stepDet.hidden = true; + stepCust.hidden = false; + saveBtn.disabled = true; + searchIn.focus(); + }; + + /* ---- Anlegen ---- */ + saveBtn.onclick = async () => { + if (!selectedCustomer) { showToast('Bitte zuerst Kunde wählen', 'error'); return; } + const title = titleIn.value.trim(); + if (!title) { showToast('Titel ist Pflicht', 'error'); titleIn.focus(); return; } + + saveBtn.disabled = true; + showToast('Lege Auftrag an…'); + try { + const res = await api.createOrder({ + socid: selectedCustomer.id, + title: title, + ref_client: refIn.value.trim(), + }); + // Recent-Liste in IDB aktualisieren (letzte 5) + try { + const recent = (await idb.get('recent_customers')) || []; + const filtered = recent.filter(c => c.id !== selectedCustomer.id); + filtered.unshift({ + id: selectedCustomer.id, + name: selectedCustomer.name, + zip: selectedCustomer.zip, + town: selectedCustomer.town, + }); + await idb.set('recent_customers', filtered.slice(0, 5)); + } catch (e) {} + + showToast('✓ Auftrag ' + (res.order.ref || '') + ' angelegt'); + modal.remove(); + router.go('#/orders/' + res.order.id); + } catch (e) { + showToast('Fehler: ' + e.message, 'error'); + saveBtn.disabled = false; + } + }; +} + /* ============================================================ * SEITEN-AKTIONEN MODAL (Notiz, Löschen, Vollbild) * ============================================================ */ diff --git a/index.php b/index.php index 19ce34d..62766d6 100644 --- a/index.php +++ b/index.php @@ -49,6 +49,8 @@ header('Expires: 0');
+ +