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:
sebseb7
2025-08-05 10:17:54 +02:00
parent 46c9e9b97d
commit d60da0a7aa
3 changed files with 243 additions and 122 deletions

View File

@@ -1,9 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button, Button,
Typography, Typography,
Box, Box,
@@ -14,6 +10,7 @@ import {
Tabs, Tabs,
Tab, Tab,
Divider, Divider,
Paper,
} from '@mui/material'; } from '@mui/material';
import { import {
CloudUpload as UploadIcon, CloudUpload as UploadIcon,
@@ -30,7 +27,7 @@ const IMPORT_TYPES = {
DATEV_LINKS: 'DATEV_LINKS', DATEV_LINKS: 'DATEV_LINKS',
}; };
class CSVImportDialog extends Component { class CSVImportPanel extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@@ -276,10 +273,6 @@ class CSVImportDialog extends Component {
datevCsvData: null, datevCsvData: null,
datevHeaders: null, datevHeaders: null,
}); });
if (this.props.onClose) {
this.props.onClose();
}
}; };
renderUploadPanel = ({ isBanking }) => { renderUploadPanel = ({ isBanking }) => {
@@ -341,86 +334,6 @@ class CSVImportDialog extends Component {
</Typography> </Typography>
</Box> </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 && ( {currentFile && (
<Box sx={{ mb: 2 }}> <Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom> <Typography variant="subtitle2" gutterBottom>
@@ -461,7 +374,6 @@ class CSVImportDialog extends Component {
}; };
render() { render() {
const { open } = this.props;
const { const {
activeTab, activeTab,
importing, importing,
@@ -476,25 +388,22 @@ class CSVImportDialog extends Component {
const hasData = isBanking ? csvData : datevCsvData; const hasData = isBanking ? csvData : datevCsvData;
return ( return (
<Dialog <Paper sx={{ p: 3 }}>
open={open} <Typography variant="h5" gutterBottom>
onClose={!importing ? this.handleClose : undefined} CSV Import
maxWidth="md" </Typography>
fullWidth
>
<DialogTitle>CSV Import</DialogTitle>
<Tabs <Tabs
value={activeTab} value={activeTab}
onChange={this.handleTabChange} onChange={this.handleTabChange}
variant="fullWidth" 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.BANKING} iconPosition="start" icon={<AccountIcon />} label="Banking Umsätze" />
<Tab value={IMPORT_TYPES.DATEV_LINKS} iconPosition="start" icon={<LinkIcon />} label="DATEV Beleglinks" /> <Tab value={IMPORT_TYPES.DATEV_LINKS} iconPosition="start" icon={<LinkIcon />} label="DATEV Beleglinks" />
</Tabs> </Tabs>
<DialogContent> <Box>
{!imported ? ( {!imported ? (
<> <>
{this.renderUploadPanel({ isBanking })} {this.renderUploadPanel({ isBanking })}
@@ -524,11 +433,21 @@ class CSVImportDialog extends Component {
{importResult && ( {importResult && (
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<Typography variant="body1" gutterBottom> <Typography variant="body1" gutterBottom>
<strong>Importiert:</strong> {importResult.imported} {isBanking ? 'Transaktionen' : 'Beleglinks'} <strong>Hinzugefügt:</strong> {importResult.imported} {isBanking ? 'Transaktionen' : 'Datevlinks'}
</Typography> </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 && ( {importResult.errors > 0 && (
<Typography variant="body1" color="warning.main"> <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>
)} )}
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}> <Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
@@ -538,26 +457,31 @@ class CSVImportDialog extends Component {
)} )}
</Box> </Box>
)} )}
</DialogContent>
<DialogActions>
<Button onClick={this.handleClose} disabled={importing}>
{imported ? 'Schließen' : 'Abbrechen'}
</Button>
{!imported && hasData && ( {!imported && hasData && (
<Button <Box sx={{ mt: 3, textAlign: 'center' }}>
onClick={this.handleImport} <Button
variant="contained" onClick={this.handleImport}
disabled={importing || !hasData} variant="contained"
startIcon={importing ? <CircularProgress size={16} /> : <UploadIcon />} size="large"
> disabled={importing || !hasData}
{importing ? 'Importiere...' : 'Importieren'} startIcon={importing ? <CircularProgress size={16} /> : <UploadIcon />}
</Button> >
{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;

View File

@@ -15,7 +15,7 @@ import {
import KreditorTable from './admin/KreditorTable'; import KreditorTable from './admin/KreditorTable';
import KontoTable from './admin/KontoTable'; import KontoTable from './admin/KontoTable';
import BUTable from './admin/BUTable'; import BUTable from './admin/BUTable';
import CSVImportDialog from './CSVImportDialog'; import CSVImportPanel from './CSVImportDialog';
class TableManagement extends Component { class TableManagement extends Component {
constructor(props) { constructor(props) {
@@ -90,9 +90,7 @@ class TableManagement extends Component {
<Typography variant="body2" color="text.secondary" paragraph> <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. 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> </Typography>
<CSVImportDialog <CSVImportPanel
open={true}
onClose={() => {}} // Always open in this tab
user={user} user={user}
/> />
</Box> </Box>

View File

@@ -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; module.exports = router;