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:
@@ -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)}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user