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 = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ 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');
+
+