feat: add account hierarchy, budget types (GWG/Anlagen/Instandhaltung), and Buchhaltung UI overhaul with collapsible tree, pending badge, and konto detail page
This commit is contained in:
173
frontend/src/pages/BuchhaltungKontoDetail.tsx
Normal file
173
frontend/src/pages/BuchhaltungKontoDetail.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
Box, Typography, Button, Grid, Card, CardContent,
|
||||
Table, TableHead, TableBody, TableRow, TableCell,
|
||||
LinearProgress, Chip, Alert, Skeleton, TableContainer, Paper,
|
||||
} from '@mui/material';
|
||||
import { ArrowBack } from '@mui/icons-material';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { buchhaltungApi } from '../services/buchhaltung';
|
||||
import { AUSGABEN_TYP_LABELS } from '../types/buchhaltung.types';
|
||||
import type { AusgabenTyp } from '../types/buchhaltung.types';
|
||||
|
||||
function BudgetCard({ label, budget, spent }: { label: string; budget: number; spent: number }) {
|
||||
const utilization = budget > 0 ? Math.min((spent / budget) * 100, 100) : 0;
|
||||
const over = spent > budget && budget > 0;
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="subtitle2" color="text.secondary">{label}</Typography>
|
||||
<Typography variant="h6">{spent.toFixed(2).replace('.', ',')} €</Typography>
|
||||
{budget > 0 && (
|
||||
<>
|
||||
<Typography variant="body2" color="text.secondary">Budget: {budget.toFixed(2).replace('.', ',')} €</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={utilization}
|
||||
color={over ? 'error' : utilization > 80 ? 'warning' : 'primary'}
|
||||
sx={{ mt: 1, height: 6, borderRadius: 3 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BuchhaltungKontoDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const kontoId = Number(id);
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['kontoDetail', kontoId],
|
||||
queryFn: () => buchhaltungApi.getKontoDetail(kontoId),
|
||||
enabled: !!kontoId,
|
||||
});
|
||||
|
||||
if (isLoading) return <DashboardLayout><Skeleton variant="rectangular" height={400} /></DashboardLayout>;
|
||||
if (isError || !data) return <DashboardLayout><Alert severity="error">Konto nicht gefunden.</Alert></DashboardLayout>;
|
||||
|
||||
const { konto, children, transaktionen } = data;
|
||||
const totalEinnahmen = transaktionen
|
||||
.filter(t => t.typ === 'einnahme' && t.status === 'gebucht')
|
||||
.reduce((sum, t) => sum + Number(t.betrag), 0);
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||
<Button startIcon={<ArrowBack />} onClick={() => navigate('/buchhaltung')}>
|
||||
Zurück
|
||||
</Button>
|
||||
<Typography variant="h5" sx={{ ml: 1 }}>
|
||||
{konto.kontonummer} — {konto.bezeichnung}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={2} sx={{ mb: 3 }}>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<BudgetCard label="GWG" budget={konto.budget_gwg} spent={
|
||||
transaktionen.filter(t => t.typ === 'ausgabe' && t.status === 'gebucht' && t.ausgaben_typ === 'gwg').reduce((s, t) => s + Number(t.betrag), 0)
|
||||
} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<BudgetCard label="Anlagen" budget={konto.budget_anlagen} spent={
|
||||
transaktionen.filter(t => t.typ === 'ausgabe' && t.status === 'gebucht' && t.ausgaben_typ === 'anlagen').reduce((s, t) => s + Number(t.betrag), 0)
|
||||
} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<BudgetCard label="Instandhaltung" budget={konto.budget_instandhaltung} spent={
|
||||
transaktionen.filter(t => t.typ === 'ausgabe' && t.status === 'gebucht' && t.ausgaben_typ === 'instandhaltung').reduce((s, t) => s + Number(t.betrag), 0)
|
||||
} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="subtitle2" color="text.secondary">Einnahmen</Typography>
|
||||
<Typography variant="h6" color="success.main">{totalEinnahmen.toFixed(2).replace('.', ',')} €</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{children.length > 0 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>Unterkonten</Typography>
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Konto</TableCell>
|
||||
<TableCell align="right">Budget GWG</TableCell>
|
||||
<TableCell align="right">Budget Anlagen</TableCell>
|
||||
<TableCell align="right">Budget Instandh.</TableCell>
|
||||
<TableCell align="right">Budget Gesamt</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{children.map(child => (
|
||||
<TableRow
|
||||
key={child.id}
|
||||
hover
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => navigate(`/buchhaltung/konto/${child.id}`)}
|
||||
>
|
||||
<TableCell>{child.kontonummer} — {child.bezeichnung}</TableCell>
|
||||
<TableCell align="right">{Number(child.budget_gwg).toFixed(2)} €</TableCell>
|
||||
<TableCell align="right">{Number(child.budget_anlagen).toFixed(2)} €</TableCell>
|
||||
<TableCell align="right">{Number(child.budget_instandhaltung).toFixed(2)} €</TableCell>
|
||||
<TableCell align="right">
|
||||
{(Number(child.budget_gwg) + Number(child.budget_anlagen) + Number(child.budget_instandhaltung)).toFixed(2)} €
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>Transaktionen</Typography>
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Datum</TableCell>
|
||||
<TableCell>Beschreibung</TableCell>
|
||||
<TableCell>Typ</TableCell>
|
||||
<TableCell>Ausgaben-Typ</TableCell>
|
||||
<TableCell align="right">Betrag</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{transaktionen.length === 0 && (
|
||||
<TableRow><TableCell colSpan={6} align="center">Keine Transaktionen</TableCell></TableRow>
|
||||
)}
|
||||
{transaktionen.map(t => (
|
||||
<TableRow key={t.id}>
|
||||
<TableCell>{new Date(t.datum).toLocaleDateString('de-DE')}</TableCell>
|
||||
<TableCell>{t.beschreibung}</TableCell>
|
||||
<TableCell>
|
||||
<Chip size="small" label={t.typ === 'einnahme' ? 'Einnahme' : 'Ausgabe'}
|
||||
color={t.typ === 'einnahme' ? 'success' : 'error'} />
|
||||
</TableCell>
|
||||
<TableCell>{t.ausgaben_typ ? AUSGABEN_TYP_LABELS[t.ausgaben_typ as AusgabenTyp] : '—'}</TableCell>
|
||||
<TableCell align="right"
|
||||
sx={{ color: t.typ === 'einnahme' ? 'success.main' : 'error.main' }}>
|
||||
{t.typ === 'einnahme' ? '+' : '-'}{Number(t.betrag).toFixed(2).replace('.', ',')} €
|
||||
</TableCell>
|
||||
<TableCell>{t.status}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
</Box>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user