diff --git a/public/js/views/invoice-view.js b/public/js/views/invoice-view.js index 78fba23..bcf5b06 100644 --- a/public/js/views/invoice-view.js +++ b/public/js/views/invoice-view.js @@ -6,8 +6,8 @@ let filterCustomer = localStorage.getItem('inv_filterCustomer') || ''; let filterStatus = localStorage.getItem('inv_filterStatus') || 'unpaid'; let groupBy = localStorage.getItem('inv_groupBy') || 'none'; let filterWorker = localStorage.getItem('inv_filterWorker') || ''; -let filterParts = localStorage.getItem('inv_filterParts') === 'true'; -let filterEmptyCost = localStorage.getItem('inv_filterEmptyCost') === 'true'; +let filterCategory = localStorage.getItem('inv_filterCategory') || ''; +let filterItemSearch = localStorage.getItem('inv_filterItemSearch') || ''; const OVERDUE_DAYS = 30; @@ -198,8 +198,8 @@ function saveSettings() { localStorage.setItem('inv_groupBy', groupBy); localStorage.setItem('inv_filterCustomer', filterCustomer); localStorage.setItem('inv_filterWorker', filterWorker); - localStorage.setItem('inv_filterParts', filterParts); - localStorage.setItem('inv_filterEmptyCost', filterEmptyCost); + localStorage.setItem('inv_filterCategory', filterCategory); + localStorage.setItem('inv_filterItemSearch', filterItemSearch); } // ============================================================ @@ -209,10 +209,8 @@ function saveSettings() { export async function loadInvoices() { try { const params = new URLSearchParams(); - if (filterParts) { - params.set('has_parts', 'true'); - if (filterEmptyCost) params.set('empty_cost_only', 'true'); - } + if (filterCategory) params.set('category', filterCategory); + if (filterItemSearch) params.set('item_search', filterItemSearch); const qs = params.toString(); const url = qs ? `/api/invoices?${qs}` : '/api/invoices'; const response = await fetch(url); @@ -610,11 +608,20 @@ export function injectToolbar() {
-
- - +
+ + +
+
+
+ +
@@ -636,6 +643,12 @@ export function injectToolbar() { populateWorkerFilter(); document.getElementById('invoice-filter-worker').addEventListener('change', (e) => { filterWorker = e.target.value; saveSettings(); renderInvoiceView(); + }); + document.getElementById('invoice-filter-category').addEventListener('change', (e) => { + filterCategory = e.target.value; saveSettings(); loadInvoices(); + }); + document.getElementById('invoice-filter-item-search').addEventListener('input', (e) => { + filterItemSearch = e.target.value; saveSettings(); loadInvoices(); }); } @@ -644,37 +657,6 @@ export function injectToolbar() { // ============================================================ export function setStatus(s) { filterStatus = s; saveSettings(); renderInvoiceView(); } -export function toggleParts() { - filterParts = !filterParts; - if (!filterParts) filterEmptyCost = false; - saveSettings(); - const itemBtn = document.getElementById('filter-item-type'); - const emptyBtn = document.getElementById('filter-empty-cost'); - if (itemBtn) { - itemBtn.className = filterParts - ? 'px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-blue-500 text-white' - : 'px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-white text-gray-600'; - } - if (emptyBtn) { - emptyBtn.style.display = filterParts ? '' : 'none'; - emptyBtn.className = filterEmptyCost - ? 'px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-blue-500 text-white' - : 'px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-white text-gray-600'; - } - loadInvoices(); -} -export function toggleEmptyCost() { - if (!filterParts) return; - filterEmptyCost = !filterEmptyCost; - saveSettings(); - const btn = document.getElementById('filter-empty-cost'); - if (btn) { - btn.className = filterEmptyCost - ? 'px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-blue-500 text-white' - : 'px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-white text-gray-600'; - } - loadInvoices(); -} export function viewPDF(id) { window.open(`/api/invoices/${id}/pdf`, '_blank'); } export function viewHTML(id) { window.open(`/api/invoices/${id}/html`, '_blank'); } @@ -915,5 +897,5 @@ async function populateWorkerFilter() { window.invoiceView = { viewPDF, viewHTML, syncFromQBO, resetQbo, markPaid, setEmailStatus, edit, remove, - loadInvoices, renderInvoiceView, setStatus, toggleParts, toggleEmptyCost, checkStripePayment, editSentDates ,_addSentDateRow, _saveSentDates + loadInvoices, renderInvoiceView, setStatus, checkStripePayment, editSentDates ,_addSentDateRow, _saveSentDates }; \ No newline at end of file diff --git a/src/routes/invoices.js b/src/routes/invoices.js index ef9de61..0a105bf 100644 --- a/src/routes/invoices.js +++ b/src/routes/invoices.js @@ -50,20 +50,28 @@ function buildPaymentLinkHtml(invoice) { // GET all invoices router.get('/', async (req, res) => { try { - const { has_parts, empty_cost_only } = req.query; + const CATEGORIES = { '5': 'Labor', '9': 'Parts', '115': 'Subscription' }; + const { category, item_search } = req.query; let whereClauses = []; - if (has_parts === 'true') { - const costCondition = empty_cost_only === 'true' - ? `AND (ii.unit_cost IS NULL OR ii.unit_cost = '')` - : ''; + let params = []; + + if (category && CATEGORIES[category]) { whereClauses.push(`EXISTS ( SELECT 1 FROM invoice_items ii - WHERE ii.invoice_id = i.id - AND ii.qbo_item_id = '9' - ${costCondition} + WHERE ii.invoice_id = i.id AND ii.qbo_item_id = $${params.length + 1} )`); + params.push(category); } + + if (item_search && item_search.trim()) { + whereClauses.push(`EXISTS ( + SELECT 1 FROM invoice_items ii + WHERE ii.invoice_id = i.id AND ii.description ILIKE $${params.length + 1} + )`); + params.push('%' + item_search.trim() + '%'); + } + const whereSQL = whereClauses.length > 0 ? 'WHERE ' + whereClauses.join(' AND ') : ''; const result = await pool.query(` @@ -73,7 +81,7 @@ router.get('/', async (req, res) => { LEFT JOIN customers c ON i.customer_id = c.id ${whereSQL} ORDER BY i.created_at DESC - `); + `, params); const rows = result.rows.map(r => ({ ...r, amount_paid: parseFloat(r.amount_paid) || 0,