Merged in feature/IO-3365-Bills-Filtering-Server-Side (pull request #2608)
IO-3365 Bills Filtering Server Side Approved-by: Patrick Fic
This commit is contained in:
@@ -42,7 +42,7 @@ export const QUERY_ALL_BILLS_PAGINATED = gql`
|
||||
ro_number
|
||||
}
|
||||
}
|
||||
bills_aggregate {
|
||||
bills_aggregate(where: $where) {
|
||||
aggregate {
|
||||
count(distinct: true)
|
||||
}
|
||||
|
||||
@@ -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) => <span>{record.vendor.name}</span>
|
||||
},
|
||||
{
|
||||
@@ -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 && <Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number}</Link>
|
||||
},
|
||||
{
|
||||
@@ -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) => <DateFormatter>{record.date}</DateFormatter>
|
||||
},
|
||||
{
|
||||
@@ -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) => <CurrencyFormatter>{record.total}</CurrencyFormatter>
|
||||
},
|
||||
{
|
||||
@@ -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) => <Checkbox checked={record.is_credit_memo} />
|
||||
},
|
||||
{
|
||||
@@ -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) => <Checkbox checked={record.exported} />
|
||||
},
|
||||
{
|
||||
@@ -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() !== "") {
|
||||
|
||||
@@ -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 <AlertComponent message={error.message} type="error" />;
|
||||
return (
|
||||
<FeatureWrapperComponent
|
||||
@@ -71,6 +164,8 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
|
||||
loading={loading}
|
||||
refetch={refetch}
|
||||
total={data ? data.bills_aggregate.aggregate.count : 0}
|
||||
handleTableChange={handleTableChange}
|
||||
sortedInfo={persistState.sortedInfo || {}}
|
||||
/>
|
||||
|
||||
<BillDetailEditContainer />
|
||||
|
||||
Reference in New Issue
Block a user