Files
fibdash/client/src/components/AccountingItemsManager.js
sebseb7 fee9f02faa Enhance Accounting Items Management with JTL Kontierung Integration
- 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.
2025-08-08 11:32:57 +02:00

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;