Paritaetsrunden 2 und 3, Designpruefung umgesetzt (2026.8.30.8)
Drei zusammenhaengende Runden, alle live bei 375x812 gegen audi_ha_test geprueft und in beiden Codebasen angewandt. Paritaetsrunde 2: der gemeinsame Rahmen war das eigentliche Problem - Seitenrand, Kachelabstaende, Kopfleiste, Tableiste und vier Bausteine des Design-Systems trugen noch den Stand vor der iOS-Entscheidung. Dazu sieben Bildschirme neu aufgebaut. Zwei Panel-Fehler dabei mitbehoben: teaser() zeigte unter "Letzte Fahrt" den aeltesten Datensatz, und "Daten bearbeiten" war ein toter Knopf. Paritaetsrunde 3: das Panel hat nie Audi Type gerendert. @font-face in einem Shadow Root wird ignoriert - Schriftschnitte registriert der Browser pro Dokument, nie pro Shadow Tree. Die drei Schnitte liegen jetzt in audi-dashboard-schriften.css und werden ins Dokument gehaengt. Damit erledigt sich eine ganze Reihe von "die Schrift sieht anders aus"-Eindruecken: die beiden Anwendungen zeigten tatsaechlich verschiedene Schriften. Ausserdem "Daten bearbeiten" im Panel gebaut und portiert, das Zeilenmenue entfernt und die letzten neun Bildschirme angeglichen. Designpruefung: die fuenf Punkte der Reihenfolge. Beim vierten hat das Messen den Befund veraendert - gezaehlt waren die deklarierten Groessen, wirksam war laengst eine saubere Sieben-Schritt-Skala mit 27 Ausreissern; die sind jetzt auf den naechsten Schritt gezogen, keiner verschiebt sich um mehr als 1px. Zuletzt: die Standortvorschau zeichnete die falsche Nadel (das Panel wechselt den Icon-Satz ab 34px, die App nahm immer den grossen), und der Kopfabstand der App ist auf den sicheren Bereich reduziert - die 56px des Panels liegen dort unter der Kopfleiste von Home Assistant, in der nativen Huelle steht darueber nichts. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -9,13 +9,16 @@
|
||||
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
|
||||
import { ActionButton, Feld, Pill, Tile } from "@audi-dash/ui"
|
||||
import { ActionButton, Feld, Fig, Tile } from "@audi-dash/ui"
|
||||
|
||||
import type { Tankvorgang } from "../api"
|
||||
import { ENTITAETEN } from "../api"
|
||||
import { useDaten } from "../daten/DatenKontext"
|
||||
import { datum, datumZeit, de, deOderStrich, eur } from "../format"
|
||||
import { KRAFTSTOFFSORTEN, Leerzustand, Wertzeile, Werteliste } from "./bausteine"
|
||||
import { datum, de, deOderStrich, eur, uhrzeit } from "../format"
|
||||
import type { SeitenName } from "../navigation"
|
||||
import { KRAFTSTOFFSORTEN, Leerzustand, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
||||
import { markenlogoUrl } from "./bilder"
|
||||
import { TankstellenKarte } from "./TankstellenKarte"
|
||||
|
||||
interface BelegErgebnis {
|
||||
ok?: boolean
|
||||
@@ -25,8 +28,8 @@ interface BelegErgebnis {
|
||||
[weitere: string]: unknown
|
||||
}
|
||||
|
||||
export function TankDetail({ id }: { id: string | undefined }) {
|
||||
const { tankvorgaenge, api } = useDaten()
|
||||
export function TankDetail({ id, geheZu }: { id: string | undefined; geheZu: (name: SeitenName) => void }) {
|
||||
const { tankvorgaenge, fahrzeug, api } = useDaten()
|
||||
const tank = tankvorgaenge.find((t) => t.tank_id === id)
|
||||
const [bearbeitenOffen, setzeBearbeitenOffen] = useState(false)
|
||||
|
||||
@@ -39,66 +42,100 @@ export function TankDetail({ id }: { id: string | undefined }) {
|
||||
)
|
||||
}
|
||||
|
||||
const loeschen = () => {
|
||||
if (!tank || !bestaetigen("Tankvorgang löschen?")) return
|
||||
void api.tankvorgangLoeschen(tank.tank_id)
|
||||
geheZu("fuel")
|
||||
}
|
||||
|
||||
if (bearbeitenOffen) {
|
||||
return <TankBearbeiten tank={tank} beiFertig={() => setzeBearbeitenOffen(false)} />
|
||||
}
|
||||
|
||||
/* Aufbau 1:1 wie vFill(): Karte, randlose „Getankt"-Kachel mit der
|
||||
52px-Zahl, dann eine einzige Wertezeilen-Kachel in der Reihenfolge des
|
||||
Panels (Zeitpunkt/Kilometerstand/Gefahrene Distanz/Kraftstoff/Getankte
|
||||
Liter/Kosten/Preis je Liter/SmartDeal-Ersparnis/Tankstelle) mit den drei
|
||||
Knöpfen darunter.
|
||||
|
||||
Vorher: eigene Kopfzeile mit Zeitstempel und Bearbeiten-Textknopf, Pillen
|
||||
für Status und Quelle (die das Panel hier nicht führt), andere Reihenfolge
|
||||
und Bezeichnungen („Gesamtbetrag", „Seit der letzten Tankung",
|
||||
„Listenpreis") und der Beleg in einer eigenen Kachel. */
|
||||
const preisJeLiter =
|
||||
tank.liters && tank.fuel_total_eur != null ? tank.fuel_total_eur / tank.liters : null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tile className="dm-kartenkachel">
|
||||
<TankstellenKarte adresse={tank.station_address} />
|
||||
</Tile>
|
||||
|
||||
<Tile variant="flat" className="dm-distanzkachel">
|
||||
<span className="ads-eyebrow">Getankt</span>
|
||||
<div style={{ marginTop: "10px" }}>
|
||||
<Fig value={deOderStrich(tank.liters, 2)} unit="l" size={52} />
|
||||
</div>
|
||||
</Tile>
|
||||
|
||||
<Tile>
|
||||
<div className="dm-abschnitt" style={{ marginBottom: 0 }}>
|
||||
<span className="ads-eyebrow">{datumZeit(tank.ts)}</span>
|
||||
<button type="button" className="dm-textknopf" onClick={() => setzeBearbeitenOffen(true)}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
</div>
|
||||
<div className="dm-detail__kopf">
|
||||
<div className="dm-detail__zahl dm-detail__zahl--gross">
|
||||
{deOderStrich(tank.liters, 2)}
|
||||
<span className="dm-detail__einheit">l</span>
|
||||
</div>
|
||||
<div className="dm-detail__marken">
|
||||
{tank.status === "unvollständig" && <Pill>unvollständig</Pill>}
|
||||
{tank.source === "beleg" && <Pill>aus Beleg</Pill>}
|
||||
{tank.source === "manual" && <Pill>selbst eingetragen</Pill>}
|
||||
{tank.source === "auto" && <Pill>automatisch erkannt</Pill>}
|
||||
</div>
|
||||
</div>
|
||||
<Werteliste
|
||||
kinder={
|
||||
<>
|
||||
<Wertzeile label="Zeitpunkt" wert={datum(tank.ts)} zusatz={`${uhrzeit(tank.ts)} Uhr`} />
|
||||
<Wertzeile
|
||||
label="Gesamtbetrag"
|
||||
wert={tank.fuel_total_eur != null ? `${eur(tank.fuel_total_eur)} €` : "—"}
|
||||
label="Kilometerstand"
|
||||
wert={tank.odometer_km != null ? `${de(tank.odometer_km)} km` : "unbekannt"}
|
||||
/>
|
||||
<Wertzeile
|
||||
label="Gefahrene Distanz"
|
||||
wert={tank.distance_km != null ? `${de(tank.distance_km)} km` : "unbekannt"}
|
||||
zusatz="seit der letzten Tankung"
|
||||
/>
|
||||
<Wertzeile label="Kraftstoff" wert={tank.fuel_type ?? KRAFTSTOFFSORTEN[0]} />
|
||||
<Wertzeile label="Getankte Liter" wert={deOderStrich(tank.liters, 2) + " l"} />
|
||||
<Wertzeile
|
||||
label="Kosten"
|
||||
wert={tank.fuel_total_eur != null ? `${eur(tank.fuel_total_eur)} €` : "–"}
|
||||
zusatz="real, nach Rabatt"
|
||||
/>
|
||||
<Wertzeile
|
||||
label="Preis je Liter"
|
||||
wert={tank.price_per_l != null ? `${eur(tank.price_per_l)} €` : "—"}
|
||||
zusatz={
|
||||
tank.list_price_per_l != null
|
||||
? `Listenpreis ${eur(tank.list_price_per_l)} €`
|
||||
: undefined
|
||||
}
|
||||
wert={preisJeLiter != null ? `${de(preisJeLiter, 2)} €` : "–"}
|
||||
zusatz="Kosten ÷ Liter"
|
||||
/>
|
||||
{tank.discount != null && tank.discount > 0 && (
|
||||
<Wertzeile label="Ersparnis" wert={`${eur(tank.discount)} €`} />
|
||||
{fahrzeug?.smartdeal.aktiv && tank.discount != null && (
|
||||
<Wertzeile
|
||||
label={
|
||||
<span className="dm-marke">
|
||||
<img
|
||||
src={markenlogoUrl()}
|
||||
alt=""
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = "none"
|
||||
}}
|
||||
/>
|
||||
SmartDeal-Ersparnis
|
||||
</span>
|
||||
}
|
||||
wert={`${eur(tank.discount)} €`}
|
||||
/>
|
||||
)}
|
||||
<Wertzeile label="Tankstelle" wert={tank.station_name ?? "—"} zusatz={tank.station_address ?? undefined} />
|
||||
<Wertzeile label="Kraftstoff" wert={tank.product_name ?? tank.fuel_type ?? "—"} />
|
||||
<Wertzeile
|
||||
label="Kilometerstand"
|
||||
wert={tank.odometer_km != null ? `${de(tank.odometer_km)} km` : "—"}
|
||||
/>
|
||||
<Wertzeile
|
||||
label="Seit der letzten Tankung"
|
||||
wert={tank.distance_km != null ? `${de(tank.distance_km)} km` : "—"}
|
||||
label="Tankstelle"
|
||||
wert={tank.station_name ?? "unbekannt"}
|
||||
zusatz={tank.station_address ?? ""}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ActionButton onClick={() => setzeBearbeitenOffen(true)}>Werte bearbeiten</ActionButton>
|
||||
<BelegKnopf tankId={tank.tank_id} belegDatei={tank.receipt_file ?? null} api={api} />
|
||||
</Tile>
|
||||
|
||||
<BelegBereich tankId={tank.tank_id} belegDatei={tank.receipt_file ?? null} belegNr={tank.receipt_no ?? null} zeitpunkt={tank.ts} api={api} />
|
||||
<ActionButton variant="destructive" onClick={() => void loeschen()}>
|
||||
Tankvorgang löschen
|
||||
</ActionButton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -266,17 +303,17 @@ function TankBearbeiten({ tank, beiFertig }: { tank: Tankvorgang; beiFertig: ()
|
||||
)
|
||||
}
|
||||
|
||||
function BelegBereich({
|
||||
/* Im Panel ist der Beleg kein eigener Abschnitt, sondern ein Knopf in der
|
||||
Wertezeilen-Kachel ("Beleg ersetzen"/"Beleg nachtragen"). Die frühere
|
||||
eigene Kachel mit Dateiname und Beleg-Nummer hatte dort keine
|
||||
Entsprechung. */
|
||||
function BelegKnopf({
|
||||
tankId,
|
||||
belegDatei,
|
||||
belegNr,
|
||||
zeitpunkt,
|
||||
api,
|
||||
}: {
|
||||
tankId: string
|
||||
belegDatei: string | null
|
||||
belegNr: string | null
|
||||
zeitpunkt: string
|
||||
api: ReturnType<typeof useDaten>["api"]
|
||||
}) {
|
||||
const dateiwahl = useRef<HTMLInputElement | null>(null)
|
||||
@@ -346,24 +383,7 @@ function BelegBereich({
|
||||
}
|
||||
|
||||
return (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Beleg</span>
|
||||
{belegDatei ? (
|
||||
<Werteliste
|
||||
kinder={
|
||||
<>
|
||||
<Wertzeile label="Hinterlegt" wert={belegDatei} />
|
||||
<Wertzeile label="Beleg-Nummer" wert={belegNr ?? "—"} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<p className="dm-fussnote">
|
||||
Für diese Tankung liegt noch kein Beleg vor. Aus einem Shell-Beleg liest die App Menge,
|
||||
Betrag, Tankstelle und Ersparnis selbst aus.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<>
|
||||
<input
|
||||
ref={dateiwahl}
|
||||
type="file"
|
||||
@@ -375,7 +395,6 @@ function BelegBereich({
|
||||
e.target.value = ""
|
||||
}}
|
||||
/>
|
||||
|
||||
{meldung && (
|
||||
<p
|
||||
className={meldung.art === "fehler" ? "dm-fehler" : "dm-erfolg"}
|
||||
@@ -384,17 +403,19 @@ function BelegBereich({
|
||||
{meldung.text}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<ActionButton onClick={() => dateiwahl.current?.click()} disabled={laeuft || wartet}>
|
||||
{laeuft ? "Lade hoch …" : wartet ? "Beleg wird gelesen …" : belegDatei ? "Beleg ersetzen" : "Beleg hochladen"}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<p className="dm-fussnote">
|
||||
Der Beleg muss vom {datum(zeitpunkt)} sein — sonst weist der Server ihn ab, damit keine
|
||||
fremde Tankung in diesen Eintrag gerät.
|
||||
</p>
|
||||
</Tile>
|
||||
<ActionButton onClick={() => dateiwahl.current?.click()} disabled={laeuft || wartet}>
|
||||
{laeuft
|
||||
? "Lade hoch …"
|
||||
: wartet
|
||||
? "Beleg wird gelesen …"
|
||||
: belegDatei
|
||||
? "Beleg ersetzen"
|
||||
: "Beleg nachtragen"}
|
||||
</ActionButton>
|
||||
{/* Der Datumshinweis darunter ist entfallen - im Panel steht an dieser
|
||||
Stelle nur im Fehlerfall eine Meldung (belegFehler), kein
|
||||
dauerhafter Erklärtext. */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user