import React, { useMemo, useState } from 'react'; import { Transaction, TransactionType, ExpenseCategory, IncomeCategory } from '../types'; import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts'; import EditTransactionModal from './EditTransactionModal'; interface LedgerPageProps { transactions: Transaction[]; onUpdateTransaction: (t: Transaction) => void; onDeleteTransaction: (id: string) => void; } const COLORS = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444', '#6b7280']; const LedgerPage: React.FC = ({ transactions, onUpdateTransaction, onDeleteTransaction }) => { const [editingTransaction, setEditingTransaction] = useState(null); // Group by date const groupedTransactions = useMemo(() => { const groups: { [key: string]: Transaction[] } = {}; transactions.forEach(t => { if (!groups[t.date]) groups[t.date] = []; groups[t.date].push(t); }); // Sort dates descending return Object.entries(groups).sort((a, b) => new Date(b[0]).getTime() - new Date(a[0]).getTime()); }, [transactions]); const totalExpense = useMemo(() => transactions.filter(t => t.type === TransactionType.EXPENSE).reduce((sum, t) => sum + t.amount, 0), [transactions]); const totalIncome = useMemo(() => transactions.filter(t => t.type === TransactionType.INCOME).reduce((sum, t) => sum + t.amount, 0), [transactions]); const balance = totalIncome - totalExpense; // Category Data for Pie Chart (Expenses Only) const expenseCategoryData = useMemo(() => { const map = new Map(); transactions.filter(t => t.type === TransactionType.EXPENSE).forEach(t => { map.set(t.category, (map.get(t.category) || 0) + t.amount); }); return Array.from(map.entries()) .map(([name, value], index) => ({ name, value, fill: COLORS[index % COLORS.length] })) .sort((a, b) => b.value - a.value); }, [transactions]); const getIcon = (t: Transaction) => { if (t.type === TransactionType.INCOME) { switch (t.category) { case IncomeCategory.SALARY: return '💰'; case IncomeCategory.BONUS: return '🧧'; case IncomeCategory.INVESTMENT: return '📈'; default: return '💵'; } } switch (t.category) { case ExpenseCategory.FOOD: return '🍔'; case ExpenseCategory.SHOPPING: return '🛍️'; case ExpenseCategory.TRANSPORT: return '🚗'; case ExpenseCategory.ENTERTAINMENT: return '🎬'; case ExpenseCategory.HOUSING: return '🏠'; case ExpenseCategory.MEDICAL: return '💊'; default: return '🧾'; } }; const handleSaveEdit = (updated: Transaction) => { onUpdateTransaction(updated); setEditingTransaction(null); }; const handleDelete = (id: string) => { onDeleteTransaction(id); setEditingTransaction(null); }; return (
{/* Header Summary Cards */}
收入 (Income) +¥{totalIncome.toFixed(0)}
结余 (Balance) ¥{balance.toFixed(0)}
支出 (Expense) -¥{totalExpense.toFixed(0)}
{/* Charts Section (Expenses) */} {expenseCategoryData.length > 0 && (

支出构成

{expenseCategoryData.map((entry, index) => ( ))} `¥${value.toFixed(2)}`} />
)} {/* Transactions List */}

收支明细

{groupedTransactions.map(([date, items]) => { const dayIncome = items.filter(t => t.type === TransactionType.INCOME).reduce((s,t)=>s+t.amount,0); const dayExpense = items.filter(t => t.type === TransactionType.EXPENSE).reduce((s,t)=>s+t.amount,0); return (
{date}
{dayIncome > 0 && 收 ¥{dayIncome}} {dayExpense > 0 && 支 ¥{dayExpense}}
{items.slice().reverse().map((t, idx) => (
setEditingTransaction(t)} className={`flex items-center justify-between p-4 hover:bg-gray-50 active:bg-gray-100 cursor-pointer transition-colors ${idx !== items.length - 1 ? 'border-b border-gray-50' : ''}`} >
{getIcon(t)}

{t.item}

{t.category}

{t.type === TransactionType.INCOME ? '+' : '-'} {t.amount.toFixed(2)}
))}
)})} {transactions.length === 0 && (

💸

还没有账目

告诉崔经理你赚了多少或花了多少!

)}
{/* Edit Modal */} {editingTransaction && ( setEditingTransaction(null)} /> )}
); }; export default LedgerPage;