feat: implement product filter in orders search
This commit is contained in:
parent
275cbfd29c
commit
58463fe548
|
|
@ -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, []);
|
||||
},
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const useOrderFilters = () => {
|
|||
store: parseAsArrayOf(parseAsString, ','),
|
||||
orderId: parseAsInteger,
|
||||
productId: parseAsInteger,
|
||||
productName: parseAsString,
|
||||
stockId: parseAsArrayOf(parseAsString, ','),
|
||||
|
||||
hasPreBox: parseAsBoolean.withDefault(false),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
Loading…
Reference in New Issue