feat: implement product filter in orders search

This commit is contained in:
JuruSysadmin 2026-01-15 16:34:23 -03:00
parent 275cbfd29c
commit 58463fe548
8 changed files with 188 additions and 6 deletions

View File

@ -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<{id: number, description: string}>>} Array de produtos correspondentes
*/
findProducts: async (
term: string
): Promise<Array<{ id: number; description: string }>> => {
if (!term || term.trim().length < 2) return [];
const response = await ordersApi.get(
`/api/v1/data-consult/products/${encodeURIComponent(term)}`
);
return unwrapApiData(response, productsResponseSchema, []);
},
};

View File

@ -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
/>
</Grid>
{/* Autocomplete do MUI para Produto */}
<Grid size={{ xs: 12, sm: 12, md: 6 }}>
<Autocomplete
size="small"
options={products.options}
getOptionLabel={(option) => 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) => (
<TextField
{...params}
label="Produto"
placeholder="Busque por nome ou código..."
/>
)}
noOptionsText={
productSearchTerm.length < 2
? 'Digite pelo menos 2 caracteres'
: 'Nenhum produto encontrado'
}
loadingText="Buscando produtos..."
filterOptions={(x) => x}
clearOnBlur={false}
selectOnFocus
handleHomeEndKeys
/>
</Grid>
</Grid>
</Collapse>
</Grid>

View File

@ -24,6 +24,7 @@ export const useOrderFilters = () => {
store: parseAsArrayOf(parseAsString, ','),
orderId: parseAsInteger,
productId: parseAsInteger,
productName: parseAsString,
stockId: parseAsArrayOf(parseAsString, ','),
hasPreBox: parseAsBoolean.withDefault(false),

View File

@ -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,
};
}

View File

@ -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));

View File

@ -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<string, string> = {
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;

View File

@ -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';

View File

@ -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<typeof productSchema>;