chore(env): update application version to 2.3.1 in production environment

feat(customers): enhance customer list filters with lazy-loaded company options

- Added InputGroup components for searching by full name and email in CustomerListFilters.
- Integrated Select component for company selection with lazy-loading capabilities.
- Implemented company search and load more functionality using useCompaniesInfiniteQuery.
- Updated useCustomerListPresenter to manage company search state and loading indicators.
- Enhanced customer list filtering experience by allowing dynamic company selection.
This commit is contained in:
AmirReza Jamali
2026-06-08 12:48:04 +03:30
parent 72948a9b55
commit a62963a93d
5 changed files with 207 additions and 44 deletions
@@ -1,9 +1,11 @@
"use client";
import InputGroup from "@/components/FormElements/InputGroup";
import { Select } from "@/components/FormElements/select";
import { Button } from "@/components/ui-elements/button";
import InlineListFiltersPanel from "@/components/ui/InlineListFiltersPanel";
import { AdminRoles, CustomerStatus, CustomerType } from "@/constants/enums";
import { ILabelValue } from "@/types/shared";
import { getEnumAsArray } from "@/utils/shared";
import {
FieldValues,
@@ -22,11 +24,25 @@ interface CustomerListFiltersProps {
watch: UseFormWatch<FieldValues>;
setValue: UseFormSetValue<any>;
onClearAll: () => void;
/** Lazy-loaded company options for the company dropdown. */
companyItems: ILabelValue[];
onCompanySearch: (query: string) => void;
onCompanyLoadMore: () => void;
companyHasMore?: boolean;
companyIsLoading: boolean;
companyIsLoadingMore: boolean;
}
function countActive(values: Record<string, unknown>): number {
let n = 0;
for (const key of ["status", "role", "type"] as const) {
for (const key of [
"status",
"role",
"type",
"full_name",
"email",
"company_id",
] as const) {
const v = values[key];
if (v !== undefined && v !== null && v !== "" && String(v).trim()) n++;
}
@@ -41,6 +57,12 @@ const CustomerListFilters = ({
watch,
setValue,
onClearAll,
companyItems,
onCompanySearch,
onCompanyLoadMore,
companyHasMore,
companyIsLoading,
companyIsLoadingMore,
}: CustomerListFiltersProps) => {
const watched = watch();
const activeCount = useMemo(
@@ -63,6 +85,36 @@ const CustomerListFilters = ({
data-cy="customers-filter-form"
>
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
<InputGroup
{...filterRegister("full_name")}
name="full_name"
label="Full name"
type="text"
placeholder="Search by full name"
/>
<InputGroup
{...filterRegister("email")}
name="email"
label="Email"
type="text"
placeholder="Search by email"
/>
<Select
{...filterRegister("company_id")}
items={companyItems}
label="Company"
name="company_id"
placeholder="Select company"
clearable
value={watch("company_id")}
onClear={() => setValue("company_id", undefined)}
searchPlaceholder="Search companies…"
onSearch={onCompanySearch}
onLoadMore={onCompanyLoadMore}
hasMore={companyHasMore}
isLoading={companyIsLoading}
isLoadingMore={companyIsLoadingMore}
/>
<Select
{...filterRegister("status")}
items={getEnumAsArray(CustomerStatus)}
+12
View File
@@ -75,6 +75,12 @@ const CustomersTable = () => {
openResetPasswordModalForCustomer,
generatedPassword,
isResettingPassword,
companyItems,
onCompanySearch,
onCompanyLoadMore,
companyHasMore,
companyIsLoading,
companyIsLoadingMore,
} = useCustomerListPresenter();
return (
@@ -88,6 +94,12 @@ const CustomersTable = () => {
search={search}
watch={watch}
onClearAll={clearAllFilters}
companyItems={companyItems}
onCompanySearch={onCompanySearch}
onCompanyLoadMore={onCompanyLoadMore}
companyHasMore={companyHasMore}
companyIsLoading={companyIsLoading}
companyIsLoadingMore={companyIsLoadingMore}
/>
<div ref={tableSectionRef}>
<TableSortProvider
@@ -3,11 +3,13 @@
import { SortDirection } from "@/components/ui/tableSortContext";
import {
useAssignCompany,
useCompaniesInfiniteQuery,
useCompanyFullList,
useCustomersQuery,
useDeleteCustomerQuery,
useResetCustomerPassword,
} from "@/hooks/queries";
import useDebounce from "@/hooks/useDebounce";
import { useHybridPagination } from "@/hooks/useHybridPagination";
import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations";
import { TAssignCustomerToCompanyCredentials, TCustomer } from "@/lib/api";
@@ -50,6 +52,35 @@ const useCustomerListPresenter = () => {
setValue: setFilterValue,
} = useForm();
// Lazy-loaded company options for the inline filter dropdown.
const [companySearch, setCompanySearch] = useState("");
const debouncedCompanySearch = useDebounce(companySearch, 400);
const companiesQuery = useCompaniesInfiniteQuery(
debouncedCompanySearch ? { search: debouncedCompanySearch } : undefined,
);
const companyItems = useMemo(
() =>
companiesQuery.data?.pages.flatMap(
(page) =>
page.data.companies?.map((company) => ({
label: company.name,
value: company.id,
})) ?? [],
) ?? [],
[companiesQuery.data],
);
const onCompanySearch = useCallback((query: string) => {
setCompanySearch(query);
}, []);
const onCompanyLoadMore = useCallback(() => {
if (companiesQuery.hasNextPage && !companiesQuery.isFetchingNextPage) {
companiesQuery.fetchNextPage();
}
}, [companiesQuery]);
const tableSectionRef = useRef<HTMLDivElement>(null);
const skeletonTbodyRef = useRef<HTMLTableSectionElement>(null);
const dataTbodyRef = useRef<HTMLTableSectionElement>(null);
@@ -125,7 +156,15 @@ const useCustomerListPresenter = () => {
};
const clearAllFilters = useCallback(() => {
filterFormReset({ status: undefined, role: undefined, type: undefined });
filterFormReset({
status: undefined,
role: undefined,
type: undefined,
full_name: undefined,
email: undefined,
company_id: undefined,
});
setCompanySearch("");
router.push(pathName);
}, [pathName, router, filterFormReset]);
@@ -228,6 +267,13 @@ const useCustomerListPresenter = () => {
openResetPasswordModalForCustomer,
generatedPassword,
isResettingPassword,
companyItems,
onCompanySearch,
onCompanyLoadMore,
companyHasMore: companiesQuery.hasNextPage,
companyIsLoading:
companiesQuery.isFetching && !companiesQuery.isFetchingNextPage,
companyIsLoadingMore: companiesQuery.isFetchingNextPage,
};
};