Add Accounting Items Management and SQL Integration
- Introduced AccountingItemsManager component for managing accounting entries within transactions. - Implemented API routes for creating, retrieving, updating, and deleting accounting items. - Added SQL queries to handle accounting items linked to transactions, supporting both numeric and string transaction IDs. - Enhanced CSV import functionality to include new accounting item handling. - Created mssql.md documentation for SQL command usage related to accounting items.
This commit is contained in:
@@ -11,45 +11,134 @@ import {
|
||||
CircularProgress,
|
||||
LinearProgress,
|
||||
Chip,
|
||||
Tabs,
|
||||
Tab,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CloudUpload as UploadIcon,
|
||||
CheckCircle as SuccessIcon,
|
||||
Error as ErrorIcon,
|
||||
Link as LinkIcon,
|
||||
AccountBalance as AccountIcon,
|
||||
InfoOutlined as InfoIcon,
|
||||
} from '@mui/icons-material';
|
||||
import AuthService from '../services/AuthService';
|
||||
|
||||
const IMPORT_TYPES = {
|
||||
BANKING: 'BANKING',
|
||||
DATEV_LINKS: 'DATEV_LINKS',
|
||||
};
|
||||
|
||||
class CSVImportDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
file: null,
|
||||
csvData: null,
|
||||
headers: null,
|
||||
// common
|
||||
activeTab: IMPORT_TYPES.BANKING,
|
||||
importing: false,
|
||||
imported: false,
|
||||
importResult: null,
|
||||
error: null,
|
||||
|
||||
// drag/drop visual
|
||||
dragOver: false,
|
||||
|
||||
// banking state
|
||||
file: null,
|
||||
csvData: null,
|
||||
headers: null,
|
||||
|
||||
// datev links state
|
||||
datevFile: null,
|
||||
datevCsvData: null,
|
||||
datevHeaders: null,
|
||||
};
|
||||
this.authService = new AuthService();
|
||||
this.fileInputRef = React.createRef();
|
||||
this.datevFileInputRef = React.createRef();
|
||||
}
|
||||
|
||||
// Tab switch resets type-specific state but keeps success state as-is
|
||||
handleTabChange = (_e, value) => {
|
||||
this.setState({
|
||||
activeTab: value,
|
||||
// clear type-specific selections and errors
|
||||
file: null,
|
||||
csvData: null,
|
||||
headers: null,
|
||||
datevFile: null,
|
||||
datevCsvData: null,
|
||||
datevHeaders: null,
|
||||
error: null,
|
||||
dragOver: false,
|
||||
// keep importing false when switching
|
||||
importing: false,
|
||||
// keep imported/result to show success for last action regardless of tab
|
||||
// Alternatively, uncomment next two lines to reset success on tab change:
|
||||
// imported: false,
|
||||
// importResult: null,
|
||||
});
|
||||
};
|
||||
|
||||
// Generic CSV parser (semicolon with quotes)
|
||||
parseCSV = (text) => {
|
||||
const lines = text.split('\n').filter(line => line.trim());
|
||||
if (lines.length < 2) {
|
||||
throw new Error('CSV-Datei muss mindestens eine Kopfzeile und eine Datenzeile enthalten');
|
||||
}
|
||||
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;
|
||||
});
|
||||
return { headers, dataRows };
|
||||
};
|
||||
|
||||
// Banking file handlers
|
||||
handleFileSelect = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.processFile(file);
|
||||
this.processFile(file, IMPORT_TYPES.BANKING);
|
||||
}
|
||||
};
|
||||
// DATEV file handlers
|
||||
handleDatevFileSelect = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.processFile(file, IMPORT_TYPES.DATEV_LINKS);
|
||||
}
|
||||
};
|
||||
|
||||
handleDrop = (event) => {
|
||||
event.preventDefault();
|
||||
this.setState({ dragOver: false });
|
||||
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (file) {
|
||||
this.processFile(file);
|
||||
// route to active tab
|
||||
this.processFile(file, this.state.activeTab);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,75 +151,49 @@ class CSVImportDialog extends Component {
|
||||
this.setState({ dragOver: false });
|
||||
};
|
||||
|
||||
processFile = (file) => {
|
||||
processFile = (file, type) => {
|
||||
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] || '';
|
||||
const { headers, dataRows } = this.parseCSV(text);
|
||||
if (type === IMPORT_TYPES.BANKING) {
|
||||
this.setState({
|
||||
file,
|
||||
csvData: dataRows,
|
||||
headers,
|
||||
error: null,
|
||||
});
|
||||
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' });
|
||||
} else {
|
||||
this.setState({
|
||||
datevFile: file,
|
||||
datevCsvData: dataRows,
|
||||
datevHeaders: headers,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing CSV:', err);
|
||||
this.setState({ error: err.message || 'Fehler beim Lesen der CSV-Datei' });
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
};
|
||||
|
||||
handleImport = async () => {
|
||||
const { csvData, headers, file } = this.state;
|
||||
|
||||
if (!csvData || csvData.length === 0) {
|
||||
const {
|
||||
activeTab,
|
||||
file, csvData, headers,
|
||||
datevFile, datevCsvData, datevHeaders,
|
||||
} = this.state;
|
||||
|
||||
const isBanking = activeTab === IMPORT_TYPES.BANKING;
|
||||
const hasData = isBanking ? (csvData && csvData.length > 0) : (datevCsvData && datevCsvData.length > 0);
|
||||
if (!hasData) {
|
||||
this.setState({ error: 'Keine Daten zum Importieren gefunden' });
|
||||
return;
|
||||
}
|
||||
@@ -138,52 +201,80 @@ class CSVImportDialog extends Component {
|
||||
this.setState({ importing: true, error: null });
|
||||
|
||||
try {
|
||||
const response = await this.authService.apiCall('/data/import-csv-transactions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
let endpoint = '';
|
||||
let payload = {};
|
||||
if (isBanking) {
|
||||
endpoint = '/data/import-csv-transactions';
|
||||
payload = {
|
||||
transactions: csvData,
|
||||
headers: headers,
|
||||
filename: file.name,
|
||||
batchId: `import_${Date.now()}_${file.name}`
|
||||
})
|
||||
batchId: `import_${Date.now()}_${file.name}`,
|
||||
};
|
||||
} else {
|
||||
// Placeholder endpoint for DATEV Beleglinks (adjust when backend is available)
|
||||
endpoint = '/data/import-datev-beleglinks';
|
||||
payload = {
|
||||
beleglinks: datevCsvData,
|
||||
headers: datevHeaders,
|
||||
filename: datevFile.name,
|
||||
batchId: `datev_${Date.now()}_${datevFile.name}`,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.authService.apiCall(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (response && response.ok) {
|
||||
const result = await response.json();
|
||||
this.setState({
|
||||
importing: false,
|
||||
imported: true,
|
||||
importResult: result
|
||||
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'
|
||||
let errorText = 'Import fehlgeschlagen';
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorText = errorData.error || errorText;
|
||||
} catch (_) {}
|
||||
this.setState({
|
||||
importing: false,
|
||||
error: errorText,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Import error:', error);
|
||||
this.setState({
|
||||
importing: false,
|
||||
error: 'Netzwerkfehler beim Import'
|
||||
this.setState({
|
||||
importing: false,
|
||||
error: 'Netzwerkfehler beim Import',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
this.setState({
|
||||
file: null,
|
||||
csvData: null,
|
||||
headers: null,
|
||||
// common
|
||||
importing: false,
|
||||
imported: false,
|
||||
importResult: null,
|
||||
error: null,
|
||||
dragOver: false,
|
||||
|
||||
// banking
|
||||
file: null,
|
||||
csvData: null,
|
||||
headers: null,
|
||||
|
||||
// datev
|
||||
datevFile: null,
|
||||
datevCsvData: null,
|
||||
datevHeaders: null,
|
||||
});
|
||||
|
||||
if (this.props.onClose) {
|
||||
@@ -191,19 +282,199 @@ class CSVImportDialog extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
renderUploadPanel = ({ isBanking }) => {
|
||||
const {
|
||||
dragOver,
|
||||
file, csvData, headers,
|
||||
datevFile, datevCsvData, datevHeaders,
|
||||
} = this.state;
|
||||
|
||||
const currentFile = isBanking ? file : datevFile;
|
||||
const currentHeaders = isBanking ? headers : datevHeaders;
|
||||
const currentData = isBanking ? csvData : datevCsvData;
|
||||
|
||||
const onClickPick = () => {
|
||||
if (isBanking) {
|
||||
this.fileInputRef.current?.click();
|
||||
} else {
|
||||
this.datevFileInputRef.current?.click();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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={onClickPick}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv"
|
||||
onChange={isBanking ? this.handleFileSelect : this.handleDatevFileSelect}
|
||||
ref={isBanking ? this.fileInputRef : this.datevFileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
{isBanking ? (
|
||||
<AccountIcon sx={{ fontSize: 48, color: 'grey.400', mb: 2 }} />
|
||||
) : (
|
||||
<LinkIcon sx={{ fontSize: 48, color: 'grey.400', mb: 2 }} />
|
||||
)}
|
||||
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{isBanking ? 'Bankkontoumsätze CSV hier ablegen oder klicken zum Auswählen' : 'DATEV Beleglinks CSV hier ablegen oder klicken zum Auswählen'}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Unterstützte Formate: .csv (Semikolon-getrennt)
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{!isBanking && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, mb: 1 }}>
|
||||
<InfoIcon sx={{ color: 'info.main', mt: '2px' }} />
|
||||
<Typography variant="subtitle1">Hinweise zum DATEV Beleglink-Upload</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" paragraph sx={{ color: 'text.secondary' }}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere, neque at efficitur
|
||||
blandit, sapien libero finibus nunc, a facilisis lacus arcu sed urna. Suspendisse potenti.
|
||||
Phasellus tincidunt, lorem in dictum lacinia, sem tortor ultrices risus, vitae porta odio
|
||||
mauris non neque. Sed vitae nibh dapibus, viverra velit nec, aliquet odio.
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph sx={{ color: 'text.secondary' }}>
|
||||
Cras lacinia, massa a sagittis placerat, enim dolor fermentum lectus, in pulvinar mi risus ut
|
||||
ipsum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis
|
||||
egestas. Mauris mattis lorem sit amet risus mattis volutpat. Proin sit amet hendrerit lectus.
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
borderRadius: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
display: 'block',
|
||||
mb: 1.5,
|
||||
overflow: 'hidden',
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="svg"
|
||||
viewBox="0 0 640 300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
sx={{ width: '100%', height: 'auto', display: 'block' }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style={{ stopColor: '#e3f2fd', stopOpacity: 1 }} />
|
||||
<stop offset="100%" style={{ stopColor: '#e8f5e9', stopOpacity: 1 }} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="640" height="300" fill="url(#grad)" />
|
||||
<rect x="20" y="20" width="600" height="60" rx="8" fill="#ffffff" stroke="#cfd8dc" />
|
||||
<circle cx="50" cy="50" r="12" fill="#81c784" />
|
||||
<rect x="75" y="38" width="200" height="12" rx="6" fill="#90caf9" />
|
||||
<rect x="75" y="56" width="140" height="10" rx="5" fill="#b0bec5" />
|
||||
|
||||
<rect x="20" y="100" width="600" height="160" rx="8" fill="#ffffff" stroke="#cfd8dc" />
|
||||
<rect x="40" y="120" width="160" height="20" rx="4" fill="#ffe082" />
|
||||
<rect x="40" y="150" width="260" height="12" rx="6" fill="#b0bec5" />
|
||||
<rect x="40" y="170" width="220" height="10" rx="5" fill="#eceff1" />
|
||||
<rect x="40" y="190" width="300" height="10" rx="5" fill="#eceff1" />
|
||||
|
||||
<rect x="330" y="120" width="270" height="120" rx="8" fill="#f1f8e9" stroke="#c5e1a5" />
|
||||
<rect x="350" y="140" width="230" height="12" rx="6" fill="#c5e1a5" />
|
||||
<rect x="350" y="160" width="180" height="10" rx="5" fill="#dcedc8" />
|
||||
<g>
|
||||
<line x1="350" y1="190" x2="580" y2="190" stroke="#aed581" strokeWidth="2" />
|
||||
<line x1="350" y1="200" x2="560" y2="200" stroke="#aed581" strokeWidth="2" />
|
||||
<line x1="350" y1="210" x2="520" y2="210" stroke="#aed581" strokeWidth="2" />
|
||||
</g>
|
||||
|
||||
<g fill="#90a4ae">
|
||||
<rect x="540" y="28" width="20" height="6" rx="3" />
|
||||
<rect x="565" y="28" width="20" height="6" rx="3" />
|
||||
<rect x="590" y="28" width="20" height="6" rx="3" />
|
||||
</g>
|
||||
|
||||
<text x="320" y="285" textAnchor="middle" fontFamily="Arial, Helvetica, sans-serif" fontSize="12" fill="#90a4ae">
|
||||
DATEV Beleglinks Beispiel – Platzhalter Illustration
|
||||
</text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ color: 'text.disabled' }}>
|
||||
Beispiel-Screenshot (SVG Platzhalter mit Shapes). Ersetzen Sie dieses Bild später durch die endgültige Anleitungsgrafik.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{currentFile && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Ausgewählte Datei:
|
||||
</Typography>
|
||||
<Chip label={currentFile.name} color="primary" />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{currentHeaders && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Erkannte Spalten ({currentHeaders.length}):
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{currentHeaders.slice(0, 10).map((header, index) => (
|
||||
<Chip key={index} label={header} size="small" variant="outlined" />
|
||||
))}
|
||||
{currentHeaders.length > 10 && (
|
||||
<Chip label={`+${currentHeaders.length - 10} weitere`} size="small" />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{currentData && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
{isBanking ? 'Gefundene Transaktionen' : 'Gefundene Beleglinks'}: {currentData.length}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Die Daten werden validiert und in die Datenbank importiert.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { open } = this.props;
|
||||
const {
|
||||
file,
|
||||
csvData,
|
||||
headers,
|
||||
importing,
|
||||
imported,
|
||||
importResult,
|
||||
error,
|
||||
dragOver
|
||||
const {
|
||||
activeTab,
|
||||
importing,
|
||||
imported,
|
||||
importResult,
|
||||
error,
|
||||
csvData,
|
||||
datevCsvData,
|
||||
} = this.state;
|
||||
|
||||
const isBanking = activeTab === IMPORT_TYPES.BANKING;
|
||||
const hasData = isBanking ? csvData : datevCsvData;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
@@ -211,82 +482,22 @@ class CSVImportDialog extends Component {
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
CSV Transaktionen Importieren
|
||||
</DialogTitle>
|
||||
<DialogTitle>CSV Import</DialogTitle>
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={this.handleTabChange}
|
||||
variant="fullWidth"
|
||||
sx={{ borderBottom: 1, borderColor: 'divider' }}
|
||||
>
|
||||
<Tab value={IMPORT_TYPES.BANKING} iconPosition="start" icon={<AccountIcon />} label="Banking Umsätze" />
|
||||
<Tab value={IMPORT_TYPES.DATEV_LINKS} iconPosition="start" icon={<LinkIcon />} label="DATEV Beleglinks" />
|
||||
</Tabs>
|
||||
|
||||
<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>
|
||||
)}
|
||||
{this.renderUploadPanel({ isBanking })}
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
@@ -298,13 +509,12 @@ class CSVImportDialog extends Component {
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<LinearProgress />
|
||||
<Typography variant="body2" sx={{ mt: 1, textAlign: 'center' }}>
|
||||
Importiere Transaktionen...
|
||||
{isBanking ? 'Importiere Transaktionen...' : 'Importiere DATEV Beleglinks...'}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
/* Import Success */
|
||||
<Box sx={{ textAlign: 'center', py: 2 }}>
|
||||
<SuccessIcon sx={{ fontSize: 64, color: 'success.main', mb: 2 }} />
|
||||
<Typography variant="h6" gutterBottom>
|
||||
@@ -314,7 +524,7 @@ class CSVImportDialog extends Component {
|
||||
{importResult && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
<strong>Importiert:</strong> {importResult.imported} Transaktionen
|
||||
<strong>Importiert:</strong> {importResult.imported} {isBanking ? 'Transaktionen' : 'Beleglinks'}
|
||||
</Typography>
|
||||
{importResult.errors > 0 && (
|
||||
<Typography variant="body1" color="warning.main">
|
||||
@@ -334,11 +544,11 @@ class CSVImportDialog extends Component {
|
||||
<Button onClick={this.handleClose} disabled={importing}>
|
||||
{imported ? 'Schließen' : 'Abbrechen'}
|
||||
</Button>
|
||||
{!imported && csvData && (
|
||||
{!imported && hasData && (
|
||||
<Button
|
||||
onClick={this.handleImport}
|
||||
variant="contained"
|
||||
disabled={importing || !csvData}
|
||||
disabled={importing || !hasData}
|
||||
startIcon={importing ? <CircularProgress size={16} /> : <UploadIcon />}
|
||||
>
|
||||
{importing ? 'Importiere...' : 'Importieren'}
|
||||
|
||||
Reference in New Issue
Block a user