feat(buchhaltung): move collapse arrows to row end, always-visible filters, summary row, sortable transactions, account manage page
This commit is contained in:
@@ -40,6 +40,7 @@ import AusruestungsanfrageNeu from './pages/AusruestungsanfrageNeu';
|
|||||||
import Checklisten from './pages/Checklisten';
|
import Checklisten from './pages/Checklisten';
|
||||||
import Buchhaltung from './pages/Buchhaltung';
|
import Buchhaltung from './pages/Buchhaltung';
|
||||||
import BuchhaltungKontoDetail from './pages/BuchhaltungKontoDetail';
|
import BuchhaltungKontoDetail from './pages/BuchhaltungKontoDetail';
|
||||||
|
import BuchhaltungKontoManage from './pages/BuchhaltungKontoManage';
|
||||||
import FahrzeugEinstellungen from './pages/FahrzeugEinstellungen';
|
import FahrzeugEinstellungen from './pages/FahrzeugEinstellungen';
|
||||||
import ChecklistAusfuehrung from './pages/ChecklistAusfuehrung';
|
import ChecklistAusfuehrung from './pages/ChecklistAusfuehrung';
|
||||||
import Issues from './pages/Issues';
|
import Issues from './pages/Issues';
|
||||||
@@ -380,6 +381,14 @@ function App() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/buchhaltung/konto/:id/verwalten"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<BuchhaltungKontoManage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/buchhaltung/konto/:id"
|
path="/buchhaltung/konto/:id"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||||
import {
|
import {
|
||||||
Accordion,
|
|
||||||
AccordionDetails,
|
|
||||||
AccordionSummary,
|
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -33,6 +30,7 @@ import {
|
|||||||
TableContainer,
|
TableContainer,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
|
TableSortLabel,
|
||||||
Tabs,
|
Tabs,
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -47,7 +45,6 @@ import {
|
|||||||
Edit,
|
Edit,
|
||||||
ExpandLess as ExpandLessIcon,
|
ExpandLess as ExpandLessIcon,
|
||||||
ExpandMore as ExpandMoreIcon,
|
ExpandMore as ExpandMoreIcon,
|
||||||
FilterList as FilterListIcon,
|
|
||||||
HowToReg,
|
HowToReg,
|
||||||
Lock,
|
Lock,
|
||||||
ThumbDown,
|
ThumbDown,
|
||||||
@@ -89,7 +86,7 @@ function fmtDate(val: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TabPanel({ children, value, index }: { children: React.ReactNode; value: number; index: number }) {
|
function TabPanel({ children, value, index }: { children: React.ReactNode; value: number; index: number }) {
|
||||||
return value === index ? <Box role="tabpanel">{children}</Box> : null;
|
return value === index ? <Box role="tabpanel" sx={{ pt: 3 }}>{children}</Box> : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Sub-components ────────────────────────────────────────────────────────────
|
// ─── Sub-components ────────────────────────────────────────────────────────────
|
||||||
@@ -415,22 +412,11 @@ function KontoRow({ konto, depth = 0, onNavigate }: { konto: KontoTreeNode; dept
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableRow>
|
<TableRow onClick={() => onNavigate(konto.id)} sx={{ cursor: 'pointer' }}>
|
||||||
<TableCell sx={{ pl: 2 + depth * 3 }}>
|
<TableCell sx={{ pl: 2 + depth * 3 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Typography variant="body2">
|
||||||
{konto.children.length > 0 && (
|
|
||||||
<IconButton size="small" onClick={() => setOpen(!open)}>
|
|
||||||
{open ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{ cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}
|
|
||||||
onClick={() => onNavigate(konto.id)}
|
|
||||||
>
|
|
||||||
{konto.kontonummer} — {konto.bezeichnung}
|
{konto.kontonummer} — {konto.bezeichnung}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
|
||||||
{totalBudget > 0 && (
|
{totalBudget > 0 && (
|
||||||
<LinearProgress variant="determinate" value={Math.min(utilization, 100)}
|
<LinearProgress variant="determinate" value={Math.min(utilization, 100)}
|
||||||
color={utilization > 100 ? 'error' : utilization > 80 ? 'warning' : 'primary'}
|
color={utilization > 100 ? 'error' : utilization > 80 ? 'warning' : 'primary'}
|
||||||
@@ -446,6 +432,13 @@ function KontoRow({ konto, depth = 0, onNavigate }: { konto: KontoTreeNode; dept
|
|||||||
<TableCell align="right">{fmtEur(konto.spent_instandhaltung)}</TableCell>
|
<TableCell align="right">{fmtEur(konto.spent_instandhaltung)}</TableCell>
|
||||||
<TableCell align="right"><strong>{fmtEur(totalSpent)}</strong></TableCell>
|
<TableCell align="right"><strong>{fmtEur(totalSpent)}</strong></TableCell>
|
||||||
<TableCell align="right">{fmtEur(konto.einnahmen_betrag)}</TableCell>
|
<TableCell align="right">{fmtEur(konto.einnahmen_betrag)}</TableCell>
|
||||||
|
<TableCell sx={{ width: 40, px: 0.5 }}>
|
||||||
|
{konto.children.length > 0 && (
|
||||||
|
<IconButton size="small" onClick={e => { e.stopPropagation(); setOpen(!open); }}>
|
||||||
|
{open ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{open && konto.children.map(child => (
|
{open && konto.children.map(child => (
|
||||||
<KontoRow key={child.id} konto={child} depth={depth + 1} onNavigate={onNavigate} />
|
<KontoRow key={child.id} konto={child} depth={depth + 1} onNavigate={onNavigate} />
|
||||||
@@ -454,44 +447,34 @@ function KontoRow({ konto, depth = 0, onNavigate }: { konto: KontoTreeNode; dept
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function KontoManageRow({ konto, depth = 0, canManage, onEdit, onDelete }: {
|
function KontoManageRow({ konto, depth = 0, onNavigate }: {
|
||||||
konto: KontoTreeNode;
|
konto: KontoTreeNode;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
canManage: boolean;
|
onNavigate: (id: number) => void;
|
||||||
onEdit: (k: Konto) => void;
|
|
||||||
onDelete: (id: number) => void;
|
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const totalBudget = Number(konto.budget_gwg) + Number(konto.budget_anlagen) + Number(konto.budget_instandhaltung);
|
const totalBudget = Number(konto.budget_gwg) + Number(konto.budget_anlagen) + Number(konto.budget_instandhaltung);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableRow hover>
|
<TableRow hover onClick={() => onNavigate(konto.id)} sx={{ cursor: 'pointer' }}>
|
||||||
<TableCell sx={{ pl: 2 + depth * 3 }}>
|
<TableCell sx={{ pl: 2 + depth * 3 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
||||||
{konto.children.length > 0 ? (
|
|
||||||
<IconButton size="small" onClick={() => setOpen(!open)}>
|
|
||||||
{open ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
|
||||||
</IconButton>
|
|
||||||
) : (
|
|
||||||
<Box sx={{ width: 28 }} />
|
|
||||||
)}
|
|
||||||
<Typography variant="body2">{konto.kontonummer} — {konto.bezeichnung}</Typography>
|
<Typography variant="body2">{konto.kontonummer} — {konto.bezeichnung}</Typography>
|
||||||
</Box>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">{fmtEur(konto.budget_gwg)}</TableCell>
|
<TableCell align="right">{fmtEur(konto.budget_gwg)}</TableCell>
|
||||||
<TableCell align="right">{fmtEur(konto.budget_anlagen)}</TableCell>
|
<TableCell align="right">{fmtEur(konto.budget_anlagen)}</TableCell>
|
||||||
<TableCell align="right">{fmtEur(konto.budget_instandhaltung)}</TableCell>
|
<TableCell align="right">{fmtEur(konto.budget_instandhaltung)}</TableCell>
|
||||||
<TableCell align="right"><strong>{fmtEur(totalBudget)}</strong></TableCell>
|
<TableCell align="right"><strong>{fmtEur(totalBudget)}</strong></TableCell>
|
||||||
{canManage && (
|
<TableCell sx={{ width: 40, px: 0.5 }}>
|
||||||
<TableCell>
|
{konto.children.length > 0 && (
|
||||||
<IconButton size="small" onClick={() => onEdit(konto as unknown as Konto)}><Edit fontSize="small" /></IconButton>
|
<IconButton size="small" onClick={e => { e.stopPropagation(); setOpen(!open); }}>
|
||||||
<IconButton size="small" color="error" onClick={() => onDelete(konto.id)}><Delete fontSize="small" /></IconButton>
|
{open ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||||
</TableCell>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{open && konto.children.map(child => (
|
{open && konto.children.map(child => (
|
||||||
<KontoManageRow key={child.id} konto={child} depth={depth + 1} canManage={canManage} onEdit={onEdit} onDelete={onDelete} />
|
<KontoManageRow key={child.id} konto={child} depth={depth + 1} onNavigate={onNavigate} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -513,8 +496,18 @@ function UebersichtTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
|||||||
|
|
||||||
const tree = buildTree(treeData);
|
const tree = buildTree(treeData);
|
||||||
|
|
||||||
const totalEinnahmen = treeData.reduce((s, k) => s + Number(k.einnahmen_betrag), 0);
|
const sumBudgetGwg = treeData.reduce((s, k) => s + Number(k.budget_gwg), 0);
|
||||||
const totalAusgaben = treeData.reduce((s, k) => s + Number(k.spent_gwg) + Number(k.spent_anlagen) + Number(k.spent_instandhaltung), 0);
|
const sumBudgetAnlagen = treeData.reduce((s, k) => s + Number(k.budget_anlagen), 0);
|
||||||
|
const sumBudgetInst = treeData.reduce((s, k) => s + Number(k.budget_instandhaltung), 0);
|
||||||
|
const sumBudgetGesamt = sumBudgetGwg + sumBudgetAnlagen + sumBudgetInst;
|
||||||
|
const sumSpentGwg = treeData.reduce((s, k) => s + Number(k.spent_gwg), 0);
|
||||||
|
const sumSpentAnlagen = treeData.reduce((s, k) => s + Number(k.spent_anlagen), 0);
|
||||||
|
const sumSpentInst = treeData.reduce((s, k) => s + Number(k.spent_instandhaltung), 0);
|
||||||
|
const sumSpentGesamt = sumSpentGwg + sumSpentAnlagen + sumSpentInst;
|
||||||
|
const sumEinnahmen = treeData.reduce((s, k) => s + Number(k.einnahmen_betrag), 0);
|
||||||
|
|
||||||
|
const totalEinnahmen = sumEinnahmen;
|
||||||
|
const totalAusgaben = sumSpentGesamt;
|
||||||
const saldo = totalEinnahmen - totalAusgaben;
|
const saldo = totalEinnahmen - totalAusgaben;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -567,15 +560,31 @@ function UebersichtTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
|||||||
<TableCell align="right">Ausgaben Instandh.</TableCell>
|
<TableCell align="right">Ausgaben Instandh.</TableCell>
|
||||||
<TableCell align="right">Ausgaben Gesamt</TableCell>
|
<TableCell align="right">Ausgaben Gesamt</TableCell>
|
||||||
<TableCell align="right">Einnahmen</TableCell>
|
<TableCell align="right">Einnahmen</TableCell>
|
||||||
|
<TableCell sx={{ width: 40 }} />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{tree.length === 0 && (
|
{tree.length === 0 && (
|
||||||
<TableRow><TableCell colSpan={10} align="center"><Typography color="text.secondary">Keine Konten</Typography></TableCell></TableRow>
|
<TableRow><TableCell colSpan={11} align="center"><Typography color="text.secondary">Keine Konten</Typography></TableCell></TableRow>
|
||||||
)}
|
)}
|
||||||
{tree.map(k => (
|
{tree.map(k => (
|
||||||
<KontoRow key={k.id} konto={k} onNavigate={(id) => navigate(`/buchhaltung/konto/${id}`)} />
|
<KontoRow key={k.id} konto={k} onNavigate={(id) => navigate(`/buchhaltung/konto/${id}`)} />
|
||||||
))}
|
))}
|
||||||
|
{tree.length > 0 && (
|
||||||
|
<TableRow sx={{ bgcolor: 'action.hover', '& td': { fontWeight: 700 } }}>
|
||||||
|
<TableCell>Gesamt</TableCell>
|
||||||
|
<TableCell align="right">{fmtEur(sumBudgetGwg)}</TableCell>
|
||||||
|
<TableCell align="right">{fmtEur(sumBudgetAnlagen)}</TableCell>
|
||||||
|
<TableCell align="right">{fmtEur(sumBudgetInst)}</TableCell>
|
||||||
|
<TableCell align="right">{fmtEur(sumBudgetGesamt)}</TableCell>
|
||||||
|
<TableCell align="right">{fmtEur(sumSpentGwg)}</TableCell>
|
||||||
|
<TableCell align="right">{fmtEur(sumSpentAnlagen)}</TableCell>
|
||||||
|
<TableCell align="right">{fmtEur(sumSpentInst)}</TableCell>
|
||||||
|
<TableCell align="right">{fmtEur(sumSpentGesamt)}</TableCell>
|
||||||
|
<TableCell align="right">{fmtEur(sumEinnahmen)}</TableCell>
|
||||||
|
<TableCell />
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
@@ -668,31 +677,30 @@ function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
|||||||
setFilters(f => ({ ...f, haushaltsjahr_id: selectedJahrId || undefined }));
|
setFilters(f => ({ ...f, haushaltsjahr_id: selectedJahrId || undefined }));
|
||||||
}, [selectedJahrId]);
|
}, [selectedJahrId]);
|
||||||
|
|
||||||
const activeFilterCount = [
|
|
||||||
filters.status,
|
|
||||||
filters.typ,
|
|
||||||
filters.search,
|
|
||||||
filterAusgabenTyp,
|
|
||||||
].filter(Boolean).length;
|
|
||||||
|
|
||||||
const filteredTransaktionen = filterAusgabenTyp
|
const filteredTransaktionen = filterAusgabenTyp
|
||||||
? transaktionen.filter(t => t.ausgaben_typ === filterAusgabenTyp)
|
? transaktionen.filter(t => t.ausgaben_typ === filterAusgabenTyp)
|
||||||
: transaktionen;
|
: transaktionen;
|
||||||
|
|
||||||
|
const [sortCol, setSortCol] = useState<'nr' | 'datum' | 'betrag'>('nr');
|
||||||
|
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
|
||||||
|
|
||||||
|
const handleSort = (col: 'nr' | 'datum' | 'betrag') => {
|
||||||
|
if (sortCol === col) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
|
||||||
|
else { setSortCol(col); setSortDir('asc'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedTransaktionen = [...filteredTransaktionen].sort((a, b) => {
|
||||||
|
let cmp = 0;
|
||||||
|
if (sortCol === 'nr') cmp = (a.laufende_nummer ?? 0) - (b.laufende_nummer ?? 0);
|
||||||
|
else if (sortCol === 'datum') cmp = new Date(a.datum).getTime() - new Date(b.datum).getTime();
|
||||||
|
else if (sortCol === 'betrag') cmp = Number(a.betrag) - Number(b.betrag);
|
||||||
|
return sortDir === 'asc' ? cmp : -cmp;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<Accordion sx={{ mb: 2 }}>
|
<Paper variant="outlined" sx={{ p: 1.5, mb: 2 }}>
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
<FilterListIcon fontSize="small" />
|
|
||||||
<Typography>Filter</Typography>
|
|
||||||
{activeFilterCount > 0 && (
|
|
||||||
<Badge badgeContent={activeFilterCount} color="primary" />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails>
|
|
||||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
<FormControl sx={{ minWidth: 200 }}>
|
<FormControl sx={{ minWidth: 200 }}>
|
||||||
<InputLabel>Haushaltsjahr</InputLabel>
|
<InputLabel>Haushaltsjahr</InputLabel>
|
||||||
@@ -742,29 +750,40 @@ function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</AccordionDetails>
|
</Paper>
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
{isLoading ? <CircularProgress /> : (
|
{isLoading ? <CircularProgress /> : (
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={Paper}>
|
||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Nr.</TableCell>
|
<TableCell sortDirection={sortCol === 'nr' ? sortDir : false}>
|
||||||
<TableCell>Datum</TableCell>
|
<TableSortLabel active={sortCol === 'nr'} direction={sortCol === 'nr' ? sortDir : 'asc'} onClick={() => handleSort('nr')}>
|
||||||
|
Nr.
|
||||||
|
</TableSortLabel>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sortDirection={sortCol === 'datum' ? sortDir : false}>
|
||||||
|
<TableSortLabel active={sortCol === 'datum'} direction={sortCol === 'datum' ? sortDir : 'asc'} onClick={() => handleSort('datum')}>
|
||||||
|
Datum
|
||||||
|
</TableSortLabel>
|
||||||
|
</TableCell>
|
||||||
<TableCell>Typ</TableCell>
|
<TableCell>Typ</TableCell>
|
||||||
<TableCell>Beschreibung</TableCell>
|
<TableCell>Beschreibung</TableCell>
|
||||||
<TableCell>Konto</TableCell>
|
<TableCell>Konto</TableCell>
|
||||||
<TableCell align="right">Betrag</TableCell>
|
<TableCell align="right" sortDirection={sortCol === 'betrag' ? sortDir : false}>
|
||||||
|
<TableSortLabel active={sortCol === 'betrag'} direction={sortCol === 'betrag' ? sortDir : 'asc'} onClick={() => handleSort('betrag')}>
|
||||||
|
Betrag
|
||||||
|
</TableSortLabel>
|
||||||
|
</TableCell>
|
||||||
<TableCell>Status</TableCell>
|
<TableCell>Status</TableCell>
|
||||||
<TableCell>Aktionen</TableCell>
|
<TableCell>Aktionen</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredTransaktionen.length === 0 && (
|
{sortedTransaktionen.length === 0 && (
|
||||||
<TableRow><TableCell colSpan={8} align="center"><Typography color="text.secondary">Keine Transaktionen</Typography></TableCell></TableRow>
|
<TableRow><TableCell colSpan={8} align="center"><Typography color="text.secondary">Keine Transaktionen</Typography></TableCell></TableRow>
|
||||||
)}
|
)}
|
||||||
{filteredTransaktionen.map((t: Transaktion) => (
|
{sortedTransaktionen.map((t: Transaktion) => (
|
||||||
<TableRow key={t.id} hover>
|
<TableRow key={t.id} hover>
|
||||||
<TableCell>{t.laufende_nummer ?? `E${t.id}`}</TableCell>
|
<TableCell>{t.laufende_nummer ?? `E${t.id}`}</TableCell>
|
||||||
<TableCell>{fmtDate(t.datum)}</TableCell>
|
<TableCell>{fmtDate(t.datum)}</TableCell>
|
||||||
@@ -965,6 +984,7 @@ function KontenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
|||||||
onJahrChange: (id: number) => void;
|
onJahrChange: (id: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const { hasPermission } = usePermissionContext();
|
const { hasPermission } = usePermissionContext();
|
||||||
const [subTab, setSubTab] = useState(0);
|
const [subTab, setSubTab] = useState(0);
|
||||||
@@ -1000,12 +1020,6 @@ function KontenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
|||||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-konten'] }); qc.invalidateQueries({ queryKey: ['kontenTree'] }); setKontoDialog({ open: false }); showSuccess('Konto aktualisiert'); },
|
onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-konten'] }); qc.invalidateQueries({ queryKey: ['kontenTree'] }); setKontoDialog({ open: false }); showSuccess('Konto aktualisiert'); },
|
||||||
onError: () => showError('Konto konnte nicht aktualisiert werden'),
|
onError: () => showError('Konto konnte nicht aktualisiert werden'),
|
||||||
});
|
});
|
||||||
const deleteKontoMut = useMutation({
|
|
||||||
mutationFn: buchhaltungApi.deleteKonto,
|
|
||||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-konten'] }); qc.invalidateQueries({ queryKey: ['kontenTree'] }); showSuccess('Konto gelöscht'); },
|
|
||||||
onError: () => showError('Löschen fehlgeschlagen'),
|
|
||||||
});
|
|
||||||
|
|
||||||
const createBankMut = useMutation({
|
const createBankMut = useMutation({
|
||||||
mutationFn: buchhaltungApi.createBankkonto,
|
mutationFn: buchhaltungApi.createBankkonto,
|
||||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['bankkonten'] }); setBankDialog({ open: false }); showSuccess('Bankkonto erstellt'); },
|
onSuccess: () => { qc.invalidateQueries({ queryKey: ['bankkonten'] }); setBankDialog({ open: false }); showSuccess('Bankkonto erstellt'); },
|
||||||
@@ -1084,18 +1098,16 @@ function KontenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
|||||||
<TableCell align="right">Anlagen</TableCell>
|
<TableCell align="right">Anlagen</TableCell>
|
||||||
<TableCell align="right">Instandh.</TableCell>
|
<TableCell align="right">Instandh.</TableCell>
|
||||||
<TableCell align="right">Gesamt</TableCell>
|
<TableCell align="right">Gesamt</TableCell>
|
||||||
{canManage && <TableCell>Aktionen</TableCell>}
|
<TableCell sx={{ width: 40 }} />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{kontenTree.length === 0 && <TableRow><TableCell colSpan={canManage ? 6 : 5} align="center"><Typography color="text.secondary">Keine Konten</Typography></TableCell></TableRow>}
|
{kontenTree.length === 0 && <TableRow><TableCell colSpan={6} align="center"><Typography color="text.secondary">Keine Konten</Typography></TableCell></TableRow>}
|
||||||
{kontenTree.map(k => (
|
{kontenTree.map(k => (
|
||||||
<KontoManageRow
|
<KontoManageRow
|
||||||
key={k.id}
|
key={k.id}
|
||||||
konto={k}
|
konto={k}
|
||||||
canManage={canManage}
|
onNavigate={(id) => navigate('/buchhaltung/konto/' + id + '/verwalten')}
|
||||||
onEdit={existing => setKontoDialog({ open: true, existing: existing as unknown as Konto })}
|
|
||||||
onDelete={id => deleteKontoMut.mutate(id)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -1309,17 +1321,16 @@ export default function Buchhaltung() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3, gap: 2 }}>
|
<Typography variant="h4" sx={{ flexGrow: 1 }}>Buchhaltung</Typography>
|
||||||
<Typography variant="h4" fontWeight={700}>Buchhaltung</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||||
<Tabs value={tabValue} onChange={handleTabChange} variant="scrollable" scrollButtons="auto" sx={{ mb: 3 }}>
|
<Tabs value={tabValue} onChange={handleTabChange} variant="scrollable" scrollButtons="auto">
|
||||||
<Tab label="Übersicht" />
|
<Tab label="Übersicht" />
|
||||||
<Tab label={<Badge badgeContent={pendingCount || 0} color="warning" invisible={!pendingCount}><span>Transaktionen</span></Badge>} />
|
<Tab label={<Badge badgeContent={pendingCount || 0} color="warning" invisible={!pendingCount}><span>Transaktionen</span></Badge>} />
|
||||||
<Tab label="Konten" />
|
<Tab label="Konten" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
</Box>
|
||||||
<TabPanel value={tabValue} index={0}>
|
<TabPanel value={tabValue} index={0}>
|
||||||
<UebersichtTab
|
<UebersichtTab
|
||||||
haushaltsjahre={haushaltsjahre}
|
haushaltsjahre={haushaltsjahre}
|
||||||
@@ -1341,7 +1352,6 @@ export default function Buchhaltung() {
|
|||||||
onJahrChange={setSelectedJahrId}
|
onJahrChange={setSelectedJahrId}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Box>
|
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ function BudgetCard({ label, budget, spent }: { label: string; budget: number; s
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="subtitle2" color="text.secondary">{label}</Typography>
|
<Typography variant="subtitle2" color="text.secondary">{label}</Typography>
|
||||||
<Typography variant="h6">{spent.toFixed(2).replace('.', ',')} €</Typography>
|
<Typography variant="h6">{Number(spent).toFixed(2).replace('.', ',')} €</Typography>
|
||||||
{budget > 0 && (
|
{budget > 0 && (
|
||||||
<>
|
<>
|
||||||
<Typography variant="body2" color="text.secondary">Budget: {budget.toFixed(2).replace('.', ',')} €</Typography>
|
<Typography variant="body2" color="text.secondary">Budget: {Number(budget).toFixed(2).replace('.', ',')} €</Typography>
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
variant="determinate"
|
variant="determinate"
|
||||||
value={utilization}
|
value={utilization}
|
||||||
|
|||||||
201
frontend/src/pages/BuchhaltungKontoManage.tsx
Normal file
201
frontend/src/pages/BuchhaltungKontoManage.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import {
|
||||||
|
Box, Button, TextField, Typography, Paper, Stack, MenuItem, Select,
|
||||||
|
FormControl, InputLabel, CircularProgress, Alert, Dialog, DialogTitle,
|
||||||
|
DialogContent, DialogActions, Skeleton,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { ArrowBack, Delete, Save } from '@mui/icons-material';
|
||||||
|
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||||
|
import { buchhaltungApi } from '../services/buchhaltung';
|
||||||
|
import { useNotification } from '../contexts/NotificationContext';
|
||||||
|
import type { KontoFormData } from '../types/buchhaltung.types';
|
||||||
|
|
||||||
|
export default function BuchhaltungKontoManage() {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const { showSuccess, showError } = useNotification();
|
||||||
|
const kontoId = Number(id);
|
||||||
|
|
||||||
|
const [form, setForm] = useState<Partial<KontoFormData>>({});
|
||||||
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ['kontoDetail', kontoId],
|
||||||
|
queryFn: () => buchhaltungApi.getKontoDetail(kontoId),
|
||||||
|
enabled: !!kontoId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load sibling konten for parent selector (exclude self)
|
||||||
|
const { data: alleKonten = [] } = useQuery({
|
||||||
|
queryKey: ['buchhaltung-konten', data?.konto.haushaltsjahr_id],
|
||||||
|
queryFn: () => buchhaltungApi.getKonten(data!.konto.haushaltsjahr_id),
|
||||||
|
enabled: !!data?.konto.haushaltsjahr_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.konto) {
|
||||||
|
const k = data.konto;
|
||||||
|
setForm({
|
||||||
|
haushaltsjahr_id: k.haushaltsjahr_id,
|
||||||
|
kontonummer: k.kontonummer,
|
||||||
|
bezeichnung: k.bezeichnung,
|
||||||
|
budget_gwg: k.budget_gwg,
|
||||||
|
budget_anlagen: k.budget_anlagen,
|
||||||
|
budget_instandhaltung: k.budget_instandhaltung,
|
||||||
|
parent_id: k.parent_id ?? undefined,
|
||||||
|
notizen: k.notizen ?? '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const updateMut = useMutation({
|
||||||
|
mutationFn: (d: Partial<KontoFormData>) => buchhaltungApi.updateKonto(kontoId, d),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['kontoDetail', kontoId] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['kontenTree'] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['buchhaltung-konten'] });
|
||||||
|
showSuccess('Konto gespeichert');
|
||||||
|
},
|
||||||
|
onError: () => showError('Speichern fehlgeschlagen'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMut = useMutation({
|
||||||
|
mutationFn: () => buchhaltungApi.deleteKonto(kontoId),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['kontenTree'] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['buchhaltung-konten'] });
|
||||||
|
showSuccess('Konto gelöscht');
|
||||||
|
navigate('/buchhaltung?tab=2');
|
||||||
|
},
|
||||||
|
onError: () => showError('Löschen fehlgeschlagen'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) return <DashboardLayout><Box sx={{ p: 3 }}><Skeleton variant="rectangular" height={400} /></Box></DashboardLayout>;
|
||||||
|
if (isError || !data) return <DashboardLayout><Box sx={{ p: 3 }}><Alert severity="error">Konto nicht gefunden.</Alert></Box></DashboardLayout>;
|
||||||
|
|
||||||
|
const konto = data.konto;
|
||||||
|
const otherKonten = alleKonten.filter(k => k.id !== kontoId);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!form.bezeichnung) return;
|
||||||
|
updateMut.mutate(form);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||||
|
<Button startIcon={<ArrowBack />} onClick={() => navigate('/buchhaltung?tab=2')}>
|
||||||
|
Zurück
|
||||||
|
</Button>
|
||||||
|
<Typography variant="h5" sx={{ flexGrow: 1, ml: 1 }}>
|
||||||
|
{konto.kontonummer} — {konto.bezeichnung}
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
startIcon={<Delete />}
|
||||||
|
onClick={() => setDeleteOpen(true)}
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<Save />}
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={updateMut.isPending || !form.bezeichnung}
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Paper sx={{ p: 3, maxWidth: 600 }}>
|
||||||
|
<Stack spacing={2.5}>
|
||||||
|
<TextField
|
||||||
|
label="Kontonummer"
|
||||||
|
type="number"
|
||||||
|
value={form.kontonummer ?? ''}
|
||||||
|
onChange={e => setForm(f => ({ ...f, kontonummer: Number(e.target.value) }))}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Bezeichnung"
|
||||||
|
value={form.bezeichnung ?? ''}
|
||||||
|
onChange={e => setForm(f => ({ ...f, bezeichnung: e.target.value }))}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>Übergeordnetes Konto</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={form.parent_id ?? ''}
|
||||||
|
label="Übergeordnetes Konto"
|
||||||
|
onChange={e => setForm(f => ({ ...f, parent_id: e.target.value ? Number(e.target.value) : undefined }))}
|
||||||
|
>
|
||||||
|
<MenuItem value=""><em>Kein übergeordnetes Konto</em></MenuItem>
|
||||||
|
{otherKonten.map(k => (
|
||||||
|
<MenuItem key={k.id} value={k.id}>{k.kontonummer} – {k.bezeichnung}</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<TextField
|
||||||
|
label="Budget GWG (€)"
|
||||||
|
type="number"
|
||||||
|
value={form.budget_gwg ?? 0}
|
||||||
|
onChange={e => setForm(f => ({ ...f, budget_gwg: parseFloat(e.target.value) || 0 }))}
|
||||||
|
inputProps={{ step: '0.01', min: '0' }}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Budget Anlagen (€)"
|
||||||
|
type="number"
|
||||||
|
value={form.budget_anlagen ?? 0}
|
||||||
|
onChange={e => setForm(f => ({ ...f, budget_anlagen: parseFloat(e.target.value) || 0 }))}
|
||||||
|
inputProps={{ step: '0.01', min: '0' }}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Budget Instandhaltung (€)"
|
||||||
|
type="number"
|
||||||
|
value={form.budget_instandhaltung ?? 0}
|
||||||
|
onChange={e => setForm(f => ({ ...f, budget_instandhaltung: parseFloat(e.target.value) || 0 }))}
|
||||||
|
inputProps={{ step: '0.01', min: '0' }}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Notizen"
|
||||||
|
value={form.notizen ?? ''}
|
||||||
|
onChange={e => setForm(f => ({ ...f, notizen: e.target.value }))}
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Dialog open={deleteOpen} onClose={() => setDeleteOpen(false)}>
|
||||||
|
<DialogTitle>Konto löschen</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography>
|
||||||
|
Möchten Sie das Konto <strong>{konto.kontonummer} — {konto.bezeichnung}</strong> wirklich löschen?
|
||||||
|
Diese Aktion kann nicht rückgängig gemacht werden.
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setDeleteOpen(false)}>Abbrechen</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
onClick={() => { deleteMut.mutate(); setDeleteOpen(false); }}
|
||||||
|
disabled={deleteMut.isPending}
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user