home quan ly tai chinh da chinh sua hoan hao

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'; import '../styles/pages/Home.css'; 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 }, ]; // Tạo state khởi tạo cho visibility const initialVisibilityState = categories.reduce((acc, category) => { acc[category.value] = false; return acc; }, { total: false }); // Thêm 'total' cho thẻ tổng số dư 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); // State mới để quản lý ẩn/hiện cho từng phần tử const [visibility, setVisibility] = useState(initialVisibilityState); const transactionsPerPage = 6; const navigate = useNavigate(); const token = localStorage.getItem('token'); const mainRef = useRef(null); // Hàm bật/tắt cho một phần tử cụ thể dựa vào key const toggleVisibility = (key) => { setVisibility(prev => ({ ...prev, [key]: !prev[key], })); }; // Hàm bật/tắt tất cả (cho nút con mắt) const toggleAllVisibility = () => { // Nếu có ít nhất một mục đang ẩn, thì hiện tất cả. Ngược lại, ẩn tất cả. const shouldShowAll = Object.values(visibility).some(v => v === false); const newState = Object.keys(visibility).reduce((acc, key) => { acc[key] = shouldShowAll; return acc; }, {}); setVisibility(newState); }; // Biến để quyết định icon con mắt nào sẽ hiển thị const isAnyAmountVisible = Object.values(visibility).some(v => v === true); 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); const paginatedTransactions = filteredTransactions .slice() .reverse() .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); }; useEffect(() => { const handleKeyDown = (e) => { if (isSearchMenuOpen && e.key === 'Escape') { setIsSearchMenuOpen(false); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isSearchMenuOpen]); const sortTransactionsByDateDesc = (transactions) => { return [...transactions].sort((a, b) => { 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 (

Bảng điều khiển

{isLoading && (
)} {error && (
{error}
)} {successMessage && (
{successMessage}
)} {!isLoading && ( <>

*38 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ư

toggleVisibility('total')}>

{visibility.total ? formatVND(totalAmount) : '******** VND'}

{visibility.total ? numberToWords(totalAmount) : '********'}

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

{cat.label}

{visibility[cat.value] ? formatVND(allocations[cat.value] || 0) : '******** VND'}

))}

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.

)}

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" />
{isSearchMenuOpen && ( <>
setIsSearchMenuOpen(false)} aria-label="Đóng bộ lọc nâng cao" />
e.stopPropagation()} >
)}

Giao dịch gần đây

{Object.keys(paginatedGrouped).length > 0 ? ( <> {Object.keys(paginatedGrouped).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; }).map((monthYear) => (
{expandedMonths[monthYear] && (
{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ó}
))}
)}
))}
{`Trang ${currentPage} / ${totalPages || 1}`}
) : (
Không tìm thấy giao dịch phù hợp với bộ lọc.
)}
)}
{showScrollTop && ( )}
); } 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