Implement new Kreditor creation functionality in BankingKreditorSelector component. Add state management for new Kreditor details, validation, and error handling. Enhance transaction handling to support banking transactions without IBAN. Update UI to allow users to create new Kreditors directly from the selector, including ID generation and validation feedback.
This commit is contained in:
@@ -11,7 +11,9 @@ import {
|
||||
CircularProgress,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Add as AddIcon } from '@mui/icons-material';
|
||||
import AuthService from '../services/AuthService';
|
||||
import KreditorService from '../services/KreditorService';
|
||||
|
||||
class BankingKreditorSelector extends Component {
|
||||
constructor(props) {
|
||||
@@ -19,22 +21,46 @@ class BankingKreditorSelector extends Component {
|
||||
this.state = {
|
||||
assignableKreditors: [],
|
||||
selectedKreditorId: '',
|
||||
notes: '',
|
||||
loading: false,
|
||||
error: null,
|
||||
saving: false,
|
||||
showCreateKreditor: false,
|
||||
newKreditor: {
|
||||
name: '',
|
||||
kreditorId: ''
|
||||
},
|
||||
creating: false,
|
||||
validationErrors: []
|
||||
};
|
||||
this.authService = new AuthService();
|
||||
this.kreditorService = new KreditorService();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadAssignableKreditors();
|
||||
this.loadExistingAssignment();
|
||||
|
||||
// Pre-fill new kreditor data with description (actual company name) instead of Beguenstigter (banking service name)
|
||||
const prefilledName = this.props.transaction?.description || '';
|
||||
if (prefilledName) {
|
||||
this.setState({
|
||||
newKreditor: {
|
||||
...this.state.newKreditor,
|
||||
name: prefilledName
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// Reload data when transaction changes
|
||||
if (this.props.transaction?.id !== prevProps.transaction?.id) {
|
||||
// Reload data when transaction changes (only for database transactions)
|
||||
const currentTransactionId = this.props.transaction?.id;
|
||||
const prevTransactionId = prevProps.transaction?.id;
|
||||
|
||||
console.log('componentDidUpdate - current:', currentTransactionId, 'prev:', prevTransactionId);
|
||||
|
||||
if (currentTransactionId !== prevTransactionId) {
|
||||
console.log('Transaction changed, reloading assignment');
|
||||
this.loadExistingAssignment();
|
||||
}
|
||||
}
|
||||
@@ -63,9 +89,18 @@ class BankingKreditorSelector extends Component {
|
||||
};
|
||||
|
||||
loadExistingAssignment = async () => {
|
||||
// For CSV transactions, we need to use csv_transaction_id instead of transaction_id
|
||||
const transactionId = this.props.transaction?.id || this.props.transaction?.csv_id;
|
||||
if (!transactionId) return;
|
||||
// Only load assignments for regular database transactions, not CSV transactions
|
||||
const transactionId = this.props.transaction?.id;
|
||||
console.log('loadExistingAssignment called with:', {
|
||||
transactionId,
|
||||
csv_id: this.props.transaction?.csv_id,
|
||||
fullTransaction: this.props.transaction
|
||||
});
|
||||
|
||||
if (!transactionId) {
|
||||
console.log('Skipping loadExistingAssignment - no transaction ID');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.authService.apiCall(
|
||||
@@ -78,7 +113,6 @@ class BankingKreditorSelector extends Component {
|
||||
const assignment = assignments[0];
|
||||
this.setState({
|
||||
selectedKreditorId: assignment.assigned_kreditor_id || '',
|
||||
notes: assignment.notes || '',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -89,16 +123,80 @@ class BankingKreditorSelector extends Component {
|
||||
};
|
||||
|
||||
handleKreditorChange = (event) => {
|
||||
this.setState({ selectedKreditorId: event.target.value });
|
||||
const value = event.target.value;
|
||||
if (value === 'create_new') {
|
||||
this.setState({ showCreateKreditor: true, selectedKreditorId: '' });
|
||||
} else {
|
||||
this.setState({ selectedKreditorId: value, showCreateKreditor: false });
|
||||
}
|
||||
};
|
||||
|
||||
handleNotesChange = (event) => {
|
||||
this.setState({ notes: event.target.value });
|
||||
handleNewKreditorChange = (field, value) => {
|
||||
this.setState({
|
||||
newKreditor: {
|
||||
...this.state.newKreditor,
|
||||
[field]: value
|
||||
},
|
||||
validationErrors: []
|
||||
});
|
||||
};
|
||||
|
||||
generateKreditorId = () => {
|
||||
const randomDigits = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
|
||||
const kreditorId = `70${randomDigits}`;
|
||||
|
||||
this.setState({
|
||||
newKreditor: {
|
||||
...this.state.newKreditor,
|
||||
kreditorId
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleCreateKreditor = async () => {
|
||||
const { newKreditor } = this.state;
|
||||
|
||||
// Create regular kreditor data (no IBAN because transaction was processed through banking account)
|
||||
const kreditorDataToValidate = {
|
||||
...newKreditor,
|
||||
iban: null,
|
||||
is_banking: false, // This is a regular kreditor (actual company) that will be assigned to banking transactions
|
||||
is_manual_assignment: true // This is a manual assignment for a banking transaction, so no IBAN is required
|
||||
};
|
||||
|
||||
// Validate the data
|
||||
const validationErrors = this.kreditorService.validateKreditorData(kreditorDataToValidate);
|
||||
if (validationErrors.length > 0) {
|
||||
this.setState({ validationErrors });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ creating: true, error: null });
|
||||
|
||||
try {
|
||||
const createdKreditor = await this.kreditorService.createKreditor(kreditorDataToValidate);
|
||||
|
||||
// Add the new kreditor to the list and select it
|
||||
this.setState({
|
||||
assignableKreditors: [...this.state.assignableKreditors, createdKreditor],
|
||||
selectedKreditorId: createdKreditor.id,
|
||||
showCreateKreditor: false,
|
||||
creating: false,
|
||||
newKreditor: { name: '', kreditorId: '' },
|
||||
validationErrors: []
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating kreditor:', error);
|
||||
this.setState({
|
||||
error: error.message,
|
||||
creating: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleSave = async () => {
|
||||
const { transaction, user, onSave } = this.props;
|
||||
const { selectedKreditorId, notes } = this.state;
|
||||
const { selectedKreditorId } = this.state;
|
||||
|
||||
if (!selectedKreditorId) {
|
||||
this.setState({ error: 'Bitte wählen Sie einen Kreditor aus' });
|
||||
@@ -108,12 +206,14 @@ class BankingKreditorSelector extends Component {
|
||||
this.setState({ saving: true, error: null });
|
||||
|
||||
try {
|
||||
// Check if assignment already exists
|
||||
let response;
|
||||
|
||||
if (transaction.id) {
|
||||
// Check for existing assignment first
|
||||
const checkResponse = await this.authService.apiCall(
|
||||
`/data/banking-transactions/${transaction.id}`
|
||||
);
|
||||
|
||||
let response;
|
||||
if (checkResponse && checkResponse.ok) {
|
||||
const existingAssignments = await checkResponse.json();
|
||||
|
||||
@@ -121,29 +221,38 @@ class BankingKreditorSelector extends Component {
|
||||
// Update existing assignment
|
||||
response = await this.authService.apiCall(
|
||||
`/data/banking-transactions/${existingAssignments[0].id}`,
|
||||
'PUT',
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
assigned_kreditor_id: parseInt(selectedKreditorId),
|
||||
notes: notes.trim() || null,
|
||||
assigned_by: user?.username || 'Unknown',
|
||||
})
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Create new assignment
|
||||
response = await this.authService.apiCall(
|
||||
'/data/banking-transactions',
|
||||
'POST',
|
||||
{
|
||||
transaction_id: transaction.id || null,
|
||||
csv_transaction_id: transaction.csv_id || transaction.id || null,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
transaction_id: transaction.isFromCSV ? null : transaction.id,
|
||||
csv_transaction_id: transaction.isFromCSV ? transaction.id : null,
|
||||
banking_iban: transaction['Kontonummer/IBAN'] || transaction.kontonummer_iban,
|
||||
assigned_kreditor_id: parseInt(selectedKreditorId),
|
||||
notes: notes.trim() || null,
|
||||
assigned_by: user?.username || 'Unknown',
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
error: 'Transaktion hat keine gültige ID',
|
||||
saving: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (response && response.ok) {
|
||||
this.setState({ saving: false });
|
||||
@@ -170,10 +279,13 @@ class BankingKreditorSelector extends Component {
|
||||
const {
|
||||
assignableKreditors,
|
||||
selectedKreditorId,
|
||||
notes,
|
||||
loading,
|
||||
error,
|
||||
saving
|
||||
saving,
|
||||
showCreateKreditor,
|
||||
newKreditor,
|
||||
creating,
|
||||
validationErrors
|
||||
} = this.state;
|
||||
|
||||
if (loading) {
|
||||
@@ -210,21 +322,84 @@ class BankingKreditorSelector extends Component {
|
||||
{kreditor.name} ({kreditor.kreditorId})
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem value="create_new" sx={{ color: 'primary.main', fontWeight: 'bold' }}>
|
||||
<AddIcon sx={{ mr: 1 }} />
|
||||
Neuen Kreditor erstellen
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{showCreateKreditor && (
|
||||
<Box sx={{ mt: 2, p: 2, bgcolor: '#f5f5f5', borderRadius: 1 }}>
|
||||
<Typography variant="body2" sx={{ mb: 2, fontWeight: 'bold' }}>
|
||||
Neuen Kreditor erstellen:
|
||||
</Typography>
|
||||
|
||||
{validationErrors.length > 0 && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
<ul style={{ margin: 0, paddingLeft: '20px' }}>
|
||||
{validationErrors.map((error, index) => (
|
||||
<li key={index}>{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label="Name *"
|
||||
value={newKreditor.name}
|
||||
onChange={(e) => this.handleNewKreditorChange('name', e.target.value)}
|
||||
fullWidth
|
||||
label="Notizen (optional)"
|
||||
multiline
|
||||
rows={2}
|
||||
value={notes}
|
||||
onChange={this.handleNotesChange}
|
||||
placeholder="Zusätzliche Informationen..."
|
||||
sx={{ mb: 2 }}
|
||||
size="small"
|
||||
sx={{ mb: 2 }}
|
||||
placeholder="Name des Kreditors"
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end', mb: 2 }}>
|
||||
<TextField
|
||||
label="Kreditor-ID *"
|
||||
value={newKreditor.kreditorId}
|
||||
onChange={(e) => this.handleNewKreditorChange('kreditorId', e.target.value)}
|
||||
size="small"
|
||||
placeholder="70001"
|
||||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
<Button
|
||||
onClick={this.generateKreditorId}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
Generieren
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="caption" color="textSecondary" sx={{ display: 'block', mb: 2 }}>
|
||||
Die Kreditor-ID muss mit "70" beginnen, gefolgt von mindestens 3 Ziffern.
|
||||
Keine IBAN erforderlich, da diese Transaktion über ein Banking-Konto abgewickelt wurde.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
onClick={this.handleCreateKreditor}
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={creating}
|
||||
startIcon={creating ? <CircularProgress size={16} /> : null}
|
||||
>
|
||||
{creating ? 'Erstellen...' : 'Kreditor erstellen'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => this.setState({ showCreateKreditor: false, validationErrors: [] })}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
disabled={creating}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={this.handleSave}
|
||||
variant="contained"
|
||||
|
||||
@@ -155,8 +155,15 @@ class KreditorSelector extends Component {
|
||||
handleCreateKreditor = async () => {
|
||||
const { newKreditor } = this.state;
|
||||
|
||||
// For banking kreditors (when allowEmptyIban is true), mark as banking if IBAN is empty
|
||||
const kreditorDataToValidate = {
|
||||
...newKreditor,
|
||||
is_banking: this.props.allowEmptyIban && (!newKreditor.iban || newKreditor.iban.trim() === ''),
|
||||
iban: newKreditor.iban || null // Convert empty string to null
|
||||
};
|
||||
|
||||
// Validate the data
|
||||
const validationErrors = this.kreditorService.validateKreditorData(newKreditor);
|
||||
const validationErrors = this.kreditorService.validateKreditorData(kreditorDataToValidate);
|
||||
if (validationErrors.length > 0) {
|
||||
this.setState({ validationErrors });
|
||||
return;
|
||||
@@ -165,7 +172,7 @@ class KreditorSelector extends Component {
|
||||
this.setState({ creating: true, error: null });
|
||||
|
||||
try {
|
||||
const createdKreditor = await this.kreditorService.createKreditor(newKreditor);
|
||||
const createdKreditor = await this.kreditorService.createKreditor(kreditorDataToValidate);
|
||||
|
||||
// Add the new kreditor to the list and select it
|
||||
const updatedKreditors = [...this.state.kreditors, createdKreditor];
|
||||
@@ -243,9 +250,16 @@ class KreditorSelector extends Component {
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Neuen Kreditor erstellen</DialogTitle>
|
||||
<DialogTitle>
|
||||
{this.props.allowEmptyIban ? 'Neuen Banking-Kreditor erstellen' : 'Neuen Kreditor erstellen'}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ pt: 1 }}>
|
||||
{this.props.allowEmptyIban && (
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
Sie erstellen einen Kreditor für eine Banking-Transaktion. Die IBAN kann leer bleiben, da diese Transaktion über ein Banking-Konto (z.B. PayPal) abgewickelt wurde.
|
||||
</Alert>
|
||||
)}
|
||||
{validationErrors.length > 0 && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
<ul style={{ margin: 0, paddingLeft: '20px' }}>
|
||||
@@ -277,8 +291,9 @@ class KreditorSelector extends Component {
|
||||
onChange={(e) => this.handleNewKreditorChange('iban', e.target.value.toUpperCase())}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
required
|
||||
placeholder="DE89 3704 0044 0532 0130 00"
|
||||
required={!this.props.allowEmptyIban}
|
||||
placeholder={this.props.allowEmptyIban ? "Leer lassen für Banking-Kreditor" : "DE89 3704 0044 0532 0130 00"}
|
||||
helperText={this.props.allowEmptyIban ? "Für Banking-Transaktionen kann die IBAN leer bleiben" : ""}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end' }}>
|
||||
|
||||
@@ -350,15 +350,26 @@ const DocumentRenderer = (params) => {
|
||||
aria-describedby="document-dialog-content"
|
||||
>
|
||||
<DialogTitle id="document-dialog-title">
|
||||
{(() => {
|
||||
const beschreibung = params?.data?.description || '';
|
||||
const beschreibungTrimmed = beschreibung ? String(beschreibung).slice(0, 120) : '';
|
||||
if (hasDocuments) {
|
||||
return `Dokumente (${totalCount})${beschreibungTrimmed ? ' — ' + beschreibungTrimmed : ''}`;
|
||||
}
|
||||
// No documents: still include description next to "Dokumentinformationen"
|
||||
return `Dokumentinformationen${beschreibungTrimmed ? ' — ' + beschreibungTrimmed : ''}`;
|
||||
})()}
|
||||
{hasDocuments ? `Dokumente (${totalCount})` : 'Dokumentinformationen'}
|
||||
{!params.data['Kontonummer/IBAN'] && (
|
||||
<Typography variant="caption" sx={{ display: 'block', color: '#ff5722', fontWeight: 'normal' }}>
|
||||
Banking-Transaktion • Kreditor-Zuordnung erforderlich
|
||||
</Typography>
|
||||
)}
|
||||
{params.data.description && (
|
||||
<Typography variant="body2" sx={{
|
||||
display: 'block',
|
||||
color: 'text.secondary',
|
||||
fontWeight: 'normal',
|
||||
mt: 1,
|
||||
p: 1,
|
||||
bgcolor: '#f5f5f5',
|
||||
borderRadius: 1,
|
||||
border: '1px solid #e0e0e0'
|
||||
}}>
|
||||
<strong>Beschreibung:</strong> {params.data.description}
|
||||
</Typography>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ p: 0 }} id="document-dialog-content">
|
||||
{error && (
|
||||
@@ -502,14 +513,48 @@ const DocumentRenderer = (params) => {
|
||||
{!params.data['Kontonummer/IBAN'] ? (
|
||||
<Box>
|
||||
<Chip
|
||||
label="Keine IBAN verfügbar"
|
||||
color="default"
|
||||
label="Banking-Transaktion"
|
||||
color="warning"
|
||||
size="small"
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Ohne IBAN kann kein Kreditor zugeordnet werden.
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
|
||||
Diese Transaktion wurde über ein Banking-Konto (z.B. PayPal) abgewickelt und benötigt eine Kreditor-Zuordnung.
|
||||
</Typography>
|
||||
|
||||
{/* Show current assignment or assignment form for banking transactions */}
|
||||
{params.data.assignedKreditor ? (
|
||||
<Box sx={{ p: 2, bgcolor: '#e8f5e8', borderRadius: 1, mb: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
✅ Zugeordnet zu: {params.data.assignedKreditor.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#666' }}>
|
||||
Kreditor ID: {params.data.assignedKreditor.kreditorId}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ mb: 2, color: '#ff5722', fontWeight: 'bold' }}>
|
||||
⚠️ Keine Zuordnung - Bitte Kreditor zuweisen
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#666', mb: 2, display: 'block' }}>
|
||||
Wählen Sie einen bestehenden Kreditor aus oder erstellen Sie einen neuen Kreditor:
|
||||
</Typography>
|
||||
<BankingKreditorSelector
|
||||
transaction={params.data}
|
||||
user={params.context?.user}
|
||||
onSave={() => {
|
||||
// Refresh the grid to show updated assignment
|
||||
if (params.api) {
|
||||
params.api.refreshCells({
|
||||
columns: ['Kontonummer/IBAN'],
|
||||
force: true
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : params.data.hasKreditor ? (
|
||||
<Box>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const processTransactionData = (transactions) => {
|
||||
return transactions.map((transaction, index) => ({
|
||||
...transaction,
|
||||
id: `row-${index}-${transaction.Buchungstag}-${transaction.numericAmount}`, // Unique ID
|
||||
// Use actual database ID, no fake ID generation
|
||||
description: transaction['Verwendungszweck'] || transaction['Buchungstext'],
|
||||
type: transaction.numericAmount >= 0 ? 'Income' : 'Expense',
|
||||
isIncome: transaction.numericAmount >= 0,
|
||||
|
||||
@@ -155,8 +155,9 @@ class KreditorService {
|
||||
// IBAN is only required for non-banking accounts that are not manual assignments
|
||||
const isBanking = kreditorData.is_banking || false;
|
||||
const hasIban = kreditorData.iban && kreditorData.iban.trim() !== '';
|
||||
const isManualAssignment = kreditorData.is_manual_assignment || false;
|
||||
|
||||
if (!isBanking && !hasIban) {
|
||||
if (!isBanking && !hasIban && !isManualAssignment) {
|
||||
errors.push('IBAN ist erforderlich (außer für Banking-Konten oder manuelle Zuordnungen)');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user