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, CircularProgress,
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import { Add as AddIcon } from '@mui/icons-material';
import AuthService from '../services/AuthService'; import AuthService from '../services/AuthService';
import KreditorService from '../services/KreditorService';
class BankingKreditorSelector extends Component { class BankingKreditorSelector extends Component {
constructor(props) { constructor(props) {
@@ -19,22 +21,46 @@ class BankingKreditorSelector extends Component {
this.state = { this.state = {
assignableKreditors: [], assignableKreditors: [],
selectedKreditorId: '', selectedKreditorId: '',
notes: '',
loading: false, loading: false,
error: null, error: null,
saving: false, saving: false,
showCreateKreditor: false,
newKreditor: {
name: '',
kreditorId: ''
},
creating: false,
validationErrors: []
}; };
this.authService = new AuthService(); this.authService = new AuthService();
this.kreditorService = new KreditorService();
} }
componentDidMount() { componentDidMount() {
this.loadAssignableKreditors(); this.loadAssignableKreditors();
this.loadExistingAssignment(); 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) { componentDidUpdate(prevProps) {
// Reload data when transaction changes // Reload data when transaction changes (only for database transactions)
if (this.props.transaction?.id !== prevProps.transaction?.id) { 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(); this.loadExistingAssignment();
} }
} }
@@ -63,9 +89,18 @@ class BankingKreditorSelector extends Component {
}; };
loadExistingAssignment = async () => { loadExistingAssignment = async () => {
// For CSV transactions, we need to use csv_transaction_id instead of transaction_id // Only load assignments for regular database transactions, not CSV transactions
const transactionId = this.props.transaction?.id || this.props.transaction?.csv_id; const transactionId = this.props.transaction?.id;
if (!transactionId) return; 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 { try {
const response = await this.authService.apiCall( const response = await this.authService.apiCall(
@@ -78,7 +113,6 @@ class BankingKreditorSelector extends Component {
const assignment = assignments[0]; const assignment = assignments[0];
this.setState({ this.setState({
selectedKreditorId: assignment.assigned_kreditor_id || '', selectedKreditorId: assignment.assigned_kreditor_id || '',
notes: assignment.notes || '',
}); });
} }
} }
@@ -89,16 +123,80 @@ class BankingKreditorSelector extends Component {
}; };
handleKreditorChange = (event) => { 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) => { handleNewKreditorChange = (field, value) => {
this.setState({ notes: event.target.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 () => { handleSave = async () => {
const { transaction, user, onSave } = this.props; const { transaction, user, onSave } = this.props;
const { selectedKreditorId, notes } = this.state; const { selectedKreditorId } = this.state;
if (!selectedKreditorId) { if (!selectedKreditorId) {
this.setState({ error: 'Bitte wählen Sie einen Kreditor aus' }); this.setState({ error: 'Bitte wählen Sie einen Kreditor aus' });
@@ -108,41 +206,52 @@ class BankingKreditorSelector extends Component {
this.setState({ saving: true, error: null }); this.setState({ saving: true, error: null });
try { try {
// Check if assignment already exists
const checkResponse = await this.authService.apiCall(
`/data/banking-transactions/${transaction.id}`
);
let response; 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) { if (checkResponse && checkResponse.ok) {
// Update existing assignment const existingAssignments = await checkResponse.json();
response = await this.authService.apiCall(
`/data/banking-transactions/${existingAssignments[0].id}`, if (existingAssignments.length > 0) {
'PUT', // Update existing assignment
{ response = await this.authService.apiCall(
assigned_kreditor_id: parseInt(selectedKreditorId), `/data/banking-transactions/${existingAssignments[0].id}`,
notes: notes.trim() || null, {
assigned_by: user?.username || 'Unknown', method: 'PUT',
} body: JSON.stringify({
); assigned_kreditor_id: parseInt(selectedKreditorId),
} else { assigned_by: user?.username || 'Unknown',
// Create new assignment })
response = await this.authService.apiCall( }
'/data/banking-transactions', );
'POST', } else {
{ // Create new assignment
transaction_id: transaction.id || null, response = await this.authService.apiCall(
csv_transaction_id: transaction.csv_id || transaction.id || null, '/data/banking-transactions',
banking_iban: transaction['Kontonummer/IBAN'] || transaction.kontonummer_iban, {
assigned_kreditor_id: parseInt(selectedKreditorId), method: 'POST',
notes: notes.trim() || null, body: JSON.stringify({
assigned_by: user?.username || 'Unknown', 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) { if (response && response.ok) {
@@ -170,10 +279,13 @@ class BankingKreditorSelector extends Component {
const { const {
assignableKreditors, assignableKreditors,
selectedKreditorId, selectedKreditorId,
notes,
loading, loading,
error, error,
saving saving,
showCreateKreditor,
newKreditor,
creating,
validationErrors
} = this.state; } = this.state;
if (loading) { if (loading) {
@@ -210,20 +322,83 @@ class BankingKreditorSelector extends Component {
{kreditor.name} ({kreditor.kreditorId}) {kreditor.name} ({kreditor.kreditorId})
</MenuItem> </MenuItem>
))} ))}
<MenuItem value="create_new" sx={{ color: 'primary.main', fontWeight: 'bold' }}>
<AddIcon sx={{ mr: 1 }} />
Neuen Kreditor erstellen
</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<TextField {showCreateKreditor && (
fullWidth <Box sx={{ mt: 2, p: 2, bgcolor: '#f5f5f5', borderRadius: 1 }}>
label="Notizen (optional)" <Typography variant="body2" sx={{ mb: 2, fontWeight: 'bold' }}>
multiline Neuen Kreditor erstellen:
rows={2} </Typography>
value={notes}
onChange={this.handleNotesChange} {validationErrors.length > 0 && (
placeholder="Zusätzliche Informationen..." <Alert severity="error" sx={{ mb: 2 }}>
sx={{ mb: 2 }} <ul style={{ margin: 0, paddingLeft: '20px' }}>
size="small" {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 <Button
onClick={this.handleSave} onClick={this.handleSave}

View File

@@ -155,8 +155,15 @@ class KreditorSelector extends Component {
handleCreateKreditor = async () => { handleCreateKreditor = async () => {
const { newKreditor } = this.state; 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 // Validate the data
const validationErrors = this.kreditorService.validateKreditorData(newKreditor); const validationErrors = this.kreditorService.validateKreditorData(kreditorDataToValidate);
if (validationErrors.length > 0) { if (validationErrors.length > 0) {
this.setState({ validationErrors }); this.setState({ validationErrors });
return; return;
@@ -165,7 +172,7 @@ class KreditorSelector extends Component {
this.setState({ creating: true, error: null }); this.setState({ creating: true, error: null });
try { 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 // Add the new kreditor to the list and select it
const updatedKreditors = [...this.state.kreditors, createdKreditor]; const updatedKreditors = [...this.state.kreditors, createdKreditor];
@@ -243,9 +250,16 @@ class KreditorSelector extends Component {
maxWidth="sm" maxWidth="sm"
fullWidth fullWidth
> >
<DialogTitle>Neuen Kreditor erstellen</DialogTitle> <DialogTitle>
{this.props.allowEmptyIban ? 'Neuen Banking-Kreditor erstellen' : 'Neuen Kreditor erstellen'}
</DialogTitle>
<DialogContent> <DialogContent>
<Box sx={{ pt: 1 }}> <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 && ( {validationErrors.length > 0 && (
<Alert severity="error" sx={{ mb: 2 }}> <Alert severity="error" sx={{ mb: 2 }}>
<ul style={{ margin: 0, paddingLeft: '20px' }}> <ul style={{ margin: 0, paddingLeft: '20px' }}>
@@ -277,8 +291,9 @@ class KreditorSelector extends Component {
onChange={(e) => this.handleNewKreditorChange('iban', e.target.value.toUpperCase())} onChange={(e) => this.handleNewKreditorChange('iban', e.target.value.toUpperCase())}
fullWidth fullWidth
margin="normal" margin="normal"
required required={!this.props.allowEmptyIban}
placeholder="DE89 3704 0044 0532 0130 00" 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' }}> <Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end' }}>

View File

@@ -135,10 +135,10 @@ const DocumentRenderer = (params) => {
setTimeout(() => URL.revokeObjectURL(blobUrl), 1000); setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
} }
} catch (error) { } catch (error) {
console.error('Error opening PDF:', error); console.error('Error opening PDF:', error);
setError('Fehler beim Öffnen des PDFs. Bitte versuchen Sie es erneut.'); setError('Fehler beim Öffnen des PDFs. Bitte versuchen Sie es erneut.');
} }
}; };
// Extract line items from document extraction data // Extract line items from document extraction data
@@ -350,15 +350,26 @@ const DocumentRenderer = (params) => {
aria-describedby="document-dialog-content" aria-describedby="document-dialog-content"
> >
<DialogTitle id="document-dialog-title"> <DialogTitle id="document-dialog-title">
{(() => { {hasDocuments ? `Dokumente (${totalCount})` : 'Dokumentinformationen'}
const beschreibung = params?.data?.description || ''; {!params.data['Kontonummer/IBAN'] && (
const beschreibungTrimmed = beschreibung ? String(beschreibung).slice(0, 120) : ''; <Typography variant="caption" sx={{ display: 'block', color: '#ff5722', fontWeight: 'normal' }}>
if (hasDocuments) { Banking-Transaktion Kreditor-Zuordnung erforderlich
return `Dokumente (${totalCount})${beschreibungTrimmed ? ' — ' + beschreibungTrimmed : ''}`; </Typography>
} )}
// No documents: still include description next to "Dokumentinformationen" {params.data.description && (
return `Dokumentinformationen${beschreibungTrimmed ? ' — ' + beschreibungTrimmed : ''}`; <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> </DialogTitle>
<DialogContent sx={{ p: 0 }} id="document-dialog-content"> <DialogContent sx={{ p: 0 }} id="document-dialog-content">
{error && ( {error && (
@@ -502,14 +513,48 @@ const DocumentRenderer = (params) => {
{!params.data['Kontonummer/IBAN'] ? ( {!params.data['Kontonummer/IBAN'] ? (
<Box> <Box>
<Chip <Chip
label="Keine IBAN verfügbar" label="Banking-Transaktion"
color="default" color="warning"
size="small" size="small"
sx={{ mb: 2 }} sx={{ mb: 2 }}
/> />
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
Ohne IBAN kann kein Kreditor zugeordnet werden. Diese Transaktion wurde über ein Banking-Konto (z.B. PayPal) abgewickelt und benötigt eine Kreditor-Zuordnung.
</Typography> </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> </Box>
) : params.data.hasKreditor ? ( ) : params.data.hasKreditor ? (
<Box> <Box>

View File

@@ -1,7 +1,7 @@
export const processTransactionData = (transactions) => { export const processTransactionData = (transactions) => {
return transactions.map((transaction, index) => ({ return transactions.map((transaction, index) => ({
...transaction, ...transaction,
id: `row-${index}-${transaction.Buchungstag}-${transaction.numericAmount}`, // Unique ID // Use actual database ID, no fake ID generation
description: transaction['Verwendungszweck'] || transaction['Buchungstext'], description: transaction['Verwendungszweck'] || transaction['Buchungstext'],
type: transaction.numericAmount >= 0 ? 'Income' : 'Expense', type: transaction.numericAmount >= 0 ? 'Income' : 'Expense',
isIncome: transaction.numericAmount >= 0, 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 // IBAN is only required for non-banking accounts that are not manual assignments
const isBanking = kreditorData.is_banking || false; const isBanking = kreditorData.is_banking || false;
const hasIban = kreditorData.iban && kreditorData.iban.trim() !== ''; 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)'); errors.push('IBAN ist erforderlich (außer für Banking-Konten oder manuelle Zuordnungen)');
} }