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