Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
81 changes: 69 additions & 12 deletions frontend/core-ui/src/modules/products/components/ProductColumns.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { useProductsEdit } from '@/products/hooks/useProductsEdit';
import { renderingProductDetailAtom } from '@/products/states/productDetailStates';
import { ProductHotKeyScope } from '@/products/types/ProductsHotKeyScope';
import {
IconCategory,
IconCurrencyDollar,
Expand All @@ -12,21 +15,64 @@ 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 { useState } from 'react';
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 [value, setValue] = useState(name);
Comment on lines +35 to +36
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Sync local state with prop changes

The local state should update when the product name changes externally.

Add a useEffect to sync the state:

       const name = cell.getValue() as string;
       const [value, setValue] = useState(name);
+      useEffect(() => {
+        setValue(name);
+      }, [name]);

Don't forget to import useEffect:

-import { useState } from 'react';
+import { useState, useEffect } from 'react';
📝 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 name = cell.getValue() as string;
const [value, setValue] = useState(name);
// At the top of the file, update the import:
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
// ...
// Inside your component, after initializing state:
const name = cell.getValue() as string;
const [value, setValue] = useState(name);
+useEffect(() => {
+ setValue(name);
+}, [name]);
// ...
🤖 Prompt for AI Agents
In frontend/core-ui/src/modules/products/components/ProductColumns.tsx around
lines 35 to 36, the local state variable 'value' initialized from the prop
'name' does not update when 'name' changes externally. To fix this, import
useEffect from React and add a useEffect hook that listens for changes to 'name'
and updates the local state 'value' accordingly, ensuring the component stays in
sync with prop changes.

const [, setProductId] = useQueryState('productId');
const setRenderingProductDetail = useSetAtom(renderingProductDetailAtom);
const { productsEdit } = useProductsEdit();
return (
<RecordTableCellDisplay>
<TextOverflowTooltip value={cell.getValue() as string} />
</RecordTableCellDisplay>
<RecordTablePopover
scope={`${ProductHotKeyScope.ProductsPage}-name-${cell.row.original._id}`}
onOpenChange={(open) => {
if (!open) {
productsEdit({
variables: {
_id: cell.row.original._id,
name: value,
},
});
}
}}
closeOnEnter
>
<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={value || ''}
onChange={(e) => {
setValue(e.target.value);
}}
/>
</RecordTableCellContent>
</RecordTablePopover>
);
},
},
Expand All @@ -53,7 +99,7 @@ export const productColumns: ColumnDef<IProduct>[] = [
<RecordTableCellDisplay>
<CurrencyFormatedDisplay
currencyValue={{
amountMicros: (cell.getValue() as number),
amountMicros: cell.getValue() as number,
currencyCode: CurrencyCode.MNT,
}}
/>
Expand All @@ -80,12 +126,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,84 @@
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;
brandId: string;
categoryId: string;
}>(['searchValue', 'created', 'updated', 'lastSeen', 'brandId', 'categoryId']);
const { searchValue } = queries || {};

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;
brandId: string;
categoryId: string;
}>(['searchValue', 'created', 'updated', 'lastSeen', 'brandId', 'categoryId']);

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
Loading