feat: expense year filter, newest-first default sort for expenses + invoices

This commit is contained in:
Krao Hasanee
2026-06-02 12:13:10 -04:00
parent 490e4f4bbf
commit 1f09ce0a1d
+39 -5
View File
@@ -78,8 +78,8 @@ export default function Invoices() {
const [activeTab, setActiveTab] = useState(() => location.state?.tab || 'invoices'); const [activeTab, setActiveTab] = useState(() => location.state?.tab || 'invoices');
const [financeTab, setFinanceTab] = useState('overview'); const [financeTab, setFinanceTab] = useState('overview');
const [filter, setFilter] = useState('all'); const [filter, setFilter] = useState('all');
const { sortKey: invSortKey, sortDir: invSortDir, toggle: invToggle, sort: invSort } = useSortable('invoice_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'); 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: subSortKey, sortDir: subSortDir, toggle: subToggle, sort: subSort } = useSortable('submitted_at');
const { sortKey: ovExpKey, sortDir: ovExpDir, toggle: ovExpToggle } = useSortable('date'); const { sortKey: ovExpKey, sortDir: ovExpDir, toggle: ovExpToggle } = useSortable('date');
const { sortKey: ovSubKey, sortDir: ovSubDir, toggle: ovSubToggle } = useSortable('date'); const { sortKey: ovSubKey, sortDir: ovSubDir, toggle: ovSubToggle } = useSortable('date');
@@ -100,6 +100,9 @@ export default function Invoices() {
const [editingExpenseId, setEditingExpenseId] = useState(''); const [editingExpenseId, setEditingExpenseId] = useState('');
const [showExpenseForm, setShowExpenseForm] = useState(false); const [showExpenseForm, setShowExpenseForm] = useState(false);
const [expenseFilter, setExpenseFilter] = useState('all'); 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 [subcontractorPOs, setSubcontractorPOs] = useState([]);
const [subcontractorLoading, setSubcontractorLoading] = useState(true); const [subcontractorLoading, setSubcontractorLoading] = useState(true);
const [subcontractorError, setSubcontractorError] = useState(''); const [subcontractorError, setSubcontractorError] = useState('');
@@ -154,6 +157,13 @@ export default function Invoices() {
loadExpenses(); 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(() => { useEffect(() => {
async function loadSubInvoices() { async function loadSubInvoices() {
const { data, error: err } = await supabase 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), netReceived: invoices.filter(i => i.status === 'paid').reduce((s, i) => s + Number(i.total) - Number(i.stripe_fee || 0), 0),
}; };
const filteredExpenses = expenseFilter === 'all' const expenseYears = [...new Set(expenses.map(e => e.date?.slice(0, 4)).filter(Boolean))].sort((a, b) => b - a);
? expenses const filteredExpenses = expenses.filter(e => {
: expenses.filter(e => e.category === expenseFilter); 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 paidSubcontractorPOs = subcontractorPOs.filter(po => po.status === 'paid');
const payableSubcontractorPOs = subcontractorPOs.filter(po => ['approved', 'ready_to_pay'].includes(po.status)); const payableSubcontractorPOs = subcontractorPOs.filter(po => ['approved', 'ready_to_pay'].includes(po.status));
const selectedSubcontractorPO = subcontractorPOs.find(po => po.id === selectedSubcontractorPOId); const selectedSubcontractorPO = subcontractorPOs.find(po => po.id === selectedSubcontractorPOId);
@@ -1120,6 +1133,27 @@ export default function Invoices() {
<option key={c} value={c}>{c}</option> <option key={c} value={c}>{c}</option>
))} ))}
</select> </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> <button className="btn btn-primary btn-sm" onClick={() => { setEditingExpenseId(''); setNewExpense(blankExpense()); setExpensesError(''); setShowExpenseForm(true); }}>+ Add Expense</button>
</div> </div>
)} )}