From 58463fe548e7f797c46cb0066968dc7547688ae1 Mon Sep 17 00:00:00 2001 From: JuruSysadmin Date: Thu, 15 Jan 2026 16:34:23 -0300 Subject: [PATCH] feat: implement product filter in orders search --- src/features/orders/api/order.service.ts | 20 +++++ src/features/orders/components/SearchBar.tsx | 76 ++++++++++++++++++- src/features/orders/hooks/useOrderFilters.ts | 1 + src/features/orders/hooks/useProducts.ts | 47 ++++++++++++ .../orders/schemas/api-responses.schema.ts | 7 ++ .../orders/schemas/order-filters.schema.ts | 24 ++++-- src/features/orders/schemas/order.schema.ts | 5 ++ src/features/orders/schemas/product.schema.ts | 14 ++++ 8 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 src/features/orders/hooks/useProducts.ts create mode 100644 src/features/orders/schemas/product.schema.ts diff --git a/src/features/orders/api/order.service.ts b/src/features/orders/api/order.service.ts index 7149289..7b1bf58 100644 --- a/src/features/orders/api/order.service.ts +++ b/src/features/orders/api/order.service.ts @@ -16,6 +16,7 @@ import { customersResponseSchema, sellersResponseSchema, partnersResponseSchema, + productsResponseSchema, unwrapApiData, } from '../schemas/order.schema'; import { @@ -197,5 +198,24 @@ export const orderService = { return unwrapApiData(response, partnersResponseSchema, []); }, + /** + * Busca produtos por código ou descrição. + * Retorna array vazio se o termo de busca tiver menos de 2 caracteres. + * + * @param {string} term - O termo de busca (mínimo 2 caracteres) + * @returns {Promise>} Array de produtos correspondentes + */ + findProducts: async ( + term: string + ): Promise> => { + if (!term || term.trim().length < 2) return []; + + const response = await ordersApi.get( + `/api/v1/data-consult/products/${encodeURIComponent(term)}` + ); + return unwrapApiData(response, productsResponseSchema, []); + }, + + }; diff --git a/src/features/orders/components/SearchBar.tsx b/src/features/orders/components/SearchBar.tsx index 130cb6f..940b2f3 100644 --- a/src/features/orders/components/SearchBar.tsx +++ b/src/features/orders/components/SearchBar.tsx @@ -21,6 +21,7 @@ import { useStores } from '../store/useStores'; import { useCustomers } from '../hooks/useCustomers'; import { useSellers } from '../hooks/useSellers'; import { usePartners } from '../hooks/usePartners'; +import { useProducts } from '../hooks/useProducts'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; @@ -42,6 +43,8 @@ interface LocalFilters { customerName: string | null; partnerId: string | null; partnerName: string | null; + productId: number | null; + productName: string | null; createDateIni: string | null; createDateEnd: string | null; store: string[] | null; @@ -59,6 +62,8 @@ const getInitialLocalFilters = ( customerName: urlFilters.customerName ?? null, partnerId: urlFilters.partnerId ?? null, partnerName: urlFilters.partnerName ?? null, + productId: urlFilters.productId ?? null, + productName: urlFilters.productName ?? null, createDateIni: urlFilters.createDateIni ?? null, createDateEnd: urlFilters.createDateEnd ?? null, store: urlFilters.store ?? null, @@ -80,6 +85,8 @@ export const SearchBar = () => { const customers = useCustomers(customerSearchTerm); const [partnerSearchTerm, setPartnerSearchTerm] = useState(''); const partners = usePartners(partnerSearchTerm); + const [productSearchTerm, setProductSearchTerm] = useState(''); + const products = useProducts(productSearchTerm); const [showAdvancedFilters, setShowAdvancedFilters] = useState(false); const { isFetching } = useOrders(); const [touchedFields, setTouchedFields] = useState<{ @@ -102,6 +109,7 @@ export const SearchBar = () => { setTouchedFields({}); setCustomerSearchTerm(''); setPartnerSearchTerm(''); + setProductSearchTerm(''); const resetState = { status: null, @@ -111,7 +119,6 @@ export const SearchBar = () => { codusur2: null, store: null, orderId: null, - productId: null, stockId: null, hasPreBox: false, includeCheckout: false, @@ -122,6 +129,8 @@ export const SearchBar = () => { customerName: null, partnerId: null, partnerName: null, + productId: null, + productName: null, }; setLocalFilters(getInitialLocalFilters(resetState)); @@ -183,6 +192,7 @@ export const SearchBar = () => { localFilters.stockId?.length, localFilters.sellerId, localFilters.partnerId, + localFilters.productId, ].filter(Boolean).length; return ( @@ -681,6 +691,70 @@ export const SearchBar = () => { handleHomeEndKeys /> + + {/* Autocomplete do MUI para Produto */} + + option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={ + products.options.find( + (option) => localFilters.productId === option.product?.id + ) || null + } + onChange={(_, newValue) => { + if (!newValue) { + updateLocalFilter('productName', null); + updateLocalFilter('productId', null); + setProductSearchTerm(''); + return; + } + + updateLocalFilter('productId', newValue.product?.id || null); + updateLocalFilter( + 'productName', + newValue.product?.description || null + ); + }} + onInputChange={(_, newInputValue, reason) => { + if (reason === 'clear') { + updateLocalFilter('productName', null); + updateLocalFilter('productId', null); + setProductSearchTerm(''); + return; + } + + if (reason === 'input') { + setProductSearchTerm(newInputValue); + if (!newInputValue) { + updateLocalFilter('productName', null); + updateLocalFilter('productId', null); + setProductSearchTerm(''); + } + } + }} + loading={products.isLoading} + renderInput={(params: AutocompleteRenderInputParams) => ( + + )} + noOptionsText={ + productSearchTerm.length < 2 + ? 'Digite pelo menos 2 caracteres' + : 'Nenhum produto encontrado' + } + loadingText="Buscando produtos..." + filterOptions={(x) => x} + clearOnBlur={false} + selectOnFocus + handleHomeEndKeys + /> + diff --git a/src/features/orders/hooks/useOrderFilters.ts b/src/features/orders/hooks/useOrderFilters.ts index ea72fa9..92bee7a 100644 --- a/src/features/orders/hooks/useOrderFilters.ts +++ b/src/features/orders/hooks/useOrderFilters.ts @@ -24,6 +24,7 @@ export const useOrderFilters = () => { store: parseAsArrayOf(parseAsString, ','), orderId: parseAsInteger, productId: parseAsInteger, + productName: parseAsString, stockId: parseAsArrayOf(parseAsString, ','), hasPreBox: parseAsBoolean.withDefault(false), diff --git a/src/features/orders/hooks/useProducts.ts b/src/features/orders/hooks/useProducts.ts new file mode 100644 index 0000000..4c57153 --- /dev/null +++ b/src/features/orders/hooks/useProducts.ts @@ -0,0 +1,47 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { orderService } from '../api/order.service'; + +export interface Product { + id: number; + description: string; +} + +export function useProducts(searchTerm: string) { + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm); + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchTerm(searchTerm); + }, 300); + + return () => clearTimeout(timer); + }, [searchTerm]); + + const isEnabled = debouncedSearchTerm.length >= 2; + + const query = useQuery({ + queryKey: ['products', debouncedSearchTerm], + queryFn: () => orderService.findProducts(debouncedSearchTerm), + enabled: isEnabled, + staleTime: 1000 * 60 * 5, + retry: 1, + retryOnMount: false, + refetchOnWindowFocus: false, + }); + + const options = + query.data?.map((product, index) => ({ + value: product.id.toString(), + label: product.description, + id: `product-${product.id}-${index}`, + product: product, + })) ?? []; + + return { + ...query, + options, + }; +} diff --git a/src/features/orders/schemas/api-responses.schema.ts b/src/features/orders/schemas/api-responses.schema.ts index 1fd38f1..a6144ee 100644 --- a/src/features/orders/schemas/api-responses.schema.ts +++ b/src/features/orders/schemas/api-responses.schema.ts @@ -3,6 +3,7 @@ import { storeSchema } from './store.schema'; import { customerSchema } from './customer.schema'; import { sellerSchema } from './seller.schema'; import { partnerSchema } from './partner.schema'; +import { productSchema } from './product.schema'; import { createApiSchema } from './api-response.schema'; /** @@ -46,3 +47,9 @@ export const sellersResponseSchema = createApiSchema(z.array(sellerSchema)); * Formato: { success: boolean, data: Partner[] } */ export const partnersResponseSchema = createApiSchema(z.array(partnerSchema)); + +/** + * Schema para validar a resposta da API ao buscar produtos. + * Formato: { success: boolean, data: Product[] } + */ +export const productsResponseSchema = createApiSchema(z.array(productSchema)); diff --git a/src/features/orders/schemas/order-filters.schema.ts b/src/features/orders/schemas/order-filters.schema.ts index f8d4580..f400fc5 100644 --- a/src/features/orders/schemas/order-filters.schema.ts +++ b/src/features/orders/schemas/order-filters.schema.ts @@ -16,6 +16,7 @@ export const findOrdersSchema = z.object({ minute: z.coerce.number().optional(), partnerId: z.string().optional(), + partnerName: z.string().optional(), codusur2: z.string().optional(), customerName: z.string().optional(), stockId: z.union([z.string(), z.array(z.string())]).optional(), @@ -27,6 +28,7 @@ export const findOrdersSchema = z.object({ orderId: z.coerce.number().optional(), invoiceId: z.coerce.number().optional(), productId: z.coerce.number().optional(), + productName: z.string().optional(), createDateIni: z.string().optional(), createDateEnd: z.string().optional(), @@ -98,19 +100,31 @@ const formatValueToString = (val: any): string => { */ export const orderApiParamsSchema = findOrdersSchema .transform((filters) => { - // Remove customerName quando customerId existe (evita redundância) - const { customerName, customerId, ...rest } = filters; - return customerId ? { customerId, ...rest } : filters; + const { + productName, + partnerName, + customerName, + customerId, + ...rest + } = filters; + + const queryParams: any = { ...rest }; + + if (customerId) queryParams.customerId = customerId; + else if (customerName) queryParams.customerName = customerName; + + if (filters.partnerId) queryParams.partnerId = filters.partnerId; + if (filters.productId) queryParams.productId = filters.productId; + + return queryParams; }) .transform((filters) => { - // Mapeamento de chaves que precisam ser renomeadas const keyMap: Record = { store: 'codfilial', }; return Object.entries(filters).reduce( (acc, [key, value]) => { - // Early return: ignora valores vazios if (isEmptyValue(value)) return acc; const apiKey = keyMap[key] ?? key; diff --git a/src/features/orders/schemas/order.schema.ts b/src/features/orders/schemas/order.schema.ts index 31cad74..27a16d7 100644 --- a/src/features/orders/schemas/order.schema.ts +++ b/src/features/orders/schemas/order.schema.ts @@ -31,6 +31,11 @@ export { partnerSchema } from './partner.schema'; export type { Partner } from './partner.schema'; export { partnersResponseSchema } from './api-responses.schema'; +// Schema de produtos +export { productSchema } from './product.schema'; +export type { Product } from './product.schema'; +export { productsResponseSchema } from './api-responses.schema'; + // Schema de itens de pedido export { orderItemSchema, orderItemsResponseSchema } from './order.item.schema'; export type { OrderItem, OrderItemsResponse } from './order.item.schema'; diff --git a/src/features/orders/schemas/product.schema.ts b/src/features/orders/schemas/product.schema.ts new file mode 100644 index 0000000..3f86a11 --- /dev/null +++ b/src/features/orders/schemas/product.schema.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +/** + * Schema Zod para validar um produto na busca. + */ +export const productSchema = z.object({ + id: z.number(), + description: z.string(), +}); + +/** + * Type para produto inferido do schema. + */ +export type Product = z.infer;