home.js

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 (
{isSearchMenuOpen && (
)}

Bảng điều khiển

Tổng số dư

{formatVND(totalAmount)}

{numberToWords(totalAmount)}

{categories.map((cat) => (

{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ả.

)}
); } export default Home;
Huyền

Một Blog Anime chia sẻ những bộ anime hay download về để xem chất lượng cao nhất. neyuhv.blogspot.com does not host any files, it merely links to 3rd party services. Legal issues should be taken up with the file hosts and providers. neyuhv.blogspot.com is not responsible for any media files shown by the video providers.

Mới hơn Cũ hơn