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' import StatusLine from '../components/finance/StatusLine' import LiquidityHead from '../components/finance/LiquidityHead' 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 (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. const refresh = useCallback(async () => { if (refreshLock.current) return refreshLock.current = true setRefreshing(true) try { await refreshBank() await reloadAll() } finally { refreshLock.current = false setRefreshing(false) } }, [reloadAll]) const handleAction = useCallback( async (aktion) => { const plan = planTodoAction(aktion) switch (plan.kind) { case 'bank-connect': { const { url } = await startBankAuth() const target = safeUrl(url) if (!target) throw new Error('Die Bank hat keine Weiterleitung geliefert.') window.location.assign(target) return } case 'bank-refresh': return refresh() case 'open-url': window.open(plan.url, '_blank', 'noopener,noreferrer') return case 'scroll': // Der Rechnungsblock (Task 5) trägt diese Id; ohne ihn passiert nichts. Erst nach dem Neuzeichnen // starten: ein Scroll mitten im Klick wird sonst vom Neuzeichnen der Aufgabenzeile abgebrochen. requestAnimationFrame(() => { document.getElementById(plan.id)?.scrollIntoView({ behavior: 'smooth', block: 'start' }) }) return case 'assign': openAssign(plan.transactionId) return case 'navigate': navigate(plan.to) return case 'fixed-cost': if (!window.confirm(plan.confirmText)) return await createFixedCost(plan.payload) await reload() return default: throw new Error(plan.message) } }, [navigate, openAssign, refresh, reload] ) let body if (!data && loading) { body = (
) } else if (!data) { body = (

{error || 'Die Übersicht konnte nicht geladen werden.'}

) } else { body = (
{error && (

Die Übersicht konnte nicht neu geladen werden: {error}{' '}

)}
openAssign(tx.$id)} />
) } return (
{assignNotice && (

{assignNotice}{' '}

)} {body} {assignTx && ( )}
) }