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 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 00:29:05 +02:00
parent 8a751fe441
commit 5aaf1cd958
9 changed files with 374 additions and 55 deletions
+46 -3
View File
@@ -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
110 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`
@@ -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 `<div class="beleg-catcher" data-belegzu></div>
<div class="beleg-popup" role="dialog" aria-modal="true" aria-label="Beleg hochladen">
<span class="label">Beleg hochladen</span>
<div class="beleg-drop" id="belegDrop">
<strong>PDF hierher ziehen</strong>
<span>oder aus der Zwischenablage einfügen</span>
</div>
${belegPopup.fehler ? `<span class="label" style="margin-top:10px;color:var(--red)">${esc(belegPopup.fehler)}</span>` : ""}
<button class="aktion primaer" data-belegeinfuegen>Aus Zwischenablage einfügen</button>
<button class="aktion" data-belegdatei>Datei auswählen</button>
<button class="aktion" data-belegzu>Abbrechen</button>
</div>`;
}
function sheetMarkup() {
if (!sheet) return "";
const kopf = `<div class="sheet-kopf"><strong>${esc(sheet.titel)}</strong>${sheet.text ? `<span>${esc(sheet.text)}</span>` : ""}</div>`;
@@ -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;
}
@@ -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}
}
@@ -1 +1 @@
{"version": 1786916894}
{"version": 1786919196}
@@ -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
+122 -24
View File
@@ -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 `<div class="beleg-catcher" data-belegzu></div>
<div class="beleg-popup" role="dialog" aria-modal="true" aria-label="Beleg hochladen">
<span class="label">Beleg hochladen</span>
<div class="beleg-drop" id="belegDrop">
<strong>PDF hierher ziehen</strong>
<span>oder aus der Zwischenablage einfügen</span>
</div>
${belegPopup.fehler ? `<span class="label" style="margin-top:10px;color:var(--red)">${esc(belegPopup.fehler)}</span>` : ""}
<button class="aktion primaer" data-belegeinfuegen>Aus Zwischenablage einfügen</button>
<button class="aktion" data-belegdatei>Datei auswählen</button>
<button class="aktion" data-belegzu>Abbrechen</button>
</div>`;
}
function sheetMarkup() {
if (!sheet) return "";
const kopf = `<div class="sheet-kopf"><strong>${esc(sheet.titel)}</strong>${sheet.text ? `<span>${esc(sheet.text)}</span>` : ""}</div>`;
@@ -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;
}
+1 -1
View File
@@ -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}
}
@@ -1 +1 @@
{"version": 1786916894}
{"version": 1786919196}
+40
View File
@@ -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