Versicherung/Steuer: Wortlaut-Feinschliff und Vertrag komplett editierbar

Mein-Audi-Kachel "Versicherung/Steuer" (vAudi()) und Kfz-Steuer-Detail
(vSteuer()): "Zusammen" heisst jetzt "Summe", die Fusszeile "feste
Kosten im Jahr" darunter entfaellt ersatzlos. Die Kfz-Steuer-Zeile
zeigt "im Jahr" statt des rohen Datenwerts "jaehrlich", deckungsgleich
mit der Versicherungs-Zeile darueber - nur wenn steuer.zeitraum
tatsaechlich "jaehrlich" ist. Der gespeicherte Wert und die
"jaehrlich"/"halbjaehrlich"-Auswahl in vSteuerBearbeiten() bleiben
unveraendert, das war ein reiner Anzeige-Fix, keine Datenmodell-
Aenderung. companion-app hatte an keiner der beiden Stellen ein
Gegenstueck (kein "Zusammen"-Tile, keine Nur-Lese-Anzeige des
Zeitraums) - dokumentiert als vorbestehende strukturelle Luecke statt
kommentarlos uebersprungen.

Vertrag-Kachel: war komplett nur lesbar (Gesellschaft, Umfang,
Vertragsnummer, Selbstbeteiligung, Schadenfreiheitsklasse). Neue
vVertragBearbeiten()-Ansicht (Panel, Route "vertrag") ueber ein neues
Zahnrad auf der Vertrag-Kachel - alle Felder editierbar,
Selbstbeteiligung zusaetzlich mit Hinzufuegen/Loeschen pro Zeile (neue
.zeile-loeschen-Klasse, 44x44pt-Tippflaeche wie .km-edit). Die
aktuelle Schadenfreiheitsklasse bleibt bewusst nur ueber "Beitrag
anpassen" editierbar (Querverweis statt Duplikat), nur "zuvor" (sfAlt)
ist neu in der Vertrag-Ansicht.

companion-app: neue Vertrag()-Seite (Route "vertrag", eigene
NaviKachel auf der Versicherungs-Seite) nach dem dortigen etablierten
Muster (lokaler Entwurf-State + expliziter Speichern-Knopf statt
Panel-Autosave pro Feld - gleiches Verhalten, eigenes Idiom). Die
bisherige Nur-Lese-Selbstbeteiligung in Vertragsdetails() entfaellt,
da sie jetzt (editierbar) in Vertrag() lebt. Schadenfreiheitsklasse
wird in companion-app bewusst nicht ergaenzt: das Feld war dort noch
nie sichtbar, auch nicht lesend - eine komplette neue UI-Sektion dafuer
waere kein "Zeile editierbar machen" mehr, sondern ein neues Feature;
als Luecke dokumentiert statt still uebergangen.

tsc --noEmit sauber, companion-app-Tests 100/100 (inkl. dem
Alle-Seiten-Rendertest, der jetzt auch "vertrag" abdeckt), vite build
erfolgreich. Panel live im Docker-Testcontainer geprueft: Zeile
hinzufuegen/bearbeiten/loeschen, Daten bleiben nach Re-Render erhalten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 16:50:58 +02:00
parent 23b6defa68
commit f115af50ab
11 changed files with 347 additions and 24 deletions
+3
View File
@@ -23,6 +23,7 @@ export type SeitenName =
| "reifen"
| "vers"
| "beitrag"
| "vertrag"
| "vertragsdetails"
| "schutz"
| "notruf"
@@ -54,6 +55,7 @@ export const ZURUECK: Partial<Record<SeitenName, SeitenName>> = {
battverlauf: "audi",
vers: "audi",
beitrag: "vers",
vertrag: "vers",
vertragsdetails: "vers",
schutz: "vertragsdetails",
steuer: "vers",
@@ -80,6 +82,7 @@ export const TITEL: Record<SeitenName, string> = {
reifen: "Reifen",
vers: "Versicherung & Steuer",
beitrag: "Beitrag",
vertrag: "Vertrag",
vertragsdetails: "Vertragsdetails",
schutz: "Schutzbrief",
notruf: "Notruf",
+108 -12
View File
@@ -56,6 +56,11 @@ export function Versicherung({ geheZu }: { geheZu: (name: SeitenName, id?: strin
<div className="dm-raster">
<NaviKachel titel="Beitrag" wert="Einzelposten bearbeiten" beiKlick={() => geheZu("beitrag")} />
<NaviKachel
titel="Vertrag"
wert="Gesellschaft, Nummer, Selbstbeteiligung"
beiKlick={() => geheZu("vertrag")}
/>
<NaviKachel
titel="Vertragsdetails"
wert="Leistungen und Schutzbrief"
@@ -155,13 +160,115 @@ export function Beitrag() {
)
}
export function Vertrag() {
const { fahrzeug, profilSpeichern } = useDaten()
const v = fahrzeug?.versicherung ?? {}
const [gesellschaft, setzeGesellschaft] = useState((v["gesellschaft"] as string) ?? "")
const [umfang, setzeUmfang] = useState((v["vertrag"] as string) ?? "")
const [nummer, setzeNummer] = useState((v["nummer"] as string) ?? "")
const [selbst, setzeSelbst] = useState<string[][]>(() =>
((v["selbst"] as (string | number)[][]) ?? []).map(([b, w]) => [String(b ?? ""), String(w ?? "")]),
)
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, gesellschaft, vertrag: umfang, nummer, selbst },
},
})
setzeGespeichert(true)
} finally {
setzeLaeuft(false)
}
}
return (
<>
<Tile>
<span className="ads-eyebrow">Vertrag</span>
<Feld label="Gesellschaft">
<input
className="dm-eingabe"
value={gesellschaft}
onChange={(e) => setzeGesellschaft(e.target.value)}
/>
</Feld>
<Feld label="Umfang">
<input className="dm-eingabe" value={umfang} onChange={(e) => setzeUmfang(e.target.value)} />
</Feld>
<Feld label="Vertragsnummer" last>
<input className="dm-eingabe" value={nummer} onChange={(e) => setzeNummer(e.target.value)} />
</Feld>
</Tile>
<Tile>
<span className="ads-eyebrow">Selbstbeteiligung</span>
{selbst.map((zeile, i) => (
<Feld key={i} label={`Position ${i + 1}`} last={i === selbst.length - 1}>
<span className="dm-selbst-zeile">
<input
className="dm-eingabe"
style={{ minWidth: 84, maxWidth: 110 }}
placeholder="Bezeichnung"
value={zeile[0]}
onChange={(e) => {
const wert = e.target.value
setzeSelbst((alt) => alt.map((z, idx) => (idx === i ? [wert, z[1] ?? ""] : z)))
}}
/>
<input
className="dm-eingabe"
style={{ minWidth: 64, maxWidth: 90 }}
placeholder="Betrag"
value={zeile[1]}
onChange={(e) => {
const wert = e.target.value
setzeSelbst((alt) => alt.map((z, idx) => (idx === i ? [z[0] ?? "", wert] : z)))
}}
/>
<button
type="button"
className="dm-zeile-loeschen"
aria-label={`Position ${i + 1} löschen`}
onClick={() => setzeSelbst((alt) => alt.filter((_, idx) => idx !== i))}
>
×
</button>
</span>
</Feld>
))}
{selbst.length === 0 && <p className="dm-fussnote">Keine Selbstbeteiligung hinterlegt.</p>}
<div style={{ marginTop: 14 }}>
<ActionButton onClick={() => setzeSelbst((alt) => [...alt, ["", ""]])}>
+ Position hinzufügen
</ActionButton>
</div>
</Tile>
{gespeichert && <p className="dm-erfolg">Gespeichert.</p>}
<div style={{ marginTop: 14 }}>
<ActionButton onClick={() => void speichern()} disabled={laeuft}>
{laeuft ? "Speichere …" : "Speichern"}
</ActionButton>
</div>
</>
)
}
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 (
<>
@@ -178,17 +285,6 @@ export function Vertragsdetails({ geheZu }: { geheZu: (name: SeitenName, id?: st
)}
</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"
+3
View File
@@ -25,6 +25,7 @@ import {
Schutzbrief,
Steuer,
Versicherung,
Vertrag,
Vertragsdetails,
} from "./Versicherung"
@@ -78,6 +79,8 @@ export function SeiteFuer(props: SeitenProps) {
return <Versicherung geheZu={geheZu} />
case "beitrag":
return <Beitrag />
case "vertrag":
return <Vertrag />
case "vertragsdetails":
return <Vertragsdetails geheZu={geheZu} />
case "schutz":
+36
View File
@@ -24,6 +24,42 @@
color: var(--fg3);
}
/* Eine Zeile aus einer bearbeitbaren Liste (z. B. Selbstbeteiligung im
Vertrag) - zwei Eingaben plus Löschen-Knopf in einer Reihe, deckungsgleich
mit .feld/.zeile-loeschen im Panel (audi-dashboard.css). */
.dm-selbst-zeile {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
}
.dm-zeile-loeschen {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border: none;
background: none;
color: var(--fg3);
font-size: 18px;
line-height: 1;
padding: 0;
cursor: pointer;
flex: 0 0 auto;
}
.dm-zeile-loeschen::before {
content: "";
position: absolute;
inset: -11px;
}
.dm-zeile-loeschen:active {
color: var(--bad);
}
.dm-auswahl {
text-align: right;
appearance: none;