Skip to content

Commit d44543b

Browse files
authored
Merge pull request #44 from DumbWareio/cursor/add-notes-entry-option-to-transactions-5e9d
Add notes entry option to transactions
2 parents 2a2f36b + 77f54ce commit d44543b

File tree

4 files changed

+93
-34
lines changed

4 files changed

+93
-34
lines changed

public/assets/styles.css

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,14 @@ header {
566566
font-weight: 500;
567567
}
568568

569+
.transaction-item .notes {
570+
font-size: 0.75rem;
571+
color: var(--text);
572+
opacity: 0.8;
573+
margin-top: 0.25rem;
574+
line-height: 1.3;
575+
}
576+
569577
.transaction-item .metadata {
570578
display: flex;
571579
align-items: center;
@@ -834,7 +842,8 @@ footer {
834842
}
835843

836844
#transactionForm input,
837-
#transactionForm select {
845+
#transactionForm select,
846+
#transactionForm textarea {
838847
padding: 0.5rem;
839848
border: 1px solid var(--border);
840849
border-radius: 6px;
@@ -844,6 +853,12 @@ footer {
844853
width: 100%;
845854
}
846855

856+
#transactionForm textarea {
857+
resize: vertical;
858+
min-height: 60px;
859+
font-family: inherit;
860+
}
861+
847862
#transactionForm button {
848863
background: var(--primary);
849864
color: white;

public/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ <h3>Transactions</h3>
156156
<input type="number" id="amount" placeholder="0.00" required step="0.01">
157157
</div>
158158
<input type="text" id="description" placeholder="Description" required>
159+
<textarea id="notes" placeholder="Notes (optional)" rows="3"></textarea>
159160
<input type="date" id="transactionDate" required>
160161
<button type="submit">Add</button>
161162
</form>

public/script.js

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ function debugLog(...args) {
4646
}
4747
}
4848

49+
// HTML escaping function to prevent XSS attacks
50+
function escapeHtml(unsafe) {
51+
if (!unsafe) return '';
52+
return unsafe
53+
.replace(/&/g, "&amp;")
54+
.replace(/</g, "&lt;")
55+
.replace(/>/g, "&gt;")
56+
.replace(/"/g, "&quot;")
57+
.replace(/'/g, "&#039;");
58+
}
59+
4960
// Helper function to join paths with base path
5061
function joinPath(path) {
5162
const basePath = window.appConfig?.basePath || '';
@@ -358,9 +369,10 @@ async function loadTransactions() {
358369
<div class="transaction-item ${isRecurring ? 'recurring-instance' : ''}" data-id="${transaction.id}" data-type="${transaction.type}">
359370
<div class="transaction-content">
360371
<div class="details">
361-
<div class="description">${transaction.description}</div>
372+
<div class="description">${escapeHtml(transaction.description)}</div>
373+
${transaction.notes ? `<div class="notes">${escapeHtml(transaction.notes)}</div>` : ''}
362374
<div class="metadata">
363-
${transaction.category ? `<span class="category">${transaction.category}</span>` : ''}
375+
${transaction.category ? `<span class="category">${escapeHtml(transaction.category)}</span>` : ''}
364376
<span class="date">${formattedDate}</span>
365377
${isRecurring ? `<span class="recurring-info">(Recurring)</span>` : ''}
366378
</div>
@@ -466,6 +478,7 @@ function editTransaction(id, transaction, isRecurringInstance) {
466478
document.getElementById('amount').value = transaction.amount;
467479
document.getElementById('description').value = transaction.description;
468480
document.getElementById('transactionDate').value = transaction.date;
481+
document.getElementById('notes').value = transaction.notes || '';
469482

470483
// Update the currentTransactionType to match the transaction being edited
471484
currentTransactionType = transaction.type;
@@ -733,7 +746,8 @@ function initModalHandling() {
733746
description: document.getElementById('description').value,
734747
category: currentTransactionType === 'expense' ? document.getElementById('category').value : null,
735748
date: document.getElementById('transactionDate').value,
736-
recurring: buildRecurringPattern()
749+
recurring: buildRecurringPattern(),
750+
notes: document.getElementById('notes').value
737751
};
738752

739753
try {
@@ -1047,24 +1061,26 @@ async function initMainPage() {
10471061
const tableData = transactions.map(t => [
10481062
t.date,
10491063
t.description,
1064+
t.notes || '-',
10501065
t.category || '-',
10511066
formatCurrency(t.type === 'expense' ? -t.amount : t.amount),
10521067
t.type
10531068
]);
10541069

10551070
doc.autoTable({
10561071
startY: 85,
1057-
head: [['Date', 'Description', 'Category', 'Amount', 'Type']],
1072+
head: [['Date', 'Description', 'Notes', 'Category', 'Amount', 'Type']],
10581073
body: tableData,
10591074
theme: 'grid',
10601075
headStyles: { fillColor: [66, 66, 66] },
1061-
styles: { fontSize: 10 },
1076+
styles: { fontSize: 9 },
10621077
columnStyles: {
1063-
0: { cellWidth: 30 }, // Date
1064-
1: { cellWidth: 60 }, // Description
1065-
2: { cellWidth: 30 }, // Category
1066-
3: { cellWidth: 30 }, // Amount
1067-
4: { cellWidth: 20 } // Type
1078+
0: { cellWidth: 25 }, // Date
1079+
1: { cellWidth: 40 }, // Description
1080+
2: { cellWidth: 35 }, // Notes
1081+
3: { cellWidth: 25 }, // Category
1082+
4: { cellWidth: 25 }, // Amount
1083+
5: { cellWidth: 15 } // Type
10681084
}
10691085
});
10701086

server.js

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ function debugLog(...args) {
3636
}
3737
}
3838

39+
// Input sanitization function to prevent XSS attacks
40+
function sanitizeInput(input) {
41+
if (typeof input !== 'string') return input;
42+
return input
43+
.replace(/&/g, '&amp;')
44+
.replace(/</g, '&lt;')
45+
.replace(/>/g, '&gt;')
46+
.replace(/"/g, '&quot;')
47+
.replace(/'/g, '&#x27;')
48+
.replace(/\//g, '&#x2F;');
49+
}
50+
3951
// Add logging to BASE_PATH extraction
4052
const BASE_PATH = (() => {
4153
if (!process.env.BASE_URL) {
@@ -362,7 +374,7 @@ async function getTransactionsInRange(startDate, endDate) {
362374
// API Routes - all under BASE_PATH
363375
app.post(BASE_PATH + '/api/transactions', authMiddleware, async (req, res) => {
364376
try {
365-
const { type, amount, description, category, date, recurring } = req.body;
377+
const { type, amount, description, category, date, recurring, notes } = req.body;
366378

367379
// Basic validation
368380
if (!type || !amount || !description || !date) {
@@ -446,8 +458,9 @@ app.post(BASE_PATH + '/api/transactions', authMiddleware, async (req, res) => {
446458
const newTransaction = {
447459
id: crypto.randomUUID(),
448460
amount: parseFloat(amount),
449-
description,
450-
date: adjustedDate
461+
description: sanitizeInput(description),
462+
date: adjustedDate,
463+
notes: sanitizeInput(notes || '')
451464
};
452465

453466
// Add recurring information if present
@@ -459,7 +472,7 @@ app.post(BASE_PATH + '/api/transactions', authMiddleware, async (req, res) => {
459472
}
460473

461474
if (type === 'expense') {
462-
newTransaction.category = category;
475+
newTransaction.category = sanitizeInput(category);
463476
transactions[key].expenses.push(newTransaction);
464477
} else {
465478
transactions[key].income.push(newTransaction);
@@ -764,9 +777,15 @@ app.get(BASE_PATH + '/api/export/:year/:month', authMiddleware, async (req, res)
764777
].sort((a, b) => new Date(b.date) - new Date(a.date));
765778

766779
// Convert to CSV
767-
const csvRows = ['Date,Type,Category,Description,Amount'];
780+
const csvRows = ['Date,Type,Category,Description,Notes,Amount'];
768781
allTransactions.forEach(t => {
769-
csvRows.push(`${t.date},${t.type},${t.category || ''},${t.description},${t.amount}`);
782+
// Escape notes and description to handle commas and quotes
783+
const escapedDescription = (t.description || '').replace(/"/g, '""');
784+
const escapedNotes = (t.notes || '').replace(/"/g, '""');
785+
const formattedDescription = escapedDescription.includes(',') ? `"${escapedDescription}"` : escapedDescription;
786+
const formattedNotes = escapedNotes.includes(',') ? `"${escapedNotes}"` : escapedNotes;
787+
788+
csvRows.push(`${t.date},${t.type},${t.category || ''},${formattedDescription},${formattedNotes},${t.amount}`);
770789
});
771790

772791
res.setHeader('Content-Type', 'text/csv');
@@ -788,15 +807,17 @@ app.get(BASE_PATH + '/api/export/range', authMiddleware, async (req, res) => {
788807
const transactions = await getTransactionsInRange(start, end);
789808

790809
// Convert to CSV with specified format
791-
const csvRows = ['Category,Date,Description,Value'];
810+
const csvRows = ['Category,Date,Description,Notes,Value'];
792811
transactions.forEach(t => {
793812
const category = t.type === 'income' ? 'Income' : t.category;
794813
const value = t.type === 'income' ? t.amount : -t.amount;
795-
// Escape description to handle commas and quotes
796-
const escapedDescription = t.description.replace(/"/g, '""');
814+
// Escape description and notes to handle commas and quotes
815+
const escapedDescription = (t.description || '').replace(/"/g, '""');
816+
const escapedNotes = (t.notes || '').replace(/"/g, '""');
797817
const formattedDescription = escapedDescription.includes(',') ? `"${escapedDescription}"` : escapedDescription;
818+
const formattedNotes = escapedNotes.includes(',') ? `"${escapedNotes}"` : escapedNotes;
798819

799-
csvRows.push(`${category},${t.date},${formattedDescription},${value}`);
820+
csvRows.push(`${category},${t.date},${formattedDescription},${formattedNotes},${value}`);
800821
});
801822

802823
res.setHeader('Content-Type', 'text/csv');
@@ -811,7 +832,7 @@ app.get(BASE_PATH + '/api/export/range', authMiddleware, async (req, res) => {
811832
app.put(BASE_PATH + '/api/transactions/:id', authMiddleware, async (req, res) => {
812833
try {
813834
const { id } = req.params;
814-
const { type, amount, description, category, date, recurring } = req.body;
835+
const { type, amount, description, category, date, recurring, notes } = req.body;
815836

816837
// Basic validation
817838
if (!type || !amount || !description || !date) {
@@ -837,21 +858,23 @@ app.put(BASE_PATH + '/api/transactions/:id', authMiddleware, async (req, res) =>
837858
// If type changed, move to expenses
838859
if (type === 'expense') {
839860
const transaction = monthData.income.splice(incomeIndex, 1)[0];
840-
transaction.category = category;
861+
transaction.category = sanitizeInput(category);
841862
monthData.expenses.push({
842863
...transaction,
843864
amount: parseFloat(amount),
844-
description,
865+
description: sanitizeInput(description),
845866
date,
846-
recurring: recurring || null
867+
recurring: recurring || null,
868+
notes: sanitizeInput(notes || '')
847869
});
848870
} else {
849871
monthData.income[incomeIndex] = {
850872
...monthData.income[incomeIndex],
851873
amount: parseFloat(amount),
852-
description,
874+
description: sanitizeInput(description),
853875
date,
854-
recurring: recurring || null
876+
recurring: recurring || null,
877+
notes: sanitizeInput(notes || '')
855878
};
856879
}
857880
found = true;
@@ -868,18 +891,20 @@ app.put(BASE_PATH + '/api/transactions/:id', authMiddleware, async (req, res) =>
868891
monthData.income.push({
869892
...transaction,
870893
amount: parseFloat(amount),
871-
description,
894+
description: sanitizeInput(description),
872895
date,
873-
recurring: recurring || null
896+
recurring: recurring || null,
897+
notes: sanitizeInput(notes || '')
874898
});
875899
} else {
876900
monthData.expenses[expenseIndex] = {
877901
...monthData.expenses[expenseIndex],
878902
amount: parseFloat(amount),
879-
description,
880-
category,
903+
description: sanitizeInput(description),
904+
category: sanitizeInput(category),
881905
date,
882-
recurring: recurring || null
906+
recurring: recurring || null,
907+
notes: sanitizeInput(notes || '')
883908
};
884909
}
885910
found = true;
@@ -1029,7 +1054,8 @@ app.get(BASE_PATH + '/api/calendar/transactions', apiAuthMiddleware, async (req,
10291054
filteredTransactions.push({
10301055
type: 'income',
10311056
...transaction,
1032-
amount: parseFloat(transaction.amount)
1057+
amount: parseFloat(transaction.amount),
1058+
notes: transaction.notes || ''
10331059
});
10341060
});
10351061

@@ -1038,7 +1064,8 @@ app.get(BASE_PATH + '/api/calendar/transactions', apiAuthMiddleware, async (req,
10381064
filteredTransactions.push({
10391065
type: 'expense',
10401066
...transaction,
1041-
amount: parseFloat(transaction.amount)
1067+
amount: parseFloat(transaction.amount),
1068+
notes: transaction.notes || ''
10421069
});
10431070
});
10441071
}

0 commit comments

Comments
 (0)