feat(crm): Finder-style sortable column headers, default sort by issue date

Replace the sort <select> dropdowns on the invoice, quote and contract
list pages with clickable column headers that toggle asc/desc and show a
chevron indicator. Adds a shared SortableHeader component + useColumnSort
hook that maps clickable columns onto the server-side sort enum.
Make issue date (newest first) the standard sort on all three lists,
set at the frontend, route and service layers. Adds issue_asc/issue_desc
to invoices and an "Issued" column to the bills table so the default is
visible and toggleable. Extends sort coverage so every clickable column
has both directions (+customer_desc on all; +issue_asc/desc on
quotes/contracts). Storno rows remain listed.
This commit is contained in:
Luca
2026-06-02 10:59:36 +02:00
parent 095edfe06d
commit d9251c0850
16 changed files with 197 additions and 61 deletions
+8 -1
View File
@@ -410,7 +410,7 @@ async function getOrCreateMonthlyDraft(customer, adminId, trx) {
// Public API
// ---------------------------------------------------------------------
async function listInvoices({ filters = {}, sort = 'newest', page = 1, pageSize = 25 } = {}) {
async function listInvoices({ filters = {}, sort = 'issue_desc', page = 1, pageSize = 25 } = {}) {
return await withRetry(async () => {
let query = db('invoices')
.leftJoin('customer_accounts', 'invoices.customer_account_id', 'customer_accounts.id')
@@ -477,6 +477,8 @@ async function listInvoices({ filters = {}, sort = 'newest', page = 1, pageSize
// reflects when the row landed in the DB. id is the tiebreaker
// for rows that share a created_at second.
case 'oldest': query = query.orderBy('invoices.created_at', 'asc').orderBy('invoices.id', 'asc'); break;
case 'issue_asc': query = query.orderBy('invoices.issue_date', 'asc').orderBy('invoices.id', 'asc'); break;
case 'issue_desc': query = query.orderBy('invoices.issue_date', 'desc').orderBy('invoices.id', 'desc'); break;
case 'due_asc': query = query.orderBy('invoices.due_date', 'asc'); break;
case 'due_desc': query = query.orderBy('invoices.due_date', 'desc'); break;
case 'value_asc': query = query.orderBy('invoices.total_amount_minor', 'asc'); break;
@@ -486,6 +488,11 @@ async function listInvoices({ filters = {}, sort = 'newest', page = 1, pageSize
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) asc')
.orderBy('invoices.id', 'desc');
break;
case 'customer_desc':
query = query
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) desc')
.orderBy('invoices.id', 'desc');
break;
case 'newest':
default:
query = query.orderBy('invoices.created_at', 'desc').orderBy('invoices.id', 'desc');