fix: auto-select open fiscal year on load, derive sub-account number from parent, replace flat konten table with collapsible tree

This commit is contained in:
Matthias Hochmeister
2026-03-30 10:51:55 +02:00
parent 0c5432b50e
commit cdaaec2971

View File

@@ -18,6 +18,7 @@ import {
FormControl,
FormControlLabel,
IconButton,
InputAdornment,
InputLabel,
LinearProgress,
MenuItem,
@@ -199,6 +200,25 @@ function KontoDialog({
const empty: KontoFormData = { haushaltsjahr_id: haushaltsjahrId, kontonummer: '', bezeichnung: '', budget_gwg: 0, budget_anlagen: 0, budget_instandhaltung: 0, parent_id: null, notizen: '' };
const [form, setForm] = useState<KontoFormData>(empty);
const selectedParent = konten.find(k => k.id === form.parent_id);
const parentPrefix = selectedParent ? selectedParent.kontonummer : '';
// suffix = part of kontonummer after parent prefix
const kontonummerSuffix = form.kontonummer.startsWith(parentPrefix) ? form.kontonummer.slice(parentPrefix.length) : form.kontonummer;
const handleParentChange = (parentId: number | null) => {
const parent = konten.find(k => k.id === parentId);
const prefix = parent ? parent.kontonummer : '';
// strip old prefix, re-apply new one
const oldPrefix = selectedParent ? selectedParent.kontonummer : '';
const suffix = form.kontonummer.startsWith(oldPrefix) ? form.kontonummer.slice(oldPrefix.length) : form.kontonummer;
setForm(f => ({ ...f, parent_id: parentId, kontonummer: prefix + suffix }));
};
const handleSuffixChange = (suffix: string) => {
const digits = suffix.replace(/\D/g, '');
setForm(f => ({ ...f, kontonummer: parentPrefix + digits }));
};
useEffect(() => {
if (existing) {
setForm({ haushaltsjahr_id: haushaltsjahrId, konto_typ_id: existing.konto_typ_id ?? undefined, kontonummer: existing.kontonummer, bezeichnung: existing.bezeichnung, budget_gwg: existing.budget_gwg, budget_anlagen: existing.budget_anlagen, budget_instandhaltung: existing.budget_instandhaltung, parent_id: existing.parent_id, notizen: existing.notizen || '' });
@@ -213,7 +233,17 @@ function KontoDialog({
<DialogTitle>{existing ? 'Konto bearbeiten' : 'Neues Konto'}</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 1 }}>
<TextField label="Kontonummer" value={form.kontonummer} onChange={e => setForm(f => ({ ...f, kontonummer: e.target.value }))} required />
<TextField
label="Kontonummer"
value={kontonummerSuffix}
onChange={e => handleSuffixChange(e.target.value)}
required
inputProps={{ inputMode: 'numeric', pattern: '[0-9]*' }}
InputProps={parentPrefix ? {
startAdornment: <InputAdornment position="start">{parentPrefix}</InputAdornment>,
} : undefined}
helperText={parentPrefix ? `Vollständige Nummer: ${form.kontonummer || parentPrefix + '…'}` : undefined}
/>
<TextField label="Bezeichnung" value={form.bezeichnung} onChange={e => setForm(f => ({ ...f, bezeichnung: e.target.value }))} required />
<FormControl fullWidth>
<InputLabel>Kontotyp</InputLabel>
@@ -226,7 +256,7 @@ function KontoDialog({
<InputLabel>Elternkonto (optional)</InputLabel>
<Select
value={form.parent_id ?? ''}
onChange={e => setForm(f => ({ ...f, parent_id: e.target.value ? Number(e.target.value) : null }))}
onChange={e => handleParentChange(e.target.value ? Number(e.target.value) : null)}
label="Elternkonto (optional)"
>
<MenuItem value=""><em>Kein Elternkonto</em></MenuItem>
@@ -431,6 +461,50 @@ function KontoRow({ konto, depth = 0, onNavigate }: { konto: KontoTreeNode; dept
);
}
function KontoManageRow({ konto, depth = 0, canManage, onEdit, onDelete }: {
konto: KontoTreeNode;
depth?: number;
canManage: boolean;
onEdit: (k: Konto) => void;
onDelete: (id: number) => void;
}) {
const [open, setOpen] = useState(false);
const totalBudget = Number(konto.budget_gwg) + Number(konto.budget_anlagen) + Number(konto.budget_instandhaltung);
return (
<>
<TableRow hover>
<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>
</Box>
</TableCell>
<TableCell>{konto.konto_typ_bezeichnung || ''}</TableCell>
<TableCell align="right">{fmtEur(konto.budget_gwg)}</TableCell>
<TableCell align="right">{fmtEur(konto.budget_anlagen)}</TableCell>
<TableCell align="right">{fmtEur(konto.budget_instandhaltung)}</TableCell>
<TableCell align="right"><strong>{fmtEur(totalBudget)}</strong></TableCell>
{canManage && (
<TableCell>
<IconButton size="small" onClick={() => onEdit(konto as unknown as Konto)}><Edit fontSize="small" /></IconButton>
<IconButton size="small" color="error" onClick={() => onDelete(konto.id)}><Delete fontSize="small" /></IconButton>
</TableCell>
)}
</TableRow>
{open && konto.children.map(child => (
<KontoManageRow key={child.id} konto={child} depth={depth + 1} canManage={canManage} onEdit={onEdit} onDelete={onDelete} />
))}
</>
);
}
// ─── Tab 0: Übersicht ─────────────────────────────────────────────────────────
function UebersichtTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
@@ -907,11 +981,18 @@ function KontenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
const [jahrDialog, setJahrDialog] = useState<{ open: boolean; existing?: Haushaltsjahr }>({ open: false });
const [wiederkehrendDialog, setWiederkehrendDialog] = useState<{ open: boolean; existing?: WiederkehrendBuchung }>({ open: false });
const { data: konten = [] } = useQuery({
const { data: kontenFlat = [] } = useQuery({
queryKey: ['buchhaltung-konten', selectedJahrId],
queryFn: () => buchhaltungApi.getKonten(selectedJahrId!),
enabled: selectedJahrId != null,
});
const { data: kontenTreeData = [] } = useQuery({
queryKey: ['kontenTree', selectedJahrId],
queryFn: () => buchhaltungApi.getKontenTree(selectedJahrId!),
enabled: selectedJahrId != null,
});
const kontenTree = buildTree(kontenTreeData);
const konten = kontenFlat;
const { data: bankkonten = [] } = useQuery({ queryKey: ['bankkonten'], queryFn: buchhaltungApi.getBankkonten });
const { data: wiederkehrend = [] } = useQuery({ queryKey: ['buchhaltung-wiederkehrend'], queryFn: buchhaltungApi.getWiederkehrend });
@@ -1006,10 +1087,8 @@ function KontenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Kontonummer</TableCell>
<TableCell>Bezeichnung</TableCell>
<TableCell>Konto</TableCell>
<TableCell>Typ</TableCell>
<TableCell>Elternkonto</TableCell>
<TableCell align="right">GWG</TableCell>
<TableCell align="right">Anlagen</TableCell>
<TableCell align="right">Instandh.</TableCell>
@@ -1018,24 +1097,15 @@ function KontenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
</TableRow>
</TableHead>
<TableBody>
{konten.length === 0 && <TableRow><TableCell colSpan={canManage ? 9 : 8} align="center"><Typography color="text.secondary">Keine Konten</Typography></TableCell></TableRow>}
{konten.map((k: Konto) => (
<TableRow key={k.id} hover>
<TableCell>{k.kontonummer}</TableCell>
<TableCell>{k.bezeichnung}</TableCell>
<TableCell>{k.konto_typ_bezeichnung || ''}</TableCell>
<TableCell>{k.parent_bezeichnung || ''}</TableCell>
<TableCell align="right">{fmtEur(k.budget_gwg)}</TableCell>
<TableCell align="right">{fmtEur(k.budget_anlagen)}</TableCell>
<TableCell align="right">{fmtEur(k.budget_instandhaltung)}</TableCell>
<TableCell align="right">{fmtEur(k.budget_gwg + k.budget_anlagen + k.budget_instandhaltung)}</TableCell>
{canManage && (
<TableCell>
<IconButton size="small" onClick={() => setKontoDialog({ open: true, existing: k })}><Edit fontSize="small" /></IconButton>
<IconButton size="small" color="error" onClick={() => deleteKontoMut.mutate(k.id)}><Delete fontSize="small" /></IconButton>
</TableCell>
)}
</TableRow>
{kontenTree.length === 0 && <TableRow><TableCell colSpan={canManage ? 7 : 6} align="center"><Typography color="text.secondary">Keine Konten</Typography></TableCell></TableRow>}
{kontenTree.map(k => (
<KontoManageRow
key={k.id}
konto={k}
canManage={canManage}
onEdit={existing => setKontoDialog({ open: true, existing: existing as unknown as Konto })}
onDelete={id => deleteKontoMut.mutate(id)}
/>
))}
</TableBody>
</Table>
@@ -1230,14 +1300,15 @@ export default function Buchhaltung() {
const { data: haushaltsjahre = [] } = useQuery({
queryKey: ['haushaltsjahre'],
queryFn: buchhaltungApi.getHaushaltsjahre,
onSuccess: (data: Haushaltsjahr[]) => {
if (data.length > 0 && !selectedJahrId) {
const openYear = data.find(hj => !hj.abgeschlossen) || data[0];
setSelectedJahrId(openYear.id);
}
},
});
useEffect(() => {
if (haushaltsjahre.length > 0 && !selectedJahrId) {
const openYear = haushaltsjahre.find((hj: Haushaltsjahr) => !hj.abgeschlossen) || haushaltsjahre[0];
setSelectedJahrId(openYear.id);
}
}, [haushaltsjahre]);
const { data: pendingCount } = useQuery({
queryKey: ['buchhaltungPending', selectedJahrId],
queryFn: () => buchhaltungApi.getPendingCount(selectedJahrId || undefined),