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,