import React, { useState, useEffect, useRef } from 'react'; import { Wallet, Trash2, TrendingUp, TrendingDown, History, X, Plus, Repeat, ArrowRightCircle, Search, Filter, Calendar, RefreshCw, Zap, Tag, Copy, PieChart, Calculator, CalendarDays, List } from 'lucide-react'; export default function App() { // --- 預設資料 --- const defaultExpenseCategories = [ { id: 'food', name: '食', icon: '🍜', color: 'bg-orange-100 text-orange-600 border-orange-200' }, { id: 'clothing', name: '衣', icon: '👕', color: 'bg-pink-100 text-pink-600 border-pink-200' }, { id: 'housing', name: '住', icon: '🏠', color: 'bg-blue-100 text-blue-600 border-blue-200' }, { id: 'transport', name: '行', icon: '🚗', color: 'bg-green-100 text-green-600 border-green-200' }, { id: 'education', name: '育', icon: '📚', color: 'bg-indigo-100 text-indigo-600 border-indigo-200' }, { id: 'entertainment', name: '樂', icon: '🎮', color: 'bg-purple-100 text-purple-600 border-purple-200' }, ]; const defaultIncomeCategories = [ { id: 'amway', name: '安麗獎金', icon: '💎', color: 'bg-emerald-100 text-emerald-600 border-emerald-200' }, { id: 'stock', name: '股票', icon: '📈', color: 'bg-red-100 text-red-600 border-red-200' }, { id: 'card', name: '牌卡', icon: '🎴', color: 'bg-violet-100 text-violet-600 border-violet-200' }, { id: 'crystal', name: '水晶', icon: '✨', color: 'bg-cyan-100 text-cyan-600 border-cyan-200' }, { id: 'other', name: '其他收入', icon: '💰', color: 'bg-gray-100 text-gray-600 border-gray-200' }, ]; // --- 狀態管理 --- const [transactions, setTransactions] = useState(() => { const saved = localStorage.getItem('my-simple-account-book'); return saved ? JSON.parse(saved) : []; }); const [expenseCats, setExpenseCats] = useState(() => { const saved = localStorage.getItem('my-expense-cats'); return saved ? JSON.parse(saved) : defaultExpenseCategories; }); const [incomeCats, setIncomeCats] = useState(() => { const saved = localStorage.getItem('my-income-cats'); return saved ? JSON.parse(saved) : defaultIncomeCategories; }); const [fixedItems, setFixedItems] = useState(() => { const saved = localStorage.getItem('my-fixed-items'); return saved ? JSON.parse(saved) : []; }); // 輸入狀態 const [quickAmount, setQuickAmount] = useState(''); const [quickNote, setQuickNote] = useState(''); const [quickCategory, setQuickCategory] = useState(''); const [quickType, setQuickType] = useState('expense'); // 篩選與搜尋狀態 const [searchTerm, setSearchTerm] = useState(''); const [filterMonth, setFilterMonth] = useState(''); const [filterCategory, setFilterCategory] = useState(''); // 介面狀態 const [showAddModal, setShowAddModal] = useState(false); const [showFixedModal, setShowFixedModal] = useState(false); // 固定收支預估選擇的月份 const [forecastDate, setForecastDate] = useState(new Date().toISOString().slice(0, 7)); // 新增分類輸入 const [newCatName, setNewCatName] = useState(''); // 新增固定收支輸入 const [newFixed, setNewFixed] = useState({ name: '', amount: '', type: 'expense', category: '', frequency: 'monthly', startMonth: String(new Date().getMonth() + 1) // 預設當前月份 (1-12字串) }); // 長按刪除邏輯 Refs const timerRef = useRef(null); const isLongPressRef = useRef(false); // --- 副作用:自動儲存 --- useEffect(() => { localStorage.setItem('my-simple-account-book', JSON.stringify(transactions)); }, [transactions]); useEffect(() => { localStorage.setItem('my-expense-cats', JSON.stringify(expenseCats)); localStorage.setItem('my-income-cats', JSON.stringify(incomeCats)); }, [expenseCats, incomeCats]); useEffect(() => { localStorage.setItem('my-fixed-items', JSON.stringify(fixedItems)); }, [fixedItems]); // --- 計算邏輯 --- const filteredTransactions = transactions.filter(t => { const matchSearch = searchTerm === '' || (t.note && t.note.includes(searchTerm)) || t.category.includes(searchTerm); const matchMonth = filterMonth === '' || t.date.startsWith(filterMonth); const matchCategory = filterCategory === '' || t.category === filterCategory; return matchSearch && matchMonth && matchCategory; }); const totalIncome = filteredTransactions .filter(t => t.type === 'income') .reduce((acc, curr) => acc + curr.amount, 0); const totalExpense = filteredTransactions .filter(t => t.type === 'expense') .reduce((acc, curr) => acc + curr.amount, 0); const balance = totalIncome - totalExpense; // --- 核心邏輯:計算特定月份的固定收支 --- const getMonthlyForecastDetails = (targetYearMonth) => { const [tYear, tMonth] = targetYearMonth.split('-').map(Number); const items = []; let mIncome = 0; let mExpense = 0; fixedItems.forEach(item => { let isOccurring = false; let startMonthNum; if (item.startMonth && item.startMonth.includes('-')) { startMonthNum = parseInt(item.startMonth.split('-')[1], 10); } else { startMonthNum = parseInt(item.startMonth || '1', 10); } switch(item.frequency) { case 'monthly': isOccurring = true; break; case 'quarterly': const monthDiff = tMonth - startMonthNum; isOccurring = (monthDiff % 3 === 0); break; case 'yearly': isOccurring = (tMonth === startMonthNum); break; default: isOccurring = true; } if (isOccurring) { items.push(item); if (item.type === 'income') mIncome += item.amount; else mExpense += item.amount; } }); return { items, mIncome, mExpense }; }; const { items: forecastItems, mIncome: forecastIncome, mExpense: forecastExpense } = getMonthlyForecastDetails(forecastDate); const forecastBalance = forecastIncome - forecastExpense; // --- 年度固定收支總計 (概覽) --- const calculateAnnualFixed = () => { let annualIncome = 0; let annualExpense = 0; fixedItems.forEach(item => { let multiplier = 0; switch(item.frequency) { case 'monthly': multiplier = 12; break; case 'quarterly': multiplier = 4; break; case 'yearly': multiplier = 1; break; default: multiplier = 12; } const totalAmount = item.amount * multiplier; if (item.type === 'income') annualIncome += totalAmount; else annualExpense += totalAmount; }); return { annualIncome, annualExpense }; }; const { annualIncome: annualFixedIncome, annualExpense: annualFixedExpense } = calculateAnnualFixed(); const annualFixedBalance = annualFixedIncome - annualFixedExpense; // --- 事件處理 --- const handleQuickAdd = (e) => { e.preventDefault(); if (!quickAmount || !quickCategory) return; addTransactionRecord(quickType, quickAmount, quickCategory, quickNote, new Date().toISOString().split('T')[0]); setQuickAmount(''); setQuickNote(''); }; const addTransactionRecord = (tType, tAmount, tCategory, tNote, tDate) => { const newTransaction = { id: Date.now(), type: tType, amount: parseFloat(tAmount), category: tCategory, date: tDate, note: tNote }; setTransactions([newTransaction, ...transactions]); }; const handleDeleteTransaction = (e, id) => { e.stopPropagation(); if (window.confirm('確定要刪除這筆紀錄嗎?')) { setTransactions(transactions.filter(t => t.id !== id)); } }; const handleReuseTransaction = (t) => { setQuickType(t.type); setQuickAmount(t.amount.toString()); setQuickCategory(t.category); setQuickNote(t.note || ''); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handleAddFixedItem = (e) => { e.preventDefault(); if (!newFixed.name || !newFixed.amount || !newFixed.category) return; const newItem = { id: Date.now(), ...newFixed, amount: parseFloat(newFixed.amount) }; setFixedItems([...fixedItems, newItem]); setNewFixed({ ...newFixed, name: '', amount: '' }); }; const handleDeleteFixedItem = (id) => { if (window.confirm('確定要刪除這個固定項目嗎?')) { setFixedItems(fixedItems.filter(item => item.id !== id)); } }; const handleRecordFixedItem = (item) => { addTransactionRecord( item.type, item.amount, item.category, `${item.name} (${getFrequencyLabel(item.frequency)})`, new Date().toISOString().split('T')[0] ); setShowFixedModal(false); alert(`已新增:${item.name}`); }; const startPress = (catId) => { isLongPressRef.current = false; timerRef.current = setTimeout(() => { isLongPressRef.current = true; if (window.confirm('【刪除分類】\n確定要刪除這個分類嗎?\n(這不會影響已存在的記帳紀錄)')) { handleDeleteCategory(catId); if (quickCategory === catId) setQuickCategory(''); } }, 600); }; const endPress = () => { if (timerRef.current) { clearTimeout(timerRef.current); } }; const handleQuickCategoryClick = (catName) => { if (isLongPressRef.current) return; setQuickCategory(catName); }; const getRandomColorClass = () => { const colors = [ 'bg-red-100 text-red-600 border-red-200', 'bg-orange-100 text-orange-600 border-orange-200', 'bg-amber-100 text-amber-600 border-amber-200', 'bg-yellow-100 text-yellow-600 border-yellow-200', 'bg-lime-100 text-lime-600 border-lime-200', 'bg-green-100 text-green-600 border-green-200', 'bg-emerald-100 text-emerald-600 border-emerald-200', 'bg-teal-100 text-teal-600 border-teal-200', 'bg-cyan-100 text-cyan-600 border-cyan-200', 'bg-sky-100 text-sky-600 border-sky-200', 'bg-blue-100 text-blue-600 border-blue-200', 'bg-indigo-100 text-indigo-600 border-indigo-200', 'bg-violet-100 text-violet-600 border-violet-200', 'bg-purple-100 text-purple-600 border-purple-200', 'bg-fuchsia-100 text-fuchsia-600 border-fuchsia-200', 'bg-pink-100 text-pink-600 border-pink-200', 'bg-rose-100 text-rose-600 border-rose-200', ]; return colors[Math.floor(Math.random() * colors.length)]; }; const handleConfirmAddCategory = (e) => { e.preventDefault(); if (newCatName && newCatName.trim()) { const newCat = { id: Date.now().toString(), name: newCatName.trim(), icon: '🏷️', color: getRandomColorClass() }; if (quickType === 'expense') { setExpenseCats([...expenseCats, newCat]); } else { setIncomeCats([...incomeCats, newCat]); } setNewCatName(''); setShowAddModal(false); } }; const handleDeleteCategory = (catId) => { if (quickType === 'expense') { setExpenseCats(expenseCats.filter(c => c.id !== catId)); } else { setIncomeCats(incomeCats.filter(c => c.id !== catId)); } if (quickCategory === expenseCats.find(c => c.id === catId)?.name || quickCategory === incomeCats.find(c => c.id === catId)?.name) { setQuickCategory(''); } }; const getFrequencyLabel = (freq) => { switch(freq) { case 'monthly': return '每月'; case 'quarterly': return '每季'; case 'yearly': return '每年'; default: return '每月'; } }; const currentCategories = quickType === 'expense' ? expenseCats : incomeCats; const allCategoryNames = Array.from(new Set([...expenseCats.map(c => c.name), ...incomeCats.map(c => c.name)])); const formatMoney = (num) => { return new Intl.NumberFormat('zh-TW', { style: 'currency', currency: 'TWD', maximumFractionDigits: 0 }).format(num); }; const renderPieChart = (income, expense) => { if (income === 0 && expense === 0) return null; const expensePercent = income === 0 ? 100 : Math.min((expense / income) * 100, 100); const dashExpense = expensePercent; return (
{filterMonth ? '本月結餘' : '目前總資產餘額'}
{searchTerm || filterMonth || filterCategory ? '沒有符合條件的紀錄' : '還沒有紀錄,快記一筆吧!'}
該月份無固定收支項目
)}年度總收
{formatMoney(annualFixedIncome)}
年度總支
{formatMoney(annualFixedExpense)}
年度固定結餘
= 0 ? 'text-emerald-600' : 'text-rose-600'}`}> {formatMoney(annualFixedBalance)}