feat(buchhaltung): budget types, erstattungen, recurring tab move, overview dividers, order completion guard
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
@@ -205,6 +206,7 @@ export default function BestellungDetail() {
|
||||
const canApprove = hasPermission('bestellungen:approve');
|
||||
const canExport = hasPermission('bestellungen:export');
|
||||
const validTransitions = bestellung ? STATUS_TRANSITIONS[bestellung.status] : [];
|
||||
const allCostsEntered = positionen.length === 0 || positionen.every(p => p.einzelpreis != null && Number(p.einzelpreis) > 0);
|
||||
|
||||
// All statuses except current, for force override
|
||||
const ALL_STATUSES: BestellungStatus[] = ['entwurf', 'wartet_auf_genehmigung', 'bereit_zur_bestellung', 'bestellt', 'teillieferung', 'lieferung_pruefen', 'abgeschlossen'];
|
||||
@@ -766,7 +768,11 @@ export default function BestellungDetail() {
|
||||
|
||||
{/* ── Status Action ── */}
|
||||
{(canManageOrders || canCreate || canApprove) && (
|
||||
<Box sx={{ mb: 3, display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{validTransitions.includes('abgeschlossen' as BestellungStatus) && !allCostsEntered && (
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>Nicht alle Positionen haben Kosten. Bitte Einzelpreise eintragen, bevor die Bestellung abgeschlossen wird.</Alert>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
{validTransitions
|
||||
.filter((s) => {
|
||||
// Approve/reject transitions from wartet_auf_genehmigung require canApprove
|
||||
@@ -787,11 +793,13 @@ export default function BestellungDetail() {
|
||||
? 'Ablehnen'
|
||||
: `Status: ${BESTELLUNG_STATUS_LABELS[s]}`;
|
||||
const color = isApprove ? 'success' : isReject ? 'error' : 'primary';
|
||||
const isAbgeschlossen = s === 'abgeschlossen';
|
||||
return (
|
||||
<Button
|
||||
key={s}
|
||||
variant="contained"
|
||||
color={color as 'success' | 'error' | 'primary'}
|
||||
disabled={isAbgeschlossen && !allCostsEntered}
|
||||
onClick={() => { setStatusForce(false); setStatusConfirmTarget(s); }}
|
||||
>
|
||||
{label}
|
||||
@@ -830,6 +838,7 @@ export default function BestellungDetail() {
|
||||
</Menu>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Checkbox,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
@@ -34,6 +35,8 @@ import {
|
||||
TableSortLabel,
|
||||
Tabs,
|
||||
TextField,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
@@ -69,6 +72,8 @@ import type {
|
||||
AusgabenTyp,
|
||||
WiederkehrendBuchung, WiederkehrendFormData,
|
||||
WiederkehrendIntervall,
|
||||
BudgetTyp,
|
||||
ErstattungFormData,
|
||||
} from '../types/buchhaltung.types';
|
||||
import {
|
||||
TRANSAKTION_STATUS_LABELS,
|
||||
@@ -83,6 +88,8 @@ function fmtEur(val: number) {
|
||||
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val);
|
||||
}
|
||||
|
||||
const dividerLeft = { borderLeft: '2px solid', borderColor: 'divider' } as const;
|
||||
|
||||
function fmtDate(val: string) {
|
||||
return new Date(val).toLocaleDateString('de-DE');
|
||||
}
|
||||
@@ -199,7 +206,7 @@ function KontoDialog({
|
||||
onSave: (data: KontoFormData) => void;
|
||||
externalError?: string | null;
|
||||
}) {
|
||||
const empty: KontoFormData = { haushaltsjahr_id: haushaltsjahrId, kontonummer: 0, bezeichnung: '', budget_gwg: 0, budget_anlagen: 0, budget_instandhaltung: 0, parent_id: null, kategorie_id: null, notizen: '' };
|
||||
const empty: KontoFormData = { haushaltsjahr_id: haushaltsjahrId, kontonummer: 0, bezeichnung: '', budget_gwg: 0, budget_anlagen: 0, budget_instandhaltung: 0, budget_typ: 'detailliert', budget_gesamt: 0, parent_id: null, kategorie_id: null, notizen: '' };
|
||||
const [form, setForm] = useState<KontoFormData>(empty);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
|
||||
@@ -234,7 +241,7 @@ function KontoDialog({
|
||||
|
||||
useEffect(() => {
|
||||
if (existing) {
|
||||
setForm({ haushaltsjahr_id: haushaltsjahrId, 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, kategorie_id: existing.kategorie_id ?? null, notizen: existing.notizen || '' });
|
||||
setForm({ haushaltsjahr_id: haushaltsjahrId, kontonummer: existing.kontonummer, bezeichnung: existing.bezeichnung, budget_gwg: existing.budget_gwg, budget_anlagen: existing.budget_anlagen, budget_instandhaltung: existing.budget_instandhaltung, budget_typ: existing.budget_typ || 'detailliert', budget_gesamt: existing.budget_gesamt || 0, parent_id: existing.parent_id, kategorie_id: existing.kategorie_id ?? null, notizen: existing.notizen || '' });
|
||||
} else {
|
||||
setForm({ ...empty, haushaltsjahr_id: haushaltsjahrId });
|
||||
}
|
||||
@@ -290,6 +297,24 @@ function KontoDialog({
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
{!form.parent_id && (
|
||||
<Box>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>Budget-Typ</Typography>
|
||||
<ToggleButtonGroup
|
||||
value={form.budget_typ || 'detailliert'}
|
||||
exclusive
|
||||
size="small"
|
||||
onChange={(_, val) => { if (val) setForm(f => ({ ...f, budget_typ: val as BudgetTyp })); }}
|
||||
>
|
||||
<ToggleButton value="detailliert">Detailliert</ToggleButton>
|
||||
<ToggleButton value="einfach">Einfach</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
)}
|
||||
{(form.budget_typ || 'detailliert') === 'einfach' ? (
|
||||
<TextField label="Budget Gesamt (€)" type="number" value={form.budget_gesamt ?? 0} onChange={e => setForm(f => ({ ...f, budget_gesamt: parseFloat(e.target.value) || 0 }))} inputProps={{ step: '0.01', min: '0' }} />
|
||||
) : (
|
||||
<>
|
||||
<TextField label="GWG Budget (€)" type="number" value={form.budget_gwg} onChange={e => setForm(f => ({ ...f, budget_gwg: parseFloat(e.target.value) || 0 }))} inputProps={{ step: '0.01', min: '0' }} />
|
||||
{selectedParent && siblingBudgets && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: -1 }}>
|
||||
@@ -308,6 +333,8 @@ function KontoDialog({
|
||||
Eltern-Budget: {fmtEur(selectedParent.budget_instandhaltung)}, vergeben: {fmtEur(siblingBudgets.instandhaltung)}, verfügbar: {fmtEur(selectedParent.budget_instandhaltung - siblingBudgets.instandhaltung)}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<TextField label="Notizen" value={form.notizen} onChange={e => setForm(f => ({ ...f, notizen: e.target.value }))} multiline rows={2} />
|
||||
{saveError && <Alert severity="error" onClose={() => setSaveError(null)}>{saveError}</Alert>}
|
||||
{!saveError && externalError && <Alert severity="error">{externalError}</Alert>}
|
||||
@@ -385,7 +412,10 @@ function TransaktionDialog({
|
||||
<MenuItem value="einnahme">Einnahme</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
{form.typ === 'ausgabe' && (
|
||||
{form.typ === 'ausgabe' && (() => {
|
||||
const selectedKonto = konten.find(k => k.id === form.konto_id);
|
||||
const isEinfach = selectedKonto && (selectedKonto.budget_typ || 'detailliert') === 'einfach';
|
||||
return !isEinfach ? (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Ausgaben-Typ</InputLabel>
|
||||
<Select
|
||||
@@ -399,7 +429,8 @@ function TransaktionDialog({
|
||||
<MenuItem value="instandhaltung">Instandhaltung</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
) : null;
|
||||
})()}
|
||||
<TextField label="Betrag (€)" type="number" value={form.betrag} onChange={e => setForm(f => ({ ...f, betrag: parseFloat(e.target.value) || 0 }))} inputProps={{ step: '0.01', min: '0' }} required />
|
||||
<TextField label="Datum" type="date" value={form.datum} onChange={e => setForm(f => ({ ...f, datum: e.target.value }))} InputLabelProps={{ shrink: true }} required />
|
||||
<FormControl fullWidth>
|
||||
@@ -455,7 +486,8 @@ function buildTree(flat: KontoTreeNode[]): KontoTreeNode[] {
|
||||
|
||||
function KontoRow({ konto, depth = 0, onNavigate }: { konto: KontoTreeNode; depth?: number; onNavigate: (id: number) => void }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const totalBudget = konto.budget_gwg + konto.budget_anlagen + konto.budget_instandhaltung;
|
||||
const isEinfach = (konto.budget_typ || 'detailliert') === 'einfach';
|
||||
const totalBudget = isEinfach ? Number(konto.budget_gesamt || 0) : konto.budget_gwg + konto.budget_anlagen + konto.budget_instandhaltung;
|
||||
const totalSpent = konto.spent_gwg + konto.spent_anlagen + konto.spent_instandhaltung;
|
||||
const utilization = totalBudget > 0 ? (totalSpent / totalBudget) * 100 : 0;
|
||||
|
||||
@@ -472,15 +504,15 @@ function KontoRow({ konto, depth = 0, onNavigate }: { konto: KontoTreeNode; dept
|
||||
sx={{ mt: 0.5, height: 4, borderRadius: 2 }} />
|
||||
)}
|
||||
</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" sx={dividerLeft}>{isEinfach ? '' : fmtEur(konto.budget_gwg)}</TableCell>
|
||||
<TableCell align="right">{isEinfach ? '' : fmtEur(konto.budget_anlagen)}</TableCell>
|
||||
<TableCell align="right">{isEinfach ? '' : fmtEur(konto.budget_instandhaltung)}</TableCell>
|
||||
<TableCell align="right"><strong>{fmtEur(totalBudget)}</strong></TableCell>
|
||||
<TableCell align="right">{fmtEur(konto.spent_gwg)}</TableCell>
|
||||
<TableCell align="right">{fmtEur(konto.spent_anlagen)}</TableCell>
|
||||
<TableCell align="right">{fmtEur(konto.spent_instandhaltung)}</TableCell>
|
||||
<TableCell align="right" sx={dividerLeft}>{isEinfach ? '' : fmtEur(konto.spent_gwg)}</TableCell>
|
||||
<TableCell align="right">{isEinfach ? '' : fmtEur(konto.spent_anlagen)}</TableCell>
|
||||
<TableCell align="right">{isEinfach ? '' : fmtEur(konto.spent_instandhaltung)}</TableCell>
|
||||
<TableCell align="right"><strong>{fmtEur(totalSpent)}</strong></TableCell>
|
||||
<TableCell align="right">{fmtEur(konto.einnahmen_betrag)}</TableCell>
|
||||
<TableCell align="right" sx={dividerLeft}>{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); }}>
|
||||
@@ -605,15 +637,15 @@ function UebersichtTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Konto</TableCell>
|
||||
<TableCell align="right">Budget GWG</TableCell>
|
||||
<TableCell align="right" sx={dividerLeft}>Budget GWG</TableCell>
|
||||
<TableCell align="right">Budget Anlagen</TableCell>
|
||||
<TableCell align="right">Budget Instandh.</TableCell>
|
||||
<TableCell align="right">Budget Gesamt</TableCell>
|
||||
<TableCell align="right">Ausgaben GWG</TableCell>
|
||||
<TableCell align="right" sx={dividerLeft}>Ausgaben GWG</TableCell>
|
||||
<TableCell align="right">Ausgaben Anlagen</TableCell>
|
||||
<TableCell align="right">Ausgaben Instandh.</TableCell>
|
||||
<TableCell align="right">Ausgaben Gesamt</TableCell>
|
||||
<TableCell align="right">Einnahmen</TableCell>
|
||||
<TableCell align="right" sx={dividerLeft}>Einnahmen</TableCell>
|
||||
<TableCell sx={{ width: 40 }} />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -650,15 +682,15 @@ function UebersichtTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
rows.push(
|
||||
<TableRow key={`cat-${key}`} sx={{ bgcolor: 'grey.100', '& td': { fontWeight: 600, fontSize: '0.8rem' } }}>
|
||||
<TableCell>{katName}</TableCell>
|
||||
<TableCell align="right">{fmtEur(catBudgetGwg)}</TableCell>
|
||||
<TableCell align="right" sx={dividerLeft}>{fmtEur(catBudgetGwg)}</TableCell>
|
||||
<TableCell align="right">{fmtEur(catBudgetAnl)}</TableCell>
|
||||
<TableCell align="right">{fmtEur(catBudgetInst)}</TableCell>
|
||||
<TableCell align="right">{fmtEur(catBudgetGwg + catBudgetAnl + catBudgetInst)}</TableCell>
|
||||
<TableCell align="right">{fmtEur(catSpentGwg)}</TableCell>
|
||||
<TableCell align="right" sx={dividerLeft}>{fmtEur(catSpentGwg)}</TableCell>
|
||||
<TableCell align="right">{fmtEur(catSpentAnl)}</TableCell>
|
||||
<TableCell align="right">{fmtEur(catSpentInst)}</TableCell>
|
||||
<TableCell align="right">{fmtEur(catSpentGwg + catSpentAnl + catSpentInst)}</TableCell>
|
||||
<TableCell align="right">{fmtEur(catEinnahmen)}</TableCell>
|
||||
<TableCell align="right" sx={dividerLeft}>{fmtEur(catEinnahmen)}</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
);
|
||||
@@ -672,15 +704,15 @@ function UebersichtTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
{tree.length > 0 && (
|
||||
<TableRow sx={{ bgcolor: 'action.hover', '& td': { fontWeight: 700 } }}>
|
||||
<TableCell>Gesamt</TableCell>
|
||||
<TableCell align="right">{fmtEur(sumBudgetGwg)}</TableCell>
|
||||
<TableCell align="right" sx={dividerLeft}>{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" sx={dividerLeft}>{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 align="right" sx={dividerLeft}>{fmtEur(sumEinnahmen)}</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
)}
|
||||
@@ -696,6 +728,141 @@ function UebersichtTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Erstattung Dialog ────────────────────────────────────────────────────────
|
||||
|
||||
function ErstattungDialog({
|
||||
open,
|
||||
onClose,
|
||||
konten,
|
||||
bankkonten,
|
||||
transaktionen,
|
||||
onSave,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
konten: Konto[];
|
||||
bankkonten: Bankkonto[];
|
||||
transaktionen: Transaktion[];
|
||||
onSave: (data: ErstattungFormData) => void;
|
||||
}) {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const [selectedKonto, setSelectedKonto] = useState<number | null>(null);
|
||||
const [selectedBank, setSelectedBank] = useState<number | null>(null);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
||||
const [betrag, setBetrag] = useState(0);
|
||||
const [datum, setDatum] = useState(today);
|
||||
const [beschreibung, setBeschreibung] = useState('');
|
||||
const [empfaenger, setEmpfaenger] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setSelectedKonto(null);
|
||||
setSelectedBank(null);
|
||||
setSelectedIds(new Set());
|
||||
setBetrag(0);
|
||||
setDatum(today);
|
||||
setBeschreibung('');
|
||||
setEmpfaenger('');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
const ausgabeTransaktionen = transaktionen.filter(
|
||||
t => t.typ === 'ausgabe' && t.status === 'gebucht' && (!selectedKonto || t.konto_id === selectedKonto)
|
||||
);
|
||||
|
||||
const autoSum = [...selectedIds].reduce((sum, id) => {
|
||||
const t = transaktionen.find(tx => tx.id === id);
|
||||
return sum + (t ? Number(t.betrag) : 0);
|
||||
}, 0);
|
||||
|
||||
useEffect(() => {
|
||||
setBetrag(autoSum);
|
||||
}, [autoSum]);
|
||||
|
||||
const toggleId = (id: number) => {
|
||||
setSelectedIds(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id); else next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Erstattung erfassen</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={2} sx={{ mt: 1 }}>
|
||||
<FormControl fullWidth required>
|
||||
<InputLabel>Konto</InputLabel>
|
||||
<Select value={selectedKonto ?? ''} label="Konto" onChange={e => { setSelectedKonto(e.target.value ? Number(e.target.value) : null); setSelectedIds(new Set()); }}>
|
||||
<MenuItem value=""><em>Alle Konten</em></MenuItem>
|
||||
{konten.map(k => <MenuItem key={k.id} value={k.id}>{k.kontonummer} – {k.bezeichnung}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Bankkonto</InputLabel>
|
||||
<Select value={selectedBank ?? ''} label="Bankkonto" onChange={e => setSelectedBank(e.target.value ? Number(e.target.value) : null)}>
|
||||
<MenuItem value=""><em>Kein Bankkonto</em></MenuItem>
|
||||
{bankkonten.map(bk => <MenuItem key={bk.id} value={bk.id}>{bk.bezeichnung}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Typography variant="subtitle2">Ausgabe-Transaktionen auswählen</Typography>
|
||||
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: 300 }}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell padding="checkbox" />
|
||||
<TableCell>Datum</TableCell>
|
||||
<TableCell>Beschreibung</TableCell>
|
||||
<TableCell align="right">Betrag</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{ausgabeTransaktionen.length === 0 && (
|
||||
<TableRow><TableCell colSpan={4} align="center"><Typography color="text.secondary" variant="body2">Keine Ausgaben gefunden</Typography></TableCell></TableRow>
|
||||
)}
|
||||
{ausgabeTransaktionen.map(t => (
|
||||
<TableRow key={t.id} hover onClick={() => toggleId(t.id)} sx={{ cursor: 'pointer' }}>
|
||||
<TableCell padding="checkbox"><Checkbox checked={selectedIds.has(t.id)} size="small" /></TableCell>
|
||||
<TableCell>{fmtDate(t.datum)}</TableCell>
|
||||
<TableCell>{t.beschreibung || t.empfaenger_auftraggeber || '–'}</TableCell>
|
||||
<TableCell align="right">{fmtEur(Number(t.betrag))}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<TextField label="Erstattungsbetrag (€)" type="number" value={betrag} onChange={e => setBetrag(parseFloat(e.target.value) || 0)} inputProps={{ step: '0.01', min: '0' }} required />
|
||||
<TextField label="Datum" type="date" value={datum} onChange={e => setDatum(e.target.value)} InputLabelProps={{ shrink: true }} required />
|
||||
<TextField label="Beschreibung" value={beschreibung} onChange={e => setBeschreibung(e.target.value)} />
|
||||
<TextField label="Empfänger" value={empfaenger} onChange={e => setEmpfaenger(e.target.value)} />
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Abbrechen</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!selectedKonto || !betrag || !datum || selectedIds.size === 0}
|
||||
onClick={() => onSave({
|
||||
konto_id: selectedKonto!,
|
||||
bankkonto_id: selectedBank,
|
||||
betrag,
|
||||
datum,
|
||||
beschreibung: beschreibung || undefined,
|
||||
empfaenger_auftraggeber: empfaenger || undefined,
|
||||
quell_transaktion_ids: [...selectedIds],
|
||||
})}
|
||||
>
|
||||
Erstattung erstellen
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Tab 1: Transaktionen ─────────────────────────────────────────────────────
|
||||
|
||||
function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
@@ -709,6 +876,21 @@ function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
const [filters, setFilters] = useState<TransaktionFilters>({ haushaltsjahr_id: selectedJahrId || undefined });
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [filterAusgabenTyp, setFilterAusgabenTyp] = useState('');
|
||||
const [txSubTab, setTxSubTab] = useState(0);
|
||||
|
||||
// ── Erstattung state ──
|
||||
const [erstattungOpen, setErstattungOpen] = useState(false);
|
||||
|
||||
// ── Wiederkehrend state ──
|
||||
const [wiederkehrendDialog, setWiederkehrendDialog] = useState<{ open: boolean; existing?: WiederkehrendBuchung }>({ open: false });
|
||||
|
||||
const { data: kontenFlat = [] } = useQuery({
|
||||
queryKey: ['buchhaltung-konten', selectedJahrId],
|
||||
queryFn: () => buchhaltungApi.getKonten(selectedJahrId!),
|
||||
enabled: selectedJahrId != null,
|
||||
});
|
||||
const { data: bankkonten = [] } = useQuery({ queryKey: ['bankkonten'], queryFn: buchhaltungApi.getBankkonten });
|
||||
const { data: wiederkehrend = [] } = useQuery({ queryKey: ['buchhaltung-wiederkehrend'], queryFn: buchhaltungApi.getWiederkehrend });
|
||||
|
||||
const { data: transaktionen = [], isLoading } = useQuery({
|
||||
queryKey: ['buchhaltung-transaktionen', filters],
|
||||
@@ -757,6 +939,32 @@ function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
onError: () => showError('Ablehnung fehlgeschlagen'),
|
||||
});
|
||||
|
||||
// ── Wiederkehrend mutations ──
|
||||
const canManage = hasPermission('buchhaltung:manage_accounts');
|
||||
|
||||
const createWiederkehrendMut = useMutation({
|
||||
mutationFn: buchhaltungApi.createWiederkehrend,
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-wiederkehrend'] }); setWiederkehrendDialog({ open: false }); showSuccess('Wiederkehrende Buchung erstellt'); },
|
||||
onError: () => showError('Erstellen fehlgeschlagen'),
|
||||
});
|
||||
const updateWiederkehrendMut = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<WiederkehrendFormData> }) => buchhaltungApi.updateWiederkehrend(id, data),
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-wiederkehrend'] }); setWiederkehrendDialog({ open: false }); showSuccess('Wiederkehrende Buchung aktualisiert'); },
|
||||
onError: () => showError('Aktualisierung fehlgeschlagen'),
|
||||
});
|
||||
const deleteWiederkehrendMut = useMutation({
|
||||
mutationFn: buchhaltungApi.deleteWiederkehrend,
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-wiederkehrend'] }); showSuccess('Wiederkehrende Buchung gelöscht'); },
|
||||
onError: () => showError('Löschen fehlgeschlagen'),
|
||||
});
|
||||
|
||||
// ── Erstattung mutation ──
|
||||
const createErstattungMut = useMutation({
|
||||
mutationFn: buchhaltungApi.createErstattung,
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-transaktionen'] }); qc.invalidateQueries({ queryKey: ['buchhaltung-stats'] }); setErstattungOpen(false); showSuccess('Erstattung erstellt'); },
|
||||
onError: () => showError('Erstattung konnte nicht erstellt werden'),
|
||||
});
|
||||
|
||||
const handleExportCsv = async () => {
|
||||
if (!filters.haushaltsjahr_id) { showError('Bitte ein Haushaltsjahr auswählen'); return; }
|
||||
try {
|
||||
@@ -798,6 +1006,15 @@ function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||
<Tabs value={txSubTab} onChange={(_, v) => setTxSubTab(v)}>
|
||||
<Tab label="Transaktionen" />
|
||||
<Tab label="Wiederkehrende Buchungen" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{txSubTab === 0 && (
|
||||
<Box>
|
||||
{/* Filters */}
|
||||
<Paper variant="outlined" sx={{ p: 1.5, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
@@ -848,6 +1065,11 @@ function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{hasPermission('buchhaltung:create') && (
|
||||
<Button size="small" variant="outlined" onClick={() => setErstattungOpen(true)}>
|
||||
Erstattung erfassen
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
@@ -970,6 +1192,74 @@ function TransaktionenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
selectedJahrId={selectedJahrId}
|
||||
onSave={data => createMut.mutate(data)}
|
||||
/>
|
||||
|
||||
<ErstattungDialog
|
||||
open={erstattungOpen}
|
||||
onClose={() => setErstattungOpen(false)}
|
||||
konten={kontenFlat}
|
||||
bankkonten={bankkonten}
|
||||
transaktionen={transaktionen}
|
||||
onSave={data => createErstattungMut.mutate(data)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{txSubTab === 1 && (
|
||||
<Box>
|
||||
<Box sx={{ mb: 2, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
{canManage && <Button variant="contained" startIcon={<AddIcon />} onClick={() => setWiederkehrendDialog({ open: true })}>Anlegen</Button>}
|
||||
</Box>
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Bezeichnung</TableCell>
|
||||
<TableCell>Typ</TableCell>
|
||||
<TableCell align="right">Betrag</TableCell>
|
||||
<TableCell>Intervall</TableCell>
|
||||
<TableCell>Nächste Ausführung</TableCell>
|
||||
<TableCell>Aktiv</TableCell>
|
||||
{canManage && <TableCell>Aktionen</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{wiederkehrend.length === 0 && <TableRow><TableCell colSpan={7} align="center"><Typography color="text.secondary">Keine wiederkehrenden Buchungen</Typography></TableCell></TableRow>}
|
||||
{wiederkehrend.map((w: WiederkehrendBuchung) => (
|
||||
<TableRow key={w.id} hover>
|
||||
<TableCell>{w.bezeichnung}</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={TRANSAKTION_TYP_LABELS[w.typ]} size="small" color={w.typ === 'einnahme' ? 'success' : 'error'} />
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: w.typ === 'einnahme' ? 'success.main' : 'error.main', fontWeight: 600 }}>
|
||||
{w.typ === 'ausgabe' ? '-' : '+'}{fmtEur(w.betrag)}
|
||||
</TableCell>
|
||||
<TableCell>{INTERVALL_LABELS[w.intervall]}</TableCell>
|
||||
<TableCell>{fmtDate(w.naechste_ausfuehrung)}</TableCell>
|
||||
<TableCell>{w.aktiv ? <Chip label="Aktiv" size="small" color="success" /> : <Chip label="Inaktiv" size="small" color="default" />}</TableCell>
|
||||
{canManage && (
|
||||
<TableCell>
|
||||
<IconButton size="small" onClick={() => setWiederkehrendDialog({ open: true, existing: w })}><Edit fontSize="small" /></IconButton>
|
||||
<IconButton size="small" color="error" onClick={() => deleteWiederkehrendMut.mutate(w.id)}><Delete fontSize="small" /></IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<WiederkehrendDialog
|
||||
open={wiederkehrendDialog.open}
|
||||
onClose={() => setWiederkehrendDialog({ open: false })}
|
||||
konten={kontenFlat}
|
||||
bankkonten={bankkonten}
|
||||
existing={wiederkehrendDialog.existing}
|
||||
onSave={data => wiederkehrendDialog.existing
|
||||
? updateWiederkehrendMut.mutate({ id: wiederkehrendDialog.existing.id, data })
|
||||
: createWiederkehrendMut.mutate(data)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1113,7 +1403,6 @@ function KontenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
const [kontoSaveError, setKontoSaveError] = useState<string | null>(null);
|
||||
const [bankDialog, setBankDialog] = useState<{ open: boolean; existing?: Bankkonto }>({ open: false });
|
||||
const [jahrDialog, setJahrDialog] = useState<{ open: boolean; existing?: Haushaltsjahr }>({ open: false });
|
||||
const [wiederkehrendDialog, setWiederkehrendDialog] = useState<{ open: boolean; existing?: WiederkehrendBuchung }>({ open: false });
|
||||
|
||||
const { data: kontenFlat = [] } = useQuery({
|
||||
queryKey: ['buchhaltung-konten', selectedJahrId],
|
||||
@@ -1128,7 +1417,6 @@ function KontenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
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 });
|
||||
const { data: kategorien = [] } = useQuery({
|
||||
queryKey: ['buchhaltung-kategorien', selectedJahrId],
|
||||
queryFn: () => buchhaltungApi.getKategorien(selectedJahrId!),
|
||||
@@ -1199,22 +1487,6 @@ function KontenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
onError: (e: Error) => showError(e.message || 'Abschluss fehlgeschlagen'),
|
||||
});
|
||||
|
||||
const createWiederkehrendMut = useMutation({
|
||||
mutationFn: buchhaltungApi.createWiederkehrend,
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-wiederkehrend'] }); setWiederkehrendDialog({ open: false }); showSuccess('Wiederkehrende Buchung erstellt'); },
|
||||
onError: () => showError('Erstellen fehlgeschlagen'),
|
||||
});
|
||||
const updateWiederkehrendMut = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<WiederkehrendFormData> }) => buchhaltungApi.updateWiederkehrend(id, data),
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-wiederkehrend'] }); setWiederkehrendDialog({ open: false }); showSuccess('Wiederkehrende Buchung aktualisiert'); },
|
||||
onError: () => showError('Aktualisierung fehlgeschlagen'),
|
||||
});
|
||||
const deleteWiederkehrendMut = useMutation({
|
||||
mutationFn: buchhaltungApi.deleteWiederkehrend,
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['buchhaltung-wiederkehrend'] }); showSuccess('Wiederkehrende Buchung gelöscht'); },
|
||||
onError: () => showError('Löschen fehlgeschlagen'),
|
||||
});
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||
@@ -1222,7 +1494,6 @@ function KontenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
<Tab label="Konten" />
|
||||
<Tab label="Bankkonten" />
|
||||
<Tab label="Haushaltsjahre" />
|
||||
<Tab label="Wiederkehrend" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
@@ -1433,64 +1704,6 @@ function KontenTab({ haushaltsjahre, selectedJahrId, onJahrChange }: {
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Sub-Tab 3: Wiederkehrend */}
|
||||
{subTab === 3 && (
|
||||
<Box>
|
||||
<Box sx={{ mb: 2, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
{canManage && <Button variant="contained" startIcon={<AddIcon />} onClick={() => setWiederkehrendDialog({ open: true })}>Anlegen</Button>}
|
||||
</Box>
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Bezeichnung</TableCell>
|
||||
<TableCell>Typ</TableCell>
|
||||
<TableCell align="right">Betrag</TableCell>
|
||||
<TableCell>Intervall</TableCell>
|
||||
<TableCell>Nächste Ausführung</TableCell>
|
||||
<TableCell>Aktiv</TableCell>
|
||||
{canManage && <TableCell>Aktionen</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{wiederkehrend.length === 0 && <TableRow><TableCell colSpan={7} align="center"><Typography color="text.secondary">Keine wiederkehrenden Buchungen</Typography></TableCell></TableRow>}
|
||||
{wiederkehrend.map((w: WiederkehrendBuchung) => (
|
||||
<TableRow key={w.id} hover>
|
||||
<TableCell>{w.bezeichnung}</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={TRANSAKTION_TYP_LABELS[w.typ]} size="small" color={w.typ === 'einnahme' ? 'success' : 'error'} />
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: w.typ === 'einnahme' ? 'success.main' : 'error.main', fontWeight: 600 }}>
|
||||
{w.typ === 'ausgabe' ? '-' : '+'}{fmtEur(w.betrag)}
|
||||
</TableCell>
|
||||
<TableCell>{INTERVALL_LABELS[w.intervall]}</TableCell>
|
||||
<TableCell>{fmtDate(w.naechste_ausfuehrung)}</TableCell>
|
||||
<TableCell>{w.aktiv ? <Chip label="Aktiv" size="small" color="success" /> : <Chip label="Inaktiv" size="small" color="default" />}</TableCell>
|
||||
{canManage && (
|
||||
<TableCell>
|
||||
<IconButton size="small" onClick={() => setWiederkehrendDialog({ open: true, existing: w })}><Edit fontSize="small" /></IconButton>
|
||||
<IconButton size="small" color="error" onClick={() => deleteWiederkehrendMut.mutate(w.id)}><Delete fontSize="small" /></IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<WiederkehrendDialog
|
||||
open={wiederkehrendDialog.open}
|
||||
onClose={() => setWiederkehrendDialog({ open: false })}
|
||||
konten={konten}
|
||||
bankkonten={bankkonten}
|
||||
existing={wiederkehrendDialog.existing}
|
||||
onSave={data => wiederkehrendDialog.existing
|
||||
? updateWiederkehrendMut.mutate({ id: wiederkehrendDialog.existing.id, data })
|
||||
: createWiederkehrendMut.mutate(data)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,9 +52,13 @@ export default function BuchhaltungKontoDetail() {
|
||||
if (isError || !data) return <DashboardLayout><Alert severity="error">Konto nicht gefunden.</Alert></DashboardLayout>;
|
||||
|
||||
const { konto, children, transaktionen } = data;
|
||||
const isEinfach = (konto.budget_typ || 'detailliert') === 'einfach';
|
||||
const totalEinnahmen = transaktionen
|
||||
.filter(t => t.typ === 'einnahme' && t.status === 'gebucht')
|
||||
.reduce((sum, t) => sum + Number(t.betrag), 0);
|
||||
const totalAusgaben = transaktionen
|
||||
.filter(t => t.typ === 'ausgabe' && t.status === 'gebucht')
|
||||
.reduce((sum, t) => sum + Number(t.betrag), 0);
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
@@ -69,29 +73,47 @@ export default function BuchhaltungKontoDetail() {
|
||||
</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">{fmtEur(totalEinnahmen)}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
{isEinfach ? (
|
||||
<>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<BudgetCard label="Budget Gesamt" budget={konto.budget_gesamt || 0} spent={totalAusgaben} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="subtitle2" color="text.secondary">Einnahmen</Typography>
|
||||
<Typography variant="h6" color="success.main">{fmtEur(totalEinnahmen)}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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">{fmtEur(totalEinnahmen)}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{children.length > 0 && (
|
||||
|
||||
@@ -5,12 +5,13 @@ import {
|
||||
Box, Button, TextField, Typography, Paper, Stack, MenuItem, Select,
|
||||
FormControl, InputLabel, Alert, Dialog, DialogTitle,
|
||||
DialogContent, DialogActions, Skeleton, Divider, LinearProgress, Grid,
|
||||
ToggleButton, ToggleButtonGroup,
|
||||
} from '@mui/material';
|
||||
import { ArrowBack, Delete, Edit, Save, Cancel } 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';
|
||||
import type { KontoFormData, BudgetTyp } from '../types/buchhaltung.types';
|
||||
|
||||
const fmtEur = (n: number) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(n);
|
||||
|
||||
@@ -86,6 +87,8 @@ export default function BuchhaltungKontoManage() {
|
||||
budget_gwg: k.budget_gwg,
|
||||
budget_anlagen: k.budget_anlagen,
|
||||
budget_instandhaltung: k.budget_instandhaltung,
|
||||
budget_typ: k.budget_typ || 'detailliert',
|
||||
budget_gesamt: k.budget_gesamt || 0,
|
||||
parent_id: k.parent_id ?? undefined,
|
||||
kategorie_id: k.kategorie_id ?? undefined,
|
||||
notizen: k.notizen ?? '',
|
||||
@@ -157,6 +160,8 @@ export default function BuchhaltungKontoManage() {
|
||||
budget_gwg: k.budget_gwg,
|
||||
budget_anlagen: k.budget_anlagen,
|
||||
budget_instandhaltung: k.budget_instandhaltung,
|
||||
budget_typ: k.budget_typ || 'detailliert',
|
||||
budget_gesamt: k.budget_gesamt || 0,
|
||||
parent_id: k.parent_id ?? undefined,
|
||||
kategorie_id: k.kategorie_id ?? undefined,
|
||||
notizen: k.notizen ?? '',
|
||||
@@ -231,22 +236,53 @@ export default function BuchhaltungKontoManage() {
|
||||
<Divider sx={{ mb: 1.5 }} />
|
||||
{isEditing ? (
|
||||
<Stack spacing={2}>
|
||||
<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 size="small"
|
||||
helperText={parentKonto && siblingBudgets ? `Eltern: ${fmtEur(parentKonto.budget_gwg)}, vergeben: ${fmtEur(siblingBudgets.gwg)}, verfügbar: ${fmtEur(parentKonto.budget_gwg - siblingBudgets.gwg)}` : undefined} />
|
||||
<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 size="small"
|
||||
helperText={parentKonto && siblingBudgets ? `Eltern: ${fmtEur(parentKonto.budget_anlagen)}, vergeben: ${fmtEur(siblingBudgets.anlagen)}, verfügbar: ${fmtEur(parentKonto.budget_anlagen - siblingBudgets.anlagen)}` : undefined} />
|
||||
<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 size="small"
|
||||
helperText={parentKonto && siblingBudgets ? `Eltern: ${fmtEur(parentKonto.budget_instandhaltung)}, vergeben: ${fmtEur(siblingBudgets.instandhaltung)}, verfügbar: ${fmtEur(parentKonto.budget_instandhaltung - siblingBudgets.instandhaltung)}` : undefined} />
|
||||
{!konto.parent_id && (
|
||||
<Box>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>Budget-Typ</Typography>
|
||||
<ToggleButtonGroup
|
||||
value={form.budget_typ || 'detailliert'}
|
||||
exclusive
|
||||
size="small"
|
||||
onChange={(_, val) => { if (val) setForm(f => ({ ...f, budget_typ: val as BudgetTyp })); }}
|
||||
>
|
||||
<ToggleButton value="detailliert">Detailliert</ToggleButton>
|
||||
<ToggleButton value="einfach">Einfach</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
)}
|
||||
{(form.budget_typ || 'detailliert') === 'einfach' ? (
|
||||
<TextField label="Budget Gesamt (€)" type="number" value={form.budget_gesamt ?? 0} onChange={e => setForm(f => ({ ...f, budget_gesamt: parseFloat(e.target.value) || 0 }))} inputProps={{ step: '0.01', min: '0' }} fullWidth size="small" />
|
||||
) : (
|
||||
<>
|
||||
<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 size="small"
|
||||
helperText={parentKonto && siblingBudgets ? `Eltern: ${fmtEur(parentKonto.budget_gwg)}, vergeben: ${fmtEur(siblingBudgets.gwg)}, verfügbar: ${fmtEur(parentKonto.budget_gwg - siblingBudgets.gwg)}` : undefined} />
|
||||
<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 size="small"
|
||||
helperText={parentKonto && siblingBudgets ? `Eltern: ${fmtEur(parentKonto.budget_anlagen)}, vergeben: ${fmtEur(siblingBudgets.anlagen)}, verfügbar: ${fmtEur(parentKonto.budget_anlagen - siblingBudgets.anlagen)}` : undefined} />
|
||||
<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 size="small"
|
||||
helperText={parentKonto && siblingBudgets ? `Eltern: ${fmtEur(parentKonto.budget_instandhaltung)}, vergeben: ${fmtEur(siblingBudgets.instandhaltung)}, verfügbar: ${fmtEur(parentKonto.budget_instandhaltung - siblingBudgets.instandhaltung)}` : undefined} />
|
||||
</>
|
||||
)}
|
||||
{saveError && <Alert severity="error" onClose={() => setSaveError(null)}>{saveError}</Alert>}
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
<BudgetBar label="GWG" budget={konto.budget_gwg} spent={spentGwg} />
|
||||
<BudgetBar label="Anlagen" budget={konto.budget_anlagen} spent={spentAnlagen} />
|
||||
<BudgetBar label="Instandhaltung" budget={konto.budget_instandhaltung} spent={spentInst} />
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<FieldRow label="Budget Gesamt" value={fmtEur(konto.budget_gwg + konto.budget_anlagen + konto.budget_instandhaltung)} />
|
||||
<FieldRow label="Ausgaben Gesamt" value={fmtEur(spentGwg + spentAnlagen + spentInst)} />
|
||||
{(konto.budget_typ || 'detailliert') === 'einfach' ? (
|
||||
<>
|
||||
<BudgetBar label="Gesamt" budget={konto.budget_gesamt || 0} spent={spentGwg + spentAnlagen + spentInst} />
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<FieldRow label="Budget Gesamt" value={fmtEur(konto.budget_gesamt || 0)} />
|
||||
<FieldRow label="Ausgaben Gesamt" value={fmtEur(spentGwg + spentAnlagen + spentInst)} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BudgetBar label="GWG" budget={konto.budget_gwg} spent={spentGwg} />
|
||||
<BudgetBar label="Anlagen" budget={konto.budget_anlagen} spent={spentAnlagen} />
|
||||
<BudgetBar label="Instandhaltung" budget={konto.budget_instandhaltung} spent={spentInst} />
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<FieldRow label="Budget Gesamt" value={fmtEur(konto.budget_gwg + konto.budget_anlagen + konto.budget_instandhaltung)} />
|
||||
<FieldRow label="Ausgaben Gesamt" value={fmtEur(spentGwg + spentAnlagen + spentInst)} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
Reference in New Issue
Block a user