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 <[email protected]>
This commit is contained in:
@@ -38,7 +38,11 @@ route — see section C; a twelfth round the same day removed `HECKKLAPPENSCHLOS
|
|||||||
half-cut-off image placeholder, gave the Inspektion its own "voraussichtlich am …" forecast, made
|
half-cut-off image placeholder, gave the Inspektion its own "voraussichtlich am …" forecast, made
|
||||||
trips fully editable (new `audi_dashboard_fahrt_aktualisieren`, "Neue Fahrt" now offers every field
|
trips fully editable (new `audi_dashboard_fahrt_aktualisieren`, "Neue Fahrt" now offers every field
|
||||||
the detail page shows), aligned the page title with the tiles on phone width, and closed two
|
the detail page shows), aligned the page title with the tiles on phone width, and closed two
|
||||||
dead-ends in the bootstrap retry loop behind the reported hanging "Lädt …" screen).
|
dead-ends in the bootstrap retry loop behind the reported hanging "Lädt …" screen; a thirteenth
|
||||||
|
round the same day replaced the trip form's editable "Dauer" with Start-/Ankunftszeit (duration is
|
||||||
|
now computed, not entered), capitalized the Art labels shown to the user, dropped the red styling
|
||||||
|
and hint text from the Arbeitsweg pill, and fixed a daylight-mode-only regression from round twelve
|
||||||
|
where the image placeholder's grey background had turned invisible-white).
|
||||||
This file is the entry point for every new agent
|
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
|
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.
|
the linked documents — this file points, it does not duplicate.
|
||||||
@@ -1278,6 +1282,46 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed
|
|||||||
war jedes Mal binnen ~2s da) - die Sackgassen sind im Code belegt, dass sie *die* Ursache
|
war jedes Mal binnen ~2s da) - die Sackgassen sind im Code belegt, dass sie *die* Ursache
|
||||||
beim Nutzer waren, ist damit aber nicht bewiesen. Tritt es erneut auf: Browser-Konsole
|
beim Nutzer waren, ist damit aber nicht bewiesen. Tritt es erneut auf: Browser-Konsole
|
||||||
erfragen, bevor weiter geraten wird.
|
erfragen, bevor weiter geraten wird.
|
||||||
|
- [x] Thirteenth round (2026-08-17, same day) — five more user-reported items on the trip form and
|
||||||
|
one theme bug found while verifying it:
|
||||||
|
(1) **"Dauer" replaced by "Ankunftszeit"; duration is no longer editable.** `fahrtFelder()`
|
||||||
|
now has Startzeit + Ankunftszeit inputs and a plain (non-input) duration display next to
|
||||||
|
them, computed by `fahrtZeitraum()`/`dauerText()` and kept live via a new
|
||||||
|
`data-fahrtzeitfeld` input handler (same "patch the one node, don't call render()" pattern
|
||||||
|
the existing `data-tankpreisfeld` handler uses, for the same reason: a full re-render would
|
||||||
|
steal focus from the field being typed in). An arrival time not later than the start time is
|
||||||
|
treated as crossing midnight (`+1 Tag`), matching how `_fahrt_beenden()` already handles a
|
||||||
|
real overnight trip. `fahrtFormularWerte()` derives `ts_start`/`ts_end` from the same
|
||||||
|
calculation - the backend never sees a separate duration field, so it can't disagree with
|
||||||
|
the times. Verified live: 09:00→10:45 shows "1 Std. 45 Min.", 23:30→00:15 shows "45 Min."
|
||||||
|
(crossed midnight correctly), and editing an existing trip's arrival time on the Einzelfahrt
|
||||||
|
page persisted the recalculated `duration_s` (7200s for a 10:00→12:00 edit, checked in
|
||||||
|
`fahrten.jsonl`).
|
||||||
|
(2) **Art values now display capitalized** ("Privat"/"Arbeitsweg") wherever shown to the user
|
||||||
|
- the trip-edit `<select>`, the trip-list row, and the Einzelfahrt pill - via a new
|
||||||
|
`artText()` helper; the stored value stays lowercase (`"privat"`/`"arbeitsweg"`), since it's
|
||||||
|
compared in several places (the pill, the "Arbeitsweg" statistic, the CSV export) and already
|
||||||
|
exists that way in every trip recorded so far. The CSV export itself was deliberately left
|
||||||
|
showing the raw value - a data file, not UI copy.
|
||||||
|
(3) **Arbeitsweg pill no longer red.** `.pill.work` (`color:var(--red)` / a translucent red
|
||||||
|
fill in the iOS overlay) is gone from both stylesheets along with the now-unused `work` class
|
||||||
|
on the button; the pill uses the same neutral styling regardless of Art, matching the request
|
||||||
|
that Arbeitsweg should look like Privat, not draw attention to itself.
|
||||||
|
(4) **"tippen zum Umschalten" hint removed** from under the Einzelfahrt pill, per explicit
|
||||||
|
request - the pill is still clickable, just without the caption.
|
||||||
|
(5) **Found while re-verifying the placeholder fix from round twelve: it broke daylight mode.**
|
||||||
|
`.bildbox.bild-fehlt .platzhalter{background:var(--tile)}` (added round twelve) reads fine in
|
||||||
|
Nacht theme, where `--tile` is a faint white overlay on a near-black canvas - but the iOS
|
||||||
|
overlay's Tag-theme block redefines `--tile:#FFFFFF`, identical to the canvas/most tiles
|
||||||
|
around it, so the placeholder box became invisible in daylight mode (measured live:
|
||||||
|
`background-color: rgb(255,255,255)` on both the placeholder and its surroundings). Switched
|
||||||
|
to `--ios-fill` (`rgba(120,120,128,.16)` in Tag theme, `.24` in Nacht) - the same token
|
||||||
|
`.feld select`/`.aktion` already use to mark "this is a surface", visibly grey in both themes.
|
||||||
|
Verified live in Tag theme: `background-color: rgba(120,120,128,.16)`, clearly a distinct box
|
||||||
|
against the white canvas (see screenshot taken during verification).
|
||||||
|
All five deployed to `audi_ha_test` (version `1787009000`) and synced to `installationspaket/`;
|
||||||
|
console showed only the pre-existing service-worker/404 noise from the missing vehicle photos,
|
||||||
|
nothing new.
|
||||||
- [ ] 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`
|
||||||
|
|||||||
@@ -2473,7 +2473,38 @@ function vReifen() {
|
|||||||
Felder decken genau das ab, was die Einzelfahrt-Seite anzeigt; Verbrauch,
|
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
|
Status und km-Quelle bleiben außen vor, die meldet das Fahrzeug bzw. leitet
|
||||||
das Backend ab. */
|
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 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
|
/* Datum für ein <input type="date"> in Ortszeit. Bewusst nicht über
|
||||||
toISOString(), das rechnet nach UTC um und liefert kurz nach Mitternacht
|
toISOString(), das rechnet nach UTC um und liefert kurz nach Mitternacht
|
||||||
@@ -2489,16 +2520,30 @@ function zeitfeld(d) {
|
|||||||
|
|
||||||
function fahrtFelder(praefix, t) {
|
function fahrtFelder(praefix, t) {
|
||||||
const start = t && t.ts_start ? new Date(t.ts_start) : new Date();
|
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 art = t && t.art ? t.art : FAHRT_ARTEN[0];
|
||||||
const wert = (v) => (v == null ? "" : v);
|
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 `
|
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}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="${t ? zeitfeld(start) : "08:00"}"></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}Dauer">Dauer</label>
|
<div class="feld"><label for="${praefix}Ende">Ankunftszeit</label><input type="time" id="${praefix}Ende" value="${ankunftzeit}" data-fahrtzeitfeld="${praefix}"></div>
|
||||||
<span class="mitEinheit"><input type="number" min="0" id="${praefix}Dauer" value="${dauer}" placeholder="0"><span>min</span></span></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>
|
<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}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}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>
|
<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) {
|
function fahrtFormularWerte(praefix) {
|
||||||
const zahl = (id) => { const v = $(`#${praefix}${id}`).value; return v === "" ? null : parseFloat(v.replace(",", ".")); };
|
const zahl = (id) => { const v = $(`#${praefix}${id}`).value; return v === "" ? null : parseFloat(v.replace(",", ".")); };
|
||||||
const text = (id) => $(`#${praefix}${id}`).value.trim() || null;
|
const text = (id) => $(`#${praefix}${id}`).value.trim() || null;
|
||||||
const dauer = parseInt($(`#${praefix}Dauer`).value, 10) || 0;
|
const z = fahrtZeitraum(praefix);
|
||||||
const tsStart = new Date(`${$(`#${praefix}Datum`).value}T${$(`#${praefix}Start`).value}:00`);
|
|
||||||
return {
|
return {
|
||||||
ts_start: tsStart.toISOString(),
|
ts_start: z ? z.start.toISOString() : null,
|
||||||
ts_end: new Date(tsStart.getTime() + dauer * 60000).toISOString(),
|
ts_end: z ? z.ende.toISOString() : null,
|
||||||
art: $(`#${praefix}Art`).value,
|
art: $(`#${praefix}Art`).value,
|
||||||
start_ort: text("Startort"), ziel_ort: text("Zielort"),
|
start_ort: text("Startort"), ziel_ort: text("Zielort"),
|
||||||
odo_start: zahl("OdoStart"), odo_end: zahl("OdoEnde"), distanz: zahl("Distanz"),
|
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}">`;
|
<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}">
|
tm.forEach((t) => { h += swipeZeile(`trip:${t.trip_id}`, `<button class="leaf" data-go="trip:${t.trip_id}">
|
||||||
<span class="k">${strecke(t)}
|
<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>
|
<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>`); });
|
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`); });
|
||||||
h += `</div></div>`;
|
h += `</div></div>`;
|
||||||
@@ -2586,9 +2630,8 @@ function vTrip(id) {
|
|||||||
<span class="label">Distanz</span>
|
<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 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">
|
<div style="margin-top:15px">
|
||||||
<button class="pill ${t.art === "arbeitsweg" ? "work" : ""}" data-art="${t.trip_id}"
|
<button class="pill" data-art="${t.trip_id}"
|
||||||
style="cursor:pointer;font-family:inherit">${esc(t.art)}</button></div>
|
style="cursor:pointer;font-family:inherit">${artText(t.art)}</button></div>
|
||||||
<span class="label" style="margin-top:12px">tippen zum Umschalten</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tile"><dl class="rows">
|
<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>
|
<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);
|
if (liste) liste.innerHTML = entitaetZeilenMarkup(sucheFeld, key, setupSuchtext, wert);
|
||||||
return;
|
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;
|
const praefix = e.target.dataset.tankpreisfeld;
|
||||||
if (!praefix) return;
|
if (!praefix) return;
|
||||||
const liter = parseFloat((ROOT.getElementById(`${praefix}Liter`).value || "0").replace(",", "."));
|
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;
|
.pill{font-size:12px;letter-spacing:.01em;text-transform:none;padding:5px 11px;
|
||||||
border:0;background:var(--ios-fill);color:var(--fg2)}
|
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{padding:14px 0 2px}
|
||||||
.status .t{font-size:16px}
|
.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
|
/* Fahrzeugbild randlos wie in einer Fahrzeug-App: der Platzhalter bleibt
|
||||||
sichtbar, aber ohne technischen Strichrahmen. */
|
sichtbar, aber ohne technischen Strichrahmen. */
|
||||||
.szene{margin-bottom:20px}
|
.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}
|
.platzhalter-datei{display:none}
|
||||||
|
|
||||||
/* Zeilen mit Wert plus Statuspunkt rechts (Sicherheitscheck) ruhiger setzen */
|
/* 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);
|
border-radius: var(--r-pill);
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
}
|
}
|
||||||
.pill.work { color: var(--red); border-color: var(--red); }
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------ Akkordeon */
|
/* ------------------------------------------------------------ Akkordeon */
|
||||||
.acc-h {
|
.acc-h {
|
||||||
|
|||||||
@@ -2473,7 +2473,38 @@ function vReifen() {
|
|||||||
Felder decken genau das ab, was die Einzelfahrt-Seite anzeigt; Verbrauch,
|
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
|
Status und km-Quelle bleiben außen vor, die meldet das Fahrzeug bzw. leitet
|
||||||
das Backend ab. */
|
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 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
|
/* Datum für ein <input type="date"> in Ortszeit. Bewusst nicht über
|
||||||
toISOString(), das rechnet nach UTC um und liefert kurz nach Mitternacht
|
toISOString(), das rechnet nach UTC um und liefert kurz nach Mitternacht
|
||||||
@@ -2489,16 +2520,30 @@ function zeitfeld(d) {
|
|||||||
|
|
||||||
function fahrtFelder(praefix, t) {
|
function fahrtFelder(praefix, t) {
|
||||||
const start = t && t.ts_start ? new Date(t.ts_start) : new Date();
|
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 art = t && t.art ? t.art : FAHRT_ARTEN[0];
|
||||||
const wert = (v) => (v == null ? "" : v);
|
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 `
|
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}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="${t ? zeitfeld(start) : "08:00"}"></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}Dauer">Dauer</label>
|
<div class="feld"><label for="${praefix}Ende">Ankunftszeit</label><input type="time" id="${praefix}Ende" value="${ankunftzeit}" data-fahrtzeitfeld="${praefix}"></div>
|
||||||
<span class="mitEinheit"><input type="number" min="0" id="${praefix}Dauer" value="${dauer}" placeholder="0"><span>min</span></span></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>
|
<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}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}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>
|
<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) {
|
function fahrtFormularWerte(praefix) {
|
||||||
const zahl = (id) => { const v = $(`#${praefix}${id}`).value; return v === "" ? null : parseFloat(v.replace(",", ".")); };
|
const zahl = (id) => { const v = $(`#${praefix}${id}`).value; return v === "" ? null : parseFloat(v.replace(",", ".")); };
|
||||||
const text = (id) => $(`#${praefix}${id}`).value.trim() || null;
|
const text = (id) => $(`#${praefix}${id}`).value.trim() || null;
|
||||||
const dauer = parseInt($(`#${praefix}Dauer`).value, 10) || 0;
|
const z = fahrtZeitraum(praefix);
|
||||||
const tsStart = new Date(`${$(`#${praefix}Datum`).value}T${$(`#${praefix}Start`).value}:00`);
|
|
||||||
return {
|
return {
|
||||||
ts_start: tsStart.toISOString(),
|
ts_start: z ? z.start.toISOString() : null,
|
||||||
ts_end: new Date(tsStart.getTime() + dauer * 60000).toISOString(),
|
ts_end: z ? z.ende.toISOString() : null,
|
||||||
art: $(`#${praefix}Art`).value,
|
art: $(`#${praefix}Art`).value,
|
||||||
start_ort: text("Startort"), ziel_ort: text("Zielort"),
|
start_ort: text("Startort"), ziel_ort: text("Zielort"),
|
||||||
odo_start: zahl("OdoStart"), odo_end: zahl("OdoEnde"), distanz: zahl("Distanz"),
|
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}">`;
|
<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}">
|
tm.forEach((t) => { h += swipeZeile(`trip:${t.trip_id}`, `<button class="leaf" data-go="trip:${t.trip_id}">
|
||||||
<span class="k">${strecke(t)}
|
<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>
|
<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>`); });
|
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`); });
|
||||||
h += `</div></div>`;
|
h += `</div></div>`;
|
||||||
@@ -2586,9 +2630,8 @@ function vTrip(id) {
|
|||||||
<span class="label">Distanz</span>
|
<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 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">
|
<div style="margin-top:15px">
|
||||||
<button class="pill ${t.art === "arbeitsweg" ? "work" : ""}" data-art="${t.trip_id}"
|
<button class="pill" data-art="${t.trip_id}"
|
||||||
style="cursor:pointer;font-family:inherit">${esc(t.art)}</button></div>
|
style="cursor:pointer;font-family:inherit">${artText(t.art)}</button></div>
|
||||||
<span class="label" style="margin-top:12px">tippen zum Umschalten</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tile"><dl class="rows">
|
<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>
|
<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);
|
if (liste) liste.innerHTML = entitaetZeilenMarkup(sucheFeld, key, setupSuchtext, wert);
|
||||||
return;
|
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;
|
const praefix = e.target.dataset.tankpreisfeld;
|
||||||
if (!praefix) return;
|
if (!praefix) return;
|
||||||
const liter = parseFloat((ROOT.getElementById(`${praefix}Liter`).value || "0").replace(",", "."));
|
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;
|
.pill{font-size:12px;letter-spacing:.01em;text-transform:none;padding:5px 11px;
|
||||||
border:0;background:var(--ios-fill);color:var(--fg2)}
|
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{padding:14px 0 2px}
|
||||||
.status .t{font-size:16px}
|
.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
|
/* Fahrzeugbild randlos wie in einer Fahrzeug-App: der Platzhalter bleibt
|
||||||
sichtbar, aber ohne technischen Strichrahmen. */
|
sichtbar, aber ohne technischen Strichrahmen. */
|
||||||
.szene{margin-bottom:20px}
|
.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}
|
.platzhalter-datei{display:none}
|
||||||
|
|
||||||
/* Zeilen mit Wert plus Statuspunkt rechts (Sicherheitscheck) ruhiger setzen */
|
/* 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);
|
border-radius: var(--r-pill);
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
}
|
}
|
||||||
.pill.work { color: var(--red); border-color: var(--red); }
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------ Akkordeon */
|
/* ------------------------------------------------------------ Akkordeon */
|
||||||
.acc-h {
|
.acc-h {
|
||||||
|
|||||||
Reference in New Issue
Block a user