Remove data.csv file and update README to reflect new features including CSV import and banking account management. Enhance TransactionsTable and KreditorTable components with banking account handling, including UI updates and validation logic. Update SQL schema to support banking accounts and adjust API routes for improved data handling. Implement new document rendering logic for banking transactions and enhance recipient rendering with banking account status. Add new views and indexes for better transaction management.

This commit is contained in:
sebseb7
2025-08-01 13:26:26 +02:00
parent 6cde543938
commit fbfd918d81
13 changed files with 1774 additions and 1409 deletions

View File

@@ -0,0 +1,252 @@
import React, { Component } from 'react';
import {
Box,
FormControl,
InputLabel,
Select,
MenuItem,
TextField,
Button,
Alert,
CircularProgress,
Typography,
} from '@mui/material';
import AuthService from '../services/AuthService';
class BankingKreditorSelector extends Component {
constructor(props) {
super(props);
this.state = {
assignableKreditors: [],
selectedKreditorId: '',
notes: '',
loading: false,
error: null,
saving: false,
};
this.authService = new AuthService();
}
componentDidMount() {
this.loadAssignableKreditors();
this.loadExistingAssignment();
}
componentDidUpdate(prevProps) {
// Reload data when transaction changes
if (this.props.transaction?.id !== prevProps.transaction?.id) {
this.loadExistingAssignment();
}
}
loadAssignableKreditors = async () => {
try {
this.setState({ loading: true, error: null });
const response = await this.authService.apiCall('/data/assignable-kreditors');
if (response && response.ok) {
const kreditors = await response.json();
this.setState({ assignableKreditors: kreditors, loading: false });
} else {
this.setState({
error: 'Fehler beim Laden der verfügbaren Kreditoren',
loading: false
});
}
} catch (error) {
console.error('Error loading assignable kreditors:', error);
this.setState({
error: 'Fehler beim Laden der verfügbaren Kreditoren',
loading: false
});
}
};
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;
try {
const response = await this.authService.apiCall(
`/data/banking-transactions/${transactionId}`
);
if (response && response.ok) {
const assignments = await response.json();
if (assignments.length > 0) {
const assignment = assignments[0];
this.setState({
selectedKreditorId: assignment.assigned_kreditor_id || '',
notes: assignment.notes || '',
});
}
}
} catch (error) {
console.error('Error loading existing assignment:', error);
// Don't show error for missing assignments - it's normal
}
};
handleKreditorChange = (event) => {
this.setState({ selectedKreditorId: event.target.value });
};
handleNotesChange = (event) => {
this.setState({ notes: event.target.value });
};
handleSave = async () => {
const { transaction, user, onSave } = this.props;
const { selectedKreditorId, notes } = this.state;
if (!selectedKreditorId) {
this.setState({ error: 'Bitte wählen Sie einen Kreditor aus' });
return;
}
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 (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 (response && response.ok) {
this.setState({ saving: false });
if (onSave) {
onSave();
}
} else {
const errorData = await response.json();
this.setState({
error: errorData.error || 'Fehler beim Speichern der Zuordnung',
saving: false
});
}
} catch (error) {
console.error('Error saving kreditor assignment:', error);
this.setState({
error: 'Fehler beim Speichern der Zuordnung',
saving: false
});
}
};
render() {
const {
assignableKreditors,
selectedKreditorId,
notes,
loading,
error,
saving
} = this.state;
if (loading) {
return (
<Box display="flex" justifyContent="center" py={2}>
<CircularProgress size={20} />
<Typography variant="caption" sx={{ ml: 1 }}>
Lade Kreditoren...
</Typography>
</Box>
);
}
return (
<Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<FormControl fullWidth sx={{ mb: 2 }} size="small">
<InputLabel id="kreditor-select-label">
Kreditor auswählen *
</InputLabel>
<Select
labelId="kreditor-select-label"
value={selectedKreditorId}
onChange={this.handleKreditorChange}
label="Kreditor auswählen *"
>
{assignableKreditors.map((kreditor) => (
<MenuItem key={kreditor.id} value={kreditor.id}>
{kreditor.name} ({kreditor.kreditorId})
</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"
/>
<Button
onClick={this.handleSave}
variant="contained"
disabled={!selectedKreditorId || saving}
size="small"
sx={{
bgcolor: '#ff5722',
'&:hover': { bgcolor: '#e64a19' }
}}
>
{saving ? (
<>
<CircularProgress size={16} sx={{ mr: 1 }} />
Speichern...
</>
) : (
'Kreditor zuordnen'
)}
</Button>
</Box>
);
}
}
export default BankingKreditorSelector;

View File

@@ -0,0 +1,350 @@
import React, { Component } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
Box,
Alert,
CircularProgress,
LinearProgress,
Chip,
} from '@mui/material';
import {
CloudUpload as UploadIcon,
CheckCircle as SuccessIcon,
Error as ErrorIcon,
} from '@mui/icons-material';
import AuthService from '../services/AuthService';
class CSVImportDialog extends Component {
constructor(props) {
super(props);
this.state = {
file: null,
csvData: null,
headers: null,
importing: false,
imported: false,
importResult: null,
error: null,
dragOver: false,
};
this.authService = new AuthService();
this.fileInputRef = React.createRef();
}
handleFileSelect = (event) => {
const file = event.target.files[0];
if (file) {
this.processFile(file);
}
};
handleDrop = (event) => {
event.preventDefault();
this.setState({ dragOver: false });
const file = event.dataTransfer.files[0];
if (file) {
this.processFile(file);
}
};
handleDragOver = (event) => {
event.preventDefault();
this.setState({ dragOver: true });
};
handleDragLeave = () => {
this.setState({ dragOver: false });
};
processFile = (file) => {
if (!file.name.toLowerCase().endsWith('.csv')) {
this.setState({ error: 'Bitte wählen Sie eine CSV-Datei aus' });
return;
}
this.setState({ file, error: null, csvData: null, headers: null });
const reader = new FileReader();
reader.onload = (e) => {
try {
const text = e.target.result;
const lines = text.split('\n').filter(line => line.trim());
if (lines.length < 2) {
this.setState({ error: 'CSV-Datei muss mindestens eine Kopfzeile und eine Datenzeile enthalten' });
return;
}
// Parse CSV (simple parsing - assumes semicolon separator and quoted fields)
const parseCSVLine = (line) => {
const result = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ';' && !inQuotes) {
result.push(current.trim());
current = '';
} else {
current += char;
}
}
result.push(current.trim());
return result;
};
const headers = parseCSVLine(lines[0]);
const dataRows = lines.slice(1).map(line => {
const values = parseCSVLine(line);
const row = {};
headers.forEach((header, index) => {
row[header] = values[index] || '';
});
return row;
});
this.setState({
csvData: dataRows,
headers,
error: null
});
} catch (error) {
console.error('Error parsing CSV:', error);
this.setState({ error: 'Fehler beim Lesen der CSV-Datei' });
}
};
reader.readAsText(file, 'UTF-8');
};
handleImport = async () => {
const { csvData, headers, file } = this.state;
if (!csvData || csvData.length === 0) {
this.setState({ error: 'Keine Daten zum Importieren gefunden' });
return;
}
this.setState({ importing: true, error: null });
try {
const response = await this.authService.apiCall('/data/import-csv-transactions', 'POST', {
transactions: csvData,
headers: headers,
filename: file.name,
batchId: `import_${Date.now()}_${file.name}`
});
if (response && response.ok) {
const result = await response.json();
this.setState({
importing: false,
imported: true,
importResult: result
});
if (this.props.onImportSuccess) {
this.props.onImportSuccess(result);
}
} else {
const errorData = await response.json();
this.setState({
importing: false,
error: errorData.error || 'Import fehlgeschlagen'
});
}
} catch (error) {
console.error('Import error:', error);
this.setState({
importing: false,
error: 'Netzwerkfehler beim Import'
});
}
};
handleClose = () => {
this.setState({
file: null,
csvData: null,
headers: null,
importing: false,
imported: false,
importResult: null,
error: null,
});
if (this.props.onClose) {
this.props.onClose();
}
};
render() {
const { open } = this.props;
const {
file,
csvData,
headers,
importing,
imported,
importResult,
error,
dragOver
} = this.state;
return (
<Dialog
open={open}
onClose={!importing ? this.handleClose : undefined}
maxWidth="md"
fullWidth
>
<DialogTitle>
CSV Transaktionen Importieren
</DialogTitle>
<DialogContent>
{!imported ? (
<>
{/* File Upload Area */}
<Box
sx={{
border: '2px dashed',
borderColor: dragOver ? 'primary.main' : 'grey.300',
borderRadius: 2,
p: 4,
textAlign: 'center',
bgcolor: dragOver ? 'action.hover' : 'background.paper',
cursor: 'pointer',
mb: 2,
}}
onDrop={this.handleDrop}
onDragOver={this.handleDragOver}
onDragLeave={this.handleDragLeave}
onClick={() => this.fileInputRef.current?.click()}
>
<input
type="file"
accept=".csv"
onChange={this.handleFileSelect}
ref={this.fileInputRef}
style={{ display: 'none' }}
/>
<UploadIcon sx={{ fontSize: 48, color: 'grey.400', mb: 2 }} />
<Typography variant="h6" gutterBottom>
CSV-Datei hier ablegen oder klicken zum Auswählen
</Typography>
<Typography variant="body2" color="textSecondary">
Unterstützte Formate: .csv (Semikolon-getrennt)
</Typography>
</Box>
{file && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Ausgewählte Datei:
</Typography>
<Chip label={file.name} color="primary" />
</Box>
)}
{headers && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Erkannte Spalten ({headers.length}):
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{headers.slice(0, 10).map((header, index) => (
<Chip key={index} label={header} size="small" variant="outlined" />
))}
{headers.length > 10 && (
<Chip label={`+${headers.length - 10} weitere`} size="small" />
)}
</Box>
</Box>
)}
{csvData && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Gefundene Transaktionen: {csvData.length}
</Typography>
<Typography variant="body2" color="textSecondary">
Die Daten werden validiert und in die Datenbank importiert.
</Typography>
</Box>
)}
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{importing && (
<Box sx={{ mb: 2 }}>
<LinearProgress />
<Typography variant="body2" sx={{ mt: 1, textAlign: 'center' }}>
Importiere Transaktionen...
</Typography>
</Box>
)}
</>
) : (
/* Import Success */
<Box sx={{ textAlign: 'center', py: 2 }}>
<SuccessIcon sx={{ fontSize: 64, color: 'success.main', mb: 2 }} />
<Typography variant="h6" gutterBottom>
Import erfolgreich abgeschlossen!
</Typography>
{importResult && (
<Box sx={{ mt: 2 }}>
<Typography variant="body1" gutterBottom>
<strong>Importiert:</strong> {importResult.imported} Transaktionen
</Typography>
{importResult.errors > 0 && (
<Typography variant="body1" color="warning.main">
<strong>Fehler:</strong> {importResult.errors} Zeilen übersprungen
</Typography>
)}
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
Batch-ID: {importResult.batchId}
</Typography>
</Box>
)}
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={this.handleClose} disabled={importing}>
{imported ? 'Schließen' : 'Abbrechen'}
</Button>
{!imported && csvData && (
<Button
onClick={this.handleImport}
variant="contained"
disabled={importing || !csvData}
startIcon={importing ? <CircularProgress size={16} /> : <UploadIcon />}
>
{importing ? 'Importiere...' : 'Importieren'}
</Button>
)}
</DialogActions>
</Dialog>
);
}
}
export default CSVImportDialog;

View File

@@ -16,6 +16,7 @@ import { Clear as ClearIcon } from '@mui/icons-material';
import { getColumnDefs, defaultColDef, gridOptions } from './config/gridConfig';
import { processTransactionData, getRowStyle, getRowClass, getSelectedDisplayName } from './utils/dataUtils';
class TransactionsTable extends Component {
constructor(props) {
super(props);
@@ -282,7 +283,7 @@ class TransactionsTable extends Component {
console.log('Selected rows:', Array.from(selectedRows));
};
render() {
const { selectedMonth, loading } = this.props;
@@ -334,6 +335,7 @@ class TransactionsTable extends Component {
selectedRows: this.state.selectedRows,
onSelectionChange: this.onSelectionChange,
onSelectAll: this.onSelectAll,
totalRows: this.state.totalRows,
displayedRows: this.state.displayedRows
}}
@@ -479,6 +481,8 @@ class TransactionsTable extends Component {
</Tooltip>
)}
</Box>
</Paper>
);
}

View File

@@ -18,6 +18,8 @@ import {
Typography,
Alert,
CircularProgress,
Checkbox,
FormControlLabel,
} from '@mui/material';
import {
Add as AddIcon,
@@ -41,6 +43,7 @@ class KreditorTable extends Component {
iban: '',
name: '',
kreditorId: '',
is_banking: false,
},
};
this.authService = new AuthService();
@@ -78,13 +81,15 @@ class KreditorTable extends Component {
dialogOpen: true,
editingKreditor: kreditor,
formData: kreditor ? {
iban: kreditor.iban,
iban: kreditor.iban || '',
name: kreditor.name,
kreditorId: kreditor.kreditorId,
is_banking: Boolean(kreditor.is_banking),
} : {
iban: '',
name: '',
kreditorId: '',
is_banking: false,
},
});
};
@@ -97,6 +102,7 @@ class KreditorTable extends Component {
iban: '',
name: '',
kreditorId: '',
is_banking: false,
},
});
@@ -117,11 +123,24 @@ class KreditorTable extends Component {
});
};
handleCheckboxChange = (field) => (event) => {
this.setState({
formData: {
...this.state.formData,
[field]: event.target.checked,
},
});
};
isFormValid = () => {
const { formData } = this.state;
return formData.iban.trim() !== '' &&
formData.name.trim() !== '' &&
formData.kreditorId.trim() !== '';
// Name and kreditorId are always required
const basicFieldsValid = formData.name.trim() !== '' && formData.kreditorId.trim() !== '';
// IBAN is optional for banking accounts, required for regular kreditors
const ibanValid = formData.is_banking || formData.iban.trim() !== '';
return basicFieldsValid && ibanValid;
};
handleSave = async () => {
@@ -244,6 +263,7 @@ class KreditorTable extends Component {
<TableCell>Kreditor ID</TableCell>
<TableCell>Name</TableCell>
<TableCell>IBAN</TableCell>
<TableCell>Typ</TableCell>
<TableCell align="right">Aktionen</TableCell>
</TableRow>
</TableHead>
@@ -252,7 +272,18 @@ class KreditorTable extends Component {
<TableRow key={kreditor.id}>
<TableCell>{kreditor.kreditorId}</TableCell>
<TableCell>{kreditor.name}</TableCell>
<TableCell>{kreditor.iban}</TableCell>
<TableCell style={{
color: kreditor.is_banking ? '#ff5722' : 'inherit',
fontWeight: kreditor.is_banking ? 'bold' : 'normal'
}}>
{kreditor.iban || 'Keine IBAN'}
</TableCell>
<TableCell>
{kreditor.is_banking ?
<span style={{ color: '#ff5722', fontWeight: 'bold' }}>Banking</span> :
'Kreditor'
}
</TableCell>
<TableCell align="right">
<IconButton
size="small"
@@ -316,6 +347,18 @@ class KreditorTable extends Component {
variant="outlined"
value={formData.iban}
onChange={this.handleInputChange('iban')}
helperText={formData.is_banking ? "IBAN ist optional für Banking-Konten" : ""}
sx={{ mb: 2 }}
/>
<FormControlLabel
control={
<Checkbox
checked={formData.is_banking}
onChange={this.handleCheckboxChange('is_banking')}
color="primary"
/>
}
label="Banking-Konto (z.B. PayPal) - benötigt manuelle Kreditor-Zuordnung"
/>
</DialogContent>
<DialogActions>

View File

@@ -28,6 +28,7 @@ import {
} from '@mui/icons-material';
import { AgGridReact } from 'ag-grid-react';
import KreditorSelector from '../KreditorSelector';
import BankingKreditorSelector from '../BankingKreditorSelector';
const DocumentRenderer = (params) => {
// Check for pdfs and links regardless of transaction source
@@ -504,12 +505,22 @@ const DocumentRenderer = (params) => {
</Box>
) : params.data.hasKreditor ? (
<Box>
<Chip
label="Kreditor gefunden"
color="success"
size="small"
sx={{ mb: 2 }}
/>
{!params.data.kreditor?.is_banking && (
<Chip
label="Kreditor gefunden"
color="success"
size="small"
sx={{ mb: 2 }}
/>
)}
{params.data.kreditor?.is_banking && (
<Chip
label="Banking-Konto erkannt"
color="warning"
size="small"
sx={{ mb: 2 }}
/>
)}
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Kreditor Details
@@ -520,7 +531,56 @@ const DocumentRenderer = (params) => {
<Typography variant="body2">
<strong>Kreditor ID:</strong> {params.data.kreditor.kreditorId}
</Typography>
<Typography variant="body2">
<strong>Typ:</strong> {params.data.kreditor.is_banking ? 'Banking-Konto' : 'Kreditor'}
</Typography>
</Box>
{/* Banking Account Assignment Section */}
{params.data.kreditor.is_banking && (
<Box sx={{ mt: 3, p: 2, bgcolor: '#fff3e0', borderRadius: 1, border: '1px solid #ff9800' }}>
<Typography variant="subtitle2" gutterBottom sx={{ color: '#ff5722', fontWeight: 'bold' }}>
🏦 Banking-Konto Zuordnung
</Typography>
<Typography variant="body2" sx={{ mb: 2, color: '#666' }}>
Dieses IBAN ist ein Banking-Konto (z.B. PayPal). Transaktionen müssen einem echten Kreditor zugeordnet werden.
</Typography>
{/* Show current assignment or assignment form */}
{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 den echten Kreditor für diese Banking-Transaktion aus:
</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>
) : (
<Box>

View File

@@ -9,7 +9,7 @@ const RecipientRenderer = (params) => {
// Stop event propagation to prevent row selection
event.stopPropagation();
// Apply filter to IBAN column using the custom IbanSelectionFilter format
// Default behavior: Apply filter to IBAN column
const currentFilterModel = params.api.getFilterModel();
params.api.setFilterModel({
...currentFilterModel,
@@ -25,9 +25,20 @@ const RecipientRenderer = (params) => {
const getIbanColor = () => {
if (!isIbanColumn || !value) return 'inherit';
// Check if this transaction has Kreditor information
if (params.data && params.data.hasKreditor) {
return '#2e7d32'; // Green for found Kreditor
// Check if the kreditor is a banking account
if (params.data.kreditor?.is_banking) {
// Check if banking transaction has assigned kreditor
if (params.data.assignedKreditor) {
return '#00e676'; // Bright neon green for banking account with assigned kreditor
} else {
return '#ff5722'; // Red-orange for banking account needing assignment
}
} else {
return '#2e7d32'; // Dark green for regular kreditor
}
} else if (params.data && value) {
return '#ed6c02'; // Orange for IBAN without Kreditor
}
@@ -39,7 +50,15 @@ const RecipientRenderer = (params) => {
if (!isIbanColumn || !value) return undefined;
if (params.data && params.data.hasKreditor) {
return `IBAN "${value}" - Kreditor: ${params.data.kreditor?.name || 'Unbekannt'} (zum Filtern klicken)`;
if (params.data.kreditor?.is_banking) {
if (params.data.assignedKreditor) {
return `Banking-IBAN "${value}" - Zugeordnet zu: ${params.data.assignedKreditor.name} (zum Filtern klicken)`;
} else {
return `Banking-IBAN "${value}" - BENÖTIGT KREDITOR-ZUORDNUNG (zum Filtern klicken)`;
}
} else {
return `IBAN "${value}" - Kreditor: ${params.data.kreditor?.name || 'Unbekannt'} (zum Filtern klicken)`;
}
} else if (params.data && value) {
return `IBAN "${value}" - Kein Kreditor gefunden (zum Filtern klicken)`;
}

View File

@@ -149,8 +149,12 @@ class KreditorService {
validateKreditorData(kreditorData) {
const errors = [];
if (!kreditorData.iban || kreditorData.iban.trim() === '') {
errors.push('IBAN ist erforderlich');
// 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() !== '';
if (!isBanking && !hasIban) {
errors.push('IBAN ist erforderlich (außer für Banking-Konten oder manuelle Zuordnungen)');
}
if (!kreditorData.name || kreditorData.name.trim() === '') {
@@ -161,14 +165,20 @@ class KreditorService {
errors.push('Kreditor-ID ist erforderlich');
}
// Basic IBAN format validation (simplified)
if (kreditorData.iban && !/^[A-Z]{2}[0-9]{2}[A-Z0-9]{4}[0-9]{7}([A-Z0-9]?){0,16}$/i.test(kreditorData.iban.replace(/\s/g, ''))) {
// Basic IBAN format validation (simplified) - only if IBAN is provided
if (hasIban && !/^[A-Z]{2}[0-9]{2}[A-Z0-9]{4}[0-9]{7}([A-Z0-9]?){0,16}$/i.test(kreditorData.iban.replace(/\s/g, ''))) {
errors.push('IBAN Format ist ungültig');
}
// Validate kreditorId format (should start with 70xxx)
if (kreditorData.kreditorId && !/^70\d{3,}$/.test(kreditorData.kreditorId)) {
errors.push('Kreditor-ID muss mit 70 beginnen gefolgt von mindestens 3 Ziffern');
// Validate kreditorId format (should start with 70xxx for regular kreditors)
if (kreditorData.kreditorId && !isBanking && !/^70\d{3,}$/.test(kreditorData.kreditorId)) {
errors.push('Kreditor-ID muss mit 70 beginnen gefolgt von mindestens 3 Ziffern (außer für Banking-Konten)');
}
// For banking accounts, warn about special handling
if (isBanking && hasIban) {
// This is just informational, not an error
console.info('Banking-Konto erkannt: Transaktionen benötigen manuelle Kreditor-Zuordnung');
}
return errors;