import React, { useMemo, useState, useEffect } from 'react'; import { Transaction, TransactionType, ExpenseCategory, IncomeCategory, BillReminder } from '../types'; import EditTransactionModal from './EditTransactionModal'; import CalendarView from './CalendarView'; import BillReminders, { getDaysRemaining } from './BillReminders'; interface LedgerPageProps { transactions: Transaction[]; onUpdateTransaction: (t: Transaction) => Promise; onDeleteTransaction: (id: string) => Promise; budgets: Record; setBudgets: (val: Record) => void; reminders: BillReminder[]; onAddReminder: (r: BillReminder) => void; onUpdateReminder: (r: BillReminder) => void; onDeleteReminder: (id: string) => void; onToggleReminder: (id: string) => void; } const CARD_THEMES = [ { id: 'dark-indigo', class: 'from-gray-900 via-indigo-950 to-black', themeColor: 'bg-indigo-600', name: '暗夜霓虹' }, { id: 'sunset', class: 'from-orange-500 via-rose-600 to-pink-700', themeColor: 'bg-rose-600', name: '落日熔金' }, { id: 'emerald', class: 'from-emerald-600 via-teal-700 to-cyan-900', themeColor: 'bg-emerald-600', name: '薄荷森林' }, { id: 'royal', class: 'from-blue-700 via-indigo-800 to-indigo-950', themeColor: 'bg-blue-700', name: '皇家礼赞' }, { id: 'rose', class: 'from-rose-500 via-pink-600 to-purple-800', themeColor: 'bg-purple-600', name: '浪漫巴黎' }, ]; const getLocalDateString = (date = new Date()) => { const offset = date.getTimezoneOffset(); const localDate = new Date(date.getTime() - (offset * 60 * 1000)); return localDate.toISOString().split('T')[0]; }; const LedgerPage: React.FC = ({ transactions, onUpdateTransaction, onDeleteTransaction, budgets, reminders, onAddReminder, onUpdateReminder, onDeleteReminder, onToggleReminder }) => { const [editingTransaction, setEditingTransaction] = useState(null); const [subTab, setSubTab] = useState<'details' | 'calendar' | 'bills'>('details'); const [selectedMonth, setSelectedMonth] = useState(getLocalDateString().slice(0, 7)); const [themeIndex, setThemeIndex] = useState(() => Number(localStorage.getItem('cui_card_theme')) || 0); useEffect(() => { localStorage.setItem('cui_card_theme', themeIndex.toString()); }, [themeIndex]); const currentTheme = CARD_THEMES[themeIndex]; // Monthly stats for the top cards const monthlyTransactions = useMemo(() => transactions.filter(t => t.date.startsWith(selectedMonth)), [transactions, selectedMonth]); const currentMonthExpense = useMemo(() => monthlyTransactions.filter(t => t.type === TransactionType.EXPENSE).reduce((sum, t) => sum + t.amount, 0), [monthlyTransactions]); const currentMonthIncome = useMemo(() => monthlyTransactions.filter(t => t.type === TransactionType.INCOME).reduce((sum, t) => sum + t.amount, 0), [monthlyTransactions]); const todayExpense = useMemo(() => transactions.filter(t => t.date === getLocalDateString() && t.type === TransactionType.EXPENSE).reduce((sum, t) => sum + t.amount, 0), [transactions]); const currentMonthBalance = currentMonthIncome - currentMonthExpense; const cycleTheme = () => setThemeIndex((prev) => (prev + 1) % CARD_THEMES.length); const getIcon = (t: Transaction) => { if (t.type === TransactionType.INCOME) return '💰'; switch (t.category) { case ExpenseCategory.FOOD: return '🍔'; case ExpenseCategory.SHOPPING: return '🛍️'; case ExpenseCategory.TRANSPORT: return '🚗'; case ExpenseCategory.HOUSING: return '🏠'; case ExpenseCategory.ENTERTAINMENT: return '🎬'; case ExpenseCategory.MEDICAL: return '💊'; case ExpenseCategory.SOCIAL: return '🤝'; case ExpenseCategory.EDUCATION: return '📚'; case ExpenseCategory.FINANCE: return '🏦'; case ExpenseCategory.CHILD: return '👶'; case ExpenseCategory.PET: return '🐱'; case ExpenseCategory.CAR: return '🚘'; case ExpenseCategory.BEAUTY: return '💄'; case ExpenseCategory.BUSINESS: return '💼'; case ExpenseCategory.MISC: return '🧺'; default: return '🧾'; } }; // Grouped transactions for "Details" - Filtered by selectedMonth const allGroupedTransactions = useMemo(() => { const grouped = monthlyTransactions.reduce((acc, t) => { if (!acc[t.date]) acc[t.date] = []; acc[t.date].push(t); return acc; }, {} as Record); return Object.entries(grouped).sort((a, b) => new Date(b[0]).getTime() - new Date(a[0]).getTime()); }, [monthlyTransactions]); return (

我的账本

Financial Ledger

setSelectedMonth(e.target.value)} className="bg-gray-100 rounded-lg px-3 py-1.5 text-sm font-bold text-gray-600 outline-none border-none shadow-sm focus:ring-1 focus:ring-indigo-300 transition-all" />
本月总支出
-¥{currentMonthExpense.toFixed(0)}
今日支出
-¥{todayExpense.toFixed(0)}
本月总收入
+¥{currentMonthIncome.toFixed(0)}
本月结余
{currentMonthBalance < 0 ? '-' : ''}¥{Math.abs(currentMonthBalance).toFixed(0)}
{subTab === 'details' && (
{allGroupedTransactions.map(([date, items]) => { const dayIncome = items.filter((t:any) => t.type === TransactionType.INCOME).reduce((s:any,t:any)=>s+t.amount,0); const dayExpense = items.filter((t:any) => t.type === TransactionType.EXPENSE).reduce((s:any,t:any)=>s+t.amount,0); const dObj = new Date(date); const isToday = getLocalDateString() === date; return (
{isToday ? '今天' : `${dObj.getMonth()+1}月${dObj.getDate()}日`} | {['周日','周一','周二','周三','周四','周五','周六'][dObj.getDay()]}
{dayIncome > 0 && 收 +¥{dayIncome.toFixed(0)}} {dayExpense > 0 && 支 -¥{dayExpense.toFixed(0)}}
{items.slice().reverse().map((t:any) => (
setEditingTransaction(t)} className="flex items-center justify-between py-4 px-4 hover:bg-gray-50 active:bg-gray-100 cursor-pointer transition-colors">
{getIcon(t)}

{t.item}

{t.category}

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

本月暂无明细

}
)} {subTab === 'calendar' && } {subTab === 'bills' && r.frequency === 'monthly' || r.targetDate.startsWith(selectedMonth))} onAdd={onAddReminder} onUpdate={onUpdateReminder} onDelete={onDeleteReminder} onToggle={onToggleReminder} />}
{editingTransaction && ( { try { await onUpdateTransaction(updated); setEditingTransaction(null); } catch (e) { // Error handled in App.tsx } }} onDelete={async (id) => { await onDeleteTransaction(id); setEditingTransaction(null); }} onClose={() => setEditingTransaction(null)} /> )}
); }; export default LedgerPage;