Finanzen: Rechnungen schreiben und verwalten, Buchungen zuordnen

Block 6 (Rechnungen: Filter, Altersgruppen, Aktionen, Dialog "Rechnung schreiben")
und Block 7 (Buchungen mit Paging, Zuordnen-Dialog, bei Rechnungen zweistufig).

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 18bc3012e0
commit 1feeb18c08
12 files changed
+2436 -8

No files matched your search

+87 -6
View File
@@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { FaSpinner } from 'react-icons/fa6'
import { PageHeader, Card, Button } from '../components/ui'
@@ -8,17 +8,71 @@ import TodoList from '../components/finance/TodoList'
import ForecastTable from '../components/finance/ForecastTable'
import TaxCard from '../components/finance/TaxCard'
import AboCard from '../components/finance/AboCard'
import InvoiceTable from '../components/finance/InvoiceTable'
import TransactionTable from '../components/finance/TransactionTable'
import AssignDialog from '../components/finance/AssignDialog'
import { useFinanceOverview } from '../hooks/useFinanceOverview'
import { useFinanceTransactions } from '../hooks/useFinanceTransactions'
import { useOpenInvoices } from '../hooks/useOpenInvoices'
import { createFixedCost, refreshBank, startBankAuth } from '../lib/financeApi'
import { planTodoAction, safeUrl } from '../lib/financeView'
export default function FinancePage() {
const navigate = useNavigate()
const { data, loading, error, reload } = useFinanceOverview()
const txs = useFinanceTransactions()
const reloadTransactions = txs.reload
const [refreshing, setRefreshing] = useState(false)
// Buchung, die gerade zugeordnet werden soll; den Dialog dazu zeigt Block 7 (Task 5)
// Buchung, die gerade zugeordnet werden soll (Knopf in Block 7 oder Aufgabe „Zahlungseingang zuordnen“)
const [assignTxId, setAssignTxId] = useState(null)
const [assignNotice, setAssignNotice] = useState('')
const refreshLock = useRef(false)
const retriedRef = useRef(null)
const assignTx = assignTxId ? txs.transactions.find((t) => t.$id === assignTxId) || null : null
// Offene Rechnungen braucht nur ein gebuchter, noch nicht zugeordneter Eingang
const needsInvoices = Boolean(assignTx && !assignTx.category && assignTx.status !== 'PDNG' && assignTx.amountCents > 0)
const openInvoices = useOpenInvoices(needsInvoices)
// Übersicht und Buchungen zusammen neu laden (nach Zuordnen, Rechnungsänderungen und Bankabruf)
const reloadAll = useCallback(() => Promise.all([reload(), reloadTransactions()]), [reload, reloadTransactions])
// Eine Aufgabe kann eine Buchung nennen, die in der geladenen Liste fehlt (neu abgerufen): einmal nachladen,
// dann melden statt still nichts zu tun
useEffect(() => {
if (!assignTxId || assignTx || txs.loading) return
if (retriedRef.current !== assignTxId) {
retriedRef.current = assignTxId
reloadTransactions()
return
}
setAssignNotice(
txs.error
? `Die Buchungen konnten nicht geladen werden: ${txs.error}`
: 'Die Buchung wurde nicht gefunden. Möglicherweise ist sie schon zugeordnet.'
)
setAssignTxId(null)
}, [assignTxId, assignTx, txs.loading, txs.error, reloadTransactions])
const openAssign = useCallback((id) => {
setAssignNotice('')
retriedRef.current = null
setAssignTxId(id)
}, [])
const handleAssignDone = useCallback(() => {
setAssignTxId(null)
return reloadAll()
}, [reloadAll])
const handleAssignClose = useCallback(
(info) => {
setAssignTxId(null)
// Nach einem Fehler kann sich der Stand geändert haben (z. B. „Bereits zugeordnet“)
if (info?.refresh) reloadAll()
},
[reloadAll]
)
// Bankabruf anstoßen, danach die Übersicht neu laden. Fehler (z. B. 429) gehen an den Aufrufer.
// Ein Abruf, der bei der Bank scheitert, antwortet mit ok:false; das steht danach in der Statuszeile.
@@ -28,12 +82,12 @@ export default function FinancePage() {
setRefreshing(true)
try {
await refreshBank()
await reload()
await reloadAll()
} finally {
refreshLock.current = false
setRefreshing(false)
}
}, [reload])
}, [reloadAll])
const handleAction = useCallback(
async (aktion) => {
@@ -59,7 +113,7 @@ export default function FinancePage() {
})
return
case 'assign':
setAssignTxId(plan.transactionId)
openAssign(plan.transactionId)
return
case 'navigate':
navigate(plan.to)
@@ -73,7 +127,7 @@ export default function FinancePage() {
throw new Error(plan.message)
}
},
[navigate, refresh, reload]
[navigate, openAssign, refresh, reload]
)
let body
@@ -115,6 +169,16 @@ export default function FinancePage() {
<div className="fin-cols fin-cols-half">
<AboCard abos={data.abos} />
</div>
<InvoiceTable altersgruppen={data.rechnungen?.altersgruppen} onChanged={reloadAll} />
<TransactionTable
transactions={txs.transactions}
stripeFehler={txs.stripeFehler}
loaded={txs.loaded}
loading={txs.loading}
error={txs.error}
onReload={reloadTransactions}
onAssign={(tx) => openAssign(tx.$id)}
/>
</div>
)
}
@@ -122,7 +186,24 @@ export default function FinancePage() {
return (
<div className="page">
<PageHeader title="Finanzen" subtitle="Kontostand, Aufgaben und Zahlungen im Blick" />
{assignNotice && (
<p className="fin-hint fin-hint-warn fin-page-notice" role="alert">
{assignNotice}{' '}
<Button size="sm" variant="ghost" onClick={() => setAssignNotice('')}>Schließen</Button>
</p>
)}
{body}
{assignTx && (
<AssignDialog
key={assignTx.$id}
transaction={assignTx}
invoices={openInvoices.invoices}
invoicesLoading={openInvoices.loading}
invoicesError={openInvoices.error}
onClose={handleAssignClose}
onDone={handleAssignDone}
/>
)}
</div>
)
}