diff --git a/README.md b/README.md index aa5afef..4db4c17 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,10 @@ Mobile Progressive Web App für die Baustellen-Doku — Foto-Upload, Sprach- und - ✅ **Web Share Target** — aus WhatsApp & Co. Bilder **und** Beschreibungstext an „Baustelle" teilen: anmelden (falls nötig, direkt in der Teilen-Seite), Auftrag suchen und wählen, ablegen. Bilder gehen über die normale Upload-Queue, der Text wird als Notiz (`notiz_*.txt`) im Auftrag gespeichert +- ✅ **Serie ohne Sucherei**: die Teilen-Seite merkt sich die letzten drei benutzten Aufträge und zeigt + sie unter „Zuletzt benutzt" ganz oben. Wurde einer davon in den letzten 30 Minuten benutzt, ist er + **vorausgewählt** — die zweite und dritte Nachricht der Kundin kosten dann nur noch einen Tipp. + Danach steht er nur noch als Schnellzugriff da, damit nichts versehentlich im Auftrag von vorgestern landet - ✅ Nativer Android-Share-Sheet für Fotos, PDFs und Dokumente (`navigator.share`) — direkt an WhatsApp / Signal / Mail - ✅ Mehrfachauswahl in Foto- und Dokument-Liste mit Batch-Teilen - ✅ Android-Hardware-Back-Button schließt Modale/Ansichten sauber (History-Modal-Stack), bricht Mehrfachauswahl ab statt App zu verlassen diff --git a/ROADMAP.md b/ROADMAP.md index a3702f2..25b5ee9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -150,6 +150,9 @@ Vorher endete jeder Teilen-Vorgang in einer Sackgasse: `api/note.php`. Schlägt die Notiz fehl (kein Netz), bleibt der Text gesichert und der Vorgang ist wiederholbar - [x] Banner in der App, wenn noch etwas Geteiltes unabgelegt bereitliegt +- [x] **Mehrere Nachrichten hintereinander**: die letzten drei Aufträge stehen unter „Zuletzt benutzt" + oben; innerhalb von 30 Minuten ist der letzte vorausgewählt (danach nur noch Schnellzugriff — + eine Dauer-Vorauswahl würde irgendwann im falschen Auftrag landen) ## Erledigt diff --git a/share.html b/share.html index d1cdbb0..4663d08 100644 --- a/share.html +++ b/share.html @@ -54,6 +54,11 @@ * anmelden"), sondern über das awl_sso-Cookie, geprüft an api/verify.php. */ const SHARE_KEY = 'shared_payload'; +const RECENT_KEY = 'share_recent_orders'; +/* Wer mehrere Sachen hintereinander teilt, will nicht jedes Mal suchen. Innerhalb dieser + * Spanne ist der zuletzt benutzte Auftrag vorausgewaehlt — danach steht er nur noch oben + * als Schnellzugriff, damit nichts versehentlich im Auftrag von vorgestern landet. */ +const VORWAHL_MS = 30 * 60 * 1000; const main = () => document.getElementById('main'); window.showToast = function (msg, kind) { @@ -74,6 +79,8 @@ document.getElementById('back-btn').addEventListener('click', () => { location.h let payload = null; // { title, text, url, files:[{name,type,blob}] } let orders = []; let selectedId = null; +let selectedRef = ''; +let recent = []; // zuletzt benutzte Auftraege, neuester zuerst let searchTimer = null; function toBlob(f) { @@ -148,7 +155,7 @@ function renderLogin() { try { await api.login(fd.get('login'), fd.get('password')); showToast('Angemeldet'); - renderPicker(); + await renderPicker(); } catch (err) { showToast(err.message || 'Anmeldung fehlgeschlagen', 'error'); } @@ -163,7 +170,7 @@ function renderLogin() { const res = await awlauthWebAuthn.login(); if (res && res.user) await idb.set('user', res.user); showToast('Angemeldet'); - renderPicker(); + await renderPicker(); } catch (err) { pk.disabled = false; showToast((err && err.message) || 'Anmeldung fehlgeschlagen', 'error'); @@ -172,6 +179,60 @@ function renderLogin() { } } +/* ----- Zuletzt benutzte Auftraege ----- */ +async function ladeRecent() { + const liste = await idb.get(RECENT_KEY).catch(() => null); + return Array.isArray(liste) ? liste : []; +} + +async function merkeAuftrag(o) { + const eintrag = { id: String(o.id), ref: o.ref || '', name: o.name || '', ort: o.ort || '', ts: Date.now() }; + const rest = recent.filter(x => String(x.id) !== eintrag.id); + await idb.set(RECENT_KEY, [eintrag].concat(rest).slice(0, 3)).catch(() => {}); +} + +function seitdem(ts) { + const min = Math.round((Date.now() - ts) / 60000); + if (min < 1) return 'gerade eben'; + if (min < 60) return 'vor ' + min + ' Min.'; + const std = Math.round(min / 60); + if (std < 24) return 'vor ' + std + ' Std.'; + const tage = Math.round(std / 24); + return tage === 1 ? 'gestern' : 'vor ' + tage + ' Tagen'; +} + +/** Auswahl setzen — gilt ueber beide Listen hinweg (Schnellzugriff und Suchtreffer). */ +function waehleAuftrag(id, ref, name, ort) { + selectedId = String(id); + selectedRef = ref || ''; + document.querySelectorAll('.order-card').forEach(c => c.classList.toggle('selected', c.dataset.id === selectedId)); + const btn = document.getElementById('btn-store'); + if (!btn) return; + btn.disabled = false; + btn.textContent = 'In ' + (selectedRef || 'Auftrag') + ' ablegen'; + btn.dataset.name = name || ''; + btn.dataset.ort = ort || ''; +} + +function karteHtml(o) { + const kunde = o.name !== undefined ? o.name : ((o.customer && o.customer.name) || ''); + const ort = o.ort !== undefined ? o.ort : (((o.customer && o.customer.zip) || '') + ' ' + ((o.customer && o.customer.town) || '')); + return ` +
+
${esc(o.ref)}
+ ${o.ref_client ? `
${esc(o.ref_client)}
` : ''} +
${esc(kunde)}
+
${esc(ort.trim())}
+
`; +} + +function bindeKarten(wurzel) { + wurzel.querySelectorAll('.order-card').forEach(c => { + c.addEventListener('click', () => waehleAuftrag(c.dataset.id, c.dataset.ref, c.dataset.name, c.dataset.ort)); + }); +} + /* ----- Auswahl: Text prüfen, Auftrag wählen ----- */ function defaultSubject() { // Der Betreff steht spaeter im Dateinamen — deshalb kurz und an der Wortgrenze @@ -190,8 +251,11 @@ function defaultText() { return parts.join('\n\n'); } -function renderPicker() { +async function renderPicker() { const n = payload.files.length; + recent = await ladeRecent(); + // Frisch benutzt heisst: der Nutzer teilt gerade eine Serie -> vorauswaehlen + const vorwahl = recent[0] && (Date.now() - recent[0].ts) < VORWAHL_MS ? recent[0] : null; main().innerHTML = ` ${n ? `
@@ -205,8 +269,15 @@ function renderPicker() {
+ ${recent.length ? `
-

Auftrag wählen

+

Zuletzt benutzt

+ ${recent.map(o => karteHtml(o)).join('')} + ${vorwahl ? `

Vorausgewählt — zuletzt ${esc(seitdem(vorwahl.ts))} benutzt

` : ''} +
` : ''} + +
+

${recent.length ? 'Anderer Auftrag' : 'Auftrag wählen'}

@@ -239,6 +310,8 @@ function renderPicker() { }); document.getElementById('btn-store').addEventListener('click', store); + bindeKarten(main()); // Schnellzugriff klickbar machen + if (vorwahl) waehleAuftrag(vorwahl.id, vorwahl.ref, vorwahl.name, vorwahl.ort); loadOrders(''); } @@ -254,24 +327,8 @@ async function loadOrders(q) { box.innerHTML = '
🔍
Kein Auftrag gefunden
'; return; } - box.innerHTML = orders.map(o => ` -
-
${esc(o.ref)}
- ${o.ref_client ? `
${esc(o.ref_client)}
` : ''} -
${esc((o.customer && o.customer.name) || '')}
-
${esc(((o.customer && o.customer.zip) || '') + ' ' + ((o.customer && o.customer.town) || ''))}
-
- `).join(''); - box.querySelectorAll('.order-card').forEach(c => { - c.addEventListener('click', () => { - selectedId = c.dataset.id; - box.querySelectorAll('.order-card').forEach(x => x.classList.toggle('selected', x === c)); - const btn = document.getElementById('btn-store'); - btn.disabled = false; - const o = orders.find(x => String(x.id) === String(selectedId)); - btn.textContent = 'In ' + ((o && o.ref) || 'Auftrag') + ' ablegen'; - }); - }); + box.innerHTML = orders.map(o => karteHtml(o)).join(''); + bindeKarten(box); } catch (e) { box.innerHTML = '
' + esc(e.message || 'Fehler') + '
'; } @@ -319,6 +376,8 @@ async function store() { } await clearPayload(); + // Fuer die naechste Ladung merken — dann steht der Auftrag oben und ist vorausgewaehlt + await merkeAuftrag({ id: orderId, ref: selectedRef, name: btn.dataset.name, ort: btn.dataset.ort }); await offline.syncQueue().catch(() => {}); showToast('Abgelegt'); location.replace('./#/orders/' + orderId); @@ -334,7 +393,7 @@ async function store() { return; } try { - if (await isLoggedIn()) renderPicker(); + if (await isLoggedIn()) await renderPicker(); else renderLogin(); } catch (e) { main().innerHTML = `
📴