expenes quản lý tài chính đã chỉnh sửa hoan hao

import React, { useState, useEffect, useRef } from 'react'; import axios from 'axios'; import numberToWords from '../utils/numberToWords'; // Category config to match Home.js const categories = [ { value: 'essentials', label: 'Tiêu dùng thiết yếu', color: '#22c55e', icon: ( ), }, { value: 'savings', label: 'Tiết kiệm bắt buộc', color: '#3b82f6', icon: ( ), }, { value: 'selfInvestment', label: 'Đầu tư bản thân', color: '#eab308', icon: ( ), }, { value: 'charity', label: 'Từ thiện', color: '#a21caf', icon: ( ), }, { value: 'emergency', label: 'Dự phòng linh hoạt', color: '#f97316', icon: ( ), }, ]; // Tạo state khởi tạo cho visibility const initialVisibilityState = categories.reduce((acc, category) => { acc[category.value] = false; return acc; }, { budgetBalance: false, totalAllocations: false }); function Expenses() { // State const [amount, setAmount] = useState(''); const [category, setCategory] = useState(''); const [date, setDate] = useState(''); const [purpose, setPurpose] = useState(''); const [location, setLocation] = useState(''); const [initialBudget, setInitialBudget] = useState(null); const [newBudget, setNewBudget] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [allocations, setAllocations] = useState({ essentials: 0, savings: 0, selfInvestment: 0, charity: 0, emergency: 0, }); const [expenses, setExpenses] = useState([]); const [error, setError] = useState(''); const [isDarkMode, setIsDarkMode] = useState( () => window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ); const [showSnackbar, setShowSnackbar] = useState(false); const [snackbarMsg, setSnackbarMsg] = useState(''); const [showAllExpenses, setShowAllExpenses] = useState(false); // For expanding/collapsing expense history const [visibility, setVisibility] = useState(initialVisibilityState); // State ẩn/hiện const token = localStorage.getItem('token'); const snackbarTimeout = useRef(null); // Hàm bật/tắt cho một phần tử cụ thể const toggleVisibility = (key) => { setVisibility(prev => ({ ...prev, [key]: !prev[key] })); }; // Biến kiểm tra xem có mục nào đang hiển thị không const isAnyAmountVisible = Object.values(visibility).some(v => v === true); // Hàm bật/tắt tất cả const toggleAllVisibility = () => { const shouldShowAll = !isAnyAmountVisible; const newState = Object.keys(visibility).reduce((acc, key) => { acc[key] = shouldShowAll; return acc; }, {}); setVisibility(newState); }; // Theme sync (like Home.js) 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]); // Hàm quản lý hiển thị thông báo (snackbar) const showNotification = (message) => { setSnackbarMsg(message); setShowSnackbar(true); if (snackbarTimeout.current) { clearTimeout(snackbarTimeout.current); } snackbarTimeout.current = setTimeout(() => { setShowSnackbar(false); }, 3000); // Tự động ẩn sau 3 giây }; // Fetch data useEffect(() => { const fetchData = async () => { try { const [budgetRes, expensesRes, allocationsRes] = await Promise.all([ axios.get('https://backend-rockefeller-finance.onrender.com/api/initial-budget', { headers: { Authorization: `Bearer ${token}` }, }), axios.get('https://backend-rockefeller-finance.onrender.com/api/expenses', { headers: { Authorization: `Bearer ${token}` }, }), axios.get('https://backend-rockefeller-finance.onrender.com/api/allocations', { headers: { Authorization: `Bearer ${token}` }, }), ]); setInitialBudget(budgetRes.data.initialBudget); setExpenses(expensesRes.data); setAllocations(allocationsRes.data); } catch (err) { setError(err.response?.data?.error || 'Lỗi lấy dữ liệu'); } }; if (token) fetchData(); // eslint-disable-next-line }, [token]); // Format currency const formatVND = (value) => { const num = parseFloat(value) || 0; return new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(num); }; // Budget submit const handleBudgetSubmit = async (e) => { e.preventDefault(); const parsedBudget = parseFloat(newBudget); if (isNaN(parsedBudget) || parsedBudget <= 0) { showNotification('Vui lòng nhập số tiền hợp lệ!'); return; } setIsSubmitting(true); try { const response = await axios.post( 'https://backend-rockefeller-finance.onrender.com/api/initial-budget', { initialBudget: parsedBudget }, { headers: { Authorization: `Bearer ${token}` } } ); setInitialBudget(response.data.initialBudget); setAllocations(response.data.allocations); setNewBudget(''); setError(''); showNotification('Ngân sách đã được cập nhật!'); } catch (err) { showNotification(err.response?.data?.error || 'Lỗi lưu ngân sách'); } finally { setIsSubmitting(false); } }; // Expense submit const handleExpenseSubmit = async (e) => { e.preventDefault(); const parsedAmount = parseFloat(amount); if (isNaN(parsedAmount) || parsedAmount <= 0) { showNotification('Vui lòng nhập số tiền hợp lệ!'); return; } if (!purpose || !location) { showNotification('Vui lòng nhập mục đích và vị trí!'); return; } setIsSubmitting(true); try { const response = await axios.post( 'https://backend-rockefeller-finance.onrender.com/api/expenses', { amount: parsedAmount, category, purpose, location, date: date || new Date().toLocaleDateString('vi-VN'), }, { headers: { Authorization: `Bearer ${token}` } } ); setExpenses(response.data); // Update allocations and budget const [budgetRes, allocRes] = await Promise.all([ axios.get('https://backend-rockefeller-finance.onrender.com/api/initial-budget', { headers: { Authorization: `Bearer ${token}` }, }), axios.get('https://backend-rockefeller-finance.onrender.com/api/allocations', { headers: { Authorization: `Bearer ${token}` }, }), ]); setInitialBudget(budgetRes.data.initialBudget); setAllocations(allocRes.data); setAmount(''); setCategory(''); setPurpose(''); setLocation(''); setDate(''); setError(''); showNotification('Chi tiêu đã được thêm!'); } catch (err) { showNotification(err.response?.data?.error || 'Lỗi thêm chi tiêu'); } finally { setIsSubmitting(false); } }; // Delete expense const handleDeleteExpense = async (index) => { try { const response = await axios.delete( `https://backend-rockefeller-finance.onrender.com/api/expenses/${index}`, { headers: { Authorization: `Bearer ${token}` } } ); setExpenses(response.data); // Update budget and allocations const [budgetRes, allocRes] = await Promise.all([ axios.get('https://backend-rockefeller-finance.onrender.com/api/initial-budget', { headers: { Authorization: `Bearer ${token}` }, }), axios.get('https://backend-rockefeller-finance.onrender.com/api/allocations', { headers: { Authorization: `Bearer ${token}` }, }), ]); setInitialBudget(budgetRes.data.initialBudget); setAllocations(allocRes.data); showNotification('Đã xóa chi tiêu!'); } catch (err) { showNotification(err.response?.data?.error || 'Lỗi xóa chi tiêu'); } }; // Theme toggle const toggleDarkMode = () => setIsDarkMode((prev) => !prev); // Total allocations const totalAmount = parseFloat(allocations.essentials || 0) + parseFloat(allocations.savings || 0) + parseFloat(allocations.selfInvestment || 0) + parseFloat(allocations.charity || 0) + parseFloat(allocations.emergency || 0); // Expense history logic const sortedExpenses = [...expenses].reverse(); const COLLAPSED_COUNT = 1; return (
{/* Snackbar */}
{snackbarMsg}
{/* Topbar - match Home.js */}

Quản lý chi tiêu

{/* Error - Chỉ hiển thị lỗi tải trang ban đầu */} {error && !initialBudget && (
{error}
)} {/* Budget input */} {initialBudget === null || initialBudget === 0 ? (

Nhập ngân sách ban đầu

setNewBudget(e.target.value)} className={`input-modern ${isDarkMode ? 'dark' : ''}`} placeholder="Nhập ngân sách ban đầu" required min="1" inputMode="numeric" />
) : ( <> {/* Add more budget */}

Nạp thêm ngân sách

setNewBudget(e.target.value)} className={`input-modern ${isDarkMode ? 'dark' : ''}`} placeholder="Nhập số tiền nạp thêm" min="1" inputMode="numeric" />
{/* Budget balance */}

Số dư ngân sách

toggleVisibility('budgetBalance')}>

{visibility.budgetBalance ? formatVND(initialBudget) : '******** VND'}

💰
{/* Add expense */}

Thêm chi tiêu

setAmount(e.target.value)} className={`input-modern ${isDarkMode ? 'dark' : ''}`} placeholder="Nhập số tiền" required min="1" inputMode="numeric" />
{/* Mobile: show icon and color for selected category */}
{(() => { const cat = categories.find(c => c.label === category); if (!cat) return null; return ( {cat.icon} {cat.label} ); })()}
setPurpose(e.target.value)} className={`input-modern ${isDarkMode ? 'dark' : ''}`} placeholder="Nhập mục đích (ví dụ: Mua thực phẩm)" required />
setLocation(e.target.value)} className={`input-modern ${isDarkMode ? 'dark' : ''}`} placeholder="Nhập vị trí (ví dụ: Siêu thị VinMart)" required />
setDate(e.target.value)} className={`input-modern ${isDarkMode ? 'dark' : ''}`} />
{/* Expense history */}

Lịch sử chi tiêu {sortedExpenses.length > COLLAPSED_COUNT && ( )}

{sortedExpenses.length === 0 ? ( ) : ( (showAllExpenses ? sortedExpenses : sortedExpenses.slice(0, COLLAPSED_COUNT)).map((expense, index) => ( )) )}
Ngày Danh mục Số tiền (VND) Mục đích Vị trí Hành động
Không có chi tiêu nào.
{expense.date} {expense.category} {formatVND(expense.amount)} {expense.purpose} {expense.location}
{/* Budget allocations */}

Phân bổ ngân sách

{categories.map((cat) => (
toggleVisibility(cat.value)} style={{ background: isDarkMode ? `linear-gradient(135deg, ${cat.color}22 0%, #22223b 100%)` : `linear-gradient(135deg, ${cat.color}22 0%, #fff 100%)`, color: isDarkMode ? '#fff' : '#22223b', minWidth: 0, wordBreak: 'break-word', }} >
{cat.icon} {cat.label}
{visibility[cat.value] ? formatVND(allocations[cat.value] || 0) : '******** VND'}
))}
{/* Total allocations */} {totalAmount > 0 && (
toggleVisibility('totalAllocations')} >

Tổng số tiền phân bổ

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

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

)} )}
{/* Modern glassmorphism and utility classes */}
); } export default Expenses;
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