Ueberschrittene Servicetermine werden markiert
Wunsch des Eigentuemers: faellige oder ueberschrittene Termine dezent
gelb/orange hinterlegen und mit "ueberschritten" markieren - Service-Seite und
Mein Audi; auf der Uebersicht zusaetzlich die Kennzahl im Warnton.
Erkannt wird am DATUM, nicht an den Restkilometern. Gemessen: die Sensoren
melden -28600 bzw. -22000 bei Faelligkeit 2028 bzw. 2027, negativ heisst hier
also "noch so weit hin". Welches Vorzeichen ein tatsaechlich ueberschrittener
Wert traegt, ist an den vorliegenden Daten nicht beobachtbar - geraten wird es
nicht. Der km-Fall faellt ohnehin unter die Datumsregel: servicePrognose()
setzt datum auf heute, sobald restKm auf 0 laeuft. Der Faelligkeitstag zaehlt
mit ("bereits faellig bzw. ueberschritten").
Dabei eine Luecke im Panel gefunden, vom Test und nicht im Betrieb:
datumAusText() ist verankert und nimmt keine vollen Zeitstempel - die
Fahrzeugmeldung ist aber genau das und bei Oelwechsel und Inspektion die
Hauptquelle. Sie waere still nie als ueberschritten erkannt worden.
zeitstempelAlsDatum() schliesst sie; die App hatte die Luecke nie.
Farbe nach Rolle geteilt wie --bad/--bad-flaeche: --warn ist die Flaeche,
--warn-text die Schrift. Grund: #FF9F0A als Text auf Weiss sind 2.06:1. Auf dem
12% hinterlegten Grund misst --warn-text 4.90:1 (Tag, #A15800) bzw. 9.30:1
(Nacht).
Geprueft: 15 Faelle gegen die echten Panel-Funktionen, 7 in der App, 14
Paritaetsfaelle Panel gegen App, 3 Rendertests ueber alle drei Anzeigestellen.
Gegenprobe: mit ausgehaengter Entscheidung fallen 9 Tests. 327/327, tsc sauber,
Panel als Modul geparst, Manifest und Buendel auf 2026.9.6.6, Container sauber
gestartet.
Offen: optisch nicht gesehen - der Testbestand enthaelt keinen ueberschrittenen
Termin. In OFFEN.md vermerkt.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# AGENTS.md — Project state, review findings, open items, and working rules
|
# AGENTS.md — Project state, review findings, open items, and working rules
|
||||||
|
|
||||||
**Last updated: 2026-09-06** (Die Batteriehistorie zeichnete stehende Werte als frische Messungen auf - der Vorschlag aus CB war messbar falsch, Abschnitt DI; davor: die ausgelieferte .ipa registriert zwei eigene Plugins nicht, Abschnitt DH). Davor: 2026-09-05, Audit-Durchgang, Abschnitt DC. (Tankstelle zweizeilig und antippbar, die Belegkarte sagt bei
|
**Last updated: 2026-09-07** (Überschrittene Servicetermine werden markiert, Abschnitt DJ). Davor: 2026-09-06 (Die Batteriehistorie zeichnete stehende Werte als frische Messungen auf - der Vorschlag aus CB war messbar falsch, Abschnitt DI; davor: die ausgelieferte .ipa registriert zwei eigene Plugins nicht, Abschnitt DH). Davor: 2026-09-05, Audit-Durchgang, Abschnitt DC. (Tankstelle zweizeilig und antippbar, die Belegkarte sagt bei
|
||||||
fehlender Position die Wahrheit statt einen toten Knopf zu zeigen, der Regler bekommt einen
|
fehlender Position die Wahrheit statt einen toten Knopf zu zeigen, der Regler bekommt einen
|
||||||
Speichern-Knopf, und die Share-Erweiterung trug eine andere Versionsnummer als die App;
|
Speichern-Knopf, und die Share-Erweiterung trug eine andere Versionsnummer als die App;
|
||||||
dazu die Kopfmarke statt der Firmierung des Betreibers und ein
|
dazu die Kopfmarke statt der Firmierung des Betreibers und ein
|
||||||
@@ -12708,3 +12708,98 @@ Daten des Eigentümers, und welche davon bleiben, entscheidet er. Sollte er
|
|||||||
später mehr als tageweises Löschen brauchen (mehrere Tage auf einmal, oder eine
|
später mehr als tageweises Löschen brauchen (mehrere Tage auf einmal, oder eine
|
||||||
Kennzeichnung verdächtiger Einträge), ist das ein eigener Auftrag - heute ist
|
Kennzeichnung verdächtiger Einträge), ist das ein eigener Auftrag - heute ist
|
||||||
nichts davon angefragt.
|
nichts davon angefragt.
|
||||||
|
|
||||||
|
## DJ. Überschrittene Servicetermine werden markiert (2026.9.6.6)
|
||||||
|
|
||||||
|
Wunsch des Eigentümers: fällige oder überschrittene Servicetermine hervorheben -
|
||||||
|
dezent gelb/orange transparent hinterlegt und mit „überschritten" markiert, auf
|
||||||
|
der Service-Seite und in „Mein Audi"; auf der Übersicht zusätzlich mit der
|
||||||
|
Kennzahl selbst im Warnton.
|
||||||
|
|
||||||
|
### Woran „überschritten" erkannt wird - und woran nicht
|
||||||
|
|
||||||
|
**Am Datum, nicht an den Restkilometern.** Das ist keine Bequemlichkeit: die
|
||||||
|
Kilometerseite trägt je nach Quelle ein anderes Vorzeichen. Am 07.09.2026 an den
|
||||||
|
echten Sensoren gemessen:
|
||||||
|
|
||||||
|
| Sensor | Wert | Fälligkeit laut Datum |
|
||||||
|
|---|---|---|
|
||||||
|
| `oil_change_distance` | **-28600** | 2028-07-23 |
|
||||||
|
| `inspection_distance` | **-22000** | 2027-10-21 |
|
||||||
|
|
||||||
|
Negativ heißt hier also „noch so weit hin", nicht „überschritten" - `Math.abs()`
|
||||||
|
in `meldungsPrognose()` ist damit richtig. Welches Vorzeichen ein *tatsächlich*
|
||||||
|
überschrittener Fahrzeugwert trägt, ist an den vorliegenden Daten **nicht
|
||||||
|
beobachtbar**; geraten wird es nicht. Dazu klemmt `servicePrognose()` ihre
|
||||||
|
Restkilometer ohnehin mit `Math.max(0, ...)`, die Information wäre dort also
|
||||||
|
auch verworfen.
|
||||||
|
|
||||||
|
Das Datum ist eindeutig, und der km-Fall fällt darunter: `servicePrognose()`
|
||||||
|
setzt `datum` auf **heute**, sobald `restKm` auf 0 läuft. Die Regel lautet
|
||||||
|
deshalb: **überschritten, sobald das Datum nicht mehr in der Zukunft liegt** -
|
||||||
|
der Fälligkeitstag selbst zählt mit, denn gefragt war „bereits fällig bzw.
|
||||||
|
überschritten".
|
||||||
|
|
||||||
|
Genommen wird das **erste lesbare** der übergebenen Daten - also das, das an
|
||||||
|
dieser Stelle auch angezeigt wird. So kann die Markierung nicht von dem
|
||||||
|
abweichen, was daneben steht.
|
||||||
|
|
||||||
|
### Eine Lücke im Panel, die der Test gefangen hat
|
||||||
|
|
||||||
|
`datumAusText()` im Panel ist verankert (`^…$`) und nimmt **keine** vollen
|
||||||
|
Zeitstempel. Die Fahrzeugmeldung (`fm.ts`) ist aber genau das - und bei Ölwechsel
|
||||||
|
und Inspektion die Hauptquelle. Ohne Korrektur wäre sie stillschweigend nie als
|
||||||
|
überschritten erkannt worden. Aufgefallen ist das im isolierten Test gegen die
|
||||||
|
echten Panel-Funktionen, nicht im Betrieb; `zeitstempelAlsDatum()` schließt sie,
|
||||||
|
bewusst eng gefasst (`/^\d{4}-\d{2}-\d{2}T/`) statt mit einem allgemeinen
|
||||||
|
`new Date()`, das auch deutete, was `datumAusText()` gerade abgelehnt hat.
|
||||||
|
|
||||||
|
Die App hatte diese Lücke nicht: ihr `ISO`-Muster akzeptiert `[T ]` seit jeher.
|
||||||
|
Damit sind beide Datumsleser jetzt gleich weit.
|
||||||
|
|
||||||
|
### Farbe: --warn ist die Fläche, --warn-text die Schrift
|
||||||
|
|
||||||
|
Gemessen statt gewählt. `--warn` ist im Tagmodus `#FF9F0A` - als **Text** auf
|
||||||
|
Weiß sind das **2,06:1**, weit unter der 4,5:1-Grenze, die dieses Projekt sich
|
||||||
|
selbst gesetzt hat (siehe Abschnitt CP, wo `--fg3` aus demselben Grund
|
||||||
|
abgedunkelt wurde). Deshalb dieselbe Rollenteilung wie bei `--bad` /
|
||||||
|
`--bad-flaeche`:
|
||||||
|
|
||||||
|
| | Fläche (12 % hinterlegt) | Schrift `--warn-text` | Kontrast auf dem hinterlegten Grund |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Tag | `#FF9F0A` → Grund `#FFF3E2` | **`#A15800`** | **4,90:1** |
|
||||||
|
| Nacht | `#FFD60A` → Grund `#33311E` | `#FFD60A` | **9,30:1** |
|
||||||
|
|
||||||
|
Nachts trägt `--warn` selbst genug Kontrast, tagsüber nicht. `#B36200` wäre
|
||||||
|
näher am reinen Orange, fällt auf dem hinterlegten Grund aber auf 4,11:1 und
|
||||||
|
damit unter die Grenze - deshalb `#A15800`.
|
||||||
|
|
||||||
|
### Geprüft
|
||||||
|
|
||||||
|
* **15 Fälle** gegen die echten Panel-Funktionen (aus der Panel-Quelle
|
||||||
|
geschnitten, nicht nachgebaut), darunter beide Zeitstempel-Richtungen und die
|
||||||
|
Tagesgrenze.
|
||||||
|
* **7 Fälle** in `service.test.ts` für die App; gegengeprüft, dass die
|
||||||
|
Tagesgrenze wirklich gemessen wird (`<=` auf `<` gedreht → genau der
|
||||||
|
„heute zählt mit"-Test fällt).
|
||||||
|
* **14 Paritätsfälle** in `paritaet_ueberschritten.test.ts`: Panel und App
|
||||||
|
urteilen über dieselben Eingaben gleich, geprüft gegen die aus der Panel-Quelle
|
||||||
|
geschnittene Funktion.
|
||||||
|
* **3 Rendertests** über alle drei Anzeigestellen, mit einem Profil, dessen
|
||||||
|
Hauptuntersuchung in der Vergangenheit liegt. Der Regelbestand der
|
||||||
|
Beispieldaten taugte dafür nicht (Wartungsplan 2026-02-10 + 24 Monate);
|
||||||
|
`beispielApi()` nimmt dafür jetzt ein abweichendes Profil entgegen - additiv,
|
||||||
|
alle bisherigen Aufrufer unverändert.
|
||||||
|
* Gegenprobe über alles: mit ausgehängter Entscheidung (`terminUeberschritten`
|
||||||
|
liefert immer `false`) fallen **9 Tests**, darunter alle drei Rendertests.
|
||||||
|
* 327/327 App-Tests, `tsc` sauber, beide Panel-Dateien als **Modul** geparst,
|
||||||
|
Manifest und Bündel gleichauf auf `2026.9.6.6`, Testcontainer sauber gestartet.
|
||||||
|
|
||||||
|
### Was offen bleibt
|
||||||
|
|
||||||
|
**Optisch nicht gesehen.** Die Markierung ist über Rendertests und gemessene
|
||||||
|
Kontraste belegt, nicht über ein Bild - der Testbestand enthält keinen
|
||||||
|
überschrittenen Termin (Ölwechsel 2028, Inspektion 2027, HU 2028), und für einen
|
||||||
|
Blick ins laufende Panel fehlte die angemeldete Sitzung. Nach der Regel dieses
|
||||||
|
Projekts (Abschnitt S: ein sauberer Testlauf beweist kein Layout) steht das
|
||||||
|
ausdrücklich als offen, nicht als erledigt.
|
||||||
|
|||||||
@@ -98,6 +98,17 @@ Beispielzahlen in der Statistik und ein überholter TODO-Kommentar.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 3b. Optisch noch nicht gesehen
|
||||||
|
|
||||||
|
**Die Markierung überschrittener Servicetermine** (`2026.9.6.6`, Abschnitt DJ)
|
||||||
|
ist über Rendertests und gemessene Kontraste belegt, aber nie am Bild geprüft -
|
||||||
|
der Testbestand hat keinen überschrittenen Termin, und für einen Blick ins
|
||||||
|
laufende Panel fehlte die angemeldete Sitzung.
|
||||||
|
|
||||||
|
*Was zu tun ist:* einmal ansehen, sobald ein Termin wirklich fällig ist - oder
|
||||||
|
mit einer angemeldeten Sitzung und einem vorübergehend zurückdatierten
|
||||||
|
Wartungsplan-Eintrag (hinterher zurücksetzen).
|
||||||
|
|
||||||
## 4. Gemeldet, aber nicht reproduzierbar
|
## 4. Gemeldet, aber nicht reproduzierbar
|
||||||
|
|
||||||
Beide mehrfach gesucht und **nicht gefunden**. Sie bleiben stehen, bis sie
|
Beide mehrfach gesucht und **nicht gefunden**. Sie bleiben stehen, bis sie
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { readFileSync } from "node:fs"
|
||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
import { terminUeberschritten } from "./service"
|
||||||
|
|
||||||
|
/* Beide Codebasen müssen denselben Termin gleich beurteilen - sonst steht auf
|
||||||
|
der Übersicht "überschritten" und auf derselben Seite im Panel nicht. Geprüft
|
||||||
|
wird gegen die ECHTE Panel-Funktion, aus der Panel-Quelle herausgeschnitten,
|
||||||
|
nicht gegen einen Nachbau. Muster wie in paritaet_hu.test.ts. */
|
||||||
|
const quelle = readFileSync(
|
||||||
|
"../custom_components/audi_dashboard/frontend/audi-dashboard-app.js", "utf8")
|
||||||
|
const block = (kopf: string) => {
|
||||||
|
const i = quelle.indexOf(kopf)
|
||||||
|
if (i < 0) throw new Error("fehlt: " + kopf)
|
||||||
|
let tiefe = 0
|
||||||
|
for (let k = quelle.indexOf("{", i); k < quelle.length; k++) {
|
||||||
|
if (quelle[k] === "{") tiefe++
|
||||||
|
else if (quelle[k] === "}" && !--tiefe) return quelle.slice(i, k + 1)
|
||||||
|
}
|
||||||
|
throw new Error("unvollstaendig: " + kopf)
|
||||||
|
}
|
||||||
|
const zeile = (muster: RegExp) => {
|
||||||
|
const m = muster.exec(quelle)
|
||||||
|
if (!m) throw new Error("fehlt: " + muster)
|
||||||
|
return m[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const panel = new Function(`
|
||||||
|
${zeile(/^const DATUM_ISO = .*$/m)}
|
||||||
|
${zeile(/^const DATUM_TAG = .*$/m)}
|
||||||
|
${zeile(/^const DATUM_MONAT = .*$/m)}
|
||||||
|
${zeile(/^const DATUM_ISO_MONAT = .*$/m)}
|
||||||
|
${zeile(/^const DATUM_ZEITSTEMPEL = .*$/m)}
|
||||||
|
${zeile(/^const gueltigesDatum = .*$/m)}
|
||||||
|
${block("function datumBauen")}
|
||||||
|
${block("function datumAusText")}
|
||||||
|
${block("function zeitstempelAlsDatum")}
|
||||||
|
${block("function terminUeberschritten")}
|
||||||
|
return { terminUeberschritten };
|
||||||
|
`)() as { terminUeberschritten: (...k: (string | Date | null)[]) => boolean }
|
||||||
|
|
||||||
|
const tag = (versatz: number) => {
|
||||||
|
const x = new Date()
|
||||||
|
x.setDate(x.getDate() + versatz)
|
||||||
|
return `${String(x.getDate()).padStart(2, "0")}.${String(x.getMonth() + 1).padStart(2, "0")}.${x.getFullYear()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("terminUeberschritten: Panel und App urteilen gleich", () => {
|
||||||
|
const faelle: { name: string; args: (string | null)[] }[] = [
|
||||||
|
{ name: "gestern", args: [tag(-1)] },
|
||||||
|
{ name: "heute", args: [tag(0)] },
|
||||||
|
{ name: "morgen", args: [tag(1)] },
|
||||||
|
{ name: "ISO vorbei", args: ["2024-03-01"] },
|
||||||
|
{ name: "ISO Zukunft", args: ["2028-07-23"] },
|
||||||
|
{ name: "Fahrzeug-Zeitstempel vorbei", args: ["2025-10-21T00:00:00+00:00"] },
|
||||||
|
{ name: "Fahrzeug-Zeitstempel Zukunft", args: ["2027-10-21T00:00:00+00:00"] },
|
||||||
|
{ name: "monatsgenau vorbei", args: ["03/2025"] },
|
||||||
|
{ name: "monatsgenau Zukunft", args: ["03/2099"] },
|
||||||
|
{ name: "kein Datum", args: [null] },
|
||||||
|
{ name: "leer", args: [""] },
|
||||||
|
{ name: "unlesbar", args: ["irgendwas"] },
|
||||||
|
{ name: "erstes lesbares: null, dann gestern", args: [null, tag(-1)] },
|
||||||
|
{ name: "erstes lesbares: morgen vor gestern", args: [tag(1), tag(-1)] },
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const f of faelle) {
|
||||||
|
it(f.name, () => {
|
||||||
|
expect(terminUeberschritten(...f.args)).toBe(panel.terminUeberschritten(...f.args))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,16 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
import {
|
import { hauptuntersuchungFaellig, inspektionPrognose, kmProTagAusFahrten, letzterOelwechsel, meldungsPrognose, meldungsPrognoseEigenesIntervall, naechsterService, oelMeldungsPrognose, oelwechselPrognose, terminUeberschritten } from "./service"
|
||||||
hauptuntersuchungFaellig,
|
|
||||||
inspektionPrognose,
|
|
||||||
kmProTagAusFahrten,
|
|
||||||
letzterOelwechsel,
|
|
||||||
meldungsPrognose,
|
|
||||||
meldungsPrognoseEigenesIntervall,
|
|
||||||
naechsterService,
|
|
||||||
oelMeldungsPrognose,
|
|
||||||
oelwechselPrognose,
|
|
||||||
} from "./service"
|
|
||||||
import type { Fahrzeug } from "./profilAdapter"
|
import type { Fahrzeug } from "./profilAdapter"
|
||||||
|
|
||||||
function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug {
|
function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug {
|
||||||
@@ -400,3 +390,50 @@ describe("Hauptuntersuchung", () => {
|
|||||||
expect(hauptuntersuchungFaellig([], "keine Ahnung")).toBeNull()
|
expect(hauptuntersuchungFaellig([], "keine Ahnung")).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
describe("terminUeberschritten", () => {
|
||||||
|
const tag = (versatz: number) => {
|
||||||
|
const x = new Date()
|
||||||
|
x.setDate(x.getDate() + versatz)
|
||||||
|
return `${String(x.getDate()).padStart(2, "0")}.${String(x.getMonth() + 1).padStart(2, "0")}.${x.getFullYear()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
it("gestern ist überschritten", () => {
|
||||||
|
expect(terminUeberschritten(tag(-1))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("heute zählt mit - 'bereits fällig' ist gemeint", () => {
|
||||||
|
expect(terminUeberschritten(tag(0))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("morgen nicht", () => {
|
||||||
|
expect(terminUeberschritten(tag(1))).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("erkennt den vollen Zeitstempel des Fahrzeugs", () => {
|
||||||
|
// Genau die Form, die oelwechselFaelligTs/inspektionFaelligTs tragen. Im
|
||||||
|
// Panel war das die Lücke: dessen Datumsleser ist verankert und nahm sie
|
||||||
|
// nicht an, die Fahrzeugmeldung wäre dort still nie überschritten gewesen.
|
||||||
|
expect(terminUeberschritten("2025-10-21T00:00:00+00:00")).toBe(true)
|
||||||
|
expect(terminUeberschritten("2027-10-21T00:00:00+00:00")).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("versteht monatsgenaue Angaben (Hauptuntersuchung)", () => {
|
||||||
|
expect(terminUeberschritten("03/2025")).toBe(true)
|
||||||
|
expect(terminUeberschritten("03/2099")).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("ohne lesbares Datum wird nichts behauptet", () => {
|
||||||
|
expect(terminUeberschritten(null)).toBe(false)
|
||||||
|
expect(terminUeberschritten("")).toBe(false)
|
||||||
|
expect(terminUeberschritten("irgendwas")).toBe(false)
|
||||||
|
expect(terminUeberschritten(undefined)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("nimmt das erste LESBARE Datum, nicht das erste beliebige", () => {
|
||||||
|
// So kann die Markierung nicht von dem abweichen, was daneben steht.
|
||||||
|
expect(terminUeberschritten(null, tag(-1))).toBe(true)
|
||||||
|
expect(terminUeberschritten(tag(1), tag(-1))).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -401,3 +401,39 @@ export function hauptuntersuchungFaellig(
|
|||||||
const ziel = monatePlus(isoTag(z.zeit), HU_MONATE_ERSTE)
|
const ziel = monatePlus(isoTag(z.zeit), HU_MONATE_ERSTE)
|
||||||
return z.genau === "monat" ? ziel.slice(0, 7) : ziel
|
return z.genau === "monat" ? ziel.slice(0, 7) : ziel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ist dieser Servicetermin bereits fällig oder überschritten?
|
||||||
|
*
|
||||||
|
* Der Fälligkeitstag selbst zählt mit („bereits fällig bzw. überschritten",
|
||||||
|
* Ansage des Eigentümers 07.09.2026).
|
||||||
|
*
|
||||||
|
* Entschieden wird am DATUM, nicht an den Restkilometern - und das ist keine
|
||||||
|
* Bequemlichkeit. Die Kilometerseite trägt je nach Quelle ein anderes
|
||||||
|
* Vorzeichen: die Sensoren des Fahrzeugs melden negativ, solange noch Strecke
|
||||||
|
* bleibt (am 07.09.2026 gemessen: oil_change_distance -28600 bei Fälligkeit
|
||||||
|
* 2028), und servicePrognose() klemmt ihre Restkilometer ohnehin mit
|
||||||
|
* Math.max(0, ...). Welches Vorzeichen ein überschrittener Fahrzeugwert trägt,
|
||||||
|
* lässt sich an den vorliegenden Daten nicht beobachten - geraten wird es
|
||||||
|
* nicht. Der km-Fall fällt ohnehin unter die Datumsregel: servicePrognose()
|
||||||
|
* setzt datum auf heute, sobald restKm auf 0 läuft.
|
||||||
|
*
|
||||||
|
* Genommen wird das ERSTE lesbare Datum der übergebenen Kandidaten - also das,
|
||||||
|
* das an dieser Stelle auch angezeigt wird. So kann die Markierung nicht von
|
||||||
|
* dem abweichen, was daneben steht. Wortgleich mit terminUeberschritten() im
|
||||||
|
* Panel. */
|
||||||
|
export function terminUeberschritten(
|
||||||
|
...kandidaten: (string | Date | null | undefined)[]
|
||||||
|
): boolean {
|
||||||
|
for (const kandidat of kandidaten) {
|
||||||
|
if (kandidat === null || kandidat === undefined || kandidat === "") continue
|
||||||
|
const z = alsZeitpunkt(kandidat)
|
||||||
|
if (!z) continue
|
||||||
|
const d = z.zeit
|
||||||
|
const h = new Date()
|
||||||
|
return (
|
||||||
|
new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime() <=
|
||||||
|
new Date(h.getFullYear(), h.getMonth(), h.getDate()).getTime()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ import { useRef, useState, type PointerEvent as ReactPointerEvent } from "react"
|
|||||||
import { Tile } from "@audi-dash/ui"
|
import { Tile } from "@audi-dash/ui"
|
||||||
|
|
||||||
import { useDaten } from "../daten/DatenKontext"
|
import { useDaten } from "../daten/DatenKontext"
|
||||||
import { hauptuntersuchungFaellig, inspektionPrognose, oelwechselPrognose } from "../daten/service"
|
import { hauptuntersuchungFaellig, inspektionPrognose, oelwechselPrognose, terminUeberschritten } from "../daten/service"
|
||||||
import { datum, de, deOderStrich, eur } from "../format"
|
import { datum, de, deOderStrich, eur } from "../format"
|
||||||
import type { SeitenName } from "../navigation"
|
import type { SeitenName } from "../navigation"
|
||||||
import { Wertzeile, Werteliste } from "./bausteine"
|
import { UeberschrittenMarke, Werteliste, Wertzeile } from "./bausteine"
|
||||||
import { Bild } from "./Bild"
|
import { Bild } from "./Bild"
|
||||||
import { GALERIE } from "./bilder"
|
import { GALERIE } from "./bilder"
|
||||||
import type { ServicebuchEintrag } from "./Service"
|
import type { ServicebuchEintrag } from "./Service"
|
||||||
@@ -115,13 +115,14 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) =
|
|||||||
const huFaellig = hauptuntersuchungFaellig(buch, fahrzeug.erstzulassung)
|
const huFaellig = hauptuntersuchungFaellig(buch, fahrzeug.erstzulassung)
|
||||||
const oelProg = oelwechselPrognose(fahrzeug, buch)
|
const oelProg = oelwechselPrognose(fahrzeug, buch)
|
||||||
const inspProg = inspektionPrognose(fahrzeug, buch)
|
const inspProg = inspektionPrognose(fahrzeug, buch)
|
||||||
const termine: { art: string; prognose: string | null; haupt: string; neben: string | null }[] = [
|
const termine: { art: string; prognose: string | null; haupt: string; neben: string | null; ueber: boolean }[] = [
|
||||||
{
|
{
|
||||||
art: "Ölwechsel",
|
art: "Ölwechsel",
|
||||||
prognose: oelProg
|
prognose: oelProg
|
||||||
? `${de(oelProg.restKm)} km, voraussichtlich am ${datum(oelProg.datum)}`
|
? `${de(oelProg.restKm)} km, voraussichtlich am ${datum(oelProg.datum)}`
|
||||||
: null,
|
: null,
|
||||||
haupt: fahrzeug.oelwechselFaelligTs ? datum(fahrzeug.oelwechselFaelligTs) : "–",
|
haupt: fahrzeug.oelwechselFaelligTs ? datum(fahrzeug.oelwechselFaelligTs) : "–",
|
||||||
|
ueber: terminUeberschritten(fahrzeug.oelwechselFaelligTs, oelProg?.datum),
|
||||||
neben:
|
neben:
|
||||||
fahrzeug.oelwechselFaelligKm != null
|
fahrzeug.oelwechselFaelligKm != null
|
||||||
? `in ${de(Math.abs(fahrzeug.oelwechselFaelligKm))} km`
|
? `in ${de(Math.abs(fahrzeug.oelwechselFaelligKm))} km`
|
||||||
@@ -131,6 +132,7 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) =
|
|||||||
art: "Inspektion",
|
art: "Inspektion",
|
||||||
prognose: inspProg ? `voraussichtlich am ${datum(inspProg.datum)}` : null,
|
prognose: inspProg ? `voraussichtlich am ${datum(inspProg.datum)}` : null,
|
||||||
haupt: fahrzeug.inspektionFaelligTs ? datum(fahrzeug.inspektionFaelligTs) : "–",
|
haupt: fahrzeug.inspektionFaelligTs ? datum(fahrzeug.inspektionFaelligTs) : "–",
|
||||||
|
ueber: terminUeberschritten(fahrzeug.inspektionFaelligTs, inspProg?.datum),
|
||||||
neben:
|
neben:
|
||||||
fahrzeug.inspektionFaelligKm != null
|
fahrzeug.inspektionFaelligKm != null
|
||||||
? `in ${de(Math.abs(fahrzeug.inspektionFaelligKm))} km`
|
? `in ${de(Math.abs(fahrzeug.inspektionFaelligKm))} km`
|
||||||
@@ -142,6 +144,7 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) =
|
|||||||
art: "Hauptuntersuchung",
|
art: "Hauptuntersuchung",
|
||||||
prognose: null,
|
prognose: null,
|
||||||
haupt: huFaellig ? datum(huFaellig) : "–",
|
haupt: huFaellig ? datum(huFaellig) : "–",
|
||||||
|
ueber: terminUeberschritten(huFaellig),
|
||||||
neben: null,
|
neben: null,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -270,7 +273,15 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) =
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
wert={t.haupt}
|
wert={
|
||||||
|
t.ueber ? (
|
||||||
|
<>
|
||||||
|
{t.haupt} <UeberschrittenMarke />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
t.haupt
|
||||||
|
)
|
||||||
|
}
|
||||||
{...(t.neben ? { zusatz: t.neben } : {})}
|
{...(t.neben ? { zusatz: t.neben } : {})}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -14,15 +14,11 @@ import { ActionButton, Feld, Fig, Tile } from "@audi-dash/ui"
|
|||||||
|
|
||||||
import { kartendienstUrl } from "../daten/tankstelle"
|
import { kartendienstUrl } from "../daten/tankstelle"
|
||||||
import { useDaten } from "../daten/DatenKontext"
|
import { useDaten } from "../daten/DatenKontext"
|
||||||
import { hauptuntersuchungFaellig, inspektionPrognose, letzterInspektion, letzterOelwechsel, meldungsPrognose, oelMeldungsPrognose, oelwechselPrognose, serviceKandidaten } from "../daten/service"
|
import { hauptuntersuchungFaellig, inspektionPrognose, letzterInspektion, letzterOelwechsel, meldungsPrognose, oelMeldungsPrognose, oelwechselPrognose, serviceKandidaten, terminUeberschritten } from "../daten/service"
|
||||||
import { datum, de, eur, isoTag } from "../format"
|
import { datum, de, eur, isoTag } from "../format"
|
||||||
import type { SeitenName } from "../navigation"
|
import type { SeitenName } from "../navigation"
|
||||||
import { SymbolInspektion, SymbolOelwechsel } from "../symbole"
|
import { SymbolInspektion, SymbolOelwechsel } from "../symbole"
|
||||||
import {
|
import { Blattzeile, Leerzustand, UeberschrittenMarke, Zahnradknopf } from "./bausteine"
|
||||||
Blattzeile,
|
|
||||||
Leerzustand,
|
|
||||||
Zahnradknopf,
|
|
||||||
} from "./bausteine"
|
|
||||||
import { LOESCH_HINWEIS, LOESCH_TEXT, bestaetigen } from "./bestaetigung"
|
import { LOESCH_HINWEIS, LOESCH_TEXT, bestaetigen } from "./bestaetigung"
|
||||||
import { Datumsfeld } from "./Datumsfeld"
|
import { Datumsfeld } from "./Datumsfeld"
|
||||||
import { terminUebernehmen } from "./kalender"
|
import { terminUebernehmen } from "./kalender"
|
||||||
@@ -127,14 +123,25 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
|
|||||||
const huDatum = huFaellig ? new Date(huFaellig) : null
|
const huDatum = huFaellig ? new Date(huFaellig) : null
|
||||||
const huGueltig = !!huDatum && !Number.isNaN(huDatum.getTime())
|
const huGueltig = !!huDatum && !Number.isNaN(huDatum.getTime())
|
||||||
|
|
||||||
|
// Dasselbe Datum, das gleich als Wert erscheint - siehe
|
||||||
|
// terminUeberschritten(). Die Fahrzeugmeldung geht vor, weil sie oben steht.
|
||||||
|
const oelUeber = terminUeberschritten(
|
||||||
|
oelHatMeldung ? fahrzeug.oelwechselFaelligTs : prognose?.datum,
|
||||||
|
)
|
||||||
|
const inspUeber = terminUeberschritten(
|
||||||
|
inspHatMeldung ? fahrzeug.inspektionFaelligTs : inspektionsprognose?.datum,
|
||||||
|
)
|
||||||
|
const huUeber = huGueltig && terminUeberschritten(huFaellig)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tile>
|
<Tile>
|
||||||
|
|
||||||
<div className="dm-termingruppe">
|
<div className={`dm-termingruppe${oelUeber ? " dm-termingruppe--ueberschritten" : ""}`}>
|
||||||
<div className="dm-tgkopf">
|
<div className="dm-tgkopf">
|
||||||
<SymbolOelwechsel groesse={26} />
|
<SymbolOelwechsel groesse={26} />
|
||||||
<span>Ölwechsel</span>
|
<span>Ölwechsel</span>
|
||||||
|
{oelUeber && <UeberschrittenMarke />}
|
||||||
</div>
|
</div>
|
||||||
{oelHatEintrag || oelHatMeldung ? (
|
{oelHatEintrag || oelHatMeldung ? (
|
||||||
<>
|
<>
|
||||||
@@ -147,10 +154,11 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="dm-termingruppe">
|
<div className={`dm-termingruppe${inspUeber ? " dm-termingruppe--ueberschritten" : ""}`}>
|
||||||
<div className="dm-tgkopf">
|
<div className="dm-tgkopf">
|
||||||
<SymbolInspektion groesse={20} />
|
<SymbolInspektion groesse={20} />
|
||||||
<span>Inspektion</span>
|
<span>Inspektion</span>
|
||||||
|
{inspUeber && <UeberschrittenMarke />}
|
||||||
</div>
|
</div>
|
||||||
{inspHatEintrag || inspHatMeldung ? (
|
{inspHatEintrag || inspHatMeldung ? (
|
||||||
<>
|
<>
|
||||||
@@ -163,9 +171,10 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="dm-termingruppe">
|
<div className={`dm-termingruppe${huUeber ? " dm-termingruppe--ueberschritten" : ""}`}>
|
||||||
<div className="dm-tgkopf">
|
<div className="dm-tgkopf">
|
||||||
<span>Hauptuntersuchung</span>
|
<span>Hauptuntersuchung</span>
|
||||||
|
{huUeber && <UeberschrittenMarke />}
|
||||||
</div>
|
</div>
|
||||||
{huGueltig ? (
|
{huGueltig ? (
|
||||||
<div className="dm-tgwert">{datum(huFaellig)}</div>
|
<div className="dm-tgwert">{datum(huFaellig)}</div>
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import { Fig, ProgressBar, StatusRow, Tile } from "@audi-dash/ui"
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
import { useDaten } from "../daten/DatenKontext"
|
import { useDaten } from "../daten/DatenKontext"
|
||||||
import { naechsterService } from "../daten/service"
|
import { naechsterService, terminUeberschritten } from "../daten/service"
|
||||||
import { datum, de, deOderStrich, eur, streckeText } from "../format"
|
import { datum, de, deOderStrich, eur, streckeText } from "../format"
|
||||||
import type { SeitenName } from "../navigation"
|
import type { SeitenName } from "../navigation"
|
||||||
import { BILDPLAETZE, bildInfo } from "./bilder"
|
import { BILDPLAETZE, bildInfo } from "./bilder"
|
||||||
import { Blattzeile, Leerzustand } from "./bausteine"
|
import { Blattzeile, Leerzustand, UeberschrittenMarke } from "./bausteine"
|
||||||
// Dieselbe Ortsangabe wie in der Fahrtenliste - eine Quelle, damit "Zuletzt"
|
// Dieselbe Ortsangabe wie in der Fahrtenliste - eine Quelle, damit "Zuletzt"
|
||||||
// und die Liste nicht auseinanderlaufen koennen.
|
// und die Liste nicht auseinanderlaufen koennen.
|
||||||
import { strecke } from "./Fahrten"
|
import { strecke } from "./Fahrten"
|
||||||
@@ -81,6 +81,9 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
|
|||||||
|
|
||||||
const buch = (fahrzeug.service["buch"] ?? []) as ServicebuchEintrag[]
|
const buch = (fahrzeug.service["buch"] ?? []) as ServicebuchEintrag[]
|
||||||
const service = naechsterService(fahrzeug, buch, fahrten)
|
const service = naechsterService(fahrzeug, buch, fahrten)
|
||||||
|
// Auf der Uebersicht wechselt zusaetzlich die Kennzahl die Farbe - sie ist
|
||||||
|
// hier die Aussage, nicht nur ihr Rahmen (Ansage des Eigentuemers).
|
||||||
|
const serviceUeber = !!service && terminUeberschritten(service.datum)
|
||||||
|
|
||||||
const letzteFahrt = fahrten
|
const letzteFahrt = fahrten
|
||||||
.slice()
|
.slice()
|
||||||
@@ -238,7 +241,11 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
|
|||||||
"differentiate controls from content" gilt auch für die
|
"differentiate controls from content" gilt auch für die
|
||||||
Abwesenheit von Inhalt. */}
|
Abwesenheit von Inhalt. */}
|
||||||
{service ? (
|
{service ? (
|
||||||
<button type="button" className="dm-serviceblock" onClick={() => geheZu("service")}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dm-serviceblock${serviceUeber ? " dm-serviceblock--ueberschritten" : ""}`}
|
||||||
|
onClick={() => geheZu("service")}
|
||||||
|
>
|
||||||
<span className="ads-eyebrow">Nächster Service:</span>
|
<span className="ads-eyebrow">Nächster Service:</span>
|
||||||
{/* Gezeichneter Pfeil wie .go im Panel, kein Schriftzeichen: das
|
{/* Gezeichneter Pfeil wie .go im Panel, kein Schriftzeichen: das
|
||||||
">" war ein 15px-Glyph der Textschrift und damit erkennbar
|
">" war ein 15px-Glyph der Textschrift und damit erkennbar
|
||||||
@@ -265,6 +272,11 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
|
|||||||
fig
|
fig
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
{serviceUeber && (
|
||||||
|
<div className="dm-servicemarke">
|
||||||
|
<UeberschrittenMarke />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Im Panel ist auch diese Nebenzeile ein `.label` (13px, --fg3),
|
{/* Im Panel ist auch diese Nebenzeile ein `.label` (13px, --fg3),
|
||||||
nicht die kleinere Fußnote (11,5px/300), die hier stand. */}
|
nicht die kleinere Fußnote (11,5px/300), die hier stand. */}
|
||||||
<span className="ads-eyebrow" style={{ marginTop: "8px" }}>
|
<span className="ads-eyebrow" style={{ marginTop: "8px" }}>
|
||||||
|
|||||||
@@ -537,3 +537,10 @@ export function Markenlogo({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** „überschritten" neben einem fälligen Servicetermin. Eigener Baustein, damit
|
||||||
|
* die drei Anzeigestellen (Service, Mein Audi, Übersicht) nicht auseinander
|
||||||
|
* laufen. Gegenstück zu UEBERSCHRITTEN_MARKE im Panel. */
|
||||||
|
export function UeberschrittenMarke() {
|
||||||
|
return <span className="dm-ueberschritten-marke">überschritten</span>
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,13 +12,19 @@ import { Shell } from "../Shell"
|
|||||||
import type { SeitenName } from "../navigation"
|
import type { SeitenName } from "../navigation"
|
||||||
import { TITEL } from "../navigation"
|
import { TITEL } from "../navigation"
|
||||||
import { SeiteFuer } from "./register"
|
import { SeiteFuer } from "./register"
|
||||||
import { beispielApi } from "../tests/beispieldaten"
|
import { beispielApi, beispielProfil } from "../tests/beispieldaten"
|
||||||
|
import type { Profil } from "../api/types"
|
||||||
import { bildschirmbreiteSetzen } from "../tests/aufbau"
|
import { bildschirmbreiteSetzen } from "../tests/aufbau"
|
||||||
|
|
||||||
const ALLE_SEITEN = Object.keys(TITEL) as SeitenName[]
|
const ALLE_SEITEN = Object.keys(TITEL) as SeitenName[]
|
||||||
|
|
||||||
function zeige(name: SeitenName, id?: string, geheZu: (name: SeitenName, id?: string) => void = () => {}) {
|
function zeige(
|
||||||
const api = beispielApi()
|
name: SeitenName,
|
||||||
|
id?: string,
|
||||||
|
geheZu: (name: SeitenName, id?: string) => void = () => {},
|
||||||
|
profil?: Profil,
|
||||||
|
) {
|
||||||
|
const api = profil ? beispielApi(profil) : beispielApi()
|
||||||
return render(
|
return render(
|
||||||
<div className="ads-root" data-theme="nacht">
|
<div className="ads-root" data-theme="nacht">
|
||||||
<DatenAnbieter api={api}>
|
<DatenAnbieter api={api}>
|
||||||
@@ -194,3 +200,38 @@ describe("Inhalte kommen wirklich aus den Daten", () => {
|
|||||||
expect(artAuswahl.value).toBe("arbeitsweg")
|
expect(artAuswahl.value).toBe("arbeitsweg")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
/* Überschrittene Servicetermine werden markiert - an allen drei Stellen, die
|
||||||
|
der Eigentümer genannt hat (07.09.2026). Die Beispieldaten tragen
|
||||||
|
erstzulassung 2023-04-01, die Hauptuntersuchung liegt damit auf 04/2026 und
|
||||||
|
ist überschritten; getestet wird also am vorhandenen Bestand, nicht an einer
|
||||||
|
eigens gebauten Fixtur. */
|
||||||
|
describe("Überschrittene Servicetermine", () => {
|
||||||
|
/* Der Regelbestand hat keinen ueberschrittenen Termin (Wartungsplan
|
||||||
|
2026-02-10 + 24 Monate). Hier deshalb ein Profil, dessen letzter Eintrag
|
||||||
|
lange zurueckliegt - die Hauptuntersuchung faellt damit in die
|
||||||
|
Vergangenheit. */
|
||||||
|
const ueberfaellig: Profil = {
|
||||||
|
...beispielProfil,
|
||||||
|
fahrzeug: { ...beispielProfil.fahrzeug, erstzulassung: "2019-04-01" },
|
||||||
|
service: { ...beispielProfil.service, buch: [] },
|
||||||
|
}
|
||||||
|
|
||||||
|
it("markiert die Gruppe auf der Service-Seite", async () => {
|
||||||
|
const { container } = zeige("service", undefined, () => {}, ueberfaellig)
|
||||||
|
await waitFor(() => expect(screen.getAllByText("überschritten").length).toBeGreaterThan(0))
|
||||||
|
expect(container.querySelector(".dm-termingruppe--ueberschritten")).not.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("markiert die Zeile in Mein Audi", async () => {
|
||||||
|
zeige("audi", undefined, () => {}, ueberfaellig)
|
||||||
|
await waitFor(() => expect(screen.getAllByText("überschritten").length).toBeGreaterThan(0))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("markiert den Serviceblock auf der Übersicht und färbt die Kennzahl", async () => {
|
||||||
|
const { container } = zeige("home", undefined, () => {}, ueberfaellig)
|
||||||
|
await waitFor(() => expect(screen.getAllByText("überschritten").length).toBeGreaterThan(0))
|
||||||
|
expect(container.querySelector(".dm-serviceblock--ueberschritten")).not.toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
+2409
-2365
File diff suppressed because it is too large
Load Diff
@@ -162,7 +162,10 @@ export const beispielTankvorgaenge: Tankvorgang[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
/** Datenschicht-Ersatz für Tests: liefert Beispieldaten, schreibt ins Leere. */
|
/** Datenschicht-Ersatz für Tests: liefert Beispieldaten, schreibt ins Leere. */
|
||||||
export function beispielApi(): DataMetricApi {
|
/** Die Beispieldaten mit einem abweichenden Profil - fuer Tests, die einen
|
||||||
|
* Zustand brauchen, den der Regelbestand nicht hergibt (etwa einen bereits
|
||||||
|
* ueberschrittenen Servicetermin). Ohne Argument bleibt alles wie bisher. */
|
||||||
|
export function beispielApi(profil: Profil = beispielProfil): DataMetricApi {
|
||||||
const horcher = { verbindung: [] as ((z: string) => void)[] }
|
const horcher = { verbindung: [] as ((z: string) => void)[] }
|
||||||
return {
|
return {
|
||||||
rest: {
|
rest: {
|
||||||
@@ -196,7 +199,7 @@ export function beispielApi(): DataMetricApi {
|
|||||||
},
|
},
|
||||||
anzahl: 0,
|
anzahl: 0,
|
||||||
},
|
},
|
||||||
profilLesen: async () => beispielProfil,
|
profilLesen: async () => profil,
|
||||||
fahrtenLesen: async () => beispielFahrten,
|
fahrtenLesen: async () => beispielFahrten,
|
||||||
tankvorgaengeLesen: async () => beispielTankvorgaenge,
|
tankvorgaengeLesen: async () => beispielTankvorgaenge,
|
||||||
fahrzeugstatusLesen: async () => beispielStatus,
|
fahrzeugstatusLesen: async () => beispielStatus,
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":"2026.9.6.5","sha256":"d7373ee38b4b6e97f5d20e5225bca6981d21f60fc34d7b2658b8571650b52726","bytes":378720,"gebaut":"2026-09-06T21:29:15Z"}
|
{"version":"2026.9.6.6","sha256":"c9e3d8f25998f38b70e38441a8c0e09d76bbe340845575e9ae769fe12c2fc45e","bytes":379100,"gebaut":"2026-09-07T11:37:53Z"}
|
||||||
Binary file not shown.
@@ -741,6 +741,52 @@ function datumAusText(wert) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ist dieser Servicetermin bereits faellig oder ueberschritten? Der
|
||||||
|
Faelligkeitstag selbst zaehlt mit ("bereits faellig bzw. ueberschritten",
|
||||||
|
Ansage des Eigentuemers 07.09.2026).
|
||||||
|
|
||||||
|
Entschieden wird am DATUM, nicht an den Restkilometern - und das ist keine
|
||||||
|
Bequemlichkeit. Die Kilometerseite traegt je nach Quelle ein anderes
|
||||||
|
Vorzeichen: die Sensoren des Fahrzeugs melden negativ, solange noch Strecke
|
||||||
|
bleibt (am 07.09.2026 gemessen: oil_change_distance -28600 bei Faelligkeit
|
||||||
|
2028), und servicePrognose() klemmt ihre Restkilometer ohnehin mit
|
||||||
|
Math.max(0, ...). Welches Vorzeichen ein ueberschrittener Fahrzeugwert
|
||||||
|
traegt, laesst sich an den vorliegenden Daten nicht beobachten - geraten wird
|
||||||
|
es nicht. Der km-Fall faellt ohnehin unter die Datumsregel: servicePrognose()
|
||||||
|
setzt datum auf heute, sobald restKm auf 0 laeuft.
|
||||||
|
|
||||||
|
Genommen wird das ERSTE lesbare Datum der uebergebenen Kandidaten - also das,
|
||||||
|
das an dieser Stelle auch angezeigt wird. So kann die Markierung nicht von
|
||||||
|
dem abweichen, was daneben steht. */
|
||||||
|
/* Ein voller ISO-Zeitstempel ("2027-10-21T00:00:00+00:00"), wie ihn die
|
||||||
|
Sensoren des Fahrzeugs liefern. Bewusst eng gefasst statt einem allgemeinen
|
||||||
|
new Date(): sonst deutete der Rueckfall auch Zeichenketten, die
|
||||||
|
datumAusText() gerade abgelehnt hat. */
|
||||||
|
const DATUM_ZEITSTEMPEL = /^\d{4}-\d{2}-\d{2}T/;
|
||||||
|
function zeitstempelAlsDatum(wert) {
|
||||||
|
if (typeof wert !== "string" || !DATUM_ZEITSTEMPEL.test(wert)) return null;
|
||||||
|
const d = new Date(wert);
|
||||||
|
return gueltigesDatum(d) ? { zeit: d, genau: "tag" } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function terminUeberschritten(...kandidaten) {
|
||||||
|
for (const kandidat of kandidaten) {
|
||||||
|
if (kandidat == null || kandidat === "") continue;
|
||||||
|
// datumAusText() ist verankert und nimmt bewusst keine Zeitstempel - die
|
||||||
|
// Fahrzeugmeldung (fm.ts) ist aber genau das, und sie ist bei Oelwechsel
|
||||||
|
// und Inspektion die Hauptquelle. Ohne diesen Zweig gaelte sie stillschweigend
|
||||||
|
// nie als ueberschritten; am 07.09.2026 vom Test gefangen, nicht im Betrieb.
|
||||||
|
const d = datumAusText(kandidat) || zeitstempelAlsDatum(kandidat);
|
||||||
|
if (!d) continue;
|
||||||
|
const z = d.zeit, h = new Date();
|
||||||
|
return new Date(z.getFullYear(), z.getMonth(), z.getDate()).getTime()
|
||||||
|
<= new Date(h.getFullYear(), h.getMonth(), h.getDate()).getTime();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UEBERSCHRITTEN_MARKE = '<span class="ueberschritten-marke">überschritten</span>';
|
||||||
|
|
||||||
/* Zeigt eine monatsgenaue Angabe als Monat statt als erfundenen Ersten. */
|
/* Zeigt eine monatsgenaue Angabe als Monat statt als erfundenen Ersten. */
|
||||||
const demonat = (d) => (gueltigesDatum(d)
|
const demonat = (d) => (gueltigesDatum(d)
|
||||||
? d.toLocaleDateString("de-DE", { month: "long", year: "numeric" })
|
? d.toLocaleDateString("de-DE", { month: "long", year: "numeric" })
|
||||||
@@ -2434,11 +2480,15 @@ function vHome() {
|
|||||||
const figMarkup = artIcon
|
const figMarkup = artIcon
|
||||||
? `<div class="fig-zeile">${ciSVG(artIcon, 31)}<div class="fig" style="font-size:${zahlGroesse}px">${hauptwert}</div></div>`
|
? `<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>`;
|
: `<div class="fig" style="font-size:${zahlGroesse}px;margin-top:8px">${hauptwert}</div>`;
|
||||||
|
// Auf der Uebersicht wechselt zusaetzlich die Kennzahl selbst die Farbe -
|
||||||
|
// sie ist hier die Aussage, nicht nur ihr Rahmen (Ansage des Eigentuemers).
|
||||||
|
const ueber = terminUeberschritten(datum, t.datum);
|
||||||
return `<div class="tile" style="margin-bottom:22px">
|
return `<div class="tile" style="margin-bottom:22px">
|
||||||
<button class="serviceblock" data-go="service:x">
|
<button class="serviceblock${ueber ? " ueberschritten" : ""}" data-go="service:x">
|
||||||
<span class="label">Nächster Service:</span>
|
<span class="label">Nächster Service:</span>
|
||||||
<svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
|
<svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
|
||||||
${figMarkup}
|
${figMarkup}
|
||||||
|
${ueber ? `<div class="servicemarke">${UEBERSCHRITTEN_MARKE}</div>` : ""}
|
||||||
<span class="label" style="margin-top:8px">${nebenzeile}</span>
|
<span class="label" style="margin-top:8px">${nebenzeile}</span>
|
||||||
</button>
|
</button>
|
||||||
${kmZeile}
|
${kmZeile}
|
||||||
@@ -2610,15 +2660,17 @@ function vAudi() {
|
|||||||
const eigen = prognoseZeile(t.art);
|
const eigen = prognoseZeile(t.art);
|
||||||
if (fm.ts == null && fm.km == null) {
|
if (fm.ts == null && fm.km == null) {
|
||||||
const x = terminText(t);
|
const x = terminText(t);
|
||||||
|
const ueber = terminUeberschritten(t.ziel, t.datum);
|
||||||
return `<div class="row" ${letzte}>
|
return `<div class="row" ${letzte}>
|
||||||
<dt>${t.art}${t.art === "Inspektion" ? eigen : ""}</dt>
|
<dt>${t.art}${t.art === "Inspektion" ? eigen : ""}</dt>
|
||||||
<dd>${x.haupt}${x.neben ? `<small style="text-align:left">${x.neben}</small>` : ""}</dd></div>`;
|
<dd>${x.haupt}${ueber ? UEBERSCHRITTEN_MARKE : ""}${x.neben ? `<small style="text-align:left">${x.neben}</small>` : ""}</dd></div>`;
|
||||||
}
|
}
|
||||||
const haupt = fm.ts ? dedat(new Date(fm.ts)) : "";
|
const haupt = fm.ts ? dedat(new Date(fm.ts)) : "";
|
||||||
const neben = fm.km != null ? "in " + de(fm.km) + " km" : "";
|
const neben = fm.km != null ? "in " + de(fm.km) + " km" : "";
|
||||||
|
const ueber = terminUeberschritten(fm.ts, t.ziel, t.datum);
|
||||||
return `<div class="row" ${letzte}>
|
return `<div class="row" ${letzte}>
|
||||||
<dt>${t.art}${eigen}</dt>
|
<dt>${t.art}${eigen}</dt>
|
||||||
<dd>${haupt}${neben ? `<span style="display:block">${neben}</span>` : ""}</dd></div>`;
|
<dd>${haupt}${ueber ? UEBERSCHRITTEN_MARKE : ""}${neben ? `<span style="display:block">${neben}</span>` : ""}</dd></div>`;
|
||||||
}).join("")}
|
}).join("")}
|
||||||
</dl></button>
|
</dl></button>
|
||||||
<button class="tile tilebtn" data-go="vers:x">
|
<button class="tile tilebtn" data-go="vers:x">
|
||||||
@@ -3024,10 +3076,13 @@ function vService() {
|
|||||||
Wartungsplan" gezeigt, waehrend die Uebersicht denselben Termin
|
Wartungsplan" gezeigt, waehrend die Uebersicht denselben Termin
|
||||||
auswies - gemeldet vom Eigentuemer, zwei Bildschirme, zwei
|
auswies - gemeldet vom Eigentuemer, zwei Bildschirme, zwei
|
||||||
Antworten. */
|
Antworten. */
|
||||||
if (!iv && !hatMeldung && t.datum) return `<div class="termingruppe">
|
if (!iv && !hatMeldung && t.datum) {
|
||||||
<div class="tg-kopf">${kopf}</div>
|
const ueber = terminUeberschritten(t.datum);
|
||||||
|
return `<div class="termingruppe${ueber ? " ueberschritten" : ""}">
|
||||||
|
<div class="tg-kopf">${kopf}${ueber ? UEBERSCHRITTEN_MARKE : ""}</div>
|
||||||
<div class="tg-wert">${datumText(t.datum, t.genau)}</div>
|
<div class="tg-wert">${datumText(t.datum, t.genau)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
}
|
||||||
if (!iv && !hatMeldung) return `<div class="termingruppe">
|
if (!iv && !hatMeldung) return `<div class="termingruppe">
|
||||||
<div class="tg-kopf">${kopf}</div>
|
<div class="tg-kopf">${kopf}</div>
|
||||||
<div class="tg-wert leer">kein Eintrag im Wartungsplan</div>
|
<div class="tg-wert leer">kein Eintrag im Wartungsplan</div>
|
||||||
@@ -3074,9 +3129,13 @@ function vService() {
|
|||||||
// als Herstellervorgabe; nur der App-eigene Eigen-Intervall-Fallback
|
// als Herstellervorgabe; nur der App-eigene Eigen-Intervall-Fallback
|
||||||
// (anzeige.eigen/eigenOel bei modus "eigen") ist es nicht.
|
// (anzeige.eigen/eigenOel bei modus "eigen") ist es nicht.
|
||||||
const herstellervorgabe = hatMeldung || (iv && iv.hersteller);
|
const herstellervorgabe = hatMeldung || (iv && iv.hersteller);
|
||||||
return `<div class="termingruppe">
|
// Dasselbe Datum, das gleich als `wert` erscheint - siehe
|
||||||
|
// terminUeberschritten().
|
||||||
|
const ueber = terminUeberschritten(
|
||||||
|
hatMeldung ? fm.ts : (eigenOel ? eigenOel.datum : (anzeige ? anzeige.datum : null)));
|
||||||
|
return `<div class="termingruppe${ueber ? " ueberschritten" : ""}">
|
||||||
<div class="tg-kopf">
|
<div class="tg-kopf">
|
||||||
${kopf}
|
${kopf}${ueber ? UEBERSCHRITTEN_MARKE : ""}
|
||||||
</div>
|
</div>
|
||||||
${herstellervorgabe ? `<div class="tg-vorgabe">Herstellervorgabe</div>` : ""}
|
${herstellervorgabe ? `<div class="tg-vorgabe">Herstellervorgabe</div>` : ""}
|
||||||
<div class="tg-wert">${wert}</div>
|
<div class="tg-wert">${wert}</div>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
--line:rgba(255,255,255,.13);
|
--line:rgba(255,255,255,.13);
|
||||||
--line-strong:rgba(255,255,255,.24);
|
--line-strong:rgba(255,255,255,.24);
|
||||||
--fg:#FFFFFF; --fg2:#BFC4CC; --fg3:#8E8E93;
|
--fg:#FFFFFF; --fg2:#BFC4CC; --fg3:#8E8E93;
|
||||||
--ok:#30D158; --warn:#FFD60A; --bad:#FF453A; --bad-flaeche:#D70015;
|
--ok:#30D158; --warn:#FFD60A; --bad:#FF453A; --bad-flaeche:#D70015; --warn-text:#FFD60A;
|
||||||
/* Ohne diese Angabe rendert der Browser native Steuerelemente (Dropdown-
|
/* Ohne diese Angabe rendert der Browser native Steuerelemente (Dropdown-
|
||||||
Aufklapp-Liste bei <select>, Scrollbars) in seiner Standard-Hellansicht,
|
Aufklapp-Liste bei <select>, Scrollbars) in seiner Standard-Hellansicht,
|
||||||
egal wie dunkel die App selbst ist - das liess z. B. "Modell" beim
|
egal wie dunkel die App selbst ist - das liess z. B. "Modell" beim
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
auf 4,75:1 bzw. 5,30:1, ohne den ruhigen Gesamteindruck zu aendern.
|
auf 4,75:1 bzw. 5,30:1, ohne den ruhigen Gesamteindruck zu aendern.
|
||||||
Der Nachtwert bleibt #8E8E93 - dort sind es ohnehin 5,86:1. */
|
Der Nachtwert bleibt #8E8E93 - dort sind es ohnehin 5,86:1. */
|
||||||
--fg:#000000; --fg2:#3C3C43; --fg3:#6B6B70;
|
--fg:#000000; --fg2:#3C3C43; --fg3:#6B6B70;
|
||||||
--ok:#34C759; --warn:#FF9F0A; --bad:#D70015; --bad-flaeche:#D70015;
|
--ok:#34C759; --warn:#FF9F0A; --bad:#D70015; --bad-flaeche:#D70015; --warn-text:#A15800;
|
||||||
--ios-sep:rgba(60,60,67,.16);
|
--ios-sep:rgba(60,60,67,.16);
|
||||||
--ios-fill:rgba(120,120,128,.16);
|
--ios-fill:rgba(120,120,128,.16);
|
||||||
--tile-deckend:#FFFFFF;
|
--tile-deckend:#FFFFFF;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
--fg3: #8a94a3;
|
--fg3: #8a94a3;
|
||||||
--ok: #15da15;
|
--ok: #15da15;
|
||||||
--warn: #ffaa00;
|
--warn: #ffaa00;
|
||||||
|
--warn-text: #ffaa00;
|
||||||
--bad: #fd2c4e;
|
--bad: #fd2c4e;
|
||||||
--shade: rgba(255,255,255,.05);
|
--shade: rgba(255,255,255,.05);
|
||||||
--font-stack: "Audi Type", Helvetica, Arial, sans-serif;
|
--font-stack: "Audi Type", Helvetica, Arial, sans-serif;
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
--fg3: #666666;
|
--fg3: #666666;
|
||||||
--ok: #0DA20D;
|
--ok: #0DA20D;
|
||||||
--warn: #ffaa00;
|
--warn: #ffaa00;
|
||||||
|
--warn-text: #ffaa00;
|
||||||
--bad: #eb0d3f;
|
--bad: #eb0d3f;
|
||||||
--shade: rgba(0,0,0,.04);
|
--shade: rgba(0,0,0,.04);
|
||||||
}
|
}
|
||||||
@@ -653,6 +655,32 @@ button.leaf:active { background: var(--tile-2); }
|
|||||||
.tg-wert.leer { font-family: var(--font-stack); font-weight: 400; font-size: 15px; color: var(--fg3); margin-top: 8px; }
|
.tg-wert.leer { font-family: var(--font-stack); font-weight: 400; font-size: 15px; color: var(--fg3); margin-top: 8px; }
|
||||||
.tg-prognose { font-size: 12.5px; color: var(--fg2); margin-top: 6px; }
|
.tg-prognose { font-size: 12.5px; color: var(--fg2); margin-top: 6px; }
|
||||||
|
|
||||||
|
/* Ueberschrittene Servicetermine: dezent hinterlegt statt umgefaerbt, damit die
|
||||||
|
Zeile lesbar bleibt und der Hinweis trotzdem auffaellt (Ansage des
|
||||||
|
Eigentuemers 07.09.2026). --warn ist die Flaeche, --warn-text die Schrift -
|
||||||
|
dieselbe Rollenteilung wie bei --bad/--bad-flaeche, und aus demselben Grund:
|
||||||
|
#FF9F0A als Text auf Weiss sind 2.06:1. Auf dem hier hinterlegten Grund
|
||||||
|
(#FFF3E2 tags, #33311E nachts) misst --warn-text 4.90:1 bzw. 9.30:1. */
|
||||||
|
.termingruppe.ueberschritten {
|
||||||
|
background: color-mix(in srgb, var(--warn) 12%, transparent);
|
||||||
|
border-radius: var(--r-tile); padding-left: 12px; padding-right: 12px;
|
||||||
|
margin-left: -12px; margin-right: -12px;
|
||||||
|
}
|
||||||
|
/* Die Trennlinie zur Vorgaengerzeile wuerde ueber die abgerundete Flaeche
|
||||||
|
laufen - die uebernimmt hier die Abgrenzung. */
|
||||||
|
.termingruppe.ueberschritten, .termingruppe.ueberschritten + .termingruppe { border-top: 0; }
|
||||||
|
.ueberschritten-marke {
|
||||||
|
font-size: 10.5px; color: var(--warn-text);
|
||||||
|
border: 1px solid var(--warn-text); border-radius: var(--r-pill);
|
||||||
|
padding: 3px 8px; white-space: nowrap; flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
/* Auf der Uebersicht wechselt zusaetzlich die Schrift der Kennzahl - dort ist
|
||||||
|
sie die Aussage, nicht nur ihr Rahmen. */
|
||||||
|
.serviceblock.ueberschritten .fig, .serviceblock.ueberschritten .fig .unit { color: var(--warn-text); }
|
||||||
|
.servicemarke { display: inline-flex; margin-top: 8px; }
|
||||||
|
/* In der Zeilenliste von "Mein Audi" reicht die Marke neben dem Wert. */
|
||||||
|
.row .ueberschritten-marke { margin-left: 8px; vertical-align: middle; }
|
||||||
|
|
||||||
.badge { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
.badge { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||||
.badge img { height: 18px; width: auto; display: none; }
|
.badge img { height: 18px; width: auto; display: none; }
|
||||||
:host([data-theme=nacht]) .badge img.negative { display: block; }
|
:host([data-theme=nacht]) .badge img.negative { display: block; }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "audi_dashboard",
|
"domain": "audi_dashboard",
|
||||||
"name": "Audi Dashboard",
|
"name": "Audi Dashboard",
|
||||||
"version": "2026.9.6.5",
|
"version": "2026.9.6.6",
|
||||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
--fg3: #8E8E93;
|
--fg3: #8E8E93;
|
||||||
--ok: #30D158;
|
--ok: #30D158;
|
||||||
--warn: #FFD60A;
|
--warn: #FFD60A;
|
||||||
|
--warn-text: #FFD60A;
|
||||||
--bad: #FF453A;
|
--bad: #FF453A;
|
||||||
/* Rot als FLAECHE, nicht als Schrift.
|
/* Rot als FLAECHE, nicht als Schrift.
|
||||||
|
|
||||||
@@ -84,6 +85,7 @@
|
|||||||
--fg3: #6B6B70;
|
--fg3: #6B6B70;
|
||||||
--ok: #34C759;
|
--ok: #34C759;
|
||||||
--warn: #FF9F0A;
|
--warn: #FF9F0A;
|
||||||
|
--warn-text: #A15800;
|
||||||
/* #FF3B30 (Apples systemRed) auf #F2F2F7 ergab 3,18 - unter den 4,5 fuer
|
/* #FF3B30 (Apples systemRed) auf #F2F2F7 ergab 3,18 - unter den 4,5 fuer
|
||||||
16px-Schrift. #D70015 ist Apples systemRed fuer erhoehten Kontrast und
|
16px-Schrift. #D70015 ist Apples systemRed fuer erhoehten Kontrast und
|
||||||
kommt auf 4,83 (auf Weiss 5,38). Gemessen am 04.09.2026 am
|
kommt auf 4,83 (auf Weiss 5,38). Gemessen am 04.09.2026 am
|
||||||
|
|||||||
Reference in New Issue
Block a user