'use client'; import React, { useState, useEffect, useCallback } from 'react'; import Link from 'next/link'; import { useAuth } from '@/contexts/AuthContext'; import { useRouter } from 'next/navigation'; const CATEGORIES = [ 'contract_labor','software_subscriptions','hosting_infrastructure', 'travel','office_supplies','legal_accounting','marketing','equipment','other', ]; function fmt(cents: number) { return '$' + (cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2 }); } export default function ExpensesPage() { const { user, loading } = useAuth(); const router = useRouter(); const [expenses, setExpenses] = useState([]); const [loadingData, setLoadingData] = useState(true); const [filterCat, setFilterCat] = useState(''); const [filterReconciled, setFilterReconciled] = useState(''); const [showForm, setShowForm] = useState(false); const [saving, setSaving] = useState(false); const [form, setForm] = useState({ vendor: '', amount: '', category: 'other', description: '', expense_date: new Date().toISOString().split('T')[0], receipt_source: 'manual' }); const [errors, setErrors] = useState>({}); const [successMsg, setSuccessMsg] = useState(''); useEffect(() => { if (!loading && !user) router.push('/login?next=' + encodeURIComponent(window.location.pathname)); }, [user, loading, router]); const load = useCallback(async () => { setLoadingData(true); const params = new URLSearchParams(); if (filterCat) params.set('category', filterCat); if (filterReconciled) params.set('reconciled', filterReconciled); const r = await fetch(`/api/accounting/expenses?${params}`); const d = await r.json(); setExpenses(d.expenses ?? []); setLoadingData(false); }, [filterCat, filterReconciled]); useEffect(() => { if (user) load(); }, [user, load]); function handleChange(key: string, value: string) { setForm(p => ({ ...p, [key]: value })); if (errors[key]) setErrors(p => { const n = { ...p }; delete n[key]; return n; }); } function validate() { const errs: Record = {}; if (!form.amount.trim()) { errs.amount = 'Amount is required.'; } else if (isNaN(parseFloat(form.amount)) || parseFloat(form.amount) <= 0) { errs.amount = 'Enter a valid positive amount.'; } if (!form.expense_date) errs.expense_date = 'Date is required.'; return errs; } async function handleCreate(e: React.FormEvent) { e.preventDefault(); const errs = validate(); if (Object.keys(errs).length > 0) { setErrors(errs); return; } setSaving(true); try { const body = { ...form, amount_cents: Math.round(parseFloat(form.amount) * 100) }; const res = await fetch('/api/accounting/expenses', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) throw new Error('Failed to log expense'); const vendor = form.vendor.trim() || 'Expense'; setShowForm(false); setErrors({}); setForm({ vendor: '', amount: '', category: 'other', description: '', expense_date: new Date().toISOString().split('T')[0], receipt_source: 'manual' }); setSuccessMsg(`${vendor} expense of $${parseFloat(form.amount).toFixed(2)} logged successfully.`); setTimeout(() => setSuccessMsg(''), 4000); load(); } catch { setErrors({ _form: 'Something went wrong. Please try again.' }); } finally { setSaving(false); } } function handleCancel() { setShowForm(false); setErrors({}); setForm({ vendor: '', amount: '', category: 'other', description: '', expense_date: new Date().toISOString().split('T')[0], receipt_source: 'manual' }); } if (loading) return
; if (!user) return null; return (

Expenses

← Accounting
{/* Success Toast */} {successMsg && (
{successMsg}
)}
{showForm && (

Log Expense

{errors._form && (
{errors._form}
)}
handleChange('vendor', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
handleChange('amount', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8] ${errors.amount ? 'border-red-500/60' : 'border-[#DCE6F2]'}`} /> {errors.amount &&

{errors.amount}

}
handleChange('expense_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8] ${errors.expense_date ? 'border-red-500/60' : 'border-[#DCE6F2]'}`} /> {errors.expense_date &&

{errors.expense_date}

}
handleChange('description', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
)}
{['Date','Vendor','Amount','Category','Source','Reconciled','Description'].map(h => ( ))} {loadingData ? ( ) : expenses.length === 0 ? ( ) : expenses.map((ex: any) => ( ))}
{h}
Loading…
No expenses found
{ex.expense_date} {ex.vendor ?? '—'} {ex.amount_cents ? fmt(ex.amount_cents) : '—'} {ex.category?.replace(/_/g, ' ') ?? '—'} {ex.receipt_source ?? 'manual'} {ex.reconciled ? 'Yes' : 'No'} {ex.description ?? '—'}
); }