164 lines
6.6 KiB
TypeScript
164 lines
6.6 KiB
TypeScript
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>
|
||
);
|
||
}
|