Tankstelle zweizeilig und antippbar, Belegkarte ohne toten Knopf (2026.9.4.5)
Sieben Meldungen des Eigentuemers, beide Codebasen. Karte im Einzelbeleg: "Position laedt nicht" und "der Button funktioniert nicht" waren dieselbe Stelle - ohne aufloesbaren Ort malte das Panel einen beliebigen Ausschnitt und liess den Zentrieren-Knopf fuer immer disabled. Beide zeigen jetzt die Auskunft statt einer Karte und unterscheiden dabei "steht nicht im Beleg" von "findet niemand". Dazu ein echter Paritaetsmangel: die App schlug nur station_address nach, das Panel faellt seit jeher auf den Namen zurueck - von 16 Testbelegen tragen vier eine Anschrift. Der Suchbegriff darf Name und Anschrift NICHT zusammensetzen: gemessen findet Nominatim "Pascalstr. 8, 85057 Ingolstadt", mit dem Betreibernamen davor nichts. Jetzt zwei Kandidaten nacheinander (Anschrift, dann Name); fuer den Kartendienst dagegen beides zusammen, weil Apple und Google Betriebsnamen kennen. Tankstelle steht ueber die volle Kachelbreite, zweizeilig (Tankstelle und Strasse / Ort) und fuehrt beim Tippen in den Standard-Kartendienst - Apple Karten auf Apple-Geraeten, sonst Google Maps. Dieselbe Wahl jetzt auch fuer Fahrzeugstandort und Autohaus. SmartDeal-Ersparnis brach um, weil die dt-Spalte 150,00px breit war und ihr Inhalt 150,20px - zwei Zehntel Pixel, Zeile dadurch 44,8 statt 26,4px hoch. Der Regler "Fahrt beenden" schreibt nicht mehr beim Loslassen: derselbe Wert stellt den Schlaf-Timeout des Dongles, das soll bewusst ausgeloest werden. Speichern/Verwerfen erscheinen nur bei echter Aenderung. Share-Erweiterung: an der ausgelieferten .ipa gemessen trug sie 1.0, die App 2026.9.3.3 - iOS registriert eine Erweiterung mit abweichender Versionsnummer nicht, sie fehlt dann im Teilen-Blatt. ios-signieren.sh schreibt die Version jetzt in beide Ziele und liest die fertige .ipa gegen. Zwischenablage: dritter Weg ueber UIPasteboard.itemProviders, und ein fehlender nativer Teil wird als solcher gemeldet statt als "Kein Zugriff". Am Geraet gesetzt: 1003 Network Ping Timeout 0 -> 60 s, 1004 ACK Type TCP/IP -> AVL. Beide liegen bei flespi als pending bereit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
# AGENTS.md — Project state, review findings, open items, and working rules
|
||||
|
||||
**Last updated: 2026-09-04** (Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt:
|
||||
**Last updated: 2026-09-04** (Tankstelle zweizeilig und antippbar, die Belegkarte sagt bei
|
||||
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 - `2026.9.4.5`,
|
||||
Abschnitt CC. Davor: Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt:
|
||||
das Fahrtfenster stand in Geraetezeit, der Verlauf war nach Ankunftszeit sortiert. Dazu die neun
|
||||
Paritaetsbefunde und die verunreinigte Batteriehistorie, `2026.9.4.1`, Abschnitt CB. Davor: Der Regler stellt den Schlaf-Timeout des Dongles, flespi lesend und
|
||||
schreibend, die eigene Warteschlange wieder entfallen, `2026.9.3.10`, Abschnitt BZ. Davor: Zugaenge: die Dienste-Token aus App und Panel bedienbar, der Wert kommt nie
|
||||
@@ -9462,3 +9465,167 @@ resting voltage, the SOC, the SOH trend and the fallback shown in "Mein Audi / Z
|
||||
the same mechanism as `wert_in_naehe()` with its 300 s window for the outside temperature. Without a
|
||||
vehicle, nothing gets recorded. The 13 existing entries are the owner's data; removal is via the
|
||||
swipe gesture in the measurement list.
|
||||
|
||||
## CC. Tankstelle zweizeilig und antippbar, die Belegkarte sagt die Wahrheit, der Regler bekommt einen Knopf (2026.9.4.5)
|
||||
|
||||
Sieben Meldungen des Eigentuemers, dazu die beiden Dongle-Parameter aus dem Vortag.
|
||||
|
||||
### Der Dongle steht jetzt auf 1003 = 60 und 1004 = AVL
|
||||
|
||||
Am Geraet nachgesehen statt angenommen: `network_ping.timeout` stand auf **0** und
|
||||
`ack_type.mode` auf **0 (TCP/IP)** — die Werte, die am 03.09. die tote TCP-Sitzung
|
||||
unbemerkt liessen (Abschnitt CB). Beide ueber `PUT /gw/devices/{id}/settings/{name}`
|
||||
mit `{"properties": …, "address": "connection"}` geschrieben, dieselbe Form wie
|
||||
`schlaf_timeout_schreiben()`. flespi haelt sie als **`pending`** vor und stellt sie
|
||||
zu, sobald sich das Geraet meldet; `current` steht bis dahin unveraendert auf 0/0.
|
||||
`records_params` (1000 = 60 s, 1001 = 30 s, 1002 = Oldest) bleibt wie es war.
|
||||
|
||||
### "Einzelbeleg/Map laedt die Position nicht" und "der Button funktioniert nicht"
|
||||
|
||||
**Dieselbe Stelle, ein Befund.** Reproduziert an einem Beleg ohne aufloesbare
|
||||
Anschrift: das Panel zeichnete dann einen beliebigen Kartenausschnitt und liess den
|
||||
Zentrieren-Knopf fuer immer `disabled`; die App zeigte ihren Leerzustand — und
|
||||
suchte gar nicht erst, weil `TankstellenKarte` ausschliesslich `station_address`
|
||||
nachschlug. Von 16 Tankvorgaengen der Testinstanz tragen **vier** eine Anschrift;
|
||||
das Panel faellt seit jeher auf den Namen zurueck (`f.station_address ||
|
||||
f.station_name`), die App nicht.
|
||||
|
||||
Beide zeigen jetzt bei fehlender Position **keine Karte**, sondern die Auskunft —
|
||||
und unterscheiden dabei "steht nicht im Beleg" von "findet niemand". Im Panel
|
||||
entscheidet das `BELEG_KARTE`, ein Merker, den `initMap()` setzt, wenn der
|
||||
Nachschlag erfolglos zurueckkommt; `go()` und das Speichern eines Tankvorgangs
|
||||
raeumen ihn wieder ab, damit eine korrigierte Anschrift erneut gesucht wird.
|
||||
|
||||
**Der Suchbegriff war der zweite Fallstrick, und er hat mich einmal erwischt.**
|
||||
Die erste Fassung setzte Name UND Anschrift zusammen, mit der Begruendung "eine
|
||||
Hausnummer ohne Betrieb trifft in Gewerbegebieten das Nachbargebaeude". Live
|
||||
gemessen ist das genau falsch herum: `"Pascalstr. 8, 85057 Ingolstadt"` findet
|
||||
Nominatim, `"A. Zrenner GmbH, Pascalstr. 8, 85057 Ingolstadt"` **nicht** — der
|
||||
Betreibername steht in keiner Karte. Sichtbar wurde es sofort als "Keine Position"
|
||||
bei einem Beleg, dessen Anschrift sich vorher aufloesen liess.
|
||||
|
||||
Deshalb jetzt zwei getrennte Begriffe:
|
||||
|
||||
* `tankstelleKandidaten()` — **erst die Anschrift, dann der Name**, nacheinander
|
||||
versucht. Der Name allein ist wertvoll (`"Shell, SÖLDEN"` findet Nominatim), nur
|
||||
eben nicht als Praefix.
|
||||
* `tankstelleZiel()` — Name UND Anschrift, aber nur fuer den Kartendienst: Apple
|
||||
Karten und Google Maps kennen Betriebsnamen und sind darin deutlich besser als
|
||||
Nominatims Freitextsuche.
|
||||
|
||||
### Die Tankstelle steht ueber die volle Breite, in zwei Zeilen, und fuehrt in die Karten
|
||||
|
||||
Vorgabe des Eigentuemers: erste Zeile Tankstelle und Strasse, zweite Zeile der Ort,
|
||||
Tippen oeffnet den Standard-Kartendienst. Vorher eine gewoehnliche Wertzeile mit
|
||||
120px Wertespalte — "Hermann Mogler Mineraloelg. GmbH, Heidenheimerstr.6, 89551
|
||||
Koenigsbronn" stand dort vier- bis fuenfzeilig.
|
||||
|
||||
Geteilt wird am **letzten** Komma (der Belegparser setzt `"<Strasse>, <PLZ Ort>"`
|
||||
zusammen), damit ein Zusatz bei der Strasse bleibt; ohne Komma bleibt alles in der
|
||||
ersten Zeile statt einen Ort zu erfinden. `kartendienstUrl()` waehlt nach Geraet:
|
||||
`maps.apple.com` auf Apple-Geraeten, sonst `google.com/maps/dir`. Dieselbe Wahl
|
||||
gilt jetzt auch fuer den Fahrzeugstandort und das Autohaus — drei Verweise, ein
|
||||
Dienst.
|
||||
|
||||
**Zwei CSS-Fallen dabei, beide erst am laufenden Panel sichtbar:**
|
||||
|
||||
1. `.tankstellenzeile dt` (0,1,1) verliert gegen `.row dt` im **iOS-Overlay**, das
|
||||
spaeter geladen wird — die Schriftgroesse blieb 16px. Die Regeln heissen deshalb
|
||||
`.row.tankstellenzeile …` (0,2,1) und gewinnen unabhaengig von der Reihenfolge.
|
||||
2. `.rows-fest dt { flex: 0 0 150px }` — in einer **Spalte** ist die Flex-Basis die
|
||||
HOEHE. Der Titel "Tankstelle" war damit 150px hoch, die Zeile 214px. `flex: 0 0
|
||||
auto` in derselben Regel hebt es auf. Gemessen 82px danach.
|
||||
|
||||
**Merkposten, zum dritten Mal in dieser Datei:** eine Regel, die im Overlay einen
|
||||
Namensvetter hat, braucht den Wert des Overlays oder hoehere Spezifitaet — und eine
|
||||
Flex-Angabe bedeutet in einer Spalte etwas anderes als in einer Zeile.
|
||||
|
||||
### "Wozu der Zeilenumbruch bei SmartDeal-Ersparnis?"
|
||||
|
||||
Kein Platzmangel, sondern ein Rundungsfehler, am Einzelbeleg bei 375px gemessen:
|
||||
die dt-Spalte war **150,00px** breit, ihr eigener Inhalt **150,20px** — der Text
|
||||
hinter dem Markenlogo rutschte in eine zweite Zeile, die Zeile wurde 44,8px statt
|
||||
26,4px hoch. Zwei Zehntel Pixel. `.marke { white-space: nowrap }` in beiden
|
||||
Codebasen, und die Wertespalte gibt in genau dieser Zeile ihre feste Mindestbreite
|
||||
auf (`.rows-fest .row-marke dd { min-width: 0 }`); ohne das stuende sie um 10px
|
||||
ueber. Live bestaetigt: 26,4px, eine Zeile.
|
||||
|
||||
### Der Regler schreibt nicht mehr beim Loslassen
|
||||
|
||||
Vorgabe des Eigentuemers, und sie ist richtig: derselbe Wert stellt den
|
||||
Schlaf-Timeout des Dongles — eine Aenderung am Fahrzeug ueber zwei Netze, die
|
||||
bewusst ausgeloest werden soll und nicht als Nebenwirkung einer Wischgeste.
|
||||
|
||||
Apples Richtlinien lassen einen Regler sonst sofort wirken; die Ausnahme ist genau
|
||||
dieser Fall, und das Muster dafuer ist der bestaetigende Knopf. Er erscheint **nur**,
|
||||
wenn der Regler wirklich woanders steht (ein dauerhaft sichtbarer, meist
|
||||
wirkungsloser Knopf waere Beiwerk), daneben "Verwerfen" als schlichter Text —
|
||||
dieselbe Anordnung wie in der Speicherleiste der App am Seitenende.
|
||||
|
||||
Panel: `PAUSENZEIT_ENTWURF` neben `CONFIG.pausenzeit`; der `input`-Zweig zieht nur
|
||||
Fuellung und Anzeige nach (ein `render()` mitten in der Geste erzeugt den Regler neu
|
||||
und der Daumen springt aus der Hand), der `change`-Zweig setzt den Entwurf und
|
||||
zeichnet neu, geschrieben wird erst auf Knopfdruck. App: eigener
|
||||
`pausenzeitEntwurf`-Zustand **neben** dem Seitenentwurf, und `speichern(zusatz)`
|
||||
nimmt den Wert als Argument entgegen — `setState` wirkt erst beim naechsten
|
||||
Durchlauf, der Seitenentwurf trueg im selben Rendern noch den alten.
|
||||
|
||||
Live in beiden geprueft (Regler bewegt, Knopf erscheint, "Verwerfen" stellt 15 Min.
|
||||
wieder her). **"Speichern" bewusst NICHT gedrueckt** — das schriebe einen neuen
|
||||
Schlaf-Timeout an den echten Dongle.
|
||||
|
||||
### Teilen-Erweiterung und Zwischenablage am iPhone
|
||||
|
||||
Die ausgelieferte `App.ipa` vom 03.09. wurde entpackt und geprueft, statt zu raten.
|
||||
**Fast alles ist in Ordnung:** die `.appex` traegt ein echtes arm64-Programm
|
||||
(110.320 Bytes), App und Erweiterung haben beide `group.app.datametric360` in ihren
|
||||
Entitlements, das URL-Schema `datametric360` steht im Info.plist, und
|
||||
`BelegZwischenablagePlugin` ist im App-Programm enthalten.
|
||||
|
||||
**Ein Befund, und er erklaert genau "die Erweiterung erscheint nicht":** die
|
||||
Versionsnummern stimmen nicht ueberein — App `2026.9.3.3`, Erweiterung `1.0`.
|
||||
Apples App Extension Programming Guide verlangt Gleichheit, und iOS registriert
|
||||
eine Erweiterung mit abweichender Nummer nicht. `ios-signieren.sh` schrieb die
|
||||
Version bis dahin nur in `ios/App/App/Info.plist`; `native/teilen/Info.plist`
|
||||
traegt die 1.0 fest. Jetzt bekommen **beide** Ziele dieselbe
|
||||
`CFBundleShortVersionString` und `CFBundleVersion`.
|
||||
|
||||
Dazu, aus der Lehre von Abschnitt AK (von fuenf Fehlern in der nativen Huelle haben
|
||||
vier einen erfolgreichen Archivlauf erzeugt): das Skript liest die **fertige .ipa**
|
||||
gegen — Programm in der `.appex` vorhanden, Versionen gleich, App-Gruppe auf beiden
|
||||
Zielen — und warnt, statt es dem naechsten Fehlerbericht zu ueberlassen.
|
||||
|
||||
Fuer die Zwischenablage kommt ein dritter Weg dazu: `UIPasteboard.itemProviders`.
|
||||
Manche Apps legen eine Datei nur als **Zusage** ab — dann gibt
|
||||
`data(forPasteboardType:)` nil zurueck und `urls` bleibt leer, nur der Anbieter
|
||||
kennt sie. Und `zwischenablage.ts` fragt jetzt `Capacitor.isPluginAvailable()`:
|
||||
fehlt der native Teil (eine App, die nur ueber OTA gewachsen ist), sagt die Meldung
|
||||
das, statt "Kein Zugriff auf die Zwischenablage" — die schickt in die falsche
|
||||
Richtung.
|
||||
|
||||
**Was das nicht kann:** ohne Mac laesst sich keine dieser Aenderungen ausprobieren.
|
||||
Der native Teil kommt ausschliesslich ueber einen Xcode-Lauf aufs Geraet, nie ueber
|
||||
das OTA-Buendel.
|
||||
|
||||
**Bewusst nicht angefasst:** die `NSExtensionActivationRule`. Sie ist heute
|
||||
grosszuegig (jede Datei, jeder Anhang) und laesst die App im Teilen-Blatt
|
||||
erscheinen; ein strenges Praedikat waere sauberer, koennte sie aber ungeprueft ganz
|
||||
verschwinden lassen.
|
||||
|
||||
### Fahrt `t-66c4209276e7` bleibt bei 3,0 km
|
||||
|
||||
Ausdrueckliche Ansage des Eigentuemers ("Fahrt nicht neu rechnen"). Das Screening
|
||||
fuellt grundsaetzlich nur leere Felder — der gespeicherte Wert bleibt damit von
|
||||
selbst stehen.
|
||||
|
||||
### Testinstanz: was von dieser Sitzung dort liegenbleibt
|
||||
|
||||
* **`f-8852746248fa` traegt jetzt `discount: 12.9`** — von Hand gesetzt, um die
|
||||
SmartDeal-Zeile ueberhaupt sichtbar zu machen (alle Testbelege hatten `null`).
|
||||
Sinnvoll fuer die naechste Sichtpruefung, aber es ist eine Aenderung an den Daten
|
||||
des Containers.
|
||||
* **`/config/www/dmapp/`** haelt das gebaute Webbuendel; unter
|
||||
`http://localhost:18123/local/dmapp/index.html` laeuft die App **gleichursprunglich**
|
||||
zur Testinstanz. Das ist der Weg, die App live zu pruefen, ohne an CORS zu
|
||||
scheitern: `cors_allowed_origins` in der `configuration.yaml` blieb wirkungslos
|
||||
(Preflight weiterhin 403), der eingetragene Block ist deshalb wieder entfernt.
|
||||
|
||||
@@ -18,12 +18,19 @@
|
||||
// auch `com.adobe.pdf`. Der Preis ist derselbe wie im Web — iOS zeigt beim
|
||||
// Zugriff seine eigene Einfügen-Rückfrage, und das ist richtig so.
|
||||
//
|
||||
// ZWEI FORMEN, IN DENEN EIN PDF IN DER ZWISCHENABLAGE LIEGT
|
||||
// DREI FORMEN, IN DENEN EIN PDF IN DER ZWISCHENABLAGE LIEGT
|
||||
// ---------------------------------------------------------
|
||||
// 1. Als Daten unter `com.adobe.pdf` — so legen es die meisten Apps ab.
|
||||
// 2. Als Datei-URL — Mail kopiert einen Anhang oft nur als Verweis auf eine
|
||||
// temporäre Kopie. Ohne den zweiten Weg bliebe genau der Fall übrig, für
|
||||
// den der Knopf gedacht ist.
|
||||
// 3. Als Zusage (`NSItemProvider`) — die Datei existiert dann noch gar nicht,
|
||||
// die kopierende App liefert sie erst auf Anforderung. `data(forPasteboardType:)`
|
||||
// gibt dabei nil zurück und `urls` bleibt leer; nur der Anbieter kennt sie.
|
||||
// Das ist Apples eigener, empfohlener Weg und der einzige, der auch eine
|
||||
// Datei aus einer Sandbox einer anderen App zuverlässig herausgibt — Weg 2
|
||||
// scheitert dort am Lesezugriff. Er ist asynchron, deshalb steht er hier
|
||||
// zuletzt: die beiden schnellen Wege sollen nicht auf ihn warten.
|
||||
//
|
||||
// DIESE DATEI IST VERSIONIERT, ios/ NICHT.
|
||||
// `npx cap add ios` erzeugt den nativen Ordner neu und würde alles darin
|
||||
@@ -69,6 +76,24 @@ public class BelegZwischenablagePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
call.resolve([:])
|
||||
// Dritter Weg: die Datei liegt nur als Zusage vor.
|
||||
let anbieter = ablage.itemProviders.first {
|
||||
$0.hasItemConformingToTypeIdentifier(UTType.pdf.identifier)
|
||||
}
|
||||
guard let anbieter else {
|
||||
return call.resolve([:])
|
||||
}
|
||||
anbieter.loadDataRepresentation(forTypeIdentifier: UTType.pdf.identifier) { daten, _ in
|
||||
guard let daten, !daten.isEmpty else {
|
||||
// Kein Fehler: die Oberfläche sagt zu einem leeren Ergebnis
|
||||
// ohnehin das Passende (zwischenablage.ts).
|
||||
return call.resolve([:])
|
||||
}
|
||||
let name = anbieter.suggestedName.map { $0.hasSuffix(".pdf") ? $0 : $0 + ".pdf" }
|
||||
call.resolve([
|
||||
"base64": daten.base64EncodedString(),
|
||||
"name": name ?? "beleg.pdf",
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,25 @@ VERSION="$(node -p "require('../custom_components/audi_dashboard/manifest.json')
|
||||
if ! /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$PLIST" 2>/dev/null; then
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string $VERSION" "$PLIST"
|
||||
fi
|
||||
echo " $VERSION"
|
||||
BUILDNR="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$PLIST" 2>/dev/null || echo 1)"
|
||||
|
||||
# Die Share-Erweiterung MUSS dieselben beiden Nummern tragen wie die App.
|
||||
#
|
||||
# Apples App Extension Programming Guide verlangt das ausdruecklich, und iOS
|
||||
# haelt sich daran: stimmen CFBundleShortVersionString und CFBundleVersion
|
||||
# nicht ueberein, registriert das System die Erweiterung nicht - sie taucht im
|
||||
# Teilen-Blatt dann gar nicht erst auf, ohne jede Fehlermeldung. Genau das war
|
||||
# in der .ipa vom 03.09.2026 der Fall: App 2026.9.3.3, Erweiterung 1.0 (an der
|
||||
# ausgelieferten Datei nachgemessen, nicht vermutet - native/teilen/Info.plist
|
||||
# traegt die 1.0 fest, und dieser Schritt schrieb bis dahin nur $PLIST).
|
||||
ERW_PLIST="ios/App/DataMetric360Share/Info.plist"
|
||||
if [ -f "$ERW_PLIST" ]; then
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$ERW_PLIST"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILDNR" "$ERW_PLIST"
|
||||
echo " $VERSION ($BUILDNR), App und Erweiterung"
|
||||
else
|
||||
echo " $VERSION ($BUILDNR) - Erweiterung nicht gefunden, nur die App"
|
||||
fi
|
||||
|
||||
# Zwischengespeicherte Bereitstellungsprofile dieser App wegraeumen.
|
||||
#
|
||||
@@ -252,6 +270,37 @@ xcodebuild -exportArchive \
|
||||
mkdir -p "$APP/auslieferung"
|
||||
cp "$AUSGABE/App.ipa" "$APP/auslieferung/App.ipa"
|
||||
|
||||
# Am FERTIGEN Buendel gegenlesen, nicht am Projekt.
|
||||
#
|
||||
# Abschnitt AK der AGENTS.md: von fuenf Fehlern in der nativen Huelle haben
|
||||
# vier einen erfolgreichen Archivlauf erzeugt und sind erst am entpackten
|
||||
# .ipa aufgefallen. Geprueft wird hier genau das, was still schiefgehen kann:
|
||||
# eine Erweiterung ohne Programm, ohne App-Gruppe, oder mit abweichender
|
||||
# Versionsnummer (dann registriert iOS sie nicht und sie fehlt im
|
||||
# Teilen-Blatt).
|
||||
echo "==> Fertige .ipa gegenlesen"
|
||||
PRUEF="$(mktemp -d)"
|
||||
if unzip -q "$APP/auslieferung/App.ipa" -d "$PRUEF"; then
|
||||
ERW="$PRUEF/Payload/App.app/PlugIns/DataMetric360Share.appex"
|
||||
if [ ! -x "$ERW/DataMetric360Share" ] && [ ! -f "$ERW/DataMetric360Share" ]; then
|
||||
echo " WARNUNG: die Erweiterung hat kein Programm - sie wird nichts tun."
|
||||
fi
|
||||
ERW_V="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$ERW/Info.plist" 2>/dev/null || echo '?')"
|
||||
APP_V="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$PRUEF/Payload/App.app/Info.plist" 2>/dev/null || echo '?')"
|
||||
if [ "$ERW_V" != "$APP_V" ]; then
|
||||
echo " WARNUNG: Erweiterung $ERW_V, App $APP_V - iOS registriert die"
|
||||
echo " Erweiterung dann nicht, sie fehlt im Teilen-Blatt."
|
||||
else
|
||||
echo " Erweiterung und App tragen beide $APP_V"
|
||||
fi
|
||||
for ZIEL in "$PRUEF/Payload/App.app" "$ERW"; do
|
||||
if ! codesign -d --entitlements :- "$ZIEL" 2>/dev/null | grep -q "group.app.datametric360"; then
|
||||
echo " WARNUNG: $(basename "$ZIEL") ohne App-Gruppe - Teilen kann nichts uebergeben."
|
||||
fi
|
||||
done
|
||||
fi
|
||||
rm -rf "$PRUEF"
|
||||
|
||||
# Auslieferung ueber die Luft gleich mitschreiben.
|
||||
#
|
||||
# manifest.plist und die Installationsseite hingen bisher an einem zweiten,
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Tests zur Tankstellen-Anzeige und zum Kartendienst.
|
||||
*
|
||||
* Die Anschriften stammen aus echten Belegen der Testinstanz (04.09.2026) -
|
||||
* genau an ihnen ist aufgefallen, dass die Wertespalte zu schmal war und die
|
||||
* App die Tankstelle ohne Anschrift gar nicht erst nachschlug.
|
||||
*/
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
import {
|
||||
adresseTeilen,
|
||||
kartendienstUrl,
|
||||
tankstelleKandidaten,
|
||||
tankstelleSuchtext,
|
||||
tankstelleZeilen,
|
||||
tankstelleZiel,
|
||||
} from "./tankstelle"
|
||||
|
||||
function alsGeraet(kennung: string) {
|
||||
vi.stubGlobal("navigator", { userAgent: kennung, platform: "" })
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe("adresseTeilen", () => {
|
||||
it("trennt Straße und Ort am Komma", () => {
|
||||
expect(adresseTeilen("Pascalstr. 8, 85057 Ingolstadt")).toEqual({
|
||||
strasse: "Pascalstr. 8",
|
||||
ort: "85057 Ingolstadt",
|
||||
})
|
||||
})
|
||||
|
||||
it("trennt am LETZTEN Komma - Zusätze bleiben bei der Straße", () => {
|
||||
expect(adresseTeilen("Nürnberger Str.74, Gewerbepark, 91522 Ansbach")).toEqual({
|
||||
strasse: "Nürnberger Str.74, Gewerbepark",
|
||||
ort: "91522 Ansbach",
|
||||
})
|
||||
})
|
||||
|
||||
it("erfindet ohne Komma keinen Ort", () => {
|
||||
expect(adresseTeilen("Heidenheimerstr.6")).toEqual({ strasse: "Heidenheimerstr.6", ort: "" })
|
||||
})
|
||||
|
||||
it("verträgt leer und null", () => {
|
||||
expect(adresseTeilen(null)).toEqual({ strasse: "", ort: "" })
|
||||
expect(adresseTeilen(" ")).toEqual({ strasse: "", ort: "" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("tankstelleZeilen", () => {
|
||||
it("setzt Name und Straße in die erste Zeile, den Ort in die zweite", () => {
|
||||
expect(tankstelleZeilen("A. Zrenner GmbH", "Pascalstr. 8, 85057 Ingolstadt")).toEqual({
|
||||
oben: "A. Zrenner GmbH, Pascalstr. 8",
|
||||
unten: "85057 Ingolstadt",
|
||||
})
|
||||
})
|
||||
|
||||
it("kommt ohne Anschrift aus", () => {
|
||||
expect(tankstelleZeilen("Testtankstelle", null)).toEqual({
|
||||
oben: "Testtankstelle",
|
||||
unten: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("kommt ohne Namen aus", () => {
|
||||
expect(tankstelleZeilen("", "Pascalstr. 8, 85057 Ingolstadt")).toEqual({
|
||||
oben: "Pascalstr. 8",
|
||||
unten: "85057 Ingolstadt",
|
||||
})
|
||||
})
|
||||
|
||||
it("sagt „unbekannt“, wenn der Beleg nichts hergibt", () => {
|
||||
expect(tankstelleZeilen(null, null)).toEqual({ oben: "unbekannt", unten: "" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("tankstelleKandidaten", () => {
|
||||
it("stellt die Anschrift voran und nimmt den Namen NICHT dazu", () => {
|
||||
// Gemessen am 04.09.2026: Nominatim findet die Anschrift, mit dem
|
||||
// Betreibernamen davor findet es nichts.
|
||||
expect(tankstelleKandidaten("A. Zrenner GmbH", "Pascalstr. 8, 85057 Ingolstadt")).toEqual([
|
||||
"Pascalstr. 8, 85057 Ingolstadt",
|
||||
"A. Zrenner GmbH",
|
||||
])
|
||||
})
|
||||
|
||||
it("laesst leere Teile weg", () => {
|
||||
expect(tankstelleKandidaten("Shell, SÖLDEN", null)).toEqual(["Shell, SÖLDEN"])
|
||||
expect(tankstelleKandidaten(null, null)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("tankstelleZiel", () => {
|
||||
it("nimmt fuer den Kartendienst Name und Anschrift zusammen", () => {
|
||||
expect(tankstelleZiel("A. Zrenner GmbH", "Pascalstr. 8, 85057 Ingolstadt")).toBe(
|
||||
"A. Zrenner GmbH, Pascalstr. 8, 85057 Ingolstadt",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("tankstelleSuchtext", () => {
|
||||
it("ist der erste Kandidat, also die Anschrift", () => {
|
||||
expect(tankstelleSuchtext("A. Zrenner GmbH", "Pascalstr. 8, 85057 Ingolstadt")).toBe(
|
||||
"Pascalstr. 8, 85057 Ingolstadt",
|
||||
)
|
||||
})
|
||||
|
||||
it("faellt auf den Namen zurueck, wenn keine Anschrift im Beleg steht", () => {
|
||||
// Genau der Fall, in dem die App bis zum 04.09.2026 gar nichts nachschlug,
|
||||
// das Panel dagegen schon (f.station_address || f.station_name).
|
||||
expect(tankstelleSuchtext("Shell, SÖLDEN", null)).toBe("Shell, SÖLDEN")
|
||||
})
|
||||
|
||||
it("ist leer, wenn beides fehlt", () => {
|
||||
expect(tankstelleSuchtext(null, "")).toBe("")
|
||||
})
|
||||
})
|
||||
|
||||
describe("kartendienstUrl", () => {
|
||||
it("nimmt auf dem iPhone Apple Karten", () => {
|
||||
alsGeraet("Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)")
|
||||
expect(kartendienstUrl("A. Zrenner GmbH", "Pascalstr. 8, 85057 Ingolstadt")).toBe(
|
||||
"https://maps.apple.com/?daddr=A.%20Zrenner%20GmbH%2C%20Pascalstr.%208%2C%2085057%20Ingolstadt",
|
||||
)
|
||||
})
|
||||
|
||||
it("zielt mit Koordinaten auf den Punkt und beschriftet ihn", () => {
|
||||
alsGeraet("Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)")
|
||||
expect(
|
||||
kartendienstUrl("A. Zrenner GmbH", "Pascalstr. 8, 85057 Ingolstadt", {
|
||||
lat: 48.7869101,
|
||||
lon: 11.39767,
|
||||
}),
|
||||
).toBe("https://maps.apple.com/?daddr=48.7869101%2C11.39767&q=A.%20Zrenner%20GmbH")
|
||||
})
|
||||
|
||||
it("nimmt sonst Google Maps", () => {
|
||||
alsGeraet("Mozilla/5.0 (X11; Linux x86_64)")
|
||||
expect(kartendienstUrl(null, "Pascalstr. 8, 85057 Ingolstadt")).toBe(
|
||||
"https://www.google.com/maps/dir/?api=1&destination=Pascalstr.%208%2C%2085057%20Ingolstadt",
|
||||
)
|
||||
})
|
||||
|
||||
it("bleibt null, wenn nichts bekannt ist", () => {
|
||||
alsGeraet("Mozilla/5.0 (iPhone)")
|
||||
expect(kartendienstUrl(null, null)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Tankstelle eines Belegs: Anzeige in zwei Zeilen und der Weg in den
|
||||
* Kartendienst des Geräts.
|
||||
*
|
||||
* WARUM ZWEI ZEILEN
|
||||
* -----------------
|
||||
* Bis zum 04.09.2026 stand die Tankstelle als gewöhnliche Wertezeile rechts in
|
||||
* der schmalen Wertespalte: Name oben, die komplette Anschrift als Kleintext
|
||||
* darunter. Gemessen an der echten Kachel blieben dafür 120 px - "Hermann
|
||||
* Mogler Mineralölg. GmbH, Heidenheimerstr.6, 89551 Königsbronn" brach dort in
|
||||
* vier bis fünf Zeilen um. Die Aufteilung hier ist die vom Eigentümer
|
||||
* verlangte: erste Zeile Tankstelle und Straße, zweite Zeile der Ort.
|
||||
*
|
||||
* WIE DIE ANSCHRIFT GETEILT WIRD
|
||||
* ------------------------------
|
||||
* Der Belegparser setzt sie als `"<Straße>, <PLZ Ort>"` zusammen
|
||||
* (shell_beleg_parser.py). Geteilt wird deshalb am LETZTEN Komma: ein Name wie
|
||||
* "Str. 5, Gewerbepark, 85057 Ingolstadt" behält so seinen Zusatz bei der
|
||||
* Straße statt beim Ort. Ohne Komma bleibt alles in der ersten Zeile - lieber
|
||||
* ein zu langer erster Eintrag als ein erfundener Ort.
|
||||
*
|
||||
* KARTENDIENST STATT EINES FESTEN ANBIETERS
|
||||
* -----------------------------------------
|
||||
* "Klick auf die Tankstelle soll den Standard-Kartendienst öffnen" (Eigentümer,
|
||||
* 04.09.2026). Auf Apple-Geräten ist das Karten, sonst Google Maps - beides
|
||||
* über die jeweilige Web-Adresse, die das System an die installierte App
|
||||
* weiterreicht. Ein `geo:`-Verweis wäre der sauberere Weg, aber Safari kennt
|
||||
* ihn nicht, und ein Verweis, der auf dem Hauptgerät ins Leere läuft, ist
|
||||
* keiner.
|
||||
*/
|
||||
|
||||
export interface Koordinate {
|
||||
lat: number
|
||||
lon: number
|
||||
}
|
||||
|
||||
/** `"Pascalstr. 8, 85057 Ingolstadt"` → Straße und Ort. */
|
||||
export function adresseTeilen(adresse: string | null | undefined): {
|
||||
strasse: string
|
||||
ort: string
|
||||
} {
|
||||
const roh = (adresse ?? "").trim()
|
||||
if (!roh) return { strasse: "", ort: "" }
|
||||
const komma = roh.lastIndexOf(",")
|
||||
if (komma < 0) return { strasse: roh, ort: "" }
|
||||
return { strasse: roh.slice(0, komma).trim(), ort: roh.slice(komma + 1).trim() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Die beiden Zeilen der Tankstellen-Angabe. `oben` ist nie leer: steht weder
|
||||
* Name noch Anschrift im Beleg, heißt es dort "unbekannt" - dieselbe Auskunft,
|
||||
* die die Zeile vorher gab.
|
||||
*/
|
||||
export function tankstelleZeilen(
|
||||
name: string | null | undefined,
|
||||
adresse: string | null | undefined,
|
||||
): { oben: string; unten: string } {
|
||||
const { strasse, ort } = adresseTeilen(adresse)
|
||||
const oben = [(name ?? "").trim(), strasse].filter(Boolean).join(", ")
|
||||
return { oben: oben || "unbekannt", unten: ort }
|
||||
}
|
||||
|
||||
/**
|
||||
* Suchbegriffe für die Geokodierung, in der Reihenfolge, in der sie versucht
|
||||
* werden: erst die Anschrift, dann der Name.
|
||||
*
|
||||
* NICHT beides zusammen. Am 04.09.2026 gemessen: `"Pascalstr. 8, 85057
|
||||
* Ingolstadt"` findet Nominatim, `"A. Zrenner GmbH, Pascalstr. 8, 85057
|
||||
* Ingolstadt"` nicht — der Betreibername steht in keiner Karte und lässt die
|
||||
* Freitextsuche ins Leere laufen. Genau daran ist die erste Fassung dieser
|
||||
* Funktion gescheitert, sichtbar als „Keine Position zur Tankstelle" bei einem
|
||||
* Beleg, dessen Anschrift sich vorher hat auflösen lassen.
|
||||
*
|
||||
* Der Name allein ist trotzdem wertvoll: `"Shell, SÖLDEN"` findet Nominatim
|
||||
* (46.9756/11.0111) — deshalb der zweite Versuch statt gar keinem.
|
||||
*/
|
||||
export function tankstelleKandidaten(
|
||||
name: string | null | undefined,
|
||||
adresse: string | null | undefined,
|
||||
): string[] {
|
||||
return [(adresse ?? "").trim(), (name ?? "").trim()].filter(Boolean)
|
||||
}
|
||||
|
||||
/** Der erste Suchbegriff - leer heißt: der Beleg nennt gar nichts. */
|
||||
export function tankstelleSuchtext(
|
||||
name: string | null | undefined,
|
||||
adresse: string | null | undefined,
|
||||
): string {
|
||||
return tankstelleKandidaten(name, adresse)[0] ?? ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Ziel für den Kartendienst. Hier gehören Name UND Anschrift zusammen:
|
||||
* Apple Karten und Google Maps kennen Betriebsnamen und treffen damit die
|
||||
* Zapfsäule statt des Nachbargebäudes - beide sind darin deutlich besser als
|
||||
* Nominatims Freitextsuche.
|
||||
*/
|
||||
export function tankstelleZiel(
|
||||
name: string | null | undefined,
|
||||
adresse: string | null | undefined,
|
||||
): string {
|
||||
return [(name ?? "").trim(), (adresse ?? "").trim()].filter(Boolean).join(", ")
|
||||
}
|
||||
|
||||
/** Apple-Gerät? Dort ist Karten der Standarddienst. */
|
||||
function apfelgeraet(): boolean {
|
||||
if (typeof navigator === "undefined") return false
|
||||
const kennung = `${navigator.userAgent || ""} ${navigator.platform || ""}`
|
||||
return /iPhone|iPad|iPod|Macintosh|Mac OS X/i.test(kennung)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verweis in den Kartendienst - `null`, wenn nichts bekannt ist, wohin.
|
||||
*
|
||||
* Mit Koordinaten wird auf sie gezielt und der Name nur als Beschriftung
|
||||
* mitgegeben; ohne sie muss die Anschrift als Suchtext genügen.
|
||||
*/
|
||||
export function kartendienstUrl(
|
||||
name: string | null | undefined,
|
||||
adresse: string | null | undefined,
|
||||
pos?: Koordinate | null,
|
||||
): string | null {
|
||||
const beschriftung = (name ?? "").trim()
|
||||
const ziel = pos ? `${pos.lat},${pos.lon}` : tankstelleZiel(name, adresse)
|
||||
if (!ziel) return null
|
||||
if (apfelgeraet()) {
|
||||
const q = pos && beschriftung ? `&q=${encodeURIComponent(beschriftung)}` : ""
|
||||
return `https://maps.apple.com/?daddr=${encodeURIComponent(ziel)}${q}`
|
||||
}
|
||||
return `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(ziel)}`
|
||||
}
|
||||
|
||||
/** Wie oben, aber für einen reinen Punkt (Fahrzeugstandort, Autohaus). */
|
||||
export function kartendienstUrlZuPunkt(pos: Koordinate | null | undefined): string | null {
|
||||
if (!pos) return null
|
||||
return kartendienstUrl(null, null, pos)
|
||||
}
|
||||
@@ -67,12 +67,23 @@ function base64ZuDatei(base64: string, name: string): File {
|
||||
*/
|
||||
export async function pdfAusZwischenablage(): Promise<File> {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
/* Der native Teil steckt in der .ipa, nicht im OTA-Bündel: eine App, die
|
||||
nur über die Aktualisierung in der App gewachsen ist, hat ihn nicht.
|
||||
Genau das war bis zum 04.09.2026 nicht zu erkennen - der Fehlschlag kam
|
||||
mit demselben Text wie eine abgelehnte Rückfrage ("Kein Zugriff"), und
|
||||
der schickt in die falsche Richtung. `isPluginAvailable` fragt die
|
||||
Brücke, ob die Klasse im Programm ueberhaupt vorhanden ist. */
|
||||
if (!Capacitor.isPluginAvailable("BelegZwischenablage")) {
|
||||
throw new ZwischenablageFehler(
|
||||
"Diese App-Fassung enthält den nativen Teil noch nicht - er kommt nur mit " +
|
||||
'einer neu installierten App, nicht über die Aktualisierung. Bis dahin "Datei auswählen".',
|
||||
)
|
||||
}
|
||||
let ergebnis: { base64?: string; name?: string }
|
||||
try {
|
||||
ergebnis = await Nativ.pdfHolen()
|
||||
} catch {
|
||||
// Abgelehnte Einfügen-Rückfrage oder ein Gerät ohne das Plugin (eine
|
||||
// .ipa von vor dem 02.09.2026).
|
||||
// Abgelehnte Einfügen-Rückfrage des Systems.
|
||||
throw new ZwischenablageFehler(
|
||||
'Kein Zugriff auf die Zwischenablage - bitte "Datei auswählen".',
|
||||
)
|
||||
|
||||
@@ -58,6 +58,12 @@ export function Einstellungen({
|
||||
// eigener Entwurf, der beim Speichern mitgeschrieben wird.
|
||||
const [erstzulassung, setzeErstzulassung] = useState<string | null>(null)
|
||||
const [entwurf, setzeEntwurf] = useState<EinstellungenWerte | null>(null)
|
||||
/* Der Regler "Fahrt beenden" fuehrt einen EIGENEN Entwurf, nicht den der
|
||||
Seite. Grund: sein Wert stellt zugleich den Schlaf-Timeout des Dongles -
|
||||
eine Aenderung am Fahrzeug, die ueber zwei Netze geht. Sie soll bewusst
|
||||
ausgeloest werden, nicht als Nebenwirkung des Loslassens (Vorgabe des
|
||||
Eigentuemers, 04.09.2026). `null` heisst: nichts angefasst. */
|
||||
const [pausenzeitEntwurf, setzePausenzeitEntwurf] = useState<number | null>(null)
|
||||
const [laeuft, setzeLaeuft] = useState(false)
|
||||
const [gespeichert, setzeGespeichert] = useState(false)
|
||||
const [importOffen, setzeImportOffen] = useState(false)
|
||||
@@ -187,6 +193,8 @@ export function Einstellungen({
|
||||
|
||||
if (!einstellungen || !fahrzeug) return null
|
||||
const werte = entwurf ?? einstellungen
|
||||
const pausenzeit = pausenzeitEntwurf ?? werte.pausenzeitMin
|
||||
const pausenzeitGeaendert = pausenzeitEntwurf !== null && pausenzeitEntwurf !== werte.pausenzeitMin
|
||||
const erstzulassungWert = erstzulassung ?? fahrzeug.erstzulassung
|
||||
// Bei Winterraedern zeigt die Seitenansicht das Winterfoto - wie bildInfo()
|
||||
// im Panel, das hier dieselbe Vorschau liefert.
|
||||
@@ -209,17 +217,22 @@ export function Einstellungen({
|
||||
const aendern = (teil: Partial<EinstellungenWerte>) =>
|
||||
setzeEntwurf({ ...werte, ...teil })
|
||||
|
||||
const speichern = async () => {
|
||||
if (!entwurf) return
|
||||
/* `zusatz` kommt vom Regler der Kachel "Fahrt beenden": der fuehrt einen
|
||||
eigenen Entwurf und uebergibt ihn erst beim Druck auf Speichern. Ohne den
|
||||
Umweg wuerde `entwurf` im selben Rendern noch den alten Wert tragen -
|
||||
setState wirkt erst beim naechsten Durchlauf. */
|
||||
const speichern = async (zusatz?: Partial<EinstellungenWerte>) => {
|
||||
if (!entwurf && !zusatz) return
|
||||
setzeLaeuft(true)
|
||||
setzeGespeichert(false)
|
||||
try {
|
||||
await profilSpeichern({
|
||||
einstellungen: entwurf,
|
||||
einstellungen: { ...werte, ...(zusatz ?? {}) },
|
||||
...(erstzulassung !== null ? { fahrzeug: { ...fahrzeug, erstzulassung } } : {}),
|
||||
})
|
||||
setzeEntwurf(null)
|
||||
setzeErstzulassung(null)
|
||||
setzePausenzeitEntwurf(null)
|
||||
setzeGespeichert(true)
|
||||
} finally {
|
||||
setzeLaeuft(false)
|
||||
@@ -462,11 +475,33 @@ export function Einstellungen({
|
||||
min={1}
|
||||
max={60}
|
||||
schritt={1}
|
||||
wert={werte.pausenzeitMin}
|
||||
onChange={(min) => aendern({ pausenzeitMin: min })}
|
||||
anzeige={werte.pausenzeitMin + " Min."}
|
||||
wert={pausenzeit}
|
||||
onChange={setzePausenzeitEntwurf}
|
||||
anzeige={pausenzeit + " Min."}
|
||||
aria-label="Minuten bis zum automatischen Fahrtende"
|
||||
/>
|
||||
{/* Erscheint erst, wenn der Regler wirklich woanders steht - ein
|
||||
dauerhaft sichtbarer, meist wirkungsloser Knopf waere Beiwerk. Die
|
||||
Anordnung folgt der Speicherleiste am Seitenende: bestaetigende
|
||||
Aktion als voller Knopf, der Ausweg daneben als schlichter Text. */}
|
||||
{pausenzeitGeaendert && (
|
||||
<div className="dm-speicherleiste dm-speicherleiste--kachel">
|
||||
<ActionButton
|
||||
onClick={() => void speichern({ pausenzeitMin: pausenzeitEntwurf as number })}
|
||||
disabled={laeuft}
|
||||
>
|
||||
{laeuft ? "Speichere …" : "Speichern"}
|
||||
</ActionButton>
|
||||
<button
|
||||
type="button"
|
||||
className="dm-textknopf"
|
||||
onClick={() => setzePausenzeitEntwurf(null)}
|
||||
disabled={laeuft}
|
||||
>
|
||||
Verwerfen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Fusszeile statt Info-Knopf: Apples eigenes Muster fuer erklaerenden
|
||||
Text unter einer Einstellung ist der Footer der Gruppe. */}
|
||||
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useRef, useState } from "react"
|
||||
|
||||
import { ActionButton, Feld, Fig, Tile } from "@audi-dash/ui"
|
||||
|
||||
import { kartendienstUrl } from "../daten/tankstelle"
|
||||
import { useDaten } from "../daten/DatenKontext"
|
||||
import {
|
||||
inspektionPrognose,
|
||||
@@ -213,7 +214,7 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
|
||||
className="ads-action dm-waehlknopf"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href={`https://maps.google.com/?q=${encodeURIComponent(autohaus.adresse ?? "")}`}
|
||||
href={kartendienstUrl(autohaus.name, autohaus.adresse) ?? undefined}
|
||||
>
|
||||
In Karten öffnen
|
||||
</a>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } f
|
||||
|
||||
import { useDaten } from "../daten/DatenKontext"
|
||||
import { koordinatenAufloesen } from "../daten/geokodierung"
|
||||
import { kartendienstUrlZuPunkt } from "../daten/tankstelle"
|
||||
import { de } from "../format"
|
||||
import type { Fahrzeug } from "../daten/profilAdapter"
|
||||
import { SymbolTanken } from "../symbole"
|
||||
@@ -450,9 +451,9 @@ export function Standort() {
|
||||
const koordinaten = fahrzeugPos ? `${de(fahrzeugPos.lat, 4)}, ${de(fahrzeugPos.lon, 4)}` : ""
|
||||
const adresseAnzeige = !bekannt ? "Kein GPS-Signal vom Fahrzeug" : (adresse ?? koordinaten)
|
||||
const zustand = standortZustand(fahrzeug)
|
||||
const kartenlink = fahrzeugPos
|
||||
? `https://www.google.com/maps/dir/?api=1&destination=${fahrzeugPos.lat},${fahrzeugPos.lon}`
|
||||
: null
|
||||
// Kartendienst des Geraets statt fest Google Maps: auf dem iPhone ist das
|
||||
// Karten. Dieselbe Wahl wie beim Tankstellen-Verweis (daten/tankstelle.ts).
|
||||
const kartenlink = kartendienstUrlZuPunkt(fahrzeugPos)
|
||||
|
||||
const teilen = () => {
|
||||
if (!kartenlink) return
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ActionButton, Feld, Fig, Tile } from "@audi-dash/ui"
|
||||
import type { Tankvorgang } from "../api"
|
||||
import { ENTITAETEN } from "../api"
|
||||
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, bestaetigen } from "./bausteine"
|
||||
@@ -122,11 +123,7 @@ export function TankDetail({ id, geheZu }: { id: string | undefined; geheZu: (na
|
||||
wert={`${eur(tank.discount)} €`}
|
||||
/>
|
||||
)}
|
||||
<Wertzeile
|
||||
label="Tankstelle"
|
||||
wert={tank.station_name ?? "unbekannt"}
|
||||
zusatz={tank.station_address ?? ""}
|
||||
/>
|
||||
<Tankstellenzeile name={tank.station_name} adresse={tank.station_address} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@@ -141,6 +138,60 @@ export function TankDetail({ id, geheZu }: { id: string | undefined; geheZu: (na
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tankstelle des Belegs: eigene Zeile über die volle Kachelbreite statt der
|
||||
* engen Wertespalte, zweizeilig (Tankstelle und Straße / Ort) und antippbar -
|
||||
* der Tipp führt in den Kartendienst des Geräts. Deckungsgleich mit
|
||||
* `.tankstellenzeile` im Panel; die Aufteilung stammt vom Eigentümer
|
||||
* (04.09.2026).
|
||||
*
|
||||
* Ohne Ziel bleibt es eine gewöhnliche Zeile: ein Verweis, der nirgendwohin
|
||||
* führt, wäre schlechter als keiner.
|
||||
*/
|
||||
function Tankstellenzeile({
|
||||
name,
|
||||
adresse,
|
||||
}: {
|
||||
name: string | null | undefined
|
||||
adresse: string | null | undefined
|
||||
}) {
|
||||
const { oben, unten } = tankstelleZeilen(name, adresse)
|
||||
const url = kartendienstUrl(name, adresse)
|
||||
const inhalt = (
|
||||
<>
|
||||
<span className="dm-tankstellenzeile__ort">
|
||||
{oben}
|
||||
{unten && <small>{unten}</small>}
|
||||
</span>
|
||||
{url && (
|
||||
<svg className="dm-blatt__chevron" viewBox="0 0 6 10" aria-hidden="true">
|
||||
<polyline points="1,1 5,5 1,9" />
|
||||
</svg>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<div className="dm-tankstellenzeile">
|
||||
<dt>Tankstelle</dt>
|
||||
<dd>
|
||||
{url ? (
|
||||
<a
|
||||
className="dm-tankstellenzeile__inhalt"
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
aria-label={`${oben} in Karten öffnen`}
|
||||
>
|
||||
{inhalt}
|
||||
</a>
|
||||
) : (
|
||||
<span className="dm-tankstellenzeile__inhalt">{inhalt}</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** "YYYY-MM-DDTHH:mm" in Ortszeit für ein <input type="datetime-local">. */
|
||||
function alsDatetimeLocal(iso: string): string {
|
||||
const d = new Date(iso)
|
||||
|
||||
@@ -3,15 +3,29 @@
|
||||
*
|
||||
* Die Koordinaten stehen nicht im Datensatz — der Beleg trägt nur eine
|
||||
* Anschrift, und das Backend füllt `station_lat`/`station_lon` nie. Sie werden
|
||||
* deshalb aus der Adresse aufgelöst (`adresseAufloesen()`), genau wie im
|
||||
* Panel. Ohne Adresse oder ohne Treffer bleibt der ehrliche Leerzustand
|
||||
* stehen, statt irgendeinen Kartenausschnitt zu zeigen.
|
||||
* deshalb aus der Anschrift aufgelöst (`adresseAufloesen()`), genau wie im
|
||||
* Panel.
|
||||
*
|
||||
* GESUCHT WIRD AUCH NACH DEM NAMEN
|
||||
* --------------------------------
|
||||
* Bis zum 04.09.2026 wurde hier ausschliesslich `station_address` nachgeschlagen.
|
||||
* Nur trägt die die Mehrzahl der Belege gar nicht: von 16 Tankvorgängen der
|
||||
* Testinstanz haben vier eine Anschrift, alle übrigen bestenfalls einen Namen.
|
||||
* Für die blieb die Karte dauerhaft leer ("Einzelbeleg/Map lädt die Position
|
||||
* der Tankstelle nicht", Eigentümer 04.09.2026) - während das Panel längst
|
||||
* `f.station_address || f.station_name` nachschlug. Hier steht jetzt dasselbe,
|
||||
* über `tankstelleSuchtext()`: Anschrift bevorzugt, sonst der Name.
|
||||
*
|
||||
* Ohne beides bleibt der ehrliche Leerzustand stehen, statt irgendeinen
|
||||
* Kartenausschnitt zu zeigen - und er sagt jetzt auch, WAS fehlt: eine
|
||||
* Anschrift, die es nicht gibt, ist etwas anderes als eine, die niemand findet.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { adresseAufloesen } from "../daten/geokodierung"
|
||||
import type { Koordinate } from "../daten/geokodierung"
|
||||
import { tankstelleKandidaten } from "../daten/tankstelle"
|
||||
import { Leerzustand } from "./bausteine"
|
||||
import { Karte } from "./Karte"
|
||||
|
||||
@@ -24,19 +38,33 @@ export function TankstellenKarte({
|
||||
}) {
|
||||
const [position, setzePosition] = useState<Koordinate | null>(null)
|
||||
const [geprueft, setzeGeprueft] = useState(false)
|
||||
// Als eine Zeichenkette, damit der Effekt nicht bei jedem Rendern neu läuft -
|
||||
// ein frisches Array wäre bei jedem Durchlauf ein anderer Wert.
|
||||
const suche = tankstelleKandidaten(name, adresse).join("\n")
|
||||
|
||||
useEffect(() => {
|
||||
let abgebrochen = false
|
||||
setzeGeprueft(false)
|
||||
void adresseAufloesen(adresse).then((treffer) => {
|
||||
if (abgebrochen) return
|
||||
setzePosition(treffer)
|
||||
setzeGeprueft(true)
|
||||
})
|
||||
const versuchen = async () => {
|
||||
for (const begriff of suche ? suche.split("\n") : []) {
|
||||
const treffer = await adresseAufloesen(begriff)
|
||||
if (abgebrochen) return
|
||||
if (treffer) {
|
||||
setzePosition(treffer)
|
||||
setzeGeprueft(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!abgebrochen) {
|
||||
setzePosition(null)
|
||||
setzeGeprueft(true)
|
||||
}
|
||||
}
|
||||
void versuchen()
|
||||
return () => {
|
||||
abgebrochen = true
|
||||
}
|
||||
}, [adresse])
|
||||
}, [suche])
|
||||
|
||||
// Wie im Panel (tankstelleAufKarteZeigen): die CI-Nadel mit Name und
|
||||
// Anschrift im Popup, dazu der Knopf, der wieder darauf zentriert.
|
||||
@@ -45,11 +73,15 @@ export function TankstellenKarte({
|
||||
// Solange die Auflösung läuft: eine leere Kartenfläche, wie im Panel (die
|
||||
// Karte steht dort schon, die Nadel kommt nach). Bewusst nicht die
|
||||
// Ladeanzeige der Seite - die meldet „der Bildschirm ist noch nicht da".
|
||||
if (!geprueft) return <div className="dm-kartenplatz" />
|
||||
if (!geprueft && suche) return <div className="dm-kartenplatz" />
|
||||
return (
|
||||
<Leerzustand
|
||||
titel="Keine Position zur Tankstelle"
|
||||
text="Zu dieser Anschrift ließ sich kein Kartenpunkt finden. Sobald der Beleg eine auflösbare Adresse trägt, erscheint die Tankstelle hier."
|
||||
text={
|
||||
suche
|
||||
? "Zu dieser Anschrift ließ sich kein Kartenpunkt finden. Trag im Beleg unter „Werte bearbeiten“ eine genauere Anschrift ein, dann erscheint die Tankstelle hier."
|
||||
: "Dieser Beleg nennt weder Tankstelle noch Anschrift. Trag sie unter „Werte bearbeiten“ nach, dann erscheint die Tankstelle hier."
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -897,6 +897,55 @@ button.dm-serviceblock:active {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Tankstelle im Einzelbeleg (Vorgabe des Eigentuemers, 04.09.2026).
|
||||
|
||||
Vorher eine gewoehnliche Wertzeile: Name rechts in der Wertespalte, die
|
||||
ganze Anschrift als Kleintext darunter. Die Spalte ist rund 120px breit -
|
||||
"Hermann Mogler Mineraloelg. GmbH, Heidenheimerstr.6, 89551 Koenigsbronn"
|
||||
stand dort vier- bis fuenfzeilig. Hier bekommt sie die volle Kachelbreite
|
||||
unter der Beschriftung, zwei Zeilen (Tankstelle und Strasse / Ort), und
|
||||
fuehrt beim Tippen in den Kartendienst - deshalb der Chevron rechts.
|
||||
|
||||
Deckungsgleich mit .tankstellenzeile im Panel. */
|
||||
.dm-tankstellenzeile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 11px 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.dm-tankstellenzeile dt {
|
||||
font-size: 13px;
|
||||
color: var(--fg2);
|
||||
}
|
||||
.dm-tankstellenzeile dd {
|
||||
margin: 0;
|
||||
}
|
||||
.dm-tankstellenzeile__inhalt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-3);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.dm-tankstellenzeile__inhalt:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.dm-tankstellenzeile__ort {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
color: var(--fg);
|
||||
line-height: 1.35;
|
||||
min-width: 0;
|
||||
}
|
||||
.dm-tankstellenzeile__ort small {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
color: var(--fg3);
|
||||
}
|
||||
|
||||
.dm-abschnitt {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
@@ -1531,6 +1580,20 @@ button.dm-listenzeile {
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
/* Dieselbe Leiste innerhalb einer Kachel (Regler "Fahrt beenden"): nicht
|
||||
klebend, kein eigener Untergrund, keine Trennlinie - sie gehoert zur Kachel
|
||||
und nicht an den Bildschirmrand. */
|
||||
.dm-speicherleiste--kachel {
|
||||
position: static;
|
||||
margin-top: var(--sp-3);
|
||||
padding: 0;
|
||||
background: none;
|
||||
border-top: 0;
|
||||
}
|
||||
.dm-speicherleiste--kachel > :first-child {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.dm-szene__zusatz {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -1913,10 +1976,18 @@ button.dm-listenzeile {
|
||||
|
||||
/* `.marke` im Panel: Bildmarke und Text nebeneinander, z. B. vor
|
||||
„SmartDeal-Ersparnis" auf dem Einzelbeleg. */
|
||||
/* white-space:nowrap: ohne die brach "SmartDeal-Ersparnis" neben dem
|
||||
Markenlogo um. Der Grund ist kein Platzmangel, sondern ein Rundungsfehler -
|
||||
am Panel gemessen (04.09.2026): die Spalte war 150,00px breit, ihr eigener
|
||||
Inhalt 150,20px, also brach der Text nach dem Logo in eine zweite Zeile und
|
||||
die Zeile wurde 44,8px statt 26,4px hoch. Die Zeile hat reichlich Platz, sie
|
||||
darf ihn nur nicht abgeschnitten bekommen. (Im Panel kommt dazu, dass die
|
||||
Wertespalte dort eine feste Mindestbreite hat - siehe .row-marke.) */
|
||||
.dm-marke {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dm-marke img {
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"2026.9.4.1","sha256":"473807fbe06bc94882a3b53fa76d167a64a15d9470c0a6f993c7042a8bf009b6","bytes":270085,"gebaut":"2026-09-04T00:09:13Z"}
|
||||
{"version":"2026.9.4.5","sha256":"ffea00a94ac842d8b6b233ebd91a1a97252c4b7b6a952c336ca7a6218eceef1e","bytes":270888,"gebaut":"2026-09-04T07:57:27Z"}
|
||||
Binary file not shown.
@@ -644,6 +644,11 @@ function ics(titel, datum, ort, notiz) {
|
||||
let MAP = null, TILE = null, LEAFLET_LADEND = null;
|
||||
// Zielpunkt des Zentrieren-Knopfs im Einzelbeleg (siehe mapBoxMitZentrieren).
|
||||
let BELEG_STATION_POS = null;
|
||||
/* Merker, ob zu EINEM Beleg überhaupt eine Position zu finden war. Der Abruf
|
||||
ist asynchron, die Ansicht aber schon gezeichnet - ohne diesen Merker
|
||||
stünde bei einem erfolglosen Nachschlag dauerhaft eine Karte ohne Nadel da
|
||||
(samt Knopf, der nie scharf wird). `{ id, zustand: "keine" }`, sonst null. */
|
||||
let BELEG_KARTE = null;
|
||||
/* Bewusst themeunabhaengig hell: Kartendienste (Google Maps & Co.) zeigen die
|
||||
Standard-Strassenkarte immer im hellen Stil, weil Strassen, Beschriftungen
|
||||
und Wegfarben darauf lesbar bleiben. Die frueher bei Nacht eingeblendeten
|
||||
@@ -843,18 +848,29 @@ async function initMap() {
|
||||
} else if (route.name === "fill") {
|
||||
BELEG_STATION_POS = null; // gehört zum vorherigen Beleg, sonst zentriert der Knopf falsch
|
||||
const f = FILLS.find((x) => x.tank_id === route.id);
|
||||
const suche = f ? tankstelleSuchtext(f.station_name, f.station_address) : "";
|
||||
if (f && f.station_lat != null) {
|
||||
tankstelleAufKarteZeigen(MAP, { lat: f.station_lat, lon: f.station_lon }, f);
|
||||
} else if (f && (f.station_address || f.station_name)) {
|
||||
/* Ohne Koordinaten die Anschrift vom Beleg nachschlagen. Bis die Antwort
|
||||
da ist, bleibt der neutrale Ausschnitt stehen - und die Karte könnte
|
||||
inzwischen zu einer anderen Ansicht gehören, deshalb vor dem Zeichnen
|
||||
prüfen, ob wir noch auf demselben Beleg sind. */
|
||||
} else if (suche) {
|
||||
/* Ohne Koordinaten die Anschrift vom Beleg nachschlagen - und wenn der
|
||||
Beleg keine trägt, den Namen (dieselbe Reihenfolge wie in der App,
|
||||
tankstelleSuchtext). Bis die Antwort da ist, bleibt der neutrale
|
||||
Ausschnitt stehen - und die Karte könnte inzwischen zu einer anderen
|
||||
Ansicht gehören, deshalb vor dem Zeichnen prüfen, ob wir noch auf
|
||||
demselben Beleg sind. */
|
||||
MAP.setView(kartenMittelpunkt(), 11);
|
||||
const karte = MAP, belegId = route.id;
|
||||
adresseAufloesen(f.station_address || f.station_name).then((pos) => {
|
||||
if (!pos || MAP !== karte || route.name !== "fill" || route.id !== belegId) return;
|
||||
tankstelleAufKarteZeigen(karte, pos, f);
|
||||
tankstellePosition(f.station_name, f.station_address).then((pos) => {
|
||||
if (MAP !== karte || route.name !== "fill" || route.id !== belegId) return;
|
||||
if (pos) return tankstelleAufKarteZeigen(karte, pos, f);
|
||||
/* Kein Treffer: statt eines beliebigen Ausschnitts mit totem
|
||||
Zentrieren-Knopf die ehrliche Auskunft. Genau die beiden Befunde
|
||||
des Eigentümers vom 04.09.2026 - "Map lädt die Position der
|
||||
Tankstelle nicht" und "der Button auf der Map funktioniert nicht"
|
||||
waren dieselbe Stelle: eine Karte ohne Nadel und ein Knopf, der
|
||||
nie scharf wurde. */
|
||||
BELEG_KARTE = { id: belegId, zustand: "keine" };
|
||||
render();
|
||||
});
|
||||
} else {
|
||||
MAP.setView(kartenMittelpunkt(), 11);
|
||||
@@ -1210,6 +1226,81 @@ async function adresseAufloesen(adresse) {
|
||||
return treffer;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------- Tankstelle
|
||||
Anzeige in zwei Zeilen und der Weg in den Kartendienst des Geraets.
|
||||
Wortgleich mit companion-app/src/daten/tankstelle.ts - dort steht die
|
||||
ausfuehrliche Begruendung.
|
||||
|
||||
Kurz: die Anschrift kommt vom Belegparser als "<Strasse>, <PLZ Ort>";
|
||||
geteilt wird am LETZTEN Komma, damit ein Zusatz bei der Strasse bleibt.
|
||||
Erste Zeile Tankstelle und Strasse, zweite Zeile der Ort (Vorgabe des
|
||||
Eigentuemers, 04.09.2026). Ohne Komma bleibt alles in der ersten Zeile -
|
||||
lieber ein langer erster Eintrag als ein erfundener Ort. */
|
||||
function adresseTeilen(adresse) {
|
||||
const roh = (adresse || "").trim();
|
||||
if (!roh) return { strasse: "", ort: "" };
|
||||
const komma = roh.lastIndexOf(",");
|
||||
if (komma < 0) return { strasse: roh, ort: "" };
|
||||
return { strasse: roh.slice(0, komma).trim(), ort: roh.slice(komma + 1).trim() };
|
||||
}
|
||||
|
||||
function tankstelleZeilen(name, adresse) {
|
||||
const { strasse, ort } = adresseTeilen(adresse);
|
||||
const oben = [(name || "").trim(), strasse].filter(Boolean).join(", ");
|
||||
return { oben: oben || "unbekannt", unten: ort };
|
||||
}
|
||||
|
||||
/* Suchbegriffe fuer die Geokodierung, in der Reihenfolge des Versuchs: erst
|
||||
die Anschrift, dann der Name. NICHT beides zusammen - am 04.09.2026
|
||||
gemessen: "Pascalstr. 8, 85057 Ingolstadt" findet Nominatim, mit dem
|
||||
Betreibernamen davor nicht. Der Name ALLEIN ist trotzdem wertvoll
|
||||
("Shell, SOELDEN" findet es), deshalb der zweite Versuch. */
|
||||
function tankstelleKandidaten(name, adresse) {
|
||||
return [(adresse || "").trim(), (name || "").trim()].filter(Boolean);
|
||||
}
|
||||
|
||||
/** Der erste Suchbegriff - leer heisst: der Beleg nennt gar nichts. */
|
||||
function tankstelleSuchtext(name, adresse) {
|
||||
return tankstelleKandidaten(name, adresse)[0] || "";
|
||||
}
|
||||
|
||||
/* Ziel fuer den Kartendienst. Hier gehoeren Name UND Anschrift zusammen:
|
||||
Apple Karten und Google Maps kennen Betriebsnamen und treffen damit die
|
||||
Zapfsaeule statt des Nachbargebaeudes. */
|
||||
function tankstelleZiel(name, adresse) {
|
||||
return [(name || "").trim(), (adresse || "").trim()].filter(Boolean).join(", ");
|
||||
}
|
||||
|
||||
/* Die Kandidaten der Reihe nach nachschlagen, erster Treffer gewinnt. */
|
||||
async function tankstellePosition(name, adresse) {
|
||||
for (const begriff of tankstelleKandidaten(name, adresse)) {
|
||||
const pos = await adresseAufloesen(begriff);
|
||||
if (pos) return pos;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Apple-Geraet? Dort ist Karten der Standarddienst. */
|
||||
function apfelgeraet() {
|
||||
const kennung = `${navigator.userAgent || ""} ${navigator.platform || ""}`;
|
||||
return /iPhone|iPad|iPod|Macintosh|Mac OS X/i.test(kennung);
|
||||
}
|
||||
|
||||
function kartendienstUrl(name, adresse, pos) {
|
||||
const beschriftung = (name || "").trim();
|
||||
const ziel = pos ? `${pos.lat},${pos.lon}` : tankstelleZiel(name, adresse);
|
||||
if (!ziel) return null;
|
||||
if (apfelgeraet()) {
|
||||
const q = pos && beschriftung ? `&q=${encodeURIComponent(beschriftung)}` : "";
|
||||
return `https://maps.apple.com/?daddr=${encodeURIComponent(ziel)}${q}`;
|
||||
}
|
||||
return `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(ziel)}`;
|
||||
}
|
||||
|
||||
function kartendienstUrlZuPunkt(pos) {
|
||||
return pos ? kartendienstUrl(null, null, pos) : null;
|
||||
}
|
||||
|
||||
/* Setzt Nadel und Ausschnitt, sobald die Koordinaten da sind. Getrennt von
|
||||
initMap(), weil der Abruf asynchron ist und die Karte bis dahin schon
|
||||
steht. */
|
||||
@@ -3107,13 +3198,24 @@ function vEinst() {
|
||||
Dieselben Felder gibt es unter Einstellungen → Geräte & Dienste → Audi Dashboard →
|
||||
Konfigurieren.</span>
|
||||
</div>` : ""}
|
||||
<div class="tile"><span class="label">Fahrt beenden</span>
|
||||
${(() => {
|
||||
/* Der Regler schreibt NICHT mehr beim Loslassen. Sein Wert stellt zugleich
|
||||
den Schlaf-Timeout des Dongles - eine Änderung am Fahrzeug, die über zwei
|
||||
Netze geht; die soll bewusst ausgelöst werden und nicht als Nebenwirkung
|
||||
einer Wischgeste entstehen (Vorgabe des Eigentümers, 04.09.2026). Der
|
||||
Knopf erscheint erst, wenn der Regler wirklich woanders steht - ein
|
||||
dauerhaft sichtbarer, meist wirkungsloser Knopf wäre Beiwerk. */
|
||||
const minuten = PAUSENZEIT_ENTWURF ?? CONFIG.pausenzeit;
|
||||
const geaendert = PAUSENZEIT_ENTWURF !== null && PAUSENZEIT_ENTWURF !== CONFIG.pausenzeit;
|
||||
return `<div class="tile"><span class="label">Fahrt beenden</span>
|
||||
<div class="schieber" style="margin-top:14px">
|
||||
<input type="range" min="1" max="60" step="1" value="${CONFIG.pausenzeit}"
|
||||
style="--fuell:${((CONFIG.pausenzeit - 1) / 59) * 100}%" data-pausenzeit
|
||||
<input type="range" min="1" max="60" step="1" value="${minuten}"
|
||||
style="--fuell:${((minuten - 1) / 59) * 100}%" data-pausenzeit
|
||||
aria-label="Minuten bis zum automatischen Fahrtende">
|
||||
<output>${CONFIG.pausenzeit} Min.</output>
|
||||
<output>${minuten} Min.</output>
|
||||
</div>
|
||||
${geaendert ? `<button class="aktion primaer" style="margin-top:14px" data-pausenzeitspeichern>Speichern</button>
|
||||
<button class="aktion" data-pausenzeitverwerfen>Verwerfen</button>` : ""}
|
||||
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">
|
||||
Hier können Sie einstellen, wie viele Minuten nach Ausschalten der Zündung die
|
||||
@@ -3121,7 +3223,8 @@ function vEinst() {
|
||||
des Dongles: so lange bleibt er nach dem Abstellen wach und wartet darauf,
|
||||
dass es gleich weitergeht. Danach schläft er — das spart Strom, kostet beim
|
||||
Fortsetzen aber ein paar Sekunden fürs Aufwachen.</span>
|
||||
</div>
|
||||
</div>`;
|
||||
})()}
|
||||
${DONGLE && DONGLE.aktiv ? `<div class="tile"><span class="label">Dongle</span>
|
||||
${DONGLE.stand && DONGLE.stand.fehler
|
||||
? `<span class="label" style="margin-top:8px;color:var(--bad);letter-spacing:0;
|
||||
@@ -3296,6 +3399,11 @@ function vEinst() {
|
||||
|
||||
let nmOffen = false;
|
||||
let einrichtenOffen = false;
|
||||
/* Entwurf des Reglers "Fahrt beenden" (Minuten) - null heisst: nicht angefasst.
|
||||
Er liegt bewusst NEBEN CONFIG.pausenzeit und nicht darin: erst der Knopf
|
||||
"Speichern" uebernimmt ihn, weil derselbe Wert den Schlaf-Timeout des
|
||||
Dongles stellt. Siehe die Kachel in vEinst(). */
|
||||
let PAUSENZEIT_ENTWURF = null;
|
||||
let bildMenuOffen = null;
|
||||
let smartdealPopupOffen = false;
|
||||
// "sommer"|"winter", welcher Reifensatz gerade sein km-Korrektur-Feld offen
|
||||
@@ -4403,6 +4511,48 @@ function vFuel() {
|
||||
${vFuelFormular()}`;
|
||||
}
|
||||
|
||||
/* Karte des Einzelbelegs - oder die Auskunft, dass es zu dieser Tankstelle
|
||||
keine Position gibt. Welches von beidem, entscheidet BELEG_KARTE: den
|
||||
Merker setzt initMap(), sobald der Nachschlag erfolglos zurueckkommt.
|
||||
Deckungsgleich mit TankstellenKarte.tsx in der App, samt der Unterscheidung
|
||||
zwischen "steht nicht im Beleg" und "findet niemand". */
|
||||
function belegKarte(f) {
|
||||
const suche = tankstelleSuchtext(f.station_name, f.station_address);
|
||||
const ohne =
|
||||
!suche || (BELEG_KARTE && BELEG_KARTE.id === f.tank_id && BELEG_KARTE.zustand === "keine");
|
||||
if (!ohne) return mapBoxMitZentrieren();
|
||||
return `<div class="tile"><div class="leer">
|
||||
<strong>Keine Position zur Tankstelle</strong>
|
||||
<p>${
|
||||
suche
|
||||
? "Zu dieser Anschrift ließ sich kein Kartenpunkt finden. Tragen Sie im Beleg unter „Werte bearbeiten“ eine genauere Anschrift ein, dann erscheint die Tankstelle hier."
|
||||
: "Dieser Beleg nennt weder Tankstelle noch Anschrift. Tragen Sie sie unter „Werte bearbeiten“ nach, dann erscheint die Tankstelle hier."
|
||||
}</p>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
/* Tankstelle des Belegs: eigene Zeile ueber die volle Kachelbreite statt der
|
||||
engen Wertespalte, zweizeilig und antippbar.
|
||||
|
||||
Vorher eine gewoehnliche .row: Name rechts in der Wertespalte (rows-fest,
|
||||
also 120px), die ganze Anschrift als Kleintext darunter - "Hermann Mogler
|
||||
Mineraloelg. GmbH, Heidenheimerstr.6, 89551 Koenigsbronn" stand dort
|
||||
vier- bis fuenfzeilig. Der Tipp fuehrt jetzt in den Kartendienst des
|
||||
Geraets; ohne Name und ohne Anschrift bleibt es eine stumme Zeile, denn
|
||||
ein Verweis, der nirgendwohin fuehrt, waere schlechter als keiner. */
|
||||
function tankstellenZeile(f) {
|
||||
const { oben, unten } = tankstelleZeilen(f.station_name, f.station_address);
|
||||
const url = kartendienstUrl(f.station_name, f.station_address);
|
||||
const inhalt =
|
||||
`<span class="tankstellenzeile-ort">${esc(oben)}${unten ? `<small>${esc(unten)}</small>` : ""}</span>` +
|
||||
(url ? `<svg class="chev" viewBox="0 0 6 10" aria-hidden="true"><polyline points="1,1 5,5 1,9"/></svg>` : "");
|
||||
return `<div class="row tankstellenzeile"><dt>Tankstelle</dt><dd>${
|
||||
url
|
||||
? `<a href="${esc(url)}" target="_blank" rel="noopener" aria-label="${esc(oben)} in Karten öffnen">${inhalt}</a>`
|
||||
: `<span>${inhalt}</span>`
|
||||
}</dd></div>`;
|
||||
}
|
||||
|
||||
function vFill(id) {
|
||||
const f = FILLS.find((x) => x.tank_id === id);
|
||||
if (!f) return `<div class="tile">Tankvorgang nicht gefunden</div>`;
|
||||
@@ -4420,7 +4570,7 @@ function vFill(id) {
|
||||
}
|
||||
|
||||
return `
|
||||
${mapBoxMitZentrieren()}
|
||||
${belegKarte(f)}
|
||||
<div class="tile flat" style="margin:18px 0 14px">
|
||||
<span class="label">Getankt</span>
|
||||
<div class="fig" style="font-size:52px;margin-top:10px">${f.liters != null ? de(f.liters, 2) : "–"}<span class="unit">l</span></div>
|
||||
@@ -4433,8 +4583,8 @@ function vFill(id) {
|
||||
<div class="row"><dt>Getankte Liter</dt><dd>${f.liters != null ? de(f.liters, 2) + " l" : "–"}</dd></div>
|
||||
<div class="row"><dt>Kosten</dt><dd>${f.fuel_total_eur != null ? eur(f.fuel_total_eur) + " €" : "–"}<small>real, nach Rabatt</small></dd></div>
|
||||
<div class="row"><dt>Preis je Liter</dt><dd>${berechneterPreis != null ? de(berechneterPreis, 2) + " €" : "–"}<small>Kosten ÷ Liter</small></dd></div>
|
||||
${CAR.smartdeal.aktiv && f.discount != null ? `<div class="row"><dt><span class="marke">${markenlogo()} SmartDeal-Ersparnis</span></dt><dd>${eur(f.discount)} €</dd></div>` : ""}
|
||||
<div class="row" style="border-bottom:0"><dt>Tankstelle</dt><dd>${esc(f.station_name || "unbekannt")}<small>${esc(f.station_address || "")}</small></dd></div>
|
||||
${CAR.smartdeal.aktiv && f.discount != null ? `<div class="row row-marke"><dt><span class="marke">${markenlogo()} SmartDeal-Ersparnis</span></dt><dd>${eur(f.discount)} €</dd></div>` : ""}
|
||||
${tankstellenZeile(f)}
|
||||
</dl>
|
||||
${belegFehler && belegFehler.context === f.tank_id ? `<span class="label" style="margin-top:14px;color:var(--red)">${esc(belegFehler.meldung)}</span>` : ""}
|
||||
<button class="aktion" data-tankbearbeiten>Werte bearbeiten</button>
|
||||
@@ -5140,6 +5290,10 @@ const go = (name, id) => {
|
||||
// no-op, falls die Daten längst da sind (datenLaden() rendert dann
|
||||
// ohnehin nichts doppelt Sichtbares).
|
||||
if (!DATEN_GELADEN) datenLaden(false);
|
||||
// Der Merker "zu diesem Beleg gibt es keine Position" gilt nur so lange, wie
|
||||
// man auf ihm steht: nach einer Korrektur der Anschrift soll erneut gesucht
|
||||
// werden, nicht die alte Absage stehen bleiben.
|
||||
BELEG_KARTE = null;
|
||||
// Ein unbestätigter Servicebuch-Entwurf (siehe data-sbneu) verlässt die
|
||||
// Anwendung nie ungespeichert - wird die Eintragsseite ohne Klick auf
|
||||
// "Speichern" verlassen, verwerfen wir ihn hier statt ihn liegen zu lassen
|
||||
@@ -5838,6 +5992,9 @@ function ereignisseVerdrahten() {
|
||||
distanz: w.distanz, kraftstoff: w.kraftstoff,
|
||||
receipt_key: beleg ? beleg.receipt_key : null, receipt_file: beleg ? beleg.receipt_file : null,
|
||||
});
|
||||
// Die Anschrift kann sich gerade geändert haben - dann soll die Karte
|
||||
// erneut suchen statt bei der alten Absage zu bleiben (siehe BELEG_KARTE).
|
||||
BELEG_KARTE = null;
|
||||
tankBearbeitenOffen = false; belegEntwurf = null; belegFehler = null; render(); return;
|
||||
}
|
||||
const belegBtn = e.target.closest("[data-beleg]");
|
||||
@@ -5988,6 +6145,15 @@ function ereignisseVerdrahten() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* Regler "Fahrt beenden": erst hier wird geschrieben. profilSpeichern()
|
||||
stellt ueber denselben Weg auch den Schlaf-Timeout des Dongles - genau
|
||||
deshalb hat der Regler einen eigenen Knopf bekommen. */
|
||||
if (e.target.closest("[data-pausenzeitspeichern]")) {
|
||||
if (PAUSENZEIT_ENTWURF !== null) CONFIG.pausenzeit = PAUSENZEIT_ENTWURF;
|
||||
PAUSENZEIT_ENTWURF = null;
|
||||
profilSpeichern(); render(); return;
|
||||
}
|
||||
if (e.target.closest("[data-pausenzeitverwerfen]")) { PAUSENZEIT_ENTWURF = null; render(); return; }
|
||||
const satz = e.target.closest("[data-satz]");
|
||||
if (satz) { serviceRufen("reifen_wechseln", { satz: satz.dataset.satz.toLowerCase() }); CAR.reifen.aktiv = satz.dataset.satz; render(); return; }
|
||||
const rfArch = e.target.closest("[data-reifenarchivieren]");
|
||||
@@ -6106,12 +6272,12 @@ function ereignisseVerdrahten() {
|
||||
ROOT.addEventListener("input", (e) => {
|
||||
if (e.target.dataset.pausenzeit !== undefined) {
|
||||
// Beim Ziehen nur die Anzeige und die Fuellung nachziehen, NICHT
|
||||
// render() und nicht speichern: ein Re-Render mitten in der Geste
|
||||
// erzeugt den Regler neu und der Daumen springt aus der Hand.
|
||||
// Gespeichert wird beim Loslassen (change-Zweig weiter unten) -
|
||||
// dieselbe Trennung wie beim Suchfeld direkt darunter.
|
||||
// render(): ein Re-Render mitten in der Geste erzeugt den Regler neu und
|
||||
// der Daumen springt aus der Hand. Dieselbe Trennung wie beim Suchfeld
|
||||
// direkt darunter. Gespeichert wird ueberhaupt nicht mehr hier, sondern
|
||||
// erst auf Knopfdruck (data-pausenzeitspeichern).
|
||||
const minuten = parseInt(e.target.value, 10);
|
||||
CONFIG.pausenzeit = minuten;
|
||||
PAUSENZEIT_ENTWURF = minuten;
|
||||
e.target.style.setProperty("--fuell", (minuten / 60) * 100 + "%");
|
||||
const feld = e.target.parentElement.querySelector("output");
|
||||
if (feld) feld.textContent = minuten === 0 ? "sofort" : minuten + " Min.";
|
||||
@@ -6316,7 +6482,9 @@ function ereignisseVerdrahten() {
|
||||
if (e.target.dataset.oelm !== undefined) { CAR.oel.monate = parseInt(e.target.value, 10); profilSpeichern(); return; }
|
||||
if (e.target.dataset.setupNurpassend !== undefined) { setupNurPassend = e.target.checked; render(); return; }
|
||||
if (e.target.dataset.startbild !== undefined) { CONFIG.startbild = e.target.value; profilSpeichern(); render(); return; }
|
||||
if (e.target.dataset.pausenzeit !== undefined) { CONFIG.pausenzeit = parseInt(e.target.value, 10); profilSpeichern(); return; }
|
||||
// Loslassen: nur den Entwurf festhalten und neu zeichnen, damit der
|
||||
// Speichern-Knopf erscheint. Geschrieben wird erst dort.
|
||||
if (e.target.dataset.pausenzeit !== undefined) { PAUSENZEIT_ENTWURF = parseInt(e.target.value, 10); render(); return; }
|
||||
if (e.target.dataset.svsel !== undefined) { svAuswahl = parseInt(e.target.value, 10); render(); return; }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -427,6 +427,29 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
|
||||
.row dd { margin: 0; font-size: 14px; color: var(--fg); text-align: right; }
|
||||
.row dd small { display: block; font-size: 12.5px; color: var(--fg3); margin-top: 3px; }
|
||||
.rows-fest dd { min-width: 120px; }
|
||||
|
||||
/* Tankstelle im Einzelbeleg (Vorgabe des Eigentuemers, 04.09.2026).
|
||||
|
||||
Vorher eine gewoehnliche Zeile: Name rechts in der 120px-Wertespalte, die
|
||||
ganze Anschrift als Kleintext darunter - "Hermann Mogler Mineraloelg. GmbH,
|
||||
Heidenheimerstr.6, 89551 Koenigsbronn" stand dort vier- bis fuenfzeilig.
|
||||
Hier bekommt sie die volle Kachelbreite unter der Beschriftung, zwei Zeilen
|
||||
(Tankstelle und Strasse / Ort), und fuehrt beim Tippen in den Kartendienst -
|
||||
deshalb der Chevron rechts. Deckungsgleich mit .dm-tankstellenzeile in der
|
||||
App. */
|
||||
.row.tankstellenzeile { flex-direction: column; align-items: stretch; gap: 4px; border-bottom: 0; }
|
||||
/* flex:0 0 auto hebt .rows-fest dt{flex:0 0 150px} auf: in einer Spalte
|
||||
ist die Flex-Basis die HOEHE - der Titel waere sonst 150px hoch. */
|
||||
.row.tankstellenzeile dt { font-size: 13px; color: var(--fg2); flex: 0 0 auto; }
|
||||
.row.tankstellenzeile dd { min-width: 0; text-align: left; flex: 0 0 auto; }
|
||||
.row.tankstellenzeile dd > a,
|
||||
.row.tankstellenzeile dd > span {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: var(--sp-3); color: inherit; text-decoration: none;
|
||||
}
|
||||
.row.tankstellenzeile dd > a:active { opacity: .6; }
|
||||
.tankstellenzeile-ort { display: block; font-size: 14px; color: var(--fg); line-height: 1.35; min-width: 0; }
|
||||
.tankstellenzeile-ort small { display: block; margin-top: 2px; font-size: 12.5px; color: var(--fg3); }
|
||||
/* .row-fussnote ist mit dem Umbau der Service-Seite auf .termingruppe
|
||||
entfallen (2026-08-18) - dort gibt es keine dt/dd-Spalten mehr, die um
|
||||
Platz konkurrieren. Die beiden Regeln dafuer standen seither ohne
|
||||
@@ -695,7 +718,16 @@ img.carfix.klein { aspect-ratio: auto; height: auto; }
|
||||
.dots i { width: 5px; height: 5px; border-radius: 50%; background: var(--line-strong); }
|
||||
.dots i.on { background: var(--red); }
|
||||
|
||||
.marke { display: inline-flex; align-items: center; gap: 6px; }
|
||||
/* white-space:nowrap: ohne die brach "SmartDeal-Ersparnis" neben dem
|
||||
Markenlogo um. Kein Platzmangel, sondern ein Rundungsfehler - am
|
||||
Einzelbeleg gemessen (04.09.2026, 375px breit): die dt-Spalte war 150,00px
|
||||
breit, ihr eigener Inhalt 150,20px, also rutschte der Text hinter dem Logo
|
||||
in eine zweite Zeile und die Zeile wurde 44,8px statt 26,4px hoch. Die
|
||||
Zeile hat reichlich Platz (307px fuer 150 + 16 + 55), sie darf ihn nur nicht
|
||||
abgeschnitten bekommen - dafuer gibt die Wertespalte in genau dieser Zeile
|
||||
ihre feste Mindestbreite auf (.row-marke). */
|
||||
.marke { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
||||
.rows-fest .row-marke dd { min-width: 0; }
|
||||
.marke-logo { display: inline-flex; align-items: center; }
|
||||
/* 19,2px = 16px + 20%. Der Eigentuemer wollte das Markenlogo in JEDER
|
||||
Darstellung um ein Fuenftel groesser; es steht ueberall neben Text der
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.9.4.1",
|
||||
"version": "2026.9.4.5",
|
||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||
"codeowners": ["@paul"],
|
||||
"codeowners": [
|
||||
"@paul"
|
||||
],
|
||||
"config_flow": true,
|
||||
"integration_type": "service",
|
||||
"single_config_entry": true,
|
||||
"iot_class": "local_polling",
|
||||
"dependencies": ["http", "frontend", "panel_custom", "recorder"],
|
||||
"requirements": ["pypdf>=4.0.0"]
|
||||
"dependencies": [
|
||||
"http",
|
||||
"frontend",
|
||||
"panel_custom",
|
||||
"recorder"
|
||||
],
|
||||
"requirements": [
|
||||
"pypdf>=4.0.0"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user