Files
audi-app/companion-app/src/screens/Versicherung.tsx
T
Paul Nothaft 8e6e5bc2ee Phase 7c: restliche Screens - alle 21 Seiten stehen
Fahrzeugdaten, Batterieverlauf, Service mit Werkstatt und Servicebuch,
Reifen, die ganze Versicherungs- und Steuergruppe, Einstellungen in voller
Tiefe und die Live-Ansicht.

Portiert dazu: die eigene Oelwechsel-Prognose (rechnet ab dem letzten
Wechsel im Servicebuch neu, weil der Langzeitschnitt dafuer zu traege ist),
Kalenderdateien fuer Termine und die CSV-Ausgabe im deutschen Format.

Die Live-Ansicht ist gebaut, aber ueber einen Schalter abgeschaltet: die
Werte dafuer liefert erst der FMM003. Ein Menuepunkt mit lauter Nullen waere
schlechter als keiner.

Zwei Stellen, an denen die App bewusst zurueckhaltend ist: ein unbekannter
Pruefpunkt erscheint als hohler Ring statt als geratenes Gruen, und der
Reifen-Kilometerstand wird nie zurueckgeschrieben, weil ihn der Server
fortschreibt.

49 Tests, darunter ein Rendertest ueber alle 21 Seiten in beiden Layouts
mit Beispieldaten in der Form, die das Backend wirklich liefert.
2026-08-11 11:17:47 +02:00

405 lines
12 KiB
TypeScript

/**
* Versicherung, Vertragsdetails, Schutzbrief, Notruf und Kfz-Steuer.
* Vorlagen: `vVers()`, `vBeitrag()`, `vVertragsdetails()`, `vSchutz()`,
* `vNotrufBearbeiten()`, `vSteuer()`.
*
* Alle Formulare hier schreiben über `profilSpeichern()` das **ganze** Profil
* zurück — das Backend kennt keinen Dienst für einzelne Felder.
*/
import { useState } from "react"
import { ActionButton, Feld, Tile } from "@audi-dash/ui"
import { useDaten } from "../daten/DatenKontext"
import { datum, de, eur } from "../format"
import type { SeitenName } from "../navigation"
import { Leerzustand, NaviKachel, Wertzeile, Werteliste } from "./bausteine"
interface Beitragsposten {
bez?: string
betrag?: number
sf?: string
satz?: string
}
export function Versicherung({ geheZu }: { geheZu: (name: SeitenName, id?: string) => void }) {
const { fahrzeug } = useDaten()
if (!fahrzeug) return null
const v = fahrzeug.versicherung
const steuer = fahrzeug.steuer
return (
<>
<Tile>
<span className="ads-eyebrow">Versicherung</span>
<Werteliste
kinder={
<>
<Wertzeile label="Gesellschaft" wert={(v["gesellschaft"] as string) || "—"} />
<Wertzeile label="Vertrag" wert={(v["vertrag"] as string) || "—"} />
<Wertzeile label="Vertragsnummer" wert={(v["nummer"] as string) || "—"} />
<Wertzeile
label="Beitrag"
wert={v["beitrag"] ? `${eur(v["beitrag"] as number)}` : "—"}
zusatz={(v["zahlweise"] as string) || undefined}
/>
<Wertzeile
label="Gültig ab"
wert={v["gueltigAb"] ? datum(v["gueltigAb"] as string) : "—"}
/>
</>
}
/>
</Tile>
<div className="dm-raster">
<NaviKachel titel="Beitrag" wert="Einzelposten bearbeiten" beiKlick={() => geheZu("beitrag")} />
<NaviKachel
titel="Vertragsdetails"
wert="Leistungen und Schutzbrief"
beiKlick={() => geheZu("vertragsdetails")}
/>
<NaviKachel
titel="Notruf"
wert={(v["notfall"] as string) || "Nummern hinterlegen"}
beiKlick={() => geheZu("notruf")}
/>
<NaviKachel
titel="Kfz-Steuer"
wert={steuer["betrag"] ? `${eur(steuer["betrag"] as number)}` : "Noch nicht hinterlegt"}
beiKlick={() => geheZu("steuer")}
/>
</div>
</>
)
}
export function Beitrag() {
const { fahrzeug, profilSpeichern } = useDaten()
const [laeuft, setzeLaeuft] = useState(false)
const [gespeichert, setzeGespeichert] = useState(false)
if (!fahrzeug) return null
const v = fahrzeug.versicherung
const teile = (v["teile"] ?? []) as Beitragsposten[]
const [entwurf, setzeEntwurf] = useState<Beitragsposten[]>(() => teile.map((t) => ({ ...t })))
const speichern = async () => {
setzeLaeuft(true)
setzeGespeichert(false)
try {
const summeBetrag = entwurf.reduce((s, t) => s + (Number(t.betrag) || 0), 0)
await profilSpeichern({
fahrzeug: {
...fahrzeug,
versicherung: { ...v, teile: entwurf, beitrag: summeBetrag },
},
})
setzeGespeichert(true)
} finally {
setzeLaeuft(false)
}
}
if (entwurf.length === 0) {
return (
<Leerzustand
titel="Keine Beitragsposten"
text="Sobald im Profil Einzelposten hinterlegt sind, kannst du sie hier bearbeiten."
/>
)
}
return (
<Tile>
<span className="ads-eyebrow">Beitragsposten</span>
{entwurf.map((posten, index) => (
<Feld
key={`${posten.bez}-${index}`}
label={posten.bez ?? `Posten ${index + 1}`}
unit="€"
last={index === entwurf.length - 1}
>
<input
className="dm-eingabe"
inputMode="decimal"
value={posten.betrag ?? ""}
onChange={(e) =>
setzeEntwurf((alt) =>
alt.map((p, i) =>
i === index ? { ...p, betrag: Number(e.target.value.replace(",", ".")) } : p,
),
)
}
/>
</Feld>
))}
<Werteliste
kinder={
<Wertzeile
label="Summe"
wert={`${eur(entwurf.reduce((s, t) => s + (Number(t.betrag) || 0), 0))}`}
/>
}
/>
{gespeichert && <p className="dm-erfolg">Gespeichert.</p>}
<div style={{ marginTop: 14 }}>
<ActionButton onClick={() => void speichern()} disabled={laeuft}>
{laeuft ? "Speichere …" : "Speichern"}
</ActionButton>
</div>
</Tile>
)
}
export function Vertragsdetails({ geheZu }: { geheZu: (name: SeitenName, id?: string) => void }) {
const { fahrzeug } = useDaten()
if (!fahrzeug) return null
const v = fahrzeug.versicherung
const leistungen = (v["leistungen"] ?? []) as { bez?: string; wert?: string }[]
const selbst = (v["selbst"] ?? []) as (string | number)[][]
return (
<>
<Tile>
<span className="ads-eyebrow">Leistungen</span>
{leistungen.length === 0 ? (
<p className="dm-fussnote">Im Profil sind noch keine Leistungen hinterlegt.</p>
) : (
<Werteliste
kinder={leistungen.map((l, i) => (
<Wertzeile key={`${l.bez}-${i}`} label={l.bez ?? "—"} wert={l.wert ?? "—"} />
))}
/>
)}
</Tile>
{selbst.length > 0 && (
<Tile>
<span className="ads-eyebrow">Selbstbeteiligung</span>
<Werteliste
kinder={selbst.map((zeile, i) => (
<Wertzeile key={i} label={String(zeile[0] ?? "")} wert={String(zeile[1] ?? "")} />
))}
/>
</Tile>
)}
<NaviKachel
titel="Schutzbrief"
wert="Bedingungen im Detail"
beiKlick={() => geheZu("schutz")}
/>
</>
)
}
export function Schutzbrief() {
const { fahrzeug } = useDaten()
if (!fahrzeug) return null
const s = (fahrzeug.versicherung["schutzbrief"] ?? {}) as Record<string, unknown>
const listen: [string, string[]][] = [
["Auslöser", (s["ausloeser"] ?? []) as string[]],
["Entweder", (s["entweder"] ?? []) as string[]],
["Leistungsgruppen", (s["gruppen"] ?? []) as string[]],
["Nicht versichert", (s["nicht"] ?? []) as string[]],
["Pflichten", (s["pflichten"] ?? []) as string[]],
]
const hatInhalt = listen.some(([, eintraege]) => eintraege.length > 0)
if (!hatInhalt) {
return (
<Leerzustand
titel="Keine Schutzbrief-Angaben"
text="Im Profil sind zum Schutzbrief noch keine Bedingungen hinterlegt."
/>
)
}
return (
<>
<Tile>
<span className="ads-eyebrow">Schutzbrief</span>
<Werteliste
kinder={
<>
<Wertzeile label="Stand" wert={(s["stand"] as string) || "—"} />
<Wertzeile label="Gilt" wert={(s["gilt"] as string) || "—"} />
<Wertzeile label="Versichert" wert={(s["versichert"] as string) || "—"} />
</>
}
/>
</Tile>
{listen
.filter(([, eintraege]) => eintraege.length > 0)
.map(([titel, eintraege]) => (
<Tile key={titel}>
<span className="ads-eyebrow">{titel}</span>
<ul className="dm-liste">
{eintraege.map((eintrag) => (
<li key={eintrag}>{eintrag}</li>
))}
</ul>
</Tile>
))}
</>
)
}
export function Notruf() {
const { fahrzeug, profilSpeichern } = useDaten()
const v = fahrzeug?.versicherung ?? {}
const [service, setzeService] = useState((v["service"] as string) ?? "")
const [notfall, setzeNotfall] = useState((v["notfall"] as string) ?? "")
const [laeuft, setzeLaeuft] = useState(false)
const [gespeichert, setzeGespeichert] = useState(false)
if (!fahrzeug) return null
const speichern = async () => {
setzeLaeuft(true)
setzeGespeichert(false)
try {
await profilSpeichern({
fahrzeug: { ...fahrzeug, versicherung: { ...v, service, notfall } },
})
setzeGespeichert(true)
} finally {
setzeLaeuft(false)
}
}
return (
<Tile>
<span className="ads-eyebrow">Nummern im Notfall</span>
<Feld label="Service">
<input
className="dm-eingabe"
type="tel"
value={service}
onChange={(e) => setzeService(e.target.value)}
/>
</Feld>
<Feld label="Notfall" last>
<input
className="dm-eingabe"
type="tel"
value={notfall}
onChange={(e) => setzeNotfall(e.target.value)}
/>
</Feld>
{(service || notfall) && (
<div className="dm-waehlen">
{service && (
<a className="dm-waehlen__knopf" href={`tel:${service.replace(/\s/g, "")}`}>
Service anrufen
</a>
)}
{notfall && (
<a className="dm-waehlen__knopf" href={`tel:${notfall.replace(/\s/g, "")}`}>
Notfall anrufen
</a>
)}
</div>
)}
{gespeichert && <p className="dm-erfolg">Gespeichert.</p>}
<div style={{ marginTop: 14 }}>
<ActionButton onClick={() => void speichern()} disabled={laeuft}>
{laeuft ? "Speichere …" : "Speichern"}
</ActionButton>
</div>
</Tile>
)
}
export function Steuer() {
const { fahrzeug, profilSpeichern } = useDaten()
const [entwurf, setzeEntwurf] = useState(() => ({ ...(fahrzeug?.steuer ?? {}) }))
const [laeuft, setzeLaeuft] = useState(false)
const [gespeichert, setzeGespeichert] = useState(false)
if (!fahrzeug) return null
const speichern = async () => {
setzeLaeuft(true)
setzeGespeichert(false)
try {
await profilSpeichern({
fahrzeug: { ...fahrzeug, steuer: entwurf as typeof fahrzeug.steuer },
})
setzeGespeichert(true)
} finally {
setzeLaeuft(false)
}
}
return (
<Tile>
<span className="ads-eyebrow">Kfz-Steuer</span>
<Feld label="Betrag" unit="€">
<input
className="dm-eingabe"
inputMode="decimal"
value={(entwurf["betrag"] as number) ?? ""}
onChange={(e) =>
setzeEntwurf((alt) => ({ ...alt, betrag: Number(e.target.value.replace(",", ".")) }))
}
/>
</Feld>
<Feld label="Zeitraum">
<input
className="dm-eingabe"
value={(entwurf["zeitraum"] as string) ?? ""}
onChange={(e) => setzeEntwurf((alt) => ({ ...alt, zeitraum: e.target.value }))}
/>
</Feld>
<Feld label="Fällig am">
<input
className="dm-eingabe"
type="date"
value={(entwurf["faellig"] as string) ?? ""}
onChange={(e) => setzeEntwurf((alt) => ({ ...alt, faellig: e.target.value }))}
/>
</Feld>
<Feld label="Finanzamt" last>
<input
className="dm-eingabe"
value={(entwurf["finanzamt"] as string) ?? ""}
onChange={(e) => setzeEntwurf((alt) => ({ ...alt, finanzamt: e.target.value }))}
/>
</Feld>
<Werteliste
kinder={
<>
<Wertzeile label="Grundlage" wert={(entwurf["grundlage"] as string) || "—"} />
<Wertzeile
label="Lastschrift"
wert={entwurf["lastschrift"] ? "eingerichtet" : "nicht eingerichtet"}
/>
<Wertzeile
label="Monatlich gerechnet"
wert={
entwurf["betrag"] ? `${eur((entwurf["betrag"] as number) / 12)}` : "—"
}
/>
</>
}
/>
{gespeichert && <p className="dm-erfolg">Gespeichert.</p>}
<div style={{ marginTop: 14 }}>
<ActionButton onClick={() => void speichern()} disabled={laeuft}>
{laeuft ? "Speichere …" : "Speichern"}
</ActionButton>
</div>
<p className="dm-fussnote">
{de(1)} Fahrzeug, {(entwurf["zeitraum"] as string) || "jährlich"} zu zahlen.
</p>
</Tile>
)
}