Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 53 additions & 13 deletions backend/controllers/receiptController.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,6 @@ const uploadReceipt = async (req, res) => {

const savedReceipt = await newReceipt.save();

// Automatically create a corresponding expense transaction
if (savedReceipt) {
const newTransaction = new IncomeExpense({
user: req.user.id,
name: savedReceipt.extractedData.merchant,
category: savedReceipt.extractedData.category,
cost: savedReceipt.extractedData.amount,
addedOn: savedReceipt.extractedData.date,
isIncome: false,
});
await newTransaction.save();
}

res.status(201).json(savedReceipt);

} catch (error) {
Expand All @@ -83,6 +70,59 @@ const uploadReceipt = async (req, res) => {
}
};

// @desc Save transaction after user confirmation and edits
// @route POST /api/receipts/save-transaction
// @access Private
const saveTransactionFromReceipt = async (req, res) => {
try {
const { receiptId, transactionData } = req.body;

// Validate required fields
if (!receiptId || !transactionData) {
return res.status(400).json({ message: 'Receipt ID and transaction data are required' });
}

// Verify the receipt belongs to the user
const receipt = await Receipt.findOne({ _id: receiptId, user: req.user.id });
if (!receipt) {
return res.status(404).json({ message: 'Receipt not found' });
}

// Create the transaction with user-confirmed data
const newTransaction = new IncomeExpense({
user: req.user.id,
name: transactionData.name,
category: transactionData.category,
cost: transactionData.cost,
addedOn: new Date(transactionData.addedOn),

Copilot AI Oct 22, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If transactionData.addedOn is an invalid date string, new Date() will create an Invalid Date object, which could cause issues downstream. Add validation to ensure the date is valid before creating the transaction.

Copilot uses AI. Check for mistakes.
isIncome: transactionData.isIncome || false,
});

const savedTransaction = await newTransaction.save();

// update the receipt with the final confirmed data

Copilot AI Oct 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected capitalization: 'update' should be 'Update' at the beginning of the comment.

Suggested change
// update the receipt with the final confirmed data
// Update the receipt with the final confirmed data

Copilot uses AI. Check for mistakes.
receipt.extractedData = {
merchant: transactionData.name,
amount: transactionData.cost,
category: transactionData.category,
date: new Date(transactionData.addedOn),
isIncome: transactionData.isIncome || false,
};
await receipt.save();

res.status(201).json({
message: 'Transaction saved successfully',
transaction: savedTransaction,
receipt: receipt

Copilot AI Oct 31, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Using shorthand property syntax would be more concise. Change receipt: receipt to just receipt.

Suggested change
receipt: receipt
receipt

Copilot uses AI. Check for mistakes.
});

} catch (error) {
console.error('Error saving transaction:', error);
res.status(500).json({ message: 'Failed to save transaction', error: error.message });
}
};

module.exports = {
uploadReceipt,
saveTransactionFromReceipt,
};
3 changes: 2 additions & 1 deletion backend/routes/receiptRoutes.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const express = require('express');
const router = express.Router();
const { uploadReceipt } = require('../controllers/receiptController');
const { uploadReceipt, saveTransactionFromReceipt } = require('../controllers/receiptController');
const { protect } = require('../middleware/authMiddleware');
const upload = require('../middleware/uploadMiddleware');

router.post('/upload', protect, upload, uploadReceipt);
router.post('/save-transaction', protect, saveTransactionFromReceipt);

module.exports = router;
141 changes: 133 additions & 8 deletions frontend/src/pages/ReceiptsPage.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import api from '../api/axios';
import TransactionModal from '../components/TransactionModal';

const ReceiptsPage = () => {
const [file, setFile] = useState(null);
Expand All @@ -9,6 +10,25 @@ const ReceiptsPage = () => {
const [error, setError] = useState('');
const navigate = useNavigate();

const [openEditReceiptResult, setOpenEditReceiptResult] = useState(false);
const [categories, setCategories] = useState([]);
const [isEditingResult, setIsEditingResult] = useState(false);
const [isSaving, setIsSaving] = useState(false);

// Fetch categories when component mounts
useEffect(() => {
const fetchCategories = async () => {
try {
const response = await api.get('/transactions/categories');
setCategories(response.data);
} catch (error) {
console.error('Failed to fetch categories:', error);
}
};

fetchCategories();
}, []);

const handleFileChange = (e) => {
setFile(e.target.files[0]);
setReceiptResult(null);
Expand All @@ -34,8 +54,9 @@ const ReceiptsPage = () => {
},
});
setReceiptResult(response.data);
alert('Receipt processed successfully and transaction created! Redirecting to dashboard...');
navigate('/dashboard');

// Open the modal to allow user to edit the extracted data
setOpenEditReceiptResult(true);
} catch (err) {
setError('Upload failed. Please try again.');
console.error(err);
Expand All @@ -44,6 +65,66 @@ const ReceiptsPage = () => {
}
};

const handleEditReceiptSubmit = async (formData, transactionId) => {
try {
// Update the receiptResult with the edited data
const updatedReceiptResult = {
...receiptResult,
extractedData: {
merchant: formData.name,
amount: parseFloat(formData.cost),
category: formData.category,
date: formData.addedOn,
isIncome: formData.isIncome
}
};

setReceiptResult(updatedReceiptResult);
setOpenEditReceiptResult(false);
} catch (err) {
setError('Failed to update receipt data. Please try again.');
console.error(err);
}
};

Copilot AI Oct 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function parameter transactionId is not used anywhere in the function body. Either remove it or implement the intended functionality that uses this parameter.

Copilot uses AI. Check for mistakes.

// Handle final save to database (second verification step)
const handleFinalSave = async () => {
try {
setIsSaving(true);

const transactionData = {
name: receiptResult.extractedData.merchant,
category: receiptResult.extractedData.category,
cost: receiptResult.extractedData.amount,
addedOn: receiptResult.extractedData.date,
isIncome: receiptResult.extractedData.isIncome || false
};

const response = await api.post('/receipts/save-transaction', {
receiptId: receiptResult._id,
transactionData: transactionData
});

alert('Transaction saved successfully! Redirecting to dashboard...');
navigate('/dashboard');
} catch (err) {
setError('Failed to save transaction. Please try again.');
console.error(err);
} finally {
setIsSaving(false);
}
};

// Handle edit button in result div
const handleEditResult = () => {
setIsEditingResult(true);
setOpenEditReceiptResult(true);
};

const handleNewCategory = (newCategory) => {
setCategories(prev => [...prev, newCategory].sort());
};

return (
<>
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-6">Upload Receipt</h1>
Expand All @@ -63,7 +144,7 @@ const ReceiptsPage = () => {
disabled={uploading}
className="mt-4 w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-blue-300"
>
{uploading ? 'Processing...' : 'Upload & Create Transaction'}
{uploading ? 'Processing...' : 'Upload & Extract Data'}
</button>
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
</form>
Expand All @@ -73,10 +154,33 @@ const ReceiptsPage = () => {
<h2 className="text-xl font-bold text-gray-800 dark:text-gray-200 mb-4">Last Upload Result</h2>
{receiptResult ? (
<div>
<p className="text-gray-700 dark:text-gray-300"><strong>Merchant:</strong> {receiptResult.extractedData.merchant}</p>
<p className="text-gray-700 dark:text-gray-300"><strong>Amount:</strong> {receiptResult.extractedData.amount.toFixed(2)}</p>
<p className="text-gray-700 dark:text-gray-300"><strong>Category:</strong> {receiptResult.extractedData.category}</p>
<p className="text-gray-700 dark:text-gray-300"><strong>Date:</strong> {new Date(receiptResult.extractedData.date).toLocaleDateString()}</p>
<div className="mb-4">
<p className="text-gray-700 dark:text-gray-300"><strong>Merchant:</strong> {receiptResult.extractedData.merchant}</p>
<p className="text-gray-700 dark:text-gray-300"><strong>Amount:</strong> {receiptResult.extractedData.amount.toFixed(2)}</p>

Copilot AI Oct 22, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential runtime error if receiptResult.extractedData.amount is null or undefined. Since the amount comes from user-edited data and parseFloat can return NaN, consider adding a fallback: {(receiptResult.extractedData.amount || 0).toFixed(2)}

Suggested change
<p className="text-gray-700 dark:text-gray-300"><strong>Amount:</strong> {receiptResult.extractedData.amount.toFixed(2)}</p>
<p className="text-gray-700 dark:text-gray-300"><strong>Amount:</strong> {(parseFloat(receiptResult.extractedData.amount) || 0).toFixed(2)}</p>

Copilot uses AI. Check for mistakes.
<p className="text-gray-700 dark:text-gray-300"><strong>Category:</strong> {receiptResult.extractedData.category}</p>
<p className="text-gray-700 dark:text-gray-300"><strong>Date:</strong> {new Date(receiptResult.extractedData.date).toLocaleDateString()}</p>
{receiptResult.extractedData.isIncome && (
<p className="text-gray-700 dark:text-gray-300"><strong>Income:</strong> Yes</p>
)}
</div>

<div className="flex gap-3 mb-4">
<button
onClick={handleEditResult}
disabled={isSaving}
className="px-4 py-2 bg-yellow-500 hover:bg-yellow-600 disabled:bg-yellow-300 text-white rounded-lg font-medium"
>
Edit
</button>
<button
onClick={handleFinalSave}
disabled={isSaving}
className="px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-green-300 text-white rounded-lg font-medium"
>
{isSaving ? 'Saving...' : 'Save Transaction'}
</button>
</div>

<img src={`http://localhost:5001${receiptResult.fileUrl}`} alt="Uploaded Receipt" className="mt-4 rounded-lg max-w-full h-auto" />
</div>
) : (
Expand All @@ -85,6 +189,27 @@ const ReceiptsPage = () => {
</div>

</div>

{/* Transaction Modal for editing receipt data */}
{openEditReceiptResult && receiptResult && (
<TransactionModal
isOpen={openEditReceiptResult}
onClose={() => {
setOpenEditReceiptResult(false);
setIsEditingResult(false);
}}
onSubmit={handleEditReceiptSubmit}
transaction={{
name: receiptResult.extractedData.merchant || '',
category: receiptResult.extractedData.category || '',
cost: receiptResult.extractedData.amount || 0,
addedOn: receiptResult.extractedData.date || new Date().toISOString().split('T')[0],
isIncome: receiptResult.extractedData.isIncome || false
}}
categories={categories}
onNewCategory={handleNewCategory}
/>
)}
</>
);
};
Expand Down