'use client'; import { useState, useEffect, useCallback } from 'react'; import { api } from '@/lib/api'; import { PageHeader } from '@/components/ui/page-header'; import { DataTable } from '@/components/ui/data-table'; import { Badge, statusBadgeVariant } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { ActionIcon } from '@/components/ui/action-icon'; import { Modal } from '@/components/ui/modal'; import { FormModal } from '@/components/ui/form-modal'; import { useToast } from '@/components/ui/toast'; import { useAuthStore } from '@/stores/auth.store'; import { CreateExpenseModal } from '@/components/modals/create-expense-modal'; const CATEGORIES = ['utilities', 'supplies', 'salary', 'maintenance', 'transport', 'equipment', 'other']; export default function ExpensesPage() { const [tab, setTab] = useState<'expenses' | 'recurring'>('expenses'); return (
{([['expenses', 'Expenses'], ['recurring', 'Recurring Setup']] as const).map(([key, label]) => ( ))}
{tab === 'expenses' && } {tab === 'recurring' && }
); } function ExpensesTab() { const { toast } = useToast(); const currentUser = useAuthStore((s) => s.user); const [expenses, setExpenses] = useState([]); const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [showCreate, setShowCreate] = useState(false); const [detailTarget, setDetailTarget] = useState(null); const [approveTarget, setApproveTarget] = useState(null); const [rejectTarget, setRejectTarget] = useState(null); const load = useCallback(async () => { try { const [r, s] = await Promise.all([api.get('/expenses'), api.get('/expenses/summary')]); setExpenses(r.data.data); setSummary(s.data.data); } catch { toast('Failed to load', 'error'); } finally { setLoading(false); } }, [toast]); useEffect(() => { load(); }, [load]); async function handleApprove() { try { await api.patch(`/expenses/${approveTarget.id}/approve`); toast('Expense approved', 'success'); load(); } catch (e: any) { toast(e.response?.data?.error || 'Failed', 'error'); } finally { setApproveTarget(null); } } async function handleReject() { try { await api.patch(`/expenses/${rejectTarget.id}/reject`); toast('Expense rejected', 'success'); load(); } catch (e: any) { toast(e.response?.data?.error || 'Failed', 'error'); } finally { setRejectTarget(null); } } const filtered = search ? expenses.filter((e: any) => e.description.toLowerCase().includes(search.toLowerCase()) || e.category.toLowerCase().includes(search.toLowerCase())) : expenses; return (
setShowCreate(true)}>New Expense} /> {summary && (

Pending Approval

PHP {summary.pending.total.toLocaleString()}

{summary.pending.count} expenses

Total Approved

PHP {summary.approved.total.toLocaleString()}

{summary.approved.count} expenses

Top Category

{summary.byCategory[0]?.category || '—'}

{summary.byCategory[0] ? `PHP ${summary.byCategory[0].total.toLocaleString()}` : 'No data'}

)}
e.id} emptyTitle="No expenses" searchPlaceholder="Search by description or category..." searchValue={search} onSearchChange={setSearch} onRowClick={(e: any) => setDetailTarget(e)} columns={[ { key: 'expenseDate', label: 'Date', sortable: true, render: (e: any) => {new Date(e.expenseDate).toLocaleDateString()} }, { key: 'category', label: 'Category', sortable: true, render: (e: any) => }, { key: 'description', label: 'Description', render: (e: any) => {e.description} }, { key: 'amount', label: 'Amount', align: 'right' as const, sortable: true, render: (e: any) => PHP {Number(e.amount).toLocaleString()} }, { key: 'status', label: 'Status', sortable: true, render: (e: any) => }, { key: 'actions', label: '', align: 'right' as const, render: (e: any) => e.status === 'pending' && e.createdById !== currentUser?.id ? (
ev.stopPropagation()}> setApproveTarget(e)} /> setRejectTarget(e)} />
) : null }, ]} />
setShowCreate(false)} onSuccess={load} /> setApproveTarget(null)} title="Approve Expense" description={`Approve PHP ${Number(approveTarget?.amount || 0).toLocaleString()} for "${approveTarget?.description}"?`} confirmLabel="Approve" onConfirm={handleApprove} /> setRejectTarget(null)} title="Reject Expense" description={`Reject "${rejectTarget?.description}"?`} variant="danger" confirmLabel="Reject" onConfirm={handleReject} /> {/* Expense Detail Modal */} setDetailTarget(null)} title="Expense Details" description={detailTarget?.description}> {detailTarget && (
Date: {new Date(detailTarget.expenseDate).toLocaleDateString()}
Category:
Amount: PHP {Number(detailTarget.amount).toLocaleString()}
Status:
{detailTarget.notes &&
Notes: {detailTarget.notes}
} {detailTarget.approvedBy &&
Approved by: {detailTarget.approvedBy.firstName} {detailTarget.approvedBy.lastName}
}
)}
); } // ─── Recurring Expenses Tab ────────────────────────────────── function RecurringTab() { const { toast } = useToast(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [showCreate, setShowCreate] = useState(false); const load = useCallback(async () => { try { const r = await api.get('/expenses/recurring'); setItems(r.data.data); } catch { toast('Failed to load', 'error'); } finally { setLoading(false); } }, [toast]); useEffect(() => { load(); }, [load]); async function toggle(id: string) { try { await api.patch(`/expenses/recurring/${id}/toggle`); load(); } catch { toast('Failed', 'error'); } } async function remove(id: string) { try { await api.delete(`/expenses/recurring/${id}`); toast('Deleted', 'success'); load(); } catch { toast('Failed', 'error'); } } const freqLabels: Record = { monthly: 'Monthly', quarterly: 'Quarterly', yearly: 'Yearly' }; return (

Recurring expenses auto-generate pending expenses on schedule. They still require approval before creating an accounting entry.

{loading ?

Loading...

: items.length === 0 ? (
No recurring expenses set up yet.
) : (
{items.map((item: any) => (

{item.description}

{!item.isActive && }

Next run: {new Date(item.nextRunDate).toLocaleDateString()}

PHP {Number(item.amount).toLocaleString()}
))}
)} setShowCreate(false)} title="Add Recurring Expense" description="Auto-generates a pending expense on schedule."> { setShowCreate(false); load(); }} onClose={() => setShowCreate(false)} />
); } function RecurringForm({ onSuccess, onClose }: { onSuccess: () => void; onClose: () => void }) { const { toast } = useToast(); const [form, setForm] = useState({ category: 'utilities', description: '', amount: 0, frequency: 'monthly' }); const [submitting, setSubmitting] = useState(false); const ic = 'mt-1 block w-full rounded-lg border border-surface-200 dark:border-surface-700 px-3.5 py-2.5 text-sm text-surface-900 dark:text-surface-100 bg-white dark:bg-surface-800 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20 transition-all duration-200'; async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setSubmitting(true); try { await api.post('/expenses/recurring', form); toast('Recurring expense created', 'success'); onSuccess(); } catch (e: any) { toast(e.response?.data?.error || 'Failed', 'error'); } finally { setSubmitting(false); } } return (
setForm({ ...form, description: e.target.value })} className={ic} placeholder="e.g. Electricity bill" />
setForm({ ...form, amount: parseFloat(e.target.value) || 0 })} className={ic} />
); }