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:
1 parent
18bc3012e0
commit
1feeb18c08
12 files changed
+2436
-8
No files matched your search
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user