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:
@@ -286,7 +286,7 @@ router.get(
|
||||
query('status').optional().isString(),
|
||||
query('customerAccountId').optional().isInt({ min: 1 }),
|
||||
query('q').optional().isString(),
|
||||
query('sort').optional().isIn(['newest', 'oldest', 'customer_asc']),
|
||||
query('sort').optional().isIn(['newest', 'oldest', 'issue_asc', 'issue_desc', 'customer_asc', 'customer_desc']),
|
||||
query('page').optional().isInt({ min: 1 }),
|
||||
query('pageSize').optional().isInt({ min: 1, max: 200 }),
|
||||
],
|
||||
@@ -302,7 +302,7 @@ router.get(
|
||||
const pageSize = parseInt(req.query.pageSize, 10) || 25;
|
||||
const result = await contractService.listContracts({
|
||||
filters,
|
||||
sort: req.query.sort || 'newest',
|
||||
sort: req.query.sort || 'issue_desc',
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
@@ -319,7 +319,7 @@ router.get(
|
||||
query('sourceQuoteId').optional({ values: 'falsy' }).isInt({ min: 1 }),
|
||||
query('unpaidOnly').optional({ values: 'falsy' }).isBoolean(),
|
||||
query('q').optional({ values: 'falsy' }).isString().isLength({ max: 255 }),
|
||||
query('sort').optional({ values: 'falsy' }).isIn(['newest', 'oldest', 'due_asc', 'due_desc', 'value_asc', 'value_desc', 'customer_asc']),
|
||||
query('sort').optional({ values: 'falsy' }).isIn(['newest', 'oldest', 'issue_asc', 'issue_desc', 'due_asc', 'due_desc', 'value_asc', 'value_desc', 'customer_asc', 'customer_desc']),
|
||||
query('page').optional({ values: 'falsy' }).isInt({ min: 1 }),
|
||||
query('pageSize').optional({ values: 'falsy' }).isInt({ min: 1, max: 100 }),
|
||||
],
|
||||
@@ -336,7 +336,7 @@ router.get(
|
||||
unpaidOnly: req.query.unpaidOnly === 'true' || req.query.unpaidOnly === true,
|
||||
q: req.query.q,
|
||||
},
|
||||
sort: req.query.sort || 'newest',
|
||||
sort: req.query.sort || 'issue_desc',
|
||||
page: req.query.page ? parseInt(req.query.page, 10) : 1,
|
||||
pageSize: req.query.pageSize ? parseInt(req.query.pageSize, 10) : 25,
|
||||
});
|
||||
|
||||
@@ -257,7 +257,7 @@ router.get(
|
||||
query('q').optional({ values: 'falsy' }).isString().isLength({ max: 255 }),
|
||||
query('from').optional({ values: 'falsy' }).isISO8601(),
|
||||
query('to').optional({ values: 'falsy' }).isISO8601(),
|
||||
query('sort').optional({ values: 'falsy' }).isIn(['newest', 'oldest', 'customer_asc', 'value_asc', 'value_desc']),
|
||||
query('sort').optional({ values: 'falsy' }).isIn(['newest', 'oldest', 'issue_asc', 'issue_desc', 'customer_asc', 'customer_desc', 'value_asc', 'value_desc']),
|
||||
query('page').optional({ values: 'falsy' }).isInt({ min: 1 }),
|
||||
query('pageSize').optional({ values: 'falsy' }).isInt({ min: 1, max: 100 }),
|
||||
],
|
||||
@@ -272,7 +272,7 @@ router.get(
|
||||
customerAccountId: req.query.customerAccountId ? parseInt(req.query.customerAccountId, 10) : null,
|
||||
from: req.query.from, to: req.query.to, q: req.query.q,
|
||||
},
|
||||
sort: req.query.sort || 'newest',
|
||||
sort: req.query.sort || 'issue_desc',
|
||||
page: req.query.page ? parseInt(req.query.page, 10) : 1,
|
||||
pageSize: req.query.pageSize ? parseInt(req.query.pageSize, 10) : 25,
|
||||
});
|
||||
|
||||
@@ -621,7 +621,7 @@ async function buildRenderContext(contract, inclusions) {
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
async function listContracts({ filters = {}, sort = 'newest', page = 1, pageSize = 25 } = {}) {
|
||||
async function listContracts({ filters = {}, sort = 'issue_desc', page = 1, pageSize = 25 } = {}) {
|
||||
return await withRetry(async () => {
|
||||
let query = db('contracts')
|
||||
.leftJoin('customer_accounts', 'contracts.customer_account_id', 'customer_accounts.id')
|
||||
@@ -658,11 +658,22 @@ async function listContracts({ filters = {}, sort = 'newest', page = 1, pageSize
|
||||
case 'oldest':
|
||||
query = query.orderBy('contracts.created_at', 'asc').orderBy('contracts.id', 'asc');
|
||||
break;
|
||||
case 'issue_asc':
|
||||
query = query.orderBy('contracts.issue_date', 'asc').orderBy('contracts.id', 'asc');
|
||||
break;
|
||||
case 'issue_desc':
|
||||
query = query.orderBy('contracts.issue_date', 'desc').orderBy('contracts.id', 'desc');
|
||||
break;
|
||||
case 'customer_asc':
|
||||
query = query
|
||||
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) asc')
|
||||
.orderBy('contracts.id', 'desc');
|
||||
break;
|
||||
case 'customer_desc':
|
||||
query = query
|
||||
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) desc')
|
||||
.orderBy('contracts.id', 'desc');
|
||||
break;
|
||||
case 'newest':
|
||||
default:
|
||||
query = query.orderBy('contracts.created_at', 'desc').orderBy('contracts.id', 'desc');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -333,7 +333,7 @@ function ensureCustomerFeatureEnabled(customer, feature) {
|
||||
* Filters: { status[], customerAccountId, from, to, q }
|
||||
* Sort: 'newest' | 'oldest' | 'customer_asc' | 'value_asc' | 'value_desc'
|
||||
*/
|
||||
async function listQuotes({ filters = {}, sort = 'newest', page = 1, pageSize = 25 } = {}) {
|
||||
async function listQuotes({ filters = {}, sort = 'issue_desc', page = 1, pageSize = 25 } = {}) {
|
||||
return await withRetry(async () => {
|
||||
let query = db('quotes')
|
||||
.leftJoin('customer_accounts', 'quotes.customer_account_id', 'customer_accounts.id')
|
||||
@@ -385,11 +385,22 @@ async function listQuotes({ filters = {}, sort = 'newest', page = 1, pageSize =
|
||||
case 'oldest':
|
||||
query = query.orderBy('quotes.created_at', 'asc').orderBy('quotes.id', 'asc');
|
||||
break;
|
||||
case 'issue_asc':
|
||||
query = query.orderBy('quotes.issue_date', 'asc').orderBy('quotes.id', 'asc');
|
||||
break;
|
||||
case 'issue_desc':
|
||||
query = query.orderBy('quotes.issue_date', 'desc').orderBy('quotes.id', 'desc');
|
||||
break;
|
||||
case 'customer_asc':
|
||||
query = query
|
||||
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) asc')
|
||||
.orderBy('quotes.id', 'desc');
|
||||
break;
|
||||
case 'customer_desc':
|
||||
query = query
|
||||
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) desc')
|
||||
.orderBy('quotes.id', 'desc');
|
||||
break;
|
||||
case 'value_asc':
|
||||
query = query.orderBy('quotes.total_amount_minor', 'asc');
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user