feat: expense year filter, newest-first default sort for expenses + invoices
This commit is contained in:
@@ -78,8 +78,8 @@ export default function Invoices() {
|
||||
const [activeTab, setActiveTab] = useState(() => location.state?.tab || 'invoices');
|
||||
const [financeTab, setFinanceTab] = useState('overview');
|
||||
const [filter, setFilter] = useState('all');
|
||||
const { sortKey: invSortKey, sortDir: invSortDir, toggle: invToggle, sort: invSort } = useSortable('invoice_date');
|
||||
const { sortKey: expSortKey, sortDir: expSortDir, toggle: expToggle, sort: expSort } = useSortable('date');
|
||||
const { sortKey: invSortKey, sortDir: invSortDir, toggle: invToggle, sort: invSort } = useSortable('invoice_date', 'desc');
|
||||
const { sortKey: expSortKey, sortDir: expSortDir, toggle: expToggle, sort: expSort } = useSortable('date', 'desc');
|
||||
const { sortKey: subSortKey, sortDir: subSortDir, toggle: subToggle, sort: subSort } = useSortable('submitted_at');
|
||||
const { sortKey: ovExpKey, sortDir: ovExpDir, toggle: ovExpToggle } = useSortable('date');
|
||||
const { sortKey: ovSubKey, sortDir: ovSubDir, toggle: ovSubToggle } = useSortable('date');
|
||||
@@ -100,6 +100,9 @@ export default function Invoices() {
|
||||
const [editingExpenseId, setEditingExpenseId] = useState('');
|
||||
const [showExpenseForm, setShowExpenseForm] = useState(false);
|
||||
const [expenseFilter, setExpenseFilter] = useState('all');
|
||||
const [expYearFilter, setExpYearFilter] = useState(String(new Date().getFullYear()));
|
||||
const [expYearMenuOpen, setExpYearMenuOpen] = useState(false);
|
||||
const expYearMenuRef = useRef(null);
|
||||
const [subcontractorPOs, setSubcontractorPOs] = useState([]);
|
||||
const [subcontractorLoading, setSubcontractorLoading] = useState(true);
|
||||
const [subcontractorError, setSubcontractorError] = useState('');
|
||||
@@ -154,6 +157,13 @@ export default function Invoices() {
|
||||
loadExpenses();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!expYearMenuOpen) return;
|
||||
const handler = e => { if (expYearMenuRef.current && !expYearMenuRef.current.contains(e.target)) setExpYearMenuOpen(false); };
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [expYearMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadSubInvoices() {
|
||||
const { data, error: err } = await supabase
|
||||
@@ -462,9 +472,12 @@ export default function Invoices() {
|
||||
netReceived: invoices.filter(i => i.status === 'paid').reduce((s, i) => s + Number(i.total) - Number(i.stripe_fee || 0), 0),
|
||||
};
|
||||
|
||||
const filteredExpenses = expenseFilter === 'all'
|
||||
? expenses
|
||||
: expenses.filter(e => e.category === expenseFilter);
|
||||
const expenseYears = [...new Set(expenses.map(e => e.date?.slice(0, 4)).filter(Boolean))].sort((a, b) => b - a);
|
||||
const filteredExpenses = expenses.filter(e => {
|
||||
if (expYearFilter !== 'all' && e.date?.slice(0, 4) !== expYearFilter) return false;
|
||||
if (expenseFilter !== 'all' && e.category !== expenseFilter) return false;
|
||||
return true;
|
||||
});
|
||||
const paidSubcontractorPOs = subcontractorPOs.filter(po => po.status === 'paid');
|
||||
const payableSubcontractorPOs = subcontractorPOs.filter(po => ['approved', 'ready_to_pay'].includes(po.status));
|
||||
const selectedSubcontractorPO = subcontractorPOs.find(po => po.id === selectedSubcontractorPOId);
|
||||
@@ -1120,6 +1133,27 @@ export default function Invoices() {
|
||||
<option key={c} value={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
<div style={{ position: 'relative' }} ref={expYearMenuRef}>
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
onClick={() => setExpYearMenuOpen(o => !o)}
|
||||
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}
|
||||
title="Filter by year"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M2 4h12M4 8h8M6 12h4" />
|
||||
</svg>
|
||||
{expYearFilter !== 'all' ? expYearFilter : 'All Years'}
|
||||
</button>
|
||||
{expYearMenuOpen && (
|
||||
<div className="site-header-avatar-menu" style={{ position: 'absolute', top: 'calc(100% + 4px)', right: 0, zIndex: 220, minWidth: 130 }}>
|
||||
<button onClick={() => { setExpYearFilter('all'); setExpYearMenuOpen(false); }} className="site-header-avatar-item" style={{ background: expYearFilter === 'all' ? 'rgba(245,165,35,0.08)' : 'transparent', color: expYearFilter === 'all' ? 'var(--accent)' : 'var(--text-primary)' }}>All Years</button>
|
||||
{expenseYears.map(y => (
|
||||
<button key={y} onClick={() => { setExpYearFilter(y); setExpYearMenuOpen(false); }} className="site-header-avatar-item" style={{ background: expYearFilter === y ? 'rgba(245,165,35,0.08)' : 'transparent', color: expYearFilter === y ? 'var(--accent)' : 'var(--text-primary)' }}>{y}</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button className="btn btn-primary btn-sm" onClick={() => { setEditingExpenseId(''); setNewExpense(blankExpense()); setExpensesError(''); setShowExpenseForm(true); }}>+ Add Expense</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user