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
|
# 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
|
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
|
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
|
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
|
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
|
vehicle, nothing gets recorded. The 13 existing entries are the owner's data; removal is via the
|
||||||
swipe gesture in the measurement list.
|
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
|
// 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.
|
// 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.
|
// 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
|
// 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
|
// temporäre Kopie. Ohne den zweiten Weg bliebe genau der Fall übrig, für
|
||||||
// den der Knopf gedacht ist.
|
// 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.
|
// DIESE DATEI IST VERSIONIERT, ios/ NICHT.
|
||||||
// `npx cap add ios` erzeugt den nativen Ordner neu und würde alles darin
|
// `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
|
if ! /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$PLIST" 2>/dev/null; then
|
||||||
/usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string $VERSION" "$PLIST"
|
/usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string $VERSION" "$PLIST"
|
||||||
fi
|
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.
|
# Zwischengespeicherte Bereitstellungsprofile dieser App wegraeumen.
|
||||||
#
|
#
|
||||||
@@ -252,6 +270,37 @@ xcodebuild -exportArchive \
|
|||||||
mkdir -p "$APP/auslieferung"
|
mkdir -p "$APP/auslieferung"
|
||||||
cp "$AUSGABE/App.ipa" "$APP/auslieferung/App.ipa"
|
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.
|
# Auslieferung ueber die Luft gleich mitschreiben.
|
||||||
#
|
#
|
||||||
# manifest.plist und die Installationsseite hingen bisher an einem zweiten,
|
# 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> {
|
export async function pdfAusZwischenablage(): Promise<File> {
|
||||||
if (Capacitor.isNativePlatform()) {
|
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 }
|
let ergebnis: { base64?: string; name?: string }
|
||||||
try {
|
try {
|
||||||
ergebnis = await Nativ.pdfHolen()
|
ergebnis = await Nativ.pdfHolen()
|
||||||
} catch {
|
} catch {
|
||||||
// Abgelehnte Einfügen-Rückfrage oder ein Gerät ohne das Plugin (eine
|
// Abgelehnte Einfügen-Rückfrage des Systems.
|
||||||
// .ipa von vor dem 02.09.2026).
|
|
||||||
throw new ZwischenablageFehler(
|
throw new ZwischenablageFehler(
|
||||||
'Kein Zugriff auf die Zwischenablage - bitte "Datei auswählen".',
|
'Kein Zugriff auf die Zwischenablage - bitte "Datei auswählen".',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ export function Einstellungen({
|
|||||||
// eigener Entwurf, der beim Speichern mitgeschrieben wird.
|
// eigener Entwurf, der beim Speichern mitgeschrieben wird.
|
||||||
const [erstzulassung, setzeErstzulassung] = useState<string | null>(null)
|
const [erstzulassung, setzeErstzulassung] = useState<string | null>(null)
|
||||||
const [entwurf, setzeEntwurf] = useState<EinstellungenWerte | 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 [laeuft, setzeLaeuft] = useState(false)
|
||||||
const [gespeichert, setzeGespeichert] = useState(false)
|
const [gespeichert, setzeGespeichert] = useState(false)
|
||||||
const [importOffen, setzeImportOffen] = useState(false)
|
const [importOffen, setzeImportOffen] = useState(false)
|
||||||
@@ -187,6 +193,8 @@ export function Einstellungen({
|
|||||||
|
|
||||||
if (!einstellungen || !fahrzeug) return null
|
if (!einstellungen || !fahrzeug) return null
|
||||||
const werte = entwurf ?? einstellungen
|
const werte = entwurf ?? einstellungen
|
||||||
|
const pausenzeit = pausenzeitEntwurf ?? werte.pausenzeitMin
|
||||||
|
const pausenzeitGeaendert = pausenzeitEntwurf !== null && pausenzeitEntwurf !== werte.pausenzeitMin
|
||||||
const erstzulassungWert = erstzulassung ?? fahrzeug.erstzulassung
|
const erstzulassungWert = erstzulassung ?? fahrzeug.erstzulassung
|
||||||
// Bei Winterraedern zeigt die Seitenansicht das Winterfoto - wie bildInfo()
|
// Bei Winterraedern zeigt die Seitenansicht das Winterfoto - wie bildInfo()
|
||||||
// im Panel, das hier dieselbe Vorschau liefert.
|
// im Panel, das hier dieselbe Vorschau liefert.
|
||||||
@@ -209,17 +217,22 @@ export function Einstellungen({
|
|||||||
const aendern = (teil: Partial<EinstellungenWerte>) =>
|
const aendern = (teil: Partial<EinstellungenWerte>) =>
|
||||||
setzeEntwurf({ ...werte, ...teil })
|
setzeEntwurf({ ...werte, ...teil })
|
||||||
|
|
||||||
const speichern = async () => {
|
/* `zusatz` kommt vom Regler der Kachel "Fahrt beenden": der fuehrt einen
|
||||||
if (!entwurf) return
|
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)
|
setzeLaeuft(true)
|
||||||
setzeGespeichert(false)
|
setzeGespeichert(false)
|
||||||
try {
|
try {
|
||||||
await profilSpeichern({
|
await profilSpeichern({
|
||||||
einstellungen: entwurf,
|
einstellungen: { ...werte, ...(zusatz ?? {}) },
|
||||||
...(erstzulassung !== null ? { fahrzeug: { ...fahrzeug, erstzulassung } } : {}),
|
...(erstzulassung !== null ? { fahrzeug: { ...fahrzeug, erstzulassung } } : {}),
|
||||||
})
|
})
|
||||||
setzeEntwurf(null)
|
setzeEntwurf(null)
|
||||||
setzeErstzulassung(null)
|
setzeErstzulassung(null)
|
||||||
|
setzePausenzeitEntwurf(null)
|
||||||
setzeGespeichert(true)
|
setzeGespeichert(true)
|
||||||
} finally {
|
} finally {
|
||||||
setzeLaeuft(false)
|
setzeLaeuft(false)
|
||||||
@@ -462,11 +475,33 @@ export function Einstellungen({
|
|||||||
min={1}
|
min={1}
|
||||||
max={60}
|
max={60}
|
||||||
schritt={1}
|
schritt={1}
|
||||||
wert={werte.pausenzeitMin}
|
wert={pausenzeit}
|
||||||
onChange={(min) => aendern({ pausenzeitMin: min })}
|
onChange={setzePausenzeitEntwurf}
|
||||||
anzeige={werte.pausenzeitMin + " Min."}
|
anzeige={pausenzeit + " Min."}
|
||||||
aria-label="Minuten bis zum automatischen Fahrtende"
|
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
|
{/* Fusszeile statt Info-Knopf: Apples eigenes Muster fuer erklaerenden
|
||||||
Text unter einer Einstellung ist der Footer der Gruppe. */}
|
Text unter einer Einstellung ist der Footer der Gruppe. */}
|
||||||
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
|
<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 { ActionButton, Feld, Fig, Tile } from "@audi-dash/ui"
|
||||||
|
|
||||||
|
import { kartendienstUrl } from "../daten/tankstelle"
|
||||||
import { useDaten } from "../daten/DatenKontext"
|
import { useDaten } from "../daten/DatenKontext"
|
||||||
import {
|
import {
|
||||||
inspektionPrognose,
|
inspektionPrognose,
|
||||||
@@ -213,7 +214,7 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
|
|||||||
className="ads-action dm-waehlknopf"
|
className="ads-action dm-waehlknopf"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
href={`https://maps.google.com/?q=${encodeURIComponent(autohaus.adresse ?? "")}`}
|
href={kartendienstUrl(autohaus.name, autohaus.adresse) ?? undefined}
|
||||||
>
|
>
|
||||||
In Karten öffnen
|
In Karten öffnen
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } f
|
|||||||
|
|
||||||
import { useDaten } from "../daten/DatenKontext"
|
import { useDaten } from "../daten/DatenKontext"
|
||||||
import { koordinatenAufloesen } from "../daten/geokodierung"
|
import { koordinatenAufloesen } from "../daten/geokodierung"
|
||||||
|
import { kartendienstUrlZuPunkt } from "../daten/tankstelle"
|
||||||
import { de } from "../format"
|
import { de } from "../format"
|
||||||
import type { Fahrzeug } from "../daten/profilAdapter"
|
import type { Fahrzeug } from "../daten/profilAdapter"
|
||||||
import { SymbolTanken } from "../symbole"
|
import { SymbolTanken } from "../symbole"
|
||||||
@@ -450,9 +451,9 @@ export function Standort() {
|
|||||||
const koordinaten = fahrzeugPos ? `${de(fahrzeugPos.lat, 4)}, ${de(fahrzeugPos.lon, 4)}` : ""
|
const koordinaten = fahrzeugPos ? `${de(fahrzeugPos.lat, 4)}, ${de(fahrzeugPos.lon, 4)}` : ""
|
||||||
const adresseAnzeige = !bekannt ? "Kein GPS-Signal vom Fahrzeug" : (adresse ?? koordinaten)
|
const adresseAnzeige = !bekannt ? "Kein GPS-Signal vom Fahrzeug" : (adresse ?? koordinaten)
|
||||||
const zustand = standortZustand(fahrzeug)
|
const zustand = standortZustand(fahrzeug)
|
||||||
const kartenlink = fahrzeugPos
|
// Kartendienst des Geraets statt fest Google Maps: auf dem iPhone ist das
|
||||||
? `https://www.google.com/maps/dir/?api=1&destination=${fahrzeugPos.lat},${fahrzeugPos.lon}`
|
// Karten. Dieselbe Wahl wie beim Tankstellen-Verweis (daten/tankstelle.ts).
|
||||||
: null
|
const kartenlink = kartendienstUrlZuPunkt(fahrzeugPos)
|
||||||
|
|
||||||
const teilen = () => {
|
const teilen = () => {
|
||||||
if (!kartenlink) return
|
if (!kartenlink) return
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { ActionButton, Feld, Fig, Tile } from "@audi-dash/ui"
|
|||||||
import type { Tankvorgang } from "../api"
|
import type { Tankvorgang } from "../api"
|
||||||
import { ENTITAETEN } from "../api"
|
import { ENTITAETEN } from "../api"
|
||||||
import { useDaten } from "../daten/DatenKontext"
|
import { useDaten } from "../daten/DatenKontext"
|
||||||
|
import { kartendienstUrl, tankstelleZeilen } from "../daten/tankstelle"
|
||||||
import { datum, de, deOderStrich, eur, uhrzeit } from "../format"
|
import { datum, de, deOderStrich, eur, uhrzeit } from "../format"
|
||||||
import type { SeitenName } from "../navigation"
|
import type { SeitenName } from "../navigation"
|
||||||
import { KRAFTSTOFFSORTEN, Leerzustand, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
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)} €`}
|
wert={`${eur(tank.discount)} €`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Wertzeile
|
<Tankstellenzeile name={tank.station_name} adresse={tank.station_address} />
|
||||||
label="Tankstelle"
|
|
||||||
wert={tank.station_name ?? "unbekannt"}
|
|
||||||
zusatz={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">. */
|
/** "YYYY-MM-DDTHH:mm" in Ortszeit für ein <input type="datetime-local">. */
|
||||||
function alsDatetimeLocal(iso: string): string {
|
function alsDatetimeLocal(iso: string): string {
|
||||||
const d = new Date(iso)
|
const d = new Date(iso)
|
||||||
|
|||||||
@@ -3,15 +3,29 @@
|
|||||||
*
|
*
|
||||||
* Die Koordinaten stehen nicht im Datensatz — der Beleg trägt nur eine
|
* 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
|
* Anschrift, und das Backend füllt `station_lat`/`station_lon` nie. Sie werden
|
||||||
* deshalb aus der Adresse aufgelöst (`adresseAufloesen()`), genau wie im
|
* deshalb aus der Anschrift aufgelöst (`adresseAufloesen()`), genau wie im
|
||||||
* Panel. Ohne Adresse oder ohne Treffer bleibt der ehrliche Leerzustand
|
* Panel.
|
||||||
* stehen, statt irgendeinen Kartenausschnitt zu zeigen.
|
*
|
||||||
|
* 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 { useEffect, useState } from "react"
|
||||||
|
|
||||||
import { adresseAufloesen } from "../daten/geokodierung"
|
import { adresseAufloesen } from "../daten/geokodierung"
|
||||||
import type { Koordinate } from "../daten/geokodierung"
|
import type { Koordinate } from "../daten/geokodierung"
|
||||||
|
import { tankstelleKandidaten } from "../daten/tankstelle"
|
||||||
import { Leerzustand } from "./bausteine"
|
import { Leerzustand } from "./bausteine"
|
||||||
import { Karte } from "./Karte"
|
import { Karte } from "./Karte"
|
||||||
|
|
||||||
@@ -24,19 +38,33 @@ export function TankstellenKarte({
|
|||||||
}) {
|
}) {
|
||||||
const [position, setzePosition] = useState<Koordinate | null>(null)
|
const [position, setzePosition] = useState<Koordinate | null>(null)
|
||||||
const [geprueft, setzeGeprueft] = useState(false)
|
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(() => {
|
useEffect(() => {
|
||||||
let abgebrochen = false
|
let abgebrochen = false
|
||||||
setzeGeprueft(false)
|
setzeGeprueft(false)
|
||||||
void adresseAufloesen(adresse).then((treffer) => {
|
const versuchen = async () => {
|
||||||
if (abgebrochen) return
|
for (const begriff of suche ? suche.split("\n") : []) {
|
||||||
setzePosition(treffer)
|
const treffer = await adresseAufloesen(begriff)
|
||||||
setzeGeprueft(true)
|
if (abgebrochen) return
|
||||||
})
|
if (treffer) {
|
||||||
|
setzePosition(treffer)
|
||||||
|
setzeGeprueft(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!abgebrochen) {
|
||||||
|
setzePosition(null)
|
||||||
|
setzeGeprueft(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void versuchen()
|
||||||
return () => {
|
return () => {
|
||||||
abgebrochen = true
|
abgebrochen = true
|
||||||
}
|
}
|
||||||
}, [adresse])
|
}, [suche])
|
||||||
|
|
||||||
// Wie im Panel (tankstelleAufKarteZeigen): die CI-Nadel mit Name und
|
// Wie im Panel (tankstelleAufKarteZeigen): die CI-Nadel mit Name und
|
||||||
// Anschrift im Popup, dazu der Knopf, der wieder darauf zentriert.
|
// 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
|
// 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
|
// Karte steht dort schon, die Nadel kommt nach). Bewusst nicht die
|
||||||
// Ladeanzeige der Seite - die meldet „der Bildschirm ist noch nicht da".
|
// 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 (
|
return (
|
||||||
<Leerzustand
|
<Leerzustand
|
||||||
titel="Keine Position zur Tankstelle"
|
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;
|
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 {
|
.dm-abschnitt {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
@@ -1531,6 +1580,20 @@ button.dm-listenzeile {
|
|||||||
border-top: 1px solid var(--line);
|
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 {
|
.dm-szene__zusatz {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@@ -1913,10 +1976,18 @@ button.dm-listenzeile {
|
|||||||
|
|
||||||
/* `.marke` im Panel: Bildmarke und Text nebeneinander, z. B. vor
|
/* `.marke` im Panel: Bildmarke und Text nebeneinander, z. B. vor
|
||||||
„SmartDeal-Ersparnis" auf dem Einzelbeleg. */
|
„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 {
|
.dm-marke {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dm-marke img {
|
.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;
|
let MAP = null, TILE = null, LEAFLET_LADEND = null;
|
||||||
// Zielpunkt des Zentrieren-Knopfs im Einzelbeleg (siehe mapBoxMitZentrieren).
|
// Zielpunkt des Zentrieren-Knopfs im Einzelbeleg (siehe mapBoxMitZentrieren).
|
||||||
let BELEG_STATION_POS = null;
|
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
|
/* Bewusst themeunabhaengig hell: Kartendienste (Google Maps & Co.) zeigen die
|
||||||
Standard-Strassenkarte immer im hellen Stil, weil Strassen, Beschriftungen
|
Standard-Strassenkarte immer im hellen Stil, weil Strassen, Beschriftungen
|
||||||
und Wegfarben darauf lesbar bleiben. Die frueher bei Nacht eingeblendeten
|
und Wegfarben darauf lesbar bleiben. Die frueher bei Nacht eingeblendeten
|
||||||
@@ -843,18 +848,29 @@ async function initMap() {
|
|||||||
} else if (route.name === "fill") {
|
} else if (route.name === "fill") {
|
||||||
BELEG_STATION_POS = null; // gehört zum vorherigen Beleg, sonst zentriert der Knopf falsch
|
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 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) {
|
if (f && f.station_lat != null) {
|
||||||
tankstelleAufKarteZeigen(MAP, { lat: f.station_lat, lon: f.station_lon }, f);
|
tankstelleAufKarteZeigen(MAP, { lat: f.station_lat, lon: f.station_lon }, f);
|
||||||
} else if (f && (f.station_address || f.station_name)) {
|
} else if (suche) {
|
||||||
/* Ohne Koordinaten die Anschrift vom Beleg nachschlagen. Bis die Antwort
|
/* Ohne Koordinaten die Anschrift vom Beleg nachschlagen - und wenn der
|
||||||
da ist, bleibt der neutrale Ausschnitt stehen - und die Karte könnte
|
Beleg keine trägt, den Namen (dieselbe Reihenfolge wie in der App,
|
||||||
inzwischen zu einer anderen Ansicht gehören, deshalb vor dem Zeichnen
|
tankstelleSuchtext). Bis die Antwort da ist, bleibt der neutrale
|
||||||
prüfen, ob wir noch auf demselben Beleg sind. */
|
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);
|
MAP.setView(kartenMittelpunkt(), 11);
|
||||||
const karte = MAP, belegId = route.id;
|
const karte = MAP, belegId = route.id;
|
||||||
adresseAufloesen(f.station_address || f.station_name).then((pos) => {
|
tankstellePosition(f.station_name, f.station_address).then((pos) => {
|
||||||
if (!pos || MAP !== karte || route.name !== "fill" || route.id !== belegId) return;
|
if (MAP !== karte || route.name !== "fill" || route.id !== belegId) return;
|
||||||
tankstelleAufKarteZeigen(karte, pos, f);
|
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 {
|
} else {
|
||||||
MAP.setView(kartenMittelpunkt(), 11);
|
MAP.setView(kartenMittelpunkt(), 11);
|
||||||
@@ -1210,6 +1226,81 @@ async function adresseAufloesen(adresse) {
|
|||||||
return treffer;
|
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
|
/* Setzt Nadel und Ausschnitt, sobald die Koordinaten da sind. Getrennt von
|
||||||
initMap(), weil der Abruf asynchron ist und die Karte bis dahin schon
|
initMap(), weil der Abruf asynchron ist und die Karte bis dahin schon
|
||||||
steht. */
|
steht. */
|
||||||
@@ -3107,13 +3198,24 @@ function vEinst() {
|
|||||||
Dieselben Felder gibt es unter Einstellungen → Geräte & Dienste → Audi Dashboard →
|
Dieselben Felder gibt es unter Einstellungen → Geräte & Dienste → Audi Dashboard →
|
||||||
Konfigurieren.</span>
|
Konfigurieren.</span>
|
||||||
</div>` : ""}
|
</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">
|
<div class="schieber" style="margin-top:14px">
|
||||||
<input type="range" min="1" max="60" step="1" value="${CONFIG.pausenzeit}"
|
<input type="range" min="1" max="60" step="1" value="${minuten}"
|
||||||
style="--fuell:${((CONFIG.pausenzeit - 1) / 59) * 100}%" data-pausenzeit
|
style="--fuell:${((minuten - 1) / 59) * 100}%" data-pausenzeit
|
||||||
aria-label="Minuten bis zum automatischen Fahrtende">
|
aria-label="Minuten bis zum automatischen Fahrtende">
|
||||||
<output>${CONFIG.pausenzeit} Min.</output>
|
<output>${minuten} Min.</output>
|
||||||
</div>
|
</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;
|
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
|
||||||
text-transform:none;font-size:12.5px;line-height:1.6">
|
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
|
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,
|
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
|
dass es gleich weitergeht. Danach schläft er — das spart Strom, kostet beim
|
||||||
Fortsetzen aber ein paar Sekunden fürs Aufwachen.</span>
|
Fortsetzen aber ein paar Sekunden fürs Aufwachen.</span>
|
||||||
</div>
|
</div>`;
|
||||||
|
})()}
|
||||||
${DONGLE && DONGLE.aktiv ? `<div class="tile"><span class="label">Dongle</span>
|
${DONGLE && DONGLE.aktiv ? `<div class="tile"><span class="label">Dongle</span>
|
||||||
${DONGLE.stand && DONGLE.stand.fehler
|
${DONGLE.stand && DONGLE.stand.fehler
|
||||||
? `<span class="label" style="margin-top:8px;color:var(--bad);letter-spacing:0;
|
? `<span class="label" style="margin-top:8px;color:var(--bad);letter-spacing:0;
|
||||||
@@ -3296,6 +3399,11 @@ function vEinst() {
|
|||||||
|
|
||||||
let nmOffen = false;
|
let nmOffen = false;
|
||||||
let einrichtenOffen = 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 bildMenuOffen = null;
|
||||||
let smartdealPopupOffen = false;
|
let smartdealPopupOffen = false;
|
||||||
// "sommer"|"winter", welcher Reifensatz gerade sein km-Korrektur-Feld offen
|
// "sommer"|"winter", welcher Reifensatz gerade sein km-Korrektur-Feld offen
|
||||||
@@ -4403,6 +4511,48 @@ function vFuel() {
|
|||||||
${vFuelFormular()}`;
|
${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) {
|
function vFill(id) {
|
||||||
const f = FILLS.find((x) => x.tank_id === id);
|
const f = FILLS.find((x) => x.tank_id === id);
|
||||||
if (!f) return `<div class="tile">Tankvorgang nicht gefunden</div>`;
|
if (!f) return `<div class="tile">Tankvorgang nicht gefunden</div>`;
|
||||||
@@ -4420,7 +4570,7 @@ function vFill(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${mapBoxMitZentrieren()}
|
${belegKarte(f)}
|
||||||
<div class="tile flat" style="margin:18px 0 14px">
|
<div class="tile flat" style="margin:18px 0 14px">
|
||||||
<span class="label">Getankt</span>
|
<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>
|
<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>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>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>
|
<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>` : ""}
|
${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>` : ""}
|
||||||
<div class="row" style="border-bottom:0"><dt>Tankstelle</dt><dd>${esc(f.station_name || "unbekannt")}<small>${esc(f.station_address || "")}</small></dd></div>
|
${tankstellenZeile(f)}
|
||||||
</dl>
|
</dl>
|
||||||
${belegFehler && belegFehler.context === f.tank_id ? `<span class="label" style="margin-top:14px;color:var(--red)">${esc(belegFehler.meldung)}</span>` : ""}
|
${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>
|
<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
|
// no-op, falls die Daten längst da sind (datenLaden() rendert dann
|
||||||
// ohnehin nichts doppelt Sichtbares).
|
// ohnehin nichts doppelt Sichtbares).
|
||||||
if (!DATEN_GELADEN) datenLaden(false);
|
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
|
// Ein unbestätigter Servicebuch-Entwurf (siehe data-sbneu) verlässt die
|
||||||
// Anwendung nie ungespeichert - wird die Eintragsseite ohne Klick auf
|
// Anwendung nie ungespeichert - wird die Eintragsseite ohne Klick auf
|
||||||
// "Speichern" verlassen, verwerfen wir ihn hier statt ihn liegen zu lassen
|
// "Speichern" verlassen, verwerfen wir ihn hier statt ihn liegen zu lassen
|
||||||
@@ -5838,6 +5992,9 @@ function ereignisseVerdrahten() {
|
|||||||
distanz: w.distanz, kraftstoff: w.kraftstoff,
|
distanz: w.distanz, kraftstoff: w.kraftstoff,
|
||||||
receipt_key: beleg ? beleg.receipt_key : null, receipt_file: beleg ? beleg.receipt_file : null,
|
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;
|
tankBearbeitenOffen = false; belegEntwurf = null; belegFehler = null; render(); return;
|
||||||
}
|
}
|
||||||
const belegBtn = e.target.closest("[data-beleg]");
|
const belegBtn = e.target.closest("[data-beleg]");
|
||||||
@@ -5988,6 +6145,15 @@ function ereignisseVerdrahten() {
|
|||||||
}
|
}
|
||||||
return;
|
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]");
|
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; }
|
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]");
|
const rfArch = e.target.closest("[data-reifenarchivieren]");
|
||||||
@@ -6106,12 +6272,12 @@ function ereignisseVerdrahten() {
|
|||||||
ROOT.addEventListener("input", (e) => {
|
ROOT.addEventListener("input", (e) => {
|
||||||
if (e.target.dataset.pausenzeit !== undefined) {
|
if (e.target.dataset.pausenzeit !== undefined) {
|
||||||
// Beim Ziehen nur die Anzeige und die Fuellung nachziehen, NICHT
|
// Beim Ziehen nur die Anzeige und die Fuellung nachziehen, NICHT
|
||||||
// render() und nicht speichern: ein Re-Render mitten in der Geste
|
// render(): ein Re-Render mitten in der Geste erzeugt den Regler neu und
|
||||||
// erzeugt den Regler neu und der Daumen springt aus der Hand.
|
// der Daumen springt aus der Hand. Dieselbe Trennung wie beim Suchfeld
|
||||||
// Gespeichert wird beim Loslassen (change-Zweig weiter unten) -
|
// direkt darunter. Gespeichert wird ueberhaupt nicht mehr hier, sondern
|
||||||
// dieselbe Trennung wie beim Suchfeld direkt darunter.
|
// erst auf Knopfdruck (data-pausenzeitspeichern).
|
||||||
const minuten = parseInt(e.target.value, 10);
|
const minuten = parseInt(e.target.value, 10);
|
||||||
CONFIG.pausenzeit = minuten;
|
PAUSENZEIT_ENTWURF = minuten;
|
||||||
e.target.style.setProperty("--fuell", (minuten / 60) * 100 + "%");
|
e.target.style.setProperty("--fuell", (minuten / 60) * 100 + "%");
|
||||||
const feld = e.target.parentElement.querySelector("output");
|
const feld = e.target.parentElement.querySelector("output");
|
||||||
if (feld) feld.textContent = minuten === 0 ? "sofort" : minuten + " Min.";
|
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.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.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.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; }
|
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 { 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; }
|
.row dd small { display: block; font-size: 12.5px; color: var(--fg3); margin-top: 3px; }
|
||||||
.rows-fest dd { min-width: 120px; }
|
.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
|
/* .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
|
entfallen (2026-08-18) - dort gibt es keine dt/dd-Spalten mehr, die um
|
||||||
Platz konkurrieren. Die beiden Regeln dafuer standen seither ohne
|
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 { width: 5px; height: 5px; border-radius: 50%; background: var(--line-strong); }
|
||||||
.dots i.on { background: var(--red); }
|
.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; }
|
.marke-logo { display: inline-flex; align-items: center; }
|
||||||
/* 19,2px = 16px + 20%. Der Eigentuemer wollte das Markenlogo in JEDER
|
/* 19,2px = 16px + 20%. Der Eigentuemer wollte das Markenlogo in JEDER
|
||||||
Darstellung um ein Fuenftel groesser; es steht ueberall neben Text der
|
Darstellung um ein Fuenftel groesser; es steht ueberall neben Text der
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
{
|
{
|
||||||
"domain": "audi_dashboard",
|
"domain": "audi_dashboard",
|
||||||
"name": "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",
|
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||||
"codeowners": ["@paul"],
|
"codeowners": [
|
||||||
|
"@paul"
|
||||||
|
],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"dependencies": ["http", "frontend", "panel_custom", "recorder"],
|
"dependencies": [
|
||||||
"requirements": ["pypdf>=4.0.0"]
|
"http",
|
||||||
|
"frontend",
|
||||||
|
"panel_custom",
|
||||||
|
"recorder"
|
||||||
|
],
|
||||||
|
"requirements": [
|
||||||
|
"pypdf>=4.0.0"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user