feat: add Buchhaltung dashboard widget, CSV export, Bestellungen linking, recurring bookings, and approval workflow

This commit is contained in:
Matthias Hochmeister
2026-03-28 20:34:53 +01:00
parent c1fca5cc67
commit bc39963746
10 changed files with 750 additions and 5 deletions

View File

@@ -0,0 +1,106 @@
import { Card, CardContent, Typography, Box, Chip, Skeleton } from '@mui/material';
import { AccountBalance } from '@mui/icons-material';
import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { buchhaltungApi } from '../../services/buchhaltung';
function fmtEur(val: number) {
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val);
}
function BuchhaltungWidget() {
const navigate = useNavigate();
const { data: haushaltsjahre = [], isLoading: loadingJahre } = useQuery({
queryKey: ['haushaltsjahre'],
queryFn: buchhaltungApi.getHaushaltsjahre,
refetchInterval: 5 * 60 * 1000,
retry: 1,
});
const activeJahr = haushaltsjahre.find(hj => !hj.abgeschlossen) || haushaltsjahre[0];
const { data: stats, isLoading: loadingStats, isError } = useQuery({
queryKey: ['buchhaltung-stats', activeJahr?.id],
queryFn: () => buchhaltungApi.getStats(activeJahr!.id),
enabled: !!activeJahr,
refetchInterval: 5 * 60 * 1000,
retry: 1,
});
const isLoading = loadingJahre || (!!activeJahr && loadingStats);
if (isLoading) {
return (
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>Buchhaltung</Typography>
<Skeleton variant="rectangular" height={60} />
</CardContent>
</Card>
);
}
if (!activeJahr) {
return (
<Card sx={{ cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }} onClick={() => navigate('/buchhaltung')}>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="h6">Buchhaltung</Typography>
<AccountBalance fontSize="small" color="action" />
</Box>
<Typography variant="body2" color="text.secondary">Kein aktives Haushaltsjahr</Typography>
</CardContent>
</Card>
);
}
if (isError || !stats) {
return (
<Card sx={{ cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }} onClick={() => navigate('/buchhaltung')}>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="h6">Buchhaltung</Typography>
<AccountBalance fontSize="small" color="action" />
</Box>
<Typography variant="body2" color="text.secondary">Daten konnten nicht geladen werden</Typography>
</CardContent>
</Card>
);
}
const overBudgetCount = stats.konten_budget.filter(k => k.auslastung_prozent >= 80).length;
return (
<Card sx={{ cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }} onClick={() => navigate('/buchhaltung')}>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
<Typography variant="h6">Buchhaltung</Typography>
<AccountBalance fontSize="small" color="action" />
</Box>
<Box sx={{ mb: 0.5 }}>
<Typography variant="body2" color="text.secondary" component="span">Einnahmen: </Typography>
<Typography variant="body2" color="success.main" component="span" fontWeight={600}>{fmtEur(stats.total_einnahmen)}</Typography>
</Box>
<Box sx={{ mb: 0.5 }}>
<Typography variant="body2" color="text.secondary" component="span">Ausgaben: </Typography>
<Typography variant="body2" color="error.main" component="span" fontWeight={600}>{fmtEur(stats.total_ausgaben)}</Typography>
</Box>
<Box sx={{ mb: overBudgetCount > 0 ? 1 : 0 }}>
<Typography variant="body2" color="text.secondary" component="span">Saldo: </Typography>
<Typography variant="body2" color={stats.saldo >= 0 ? 'success.main' : 'error.main'} component="span" fontWeight={600}>{fmtEur(stats.saldo)}</Typography>
</Box>
{overBudgetCount > 0 && (
<Chip
label={`${overBudgetCount} Konto${overBudgetCount > 1 ? 'n' : ''} über 80% Budget`}
color="warning"
size="small"
variant="outlined"
/>
)}
</CardContent>
</Card>
);
}
export default BuchhaltungWidget;

View File

@@ -24,3 +24,4 @@ export { default as IssueQuickAddWidget } from './IssueQuickAddWidget';
export { default as IssueOverviewWidget } from './IssueOverviewWidget';
export { default as ChecklistWidget } from './ChecklistWidget';
export { default as SortableWidget } from './SortableWidget';
export { default as BuchhaltungWidget } from './BuchhaltungWidget';