diff --git a/backend/controllers/transactionController.js b/backend/controllers/transactionController.js
index 1e49568..cad2499 100644
--- a/backend/controllers/transactionController.js
+++ b/backend/controllers/transactionController.js
@@ -28,10 +28,13 @@ const addTransaction = async (req, res) => {
// @access Private
const getTransactions = async (req, res) => {
try {
- const { isIncome, category, startDate, endDate, page = 1, limit = 10 } = req.query;
+ const { search, isIncome, category, startDate, endDate, page = 1, limit = 10 } = req.query;
const filter = { user: req.user.id, isDeleted: false };
-
+
+ if (search) {
+ filter.name = { $regex: search, $options: 'i' };
+ }
if (isIncome) filter.isIncome = isIncome;
if (category) filter.category = category;
if (startDate || endDate) {
diff --git a/frontend/src/pages/TransactionsPage.jsx b/frontend/src/pages/TransactionsPage.jsx
index 5d703c6..37a1eb9 100644
--- a/frontend/src/pages/TransactionsPage.jsx
+++ b/frontend/src/pages/TransactionsPage.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, { useState, useEffect, useCallback, useRef } from 'react';
import api from '../api/axios';
import TransactionModal from '../components/TransactionModal';
import ManageCategoriesModal from '../components/ManageCategoriesModal';
@@ -29,36 +29,108 @@ const handleExportCSV = async () => {
const TransactionsPage = () => {
const [transactions, setTransactions] = useState([]);
const [loading, setLoading] = useState(true);
+ const [isFiltering, setIsFiltering] = useState(false);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [editingTransaction, setEditingTransaction] = useState(null);
const [categories, setCategories] = useState([]);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [typeFilter, setTypeFilter] = useState('all');
+ const [categoryFilter, setCategoryFilter] = useState('all');
+ const [dateFrom, setDateFrom] = useState('');
+ const [dateTo, setDateTo] = useState('');
+ const debounceTimer = useRef(null); // Changed to useRef
const [isTransactionModalOpen, setIsTransactionModalOpen] = useState(false);
const [isCategoryModalOpen, setIsCategoryModalOpen] = useState(false);
const { currency } = useCurrency();
+ const isInitialMount = useRef(true);
+
+ const fetchData = useCallback(async (search = searchTerm) => {
+ if (isInitialMount.current) {
+ setLoading(true);
+ } else {
+ setIsFiltering(true);
+ }
- const fetchData = useCallback(async () => {
- setLoading(true);
try {
- const [transactionsRes, categoriesRes] = await Promise.all([
- api.get(`/transactions?page=${page}&limit=10`),
- api.get('/transactions/categories')
- ]);
+ const params = new URLSearchParams({
+ page: page.toString(),
+ limit: '10'
+ });
+
+ if (search) {
+ params.append('search', search);
+ }
+ if (typeFilter !== 'all') {
+ params.append('isIncome', typeFilter === 'income' ? 'true' : 'false');
+ }
+ if (categoryFilter !== 'all') {
+ params.append('category', categoryFilter);
+ }
+ if (dateFrom) {
+ params.append('startDate', dateFrom);
+ }
+ if (dateTo) {
+ params.append('endDate', dateTo);
+ }
+
+ const transactionsRes = await api.get(`/transactions?${params.toString()}`);
setTransactions(transactionsRes.data.transactions);
setTotalPages(transactionsRes.data.totalPages);
- setCategories(categoriesRes.data);
+
} catch (error) {
console.error("Failed to fetch transactions data", error);
} finally {
setLoading(false);
+ setIsFiltering(false);
+ isInitialMount.current = false;
}
- }, [page]);
+ }, [page, searchTerm, typeFilter, categoryFilter, dateFrom, dateTo]);
+
+ // Fetch categories only on component mount
+ useEffect(() => {
+ const fetchCategories = async () => {
+ try {
+ const categoriesRes = await api.get('/transactions/categories');
+ setCategories(categoriesRes.data);
+ } catch (error) {
+ console.error("Failed to fetch categories", error);
+ }
+ };
+ fetchCategories();
+ }, []);
+ // Fetch transactions when fetchData changes
useEffect(() => {
fetchData();
}, [fetchData]);
+ const handleSearchChange = (e) => {
+ const value = e.target.value;
+ setSearchTerm(value);
+
+ if (debounceTimer.current) {
+ clearTimeout(debounceTimer.current);
+ }
+
+ debounceTimer.current = setTimeout(() => {
+ setPage(1);
+ fetchData(value);
+ }, 300);
+ };
+
+ const clearAllFilters = () => {
+ setSearchTerm('');
+ setTypeFilter('all');
+ setCategoryFilter('all');
+ setDateFrom('');
+ setDateTo('');
+ setPage(1);
+ };
+
+ const hasActiveFilters = searchTerm || typeFilter !== 'all' || categoryFilter !== 'all' || dateFrom || dateTo;
+
const handleOpenTransactionModal = (transaction = null) => {
setEditingTransaction(transaction);
setIsTransactionModalOpen(true);
@@ -127,48 +199,174 @@ const TransactionsPage = () => {
+ {/* Search and Filters */}
+
+
+ {/* Search Bar */}
+
+
+
+
+ {/* Type Filter */}
+
+
+
+
+ {/* Category Filter */}
+
+
+
+
+ {/* Start Date */}
+
+
+ From:
+
+
{
+ setDateFrom(e.target.value);
+ setPage(1);
+ }}
+ className="w-full pl-14 pr-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+ />
+
+
+ {/* End Date */}
+
+
+ To:
+
+
{
+ setDateTo(e.target.value);
+ setPage(1);
+ }}
+ className="w-full pl-10 pr-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+ />
+
+
+
+ {/* Active Filters and Clear Button */}
+ {hasActiveFilters && (
+
+
+ Active:
+ {searchTerm && (
+
+ "{searchTerm}"
+
+ )}
+ {typeFilter !== 'all' && (
+
+ {typeFilter === 'income' ? 'Income' : 'Expense'}
+
+ )}
+ {categoryFilter !== 'all' && (
+
+ {categoryFilter}
+
+ )}
+ {dateFrom && (
+
+ From: {new Date(dateFrom).toLocaleDateString()}
+
+ )}
+ {dateTo && (
+
+ To: {new Date(dateTo).toLocaleDateString()}
+
+ )}
+
+
+
+ )}
+
+
{loading ? (
) : (
-
- {transactions.length > 0 ? (
-
-
-
- | Name |
- Category |
- Amount |
- Date |
- Actions |
-
-
-
- {transactions.map((tx) => (
-
- | {tx.name} |
- {tx.category} |
-
- {tx.isIncome ? '+' : '-'}{new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: currency.code,
- }).format(tx.cost)}
- |
- {new Date(tx.addedOn).toLocaleDateString()} |
-
-
-
- |
-
- ))}
-
-
- ) : (
-
-
-
- )}
-
-
+
+ {transactions.length > 0 ? (
+
+
+
+ | Name |
+ Category |
+ Amount |
+ Date |
+ Actions |
+
+
+
+ {transactions.map((tx) => (
+
+ | {tx.name} |
+ {tx.category} |
+
+ {tx.isIncome ? '+' : '-'}{new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: currency.code,
+ }).format(tx.cost)}
+ |
+ {new Date(tx.addedOn).toLocaleDateString()} |
+
+
+
+ |
+
+ ))}
+
+
+ ) : (
+
+
+
+ )}
+
)}