Enhance webpack configuration for improved performance and development experience. Add filesystem caching and watch options. Update KreditorSelector to handle prefilled data and improve state management. Refactor TransactionsTable to manage focus during dialog interactions. Update admin tables to manage focus restoration and improve dialog handling. Implement IBAN filtering in IbanSelectionFilter and enhance document rendering with Kreditor information. Update SQL schema to allow multiple IBANs for the same Kreditor and adjust API routes for better data handling.
This commit is contained in:
@@ -8,6 +8,7 @@ GO
|
||||
|
||||
|
||||
-- Create Kreditor table
|
||||
-- Multiple IBANs can have the same kreditor name and kreditorId
|
||||
CREATE TABLE fibdash.Kreditor (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
iban NVARCHAR(34) NOT NULL,
|
||||
@@ -15,9 +16,10 @@ CREATE TABLE fibdash.Kreditor (
|
||||
kreditorId NVARCHAR(50) NOT NULL
|
||||
);
|
||||
|
||||
-- Ensure kreditorId is unique to support FK references
|
||||
-- Create unique index on IBAN to prevent duplicate IBANs
|
||||
-- but allow same kreditorId and name for multiple IBANs
|
||||
ALTER TABLE fibdash.Kreditor
|
||||
ADD CONSTRAINT UQ_Kreditor_kreditorId UNIQUE (kreditorId);
|
||||
ADD CONSTRAINT UQ_Kreditor_IBAN UNIQUE (iban);
|
||||
|
||||
-- Create AccountingItems table
|
||||
-- Based on CSV structure: umsatz brutto, soll/haben kz, konto, gegenkonto, bu, buchungsdatum, rechnungsnummer, buchungstext, beleglink
|
||||
|
||||
@@ -22,7 +22,7 @@ router.get('/system-info', authenticateToken, (req, res) => {
|
||||
// Get all kreditoren
|
||||
router.get('/kreditoren', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const result = await executeQuery('SELECT * FROM fibdash.Kreditor ORDER BY name');
|
||||
const result = await executeQuery('SELECT id, iban, name, kreditorId FROM fibdash.Kreditor ORDER BY name, iban');
|
||||
res.json({ kreditoren: result.recordset });
|
||||
} catch (error) {
|
||||
console.error('Error fetching kreditoren:', error);
|
||||
|
||||
@@ -235,7 +235,18 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Add JTL status to each CSV transaction
|
||||
// Get Kreditor information for IBAN lookup
|
||||
let kreditorData = [];
|
||||
try {
|
||||
const { executeQuery } = require('../config/database');
|
||||
const kreditorQuery = `SELECT id, iban, name, kreditorId FROM fibdash.Kreditor`;
|
||||
const kreditorResult = await executeQuery(kreditorQuery);
|
||||
kreditorData = kreditorResult.recordset || [];
|
||||
} catch (error) {
|
||||
console.log('Kreditor database not available, continuing without Kreditor data');
|
||||
}
|
||||
|
||||
// Add JTL status and Kreditor information to each CSV transaction
|
||||
const transactionsWithJTL = monthTransactions.map(transaction => {
|
||||
// Try to match by amount and date (approximate matching)
|
||||
const amount = transaction.numericAmount;
|
||||
@@ -255,6 +266,10 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
|
||||
return amountMatch && dateMatch;
|
||||
});
|
||||
|
||||
// Look up Kreditor by IBAN
|
||||
const transactionIban = transaction['Kontonummer/IBAN'];
|
||||
const kreditorMatch = transactionIban ? kreditorData.find(k => k.iban === transactionIban) : null;
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
hasJTL: jtlDatabaseAvailable ? !!jtlMatch : undefined,
|
||||
@@ -263,7 +278,15 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
|
||||
jtlDatabaseAvailable,
|
||||
// Include document data from JTL match
|
||||
pdfs: jtlMatch ? jtlMatch.pdfs || [] : [],
|
||||
links: jtlMatch ? jtlMatch.links || [] : []
|
||||
links: jtlMatch ? jtlMatch.links || [] : [],
|
||||
// Include Kreditor information
|
||||
kreditor: kreditorMatch ? {
|
||||
id: kreditorMatch.id,
|
||||
name: kreditorMatch.name,
|
||||
kreditorId: kreditorMatch.kreditorId,
|
||||
iban: kreditorMatch.iban
|
||||
} : null,
|
||||
hasKreditor: !!kreditorMatch
|
||||
};
|
||||
});
|
||||
|
||||
@@ -299,6 +322,7 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
|
||||
'Verwendungszweck': jtl.cVerwendungszweck || '',
|
||||
'Buchungstext': 'JTL Transaction',
|
||||
'Beguenstigter/Zahlungspflichtiger': jtl.cName || '',
|
||||
'Kontonummer/IBAN': '', // JTL transactions don't have IBAN data
|
||||
'Betrag': jtl.fBetrag ? jtl.fBetrag.toString().replace('.', ',') : '0,00',
|
||||
numericAmount: parseFloat(jtl.fBetrag) || 0,
|
||||
parsedDate: new Date(jtl.dBuchungsdatum),
|
||||
@@ -309,7 +333,10 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
|
||||
isJTLOnly: true,
|
||||
// Include document data from JTL transaction
|
||||
pdfs: jtl.pdfs || [],
|
||||
links: jtl.links || []
|
||||
links: jtl.links || [],
|
||||
// JTL transactions don't have IBAN data, so no Kreditor match
|
||||
kreditor: null,
|
||||
hasKreditor: false
|
||||
}));
|
||||
|
||||
// Combine CSV and JTL-only transactions
|
||||
@@ -565,10 +592,9 @@ router.get('/kreditors', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { executeQuery } = require('../config/database');
|
||||
const query = `
|
||||
SELECT id, iban, name, kreditorId, created_at, updated_at
|
||||
FROM Kreditor
|
||||
WHERE is_active = 1
|
||||
ORDER BY name ASC
|
||||
SELECT id, iban, name, kreditorId
|
||||
FROM fibdash.Kreditor
|
||||
ORDER BY name ASC, iban ASC
|
||||
`;
|
||||
|
||||
const result = await executeQuery(query);
|
||||
@@ -586,14 +612,12 @@ router.get('/kreditors/:id', authenticateToken, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const query = `
|
||||
SELECT id, iban, name, kreditorId, created_at, updated_at
|
||||
FROM Kreditor
|
||||
WHERE id = @id AND is_active = 1
|
||||
SELECT id, iban, name, kreditorId
|
||||
FROM fibdash.Kreditor
|
||||
WHERE id = @id
|
||||
`;
|
||||
|
||||
const result = await executeQuery(query, [
|
||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
||||
]);
|
||||
const result = await executeQuery(query, { id: parseInt(id) });
|
||||
|
||||
if (result.recordset.length === 0) {
|
||||
return res.status(404).json({ error: 'Kreditor not found' });
|
||||
@@ -617,32 +641,25 @@ router.post('/kreditors', authenticateToken, async (req, res) => {
|
||||
return res.status(400).json({ error: 'IBAN, name, and kreditorId are required' });
|
||||
}
|
||||
|
||||
// Check if kreditor with same IBAN or kreditorId already exists
|
||||
// Check if IBAN already exists (only IBAN needs to be unique)
|
||||
const checkQuery = `
|
||||
SELECT id FROM Kreditor
|
||||
WHERE (iban = @iban OR kreditorId = @kreditorId) AND is_active = 1
|
||||
SELECT id FROM fibdash.Kreditor
|
||||
WHERE iban = @iban
|
||||
`;
|
||||
|
||||
const checkResult = await executeQuery(checkQuery, [
|
||||
{ name: 'iban', type: 'nvarchar', value: iban },
|
||||
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId }
|
||||
]);
|
||||
const checkResult = await executeQuery(checkQuery, { iban });
|
||||
|
||||
if (checkResult.recordset.length > 0) {
|
||||
return res.status(409).json({ error: 'Kreditor with this IBAN or kreditorId already exists' });
|
||||
return res.status(409).json({ error: 'Kreditor with this IBAN already exists' });
|
||||
}
|
||||
|
||||
const insertQuery = `
|
||||
INSERT INTO Kreditor (iban, name, kreditorId, created_at, updated_at)
|
||||
OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId, INSERTED.created_at, INSERTED.updated_at
|
||||
VALUES (@iban, @name, @kreditorId, GETDATE(), GETDATE())
|
||||
INSERT INTO fibdash.Kreditor (iban, name, kreditorId)
|
||||
OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId
|
||||
VALUES (@iban, @name, @kreditorId)
|
||||
`;
|
||||
|
||||
const result = await executeQuery(insertQuery, [
|
||||
{ name: 'iban', type: 'nvarchar', value: iban },
|
||||
{ name: 'name', type: 'nvarchar', value: name },
|
||||
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId }
|
||||
]);
|
||||
const result = await executeQuery(insertQuery, { iban, name, kreditorId });
|
||||
|
||||
res.status(201).json(result.recordset[0]);
|
||||
} catch (error) {
|
||||
@@ -664,44 +681,33 @@ router.put('/kreditors/:id', authenticateToken, async (req, res) => {
|
||||
}
|
||||
|
||||
// Check if kreditor exists
|
||||
const checkQuery = `SELECT id FROM Kreditor WHERE id = @id AND is_active = 1`;
|
||||
const checkResult = await executeQuery(checkQuery, [
|
||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
||||
]);
|
||||
const checkQuery = `SELECT id FROM fibdash.Kreditor WHERE id = @id`;
|
||||
const checkResult = await executeQuery(checkQuery, { id: parseInt(id) });
|
||||
|
||||
if (checkResult.recordset.length === 0) {
|
||||
return res.status(404).json({ error: 'Kreditor not found' });
|
||||
}
|
||||
|
||||
// Check for conflicts with other kreditors
|
||||
// Check for conflicts with other kreditors (only IBAN needs to be unique)
|
||||
const conflictQuery = `
|
||||
SELECT id FROM Kreditor
|
||||
WHERE (iban = @iban OR kreditorId = @kreditorId) AND id != @id AND is_active = 1
|
||||
SELECT id FROM fibdash.Kreditor
|
||||
WHERE iban = @iban AND id != @id
|
||||
`;
|
||||
|
||||
const conflictResult = await executeQuery(conflictQuery, [
|
||||
{ name: 'iban', type: 'nvarchar', value: iban },
|
||||
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId },
|
||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
||||
]);
|
||||
const conflictResult = await executeQuery(conflictQuery, { iban, id: parseInt(id) });
|
||||
|
||||
if (conflictResult.recordset.length > 0) {
|
||||
return res.status(409).json({ error: 'Another kreditor with this IBAN or kreditorId already exists' });
|
||||
return res.status(409).json({ error: 'Another kreditor with this IBAN already exists' });
|
||||
}
|
||||
|
||||
const updateQuery = `
|
||||
UPDATE Kreditor
|
||||
SET iban = @iban, name = @name, kreditorId = @kreditorId, updated_at = GETDATE()
|
||||
OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId, INSERTED.created_at, INSERTED.updated_at
|
||||
UPDATE fibdash.Kreditor
|
||||
SET iban = @iban, name = @name, kreditorId = @kreditorId
|
||||
OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId
|
||||
WHERE id = @id
|
||||
`;
|
||||
|
||||
const result = await executeQuery(updateQuery, [
|
||||
{ name: 'iban', type: 'nvarchar', value: iban },
|
||||
{ name: 'name', type: 'nvarchar', value: name },
|
||||
{ name: 'kreditorId', type: 'nvarchar', value: kreditorId },
|
||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
||||
]);
|
||||
const result = await executeQuery(updateQuery, { iban, name, kreditorId, id: parseInt(id) });
|
||||
|
||||
res.json(result.recordset[0]);
|
||||
} catch (error) {
|
||||
@@ -710,21 +716,18 @@ router.put('/kreditors/:id', authenticateToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Delete kreditor (soft delete)
|
||||
// Delete kreditor (hard delete)
|
||||
router.delete('/kreditors/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { executeQuery } = require('../config/database');
|
||||
const { id } = req.params;
|
||||
|
||||
const query = `
|
||||
UPDATE Kreditor
|
||||
SET is_active = 0, updated_at = GETDATE()
|
||||
WHERE id = @id AND is_active = 1
|
||||
DELETE FROM fibdash.Kreditor
|
||||
WHERE id = @id
|
||||
`;
|
||||
|
||||
const result = await executeQuery(query, [
|
||||
{ name: 'id', type: 'int', value: parseInt(id) }
|
||||
]);
|
||||
const result = await executeQuery(query, { id: parseInt(id) });
|
||||
|
||||
if (result.rowsAffected[0] === 0) {
|
||||
return res.status(404).json({ error: 'Kreditor not found' });
|
||||
|
||||
Reference in New Issue
Block a user