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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user