new Filter
This commit is contained in:
1
migrations/add-invoice-items-filter-index.sql
Normal file
1
migrations/add-invoice-items-filter-index.sql
Normal file
@@ -0,0 +1 @@
|
||||
CREATE INDEX idx_invoice_items_invoice_qbo ON public.invoice_items USING btree (invoice_id, qbo_item_id);
|
||||
@@ -6,6 +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 filterItemType = localStorage.getItem('inv_filterItemType') === 'true';
|
||||
let filterEmptyCost = localStorage.getItem('inv_filterEmptyCost') === 'true';
|
||||
|
||||
const OVERDUE_DAYS = 30;
|
||||
|
||||
@@ -196,6 +198,8 @@ function saveSettings() {
|
||||
localStorage.setItem('inv_groupBy', groupBy);
|
||||
localStorage.setItem('inv_filterCustomer', filterCustomer);
|
||||
localStorage.setItem('inv_filterWorker', filterWorker);
|
||||
localStorage.setItem('inv_filterItemType', filterItemType);
|
||||
localStorage.setItem('inv_filterEmptyCost', filterEmptyCost);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -204,7 +208,14 @@ function saveSettings() {
|
||||
|
||||
export async function loadInvoices() {
|
||||
try {
|
||||
const response = await fetch('/api/invoices');
|
||||
const params = new URLSearchParams();
|
||||
if (filterItemType) {
|
||||
params.set('has_parts_or_subscription', 'true');
|
||||
if (filterEmptyCost) params.set('empty_cost_only', 'true');
|
||||
}
|
||||
const qs = params.toString();
|
||||
const url = qs ? `/api/invoices?${qs}` : '/api/invoices';
|
||||
const response = await fetch(url);
|
||||
invoices = await response.json();
|
||||
renderInvoiceView();
|
||||
loadLastSync();
|
||||
@@ -599,6 +610,13 @@ export function injectToolbar() {
|
||||
</select>
|
||||
</div>
|
||||
<div class="w-px h-8 bg-gray-300"></div>
|
||||
<div class="flex items-center gap-1 border border-gray-300 rounded-lg p-1 bg-gray-100">
|
||||
<button id="filter-item-type" onclick="window.invoiceView.toggleItemType()"
|
||||
class="px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${filterItemType ? 'bg-blue-500 text-white' : 'bg-white text-gray-600'}">Cost Items</button>
|
||||
<button id="filter-empty-cost" onclick="window.invoiceView.toggleEmptyCost()" style="${filterItemType ? '' : 'display:none'}"
|
||||
class="px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${filterEmptyCost ? 'bg-blue-500 text-white' : 'bg-white text-gray-600'}">Empty cost only</button>
|
||||
</div>
|
||||
<div class="w-px h-8 bg-gray-300"></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="window.invoiceView.syncFromQBO()" class="px-3 py-1.5 bg-indigo-600 text-white rounded-md text-xs font-medium hover:bg-indigo-700">
|
||||
⟳ Sync from QBO
|
||||
@@ -631,6 +649,37 @@ export function injectToolbar() {
|
||||
// ============================================================
|
||||
|
||||
export function setStatus(s) { filterStatus = s; saveSettings(); renderInvoiceView(); }
|
||||
export function toggleItemType() {
|
||||
filterItemType = !filterItemType;
|
||||
if (!filterItemType) filterEmptyCost = false;
|
||||
saveSettings();
|
||||
const itemBtn = document.getElementById('filter-item-type');
|
||||
const emptyBtn = document.getElementById('filter-empty-cost');
|
||||
if (itemBtn) {
|
||||
itemBtn.className = filterItemType
|
||||
? '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 = filterItemType ? '' : '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 (!filterItemType) 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'); }
|
||||
|
||||
@@ -871,5 +920,5 @@ async function populateWorkerFilter() {
|
||||
|
||||
window.invoiceView = {
|
||||
viewPDF, viewHTML, syncFromQBO, resetQbo, markPaid, setEmailStatus, edit, remove,
|
||||
loadInvoices, renderInvoiceView, setStatus, checkStripePayment, editSentDates ,_addSentDateRow, _saveSentDates
|
||||
loadInvoices, renderInvoiceView, setStatus, toggleItemType, toggleEmptyCost, checkStripePayment, editSentDates ,_addSentDateRow, _saveSentDates
|
||||
};
|
||||
@@ -515,6 +515,13 @@ CREATE UNIQUE INDEX idx_customers_qbo_id ON public.customers USING btree (qbo_id
|
||||
CREATE INDEX idx_invoice_items_invoice_id ON public.invoice_items USING btree (invoice_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx_invoice_items_invoice_qbo; Type: INDEX; Schema: public; Owner: quoteuser
|
||||
--
|
||||
|
||||
CREATE INDEX idx_invoice_items_invoice_qbo ON public.invoice_items USING btree (invoice_id, qbo_item_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx_invoices_created_from_quote; Type: INDEX; Schema: public; Owner: quoteuser
|
||||
--
|
||||
|
||||
@@ -50,11 +50,28 @@ function buildPaymentLinkHtml(invoice) {
|
||||
// GET all invoices
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { has_parts_or_subscription, empty_cost_only } = req.query;
|
||||
|
||||
let whereClauses = [];
|
||||
if (has_parts_or_subscription === 'true') {
|
||||
const costCondition = empty_cost_only === 'true'
|
||||
? `AND (ii.unit_cost IS NULL OR ii.unit_cost = '')`
|
||||
: '';
|
||||
whereClauses.push(`EXISTS (
|
||||
SELECT 1 FROM invoice_items ii
|
||||
WHERE ii.invoice_id = i.id
|
||||
AND (ii.qbo_item_id = '9' OR ii.qbo_item_id = '115')
|
||||
${costCondition}
|
||||
)`);
|
||||
}
|
||||
const whereSQL = whereClauses.length > 0 ? 'WHERE ' + whereClauses.join(' AND ') : '';
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT i.*, c.name as customer_name, c.qbo_id as customer_qbo_id,
|
||||
COALESCE((SELECT SUM(pi.amount) FROM payment_invoices pi WHERE pi.invoice_id = i.id), 0) as amount_paid
|
||||
FROM invoices i
|
||||
LEFT JOIN customers c ON i.customer_id = c.id
|
||||
${whereSQL}
|
||||
ORDER BY i.created_at DESC
|
||||
`);
|
||||
const rows = result.rows.map(r => ({
|
||||
|
||||
Reference in New Issue
Block a user