Service-Termine: CI-Icons statt Wortlaut, Anstehende-Termine neu gegliedert

Naechster-Service-Kachel (Uebersicht): das "bis zum Oelwechsel"/
"bis zur Inspektion" vor der Kennzahl entfaellt zugunsten der echten
Audi-CI-Icons oil-change/inspection (vom Nutzer als SVG geliefert,
verbatim uebernommen wie die uebrigen CI-Icons). Hauptuntersuchung hat
kein eigenes Icon (kein Restkilometer-Ziel) und zeigt stattdessen ihr
Datum in derselben 40px-Groesse wie die km-Zahlen, mit dem Wort
"Hauptuntersuchung" darunter statt "bis zur Hauptuntersuchung" - vorher
war das Datumsfeld kleiner (30px), um Umbruch zu vermeiden; am
laufenden Panel nachgemessen, dass 40px keinen Umbruch verursacht.
"vsl." wird ausgeschrieben zu "voraussichtlich am". bisText()/ART_BIS
(Panel) und bisText()/BIS_TEXT (companion-app) wurden durch die
Aenderung ueberfluessig und als Orphans entfernt.

Anstehende-Termine-Box (Service-Seite): aus der dt/dd-Liste werden drei
sichtbar getrennte Bloecke (Kopfzeile mit Icon + Name + optionalem
Werkstatt-Chip, optionale Herstellervorgabe-Zeile, grosse Kennzahl,
optionale Prognose-Fusszeile) - Apple HIG (Hierarchie ueber
Position/Groesse, Inhalt nicht mit Nebensaechlichem ueberladen) plus
Audi-CI (Flaeche mit Haarlinien statt Karten). Durchlief drei
Mockup-Runden als Artefakt vor der Umsetzung, zwei davon vom Nutzer
zurueckgewiesen ("Major Information is missing" bzw. Kommentare zu
Icon-Groesse/Herstellervorgabe/Ausrichtung).

Herstellervorgabe-Logik korrigiert: eine Fahrzeugmeldung stammt immer
aus dem werkseigenen Wartungsprogramm des Bordcomputers, unabhaengig
vom in "Einrichten" gewaehlten App-Intervall - zaehlt also immer als
Herstellervorgabe, nicht nur wenn der App-Modus zufaellig "hersteller"
ist. Die Prognose-Fusszeile zeigt die Restkilometerzahl nur noch beim
eigenen (kuerzeren) Intervall, bei Herstellervorgabe nur noch das
Datum. "Eigenes" in der Ölwechsel-Intervall-Auswahl umbenannt zu
"Individuell".

companion-app-Portierung ist eine echte Neustrukturierung: die
bisherige "Naechster Oelwechsel"-Kachel mischte Oelwechsel-,
Inspektions- und Hauptuntersuchungs-Zeilen in einer gemeinsamen
Werteliste, jetzt "Anstehende Termine" mit denselben drei Bloecken wie
im Panel. Neuer Export letzterInspektion() in service.ts. Zwei
dokumentierte, vorbestehende Luecken bleiben bestehen statt neu
kaschiert zu werden: kein Werkstatt-Chip (companion-app hat keine
"Termin vereinbaren"-Funktion), Herstellervorgabe kommt aus
fahrzeug.oel.modus statt einem intervalle()-Aequivalent (das es dort
nicht gibt).

npm run typecheck sauber, npm run test 99/99, npm run build erfolgreich.
Panel deployed als Version 1787013003, synchron in installationspaket/,
live im Docker-Testcontainer per DOM-Abfrage geprueft.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 00:03:16 +02:00
parent 2848b5286f
commit 23b6defa68
13 changed files with 600 additions and 179 deletions
+134 -2
View File
@@ -1,6 +1,6 @@
# AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-08-17** (merged the `umsetzung-datametric360` branch — companion app phases
**Last updated: 2026-08-18** (merged the `umsetzung-datametric360` branch — companion app phases
110 done, see `UMSETZUNGSPLAN.md`; 2026-08-13: cleaned up remaining EU Data Act residue, fixed
oversized toggle switches, and fixed a desktop-layout audit (settings button / rings logo / popups
overflowing past the capped content column) — see `DESIGN_AUDIT_2026-08-13.md`; 2026-08-16: fixed
@@ -47,7 +47,12 @@ where the image placeholder's grey background had turned invisible-white; separa
panel) was brought back in sync with everything the panel gained since 2026-08-11 - see the
"Gap found while auditing..." entry near the end of section C; finally, the Übersicht's
Kilometerstand/Service tiles were redesigned after the owner asked how Apple HIG and Audi CI would
solve them - both answer the same way, and the merged "9,0 tkm/ 04/27" number field is gone).
solve them - both answer the same way, and the merged "9,0 tkm/ 04/27" number field is gone;
2026-08-18: the owner rejected a follow-up mockup for the "Mein Audi" service preview and
"Anstehende Termine" boxes ("Lost information") - no code from that proposal was built; instead the
existing Nächster-Service box on the Übersicht had its "bis zum/zur ..." wording replaced by the
real Audi CI oil-change/inspection icons (user-supplied SVGs) placed left of the figure, in both
codebases, per the owner's explicit request).
This file is the entry point for every new agent
session: what this repo is, what is finished, what is missing, and how to work here. Detail lives in
the linked documents — this file points, it does not duplicate.
@@ -1476,6 +1481,133 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed
{opacity:.55}` als Tipp-Rückmeldung ergänzt, da der übliche `.tilebtn`-Hintergrundwechsel
hier nicht geht), synchron in `installationspaket/`. Entwurf und Begründung als Artefakt:
„Zwei Kacheln, drei Antworten“.
- [x] Nächster-Service-Box: CI-Icon statt "bis zum/zur ..."-Wortlaut (2026-08-18) — der Nutzer
lehnte zunächst einen Folge-Entwurf ab, der dieselbe HIG/CI-Systematik auf die "Mein Audi"-
Service-Vorschau und "Anstehende Termine" auf der Service-Seite anwenden sollte ("I don't like
it" → per Rückfrage: "Lost information", also zu viel weggelassen) — dieser Vorschlag wurde
**nicht** umgesetzt, keine Codeänderung daraus übernommen. Stattdessen ein präziser, neuer
Wunsch für die bereits bestehende Nächster-Service-Box auf der Übersicht: das Wort
("bis zum Ölwechsel"/"bis zur Inspektion") vor der Kennzahl entfällt zugunsten der echten Audi-
CI-Icons `inspection-l/-s`/`oil-change-l/-s` (vom Nutzer als SVG geliefert, aus
`C:\Users\tobia\Downloads\Icons 2026-08-09 13_04_03\`, verbatim übernommen wie die übrigen
CI-Icons dieses Projekts — l im 48er-, s im 24er-Grid), links der Zahl platziert; auf
Nutzerwunsch anschließend nochmal um 30% vergrößert (24px → 31px). Umgesetzt in beiden
Codebasen: Panel bekam `CI.oilChangeL/-S`/`CI.inspectionL/-S` (neben den bestehenden CI-
Icon-Konstanten) und ein neues `.fig-zeile`-Wrapper-Div (`display:flex;align-items:center`)
um Icon + `.fig` in `vHome()`'s Serviceblock; die Nebenzeile zeigt jetzt nur noch
`vsl. DATUM`. Companion-app bekam die identischen Pfade als neue `SymbolOelwechsel`/
`SymbolInspektion`-Komponenten in `symbole.tsx` (eigener `RahmenFlaeche`-Wrapper, da diese
zwei — anders als die generischen Tab-Icons dort — gefüllte Flächen-Icons sind, kein
stroke-Wrapper) und eine neue `.dm-servicefig`-CSS-Klasse in `screens.css`; `Uebersicht.tsx`
wählt das Symbol über eine kleine `SERVICE_SYMBOL`-Tabelle nach `service.art`. **Wichtig:** die
Icons blieben bewusst außerhalb von `design-system/` (Lizenzregel oben — echte Audi-CI-Assets
dürfen nur in Panel/companion-app, nicht in das markenfreie `@audi-dash/ui`) — die Verdrahtung
liegt deshalb direkt im jeweiligen Screen, nicht in der gemeinsamen `Fig`-Komponente. Die
Hauptuntersuchung hat kein eigenes Icon (kein Restkilometer-Ziel, `restKm` ist dort immer
`null`) und behält `bisText()` als einzige Kennzeichnung — das deckt sich in beiden Codebasen
genau mit der Bedingung, unter der ein Icon überhaupt existiert. `npm run typecheck`/
`npm run test` (100/100)/`npm run build` sauber. Live im Docker-Testcontainer geprüft (Version
`1787012001`, synchron in `installationspaket/`): Ölwechsel-Icon rendert links der "8.999 km"-
Kennzahl, Unterzeile zeigt nur noch "vsl. 18.04.2027", keine neuen Konsolenfehler.
- [x] Nächster-Service-Box: Hauptuntersuchung-Datum in Zahlengröße + Art-Name statt "bis zur ..."
(2026-08-18) — Folgewunsch zum Icon-Umbau: das Datum im Zahlenfeld war für die
Hauptuntersuchung (kein Restkilometer-Ziel) bisher kleiner gesetzt (30px statt 40px), extra
damit lange Daten wie "15.04.2027" auf 375px nicht umbrechen; der Nutzer wollte stattdessen
dieselbe 40px-Größe wie bei Ölwechsel/Inspektion, plus darunter statt "bis zur
Hauptuntersuchung" nur noch das Wort "Hauptuntersuchung" selbst. Vor der Umsetzung am
laufenden Panel nachgemessen (nicht angenommen): "15.04.2027" misst bei 40px/Audi Type Wide
182px, der Kachel-Container hat 322px — die ursprüngliche Wrap-Sorge, die zur 30px-Regel
geführt hatte, bestätigt sich bei den tatsächlichen Container-Maßen nicht. `zahlGroesse` ist
jetzt fest 40 (Panel `vHome()` und companion-app `Uebersicht.tsx`, `Fig size={40}`
unabhängig von `restKm`). Da `bisText()`/`ART_BIS` (Panel) und `bisText()`/`BIS_TEXT`
(companion-app `service.ts`) dadurch ihre einzige Aufrufstelle verloren, wurden beide als
Orphans entfernt (inkl. des zugehörigen companion-app-Tests in `service.test.ts` — von 100 auf
99 Tests, kein Funktionsverlust, nur der jetzt toten Funktion beraubt). Live im
Docker-Testcontainer per DOM-Patch simuliert (Testfixture hat aktuell Ölwechsel als
nächstgelegenen Termin, nicht die Hauptuntersuchung) und bei 390px Breite screenshotet:
"06.08.2028" einzeilig bei 40px, "Hauptuntersuchung" darunter, kein Icon (wie vorgesehen — es
gibt kein CI-Icon für die Hauptuntersuchung). `npm run typecheck`/`npm run test`
(99/99)/`npm run build` in companion-app sauber. Deployed als Panel-Version `1787012002`,
synchron in `installationspaket/`.
- [x] "Anstehende Termine"-Box (Service-Seite) umgebaut: drei sichtbar getrennte Blöcke
(2026-08-18) — auf Wunsch des Nutzers ein neuer Entwurf (Apple HIG + Audi CI, wie beim
Übersicht-Umbau), mit expliziten Vorgaben: Herstellervorgabe bleibt für Ölwechsel und
Inspektion möglich, "voraussichtlich ..." bleibt für beide erhalten, alle drei Arten werden
sichtbar getrennt, die Hauptuntersuchung bekommt bewusst weder Herstellervorgabe noch
Prognosezeile. Durchlief drei Mockup-Runden als Artefakt ("Drei Termine, drei Blöcke") vor der
Umsetzung: **Runde 1** abgelehnt ("Major Information is missing"). **Runde 2** ergänzte ein
vollständiges Feld-Inventar (Tabelle: jedes Element aus `vService()` mit Fundstelle im
Vorschlag), integrierte den vereinbarten Werkstatttermin in den Hauptvergleich statt als
Anhang, zeigte den "kein Eintrag im Servicebuch"-Zustand. **Runde 3** (auf drei
Inline-Kommentare des Nutzers im Artefakt): Ölwechsel-Icon von 20px auf 26px angehoben (füllt
in seinem 24×24-Raster nur eine 24×10-Fläche statt der fast vollen Höhe des
Inspektion-Icons — per `getBBox()` nachgemessen, kein exakter Ausgleich möglich ohne das Icon
unverhältnismäßig breit zu machen), Herstellervorgabe-Zeile auch am Ölwechsel-Beispiel gezeigt
(vorher nur bei Inspektion), der Hauptuntersuchungs-Block von einem Icon-Platzhalter-Abstand
auf echt linksbündig geändert. Danach vom Nutzer freigegeben ("Ok, implement it").
**Umsetzung, beide Codebasen:**
Panel (`vService()`, `audi-dashboard-app.js`): `<dl class="rows">` durch drei
`<div class="termingruppe">` ersetzt (Kopfzeile mit Icon + Art-Name + optionalem
`.tg-chip` für einen vereinbarten Werkstatttermin, optionale `.tg-vorgabe`-Zeile, `.tg-wert`
als 22px/Gewicht-300-Zahl, optionale `.tg-prognose`-Fußzeile) — Auswahllogik für Wert/
Fußzeile/Herstellervorgabe unverändert aus dem bisherigen Code übernommen, nur die Markup-
Struktur geändert. `ART_ICON`/`ART_ICON_GROESSE` (26px Ölwechsel, 20px Inspektion) von
`vHome()` auf Modulebene gehoben, da jetzt von zwei Stellen genutzt — `vHome()`s eigene
Icon-Größe (31px, beide gleich) bewusst unverändert gelassen, das ist eine offene, dem Nutzer
gestellte Rückfrage. Der Werkstatt-Chip nutzt `var(--red)` nur als Text-/Rahmenfarbe, keine
Füllung — deckt sich mit der bereits bestehenden Projektregel "Rot ist Akzent, nie Fläche"
(siehe iOS-Overlay-Fix vom 2026-08-13). Deployed als Version `1787013000`, synchron in
`installationspaket/`, live im Docker-Testcontainer per direkter DOM-Abfrage geprüft (JS statt
Screenshot, da die Browser-Pane in dieser Sitzung zeitweise nicht kompositierte): alle drei
Blöcke rendern mit korrektem Text, Icons in 26px/20px, Hauptuntersuchung ohne Icon, keine
neuen Konsolenfehler.
companion-app (`Service.tsx`): dieselbe Drei-Blöcke-Struktur, aber eine **echte
Neustrukturierung, kein 1:1-Kopieren** — die bisherige Kachel hieß "Nächster Ölwechsel" und
mischte Ölwechsel-, Inspektions- und Hauptuntersuchungs-Zeilen fachfremd in einer gemeinsamen
`Werteliste`; jetzt "Anstehende Termine" wie im Panel, mit denselben drei `.dm-termingruppe`-
Blöcken. Neue `SymbolOelwechsel`/`SymbolInspektion`-Komponenten in `symbole.tsx` (bereits für
die Übersicht angelegt, hier wiederverwendet). Neuer Export `letzterInspektion()` in
`service.ts` (Gegenstück zu `letzterOelwechsel()`) für den "kein Eintrag"-Zustand der
Inspektion. **Zwei bewusste, dokumentierte Vereinfachungen gegenüber dem Panel** (vorbestehende
Lücken, nicht durch diese Änderung verursacht): (1) companion-app hat keine "Termin
vereinbaren"-Funktion und keine `vereinbart`-Datenfelder für Service-Termine — der Werkstatt-
Chip entfällt deshalb ersatzlos, es gibt nichts anzuzeigen. (2) companion-app hat kein
Gegenstück zu `intervalle()`/dem generischen Herstellerintervall-Fallback des Panels; die
Herstellervorgabe-Zeile wird stattdessen direkt aus `fahrzeug.oel.modus` abgeleitet (Ölwechsel:
nur bei `modus === "hersteller"`, Inspektion: immer, da sie kein einstellbares Intervall hat —
deckt sich inhaltlich mit dem Panel-Verhalten, auch ohne dieselbe Funktion zu teilen). Beide
Punkte sind funktional gleichwertig zum bisherigen companion-app-Stand, nicht neu eingeführte
Lücken. `npm run typecheck` sauber, `npm run test` 99/99, `npm run build` erfolgreich; die
bestehenden "Alle Seiten rendern mit Beispieldaten"-Tests rendern die Service-Seite weiterhin
ohne Absturz. Kein Live-Screenshot in dieser Sitzung (bräuchte einen laufenden Dev-Server samt
Backend-Anbindung) — strukturell verifiziert über Typecheck/Tests/Build.
**Zwei Nachbesserungen direkt nach der ersten Live-Prüfung, beide vom Nutzer gemeldet:**
(1) "Herstellervorgabe" fehlte beim Ölwechsel, obwohl der angezeigte Wert (29.100 km /
25.07.2028) vom Fahrzeug selbst gemeldet wird (`hatMeldung`/`fahrzeug.oelwechselFaelligTs`).
Root Cause: die Zeile war bisher ausschließlich an `iv.hersteller` (Panel) bzw.
`fahrzeug.oel.modus === "hersteller"` (companion-app) gekoppelt — dem *App-eigenen*
Intervall-Modus aus "Einrichten". Der Bordcomputer kennt diesen App-Modus aber gar nicht;
eine Fahrzeugmeldung stammt immer aus dem werkseigenen Wartungsprogramm und ist damit per
Definition Herstellervorgabe, unabhängig vom gewählten App-Modus. Das Testprofil hat den
Ölwechsel aktuell auf `modus: "eigen"` stehen (10.000 km/12 Monate statt 30.000 km/24
Monate) — das war korrektes Verhalten der alten Bedingung, nur die Bedingung selbst war zu
eng. Fix: `herstellervorgabe = hatMeldung || iv.hersteller` (Panel) bzw.
`oelHatMeldung || fahrzeug.oel.modus === "hersteller"` (companion-app) — dieselbe alte
Bedingung bleibt für den Fall ohne Fahrzeugmeldung erhalten (App-eigener Fallback auf das
Herstellerintervall), kommt jetzt aber zusätzlich bei einer echten Fahrzeugmeldung zum
Tragen. (2) Die Prognose-Fußzeile zeigte "8.999 km, voraussichtlich am 18.04.2027" auch dann,
wenn in "Einrichten" tatsächlich Herstellervorgabe gewählt ist — dort rechnet die eigene
Prognose ohnehin schon mit dem Herstellerintervall, die Restkilometerzahl wäre also nur eine
zweite, verwirrende km-Angabe neben dem Fahrzeug-Wert direkt darüber. Fix: die "X km,"-
Vorsilbe entfällt, sobald `CAR.oel.modus`/`fahrzeug.oel.modus === "hersteller"` ist — nur beim
eigenen (kürzeren) Intervall bleibt sie stehen, weil sie dort echte Zusatzinformation ist
(matcht damit exakt, wie die Inspektion-Fußzeile schon immer aussah, da sie kein einstellbares
Intervall hat). Mit dem Testprofil (`modus: "eigen"`) bleibt die Kilometerzahl also weiterhin
sichtbar — korrekt, das ist genau der Fall, für den sie gedacht ist; umschalten auf
Herstellervorgabe in "Einrichten" würde sie ausblenden. Beide Fixes deployed als Panel-Version
`1787013001`, synchron in `installationspaket/`, live per DOM-Abfrage geprüft: Ölwechsel-Block
zeigt jetzt "Herstellervorgabe", keine neuen Konsolenfehler. companion-app: `npm run typecheck`
sauber, `npm run test` 99/99, `npm run build` erfolgreich.
- [x] **Parity rule eingeführt (2026-08-17):** der Nutzer stellte klar, dass Panel und
`companion-app/` immer denselben funktionalen Stand haben sollen — auch wenn eine Anfrage nur
das Panel nennt, das ist reiner Testkomfort (Docker-Instanz), keine Scope-Entscheidung. Regel
+1 -7
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest"
import { bisText, inspektionPrognose, letzterOelwechsel, naechsterService, oelwechselPrognose } from "./service"
import { inspektionPrognose, letzterOelwechsel, naechsterService, oelwechselPrognose } from "./service"
import type { Fahrzeug } from "./profilAdapter"
function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug {
@@ -142,10 +142,4 @@ describe("naechsterService", () => {
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")
})
})
+5 -13
View File
@@ -51,6 +51,11 @@ export function letzterOelwechsel(buch: readonly Buchhaltung[]): Buchhaltung | n
return letzterEintrag(buch, /öl|oel/i)
}
/** Der jüngste Servicebucheintrag, dessen Art auf eine Inspektion hindeutet. */
export function letzterInspektion(buch: readonly Buchhaltung[]): Buchhaltung | null {
return letzterEintrag(buch, /inspektion/i)
}
/** Gemeinsamer Kern für Ölwechsel- und Inspektionsprognose - beide rechnen
identisch, nur mit anderem Intervall und anderer Suche im Servicebuch. */
function servicePrognose(
@@ -134,19 +139,6 @@ export interface NaechsterService {
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.
+100 -46
View File
@@ -13,9 +13,10 @@ import { useState } from "react"
import { ActionButton, Feld, Tile } from "@audi-dash/ui"
import { useDaten } from "../daten/DatenKontext"
import { inspektionPrognose, oelwechselPrognose } from "../daten/service"
import { inspektionPrognose, letzterInspektion, letzterOelwechsel, oelwechselPrognose } from "../daten/service"
import { datum, de, isoTag } from "../format"
import type { SeitenName } from "../navigation"
import { SymbolInspektion, SymbolOelwechsel } from "../symbole"
import { Leerzustand, NaviKachel, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
import { kalenderDateiLaden } from "./kalender"
@@ -44,53 +45,106 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
const prognose = oelwechselPrognose(fahrzeug, buch)
const inspektionsprognose = inspektionPrognose(fahrzeug, buch)
// Ölwechsel und Inspektion je einzeln, statt einer gemeinsamen Wertzeilen-
// Liste unter "Nächster Ölwechsel" (die Inspektion/Hauptuntersuchung dort
// fachfremd mitschleppte) - deckungsgleich mit vService() im Panel: drei
// sichtbar getrennte Blöcke, jeder mit eigenem Icon, optionaler
// Herstellervorgabe-Zeile und optionaler Prognose-Fußzeile.
const oelHatEintrag = letzterOelwechsel(buch) != null
const oelHatMeldung = fahrzeug.oelwechselFaelligTs != null || fahrzeug.oelwechselFaelligKm != null
const oelWert = oelHatMeldung
? [
fahrzeug.oelwechselFaelligKm != null ? `${de(Math.abs(fahrzeug.oelwechselFaelligKm))} km` : null,
fahrzeug.oelwechselFaelligTs ? datum(fahrzeug.oelwechselFaelligTs) : null,
]
.filter(Boolean)
.join(" / ")
: prognose
? `${de(prognose.restKm)} km · ${datum(prognose.datum)}`
: null
// Bei Herstellervorgabe rechnet die eigene Prognose ohnehin schon mit dem
// Herstellerintervall - die Restkilometerzahl daneben wäre nur eine zweite,
// verwirrende km-Angabe neben dem Fahrzeug-Wert oben. Nur beim eigenen
// (kürzeren) Intervall ist sie eine echte Zusatzinformation.
const oelFussnote = oelHatMeldung && prognose
? fahrzeug.oel.modus === "hersteller"
? `voraussichtlich am ${datum(prognose.datum)}`
: `${de(prognose.restKm)} km, voraussichtlich am ${datum(prognose.datum)}`
: null
// Der Ölwechsel folgt in "Einrichten" wahlweise der Herstellervorgabe oder
// einem kürzeren eigenen Intervall (fahrzeug.oel.modus) - die Inspektion hat
// kein einstellbares Intervall, rechnet also immer mit der Herstellervorgabe
// (siehe inspektionPrognose()'s eigener Kommentar dazu). Eine Fahrzeugmeldung
// stammt aber immer aus dem werkseigenen Wartungsprogramm des Bordcomputers -
// der kennt das eigene App-Intervall gar nicht -, zählt also unabhängig vom
// gewählten Modus als Herstellervorgabe.
const oelHerstellervorgabe = oelHatMeldung || fahrzeug.oel.modus === "hersteller"
const inspHatEintrag = letzterInspektion(buch) != null
const inspHatMeldung = fahrzeug.inspektionFaelligTs != null || fahrzeug.inspektionFaelligKm != null
const inspWert = inspHatMeldung
? [
fahrzeug.inspektionFaelligKm != null ? `${de(Math.abs(fahrzeug.inspektionFaelligKm))} km` : null,
fahrzeug.inspektionFaelligTs ? datum(fahrzeug.inspektionFaelligTs) : null,
]
.filter(Boolean)
.join(" / ")
: inspektionsprognose
? datum(inspektionsprognose.datum)
: null
const inspFussnote = inspektionsprognose ? `voraussichtlich am ${datum(inspektionsprognose.datum)}` : null
const huDatum = fahrzeug.hu ? new Date(fahrzeug.hu) : null
const huGueltig = !!huDatum && !Number.isNaN(huDatum.getTime())
return (
<>
<Tile>
<span className="ads-eyebrow">Nächster Ölwechsel</span>
<Werteliste
kinder={
<span className="ads-eyebrow">Anstehende Termine</span>
<div className="dm-termingruppe">
<div className="dm-tgkopf">
<SymbolOelwechsel groesse={26} />
<span>Ölwechsel</span>
</div>
{oelHatEintrag ? (
<>
<Wertzeile
label="Vom Fahrzeug gemeldet"
wert={fahrzeug.oelwechselFaelligTs ? datum(fahrzeug.oelwechselFaelligTs) : "—"}
zusatz={
fahrzeug.oelwechselFaelligKm != null
? `in ${de(Math.abs(fahrzeug.oelwechselFaelligKm))} km`
: undefined
}
/>
<Wertzeile
label="Eigene Prognose"
wert={prognose ? datum(prognose.datum) : "—"}
zusatz={
prognose
? `${de(prognose.restKm)} km offen · ${de(prognose.kmProTag, 1)} km/Tag`
: "Noch kein Ölwechsel im Servicebuch"
}
/>
<Wertzeile
label="Nächste Inspektion"
wert={fahrzeug.inspektionFaelligTs ? datum(fahrzeug.inspektionFaelligTs) : "—"}
zusatz={
fahrzeug.inspektionFaelligKm != null
? `in ${de(Math.abs(fahrzeug.inspektionFaelligKm))} km`
: undefined
}
/>
{inspektionsprognose && (
<Wertzeile
label="Eigene Prognose (Inspektion)"
wert={datum(inspektionsprognose.datum)}
/>
)}
<Wertzeile
label="Hauptuntersuchung"
wert={fahrzeug.hu ? datum(fahrzeug.hu) : "—"}
/>
{oelHerstellervorgabe && <div className="dm-tgvorgabe">Herstellervorgabe</div>}
<div className="dm-tgwert">{oelWert}</div>
{oelFussnote && <div className="dm-tgprognose">{oelFussnote}</div>}
</>
}
/>
) : (
<div className="dm-tgwert dm-tgwert--leer">kein Eintrag im Servicebuch</div>
)}
</div>
<div className="dm-termingruppe">
<div className="dm-tgkopf">
<SymbolInspektion groesse={20} />
<span>Inspektion</span>
</div>
{inspHatEintrag ? (
<>
<div className="dm-tgvorgabe">Herstellervorgabe</div>
<div className="dm-tgwert">{inspWert}</div>
{inspFussnote && <div className="dm-tgprognose">{inspFussnote}</div>}
</>
) : (
<div className="dm-tgwert dm-tgwert--leer">kein Eintrag im Servicebuch</div>
)}
</div>
<div className="dm-termingruppe">
<div className="dm-tgkopf">
<span>Hauptuntersuchung</span>
</div>
{huGueltig ? (
<div className="dm-tgwert">{datum(fahrzeug.hu)}</div>
) : (
<div className="dm-tgwert dm-tgwert--leer">kein Eintrag im Servicebuch</div>
)}
</div>
{prognose && (
<div style={{ marginTop: 14 }}>
<ActionButton
@@ -103,9 +157,9 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
</div>
)}
<p className="dm-fussnote">
Die eigene Prognose rechnet ab dem letzten Ölwechsel im Servicebuch neu und nutzt die
seither tatsächlich gefahrene Strecke. Sie ersetzt die Meldung des Fahrzeugs nicht,
sondern ergänzt sie.
Die eigene Prognose rechnet ab dem letzten Eintrag der jeweiligen Art im Servicebuch neu
und nutzt die seither tatsächlich gefahrene Strecke. Sie ersetzt die Meldung des
Fahrzeugs nicht, sondern ergänzt sie.
</p>
</Tile>
+30 -9
View File
@@ -11,14 +11,24 @@ 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 { naechsterService } from "../daten/service"
import { datum, datumZeit, dauer, de, deOderStrich, eur } from "../format"
import type { SeitenName } from "../navigation"
import { Leerzustand, NaviKachel } from "./bausteine"
import { Bild } from "./Bild"
import type { ServicebuchEintrag } from "./Service"
import { SymbolInspektion, SymbolOelwechsel } from "../symbole"
import { Typenschild, schildErkennen } from "./Typenschild"
// "bis zum Ölwechsel"/"bis zur Inspektion" entfaellt zugunsten des CI-Icons
// links der Kennzahl - deckungsgleich mit ART_ICON im Panel (vHome()). Die
// Hauptuntersuchung hat kein Restkilometer-Ziel und zeigt stattdessen den
// Art-Namen selbst unter dem (gleich großen) Datum.
const SERVICE_SYMBOL: Partial<Record<string, typeof SymbolOelwechsel>> = {
"Ölwechsel": SymbolOelwechsel,
"Inspektion": SymbolInspektion,
}
export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string) => void }) {
const { fahrzeug, einstellungen, fahrten, tankvorgaenge, jetztAktualisieren, statusStand } =
useDaten()
@@ -143,15 +153,26 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
</span>
{service ? (
<>
<Fig
value={service.restKm !== null ? de(service.restKm) : datum(service.datum)}
{...(service.restKm !== null ? { unit: "km" } : {})}
size={service.restKm !== null ? 40 : 30}
/>
{(() => {
const Symbol = SERVICE_SYMBOL[service.art]
const fig = (
<Fig
value={service.restKm !== null ? de(service.restKm) : datum(service.datum)}
{...(service.restKm !== null ? { unit: "km" } : {})}
size={40}
/>
)
return Symbol ? (
<div className="dm-servicefig">
<Symbol groesse={31} />
{fig}
</div>
) : (
fig
)
})()}
<span className="dm-fussnote">
{service.restKm !== null
? `${bisText(service.art)} · vsl. ${datum(service.datum)}`
: bisText(service.art)}
{service.restKm !== null ? `voraussichtlich am ${datum(service.datum)}` : service.art}
</span>
</>
) : (
+62
View File
@@ -214,6 +214,17 @@
line-height: 1;
color: var(--fg3);
}
/* Serviceart-Icon links der Kennzahl statt des Wortes "bis zum/zur ..." -
deckungsgleich mit .fig-zeile im Panel (audi-dashboard.css). */
.dm-servicefig {
display: flex;
align-items: center;
gap: 8px;
}
.dm-servicefig svg {
color: var(--fg2);
flex: none;
}
.dm-servicezeile {
display: flex;
align-items: baseline;
@@ -229,6 +240,57 @@
color: var(--fg2);
}
/* Service-Seite, "Anstehende Termine": drei sichtbar getrennte Bloecke statt
einer Wertzeilen-Liste - deckungsgleich mit .termingruppe im Panel
(audi-dashboard.css, vService()). Trennung ueber Abstand und eine Haarlinie
zwischen den Bloecken, nicht ueber verschachtelte Kacheln (Audi-CI). */
.dm-termingruppe {
padding: 18px 0;
border-top: 1px solid var(--line);
}
.dm-termingruppe:first-of-type {
padding-top: 2px;
border-top: 0;
}
.dm-tgkopf {
display: flex;
align-items: center;
gap: 9px;
min-width: 0;
}
.dm-tgkopf svg {
color: var(--fg2);
flex: none;
}
.dm-tgkopf span {
font-size: 16.5px;
color: var(--fg);
font-weight: 400;
}
.dm-tgvorgabe {
font-size: 11.5px;
color: var(--fg2);
margin-top: 8px;
}
.dm-tgwert {
font-weight: 300;
font-size: 22px;
color: var(--fg);
letter-spacing: -0.005em;
margin-top: 4px;
}
.dm-tgwert--leer {
font-weight: 400;
font-size: 15px;
color: var(--fg3);
margin-top: 8px;
}
.dm-tgprognose {
font-size: 12px;
color: var(--fg2);
margin-top: 6px;
}
.dm-tanktext {
display: flex;
justify-content: space-between;
+42
View File
@@ -147,6 +147,48 @@ export function SymbolMehr({ groesse }: SymbolProps) {
)
}
/**
* oil-change-s/inspection-s aus dem Audi-CI-Icon-Satz (vom Nutzer als SVG
* geliefert, verbatim übernommen 24er-Grid, wie im Panel als CI.oilChangeS/
* CI.inspectionS). Anders als die generischen Rahmen()-Symbole oben sind das
* gefüllte Flächen-Icons, deshalb kein stroke-Wrapper.
*/
function RahmenFlaeche({ groesse = 24, kinder }: SymbolProps & { kinder: ReactElement }) {
return (
<svg width={groesse} height={groesse} viewBox="0 0 24 24" fill="none" aria-hidden="true">
{kinder}
</svg>
)
}
export function SymbolOelwechsel({ groesse }: SymbolProps) {
return (
<RahmenFlaeche
groesse={groesse}
kinder={
<path
fill="currentColor"
d="M9 8H7.5V7h4v1H10v2h2.77l.94 1.41 7.93-2.48 2.21 2.22-.7.7-1.79-1.78-1.03.32L15.25 17H4v-6.8L2.8 9H0V8h3.2l2 2H9zm-4 3v5h9.75l3.92-5.1-5.38 1.69L12.23 11z"
/>
}
/>
)
}
export function SymbolInspektion({ groesse }: SymbolProps) {
return (
<RahmenFlaeche
groesse={groesse}
kinder={
<path
fill="currentColor"
d="M3.3 0H5v3.3l.7.7h2.6l.7-.7V0h1.7L13 2.3v4.4l-3 3V21c0 .69-.3 1.42-.76 1.98A2.57 2.57 0 0 1 7.3 24h-.6c-.79 0-1.47-.42-1.93-.98A3.22 3.22 0 0 1 4 21V9.7l-3-3V2.3zm.4 1L2 2.7v3.6l3 3V21c0 .47.2.98.53 1.38.34.4.76.62 1.17.62h.6c.4 0 .82-.24 1.16-.65.34-.42.54-.94.54-1.35V9.3l3-3V2.7L10.3 1H10v2.7L8.7 5H5.3L4 3.7V1zm12.44-1h3.72L21 3.42V6.7l-1 1V14h1v7.5a2.5 2.5 0 0 1-2.5 2.5h-1a2.5 2.5 0 0 1-2.5-2.5V14h1V7.7l-1-1V3.43zM16 15v6.5c0 .82.68 1.5 1.5 1.5h1c.82 0 1.5-.68 1.5-1.5V15zm3-1V7.3l1-1V3.57L19.14 1h-2.28L16 3.58V6.3l1 1V14z"
/>
}
/>
)
}
export function SymbolWarnung({ groesse }: SymbolProps) {
return (
<Rahmen
@@ -971,6 +971,16 @@ const CI = {
verbatim uebernommen) - l im 48er-, s im 24er-Grid wie poi-l/poi-s. */
editL: '<path fill="currentColor" d="M28.99 3.6c.34-.1.73-.11 1.1.04h.02l3.61 1.81c.35.18.58.5.68.84s.11.73-.04 1.1v.01l-2.01 4.06 4.02 2.02-6.72 13.44-.9-.44 6.28-12.56-3.13-1.56-13.6 27.47-6.3 4.66v-7.8L28.15 4.27c.18-.35.5-.58.84-.68Zm2.46 7.42 2-4.02a.65.65 0 0 0 0-.41c-.05-.14-.12-.22-.17-.24l-3.58-1.8a.65.65 0 0 0-.41 0c-.14.05-.22.12-.24.17L27.02 8.8zm-4.88-1.33-13.4 26.89 4.5 2.25L31.01 11.9l-4.44-2.2ZM13 37.61v4.9l3.95-2.93z"/>',
editS: '<path fill="currentColor" d="m14.14 1.06 2.4 1.1.04.02c.23.16.42.39.52.65.1.27.13.6-.06.91l-.91 1.94 2.94 1.38-2.82 5.95-.9-.42 2.38-5.05-2.02-.95L9.5 19.8 5 23.57v-5.88l7.56-16.14.02-.03c.16-.23.39-.42.65-.52a1 1 0 0 1 .9.06Zm1.08 4.2.94-2 .02-.03v-.04a.4.4 0 0 0-.13-.16l-2.4-1.1-.02-.01h-.04a.4.4 0 0 0-.16.13L12.53 4l2.7 1.27Zm-3.12-.37L6.16 17.56l2.7 1.27L14.8 6.16 12.1 4.9ZM6 18.6v2.84l2.18-1.81L6 18.59Z"/>',
/* oil-change-l/-s, inspection-l/-s aus dem Audi-CI-Icon-Satz (vom Nutzer als
SVG geliefert, verbatim uebernommen) - l im 48er-, s im 24er-Grid wie
poi-l/poi-s. Die -s-Variante steht links neben der Kennzahl im
Nächster-Service-Block (vHome()) statt des vorherigen "bis zum/zur..."
Textes - die Serviceart wird jetzt ueber das Icon erkannt, nicht ueber
Worte. */
oilChangeL: '<path fill="currentColor" d="M15 16a.5.5 0 0 0-.5.5c0 .28.22.5.5.5h5a.5.5 0 0 0 .5-.5.5.5 0 0 0-.5-.5zm-1.5.5c0-.83.66-1.5 1.5-1.5h5a1.5 1.5 0 1 1 0 3h-2v3h5.67l3.92 2.94 16.45-5.48 3.72 1.86-2.11 2.81-2.17-1.08-9.65 3.86L27.77 35H9V24.73L5.82 22H1v-3.5h6.16l3.5 2.5H17v-3h-2a1.5 1.5 0 0 1-1.5-1.5M2 19.5V21h4.18L10 24.27V34h17.23l5.94-8.9 10.35-4.15 1.83.92.89-1.2-2.28-1.13-16.55 5.52L23.33 22H10.34l-3.5-2.5zm43.5 4.1.42.62-.42.28-.42-.28zm0 1.84a17 17 0 0 0-.8 1.55c-.14.34-.2.59-.2.73 0 .8.52 1.28 1 1.28s1-.49 1-1.28c0-.14-.06-.39-.2-.73a10 10 0 0 0-.8-1.55m0-.94.42-.28v.01l.01.02.02.03.03.05a18 18 0 0 1 1.25 2.29c.14.36.27.76.27 1.1 0 1.18-.82 2.28-2 2.28s-2-1.1-2-2.28c0-.34.13-.74.27-1.1.15-.38.35-.77.54-1.12a19 19 0 0 1 .76-1.25v-.02h.01z"/>',
oilChangeS: '<path fill="currentColor" d="M9 8H7.5V7h4v1H10v2h2.77l.94 1.41 7.93-2.48 2.21 2.22-.7.7-1.79-1.78-1.03.32L15.25 17H4v-6.8L2.8 9H0V8h3.2l2 2H9zm-4 3v5h9.75l3.92-5.1-5.38 1.69L12.23 11z"/>',
inspectionL: '<path fill="currentColor" d="M11.5 1.8 13 3.3v4l.7.7h4.6l.7-.7v-4l1.5-1.5L25 6.3v6.4l-5 5v24.8c0 1.98-1.52 3.5-3.5 3.5h-1a3.45 3.45 0 0 1-3.5-3.5V17.7l-5-5V6.3zm21.2.2h4.6L40 6.87v4.84l-3 3L36.9 26H40v3.12l-1 2v5.26l1 2v5.12a2.5 2.5 0 0 1-2.5 2.5h-5a2.5 2.5 0 0 1-2.5-2.5v-5.12l1-2v-5.26l-1-2V26h3V14.7l-3-3V6.88zm3.2 23.9.1-11.6 3-3V7.12L36.7 3h-3.4L31 7.13v4.16l3 3V25.9zM8 6.7v5.6l5 5v25.2c0 1.42 1.08 2.5 2.5 2.5h1c1.42 0 2.5-1.08 2.5-2.5V17.3l5-5V6.7l-3.5-3.5-.5.5v4L18.7 9h-5.4L12 7.7v-4l-.5-.5zM31 27v1.88l1 2v5.74l-1 2v4.88c0 .82.68 1.5 1.5 1.5h5c.82 0 1.5-.68 1.5-1.5v-4.88l-1-2v-5.74l1-2V27z"/>',
inspectionS: '<path fill="currentColor" d="M3.3 0H5v3.3l.7.7h2.6l.7-.7V0h1.7L13 2.3v4.4l-3 3V21c0 .69-.3 1.42-.76 1.98A2.57 2.57 0 0 1 7.3 24h-.6c-.79 0-1.47-.42-1.93-.98A3.22 3.22 0 0 1 4 21V9.7l-3-3V2.3zm.4 1L2 2.7v3.6l3 3V21c0 .47.2.98.53 1.38.34.4.76.62 1.17.62h.6c.4 0 .82-.24 1.16-.65.34-.42.54-.94.54-1.35V9.3l3-3V2.7L10.3 1H10v2.7L8.7 5H5.3L4 3.7V1zm12.44-1h3.72L21 3.42V6.7l-1 1V14h1v7.5a2.5 2.5 0 0 1-2.5 2.5h-1a2.5 2.5 0 0 1-2.5-2.5V14h1V7.7l-1-1V3.43zM16 15v6.5c0 .82.68 1.5 1.5 1.5h1c.82 0 1.5-.68 1.5-1.5V15zm3-1V7.3l1-1V3.57L19.14 1h-2.28L16 3.58V6.3l1 1V14z"/>',
};
function ciSVG(pfad, px = 16) {
return `<svg viewBox="0 0 24 24" width="${px}" height="${px}" fill="none" aria-hidden="true">${pfad}</svg>`;
@@ -1089,14 +1099,15 @@ function teaser() {
function naechsterTermin() { return termine().slice().sort((a, b) => a.ziel - b.ziel)[0]; }
/* "der Ölwechsel", aber "die Inspektion"/"die Hauptuntersuchung" - ohne diese
Zuordnung stünde auf der Übersicht "bis zum Inspektion". */
const ART_BIS = {
"Ölwechsel": "bis zum Ölwechsel",
"Inspektion": "bis zur Inspektion",
"Hauptuntersuchung": "bis zur Hauptuntersuchung",
};
const bisText = (art) => ART_BIS[art] || `bis zum nächsten Service (${art})`;
// CI-Icon je Serviceart statt eines Wort-Zusatzes ("bis zum Ölwechsel" o.ä.) -
// nur Ölwechsel/Inspektion haben eines, die Hauptuntersuchung nicht. Verwendet
// von vHome() (Nächster-Service-Block) und vService() (Anstehende Termine).
// Das Ölwechsel-Icon ist bei gleicher Boxgröße optisch kleiner als das
// Inspektion-Icon (füllt in seinem eigenen 24x24-Raster nur eine 24x10-Fläche
// statt fast der vollen Höhe) - deshalb eigene, größere Boxgröße statt einer
// gemeinsamen Konstante.
const ART_ICON = { "Ölwechsel": CI.oilChangeS, "Inspektion": CI.inspectionS };
const ART_ICON_GROESSE = { "Ölwechsel": 26, "Inspektion": 20 };
function intervalle(t) {
const oel = CAR.oel, b = t.basis;
@@ -1281,17 +1292,26 @@ function vHome() {
const hauptwert = restKm !== null
? `${de(restKm)}<span class="unit">km</span>`
: (datum ? dedat(datum) : "");
// Ohne Restkilometer steht das Datum selbst im Zahlenfeld - dann kleiner,
// damit "15.04.2027" auf 375px nicht umbricht.
const zahlGroesse = restKm !== null ? 40 : 30;
// Ohne Restkilometer steht das Datum selbst im Zahlenfeld - auf Nutzerwunsch
// jetzt in derselben Groesse wie die km-Zahl (vorher 30px, damit "15.04.2027"
// auf 375px nicht umbricht - am laufenden Panel geprueft, dass es bei 40px
// weiterhin einzeilig bleibt).
const zahlGroesse = 40;
// Icon statt "bis zum Ölwechsel"/"bis zur Inspektion" (nur fuer die beiden
// Arten, die eines haben). Die Hauptuntersuchung hat kein Restkilometer-Ziel
// und zeigt stattdessen den Art-Namen selbst unter dem Datum.
const artIcon = ART_ICON[t.art];
const nebenzeile = restKm !== null && datum
? `${bisText(t.art)} · vsl. ${dedat(datum)}`
: bisText(t.art);
? `voraussichtlich am ${dedat(datum)}`
: t.art;
const figMarkup = artIcon
? `<div class="fig-zeile">${ciSVG(artIcon, 31)}<div class="fig" style="font-size:${zahlGroesse}px">${hauptwert}</div></div>`
: `<div class="fig" style="font-size:${zahlGroesse}px;margin-top:8px">${hauptwert}</div>`;
return `<div class="tile" style="margin-bottom:22px">
<button class="serviceblock" data-go="service:x">
<span class="label">Nächster Service:</span>
<svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
<div class="fig" style="font-size:${zahlGroesse}px;margin-top:8px">${hauptwert}</div>
${figMarkup}
<span class="label" style="margin-top:8px">${nebenzeile}</span>
</button>
${kmZeile}
@@ -1672,42 +1692,52 @@ function vService() {
const sv = CAR.service;
return `
<div class="tile"><span class="label">Anstehende Termine</span>
<dl class="rows" style="margin-top:10px">
${termine().map((t, i, arr) => {
const letzte = i === arr.length - 1 ? 'style="border-bottom:0"' : "";
const iv = intervalle(t);
if (!iv) return `<div class="row" ${letzte}>
<dt>${t.art}</dt><dd style="color:var(--fg3)">kein Eintrag im Servicebuch</dd></div>`;
const anzeige = iv.hersteller || iv.eigen;
const fm = fahrzeugMeldungRoh(t.art);
const fmDatum = fm.ts ? dedat(new Date(fm.ts)) : null;
// Forecast-Zeile aus dem kundeneigenen Intervall in "Einrichten" mit
// eigenständiger Reset-nach-Service-Logik (servicePrognose()): beim
// Ölwechsel mit Restkilometern, bei der Inspektion auf Wunsch nur mit
// dem Datum. Hauptuntersuchung bleibt bewusst ohne.
const eigenOel = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
const eigenInsp = t.art === "Inspektion" ? inspektionPrognose() : null;
const hatMeldung = fm.km != null || fmDatum;
// Die Prognose steht als eigene Zeile ueber die volle Kachelbreite
// unter dt/dd (.row-fussnote), nicht mehr in der rechten Spalte: dort
// standen nur ~217px zur Verfuegung, in die "9.029 km, voraussichtlich
// am 12.04.2027" zwangslaeufig zweizeilig umbrach.
const fussnote = hatMeldung && eigenOel
? `${de(eigenOel.restKm)} km, voraussichtlich am ${dedat(eigenOel.datum)}`
: eigenInsp ? `voraussichtlich am ${dedat(eigenInsp.datum)}` : "";
return `<div class="row" ${letzte}>
<dt>${t.art}
${t.vereinbart ? `<br><span style="color:var(--red);font-size:11px">Werkstatt am ${dedat(new Date(t.vereinbart))}</span>` : ""}
</dt>
<dd>
${iv.hersteller ? `<span class="label" style="margin-bottom:6px">Herstellervorgabe</span>` : ""}
<span class="werte">${hatMeldung
? `${fm.km != null ? de(fm.km) + " km" : ""}${fm.km != null && fmDatum ? " / " : ""}${fmDatum || ""}`
: (eigenOel ? de(eigenOel.restKm) + " km · " + dedat(eigenOel.datum) : (anzeige.km !== null ? de(anzeige.km) + " km · " : "") + dedat(anzeige.datum))}</span>
</dd>
${fussnote ? `<small class="row-fussnote">${fussnote}</small>` : ""}
${termine().map((t) => {
const artIcon = ART_ICON[t.art];
const iconMarkup = artIcon ? ciSVG(artIcon, ART_ICON_GROESSE[t.art]) : "";
const kopf = `<div class="tg-name">${iconMarkup}<span>${t.art}</span></div>`;
const iv = intervalle(t);
if (!iv) return `<div class="termingruppe">
<div class="tg-kopf">${kopf}</div>
<div class="tg-wert leer">kein Eintrag im Servicebuch</div>
</div>`;
const anzeige = iv.hersteller || iv.eigen;
const fm = fahrzeugMeldungRoh(t.art);
const fmDatum = fm.ts ? dedat(new Date(fm.ts)) : null;
// Forecast-Zeile aus dem kundeneigenen Intervall in "Einrichten" mit
// eigenständiger Reset-nach-Service-Logik (servicePrognose()): beim
// Ölwechsel mit Restkilometern, bei der Inspektion auf Wunsch nur mit
// dem Datum. Hauptuntersuchung bleibt bewusst ohne.
const eigenOel = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
const eigenInsp = t.art === "Inspektion" ? inspektionPrognose() : null;
const hatMeldung = fm.km != null || fmDatum;
// Bei Herstellervorgabe rechnet die eigene Prognose ohnehin schon mit dem
// Herstellerintervall - die Restkilometerzahl daneben waere nur eine
// zweite, verwirrende km-Angabe neben dem Fahrzeug-Wert oben. Nur beim
// eigenen (kuerzeren) Intervall ist sie eine echte Zusatzinformation.
const fussnote = hatMeldung && eigenOel
? (CAR.oel.modus === "hersteller"
? `voraussichtlich am ${dedat(eigenOel.datum)}`
: `${de(eigenOel.restKm)} km, voraussichtlich am ${dedat(eigenOel.datum)}`)
: eigenInsp ? `voraussichtlich am ${dedat(eigenInsp.datum)}` : "";
const wert = hatMeldung
? `${fm.km != null ? de(fm.km) + " km" : ""}${fm.km != null && fmDatum ? " / " : ""}${fmDatum || ""}`
: (eigenOel ? de(eigenOel.restKm) + " km · " + dedat(eigenOel.datum) : (anzeige.km !== null ? de(anzeige.km) + " km · " : "") + dedat(anzeige.datum));
// Eine Fahrzeugmeldung stammt immer aus dem werkseigenen Wartungsprogramm
// des Bordcomputers - das kennt das eigene, in "Einrichten" gewaehlte
// Intervall gar nicht. Deshalb zaehlt sie unabhaengig von iv.hersteller
// als Herstellervorgabe; nur der App-eigene Eigen-Intervall-Fallback
// (anzeige.eigen/eigenOel bei modus "eigen") ist es nicht.
const herstellervorgabe = hatMeldung || iv.hersteller;
return `<div class="termingruppe">
<div class="tg-kopf">
${kopf}
${t.vereinbart ? `<span class="tg-chip">Werkstatt am ${dedat(new Date(t.vereinbart))}</span>` : ""}
</div>
${herstellervorgabe ? `<div class="tg-vorgabe">Herstellervorgabe</div>` : ""}
<div class="tg-wert">${wert}</div>
${fussnote ? `<div class="tg-prognose">${fussnote}</div>` : ""}
</div>`; }).join("")}
</dl>
</div>
<div class="tile"><span class="label">Termin vereinbaren</span>
${(() => { const ts = termine(), t = ts[Math.min(svAuswahl, ts.length - 1)];
@@ -2058,7 +2088,7 @@ function vEinst() {
<span class="label" style="margin-top:20px">Ölwechsel-Intervall</span>
<div class="seg" style="margin-top:8px">
<button data-oel="hersteller" class="${CAR.oel.modus === "hersteller" ? "on" : ""}">Herstellervorgabe</button>
<button data-oel="eigen" class="${CAR.oel.modus === "eigen" ? "on" : ""}">Eigenes</button>
<button data-oel="eigen" class="${CAR.oel.modus === "eigen" ? "on" : ""}">Individuell</button>
</div>
${CAR.oel.modus === "hersteller"
? `<dl class="rows" style="margin-top:16px">
@@ -1 +1 @@
{"version": 1787007000}
{"version": 1787013003}
@@ -482,6 +482,9 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
.serviceblock:active { opacity: .55; }
/* Beschriftung nie unter dem Chevron hindurchlaufen lassen */
.serviceblock > .label:first-child { padding-right: 24px; }
/* Serviceart-Icon links der Kennzahl statt des Wortes "bis zum/zur ...". */
.fig-zeile { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.fig-zeile svg { color: var(--fg2); flex: 0 0 auto; }
.servicezeile {
display: flex; align-items: baseline; justify-content: space-between;
gap: var(--sp-4);
@@ -489,6 +492,35 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
margin-top: 14px; padding-top: 13px;
}
/* Anstehende Termine (vService): drei sichtbar getrennte Bloecke statt einer
dt/dd-Liste - Apple HIG ("essential information... don't obscure it by
crowding it with nonessential details") plus Audi-CI (Flaeche mit Haarlinien
statt Karten, eigene Groessenklasse fuer Zahlen). Trennung ueber Abstand und
eine Haarlinie zwischen den Bloecken, nicht ueber verschachtelte Kacheln.
Jeder Block: Kopfzeile (Icon + Art-Name, optional ein Chip fuer einen
vereinbarten Werkstatttermin), optionale Herstellervorgabe-Zeile, der Wert,
optional die eigene Prognose als Fusszeile. */
.termingruppe { padding: 18px 0; border-top: 1px solid var(--line); }
.termingruppe:first-of-type { padding-top: 2px; border-top: 0; }
.tg-kopf { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.tg-name { display: flex; align-items: center; gap: 9px; min-width: 0; }
.tg-name svg { color: var(--fg2); flex: 0 0 auto; }
.tg-name span { font-size: 16.5px; color: var(--fg); font-weight: 400; }
/* Chip statt rotem Fliesstext fuer einen vereinbarten Werkstatttermin - Rot
bleibt Akzent (Text/Rahmen), nie Flaeche, wie im Rest der App. */
.tg-chip {
font-size: 10.5px; color: var(--red);
border: 1px solid var(--red); border-radius: var(--r-pill);
padding: 3px 8px; white-space: nowrap; flex: 0 0 auto;
}
.tg-vorgabe { font-size: 11.5px; color: var(--fg2); margin-top: 8px; }
.tg-wert {
font-family: "Audi Type Wide", var(--font-stack); font-weight: 300;
font-size: 22px; color: var(--fg); letter-spacing: -.005em; margin-top: 4px;
}
.tg-wert.leer { font-family: var(--font-stack); font-weight: 400; font-size: 15px; color: var(--fg3); margin-top: 8px; }
.tg-prognose { font-size: 12px; color: var(--fg2); margin-top: 6px; }
.badge { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.badge img { height: 18px; width: auto; display: none; }
:host([data-theme=nacht]) .badge img.negative { display: block; }
+80 -50
View File
@@ -971,6 +971,16 @@ const CI = {
verbatim uebernommen) - l im 48er-, s im 24er-Grid wie poi-l/poi-s. */
editL: '<path fill="currentColor" d="M28.99 3.6c.34-.1.73-.11 1.1.04h.02l3.61 1.81c.35.18.58.5.68.84s.11.73-.04 1.1v.01l-2.01 4.06 4.02 2.02-6.72 13.44-.9-.44 6.28-12.56-3.13-1.56-13.6 27.47-6.3 4.66v-7.8L28.15 4.27c.18-.35.5-.58.84-.68Zm2.46 7.42 2-4.02a.65.65 0 0 0 0-.41c-.05-.14-.12-.22-.17-.24l-3.58-1.8a.65.65 0 0 0-.41 0c-.14.05-.22.12-.24.17L27.02 8.8zm-4.88-1.33-13.4 26.89 4.5 2.25L31.01 11.9l-4.44-2.2ZM13 37.61v4.9l3.95-2.93z"/>',
editS: '<path fill="currentColor" d="m14.14 1.06 2.4 1.1.04.02c.23.16.42.39.52.65.1.27.13.6-.06.91l-.91 1.94 2.94 1.38-2.82 5.95-.9-.42 2.38-5.05-2.02-.95L9.5 19.8 5 23.57v-5.88l7.56-16.14.02-.03c.16-.23.39-.42.65-.52a1 1 0 0 1 .9.06Zm1.08 4.2.94-2 .02-.03v-.04a.4.4 0 0 0-.13-.16l-2.4-1.1-.02-.01h-.04a.4.4 0 0 0-.16.13L12.53 4l2.7 1.27Zm-3.12-.37L6.16 17.56l2.7 1.27L14.8 6.16 12.1 4.9ZM6 18.6v2.84l2.18-1.81L6 18.59Z"/>',
/* oil-change-l/-s, inspection-l/-s aus dem Audi-CI-Icon-Satz (vom Nutzer als
SVG geliefert, verbatim uebernommen) - l im 48er-, s im 24er-Grid wie
poi-l/poi-s. Die -s-Variante steht links neben der Kennzahl im
Nächster-Service-Block (vHome()) statt des vorherigen "bis zum/zur..."
Textes - die Serviceart wird jetzt ueber das Icon erkannt, nicht ueber
Worte. */
oilChangeL: '<path fill="currentColor" d="M15 16a.5.5 0 0 0-.5.5c0 .28.22.5.5.5h5a.5.5 0 0 0 .5-.5.5.5 0 0 0-.5-.5zm-1.5.5c0-.83.66-1.5 1.5-1.5h5a1.5 1.5 0 1 1 0 3h-2v3h5.67l3.92 2.94 16.45-5.48 3.72 1.86-2.11 2.81-2.17-1.08-9.65 3.86L27.77 35H9V24.73L5.82 22H1v-3.5h6.16l3.5 2.5H17v-3h-2a1.5 1.5 0 0 1-1.5-1.5M2 19.5V21h4.18L10 24.27V34h17.23l5.94-8.9 10.35-4.15 1.83.92.89-1.2-2.28-1.13-16.55 5.52L23.33 22H10.34l-3.5-2.5zm43.5 4.1.42.62-.42.28-.42-.28zm0 1.84a17 17 0 0 0-.8 1.55c-.14.34-.2.59-.2.73 0 .8.52 1.28 1 1.28s1-.49 1-1.28c0-.14-.06-.39-.2-.73a10 10 0 0 0-.8-1.55m0-.94.42-.28v.01l.01.02.02.03.03.05a18 18 0 0 1 1.25 2.29c.14.36.27.76.27 1.1 0 1.18-.82 2.28-2 2.28s-2-1.1-2-2.28c0-.34.13-.74.27-1.1.15-.38.35-.77.54-1.12a19 19 0 0 1 .76-1.25v-.02h.01z"/>',
oilChangeS: '<path fill="currentColor" d="M9 8H7.5V7h4v1H10v2h2.77l.94 1.41 7.93-2.48 2.21 2.22-.7.7-1.79-1.78-1.03.32L15.25 17H4v-6.8L2.8 9H0V8h3.2l2 2H9zm-4 3v5h9.75l3.92-5.1-5.38 1.69L12.23 11z"/>',
inspectionL: '<path fill="currentColor" d="M11.5 1.8 13 3.3v4l.7.7h4.6l.7-.7v-4l1.5-1.5L25 6.3v6.4l-5 5v24.8c0 1.98-1.52 3.5-3.5 3.5h-1a3.45 3.45 0 0 1-3.5-3.5V17.7l-5-5V6.3zm21.2.2h4.6L40 6.87v4.84l-3 3L36.9 26H40v3.12l-1 2v5.26l1 2v5.12a2.5 2.5 0 0 1-2.5 2.5h-5a2.5 2.5 0 0 1-2.5-2.5v-5.12l1-2v-5.26l-1-2V26h3V14.7l-3-3V6.88zm3.2 23.9.1-11.6 3-3V7.12L36.7 3h-3.4L31 7.13v4.16l3 3V25.9zM8 6.7v5.6l5 5v25.2c0 1.42 1.08 2.5 2.5 2.5h1c1.42 0 2.5-1.08 2.5-2.5V17.3l5-5V6.7l-3.5-3.5-.5.5v4L18.7 9h-5.4L12 7.7v-4l-.5-.5zM31 27v1.88l1 2v5.74l-1 2v4.88c0 .82.68 1.5 1.5 1.5h5c.82 0 1.5-.68 1.5-1.5v-4.88l-1-2v-5.74l1-2V27z"/>',
inspectionS: '<path fill="currentColor" d="M3.3 0H5v3.3l.7.7h2.6l.7-.7V0h1.7L13 2.3v4.4l-3 3V21c0 .69-.3 1.42-.76 1.98A2.57 2.57 0 0 1 7.3 24h-.6c-.79 0-1.47-.42-1.93-.98A3.22 3.22 0 0 1 4 21V9.7l-3-3V2.3zm.4 1L2 2.7v3.6l3 3V21c0 .47.2.98.53 1.38.34.4.76.62 1.17.62h.6c.4 0 .82-.24 1.16-.65.34-.42.54-.94.54-1.35V9.3l3-3V2.7L10.3 1H10v2.7L8.7 5H5.3L4 3.7V1zm12.44-1h3.72L21 3.42V6.7l-1 1V14h1v7.5a2.5 2.5 0 0 1-2.5 2.5h-1a2.5 2.5 0 0 1-2.5-2.5V14h1V7.7l-1-1V3.43zM16 15v6.5c0 .82.68 1.5 1.5 1.5h1c.82 0 1.5-.68 1.5-1.5V15zm3-1V7.3l1-1V3.57L19.14 1h-2.28L16 3.58V6.3l1 1V14z"/>',
};
function ciSVG(pfad, px = 16) {
return `<svg viewBox="0 0 24 24" width="${px}" height="${px}" fill="none" aria-hidden="true">${pfad}</svg>`;
@@ -1089,14 +1099,15 @@ function teaser() {
function naechsterTermin() { return termine().slice().sort((a, b) => a.ziel - b.ziel)[0]; }
/* "der Ölwechsel", aber "die Inspektion"/"die Hauptuntersuchung" - ohne diese
Zuordnung stünde auf der Übersicht "bis zum Inspektion". */
const ART_BIS = {
"Ölwechsel": "bis zum Ölwechsel",
"Inspektion": "bis zur Inspektion",
"Hauptuntersuchung": "bis zur Hauptuntersuchung",
};
const bisText = (art) => ART_BIS[art] || `bis zum nächsten Service (${art})`;
// CI-Icon je Serviceart statt eines Wort-Zusatzes ("bis zum Ölwechsel" o.ä.) -
// nur Ölwechsel/Inspektion haben eines, die Hauptuntersuchung nicht. Verwendet
// von vHome() (Nächster-Service-Block) und vService() (Anstehende Termine).
// Das Ölwechsel-Icon ist bei gleicher Boxgröße optisch kleiner als das
// Inspektion-Icon (füllt in seinem eigenen 24x24-Raster nur eine 24x10-Fläche
// statt fast der vollen Höhe) - deshalb eigene, größere Boxgröße statt einer
// gemeinsamen Konstante.
const ART_ICON = { "Ölwechsel": CI.oilChangeS, "Inspektion": CI.inspectionS };
const ART_ICON_GROESSE = { "Ölwechsel": 26, "Inspektion": 20 };
function intervalle(t) {
const oel = CAR.oel, b = t.basis;
@@ -1281,17 +1292,26 @@ function vHome() {
const hauptwert = restKm !== null
? `${de(restKm)}<span class="unit">km</span>`
: (datum ? dedat(datum) : "");
// Ohne Restkilometer steht das Datum selbst im Zahlenfeld - dann kleiner,
// damit "15.04.2027" auf 375px nicht umbricht.
const zahlGroesse = restKm !== null ? 40 : 30;
// Ohne Restkilometer steht das Datum selbst im Zahlenfeld - auf Nutzerwunsch
// jetzt in derselben Groesse wie die km-Zahl (vorher 30px, damit "15.04.2027"
// auf 375px nicht umbricht - am laufenden Panel geprueft, dass es bei 40px
// weiterhin einzeilig bleibt).
const zahlGroesse = 40;
// Icon statt "bis zum Ölwechsel"/"bis zur Inspektion" (nur fuer die beiden
// Arten, die eines haben). Die Hauptuntersuchung hat kein Restkilometer-Ziel
// und zeigt stattdessen den Art-Namen selbst unter dem Datum.
const artIcon = ART_ICON[t.art];
const nebenzeile = restKm !== null && datum
? `${bisText(t.art)} · vsl. ${dedat(datum)}`
: bisText(t.art);
? `voraussichtlich am ${dedat(datum)}`
: t.art;
const figMarkup = artIcon
? `<div class="fig-zeile">${ciSVG(artIcon, 31)}<div class="fig" style="font-size:${zahlGroesse}px">${hauptwert}</div></div>`
: `<div class="fig" style="font-size:${zahlGroesse}px;margin-top:8px">${hauptwert}</div>`;
return `<div class="tile" style="margin-bottom:22px">
<button class="serviceblock" data-go="service:x">
<span class="label">Nächster Service:</span>
<svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
<div class="fig" style="font-size:${zahlGroesse}px;margin-top:8px">${hauptwert}</div>
${figMarkup}
<span class="label" style="margin-top:8px">${nebenzeile}</span>
</button>
${kmZeile}
@@ -1672,42 +1692,52 @@ function vService() {
const sv = CAR.service;
return `
<div class="tile"><span class="label">Anstehende Termine</span>
<dl class="rows" style="margin-top:10px">
${termine().map((t, i, arr) => {
const letzte = i === arr.length - 1 ? 'style="border-bottom:0"' : "";
const iv = intervalle(t);
if (!iv) return `<div class="row" ${letzte}>
<dt>${t.art}</dt><dd style="color:var(--fg3)">kein Eintrag im Servicebuch</dd></div>`;
const anzeige = iv.hersteller || iv.eigen;
const fm = fahrzeugMeldungRoh(t.art);
const fmDatum = fm.ts ? dedat(new Date(fm.ts)) : null;
// Forecast-Zeile aus dem kundeneigenen Intervall in "Einrichten" mit
// eigenständiger Reset-nach-Service-Logik (servicePrognose()): beim
// Ölwechsel mit Restkilometern, bei der Inspektion auf Wunsch nur mit
// dem Datum. Hauptuntersuchung bleibt bewusst ohne.
const eigenOel = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
const eigenInsp = t.art === "Inspektion" ? inspektionPrognose() : null;
const hatMeldung = fm.km != null || fmDatum;
// Die Prognose steht als eigene Zeile ueber die volle Kachelbreite
// unter dt/dd (.row-fussnote), nicht mehr in der rechten Spalte: dort
// standen nur ~217px zur Verfuegung, in die "9.029 km, voraussichtlich
// am 12.04.2027" zwangslaeufig zweizeilig umbrach.
const fussnote = hatMeldung && eigenOel
? `${de(eigenOel.restKm)} km, voraussichtlich am ${dedat(eigenOel.datum)}`
: eigenInsp ? `voraussichtlich am ${dedat(eigenInsp.datum)}` : "";
return `<div class="row" ${letzte}>
<dt>${t.art}
${t.vereinbart ? `<br><span style="color:var(--red);font-size:11px">Werkstatt am ${dedat(new Date(t.vereinbart))}</span>` : ""}
</dt>
<dd>
${iv.hersteller ? `<span class="label" style="margin-bottom:6px">Herstellervorgabe</span>` : ""}
<span class="werte">${hatMeldung
? `${fm.km != null ? de(fm.km) + " km" : ""}${fm.km != null && fmDatum ? " / " : ""}${fmDatum || ""}`
: (eigenOel ? de(eigenOel.restKm) + " km · " + dedat(eigenOel.datum) : (anzeige.km !== null ? de(anzeige.km) + " km · " : "") + dedat(anzeige.datum))}</span>
</dd>
${fussnote ? `<small class="row-fussnote">${fussnote}</small>` : ""}
${termine().map((t) => {
const artIcon = ART_ICON[t.art];
const iconMarkup = artIcon ? ciSVG(artIcon, ART_ICON_GROESSE[t.art]) : "";
const kopf = `<div class="tg-name">${iconMarkup}<span>${t.art}</span></div>`;
const iv = intervalle(t);
if (!iv) return `<div class="termingruppe">
<div class="tg-kopf">${kopf}</div>
<div class="tg-wert leer">kein Eintrag im Servicebuch</div>
</div>`;
const anzeige = iv.hersteller || iv.eigen;
const fm = fahrzeugMeldungRoh(t.art);
const fmDatum = fm.ts ? dedat(new Date(fm.ts)) : null;
// Forecast-Zeile aus dem kundeneigenen Intervall in "Einrichten" mit
// eigenständiger Reset-nach-Service-Logik (servicePrognose()): beim
// Ölwechsel mit Restkilometern, bei der Inspektion auf Wunsch nur mit
// dem Datum. Hauptuntersuchung bleibt bewusst ohne.
const eigenOel = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
const eigenInsp = t.art === "Inspektion" ? inspektionPrognose() : null;
const hatMeldung = fm.km != null || fmDatum;
// Bei Herstellervorgabe rechnet die eigene Prognose ohnehin schon mit dem
// Herstellerintervall - die Restkilometerzahl daneben waere nur eine
// zweite, verwirrende km-Angabe neben dem Fahrzeug-Wert oben. Nur beim
// eigenen (kuerzeren) Intervall ist sie eine echte Zusatzinformation.
const fussnote = hatMeldung && eigenOel
? (CAR.oel.modus === "hersteller"
? `voraussichtlich am ${dedat(eigenOel.datum)}`
: `${de(eigenOel.restKm)} km, voraussichtlich am ${dedat(eigenOel.datum)}`)
: eigenInsp ? `voraussichtlich am ${dedat(eigenInsp.datum)}` : "";
const wert = hatMeldung
? `${fm.km != null ? de(fm.km) + " km" : ""}${fm.km != null && fmDatum ? " / " : ""}${fmDatum || ""}`
: (eigenOel ? de(eigenOel.restKm) + " km · " + dedat(eigenOel.datum) : (anzeige.km !== null ? de(anzeige.km) + " km · " : "") + dedat(anzeige.datum));
// Eine Fahrzeugmeldung stammt immer aus dem werkseigenen Wartungsprogramm
// des Bordcomputers - das kennt das eigene, in "Einrichten" gewaehlte
// Intervall gar nicht. Deshalb zaehlt sie unabhaengig von iv.hersteller
// als Herstellervorgabe; nur der App-eigene Eigen-Intervall-Fallback
// (anzeige.eigen/eigenOel bei modus "eigen") ist es nicht.
const herstellervorgabe = hatMeldung || iv.hersteller;
return `<div class="termingruppe">
<div class="tg-kopf">
${kopf}
${t.vereinbart ? `<span class="tg-chip">Werkstatt am ${dedat(new Date(t.vereinbart))}</span>` : ""}
</div>
${herstellervorgabe ? `<div class="tg-vorgabe">Herstellervorgabe</div>` : ""}
<div class="tg-wert">${wert}</div>
${fussnote ? `<div class="tg-prognose">${fussnote}</div>` : ""}
</div>`; }).join("")}
</dl>
</div>
<div class="tile"><span class="label">Termin vereinbaren</span>
${(() => { const ts = termine(), t = ts[Math.min(svAuswahl, ts.length - 1)];
@@ -2058,7 +2088,7 @@ function vEinst() {
<span class="label" style="margin-top:20px">Ölwechsel-Intervall</span>
<div class="seg" style="margin-top:8px">
<button data-oel="hersteller" class="${CAR.oel.modus === "hersteller" ? "on" : ""}">Herstellervorgabe</button>
<button data-oel="eigen" class="${CAR.oel.modus === "eigen" ? "on" : ""}">Eigenes</button>
<button data-oel="eigen" class="${CAR.oel.modus === "eigen" ? "on" : ""}">Individuell</button>
</div>
${CAR.oel.modus === "hersteller"
? `<dl class="rows" style="margin-top:16px">
@@ -1 +1 @@
{"version": 1787007000}
{"version": 1787013003}
+32
View File
@@ -482,6 +482,9 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
.serviceblock:active { opacity: .55; }
/* Beschriftung nie unter dem Chevron hindurchlaufen lassen */
.serviceblock > .label:first-child { padding-right: 24px; }
/* Serviceart-Icon links der Kennzahl statt des Wortes "bis zum/zur ...". */
.fig-zeile { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.fig-zeile svg { color: var(--fg2); flex: 0 0 auto; }
.servicezeile {
display: flex; align-items: baseline; justify-content: space-between;
gap: var(--sp-4);
@@ -489,6 +492,35 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
margin-top: 14px; padding-top: 13px;
}
/* Anstehende Termine (vService): drei sichtbar getrennte Bloecke statt einer
dt/dd-Liste - Apple HIG ("essential information... don't obscure it by
crowding it with nonessential details") plus Audi-CI (Flaeche mit Haarlinien
statt Karten, eigene Groessenklasse fuer Zahlen). Trennung ueber Abstand und
eine Haarlinie zwischen den Bloecken, nicht ueber verschachtelte Kacheln.
Jeder Block: Kopfzeile (Icon + Art-Name, optional ein Chip fuer einen
vereinbarten Werkstatttermin), optionale Herstellervorgabe-Zeile, der Wert,
optional die eigene Prognose als Fusszeile. */
.termingruppe { padding: 18px 0; border-top: 1px solid var(--line); }
.termingruppe:first-of-type { padding-top: 2px; border-top: 0; }
.tg-kopf { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.tg-name { display: flex; align-items: center; gap: 9px; min-width: 0; }
.tg-name svg { color: var(--fg2); flex: 0 0 auto; }
.tg-name span { font-size: 16.5px; color: var(--fg); font-weight: 400; }
/* Chip statt rotem Fliesstext fuer einen vereinbarten Werkstatttermin - Rot
bleibt Akzent (Text/Rahmen), nie Flaeche, wie im Rest der App. */
.tg-chip {
font-size: 10.5px; color: var(--red);
border: 1px solid var(--red); border-radius: var(--r-pill);
padding: 3px 8px; white-space: nowrap; flex: 0 0 auto;
}
.tg-vorgabe { font-size: 11.5px; color: var(--fg2); margin-top: 8px; }
.tg-wert {
font-family: "Audi Type Wide", var(--font-stack); font-weight: 300;
font-size: 22px; color: var(--fg); letter-spacing: -.005em; margin-top: 4px;
}
.tg-wert.leer { font-family: var(--font-stack); font-weight: 400; font-size: 15px; color: var(--fg3); margin-top: 8px; }
.tg-prognose { font-size: 12px; color: var(--fg2); margin-top: 6px; }
.badge { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.badge img { height: 18px; width: auto; display: none; }
:host([data-theme=nacht]) .badge img.negative { display: block; }