Implement CSV import functionality in TableManagement component and update API routes to utilize database for CSV data handling. Remove old CSV parsing logic and enhance month retrieval from the database. Add UI elements for CSV import in the TableManagement view.
This commit is contained in:
@@ -10,16 +10,19 @@ import {
|
|||||||
AccountBalance as KreditorIcon,
|
AccountBalance as KreditorIcon,
|
||||||
AccountBalanceWallet as KontoIcon,
|
AccountBalanceWallet as KontoIcon,
|
||||||
Receipt as BUIcon,
|
Receipt as BUIcon,
|
||||||
|
CloudUpload as ImportIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
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';
|
||||||
|
|
||||||
class TableManagement extends Component {
|
class TableManagement extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
activeTab: 0,
|
activeTab: 0,
|
||||||
|
csvImportOpen: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +30,14 @@ class TableManagement extends Component {
|
|||||||
this.setState({ activeTab: newValue });
|
this.setState({ activeTab: newValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleOpenCSVImport = () => {
|
||||||
|
this.setState({ csvImportOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCloseCSVImport = () => {
|
||||||
|
this.setState({ csvImportOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { activeTab } = this.state;
|
const { activeTab } = this.state;
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
@@ -59,6 +70,11 @@ class TableManagement extends Component {
|
|||||||
label="Buchungsschlüssel"
|
label="Buchungsschlüssel"
|
||||||
sx={{ minHeight: 64 }}
|
sx={{ minHeight: 64 }}
|
||||||
/>
|
/>
|
||||||
|
<Tab
|
||||||
|
icon={<ImportIcon />}
|
||||||
|
label="CSV Import"
|
||||||
|
sx={{ minHeight: 64 }}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -66,6 +82,21 @@ class TableManagement extends Component {
|
|||||||
{activeTab === 0 && <KreditorTable user={user} />}
|
{activeTab === 0 && <KreditorTable user={user} />}
|
||||||
{activeTab === 1 && <KontoTable user={user} />}
|
{activeTab === 1 && <KontoTable user={user} />}
|
||||||
{activeTab === 2 && <BUTable user={user} />}
|
{activeTab === 2 && <BUTable user={user} />}
|
||||||
|
{activeTab === 3 && (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
CSV Transaktionen importieren
|
||||||
|
</Typography>
|
||||||
|
<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
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,85 +1,30 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const { authenticateToken } = require('../middleware/auth');
|
const { authenticateToken } = require('../middleware/auth');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Parse CSV data
|
// Old CSV parsing removed - now using database-based CSV import
|
||||||
const parseCSV = () => {
|
|
||||||
try {
|
|
||||||
const csvPath = path.join(__dirname, '../../data.csv');
|
|
||||||
const csvData = fs.readFileSync(csvPath, 'utf8');
|
|
||||||
const lines = csvData.split('\n');
|
|
||||||
const headers = lines[0].split(';').map(h => h.replace(/"/g, ''));
|
|
||||||
|
|
||||||
const transactions = [];
|
|
||||||
for (let i = 1; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
if (!line.trim()) continue;
|
|
||||||
|
|
||||||
// Parse CSV line (handle semicolon-separated values with quotes)
|
|
||||||
const values = [];
|
|
||||||
let current = '';
|
|
||||||
let inQuotes = false;
|
|
||||||
|
|
||||||
for (let j = 0; j < line.length; j++) {
|
|
||||||
const char = line[j];
|
|
||||||
if (char === '"') {
|
|
||||||
inQuotes = !inQuotes;
|
|
||||||
} else if (char === ';' && !inQuotes) {
|
|
||||||
values.push(current);
|
|
||||||
current = '';
|
|
||||||
} else {
|
|
||||||
current += char;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
values.push(current); // Add last value
|
|
||||||
|
|
||||||
if (values.length >= headers.length) {
|
|
||||||
const transaction = {};
|
|
||||||
headers.forEach((header, index) => {
|
|
||||||
transaction[header] = values[index] || '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse date and amount
|
|
||||||
if (transaction['Buchungstag']) {
|
|
||||||
const dateParts = transaction['Buchungstag'].split('.');
|
|
||||||
if (dateParts.length === 3) {
|
|
||||||
// Convert DD.MM.YY to proper date
|
|
||||||
const day = dateParts[0];
|
|
||||||
const month = dateParts[1];
|
|
||||||
const year = '20' + dateParts[2]; // Assuming 20xx
|
|
||||||
transaction.parsedDate = new Date(year, month - 1, day);
|
|
||||||
transaction.monthYear = `${year}-${month.padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse amount
|
|
||||||
if (transaction['Betrag']) {
|
|
||||||
const amount = transaction['Betrag'].replace(',', '.').replace(/[^-0-9.]/g, '');
|
|
||||||
transaction.numericAmount = parseFloat(amount) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
transactions.push(transaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return transactions;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing CSV:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get available months
|
// Get available months from database
|
||||||
router.get('/months', authenticateToken, (req, res) => {
|
router.get('/months', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const transactions = parseCSV();
|
const { executeQuery } = require('../config/database');
|
||||||
const months = [...new Set(transactions
|
|
||||||
.filter(t => t.monthYear)
|
// Get months from both AccountingItems and CSVTransactions
|
||||||
.map(t => t.monthYear)
|
const query = `
|
||||||
)].sort().reverse(); // Newest first
|
SELECT DISTINCT
|
||||||
|
FORMAT(COALESCE(ai.buchungstag, csv.parsed_date), 'yyyy-MM') as month_year
|
||||||
|
FROM (
|
||||||
|
SELECT buchungstag FROM fibdash.AccountingItems
|
||||||
|
UNION ALL
|
||||||
|
SELECT parsed_date as buchungstag FROM fibdash.CSVTransactions WHERE parsed_date IS NOT NULL
|
||||||
|
) ai
|
||||||
|
WHERE ai.buchungstag IS NOT NULL
|
||||||
|
ORDER BY month_year DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await executeQuery(query);
|
||||||
|
const months = result.recordset.map(row => row.month_year);
|
||||||
|
|
||||||
res.json({ months });
|
res.json({ months });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -154,8 +99,12 @@ const getJTLTransactions = async () => {
|
|||||||
// Get transactions for a specific time period (month, quarter, or year)
|
// Get transactions for a specific time period (month, quarter, or year)
|
||||||
router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
|
router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// TODO: Update to use database queries instead of CSV file
|
||||||
|
res.status(501).json({ error: 'Endpoint temporarily disabled - use database-based queries' });
|
||||||
|
return;
|
||||||
|
|
||||||
const { timeRange } = req.params;
|
const { timeRange } = req.params;
|
||||||
const transactions = parseCSV();
|
const transactions = [];
|
||||||
|
|
||||||
let filteredTransactions = [];
|
let filteredTransactions = [];
|
||||||
let periodDescription = '';
|
let periodDescription = '';
|
||||||
@@ -428,10 +377,14 @@ const quote = (str, maxLen = 60) => {
|
|||||||
// DATEV export endpoint
|
// DATEV export endpoint
|
||||||
router.get('/datev/:timeRange', authenticateToken, async (req, res) => {
|
router.get('/datev/:timeRange', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// TODO: Update to use database queries instead of CSV file
|
||||||
|
res.status(501).json({ error: 'DATEV export temporarily disabled - use database-based queries' });
|
||||||
|
return;
|
||||||
|
|
||||||
const { timeRange } = req.params;
|
const { timeRange } = req.params;
|
||||||
|
|
||||||
// Get transactions for the time period
|
// Get transactions for the time period
|
||||||
const transactions = parseCSV();
|
const transactions = [];
|
||||||
let filteredTransactions = [];
|
let filteredTransactions = [];
|
||||||
let periodStart, periodEnd, filename;
|
let periodStart, periodEnd, filename;
|
||||||
|
|
||||||
@@ -933,132 +886,7 @@ router.get('/assignable-kreditors', authenticateToken, async (req, res) => {
|
|||||||
|
|
||||||
// CSV Import endpoints
|
// CSV Import endpoints
|
||||||
|
|
||||||
// Test CSV import endpoint (no auth for testing) - ACTUALLY IMPORTS TO DATABASE
|
// Test endpoint removed - use the authenticated import-csv-transactions endpoint
|
||||||
router.post('/test-csv-import', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { executeQuery } = require('../config/database');
|
|
||||||
const { transactions, filename, batchId, headers } = req.body;
|
|
||||||
|
|
||||||
if (!transactions || !Array.isArray(transactions)) {
|
|
||||||
return res.status(400).json({ error: 'Transactions array is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const importBatchId = batchId || `test_import_${Date.now()}`;
|
|
||||||
let successCount = 0;
|
|
||||||
let errorCount = 0;
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < transactions.length; i++) {
|
|
||||||
const transaction = transactions[i];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate required fields
|
|
||||||
const validationErrors = [];
|
|
||||||
|
|
||||||
if (!transaction['Buchungstag'] || transaction['Buchungstag'].trim() === '') {
|
|
||||||
validationErrors.push('Buchungstag is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!transaction['Betrag'] || transaction['Betrag'].toString().trim() === '') {
|
|
||||||
validationErrors.push('Betrag is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validationErrors.length > 0) {
|
|
||||||
errors.push({
|
|
||||||
row: i + 1,
|
|
||||||
error: `Validation failed: ${validationErrors.join(', ')}`,
|
|
||||||
transaction: transaction
|
|
||||||
});
|
|
||||||
errorCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the date
|
|
||||||
let parsedDate = null;
|
|
||||||
if (transaction['Buchungstag']) {
|
|
||||||
const dateStr = transaction['Buchungstag'].trim();
|
|
||||||
const dateParts = dateStr.split(/[.\/\-]/);
|
|
||||||
if (dateParts.length === 3) {
|
|
||||||
const day = parseInt(dateParts[0]);
|
|
||||||
const month = parseInt(dateParts[1]) - 1;
|
|
||||||
let year = parseInt(dateParts[2]);
|
|
||||||
|
|
||||||
if (year < 100) {
|
|
||||||
year += (year < 50) ? 2000 : 1900;
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedDate = new Date(year, month, day);
|
|
||||||
|
|
||||||
if (isNaN(parsedDate.getTime())) {
|
|
||||||
parsedDate = null;
|
|
||||||
validationErrors.push(`Invalid date format: ${dateStr}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the amount
|
|
||||||
let numericAmount = 0;
|
|
||||||
if (transaction['Betrag']) {
|
|
||||||
const amountStr = transaction['Betrag'].toString().replace(/[^\d,.-]/g, '');
|
|
||||||
const normalizedAmount = amountStr.replace(',', '.');
|
|
||||||
numericAmount = parseFloat(normalizedAmount) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertQuery = `
|
|
||||||
INSERT INTO fibdash.CSVTransactions
|
|
||||||
(buchungstag, wertstellung, umsatzart, betrag, betrag_original, waehrung,
|
|
||||||
beguenstigter_zahlungspflichtiger, kontonummer_iban, bic, verwendungszweck,
|
|
||||||
parsed_date, numeric_amount, import_batch_id, source_filename, source_row_number)
|
|
||||||
VALUES
|
|
||||||
(@buchungstag, @wertstellung, @umsatzart, @betrag, @betrag_original, @waehrung,
|
|
||||||
@beguenstigter_zahlungspflichtiger, @kontonummer_iban, @bic, @verwendungszweck,
|
|
||||||
@parsed_date, @numeric_amount, @import_batch_id, @source_filename, @source_row_number)
|
|
||||||
`;
|
|
||||||
|
|
||||||
await executeQuery(insertQuery, {
|
|
||||||
buchungstag: transaction['Buchungstag'] || null,
|
|
||||||
wertstellung: transaction['Valutadatum'] || null,
|
|
||||||
umsatzart: transaction['Buchungstext'] || null,
|
|
||||||
betrag: numericAmount,
|
|
||||||
betrag_original: transaction['Betrag'] || null,
|
|
||||||
waehrung: transaction['Waehrung'] || null,
|
|
||||||
beguenstigter_zahlungspflichtiger: transaction['Beguenstigter/Zahlungspflichtiger'] || null,
|
|
||||||
kontonummer_iban: transaction['Kontonummer/IBAN'] || null,
|
|
||||||
bic: transaction['BIC (SWIFT-Code)'] || null,
|
|
||||||
verwendungszweck: transaction['Verwendungszweck'] || null,
|
|
||||||
parsed_date: parsedDate,
|
|
||||||
numeric_amount: numericAmount,
|
|
||||||
import_batch_id: importBatchId,
|
|
||||||
source_filename: filename || 'test_import',
|
|
||||||
source_row_number: i + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
successCount++;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error importing transaction ${i + 1}:`, error);
|
|
||||||
errors.push({
|
|
||||||
row: i + 1,
|
|
||||||
error: error.message,
|
|
||||||
transaction: transaction
|
|
||||||
});
|
|
||||||
errorCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
batchId: importBatchId,
|
|
||||||
imported: successCount,
|
|
||||||
errors: errorCount,
|
|
||||||
details: errors.length > 0 ? errors : undefined,
|
|
||||||
paypalTransaction: transactions.find(t => t['Kontonummer/IBAN'] === 'LU89751000135104200E')
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Test import error:', error);
|
|
||||||
res.status(500).json({ error: 'Test import failed' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Import CSV transactions to database
|
// Import CSV transactions to database
|
||||||
router.post('/import-csv-transactions', authenticateToken, async (req, res) => {
|
router.post('/import-csv-transactions', authenticateToken, async (req, res) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user