feat: implement product filter in orders search
This commit is contained in:
parent
275cbfd29c
commit
58463fe548
|
|
@ -16,6 +16,7 @@ import {
|
||||||
customersResponseSchema,
|
customersResponseSchema,
|
||||||
sellersResponseSchema,
|
sellersResponseSchema,
|
||||||
partnersResponseSchema,
|
partnersResponseSchema,
|
||||||
|
productsResponseSchema,
|
||||||
unwrapApiData,
|
unwrapApiData,
|
||||||
} from '../schemas/order.schema';
|
} from '../schemas/order.schema';
|
||||||
import {
|
import {
|
||||||
|
|
@ -197,5 +198,24 @@ export const orderService = {
|
||||||
return unwrapApiData(response, partnersResponseSchema, []);
|
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 { useCustomers } from '../hooks/useCustomers';
|
||||||
import { useSellers } from '../hooks/useSellers';
|
import { useSellers } from '../hooks/useSellers';
|
||||||
import { usePartners } from '../hooks/usePartners';
|
import { usePartners } from '../hooks/usePartners';
|
||||||
|
import { useProducts } from '../hooks/useProducts';
|
||||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment';
|
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment';
|
||||||
|
|
@ -42,6 +43,8 @@ interface LocalFilters {
|
||||||
customerName: string | null;
|
customerName: string | null;
|
||||||
partnerId: string | null;
|
partnerId: string | null;
|
||||||
partnerName: string | null;
|
partnerName: string | null;
|
||||||
|
productId: number | null;
|
||||||
|
productName: string | null;
|
||||||
createDateIni: string | null;
|
createDateIni: string | null;
|
||||||
createDateEnd: string | null;
|
createDateEnd: string | null;
|
||||||
store: string[] | null;
|
store: string[] | null;
|
||||||
|
|
@ -59,6 +62,8 @@ const getInitialLocalFilters = (
|
||||||
customerName: urlFilters.customerName ?? null,
|
customerName: urlFilters.customerName ?? null,
|
||||||
partnerId: urlFilters.partnerId ?? null,
|
partnerId: urlFilters.partnerId ?? null,
|
||||||
partnerName: urlFilters.partnerName ?? null,
|
partnerName: urlFilters.partnerName ?? null,
|
||||||
|
productId: urlFilters.productId ?? null,
|
||||||
|
productName: urlFilters.productName ?? null,
|
||||||
createDateIni: urlFilters.createDateIni ?? null,
|
createDateIni: urlFilters.createDateIni ?? null,
|
||||||
createDateEnd: urlFilters.createDateEnd ?? null,
|
createDateEnd: urlFilters.createDateEnd ?? null,
|
||||||
store: urlFilters.store ?? null,
|
store: urlFilters.store ?? null,
|
||||||
|
|
@ -80,6 +85,8 @@ export const SearchBar = () => {
|
||||||
const customers = useCustomers(customerSearchTerm);
|
const customers = useCustomers(customerSearchTerm);
|
||||||
const [partnerSearchTerm, setPartnerSearchTerm] = useState('');
|
const [partnerSearchTerm, setPartnerSearchTerm] = useState('');
|
||||||
const partners = usePartners(partnerSearchTerm);
|
const partners = usePartners(partnerSearchTerm);
|
||||||
|
const [productSearchTerm, setProductSearchTerm] = useState('');
|
||||||
|
const products = useProducts(productSearchTerm);
|
||||||
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
|
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
|
||||||
const { isFetching } = useOrders();
|
const { isFetching } = useOrders();
|
||||||
const [touchedFields, setTouchedFields] = useState<{
|
const [touchedFields, setTouchedFields] = useState<{
|
||||||
|
|
@ -102,6 +109,7 @@ export const SearchBar = () => {
|
||||||
setTouchedFields({});
|
setTouchedFields({});
|
||||||
setCustomerSearchTerm('');
|
setCustomerSearchTerm('');
|
||||||
setPartnerSearchTerm('');
|
setPartnerSearchTerm('');
|
||||||
|
setProductSearchTerm('');
|
||||||
|
|
||||||
const resetState = {
|
const resetState = {
|
||||||
status: null,
|
status: null,
|
||||||
|
|
@ -111,7 +119,6 @@ export const SearchBar = () => {
|
||||||
codusur2: null,
|
codusur2: null,
|
||||||
store: null,
|
store: null,
|
||||||
orderId: null,
|
orderId: null,
|
||||||
productId: null,
|
|
||||||
stockId: null,
|
stockId: null,
|
||||||
hasPreBox: false,
|
hasPreBox: false,
|
||||||
includeCheckout: false,
|
includeCheckout: false,
|
||||||
|
|
@ -122,6 +129,8 @@ export const SearchBar = () => {
|
||||||
customerName: null,
|
customerName: null,
|
||||||
partnerId: null,
|
partnerId: null,
|
||||||
partnerName: null,
|
partnerName: null,
|
||||||
|
productId: null,
|
||||||
|
productName: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
setLocalFilters(getInitialLocalFilters(resetState));
|
setLocalFilters(getInitialLocalFilters(resetState));
|
||||||
|
|
@ -183,6 +192,7 @@ export const SearchBar = () => {
|
||||||
localFilters.stockId?.length,
|
localFilters.stockId?.length,
|
||||||
localFilters.sellerId,
|
localFilters.sellerId,
|
||||||
localFilters.partnerId,
|
localFilters.partnerId,
|
||||||
|
localFilters.productId,
|
||||||
].filter(Boolean).length;
|
].filter(Boolean).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -681,6 +691,70 @@ export const SearchBar = () => {
|
||||||
handleHomeEndKeys
|
handleHomeEndKeys
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</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>
|
</Grid>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export const useOrderFilters = () => {
|
||||||
store: parseAsArrayOf(parseAsString, ','),
|
store: parseAsArrayOf(parseAsString, ','),
|
||||||
orderId: parseAsInteger,
|
orderId: parseAsInteger,
|
||||||
productId: parseAsInteger,
|
productId: parseAsInteger,
|
||||||
|
productName: parseAsString,
|
||||||
stockId: parseAsArrayOf(parseAsString, ','),
|
stockId: parseAsArrayOf(parseAsString, ','),
|
||||||
|
|
||||||
hasPreBox: parseAsBoolean.withDefault(false),
|
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 { customerSchema } from './customer.schema';
|
||||||
import { sellerSchema } from './seller.schema';
|
import { sellerSchema } from './seller.schema';
|
||||||
import { partnerSchema } from './partner.schema';
|
import { partnerSchema } from './partner.schema';
|
||||||
|
import { productSchema } from './product.schema';
|
||||||
import { createApiSchema } from './api-response.schema';
|
import { createApiSchema } from './api-response.schema';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,3 +47,9 @@ export const sellersResponseSchema = createApiSchema(z.array(sellerSchema));
|
||||||
* Formato: { success: boolean, data: Partner[] }
|
* Formato: { success: boolean, data: Partner[] }
|
||||||
*/
|
*/
|
||||||
export const partnersResponseSchema = createApiSchema(z.array(partnerSchema));
|
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(),
|
minute: z.coerce.number().optional(),
|
||||||
|
|
||||||
partnerId: z.string().optional(),
|
partnerId: z.string().optional(),
|
||||||
|
partnerName: z.string().optional(),
|
||||||
codusur2: z.string().optional(),
|
codusur2: z.string().optional(),
|
||||||
customerName: z.string().optional(),
|
customerName: z.string().optional(),
|
||||||
stockId: z.union([z.string(), z.array(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(),
|
orderId: z.coerce.number().optional(),
|
||||||
invoiceId: z.coerce.number().optional(),
|
invoiceId: z.coerce.number().optional(),
|
||||||
productId: z.coerce.number().optional(),
|
productId: z.coerce.number().optional(),
|
||||||
|
productName: z.string().optional(),
|
||||||
|
|
||||||
createDateIni: z.string().optional(),
|
createDateIni: z.string().optional(),
|
||||||
createDateEnd: z.string().optional(),
|
createDateEnd: z.string().optional(),
|
||||||
|
|
@ -98,19 +100,31 @@ const formatValueToString = (val: any): string => {
|
||||||
*/
|
*/
|
||||||
export const orderApiParamsSchema = findOrdersSchema
|
export const orderApiParamsSchema = findOrdersSchema
|
||||||
.transform((filters) => {
|
.transform((filters) => {
|
||||||
// Remove customerName quando customerId existe (evita redundância)
|
const {
|
||||||
const { customerName, customerId, ...rest } = filters;
|
productName,
|
||||||
return customerId ? { customerId, ...rest } : filters;
|
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) => {
|
.transform((filters) => {
|
||||||
// Mapeamento de chaves que precisam ser renomeadas
|
|
||||||
const keyMap: Record<string, string> = {
|
const keyMap: Record<string, string> = {
|
||||||
store: 'codfilial',
|
store: 'codfilial',
|
||||||
};
|
};
|
||||||
|
|
||||||
return Object.entries(filters).reduce(
|
return Object.entries(filters).reduce(
|
||||||
(acc, [key, value]) => {
|
(acc, [key, value]) => {
|
||||||
// Early return: ignora valores vazios
|
|
||||||
if (isEmptyValue(value)) return acc;
|
if (isEmptyValue(value)) return acc;
|
||||||
|
|
||||||
const apiKey = keyMap[key] ?? key;
|
const apiKey = keyMap[key] ?? key;
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,11 @@ export { partnerSchema } from './partner.schema';
|
||||||
export type { Partner } from './partner.schema';
|
export type { Partner } from './partner.schema';
|
||||||
export { partnersResponseSchema } from './api-responses.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
|
// Schema de itens de pedido
|
||||||
export { orderItemSchema, orderItemsResponseSchema } from './order.item.schema';
|
export { orderItemSchema, orderItemsResponseSchema } from './order.item.schema';
|
||||||
export type { OrderItem, OrderItemsResponse } 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