Phase 7a: Uebersicht, Fahrzeugstatus und Mein Audi
Erste drei Screens auf der fertigen Datenschicht. Uebersicht mit Fahrzeugbild, Sicherungsstatus, Reichweite und Tankfuellstand, Kilometerstand samt Service-Faelligkeit und Vorschau auf letzte Fahrt und Tankung. Fahrzeugstatus zeigt die 16 Einzelpruefungen, wobei ein unbekannter Sensor als hohler Ring erscheint statt als gruen geratener Punkt - dieselbe Vorsicht, die schon das Backend walten laesst. Mein Audi buendelt Fotogalerie und Wege zu den Unterseiten. Dazu die geteilte Rechenlogik der Statistik portiert (Zeitraeume, Langzeitverbrauch mit Plausibilitaetsfenster, Gruppierung nach Jahr und Monat) und Formatierung im deutschen Format. Bild-Komponente kapselt eine Reibung zwischen App und Bibliothek: die App laeuft mit exactOptionalPropertyTypes, ein undefined an einem optionalen Prop waere sonst an jeder Aufrufstelle einzeln zu behandeln.
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Fahrzeugbild mit Platzhalter.
|
||||
*
|
||||
* Kapselt eine Reibung zwischen App und Bibliothek: Die App läuft mit
|
||||
* `exactOptionalPropertyTypes`, weshalb `src={undefined}` an einem `src?: string`
|
||||
* nicht zulässig ist. Statt an jeder Aufrufstelle bedingt zu verteilen, passiert
|
||||
* es hier einmal.
|
||||
*/
|
||||
|
||||
import { ImagePlaceholder } from "@audi-dash/ui"
|
||||
|
||||
import { bildUrl } from "./bilder"
|
||||
|
||||
export function Bild({
|
||||
datei,
|
||||
beschreibung,
|
||||
name,
|
||||
groesse,
|
||||
}: {
|
||||
datei: string | null | undefined
|
||||
beschreibung: string
|
||||
name: string
|
||||
groesse?: "default" | "mini" | "round"
|
||||
}) {
|
||||
const url = bildUrl(datei)
|
||||
return (
|
||||
<ImagePlaceholder
|
||||
{...(url ? { src: url } : {})}
|
||||
{...(groesse ? { size: groesse } : {})}
|
||||
alt={beschreibung}
|
||||
label={name}
|
||||
hint={datei ?? undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { useState } from "react"
|
||||
import { ActionButton, Feld, Tile } from "@audi-dash/ui"
|
||||
|
||||
import { ApiFehler, HassRest, basisUrlNormalisieren, zugangSpeichern } from "../api"
|
||||
import { zugangMerken } from "./zugang"
|
||||
|
||||
export function Einrichtung({ fertig }: { fertig: () => void }) {
|
||||
const [adresse, setzeAdresse] = useState("")
|
||||
@@ -37,6 +38,7 @@ export function Einrichtung({ fertig }: { fertig: () => void }) {
|
||||
// die nachweislich nicht funktionieren.
|
||||
await new HassRest(zugang).verbindungPruefen()
|
||||
await zugangSpeichern(zugang)
|
||||
await zugangMerken()
|
||||
fertig()
|
||||
} catch (ursache) {
|
||||
if (ursache instanceof ApiFehler && ursache.istAnmeldeproblem) {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Fahrzeugstatus im Detail. Vorlage: `vSicherheit()` im alten Panel.
|
||||
*
|
||||
* Zeigt die 16 Einzelprüfungen aus `_sicherheitscheck`. Entscheidend:
|
||||
* `ok === null` heißt „Sensor meldet nichts" und wird als unbekannt
|
||||
* dargestellt — nie als grün geraten. Deshalb ist auch die Gesamtaussage
|
||||
* unbekannt, sobald eine einzige Prüfung unbekannt ist.
|
||||
*/
|
||||
|
||||
import { StatusRow, Tile } from "@audi-dash/ui"
|
||||
|
||||
import { useDaten } from "../daten/DatenKontext"
|
||||
import { datumZeit } from "../format"
|
||||
import { Leerzustand } from "./bausteine"
|
||||
|
||||
export function Fahrzeugstatus() {
|
||||
const { fahrzeug, statusStand } = useDaten()
|
||||
if (!fahrzeug) return null
|
||||
|
||||
const punkte = fahrzeug.sicherheitscheck
|
||||
if (punkte.length === 0) {
|
||||
return (
|
||||
<Leerzustand
|
||||
titel="Keine Zustandsdaten"
|
||||
text="Das Fahrzeug meldet derzeit keine Tür-, Fenster- und Schlossdaten. Sobald es das tut, stehen sie hier."
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const unbekannt = punkte.filter((p) => p.ok === null).length
|
||||
const offen = punkte.filter((p) => p.ok === false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusRow
|
||||
status={fahrzeug.gesichert === true ? "ok" : fahrzeug.gesichert === false ? "bad" : "warn"}
|
||||
title={
|
||||
fahrzeug.gesichert === true
|
||||
? "Alles verschlossen"
|
||||
: fahrzeug.gesichert === false
|
||||
? `${offen.length} Stelle${offen.length === 1 ? "" : "n"} offen`
|
||||
: "Zustand nicht vollständig bekannt"
|
||||
}
|
||||
{...(statusStand ? { subtitle: `Stand ${datumZeit(statusStand)}` } : {})}
|
||||
/>
|
||||
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Einzelprüfungen</span>
|
||||
<ul className="dm-pruefliste">
|
||||
{punkte.map((punkt) => (
|
||||
<li key={punkt.label} className="dm-pruefliste__zeile">
|
||||
<span
|
||||
className={
|
||||
punkt.ok === true
|
||||
? "dm-punkt dm-punkt--ok"
|
||||
: punkt.ok === false
|
||||
? "dm-punkt dm-punkt--offen"
|
||||
: "dm-punkt dm-punkt--unbekannt"
|
||||
}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="dm-pruefliste__label">{punkt.label}</span>
|
||||
<span className="dm-pruefliste__wert">
|
||||
{punkt.ok === true ? "zu" : punkt.ok === false ? "offen" : "unbekannt"}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Tile>
|
||||
|
||||
{unbekannt > 0 && (
|
||||
<p className="dm-fussnote">
|
||||
{unbekannt === 1 ? "Eine Prüfung ist" : `${unbekannt} Prüfungen sind`} unbekannt, weil das
|
||||
Fahrzeug dazu nichts meldet. Solange das so ist, gilt die Gesamtaussage bewusst als
|
||||
unbekannt statt als sicher.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* „Mein Audi" — Sammelseite mit Fotogalerie und Wegen zu den Unterseiten.
|
||||
* Vorlage: `vAudi()` im alten Panel.
|
||||
*/
|
||||
|
||||
import { Tile } from "@audi-dash/ui"
|
||||
|
||||
import { useDaten } from "../daten/DatenKontext"
|
||||
import { de, deOderStrich } from "../format"
|
||||
import type { SeitenName } from "../navigation"
|
||||
import { NaviKachel } from "./bausteine"
|
||||
import { Bild } from "./Bild"
|
||||
import { BILDPLAETZE } from "./bilder"
|
||||
|
||||
export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) => void }) {
|
||||
const { fahrzeug, einstellungen } = useDaten()
|
||||
if (!fahrzeug || !einstellungen) return null
|
||||
|
||||
const aktiverSatz =
|
||||
fahrzeug.reifen.aktiv === "Winter" ? fahrzeug.reifen.winter : fahrzeug.reifen.sommer
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Fotogalerie</span>
|
||||
<div className="dm-galerie">
|
||||
{BILDPLAETZE.map((platz) => (
|
||||
<Bild
|
||||
key={platz.datei}
|
||||
datei={platz.datei}
|
||||
beschreibung={platz.beschreibung}
|
||||
name={platz.name}
|
||||
groesse="mini"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="dm-fussnote">
|
||||
Fotos verwaltest du in den Einstellungen. Fehlende Bilder zeigen ihren erwarteten
|
||||
Dateinamen, damit klar ist, was wohin gehört.
|
||||
</p>
|
||||
</Tile>
|
||||
|
||||
<div className="dm-raster">
|
||||
<NaviKachel
|
||||
titel="Fahrzeugdaten"
|
||||
wert={fahrzeug.fin || "Technik und Ausstattung"}
|
||||
beiKlick={() => geheZu("ident")}
|
||||
/>
|
||||
<NaviKachel
|
||||
titel="Batterie"
|
||||
wert={
|
||||
fahrzeug.batteriespannung != null
|
||||
? `${de(fahrzeug.batteriespannung, 2)} V`
|
||||
: "Kein Spannungssensor"
|
||||
}
|
||||
beiKlick={() => geheZu("battverlauf")}
|
||||
/>
|
||||
<NaviKachel titel="Service" wert="Termine und Servicebuch" beiKlick={() => geheZu("service")} />
|
||||
<NaviKachel
|
||||
titel="Reifen"
|
||||
wert={`${fahrzeug.reifen.aktiv} · ${deOderStrich(aktiverSatz.km)} km`}
|
||||
beiKlick={() => geheZu("reifen")}
|
||||
/>
|
||||
<NaviKachel
|
||||
titel="Versicherung & Steuer"
|
||||
wert="Beitrag, Vertrag, Kfz-Steuer"
|
||||
beiKlick={() => geheZu("vers")}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Übersicht — die Startseite. Vorlage: `vHome()` im alten Panel.
|
||||
*
|
||||
* Fahrzeugbild, Status „sicher abgestellt", Reichweite und Tankfüllstand,
|
||||
* Kilometerstand, nächster Service, Vorschau auf letzte Fahrt und letzte
|
||||
* Tankung. Ziehen zum Aktualisieren stößt eine Neuabfrage beim Fahrzeug an.
|
||||
*/
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import { Fig, ProgressBar, StatusRow, Tile } from "@audi-dash/ui"
|
||||
|
||||
import { useDaten } from "../daten/DatenKontext"
|
||||
import { datum, datumZeit, dauer, de, deOderStrich, eur } from "../format"
|
||||
import type { SeitenName } from "../navigation"
|
||||
import { Leerzustand, NaviKachel, Wertzeile, Werteliste } from "./bausteine"
|
||||
import { Bild } from "./Bild"
|
||||
|
||||
export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string) => void }) {
|
||||
const { fahrzeug, einstellungen, fahrten, tankvorgaenge, jetztAktualisieren, statusStand } =
|
||||
useDaten()
|
||||
const [aktualisiert, setzeAktualisiert] = useState(false)
|
||||
|
||||
if (!fahrzeug || !einstellungen) return null
|
||||
|
||||
const letzteFahrt = fahrten
|
||||
.slice()
|
||||
.sort((a, b) => new Date(b.ts_start).getTime() - new Date(a.ts_start).getTime())[0]
|
||||
const letzteTankung = tankvorgaenge
|
||||
.slice()
|
||||
.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())[0]
|
||||
|
||||
const sicherheitsstatus =
|
||||
fahrzeug.gesichert === true ? "ok" : fahrzeug.gesichert === false ? "bad" : "warn"
|
||||
const sicherheitstext =
|
||||
fahrzeug.gesichert === true
|
||||
? "Sicher abgestellt"
|
||||
: fahrzeug.gesichert === false
|
||||
? "Nicht vollständig verschlossen"
|
||||
: "Zustand unbekannt"
|
||||
|
||||
const aktualisieren = async () => {
|
||||
setzeAktualisiert(true)
|
||||
try {
|
||||
await jetztAktualisieren()
|
||||
} finally {
|
||||
setzeAktualisiert(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dm-szene">
|
||||
<Bild
|
||||
datei={einstellungen.startbild}
|
||||
beschreibung={einstellungen.fahrzeugtitel}
|
||||
name="Fahrzeugbild"
|
||||
/>
|
||||
<div className="dm-szene__text">
|
||||
<div className="ads-title">
|
||||
{einstellungen.fahrzeugtitel}{" "}
|
||||
{einstellungen.fahrzeugzusatz && (
|
||||
<span className="ads-sport">{einstellungen.fahrzeugzusatz}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="ads-eyebrow">
|
||||
{einstellungen.kennzeichen || "Kennzeichen nicht hinterlegt"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatusRow
|
||||
status={sicherheitsstatus}
|
||||
title={sicherheitstext}
|
||||
{...(statusStand ? { subtitle: `Stand ${datumZeit(statusStand)}` } : {})}
|
||||
onClick={() => geheZu("sicherheit")}
|
||||
/>
|
||||
|
||||
<div className="dm-raster">
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Reichweite</span>
|
||||
<Fig value={deOderStrich(fahrzeug.reichweite)} unit="km" size={40} />
|
||||
<ProgressBar percent={Math.max(0, Math.min(100, fahrzeug.tankPct))} />
|
||||
<div className="dm-tanktext">
|
||||
<span>{de(fahrzeug.tankPct)} % im Tank</span>
|
||||
<span>
|
||||
rund {de((einstellungen.tankvolumen * fahrzeug.tankPct) / 100, 1)} von{" "}
|
||||
{de(einstellungen.tankvolumen)} l
|
||||
</span>
|
||||
</div>
|
||||
</Tile>
|
||||
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Kilometerstand</span>
|
||||
<Fig
|
||||
value={fahrzeug.odoBekannt ? de(fahrzeug.odo) : "—"}
|
||||
unit="km"
|
||||
size={40}
|
||||
/>
|
||||
<Werteliste
|
||||
kinder={
|
||||
<>
|
||||
<Wertzeile
|
||||
label="Nächster Ölwechsel"
|
||||
wert={fahrzeug.oelwechselFaelligTs ? datum(fahrzeug.oelwechselFaelligTs) : "—"}
|
||||
zusatz={
|
||||
fahrzeug.oelwechselFaelligKm != null
|
||||
? `in ${de(Math.abs(fahrzeug.oelwechselFaelligKm))} km`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<Wertzeile
|
||||
label="Nächste Inspektion"
|
||||
wert={fahrzeug.inspektionFaelligTs ? datum(fahrzeug.inspektionFaelligTs) : "—"}
|
||||
zusatz={
|
||||
fahrzeug.inspektionFaelligKm != null
|
||||
? `in ${de(Math.abs(fahrzeug.inspektionFaelligKm))} km`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Tile>
|
||||
</div>
|
||||
|
||||
<div className="dm-raster">
|
||||
{letzteFahrt ? (
|
||||
<NaviKachel
|
||||
titel="Letzte Fahrt"
|
||||
wert={
|
||||
<>
|
||||
<div className="dm-navikachel__zahl">
|
||||
{deOderStrich(letzteFahrt.distance_km, 1)} km
|
||||
</div>
|
||||
<small>
|
||||
{datum(letzteFahrt.ts_start)} · {dauer(letzteFahrt.duration_s)}
|
||||
</small>
|
||||
</>
|
||||
}
|
||||
beiKlick={() => geheZu("trip", letzteFahrt.trip_id)}
|
||||
/>
|
||||
) : (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Letzte Fahrt</span>
|
||||
<Leerzustand
|
||||
titel="Noch keine Fahrt erfasst"
|
||||
text="Sobald du losfährst, erscheint hier die letzte Fahrt."
|
||||
/>
|
||||
</Tile>
|
||||
)}
|
||||
|
||||
{letzteTankung ? (
|
||||
<NaviKachel
|
||||
titel="Letzte Tankung"
|
||||
wert={
|
||||
<>
|
||||
<div className="dm-navikachel__zahl">
|
||||
{deOderStrich(letzteTankung.liters, 2)} l
|
||||
</div>
|
||||
<small>
|
||||
{datum(letzteTankung.ts)} ·{" "}
|
||||
{letzteTankung.fuel_total_eur != null
|
||||
? `${eur(letzteTankung.fuel_total_eur)} €`
|
||||
: "Betrag offen"}
|
||||
</small>
|
||||
</>
|
||||
}
|
||||
beiKlick={() => geheZu("fill", letzteTankung.tank_id)}
|
||||
/>
|
||||
) : (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Letzte Tankung</span>
|
||||
<Leerzustand
|
||||
titel="Noch keine Tankung erfasst"
|
||||
text="Tankvorgänge erkennt die App automatisch am steigenden Füllstand — oder du trägst sie selbst ein."
|
||||
/>
|
||||
</Tile>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="dm-aktionen">
|
||||
<button
|
||||
type="button"
|
||||
className="dm-textknopf"
|
||||
onClick={() => void aktualisieren()}
|
||||
disabled={aktualisiert}
|
||||
>
|
||||
{aktualisiert ? "Frage das Fahrzeug …" : "Jetzt beim Fahrzeug nachfragen"}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Kleine, über mehrere Seiten geteilte Bausteine. Alles, was mehr als einmal
|
||||
* gebraucht wird, aber zu speziell für das Design-System ist.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
import { ActionButton, Tile } from "@audi-dash/ui"
|
||||
|
||||
/**
|
||||
* Leerzustand. Aus dem Design-Auftrag: freundlich formuliert, mit einem
|
||||
* Hinweis, was als Nächstes passiert — nie nur „keine Daten".
|
||||
* Wird real gebraucht: Fahrten und Tankvorgänge sind zu Beginn leer.
|
||||
*/
|
||||
export function Leerzustand({
|
||||
titel,
|
||||
text,
|
||||
aktion,
|
||||
}: {
|
||||
titel: string
|
||||
text: string
|
||||
aktion?: { beschriftung: string; beiKlick: () => void }
|
||||
}) {
|
||||
return (
|
||||
<div className="dm-leer">
|
||||
<div className="dm-leer__titel">{titel}</div>
|
||||
<p className="dm-leer__text">{text}</p>
|
||||
{aktion && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<ActionButton onClick={aktion.beiKlick}>{aktion.beschriftung}</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Abschnittsüberschrift zwischen Kacheln. */
|
||||
export function Abschnitt({ titel, kinder }: { titel: string; kinder?: ReactNode }) {
|
||||
return (
|
||||
<div className="dm-abschnitt">
|
||||
<span className="ads-eyebrow">{titel}</span>
|
||||
{kinder}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Kachel, die zu einer Unterseite führt. */
|
||||
export function NaviKachel({
|
||||
titel,
|
||||
wert,
|
||||
beiKlick,
|
||||
}: {
|
||||
titel: string
|
||||
wert?: ReactNode
|
||||
beiKlick: () => void
|
||||
}) {
|
||||
return (
|
||||
<Tile variant="button" chevron onClick={beiKlick}>
|
||||
<span className="ads-eyebrow">{titel}</span>
|
||||
{wert !== undefined && <div className="dm-navikachel__wert">{wert}</div>}
|
||||
</Tile>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Wert mit Beschriftung. „—" statt eines erfundenen Werts, wenn nichts
|
||||
* bekannt ist — die Regel zieht sich durch die ganze App.
|
||||
*/
|
||||
export function Wertzeile({
|
||||
label,
|
||||
wert,
|
||||
zusatz,
|
||||
}: {
|
||||
label: ReactNode
|
||||
wert: ReactNode
|
||||
zusatz?: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="dm-wertzeile">
|
||||
<dt>{label}</dt>
|
||||
<dd>
|
||||
{wert}
|
||||
{zusatz && <small>{zusatz}</small>}
|
||||
</dd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Werteliste({ kinder }: { kinder: ReactNode }) {
|
||||
return <dl className="dm-werteliste">{kinder}</dl>
|
||||
}
|
||||
|
||||
/** Bestätigung vor unwiderruflichen Aktionen. */
|
||||
export function bestaetigen(frage: string): boolean {
|
||||
return window.confirm(frage)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Fahrzeugfotos. Die Dateinamen sind fest: `bilderverwaltung.py` prüft jeden
|
||||
* Upload gegen eine Positivliste und lehnt alles andere ab — hier steht
|
||||
* dieselbe Liste, damit die Oberfläche gar nicht erst etwas anbieten kann,
|
||||
* das der Server ablehnen würde.
|
||||
*/
|
||||
|
||||
import { zugangLesenSynchron } from "./zugang"
|
||||
|
||||
export interface Bildplatz {
|
||||
name: string
|
||||
beschreibung: string
|
||||
datei: string
|
||||
}
|
||||
|
||||
/** Die sieben Plätze der Fotogalerie unter „Mein Audi". */
|
||||
export const BILDPLAETZE: Bildplatz[] = [
|
||||
{ name: "Seitenansicht", beschreibung: "Auto von der Seite", datei: "seitenansicht.webp" },
|
||||
{ name: "Front schräg", beschreibung: "Auto von vorn schräg", datei: "front-schraeg.webp" },
|
||||
{ name: "Frontansicht", beschreibung: "Auto von vorn", datei: "frontansicht.webp" },
|
||||
{ name: "Heckansicht", beschreibung: "Auto von hinten", datei: "heckansicht.webp" },
|
||||
{ name: "Cockpit", beschreibung: "Innenraum Cockpit", datei: "cockpit.webp" },
|
||||
{ name: "Scheinwerfer", beschreibung: "Scheinwerfer", datei: "scheinwerfer.webp" },
|
||||
{ name: "Sitze", beschreibung: "Innenraum Sitze", datei: "sitze.webp" },
|
||||
]
|
||||
|
||||
/** Zusätzlich erlaubt, aber außerhalb der Galerie geführt. */
|
||||
export const WEITERE_BILDER = [
|
||||
"seitenansicht-winter.webp",
|
||||
"rad-sommer.webp",
|
||||
"rad-winter.webp",
|
||||
] as const
|
||||
|
||||
export const ERLAUBTE_DATEINAMEN: readonly string[] = [
|
||||
...BILDPLAETZE.map((b) => b.datei),
|
||||
...WEITERE_BILDER,
|
||||
]
|
||||
|
||||
/**
|
||||
* Adresse eines Fahrzeugfotos auf dem Server. Die Bilder liegen unter
|
||||
* `/local/bilder/` — anders als beim alten Panel läuft die App nicht
|
||||
* zwangsläufig auf derselben Adresse, deshalb die vollständige URL.
|
||||
*/
|
||||
export function bildUrl(dateiname: string | null | undefined): string | undefined {
|
||||
if (!dateiname) return undefined
|
||||
const basis = zugangLesenSynchron()?.basisUrl
|
||||
if (!basis) return undefined
|
||||
return `${basis}/local/bilder/${dateiname}`
|
||||
}
|
||||
@@ -5,7 +5,10 @@
|
||||
|
||||
import { useDaten } from "../daten/DatenKontext"
|
||||
import type { Route, SeitenName } from "../navigation"
|
||||
import { Fahrzeugstatus } from "./Fahrzeugstatus"
|
||||
import { MeinAudi } from "./MeinAudi"
|
||||
import { Platzhalter } from "./Platzhalter"
|
||||
import { Uebersicht } from "./Uebersicht"
|
||||
|
||||
export interface SeitenProps {
|
||||
route: Route
|
||||
@@ -18,8 +21,8 @@ export interface SeitenProps {
|
||||
export function SeiteFuer(props: SeitenProps) {
|
||||
const daten = useDaten()
|
||||
|
||||
// Erste Ladung: nichts anzeigen außer einem Hinweis. Ein leeres Gerüst
|
||||
// wäre schlimmer als ein ehrliches „lädt".
|
||||
// Erste Ladung: nichts anzeigen außer einem Hinweis. Ein leeres Gerüst wäre
|
||||
// schlimmer als ein ehrliches „lädt".
|
||||
if (!daten.bereit) {
|
||||
if (daten.ladefehler) {
|
||||
return (
|
||||
@@ -32,5 +35,16 @@ export function SeiteFuer(props: SeitenProps) {
|
||||
return <div className="dm-laden">Daten werden geladen …</div>
|
||||
}
|
||||
|
||||
return <Platzhalter route={props.route} geheZu={props.geheZu} />
|
||||
const { route, geheZu } = props
|
||||
|
||||
switch (route.name) {
|
||||
case "home":
|
||||
return <Uebersicht geheZu={geheZu} />
|
||||
case "sicherheit":
|
||||
return <Fahrzeugstatus />
|
||||
case "audi":
|
||||
return <MeinAudi geheZu={geheZu} />
|
||||
default:
|
||||
return <Platzhalter route={route} geheZu={geheZu} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Synchroner Zugriff auf die Basis-Adresse.
|
||||
*
|
||||
* Die Datenschicht liest Zugangsdaten bewusst asynchron (die native Ablage
|
||||
* unter iOS/Android ist es auch). Für Bild-URLs im Rendern wird die Adresse
|
||||
* aber synchron gebraucht — deshalb wird sie einmal beim Start gemerkt.
|
||||
*/
|
||||
|
||||
import { zugangLesen, type Zugang } from "../api"
|
||||
|
||||
let gemerkt: Zugang | null = null
|
||||
|
||||
export async function zugangMerken(): Promise<void> {
|
||||
gemerkt = await zugangLesen()
|
||||
}
|
||||
|
||||
export function zugangLesenSynchron(): Zugang | null {
|
||||
return gemerkt
|
||||
}
|
||||
|
||||
export function zugangVergessen(): void {
|
||||
gemerkt = null
|
||||
}
|
||||
Reference in New Issue
Block a user