From d60da0a7aae89a34c5fe1b01d8af9c289bdf479d Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Tue, 5 Aug 2025 10:17:54 +0200 Subject: [PATCH] Refactor CSVImportDialog to CSVImportPanel and enhance UI components - Renamed CSVImportDialog component to CSVImportPanel for clarity. - Replaced Dialog with Paper component for improved layout. - Removed unused code and comments to streamline the component. - Updated import result messages for better user feedback. - Enhanced button styles and layout for a more user-friendly interface. - Added new API route for importing DATEV Beleglinks to the database, including validation and error handling. --- client/src/components/CSVImportDialog.js | 160 +++++------------- client/src/components/TableManagement.js | 6 +- src/routes/data/csvImport.js | 199 +++++++++++++++++++++++ 3 files changed, 243 insertions(+), 122 deletions(-) diff --git a/client/src/components/CSVImportDialog.js b/client/src/components/CSVImportDialog.js index 177bffd..76becc4 100644 --- a/client/src/components/CSVImportDialog.js +++ b/client/src/components/CSVImportDialog.js @@ -1,9 +1,5 @@ import React, { Component } from 'react'; import { - Dialog, - DialogTitle, - DialogContent, - DialogActions, Button, Typography, Box, @@ -14,6 +10,7 @@ import { Tabs, Tab, Divider, + Paper, } from '@mui/material'; import { CloudUpload as UploadIcon, @@ -30,7 +27,7 @@ const IMPORT_TYPES = { DATEV_LINKS: 'DATEV_LINKS', }; -class CSVImportDialog extends Component { +class CSVImportPanel extends Component { constructor(props) { super(props); this.state = { @@ -276,10 +273,6 @@ class CSVImportDialog extends Component { datevCsvData: null, datevHeaders: null, }); - - if (this.props.onClose) { - this.props.onClose(); - } }; renderUploadPanel = ({ isBanking }) => { @@ -341,86 +334,6 @@ class CSVImportDialog extends Component { - {!isBanking && ( - - - - - Hinweise zum DATEV Beleglink-Upload - - - 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. - - - 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. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DATEV Beleglinks Beispiel – Platzhalter Illustration - - - - - Beispiel-Screenshot (SVG Platzhalter mit Shapes). Ersetzen Sie dieses Bild später durch die endgültige Anleitungsgrafik. - - - )} - {currentFile && ( @@ -461,7 +374,6 @@ class CSVImportDialog extends Component { }; render() { - const { open } = this.props; const { activeTab, importing, @@ -476,25 +388,22 @@ class CSVImportDialog extends Component { const hasData = isBanking ? csvData : datevCsvData; return ( - - CSV Import + + + CSV Import + } label="Banking Umsätze" /> } label="DATEV Beleglinks" /> - + {!imported ? ( <> {this.renderUploadPanel({ isBanking })} @@ -524,11 +433,21 @@ class CSVImportDialog extends Component { {importResult && ( - Importiert: {importResult.imported} {isBanking ? 'Transaktionen' : 'Beleglinks'} + 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 übersprungen + Fehler: {importResult.errors} Zeilen konnten nicht verarbeitet werden + + )} + {importResult.message && ( + + {importResult.message} )} @@ -538,26 +457,31 @@ class CSVImportDialog extends Component { )} )} - - - - {!imported && hasData && ( - + + + )} - - + + {imported && ( + + + + )} + + ); } } -export default CSVImportDialog; \ No newline at end of file +export default CSVImportPanel; \ No newline at end of file diff --git a/client/src/components/TableManagement.js b/client/src/components/TableManagement.js index 0d1d2ba..a936e91 100644 --- a/client/src/components/TableManagement.js +++ b/client/src/components/TableManagement.js @@ -15,7 +15,7 @@ import { import KreditorTable from './admin/KreditorTable'; import KontoTable from './admin/KontoTable'; import BUTable from './admin/BUTable'; -import CSVImportDialog from './CSVImportDialog'; +import CSVImportPanel from './CSVImportDialog'; class TableManagement extends Component { constructor(props) { @@ -90,9 +90,7 @@ class TableManagement extends Component { Hier können Sie CSV-Dateien von Ihrer Bank importieren. Die Daten werden in die Datenbank gespeichert und können dann Banking-Konten zugeordnet werden. - {}} // Always open in this tab + diff --git a/src/routes/data/csvImport.js b/src/routes/data/csvImport.js index be8f4ca..00965f4 100644 --- a/src/routes/data/csvImport.js +++ b/src/routes/data/csvImport.js @@ -370,4 +370,203 @@ router.get('/csv-import-batches', authenticateToken, async (req, res) => { } }); +// Import DATEV Beleglinks to database +router.post('/import-datev-beleglinks', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../../config/database'); + const { beleglinks, filename, batchId, headers } = req.body; + + if (!beleglinks || !Array.isArray(beleglinks)) { + return res.status(400).json({ error: 'Beleglinks array is required' }); + } + + // Expected DATEV CSV headers from the example + const expectedHeaders = [ + 'Belegart', 'Geschäftspartner-Name', 'Geschäftspartner-Konto', 'Rechnungsbetrag', 'WKZ', + 'Rechnungs-Nr.', 'Interne Re.-Nr.', 'Rechnungsdatum', 'BU', 'Konto', 'Konto-Bezeichnung', + 'Ware/Leistung', 'Zahlungszuordnung', 'Kontoumsatzzuordnung', 'Gebucht', 'Festgeschrieben', + 'Kopie', 'Eingangsdatum', 'Bezahlt', 'BezahltAm', 'Geschäftspartner-Ort', 'Skonto-Betrag 1', + 'Fällig mit Skonto 1', 'Skonto 1 in %', 'Skonto-Betrag 2', 'Fällig mit Skonto 2', + 'Skonto 2 in %', 'Fällig ohne Skonto', 'Steuer in %', 'USt-IdNr.', 'Kunden-Nr.', + 'KOST 1', 'KOST 2', 'KOST-Menge', 'Kurs', 'Nachricht', 'Freier Text', 'IBAN', 'BIC', + 'Bankkonto-Nr.', 'BLZ', 'Notiz', 'Land', 'Personalnummer', 'Nachname', 'Vorname', + 'Belegkategorie', 'Bezeichnung', 'Abrechnungsmonat', 'Gültig bis', 'Prüfungsrelevant', + 'Ablageort', 'Belegtyp', 'Herkunft', 'Leistungsdatum', 'Buchungstext', 'Beleg-ID', + 'Zahlungsbedingung', 'Geheftet', 'Gegenkonto', 'keine Überweisung/Lastschrift erstellen', + 'Aufgeteilt', 'Bereitgestellt', 'Freigegeben', 'FreigegebenAm', 'Erweiterte Belegdaten fehlen', + 'Periode fehlt', 'Rechnungsdaten beim Import fehlen' + ]; + + if (beleglinks.length === 0) { + return res.status(400).json({ error: 'No beleglink data found' }); + } + + const importBatchId = batchId || 'datev_import_' + Date.now(); + let successCount = 0; + let errorCount = 0; + let updateCount = 0; + let insertCount = 0; + let skippedCount = 0; + const errors = []; + + for (let i = 0; i < beleglinks.length; i++) { + const beleglink = beleglinks[i]; + + try { + // Skip empty rows or rows without Beleg-ID + const belegId = beleglink['Beleg-ID']; + if (!belegId || belegId.trim() === '') { + console.log(`Skipping row ${i + 1}: No Beleg-ID found`); + skippedCount++; + continue; + } + + const validationErrors = []; + + // Parse amount if available + let numericAmount = null; + if (beleglink['Rechnungsbetrag']) { + const amountStr = beleglink['Rechnungsbetrag'].toString().replace(/[^\d,.-]/g, ''); + const normalizedAmount = amountStr.replace(',', '.'); + numericAmount = parseFloat(normalizedAmount) || null; + } + + // Parse date if available + let parsedDate = null; + if (beleglink['Rechnungsdatum']) { + const dateStr = beleglink['Rechnungsdatum'].trim(); + const dateParts = dateStr.split(/[.\/\-]/); + if (dateParts.length === 3) { + const day = parseInt(dateParts[0], 10); + const month = parseInt(dateParts[1], 10) - 1; + let year = parseInt(dateParts[2], 10); + + if (year < 100) { + year += (year < 50) ? 2000 : 1900; + } + + parsedDate = new Date(year, month, day); + + if (isNaN(parsedDate.getTime())) { + parsedDate = null; + } + } + } + + // First, check if a record with this datevlink already exists + const checkExistingDatevLink = ` + SELECT kUmsatzBeleg FROM eazybusiness.dbo.tUmsatzBeleg WHERE datevlink = @datevlink + `; + + const existingDatevLink = await executeQuery(checkExistingDatevLink, { datevlink: belegId }); + + if (existingDatevLink.recordset.length > 0) { + // Record with this datevlink already exists - skip + console.log(`Datevlink already exists, skipping: ${belegId}`); + skippedCount++; + continue; + } + + // Extract key from filename in 'Herkunft' column + // Examples: "Rechnung146.pdf" -> key 146 for tRechnung + // "UmsatzBeleg192.pdf" -> key 192 for tUmsatzBeleg + const herkunft = beleglink['Herkunft']; + if (!herkunft || herkunft.trim() === '') { + console.log(`Skipping row ${i + 1}: No filename in Herkunft column`); + skippedCount++; + continue; + } + + // Extract the key from filename patterns + let matchFound = false; + + // Pattern: UmsatzBeleg{key}.pdf -> match with tUmsatzBeleg.kUmsatzBeleg + const umsatzBelegMatch = herkunft.match(/UmsatzBeleg(\d+)\.pdf/i); + if (umsatzBelegMatch) { + const kUmsatzBeleg = parseInt(umsatzBelegMatch[1], 10); + + const updateQuery = ` + UPDATE eazybusiness.dbo.tUmsatzBeleg + SET datevlink = @datevlink + WHERE kUmsatzBeleg = @kUmsatzBeleg AND (datevlink IS NULL OR datevlink = '') + `; + + const updateResult = await executeQuery(updateQuery, { + datevlink: belegId, + kUmsatzBeleg: kUmsatzBeleg + }); + + if (updateResult.rowsAffected && updateResult.rowsAffected[0] > 0) { + updateCount++; + console.log(`Added datevlink ${belegId} to tUmsatzBeleg.kUmsatzBeleg: ${kUmsatzBeleg}`); + matchFound = true; + } else { + console.log(`Skipping row ${i + 1}: UmsatzBeleg ${kUmsatzBeleg} nicht gefunden oder datevlink bereits gesetzt`); + skippedCount++; + } + } + + // Pattern: Rechnung{key}.pdf -> match with tPdfObjekt.kPdfObjekt + const rechnungMatch = herkunft.match(/Rechnung(\d+)\.pdf/i); + if (!matchFound && rechnungMatch) { + const kPdfObjekt = parseInt(rechnungMatch[1], 10); + + const updateQuery = ` + UPDATE eazybusiness.dbo.tPdfObjekt + SET datevlink = @datevlink + WHERE kPdfObjekt = @kPdfObjekt AND (datevlink IS NULL OR datevlink = '') + `; + + const updateResult = await executeQuery(updateQuery, { + datevlink: belegId, + kPdfObjekt: kPdfObjekt + }); + + if (updateResult.rowsAffected && updateResult.rowsAffected[0] > 0) { + updateCount++; + console.log(`Added datevlink ${belegId} to tPdfObjekt.kPdfObjekt: ${kPdfObjekt}`); + matchFound = true; + } else { + console.log(`Skipping row ${i + 1}: PdfObjekt ${kPdfObjekt} nicht gefunden oder datevlink bereits gesetzt`); + skippedCount++; + } + } + + if (!matchFound) { + console.log(`Skipping row ${i + 1}: Unbekanntes Dateiformat '${herkunft}' (erwartet: UmsatzBeleg{key}.pdf oder Rechnung{key}.pdf)`); + skippedCount++; + continue; + } + + successCount++; + } catch (error) { + console.error('Error processing beleglink ' + (i + 1) + ':', error); + errors.push({ + row: i + 1, + error: error.message, + beleglink: beleglink + }); + errorCount++; + } + } + + res.json({ + success: true, + batchId: importBatchId, + imported: updateCount, // Number of datevlinks actually added/updated + processed: successCount, + updated: updateCount, + inserted: insertCount, + skipped: skippedCount, // Records skipped (existing datevlinks) + errors: errorCount, // Only actual errors, not skipped records + details: errors.length > 0 ? errors : undefined, + message: `${updateCount} datevlinks hinzugefügt, ${skippedCount} bereits vorhanden, ${errorCount} Fehler` + }); + + } catch (error) { + console.error('Error importing DATEV beleglinks:', error); + res.status(500).json({ error: 'Failed to import DATEV beleglinks' }); + } +}); + module.exports = router; \ No newline at end of file