Add Accounting Items Management and SQL Integration
- Introduced AccountingItemsManager component for managing accounting entries within transactions. - Implemented API routes for creating, retrieving, updating, and deleting accounting items. - Added SQL queries to handle accounting items linked to transactions, supporting both numeric and string transaction IDs. - Enhanced CSV import functionality to include new accounting item handling. - Created mssql.md documentation for SQL command usage related to accounting items.
This commit is contained in:
508
client/src/components/AccountingItemsManager.js
Normal file
508
client/src/components/AccountingItemsManager.js
Normal file
@@ -0,0 +1,508 @@
|
||||
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,
|
||||
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();
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<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;
|
||||
Reference in New Issue
Block a user