companion-app: catch up six days of panel drift before the native build
The phone-app codebase (companion-app/) hadn't been touched since 2026-08-11 - every panel fix and feature since then (HIG audit rounds, receipt upload, trip editing, Inspektion forecast, ...) existed only in the HA panel. Found while auditing what's needed for a real iPhone build; user decision was to port everything now rather than ship a stale app. Six real, verified gaps (not blind copies of panel CSS/markup, which doesn't transfer to the @audi-dash/ui component set): - Battery voltage cutoff was still 13.2V, not the panel's 12.8V fix. - Dead wlan_name field (WLAN trip detection was fully removed from the backend 2026-08-12) - removed from the adapter and the settings screen instead of leaving a form field that silently does nothing. - No Inspektion forecast - added inspektionPrognose() alongside the existing oelwechselPrognose(), sharing a refactored core. - Arbeitsweg pill used the design-system's "work" variant, which is documented as recoloring to red - stopped passing it, same fix as the panel. - Trip creation only took Beginn/Ende/Art; extended with Startort/ Zielort/Kilometerstand/Distanz via a new shared FahrtFelder.tsx. - FahrtDetail.tsx was read-only - added an edit mode using the same shared fields, backed by a new DataMetricApi.fahrtAktualisieren() calling the backend service built earlier this session. Explicitly checked and found not applicable: price rounding (already 2 decimals here), pull-to-refresh CSS (no native gesture to fix), the panel's drag/paste receipt dialog (solves a desktop-browser problem this native app doesn't have - the plain file picker already gets iOS's native Files integration), and the purely cosmetic panel CSS fixes. npm install run at the repo root (node_modules was incomplete/stale), package-lock.json reflects the real dependency tree. Verified with npm run typecheck (clean), npm run test (95/95, up from 90 - added interaction tests for the new form and inspektionPrognose), and npm run build (succeeds). Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -19,7 +19,6 @@ function beispielprofil(aenderung: Partial<Profil> = {}): Profil {
|
||||
untertitel: "quattro · 331 kW",
|
||||
kennzeichen: "M AB 1234",
|
||||
tankvolumen_liter: 58,
|
||||
wlan_name: "Audi_MMI",
|
||||
},
|
||||
einstellungen: {
|
||||
uebersichtsbild: "seitenansicht.webp",
|
||||
|
||||
@@ -21,7 +21,6 @@ export interface Einstellungen {
|
||||
nachtBis: string
|
||||
pauseMin: number
|
||||
markenname: string
|
||||
wlanName: string
|
||||
backupIntervall: string
|
||||
letztesBackup: string | null
|
||||
ausfuehrung: string
|
||||
@@ -114,7 +113,6 @@ export function profilZuEinstellungen(profil: Profil): Einstellungen {
|
||||
nachtBis: text(einst, "nacht_bis", "06:00"),
|
||||
pauseMin: zahl(einst, "fahrten_pausenzeit_min") ?? 15,
|
||||
markenname: text(einst, "kraftstoffanbieter", "Shell"),
|
||||
wlanName: text(fahrzeug, "wlan_name"),
|
||||
backupIntervall: text(einst, "backup_intervall", "aus"),
|
||||
letztesBackup: text(einst, "letztes_backup") || null,
|
||||
ausfuehrung: text(fahrzeug, "ausfuehrung"),
|
||||
@@ -199,7 +197,6 @@ export function zusammenfuehren(
|
||||
f["zusatz"] = einstellungen.fahrzeugzusatz
|
||||
f["untertitel"] = einstellungen.fahrzeuguntertitel
|
||||
f["kennzeichen"] = einstellungen.kennzeichen
|
||||
f["wlan_name"] = einstellungen.wlanName
|
||||
f["ausfuehrung"] = einstellungen.ausfuehrung
|
||||
f["tankvolumen_liter"] = einstellungen.tankvolumen
|
||||
f["fin"] = fahrzeug.fin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { letzterOelwechsel, oelwechselPrognose } from "./service"
|
||||
import { inspektionPrognose, letzterOelwechsel, oelwechselPrognose } from "./service"
|
||||
import type { Fahrzeug } from "./profilAdapter"
|
||||
|
||||
function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug {
|
||||
@@ -92,3 +92,23 @@ describe("oelwechselPrognose", () => {
|
||||
expect(oelwechselPrognose(fahrzeug(46000), [], jetzt)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("inspektionPrognose", () => {
|
||||
const jetzt = new Date("2026-08-11T12:00:00")
|
||||
|
||||
it("rechnet mit dem festen Intervall 30.000 km / 24 Monate, nicht dem Ölwechsel-Intervall", () => {
|
||||
// Letzte Inspektion 05.03.2025 bei 25000 km -> Ziel 55000. Bei 46000 km
|
||||
// sind das 9000 km offen, unabhängig vom oel-Feld des Fahrzeugs (15000/12).
|
||||
const p = inspektionPrognose(fahrzeug(46000), BUCH, jetzt)
|
||||
expect(p).not.toBeNull()
|
||||
expect(p!.restKm).toBe(9000)
|
||||
})
|
||||
|
||||
it("ignoriert den Ölwechsel-Eintrag bei der Suche nach der letzten Inspektion", () => {
|
||||
expect(inspektionPrognose(fahrzeug(46000), [BUCH[0]!], jetzt)).toBeNull()
|
||||
})
|
||||
|
||||
it("liefert nichts, wenn nie eine Inspektion eingetragen wurde", () => {
|
||||
expect(inspektionPrognose(fahrzeug(46000), [], jetzt)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/**
|
||||
* Eigene Ölwechsel-Prognose. Portiert aus `oelwechselPrognose()` im alten
|
||||
* Panel.
|
||||
* Eigene Service-Prognosen (Ölwechsel, Inspektion). Portiert aus
|
||||
* `oelwechselPrognose()` im alten Panel; die Inspektion kam am 2026-08-17
|
||||
* hinzu (§ AGENTS.md, "Inspektion: voraussichtliches Datum ergänzen").
|
||||
*
|
||||
* 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.
|
||||
* Eintrag dieser Art — der allgemeine Langzeitdurchschnitt seit dem
|
||||
* allerersten Servicebucheintrag wäre dafür zu träge. Aus km und Tagen seit
|
||||
* dem letzten Eintrag ergibt sich die aktuelle Fahrleistung pro Tag;
|
||||
* hochgerechnet auf die verbleibenden km bis zum Intervall ergibt das ein
|
||||
* Datum, gedeckelt auf spätestens das Zeitlimit des Intervalls.
|
||||
*/
|
||||
|
||||
import type { Fahrzeug } from "./profilAdapter"
|
||||
@@ -36,33 +37,31 @@ function monatePlus(isoDatum: string, monate: number): string {
|
||||
return d.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
/** Der jüngste Servicebucheintrag, dessen Art auf einen Ölwechsel hindeutet. */
|
||||
export function letzterOelwechsel(buch: readonly Buchhaltung[]): Buchhaltung | null {
|
||||
/** Der jüngste Servicebucheintrag, dessen Art auf das gesuchte Stichwort
|
||||
passt (z. B. "öl"/"oel" oder "inspektion"). */
|
||||
function letzterEintrag(buch: readonly Buchhaltung[], stichwort: RegExp): Buchhaltung | null {
|
||||
const treffer = buch
|
||||
.filter((e) => typeof e.art === "string" && /öl|oel/i.test(e.art) && e.datum && e.km != null)
|
||||
.filter((e) => typeof e.art === "string" && stichwort.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(
|
||||
/** Der jüngste Servicebucheintrag, dessen Art auf einen Ölwechsel hindeutet. */
|
||||
export function letzterOelwechsel(buch: readonly Buchhaltung[]): Buchhaltung | null {
|
||||
return letzterEintrag(buch, /öl|oel/i)
|
||||
}
|
||||
|
||||
/** Gemeinsamer Kern für Ölwechsel- und Inspektionsprognose - beide rechnen
|
||||
identisch, nur mit anderem Intervall und anderer Suche im Servicebuch. */
|
||||
function servicePrognose(
|
||||
fahrzeug: Fahrzeug,
|
||||
buch: readonly Buchhaltung[],
|
||||
jetzt = new Date(),
|
||||
letzter: Buchhaltung | null,
|
||||
intervallKm: number,
|
||||
intervallMonate: number,
|
||||
jetzt: 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
|
||||
@@ -76,7 +75,7 @@ export function oelwechselPrognose(
|
||||
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
|
||||
// Ohne belastbare Fahrleistung (direkt nach dem Eintrag) gibt es keine
|
||||
// seriöse Kilometerprognose — dann gilt das Zeitlimit.
|
||||
if (tageSeit <= 0 || kmSeit <= 0) {
|
||||
return { datum: kappDatum, restKm, kmProTag: 0, durchZeitlimit: true }
|
||||
@@ -94,3 +93,30 @@ export function oelwechselPrognose(
|
||||
durchZeitlimit,
|
||||
}
|
||||
}
|
||||
|
||||
export function oelwechselPrognose(
|
||||
fahrzeug: Fahrzeug,
|
||||
buch: readonly Buchhaltung[],
|
||||
jetzt = new Date(),
|
||||
): Oelprognose | 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)
|
||||
return servicePrognose(fahrzeug, letzterOelwechsel(buch), intervallKm, intervallMonate, jetzt)
|
||||
}
|
||||
|
||||
/** Inspektion: feste 30.000 km / 24 Monate, wie auch das Fahrzeug selbst und
|
||||
das alte Panel rechnen - kein einstellbares Intervall wie beim Ölwechsel. */
|
||||
export function inspektionPrognose(
|
||||
fahrzeug: Fahrzeug,
|
||||
buch: readonly Buchhaltung[],
|
||||
jetzt = new Date(),
|
||||
): Oelprognose | null {
|
||||
return servicePrognose(fahrzeug, letzterEintrag(buch, /inspektion/i), 30000, 24, jetzt)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user