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 (