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.
This commit is contained in:
@@ -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 {
|
||||
</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>
|
||||
@@ -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 (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={!importing ? this.handleClose : undefined}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>CSV Import</DialogTitle>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
CSV Import
|
||||
</Typography>
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={this.handleTabChange}
|
||||
variant="fullWidth"
|
||||
sx={{ borderBottom: 1, borderColor: 'divider' }}
|
||||
sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}
|
||||
>
|
||||
<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>
|
||||
<Box>
|
||||
{!imported ? (
|
||||
<>
|
||||
{this.renderUploadPanel({ isBanking })}
|
||||
@@ -524,11 +433,21 @@ class CSVImportDialog extends Component {
|
||||
{importResult && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
<strong>Importiert:</strong> {importResult.imported} {isBanking ? 'Transaktionen' : 'Beleglinks'}
|
||||
<strong>Hinzugefügt:</strong> {importResult.imported} {isBanking ? 'Transaktionen' : 'Datevlinks'}
|
||||
</Typography>
|
||||
{importResult.skipped > 0 && (
|
||||
<Typography variant="body1" color="info.main">
|
||||
<strong>Übersprungen:</strong> {importResult.skipped} Zeilen (bereits vorhanden, unbekanntes Format, etc.)
|
||||
</Typography>
|
||||
)}
|
||||
{importResult.errors > 0 && (
|
||||
<Typography variant="body1" color="warning.main">
|
||||
<strong>Fehler:</strong> {importResult.errors} Zeilen übersprungen
|
||||
<strong>Fehler:</strong> {importResult.errors} Zeilen konnten nicht verarbeitet werden
|
||||
</Typography>
|
||||
)}
|
||||
{importResult.message && (
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||
{importResult.message}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||
@@ -538,26 +457,31 @@ class CSVImportDialog extends Component {
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={this.handleClose} disabled={importing}>
|
||||
{imported ? 'Schließen' : 'Abbrechen'}
|
||||
</Button>
|
||||
{!imported && hasData && (
|
||||
<Button
|
||||
onClick={this.handleImport}
|
||||
variant="contained"
|
||||
disabled={importing || !hasData}
|
||||
startIcon={importing ? <CircularProgress size={16} /> : <UploadIcon />}
|
||||
>
|
||||
{importing ? 'Importiere...' : 'Importieren'}
|
||||
</Button>
|
||||
<Box sx={{ mt: 3, textAlign: 'center' }}>
|
||||
<Button
|
||||
onClick={this.handleImport}
|
||||
variant="contained"
|
||||
size="large"
|
||||
disabled={importing || !hasData}
|
||||
startIcon={importing ? <CircularProgress size={16} /> : <UploadIcon />}
|
||||
>
|
||||
{importing ? 'Importiere...' : 'Importieren'}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{imported && (
|
||||
<Box sx={{ mt: 3, textAlign: 'center' }}>
|
||||
<Button onClick={this.handleClose} variant="outlined" size="large">
|
||||
Neuer Import
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CSVImportDialog;
|
||||
export default CSVImportPanel;
|
||||
@@ -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 {
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
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.
|
||||
</Typography>
|
||||
<CSVImportDialog
|
||||
open={true}
|
||||
onClose={() => {}} // Always open in this tab
|
||||
<CSVImportPanel
|
||||
user={user}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user