refactor(buchhaltung): simplify transaction workflow to two states, reorder tabs, guard booking, add overview divider

This commit is contained in:
Matthias Hochmeister
2026-04-14 13:16:45 +02:00
parent 588d8e81db
commit 3a8f166121
2 changed files with 12 additions and 49 deletions

View File

@@ -48,13 +48,10 @@ import {
Edit, Edit,
ExpandLess as ExpandLessIcon, ExpandLess as ExpandLessIcon,
ExpandMore as ExpandMoreIcon, ExpandMore as ExpandMoreIcon,
HowToReg,
Lock, Lock,
Save, Save,
SwapHoriz, SwapHoriz,
PictureAsPdf as PdfIcon, PictureAsPdf as PdfIcon,
ThumbDown,
ThumbUp,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
@@ -96,6 +93,7 @@ function fmtEur(val: number) {
} }
const dividerLeft = { borderLeft: '2px solid', borderColor: 'divider' } as const; const dividerLeft = { borderLeft: '2px solid', borderColor: 'divider' } as const;
const dividerRight = { borderRight: '2px solid', borderColor: 'divider' } as const;
function fmtDate(val: string) { function fmtDate(val: string) {
return new Date(val).toLocaleDateString('de-DE'); return new Date(val).toLocaleDateString('de-DE');
@@ -656,7 +654,7 @@ function KontoRow({ konto, depth = 0, onNavigate }: { konto: KontoTreeNode; dept
<TableCell align="right">{isEinfach ? '' : fmtEur(konto.spent_anlagen)}</TableCell> <TableCell align="right">{isEinfach ? '' : fmtEur(konto.spent_anlagen)}</TableCell>
<TableCell align="right">{isEinfach ? '' : fmtEur(konto.spent_instandhaltung)}</TableCell> <TableCell align="right">{isEinfach ? '' : fmtEur(konto.spent_instandhaltung)}</TableCell>
<TableCell align="right"><strong>{fmtEur(totalSpent)}</strong></TableCell> <TableCell align="right"><strong>{fmtEur(totalSpent)}</strong></TableCell>
<TableCell align="right" sx={dividerLeft}>{fmtEur(konto.einnahmen_betrag)}</TableCell> <TableCell align="right" sx={{ ...dividerLeft, ...dividerRight }}>{fmtEur(konto.einnahmen_betrag)}</TableCell>
<TableCell sx={{ width: 40, px: 0.5 }}> <TableCell sx={{ width: 40, px: 0.5 }}>
{konto.children.length > 0 && ( {konto.children.length > 0 && (
<IconButton size="small" onClick={e => { e.stopPropagation(); setOpen(!open); }}> <IconButton size="small" onClick={e => { e.stopPropagation(); setOpen(!open); }}>
@@ -816,7 +814,7 @@ function UebersichtTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
<TableCell align="right">Ausgaben Anlagen</TableCell> <TableCell align="right">Ausgaben Anlagen</TableCell>
<TableCell align="right">Ausgaben Instandh.</TableCell> <TableCell align="right">Ausgaben Instandh.</TableCell>
<TableCell align="right">Ausgaben Gesamt</TableCell> <TableCell align="right">Ausgaben Gesamt</TableCell>
<TableCell align="right" sx={dividerLeft}>Einnahmen</TableCell> <TableCell align="right" sx={{ ...dividerLeft, ...dividerRight }}>Einnahmen</TableCell>
<TableCell sx={{ width: 40 }} /> <TableCell sx={{ width: 40 }} />
</TableRow> </TableRow>
</TableHead> </TableHead>
@@ -861,7 +859,7 @@ function UebersichtTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
<TableCell align="right">{fmtEur(catSpentAnl)}</TableCell> <TableCell align="right">{fmtEur(catSpentAnl)}</TableCell>
<TableCell align="right">{fmtEur(catSpentInst)}</TableCell> <TableCell align="right">{fmtEur(catSpentInst)}</TableCell>
<TableCell align="right">{fmtEur(catSpentGwg + catSpentAnl + catSpentInst)}</TableCell> <TableCell align="right">{fmtEur(catSpentGwg + catSpentAnl + catSpentInst)}</TableCell>
<TableCell align="right" sx={dividerLeft}>{fmtEur(catEinnahmen)}</TableCell> <TableCell align="right" sx={{ ...dividerLeft, ...dividerRight }}>{fmtEur(catEinnahmen)}</TableCell>
<TableCell /> <TableCell />
</TableRow> </TableRow>
); );
@@ -883,7 +881,7 @@ function UebersichtTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
<TableCell align="right">{fmtEur(sumSpentAnlagen)}</TableCell> <TableCell align="right">{fmtEur(sumSpentAnlagen)}</TableCell>
<TableCell align="right">{fmtEur(sumSpentInst)}</TableCell> <TableCell align="right">{fmtEur(sumSpentInst)}</TableCell>
<TableCell align="right">{fmtEur(sumSpentGesamt)}</TableCell> <TableCell align="right">{fmtEur(sumSpentGesamt)}</TableCell>
<TableCell align="right" sx={dividerLeft}>{fmtEur(sumEinnahmen)}</TableCell> <TableCell align="right" sx={{ ...dividerLeft, ...dividerRight }}>{fmtEur(sumEinnahmen)}</TableCell>
<TableCell /> <TableCell />
</TableRow> </TableRow>
)} )}
@@ -1171,24 +1169,6 @@ function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
onError: () => showError('Löschen fehlgeschlagen'), onError: () => showError('Löschen fehlgeschlagen'),
}); });
const freigabeMut = useMutation({
mutationFn: (id: number) => buchhaltungApi.requestFreigabe(id),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-transaktionen'] }); showSuccess('Freigabe angefragt'); },
onError: () => showError('Freigabe konnte nicht angefragt werden'),
});
const approveMut = useMutation({
mutationFn: (id: number) => buchhaltungApi.approveFreigabe(id),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-transaktionen'] }); qc.invalidateQueries({ queryKey: ['buchhaltung-stats'] }); showSuccess('Freigabe genehmigt'); },
onError: () => showError('Genehmigung fehlgeschlagen'),
});
const rejectMut = useMutation({
mutationFn: (id: number) => buchhaltungApi.rejectFreigabe(id),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-transaktionen'] }); showSuccess('Freigabe abgelehnt'); },
onError: () => showError('Ablehnung fehlgeschlagen'),
});
// ── Wiederkehrend mutations ── // ── Wiederkehrend mutations ──
const canManage = hasPermission('buchhaltung:manage_accounts'); const canManage = hasPermission('buchhaltung:manage_accounts');
@@ -1263,7 +1243,7 @@ function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
const subTabTransaktionen = sortedTransaktionen.filter(t => { const subTabTransaktionen = sortedTransaktionen.filter(t => {
if (txSubTab === 0) return t.status === 'entwurf'; if (txSubTab === 0) return t.status === 'entwurf';
if (txSubTab === 1) return t.status === 'gebucht' || t.status === 'freigegeben'; if (txSubTab === 1) return t.status === 'gebucht';
return false; return false;
}); });
@@ -1391,30 +1371,15 @@ function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
<TableCell> <TableCell>
<Stack direction="row" spacing={0.5} flexWrap="wrap"> <Stack direction="row" spacing={0.5} flexWrap="wrap">
{t.status === 'entwurf' && hasPermission('buchhaltung:edit') && ( {t.status === 'entwurf' && hasPermission('buchhaltung:edit') && (
<Tooltip title={!t.konto_id ? 'Kein Konto ausgewählt' : ''}> <Tooltip title={kontenFlat.length === 0 ? 'Keine Konten konfiguriert' : bankkonten.length === 0 ? 'Keine Bankkonten konfiguriert' : !t.konto_id ? 'Kein Konto ausgewählt' : ''}>
<span> <span>
<Button size="small" variant="outlined" startIcon={<BookmarkAdd fontSize="small" />} disabled={!t.konto_id} onClick={() => buchenMut.mutate(t.id)}> <Button size="small" variant="outlined" startIcon={<BookmarkAdd fontSize="small" />} disabled={kontenFlat.length === 0 || bankkonten.length === 0 || !t.konto_id} onClick={() => buchenMut.mutate(t.id)}>
Buchen Buchen
</Button> </Button>
</span> </span>
</Tooltip> </Tooltip>
)} )}
{t.status === 'gebucht' && hasPermission('buchhaltung:edit') && ( {t.status === 'gebucht' && hasPermission('buchhaltung:edit') && (
<Button size="small" variant="outlined" color="info" startIcon={<HowToReg fontSize="small" />} onClick={() => freigabeMut.mutate(t.id)}>
Freigabe
</Button>
)}
{t.status === 'freigegeben' && hasPermission('buchhaltung:manage_accounts') && (
<>
<Button size="small" variant="outlined" color="success" startIcon={<ThumbUp fontSize="small" />} onClick={() => approveMut.mutate(t.id)}>
Genehmigen
</Button>
<Button size="small" variant="outlined" color="error" startIcon={<ThumbDown fontSize="small" />} onClick={() => rejectMut.mutate(t.id)}>
Ablehnen
</Button>
</>
)}
{(t.status === 'gebucht' || t.status === 'freigegeben') && hasPermission('buchhaltung:edit') && (
<Button size="small" variant="outlined" color="warning" startIcon={<Cancel fontSize="small" />} onClick={() => stornoMut.mutate(t.id)}> <Button size="small" variant="outlined" color="warning" startIcon={<Cancel fontSize="small" />} onClick={() => stornoMut.mutate(t.id)}>
Stornieren Stornieren
</Button> </Button>
@@ -2140,20 +2105,20 @@ export default function Buchhaltung() {
</Box> </Box>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}> <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tabValue} onChange={handleTabChange} variant="scrollable" scrollButtons="auto"> <Tabs value={tabValue} onChange={handleTabChange} variant="scrollable" scrollButtons="auto">
<Tab label={<Badge badgeContent={pendingCount || 0} color="warning" invisible={!pendingCount}><span>Transaktionen Übersicht</span></Badge>} />
<Tab label="Übersicht" /> <Tab label="Übersicht" />
<Tab label={<Badge badgeContent={pendingCount || 0} color="warning" invisible={!pendingCount}><span>Transaktionen</span></Badge>} />
<Tab label="Konten" /> <Tab label="Konten" />
</Tabs> </Tabs>
</Box> </Box>
<TabPanel value={tabValue} index={0}> <TabPanel value={tabValue} index={0}>
<UebersichtTab <TransaktionenTab
haushaltsjahre={haushaltsjahre} haushaltsjahre={haushaltsjahre}
selectedJahrId={selectedJahrId} selectedJahrId={selectedJahrId}
onJahrChange={setSelectedJahrId} onJahrChange={setSelectedJahrId}
/> />
</TabPanel> </TabPanel>
<TabPanel value={tabValue} index={1}> <TabPanel value={tabValue} index={1}>
<TransaktionenTab <UebersichtTab
haushaltsjahre={haushaltsjahre} haushaltsjahre={haushaltsjahre}
selectedJahrId={selectedJahrId} selectedJahrId={selectedJahrId}
onJahrChange={setSelectedJahrId} onJahrChange={setSelectedJahrId}

View File

@@ -1,7 +1,7 @@
// Lookup types // Lookup types
export type KontoArt = 'einnahme' | 'ausgabe' | 'vermoegen' | 'verbindlichkeit'; export type KontoArt = 'einnahme' | 'ausgabe' | 'vermoegen' | 'verbindlichkeit';
export type TransaktionTyp = 'einnahme' | 'ausgabe' | 'transfer'; export type TransaktionTyp = 'einnahme' | 'ausgabe' | 'transfer';
export type TransaktionStatus = 'entwurf' | 'gebucht' | 'freigegeben' | 'storniert'; export type TransaktionStatus = 'entwurf' | 'gebucht' | 'storniert';
export type FreigabeStatus = 'ausstehend' | 'genehmigt' | 'abgelehnt'; export type FreigabeStatus = 'ausstehend' | 'genehmigt' | 'abgelehnt';
export type WiederkehrendIntervall = 'monatlich' | 'quartalsweise' | 'halbjaehrlich' | 'jaehrlich'; export type WiederkehrendIntervall = 'monatlich' | 'quartalsweise' | 'halbjaehrlich' | 'jaehrlich';
export type PlanungStatus = 'entwurf' | 'aktiv' | 'abgeschlossen'; export type PlanungStatus = 'entwurf' | 'aktiv' | 'abgeschlossen';
@@ -18,14 +18,12 @@ export const AUSGABEN_TYP_LABELS: Record<AusgabenTyp, string> = {
export const TRANSAKTION_STATUS_LABELS: Record<TransaktionStatus, string> = { export const TRANSAKTION_STATUS_LABELS: Record<TransaktionStatus, string> = {
entwurf: 'Entwurf', entwurf: 'Entwurf',
gebucht: 'Gebucht', gebucht: 'Gebucht',
freigegeben: 'Freigegeben',
storniert: 'Storniert', storniert: 'Storniert',
}; };
export const TRANSAKTION_STATUS_COLORS: Record<TransaktionStatus, 'default' | 'warning' | 'success' | 'error'> = { export const TRANSAKTION_STATUS_COLORS: Record<TransaktionStatus, 'default' | 'warning' | 'success' | 'error'> = {
entwurf: 'default', entwurf: 'default',
gebucht: 'warning', gebucht: 'warning',
freigegeben: 'success',
storniert: 'error', storniert: 'error',
}; };