Fix clipboard paste on PC, round price per litre to 2 decimals

The "paste from clipboard" button could never work on a desktop:
clipboard.read() only ever exposes text, HTML and images by spec, so a
file copied in Explorer is not offered through it - and the dialog then
wrongly blamed the file for not being a PDF. It is now hidden on
pointer-coarse devices only, where it does work; on a PC the drop zone
names Strg+V, which is the path that carries the actual file.

Detection also relied solely on the MIME type, but a file copied or
dragged from Explorer can arrive with an empty type and was rejected -
istPdf() now falls back to the extension.

Price per litre now shows 2 decimals everywhere (list, receipt detail,
both entry forms, CSV) instead of 3.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-17 00:38:55 +02:00
co-authored by Claude Opus 5
parent 5aaf1cd958
commit e5e82b3634
5 changed files with 124 additions and 34 deletions
+34
View File
@@ -1169,6 +1169,40 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed
drop → zone highlights (`darueber`), dialog closes, service called with the right filename and 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 → base64 payload; paste → same; a dropped PNG → "Das war kein PDF." and no service call; Escape →
closes. Synced to `installationspaket/`. closes. Synced to `installationspaket/`.
**Nachbesserung am selben Tag, nach Fehlermeldung des Nutzers** ("copy paste from pc brings
error no pdf but it is the latest bill") — zwei echte Fehler, beide bestätigt:
(1) `navigator.clipboard.read()` (der "Aus Zwischenablage einfügen"-Knopf) gibt laut
Spezifikation **nur Text, HTML und Bilder** heraus. Eine im Explorer/Finder kopierte PDF-Datei
taucht dort grundsätzlich nicht auf - der Knopf konnte am Rechner also gar nicht funktionieren
und behauptete dann fälschlich, es liege kein PDF vor. Der Weg, der dort funktioniert, ist
Strg+V: das `paste`-Ereignis trägt die Datei in `clipboardData.files`. Die Meldung nennt jetzt
genau das, statt die Datei zu beschuldigen; die Ablagefläche sagt es von vornherein
("oder mit Strg+V einfügen", per `tastenkuerzelEinfuegen()` auf Mac Cmd+V).
(2) Die Erkennung hing allein am MIME-Typ (`type === "application/pdf"`) - eine aus dem
Explorer kopierte oder gezogene Datei kommt je nach Browser und Quelle aber mit **leerem**
`type` an und wurde dadurch abgelehnt. Neue Hilfsfunktion `istPdf()` prüft zusätzlich die
Dateiendung; live nachgestellt mit genau diesem Fall (`new File([...], "Tankbeleg_August.pdf",
{})`, `type === ""`) - vorher abgelehnt, jetzt korrekt hochgeladen.
(3) Auf Rückfrage des Nutzers ("why is it that way? computer has copy paste function") noch
einen Schritt weiter: Der Einfügen-Knopf wird am Rechner jetzt **gar nicht mehr angeboten**
(`zwischenablageKnopfSinnvoll()`, Prüfung auf `(pointer: coarse)`), weil er dort prinzipiell
nicht funktionieren kann - ein Knopf, der zwangsläufig in eine Fehlermeldung läuft, ist
schlechter als keiner. "Datei auswählen" wird dort zur primären Aktion; auf dem Telefon bleibt
alles wie zuvor. Der Unterschied dahinter, kurz: Strg+V ist eine bewusste Nutzergeste an die
Seite, deshalb bekommt die Seite den vollen Inhalt inklusive Dateien - `clipboard.read()` ist
die Seite, die von sich aus die Zwischenablage ausliest, und das wird aus Datenschutzgründen
stark eingeschränkt (sonst könnte jede Seite still mitlesen, was zuletzt kopiert wurde).
Live geprüft: Desktop zeigt nur noch "Datei auswählen [primaer]"/"Abbrechen", Mobil (375px,
`pointer: coarse`) weiterhin alle drei Knöpfe.
- [x] Preis je Liter überall auf 2 Nachkommastellen (2026-08-17, Nutzerwunsch: "€/l for gas is
common to be displayed as x,xx €/l") — alle sieben Stellen von `de(..., 3)` auf `de(..., 2)`
umgestellt: Jahres- und Monatsschnitt in der Tankliste, Zeilenwert je Tankvorgang, "Preis je
Liter" im Einzelbeleg, die Live-Vorschau in beiden Tankformularen (statisch und im
`data-tankpreisfeld`-Handler) sowie die CSV-Spalte "€/l". Im CSV kein Informationsverlust: der
Wert ist aus den ebenfalls exportierten Spalten Liter und Kosten jederzeit neu berechenbar.
`companion-app` (Nachfolger-App) brauchte keine Änderung - deren `eur()` rundet ohnehin schon
auf 2 Stellen. Live geprüft: keine `de(..., 3)`-Aufrufe mehr in der Datei, Liste und
Einzelbeleg zeigen "2,07 €/l", "1,99 €/l" usw.
- [ ] Fix remaining documentation drift (statistics claim, README gaps, obsolete TODO comment) — - [ ] 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 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` 2026-08-12 (see section B); `DESIGN_REVIEW_2026-08-13.md` and `REVIEW_main_2026-08-13.md`
@@ -2805,7 +2805,7 @@ function tankFelder(praefix, f) {
const kraftstoff = q && q.fuel_type ? q.fuel_type : KRAFTSTOFFSORTEN[0]; const kraftstoff = q && q.fuel_type ? q.fuel_type : KRAFTSTOFFSORTEN[0];
const station = q && q.station_name ? q.station_name : ""; const station = q && q.station_name ? q.station_name : "";
const preisText = q && q.liters && q.fuel_total_eur != null && q.liters > 0 const preisText = q && q.liters && q.fuel_total_eur != null && q.liters > 0
? `Preis je Liter: ${de(q.fuel_total_eur / q.liters, 3)}` : "Preis je Liter: wird berechnet"; ? `Preis je Liter: ${de(q.fuel_total_eur / q.liters, 2)}` : "Preis je Liter: wird berechnet";
return ` return `
<div class="feld"><label>Datum</label><input type="date" id="${praefix}Datum" value="${datum}"></div> <div class="feld"><label>Datum</label><input type="date" id="${praefix}Datum" value="${datum}"></div>
<div class="feld"><label>Uhrzeit</label><input type="time" id="${praefix}Zeit" value="${zeit}"></div> <div class="feld"><label>Uhrzeit</label><input type="time" id="${praefix}Zeit" value="${zeit}"></div>
@@ -2871,7 +2871,7 @@ function vFuel() {
<span class="k">${j}</span> <span class="k">${j}</span>
<span class="v" style="display:flex;flex-direction:column;align-items:flex-end;gap:5px"> <span class="v" style="display:flex;flex-direction:column;align-items:flex-end;gap:5px">
<span style="font-size:17px;color:var(--fg)">${eur(sum(fj, (f) => f.fuel_total_eur || 0))} €</span> <span style="font-size:17px;color:var(--fg)">${eur(sum(fj, (f) => f.fuel_total_eur || 0))} €</span>
<span>${de(sum(fj, (f) => f.liters || 0), 1)} l · Ø ${de(schnittpreis(fj), 3)} €/l</span> <span>${de(sum(fj, (f) => f.liters || 0), 1)} l · Ø ${de(schnittpreis(fj), 2)} €/l</span>
${CAR.smartdeal.aktiv ? `<span class="marke"><span class="pill" style="font-size:11.5px;padding:4px 10px">${markenlogo()}<span style="margin-left:6px">${eur(ersparnisJahr)} €</span></span></span>` : ""} ${CAR.smartdeal.aktiv ? `<span class="marke"><span class="pill" style="font-size:11.5px;padding:4px 10px">${markenlogo()}<span style="margin-left:6px">${eur(ersparnisJahr)} €</span></span></span>` : ""}
</span></button> </span></button>
<div class="acc-b ${ji === 0 ? "on" : ""}" id="fy${j}">`; <div class="acc-b ${ji === 0 ? "on" : ""}" id="fy${j}">`;
@@ -2882,11 +2882,11 @@ function vFuel() {
<span class="mark"></span><span class="k">${m}</span> <span class="mark"></span><span class="k">${m}</span>
<span class="v" style="display:flex;flex-direction:column;align-items:flex-end;gap:3px"> <span class="v" style="display:flex;flex-direction:column;align-items:flex-end;gap:3px">
<span style="color:var(--fg)">${eur(sum(fm, (f) => f.fuel_total_eur || 0))} €</span> <span style="color:var(--fg)">${eur(sum(fm, (f) => f.fuel_total_eur || 0))} €</span>
<span>Ø ${de(schnittpreis(fm), 3)} €/l</span></span></button> <span>Ø ${de(schnittpreis(fm), 2)} €/l</span></span></button>
<div class="acc-b ${mi === 0 ? "on" : ""}" id="fm${j}${m}">`; <div class="acc-b ${mi === 0 ? "on" : ""}" id="fm${j}${m}">`;
fm.forEach((f) => { const preis = f.liters ? f.fuel_total_eur / f.liters : null; h += swipeZeile(`fill:${f.tank_id}`, `<button class="leaf" data-go="fill:${f.tank_id}"> fm.forEach((f) => { const preis = f.liters ? f.fuel_total_eur / f.liters : null; h += swipeZeile(`fill:${f.tank_id}`, `<button class="leaf" data-go="fill:${f.tank_id}">
<span class="k">${dedat(new Date(f.ts))}<small>${esc(f.station_name || "")}</small></span> <span class="k">${dedat(new Date(f.ts))}<small>${esc(f.station_name || "")}</small></span>
<span class="v">${f.liters != null ? de(f.liters, 2) + " l" : ""}<small>${preis != null ? de(preis, 3) + " €/l" : ""}</small></span> <span class="v">${f.liters != null ? de(f.liters, 2) + " l" : ""}<small>${preis != null ? de(preis, 2) + " €/l" : ""}</small></span>
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`); }); <svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`); });
h += `</div></div>`; h += `</div></div>`;
}); });
@@ -2925,7 +2925,7 @@ function vFill(id) {
<div class="row"><dt>Kraftstoff</dt><dd>${esc(f.fuel_type || KRAFTSTOFFSORTEN[0])}</dd></div> <div class="row"><dt>Kraftstoff</dt><dd>${esc(f.fuel_type || KRAFTSTOFFSORTEN[0])}</dd></div>
<div class="row"><dt>Getankte Liter</dt><dd>${f.liters != null ? de(f.liters, 2) + " l" : ""}</dd></div> <div class="row"><dt>Getankte Liter</dt><dd>${f.liters != null ? de(f.liters, 2) + " l" : ""}</dd></div>
<div class="row"><dt>Kosten</dt><dd>${f.fuel_total_eur != null ? eur(f.fuel_total_eur) + " €" : ""}<small>real, nach Rabatt</small></dd></div> <div class="row"><dt>Kosten</dt><dd>${f.fuel_total_eur != null ? eur(f.fuel_total_eur) + " €" : ""}<small>real, nach Rabatt</small></dd></div>
<div class="row"><dt>Preis je Liter</dt><dd>${berechneterPreis != null ? de(berechneterPreis, 3) + " €" : ""}<small>Kosten ÷ Liter</small></dd></div> <div class="row"><dt>Preis je Liter</dt><dd>${berechneterPreis != null ? de(berechneterPreis, 2) + " €" : ""}<small>Kosten ÷ Liter</small></dd></div>
${CAR.smartdeal.aktiv && f.discount != null ? `<div class="row"><dt><span class="marke">${markenlogo()} SmartDeal-Ersparnis</span></dt><dd>${eur(f.discount)} €</dd></div>` : ""} ${CAR.smartdeal.aktiv && f.discount != null ? `<div class="row"><dt><span class="marke">${markenlogo()} SmartDeal-Ersparnis</span></dt><dd>${eur(f.discount)} €</dd></div>` : ""}
<div class="row" style="border-bottom:0"><dt>Tankstelle</dt><dd>${esc(f.station_name || "unbekannt")}<small>${esc(f.station_address || "")}</small></dd></div> <div class="row" style="border-bottom:0"><dt>Tankstelle</dt><dd>${esc(f.station_name || "unbekannt")}<small>${esc(f.station_address || "")}</small></dd></div>
</dl> </dl>
@@ -3006,7 +3006,7 @@ function hinweis(titel, text) {
unverändert. */ unverändert. */
function belegDateiVerarbeiten(datei, tankId, kontext) { function belegDateiVerarbeiten(datei, tankId, kontext) {
if (!datei) return; if (!datei) return;
if (datei.type && datei.type !== "application/pdf") { if (!istPdf(datei)) {
belegFehler = { context: kontext, meldung: "Nur PDF-Belege werden unterstützt" }; belegFehler = { context: kontext, meldung: "Nur PDF-Belege werden unterstützt" };
render(); return; render(); return;
} }
@@ -3025,9 +3025,16 @@ function belegDateiVerarbeiten(datei, tankId, kontext) {
leser.readAsDataURL(datei); leser.readAsDataURL(datei);
} }
/* Nicht nur auf den MIME-Typ verlassen: eine aus dem Explorer/Finder kopierte
oder gezogene Datei kommt je nach Browser und Quelle mit leerem type an. */
function istPdf(datei) {
if (!datei) return false;
return datei.type === "application/pdf" || /\.pdf$/i.test(datei.name || "");
}
/* Nimmt das erste PDF aus einer DataTransfer-/Clipboard-Dateiliste. */ /* Nimmt das erste PDF aus einer DataTransfer-/Clipboard-Dateiliste. */
function belegAusDateiliste(dateien) { function belegAusDateiliste(dateien) {
return [...(dateien || [])].find((d) => d.type === "application/pdf") || null; return [...(dateien || [])].find(istPdf) || null;
} }
function belegPopupSchliessenUndLaden(datei) { function belegPopupSchliessenUndLaden(datei) {
@@ -3040,12 +3047,21 @@ function belegPopupSchliessenUndLaden(datei) {
/* Telefon-Weg: das aus der Mail kopierte PDF liegt in der Zwischenablage. /* 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 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. */ Eingabefeld tippen muss - iOS zeigt dafür seine eigene Einfügen-Abfrage.
WICHTIG (Ursache eines gemeldeten Fehlers): Am Rechner hilft dieser Knopf
bei einer im Explorer/Finder kopierten Datei NICHT. clipboard.read() gibt
laut Spezifikation nur Text, HTML und Bilder heraus - eine kopierte
PDF-Datei taucht dort gar nicht auf, egal wie frisch sie ist. Der Weg, der
dort funktioniert, ist Strg+V: das paste-Ereignis trägt die echte Datei in
clipboardData.files. Deshalb sagt die Meldung unten genau das, statt
pauschal "kein PDF" zu behaupten. */
async function belegAusZwischenablage() { async function belegAusZwischenablage() {
const ctx = belegPopup; const ctx = belegPopup;
if (!ctx) return; if (!ctx) return;
const strgVHinweis = `Am Rechner kann der Browser eine kopierte Datei nur über ${tastenkuerzelEinfuegen()} entgegennehmen - bitte so einfügen oder das PDF hineinziehen.`;
if (!navigator.clipboard || !navigator.clipboard.read) { if (!navigator.clipboard || !navigator.clipboard.read) {
belegPopup.fehler = "Dieser Browser kann die Zwischenablage nicht lesen - bitte ziehen oder Datei auswählen."; belegPopup.fehler = strgVHinweis;
render(); return; render(); return;
} }
try { try {
@@ -3053,13 +3069,25 @@ async function belegAusZwischenablage() {
const typ = (eintrag.types || []).find((t) => t === "application/pdf"); const typ = (eintrag.types || []).find((t) => t === "application/pdf");
if (typ) { belegPopupSchliessenUndLaden(new File([await eintrag.getType(typ)], "beleg.pdf", { type: "application/pdf" })); return; } if (typ) { belegPopupSchliessenUndLaden(new File([await eintrag.getType(typ)], "beleg.pdf", { type: "application/pdf" })); return; }
} }
belegPopup.fehler = "In der Zwischenablage liegt kein PDF."; belegPopup.fehler = strgVHinweis;
} catch (err) { } catch (err) {
belegPopup.fehler = "Kein Zugriff auf die Zwischenablage."; belegPopup.fehler = "Kein Zugriff auf die Zwischenablage. " + strgVHinweis;
} }
render(); render();
} }
function tastenkuerzelEinfuegen() {
return /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent) ? "Cmd+V" : "Strg+V";
}
/* Der Einfügen-Knopf wird nur auf Tippgeräten angeboten. Am Rechner kann er
aus Datenschutzgründen prinzipiell nicht funktionieren (clipboard.read()
gibt Dateien nie heraus, siehe oben) - ein Knopf, der dort zwangsläufig
scheitert, führt nur in eine Sackgasse. Dort bleiben Ziehen und Strg+V. */
function zwischenablageKnopfSinnvoll() {
return window.matchMedia && window.matchMedia("(pointer: coarse)").matches;
}
function vBelegPopup() { function vBelegPopup() {
if (!belegPopup) return ""; if (!belegPopup) return "";
return `<div class="beleg-catcher" data-belegzu></div> return `<div class="beleg-catcher" data-belegzu></div>
@@ -3067,11 +3095,11 @@ function vBelegPopup() {
<span class="label">Beleg hochladen</span> <span class="label">Beleg hochladen</span>
<div class="beleg-drop" id="belegDrop"> <div class="beleg-drop" id="belegDrop">
<strong>PDF hierher ziehen</strong> <strong>PDF hierher ziehen</strong>
<span>oder aus der Zwischenablage einfügen</span> <span>oder mit ${tastenkuerzelEinfuegen()} einfügen</span>
</div> </div>
${belegPopup.fehler ? `<span class="label" style="margin-top:10px;color:var(--red)">${esc(belegPopup.fehler)}</span>` : ""} ${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> ${zwischenablageKnopfSinnvoll() ? `<button class="aktion primaer" data-belegeinfuegen>Aus Zwischenablage einfügen</button>` : ""}
<button class="aktion" data-belegdatei>Datei auswählen</button> <button class="aktion ${zwischenablageKnopfSinnvoll() ? "" : "primaer"}" data-belegdatei>Datei auswählen</button>
<button class="aktion" data-belegzu>Abbrechen</button> <button class="aktion" data-belegzu>Abbrechen</button>
</div>`; </div>`;
} }
@@ -3928,7 +3956,7 @@ function ereignisseVerdrahten() {
if (w === "fahrten") csvDatei("fahrten.csv", ["Start", "Ende", "km", "Art", "Status"], if (w === "fahrten") csvDatei("fahrten.csv", ["Start", "Ende", "km", "Art", "Status"],
TRIPS.map((t) => [t.ts_start, t.ts_end, t.distance_km != null ? de(t.distance_km) : "", t.art, t.status])); TRIPS.map((t) => [t.ts_start, t.ts_end, t.distance_km != null ? de(t.distance_km) : "", t.art, t.status]));
else if (w === "tanken") csvDatei("tankvorgaenge.csv", ["Zeitpunkt", "Station", "Liter", "€/l", "Kosten €"], else if (w === "tanken") csvDatei("tankvorgaenge.csv", ["Zeitpunkt", "Station", "Liter", "€/l", "Kosten €"],
FILLS.map((f) => [f.ts, f.station_name || "", f.liters != null ? de(f.liters, 2) : "", f.liters ? de(f.fuel_total_eur / f.liters, 3) : "", f.fuel_total_eur != null ? eur(f.fuel_total_eur) : ""])); FILLS.map((f) => [f.ts, f.station_name || "", f.liters != null ? de(f.liters, 2) : "", f.liters ? de(f.fuel_total_eur / f.liters, 2) : "", f.fuel_total_eur != null ? eur(f.fuel_total_eur) : ""]));
else csvDatei("servicebuch.csv", ["Datum", "km", "Art", "Werkstatt", "Kosten €"], else csvDatei("servicebuch.csv", ["Datum", "km", "Art", "Werkstatt", "Kosten €"],
CAR.service.buch.map((e2) => [e2.datum, de(e2.km), e2.art, e2.werkstatt, e2.kosten != null ? eur(e2.kosten) : ""])); CAR.service.buch.map((e2) => [e2.datum, de(e2.km), e2.art, e2.werkstatt, e2.kosten != null ? eur(e2.kosten) : ""]));
return; return;
@@ -3995,7 +4023,7 @@ function ereignisseVerdrahten() {
const liter = parseFloat((ROOT.getElementById(`${praefix}Liter`).value || "0").replace(",", ".")); const liter = parseFloat((ROOT.getElementById(`${praefix}Liter`).value || "0").replace(",", "."));
const kosten = parseFloat((ROOT.getElementById(`${praefix}Kosten`).value || "0").replace(",", ".")); const kosten = parseFloat((ROOT.getElementById(`${praefix}Kosten`).value || "0").replace(",", "."));
const anzeige = ROOT.getElementById(`${praefix}PreisAnzeige`); const anzeige = ROOT.getElementById(`${praefix}PreisAnzeige`);
if (anzeige) anzeige.textContent = liter > 0 ? `Preis je Liter: ${de(kosten / liter, 3)}` : "Preis je Liter: wird berechnet"; if (anzeige) anzeige.textContent = liter > 0 ? `Preis je Liter: ${de(kosten / liter, 2)}` : "Preis je Liter: wird berechnet";
}); });
ROOT.addEventListener("change", (e) => { ROOT.addEventListener("change", (e) => {
@@ -1 +1 @@
{"version": 1786919196} {"version": 1786919856}
+44 -16
View File
@@ -2805,7 +2805,7 @@ function tankFelder(praefix, f) {
const kraftstoff = q && q.fuel_type ? q.fuel_type : KRAFTSTOFFSORTEN[0]; const kraftstoff = q && q.fuel_type ? q.fuel_type : KRAFTSTOFFSORTEN[0];
const station = q && q.station_name ? q.station_name : ""; const station = q && q.station_name ? q.station_name : "";
const preisText = q && q.liters && q.fuel_total_eur != null && q.liters > 0 const preisText = q && q.liters && q.fuel_total_eur != null && q.liters > 0
? `Preis je Liter: ${de(q.fuel_total_eur / q.liters, 3)}` : "Preis je Liter: wird berechnet"; ? `Preis je Liter: ${de(q.fuel_total_eur / q.liters, 2)}` : "Preis je Liter: wird berechnet";
return ` return `
<div class="feld"><label>Datum</label><input type="date" id="${praefix}Datum" value="${datum}"></div> <div class="feld"><label>Datum</label><input type="date" id="${praefix}Datum" value="${datum}"></div>
<div class="feld"><label>Uhrzeit</label><input type="time" id="${praefix}Zeit" value="${zeit}"></div> <div class="feld"><label>Uhrzeit</label><input type="time" id="${praefix}Zeit" value="${zeit}"></div>
@@ -2871,7 +2871,7 @@ function vFuel() {
<span class="k">${j}</span> <span class="k">${j}</span>
<span class="v" style="display:flex;flex-direction:column;align-items:flex-end;gap:5px"> <span class="v" style="display:flex;flex-direction:column;align-items:flex-end;gap:5px">
<span style="font-size:17px;color:var(--fg)">${eur(sum(fj, (f) => f.fuel_total_eur || 0))} €</span> <span style="font-size:17px;color:var(--fg)">${eur(sum(fj, (f) => f.fuel_total_eur || 0))} €</span>
<span>${de(sum(fj, (f) => f.liters || 0), 1)} l · Ø ${de(schnittpreis(fj), 3)} €/l</span> <span>${de(sum(fj, (f) => f.liters || 0), 1)} l · Ø ${de(schnittpreis(fj), 2)} €/l</span>
${CAR.smartdeal.aktiv ? `<span class="marke"><span class="pill" style="font-size:11.5px;padding:4px 10px">${markenlogo()}<span style="margin-left:6px">${eur(ersparnisJahr)} €</span></span></span>` : ""} ${CAR.smartdeal.aktiv ? `<span class="marke"><span class="pill" style="font-size:11.5px;padding:4px 10px">${markenlogo()}<span style="margin-left:6px">${eur(ersparnisJahr)} €</span></span></span>` : ""}
</span></button> </span></button>
<div class="acc-b ${ji === 0 ? "on" : ""}" id="fy${j}">`; <div class="acc-b ${ji === 0 ? "on" : ""}" id="fy${j}">`;
@@ -2882,11 +2882,11 @@ function vFuel() {
<span class="mark"></span><span class="k">${m}</span> <span class="mark"></span><span class="k">${m}</span>
<span class="v" style="display:flex;flex-direction:column;align-items:flex-end;gap:3px"> <span class="v" style="display:flex;flex-direction:column;align-items:flex-end;gap:3px">
<span style="color:var(--fg)">${eur(sum(fm, (f) => f.fuel_total_eur || 0))} €</span> <span style="color:var(--fg)">${eur(sum(fm, (f) => f.fuel_total_eur || 0))} €</span>
<span>Ø ${de(schnittpreis(fm), 3)} €/l</span></span></button> <span>Ø ${de(schnittpreis(fm), 2)} €/l</span></span></button>
<div class="acc-b ${mi === 0 ? "on" : ""}" id="fm${j}${m}">`; <div class="acc-b ${mi === 0 ? "on" : ""}" id="fm${j}${m}">`;
fm.forEach((f) => { const preis = f.liters ? f.fuel_total_eur / f.liters : null; h += swipeZeile(`fill:${f.tank_id}`, `<button class="leaf" data-go="fill:${f.tank_id}"> fm.forEach((f) => { const preis = f.liters ? f.fuel_total_eur / f.liters : null; h += swipeZeile(`fill:${f.tank_id}`, `<button class="leaf" data-go="fill:${f.tank_id}">
<span class="k">${dedat(new Date(f.ts))}<small>${esc(f.station_name || "")}</small></span> <span class="k">${dedat(new Date(f.ts))}<small>${esc(f.station_name || "")}</small></span>
<span class="v">${f.liters != null ? de(f.liters, 2) + " l" : ""}<small>${preis != null ? de(preis, 3) + " €/l" : ""}</small></span> <span class="v">${f.liters != null ? de(f.liters, 2) + " l" : ""}<small>${preis != null ? de(preis, 2) + " €/l" : ""}</small></span>
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`); }); <svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`); });
h += `</div></div>`; h += `</div></div>`;
}); });
@@ -2925,7 +2925,7 @@ function vFill(id) {
<div class="row"><dt>Kraftstoff</dt><dd>${esc(f.fuel_type || KRAFTSTOFFSORTEN[0])}</dd></div> <div class="row"><dt>Kraftstoff</dt><dd>${esc(f.fuel_type || KRAFTSTOFFSORTEN[0])}</dd></div>
<div class="row"><dt>Getankte Liter</dt><dd>${f.liters != null ? de(f.liters, 2) + " l" : ""}</dd></div> <div class="row"><dt>Getankte Liter</dt><dd>${f.liters != null ? de(f.liters, 2) + " l" : ""}</dd></div>
<div class="row"><dt>Kosten</dt><dd>${f.fuel_total_eur != null ? eur(f.fuel_total_eur) + " €" : ""}<small>real, nach Rabatt</small></dd></div> <div class="row"><dt>Kosten</dt><dd>${f.fuel_total_eur != null ? eur(f.fuel_total_eur) + " €" : ""}<small>real, nach Rabatt</small></dd></div>
<div class="row"><dt>Preis je Liter</dt><dd>${berechneterPreis != null ? de(berechneterPreis, 3) + " €" : ""}<small>Kosten ÷ Liter</small></dd></div> <div class="row"><dt>Preis je Liter</dt><dd>${berechneterPreis != null ? de(berechneterPreis, 2) + " €" : ""}<small>Kosten ÷ Liter</small></dd></div>
${CAR.smartdeal.aktiv && f.discount != null ? `<div class="row"><dt><span class="marke">${markenlogo()} SmartDeal-Ersparnis</span></dt><dd>${eur(f.discount)} €</dd></div>` : ""} ${CAR.smartdeal.aktiv && f.discount != null ? `<div class="row"><dt><span class="marke">${markenlogo()} SmartDeal-Ersparnis</span></dt><dd>${eur(f.discount)} €</dd></div>` : ""}
<div class="row" style="border-bottom:0"><dt>Tankstelle</dt><dd>${esc(f.station_name || "unbekannt")}<small>${esc(f.station_address || "")}</small></dd></div> <div class="row" style="border-bottom:0"><dt>Tankstelle</dt><dd>${esc(f.station_name || "unbekannt")}<small>${esc(f.station_address || "")}</small></dd></div>
</dl> </dl>
@@ -3006,7 +3006,7 @@ function hinweis(titel, text) {
unverändert. */ unverändert. */
function belegDateiVerarbeiten(datei, tankId, kontext) { function belegDateiVerarbeiten(datei, tankId, kontext) {
if (!datei) return; if (!datei) return;
if (datei.type && datei.type !== "application/pdf") { if (!istPdf(datei)) {
belegFehler = { context: kontext, meldung: "Nur PDF-Belege werden unterstützt" }; belegFehler = { context: kontext, meldung: "Nur PDF-Belege werden unterstützt" };
render(); return; render(); return;
} }
@@ -3025,9 +3025,16 @@ function belegDateiVerarbeiten(datei, tankId, kontext) {
leser.readAsDataURL(datei); leser.readAsDataURL(datei);
} }
/* Nicht nur auf den MIME-Typ verlassen: eine aus dem Explorer/Finder kopierte
oder gezogene Datei kommt je nach Browser und Quelle mit leerem type an. */
function istPdf(datei) {
if (!datei) return false;
return datei.type === "application/pdf" || /\.pdf$/i.test(datei.name || "");
}
/* Nimmt das erste PDF aus einer DataTransfer-/Clipboard-Dateiliste. */ /* Nimmt das erste PDF aus einer DataTransfer-/Clipboard-Dateiliste. */
function belegAusDateiliste(dateien) { function belegAusDateiliste(dateien) {
return [...(dateien || [])].find((d) => d.type === "application/pdf") || null; return [...(dateien || [])].find(istPdf) || null;
} }
function belegPopupSchliessenUndLaden(datei) { function belegPopupSchliessenUndLaden(datei) {
@@ -3040,12 +3047,21 @@ function belegPopupSchliessenUndLaden(datei) {
/* Telefon-Weg: das aus der Mail kopierte PDF liegt in der Zwischenablage. /* 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 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. */ Eingabefeld tippen muss - iOS zeigt dafür seine eigene Einfügen-Abfrage.
WICHTIG (Ursache eines gemeldeten Fehlers): Am Rechner hilft dieser Knopf
bei einer im Explorer/Finder kopierten Datei NICHT. clipboard.read() gibt
laut Spezifikation nur Text, HTML und Bilder heraus - eine kopierte
PDF-Datei taucht dort gar nicht auf, egal wie frisch sie ist. Der Weg, der
dort funktioniert, ist Strg+V: das paste-Ereignis trägt die echte Datei in
clipboardData.files. Deshalb sagt die Meldung unten genau das, statt
pauschal "kein PDF" zu behaupten. */
async function belegAusZwischenablage() { async function belegAusZwischenablage() {
const ctx = belegPopup; const ctx = belegPopup;
if (!ctx) return; if (!ctx) return;
const strgVHinweis = `Am Rechner kann der Browser eine kopierte Datei nur über ${tastenkuerzelEinfuegen()} entgegennehmen - bitte so einfügen oder das PDF hineinziehen.`;
if (!navigator.clipboard || !navigator.clipboard.read) { if (!navigator.clipboard || !navigator.clipboard.read) {
belegPopup.fehler = "Dieser Browser kann die Zwischenablage nicht lesen - bitte ziehen oder Datei auswählen."; belegPopup.fehler = strgVHinweis;
render(); return; render(); return;
} }
try { try {
@@ -3053,13 +3069,25 @@ async function belegAusZwischenablage() {
const typ = (eintrag.types || []).find((t) => t === "application/pdf"); const typ = (eintrag.types || []).find((t) => t === "application/pdf");
if (typ) { belegPopupSchliessenUndLaden(new File([await eintrag.getType(typ)], "beleg.pdf", { type: "application/pdf" })); return; } if (typ) { belegPopupSchliessenUndLaden(new File([await eintrag.getType(typ)], "beleg.pdf", { type: "application/pdf" })); return; }
} }
belegPopup.fehler = "In der Zwischenablage liegt kein PDF."; belegPopup.fehler = strgVHinweis;
} catch (err) { } catch (err) {
belegPopup.fehler = "Kein Zugriff auf die Zwischenablage."; belegPopup.fehler = "Kein Zugriff auf die Zwischenablage. " + strgVHinweis;
} }
render(); render();
} }
function tastenkuerzelEinfuegen() {
return /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent) ? "Cmd+V" : "Strg+V";
}
/* Der Einfügen-Knopf wird nur auf Tippgeräten angeboten. Am Rechner kann er
aus Datenschutzgründen prinzipiell nicht funktionieren (clipboard.read()
gibt Dateien nie heraus, siehe oben) - ein Knopf, der dort zwangsläufig
scheitert, führt nur in eine Sackgasse. Dort bleiben Ziehen und Strg+V. */
function zwischenablageKnopfSinnvoll() {
return window.matchMedia && window.matchMedia("(pointer: coarse)").matches;
}
function vBelegPopup() { function vBelegPopup() {
if (!belegPopup) return ""; if (!belegPopup) return "";
return `<div class="beleg-catcher" data-belegzu></div> return `<div class="beleg-catcher" data-belegzu></div>
@@ -3067,11 +3095,11 @@ function vBelegPopup() {
<span class="label">Beleg hochladen</span> <span class="label">Beleg hochladen</span>
<div class="beleg-drop" id="belegDrop"> <div class="beleg-drop" id="belegDrop">
<strong>PDF hierher ziehen</strong> <strong>PDF hierher ziehen</strong>
<span>oder aus der Zwischenablage einfügen</span> <span>oder mit ${tastenkuerzelEinfuegen()} einfügen</span>
</div> </div>
${belegPopup.fehler ? `<span class="label" style="margin-top:10px;color:var(--red)">${esc(belegPopup.fehler)}</span>` : ""} ${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> ${zwischenablageKnopfSinnvoll() ? `<button class="aktion primaer" data-belegeinfuegen>Aus Zwischenablage einfügen</button>` : ""}
<button class="aktion" data-belegdatei>Datei auswählen</button> <button class="aktion ${zwischenablageKnopfSinnvoll() ? "" : "primaer"}" data-belegdatei>Datei auswählen</button>
<button class="aktion" data-belegzu>Abbrechen</button> <button class="aktion" data-belegzu>Abbrechen</button>
</div>`; </div>`;
} }
@@ -3928,7 +3956,7 @@ function ereignisseVerdrahten() {
if (w === "fahrten") csvDatei("fahrten.csv", ["Start", "Ende", "km", "Art", "Status"], if (w === "fahrten") csvDatei("fahrten.csv", ["Start", "Ende", "km", "Art", "Status"],
TRIPS.map((t) => [t.ts_start, t.ts_end, t.distance_km != null ? de(t.distance_km) : "", t.art, t.status])); TRIPS.map((t) => [t.ts_start, t.ts_end, t.distance_km != null ? de(t.distance_km) : "", t.art, t.status]));
else if (w === "tanken") csvDatei("tankvorgaenge.csv", ["Zeitpunkt", "Station", "Liter", "€/l", "Kosten €"], else if (w === "tanken") csvDatei("tankvorgaenge.csv", ["Zeitpunkt", "Station", "Liter", "€/l", "Kosten €"],
FILLS.map((f) => [f.ts, f.station_name || "", f.liters != null ? de(f.liters, 2) : "", f.liters ? de(f.fuel_total_eur / f.liters, 3) : "", f.fuel_total_eur != null ? eur(f.fuel_total_eur) : ""])); FILLS.map((f) => [f.ts, f.station_name || "", f.liters != null ? de(f.liters, 2) : "", f.liters ? de(f.fuel_total_eur / f.liters, 2) : "", f.fuel_total_eur != null ? eur(f.fuel_total_eur) : ""]));
else csvDatei("servicebuch.csv", ["Datum", "km", "Art", "Werkstatt", "Kosten €"], else csvDatei("servicebuch.csv", ["Datum", "km", "Art", "Werkstatt", "Kosten €"],
CAR.service.buch.map((e2) => [e2.datum, de(e2.km), e2.art, e2.werkstatt, e2.kosten != null ? eur(e2.kosten) : ""])); CAR.service.buch.map((e2) => [e2.datum, de(e2.km), e2.art, e2.werkstatt, e2.kosten != null ? eur(e2.kosten) : ""]));
return; return;
@@ -3995,7 +4023,7 @@ function ereignisseVerdrahten() {
const liter = parseFloat((ROOT.getElementById(`${praefix}Liter`).value || "0").replace(",", ".")); const liter = parseFloat((ROOT.getElementById(`${praefix}Liter`).value || "0").replace(",", "."));
const kosten = parseFloat((ROOT.getElementById(`${praefix}Kosten`).value || "0").replace(",", ".")); const kosten = parseFloat((ROOT.getElementById(`${praefix}Kosten`).value || "0").replace(",", "."));
const anzeige = ROOT.getElementById(`${praefix}PreisAnzeige`); const anzeige = ROOT.getElementById(`${praefix}PreisAnzeige`);
if (anzeige) anzeige.textContent = liter > 0 ? `Preis je Liter: ${de(kosten / liter, 3)}` : "Preis je Liter: wird berechnet"; if (anzeige) anzeige.textContent = liter > 0 ? `Preis je Liter: ${de(kosten / liter, 2)}` : "Preis je Liter: wird berechnet";
}); });
ROOT.addEventListener("change", (e) => { ROOT.addEventListener("change", (e) => {
@@ -1 +1 @@
{"version": 1786919196} {"version": 1786919856}