From 5aaf1cd9584d69bda321ac693a7db8daab8fa6e0 Mon Sep 17 00:00:00 2001 From: Tobi G Date: Mon, 17 Aug 2026 00:29:05 +0200 Subject: [PATCH] Fix pull-to-refresh on touch devices, add drag/paste receipt upload The pull-to-refresh gesture logic was already correct (verified by driving real pointer events through it); what was missing was overscroll-behavior on the scroll container, so on real touch hardware the browser's own native overscroll could take the gesture over before it hit the threshold. "Beleg hochladen" now opens a dialog instead of the native file picker: drag a PDF in at the computer, paste it from the clipboard on the phone (copied out of a mail), file picker kept as a third route. All three sources share one upload path. Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 49 +++++- .../www/audi-dashboard-app.js | 146 +++++++++++++++--- .../www/audi-dashboard-ios.css | 2 +- .../www/audi-dashboard-version.json | 2 +- .../installationspaket/www/audi-dashboard.css | 40 +++++ homeassistant/www/audi-dashboard-app.js | 146 +++++++++++++++--- homeassistant/www/audi-dashboard-ios.css | 2 +- homeassistant/www/audi-dashboard-version.json | 2 +- homeassistant/www/audi-dashboard.css | 40 +++++ 9 files changed, 374 insertions(+), 55 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a3e5c0a..4752d97 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md — Project state, review findings, open items, and working rules -**Last updated: 2026-08-16** (merged the `umsetzung-datametric360` branch — companion app phases +**Last updated: 2026-08-17** (merged the `umsetzung-datametric360` branch — companion app phases 1–10 done, see `UMSETZUNGSPLAN.md`; 2026-08-13: cleaned up remaining EU Data Act residue, fixed oversized toggle switches, and fixed a desktop-layout audit (settings button / rings logo / popups overflowing past the capped content column) — see `DESIGN_AUDIT_2026-08-13.md`; 2026-08-16: fixed @@ -28,8 +28,13 @@ audit (Navigation/Presentation/Selection & Input/Patterns categories, not just t 5-page pass) and fixed the three findings that survived: closed a second, larger silent-failure gap in `profilSpeichern()` itself (the shared save path for ~25 fields, previously an unhandled promise rejection with zero user feedback), added a spinner to the "Lädt …" bootstrap screen, and gave the -Anzugsmoment/km-correction popups the same primary-button styling the Setup popup already used — -see section C). This file is the entry point for every new agent +Anzugsmoment/km-correction popups the same primary-button styling the Setup popup already used; +2026-08-17: fixed pull-to-refresh on Übersicht not working — the JS gesture logic was already +correct (verified via live pointer-event simulation), the real gap was a missing +`overscroll-behavior` on the scroll container letting the browser's own native overscroll compete +with it on real touch devices; same day: the fuel-receipt upload got a proper dialog — drag the PDF +in at the computer, paste it from the clipboard on the phone, file picker still available as a third +route — see section C). This file is the entry point for every new agent session: what this repo is, what is finished, what is missing, and how to work here. Detail lives in the linked documents — this file points, it does not duplicate. @@ -1126,6 +1131,44 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed race-condition-timed screenshot proved too fast to catch reliably; Speichern/Abbrechen hierarchy confirmed via screenshot on both popups; error-sheet fix confirmed via the monkey-patch test above. Synced to `installationspaket/`. +- [x] Eleventh round (2026-08-17) — "why can't I pull to refresh on Übersicht", investigated as a + real bug report rather than assumed working: live-tested the gesture by dispatching real + `PointerEvent`s through the exact `pointerdown`/`pointermove`/`pointerup` handlers in + `ereignisseVerdrahten()` (the `ptrY0`/`ptrDrag` logic) — every step fired correctly (drag + threshold, "Loslassen zum Aktualisieren" label swap, spinner, `datenAktualisieren()` call, + collapse), so the app's own JS/CSS was never the defect. Root cause instead: `main#view` (the + actual scroll container) had no `overscroll-behavior` set at all, so on a **real** touchscreen + the browser's own native overscroll/pull gesture can intercept or compete with the custom drag + before it reaches the threshold — this can't reproduce via desktop testing or synthetic pointer + events, only real touch hardware, which is exactly why the live test above passed while the + user's real phone didn't work. Fixed with `overscroll-behavior-y: contain` on `main#view` plus + `overscroll-behavior: none` on the outer `:host > div` wrapper, so a gesture leaving `main#view` + never chains up into the underlying Home Assistant page either. Re-ran the same synthetic + pointer-event test after deploying — gesture still fires identically end-to-end, confirmed via + `getComputedStyle` that `overscroll-behavior-y` computes to `contain`; no visual regressions, + no new console errors. Synced to `installationspaket/`. Not yet confirmed fixed on the user's + actual device — the code-level fix is the standard remedy for this exact symptom class, but ask + if it's still reproducing before assuming this is fully closed. +- [x] Beleg-Upload: Ziehen und Einfügen (2026-08-17) — "Beleg hochladen" opened the native file + picker directly, which is the wrong affordance for both of the user's actual routes: at the + computer the PDF should be draggable in, on the phone it arrives as a PDF copied out of a mail + and needs pasting. Replaced the immediate picker with a small dialog (`belegPopup` state, + `vBelegPopup()`, rendered into the existing `#overlay` alongside `sheetMarkup()`/ + `vSetupPopup()`): a dashed drop area plus "Aus Zwischenablage einfügen" and "Datei auswählen" + (the old path, kept as the third route). The upload itself was extracted out of the click + handler into `belegDateiVerarbeiten()` so all three sources share one code path — including the + deliberate non-`serviceRufen()` error handling whose reasoning is unchanged (see its comment). + Notes worth keeping: `dragover` **must** `preventDefault()` or the browser just opens the PDF + itself and no `drop` ever fires; the drag/drop/paste listeners hang on `document`, not on the + drop zone, because the zone lives in the shadow DOM and a drag crossing its edge would + otherwise be lost — and because `render()` replaces the overlay's `innerHTML`, so per-element + listeners wouldn't survive. Phone pasting uses `navigator.clipboard.read()` (iOS shows its own + paste prompt) with the keyboard `paste` event as the desktop path. Non-PDFs are rejected with + an in-dialog message instead of being uploaded. Verified live in `audi_ha_test` by + instrumenting `hass.callService` and driving real `DragEvent`/`ClipboardEvent`/`KeyboardEvent`s: + drop → zone highlights (`darueber`), dialog closes, service called with the right filename and + base64 payload; paste → same; a dropped PNG → "Das war kein PDF." and no service call; Escape → + closes. Synced to `installationspaket/`. - [ ] Fix remaining documentation drift (statistics claim, README gaps, obsolete TODO comment) — text-only changes; INSTALL.md's WLAN/TommiG1 drift and stale variable names were fixed 2026-08-12 (see section B); `DESIGN_REVIEW_2026-08-13.md` and `REVIEW_main_2026-08-13.md` diff --git a/homeassistant/installationspaket/www/audi-dashboard-app.js b/homeassistant/installationspaket/www/audi-dashboard-app.js index 852312b..371acff 100644 --- a/homeassistant/installationspaket/www/audi-dashboard-app.js +++ b/homeassistant/installationspaket/www/audi-dashboard-app.js @@ -2648,6 +2648,10 @@ let belegFehler = null; // Entwurf auf das Ergebnis wartet - die Antwort selbst trägt dafür keine // praefix-Information, nur tank_id (bei einem Parse-Ergebnis immer null). let belegZielPraefix = null; +// Offenes Beleg-Fenster: { tankId, kontext, fehler }. Am Rechner wird der Beleg +// hineingezogen, am Telefon aus der Zwischenablage eingefügt (aus der Mail +// kopiertes PDF) - der Dateiauswahl-Knopf bleibt als dritter Weg erhalten. +let belegPopup = null; /* Wischen nach links zum Löschen (Fahrten/Tankvorgänge/Servicebuch), analog zu iOS-Listen. swipeZeile() umschließt eine bestehende Listenzeile mit @@ -2995,6 +2999,83 @@ function hinweis(titel, text) { sheet = { titel, text, knopf: null, aktion: null }; render(); } +/* ------------------------------------------------------- Beleg-Upload + Ein Weg für alle drei Quellen (Ziehen, Einfügen, Dateiauswahl). Kein + serviceRufen() hier: dessen catch loggt nur in die Konsole - siehe den + ausführlichen Kommentar an der früheren Aufrufstelle, der Grund gilt + unverändert. */ +function belegDateiVerarbeiten(datei, tankId, kontext) { + if (!datei) return; + if (datei.type && datei.type !== "application/pdf") { + belegFehler = { context: kontext, meldung: "Nur PDF-Belege werden unterstützt" }; + render(); return; + } + const leser = new FileReader(); + leser.onload = () => { + const base64 = leser.result.split(",")[1]; + HASS.callService("pyscript", "audi_dashboard_beleg_hochladen", + { pdf_base64: base64, dateiname: datei.name || "beleg.pdf", tank_id: tankId } + ).catch((err) => { + console.error("audi_dashboard: audi_dashboard_beleg_hochladen", err); + belegFehler = { context: kontext, meldung: "Beleg konnte nicht hochgeladen werden" }; + if (!tankId) belegEntwurf = null; + render(); + }); + }; + leser.readAsDataURL(datei); +} + +/* Nimmt das erste PDF aus einer DataTransfer-/Clipboard-Dateiliste. */ +function belegAusDateiliste(dateien) { + return [...(dateien || [])].find((d) => d.type === "application/pdf") || null; +} + +function belegPopupSchliessenUndLaden(datei) { + const ctx = belegPopup; + if (!ctx || !datei) return; + belegPopup = null; + belegDateiVerarbeiten(datei, ctx.tankId, ctx.kontext); + render(); +} + +/* Telefon-Weg: das aus der Mail kopierte PDF liegt in der Zwischenablage. + navigator.clipboard.read() ist der einzige Weg, ohne dass der Nutzer in ein + Eingabefeld tippen muss - iOS zeigt dafür seine eigene Einfügen-Abfrage. */ +async function belegAusZwischenablage() { + const ctx = belegPopup; + if (!ctx) return; + if (!navigator.clipboard || !navigator.clipboard.read) { + belegPopup.fehler = "Dieser Browser kann die Zwischenablage nicht lesen - bitte ziehen oder Datei auswählen."; + render(); return; + } + try { + for (const eintrag of await navigator.clipboard.read()) { + const typ = (eintrag.types || []).find((t) => t === "application/pdf"); + if (typ) { belegPopupSchliessenUndLaden(new File([await eintrag.getType(typ)], "beleg.pdf", { type: "application/pdf" })); return; } + } + belegPopup.fehler = "In der Zwischenablage liegt kein PDF."; + } catch (err) { + belegPopup.fehler = "Kein Zugriff auf die Zwischenablage."; + } + render(); +} + +function vBelegPopup() { + if (!belegPopup) return ""; + return `
+ `; +} + function sheetMarkup() { if (!sheet) return ""; const kopf = `
${esc(sheet.titel)}${sheet.text ? `${esc(sheet.text)}` : ""}
`; @@ -3157,7 +3238,7 @@ function render() { // faellt sonst bei jedem render(), z. B. beim Umschalten des Filters, // auf 0 zurueck). const setupKoerperScroll = setupOffen ? (ROOT.querySelector(".setup-koerper") || {}).scrollTop : 0; - ROOT.getElementById("overlay").innerHTML = sheetMarkup() + vSetupPopup(); + ROOT.getElementById("overlay").innerHTML = sheetMarkup() + vSetupPopup() + vBelegPopup(); if (setupOffen) { const koerper = ROOT.querySelector(".setup-koerper"); if (koerper) koerper.scrollTop = setupKoerperScroll || 0; @@ -3274,8 +3355,38 @@ function ereignisseVerdrahten() { Hintergrund wandert. Beides gehoert zum Vertrag dieser Rolle. Der Handler haengt am Dokument, weil Fokus im Shadow DOM auch ausserhalb von ROOT liegen kann. */ + /* Ziehen und Ablegen (Rechner) sowie Einfügen per Tastatur, solange das + Beleg-Fenster offen ist. Die Listener hängen am Dokument, weil ein Zug + über der Ablagefläche im Shadow DOM sonst schon am Rand verloren geht - + und weil ohne preventDefault auf dragover der Browser das PDF einfach + selbst öffnet, statt ein drop-Ereignis zu liefern. */ + document.addEventListener("dragover", (e) => { + if (!belegPopup) return; + e.preventDefault(); + const zone = ROOT.getElementById("belegDrop"); + if (zone) zone.classList.add("darueber"); + }); + document.addEventListener("dragleave", (e) => { + if (!belegPopup || e.relatedTarget) return; + const zone = ROOT.getElementById("belegDrop"); + if (zone) zone.classList.remove("darueber"); + }); + document.addEventListener("drop", (e) => { + if (!belegPopup) return; + e.preventDefault(); + const datei = belegAusDateiliste(e.dataTransfer && e.dataTransfer.files); + if (datei) { belegPopupSchliessenUndLaden(datei); return; } + belegPopup.fehler = "Das war kein PDF."; + render(); + }); + document.addEventListener("paste", (e) => { + if (!belegPopup) return; + const datei = belegAusDateiliste(e.clipboardData && e.clipboardData.files); + if (datei) { e.preventDefault(); belegPopupSchliessenUndLaden(datei); } + }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") { + if (belegPopup) { belegPopup = null; render(); e.preventDefault(); return; } if (sheet) { sheetSchliessen(); e.preventDefault(); return; } if (setupSucheOffen) { setupSucheOffen = null; setupSuchtext = ""; render(); e.preventDefault(); return; } if (setupOffen) { setupSchliessen(); e.preventDefault(); return; } @@ -3740,31 +3851,18 @@ function ereignisseVerdrahten() { const kontext = nurParsen ? praefix : ziel; if (belegFehler && belegFehler.context === kontext) belegFehler = null; belegZielPraefix = praefix; + // Statt direkt die Dateiauswahl zu öffnen: Fenster mit Ablagefläche + // (Rechner) und Einfügen-Knopf (Telefon, PDF aus der Mail kopiert). + belegPopup = { tankId, kontext, fehler: null }; + render(); + return; + } + if (e.target.closest("[data-belegzu]")) { belegPopup = null; render(); return; } + if (e.target.closest("[data-belegeinfuegen]")) { belegAusZwischenablage(); return; } + if (e.target.closest("[data-belegdatei]")) { const eingabe = document.createElement("input"); eingabe.type = "file"; eingabe.accept = "application/pdf"; - eingabe.onchange = () => { - const datei = eingabe.files[0]; if (!datei) return; - const leser = new FileReader(); - leser.onload = () => { - const base64 = leser.result.split(",")[1]; - // Kein serviceRufen() hier: dessen catch loggt nur in die Browser- - // Konsole (bei allen anderen Aufrufen ok, die reagieren auf - // datenLaden() statt auf den Rückgabewert) - ein rejizierter - // Service-Aufruf (Backend nicht erreichbar, Timeout, ...) blieb - // für den Beleg-Upload sonst komplett unsichtbar in der - // Oberfläche, obwohl der Backend-seitige Parser-Fehlerpfad selbst - // schon einen Fehler veröffentlicht (siehe belegverarbeitung.py). - HASS.callService("pyscript", "audi_dashboard_beleg_hochladen", - { pdf_base64: base64, dateiname: datei.name, tank_id: tankId } - ).catch((err) => { - console.error("audi_dashboard: audi_dashboard_beleg_hochladen", err); - belegFehler = { context: kontext, meldung: "Beleg konnte nicht hochgeladen werden" }; - if (!tankId) belegEntwurf = null; - render(); - }); - }; - leser.readAsDataURL(datei); - }; + eingabe.onchange = () => belegPopupSchliessenUndLaden(eingabe.files[0]); eingabe.click(); return; } diff --git a/homeassistant/installationspaket/www/audi-dashboard-ios.css b/homeassistant/installationspaket/www/audi-dashboard-ios.css index 83ecf0f..5bac293 100644 --- a/homeassistant/installationspaket/www/audi-dashboard-ios.css +++ b/homeassistant/installationspaket/www/audi-dashboard-ios.css @@ -316,7 +316,7 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo ruhigen Karte. max-width + margin:auto zentriert sie innerhalb des bestehenden left/right-Abstands, ohne die transform-basierte Einblend-Animation dieser Elemente zu beruehren. */ - .setup-popup,.sdpopup,.sheet{max-width:560px;margin-left:auto;margin-right:auto} + .setup-popup,.sdpopup,.sheet,.beleg-popup{max-width:560px;margin-left:auto;margin-right:auto} .standortmenu{max-width:640px;margin-left:auto;margin-right:auto} } diff --git a/homeassistant/installationspaket/www/audi-dashboard-version.json b/homeassistant/installationspaket/www/audi-dashboard-version.json index 03c0549..fa9ec75 100644 --- a/homeassistant/installationspaket/www/audi-dashboard-version.json +++ b/homeassistant/installationspaket/www/audi-dashboard-version.json @@ -1 +1 @@ -{"version": 1786916894} +{"version": 1786919196} diff --git a/homeassistant/installationspaket/www/audi-dashboard.css b/homeassistant/installationspaket/www/audi-dashboard.css index cc29dd8..1a46f0c 100644 --- a/homeassistant/installationspaket/www/audi-dashboard.css +++ b/homeassistant/installationspaket/www/audi-dashboard.css @@ -83,6 +83,12 @@ height: min(880px, calc(100vh - 24px)); height: min(880px, calc(100dvh - 24px)); min-height: 0; + /* Verhindert, dass eine Zieh-Geste, die main#view unten nach oben hin + verlaesst, an die dahinterliegende HA-Seite weitergereicht wird - sonst + kann der Browser selbst ein natives Overscroll/Pull-to-Refresh starten + und der eigene Ziehen-zum-Aktualisieren-Indikator bekommt die Geste nie + zu Ende. */ + overscroll-behavior: none; } /* ------------------------------------------------------------- Gerüst */ @@ -226,6 +232,12 @@ main#view { scroll-behavior: smooth; -webkit-overflow-scrolling: touch; scrollbar-width: none; + /* Ohne das hier greift auf echten Touch-Geraeten oft das native + Overscroll/Pull-to-Refresh des Browsers, sobald am oberen Rand weiter + nach unten gezogen wird - das nimmt der eigenen "Ziehen zum + Aktualisieren"-Geste (siehe ptrY0-Logik in ereignisseVerdrahten()) die + Kontrolle weg, obwohl deren JS/CSS für sich genommen korrekt arbeitet. */ + overscroll-behavior-y: contain; } main#view::-webkit-scrollbar { width: 0; height: 0; } @@ -915,6 +927,34 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; } padding: var(--sp-4); } +/* Beleg-Fenster: PDF hineinziehen (Rechner) oder einfügen (Telefon). */ +.beleg-catcher { + position: absolute; inset: 0; z-index: 1000; + background: rgba(0,0,0,.45); + animation: sheet-fade .18s ease-out; +} +.beleg-popup { + position: absolute; z-index: 1001; + left: var(--sp-5); right: var(--sp-5); + top: 50%; transform: translateY(-50%); + /* Wie .setup-popup: --tile-2 ist im Nacht-Theme transluzent, ein frei + schwebendes Fenster braucht einen blickdichten Grund. */ + background: var(--tile-deckend, var(--canvas)); + border-radius: var(--r-tile); + padding: var(--sp-4); +} +.beleg-drop { + margin-top: var(--sp-3); + border: 1px dashed var(--line-strong); + border-radius: 14px; + padding: 28px var(--sp-4); + text-align: center; + display: flex; flex-direction: column; gap: 6px; +} +.beleg-drop strong { font-size: 15px; color: var(--fg); } +.beleg-drop span { font-size: 12.5px; color: var(--fg2); } +.beleg-drop.darueber { border-color: var(--fg); border-style: solid; background: var(--shade); } + /* Kopfzeile der Reifen-Kachel: Überschrift+km links, Radfoto rechts, beide oben bündig - ersetzt die frühere Überlagerung (Montiert absolut über dem Foto), seit Überschrift und km ihren eigenen Platz über Montiert bekommen diff --git a/homeassistant/www/audi-dashboard-app.js b/homeassistant/www/audi-dashboard-app.js index 852312b..371acff 100644 --- a/homeassistant/www/audi-dashboard-app.js +++ b/homeassistant/www/audi-dashboard-app.js @@ -2648,6 +2648,10 @@ let belegFehler = null; // Entwurf auf das Ergebnis wartet - die Antwort selbst trägt dafür keine // praefix-Information, nur tank_id (bei einem Parse-Ergebnis immer null). let belegZielPraefix = null; +// Offenes Beleg-Fenster: { tankId, kontext, fehler }. Am Rechner wird der Beleg +// hineingezogen, am Telefon aus der Zwischenablage eingefügt (aus der Mail +// kopiertes PDF) - der Dateiauswahl-Knopf bleibt als dritter Weg erhalten. +let belegPopup = null; /* Wischen nach links zum Löschen (Fahrten/Tankvorgänge/Servicebuch), analog zu iOS-Listen. swipeZeile() umschließt eine bestehende Listenzeile mit @@ -2995,6 +2999,83 @@ function hinweis(titel, text) { sheet = { titel, text, knopf: null, aktion: null }; render(); } +/* ------------------------------------------------------- Beleg-Upload + Ein Weg für alle drei Quellen (Ziehen, Einfügen, Dateiauswahl). Kein + serviceRufen() hier: dessen catch loggt nur in die Konsole - siehe den + ausführlichen Kommentar an der früheren Aufrufstelle, der Grund gilt + unverändert. */ +function belegDateiVerarbeiten(datei, tankId, kontext) { + if (!datei) return; + if (datei.type && datei.type !== "application/pdf") { + belegFehler = { context: kontext, meldung: "Nur PDF-Belege werden unterstützt" }; + render(); return; + } + const leser = new FileReader(); + leser.onload = () => { + const base64 = leser.result.split(",")[1]; + HASS.callService("pyscript", "audi_dashboard_beleg_hochladen", + { pdf_base64: base64, dateiname: datei.name || "beleg.pdf", tank_id: tankId } + ).catch((err) => { + console.error("audi_dashboard: audi_dashboard_beleg_hochladen", err); + belegFehler = { context: kontext, meldung: "Beleg konnte nicht hochgeladen werden" }; + if (!tankId) belegEntwurf = null; + render(); + }); + }; + leser.readAsDataURL(datei); +} + +/* Nimmt das erste PDF aus einer DataTransfer-/Clipboard-Dateiliste. */ +function belegAusDateiliste(dateien) { + return [...(dateien || [])].find((d) => d.type === "application/pdf") || null; +} + +function belegPopupSchliessenUndLaden(datei) { + const ctx = belegPopup; + if (!ctx || !datei) return; + belegPopup = null; + belegDateiVerarbeiten(datei, ctx.tankId, ctx.kontext); + render(); +} + +/* Telefon-Weg: das aus der Mail kopierte PDF liegt in der Zwischenablage. + navigator.clipboard.read() ist der einzige Weg, ohne dass der Nutzer in ein + Eingabefeld tippen muss - iOS zeigt dafür seine eigene Einfügen-Abfrage. */ +async function belegAusZwischenablage() { + const ctx = belegPopup; + if (!ctx) return; + if (!navigator.clipboard || !navigator.clipboard.read) { + belegPopup.fehler = "Dieser Browser kann die Zwischenablage nicht lesen - bitte ziehen oder Datei auswählen."; + render(); return; + } + try { + for (const eintrag of await navigator.clipboard.read()) { + const typ = (eintrag.types || []).find((t) => t === "application/pdf"); + if (typ) { belegPopupSchliessenUndLaden(new File([await eintrag.getType(typ)], "beleg.pdf", { type: "application/pdf" })); return; } + } + belegPopup.fehler = "In der Zwischenablage liegt kein PDF."; + } catch (err) { + belegPopup.fehler = "Kein Zugriff auf die Zwischenablage."; + } + render(); +} + +function vBelegPopup() { + if (!belegPopup) return ""; + return `
+ `; +} + function sheetMarkup() { if (!sheet) return ""; const kopf = `
${esc(sheet.titel)}${sheet.text ? `${esc(sheet.text)}` : ""}
`; @@ -3157,7 +3238,7 @@ function render() { // faellt sonst bei jedem render(), z. B. beim Umschalten des Filters, // auf 0 zurueck). const setupKoerperScroll = setupOffen ? (ROOT.querySelector(".setup-koerper") || {}).scrollTop : 0; - ROOT.getElementById("overlay").innerHTML = sheetMarkup() + vSetupPopup(); + ROOT.getElementById("overlay").innerHTML = sheetMarkup() + vSetupPopup() + vBelegPopup(); if (setupOffen) { const koerper = ROOT.querySelector(".setup-koerper"); if (koerper) koerper.scrollTop = setupKoerperScroll || 0; @@ -3274,8 +3355,38 @@ function ereignisseVerdrahten() { Hintergrund wandert. Beides gehoert zum Vertrag dieser Rolle. Der Handler haengt am Dokument, weil Fokus im Shadow DOM auch ausserhalb von ROOT liegen kann. */ + /* Ziehen und Ablegen (Rechner) sowie Einfügen per Tastatur, solange das + Beleg-Fenster offen ist. Die Listener hängen am Dokument, weil ein Zug + über der Ablagefläche im Shadow DOM sonst schon am Rand verloren geht - + und weil ohne preventDefault auf dragover der Browser das PDF einfach + selbst öffnet, statt ein drop-Ereignis zu liefern. */ + document.addEventListener("dragover", (e) => { + if (!belegPopup) return; + e.preventDefault(); + const zone = ROOT.getElementById("belegDrop"); + if (zone) zone.classList.add("darueber"); + }); + document.addEventListener("dragleave", (e) => { + if (!belegPopup || e.relatedTarget) return; + const zone = ROOT.getElementById("belegDrop"); + if (zone) zone.classList.remove("darueber"); + }); + document.addEventListener("drop", (e) => { + if (!belegPopup) return; + e.preventDefault(); + const datei = belegAusDateiliste(e.dataTransfer && e.dataTransfer.files); + if (datei) { belegPopupSchliessenUndLaden(datei); return; } + belegPopup.fehler = "Das war kein PDF."; + render(); + }); + document.addEventListener("paste", (e) => { + if (!belegPopup) return; + const datei = belegAusDateiliste(e.clipboardData && e.clipboardData.files); + if (datei) { e.preventDefault(); belegPopupSchliessenUndLaden(datei); } + }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") { + if (belegPopup) { belegPopup = null; render(); e.preventDefault(); return; } if (sheet) { sheetSchliessen(); e.preventDefault(); return; } if (setupSucheOffen) { setupSucheOffen = null; setupSuchtext = ""; render(); e.preventDefault(); return; } if (setupOffen) { setupSchliessen(); e.preventDefault(); return; } @@ -3740,31 +3851,18 @@ function ereignisseVerdrahten() { const kontext = nurParsen ? praefix : ziel; if (belegFehler && belegFehler.context === kontext) belegFehler = null; belegZielPraefix = praefix; + // Statt direkt die Dateiauswahl zu öffnen: Fenster mit Ablagefläche + // (Rechner) und Einfügen-Knopf (Telefon, PDF aus der Mail kopiert). + belegPopup = { tankId, kontext, fehler: null }; + render(); + return; + } + if (e.target.closest("[data-belegzu]")) { belegPopup = null; render(); return; } + if (e.target.closest("[data-belegeinfuegen]")) { belegAusZwischenablage(); return; } + if (e.target.closest("[data-belegdatei]")) { const eingabe = document.createElement("input"); eingabe.type = "file"; eingabe.accept = "application/pdf"; - eingabe.onchange = () => { - const datei = eingabe.files[0]; if (!datei) return; - const leser = new FileReader(); - leser.onload = () => { - const base64 = leser.result.split(",")[1]; - // Kein serviceRufen() hier: dessen catch loggt nur in die Browser- - // Konsole (bei allen anderen Aufrufen ok, die reagieren auf - // datenLaden() statt auf den Rückgabewert) - ein rejizierter - // Service-Aufruf (Backend nicht erreichbar, Timeout, ...) blieb - // für den Beleg-Upload sonst komplett unsichtbar in der - // Oberfläche, obwohl der Backend-seitige Parser-Fehlerpfad selbst - // schon einen Fehler veröffentlicht (siehe belegverarbeitung.py). - HASS.callService("pyscript", "audi_dashboard_beleg_hochladen", - { pdf_base64: base64, dateiname: datei.name, tank_id: tankId } - ).catch((err) => { - console.error("audi_dashboard: audi_dashboard_beleg_hochladen", err); - belegFehler = { context: kontext, meldung: "Beleg konnte nicht hochgeladen werden" }; - if (!tankId) belegEntwurf = null; - render(); - }); - }; - leser.readAsDataURL(datei); - }; + eingabe.onchange = () => belegPopupSchliessenUndLaden(eingabe.files[0]); eingabe.click(); return; } diff --git a/homeassistant/www/audi-dashboard-ios.css b/homeassistant/www/audi-dashboard-ios.css index 83ecf0f..5bac293 100644 --- a/homeassistant/www/audi-dashboard-ios.css +++ b/homeassistant/www/audi-dashboard-ios.css @@ -316,7 +316,7 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo ruhigen Karte. max-width + margin:auto zentriert sie innerhalb des bestehenden left/right-Abstands, ohne die transform-basierte Einblend-Animation dieser Elemente zu beruehren. */ - .setup-popup,.sdpopup,.sheet{max-width:560px;margin-left:auto;margin-right:auto} + .setup-popup,.sdpopup,.sheet,.beleg-popup{max-width:560px;margin-left:auto;margin-right:auto} .standortmenu{max-width:640px;margin-left:auto;margin-right:auto} } diff --git a/homeassistant/www/audi-dashboard-version.json b/homeassistant/www/audi-dashboard-version.json index 03c0549..fa9ec75 100644 --- a/homeassistant/www/audi-dashboard-version.json +++ b/homeassistant/www/audi-dashboard-version.json @@ -1 +1 @@ -{"version": 1786916894} +{"version": 1786919196} diff --git a/homeassistant/www/audi-dashboard.css b/homeassistant/www/audi-dashboard.css index cc29dd8..1a46f0c 100644 --- a/homeassistant/www/audi-dashboard.css +++ b/homeassistant/www/audi-dashboard.css @@ -83,6 +83,12 @@ height: min(880px, calc(100vh - 24px)); height: min(880px, calc(100dvh - 24px)); min-height: 0; + /* Verhindert, dass eine Zieh-Geste, die main#view unten nach oben hin + verlaesst, an die dahinterliegende HA-Seite weitergereicht wird - sonst + kann der Browser selbst ein natives Overscroll/Pull-to-Refresh starten + und der eigene Ziehen-zum-Aktualisieren-Indikator bekommt die Geste nie + zu Ende. */ + overscroll-behavior: none; } /* ------------------------------------------------------------- Gerüst */ @@ -226,6 +232,12 @@ main#view { scroll-behavior: smooth; -webkit-overflow-scrolling: touch; scrollbar-width: none; + /* Ohne das hier greift auf echten Touch-Geraeten oft das native + Overscroll/Pull-to-Refresh des Browsers, sobald am oberen Rand weiter + nach unten gezogen wird - das nimmt der eigenen "Ziehen zum + Aktualisieren"-Geste (siehe ptrY0-Logik in ereignisseVerdrahten()) die + Kontrolle weg, obwohl deren JS/CSS für sich genommen korrekt arbeitet. */ + overscroll-behavior-y: contain; } main#view::-webkit-scrollbar { width: 0; height: 0; } @@ -915,6 +927,34 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; } padding: var(--sp-4); } +/* Beleg-Fenster: PDF hineinziehen (Rechner) oder einfügen (Telefon). */ +.beleg-catcher { + position: absolute; inset: 0; z-index: 1000; + background: rgba(0,0,0,.45); + animation: sheet-fade .18s ease-out; +} +.beleg-popup { + position: absolute; z-index: 1001; + left: var(--sp-5); right: var(--sp-5); + top: 50%; transform: translateY(-50%); + /* Wie .setup-popup: --tile-2 ist im Nacht-Theme transluzent, ein frei + schwebendes Fenster braucht einen blickdichten Grund. */ + background: var(--tile-deckend, var(--canvas)); + border-radius: var(--r-tile); + padding: var(--sp-4); +} +.beleg-drop { + margin-top: var(--sp-3); + border: 1px dashed var(--line-strong); + border-radius: 14px; + padding: 28px var(--sp-4); + text-align: center; + display: flex; flex-direction: column; gap: 6px; +} +.beleg-drop strong { font-size: 15px; color: var(--fg); } +.beleg-drop span { font-size: 12.5px; color: var(--fg2); } +.beleg-drop.darueber { border-color: var(--fg); border-style: solid; background: var(--shade); } + /* Kopfzeile der Reifen-Kachel: Überschrift+km links, Radfoto rechts, beide oben bündig - ersetzt die frühere Überlagerung (Montiert absolut über dem Foto), seit Überschrift und km ihren eigenen Platz über Montiert bekommen