From b0ec8b4982da26845613b100a78ec7cbc6fe0d2e Mon Sep 17 00:00:00 2001 From: Tobi G Date: Fri, 4 Sep 2026 18:11:07 +0200 Subject: [PATCH] Pruefdurchgang ueber alle 26 Bildschirme - fuenf Fehler behoben, HIG nachgezogen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gemessen statt geschaut: eine Pruefroutine lief auf jedem Bildschirm und pruefte Ueberlauf, Tippziele, abgeschnittenen Text, Bilder ohne Quelle und WCAG-Kontrast fuer jeden Textknoten. Dazu die Live-App mit echten Daten, die Musterseite fuer Leerzustaende und das Panel zum Abgleich. 1. Deutsche Datumsangaben wurden falsch gelesen. new Date("01.03.2025") liest amerikanisch: Tage bis 12 wurden still vertauscht, ab Tag 13 fiel der Wert ganz weg. Die Erstzulassung stand auf zwei Bildschirmen unterschiedlich da. alsZeitpunkt() erkennt jetzt ISO, TT.MM.JJJJ und MM/JJJJ, weist unmoegliche Daten ab und fuehrt die Genauigkeit mit - aus "08/2026" wird "August 2026", kein erfundener Erster. 2. Die Hauptuntersuchung war in beiden Oberflaechen unerreichbar. Das Panel leitete sie aus der Erstzulassung ab, aber nur bei MM/JJJJ, und benutzte den eingetragenen Wert gar nicht. Jetzt beide: eingetragene Faelligkeit vor Servicebuch vor Erstzulassung + 24 Monate. 3. Die Tankstellenmarke stand zweimal da, wenn der Name kein Komma hat ("Shell München Ost"). ohneMarkeVorn() nimmt sie vorn ab. 4. Bilder wurden geholt, obwohl beide wussten, dass es sie nicht gibt - fuenf 404 fuer dieselbe Datei in einem Ladevorgang. Jetzt gar keine Adresse bei Stand 0; nachgemessen 4 Anfragen, alle 200. 5. Ein Bildelement ohne Quelle zeigte das kaputte Bildsymbol, und onerror greift dort nicht: ohne src gibt es keinen Ladeversuch. Dazu: leere Klammern beim Steuersatz, leere graue Kachel bei leerer Leistungsgruppe, Umbruch auf dem Einrichtungsbildschirm, drei Leerdarstellungen in einer Kachel, fehlende Grundschrift. Apple HIG: Kontrast war an einer Stelle 3,18 statt 4,5, weisse Schrift auf der Loeschflaeche 3,41/3,55. Rot als Schrift und Rot als Flaeche sind jetzt getrennte Token (--bad / --bad-flaeche), beide Apples systemRed fuer erhoehten Kontrast. Tippziele: unsichtbares 44x44-Overlay, wo die sichtbare Groesse Teil des Bildes ist, echte Mindesthoehe, wo das Element Flaeche ist - Formulare werden dadurch sichtbar hoeher. Neu auf Wunsch: Wischen in der Bildergalerie (beide Richtungen) und die HIG-konforme Loeschgeste - ein voller Wisch loescht ohne zweiten Tipp, ab 55 % der Zeilenbreite, mit wachsender roter Flaeche und erhaltener Rueckfrage. Dabei fiel auf, dass der Zugwert aus dem React-Zustand gelesen wurde und ein schneller Wisch dadurch verlorenging; er liegt jetzt in einer Referenz. Paritaet: alle 26 Routen und Titel decken sich, Fahrzeugstatus zeilengleich, zwei ungeplante Abweichungen (Datumsformatierung, Herkunft der Hauptuntersuchung) geschlossen. tsc sauber, 271 Tests, vite build sauber, Design-System gebaut, Panel als Modul geparst, audi_ha_test auf 2026.9.4.23 ohne Traceback. Live nachgemessen: Datum auf beiden Bildschirmen gleich, keine 404 mehr, Zurueck-Pfeil sichtbar 34x34 und treffbar 44x44, Galerie wischt in beide Richtungen, voller Wisch loest die Rueckfrage aus, keine Kontrast-Unterschreitung mehr. --- AGENTS.md | 203 ++++++++++++++ companion-app/src/daten/tankstelle.test.ts | 30 +++ companion-app/src/daten/tankstelle.ts | 35 ++- companion-app/src/format.test.ts | 79 +++++- companion-app/src/format.ts | 128 ++++++++- companion-app/src/screens/Einstellungen.tsx | 13 +- companion-app/src/screens/MeinAudi.tsx | 55 +++- companion-app/src/screens/TankDetail.tsx | 11 +- companion-app/src/screens/Tanken.tsx | 9 +- companion-app/src/screens/Versicherung.tsx | 12 +- companion-app/src/screens/bausteine.tsx | 99 ++++++- companion-app/src/screens/bilder.ts | 16 +- companion-app/src/stile/grundlage.css | 5 + companion-app/src/stile/rahmen.css | 28 ++ companion-app/src/stile/screens.css | 70 ++++- .../audi_dashboard/frontend/app/bundle.json | 2 +- .../audi_dashboard/frontend/app/bundle.zip | Bin 323041 -> 324157 bytes .../frontend/audi-dashboard-app.js | 247 ++++++++++++++++-- .../frontend/audi-dashboard-ios.css | 31 ++- .../frontend/audi-dashboard.css | 7 +- .../audi_dashboard/manifest.json | 2 +- .../components/ActionButton/ActionButton.css | 2 +- .../src/components/IconButton/IconButton.css | 25 ++ .../src/components/SwipeRow/SwipeRow.css | 11 +- .../src/components/SwipeRow/SwipeRow.tsx | 91 ++++++- design-system/src/tokens/tokens.css | 28 +- 26 files changed, 1125 insertions(+), 114 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c2e1b2b..4c81edc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10914,3 +10914,206 @@ Parser-Suite jetzt **17 Tests** (von 11): die neue Klasse und dass ein Aufgeben den Beleg nicht beschaedigt. Alle zwoelf Belege liefern weiterhin `Shell`. `audi_ha_test` auf 2026.9.4.22, 0 Tracebacks, das Nachtragen lief erwartungsgemaess **nicht** erneut. + +## CP. Der grosse Pruefdurchgang - und was er gefunden hat (2026.9.4.23) + +Auftrag des Eigentuemers: alle Funktionen, alle Bildschirme, optisch und +funktional, Audi CI und Apple HIG, danach die Paritaet. Danach: alles beheben. + +### Wie geprueft wurde + +Nicht nach Augenmass. Eine Pruefroutine (`/local/pruef.js` im Testcontainer) +lief auf **jedem der 26 Bildschirme** und mass seitlichen Ueberlauf, +Tippzielgroessen, abgeschnittenen Text, Schriftgroessen unter 11 px, Bilder +ohne Quelle und **WCAG-Kontrast fuer jeden einzelnen Textknoten**. Dazu die +Live-App mit den echten Daten (der Eigentuemer hat den Zugang selbst +eingetragen), die Musterseite fuer Leer- und Fehlerzustaende, und das Panel +zum Abgleich. + +**Merkposten fuer kuenftige Pruefungen:** ohne Token zeigt die App nur den +Einrichtungsbildschirm. Statt wieder auf die Musterseite auszuweichen und die +Live-Pruefung als "nicht moeglich" zu vermerken - **danach fragen**. Der +Eigentuemer hat sie sofort gegeben. + +### Befund 1: deutsche Datumsangaben wurden falsch gelesen + +`new Date("01.03.2025")` liest amerikanisch, also Monat vor Tag: + +| gespeichert | angezeigt | +|---|---| +| `01.03.2025` (1. Maerz) | **`03.01.2025`** | +| `12.11.2024` | **`11.12.2024`** | +| `25.03.2025` | **"-"** - der Wert verschwand | +| `08/2026` (Hauptuntersuchung) | **"-"** | + +Tage bis 12 wurden **still vertauscht**, ab Tag 13 fiel der Wert ganz weg. +Sichtbar an der Erstzulassung: "Mein Audi" zeigte den Rohwert richtig, +"Identitaet und Technik" formatierte ihn und drehte ihn dabei um - **derselbe +Wert, zwei Ergebnisse auf zwei Bildschirmen**. + +`alsZeitpunkt()` in `format.ts` erkennt jetzt genau die drei Formen, die in +diesem Projekt vorkommen: ISO vom Backend, `TT.MM.JJJJ` aus den +Eingabefeldern, `MM/JJJJ` fuer die Hauptuntersuchung. Alles andere bleibt +`null` - **lieber kein Datum als ein erfundenes**. Unmoegliche Daten werden +abgewiesen statt still verschoben (`new Date(2025,1,31)` waere der 3. Maerz). + +**Monatsgenau ist eine eigene Genauigkeit.** `08/2026` nennt keinen Tag; +daraus "01.08.2026" zu machen waere eine Erfindung. Das Ergebnis traegt seine +Genauigkeit mit sich, und angezeigt wird "August 2026". + +### Befund 2: die Hauptuntersuchung war in beiden Oberflaechen unerreichbar + +`08/2026` stand im Profil, gezeigt wurde "-". In der App aus Befund 1. Im +Panel aus einem anderen Grund, der genauso stillschweigend war: es leitete die +Faelligkeit aus der **Erstzulassung** ab - aber nur, wenn die ein Schraegstrich +enthielt. Bei `01.03.2025` (eine voellig richtige Eingabe) blieb der Termin +leer, und den ausdruecklich eingetragenen Wert benutzte es gar nicht. + +Beide Oberflaechen nehmen jetzt dieselbe Reihenfolge: **eingetragene +Faelligkeit vor Servicebuch vor Erstzulassung + 24 Monate**. Das Panel hat dazu +`datumAusText()` bekommen - wortgleich mit `alsZeitpunkt()` in der App. + +### Befund 3: die Tankstellenmarke stand zweimal da + +Mein eigener Fehler vom selben Tag (Abschnitt CN). Ein von Hand eingetragener +Name ohne Komma rutschte komplett als "Ort" durch: + + "Shell München Ost" + "Musterstraße 5" + -> "Shell, Musterstr., Shell München Ost" + +Die Regel uebersprang nur Teile, die **fuer sich** eine Marke sind. +`ohneMarkeVorn()` nimmt die Marke jetzt vorn ab. Steht sie NICHT am Anfang +("Autohof Shell Nord"), bleibt der Teil unangetastet - dort ist nicht sicher, +was Marke und was Ortsname ist. + +### Befund 4: Bilder wurden geholt, obwohl beide wussten, dass es sie nicht gibt + +Der Code sagt es selbst - "0 heisst: dieses Foto gibt es nicht" - und baute +die Adresse trotzdem. Gemessen: **fuenf Anfragen fuer `front-schraeg.webp` in +einem einzigen Ladevorgang**, dazu heckansicht, cockpit, sitze und +seitenansicht-winter, jede eine 404, bei jedem Rendern erneut. + +Sichtbar war nichts (der Platzhalter griff ueber `onerror`), die Konsole aber +voll. Jetzt liefern `bildUrl()` bzw. `bildAdresse()` gar keine Adresse mehr, +wenn der Stand 0 ist. **Nach der Korrektur gemessen: 4 Anfragen, alle 200, +kein einziges `v=0`.** + +### Befund 5: ein Bildelement ohne Quelle zeigt das kaputte Bildsymbol + +Und der Rettungsanker greift dabei nicht: **ohne `src` findet kein Ladeversuch +statt, also feuert `onerror` nie**. Genau dann, wenn er gebraucht wuerde. Auf +der Musterseite als kaputtes Symbol samt Alternativtext "Shell" gesehen. + +Neu `Markenlogo` (App) - rendert bei fehlender Adresse **nichts**. Im Panel +faellt das `src`-Attribut weg und die Box startet gleich als `bild-fehlt`; das +Bildelement bleibt trotzdem stehen, weil die Galerie es weiterschaltet. + +### Kleinere Maengel, alle behoben + +* `Versicherungsteuer ()` - leere Klammern ohne Steuersatz (beide). +* Eine Leistungsgruppe ohne Namen UND ohne Posten wurde zur **leeren grauen + Kachel** (beide). +* "Server-Adresse" brach am Bindestrich um - auf dem allerersten Bildschirm. + Seitlicher Rand von 32 auf 20 px. +* Drei Leerdarstellungen in EINER Kachel (Strich, "noch offen", nichts). Die + Erstzulassung wird jetzt formatiert und zeigt einen Strich, wenn nichts da + ist. +* Das Wurzelelement hatte keine Schriftfamilie - Rueckfall auf Times New + Roman. Sichtbar war heute nichts, die Flanke ist trotzdem zu. + +### Apple HIG + +**Kontrast**: auf 25 von 26 Bildschirmen einwandfrei. Eine Unterschreitung - +"Tankvorgang loeschen", `#FF3B30` auf `#F2F2F7` = **3,18** statt 4,5. + +Dabei kam ein zweites, groesseres heraus: **weisse Schrift auf der roten +Loeschflaeche** ergibt 3,41 (Nacht) und 3,55 (Tag) - beide zu wenig. + +Geloest ueber eine **Trennung der Rollen**: Rot als Schrift und Rot als Flaeche +brauchen Gegensaetzliches. Rote Schrift will Abstand zum HELLEN Grund, eine +rote Flaeche will Abstand zur WEISSEN Schrift darauf. Hell ist `--bad` jetzt +`#D70015` (4,83), und `--bad-flaeche` ist in beiden Modi `#D70015` (weiss +darauf: 5,38). Beides Apples eigenes systemRed fuer erhoehten Kontrast - die +HIG-Palette bleibt. + +**Tippziele**: durchgehend unter 44x44 pt - der Zurueck-Pfeil mit 34x34, und +**ohne jede Vergroesserung**, beim meistbenutzten Element der App. + +Zwei Wege, je nach Element: + +* **Unsichtbares Overlay**, wo die sichtbare Groesse Teil des Bildes ist + (Zurueck-Pfeil, Symbolknoepfe, Griff des Standort-Blatts). Nachgemessen mit + `elementFromPoint`: **sichtbar 34x34, treffbar 44x44**. +* **Echte Mindesthoehe** 44 px, wo das Element ohnehin Flaeche ist + (Eingabefelder, Auswahlfelder, Pillen, Segmente). + +Der zweite Weg **veraendert das Bild sichtbar**: Formulare werden hoeher. Das +ist Absicht und dem Eigentuemer so gemeldet. + +### Wischen in der Bildergalerie (Wunsch des Eigentuemers) + +Links/rechts wischen schaltet durch, in beide Richtungen. Zeigerereignisse +statt Beruehrungsereignisse - dieselbe Verdrahtung traegt Finger, Stift und +Maus, genau wie das Standort-Blatt es schon macht. Ueberwiegt die senkrechte +Bewegung, war es ein Scrollen. Ein Wisch schluckt den nachlaufenden Klick, +sonst schaltete die Galerie zweimal weiter. + +### Loeschgeste HIG-konform (Wunsch des Eigentuemers) + +Ein **voller** Wisch loescht jetzt sofort, ohne zweiten Tipp. Schwelle: 55 % +der Zeilenbreite (mindestens 120, hoechstens 220 px) - eine halbe Zeile ist +beim seitlichen Scrollen leicht versehentlich erreicht, eine klare Mehrheit +nicht. Die rote Flaeche waechst mit dem Zug und wird beim Ueberschreiten +heller: **Farbe heisst entschieden**. Die Rueckfrage bleibt - geloescht werden +Fahrten, Belege und Messwerte, die nicht wiederkommen. + +**Dabei einen echten Fehler gefunden:** die Abschlussbehandlung las den +Zugwert aus dem React-Zustand, und der hinkt eine Darstellung hinterher. +Treffen die letzte Bewegung und das Loslassen in derselben Aufgabe ein - genau +das tut ein schneller Wisch -, war die Geste verloren. Nachgestellt: **ein +voller Wisch tat gar nichts.** Der Zug liegt jetzt zusaetzlich in einer +Referenz, und die entscheidet. + +### Paritaet + +Alle 26 Routen und Titel decken sich; der Fahrzeugstatus ist **zeilengleich**; +`live` bleibt der einzige app-eigene Bildschirm. + +**Zwei ungeplante Abweichungen gefunden, beide geschlossen:** die +Datumsformatierung (App formatierte, Panel gab roh aus - deshalb war das Panel +richtig und die App falsch) und die Herkunft der Hauptuntersuchung. Beide +zeigen jetzt dasselbe. + +Die Tankstellen-Funktionen weiterhin **byteweise gleich** ueber elf Faelle - +die Panel-Funktionen aus dem Buendel geschnitten und in Node gegen dieselben +Eingaben laufen lassen. + +### Verifiziert + +`tsc --noEmit` sauber, **271 Tests** (von 258: 9 fuer die Datumsformen, 4 fuer +die Marke am Wortanfang), `vite build` sauber, Design-System gebaut, Panel als +**Modul** geparst, `audi_ha_test` auf `2026.9.4.23`, **0 Tracebacks**. + +**Live nachgemessen, nicht nur kompiliert:** + +* Erstzulassung `01.03.2025` auf BEIDEN Bildschirmen, Hauptuntersuchung + `August 2026` - in App **und** Panel. +* Bildanfragen: 4, alle 200, kein `v=0`. +* Zurueck-Pfeil: sichtbar 34x34, treffbar 44x44. Eingabefelder 44 px hoch. +* Galerie: Wisch links vor, Wisch rechts zurueck, kurzer Tipp vor. +* Loeschzeile: kurzer Wisch klappt auf, voller Wisch (-260 bei 343 px Breite) + loest die Rueckfrage "Fahrt vom 10.08.2026 loeschen?" aus. +* Kontrastpruefung ueber alle Bildschirme: **keine Unterschreitung mehr**. + +### Nicht geprueft - und warum + +Live-Fahrt (braucht eine laufende Fahrt), QR-Scanner (Kamera in der Vorschau +gesperrt), Zwischenablage/Teilen/Kalender (nativ, brauchen das Geraet), das +echte iPhone (Notch, Dynamic Type), und Loeschvorgaenge wurden nicht +ausgeloest - das sind die Daten des Eigentuemers. + +**Eine Korrektur an mir selbst:** ich hatte zwischendurch gemeldet, das +Standort-Blatt sei fehlerhaft, weil "Route" und "Teilen" hinter der Tab-Leiste +liegen. Das war falsch - die 96-px-Vorschau ist Absicht, aufgezogen sitzt das +Blatt sauber 30 px ueber der Leiste. Mein Klick-Test war der Fehler, nicht die +App: das Blatt hoert auf Zeigerereignisse, nicht auf `click`. diff --git a/companion-app/src/daten/tankstelle.test.ts b/companion-app/src/daten/tankstelle.test.ts index c304861..6970809 100644 --- a/companion-app/src/daten/tankstelle.test.ts +++ b/companion-app/src/daten/tankstelle.test.ts @@ -11,6 +11,7 @@ import { afterEach, describe, expect, it, vi } from "vitest" import { kartendienstUrl, markeErkennen, + ohneMarkeVorn, tankstelleKandidaten, tankstelleSuchtext, tankstelleKurz, @@ -306,3 +307,32 @@ describe("gespeicherte Marke (station_brand)", () => { ) }) }) + +describe("Marke am Anfang eines Namensteils", () => { + /* Am 04.09.2026 an der Musterseite aufgefallen: ein von Hand eingetragener + Name ohne Komma rutschte komplett als "Ort" durch. */ + + it("laesst die Marke nicht zweimal erscheinen", () => { + expect(tankstelleKurz("Shell München Ost", "Musterstraße 5")).toBe( + "Shell, Musterstr., München Ost", + ) + }) + + it("nimmt der Zeile die Marke ab, laesst den Ort stehen", () => { + expect(ohneMarkeVorn("Shell München Ost")).toBe("München Ost") + expect(ohneMarkeVorn("Shell, SÖLDEN")).toBe("SÖLDEN") + expect(ohneMarkeVorn("Shell")).toBe("") + }) + + it("laesst einen Teil in Ruhe, in dem die Marke nicht vorn steht", () => { + // Dort ist nicht sicher, was Marke und was Ortsname ist. + expect(ohneMarkeVorn("Autohof Shell Nord")).toBe("Autohof Shell Nord") + expect(ohneMarkeVorn("Musterstraße 5")).toBe("Musterstraße 5") + }) + + it("achtet auf die Wortgrenze", () => { + // "Star" darf nicht in "Starnberg" treffen. + expect(ohneMarkeVorn("Starnberg")).toBe("Starnberg") + expect(ohneMarkeVorn("Sprintstraße 5")).toBe("Sprintstraße 5") + }) +}) diff --git a/companion-app/src/daten/tankstelle.ts b/companion-app/src/daten/tankstelle.ts index b2e1573..c183473 100644 --- a/companion-app/src/daten/tankstelle.ts +++ b/companion-app/src/daten/tankstelle.ts @@ -69,6 +69,30 @@ const MARKEN = [ * geraten wird nur, wo es fehlt: bei Vorgaengen ohne Beleg, bei von Hand * eingetragenen Tankstellen und solange das Nachtragen nicht gelaufen ist. */ +/** + * Nimmt einem Namensteil die fuehrende Kopfmarke ab. + * + * WARUM NICHT NUR VERGLEICHEN: bis zum 04.09.2026 wurde ein Namensteil nur + * dann uebersprungen, wenn er FUER SICH eine Marke ist ("Shell"). Ein von + * Hand eingetragener Name wie "Shell München Ost" hat aber kein Komma - er + * rutschte komplett durch und landete als ORT in der Zeile: + * + * "Shell München Ost" + "Musterstraße 5" + * -> "Shell, Musterstr., Shell München Ost" + * + * Jetzt faellt die Marke vorn ab und der Rest bleibt der Ort. Steht die Marke + * NICHT am Anfang ("Autohof Shell Nord"), bleibt der Teil unangetastet - + * dort ist nicht sicher, was Marke und was Ortsname ist. + */ +export function ohneMarkeVorn(teil: string): string { + const marke = markeErkennen(teil) + if (!marke) return teil + // Kein Maskieren noetig: `marke` stammt immer aus MARKEN, und dort steht + // kein einziges Sonderzeichen der regulaeren Ausdruecke. + const muster = new RegExp(`^${marke}\\b[\\s,]*`, "i") + return muster.test(teil) ? teil.replace(muster, "").trim() : teil +} + export function markeErkennen(name: string | null | undefined): string | null { const roh = (name ?? "").trim() if (!roh) return null @@ -162,13 +186,10 @@ export function tankstelleTeile( const quellen: string[] = [] for (const stueck of (adresse ?? "").split(",")) quellen.push(stueck) for (const stueck of (name ?? "").split(",")) { - const t = stueck.trim() - // Ein Teil, der fuer sich genommen eine Kopfmarke IST, taugt nie als Ort. - // Frueher stand hier nur der Vergleich mit der gewaehlten Marke; seit die - // gespeicherte Angabe vorgeht, koennen beide auseinanderfallen, und dann - // rutschte die Marke aus dem Namen in den Ort. - if (t && markeErkennen(t)?.toLowerCase() === t.toLowerCase()) continue - quellen.push(t) + // Die Marke faellt vorn ab - sie steht schon fest und taugt nie als Ort. + // Bleibt nichts uebrig, war der Teil nur die Marke selbst. + const t = ohneMarkeVorn(stueck.trim()) + if (t) quellen.push(t) } for (const stueck of quellen) { diff --git a/companion-app/src/format.test.ts b/companion-app/src/format.test.ts index 271f14a..23aa23b 100644 --- a/companion-app/src/format.test.ts +++ b/companion-app/src/format.test.ts @@ -2,7 +2,20 @@ import { describe, expect, it } from "vitest" import { alsCsv } from "./screens/csv" import { kalenderDatei } from "./screens/kalender" -import { dauer, de, deOderStrich, eur, isoTag, schnittpreis, summe } from "./format" +import { + datum, + datumLang, + datumZeit, + dauer, + de, + deOderStrich, + eur, + isoTag, + monatJahr, + schnittpreis, + summe, + uhrzeit, +} from "./format" describe("Zahlen im deutschen Format", () => { it("setzt Punkt als Tausender- und Komma als Dezimaltrennzeichen", () => { @@ -101,3 +114,67 @@ describe("Kalenderdatei", () => { ) }) }) + +describe("Datumsangaben aus dem Bestand", () => { + /* Alle Faelle am 04.09.2026 an der laufenden Instanz gemessen - vorher + vertauschte `new Date()` Tag und Monat, oder verwarf den Wert ganz. */ + + it("liest deutsche Tagesdaten richtig herum", () => { + // Der gemeldete Fall: die Erstzulassung stand als 03.01.2025 da. + expect(datum("01.03.2025")).toBe("01.03.2025") + expect(datum("12.11.2024")).toBe("12.11.2024") + expect(datum("1.3.2025")).toBe("01.03.2025") + }) + + it("verliert Tage ab dem 13. nicht mehr", () => { + // Die waren vorher ungueltig und verschwanden als "–". + expect(datum("25.03.2025")).toBe("25.03.2025") + expect(datum("31.12.2023")).toBe("31.12.2023") + }) + + it("zeigt monatsgenaue Angaben als Monat, nicht als erfundenen Tag", () => { + // Die Hauptuntersuchung steht als "MM/JJJJ" im Profil (so verlangt es das + // Einrichtungsfeld). Vorher: "–". Ein "01.08.2026" waere eine Erfindung. + expect(datum("08/2026")).toBe("August 2026") + expect(datum("08.2026")).toBe("August 2026") + expect(datum("2026-08")).toBe("August 2026") + expect(datumLang("08/2026")).toBe("August 2026") + }) + + it("gibt einer monatsgenauen Angabe keine Uhrzeit", () => { + // 00:00 waere erfunden. + expect(uhrzeit("08/2026")).toBe("–") + expect(datumZeit("08/2026")).toBe("August 2026") + }) + + it("laesst ISO vom Backend unveraendert richtig", () => { + expect(datum("2026-08-14")).toBe("14.08.2026") + expect(datum("2024-09-25T15:47:00")).toBe("25.09.2024") + expect(datumZeit("2024-09-25T15:47:00")).toBe("25.09.2024, 15:47 Uhr") + }) + + it("weist unmoegliche Daten ab, statt still ein anderes zu zeigen", () => { + // new Date(2025,1,31) waere in JavaScript der 3. Maerz. + expect(datum("31.02.2025")).toBe("–") + expect(datum("32.01.2025")).toBe("–") + expect(datum("00.01.2025")).toBe("–") + expect(datum("01.13.2025")).toBe("–") + }) + + it("bleibt bei Unbekanntem beim Strich", () => { + expect(datum(null)).toBe("–") + expect(datum("")).toBe("–") + expect(datum("demnaechst")).toBe("–") + expect(datum("01/03/2025")).toBe("–") + }) + + it("nimmt Date und Zahl weiterhin an", () => { + expect(datum(new Date(2025, 2, 1))).toBe("01.03.2025") + expect(datum(new Date(2025, 2, 1).getTime())).toBe("01.03.2025") + }) + + it("monatJahr kann beide Genauigkeiten", () => { + expect(monatJahr("2026-08-14")).toBe("August 2026") + expect(monatJahr("08/2026")).toBe("August 2026") + }) +}) diff --git a/companion-app/src/format.ts b/companion-app/src/format.ts index 465b1a8..20b445b 100644 --- a/companion-app/src/format.ts +++ b/companion-app/src/format.ts @@ -88,35 +88,137 @@ const UHRZEIT = new Intl.DateTimeFormat("de-DE", { hour: "2-digit", minute: "2-d const MONAT_JAHR = new Intl.DateTimeFormat("de-DE", { month: "long", year: "numeric" }) -function alsDatum(wert: string | number | Date | null | undefined): Date | null { +/** + * Datumsangaben aus dem Bestand kommen in DREI Formen - deshalb reicht + * `new Date(wert)` hier nicht. + * + * WAS AM 04.09.2026 GEMESSEN WURDE + * -------------------------------- + * `new Date()` liest einen nicht-ISO-Text nach amerikanischer Lesart, also + * Monat vor Tag. Fuer deutsche Eingaben heisst das: + * + * "01.03.2025" (1. Maerz) -> 3. Januar 2025 - Tag und Monat vertauscht + * "12.11.2024" -> 11. Dezember - dito + * "25.03.2025" -> ungueltig - Wert verschwand als "-" + * "08/2026" (Hauptunters.) -> ungueltig - dito + * + * Tage bis 12 wurden also STILL vertauscht, ab Tag 13 fiel der Wert ganz weg. + * Sichtbar war es an der Erstzulassung: "Mein Audi" zeigte den Rohwert + * richtig, "Identitaet und Technik" formatierte ihn und drehte ihn dabei um - + * derselbe Wert, zwei Ergebnisse. Und die Hauptuntersuchung stand als "-" da, + * obwohl "08/2026" gespeichert war. + * + * WARUM EIGENE MUSTER STATT EINER BIBLIOTHEK + * ------------------------------------------ + * Es sind genau drei Formen, alle aus diesem Projekt: ISO vom Backend, + * "TT.MM.JJJJ" aus den Eingabefeldern, "MM/JJJJ" fuer die Hauptuntersuchung + * (so verlangt es das Einrichtungsfeld, siehe Panel `einErstzulassung`). + * Erkannt wird nur, was hier wirklich vorkommt - alles andere bleibt `null` + * und damit "-". Lieber kein Datum als ein erfundenes. + * + * MONATSGENAU IST EINE EIGENE GENAUIGKEIT + * --------------------------------------- + * "08/2026" nennt keinen Tag. Daraus den 1. August zu machen und + * "01.08.2026" anzuzeigen waere eine Erfindung - deshalb traegt das Ergebnis + * seine Genauigkeit mit sich, und `datum()` zeigt dann "August 2026". + */ +type Genauigkeit = "tag" | "monat" +interface Zeitpunkt { + zeit: Date + genau: Genauigkeit +} + +/* ISO vom Backend, mit oder ohne Uhrzeit - das kann `new Date` selbst, und + zwar nach Norm statt nach Gutduenken. */ +const ISO = /^\d{4}-\d{2}-\d{2}([T ]|$)/ +/* "1.3.2025" wie "01.03.2025". */ +const DE_TAG = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/ +/* "08/2026" und "08.2026" - Monat und Jahr, ohne Tag. */ +const DE_MONAT = /^(\d{1,2})[./](\d{4})$/ +/* "2026-08" - dieselbe Genauigkeit, in ISO-Schreibweise. */ +const ISO_MONAT = /^(\d{4})-(\d{1,2})$/ + +/** + * Baut das Datum und weist die Rollover-Falle ab: `new Date(2025, 1, 31)` ist + * in JavaScript der 3. Maerz, nicht der 31. Februar. Ein unmoegliches Datum + * soll "-" ergeben, nicht klammheimlich ein anderes. + */ +function bauen(jahr: number, monat: number, tag: number): Date | null { + if (monat < 1 || monat > 12 || tag < 1 || tag > 31) return null + const d = new Date(jahr, monat - 1, tag) + if (d.getFullYear() !== jahr || d.getMonth() !== monat - 1 || d.getDate() !== tag) return null + return d +} + +function alsZeitpunkt(wert: string | number | Date | null | undefined): Zeitpunkt | null { if (wert === null || wert === undefined || wert === "") return null - const datum = wert instanceof Date ? wert : new Date(wert) - return Number.isNaN(datum.getTime()) ? null : datum + if (wert instanceof Date) return Number.isNaN(wert.getTime()) ? null : { zeit: wert, genau: "tag" } + if (typeof wert === "number") { + const d = new Date(wert) + return Number.isNaN(d.getTime()) ? null : { zeit: d, genau: "tag" } + } + + const text = wert.trim() + if (!text) return null + + if (ISO.test(text)) { + const d = new Date(text) + return Number.isNaN(d.getTime()) ? null : { zeit: d, genau: "tag" } + } + + const tag = DE_TAG.exec(text) + if (tag) { + const d = bauen(Number(tag[3]), Number(tag[2]), Number(tag[1])) + return d ? { zeit: d, genau: "tag" } : null + } + + const monat = DE_MONAT.exec(text) + if (monat) { + const d = bauen(Number(monat[2]), Number(monat[1]), 1) + return d ? { zeit: d, genau: "monat" } : null + } + + const isoMonat = ISO_MONAT.exec(text) + if (isoMonat) { + const d = bauen(Number(isoMonat[1]), Number(isoMonat[2]), 1) + return d ? { zeit: d, genau: "monat" } : null + } + + return null +} + +function alsDatum(wert: string | number | Date | null | undefined): Date | null { + return alsZeitpunkt(wert)?.zeit ?? null } export function datum(wert: string | number | Date | null | undefined): string { - const d = alsDatum(wert) - return d ? DATUM_KURZ.format(d) : "–" + const z = alsZeitpunkt(wert) + if (!z) return "–" + return z.genau === "monat" ? MONAT_JAHR.format(z.zeit) : DATUM_KURZ.format(z.zeit) } export function datumLang(wert: string | number | Date | null | undefined): string { - const d = alsDatum(wert) - return d ? DATUM_LANG.format(d) : "–" + const z = alsZeitpunkt(wert) + if (!z) return "–" + return z.genau === "monat" ? MONAT_JAHR.format(z.zeit) : DATUM_LANG.format(z.zeit) } export function uhrzeit(wert: string | number | Date | null | undefined): string { - const d = alsDatum(wert) - return d ? UHRZEIT.format(d) : "–" + const z = alsZeitpunkt(wert) + // Eine monatsgenaue Angabe hat keine Uhrzeit - 00:00 waere erfunden. + return z && z.genau === "tag" ? UHRZEIT.format(z.zeit) : "–" } export function datumZeit(wert: string | number | Date | null | undefined): string { - const d = alsDatum(wert) - return d ? `${DATUM_KURZ.format(d)}, ${UHRZEIT.format(d)} Uhr` : "–" + const z = alsZeitpunkt(wert) + if (!z) return "–" + if (z.genau === "monat") return MONAT_JAHR.format(z.zeit) + return `${DATUM_KURZ.format(z.zeit)}, ${UHRZEIT.format(z.zeit)} Uhr` } export function monatJahr(wert: string | number | Date | null | undefined): string { - const d = alsDatum(wert) - return d ? MONAT_JAHR.format(d) : "–" + const z = alsZeitpunkt(wert) + return z ? MONAT_JAHR.format(z.zeit) : "–" } /** Dauer in Sekunden als „2 h 14 min" bzw. „14 min". */ diff --git a/companion-app/src/screens/Einstellungen.tsx b/companion-app/src/screens/Einstellungen.tsx index caa41ba..89a2f23 100644 --- a/companion-app/src/screens/Einstellungen.tsx +++ b/companion-app/src/screens/Einstellungen.tsx @@ -17,9 +17,9 @@ import { OtaFehler, buendelAnwenden, buendelPasst, otaMoeglich } from "../daten/ import type { Einstellungen as EinstellungenWerte } from "../daten/profilAdapter" import { datum, datumZeit, de, isoTag, standAlter } from "../format" import { useTheme } from "../theme" -import { BildMitMenue, Wertzeile, Werteliste } from "./bausteine" +import { BildMitMenue, Markenlogo, Werteliste, Wertzeile } from "./bausteine" import { bestaetigen } from "./bestaetigung" -import { BILDPLAETZE, SEITE_WINTER, markenlogoUrl } from "./bilder" +import { BILDPLAETZE, SEITE_WINTER } from "./bilder" import { DatensatzPopup } from "./DatensatzPopup" import { HistorienImport } from "./HistorienImport" import type { ServicebuchEintrag } from "./Service" @@ -530,14 +530,7 @@ export function Einstellungen({ - {werte.markenname} { - e.currentTarget.style.display = "none" - }} - /> + {/* Das Panel nennt den Schalter "SmartDeal aktiv" - fest, nicht aus dem Markennamen gebaut. */} SmartDeal aktiv diff --git a/companion-app/src/screens/MeinAudi.tsx b/companion-app/src/screens/MeinAudi.tsx index acf1a8d..9d723fc 100644 --- a/companion-app/src/screens/MeinAudi.tsx +++ b/companion-app/src/screens/MeinAudi.tsx @@ -13,7 +13,7 @@ * Werteliste + Chevron statt einer Zeile Fließtext. */ -import { useState } from "react" +import { useRef, useState, type PointerEvent as ReactPointerEvent } from "react" import { Tile } from "@audi-dash/ui" @@ -58,6 +58,36 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) = return wert }) } + /* Wischen in der Bildergalerie (Wunsch des Eigentuemers, 04.09.2026). + + Zeigerereignisse statt Beruehrungsereignisse: dieselbe Verdrahtung + traegt Finger, Stift und Maus, und genau so macht es das Standort- + Blatt schon (Standort.tsx). Ein Wisch ab 40 px zaehlt als Wisch, alles + darunter bleibt ein Tipp - dieselbe Schwelle wie dort. + + Ueberwiegt die senkrechte Bewegung, war es ein Scrollen und kein + Blaettern - dann passiert nichts. + + `gewischt` schluckt den anschliessenden Klick: ein Wisch loest auf dem + Knopf auch ein click aus, und ohne diese Sperre schaltete die Galerie + zweimal weiter. Wortgleich mit galerieWischenVerdrahten() im Panel. */ + const zug = useRef<{ x0: number; y0: number } | null>(null) + const gewischt = useRef(false) + const weiter = (richtung: number) => + setzeBildIndex((i) => (i + richtung + GALERIE.length) % GALERIE.length) + const zugStart = (e: ReactPointerEvent) => { + zug.current = { x0: e.clientX, y0: e.clientY } + } + const zugEnde = (e: ReactPointerEvent) => { + const z = zug.current + zug.current = null + if (!z) return + const dx = e.clientX - z.x0 + if (Math.abs(dx) <= 40 || Math.abs(e.clientY - z.y0) > Math.abs(dx)) return + gewischt.current = true + weiter(dx < 0 ? 1 : -1) + } + if (!fahrzeug || !einstellungen) return null const aktiverSatz = @@ -128,8 +158,19 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) = @@ -167,9 +208,11 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) = Hinweis, dass es sie gibt, statt einer abgeschnittenen Motorzeile. */} - {/* Leerer Wert statt Strich, wie vAudi(): das Panel gibt hier - den Rohwert aus. */} - + {/* Formatiert und mit Strich, wenn nichts da ist - vorher stand + hier der Rohwert und bei leerem Feld gar nichts, waehrend + FIN daneben "–" und Ausfuehrung "noch offen" zeigte: + drei Leerdarstellungen in einer Kachel (04.09.2026). */} + diff --git a/companion-app/src/screens/TankDetail.tsx b/companion-app/src/screens/TankDetail.tsx index 3538867..efe3756 100644 --- a/companion-app/src/screens/TankDetail.tsx +++ b/companion-app/src/screens/TankDetail.tsx @@ -17,9 +17,8 @@ import { useDaten } from "../daten/DatenKontext" import { kartendienstUrl, tankstelleZeilen } from "../daten/tankstelle" import { datum, de, deOderStrich, eur, uhrzeit } from "../format" import type { SeitenName } from "../navigation" -import { KRAFTSTOFFSORTEN, Leerzustand, Wertzeile, Werteliste } from "./bausteine" +import { KRAFTSTOFFSORTEN, Leerzustand, Markenlogo, Werteliste, Wertzeile } from "./bausteine" import { LOESCH_HINWEIS, LOESCH_TEXT, bestaetigen } from "./bestaetigung" -import { markenlogoUrl } from "./bilder" import { BelegPopup } from "./BelegPopup" import { TankstellenKarte } from "./TankstellenKarte" @@ -116,13 +115,7 @@ export function TankDetail({ id, geheZu }: { id: string | undefined; geheZu: (na - { - e.currentTarget.style.display = "none" - }} - /> + SmartDeal-Ersparnis } diff --git a/companion-app/src/screens/Tanken.tsx b/companion-app/src/screens/Tanken.tsx index 9688fcb..4dc507c 100644 --- a/companion-app/src/screens/Tanken.tsx +++ b/companion-app/src/screens/Tanken.tsx @@ -21,9 +21,8 @@ import { geteiltenBelegSetzen, geteiltenBelegStand, } from "../daten/geteilterBeleg" -import { Blattzeile, KRAFTSTOFFSORTEN, Leerzustand } from "./bausteine" +import { Blattzeile, KRAFTSTOFFSORTEN, Leerzustand, Markenlogo } from "./bausteine" import { BelegPopup } from "./BelegPopup" -import { markenlogoUrl } from "./bilder" import { belegFormularwerte, type BelegEntwurf } from "../daten/belegentwurf" import { tankstelleKurz } from "../daten/tankstelle" import { LOESCH_HINWEIS } from "./bestaetigung" @@ -75,14 +74,10 @@ export function Tanken({ geheZu }: { geheZu: (name: SeitenName, id?: string) => {smartdealAktiv && ( - {einstellungen?.markenname { - e.currentTarget.style.display = "none" - }} /> −{eur(ersparnisJahr)} € diff --git a/companion-app/src/screens/Versicherung.tsx b/companion-app/src/screens/Versicherung.tsx index cba93e3..e6448f5 100644 --- a/companion-app/src/screens/Versicherung.tsx +++ b/companion-app/src/screens/Versicherung.tsx @@ -39,6 +39,7 @@ export function Versicherung({ geheZu }: { geheZu: (name: SeitenName, id?: strin const v = fahrzeug.versicherung const teile = (v["teile"] ?? []) as Beitragsposten[] const beitrag = (v["beitrag"] as number) ?? 0 + const steuersatz = ((v["steuersatz"] as string) ?? "").trim() const selbst = (v["selbst"] ?? []) as [string, string][] const service = (v["service"] as string) ?? "" const notfall = (v["notfall"] as string) ?? "" @@ -67,7 +68,9 @@ export function Versicherung({ geheZu }: { geheZu: (name: SeitenName, id?: strin {(v["zahlweise"] as string) ?? ""} · enthält {eur((v["steueranteil"] as number) ?? 0)} € - Versicherungsteuer ({(v["steuersatz"] as string) ?? ""}) + {/* Ohne Steuersatz KEINE leeren Klammern - bis zum 04.09.2026 + stand dort ein nacktes Klammerpaar. */} + Versicherungsteuer{steuersatz ? ` (${steuersatz})` : ""} ( @@ -411,7 +414,12 @@ export function Vertragsdetails({ geheZu }: { geheZu: (name: SeitenName, id?: st if (!fahrzeug) return null const v = fahrzeug.versicherung - const leistungen = (v["leistungen"] ?? []) as Leistungsgruppe[] + /* Gruppen ohne Namen UND ohne Posten fallen raus: sie ergaben eine leere + graue Kachel (am 04.09.2026 auf der Musterseite gesehen). Eine Kachel + ohne Inhalt sagt nichts und sieht aus wie ein Ladefehler. */ + const leistungen = ((v["leistungen"] ?? []) as Leistungsgruppe[]).filter( + (g) => (g.gruppe ?? "").trim() || (g.posten ?? []).length, + ) return ( <> diff --git a/companion-app/src/screens/bausteine.tsx b/companion-app/src/screens/bausteine.tsx index 6811a6b..bee8b62 100644 --- a/companion-app/src/screens/bausteine.tsx +++ b/companion-app/src/screens/bausteine.tsx @@ -3,7 +3,13 @@ * gebraucht wird, aber zu speziell für das Design-System ist. */ -import { useRef, useState, type ReactNode, type TouchEvent as ReactTouchEvent } from "react" +import { + useRef, + useState, + type CSSProperties, + type ReactNode, + type TouchEvent as ReactTouchEvent, +} from "react" import { ActionButton, ActionSheet, Tile } from "@audi-dash/ui" @@ -11,7 +17,7 @@ import type { SicherheitsPunkt } from "../api" import { useDaten } from "../daten/DatenKontext" import { SymbolZahnradVoll } from "../symbole" import { Bild } from "./Bild" -import { bildAlsBase64, bildVerkleinern, bildVersionErneuern } from "./bilder" +import { bildAlsBase64, bildVerkleinern, bildVersionErneuern, markenlogoUrl } from "./bilder" import { LOESCH_HINWEIS, LOESCH_TEXT, bestaetigen } from "./bestaetigung" /** Kraftstoffsorten zur Auswahl - dieselbe feste Liste wie `KRAFTSTOFFSORTEN` @@ -350,6 +356,16 @@ export function StatusKreis({ ok }: { ok: boolean | null }) { */ const WISCH_BREITE = 88 const WISCH_START = 10 +/* Ab diesem Anteil der Zeilenbreite loescht das Loslassen sofort, ohne + zweiten Tipp - so macht es iOS, und genau das fehlte hier: ein voller + Wisch blieb wie ein kurzer an der Raste stehen. + + 0,55 haelt die Geste absichtsvoll. Eine halbe Zeile ist beim seitlichen + Scrollen leicht versehentlich erreicht, eine klare Mehrheit nicht. + Wortgleich mit FULL_SWIPE_FRACTION in SwipeRow (Design-System). */ +const WISCH_VOLL_ANTEIL = 0.55 +const WISCH_VOLL_MAX = 220 +const WISCH_VOLL_MIN = 120 export function WischLoeschen({ beiLoeschen, @@ -362,11 +378,27 @@ export function WischLoeschen({ children: ReactNode }) { const [offen, setzeOffen] = useState(false) - const [zug, setzeZug] = useState(null) + /* Der Zug liegt zusaetzlich in einer Referenz, und die entscheidet. + Der Zustand hinkt eine Darstellung hinterher: treffen die letzte + Bewegung und das Loslassen in derselben Aufgabe ein - genau das tut + ein schneller Wisch -, laese `beiEnde` noch `null` und die Geste waere + verloren. Am 04.09.2026 gemessen: ein voller Wisch tat gar nichts. */ + const [zug, setzeZugZustand] = useState(null) + const zugRef = useRef(null) + const setzeZug = (wert: number | null) => { + zugRef.current = wert + setzeZugZustand(wert) + } const start = useRef<{ x: number; y: number } | null>(null) const entschieden = useRef(false) + const rahmen = useRef(null) const versatz = zug ?? (offen ? -WISCH_BREITE : 0) + const vollAb = () => { + const breite = rahmen.current?.offsetWidth ?? 0 + return -Math.min(WISCH_VOLL_MAX, Math.max(WISCH_VOLL_MIN, breite * WISCH_VOLL_ANTEIL)) + } + const scharf = versatz <= vollAb() const beiStart = (e: ReactTouchEvent) => { if (e.touches.length !== 1) return @@ -389,13 +421,24 @@ export function WischLoeschen({ entschieden.current = true } const basis = offen ? -WISCH_BREITE : 0 - setzeZug(Math.max(-WISCH_BREITE, Math.min(0, basis + dx))) + // Nicht mehr bei WISCH_BREITE abschneiden: ohne den weiteren Weg gibt + // es keinen vollen Wisch und keine Rueckmeldung, dass er loescht. + setzeZug(Math.min(0, basis + dx)) } const beiEnde = () => { - if (zug !== null) setzeOffen(zug < -WISCH_BREITE / 2) + const versatzJetzt = zugRef.current start.current = null setzeZug(null) + if (versatzJetzt === null) return + if (versatzJetzt <= vollAb()) { + // Ganz durchgezogen: jetzt handeln. `beiLoeschen` ist der Handler des + // Aufrufers und fragt dort nach, wo die Daten nicht wiederkommen. + setzeOffen(false) + beiLoeschen() + return + } + setzeOffen(versatzJetzt < -WISCH_BREITE / 2) } // Sichtbar erst, wenn wirklich gewischt wird. Ohne das liegt der rote @@ -406,10 +449,15 @@ export function WischLoeschen({ const zeigt = offen || zug !== null return ( -
+
${eur(v.beitrag)}
- ${esc(v.zahlweise)} · enthält ${eur(v.steueranteil)} € Versicherungsteuer (${esc(v.steuersatz)}) + ${esc(v.zahlweise)} · enthält ${eur(v.steueranteil)} € Versicherungsteuer${(v.steuersatz || "").trim() ? ` (${esc(v.steuersatz)})` : ""}
${v.teile.map((t, i) => `
${esc(t.bez)}${t.sf ? `
${esc(t.sf)} · Beitragssatz ${esc(t.satz)}
` : ""} @@ -3211,7 +3368,7 @@ function vVertragsdetails() {
Panne, Unfall, Diebstahl, Fahrerausfall
Nummern und Leistungen für den Ernstfall - ${v.leistungen.map((g) => `
${esc(g.gruppe)} + ${v.leistungen.filter((g) => (g.gruppe || "").trim() || (g.posten || []).length).map((g) => `
${esc(g.gruppe)}
${g.posten.map((x, i) => `
${esc(x)}
`).join("")} @@ -4479,6 +4636,20 @@ let belegPopup = null; let swipeAktivWrap = null, swipeStartX = 0, swipeStartY = 0, swipeGeste = null, swipeDX = 0; let swipeWarGezogen = false; const SWIPE_BREITE = 84; +/* Ab diesem Anteil der Zeilenbreite loescht das Loslassen sofort, ohne + zweiten Tipp - so macht es iOS, und genau das fehlte: ein voller Wisch + blieb wie ein kurzer an der Raste stehen (Wunsch des Eigentuemers, + 04.09.2026). 0,55 haelt die Geste absichtsvoll: eine halbe Zeile ist beim + seitlichen Scrollen leicht versehentlich erreicht, eine klare Mehrheit + nicht. Wortgleich mit WISCH_VOLL_* in bausteine.tsx und + FULL_SWIPE_FRACTION in SwipeRow (Design-System). */ +const SWIPE_VOLL_ANTEIL = 0.55; +const SWIPE_VOLL_MAX = 220; +const SWIPE_VOLL_MIN = 120; +function swipeVollAb(wrap) { + const breite = wrap ? wrap.offsetWidth : 0; + return -Math.min(SWIPE_VOLL_MAX, Math.max(SWIPE_VOLL_MIN, breite * SWIPE_VOLL_ANTEIL)); +} /* Ziehen zum Aktualisieren (nur Übersicht) - ptrY0 ist die Y-Position des Fingers/der Maus beim Start, null wenn gerade keine Geste läuft. Eigene @@ -5865,19 +6036,40 @@ function ereignisseVerdrahten() { // audi-dashboard.css) - sonst blitzt er dauerhaft an den Zeilenraendern durch. swipeAktivWrap.classList.add("wischt"); const basis = swipeAktivWrap.classList.contains("swiped") ? -SWIPE_BREITE : 0; - swipeDX = Math.max(-SWIPE_BREITE - 20, Math.min(0, basis + dx)); + // Nicht mehr bei SWIPE_BREITE abschneiden: ohne den weiteren Weg gibt es + // keinen vollen Wisch und keine Rueckmeldung, dass er loescht. + swipeDX = Math.min(0, basis + dx); + const flaeche = swipeAktivWrap.querySelector(".swipe-delete"); + if (flaeche) { + // Je weiter gezogen, desto mehr Rot - dieselbe Lesart wie auf iOS. + flaeche.style.width = Math.max(SWIPE_BREITE, -swipeDX) + "px"; + flaeche.classList.toggle("swipe-delete--scharf", swipeDX <= swipeVollAb(swipeAktivWrap)); + } const inhalt = swipeAktivWrap.querySelector(".swipe-content"); inhalt.style.transition = "none"; inhalt.style.transform = `translateX(${swipeDX}px)`; }, { passive: false }); function swipeBeenden() { if (!swipeAktivWrap) return; + const wrapJetzt = swipeAktivWrap; + const flaecheJetzt = wrapJetzt.querySelector(".swipe-delete"); + if (flaecheJetzt) { flaecheJetzt.style.width = ""; flaecheJetzt.classList.remove("swipe-delete--scharf"); } const inhalt = swipeAktivWrap.querySelector(".swipe-content"); inhalt.style.transition = ""; inhalt.style.transform = ""; swipeAktivWrap.classList.remove("wischt"); if (swipeGeste === "x") { - if (swipeDX <= -SWIPE_BREITE / 2) { schliesseAlleSwipes(); swipeAktivWrap.classList.add("swiped"); } - else { swipeAktivWrap.classList.remove("swiped"); } + if (swipeDX <= swipeVollAb(wrapJetzt)) { + // Ganz durchgezogen: jetzt handeln. Die Rueckfrage bleibt - geloescht + // werden hier Fahrten, Belege und Messwerte, die nicht wiederkommen. + schliesseAlleSwipes(); + const knopf = wrapJetzt.querySelector("[data-swipe-loeschen]"); + if (knopf) { + const [art, id] = knopf.dataset.swipeLoeschen.split(":"); + bestaetigen(LOESCH_TEXT[art], LOESCH_HINWEIS[art], "Löschen", () => { eintragLoeschen(art, id); schliesseAlleSwipes(); }); + } + } + else if (swipeDX <= -SWIPE_BREITE / 2) { schliesseAlleSwipes(); wrapJetzt.classList.add("swiped"); } + else { wrapJetzt.classList.remove("swiped"); } } swipeAktivWrap = null; swipeGeste = null; swipeDX = 0; // Der nachlaufende Klick derselben Geste feuert synchron noch in diesem @@ -6170,7 +6362,7 @@ function ereignisseVerdrahten() { smartdealPopupOffen = false; profilSpeichern(); render(); return; } - if (e.target.closest("[data-bildgalerie]")) { bildWeiter(); return; } + if (e.target.closest("[data-bildgalerie]")) { if (galerieGewischt) { galerieGewischt = false; return; } bildWeiter(); return; } if (e.target.closest("[data-sbneu]")) { CAR.service.buch.unshift({ datum: dedat(heute()), km: CAR.odoBekannt ? CAR.odo : 0, art: SERVICE_ARTEN[0], werkstatt: CAR.service.autohaus.name, kosten: null, arbeiten: "" }); entwurfIndex = 0; go("sbuch", "0"); return; @@ -7104,6 +7296,7 @@ class AudiDashboardPanel extends HTMLElement { ereignisseVerdrahten(); randwischenVerdrahten(); standortMenuVerdrahten(); + galerieWischenVerdrahten(); themeIcon(); setInterval(standAlterTicken, 1000); diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css b/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css index f5035d6..7a22aaf 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css @@ -25,7 +25,7 @@ --line:rgba(255,255,255,.13); --line-strong:rgba(255,255,255,.24); --fg:#FFFFFF; --fg2:#BFC4CC; --fg3:#8E8E93; - --ok:#30D158; --warn:#FFD60A; --bad:#FF453A; + --ok:#30D158; --warn:#FFD60A; --bad:#FF453A; --bad-flaeche:#D70015; /* Ohne diese Angabe rendert der Browser native Steuerelemente (Dropdown- Aufklapp-Liste bei