Files
dashboard/frontend/src/pages/BuchhaltungTransaktionForm.tsx

370 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef } from 'react';
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { useQuery, useMutation } from '@tanstack/react-query';
import {
Alert, Box, Button, Card, CardContent, CardHeader, Divider,
FormControl, Grid, InputLabel, MenuItem, Select, Skeleton,
Stack, TextField, Typography,
} from '@mui/material';
import { ArrowBack, Save } from '@mui/icons-material';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { buchhaltungApi } from '../services/buchhaltung';
import { bestellungApi } from '../services/bestellung';
import { useNotification } from '../contexts/NotificationContext';
import type { AusgabenTyp, TransaktionFormData } from '../types/buchhaltung.types';
export default function BuchhaltungTransaktionForm() {
const { id } = useParams<{ id: string }>();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { showSuccess, showError } = useNotification();
const isEdit = !!id;
const today = new Date().toISOString().slice(0, 10);
const [form, setForm] = useState<TransaktionFormData>({
haushaltsjahr_id: 0,
typ: 'ausgabe',
betrag: 0,
datum: today,
konto_id: null,
bankkonto_id: null,
beschreibung: '',
empfaenger_auftraggeber: '',
verwendungszweck: '',
beleg_nr: '',
bestellung_id: null,
ausgaben_typ: null,
});
const { data: haushaltsjahre = [], isLoading: loadingJahre } = useQuery({
queryKey: ['haushaltsjahre'],
queryFn: buchhaltungApi.getHaushaltsjahre,
});
const { data: existing, isLoading: loadingExisting } = useQuery({
queryKey: ['transaktion', Number(id)],
queryFn: () => buchhaltungApi.getTransaktion(Number(id)),
enabled: isEdit,
});
const { data: konten = [] } = useQuery({
queryKey: ['buchhaltung-konten', form.haushaltsjahr_id],
queryFn: () => buchhaltungApi.getKonten(form.haushaltsjahr_id),
enabled: form.haushaltsjahr_id > 0,
});
const { data: bankkonten = [] } = useQuery({
queryKey: ['bankkonten'],
queryFn: buchhaltungApi.getBankkonten,
});
const { data: bestellungen = [] } = useQuery({
queryKey: ['bestellungen-all'],
queryFn: () => bestellungApi.getOrders(),
staleTime: 5 * 60 * 1000,
});
// Default to active fiscal year on create
useEffect(() => {
if (!isEdit && haushaltsjahre.length > 0 && form.haushaltsjahr_id === 0) {
const jahrParam = searchParams.get('jahr');
const openYear = haushaltsjahre.find(hj => !hj.abgeschlossen) || haushaltsjahre[0];
setForm(f => ({ ...f, haushaltsjahr_id: jahrParam ? Number(jahrParam) : openYear.id }));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [haushaltsjahre]);
const hasPrefilled = useRef(false);
// Pre-fill from existing transaction when editing — only once
useEffect(() => {
if (existing && !hasPrefilled.current) {
hasPrefilled.current = true;
setForm({
haushaltsjahr_id: existing.haushaltsjahr_id,
typ: existing.typ as 'einnahme' | 'ausgabe',
betrag: Number(existing.betrag),
datum: existing.datum.slice(0, 10),
konto_id: existing.konto_id,
bankkonto_id: existing.bankkonto_id,
beschreibung: existing.beschreibung || '',
empfaenger_auftraggeber: existing.empfaenger_auftraggeber || '',
verwendungszweck: existing.verwendungszweck || '',
beleg_nr: existing.beleg_nr || '',
bestellung_id: existing.bestellung_id,
ausgaben_typ: existing.ausgaben_typ,
});
}
}, [existing]);
const createMut = useMutation({
mutationFn: buchhaltungApi.createTransaktion,
onSuccess: () => { showSuccess('Transaktion erstellt'); navigate('/buchhaltung'); },
onError: () => showError('Transaktion konnte nicht erstellt werden'),
});
const updateMut = useMutation({
mutationFn: (data: Partial<TransaktionFormData>) =>
buchhaltungApi.updateTransaktion(Number(id), data),
onSuccess: () => { showSuccess('Transaktion aktualisiert'); navigate('/buchhaltung'); },
onError: () => showError('Aktualisierung fehlgeschlagen'),
});
const selectedKonto = konten.find(k => k.id === form.konto_id);
const isEinfach = selectedKonto && (selectedKonto.budget_typ || 'detailliert') === 'einfach';
const isPending = createMut.isPending || updateMut.isPending;
const canSubmit =
!!form.haushaltsjahr_id &&
!!form.betrag &&
!!form.datum &&
!!form.konto_id &&
!!form.bankkonto_id &&
!!form.beschreibung;
const handleSubmit = () => {
if (isEdit) updateMut.mutate(form);
else createMut.mutate(form);
};
if (loadingJahre || (isEdit && loadingExisting)) {
return (
<DashboardLayout>
<Box sx={{ p: 3, maxWidth: 800, mx: 'auto' }}>
<Skeleton variant="rectangular" height={60} sx={{ mb: 2 }} />
<Skeleton variant="rectangular" height={200} sx={{ mb: 2 }} />
<Skeleton variant="rectangular" height={160} />
</Box>
</DashboardLayout>
);
}
return (
<DashboardLayout>
<Box sx={{ p: 3, maxWidth: 800, mx: 'auto' }}>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<Button startIcon={<ArrowBack />} onClick={() => navigate('/buchhaltung')}>
Zurück
</Button>
<Typography variant="h5" sx={{ ml: 1 }}>
{isEdit ? 'Transaktion bearbeiten' : 'Neue Transaktion'}
</Typography>
</Box>
<Stack spacing={3}>
{/* Section: General */}
<Card variant="outlined">
<CardHeader
title="Allgemein"
titleTypographyProps={{ variant: 'subtitle1', fontWeight: 600 }}
/>
<Divider />
<CardContent>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<FormControl fullWidth required>
<InputLabel>Haushaltsjahr</InputLabel>
<Select
value={form.haushaltsjahr_id || ''}
label="Haushaltsjahr"
onChange={e => setForm(f => ({ ...f, haushaltsjahr_id: Number(e.target.value), konto_id: null }))}
>
{haushaltsjahre.map(hj => (
<MenuItem key={hj.id} value={hj.id}>{hj.bezeichnung}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} sm={6}>
<FormControl fullWidth required>
<InputLabel>Typ</InputLabel>
<Select
value={form.typ}
label="Typ"
onChange={e => setForm(f => ({ ...f, typ: e.target.value as 'einnahme' | 'ausgabe', ausgaben_typ: null }))}
>
<MenuItem value="ausgabe">Ausgabe</MenuItem>
<MenuItem value="einnahme">Einnahme</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
fullWidth required
label="Betrag (€)"
type="number"
value={form.betrag || ''}
onChange={e => setForm(f => ({ ...f, betrag: parseFloat(e.target.value) || 0 }))}
inputProps={{ step: '0.01', min: '0.01' }}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
fullWidth required
label="Datum"
type="date"
value={form.datum}
onChange={e => setForm(f => ({ ...f, datum: e.target.value }))}
InputLabelProps={{ shrink: true }}
/>
</Grid>
{form.typ === 'ausgabe' && !isEinfach && (
<Grid item xs={12} sm={6}>
<FormControl fullWidth>
<InputLabel>Ausgaben-Typ</InputLabel>
<Select
value={form.ausgaben_typ || ''}
label="Ausgaben-Typ"
onChange={e => setForm(f => ({ ...f, ausgaben_typ: (e.target.value as AusgabenTyp) || null }))}
>
<MenuItem value=""><em>Kein Typ</em></MenuItem>
<MenuItem value="gwg">GWG</MenuItem>
<MenuItem value="anlagen">Anlagen</MenuItem>
<MenuItem value="instandhaltung">Instandhaltung</MenuItem>
</Select>
</FormControl>
</Grid>
)}
</Grid>
</CardContent>
</Card>
{/* Section: Accounts */}
<Card variant="outlined">
<CardHeader
title="Konten"
titleTypographyProps={{ variant: 'subtitle1', fontWeight: 600 }}
/>
<Divider />
<CardContent>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<FormControl fullWidth required>
<InputLabel>Konto (Topf)</InputLabel>
<Select
value={form.konto_id ?? ''}
label="Konto (Topf)"
onChange={e => setForm(f => ({ ...f, konto_id: e.target.value ? Number(e.target.value) : null, ausgaben_typ: null }))}
>
<MenuItem value=""><em>Kein Konto</em></MenuItem>
{konten.map(k => (
<MenuItem key={k.id} value={k.id}>{k.kontonummer} {k.bezeichnung}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} sm={6}>
<FormControl fullWidth required>
<InputLabel>Bankkonto</InputLabel>
<Select
value={form.bankkonto_id ?? ''}
label="Bankkonto"
onChange={e => setForm(f => ({ ...f, bankkonto_id: 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}{bk.iban ? ` (${bk.iban})` : ''}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
</Grid>
</CardContent>
</Card>
{/* Section: Details */}
<Card variant="outlined">
<CardHeader
title="Details"
titleTypographyProps={{ variant: 'subtitle1', fontWeight: 600 }}
/>
<Divider />
<CardContent>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<TextField
fullWidth required
label="Beschreibung"
value={form.beschreibung}
onChange={e => setForm(f => ({ ...f, beschreibung: e.target.value }))}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
fullWidth
label="Empfänger / Auftraggeber"
value={form.empfaenger_auftraggeber}
onChange={e => setForm(f => ({ ...f, empfaenger_auftraggeber: e.target.value }))}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
fullWidth
label="Verwendungszweck"
value={form.verwendungszweck}
onChange={e => setForm(f => ({ ...f, verwendungszweck: e.target.value }))}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
fullWidth
label="Belegnummer"
value={form.beleg_nr}
onChange={e => setForm(f => ({ ...f, beleg_nr: e.target.value }))}
/>
</Grid>
</Grid>
</CardContent>
</Card>
{/* Section: Order link (optional) */}
<Card variant="outlined">
<CardHeader
title="Bestellung verknüpfen"
subheader="Optional"
titleTypographyProps={{ variant: 'subtitle1', fontWeight: 600 }}
/>
<Divider />
<CardContent>
<FormControl fullWidth>
<InputLabel>Bestellung</InputLabel>
<Select
value={form.bestellung_id ?? ''}
label="Bestellung"
onChange={e => setForm(f => ({ ...f, bestellung_id: e.target.value ? Number(e.target.value) : null }))}
>
<MenuItem value=""><em>Keine</em></MenuItem>
{bestellungen.map(b => (
<MenuItem key={b.id} value={b.id}>
{b.laufende_nummer ? `#${b.laufende_nummer} ` : ''}{b.bezeichnung}
</MenuItem>
))}
</Select>
</FormControl>
</CardContent>
</Card>
{!canSubmit && (
<Alert severity="info">
Pflichtfelder: Haushaltsjahr, Typ, Betrag, Datum, Konto (Topf), Bankkonto und Beschreibung müssen ausgefüllt sein.
</Alert>
)}
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
<Button onClick={() => navigate('/buchhaltung')}>Abbrechen</Button>
<Button
variant="contained"
startIcon={<Save />}
disabled={isPending || !canSubmit}
onClick={handleSubmit}
>
{isEdit ? 'Speichern' : 'Erstellen'}
</Button>
</Box>
</Stack>
</Box>
</DashboardLayout>
);
}