feat(buchhaltung): budget types, erstattungen, recurring tab move, overview dividers, order completion guard
This commit is contained in:
@@ -486,6 +486,33 @@ class BuchhaltungController {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Erstattungen ────────────────────────────────────────────────────────────
|
||||
|
||||
async createErstattung(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const data = await buchhaltungService.createErstattung({
|
||||
...req.body,
|
||||
erstellt_von: req.user!.id,
|
||||
});
|
||||
res.status(201).json({ success: true, data });
|
||||
} catch (error) {
|
||||
logger.error('BuchhaltungController.createErstattung', { error });
|
||||
res.status(500).json({ success: false, message: 'Erstattung konnte nicht erstellt werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async getErstattungLinks(req: Request, res: Response): Promise<void> {
|
||||
const id = parseInt(param(req, 'id'), 10);
|
||||
if (isNaN(id)) { res.status(400).json({ success: false, message: 'Ungültige ID' }); return; }
|
||||
try {
|
||||
const data = await buchhaltungService.getErstattungLinks(id);
|
||||
res.json({ success: true, data });
|
||||
} catch (error) {
|
||||
logger.error('BuchhaltungController.getErstattungLinks', { error });
|
||||
res.status(500).json({ success: false, message: 'Erstattungsverknüpfungen konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Freigaben ────────────────────────────────────────────────────────────────
|
||||
|
||||
async requestFreigabe(req: Request, res: Response): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Add budget type support to buchhaltung_konten
|
||||
ALTER TABLE buchhaltung_konten
|
||||
ADD COLUMN IF NOT EXISTS budget_typ TEXT NOT NULL DEFAULT 'detailliert';
|
||||
|
||||
ALTER TABLE buchhaltung_konten
|
||||
ADD COLUMN IF NOT EXISTS budget_gesamt NUMERIC(12,2) NOT NULL DEFAULT 0;
|
||||
|
||||
-- Erstattung (reimbursement) linking table
|
||||
CREATE TABLE IF NOT EXISTS buchhaltung_erstattung_zuordnungen (
|
||||
erstattung_transaktion_id INT NOT NULL REFERENCES buchhaltung_transaktionen(id) ON DELETE CASCADE,
|
||||
ausgabe_transaktion_id INT NOT NULL REFERENCES buchhaltung_transaktionen(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (erstattung_transaktion_id, ausgabe_transaktion_id)
|
||||
);
|
||||
@@ -53,6 +53,10 @@ router.post('/wiederkehrend', authenticate, requirePermission('buchhaltung
|
||||
router.patch('/wiederkehrend/:id', authenticate, requirePermission('buchhaltung:manage_accounts'), buchhaltungController.updateWiederkehrend.bind(buchhaltungController));
|
||||
router.delete('/wiederkehrend/:id', authenticate, requirePermission('buchhaltung:manage_accounts'), buchhaltungController.deleteWiederkehrend.bind(buchhaltungController));
|
||||
|
||||
// ── Erstattungen ──────────────────────────────────────────────────────────────
|
||||
router.post('/erstattungen', authenticate, requirePermission('buchhaltung:create'), buchhaltungController.createErstattung.bind(buchhaltungController));
|
||||
router.get('/transaktionen/:id/erstattung-links', authenticate, requirePermission('buchhaltung:view'), buchhaltungController.getErstattungLinks.bind(buchhaltungController));
|
||||
|
||||
// ── CSV Export ─────────────────────────────────────────────────────────────────
|
||||
router.get('/export/csv', authenticate, requirePermission('buchhaltung:export'), buchhaltungController.exportCsv.bind(buchhaltungController));
|
||||
|
||||
|
||||
@@ -471,18 +471,28 @@ async function validateSubPotBudget(
|
||||
}
|
||||
|
||||
async function createKonto(
|
||||
data: { haushaltsjahr_id: number; konto_typ_id?: number; kontonummer: string; bezeichnung: string; parent_id?: number | null; budget_gwg?: number; budget_anlagen?: number; budget_instandhaltung?: number; notizen?: string },
|
||||
data: { haushaltsjahr_id: number; konto_typ_id?: number; kontonummer: string; bezeichnung: string; parent_id?: number | null; budget_gwg?: number; budget_anlagen?: number; budget_instandhaltung?: number; notizen?: string; kategorie_id?: number | null; budget_typ?: string; budget_gesamt?: number },
|
||||
userId: string
|
||||
) {
|
||||
try {
|
||||
if (data.parent_id && (data.budget_gwg || data.budget_anlagen || data.budget_instandhaltung)) {
|
||||
await validateSubPotBudget(data.parent_id, data.budget_gwg || 0, data.budget_anlagen || 0, data.budget_instandhaltung || 0);
|
||||
}
|
||||
|
||||
// Child konten inherit parent's budget_typ
|
||||
let budgetTyp = data.budget_typ || 'detailliert';
|
||||
if (data.parent_id) {
|
||||
const parentRow = await pool.query(`SELECT budget_typ FROM buchhaltung_konten WHERE id = $1`, [data.parent_id]);
|
||||
if (parentRow.rows[0]) {
|
||||
budgetTyp = parentRow.rows[0].budget_typ;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO buchhaltung_konten (haushaltsjahr_id, konto_typ_id, kontonummer, bezeichnung, parent_id, budget_gwg, budget_anlagen, budget_instandhaltung, notizen, erstellt_von)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
`INSERT INTO buchhaltung_konten (haushaltsjahr_id, konto_typ_id, kontonummer, bezeichnung, parent_id, budget_gwg, budget_anlagen, budget_instandhaltung, notizen, erstellt_von, kategorie_id, budget_typ, budget_gesamt)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
RETURNING *`,
|
||||
[data.haushaltsjahr_id, data.konto_typ_id || null, data.kontonummer, data.bezeichnung, data.parent_id || null, data.budget_gwg || 0, data.budget_anlagen || 0, data.budget_instandhaltung || 0, data.notizen || null, userId]
|
||||
[data.haushaltsjahr_id, data.konto_typ_id || null, data.kontonummer, data.bezeichnung, data.parent_id || null, data.budget_gwg || 0, data.budget_anlagen || 0, data.budget_instandhaltung || 0, data.notizen || null, userId, data.kategorie_id ?? null, budgetTyp, data.budget_gesamt || 0]
|
||||
);
|
||||
return result.rows[0];
|
||||
} catch (error: any) {
|
||||
@@ -497,7 +507,7 @@ async function createKonto(
|
||||
|
||||
async function updateKonto(
|
||||
id: number,
|
||||
data: { konto_typ_id?: number; kontonummer?: string; bezeichnung?: string; parent_id?: number | null; budget_gwg?: number; budget_anlagen?: number; budget_instandhaltung?: number; notizen?: string }
|
||||
data: { konto_typ_id?: number; kontonummer?: string; bezeichnung?: string; parent_id?: number | null; budget_gwg?: number; budget_anlagen?: number; budget_instandhaltung?: number; notizen?: string; kategorie_id?: number | null; budget_typ?: string; budget_gesamt?: number }
|
||||
) {
|
||||
try {
|
||||
// Budget validation for sub-pots
|
||||
@@ -527,7 +537,27 @@ async function updateKonto(
|
||||
if (data.budget_anlagen !== undefined) { fields.push(`budget_anlagen = $${idx++}`); values.push(data.budget_anlagen); }
|
||||
if (data.budget_instandhaltung !== undefined) { fields.push(`budget_instandhaltung = $${idx++}`); values.push(data.budget_instandhaltung); }
|
||||
if (data.notizen !== undefined) { fields.push(`notizen = $${idx++}`); values.push(data.notizen || null); }
|
||||
if ('kategorie_id' in data) { fields.push(`kategorie_id = $${idx++}`); values.push(data.kategorie_id ?? null); }
|
||||
if (data.budget_typ !== undefined) { fields.push(`budget_typ = $${idx++}`); values.push(data.budget_typ); }
|
||||
if (data.budget_gesamt !== undefined) { fields.push(`budget_gesamt = $${idx++}`); values.push(data.budget_gesamt); }
|
||||
if (fields.length === 0) throw new Error('Keine Felder zum Aktualisieren');
|
||||
|
||||
// Child konten must inherit parent's budget_typ
|
||||
if (data.budget_typ !== undefined) {
|
||||
const currentRow = await pool.query(`SELECT parent_id FROM buchhaltung_konten WHERE id = $1`, [id]);
|
||||
const parentId = data.parent_id !== undefined ? data.parent_id : currentRow.rows[0]?.parent_id;
|
||||
if (parentId) {
|
||||
const parentRow = await pool.query(`SELECT budget_typ FROM buchhaltung_konten WHERE id = $1`, [parentId]);
|
||||
if (parentRow.rows[0]) {
|
||||
// Override budget_typ with parent's value
|
||||
const btIdx = fields.findIndex(f => f.startsWith('budget_typ'));
|
||||
if (btIdx !== -1) {
|
||||
values[btIdx] = parentRow.rows[0].budget_typ;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
const result = await pool.query(
|
||||
`UPDATE buchhaltung_konten SET ${fields.join(', ')} WHERE id = $${idx} RETURNING *`,
|
||||
@@ -1231,6 +1261,94 @@ async function exportTransaktionenCsv(haushaltsjahrId: number): Promise<string>
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Erstattungen (Reimbursements)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface Transaktion {
|
||||
id: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
async function createErstattung(data: {
|
||||
konto_id: number;
|
||||
bankkonto_id: number;
|
||||
betrag: number;
|
||||
datum: string;
|
||||
beschreibung?: string;
|
||||
empfaenger_auftraggeber?: string;
|
||||
verwendungszweck?: string;
|
||||
ausgabe_ids: number[];
|
||||
erstellt_von?: number;
|
||||
}): Promise<Transaktion> {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Look up haushaltsjahr_id from the konto
|
||||
const kontoResult = await client.query(
|
||||
`SELECT haushaltsjahr_id FROM buchhaltung_konten WHERE id = $1`,
|
||||
[data.konto_id]
|
||||
);
|
||||
const haushaltsjahrId = kontoResult.rows[0]?.haushaltsjahr_id;
|
||||
|
||||
const txResult = await client.query(
|
||||
`INSERT INTO buchhaltung_transaktionen (typ, konto_id, bankkonto_id, betrag, datum, beschreibung, empfaenger_auftraggeber, verwendungszweck, erstellt_von, haushaltsjahr_id, status)
|
||||
VALUES ('einnahme', $1, $2, $3, $4, $5, $6, $7, $8, $9, 'entwurf')
|
||||
RETURNING *`,
|
||||
[data.konto_id, data.bankkonto_id, data.betrag, data.datum, data.beschreibung || null, data.empfaenger_auftraggeber || null, data.verwendungszweck || null, data.erstellt_von || null, haushaltsjahrId]
|
||||
);
|
||||
const tx = txResult.rows[0];
|
||||
|
||||
for (const ausgabeId of data.ausgabe_ids) {
|
||||
await client.query(
|
||||
`INSERT INTO buchhaltung_erstattung_zuordnungen (erstattung_transaktion_id, ausgabe_transaktion_id) VALUES ($1, $2)`,
|
||||
[tx.id, ausgabeId]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
return tx;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('BuchhaltungService.createErstattung failed', { error });
|
||||
throw new Error('Erstattung konnte nicht erstellt werden');
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function getErstattungLinks(transaktionId: number): Promise<{
|
||||
erstattung?: any;
|
||||
ausgaben?: any[];
|
||||
}> {
|
||||
try {
|
||||
// If this transaction is an Ausgabe, find its reimbursement
|
||||
const erstattungResult = await pool.query(
|
||||
`SELECT t.* FROM buchhaltung_transaktionen t
|
||||
JOIN buchhaltung_erstattung_zuordnungen ez ON t.id = ez.erstattung_transaktion_id
|
||||
WHERE ez.ausgabe_transaktion_id = $1`,
|
||||
[transaktionId]
|
||||
);
|
||||
|
||||
// If this transaction is an Erstattung, find its linked Ausgaben
|
||||
const ausgabenResult = await pool.query(
|
||||
`SELECT t.* FROM buchhaltung_transaktionen t
|
||||
JOIN buchhaltung_erstattung_zuordnungen ez ON t.id = ez.ausgabe_transaktion_id
|
||||
WHERE ez.erstattung_transaktion_id = $1`,
|
||||
[transaktionId]
|
||||
);
|
||||
|
||||
return {
|
||||
erstattung: erstattungResult.rows[0] || undefined,
|
||||
ausgaben: ausgabenResult.rows,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('BuchhaltungService.getErstattungLinks failed', { error, transaktionId });
|
||||
throw new Error('Erstattungsverknüpfungen konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Export
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1286,6 +1404,8 @@ const buchhaltungService = {
|
||||
updateWiederkehrend,
|
||||
deleteWiederkehrend,
|
||||
exportTransaktionenCsv,
|
||||
createErstattung,
|
||||
getErstattungLinks,
|
||||
};
|
||||
|
||||
export default buchhaltungService;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
WiederkehrendBuchung, WiederkehrendFormData,
|
||||
Freigabe,
|
||||
Kategorie,
|
||||
ErstattungFormData, ErstattungLinks,
|
||||
} from '../types/buchhaltung.types';
|
||||
|
||||
export const buchhaltungApi = {
|
||||
@@ -196,4 +197,14 @@ export const buchhaltungApi = {
|
||||
const r = await api.patch(`/api/buchhaltung/freigaben/${id}/ablehnen`, { kommentar });
|
||||
return r.data.data;
|
||||
},
|
||||
|
||||
// ── Erstattungen ─────────────────────────────────────────────────────────────
|
||||
createErstattung: async (data: ErstattungFormData): Promise<Transaktion> => {
|
||||
const r = await api.post('/api/buchhaltung/erstattungen', data);
|
||||
return r.data.data;
|
||||
},
|
||||
getErstattungLinks: async (transaktionId: number): Promise<ErstattungLinks> => {
|
||||
const r = await api.get(`/api/buchhaltung/transaktionen/${transaktionId}/erstattung-links`);
|
||||
return r.data.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export type FreigabeStatus = 'ausstehend' | 'genehmigt' | 'abgelehnt';
|
||||
export type WiederkehrendIntervall = 'monatlich' | 'quartalsweise' | 'halbjaehrlich' | 'jaehrlich';
|
||||
export type PlanungStatus = 'entwurf' | 'aktiv' | 'abgeschlossen';
|
||||
export type AusgabenTyp = 'gwg' | 'anlagen' | 'instandhaltung';
|
||||
export type BudgetTyp = 'detailliert' | 'einfach';
|
||||
|
||||
export const AUSGABEN_TYP_LABELS: Record<AusgabenTyp, string> = {
|
||||
gwg: 'GWG',
|
||||
@@ -98,6 +99,8 @@ export interface Konto {
|
||||
budget_gwg: number;
|
||||
budget_anlagen: number;
|
||||
budget_instandhaltung: number;
|
||||
budget_typ: BudgetTyp;
|
||||
budget_gesamt: number;
|
||||
notizen: string | null;
|
||||
aktiv: boolean;
|
||||
erstellt_von: string | null;
|
||||
@@ -235,6 +238,8 @@ export interface KontoFormData {
|
||||
budget_gwg: number;
|
||||
budget_anlagen: number;
|
||||
budget_instandhaltung: number;
|
||||
budget_typ?: BudgetTyp;
|
||||
budget_gesamt?: number;
|
||||
parent_id?: number | null;
|
||||
kategorie_id?: number | null;
|
||||
notizen?: string;
|
||||
@@ -286,3 +291,18 @@ export interface KontoDetailResponse {
|
||||
children: KontoTreeNode[];
|
||||
transaktionen: Transaktion[];
|
||||
}
|
||||
|
||||
export interface ErstattungFormData {
|
||||
konto_id: number;
|
||||
bankkonto_id: number | null;
|
||||
betrag: number;
|
||||
datum: string;
|
||||
beschreibung?: string;
|
||||
empfaenger_auftraggeber?: string;
|
||||
quell_transaktion_ids: number[];
|
||||
}
|
||||
|
||||
export interface ErstattungLinks {
|
||||
erstattung_transaktion_id: number | null;
|
||||
quell_transaktion_ids: number[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user