From 86cb175aebfaeec701bb4bbfc74f343d4890bc90 Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Mon, 30 Mar 2026 11:59:05 +0200 Subject: [PATCH] feat(buchhaltung): move collapse arrows to row end, always-visible filters, summary row, sortable transactions, account manage page --- frontend/src/App.tsx | 9 + frontend/src/pages/Buchhaltung.tsx | 326 +++++++++--------- frontend/src/pages/BuchhaltungKontoDetail.tsx | 4 +- frontend/src/pages/BuchhaltungKontoManage.tsx | 201 +++++++++++ 4 files changed, 380 insertions(+), 160 deletions(-) create mode 100644 frontend/src/pages/BuchhaltungKontoManage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 13048ec..aba7bc1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -40,6 +40,7 @@ import AusruestungsanfrageNeu from './pages/AusruestungsanfrageNeu'; import Checklisten from './pages/Checklisten'; import Buchhaltung from './pages/Buchhaltung'; import BuchhaltungKontoDetail from './pages/BuchhaltungKontoDetail'; +import BuchhaltungKontoManage from './pages/BuchhaltungKontoManage'; import FahrzeugEinstellungen from './pages/FahrzeugEinstellungen'; import ChecklistAusfuehrung from './pages/ChecklistAusfuehrung'; import Issues from './pages/Issues'; @@ -380,6 +381,14 @@ function App() { } /> + + + + } + /> {children} : null; + return value === index ? {children} : null; } // ─── Sub-components ──────────────────────────────────────────────────────────── @@ -415,22 +412,11 @@ function KontoRow({ konto, depth = 0, onNavigate }: { konto: KontoTreeNode; dept return ( <> - + onNavigate(konto.id)} sx={{ cursor: 'pointer' }}> - - {konto.children.length > 0 && ( - setOpen(!open)}> - {open ? : } - - )} - onNavigate(konto.id)} - > - {konto.kontonummer} — {konto.bezeichnung} - - + + {konto.kontonummer} — {konto.bezeichnung} + {totalBudget > 0 && ( 100 ? 'error' : utilization > 80 ? 'warning' : 'primary'} @@ -446,6 +432,13 @@ function KontoRow({ konto, depth = 0, onNavigate }: { konto: KontoTreeNode; dept {fmtEur(konto.spent_instandhaltung)} {fmtEur(totalSpent)} {fmtEur(konto.einnahmen_betrag)} + + {konto.children.length > 0 && ( + { e.stopPropagation(); setOpen(!open); }}> + {open ? : } + + )} + {open && konto.children.map(child => ( @@ -454,44 +447,34 @@ function KontoRow({ konto, depth = 0, onNavigate }: { konto: KontoTreeNode; dept ); } -function KontoManageRow({ konto, depth = 0, canManage, onEdit, onDelete }: { +function KontoManageRow({ konto, depth = 0, onNavigate }: { konto: KontoTreeNode; depth?: number; - canManage: boolean; - onEdit: (k: Konto) => void; - onDelete: (id: number) => void; + onNavigate: (id: number) => void; }) { const [open, setOpen] = useState(false); const totalBudget = Number(konto.budget_gwg) + Number(konto.budget_anlagen) + Number(konto.budget_instandhaltung); return ( <> - + onNavigate(konto.id)} sx={{ cursor: 'pointer' }}> - - {konto.children.length > 0 ? ( - setOpen(!open)}> - {open ? : } - - ) : ( - - )} - {konto.kontonummer} — {konto.bezeichnung} - + {konto.kontonummer} — {konto.bezeichnung} {fmtEur(konto.budget_gwg)} {fmtEur(konto.budget_anlagen)} {fmtEur(konto.budget_instandhaltung)} {fmtEur(totalBudget)} - {canManage && ( - - onEdit(konto as unknown as Konto)}> - onDelete(konto.id)}> - - )} + + {konto.children.length > 0 && ( + { e.stopPropagation(); setOpen(!open); }}> + {open ? : } + + )} + {open && konto.children.map(child => ( - + ))} ); @@ -513,8 +496,18 @@ function UebersichtTab({ haushaltsjahre, selectedJahrId, onJahrChange }: { const tree = buildTree(treeData); - const totalEinnahmen = treeData.reduce((s, k) => s + Number(k.einnahmen_betrag), 0); - const totalAusgaben = treeData.reduce((s, k) => s + Number(k.spent_gwg) + Number(k.spent_anlagen) + Number(k.spent_instandhaltung), 0); + const sumBudgetGwg = treeData.reduce((s, k) => s + Number(k.budget_gwg), 0); + const sumBudgetAnlagen = treeData.reduce((s, k) => s + Number(k.budget_anlagen), 0); + const sumBudgetInst = treeData.reduce((s, k) => s + Number(k.budget_instandhaltung), 0); + const sumBudgetGesamt = sumBudgetGwg + sumBudgetAnlagen + sumBudgetInst; + const sumSpentGwg = treeData.reduce((s, k) => s + Number(k.spent_gwg), 0); + const sumSpentAnlagen = treeData.reduce((s, k) => s + Number(k.spent_anlagen), 0); + const sumSpentInst = treeData.reduce((s, k) => s + Number(k.spent_instandhaltung), 0); + const sumSpentGesamt = sumSpentGwg + sumSpentAnlagen + sumSpentInst; + const sumEinnahmen = treeData.reduce((s, k) => s + Number(k.einnahmen_betrag), 0); + + const totalEinnahmen = sumEinnahmen; + const totalAusgaben = sumSpentGesamt; const saldo = totalEinnahmen - totalAusgaben; return ( @@ -567,15 +560,31 @@ function UebersichtTab({ haushaltsjahre, selectedJahrId, onJahrChange }: { Ausgaben Instandh. Ausgaben Gesamt Einnahmen + {tree.length === 0 && ( - Keine Konten + Keine Konten )} {tree.map(k => ( navigate(`/buchhaltung/konto/${id}`)} /> ))} + {tree.length > 0 && ( + + Gesamt + {fmtEur(sumBudgetGwg)} + {fmtEur(sumBudgetAnlagen)} + {fmtEur(sumBudgetInst)} + {fmtEur(sumBudgetGesamt)} + {fmtEur(sumSpentGwg)} + {fmtEur(sumSpentAnlagen)} + {fmtEur(sumSpentInst)} + {fmtEur(sumSpentGesamt)} + {fmtEur(sumEinnahmen)} + + + )} @@ -668,103 +677,113 @@ function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: { setFilters(f => ({ ...f, haushaltsjahr_id: selectedJahrId || undefined })); }, [selectedJahrId]); - const activeFilterCount = [ - filters.status, - filters.typ, - filters.search, - filterAusgabenTyp, - ].filter(Boolean).length; - const filteredTransaktionen = filterAusgabenTyp ? transaktionen.filter(t => t.ausgaben_typ === filterAusgabenTyp) : transaktionen; + const [sortCol, setSortCol] = useState<'nr' | 'datum' | 'betrag'>('nr'); + const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc'); + + const handleSort = (col: 'nr' | 'datum' | 'betrag') => { + if (sortCol === col) setSortDir(d => d === 'asc' ? 'desc' : 'asc'); + else { setSortCol(col); setSortDir('asc'); } + }; + + const sortedTransaktionen = [...filteredTransaktionen].sort((a, b) => { + let cmp = 0; + if (sortCol === 'nr') cmp = (a.laufende_nummer ?? 0) - (b.laufende_nummer ?? 0); + else if (sortCol === 'datum') cmp = new Date(a.datum).getTime() - new Date(b.datum).getTime(); + else if (sortCol === 'betrag') cmp = Number(a.betrag) - Number(b.betrag); + return sortDir === 'asc' ? cmp : -cmp; + }); + return ( {/* Filters */} - - }> - - - Filter - {activeFilterCount > 0 && ( - - )} - - - - - - Haushaltsjahr - - - - Status - - - - Typ - - - setFilters(f => ({ ...f, search: e.target.value || undefined }))} sx={{ minWidth: 180 }} /> - - Ausgaben-Typ - - - {hasPermission('buchhaltung:export') && ( - - - - - - )} - - - + + + + Haushaltsjahr + + + + Status + + + + Typ + + + setFilters(f => ({ ...f, search: e.target.value || undefined }))} sx={{ minWidth: 180 }} /> + + Ausgaben-Typ + + + {hasPermission('buchhaltung:export') && ( + + + + + + )} + + {isLoading ? : ( - Nr. - Datum + + handleSort('nr')}> + Nr. + + + + handleSort('datum')}> + Datum + + Typ Beschreibung Konto - Betrag + + handleSort('betrag')}> + Betrag + + Status Aktionen - {filteredTransaktionen.length === 0 && ( + {sortedTransaktionen.length === 0 && ( Keine Transaktionen )} - {filteredTransaktionen.map((t: Transaktion) => ( + {sortedTransaktionen.map((t: Transaktion) => ( {t.laufende_nummer ?? `E${t.id}`} {fmtDate(t.datum)} @@ -965,6 +984,7 @@ function KontenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: { onJahrChange: (id: number) => void; }) { const qc = useQueryClient(); + const navigate = useNavigate(); const { showSuccess, showError } = useNotification(); const { hasPermission } = usePermissionContext(); const [subTab, setSubTab] = useState(0); @@ -1000,12 +1020,6 @@ function KontenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: { onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-konten'] }); qc.invalidateQueries({ queryKey: ['kontenTree'] }); setKontoDialog({ open: false }); showSuccess('Konto aktualisiert'); }, onError: () => showError('Konto konnte nicht aktualisiert werden'), }); - const deleteKontoMut = useMutation({ - mutationFn: buchhaltungApi.deleteKonto, - onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-konten'] }); qc.invalidateQueries({ queryKey: ['kontenTree'] }); showSuccess('Konto gelöscht'); }, - onError: () => showError('Löschen fehlgeschlagen'), - }); - const createBankMut = useMutation({ mutationFn: buchhaltungApi.createBankkonto, onSuccess: () => { qc.invalidateQueries({ queryKey: ['bankkonten'] }); setBankDialog({ open: false }); showSuccess('Bankkonto erstellt'); }, @@ -1084,18 +1098,16 @@ function KontenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: { Anlagen Instandh. Gesamt - {canManage && Aktionen} + - {kontenTree.length === 0 && Keine Konten} + {kontenTree.length === 0 && Keine Konten} {kontenTree.map(k => ( setKontoDialog({ open: true, existing: existing as unknown as Konto })} - onDelete={id => deleteKontoMut.mutate(id)} + onNavigate={(id) => navigate('/buchhaltung/konto/' + id + '/verwalten')} /> ))} @@ -1309,39 +1321,37 @@ export default function Buchhaltung() { return ( - - - Buchhaltung - - - + + Buchhaltung + + + Transaktionen} /> - - - - - - - - - - + + + + + + + + + ); } diff --git a/frontend/src/pages/BuchhaltungKontoDetail.tsx b/frontend/src/pages/BuchhaltungKontoDetail.tsx index 3987d69..3e8cac5 100644 --- a/frontend/src/pages/BuchhaltungKontoDetail.tsx +++ b/frontend/src/pages/BuchhaltungKontoDetail.tsx @@ -18,10 +18,10 @@ function BudgetCard({ label, budget, spent }: { label: string; budget: number; s {label} - {spent.toFixed(2).replace('.', ',')} € + {Number(spent).toFixed(2).replace('.', ',')} € {budget > 0 && ( <> - Budget: {budget.toFixed(2).replace('.', ',')} € + Budget: {Number(budget).toFixed(2).replace('.', ',')} € (); + const navigate = useNavigate(); + const qc = useQueryClient(); + const { showSuccess, showError } = useNotification(); + const kontoId = Number(id); + + const [form, setForm] = useState>({}); + const [deleteOpen, setDeleteOpen] = useState(false); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['kontoDetail', kontoId], + queryFn: () => buchhaltungApi.getKontoDetail(kontoId), + enabled: !!kontoId, + }); + + // Load sibling konten for parent selector (exclude self) + const { data: alleKonten = [] } = useQuery({ + queryKey: ['buchhaltung-konten', data?.konto.haushaltsjahr_id], + queryFn: () => buchhaltungApi.getKonten(data!.konto.haushaltsjahr_id), + enabled: !!data?.konto.haushaltsjahr_id, + }); + + useEffect(() => { + if (data?.konto) { + const k = data.konto; + setForm({ + haushaltsjahr_id: k.haushaltsjahr_id, + kontonummer: k.kontonummer, + bezeichnung: k.bezeichnung, + budget_gwg: k.budget_gwg, + budget_anlagen: k.budget_anlagen, + budget_instandhaltung: k.budget_instandhaltung, + parent_id: k.parent_id ?? undefined, + notizen: k.notizen ?? '', + }); + } + }, [data]); + + const updateMut = useMutation({ + mutationFn: (d: Partial) => buchhaltungApi.updateKonto(kontoId, d), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['kontoDetail', kontoId] }); + qc.invalidateQueries({ queryKey: ['kontenTree'] }); + qc.invalidateQueries({ queryKey: ['buchhaltung-konten'] }); + showSuccess('Konto gespeichert'); + }, + onError: () => showError('Speichern fehlgeschlagen'), + }); + + const deleteMut = useMutation({ + mutationFn: () => buchhaltungApi.deleteKonto(kontoId), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['kontenTree'] }); + qc.invalidateQueries({ queryKey: ['buchhaltung-konten'] }); + showSuccess('Konto gelöscht'); + navigate('/buchhaltung?tab=2'); + }, + onError: () => showError('Löschen fehlgeschlagen'), + }); + + if (isLoading) return ; + if (isError || !data) return Konto nicht gefunden.; + + const konto = data.konto; + const otherKonten = alleKonten.filter(k => k.id !== kontoId); + + const handleSave = () => { + if (!form.bezeichnung) return; + updateMut.mutate(form); + }; + + return ( + + + + + {konto.kontonummer} — {konto.bezeichnung} + + + + + + + + setForm(f => ({ ...f, kontonummer: Number(e.target.value) }))} + required + fullWidth + /> + setForm(f => ({ ...f, bezeichnung: e.target.value }))} + required + fullWidth + /> + + Übergeordnetes Konto + + + setForm(f => ({ ...f, budget_gwg: parseFloat(e.target.value) || 0 }))} + inputProps={{ step: '0.01', min: '0' }} + fullWidth + /> + setForm(f => ({ ...f, budget_anlagen: parseFloat(e.target.value) || 0 }))} + inputProps={{ step: '0.01', min: '0' }} + fullWidth + /> + setForm(f => ({ ...f, budget_instandhaltung: parseFloat(e.target.value) || 0 }))} + inputProps={{ step: '0.01', min: '0' }} + fullWidth + /> + setForm(f => ({ ...f, notizen: e.target.value }))} + multiline + rows={3} + fullWidth + /> + + + + setDeleteOpen(false)}> + Konto löschen + + + Möchten Sie das Konto {konto.kontonummer} — {konto.bezeichnung} wirklich löschen? + Diese Aktion kann nicht rückgängig gemacht werden. + + + + + + + + + ); +}