import React, { useState, useContext, useMemo } from 'react';
import { FinanceContext } from '../contexts/FinanceContext';
import numberToWords from '../utils/numberToWords';
import { Doughnut } from 'react-chartjs-2';
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';
import '../styles/pages/Home.css'; // Import CSS
ChartJS.register(ArcElement, Tooltip, Legend);
function Home() {
const context = useContext(FinanceContext);
const { allocations = {}, transactionHistory = [] } = context || {};
const [isDarkMode, setIsDarkMode] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [searchAmount, setSearchAmount] = 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 transactionsPerPage = 5;
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 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' },
];
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}: ${new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(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(',')[0].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 amountMatch = searchAmount ? parseFloat(tx.amount || 0) === parseFloat(searchAmount) : true;
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(',')[0].split('/').reverse().join('-')).toLocaleString('vi-VN', { month: 'long', year: 'numeric' }) === selectedMonthYear
: true;
return amountMatch && detailsMatch && locationMatch && categoryMatch && monthMatch;
});
}, [transactionHistory, searchAmount, 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(',')[0].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 = () => {
setSearchAmount('');
setSearchDetails('');
setSearchLocation('');
setSelectedCategory('');
setSelectedMonthYear('');
setCurrentPage(1);
};
return (
);
}
export default Home;
{isSearchMenuOpen && (
)}
{categories.map((cat) => (
))}
Bảng điều khiể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
setSearchAmount(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'} ${isSearchMenuOpen ? 'pointer-events-none' : ''}`}
placeholder="Nhập số tiền"
/>
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'} ${isSearchMenuOpen ? 'pointer-events-none' : ''}`}
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'} ${isSearchMenuOpen ? 'pointer-events-none' : ''}`}
placeholder="Nhập vị trí (ví dụ: Siêu thị Coopmart)"
/>
{isSearchMenuOpen && (
)}
{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) => (
{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}`}
>
) : (
Chưa có giao dịch nào hoặc không tìm thấy kết quả.
)}