Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d59d5dc
backup
Kato-111 Jul 3, 2025
0dd4b76
Merge branch 'main' of github.com:erxes/erxes-next into kato
Kato-111 Jul 4, 2025
8b81027
phone validation status logic separation
Kato-111 Jul 4, 2025
7ad9d3c
backup
Kato-111 Jul 4, 2025
b14cfc0
backup
Kato-111 Jul 6, 2025
2e5fd2a
backup
Kato-111 Jul 7, 2025
478173d
Merge branch 'main' of github.com:erxes/erxes-next into kato
Kato-111 Jul 7, 2025
16ffd7a
pending
Kato-111 Jul 21, 2025
5958f1a
follow up
Kato-111 Jul 21, 2025
76cd8c5
add missing fields
batmnkh2344 Jul 22, 2025
191b0e3
-ui module improvements and contact detail
Kato-111 Jul 25, 2025
6526b4b
phone input and field improvements and implementation on customer detail
Kato-111 Jul 26, 2025
daa1b4e
Enhance customer detail functionality with subscription and descripti…
Kato-111 Jul 27, 2025
0ea3d73
Merge branches 'kato' and 'kato' of github.com:erxes/erxes-next into …
Kato-111 Jul 27, 2025
5678117
Merge branch 'main' of github.com:erxes/erxes-next into kato
Kato-111 Jul 27, 2025
7cb64ee
Enhance customer detail components with error handling and loading st…
Kato-111 Jul 28, 2025
1620153
Refactor CustomerGeneral component to simplify customerDetail check a…
Kato-111 Jul 28, 2025
6d86584
Add type attributes to NumberField and PhoneInput components for impr…
Kato-111 Jul 28, 2025
f2fef86
Add phone validation and improve PhoneInput component with validation…
Kato-111 Jul 28, 2025
1cf5e91
Merge branch 'main' of github.com:erxes/erxes-next into product-fix
Kato-111 Jul 28, 2025
35a9f77
Merge branch 'main' of github.com:erxes/erxes-next into product-fix
Kato-111 Jul 28, 2025
efd2267
fix: table data query being cancelled on initial render due to a vari…
Kato-111 Jul 28, 2025
1a309db
feat: enhance product management with improved filtering and detail h…
Kato-111 Jul 30, 2025
08054bd
feat: implement category selection and inline display components
Kato-111 Aug 4, 2025
ff8b238
Merge branch 'main' of github.com:erxes/erxes-next into product-fix
Kato-111 Aug 4, 2025
a708f0b
feat: enhance product editing and filtering functionality
Kato-111 Aug 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const CompaniesRecordTable = () => {
return (
<RecordTable.Provider
columns={companyColumns}
data={companies || []}
data={companies || [{}]}
stickyColumns={['more', 'checkbox', 'avatar', 'primaryName']}
className="m-3"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const GET_COMPANIES = gql`
ownerId
tagIds
score
cursor
}
${GQL_PAGE_INFO}
}
Expand Down
109 changes: 61 additions & 48 deletions frontend/core-ui/src/modules/contacts/customers/hooks/useCustomers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,70 +17,83 @@ import { useIsCustomerLeadSessionKey } from './useCustomerLeadSessionKey';

const CUSTOMERS_PER_PAGE = 30;


export const useCustomersVariables = (variables?: QueryHookOptions<ICursorListResponse<ICustomer>>['variables']) => {
export const useCustomersVariables = (
variables?: QueryHookOptions<ICursorListResponse<ICustomer>>['variables'],
) => {
const { isLead } = useIsCustomerLeadSessionKey();
const [{ searchValue, tags, created, updated, lastSeen, brand, birthday }] =
useMultiQueryState<{
searchValue: string;
tags: string[];
created: string;
updated: string;
lastSeen: string;
brand: string;
birthday: string;
}>(['searchValue', 'tags', 'created', 'updated', 'lastSeen', 'brand', 'birthday']);
const { sessionKey } = useIsCustomerLeadSessionKey();
useMultiQueryState<{
searchValue: string;
tags: string[];
created: string;
updated: string;
lastSeen: string;
brand: string;
birthday: string;
}>([
'searchValue',
'tags',
'created',
'updated',
'lastSeen',
'brand',
'birthday',
]);
const { sessionKey } = useIsCustomerLeadSessionKey();

const { cursor } = useRecordTableCursor({
sessionKey,
});
const { cursor } = useRecordTableCursor({
sessionKey,
});

const customersQueryVariables = {
limit: CUSTOMERS_PER_PAGE,
orderBy: {
createdAt: -1,
},
cursor,
searchValue: searchValue || undefined,
tagIds: tags || undefined,
brandIds: brand ? [brand] : undefined,
dateFilters: JSON.stringify({
createdAt: {
gte: parseDateRangeFromString(created)?.from,
lte: parseDateRangeFromString(created)?.to,
},
updatedAt: {
gte: parseDateRangeFromString(updated)?.from,
lte: parseDateRangeFromString(updated)?.to,
const customersQueryVariables = {
limit: CUSTOMERS_PER_PAGE,
orderBy: {
createdAt: -1,
},
lastSeenAt: {
gte: parseDateRangeFromString(lastSeen)?.from,
lte: parseDateRangeFromString(lastSeen)?.to,
},
birthDate: {
gte: parseDateRangeFromString(birthday)?.from,
lte: parseDateRangeFromString(birthday)?.to,
}
}),
type: isLead ? 'lead' : 'customer',
...variables,
cursor,
searchValue: searchValue || undefined,
tagIds: tags || undefined,
brandIds: brand ? [brand] : undefined,
dateFilters: JSON.stringify({
createdAt: {
gte: parseDateRangeFromString(created)?.from,
lte: parseDateRangeFromString(created)?.to,
},
updatedAt: {
gte: parseDateRangeFromString(updated)?.from,
lte: parseDateRangeFromString(updated)?.to,
},
lastSeenAt: {
gte: parseDateRangeFromString(lastSeen)?.from,
lte: parseDateRangeFromString(lastSeen)?.to,
},
birthDate: {
gte: parseDateRangeFromString(birthday)?.from,
lte: parseDateRangeFromString(birthday)?.to,
},
}),
type: isLead ? 'lead' : 'customer',
...variables,
};
return { customersQueryVariables };
};
return {customersQueryVariables};
}


export const useCustomers = (
options?: QueryHookOptions<ICursorListResponse<ICustomer>>,
) => {

const { sessionKey } = useIsCustomerLeadSessionKey();
const { cursor } = useRecordTableCursor({
sessionKey,
});
const setCustomerTotalCount = useSetAtom(customerTotalCountAtom);
// Customer Filter implementation
const { data, loading, fetchMore } = useQuery<ICursorListResponse<ICustomer>>(
GET_CUSTOMERS,
{
...options,
variables: useCustomersVariables(options?.variables)?.customersQueryVariables,
variables: useCustomersVariables(options?.variables)
?.customersQueryVariables,
skip: cursor === undefined,
},
);

Expand Down
57 changes: 46 additions & 11 deletions frontend/core-ui/src/modules/products/components/ProductColumns.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useProductsEdit } from '@/products/hooks/useProductsEdit';
import { renderingProductDetailAtom } from '@/products/states/productDetailStates';
import {
IconCategory,
IconCurrencyDollar,
Expand All @@ -12,21 +14,43 @@ import {
RecordTableCellDisplay,
CurrencyFormatedDisplay,
CurrencyCode,
useQueryState,
RecordTablePopover,
RecordTableCellTrigger,
Badge,
RecordTableCellContent,
Input,
} from 'erxes-ui';
import { IProduct } from 'ui-modules';
import { productMoreColumn } from './ProductMoreColumn';
import { useSetAtom } from 'jotai';
import { IProduct, SelectCategory } from 'ui-modules';
export const productColumns: ColumnDef<IProduct>[] = [
productMoreColumn,
RecordTable.checkboxColumn as ColumnDef<IProduct>,
{
id: 'name',
accessorKey: 'name',
header: () => <RecordTable.InlineHead icon={IconLabel} label="Name" />,
cell: ({ cell }) => {
const name = cell.getValue() as string;
const [, setProductId] = useQueryState('productId');
const setRenderingProductDetail = useSetAtom(renderingProductDetailAtom);
return (
<RecordTableCellDisplay>
<TextOverflowTooltip value={cell.getValue() as string} />
</RecordTableCellDisplay>
<RecordTablePopover>
<RecordTableCellTrigger>
<Badge
variant="secondary"
onClick={(e) => {
e.stopPropagation();
setRenderingProductDetail(true);
setProductId(cell.row.original._id);
}}
>
{name}
</Badge>
</RecordTableCellTrigger>
<RecordTableCellContent className="min-w-72">
<Input value={name || ''} />
</RecordTableCellContent>
</RecordTablePopover>
);
},
},
Expand Down Expand Up @@ -80,12 +104,23 @@ export const productColumns: ColumnDef<IProduct>[] = [
<RecordTable.InlineHead icon={IconCategory} label="Category" />
),
cell: ({ cell }) => {
const { productsEdit } = useProductsEdit();
return (
<RecordTableCellDisplay>
<TextOverflowTooltip
value={cell.row.original?.category?.name || ''}
/>
</RecordTableCellDisplay>
<SelectCategory.InlineCell
mode="single"
value={cell.getValue() as string}
onValueChange={(value) => {
productsEdit({
variables: {
_id: cell.row.original._id,
categoryId: value,
},
});
}}
categories={
cell.row.original.category ? [cell.row.original.category] : []
}
/>
);
},
},
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,38 +1,80 @@
import { ProductHotKeyScope } from '@/products/types/ProductsHotKeyScope';
import { IconSearch } from '@tabler/icons-react';
import { Combobox, Command, Filter } from 'erxes-ui';
import { TagsFilter } from 'ui-modules';
import { Combobox, Command, Filter, useMultiQueryState } from 'erxes-ui';
import { TagsFilter, SelectBrand, SelectCategory } from 'ui-modules';
import { PRODUCTS_CURSOR_SESSION_KEY } from '../constants/productsCursorSessionKey';

export const ProductsFilter = () => {
const [queries] = useMultiQueryState<{
searchValue: string;
created: string;
updated: string;
lastSeen: string;
}>(['searchValue', 'created', 'updated', 'lastSeen']);
const { searchValue } = queries || {};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing 'brand' to query state type

The brand query parameter is used by SelectBrand.FilterBar but is missing from the type definition in useMultiQueryState.

Apply this diff to include the missing type:

   const [queries] = useMultiQueryState<{
     searchValue: string;
     created: string;
     updated: string;
     lastSeen: string;
+    brand: string;
-  }>(['searchValue', 'created', 'updated', 'lastSeen']);
+  }>(['searchValue', 'created', 'updated', 'lastSeen', 'brand']);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [queries] = useMultiQueryState<{
searchValue: string;
created: string;
updated: string;
lastSeen: string;
}>(['searchValue', 'created', 'updated', 'lastSeen']);
const { searchValue } = queries || {};
const [queries] = useMultiQueryState<{
searchValue: string;
created: string;
updated: string;
lastSeen: string;
brand: string;
}>(['searchValue', 'created', 'updated', 'lastSeen', 'brand']);
🤖 Prompt for AI Agents
In frontend/core-ui/src/modules/products/components/ProductsFilter.tsx around
lines 8 to 14, the type definition for useMultiQueryState is missing the 'brand'
property, which is used by SelectBrand.FilterBar. Update the generic type
argument to include 'brand: string' along with the existing properties to ensure
the query state type correctly reflects all used query parameters.


return (
<Filter id="products-filter" sessionKey={PRODUCTS_CURSOR_SESSION_KEY}>
<Filter.Bar>
{searchValue && (
<Filter.BarItem>
<Filter.BarName>
<IconSearch />
Search
</Filter.BarName>
<Filter.BarButton filterKey="searchValue" inDialog>
{searchValue}
</Filter.BarButton>
<Filter.BarClose filterKey="searchValue" />
</Filter.BarItem>
)}
<TagsFilter.Bar tagType="core:product" />
<SelectBrand.FilterBar />
<SelectCategory.FilterBar />
<ProductsFilterPopover />
</Filter.Bar>
</Filter>
);
};

export const ProductsFilterPopover = () => {
const ProductsFilterPopover = () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use named export for ProductsFilterPopover

Per the coding guidelines, components should use named exports rather than local constants.

Apply this diff to export the component:

-const ProductsFilterPopover = () => {
+export const ProductsFilterPopover = () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ProductsFilterPopover = () => {
export const ProductsFilterPopover = () => {
🤖 Prompt for AI Agents
In frontend/core-ui/src/modules/products/components/ProductsFilter.tsx at line
40, change the declaration of ProductsFilterPopover from a local constant to a
named export by adding the export keyword before the component definition. This
aligns with the coding guidelines requiring components to use named exports.

const [queries] = useMultiQueryState<{
searchValue: string;
created: string;
updated: string;
lastSeen: string;
}>(['searchValue', 'created', 'updated', 'lastSeen']);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove duplicate useMultiQueryState call

The same useMultiQueryState hook is called again in ProductsFilterPopover with identical parameters. Consider passing the queries as props to avoid redundant hook calls.

Apply this diff to optimize:

-const ProductsFilterPopover = () => {
-  const [queries] = useMultiQueryState<{
-    searchValue: string;
-    created: string;
-    updated: string;
-    lastSeen: string;
-  }>(['searchValue', 'created', 'updated', 'lastSeen']);
+const ProductsFilterPopover = ({ queries }: { queries: any }) => {

And update the usage:

-        <ProductsFilterPopover />
+        <ProductsFilterPopover queries={queries} />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [queries] = useMultiQueryState<{
searchValue: string;
created: string;
updated: string;
lastSeen: string;
}>(['searchValue', 'created', 'updated', 'lastSeen']);
// In frontend/core-ui/src/modules/products/components/ProductsFilter.tsx
// --- Parent component: lift the hook up and pass queries down ---
export function ProductsFilter() {
const [queries] = useMultiQueryState<{
searchValue: string;
created: string;
updated: string;
lastSeen: string;
}>(['searchValue', 'created', 'updated', 'lastSeen']);
return (
<div>
{/* other filter UI */}
- <ProductsFilterPopover />
+ <ProductsFilterPopover queries={queries} />
</div>
);
}
// --- Child component: accept queries as a prop instead of re-calling the hook ---
interface ProductsFilterPopoverProps {
queries: {
searchValue: string;
created: string;
updated: string;
lastSeen: string;
};
}
export function ProductsFilterPopover({ queries }: ProductsFilterPopoverProps) {
// Removed duplicate useMultiQueryState call here—
// use the `queries` prop directly to render your filters.
return (
<div>
{/* render filter inputs, e.g.: */}
<input value={queries.searchValue} /* … */ />
{/* etc. */}
</div>
);
}
🤖 Prompt for AI Agents
In frontend/core-ui/src/modules/products/components/ProductsFilter.tsx around
lines 41 to 46, the useMultiQueryState hook is called redundantly both here and
in ProductsFilterPopover with the same parameters. To fix this, remove the
duplicate useMultiQueryState call from ProductsFilterPopover and instead pass
the queries state obtained here as props to ProductsFilterPopover. Update
ProductsFilterPopover to accept these props and use them directly, eliminating
the redundant hook invocation.


const hasFilters = Object.values(queries || {}).some(
(value) => value !== null,
);

return (
<>
<Filter.Popover scope={ProductHotKeyScope.ProductsPage}>
<Filter.Trigger />
<Filter.Trigger isFiltered={hasFilters} />
<Combobox.Content>
<Filter.View>
<Command>
<Filter.CommandInput placeholder="Filter" variant="secondary" />

<Filter.CommandInput
placeholder="Filter"
variant="secondary"
className="bg-background"
/>
<Command.List className="p-1">
<Filter.Item value="searchValue" inDialog>
<IconSearch />
Search
</Filter.Item>
<TagsFilter />
<SelectBrand.FilterItem />
<SelectCategory.FilterItem />
</Command.List>
</Command>
</Filter.View>
<TagsFilter.View tagType="core:product" />
<SelectBrand.FilterView />
<SelectCategory.FilterView />
</Combobox.Content>
</Filter.Popover>
<Filter.Dialog>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const ProductBasicFields: React.FC<ProductBasicFieldsProps> = ({
<SelectCategory
selected={field.value}
onSelect={field.onChange}
className="w-full border border-gray-300"
className="w-full"
size="lg"
/>
</Form.Control>
Expand Down Expand Up @@ -253,7 +253,7 @@ export const ProductBasicFields: React.FC<ProductBasicFieldsProps> = ({
<Label className={formLabelClassName}>PDF</Label>
<Button
variant="outline"
className="w-full justify-between h-8"
className="w-full justify-between h-8 shadow-xs"
type="button"
>
Upload a PDF
Expand Down
Loading