From 7392bfc29fca42caf768a8514033b2a505ab0c92 Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Tue, 14 Apr 2026 14:41:30 +0200 Subject: [PATCH] feat(buchhaltung): replace transaction dialog with dedicated form page, enforce full field validation before booking --- frontend/src/App.tsx | 17 + frontend/src/pages/Buchhaltung.tsx | 211 ++-------- .../src/pages/BuchhaltungTransaktionForm.tsx | 366 ++++++++++++++++++ 3 files changed, 409 insertions(+), 185 deletions(-) create mode 100644 frontend/src/pages/BuchhaltungTransaktionForm.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0ffffc7..f9216f7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -45,6 +45,7 @@ import Buchhaltung from './pages/Buchhaltung'; import BuchhaltungKontoDetail from './pages/BuchhaltungKontoDetail'; import BuchhaltungKontoManage from './pages/BuchhaltungKontoManage'; import BuchhaltungBankkontoDetail from './pages/BuchhaltungBankkontoDetail'; +import BuchhaltungTransaktionForm from './pages/BuchhaltungTransaktionForm'; import Haushaltsplan from './pages/Haushaltsplan'; import HaushaltsplanDetail from './pages/HaushaltsplanDetail'; import FahrzeugEinstellungen from './pages/FahrzeugEinstellungen'; @@ -411,6 +412,22 @@ function App() { } /> + + + + } + /> + + + + } + /> void; - haushaltsjahre: Haushaltsjahr[]; - selectedJahrId: number | null; - onSave: (data: TransaktionFormData) => void; - existing?: Transaktion; -}) { - const today = new Date().toISOString().slice(0, 10); - const [form, setForm] = useState({ - haushaltsjahr_id: selectedJahrId || 0, - typ: 'ausgabe', - betrag: 0, - datum: today, - konto_id: null, - bankkonto_id: null, - beschreibung: '', - empfaenger_auftraggeber: '', - verwendungszweck: '', - beleg_nr: '', - bestellung_id: null, - ausgaben_typ: null, - }); - - const { data: konten = [] } = useQuery({ - queryKey: ['buchhaltung-konten', form.haushaltsjahr_id], - queryFn: () => buchhaltungApi.getKonten(form.haushaltsjahr_id), - enabled: form.haushaltsjahr_id > 0, - }); - const { data: bankkonten = [] } = useQuery({ queryKey: ['bankkonten'], queryFn: buchhaltungApi.getBankkonten }); - const { data: bestellungen = [] } = useQuery({ - queryKey: ['bestellungen-all'], - queryFn: () => bestellungApi.getOrders(), - staleTime: 5 * 60 * 1000, - }); - - useEffect(() => { - if (open) { - if (existing) { - setForm({ - haushaltsjahr_id: existing.haushaltsjahr_id, - typ: existing.typ as 'einnahme' | 'ausgabe', - betrag: Number(existing.betrag), - datum: existing.datum.slice(0, 10), - konto_id: existing.konto_id, - bankkonto_id: existing.bankkonto_id, - beschreibung: existing.beschreibung || '', - empfaenger_auftraggeber: existing.empfaenger_auftraggeber || '', - verwendungszweck: existing.verwendungszweck || '', - beleg_nr: existing.beleg_nr || '', - bestellung_id: existing.bestellung_id, - ausgaben_typ: existing.ausgaben_typ, - }); - } else { - setForm(f => ({ ...f, haushaltsjahr_id: selectedJahrId || 0, datum: today })); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open]); - - return ( - - {existing ? 'Transaktion bearbeiten' : 'Neue Transaktion'} - - - - Haushaltsjahr - - - - Typ - - - {form.typ === 'ausgabe' && (() => { - const selectedKonto = konten.find(k => k.id === form.konto_id); - const isEinfach = selectedKonto && (selectedKonto.budget_typ || 'detailliert') === 'einfach'; - return !isEinfach ? ( - - Ausgaben-Typ - - - ) : null; - })()} - setForm(f => ({ ...f, betrag: parseFloat(e.target.value) || 0 }))} inputProps={{ step: '0.01', min: '0' }} required /> - setForm(f => ({ ...f, datum: e.target.value }))} InputLabelProps={{ shrink: true }} required /> - - Konto - - - - Bankkonto - - - setForm(f => ({ ...f, beschreibung: e.target.value }))} /> - setForm(f => ({ ...f, empfaenger_auftraggeber: e.target.value }))} /> - setForm(f => ({ ...f, verwendungszweck: e.target.value }))} /> - setForm(f => ({ ...f, beleg_nr: e.target.value }))} /> - - Bestellung verknüpfen - - - - - - - - - - ); -} - // ─── Tree helpers ───────────────────────────────────────────────────────────── function buildTree(flat: KontoTreeNode[]): KontoTreeNode[] { @@ -1136,11 +994,10 @@ function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: { onJahrChange: (id: number) => void; }) { const qc = useQueryClient(); + const navigate = useNavigate(); const { showSuccess, showError } = useNotification(); const { hasPermission } = usePermissionContext(); const [filters, setFilters] = useState({ haushaltsjahr_id: selectedJahrId || undefined }); - const [createOpen, setCreateOpen] = useState(false); - const [editingTx, setEditingTx] = useState(null); const [filterAusgabenTyp, setFilterAusgabenTyp] = useState(''); const [txSubTab, setTxSubTab] = useState(0); @@ -1166,18 +1023,6 @@ function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: { queryFn: () => buchhaltungApi.getTransaktionen(filters), }); - const createMut = useMutation({ - mutationFn: buchhaltungApi.createTransaktion, - onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-transaktionen'] }); setCreateOpen(false); showSuccess('Transaktion erstellt'); }, - onError: () => showError('Transaktion konnte nicht erstellt werden'), - }); - - const updateMut = useMutation({ - mutationFn: ({ id, data }: { id: number; data: Partial }) => buchhaltungApi.updateTransaktion(id, data), - onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-transaktionen'] }); setEditingTx(null); showSuccess('Transaktion aktualisiert'); }, - onError: () => showError('Aktualisierung fehlgeschlagen'), - }); - const buchenMut = useMutation({ mutationFn: (id: number) => buchhaltungApi.buchenTransaktion(id), onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-transaktionen'] }); qc.invalidateQueries({ queryKey: ['buchhaltung-stats'] }); showSuccess('Transaktion gebucht'); }, @@ -1397,15 +1242,28 @@ function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: { - {t.status === 'entwurf' && hasPermission('buchhaltung:edit') && ( - - - - - - )} + {t.status === 'entwurf' && hasPermission('buchhaltung:edit') && (() => { + const buchenDisabled = + kontenFlat.length === 0 || bankkonten.length === 0 || + !t.konto_id || !t.bankkonto_id || !Number(t.betrag) || !t.datum || !t.beschreibung; + const buchenTooltip = + kontenFlat.length === 0 ? 'Keine Konten konfiguriert' : + bankkonten.length === 0 ? 'Keine Bankkonten konfiguriert' : + !t.konto_id ? 'Kein Konto ausgewählt' : + !t.bankkonto_id ? 'Kein Bankkonto ausgewählt' : + !Number(t.betrag) ? 'Kein Betrag angegeben' : + !t.datum ? 'Kein Datum angegeben' : + !t.beschreibung ? 'Keine Beschreibung angegeben' : ''; + return ( + + + + + + ); + })()} {t.status === 'gebucht' && hasPermission('buchhaltung:edit') && ( + + {isEdit ? 'Transaktion bearbeiten' : 'Neue Transaktion'} + + + + + {/* Section: General */} + + + + + + + + Haushaltsjahr + + + + + + Typ + + + + + setForm(f => ({ ...f, betrag: parseFloat(e.target.value) || 0 }))} + inputProps={{ step: '0.01', min: '0.01' }} + /> + + + setForm(f => ({ ...f, datum: e.target.value }))} + InputLabelProps={{ shrink: true }} + /> + + {form.typ === 'ausgabe' && !isEinfach && ( + + + Ausgaben-Typ + + + + )} + + + + + {/* Section: Accounts */} + + + + + + + + Konto (Topf) + + + + + + Bankkonto + + + + + + + + {/* Section: Details */} + + + + + + + setForm(f => ({ ...f, beschreibung: e.target.value }))} + /> + + + setForm(f => ({ ...f, empfaenger_auftraggeber: e.target.value }))} + /> + + + setForm(f => ({ ...f, verwendungszweck: e.target.value }))} + /> + + + setForm(f => ({ ...f, beleg_nr: e.target.value }))} + /> + + + + + + {/* Section: Order link (optional) */} + + + + + + Bestellung + + + + + + {!canSubmit && ( + + Pflichtfelder: Haushaltsjahr, Typ, Betrag, Datum, Konto (Topf), Bankkonto und Beschreibung müssen ausgefüllt sein. + + )} + + + + + + + + + ); +}