Phase 7c: restliche Screens - alle 21 Seiten stehen

Fahrzeugdaten, Batterieverlauf, Service mit Werkstatt und Servicebuch,
Reifen, die ganze Versicherungs- und Steuergruppe, Einstellungen in voller
Tiefe und die Live-Ansicht.

Portiert dazu: die eigene Oelwechsel-Prognose (rechnet ab dem letzten
Wechsel im Servicebuch neu, weil der Langzeitschnitt dafuer zu traege ist),
Kalenderdateien fuer Termine und die CSV-Ausgabe im deutschen Format.

Die Live-Ansicht ist gebaut, aber ueber einen Schalter abgeschaltet: die
Werte dafuer liefert erst der FMM003. Ein Menuepunkt mit lauter Nullen waere
schlechter als keiner.

Zwei Stellen, an denen die App bewusst zurueckhaltend ist: ein unbekannter
Pruefpunkt erscheint als hohler Ring statt als geratenes Gruen, und der
Reifen-Kilometerstand wird nie zurueckgeschrieben, weil ihn der Server
fortschreibt.

49 Tests, darunter ein Rendertest ueber alle 21 Seiten in beiden Layouts
mit Beispieldaten in der Form, die das Backend wirklich liefert.
This commit is contained in:
Paul Nothaft
2026-08-11 11:17:47 +02:00
parent 8b64a808ed
commit 8e6e5bc2ee
16 changed files with 2258 additions and 54 deletions
+96
View File
@@ -0,0 +1,96 @@
/**
* Eigene Ölwechsel-Prognose. Portiert aus `oelwechselPrognose()` im alten
* Panel.
*
* Die Rechnung beginnt bewusst neu ab dem letzten tatsächlich durchgeführten
* Ölwechsel — der allgemeine Langzeitdurchschnitt seit dem allerersten
* Servicebucheintrag wäre dafür zu träge. Aus km und Tagen seit dem letzten
* Wechsel ergibt sich die aktuelle Fahrleistung pro Tag; hochgerechnet auf die
* verbleibenden km bis zum eingestellten Intervall ergibt das ein Datum,
* gedeckelt auf spätestens das Zeitlimit des Intervalls.
*/
import type { Fahrzeug } from "./profilAdapter"
const TAG_MS = 86_400_000
export interface Oelprognose {
/** ISO-Tagesdatum. */
datum: string
restKm: number
kmProTag: number
/** Die Zeitgrenze hat gegriffen, nicht die Kilometergrenze. */
durchZeitlimit: boolean
}
interface Buchhaltung {
datum?: string
km?: number
art?: string
}
function monatePlus(isoDatum: string, monate: number): string {
const d = new Date(isoDatum)
if (Number.isNaN(d.getTime())) return isoDatum
d.setMonth(d.getMonth() + monate)
return d.toISOString().slice(0, 10)
}
/** Der jüngste Servicebucheintrag, dessen Art auf einen Ölwechsel hindeutet. */
export function letzterOelwechsel(buch: readonly Buchhaltung[]): Buchhaltung | null {
const treffer = buch
.filter((e) => typeof e.art === "string" && /öl|oel/i.test(e.art) && e.datum && e.km != null)
.sort((a, b) => String(b.datum).localeCompare(String(a.datum)))
return treffer[0] ?? null
}
export function oelwechselPrognose(
fahrzeug: Fahrzeug,
buch: readonly Buchhaltung[],
jetzt = new Date(),
): Oelprognose | null {
if (!fahrzeug.odoBekannt) return null
const letzter = letzterOelwechsel(buch)
if (!letzter?.datum || letzter.km == null) return null
const oel = fahrzeug.oel
const intervallKm =
oel.modus === "hersteller"
? (oel.herstellerKm ?? 0)
: Math.min(oel.km ?? Infinity, oel.herstellerKm ?? Infinity)
const intervallMonate =
oel.modus === "hersteller"
? (oel.herstellerMonate ?? 0)
: Math.min(oel.monate ?? Infinity, oel.herstellerMonate ?? Infinity)
if (!Number.isFinite(intervallKm) || intervallKm <= 0) return null
const zielKm = letzter.km + intervallKm
const restKm = Math.max(0, zielKm - fahrzeug.odo)
const kappDatum = monatePlus(letzter.datum, Number.isFinite(intervallMonate) ? intervallMonate : 24)
if (restKm <= 0) {
return { datum: jetzt.toISOString().slice(0, 10), restKm: 0, kmProTag: 0, durchZeitlimit: false }
}
const tageSeit = (jetzt.getTime() - new Date(letzter.datum).getTime()) / TAG_MS
const kmSeit = fahrzeug.odo - letzter.km
// Ohne belastbare Fahrleistung (direkt nach dem Wechsel) gibt es keine
// seriöse Kilometerprognose — dann gilt das Zeitlimit.
if (tageSeit <= 0 || kmSeit <= 0) {
return { datum: kappDatum, restKm, kmProTag: 0, durchZeitlimit: true }
}
const kmProTag = kmSeit / tageSeit
const tageBisZiel = restKm / kmProTag
const ausKm = new Date(jetzt.getTime() + tageBisZiel * TAG_MS).toISOString().slice(0, 10)
const durchZeitlimit = kappDatum < ausKm
return {
datum: durchZeitlimit ? kappDatum : ausKm,
restKm,
kmProTag,
durchZeitlimit,
}
}