Fahrt-Formular: Ankunftszeit statt Dauer, Art gross, neutrale Pille

Fuenf gemeldete Punkte plus ein Theme-Bug beim Nachpruefen:

- "Dauer" ist kein Eingabefeld mehr - stattdessen Ankunftszeit, die Dauer
  wird aus Start/Ankunft berechnet und live angezeigt (fahrtZeitraum()/
  dauerText()), inklusive Fahrten ueber Mitternacht.
- Art-Werte werden grossgeschrieben angezeigt (Privat/Arbeitsweg), der
  gespeicherte Wert bleibt klein (artText()-Helfer).
- Arbeitsweg-Pille ist nicht mehr rot - sieht jetzt aus wie Privat.
- "tippen zum Umschalten" unter der Pille entfernt.
- Bild-Platzhalter (Einstellungen) war im Tag-Theme unsichtbar: --tile wird
  dort vom iOS-Overlay auf reines Weiss gesetzt, identisch mit Canvas/
  Kacheln. Auf --ios-fill umgestellt, in beiden Themes sichtbar grau.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 11:49:55 +02:00
parent 2d76f10c1c
commit 126033e4dd
9 changed files with 191 additions and 37 deletions
@@ -2473,7 +2473,38 @@ function vReifen() {
Felder decken genau das ab, was die Einzelfahrt-Seite anzeigt; Verbrauch,
Status und km-Quelle bleiben außen vor, die meldet das Fahrzeug bzw. leitet
das Backend ab. */
/* Gespeichert bleibt der kleingeschriebene Schlüssel - er wird an mehreren
Stellen verglichen (Pille auf der Einzelfahrt, Statistik "Arbeitsweg", CSV)
und steht so auch in allen bereits erfassten Fahrten. Angezeigt wird er
groß, deshalb artText() statt eines geänderten Werts. */
const FAHRT_ARTEN = ["privat", "arbeitsweg"];
const artText = (a) => (a ? a.charAt(0).toUpperCase() + a.slice(1) : "");
/* "1 Std. 15 Min." statt "75 min" - bei mehrstündigen Fahrten liest sich die
reine Minutenzahl schlecht. */
function dauerText(sekunden) {
if (sekunden == null || !isFinite(sekunden)) return "";
const min = Math.max(0, Math.round(sekunden / 60));
const std = Math.floor(min / 60);
return std ? `${std} Std. ${min % 60} Min.` : `${min} Min.`;
}
/* Start und Ankunft eines Formulars als echte Zeitpunkte. Liegt die Ankunft
vor der Abfahrt, wurde über Mitternacht gefahren - dann zählt der Folgetag.
null, solange Datum oder eine der beiden Uhrzeiten fehlt. */
function fahrtZeitraum(praefix) {
const datum = $(`#${praefix}Datum`).value, ab = $(`#${praefix}Start`).value, an = $(`#${praefix}Ende`).value;
if (!datum || !ab || !an) return null;
const start = new Date(`${datum}T${ab}:00`);
let ende = new Date(`${datum}T${an}:00`);
if (isNaN(start) || isNaN(ende)) return null;
if (ende <= start) ende = new Date(ende.getTime() + dTag);
return { start, ende };
}
function fahrtDauerText(praefix) {
const z = fahrtZeitraum(praefix);
return z ? dauerText((z.ende - z.start) / 1000) : "";
}
/* Datum für ein <input type="date"> in Ortszeit. Bewusst nicht über
toISOString(), das rechnet nach UTC um und liefert kurz nach Mitternacht
@@ -2489,16 +2520,30 @@ function zeitfeld(d) {
function fahrtFelder(praefix, t) {
const start = t && t.ts_start ? new Date(t.ts_start) : new Date();
const dauer = t && t.duration_s != null ? Math.round(t.duration_s / 60) : "";
const ende = t && t.ts_end ? new Date(t.ts_end) : null;
const art = t && t.art ? t.art : FAHRT_ARTEN[0];
const wert = (v) => (v == null ? "" : v);
const startzeit = t ? zeitfeld(start) : "08:00", ankunftzeit = ende ? zeitfeld(ende) : "08:30";
// Die Dauer ist bewusst kein Eingabefeld: sie ergibt sich aus Start und
// Ankunft und würde sich sonst mit ihnen widersprechen können. Aus
// t.duration_s statt neu berechnet, damit eine über Mitternacht laufende
// bestehende Fahrt (Ankunft "kleiner" als Start) korrekt bleibt; bei einer
// neuen Fahrt (t === null) ergibt sich der Startwert aus denselben
// Vorgabezeiten wie die Felder selbst. Der data-fahrtzeitfeld-Handler
// rechnet bei jeder Änderung neu.
let dauerSekunden = t && t.duration_s != null ? t.duration_s : null;
if (dauerSekunden == null) {
const [sh, sm] = startzeit.split(":").map(Number), [eh, em] = ankunftzeit.split(":").map(Number);
dauerSekunden = ((eh * 60 + em) - (sh * 60 + sm) + (eh * 60 + em <= sh * 60 + sm ? 1440 : 0)) * 60;
}
const dauer = dauerText(dauerSekunden);
return `
<div class="feld"><label for="${praefix}Datum">Datum</label><input type="date" id="${praefix}Datum" value="${datumsfeld(start)}"></div>
<div class="feld"><label for="${praefix}Start">Startzeit</label><input type="time" id="${praefix}Start" value="${t ? zeitfeld(start) : "08:00"}"></div>
<div class="feld"><label for="${praefix}Dauer">Dauer</label>
<span class="mitEinheit"><input type="number" min="0" id="${praefix}Dauer" value="${dauer}" placeholder="0"><span>min</span></span></div>
<div class="feld"><label for="${praefix}Datum">Datum</label><input type="date" id="${praefix}Datum" value="${datumsfeld(start)}" data-fahrtzeitfeld="${praefix}"></div>
<div class="feld"><label for="${praefix}Start">Startzeit</label><input type="time" id="${praefix}Start" value="${startzeit}" data-fahrtzeitfeld="${praefix}"></div>
<div class="feld"><label for="${praefix}Ende">Ankunftszeit</label><input type="time" id="${praefix}Ende" value="${ankunftzeit}" data-fahrtzeitfeld="${praefix}"></div>
<div class="feld"><label>Dauer</label><span id="${praefix}DauerAnzeige" style="color:var(--fg2)">${dauer}</span></div>
<div class="feld"><label for="${praefix}Art">Art</label>
<select id="${praefix}Art">${FAHRT_ARTEN.map((a) => `<option ${a === art ? "selected" : ""}>${a}</option>`).join("")}</select></div>
<select id="${praefix}Art">${FAHRT_ARTEN.map((a) => `<option value="${a}" ${a === art ? "selected" : ""}>${artText(a)}</option>`).join("")}</select></div>
<div class="feld"><label for="${praefix}Startort">Startort</label><input id="${praefix}Startort" value="${esc(wert(t && t.start_address))}" placeholder="unbekannt"></div>
<div class="feld"><label for="${praefix}Zielort">Zielort</label><input id="${praefix}Zielort" value="${esc(wert(t && t.end_address))}" placeholder="unbekannt"></div>
<div class="feld"><label for="${praefix}OdoStart">Startkilometer</label><input type="number" id="${praefix}OdoStart" value="${wert(t && t.odo_start)}" placeholder="wird ergänzt"></div>
@@ -2510,11 +2555,10 @@ function fahrtFelder(praefix, t) {
function fahrtFormularWerte(praefix) {
const zahl = (id) => { const v = $(`#${praefix}${id}`).value; return v === "" ? null : parseFloat(v.replace(",", ".")); };
const text = (id) => $(`#${praefix}${id}`).value.trim() || null;
const dauer = parseInt($(`#${praefix}Dauer`).value, 10) || 0;
const tsStart = new Date(`${$(`#${praefix}Datum`).value}T${$(`#${praefix}Start`).value}:00`);
const z = fahrtZeitraum(praefix);
return {
ts_start: tsStart.toISOString(),
ts_end: new Date(tsStart.getTime() + dauer * 60000).toISOString(),
ts_start: z ? z.start.toISOString() : null,
ts_end: z ? z.ende.toISOString() : null,
art: $(`#${praefix}Art`).value,
start_ort: text("Startort"), ziel_ort: text("Zielort"),
odo_start: zahl("OdoStart"), odo_end: zahl("OdoEnde"), distanz: zahl("Distanz"),
@@ -2556,7 +2600,7 @@ function vTrips() {
<div class="acc-b ${mi === 0 ? "on" : ""}" id="m${j}${m}">`;
tm.forEach((t) => { h += swipeZeile(`trip:${t.trip_id}`, `<button class="leaf" data-go="trip:${t.trip_id}">
<span class="k">${strecke(t)}
<small>${dedat(new Date(t.ts_start))} · ${esc(t.art)}</small></span>
<small>${dedat(new Date(t.ts_start))} · ${artText(t.art)}</small></span>
<span class="v">${t.distance_km != null ? de(t.distance_km) + " km" : "offen"}<small>${fahrtVerbrauch(t) != null ? de(fahrtVerbrauch(t), 1) + " l/100 km" : t.status}</small></span>
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`); });
h += `</div></div>`;
@@ -2586,9 +2630,8 @@ function vTrip(id) {
<span class="label">Distanz</span>
<div class="fig" style="font-size:52px;margin-top:10px">${t.distance_km != null ? de(t.distance_km) : ""}<span class="unit">km</span></div>
<div style="margin-top:15px">
<button class="pill ${t.art === "arbeitsweg" ? "work" : ""}" data-art="${t.trip_id}"
style="cursor:pointer;font-family:inherit">${esc(t.art)}</button></div>
<span class="label" style="margin-top:12px">tippen zum Umschalten</span>
<button class="pill" data-art="${t.trip_id}"
style="cursor:pointer;font-family:inherit">${artText(t.art)}</button></div>
</div>
<div class="tile"><dl class="rows">
<div class="row"><dt>Startort</dt><dd>${esc(t.start_address || "unbekannt")}<small>${dedat(new Date(t.ts_start))}</small></dd></div>
@@ -4128,6 +4171,15 @@ function ereignisseVerdrahten() {
if (liste) liste.innerHTML = entitaetZeilenMarkup(sucheFeld, key, setupSuchtext, wert);
return;
}
// Gleicher Grund wie beim tankpreisfeld-Handler darunter: nur die
// Anzeige patchen, kein render() - das würde das gerade bearbeitete Feld
// neu erzeugen und den Fokus verlieren.
const zeitfeldPraefix = e.target.dataset.fahrtzeitfeld;
if (zeitfeldPraefix) {
const anzeige = ROOT.getElementById(`${zeitfeldPraefix}DauerAnzeige`);
if (anzeige) anzeige.textContent = fahrtDauerText(zeitfeldPraefix);
return;
}
const praefix = e.target.dataset.tankpreisfeld;
if (!praefix) return;
const liter = parseFloat((ROOT.getElementById(`${praefix}Liter`).value || "0").replace(",", "."));
@@ -196,7 +196,6 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
.pill{font-size:12px;letter-spacing:.01em;text-transform:none;padding:5px 11px;
border:0;background:var(--ios-fill);color:var(--fg2)}
.pill.work{background:rgba(245,5,55,.16);color:var(--ios-tint)}
.status{padding:14px 0 2px}
.status .t{font-size:16px}
@@ -333,7 +332,12 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
/* Fahrzeugbild randlos wie in einer Fahrzeug-App: der Platzhalter bleibt
sichtbar, aber ohne technischen Strichrahmen. */
.szene{margin-bottom:20px}
.bildbox.bild-fehlt .platzhalter{border:0;background:var(--tile);border-radius:var(--r-tile)}
/* --tile statt --ios-fill waere hier naheliegend, faellt im Tag-Theme aber
auf reines Weiss (#FFFFFF) - ununterscheidbar von Canvas und den meisten
Kacheln drumherum, die Box verschwand. --ios-fill ist in beiden Themes ein
deutlich sichtbares transluzentes Grau, dieselbe Konvention wie
.feld select/.aktion ("das hier ist eine Flaeche"). */
.bildbox.bild-fehlt .platzhalter{border:0;background:var(--ios-fill);border-radius:var(--r-tile)}
.platzhalter-datei{display:none}
/* Zeilen mit Wert plus Statuspunkt rechts (Sicherheitscheck) ruhiger setzen */
@@ -1 +1 @@
{"version": 1787000000}
{"version": 1787009000}
@@ -676,7 +676,6 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
border-radius: var(--r-pill);
padding: 6px 12px;
}
.pill.work { color: var(--red); border-color: var(--red); }
/* ------------------------------------------------------------ Akkordeon */
.acc-h {
+66 -14
View File
@@ -2473,7 +2473,38 @@ function vReifen() {
Felder decken genau das ab, was die Einzelfahrt-Seite anzeigt; Verbrauch,
Status und km-Quelle bleiben außen vor, die meldet das Fahrzeug bzw. leitet
das Backend ab. */
/* Gespeichert bleibt der kleingeschriebene Schlüssel - er wird an mehreren
Stellen verglichen (Pille auf der Einzelfahrt, Statistik "Arbeitsweg", CSV)
und steht so auch in allen bereits erfassten Fahrten. Angezeigt wird er
groß, deshalb artText() statt eines geänderten Werts. */
const FAHRT_ARTEN = ["privat", "arbeitsweg"];
const artText = (a) => (a ? a.charAt(0).toUpperCase() + a.slice(1) : "");
/* "1 Std. 15 Min." statt "75 min" - bei mehrstündigen Fahrten liest sich die
reine Minutenzahl schlecht. */
function dauerText(sekunden) {
if (sekunden == null || !isFinite(sekunden)) return "";
const min = Math.max(0, Math.round(sekunden / 60));
const std = Math.floor(min / 60);
return std ? `${std} Std. ${min % 60} Min.` : `${min} Min.`;
}
/* Start und Ankunft eines Formulars als echte Zeitpunkte. Liegt die Ankunft
vor der Abfahrt, wurde über Mitternacht gefahren - dann zählt der Folgetag.
null, solange Datum oder eine der beiden Uhrzeiten fehlt. */
function fahrtZeitraum(praefix) {
const datum = $(`#${praefix}Datum`).value, ab = $(`#${praefix}Start`).value, an = $(`#${praefix}Ende`).value;
if (!datum || !ab || !an) return null;
const start = new Date(`${datum}T${ab}:00`);
let ende = new Date(`${datum}T${an}:00`);
if (isNaN(start) || isNaN(ende)) return null;
if (ende <= start) ende = new Date(ende.getTime() + dTag);
return { start, ende };
}
function fahrtDauerText(praefix) {
const z = fahrtZeitraum(praefix);
return z ? dauerText((z.ende - z.start) / 1000) : "";
}
/* Datum für ein <input type="date"> in Ortszeit. Bewusst nicht über
toISOString(), das rechnet nach UTC um und liefert kurz nach Mitternacht
@@ -2489,16 +2520,30 @@ function zeitfeld(d) {
function fahrtFelder(praefix, t) {
const start = t && t.ts_start ? new Date(t.ts_start) : new Date();
const dauer = t && t.duration_s != null ? Math.round(t.duration_s / 60) : "";
const ende = t && t.ts_end ? new Date(t.ts_end) : null;
const art = t && t.art ? t.art : FAHRT_ARTEN[0];
const wert = (v) => (v == null ? "" : v);
const startzeit = t ? zeitfeld(start) : "08:00", ankunftzeit = ende ? zeitfeld(ende) : "08:30";
// Die Dauer ist bewusst kein Eingabefeld: sie ergibt sich aus Start und
// Ankunft und würde sich sonst mit ihnen widersprechen können. Aus
// t.duration_s statt neu berechnet, damit eine über Mitternacht laufende
// bestehende Fahrt (Ankunft "kleiner" als Start) korrekt bleibt; bei einer
// neuen Fahrt (t === null) ergibt sich der Startwert aus denselben
// Vorgabezeiten wie die Felder selbst. Der data-fahrtzeitfeld-Handler
// rechnet bei jeder Änderung neu.
let dauerSekunden = t && t.duration_s != null ? t.duration_s : null;
if (dauerSekunden == null) {
const [sh, sm] = startzeit.split(":").map(Number), [eh, em] = ankunftzeit.split(":").map(Number);
dauerSekunden = ((eh * 60 + em) - (sh * 60 + sm) + (eh * 60 + em <= sh * 60 + sm ? 1440 : 0)) * 60;
}
const dauer = dauerText(dauerSekunden);
return `
<div class="feld"><label for="${praefix}Datum">Datum</label><input type="date" id="${praefix}Datum" value="${datumsfeld(start)}"></div>
<div class="feld"><label for="${praefix}Start">Startzeit</label><input type="time" id="${praefix}Start" value="${t ? zeitfeld(start) : "08:00"}"></div>
<div class="feld"><label for="${praefix}Dauer">Dauer</label>
<span class="mitEinheit"><input type="number" min="0" id="${praefix}Dauer" value="${dauer}" placeholder="0"><span>min</span></span></div>
<div class="feld"><label for="${praefix}Datum">Datum</label><input type="date" id="${praefix}Datum" value="${datumsfeld(start)}" data-fahrtzeitfeld="${praefix}"></div>
<div class="feld"><label for="${praefix}Start">Startzeit</label><input type="time" id="${praefix}Start" value="${startzeit}" data-fahrtzeitfeld="${praefix}"></div>
<div class="feld"><label for="${praefix}Ende">Ankunftszeit</label><input type="time" id="${praefix}Ende" value="${ankunftzeit}" data-fahrtzeitfeld="${praefix}"></div>
<div class="feld"><label>Dauer</label><span id="${praefix}DauerAnzeige" style="color:var(--fg2)">${dauer}</span></div>
<div class="feld"><label for="${praefix}Art">Art</label>
<select id="${praefix}Art">${FAHRT_ARTEN.map((a) => `<option ${a === art ? "selected" : ""}>${a}</option>`).join("")}</select></div>
<select id="${praefix}Art">${FAHRT_ARTEN.map((a) => `<option value="${a}" ${a === art ? "selected" : ""}>${artText(a)}</option>`).join("")}</select></div>
<div class="feld"><label for="${praefix}Startort">Startort</label><input id="${praefix}Startort" value="${esc(wert(t && t.start_address))}" placeholder="unbekannt"></div>
<div class="feld"><label for="${praefix}Zielort">Zielort</label><input id="${praefix}Zielort" value="${esc(wert(t && t.end_address))}" placeholder="unbekannt"></div>
<div class="feld"><label for="${praefix}OdoStart">Startkilometer</label><input type="number" id="${praefix}OdoStart" value="${wert(t && t.odo_start)}" placeholder="wird ergänzt"></div>
@@ -2510,11 +2555,10 @@ function fahrtFelder(praefix, t) {
function fahrtFormularWerte(praefix) {
const zahl = (id) => { const v = $(`#${praefix}${id}`).value; return v === "" ? null : parseFloat(v.replace(",", ".")); };
const text = (id) => $(`#${praefix}${id}`).value.trim() || null;
const dauer = parseInt($(`#${praefix}Dauer`).value, 10) || 0;
const tsStart = new Date(`${$(`#${praefix}Datum`).value}T${$(`#${praefix}Start`).value}:00`);
const z = fahrtZeitraum(praefix);
return {
ts_start: tsStart.toISOString(),
ts_end: new Date(tsStart.getTime() + dauer * 60000).toISOString(),
ts_start: z ? z.start.toISOString() : null,
ts_end: z ? z.ende.toISOString() : null,
art: $(`#${praefix}Art`).value,
start_ort: text("Startort"), ziel_ort: text("Zielort"),
odo_start: zahl("OdoStart"), odo_end: zahl("OdoEnde"), distanz: zahl("Distanz"),
@@ -2556,7 +2600,7 @@ function vTrips() {
<div class="acc-b ${mi === 0 ? "on" : ""}" id="m${j}${m}">`;
tm.forEach((t) => { h += swipeZeile(`trip:${t.trip_id}`, `<button class="leaf" data-go="trip:${t.trip_id}">
<span class="k">${strecke(t)}
<small>${dedat(new Date(t.ts_start))} · ${esc(t.art)}</small></span>
<small>${dedat(new Date(t.ts_start))} · ${artText(t.art)}</small></span>
<span class="v">${t.distance_km != null ? de(t.distance_km) + " km" : "offen"}<small>${fahrtVerbrauch(t) != null ? de(fahrtVerbrauch(t), 1) + " l/100 km" : t.status}</small></span>
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`); });
h += `</div></div>`;
@@ -2586,9 +2630,8 @@ function vTrip(id) {
<span class="label">Distanz</span>
<div class="fig" style="font-size:52px;margin-top:10px">${t.distance_km != null ? de(t.distance_km) : ""}<span class="unit">km</span></div>
<div style="margin-top:15px">
<button class="pill ${t.art === "arbeitsweg" ? "work" : ""}" data-art="${t.trip_id}"
style="cursor:pointer;font-family:inherit">${esc(t.art)}</button></div>
<span class="label" style="margin-top:12px">tippen zum Umschalten</span>
<button class="pill" data-art="${t.trip_id}"
style="cursor:pointer;font-family:inherit">${artText(t.art)}</button></div>
</div>
<div class="tile"><dl class="rows">
<div class="row"><dt>Startort</dt><dd>${esc(t.start_address || "unbekannt")}<small>${dedat(new Date(t.ts_start))}</small></dd></div>
@@ -4128,6 +4171,15 @@ function ereignisseVerdrahten() {
if (liste) liste.innerHTML = entitaetZeilenMarkup(sucheFeld, key, setupSuchtext, wert);
return;
}
// Gleicher Grund wie beim tankpreisfeld-Handler darunter: nur die
// Anzeige patchen, kein render() - das würde das gerade bearbeitete Feld
// neu erzeugen und den Fokus verlieren.
const zeitfeldPraefix = e.target.dataset.fahrtzeitfeld;
if (zeitfeldPraefix) {
const anzeige = ROOT.getElementById(`${zeitfeldPraefix}DauerAnzeige`);
if (anzeige) anzeige.textContent = fahrtDauerText(zeitfeldPraefix);
return;
}
const praefix = e.target.dataset.tankpreisfeld;
if (!praefix) return;
const liter = parseFloat((ROOT.getElementById(`${praefix}Liter`).value || "0").replace(",", "."));
+6 -2
View File
@@ -196,7 +196,6 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
.pill{font-size:12px;letter-spacing:.01em;text-transform:none;padding:5px 11px;
border:0;background:var(--ios-fill);color:var(--fg2)}
.pill.work{background:rgba(245,5,55,.16);color:var(--ios-tint)}
.status{padding:14px 0 2px}
.status .t{font-size:16px}
@@ -333,7 +332,12 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
/* Fahrzeugbild randlos wie in einer Fahrzeug-App: der Platzhalter bleibt
sichtbar, aber ohne technischen Strichrahmen. */
.szene{margin-bottom:20px}
.bildbox.bild-fehlt .platzhalter{border:0;background:var(--tile);border-radius:var(--r-tile)}
/* --tile statt --ios-fill waere hier naheliegend, faellt im Tag-Theme aber
auf reines Weiss (#FFFFFF) - ununterscheidbar von Canvas und den meisten
Kacheln drumherum, die Box verschwand. --ios-fill ist in beiden Themes ein
deutlich sichtbares transluzentes Grau, dieselbe Konvention wie
.feld select/.aktion ("das hier ist eine Flaeche"). */
.bildbox.bild-fehlt .platzhalter{border:0;background:var(--ios-fill);border-radius:var(--r-tile)}
.platzhalter-datei{display:none}
/* Zeilen mit Wert plus Statuspunkt rechts (Sicherheitscheck) ruhiger setzen */
@@ -1 +1 @@
{"version": 1787000000}
{"version": 1787009000}
-1
View File
@@ -676,7 +676,6 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
border-radius: var(--r-pill);
padding: 6px 12px;
}
.pill.work { color: var(--red); border-color: var(--red); }
/* ------------------------------------------------------------ Akkordeon */
.acc-h {