home dinh cao cua nha loai

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); // Professional, tranquil, and reassuring SVG icons for each category 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(() => { // Auto-detect system preference 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); // Responsive: scroll to top on page change useEffect(() => { if (mainRef.current) { mainRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [currentPage]); // Scroll-to-top button useEffect(() => { const handleScroll = () => { setShowScrollTop(window.scrollY > 200); }; window.addEventListener('scroll', handleScroll, { passive: true }); return () => window.removeEventListener('scroll', handleScroll); }, []); // Fetch data 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]); // Theme toggle 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); // Format currency const formatVND = (value) => { const num = parseFloat(value) || 0; return new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(num); }; // Total const totalAmount = useMemo(() => categories.reduce((sum, cat) => sum + parseFloat(allocations[cat.value] || 0), 0) , [allocations]); // Chart data 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%', }; // Month filter 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]); // Filtered transactions 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]); // Pagination const totalPages = Math.ceil(filteredTransactions.length / transactionsPerPage); const paginatedTransactions = filteredTransactions.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]); // UI Handlers const toggleMonth = (monthYear) => { setExpandedMonths((prev) => ({ ...prev, [monthYear]: !prev[monthYear], })); }; const toggleSearchMenu = () => setIsSearchMenuOpen((prev) => !prev); const handleResetSearch = () => { setMinAmount(''); setMaxAmount(''); setSearchDetails(''); setSearchLocation(''); setSelectedCategory(''); setSelectedMonthYear(''); setCurrentPage(1); }; // Keyboard accessibility for search menu useEffect(() => { const handleKeyDown = (e) => { if (isSearchMenuOpen && e.key === 'Escape') { setIsSearchMenuOpen(false); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isSearchMenuOpen]); // Main UI 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) => { 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] && (
{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