diff --git a/packages/api/src/__generated__/schema.ts b/packages/api/src/__generated__/schema.ts index f89d1c3b8f..7a2c212e40 100644 --- a/packages/api/src/__generated__/schema.ts +++ b/packages/api/src/__generated__/schema.ts @@ -867,6 +867,8 @@ export type Query = { redirect?: Maybe; /** Returns the result of a product, facet, or suggestion search. */ search: StoreSearchResult; + /** Returns information about the Shoppers. */ + searchShopper?: Maybe; /** Returns a list of sellers available for a specific localization. */ sellers?: Maybe; /** Returns information about shipping simulation. */ @@ -944,6 +946,12 @@ export type QuerySearchArgs = { }; +export type QuerySearchShopperArgs = { + name?: Maybe; + userId?: Maybe; +}; + + export type QuerySellersArgs = { country: Scalars['String']; geoCoordinates?: Maybe; @@ -974,6 +982,20 @@ export type SearchMetadata = { logicalOperator: Scalars['String']; }; +export type SearchShopper = { + __typename?: 'SearchShopper'; + firstName?: Maybe; + fullName?: Maybe; + lastName?: Maybe; + userId?: Maybe; +}; + +/** SearchShopperResult information. */ +export type SearchShopperResult = { + __typename?: 'SearchShopperResult'; + shoppers?: Maybe>>; +}; + /** Information of sellers. */ export type SellerInfo = { __typename?: 'SellerInfo'; @@ -2453,6 +2475,7 @@ export type UserOrderShippingData = { export type UserOrderShopperName = { __typename?: 'UserOrderShopperName'; firstName?: Maybe; + fullName?: Maybe; lastName?: Maybe; }; diff --git a/packages/api/src/platforms/vtex/clients/commerce/index.ts b/packages/api/src/platforms/vtex/clients/commerce/index.ts index 5fcf6e8559..5da7910476 100644 --- a/packages/api/src/platforms/vtex/clients/commerce/index.ts +++ b/packages/api/src/platforms/vtex/clients/commerce/index.ts @@ -8,6 +8,7 @@ import type { IProcessOrderAuthorization, IUserOrderCancel, QueryListUserOrdersArgs, + QuerySearchShopperArgs, StoreMarketingData, UserOrder, UserOrderCancel, @@ -16,9 +17,11 @@ import type { import type { Context, Options } from '../../index' import type { Channel } from '../../utils/channel' import { + getAuthCookie, getStoreCookie, getWithAutCookie, getWithCookie, + parseJwt, } from '../../utils/cookies' import type { ContractResponse } from './Contract' import type { Address, AddressInput } from './types/Address' @@ -686,24 +689,42 @@ export const VtexCommerce = ( {} ) }, - getShopperNameById: ({ + searchShopper: ({ userId, - }: { userId: string }): Promise< + name, + }: QuerySearchShopperArgs): Promise< Array<{ firstName: string lastName: string + fullName: string + userId: string }> > => { - if (!userId) { - throw new Error('Missing userId to fetch shopper name') + if (!userId && !name) { + throw new Error('You must provide userId or name to search shopper') } - const userIdNormalized = userId.replace(/-/g, '') // Normalize userId by removing hyphens - const headers: HeadersInit = withAutCookie(forwardedHost, account) + // Normalize userId by removing hyphens if present + const userIdNormalized = userId ? userId.replace(/-/g, '') : undefined + + const whereParts = [] + if (userIdNormalized) { + whereParts.push(`userId=${userIdNormalized}`) + } + if (name) { + const jwt = parseJwt(getAuthCookie(headers?.cookie ?? '', account)) + const customerId = jwt?.customerId + whereParts.push(`(fullName = *${name}*)`) + + if (customerId) whereParts.push(`(contractIds=${customerId})`) + } + + const where = whereParts.join(' AND ') + return fetchAPI( - `${base}/api/dataentities/shopper/search?_where=(userId=${userIdNormalized})&_fields=_all&_schema=v1`, + `${base}/api/dataentities/shopper/search?_where=(${encodeURIComponent(where)})&_fields=_all&_schema=v1`, { method: 'GET', headers, diff --git a/packages/api/src/platforms/vtex/resolvers/query.ts b/packages/api/src/platforms/vtex/resolvers/query.ts index f020a2878e..029ccd2565 100644 --- a/packages/api/src/platforms/vtex/resolvers/query.ts +++ b/packages/api/src/platforms/vtex/resolvers/query.ts @@ -10,6 +10,7 @@ import type { QueryProfileArgs, QueryRedirectArgs, QuerySearchArgs, + QuerySearchShopperArgs, QuerySellersArgs, QueryShippingArgs, QueryUserOrderArgs, @@ -418,7 +419,7 @@ export const Query = { } catch (err: any) {} const shopperSearch = - (await commerce.masterData.getShopperNameById({ + (await commerce.masterData.searchShopper({ userId: order.purchaseAgentData?.purchaseAgents?.[0]?.userId ?? '', })) ?? [] const shopper = shopperSearch[0] ?? {} @@ -444,6 +445,7 @@ export const Query = { shopperName: { firstName: shopper?.firstName || '', lastName: shopper?.lastName || '', + fullName: shopper?.fullName || '', }, } } catch (error) { @@ -492,6 +494,26 @@ export const Query = { paging: orders.paging, } }, + searchShopper: async ( + _: unknown, + filters: QuerySearchShopperArgs, + ctx: Context + ) => { + const { + clients: { commerce }, + } = ctx + const shopperSearch = + (await commerce.masterData.searchShopper(filters)) ?? [] + return { + shoppers: + shopperSearch?.map((shopper) => ({ + userId: shopper?.userId, + firstName: shopper?.firstName || '', + lastName: shopper?.lastName || '', + fullName: shopper?.fullName || '', + })) ?? [], + } + }, accountName: async (_: unknown, __: unknown, ctx: Context) => { const { account, diff --git a/packages/api/src/typeDefs/query.graphql b/packages/api/src/typeDefs/query.graphql index 8f9e329021..faa1ca83e0 100644 --- a/packages/api/src/typeDefs/query.graphql +++ b/packages/api/src/typeDefs/query.graphql @@ -413,6 +413,19 @@ type Query { clientEmail: String ): UserOrderListMinimalResult @cacheControl(scope: "public", sMaxAge: 120, staleWhileRevalidate: 3600) + """ + Returns information about the Shoppers. + """ + searchShopper( + """ + Shopper's userId. + """ + userId: String + """ + Shopper's name. + """ + name: String + ): SearchShopperResult """ Returns information about the current user details. diff --git a/packages/api/src/typeDefs/shopper.graphql b/packages/api/src/typeDefs/shopper.graphql new file mode 100644 index 0000000000..9a2af7d3cb --- /dev/null +++ b/packages/api/src/typeDefs/shopper.graphql @@ -0,0 +1,13 @@ +""" +SearchShopperResult information. +""" +type SearchShopperResult { + shoppers: [SearchShopper] +} + +type SearchShopper { + userId: String + firstName: String + lastName: String + fullName: String +} diff --git a/packages/api/src/typeDefs/userOrder.graphql b/packages/api/src/typeDefs/userOrder.graphql index c53f74b7e0..fbfa0371c5 100644 --- a/packages/api/src/typeDefs/userOrder.graphql +++ b/packages/api/src/typeDefs/userOrder.graphql @@ -96,6 +96,7 @@ type UserOrderResult { type UserOrderShopperName { firstName: String lastName: String + fullName: String } type UserOrderListResult { diff --git a/packages/api/test/integration/schema.test.ts b/packages/api/test/integration/schema.test.ts index 7d57091034..5575ee0b68 100644 --- a/packages/api/test/integration/schema.test.ts +++ b/packages/api/test/integration/schema.test.ts @@ -71,6 +71,7 @@ const QUERIES = [ 'productCount', 'userOrder', 'listUserOrders', + 'searchShopper', 'userDetails', 'accountProfile', 'accountName', diff --git a/packages/core/@generated/gql.ts b/packages/core/@generated/gql.ts index c55924ebf5..95f1b67334 100644 --- a/packages/core/@generated/gql.ts +++ b/packages/core/@generated/gql.ts @@ -76,6 +76,8 @@ const documents = { types.ClientProductGalleryQueryDocument, '\n query ClientProductQuery($locator: [IStoreSelectedFacet!]!) {\n ...ClientProduct\n product(locator: $locator) {\n ...ProductDetailsFragment_product\n }\n }\n': types.ClientProductQueryDocument, + '\n query ClientSearchShopperQuery($userId: String, $name: String) {\n searchShopper(userId: $userId, name: $name) {\n shoppers {\n userId\n firstName\n lastName\n fullName\n }\n }\n }\n': + types.ClientSearchShopperQueryDocument, '\n query ClientProfileQuery($id: String!) {\n profile(id: $id) {\n addresses {\n country\n postalCode\n geoCoordinate\n city\n }\n }\n }\n': types.ClientProfileQueryDocument, '\n query ClientSearchSuggestionsQuery(\n $term: String!\n $selectedFacets: [IStoreSelectedFacet!]\n ) {\n ...ClientSearchSuggestions\n search(first: 5, term: $term, selectedFacets: $selectedFacets) {\n suggestions {\n terms {\n value\n }\n products {\n ...ProductSummary_product\n }\n }\n products {\n pageInfo {\n totalCount\n }\n }\n metadata {\n ...SearchEvent_metadata\n }\n }\n }\n': @@ -282,6 +284,12 @@ export function gql( export function gql( source: '\n query ClientProductQuery($locator: [IStoreSelectedFacet!]!) {\n ...ClientProduct\n product(locator: $locator) {\n ...ProductDetailsFragment_product\n }\n }\n' ): typeof import('./graphql').ClientProductQueryDocument +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql( + source: '\n query ClientSearchShopperQuery($userId: String, $name: String) {\n searchShopper(userId: $userId, name: $name) {\n shoppers {\n userId\n firstName\n lastName\n fullName\n }\n }\n }\n' +): typeof import('./graphql').ClientSearchShopperQueryDocument /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/core/@generated/graphql.ts b/packages/core/@generated/graphql.ts index 80df0ad64e..abcb5179ce 100644 --- a/packages/core/@generated/graphql.ts +++ b/packages/core/@generated/graphql.ts @@ -847,6 +847,8 @@ export type Query = { redirect: Maybe /** Returns the result of a product, facet, or suggestion search. */ search: StoreSearchResult + /** Returns information about the Shoppers. */ + searchShopper: Maybe /** Returns a list of sellers available for a specific localization. */ sellers: Maybe /** Returns information about shipping simulation. */ @@ -913,6 +915,11 @@ export type QuerySearchArgs = { term?: InputMaybe } +export type QuerySearchShopperArgs = { + name: InputMaybe + userId: InputMaybe +} + export type QuerySellersArgs = { country: Scalars['String']['input'] geoCoordinates: InputMaybe @@ -940,6 +947,18 @@ export type SearchMetadata = { logicalOperator: Scalars['String']['output'] } +export type SearchShopper = { + firstName: Maybe + fullName: Maybe + lastName: Maybe + userId: Maybe +} + +/** SearchShopperResult information. */ +export type SearchShopperResult = { + shoppers: Maybe>> +} + /** Information of sellers. */ export type SellerInfo = { /** Identification of the seller */ @@ -2293,6 +2312,7 @@ export type UserOrderShippingData = { export type UserOrderShopperName = { firstName: Maybe + fullName: Maybe lastName: Maybe } @@ -3274,6 +3294,22 @@ export type ClientProductQueryQuery = { } } +export type ClientSearchShopperQueryQueryVariables = Exact<{ + userId: InputMaybe + name: InputMaybe +}> + +export type ClientSearchShopperQueryQuery = { + searchShopper: { + shoppers: Array<{ + userId: string | null + firstName: string | null + lastName: string | null + fullName: string | null + } | null> | null + } | null +} + export type ClientProfileQueryQueryVariables = Exact<{ id: Scalars['String']['input'] }> @@ -4135,6 +4171,15 @@ export const ClientProductQueryDocument = { ClientProductQueryQuery, ClientProductQueryQueryVariables > +export const ClientSearchShopperQueryDocument = { + __meta__: { + operationName: 'ClientSearchShopperQuery', + operationHash: '4dc1daf8ceb850279427dd59f35193ebe490b3c1', + }, +} as unknown as TypedDocumentString< + ClientSearchShopperQueryQuery, + ClientSearchShopperQueryQueryVariables +> export const ClientProfileQueryDocument = { __meta__: { operationName: 'ClientProfileQuery', diff --git a/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountFilterSlider/MyAccountFilterFacetPlacedBy/MyAccountFilterFacetPlacedBy.tsx b/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountFilterSlider/MyAccountFilterFacetPlacedBy/MyAccountFilterFacetPlacedBy.tsx index f1de7e0fab..8e778874f9 100644 --- a/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountFilterSlider/MyAccountFilterFacetPlacedBy/MyAccountFilterFacetPlacedBy.tsx +++ b/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountFilterSlider/MyAccountFilterFacetPlacedBy/MyAccountFilterFacetPlacedBy.tsx @@ -1,8 +1,8 @@ -import { useEffect, useMemo, useRef, useState } from 'react' -import { Input, IconButton, Icon, Loader } from '@faststore/ui' -import type { SelectedFacet } from 'src/sdk/search/useMyAccountFilter' -import useShopperSuggestions from 'src/sdk/account/useShopperSuggestions' +import { Icon, IconButton, Input, Loader } from '@faststore/ui' +import { useDeferredValue, useEffect, useMemo, useRef, useState } from 'react' import type { Shopper } from 'src/sdk/account/useShopperSuggestions' +import useShopperSuggestions from 'src/sdk/account/useShopperSuggestions' +import type { SelectedFacet } from 'src/sdk/search/useMyAccountFilter' export interface MyAccountFilterFacetPlacedByProps { /** @@ -23,15 +23,15 @@ function MyAccountFilterFacetPlacedBy({ const [query, setQuery] = useState('') const [selectedShopper, setSelectedShopper] = useState(null) const [isOpen, setIsOpen] = useState(false) + const searchQueryDeferred = useDeferredValue(query) - // Use the new hook for shoppers suggestions - const { data, isLoading, findShopperById } = useShopperSuggestions(query) + const { data, isLoading, findShopperById } = + useShopperSuggestions(searchQueryDeferred) - // Get the filtered shoppers from hook data const filteredShoppers = data?.shoppers || [] const selectedId = useMemo( - () => selected.find((f) => f.key === 'purchaseAgentId')?.value, + () => selected.find((f) => f.key === 'purchaseAgentIds')?.value, [selected] ) @@ -66,7 +66,7 @@ function MyAccountFilterFacetPlacedBy({ dispatch({ type: 'setFacet', payload: { - facet: { key: 'purchaseAgentId', value: shopper.purchase_agent_id }, + facet: { key: 'purchaseAgentIds', value: shopper.userId }, unique: true, }, }) @@ -74,13 +74,13 @@ function MyAccountFilterFacetPlacedBy({ function handleClearTag() { if (selectedShopper) { - // Using toggleFacet here removes the purchaseAgentId from selected facets + // Using toggleFacet here removes the purchaseAgentIds from selected facets // because toggleFacet will remove the facet if it already exists in the selected facets dispatch({ type: 'toggleFacet', payload: { - key: 'purchaseAgentId', - value: selectedShopper.purchase_agent_id, + key: 'purchaseAgentIds', + value: selectedShopper.userId, }, }) } @@ -94,7 +94,11 @@ function MyAccountFilterFacetPlacedBy({ id="placed-by-input" placeholder="Enter the shopper's name..." ref={inputRef} - value={selectedShopper ? selectedShopper.name : query} + value={ + selectedShopper + ? `${selectedShopper.firstName} ${selectedShopper.lastName}` + : query + } readOnly={Boolean(selectedShopper)} onFocus={() => { if (!selectedShopper) setIsOpen(true) @@ -149,7 +153,7 @@ function MyAccountFilterFacetPlacedBy({ >
    {filteredShoppers.map((s) => ( -
  • +
  • diff --git a/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountFilterSlider/MyAccountFilterSlider.tsx b/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountFilterSlider/MyAccountFilterSlider.tsx index 17482e0697..9e34361f5b 100644 --- a/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountFilterSlider/MyAccountFilterSlider.tsx +++ b/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountFilterSlider/MyAccountFilterSlider.tsx @@ -92,8 +92,8 @@ function MyAccountFilterSlider({ : [value] } - if (key === 'purchaseAgentId') { - acc['purchaseAgentId'] = value + if (key === 'purchaseAgentIds') { + acc['purchaseAgentIds'] = value } return acc diff --git a/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountListOrders.tsx b/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountListOrders.tsx index 4e338e8e47..ce738fc2bc 100644 --- a/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountListOrders.tsx +++ b/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountListOrders.tsx @@ -6,9 +6,9 @@ import { useRouter } from 'next/router' import { Button, EmptyState, - Icon as UIIcon, LinkButton, SearchInputField, + Icon as UIIcon, useUI, type SearchInputFieldRef, } from '@faststore/ui' @@ -39,7 +39,7 @@ export type MyAccountListOrdersProps = { dateFinal: string text: string clientEmail: string - purchaseAgentId?: string + purchaseAgentIds?: string } } @@ -74,9 +74,9 @@ function getSelectedFacets({ key: 'dateFinal', value: String(value), }) - } else if (filter === 'purchaseAgentId' && value) { + } else if (filter === 'purchaseAgentIds' && value) { acc.push({ - key: 'purchaseAgentId', + key: 'purchaseAgentIds', value: String(value), }) } @@ -104,7 +104,7 @@ function getAllFacets({ }, { __typename: 'StoreFacetPlacedBy', - key: 'purchaseAgentId', + key: 'purchaseAgentIds', label: 'Placed by', } as any, { @@ -244,7 +244,7 @@ export default function MyAccountListOrders({ status: filters.status, dateInitial: filters.dateInitial, dateFinal: filters.dateFinal, - purchaseAgentId: filters.purchaseAgentId, + purchaseAgentIds: filters.purchaseAgentIds, }} onClearAll={() => { window.location.href = '/account/orders' @@ -259,8 +259,8 @@ export default function MyAccountListOrders({ } else if (key === 'dateInitial' || key === 'dateFinal') { delete updatedFilters.dateInitial delete updatedFilters.dateFinal - } else if (key === 'purchaseAgentId') { - delete updatedFilters.purchaseAgentId + } else if (key === 'purchaseAgentIds') { + delete updatedFilters.purchaseAgentIds } else { delete updatedFilters[key] } @@ -272,8 +272,8 @@ export default function MyAccountListOrders({ } else if (key === 'dateInitial' || key === 'dateFinal') { delete updatedFilters.dateInitial delete updatedFilters.dateFinal - } else if (key === 'purchaseAgentId') { - delete updatedFilters.purchaseAgentId + } else if (key === 'purchaseAgentIds') { + delete updatedFilters.purchaseAgentIds } else { delete updatedFilters[key] } diff --git a/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountSelectedTags/MyAccountSelectedTags.tsx b/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountSelectedTags/MyAccountSelectedTags.tsx index c79d8b4e28..12696f9cbc 100644 --- a/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountSelectedTags/MyAccountSelectedTags.tsx +++ b/packages/core/src/components/account/orders/MyAccountListOrders/MyAccountSelectedTags/MyAccountSelectedTags.tsx @@ -7,11 +7,11 @@ type MyAccountSelectedTagsProps = { status?: string[] dateInitial?: string dateFinal?: string - purchaseAgentId?: string + purchaseAgentIds?: string } onClearAll: () => void onRemoveFilter: ( - key: 'status' | 'dateInitial' | 'dateFinal' | 'purchaseAgentId', + key: 'status' | 'dateInitial' | 'dateFinal' | 'purchaseAgentIds', value: string ) => void } @@ -41,7 +41,7 @@ function Tags({ onRemoveFilter, }: Pick) { const { locale } = useSession() - const { dateInitial, dateFinal, status, purchaseAgentId } = filters + const { dateInitial, dateFinal, status, purchaseAgentIds } = filters const formattedDateInitial = dateInitial ? formatFilterDate(dateInitial, locale) : '' @@ -86,12 +86,12 @@ function Tags({ )) - const placedByTag = purchaseAgentId && ( -
    - Placed by: {purchaseAgentId} + const placedByTag = purchaseAgentIds && ( +
    + Placed by: {purchaseAgentIds} @@ -117,7 +117,7 @@ function MyAccountSelectedTags({ (key === 'status' || key === 'dateInitial' || key === 'dateFinal' || - key === 'purchaseAgentId') && + key === 'purchaseAgentIds') && values && (Array.isArray(values) ? values.length > 0 : true) ) diff --git a/packages/core/src/pages/account/orders/index.tsx b/packages/core/src/pages/account/orders/index.tsx index d15665e620..2ff5a3bc09 100644 --- a/packages/core/src/pages/account/orders/index.tsx +++ b/packages/core/src/pages/account/orders/index.tsx @@ -46,7 +46,7 @@ type ListOrdersPageProps = { dateFinal: string text: string clientEmail: string - purchaseAgentId?: string + purchaseAgentIds?: string } } & MyAccountProps @@ -172,10 +172,10 @@ export const getServerSideProps: GetServerSideProps< const dateFinal = (context.query.dateFinal as string | undefined) || '' const text = (context.query.text as string | undefined) || '' const clientEmail = (context.query.clientEmail as string | undefined) || '' - // TODO: Integration: ensure `purchaseAgentId` is mapped to `purchase_agent_id` + // TODO: Integration: ensure `purchaseAgentIds` is mapped to `purchase_agent_id` // when calling the OMS API. Keep camelCase across the frontend. - const purchaseAgentId = - (context.query.purchaseAgentId as string | undefined) || '' + const purchaseAgentIds = + (context.query.purchaseAgentIds as string | undefined) || '' // Map labels from FastStore status to API status const groupedStatus = groupOrderStatusByLabel() @@ -251,7 +251,7 @@ export const getServerSideProps: GetServerSideProps< dateFinal, text, clientEmail, - purchaseAgentId, + purchaseAgentIds, }, isRepresentative, }, diff --git a/packages/core/src/sdk/account/useShopperSuggestions.ts b/packages/core/src/sdk/account/useShopperSuggestions.ts index 2caa67df41..901b791d4d 100644 --- a/packages/core/src/sdk/account/useShopperSuggestions.ts +++ b/packages/core/src/sdk/account/useShopperSuggestions.ts @@ -1,36 +1,12 @@ -import { useMemo, useState, useCallback, useEffect } from 'react' +import { useMemo } from 'react' +import { useSearchShopperQuery } from '../product/useSearchShopperQuery' -// This will be replaced with an imported type from a GraphQL schema in the future export type Shopper = { - purchase_agent_id: string - name: string - email: string + userId: string + firstName: string + lastName: string } -// Mock data for now, will be fetched from API in the future -const MOCK_SHOPPERS: Shopper[] = [ - { - purchase_agent_id: '1', - name: 'Robert Fox', - email: 'robert.fox@example.com', - }, - { - purchase_agent_id: '2', - name: 'Ronald Wilson', - email: 'ronald.wilson@example.com', - }, - { - purchase_agent_id: '3', - name: 'Cameron Williamson', - email: 'cameron.williamson@example.com', - }, - { - purchase_agent_id: '4', - name: 'Brooklyn Simmons', - email: 'brooklyn.simmons@example.com', - }, -] - interface ShopperSuggestionsData { /** * Array of shoppers that match the search term @@ -67,82 +43,34 @@ interface ShopperSuggestionsResult { export function useShopperSuggestions( searchTerm = '' ): ShopperSuggestionsResult { - const [data, setData] = useState({ - shoppers: MOCK_SHOPPERS, - }) - const [error, setError] = useState(null) - const [isLoading, setIsLoading] = useState(false) - - // Function to search for shoppers - const searchShoppers = useCallback( - async (term: string) => { - // Don't search if term is empty or null - if (!term?.trim()) { - setData({ shoppers: MOCK_SHOPPERS }) - setIsLoading(false) - return - } - - setIsLoading(true) - setError(null) - - try { - // Simulate API call with timeout - const results = await new Promise((resolve) => { - setTimeout(() => { - // Filter logic to simulate server-side filtering - const q = term.trim().toLowerCase() - const filtered = MOCK_SHOPPERS.filter( - (shopper) => - shopper.name.toLowerCase().includes(q) || - shopper.email.toLowerCase().includes(q) - ) - resolve(filtered) - }, 300) // Simulate network delay - }) - - setData({ shoppers: results }) - } catch (err) { - setError( - err instanceof Error - ? err - : new Error('Failed to search for shoppers') - ) - setData({ shoppers: [] }) - } finally { - setIsLoading(false) - } - }, - [] // No dependencies needed for this mock implementation - ) - - // Setup debouncing for the search term - useEffect(() => { - // Don't run search if term is empty and we already have null data - if (!searchTerm && data === null) return + const { + data: queryData, + error, + isLoading, + } = useSearchShopperQuery({ name: searchTerm }) - const handler = setTimeout(() => { - searchShoppers(searchTerm) - }, 300) + const data = useMemo(() => { + if (!queryData?.searchShopper?.shoppers) return { shoppers: [] } - return () => { - clearTimeout(handler) + return { + shoppers: queryData.searchShopper?.shoppers?.map((shopper) => ({ + userId: shopper.userId, + firstName: shopper.firstName, + lastName: shopper.lastName, + fullName: shopper.fullName, + })), } - }, [searchTerm]) + }, [queryData]) - // Helper function to find a shopper by ID - // We use useMemo instead of useCallback to ensure this function has a stable reference - // and doesn't cause infinite loops in dependencies of other hooks const findShopperById = useMemo(() => { - // Return a stable function that won't change between renders return (id: string): Shopper | undefined => { - return data?.shoppers.find((s) => s.purchase_agent_id === id) + return data?.shoppers.find((s: Shopper) => s.userId === id) } - }, []) + }, [data]) return { data, - error, + error: error ?? null, isLoading, findShopperById, } diff --git a/packages/core/src/sdk/product/useSearchShopperQuery.ts b/packages/core/src/sdk/product/useSearchShopperQuery.ts new file mode 100644 index 0000000000..4e6143dfe9 --- /dev/null +++ b/packages/core/src/sdk/product/useSearchShopperQuery.ts @@ -0,0 +1,43 @@ +import { useMemo } from 'react' + +import { gql } from '@generated' + +import type { + ClientSearchShopperQueryQuery, + ClientSearchShopperQueryQueryVariables, +} from '@generated/graphql' +import { useQuery } from '../graphql/useQuery' + +const query = gql(` + query ClientSearchShopperQuery($userId: String, $name: String) { + searchShopper(userId: $userId, name: $name) { + shoppers { + userId + firstName + lastName + fullName + } + } + } +`) + +export const useSearchShopperQuery = ({ + name, +}: { + userId?: string + name?: string +}) => { + const variables = useMemo(() => { + return { + name, + userId: undefined, + } + }, [name]) + + return useQuery< + ClientSearchShopperQueryQuery & T, + ClientSearchShopperQueryQueryVariables + >(query, variables, { + doNotRun: !name, + }) +} diff --git a/packages/core/test/server/index.test.ts b/packages/core/test/server/index.test.ts index 736d73f005..e78292a4ba 100644 --- a/packages/core/test/server/index.test.ts +++ b/packages/core/test/server/index.test.ts @@ -76,6 +76,7 @@ const QUERIES = [ 'productCount', 'userOrder', 'listUserOrders', + 'searchShopper', 'userDetails', 'accountProfile', 'accountName',