8e6e5bc2ee
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.
405 lines
12 KiB
TypeScript
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>
|
|
)
|
|
}
|