feat: implement partner filter in orders search

This commit is contained in:
JuruSysadmin 2026-01-15 16:19:49 -03:00
parent c66bc9620b
commit 275cbfd29c
7 changed files with 176 additions and 0 deletions

View File

@ -15,6 +15,7 @@ import {
storesResponseSchema,
customersResponseSchema,
sellersResponseSchema,
partnersResponseSchema,
unwrapApiData,
} from '../schemas/order.schema';
import {
@ -178,5 +179,23 @@ export const orderService = {
return unwrapApiData(response, cuttingItemResponseSchema, []);
},
/**
* Busca parceiros por nome/CPF.
* Retorna array vazio se o termo de busca tiver menos de 2 caracteres.
*
* @param {string} filter - O termo de busca (mínimo 2 caracteres)
* @returns {Promise<Array<{id: number, cpf: string, nome: string}>>} Array de parceiros correspondentes
*/
findPartners: async (
filter: string
): Promise<Array<{ id: number; cpf: string; nome: string }>> => {
if (!filter || filter.trim().length < 2) return [];
const response = await ordersApi.get(
`/api/v1/parceiros/${encodeURIComponent(filter)}`
);
return unwrapApiData(response, partnersResponseSchema, []);
},
};

View File

@ -20,6 +20,7 @@ import {
import { useStores } from '../store/useStores';
import { useCustomers } from '../hooks/useCustomers';
import { useSellers } from '../hooks/useSellers';
import { usePartners } from '../hooks/usePartners';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment';
@ -39,6 +40,8 @@ interface LocalFilters {
orderId: number | null;
customerId: number | null;
customerName: string | null;
partnerId: string | null;
partnerName: string | null;
createDateIni: string | null;
createDateEnd: string | null;
store: string[] | null;
@ -54,6 +57,8 @@ const getInitialLocalFilters = (
orderId: urlFilters.orderId ?? null,
customerId: urlFilters.customerId ?? null,
customerName: urlFilters.customerName ?? null,
partnerId: urlFilters.partnerId ?? null,
partnerName: urlFilters.partnerName ?? null,
createDateIni: urlFilters.createDateIni ?? null,
createDateEnd: urlFilters.createDateEnd ?? null,
store: urlFilters.store ?? null,
@ -73,6 +78,8 @@ export const SearchBar = () => {
const sellers = useSellers();
const [customerSearchTerm, setCustomerSearchTerm] = useState('');
const customers = useCustomers(customerSearchTerm);
const [partnerSearchTerm, setPartnerSearchTerm] = useState('');
const partners = usePartners(partnerSearchTerm);
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
const { isFetching } = useOrders();
const [touchedFields, setTouchedFields] = useState<{
@ -94,6 +101,7 @@ export const SearchBar = () => {
const handleReset = useCallback(() => {
setTouchedFields({});
setCustomerSearchTerm('');
setPartnerSearchTerm('');
const resetState = {
status: null,
@ -112,6 +120,8 @@ export const SearchBar = () => {
searchTriggered: false,
customerId: null,
customerName: null,
partnerId: null,
partnerName: null,
};
setLocalFilters(getInitialLocalFilters(resetState));
@ -172,6 +182,7 @@ export const SearchBar = () => {
localFilters.store?.length,
localFilters.stockId?.length,
localFilters.sellerId,
localFilters.partnerId,
].filter(Boolean).length;
return (
@ -601,6 +612,75 @@ export const SearchBar = () => {
}}
/>
</Grid>
{/* Autocomplete do MUI para Parceiro */}
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<Autocomplete
size="small"
options={partners.options}
getOptionLabel={(option) => option.label}
isOptionEqualToValue={(option, value) => option.id === value.id}
value={
partners.options.find(
(option) =>
localFilters.partnerId ===
option.partner?.id?.toString()
) || null
}
onChange={(_, newValue) => {
if (!newValue) {
updateLocalFilter('partnerName', null);
updateLocalFilter('partnerId', null);
setPartnerSearchTerm('');
return;
}
updateLocalFilter(
'partnerId',
newValue.partner?.id?.toString() || null
);
updateLocalFilter(
'partnerName',
newValue.partner?.nome || null
);
}}
onInputChange={(_, newInputValue, reason) => {
if (reason === 'clear') {
updateLocalFilter('partnerName', null);
updateLocalFilter('partnerId', null);
setPartnerSearchTerm('');
return;
}
if (reason === 'input') {
setPartnerSearchTerm(newInputValue);
if (!newInputValue) {
updateLocalFilter('partnerName', null);
updateLocalFilter('partnerId', null);
setPartnerSearchTerm('');
}
}
}}
loading={partners.isLoading}
renderInput={(params: AutocompleteRenderInputParams) => (
<TextField
{...params}
label="Parceiro"
placeholder="Digite para buscar..."
/>
)}
noOptionsText={
partnerSearchTerm.length < 2
? 'Digite pelo menos 2 caracteres'
: 'Nenhum parceiro encontrado'
}
loadingText="Buscando parceiros..."
filterOptions={(x) => x}
clearOnBlur={false}
selectOnFocus
handleHomeEndKeys
/>
</Grid>
</Grid>
</Collapse>
</Grid>

View File

@ -16,6 +16,8 @@ export const useOrderFilters = () => {
sellerId: parseAsString,
customerName: parseAsString,
customerId: parseAsInteger,
partnerName: parseAsString,
partnerId: parseAsString,
codfilial: parseAsArrayOf(parseAsString, ','),
codusur2: parseAsArrayOf(parseAsString, ','),

View File

@ -0,0 +1,48 @@
'use client';
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { orderService } from '../api/order.service';
export interface Partner {
id: number;
cpf: string;
nome: string;
}
export function usePartners(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: ['partners', debouncedSearchTerm],
queryFn: () => orderService.findPartners(debouncedSearchTerm),
enabled: isEnabled,
staleTime: 1000 * 60 * 5,
retry: 1,
retryOnMount: false,
refetchOnWindowFocus: false,
});
const options =
query.data?.map((partner, index) => ({
value: partner.id.toString(),
label: partner.nome,
id: `partner-${partner.id}-${index}`,
partner: partner,
})) ?? [];
return {
...query,
options,
};
}

View File

@ -2,6 +2,7 @@ import { z } from 'zod';
import { storeSchema } from './store.schema';
import { customerSchema } from './customer.schema';
import { sellerSchema } from './seller.schema';
import { partnerSchema } from './partner.schema';
import { createApiSchema } from './api-response.schema';
/**
@ -39,3 +40,9 @@ export const customersResponseSchema = createApiSchema(z.array(customerSchema));
* Formato: { success: boolean, data: Seller[] }
*/
export const sellersResponseSchema = createApiSchema(z.array(sellerSchema));
/**
* Schema para validar a resposta da API ao buscar parceiros.
* Formato: { success: boolean, data: Partner[] }
*/
export const partnersResponseSchema = createApiSchema(z.array(partnerSchema));

View File

@ -26,6 +26,11 @@ export { sellerSchema } from './seller.schema';
export type { Seller } from './seller.schema';
export { sellersResponseSchema } from './api-responses.schema';
// Schema de parceiros
export { partnerSchema } from './partner.schema';
export type { Partner } from './partner.schema';
export { partnersResponseSchema } 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,15 @@
import { z } from 'zod';
/**
* Schema Zod para validar um parceiro.
*/
export const partnerSchema = z.object({
id: z.number(),
cpf: z.string(),
nome: z.string(),
});
/**
* Type para parceiro inferido do schema.
*/
export type Partner = z.infer<typeof partnerSchema>;