Finanzen: Vorschau, Steuer und Abos

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-01 15:12:18 +00:00
1 parent 88a98b858f
commit 3826f1ab72
9 files changed
+780 -1

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
import { FaTriangleExclamation } from 'react-icons/fa6'
import { Badge, Card, EmptyState } from '../ui'
import { aboView } from '../../lib/financeView'
/**
* Block 5: „Abos“. Einnahmen pro Monat (aktiv und Zahlung fehlgeschlagen), zahlende Kunden, gefährdete Einnahmen
* (nur wenn > 0) und die Liste Kunde · Produkt · Status · Betrag · nächste Abbuchung. Konnten die Abos nicht
* geladen werden, steht stattdessen die Meldung des Servers da (keine Zahlen).
*/
export default function AboCard({ abos }) {
const v = aboView(abos)
if (v.error) {
return (
<Card title="Abos" className="fin-abo-card">
<p className="fin-hint fin-hint-warn" role="alert">
<FaTriangleExclamation aria-hidden="true" /> {v.error}
</p>
</Card>
)
}
return (
<Card title="Abos" className="fin-abo-card" bodyStyle={{ padding: 0 }}>
<div className="fin-stats">
<span className="fin-amount fin-stat-value">{v.monthlyText}</span>
<span className="fin-stat-label">Abo-Einnahmen pro Monat</span>
<span className="fin-amount fin-stat-value">{v.payingText}</span>
<span className="fin-stat-label">zahlende Kunden</span>
{v.atRiskText && (
<>
<span className="fin-amount fin-stat-value fin-flag">{v.atRiskText}</span>
<span className="fin-stat-label fin-flag">
<FaTriangleExclamation aria-hidden="true" /> gefährdet
</span>
</>
)}
</div>
{v.rows.length === 0 ? (
<EmptyState title="Noch keine Abos." />
) : (
<ul className="fin-abos">
{v.rows.map((row) => (
<li key={row.key} className="fin-abo">
<div className="fin-abo-main">
<span className="fin-abo-customer">{row.customer}</span>
<span className="fin-abo-product muted"> · {row.product}</span>
</div>
<div className="fin-amount fin-abo-amount">{row.amountText}</div>
<div className="fin-abo-meta">
<Badge tone={row.tone}>{row.statusLabel}</Badge>
<span className="fin-abo-next muted">nächste Abbuchung: {row.nextText}</span>
</div>
</li>
))}
</ul>
)}
</Card>
)
}
+84
View File
@@ -0,0 +1,84 @@
import { FaTriangleExclamation } from 'react-icons/fa6'
import { Badge, Card, EmptyState } from '../ui'
import { forecastNotes, forecastRows } from '../../lib/financeView'
/**
* Block 3: „Vorschau 30 Tage“. Je Posten Datum, Bezeichnung, „sicher“/„erwartet“ (als Text), Betrag mit Vorzeichen
* und der laufende Saldo beider Linien ab dem Kontostand. Ohne Kontostand entfallen die Saldo-Spalten (mit Hinweis).
* Darunter die Annahme des Servers und Hinweise, wenn Stripe oder die Abos fehlen. Schmal (Karte unter ca. 560 px)
* wird jede Zeile zweizeilig: Datum, Posten, Betrag / Sicherheit, Salden.
*/
export default function ForecastTable({ vorschau, kontostandCents, stripeDown = false, abosFehler = false }) {
const v = vorschau && typeof vorschau === 'object' ? vorschau : {}
const { rows, hasBalance } = forecastRows(v.posten, kontostandCents)
const notes = forecastNotes({ kontostandCents, stripeDown, abosFehler, hasRows: rows.length > 0 })
const assumption = typeof v.annahme === 'string' ? v.annahme.trim() : ''
return (
<Card title="Vorschau 30 Tage" className="fin-forecast-card" bodyStyle={{ padding: 0 }}>
{rows.length === 0 ? (
<EmptyState title="Keine Zahlungen in den nächsten 30 Tagen bekannt." />
) : (
<div className="fin-table-wrap">
<table className={`ui-table fin-forecast${hasBalance ? '' : ' fin-forecast-nobal'}`} role="table">
<caption className="fin-sr">Erwartete Zahlungen der nächsten 30 Tage</caption>
<thead role="rowgroup">
<tr role="row">
<th role="columnheader" scope="col">Datum</th>
<th role="columnheader" scope="col">Posten</th>
<th role="columnheader" scope="col">Sicherheit</th>
<th role="columnheader" scope="col" className="fin-amount">Betrag</th>
{hasBalance && (
<>
<th role="columnheader" scope="col" className="fin-amount">Saldo sicher</th>
<th role="columnheader" scope="col" className="fin-amount">Saldo erwartet</th>
</>
)}
</tr>
</thead>
<tbody role="rowgroup">
{rows.map((row) => (
<tr key={row.key} role="row">
<td role="cell" className="fin-forecast-date">{row.dateText}</td>
<td role="cell" className="fin-forecast-label">{row.label}</td>
<td role="cell" className="fin-forecast-kind">
<Badge tone={row.sicherheit === 'sicher' ? 'ok' : 'info'}>{row.sicherheit}</Badge>
</td>
<td role="cell" className="fin-amount fin-forecast-amount">{row.amountText}</td>
{hasBalance && (
<>
<td
role="cell"
data-label="Saldo sicher"
className={`fin-amount fin-forecast-sicher${row.saldoSicherNegative ? ' fin-neg' : ''}`}
>
<span className="fin-saldo">{row.saldoSicherText}</span>
</td>
<td
role="cell"
data-label="Saldo erwartet"
className={`fin-amount fin-forecast-erwartet${row.saldoErwartetNegative ? ' fin-neg' : ''}`}
>
<span className="fin-saldo">{row.saldoErwartetText}</span>
</td>
</>
)}
</tr>
))}
</tbody>
</table>
</div>
)}
{(notes.length > 0 || assumption) && (
<div className="fin-forecast-foot">
{notes.map((note) => (
<p key={note.key} className="fin-hint fin-hint-warn" role="status">
<FaTriangleExclamation aria-hidden="true" /> {note.text}
</p>
))}
{assumption && <p className="fin-hint">{assumption}</p>}
</div>
)}
</Card>
)
}
+49
View File
@@ -0,0 +1,49 @@
import { Card } from '../ui'
import { taxView } from '../../lib/financeView'
/**
* Block 4: „Steuer“. Rücklage (Betrag und woraus sie sich ergibt), Kleinunternehmer-Zähler gegen die Grenzen,
* Vorjahr (nur wenn eingetragen), Gewerbesteuer-Hinweis (nur wenn nötig) und die nächste ESt-Vorauszahlung
* (nur wenn eingestellt). Ist der Zähler unbekannt (Stripe nicht erreichbar), steht das als Text da, nie eine 0.
*/
export default function TaxCard({ steuer }) {
const v = taxView(steuer)
return (
<Card title="Steuer" className="fin-tax-card">
<div className="fin-facts">
<div className="fin-fact">
<div className="fin-fact-row">
<span className="fin-fact-label">Steuer-Rücklage</span>
<span className="fin-amount fin-fact-big">{v.reserve.amountText}</span>
</div>
{v.reserve.basisText && <div className="fin-fact-sub">{v.reserve.basisText}</div>}
</div>
<div className="fin-fact">
<div className="fin-fact-label">Kleinunternehmer (§ 19 UStG)</div>
<div className={`fin-fact-value${v.small.known ? '' : ' fin-fact-unknown'}`}>{v.small.counterText}</div>
{v.small.shareText && <div className="fin-fact-sub">{v.small.shareText}</div>}
<div className="fin-fact-sub">{v.small.hardText}</div>
{v.small.previousText && <div className="fin-fact-sub">{v.small.previousText}</div>}
</div>
{v.gewerbeText && (
<div className="fin-fact">
<div className="fin-fact-label">Gewerbesteuer</div>
<div className="fin-fact-sub fin-fact-sub-strong">{v.gewerbeText}</div>
</div>
)}
{v.prepayment && (
<div className="fin-fact">
<div className="fin-fact-row">
<span className="fin-fact-label">Nächste ESt-Vorauszahlung</span>
<span className="fin-amount fin-fact-strong">{v.prepayment.amountText}</span>
</div>
<div className="fin-fact-sub">am {v.prepayment.dateText}</div>
</div>
)}
</div>
</Card>
)
}