Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
72 changes: 59 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,65 @@ 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' });
}

// Validate and parse the date
const transactionDate = new Date(transactionData.addedOn);
if (isNaN(transactionDate.getTime())) {
return res.status(400).json({ message: 'Invalid date format provided' });
}

// 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: transactionDate,
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: transactionDate,
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;
136 changes: 128 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,61 @@ const ReceiptsPage = () => {
}
};

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

setReceiptResult(updatedReceiptResult);
setOpenEditReceiptResult(false);
};

// 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 +139,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 +149,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> {(parseFloat(receiptResult.extractedData.amount) || 0).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>
{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 +184,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