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:
@@ -11,10 +11,12 @@ import { useState } from "react"
|
||||
import { Fig, ProgressBar, StatusRow, Tile } from "@audi-dash/ui"
|
||||
|
||||
import { useDaten } from "../daten/DatenKontext"
|
||||
import { bisText, naechsterService } from "../daten/service"
|
||||
import { datum, datumZeit, dauer, de, deOderStrich, eur } from "../format"
|
||||
import type { SeitenName } from "../navigation"
|
||||
import { Leerzustand, NaviKachel, Wertzeile, Werteliste } from "./bausteine"
|
||||
import { Leerzustand, NaviKachel } from "./bausteine"
|
||||
import { Bild } from "./Bild"
|
||||
import type { ServicebuchEintrag } from "./Service"
|
||||
import { Typenschild, schildErkennen } from "./Typenschild"
|
||||
|
||||
export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string) => void }) {
|
||||
@@ -40,6 +42,9 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
|
||||
.join(" ")
|
||||
.trim()
|
||||
|
||||
const buch = (fahrzeug.service["buch"] ?? []) as ServicebuchEintrag[]
|
||||
const service = naechsterService(fahrzeug, buch)
|
||||
|
||||
const letzteFahrt = fahrten
|
||||
.slice()
|
||||
.sort((a, b) => new Date(b.ts_start).getTime() - new Date(a.ts_start).getTime())[0]
|
||||
@@ -103,62 +108,60 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
|
||||
onClick={() => geheZu("sicherheit")}
|
||||
/>
|
||||
|
||||
<div className="dm-raster">
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Reichweite</span>
|
||||
<Fig value={deOderStrich(fahrzeug.reichweite)} unit="km" size={40} />
|
||||
<ProgressBar percent={Math.max(0, Math.min(100, fahrzeug.tankPct))} />
|
||||
<div className="dm-tanktext">
|
||||
<span>{de(fahrzeug.tankPct)} % im Tank</span>
|
||||
<span>
|
||||
rund {de((einstellungen.tankvolumen * fahrzeug.tankPct) / 100, 1)} von{" "}
|
||||
{de(einstellungen.tankvolumen)} l
|
||||
</span>
|
||||
</div>
|
||||
</Tile>
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Reichweite</span>
|
||||
<Fig value={deOderStrich(fahrzeug.reichweite)} unit="km" size={40} />
|
||||
<ProgressBar percent={Math.max(0, Math.min(100, fahrzeug.tankPct))} />
|
||||
<div className="dm-tanktext">
|
||||
<span>{de(fahrzeug.tankPct)} % im Tank</span>
|
||||
<span>
|
||||
rund {de((einstellungen.tankvolumen * fahrzeug.tankPct) / 100, 1)} von{" "}
|
||||
{de(einstellungen.tankvolumen)} l
|
||||
</span>
|
||||
</div>
|
||||
</Tile>
|
||||
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Kilometerstand</span>
|
||||
<Fig
|
||||
value={fahrzeug.odoBekannt ? de(fahrzeug.odo) : "—"}
|
||||
unit="km"
|
||||
size={40}
|
||||
/>
|
||||
</Tile>
|
||||
</div>
|
||||
{/* Kilometerstand und Service standen bis 2026-08-17 als zwei gleich
|
||||
große Kacheln nebeneinander — gleiche Zahlengröße, gleiche Farbe,
|
||||
obwohl die beiden Zahlen Grundverschiedenes meinen: ein gemessener
|
||||
Ist-Wert gegen einen täglich schrumpfenden Countdown.
|
||||
|
||||
{/* Service stand bis 2026-08-17 als Werteliste INNERHALB der
|
||||
Kilometerstand-Kachel - zwei Termine unter einer Überschrift, die
|
||||
nichts mit ihnen zu tun hat. Apple HIG "Layout" verlangt, verwandte
|
||||
Dinge zu gruppieren und Unverwandtes zu trennen; außerdem führte von
|
||||
hier kein Weg zur Service-Seite, obwohl es sie gibt. Jetzt eigene,
|
||||
antippbare Kachel - dieselbe Auflösung wie im Panel (vHome). */}
|
||||
<Tile variant="button" chevron onClick={() => geheZu("service")}>
|
||||
<span className="ads-eyebrow">Service</span>
|
||||
<Werteliste
|
||||
kinder={
|
||||
Jetzt eine führende Kennzahl, darunter der Kilometerstand als ruhige
|
||||
Wertezeile — Audi-CI (Hierarchie über Größe und Farbe) plus Apple HIGs
|
||||
"differentiate controls from content": nur der obere Teil ist der
|
||||
Knopf zur Service-Seite, die Kilometerstand-Zeile bleibt reine
|
||||
Anzeige. Deckungsgleich mit vHome() im Panel. */}
|
||||
<Tile>
|
||||
<button
|
||||
type="button"
|
||||
className="dm-serviceblock"
|
||||
onClick={() => geheZu("service")}
|
||||
>
|
||||
<span className="ads-eyebrow">Nächster Service:</span>
|
||||
<span className="dm-serviceblock__chevron" aria-hidden="true">
|
||||
›
|
||||
</span>
|
||||
{service ? (
|
||||
<>
|
||||
<Wertzeile
|
||||
label="Nächster Ölwechsel"
|
||||
wert={fahrzeug.oelwechselFaelligTs ? datum(fahrzeug.oelwechselFaelligTs) : "—"}
|
||||
zusatz={
|
||||
fahrzeug.oelwechselFaelligKm != null
|
||||
? `in ${de(Math.abs(fahrzeug.oelwechselFaelligKm))} km`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<Wertzeile
|
||||
label="Nächste Inspektion"
|
||||
wert={fahrzeug.inspektionFaelligTs ? datum(fahrzeug.inspektionFaelligTs) : "—"}
|
||||
zusatz={
|
||||
fahrzeug.inspektionFaelligKm != null
|
||||
? `in ${de(Math.abs(fahrzeug.inspektionFaelligKm))} km`
|
||||
: undefined
|
||||
}
|
||||
<Fig
|
||||
value={service.restKm !== null ? de(service.restKm) : datum(service.datum)}
|
||||
{...(service.restKm !== null ? { unit: "km" } : {})}
|
||||
size={service.restKm !== null ? 40 : 30}
|
||||
/>
|
||||
<span className="dm-fussnote">
|
||||
{service.restKm !== null
|
||||
? `${bisText(service.art)} · vsl. ${datum(service.datum)}`
|
||||
: bisText(service.art)}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="dm-fussnote">kein Eintrag im Servicebuch</span>
|
||||
)}
|
||||
</button>
|
||||
<div className="dm-servicezeile">
|
||||
<span className="dm-servicezeile__k">Kilometerstand</span>
|
||||
<Fig value={fahrzeug.odoBekannt ? de(fahrzeug.odo) : "—"} unit="km" size={20} />
|
||||
</div>
|
||||
</Tile>
|
||||
|
||||
<div className="dm-raster">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
|
||||
import { beforeEach, describe, expect, it } from "vitest"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
import { DatenAnbieter } from "../daten/DatenKontext"
|
||||
import { Shell } from "../Shell"
|
||||
@@ -17,19 +17,19 @@ import { bildschirmbreiteSetzen } from "../tests/aufbau"
|
||||
|
||||
const ALLE_SEITEN = Object.keys(TITEL) as SeitenName[]
|
||||
|
||||
function zeige(name: SeitenName, id?: string) {
|
||||
function zeige(name: SeitenName, id?: string, geheZu: (name: SeitenName, id?: string) => void = () => {}) {
|
||||
const api = beispielApi()
|
||||
return render(
|
||||
<div className="ads-root" data-theme="nacht">
|
||||
<DatenAnbieter api={api}>
|
||||
<Shell
|
||||
route={id === undefined ? { name } : { name, id }}
|
||||
geheZu={() => {}}
|
||||
geheZu={geheZu}
|
||||
tabBeschriftung
|
||||
>
|
||||
<SeiteFuer
|
||||
route={id === undefined ? { name } : { name, id }}
|
||||
geheZu={() => {}}
|
||||
geheZu={geheZu}
|
||||
tabBeschriftung
|
||||
setzeTabBeschriftung={() => {}}
|
||||
beiAbmeldung={() => {}}
|
||||
@@ -82,6 +82,23 @@ describe("Inhalte kommen wirklich aus den Daten", () => {
|
||||
expect(screen.getByText("385")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("Übersicht: der Serviceblock navigiert, die Kilometerstand-Zeile nicht", async () => {
|
||||
// Reproduziert die eigentliche HIG-Vorgabe hinter dem Layout
|
||||
// ("differentiate controls from content"), nicht nur das Aussehen:
|
||||
// nur der obere Teil der Kachel ist ein Knopf.
|
||||
const geheZu = vi.fn()
|
||||
const { container } = zeige("home", undefined, geheZu)
|
||||
await waitFor(() => expect(screen.getByText("Nächster Service:")).toBeTruthy())
|
||||
|
||||
fireEvent.click(screen.getByText("Nächster Service:"))
|
||||
expect(geheZu).toHaveBeenCalledWith("service")
|
||||
|
||||
geheZu.mockClear()
|
||||
const zeile = container.querySelector(".dm-servicezeile")
|
||||
if (zeile) fireEvent.click(zeile)
|
||||
expect(geheZu).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("zeigt einen unbekannten Prüfpunkt als unbekannt, nicht als sicher", async () => {
|
||||
const { container } = zeige("sicherheit")
|
||||
await waitFor(() => expect(container.querySelector(".dm-pruefliste")).not.toBeNull())
|
||||
|
||||
Reference in New Issue
Block a user