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:
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user