Parity-Regel: Uebersicht-Service-Block in companion-app portiert
Nutzer stellte klar: Panel und companion-app sollen immer denselben
funktionalen Stand haben, auch wenn eine Anfrage nur das Panel nennt -
das ist reiner Testkomfort (Docker-Instanz laesst sich am schnellsten
ansehen), keine Scope-Entscheidung. Regel als verbindlicher Absatz in
AGENTS.md verankert, direkt unter der bestehenden Maintenance-Regel.
Erste Anwendung im selben Zug: die eben im Panel gebaute
"Naechster Service"-Kachel nach companion-app/ portiert.
- daten/service.ts: neues naechsterService() als Gegenstueck zu
naechsterTermin() im Panel - waehlt ueber Oelwechsel-/Inspektions-
Prognose und die von Hand gepflegte Hauptuntersuchung hinweg den
zeitlich naechsten Termin. bisText() liefert denselben Artikel wie
ART_BIS im Panel ("bis zum Oelwechsel" / "bis zur Inspektion").
- screens/Uebersicht.tsx: die bisherigen Kacheln "Reichweite"/
"Kilometerstand" nebeneinander plus eine separate "Service"-Kachel
mit Werteliste weichen einer Reichweiten-Kachel plus einer
Service-Kachel mit .dm-serviceblock (Knopf, nur der obere Teil) und
.dm-servicezeile (reine Anzeige) darunter - dieselbe Control/Content-
Trennung wie im Panel.
- stile/screens.css: .dm-serviceblock/.dm-servicezeile ergaenzt.
Tests: 4 neue in service.test.ts (naechsterService waehlt das frueher
faellige Datum, nimmt die Hauptuntersuchung auf, liefert nichts ohne
Servicebuch/HU, bisText-Artikel je Art), 1 neuer in screens.test.tsx,
der mit einem vi.fn() als geheZu wirklich belegt, dass ein Klick auf
den Serviceblock navigiert und ein Klick auf die Kilometerstand-Zeile
es nicht tut - dafuer bekam zeige() in screens.test.tsx erst einen
injizierbaren geheZu-Parameter (vorher hart auf () => {} verdrahtet).
npm run typecheck sauber, npm run test 100/100 (von 95), npm run build
erfolgreich.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { inspektionPrognose, letzterOelwechsel, oelwechselPrognose } from "./service"
|
||||
import { bisText, inspektionPrognose, letzterOelwechsel, naechsterService, oelwechselPrognose } from "./service"
|
||||
import type { Fahrzeug } from "./profilAdapter"
|
||||
|
||||
function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug {
|
||||
@@ -112,3 +112,40 @@ describe("inspektionPrognose", () => {
|
||||
expect(inspektionPrognose(fahrzeug(46000), [], jetzt)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("naechsterService", () => {
|
||||
const jetzt = new Date("2026-08-11T12:00:00")
|
||||
|
||||
it("wählt über Ölwechsel und Inspektion hinweg das zeitlich frühere Datum", () => {
|
||||
// Beide unabhängig mit derselben Formel berechnet, statt ein erwartetes
|
||||
// Datum von Hand vorzurechnen - so bleibt der Test robust gegen spätere
|
||||
// Änderungen an den Intervallen, prüft aber trotzdem die Auswahllogik.
|
||||
const oel = oelwechselPrognose(fahrzeug(46000), BUCH, jetzt)
|
||||
const insp = inspektionPrognose(fahrzeug(46000), BUCH, jetzt)
|
||||
const erwarteteArt = oel!.datum <= insp!.datum ? "Ölwechsel" : "Inspektion"
|
||||
|
||||
const s = naechsterService(fahrzeug(46000), BUCH, jetzt)
|
||||
expect(s).not.toBeNull()
|
||||
expect(s!.art).toBe(erwarteteArt)
|
||||
expect(s!.datum).toBe(erwarteteArt === "Ölwechsel" ? oel!.datum : insp!.datum)
|
||||
})
|
||||
|
||||
it("nimmt die Hauptuntersuchung auf, obwohl sie keine eigene Prognose hat", () => {
|
||||
const f = { ...fahrzeug(46000), hu: "2026-09-01" }
|
||||
const s = naechsterService(f, [], jetzt)
|
||||
expect(s).not.toBeNull()
|
||||
expect(s!.art).toBe("Hauptuntersuchung")
|
||||
expect(s!.restKm).toBeNull()
|
||||
expect(s!.datum).toBe("2026-09-01")
|
||||
})
|
||||
|
||||
it("liefert nichts ohne Servicebuch und ohne gepflegte Hauptuntersuchung", () => {
|
||||
expect(naechsterService(fahrzeug(46000), [], jetzt)).toBeNull()
|
||||
})
|
||||
|
||||
it("bisText nennt den richtigen Artikel je Art", () => {
|
||||
expect(bisText("Ölwechsel")).toBe("bis zum Ölwechsel")
|
||||
expect(bisText("Inspektion")).toBe("bis zur Inspektion")
|
||||
expect(bisText("Hauptuntersuchung")).toBe("bis zur Hauptuntersuchung")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -120,3 +120,66 @@ export function inspektionPrognose(
|
||||
): Oelprognose | null {
|
||||
return servicePrognose(fahrzeug, letzterEintrag(buch, /inspektion/i), 30000, 24, jetzt)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------ Nächster Service */
|
||||
|
||||
export type Serviceart = "Ölwechsel" | "Inspektion" | "Hauptuntersuchung"
|
||||
|
||||
export interface NaechsterService {
|
||||
art: Serviceart
|
||||
/** Restkilometer bis zur Fälligkeit. null bei der Hauptuntersuchung — die
|
||||
hat kein Kilometerziel, nur einen Termin. */
|
||||
restKm: number | null
|
||||
/** ISO-Tagesdatum der voraussichtlichen Fälligkeit. */
|
||||
datum: string
|
||||
}
|
||||
|
||||
/* "der Ölwechsel", aber "die Inspektion"/"die Hauptuntersuchung" - ohne diese
|
||||
Zuordnung stünde auf der Übersicht "bis zum Inspektion". Gleiche Tabelle wie
|
||||
ART_BIS im Panel (audi-dashboard-app.js). */
|
||||
const BIS_TEXT: Record<Serviceart, string> = {
|
||||
"Ölwechsel": "bis zum Ölwechsel",
|
||||
"Inspektion": "bis zur Inspektion",
|
||||
"Hauptuntersuchung": "bis zur Hauptuntersuchung",
|
||||
}
|
||||
|
||||
export function bisText(art: Serviceart): string {
|
||||
return BIS_TEXT[art]
|
||||
}
|
||||
|
||||
/**
|
||||
* Der zeitlich nächste anstehende Service über alle drei Arten hinweg —
|
||||
* Gegenstück zu `naechsterTermin()` im Panel.
|
||||
*
|
||||
* Ölwechsel und Inspektion kommen aus der eigenen Prognose (Servicebuch plus
|
||||
* tatsächliche Fahrleistung), die Hauptuntersuchung aus dem im Profil
|
||||
* gepflegten Termin: sie hat kein Kilometerziel, das Fahrzeug meldet sie nicht,
|
||||
* und hochrechnen lässt sie sich auch nicht.
|
||||
*/
|
||||
export function naechsterService(
|
||||
fahrzeug: Fahrzeug,
|
||||
buch: readonly Buchhaltung[],
|
||||
jetzt = new Date(),
|
||||
): NaechsterService | null {
|
||||
const kandidaten: NaechsterService[] = []
|
||||
|
||||
const oel = oelwechselPrognose(fahrzeug, buch, jetzt)
|
||||
if (oel) kandidaten.push({ art: "Ölwechsel", restKm: oel.restKm, datum: oel.datum })
|
||||
|
||||
const insp = inspektionPrognose(fahrzeug, buch, jetzt)
|
||||
if (insp) kandidaten.push({ art: "Inspektion", restKm: insp.restKm, datum: insp.datum })
|
||||
|
||||
// fahrzeug.hu wird von Hand gepflegt und kann leer oder unlesbar sein.
|
||||
const hu = new Date(fahrzeug.hu)
|
||||
if (fahrzeug.hu && !Number.isNaN(hu.getTime())) {
|
||||
kandidaten.push({
|
||||
art: "Hauptuntersuchung",
|
||||
restKm: null,
|
||||
datum: hu.toISOString().slice(0, 10),
|
||||
})
|
||||
}
|
||||
|
||||
if (kandidaten.length === 0) return null
|
||||
// ISO-Tagesdaten, deshalb reicht der Zeichenkettenvergleich.
|
||||
return kandidaten.sort((a, b) => a.datum.localeCompare(b.datum))[0] ?? null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user