diff --git a/client/src/graphql/bills.queries.js b/client/src/graphql/bills.queries.js
index a9c3433fa..03be22e61 100644
--- a/client/src/graphql/bills.queries.js
+++ b/client/src/graphql/bills.queries.js
@@ -42,7 +42,7 @@ export const QUERY_ALL_BILLS_PAGINATED = gql`
ro_number
}
}
- bills_aggregate {
+ bills_aggregate(where: $where) {
aggregate {
count(distinct: true)
}
diff --git a/client/src/pages/bills/bills.page.component.jsx b/client/src/pages/bills/bills.page.component.jsx
index e3d2f55b0..0fb17a978 100644
--- a/client/src/pages/bills/bills.page.component.jsx
+++ b/client/src/pages/bills/bills.page.component.jsx
@@ -16,7 +16,6 @@ import { DateFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
-import useLocalStorage from "../../utils/useLocalStorage";
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { logImEXEvent } from "../../firebase/firebase.utils";
@@ -24,16 +23,12 @@ const mapDispatchToProps = (dispatch) => ({
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" }))
});
-export function BillsListPage({ loading, data, refetch, total, setBillEnterContext }) {
+export function BillsListPage({ loading, data, refetch, total, setBillEnterContext, handleTableChange, sortedInfo }) {
const search = queryString.parse(useLocation().search);
const [openSearchResults, setOpenSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const { page } = search;
const history = useNavigate();
- const [state, setState] = useLocalStorage("bills_list_sort", {
- sortedInfo: {},
- filteredInfo: { vendorname: [] }
- });
const Templates = TemplateList("bill");
const { t } = useTranslation();
@@ -50,7 +45,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
}),
filters: (vendorsData?.vendors || []).map((v) => ({ text: v.name, value: v.id })),
filteredValue: search.vendorIds ? search.vendorIds.split(",") : null,
- sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
+ sortOrder: sortedInfo.columnKey === "vendorname" && sortedInfo.order,
render: (text, record) => {record.vendor.name}
},
{
@@ -58,7 +53,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "invoice_number",
key: "invoice_number",
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
- sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order
+ sortOrder: sortedInfo.columnKey === "invoice_number" && sortedInfo.order
},
{
title: t("jobs.fields.ro_number"),
@@ -68,7 +63,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
sortObject: (order) => ({
job: { ro_number: order === "descend" ? "desc" : "asc" }
}),
- sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
+ sortOrder: sortedInfo.columnKey === "ro_number" && sortedInfo.order,
render: (text, record) => record.job && {record.job.ro_number}
},
{
@@ -76,7 +71,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "date",
key: "date",
sorter: (a, b) => dateSort(a.date, b.date),
- sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
+ sortOrder: sortedInfo.columnKey === "date" && sortedInfo.order,
render: (text, record) => {record.date}
},
{
@@ -84,7 +79,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "total",
key: "total",
sorter: (a, b) => a.total - b.total,
- sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
+ sortOrder: sortedInfo.columnKey === "total" && sortedInfo.order,
render: (text, record) => {record.total}
},
{
@@ -92,7 +87,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "is_credit_memo",
key: "is_credit_memo",
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
- sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order,
+ sortOrder: sortedInfo.columnKey === "is_credit_memo" && sortedInfo.order,
render: (text, record) =>
},
{
@@ -100,7 +95,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "exported",
key: "exported",
sorter: (a, b) => a.exported - b.exported,
- sortOrder: state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
+ sortOrder: sortedInfo.columnKey === "exported" && sortedInfo.order,
render: (text, record) =>
},
{
@@ -164,37 +159,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
}
];
- const handleTableChange = (pagination, filters, sorter) => {
- setState({
- sortedInfo: sorter,
- filteredInfo: { ...state.filteredInfo, vendorname: filters.vendorname || [] }
- });
-
- search.page = pagination.current;
- if (filters.vendorname && filters.vendorname.length) {
- search.vendorIds = filters.vendorname.join(",");
- } else {
- delete search.vendorIds;
- }
- if (sorter && sorter.column && sorter.column.sortObject) {
- search.searchObj = JSON.stringify(sorter.column.sortObject(sorter.order));
- delete search.sortcolumn;
- delete search.sortorder;
- } else {
- delete search.searchObj;
- search.sortcolumn = sorter.order ? sorter.columnKey : null;
- search.sortorder = sorter.order;
- }
- history({ search: queryString.stringify(search) });
- logImEXEvent("bills_list_sort_filter", { pagination, filters, sorter });
- };
-
- useEffect(() => {
- if (!search.vendorIds && state.filteredInfo.vendorname && state.filteredInfo.vendorname.length) {
- search.vendorIds = state.filteredInfo.vendorname.join(",");
- history({ search: queryString.stringify(search) });
- }
- }, []);
+ // (State & URL handling moved to container - Option C)
useEffect(() => {
if (search.search && search.search.trim() !== "") {
diff --git a/client/src/pages/bills/bills.page.container.jsx b/client/src/pages/bills/bills.page.container.jsx
index 68c5d4c01..f4a7a1859 100644
--- a/client/src/pages/bills/bills.page.container.jsx
+++ b/client/src/pages/bills/bills.page.container.jsx
@@ -3,13 +3,14 @@ import queryString from "query-string";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
-import { useLocation } from "react-router-dom";
+import { useLocation, useNavigate } from "react-router-dom";
import AlertComponent from "../../components/alert/alert.component";
import BillDetailEditContainer from "../../components/bill-detail-edit/bill-detail-edit.container";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { QUERY_ALL_BILLS_PAGINATED } from "../../graphql/bills.queries";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import BillsPageComponent from "./bills.page.component";
+import useLocalStorage from "../../utils/useLocalStorage";
import { pageLimit } from "../../utils/config";
import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wrapper.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
@@ -23,7 +24,9 @@ const mapDispatchToProps = (dispatch) => ({
export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation();
- const searchParams = queryString.parse(useLocation().search);
+ const location = useLocation();
+ const history = useNavigate();
+ const searchParams = queryString.parse(location.search);
const { page, sortcolumn, sortorder, searchObj } = searchParams;
useEffect(() => {
@@ -37,6 +40,12 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
setBreadcrumbs([{ link: "/manage/bills", label: t("titles.bc.bills-list") }]);
}, [t, setBreadcrumbs, setSelectedHeader]);
+ // Persisted table state (sorting & filtering)
+ const [persistState, setPersistState] = useLocalStorage("bills_list_sort", {
+ sortedInfo: {},
+ filteredInfo: { vendorname: [] }
+ });
+
const { loading, error, data, refetch } = useQuery(QUERY_ALL_BILLS_PAGINATED, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
@@ -54,6 +63,90 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
}
});
+ const handleTableChange = (pagination, filters, sorter) => {
+ const search = queryString.parse(location.search);
+
+ const vendorArr = filters?.vendorname ?? [];
+ const newVendorIds = vendorArr.length ? vendorArr.join(",") : undefined;
+ const vendorFilterChanged = search.vendorIds !== newVendorIds;
+
+ search.page = vendorFilterChanged || !search.page ? 1 : pagination.current;
+ newVendorIds ? (search.vendorIds = newVendorIds) : delete search.vendorIds;
+
+ const { columnKey, order, column } = sorter || {};
+ if (column?.sortObject) {
+ search.searchObj = JSON.stringify(column.sortObject(order));
+ delete search.sortcolumn;
+ delete search.sortorder;
+ } else {
+ delete search.searchObj;
+ search.sortcolumn = order ? columnKey : null;
+ search.sortorder = order ?? null; // keep explicit null to mirror prior behavior
+ }
+
+ setPersistState({
+ sortedInfo: sorter || {},
+ filteredInfo: { vendorname: vendorArr }
+ });
+
+ history({ search: queryString.stringify(search) });
+ };
+
+ useEffect(() => {
+ const search = queryString.parse(location.search);
+ let changed = false;
+
+ const vendorPersisted = persistState.filteredInfo.vendorname || [];
+ if (!search.vendorIds && vendorPersisted.length) {
+ search.vendorIds = vendorPersisted.join(",");
+ search.page = 1; // reset page when injecting filter
+ changed = true;
+ }
+
+ const { sortedInfo } = persistState;
+ if (!search.searchObj && !search.sortcolumn && sortedInfo?.order) {
+ const { columnKey, order } = sortedInfo;
+ if (columnKey) {
+ const dir = order === "descend" ? "desc" : "asc";
+ if (columnKey === "vendorname") {
+ search.searchObj = JSON.stringify({ vendor: { name: dir } });
+ } else if (columnKey === "ro_number") {
+ search.searchObj = JSON.stringify({ job: { ro_number: dir } });
+ } else {
+ search.sortcolumn = columnKey;
+ search.sortorder = order;
+ }
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ history({ search: queryString.stringify(search) });
+ return;
+ }
+
+ const hasPersistSort = !!sortedInfo?.order;
+ const hasUrlSort = !!(search.searchObj || (search.sortcolumn && search.sortorder));
+ if (!hasPersistSort && hasUrlSort) {
+ let derived = {};
+ if (search.searchObj) {
+ try {
+ const o = JSON.parse(search.searchObj);
+ if (o.vendor?.name) {
+ derived = { columnKey: "vendorname", order: o.vendor.name === "desc" ? "descend" : "ascend" };
+ } else if (o.job?.ro_number) {
+ derived = { columnKey: "ro_number", order: o.job.ro_number === "desc" ? "descend" : "ascend" };
+ }
+ } catch {
+ /* ignore parse errors */
+ }
+ } else {
+ derived = { columnKey: search.sortcolumn, order: search.sortorder };
+ }
+ if (derived.order) setPersistState((prev) => ({ ...prev, sortedInfo: derived }));
+ }
+ }, [location.search]);
+
if (error) return ;
return (