import React, { Component } from 'react'; import { Button, Typography, Box, Alert, CircularProgress, LinearProgress, Chip, Tabs, Tab, Divider, Paper, } 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 CSVImportPanel extends Component { constructor(props) { super(props); this.state = { // 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(); } componentDidMount() { // Check if we should navigate to a specific tab if (this.props.targetTab) { this.setState({ activeTab: this.props.targetTab }); } } componentDidUpdate(prevProps) { // Handle targetTab changes if (this.props.targetTab !== prevProps.targetTab && this.props.targetTab) { this.setState({ activeTab: this.props.targetTab }); } } // 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, 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) { // route to active tab this.processFile(file, this.state.activeTab); } }; handleDragOver = (event) => { event.preventDefault(); this.setState({ dragOver: true }); }; handleDragLeave = () => { this.setState({ dragOver: false }); }; processFile = (file, type) => { if (!file.name.toLowerCase().endsWith('.csv')) { this.setState({ error: 'Bitte wählen Sie eine CSV-Datei aus' }); return; } const reader = new FileReader(); reader.onload = (e) => { try { const text = e.target.result; const { headers, dataRows } = this.parseCSV(text); if (type === IMPORT_TYPES.BANKING) { this.setState({ file, csvData: dataRows, headers, error: null, }); } 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 { 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; } this.setState({ importing: true, error: null }); try { 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}`, }; } 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, }); if (this.props.onImportSuccess) { this.props.onImportSuccess(result); } } else { 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', }); } }; handleClose = () => { this.setState({ // 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, }); }; 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 ( <> {isBanking ? ( ) : ( )} {isBanking ? 'Bankkontoumsätze CSV hier ablegen oder klicken zum Auswählen' : 'DATEV Beleglinks CSV hier ablegen oder klicken zum Auswählen'} Unterstützte Formate: .csv (Semikolon-getrennt) {currentFile && ( Ausgewählte Datei: )} {currentHeaders && ( Erkannte Spalten ({currentHeaders.length}): {currentHeaders.slice(0, 10).map((header, index) => ( ))} {currentHeaders.length > 10 && ( )} )} {currentData && ( {isBanking ? 'Gefundene Transaktionen' : 'Gefundene Beleglinks'}: {currentData.length} Die Daten werden validiert und in die Datenbank importiert. )} ); }; render() { const { activeTab, importing, imported, importResult, error, csvData, datevCsvData, } = this.state; const isBanking = activeTab === IMPORT_TYPES.BANKING; const hasData = isBanking ? csvData : datevCsvData; return ( CSV Import } label="Banking Umsätze" /> } label="DATEV Beleglinks" /> {!imported ? ( <> {this.renderUploadPanel({ isBanking })} {error && ( {error} )} {importing && ( {isBanking ? 'Importiere Transaktionen...' : 'Importiere DATEV Beleglinks...'} )} ) : ( Import erfolgreich abgeschlossen! {importResult && ( Hinzugefügt: {importResult.imported} {isBanking ? 'Transaktionen' : 'Datevlinks'} {importResult.skipped > 0 && ( Übersprungen: {importResult.skipped} Zeilen (bereits vorhanden, unbekanntes Format, etc.) )} {importResult.errors > 0 && ( Fehler: {importResult.errors} Zeilen konnten nicht verarbeitet werden. )} {importResult.message && ( {importResult.message} )} Batch-ID: {importResult.batchId} )} )} {!imported && hasData && ( )} {imported && ( )} ); } } export default CSVImportPanel;