Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
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 @@ -75,19 +75,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) {
console.error("Error with Gemini API:", error);
Expand All @@ -103,6 +90,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),
Copy link

Copilot AI Oct 22, 2025

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
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
});

} 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) => {
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);
}
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

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

The function is marked as async but does not contain any await operations. Since it only performs synchronous state updates, the try-catch block will not catch errors from asynchronous operations that don't exist. Remove the async keyword and consider removing the try-catch block as synchronous state updates are unlikely to throw errors.

Suggested change
const handleEditReceiptSubmit = async (formData) => {
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);
}
const handleEditReceiptSubmit = (formData) => {
// 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);

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>
Copy link

Copilot AI Oct 22, 2025

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