import React, { useState, useEffect, useMemo } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import axios from 'axios';
import { Doughnut } from 'react-chartjs-2';
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';
import numberToWords from '../utils/numberToWords';
import '../styles/pages/Home.css';
ChartJS.register(ArcElement, Tooltip, Legend);
function Home() {
const [allocations, setAllocations] = useState({
essentials: 0,
savings: 0,
selfInvestment: 0,
charity: 0,
emergency: 0,
});
const [transactionHistory, setTransactionHistory] = useState([]);
const [isDarkMode, setIsDarkMode] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [minAmount, setMinAmount] = useState('');
const [maxAmount, setMaxAmount] = useState('');
const [searchDetails, setSearchDetails] = useState('');
const [searchLocation, setSearchLocation] = useState('');
const [selectedCategory, setSelectedCategory] = useState('');
const [selectedMonthYear, setSelectedMonthYear] = useState('');
const [isSearchMenuOpen, setIsSearchMenuOpen] = useState(false);
const [expandedMonths, setExpandedMonths] = useState({});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const transactionsPerPage = 5;
const navigate = useNavigate();
const token = localStorage.getItem('token');
const categories = [
{ value: 'essentials', label: 'Tiêu dùng thiết yếu (50%)', color: '#10B981' },
{ value: 'savings', label: 'Tiết kiệm bắt buộc (20%)', color: '#3B82F6' },
{ value: 'selfInvestment', label: 'Đầu tư bản thân (15%)', color: '#F59E0B' },
{ value: 'charity', label: 'Từ thiện (5%)', color: '#8B5CF6' },
{ value: 'emergency', label: 'Dự phòng linh hoạt (10%)', color: '#F97316' },
];
useEffect(() => {
const fetchData = async () => {
if (!token) {
navigate('/login');
return;
}
setIsLoading(true);
try {
const [allocRes, expenseRes] = await Promise.all([
axios.get('https://backend-rockefeller-finance.onrender.com/api/allocations', {
headers: { Authorization: `Bearer ${token}` },
}),
axios.get('https://backend-rockefeller-finance.onrender.com/api/expenses', {
headers: { Authorization: `Bearer ${token}` },
}),
]);
setAllocations(allocRes.data);
setTransactionHistory(
expenseRes.data.map((tx) => ({
...tx,
category: {
'Tiêu dùng thiết yếu': 'essentials',
'Tiết kiệm bắt buộc': 'savings',
'Đầu tư bản thân': 'selfInvestment',
'Từ thiện': 'charity',
'Dự phòng linh hoạt': 'emergency',
}[tx.category],
details: tx.purpose,
timestamp: tx.date,
}))
);
setError('');
} catch (err) {
if (err.response?.status === 401) {
localStorage.removeItem('token');
navigate('/login');
setError('Phiên đăng nhập hết hạn. Vui lòng đăng nhập lại.');
} else {
setError(err.response?.data?.error || 'Lỗi tải dữ liệu');
}
} finally {
setIsLoading(false);
}
};
fetchData();
}, [token, navigate]);
const toggleDarkMode = () => {
setIsDarkMode(!isDarkMode);
};
const formatVND = (value) => {
const num = parseFloat(value) || 0;
return new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(num);
};
const totalAmount =
parseFloat(allocations.essentials || 0) +
parseFloat(allocations.savings || 0) +
parseFloat(allocations.selfInvestment || 0) +
parseFloat(allocations.charity || 0) +
parseFloat(allocations.emergency || 0);
const recentTransactionsForChart = transactionHistory.slice(0, 5);
const categoryTotals = categories.reduce((acc, cat) => {
acc[cat.value] = recentTransactionsForChart
.filter((tx) => tx?.category === cat.value)
.reduce((sum, tx) => sum + (parseFloat(tx?.amount) || 0), 0);
return acc;
}, { essentials: 0, savings: 0, selfInvestment: 0, charity: 0, emergency: 0 });
const chartData = {
labels: categories.map((cat) => cat.label),
datasets: [
{
data: categories.map((cat) => categoryTotals[cat.value]),
backgroundColor: categories.map((cat) => cat.color),
hoverBackgroundColor: categories.map((cat) => cat.color + 'CC'),
borderWidth: 1,
borderColor: isDarkMode ? '#1F2937' : '#FFFFFF',
},
],
};
const chartOptions = {
plugins: {
legend: {
position: 'bottom',
labels: {
color: isDarkMode ? '#FFFFFF' : '#1F2937',
font: { size: 14 },
},
},
tooltip: {
callbacks: {
label: function (context) {
return `${context.label}: ${formatVND(context.raw)}`;
},
},
},
},
maintainAspectRatio: false,
responsive: true,
};
const availableMonths = useMemo(() => {
const months = new Set();
transactionHistory.forEach((transaction) => {
if (!transaction?.timestamp) return;
const date = new Date(transaction.timestamp.split('/').reverse().join('-'));
const monthYear = date.toLocaleString('vi-VN', { month: 'long', year: 'numeric' });
months.add(monthYear);
});
return [...months].sort((a, b) => {
const dateA = new Date(a.split(' ')[1], new Date().toLocaleString('vi-VN', { month: 'long' }).indexOf(a.split(' ')[0]));
const dateB = new Date(b.split(' ')[1], new Date().toLocaleString('vi-VN', { month: 'long' }).indexOf(b.split(' ')[0]));
return dateB - dateA;
});
}, [transactionHistory]);
const filteredTransactions = useMemo(() => {
return transactionHistory.filter((tx) => {
if (!tx) return false;
const amount = parseFloat(tx.amount || 0);
const min = parseFloat(minAmount) || 0;
const max = parseFloat(maxAmount) || Infinity;
const amountMatch = amount >= min && amount <= max;
const detailsMatch = searchDetails ? (tx.details || '').toLowerCase().includes(searchDetails.toLowerCase()) : true;
const locationMatch = searchLocation ? (tx.location || '').toLowerCase().includes(searchLocation.toLowerCase()) : true;
const categoryMatch = selectedCategory ? tx.category === selectedCategory : true;
const monthMatch = selectedMonthYear
? new Date(tx.timestamp.split('/').reverse().join('-')).toLocaleString('vi-VN', { month: 'long', year: 'numeric' }) === selectedMonthYear
: true;
return amountMatch && detailsMatch && locationMatch && categoryMatch && monthMatch;
});
}, [transactionHistory, minAmount, maxAmount, searchDetails, searchLocation, selectedCategory, selectedMonthYear]);
const totalPages = Math.ceil(filteredTransactions.length / transactionsPerPage);
const paginatedTransactions = filteredTransactions.slice(
(currentPage - 1) * transactionsPerPage,
currentPage * transactionsPerPage
);
const paginatedGrouped = useMemo(() => {
const grouped = {};
paginatedTransactions.forEach((transaction) => {
if (!transaction?.timestamp) return;
const date = new Date(transaction.timestamp.split('/').reverse().join('-'));
const monthYear = date.toLocaleString('vi-VN', { month: 'long', year: 'numeric' });
if (!grouped[monthYear]) {
grouped[monthYear] = [];
}
grouped[monthYear].push(transaction);
});
return grouped;
}, [paginatedTransactions]);
const toggleMonth = (monthYear) => {
setExpandedMonths((prev) => ({
...prev,
[monthYear]: !prev[monthYear],
}));
};
const toggleSearchMenu = () => {
setIsSearchMenuOpen((prev) => !prev);
};
const handleResetSearch = () => {
setMinAmount('');
setMaxAmount('');
setSearchDetails('');
setSearchLocation('');
setSelectedCategory('');
setSelectedMonthYear('');
setCurrentPage(1);
};
return (
);
}
export default Home;
{isSearchMenuOpen && (
)}
{isLoading && (
)}
{error && (
{categories.map((cat) => (
))}
{Object.keys(paginatedGrouped).length > 0 ? (
<>
{Object.keys(paginatedGrouped).sort((a, b) => {
const dateA = new Date(a.split(' ')[1], new Date().toLocaleString('vi-VN', { month: 'long' }).indexOf(a.split(' ')[0]));
const dateB = new Date(b.split(' ')[1], new Date().toLocaleString('vi-VN', { month: 'long' }).indexOf(b.split(' ')[0]));
return dateB - dateA;
}).map((monthYear) => (
{error}
)}
{successMessage && (
{successMessage}
)}
{!isLoading && (
<>
Bảng điều khiển
*32 Lá Thư*: "Kỷ luật tài chính bắt đầu từ việc theo dõi chi tiêu hàng ngày. Hãy kiểm tra ngân sách của bạn thường xuyên!"
Tổng số dư
{formatVND(totalAmount)}
{numberToWords(totalAmount)}
{cat.label}
{formatVND(allocations[cat.value] || 0)}
Phân tích chi tiêu gần đây
{recentTransactionsForChart.length > 0 ? (
) : (
Chưa có giao dịch để phân tích.
)}Giao dịch gần đây
setMinAmount(e.target.value)}
className={`w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition ${isDarkMode ? 'bg-gray-800 text-white border-gray-600' : 'bg-white text-gray-800 border-gray-300'}`}
placeholder="Số tiền tối thiểu"
/>
setMaxAmount(e.target.value)}
className={`w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition ${isDarkMode ? 'bg-gray-800 text-white border-gray-600' : 'bg-white text-gray-800 border-gray-300'}`}
placeholder="Số tiền tối đa"
/>
setSearchDetails(e.target.value)}
className={`w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition ${isDarkMode ? 'bg-gray-800 text-white border-gray-600' : 'bg-white text-gray-800 border-gray-300'}`}
placeholder="Nhập chi tiết (ví dụ: Mua thực phẩm)"
/>
setSearchLocation(e.target.value)}
className={`w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition ${isDarkMode ? 'bg-gray-800 text-white border-gray-600' : 'bg-white text-gray-800 border-gray-300'}`}
placeholder="Nhập vị trí (ví dụ: Siêu thị VinMart)"
/>
{isSearchMenuOpen && (
)}
{expandedMonths[monthYear] && (
))}
{paginatedGrouped[monthYear].map((transaction, index) => (
))}
)}
Danh mục: {categories.find(cat => cat.value === transaction.category)?.label || 'Không xác định'}
Số tiền: {formatVND(transaction.amount)}
Chi tiết: {transaction.details || 'Không có'}
Vị trí: {transaction.location || 'Không có'}
Thời gian: {transaction.timestamp || 'Không có'}
{`Trang ${currentPage} / ${totalPages || 1}`}
>
) : (
Không tìm thấy giao dịch phù hợp với bộ lọc.
)}
>
)}