refactor: Move inline JavaScript to a separate file

- Move all JavaScript code from index.html to app.js.

- Update index.html to include a script tag for app.js.
This commit is contained in:
chris 2025-11-05 20:04:43 -05:00
parent 0ae3bda8be
commit bc8998d57e
5 changed files with 1359 additions and 1316 deletions

1165
app.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,9 @@
version: '3.8'
services:
app:
build: .
ports:
- "3001:3000"
- "3000:3000"
volumes:
- ./ledger_db:/usr/src/app/data
volumes:
ledger_db:
- ./simpleledger.db:/usr/src/app/data/simpleledger.db

1180
index.html

File diff suppressed because it is too large Load Diff

View File

@ -552,16 +552,26 @@ app.get('/api/dashboard-summary', authenticateToken, (req, res) => {
app.post('/api/upload', authenticateToken, upload.fields([{ name: 'bankFile', maxCount: 1 }, { name: 'salesFile', maxCount: 1 }]), async (req, res) => {
const userId = req.user.id;
const rules = await new Promise((resolve, reject) => {
try {
const [rules, existingTransactions] = await Promise.all([
new Promise((resolve, reject) => {
db.all('SELECT * FROM rules WHERE userId = ?', [userId], (err, rows) => {
if (err) reject(err);
resolve(rows);
});
}),
new Promise((resolve, reject) => {
db.all('SELECT date, description, amount FROM transactions WHERE userId = ?', [userId], (err, rows) => {
if (err) reject(err);
resolve(rows);
});
})
]);
let transactionsToInsert = []; // This will hold { tx, split } objects
const existingTxKeys = new Set(existingTransactions.map(tx => `${new Date(tx.date).toISOString().slice(0, 10)}:${tx.description.trim()}:${tx.amount.toFixed(2)}`));
let transactionsToInsert = [];
let skippedCount = 0;
// --- NEW: High-confidence "smart guess" logic ---
const smartGuessCategory = (description) => {
const desc = description.toUpperCase();
const guesses = {
@ -573,40 +583,36 @@ app.post('/api/upload', authenticateToken, upload.fields([{ name: 'bankFile', ma
'Expense - Software/Subscriptions': ['OPENAI', 'CHATGPT', 'GOOGLE *', 'MSFT *', 'ADOBE'],
'Expense - Meals & Entertainment': ['RESTAURANT', 'DOORDASH', 'UBER EATS', 'GRUBHUB'],
'Expense - Bank Fees': ['MAINTENANCE FEE', 'SQUARE PAYMENT PROCESSING'],
'Liability - Owner\'s Draw': ['PERSONAL', 'TRANSFER TO SELF'] // Added for owner's draw
'Liability - Owner\'s Draw': ['PERSONAL', 'TRANSFER TO SELF']
};
for (const [category, keywords] of Object.entries(guesses)) {
for (const keyword of keywords) {
if (desc.includes(keyword)) {
return category; // Found a confident match
return category;
}
}
}
return null; // No confident guess
return null;
};
// --- MODIFIED: This function now tries smart guess first, then user rules ---
const applyCategorization = (tx, split) => {
const desc = tx.description.toUpperCase();
// 1. Try smart guess first
const guess = smartGuessCategory(tx.description);
if (guess) {
split.category = guess;
tx.reconciliation_status = 'categorized';
return; // Found a confident match
return;
}
// 2. If no guess, apply user's rules
for (const rule of rules) {
if (desc.includes(rule.keyword.toUpperCase())) {
split.category = rule.category;
tx.reconciliation_status = 'categorized'; // Mark parent as categorized
return; // Stop after first match
tx.reconciliation_status = 'categorized';
return;
}
}
// If no guess and no rule, it remains 'Expense - Uncategorized'
};
const processBankCSV = (data) => {
@ -616,8 +622,15 @@ app.post('/api/upload', authenticateToken, upload.fields([{ name: 'bankFile', ma
const credit = parseFloat(row.Credit) || 0;
const amount = credit - debit;
const description = row.Description ? row.Description.trim() : '';
const key = `${date.toISOString().slice(0, 10)}:${description}:${amount.toFixed(2)}`;
if (!isNaN(date) && amount !== 0) {
if (existingTxKeys.has(key)) {
skippedCount++;
return;
}
existingTxKeys.add(key);
let tx = {
date: date.toISOString(),
description: description,
@ -631,7 +644,6 @@ app.post('/api/upload', authenticateToken, upload.fields([{ name: 'bankFile', ma
description: description,
amount: amount
};
// --- MODIFIED: Call the new function ---
applyCategorization(tx, split);
transactionsToInsert.push({ tx, split });
}
@ -660,12 +672,19 @@ app.post('/api/upload', authenticateToken, upload.fields([{ name: 'bankFile', ma
const addTx = (desc, cat, amount) => {
if (amount !== 0) {
const key = `${txDate.toISOString().slice(0, 10)}:${desc}:${amount.toFixed(2)}`;
if (existingTxKeys.has(key)) {
skippedCount++;
return;
}
existingTxKeys.add(key);
let tx = {
date: txDate.toISOString(),
description: desc,
amount: amount,
source: 'sales_summary_csv',
reconciliation_status: 'categorized', // Sales data is pre-categorized
reconciliation_status: 'categorized',
createdAt: new Date().toISOString()
};
let split = {
@ -684,8 +703,6 @@ app.post('/api/upload', authenticateToken, upload.fields([{ name: 'bankFile', ma
addTx('Square Payment Processing Fees', 'Expense - Bank Fees', salesData['Square payment processing fees'] || 0);
};
// 2. Parse files
try {
if (req.files.bankFile) {
const file = req.files.bankFile[0];
const content = fs.readFileSync(file.path, 'utf8');
@ -701,14 +718,9 @@ app.post('/api/upload', authenticateToken, upload.fields([{ name: 'bankFile', ma
processSalesCSV(results.data, file.originalname);
fs.unlinkSync(file.path);
}
} catch (parseError) {
console.error("File parse error:", parseError);
return res.status(500).send('Error parsing files.');
}
// 3. Insert transactions and their default splits
if (transactionsToInsert.length === 0) {
return res.status(400).send('No valid transactions found to insert.');
return res.status(200).send(`No new transactions to insert. Skipped ${skippedCount} duplicate transactions.`);
}
const txStmt = db.prepare('INSERT INTO transactions (userId, date, description, amount, source, createdAt, reconciliation_status) VALUES (?, ?, ?, ?, ?, ?, ?)');
@ -736,9 +748,14 @@ app.post('/api/upload', authenticateToken, upload.fields([{ name: 'bankFile', ma
console.error("DB commit error:", err);
return res.status(500).send('Error saving transactions.');
}
res.status(201).send(`Successfully inserted ${transactionsToInsert.length} transactions.`);
res.status(201).send(`Successfully inserted ${transactionsToInsert.length} new transactions. Skipped ${skippedCount} duplicate transactions.`);
});
});
} catch (error) {
console.error("Error processing upload:", error);
res.status(500).send('Server error during file upload.');
}
});
// --- Serve Frontend ---

Binary file not shown.