home dinh cao cua nhan loai nhung khong ung lam ban chinh sua vi tri phan tu giao dich gan day trong thang

import React, { useState, useEffect, useMemo, useRef } from 'react'; import { useNavigate } 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'; ChartJS.register(ArcElement, Tooltip, Legend); const CategoryIcons = { essentials: ( ), savings: ( ), selfInvestment: ( ), charity: ( ), emergency: ( ), }; const categories = [ { value: 'essentials', label: 'Tiêu dùng thiết yếu (50%)', color: '#10B981', icon: CategoryIcons.essentials }, { value: 'savings', label: 'Tiết kiệm bắt buộc (20%)', color: '#3B82F6', icon: CategoryIcons.savings }, { value: 'selfInvestment', label: 'Đầu tư bản thân (15%)', color: '#F59E0B', icon: CategoryIcons.selfInvestment }, { value: 'charity', label: 'Từ thiện (5%)', color: '#8B5CF6', icon: CategoryIcons.charity }, { value: 'emergency', label: 'Dự phòng linh hoạt (10%)', color: '#F97316', icon: CategoryIcons.emergency }, ]; function Home() { // State const [allocations, setAllocations] = useState({ essentials: 0, savings: 0, selfInvestment: 0, charity: 0, emergency: 0, }); const [transactionHistory, setTransactionHistory] = useState([]); const [isDarkMode, setIsDarkMode] = useState(() => { if (typeof window !== 'undefined') { return window.matchMedia('(prefers-color-scheme: dark)').matches; } return 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 [showScrollTop, setShowScrollTop] = useState(false); const transactionsPerPage = 6; const navigate = useNavigate(); const token = localStorage.getItem('token'); const mainRef = useRef(null); useEffect(() => { if (mainRef.current) { mainRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [currentPage]); useEffect(() => { const handleScroll = () => { setShowScrollTop(window.scrollY > 200); }; window.addEventListener('scroll', handleScroll, { passive: true }); return () => window.removeEventListener('scroll', handleScroll); }, []); 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(); // eslint-disable-next-line }, [token, navigate]); useEffect(() => { if (isDarkMode) { document.documentElement.classList.add('dark'); document.body.style.background = '#111827'; } else { document.documentElement.classList.remove('dark'); document.body.style.background = '#F3F4F6'; } }, [isDarkMode]); const toggleDarkMode = () => setIsDarkMode((prev) => !prev); const formatVND = (value) => { const num = parseFloat(value) || 0; return new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(num); }; const totalAmount = useMemo(() => categories.reduce((sum, cat) => sum + parseFloat(allocations[cat.value] || 0), 0) , [allocations]); 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: 2, borderColor: isDarkMode ? '#1F2937' : '#FFFFFF', }, ], }; const chartOptions = { plugins: { legend: { position: 'bottom', labels: { color: isDarkMode ? '#F3F4F6' : '#1F2937', font: { size: 15, family: 'Inter, sans-serif', weight: 'bold' }, padding: 18, }, }, tooltip: { callbacks: { label: function (context) { return `${context.label}: ${formatVND(context.raw)}`; }, }, backgroundColor: isDarkMode ? '#22223b' : '#fff', titleColor: isDarkMode ? '#fff' : '#22223b', bodyColor: isDarkMode ? '#fff' : '#22223b', borderColor: isDarkMode ? '#fff' : '#22223b', borderWidth: 1, }, }, maintainAspectRatio: false, responsive: true, cutout: '70%', }; 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 [monthA, yearA] = a.split(' '); const [monthB, yearB] = b.split(' '); const dateA = new Date(yearA, new Date(Date.parse(monthA + " 1, 2000")).getMonth()); const dateB = new Date(yearB, new Date(Date.parse(monthB + " 1, 2000")).getMonth()); 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); // Đảo ngược thứ tự giao dịch để giao dịch mới nhất lên đầu const paginatedTransactions = filteredTransactions .slice() .reverse() .slice((currentPage - 1) * transactionsPerPage, currentPage * transactionsPerPage); // Group by month 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); }; useEffect(() => { const handleKeyDown = (e) => { if (isSearchMenuOpen && e.key === 'Escape') { setIsSearchMenuOpen(false); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isSearchMenuOpen]); // Helper: sort transactions by timestamp descending (mới nhất lên đầu) const sortTransactionsByDateDesc = (transactions) => { return [...transactions].sort((a, b) => { // timestamp dạng dd/mm/yyyy if (!a?.timestamp || !b?.timestamp) return 0; const [da, ma, ya] = a.timestamp.split('/').map(Number); const [db, mb, yb] = b.timestamp.split('/').map(Number); const dateA = new Date(ya, ma - 1, da); const dateB = new Date(yb, mb - 1, db); return dateB - dateA; }); }; return (
{/* Mobile Topbar */}

Bảng điều khiển

{/* Notification */} {isLoading && (
)} {error && (
{error}
)} {successMessage && (
{successMessage}
)} {!isLoading && ( <> {/* Financial Quote */}

*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!"

{/* Overview Cards */}

Tổng số dư

{formatVND(totalAmount)}

{numberToWords(totalAmount)}

{categories.map((cat) => (
{cat.icon}

{cat.label}

{formatVND(allocations[cat.value] || 0)}

))}
{/* Chart */}

Phân tích chi tiêu gần đây

{recentTransactionsForChart.length > 0 ? (
Tổng {formatVND( recentTransactionsForChart.reduce((sum, tx) => sum + (parseFloat(tx.amount) || 0), 0) )}
) : (

Chưa có giao dịch để phân tích.

)}
{/* Filters */}

Bộ lọc giao dịch

setMinAmount(e.target.value)} className={`w-full p-2 border rounded-lg 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="Tối thiểu" min={0} />
setMaxAmount(e.target.value)} className={`w-full p-2 border rounded-lg 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="Tối đa" min={0} />
setSearchDetails(e.target.value)} className={`w-full p-2 border rounded-lg 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="Ví dụ: Mua thực phẩm" />
setSearchLocation(e.target.value)} className={`w-full p-2 border rounded-lg 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="Ví dụ: VinMart" />
{/* Advanced Filters */} {isSearchMenuOpen && ( <> {/* Overlay chỉ làm mờ nền, không làm mờ advanced filter */}
setIsSearchMenuOpen(false)} aria-label="Đóng bộ lọc nâng cao" />
e.stopPropagation()} >
)}
{/* Transactions */}

Giao dịch gần đây

{Object.keys(paginatedGrouped).length > 0 ? ( <> {Object.keys(paginatedGrouped).sort((a, b) => { // Đảo ngược thứ tự tháng: tháng mới nhất lên đầu const [monthA, yearA] = a.split(' '); const [monthB, yearB] = b.split(' '); const dateA = new Date(yearA, new Date(Date.parse(monthA + " 1, 2000")).getMonth()); const dateB = new Date(yearB, new Date(Date.parse(monthB + " 1, 2000")).getMonth()); return dateB - dateA; }).map((monthYear) => (
{expandedMonths[monthYear] && (
{/* Hiển thị giao dịch mới nhất lên đầu, sắp xếp theo timestamp giảm dần */} {sortTransactionsByDateDesc(paginatedGrouped[monthYear]).map((transaction, index) => (
{categories.find(cat => cat.value === transaction.category)?.icon} {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ó}
))}
)}
))} {/* Pagination */}
{`Trang ${currentPage} / ${totalPages || 1}`}
) : (
Không tìm thấy giao dịch phù hợp với bộ lọc.
)}
)}
{/* Scroll to top button */} {showScrollTop && ( )} {/* Đã chuyển overlay và advanced filter thành fixed, overlay không làm mờ advanced filter nữa. */}
); } 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