From 275cbfd29cd73c40f1ca2a428d1063ff679e8ad9 Mon Sep 17 00:00:00 2001 From: JuruSysadmin Date: Thu, 15 Jan 2026 16:19:49 -0300 Subject: [PATCH 1/3] feat: implement partner filter in orders search --- src/features/orders/api/order.service.ts | 19 +++++ src/features/orders/components/SearchBar.tsx | 80 +++++++++++++++++++ src/features/orders/hooks/useOrderFilters.ts | 2 + src/features/orders/hooks/usePartners.ts | 48 +++++++++++ .../orders/schemas/api-responses.schema.ts | 7 ++ src/features/orders/schemas/order.schema.ts | 5 ++ src/features/orders/schemas/partner.schema.ts | 15 ++++ 7 files changed, 176 insertions(+) create mode 100644 src/features/orders/hooks/usePartners.ts create mode 100644 src/features/orders/schemas/partner.schema.ts diff --git a/src/features/orders/api/order.service.ts b/src/features/orders/api/order.service.ts index d21f0ea..7149289 100644 --- a/src/features/orders/api/order.service.ts +++ b/src/features/orders/api/order.service.ts @@ -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 de parceiros correspondentes + */ + findPartners: async ( + filter: string + ): Promise> => { + if (!filter || filter.trim().length < 2) return []; + + const response = await ordersApi.get( + `/api/v1/parceiros/${encodeURIComponent(filter)}` + ); + return unwrapApiData(response, partnersResponseSchema, []); + }, + }; diff --git a/src/features/orders/components/SearchBar.tsx b/src/features/orders/components/SearchBar.tsx index 626bfe5..130cb6f 100644 --- a/src/features/orders/components/SearchBar.tsx +++ b/src/features/orders/components/SearchBar.tsx @@ -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 = () => { }} /> + + {/* Autocomplete do MUI para Parceiro */} + + 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) => ( + + )} + noOptionsText={ + partnerSearchTerm.length < 2 + ? 'Digite pelo menos 2 caracteres' + : 'Nenhum parceiro encontrado' + } + loadingText="Buscando parceiros..." + filterOptions={(x) => x} + clearOnBlur={false} + selectOnFocus + handleHomeEndKeys + /> + diff --git a/src/features/orders/hooks/useOrderFilters.ts b/src/features/orders/hooks/useOrderFilters.ts index dabce54..ea72fa9 100644 --- a/src/features/orders/hooks/useOrderFilters.ts +++ b/src/features/orders/hooks/useOrderFilters.ts @@ -16,6 +16,8 @@ export const useOrderFilters = () => { sellerId: parseAsString, customerName: parseAsString, customerId: parseAsInteger, + partnerName: parseAsString, + partnerId: parseAsString, codfilial: parseAsArrayOf(parseAsString, ','), codusur2: parseAsArrayOf(parseAsString, ','), diff --git a/src/features/orders/hooks/usePartners.ts b/src/features/orders/hooks/usePartners.ts new file mode 100644 index 0000000..08a5691 --- /dev/null +++ b/src/features/orders/hooks/usePartners.ts @@ -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, + }; +} diff --git a/src/features/orders/schemas/api-responses.schema.ts b/src/features/orders/schemas/api-responses.schema.ts index c8fe4d4..1fd38f1 100644 --- a/src/features/orders/schemas/api-responses.schema.ts +++ b/src/features/orders/schemas/api-responses.schema.ts @@ -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)); diff --git a/src/features/orders/schemas/order.schema.ts b/src/features/orders/schemas/order.schema.ts index 7f87de1..31cad74 100644 --- a/src/features/orders/schemas/order.schema.ts +++ b/src/features/orders/schemas/order.schema.ts @@ -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'; diff --git a/src/features/orders/schemas/partner.schema.ts b/src/features/orders/schemas/partner.schema.ts new file mode 100644 index 0000000..92d180c --- /dev/null +++ b/src/features/orders/schemas/partner.schema.ts @@ -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; From 58463fe548e7f797c46cb0066968dc7547688ae1 Mon Sep 17 00:00:00 2001 From: JuruSysadmin Date: Thu, 15 Jan 2026 16:34:23 -0300 Subject: [PATCH 2/3] 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; From 5558ca6a0d118c659cf6bc1669a852ec3db8332a Mon Sep 17 00:00:00 2001 From: JuruSysadmin Date: Thu, 15 Jan 2026 16:44:15 -0300 Subject: [PATCH 3/3] feat: redesign searchbar with drawer lateral for better scalability --- src/features/orders/components/SearchBar.tsx | 757 ++++++------------- 1 file changed, 231 insertions(+), 526 deletions(-) diff --git a/src/features/orders/components/SearchBar.tsx b/src/features/orders/components/SearchBar.tsx index 940b2f3..ed5832b 100644 --- a/src/features/orders/components/SearchBar.tsx +++ b/src/features/orders/components/SearchBar.tsx @@ -12,9 +12,13 @@ import { Autocomplete, Paper, Tooltip, - Collapse, CircularProgress, Badge, + Drawer, + IconButton, + Typography, + Divider, + Stack, type AutocompleteRenderInputParams, } from '@mui/material'; import { useStores } from '../store/useStores'; @@ -28,8 +32,8 @@ import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; import { Search as SearchIcon, RestartAlt as ResetIcon, - ExpandLess as ExpandLessIcon, - ExpandMore as ExpandMoreIcon, + Tune as TuneIcon, + Close as CloseIcon, } from '@mui/icons-material'; import moment from 'moment'; import 'moment/locale/pt-br'; @@ -186,8 +190,10 @@ export const SearchBar = () => { touchedFields.createDateIni && !localFilters.createDateIni; const showDateEndError = touchedFields.createDateEnd && dateError; - // Contador de filtros avançados ativos - const advancedFiltersCount = [ + // Contador de filtros ativos no Drawer + const activeDrawerFiltersCount = [ + localFilters.status, + localFilters.customerId, localFilters.store?.length, localFilters.stockId?.length, localFilters.sellerId, @@ -195,10 +201,21 @@ export const SearchBar = () => { localFilters.productId, ].filter(Boolean).length; + const toggleDrawer = (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => { + if ( + event.type === 'keydown' && + ((event as React.KeyboardEvent).key === 'Tab' || + (event as React.KeyboardEvent).key === 'Shift') + ) { + return; + } + setShowAdvancedFilters(open); + }; + return ( { elevation={0} onKeyDown={handleKeyDown} > - - {/* --- Primary Filters (Always Visible) --- */} - - {/* Campo de Texto Simples (Nº Pedido) */} + + {/* Nº do Pedido */} { /> - {/* Select do MUI (Situação) */} - - { - const value = e.target.value || null; - updateLocalFilter('status', value); - }} - > - Todos - Pendente - Faturado - Cancelado - - - - {/* Autocomplete do MUI para Cliente */} - - option.label} - isOptionEqualToValue={(option, value) => option.id === value.id} - value={ - customers.options.find( - (option) => localFilters.customerId === option.customer?.id - ) || null - } - onChange={(_, newValue) => { - if (!newValue) { - updateLocalFilter('customerName', null); - updateLocalFilter('customerId', null); - setCustomerSearchTerm(''); - return; - } - - updateLocalFilter('customerId', newValue.customer?.id || null); - updateLocalFilter( - 'customerName', - newValue.customer?.name || null - ); - }} - onInputChange={(_, newInputValue, reason) => { - if (reason === 'clear') { - updateLocalFilter('customerName', null); - updateLocalFilter('customerId', null); - setCustomerSearchTerm(''); - return; - } - - if (reason === 'input') { - setCustomerSearchTerm(newInputValue); - if (!newInputValue) { - updateLocalFilter('customerName', null); - updateLocalFilter('customerId', null); - setCustomerSearchTerm(''); - } - } - }} - loading={customers.isLoading} - renderInput={(params: AutocompleteRenderInputParams) => ( - + + + { + setTouchedFields(prev => ({ ...prev, createDateIni: true })); + updateLocalFilter('createDateIni', date ? date.format('YYYY-MM-DD') : null); + }} + format="DD/MM/YYYY" + slotProps={{ + textField: { + size: 'small', + fullWidth: true, + required: true, + error: showDateIniError, + helperText: showDateIniError ? 'Obrigatório' : '', + } + }} /> - )} - noOptionsText={ - customerSearchTerm.length < 2 - ? 'Digite pelo menos 2 caracteres' - : 'Nenhum cliente encontrado' - } - loadingText="Buscando clientes..." - filterOptions={(x) => x} - clearOnBlur={false} - selectOnFocus - handleHomeEndKeys - /> - - - {/* Campos de Data */} - - - - - { + setTouchedFields(prev => ({ ...prev, createDateEnd: true })); + updateLocalFilter('createDateEnd', date ? date.format('YYYY-MM-DD') : null); + }} + format="DD/MM/YYYY" + slotProps={{ + textField: { + size: 'small', + fullWidth: true, + error: !!showDateEndError, + helperText: showDateEndError || '', } - onChange={(date: moment.Moment | null) => { - setTouchedFields((prev) => ({ - ...prev, - createDateIni: true, - })); - updateLocalFilter( - 'createDateIni', - date ? date.format('YYYY-MM-DD') : null - ); - }} - format="DD/MM/YYYY" - maxDate={ - localFilters.createDateEnd - ? moment(localFilters.createDateEnd, 'YYYY-MM-DD') - : undefined - } - slotProps={{ - textField: { - size: 'small', - fullWidth: true, - required: true, - error: showDateIniError, - helperText: showDateIniError - ? 'Data inicial é obrigatória' - : '', - onBlur: () => - setTouchedFields((prev) => ({ - ...prev, - createDateIni: true, - })), - inputProps: { - 'aria-required': true, - }, - }, - }} - /> - - - { - setTouchedFields((prev) => ({ - ...prev, - createDateEnd: true, - })); - updateLocalFilter( - 'createDateEnd', - date ? date.format('YYYY-MM-DD') : null - ); - }} - format="DD/MM/YYYY" - minDate={ - localFilters.createDateIni - ? moment(localFilters.createDateIni, 'YYYY-MM-DD') - : undefined - } - slotProps={{ - textField: { - size: 'small', - fullWidth: true, - error: !!showDateEndError, - helperText: showDateEndError || '', - onBlur: () => - setTouchedFields((prev) => ({ - ...prev, - createDateEnd: true, - })), - inputProps: { - placeholder: 'Opcional', - }, - }, - }} - /> - + }} + /> - {/* Botão Mais Filtros - inline com filtros primários */} - - + + + - - {/* Botões de Ação - nova linha abaixo */} - - - - - - - - - - - - - - - - {/* --- Advanced Filters (Collapsible) --- */} - - - - {/* Autocomplete do MUI para Múltiplas Filiais (codfilial) */} - - option.label} - isOptionEqualToValue={(option, value) => - option.id === value.id - } - value={stores.options.filter((option) => - localFilters.store?.includes(option.value) - )} - onChange={(_, newValue) => { - updateLocalFilter( - 'store', - newValue.map((option) => option.value) - ); - }} - loading={stores.isLoading} - renderInput={(params: AutocompleteRenderInputParams) => ( - - )} - /> - - - {/* Autocomplete do MUI para Filial de Estoque */} - - option.label} - isOptionEqualToValue={(option, value) => - option.id === value.id - } - value={stores.options.filter((option) => - localFilters.stockId?.includes(option.value) - )} - onChange={(_, newValue) => { - updateLocalFilter( - 'stockId', - newValue.map((option) => option.value) - ); - }} - loading={stores.isLoading} - renderInput={(params: AutocompleteRenderInputParams) => ( - - )} - /> - - - {/* Autocomplete do MUI para Vendedor */} - - option.label} - isOptionEqualToValue={(option, value) => - option.id === value.id - } - value={ - sellers.options.find( - (option) => - localFilters.sellerId === option.seller.id.toString() - ) || null - } - onChange={(_, newValue) => { - updateLocalFilter( - 'sellerId', - newValue?.seller.id.toString() || null - ); - updateLocalFilter( - 'sellerName', - newValue?.seller.name || null - ); - }} - loading={sellers.isLoading} - renderInput={(params) => ( - - )} - renderOption={(props, option) => { - const { key, ...otherProps } = props; - return ( -
  • - {option.label} -
  • - ); - }} - /> -
    - - {/* Autocomplete do MUI para Parceiro */} - - 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) => ( - - )} - noOptionsText={ - partnerSearchTerm.length < 2 - ? 'Digite pelo menos 2 caracteres' - : 'Nenhum parceiro encontrado' - } - loadingText="Buscando parceiros..." - filterOptions={(x) => x} - clearOnBlur={false} - selectOnFocus - 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 - /> - -
    -
    +
    + + {/* Sidebar de Filtros */} + + + {/* Header */} + + Todos os Filtros + + + + + + {/* Filtros Content */} + + + {/* Situação */} + updateLocalFilter('status', e.target.value || null)} + > + Todos + Pendente + Faturado + Cancelado + + + {/* Cliente */} + option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={customers.options.find(opt => localFilters.customerId === opt.customer?.id) || null} + onChange={(_, newValue) => { + updateLocalFilter('customerId', newValue?.customer?.id || null); + updateLocalFilter('customerName', newValue?.customer?.name || null); + }} + onInputChange={(_, val) => setCustomerSearchTerm(val)} + loading={customers.isLoading} + renderInput={(params) => } + noOptionsText={customerSearchTerm.length < 2 ? 'Digite 2+ caracteres' : 'Nenhum cliente'} + filterOptions={(x) => x} + /> + + {/* Filiais */} + option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={stores.options.filter((opt) => localFilters.store?.includes(opt.value))} + onChange={(_, newValue) => updateLocalFilter('store', newValue.map((opt) => opt.value))} + loading={stores.isLoading} + renderInput={(params) => } + /> + + {/* Filial de Estoque */} + option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={stores.options.filter((opt) => localFilters.stockId?.includes(opt.value))} + onChange={(_, newValue) => updateLocalFilter('stockId', newValue.map((opt) => opt.value))} + loading={stores.isLoading} + renderInput={(params) => } + /> + + {/* Vendedor */} + option.label} + value={sellers.options.find(opt => localFilters.sellerId === opt.seller.id.toString()) || null} + onChange={(_, newValue) => { + updateLocalFilter('sellerId', newValue?.seller.id.toString() || null); + updateLocalFilter('sellerName', newValue?.seller.name || null); + }} + loading={sellers.isLoading} + renderInput={(params) => } + /> + + {/* Parceiro */} + option.label} + value={partners.options.find(opt => localFilters.partnerId === opt.partner?.id?.toString()) || null} + onChange={(_, newValue) => { + updateLocalFilter('partnerId', newValue?.partner?.id?.toString() || null); + updateLocalFilter('partnerName', newValue?.partner?.nome || null); + }} + onInputChange={(_, val) => setPartnerSearchTerm(val)} + loading={partners.isLoading} + renderInput={(params) => } + noOptionsText={partnerSearchTerm.length < 2 ? 'Digite 2+ caracteres' : 'Nenhum parceiro'} + filterOptions={(x) => x} + /> + + {/* Produto */} + option.label} + value={products.options.find(opt => localFilters.productId === opt.product?.id) || null} + onChange={(_, newValue) => { + updateLocalFilter('productId', newValue?.product?.id || null); + updateLocalFilter('productName', newValue?.product?.description || null); + }} + onInputChange={(_, val) => setProductSearchTerm(val)} + loading={products.isLoading} + renderInput={(params) => } + noOptionsText={productSearchTerm.length < 2 ? 'Digite 2+ caracteres' : 'Nenhum produto'} + filterOptions={(x) => x} + /> + + + + + {/* Footer */} + + + + + +
    ); };