feat(buchhaltung): add transfers, bank statements, Haushaltsplan, and PDF export
This commit is contained in:
163
frontend/src/pages/Haushaltsplan.tsx
Normal file
163
frontend/src/pages/Haushaltsplan.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useState } from 'react';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Add as AddIcon, Delete, Visibility } from '@mui/icons-material';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { buchhaltungApi } from '../services/buchhaltung';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||
import type { Planung, PlanungStatus, Haushaltsjahr } from '../types/buchhaltung.types';
|
||||
|
||||
const STATUS_LABELS: Record<PlanungStatus, string> = {
|
||||
entwurf: 'Entwurf',
|
||||
aktiv: 'Aktiv',
|
||||
abgeschlossen: 'Abgeschlossen',
|
||||
};
|
||||
const STATUS_COLORS: Record<PlanungStatus, 'default' | 'success' | 'info'> = {
|
||||
entwurf: 'default',
|
||||
aktiv: 'success',
|
||||
abgeschlossen: 'info',
|
||||
};
|
||||
|
||||
function fmtDate(val: string) {
|
||||
return new Date(val).toLocaleDateString('de-DE');
|
||||
}
|
||||
|
||||
function fmtEur(val: number) {
|
||||
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val);
|
||||
}
|
||||
|
||||
export default function Haushaltsplan() {
|
||||
const navigate = useNavigate();
|
||||
const qc = useQueryClient();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
const { hasPermission } = usePermissionContext();
|
||||
const canManage = hasPermission('buchhaltung:manage_accounts');
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [form, setForm] = useState<{ bezeichnung: string; haushaltsjahr_id: string }>({ bezeichnung: '', haushaltsjahr_id: '' });
|
||||
|
||||
const { data: planungen = [], isLoading } = useQuery({
|
||||
queryKey: ['planungen'],
|
||||
queryFn: buchhaltungApi.listPlanungen,
|
||||
});
|
||||
const { data: haushaltsjahre = [] } = useQuery({
|
||||
queryKey: ['haushaltsjahre'],
|
||||
queryFn: buchhaltungApi.getHaushaltsjahre,
|
||||
});
|
||||
|
||||
const createMut = useMutation({
|
||||
mutationFn: buchhaltungApi.createPlanung,
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['planungen'] }); setDialogOpen(false); showSuccess('Haushaltsplan erstellt'); },
|
||||
onError: () => showError('Haushaltsplan konnte nicht erstellt werden'),
|
||||
});
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: buchhaltungApi.deletePlanung,
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['planungen'] }); showSuccess('Haushaltsplan gelöscht'); },
|
||||
onError: () => showError('Haushaltsplan konnte nicht gelöscht werden'),
|
||||
});
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||
<Typography variant="h4" sx={{ flexGrow: 1 }}>Haushaltspläne</Typography>
|
||||
{canManage && (
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => { setForm({ bezeichnung: '', haushaltsjahr_id: '' }); setDialogOpen(true); }}>
|
||||
Neuer Haushaltsplan
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Bezeichnung</TableCell>
|
||||
<TableCell>Haushaltsjahr</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="right">Positionen</TableCell>
|
||||
<TableCell align="right">Gesamt (GWG + Anl. + Inst.)</TableCell>
|
||||
<TableCell>Erstellt am</TableCell>
|
||||
<TableCell>Aktionen</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{!isLoading && planungen.length === 0 && (
|
||||
<TableRow><TableCell colSpan={7} align="center"><Typography color="text.secondary">Keine Haushaltspläne vorhanden</Typography></TableCell></TableRow>
|
||||
)}
|
||||
{planungen.map((p: Planung) => {
|
||||
const total = Number(p.total_gwg) + Number(p.total_anlagen) + Number(p.total_instandhaltung);
|
||||
return (
|
||||
<TableRow key={p.id} hover sx={{ cursor: 'pointer' }} onClick={() => navigate(`/haushaltsplan/${p.id}`)}>
|
||||
<TableCell>{p.bezeichnung}</TableCell>
|
||||
<TableCell>{p.haushaltsjahr_bezeichnung || '–'}</TableCell>
|
||||
<TableCell><Chip label={STATUS_LABELS[p.status] || p.status} size="small" color={STATUS_COLORS[p.status] || 'default'} /></TableCell>
|
||||
<TableCell align="right">{p.positionen_count}</TableCell>
|
||||
<TableCell align="right">{fmtEur(total)}</TableCell>
|
||||
<TableCell>{fmtDate(p.erstellt_am)}</TableCell>
|
||||
<TableCell onClick={e => e.stopPropagation()}>
|
||||
<IconButton size="small" onClick={() => navigate(`/haushaltsplan/${p.id}`)}><Visibility fontSize="small" /></IconButton>
|
||||
{canManage && <IconButton size="small" color="error" onClick={() => deleteMut.mutate(p.id)}><Delete fontSize="small" /></IconButton>}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Neuer Haushaltsplan</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={2} sx={{ mt: 1 }}>
|
||||
<TextField label="Bezeichnung" value={form.bezeichnung} onChange={e => setForm(f => ({ ...f, bezeichnung: e.target.value }))} required />
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Haushaltsjahr (optional)</InputLabel>
|
||||
<Select value={form.haushaltsjahr_id} label="Haushaltsjahr (optional)" onChange={e => setForm(f => ({ ...f, haushaltsjahr_id: e.target.value as string }))}>
|
||||
<MenuItem value="">Kein Haushaltsjahr</MenuItem>
|
||||
{haushaltsjahre.map((hj: Haushaltsjahr) => <MenuItem key={hj.id} value={hj.id}>{hj.bezeichnung}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDialogOpen(false)}>Abbrechen</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!form.bezeichnung.trim()}
|
||||
onClick={() => createMut.mutate({
|
||||
bezeichnung: form.bezeichnung.trim(),
|
||||
haushaltsjahr_id: form.haushaltsjahr_id ? Number(form.haushaltsjahr_id) : undefined,
|
||||
})}
|
||||
>
|
||||
Erstellen
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user