- Added a new API route to fetch JTL Kontierung data based on transaction ID. - Implemented loading of JTL Kontierung data in the AccountingItemsManager component. - Updated UI to display JTL Kontierung data for debugging purposes. - Enhanced user feedback during processing tasks in the App component with tooltips and progress indicators.
550 lines
16 KiB
JavaScript
550 lines
16 KiB
JavaScript
import React, { Component } from 'react';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Button,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Paper,
|
|
TextField,
|
|
Select,
|
|
MenuItem,
|
|
FormControl,
|
|
InputLabel,
|
|
IconButton,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
Alert,
|
|
Chip
|
|
} from '@mui/material';
|
|
import {
|
|
Add as AddIcon,
|
|
Delete as DeleteIcon,
|
|
Edit as EditIcon,
|
|
Save as SaveIcon,
|
|
Cancel as CancelIcon
|
|
} from '@mui/icons-material';
|
|
import AuthService from '../services/AuthService';
|
|
|
|
class AccountingItemsManager extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
accountingItems: [],
|
|
kontos: [],
|
|
bus: [],
|
|
loading: true,
|
|
editingItem: null,
|
|
showCreateDialog: false,
|
|
showCreateKontoDialog: false,
|
|
jtlKontierung: null,
|
|
newItem: {
|
|
umsatz_brutto: '',
|
|
soll_haben_kz: 'S',
|
|
konto: '',
|
|
bu: '',
|
|
rechnungsnummer: '',
|
|
buchungstext: ''
|
|
},
|
|
newKonto: {
|
|
konto: '',
|
|
name: ''
|
|
},
|
|
error: null,
|
|
saving: false
|
|
};
|
|
|
|
this.authService = new AuthService();
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.loadData();
|
|
this.loadJtlKontierung();
|
|
}
|
|
|
|
loadData = async () => {
|
|
try {
|
|
// Load accounting items for this transaction
|
|
await this.loadAccountingItems();
|
|
|
|
// Load Konto and BU options
|
|
await Promise.all([
|
|
this.loadKontos(),
|
|
this.loadBUs()
|
|
]);
|
|
|
|
this.setState({ loading: false });
|
|
} catch (error) {
|
|
console.error('Error loading data:', error);
|
|
this.setState({
|
|
error: 'Fehler beim Laden der Daten',
|
|
loading: false
|
|
});
|
|
}
|
|
};
|
|
|
|
loadJtlKontierung = async () => {
|
|
try {
|
|
const { transaction } = this.props;
|
|
if (!transaction || !transaction.jtlId) {
|
|
this.setState({ jtlKontierung: undefined });
|
|
return;
|
|
}
|
|
|
|
const response = await this.authService.apiCall(`/data/jtl-kontierung/${transaction.jtlId}`);
|
|
if (!response) return;
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
this.setState({ jtlKontierung: data });
|
|
} else {
|
|
const err = await response.json();
|
|
console.error('Failed to load JTL Kontierung:', err);
|
|
this.setState({ jtlKontierung: undefined });
|
|
}
|
|
} catch (e) {
|
|
console.error('Error loading JTL Kontierung:', e);
|
|
this.setState({ jtlKontierung: undefined });
|
|
}
|
|
}
|
|
|
|
loadAccountingItems = async () => {
|
|
const { transaction } = this.props;
|
|
if (!transaction?.id) return;
|
|
|
|
try {
|
|
const response = await this.authService.apiCall(`/data/accounting-items/${transaction.id}`);
|
|
if (response && response.ok) {
|
|
const items = await response.json();
|
|
this.setState({ accountingItems: items });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading accounting items:', error);
|
|
}
|
|
};
|
|
|
|
loadKontos = async () => {
|
|
try {
|
|
const response = await this.authService.apiCall('/data/kontos');
|
|
if (response && response.ok) {
|
|
const kontos = await response.json();
|
|
this.setState({ kontos });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading kontos:', error);
|
|
}
|
|
};
|
|
|
|
loadBUs = async () => {
|
|
try {
|
|
const response = await this.authService.apiCall('/data/bus');
|
|
if (response && response.ok) {
|
|
const bus = await response.json();
|
|
this.setState({ bus });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading BUs:', error);
|
|
}
|
|
};
|
|
|
|
handleCreateItem = () => {
|
|
const { transaction } = this.props;
|
|
this.setState({
|
|
showCreateDialog: true,
|
|
newItem: {
|
|
umsatz_brutto: Math.abs(transaction.numericAmount || 0).toString(),
|
|
soll_haben_kz: (transaction.numericAmount || 0) >= 0 ? 'H' : 'S',
|
|
konto: '',
|
|
bu: '',
|
|
rechnungsnummer: '',
|
|
buchungstext: transaction.description || ''
|
|
}
|
|
});
|
|
};
|
|
|
|
handleSaveItem = async () => {
|
|
const { transaction } = this.props;
|
|
const { newItem } = this.state;
|
|
|
|
if (!newItem.umsatz_brutto || !newItem.konto) {
|
|
this.setState({ error: 'Betrag und Konto sind erforderlich' });
|
|
return;
|
|
}
|
|
|
|
this.setState({ saving: true, error: null });
|
|
|
|
try {
|
|
const itemData = {
|
|
...newItem,
|
|
transaction_id: transaction.isFromCSV ? null : transaction.id,
|
|
csv_transaction_id: transaction.isFromCSV ? transaction.id : null,
|
|
buchungsdatum: transaction.parsed_date || new Date().toISOString().split('T')[0]
|
|
};
|
|
|
|
const response = await this.authService.apiCall('/data/accounting-items', {
|
|
method: 'POST',
|
|
body: JSON.stringify(itemData)
|
|
});
|
|
|
|
if (response && response.ok) {
|
|
await this.loadAccountingItems();
|
|
this.setState({
|
|
showCreateDialog: false,
|
|
saving: false,
|
|
newItem: {
|
|
umsatz_brutto: '',
|
|
soll_haben_kz: 'S',
|
|
konto: '',
|
|
bu: '',
|
|
rechnungsnummer: '',
|
|
buchungstext: ''
|
|
}
|
|
});
|
|
} else {
|
|
const errorData = await response.json();
|
|
this.setState({
|
|
error: errorData.error || 'Fehler beim Speichern',
|
|
saving: false
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving accounting item:', error);
|
|
this.setState({
|
|
error: 'Fehler beim Speichern',
|
|
saving: false
|
|
});
|
|
}
|
|
};
|
|
|
|
handleCreateKonto = async () => {
|
|
const { newKonto } = this.state;
|
|
|
|
if (!newKonto.konto || !newKonto.name) {
|
|
this.setState({ error: 'Konto-Nummer und Name sind erforderlich' });
|
|
return;
|
|
}
|
|
|
|
this.setState({ saving: true, error: null });
|
|
|
|
try {
|
|
const response = await this.authService.apiCall('/data/kontos', {
|
|
method: 'POST',
|
|
body: JSON.stringify(newKonto)
|
|
});
|
|
|
|
if (response && response.ok) {
|
|
await this.loadKontos();
|
|
this.setState({
|
|
showCreateKontoDialog: false,
|
|
saving: false,
|
|
newKonto: { konto: '', name: '' }
|
|
});
|
|
} else {
|
|
const errorData = await response.json();
|
|
this.setState({
|
|
error: errorData.error || 'Fehler beim Erstellen des Kontos',
|
|
saving: false
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating konto:', error);
|
|
this.setState({
|
|
error: 'Fehler beim Erstellen des Kontos',
|
|
saving: false
|
|
});
|
|
}
|
|
};
|
|
|
|
handleDeleteItem = async (itemId) => {
|
|
if (!window.confirm('Buchungsposten wirklich löschen?')) return;
|
|
|
|
try {
|
|
const response = await this.authService.apiCall(`/data/accounting-items/${itemId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (response && response.ok) {
|
|
await this.loadAccountingItems();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting accounting item:', error);
|
|
this.setState({ error: 'Fehler beim Löschen' });
|
|
}
|
|
};
|
|
|
|
calculateTotal = () => {
|
|
return this.state.accountingItems.reduce((sum, item) => {
|
|
const amount = parseFloat(item.umsatz_brutto) || 0;
|
|
return sum + (item.soll_haben_kz === 'S' ? amount : -amount);
|
|
}, 0);
|
|
};
|
|
|
|
render() {
|
|
const { transaction } = this.props;
|
|
const {
|
|
accountingItems,
|
|
kontos,
|
|
bus,
|
|
loading,
|
|
showCreateDialog,
|
|
showCreateKontoDialog,
|
|
newItem,
|
|
newKonto,
|
|
error,
|
|
saving
|
|
} = this.state;
|
|
|
|
if (loading) {
|
|
return <Typography>Lade Buchungsdaten...</Typography>;
|
|
}
|
|
|
|
const transactionAmount = transaction.numericAmount || 0;
|
|
const currentTotal = this.calculateTotal();
|
|
const isBalanced = Math.abs(currentTotal - Math.abs(transactionAmount)) < 0.01;
|
|
|
|
return (
|
|
<Box>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
|
<Typography variant="h6">
|
|
Buchungsposten
|
|
</Typography>
|
|
<Button
|
|
variant="contained"
|
|
startIcon={<AddIcon />}
|
|
onClick={this.handleCreateItem}
|
|
size="small"
|
|
>
|
|
Hinzufügen
|
|
</Button>
|
|
</Box>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
<Box sx={{ mb: 2, p: 2, bgcolor: isBalanced ? '#e8f5e8' : '#fff3e0', borderRadius: 1 }}>
|
|
<Typography variant="body2">
|
|
<strong>Transaktionsbetrag:</strong> {Math.abs(transactionAmount).toFixed(2)} €
|
|
</Typography>
|
|
<Typography variant="body2">
|
|
<strong>Summe Buchungsposten:</strong> {Math.abs(currentTotal).toFixed(2)} €
|
|
</Typography>
|
|
<Chip
|
|
label={isBalanced ? "✅ Ausgeglichen" : "⚠️ Nicht ausgeglichen"}
|
|
color={isBalanced ? "success" : "warning"}
|
|
size="small"
|
|
sx={{ mt: 1 }}
|
|
/>
|
|
</Box>
|
|
|
|
{transaction?.jtlId && (
|
|
<Box sx={{ mb: 2, p: 2, border: '1px dashed #999', borderRadius: 1 }}>
|
|
<Typography variant="subtitle2">Debug: tUmsatzKontierung.data</Typography>
|
|
<Typography variant="caption" component="div" sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
|
{this.state.jtlKontierung === undefined
|
|
? 'undefined'
|
|
: this.state.jtlKontierung === null
|
|
? 'null'
|
|
: typeof this.state.jtlKontierung === 'object'
|
|
? JSON.stringify(this.state.jtlKontierung, null, 2)
|
|
: String(this.state.jtlKontierung)}
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
|
|
<TableContainer component={Paper}>
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>Betrag</TableCell>
|
|
<TableCell>S/H</TableCell>
|
|
<TableCell>Konto</TableCell>
|
|
<TableCell>BU</TableCell>
|
|
<TableCell>Buchungstext</TableCell>
|
|
<TableCell>Aktionen</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{accountingItems.map((item) => (
|
|
<TableRow key={item.id}>
|
|
<TableCell>{parseFloat(item.umsatz_brutto).toFixed(2)} €</TableCell>
|
|
<TableCell>
|
|
<Chip
|
|
label={item.soll_haben_kz}
|
|
color={item.soll_haben_kz === 'S' ? 'primary' : 'secondary'}
|
|
size="small"
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
{item.konto} - {item.konto_name}
|
|
</TableCell>
|
|
<TableCell>
|
|
{item.bu ? `${item.bu} - ${item.bu_name}` : '-'}
|
|
</TableCell>
|
|
<TableCell>{item.buchungstext || '-'}</TableCell>
|
|
<TableCell>
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => this.handleDeleteItem(item.id)}
|
|
color="error"
|
|
>
|
|
<DeleteIcon />
|
|
</IconButton>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{accountingItems.length === 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={6} align="center">
|
|
<Typography color="textSecondary">
|
|
Keine Buchungsposten vorhanden
|
|
</Typography>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
|
|
{/* Create Item Dialog */}
|
|
<Dialog open={showCreateDialog} onClose={() => this.setState({ showCreateDialog: false })} maxWidth="sm" fullWidth>
|
|
<DialogTitle>Neuen Buchungsposten erstellen</DialogTitle>
|
|
<DialogContent>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
|
<TextField
|
|
label="Betrag"
|
|
type="number"
|
|
value={newItem.umsatz_brutto}
|
|
onChange={(e) => this.setState({
|
|
newItem: { ...newItem, umsatz_brutto: e.target.value }
|
|
})}
|
|
required
|
|
fullWidth
|
|
/>
|
|
|
|
<FormControl fullWidth>
|
|
<InputLabel>Soll/Haben</InputLabel>
|
|
<Select
|
|
value={newItem.soll_haben_kz}
|
|
onChange={(e) => this.setState({
|
|
newItem: { ...newItem, soll_haben_kz: e.target.value }
|
|
})}
|
|
>
|
|
<MenuItem value="S">Soll (S)</MenuItem>
|
|
<MenuItem value="H">Haben (H)</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
|
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end' }}>
|
|
<FormControl fullWidth>
|
|
<InputLabel>Konto</InputLabel>
|
|
<Select
|
|
value={newItem.konto}
|
|
onChange={(e) => this.setState({
|
|
newItem: { ...newItem, konto: e.target.value }
|
|
})}
|
|
>
|
|
{kontos.map((konto) => (
|
|
<MenuItem key={konto.id} value={konto.konto}>
|
|
{konto.konto} - {konto.name}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
<Button
|
|
variant="outlined"
|
|
onClick={() => this.setState({ showCreateKontoDialog: true })}
|
|
sx={{ minWidth: 'auto', px: 1 }}
|
|
>
|
|
<AddIcon />
|
|
</Button>
|
|
</Box>
|
|
|
|
<FormControl fullWidth>
|
|
<InputLabel>BU (Steuercode)</InputLabel>
|
|
<Select
|
|
value={newItem.bu}
|
|
onChange={(e) => this.setState({
|
|
newItem: { ...newItem, bu: e.target.value }
|
|
})}
|
|
>
|
|
<MenuItem value="">Kein BU</MenuItem>
|
|
{bus.map((bu) => (
|
|
<MenuItem key={bu.id} value={bu.bu}>
|
|
{bu.bu} - {bu.name} ({bu.vst}%)
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
|
|
<TextField
|
|
label="Buchungstext"
|
|
value={newItem.buchungstext}
|
|
onChange={(e) => this.setState({
|
|
newItem: { ...newItem, buchungstext: e.target.value }
|
|
})}
|
|
fullWidth
|
|
multiline
|
|
rows={2}
|
|
/>
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => this.setState({ showCreateDialog: false })}>
|
|
Abbrechen
|
|
</Button>
|
|
<Button onClick={this.handleSaveItem} variant="contained" disabled={saving}>
|
|
{saving ? 'Speichern...' : 'Speichern'}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
{/* Create Konto Dialog */}
|
|
<Dialog open={showCreateKontoDialog} onClose={() => this.setState({ showCreateKontoDialog: false })} maxWidth="xs" fullWidth>
|
|
<DialogTitle>Neues Konto erstellen</DialogTitle>
|
|
<DialogContent>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
|
<TextField
|
|
label="Konto-Nummer"
|
|
value={newKonto.konto}
|
|
onChange={(e) => this.setState({
|
|
newKonto: { ...newKonto, konto: e.target.value }
|
|
})}
|
|
required
|
|
fullWidth
|
|
/>
|
|
<TextField
|
|
label="Konto-Name"
|
|
value={newKonto.name}
|
|
onChange={(e) => this.setState({
|
|
newKonto: { ...newKonto, name: e.target.value }
|
|
})}
|
|
required
|
|
fullWidth
|
|
/>
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => this.setState({ showCreateKontoDialog: false })}>
|
|
Abbrechen
|
|
</Button>
|
|
<Button onClick={this.handleCreateKonto} variant="contained" disabled={saving}>
|
|
{saving ? 'Erstellen...' : 'Erstellen'}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</Box>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default AccountingItemsManager; |