'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 (
);
}