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 [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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user