home ung

import React, { useState, useEffect, useMemo } from 'react'; import { useNavigate, Link } 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); function Home() { const [allocations, setAllocations] = useState({ essentials: 0, savings: 0, selfInvestment: 0, charity: 0, emergency: 0, }); const [transactionHistory, setTransactionHistory] = useState([]); const [isDarkMode, setIsDarkMode] = useState(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 transactionsPerPage = 5; const navigate = useNavigate(); const token = localStorage.getItem('token'); 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' }, ]; 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(); }, [token, navigate]); 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 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}: ${formatVND(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('/').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 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( (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); }; return (
{isSearchMenuOpen && (
)}
{isLoading && (
)} {error && (
{error}
)} {successMessage && (
{successMessage}
)} {!isLoading && ( <>

Bảng điều khiển

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

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

setMinAmount(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'}`} placeholder="Số tiền tối thiểu" />
setMaxAmount(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'}`} placeholder="Số tiền tối đa" />
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'}`} 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'}`} placeholder="Nhập vị trí (ví dụ: Siêu thị VinMart)" />
{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}`}
) : (
Không tìm thấy giao dịch phù hợp với bộ lọc.
)} )}
); } 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