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:
sebseb7
2025-08-02 09:14:49 +02:00
parent da435d2e66
commit 20cd0b34bc
5 changed files with 316 additions and 80 deletions

View File

@@ -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,41 +206,52 @@ class BankingKreditorSelector extends Component {
this.setState({ saving: true, error: null });
try {
// Check if assignment already exists
const checkResponse = await this.authService.apiCall(
`/data/banking-transactions/${transaction.id}`
);
let response;
if (checkResponse && checkResponse.ok) {
const existingAssignments = await checkResponse.json();
if (transaction.id) {
// Check for existing assignment first
const checkResponse = await this.authService.apiCall(
`/data/banking-transactions/${transaction.id}`
);
if (existingAssignments.length > 0) {
// Update existing assignment
response = await this.authService.apiCall(
`/data/banking-transactions/${existingAssignments[0].id}`,
'PUT',
{
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,
banking_iban: transaction['Kontonummer/IBAN'] || transaction.kontonummer_iban,
assigned_kreditor_id: parseInt(selectedKreditorId),
notes: notes.trim() || null,
assigned_by: user?.username || 'Unknown',
}
);
if (checkResponse && checkResponse.ok) {
const existingAssignments = await checkResponse.json();
if (existingAssignments.length > 0) {
// Update existing assignment
response = await this.authService.apiCall(
`/data/banking-transactions/${existingAssignments[0].id}`,
{
method: 'PUT',
body: JSON.stringify({
assigned_kreditor_id: parseInt(selectedKreditorId),
assigned_by: user?.username || 'Unknown',
})
}
);
} else {
// Create new assignment
response = await this.authService.apiCall(
'/data/banking-transactions',
{
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),
assigned_by: user?.username || 'Unknown',
})
}
);
}
}
} else {
this.setState({
error: 'Transaktion hat keine gültige ID',
saving: false
});
return;
}
if (response && response.ok) {
@@ -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,20 +322,83 @@ 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>
<TextField
fullWidth
label="Notizen (optional)"
multiline
rows={2}
value={notes}
onChange={this.handleNotesChange}
placeholder="Zusätzliche Informationen..."
sx={{ mb: 2 }}
size="small"
/>
{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
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}

View File

@@ -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' }}>

View File

@@ -135,10 +135,10 @@ const DocumentRenderer = (params) => {
setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
}
} catch (error) {
console.error('Error opening PDF:', error);
setError('Fehler beim Öffnen des PDFs. Bitte versuchen Sie es erneut.');
}
} catch (error) {
console.error('Error opening PDF:', error);
setError('Fehler beim Öffnen des PDFs. Bitte versuchen Sie es erneut.');
}
};
// Extract line items from document extraction data
@@ -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>

View File

@@ -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,

View File

@@ -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)');
}