feat: add Buchhaltung dashboard widget, CSV export, Bestellungen linking, recurring bookings, and approval workflow
This commit is contained in:
106
frontend/src/components/dashboard/BuchhaltungWidget.tsx
Normal file
106
frontend/src/components/dashboard/BuchhaltungWidget.tsx
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user