import React, { useState, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { RootState } from '../store'; import { addExpenses, addExpense, updateExpense, deleteExpense } from '../store/slices/expenseSlice'; import { formatAustralianDate } from '../utils/dateFormatter'; import { Upload, Plus, Search, Edit, Trash2, Receipt, Download, DollarSign } from 'lucide-react'; import toast from 'react-hot-toast'; interface ExpenseFormData { description: string; amount: number; category: string; date: string; taxDeductible: boolean; vendor: string; receiptUrl?: string; } const Expenses = () => { const dispatch = useDispatch(); const { expenses } = useSelector((state: RootState) => state.expenses); const [showAddForm, setShowAddForm] = useState(false); const [editingExpense, setEditingExpense] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [categoryFilter, setCategoryFilter] = useState('all'); const [dateRange, setDateRange] = useState('all'); const [showTaxDeductibleOnly, setShowTaxDeductibleOnly] = useState(false); const fileInputRef = useRef(null); const etsyFileInputRef = useRef(null); const [formData, setFormData] = useState({ description: '', amount: 0, category: '', date: new Date().toISOString().split('T')[0], taxDeductible: false, vendor: '' }); const expenseCategories = [ 'Shipping & Postage', 'Materials & Supplies', 'Listing Fees', 'Transaction Fees', 'Payment Processing Fees', 'Marketing & Advertising', 'Taxes & GST', 'Packaging', 'Office Supplies', 'Professional Services', 'Software & Subscriptions', 'Travel & Transport', 'Other' ]; const handleAusPostCSVImport = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; if (!file.name.endsWith('.csv')) { toast.error('Please select a valid CSV file'); return; } const reader = new FileReader(); reader.onload = async (e) => { try { const text = e.target?.result as string; const lines = text.split('\n'); const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, '').toLowerCase()); toast.loading('Importing expenses from CSV...'); const importedExpenses = []; for (let i = 1; i < lines.length; i++) { if (lines[i].trim() === '') continue; const values = lines[i].split(',').map(v => v.trim().replace(/"/g, '')); const expenseData: any = {}; headers.forEach((header, index) => { const value = values[index] || ''; switch (header) { case 'transaction date': case 'date': case 'posted date': // Convert DD/MM/YYYY to YYYY-MM-DD format if (value.includes('/')) { const [day, month, year] = value.split('/'); expenseData.date = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; } else { expenseData.date = value; } break; case 'total cost': case 'amount': case 'cost': case 'charge': const amount = parseFloat(value.replace(/[$,AUD\s-]/g, '')); expenseData.amount = Math.abs(amount) || 0; // Make positive break; case 'tracking number': case 'transaction id': case 'reference': case 'receipt': expenseData.reference = value; break; case 'description': case 'merchant': case 'vendor': expenseData.description = value; expenseData.vendor = value; break; case 'category': case 'type': expenseData.category = value || 'Shipping & Postage'; break; } }); // Auto-categorize based on description or set default for Australia Post if (!expenseData.category) { expenseData.category = 'Shipping & Postage'; } // If no description, create one from the tracking number or transaction ID if (!expenseData.description) { expenseData.description = expenseData.reference ? `Australia Post - ${expenseData.reference}` : 'Australia Post Shipping'; } if (!expenseData.vendor) { expenseData.vendor = 'Australia Post'; } if (expenseData.amount > 0) { importedExpenses.push({ _id: `temp-${Date.now()}-${i}`, description: expenseData.description || 'Imported Expense', amount: expenseData.amount, category: expenseData.category || 'Other', date: expenseData.date || new Date().toISOString().split('T')[0], taxDeductible: true, // Assume business expenses are tax deductible vendor: expenseData.vendor || expenseData.description || 'Unknown', reference: expenseData.reference }); } } // Simulate API call - replace with actual API call console.log('Imported expenses:', importedExpenses); // Add the imported expenses to the store dispatch(addExpenses(importedExpenses)); toast.success(`Successfully imported ${importedExpenses.length} expenses`); } catch (error) { console.error('Import error:', error); toast.error('Failed to import CSV file. Please check the format.'); } }; reader.readAsText(file); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; const handleEtsyCSVImport = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; toast.error('Please use the Data Import page for Etsy CSV files. This ensures proper separation of sales and expenses.'); // Clear the input if (etsyFileInputRef.current) { etsyFileInputRef.current.value = ''; } // Optionally redirect to Data Import page setTimeout(() => { window.location.href = '/data-import'; }, 2000); }; const handleExportCSV = () => { const headers = [ 'Date', 'Description', 'Category', 'Amount', 'Vendor', 'Tax Deductible', 'Reference' ]; const csvData = expenses.map(expense => [ expense.date, expense.description, expense.category, expense.amount.toFixed(2), expense.vendor || '', expense.taxDeductible ? 'Yes' : 'No', expense.reference || '' ]); const csv = [headers, ...csvData].map(row => row.map(field => `"${field}"`).join(',') ).join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `business-expenses-${new Date().toISOString().split('T')[0]}.csv`; a.click(); window.URL.revokeObjectURL(url); toast.success('Expenses exported successfully'); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { if (editingExpense) { // Update existing expense dispatch(updateExpense({ _id: editingExpense, ...formData })); toast.success('Expense updated successfully!'); setEditingExpense(null); } else { // Add new expense dispatch(addExpense({ _id: `expense-${Date.now()}`, ...formData })); toast.success('Expense added successfully!'); } setShowAddForm(false); setFormData({ description: '', amount: 0, category: '', date: new Date().toISOString().split('T')[0], taxDeductible: false, vendor: '' }); } catch (error) { toast.error('Failed to save expense'); } }; const handleEdit = (expense: any) => { setFormData({ description: expense.description, amount: expense.amount, category: expense.category, date: expense.date, taxDeductible: expense.taxDeductible, vendor: expense.vendor || '' }); setEditingExpense(expense._id); setShowAddForm(true); }; const handleDelete = (expenseId: string) => { if (window.confirm('Are you sure you want to delete this expense?')) { dispatch(deleteExpense(expenseId)); toast.success('Expense deleted successfully!'); } }; const handleCancel = () => { setShowAddForm(false); setEditingExpense(null); setFormData({ description: '', amount: 0, category: '', date: new Date().toISOString().split('T')[0], taxDeductible: false, vendor: '' }); }; const filteredExpenses = expenses .filter(expense => { const matchesSearch = expense.description.toLowerCase().includes(searchTerm.toLowerCase()) || expense.category.toLowerCase().includes(searchTerm.toLowerCase()); const matchesCategory = categoryFilter === 'all' || expense.category === categoryFilter; const matchesTaxDeductible = !showTaxDeductibleOnly || expense.taxDeductible; let matchesDate = true; if (dateRange !== 'all') { const expenseDate = new Date(expense.date); const now = new Date(); switch (dateRange) { case 'week': matchesDate = (now.getTime() - expenseDate.getTime()) <= 7 * 24 * 60 * 60 * 1000; break; case 'month': matchesDate = (now.getTime() - expenseDate.getTime()) <= 30 * 24 * 60 * 60 * 1000; break; case 'quarter': matchesDate = (now.getTime() - expenseDate.getTime()) <= 90 * 24 * 60 * 60 * 1000; break; case 'year': matchesDate = expenseDate.getFullYear() === now.getFullYear(); break; } } return matchesSearch && matchesCategory && matchesTaxDeductible && matchesDate; }) .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); // Sort by newest first const totalExpenses = filteredExpenses.reduce((sum, expense) => sum + expense.amount, 0); const taxDeductibleTotal = filteredExpenses.filter(e => e.taxDeductible).reduce((sum, expense) => sum + expense.amount, 0); return (

Expenses

Track business expenses and tax deductions

{/* Summary Cards */}

Total Expenses

${totalExpenses.toFixed(2)}

Tax Deductible

${taxDeductibleTotal.toFixed(2)}

{/* Filters */}
setSearchTerm(e.target.value)} />
{/* Add Expense Form Modal */} {showAddForm && (

{editingExpense ? 'Edit Expense' : 'Add New Expense'}

setFormData({...formData, description: e.target.value})} />
setFormData({...formData, amount: parseFloat(e.target.value) || 0})} />
setFormData({...formData, date: e.target.value})} />
setFormData({...formData, vendor: e.target.value})} />
setFormData({...formData, taxDeductible: e.target.checked})} className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
)} {/* Expenses List */}
{filteredExpenses.length === 0 ? (

No expenses found

Import your expense data or add expenses manually.

) : (
{filteredExpenses.map((expense) => ( ))}
Date (↓ newest first)
Description Category Amount Tax Deductible Actions
{formatAustralianDate(expense.date)}
{expense.description}
{expense.category} ${expense.amount.toFixed(2)} {expense.taxDeductible ? ( Yes ) : ( No )}
)}
); }; export default Expenses;