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