import { useState, useEffect, useCallback, useRef } from "react"; import { ordersApi, dataConsultApi } from "@/src/lib/api"; import { OrderSearchParams, OrderItem, Customer, Order, Cliente, cutitens } from "@/src/components/types"; import { validateDateRange } from "@/src/utils/date-helpers"; import { extractApiData } from "@/src/utils/api-helpers"; import { useSearchParams, useRouter } from "next/navigation"; import { clientesApi } from "@/src/lib/api" import { setStorage, getStorage } from "@/src/utils/storage"; import { clearUrlAndNavigate } from "@/src/utils/url-helpers"; /** * Função de utilidade para aplicar debounce em funções * Evita execuções repetidas em curtos intervalos de tempo * * @template T - Tipo da função a ser debounced * @param {T} callback - Função a aplicar debounce * @param {number} delay - Tempo de espera em ms * @returns {Function} Função com debounce aplicado */ function useDebounce any>( callback: T, delay: number ): (...args: Parameters) => void { const timeoutRef = useRef(null); return useCallback( (...args: Parameters) => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => { callback(...args); }, delay); }, [callback, delay] ); } interface UseOrderSearchReturn { orders: Order[]; loading: boolean; error: string | null; dateError: string | null; hasSearched: boolean; selectedOrderId: string | null; orderItems: OrderItem[]; cutitens: cutitens[]; loadingItems: boolean; itemsError: string | null; currentPage: number; totalPages: number; currentOrders: Order[]; indexOfFirstOrder: number; indexOfLastOrder: number; searchParams: OrderSearchParams; setSearchParams: React.Dispatch>; handleSearch: () => Promise; handleRowClick: (orderId: string) => Promise; goToNextPage: () => void; goToPreviousPage: () => void; goToPage: (page: number) => void; loadMoreOrders: () => void; visibleOrdersCount: number; handleInputChange: (field: keyof OrderSearchParams, value: any) => void; handleCustomerSelect: (customer: Cliente | null) => void; handleCustomerId: (value: string) => Promise; handleCustomerFilter: (value: string) => Promise; setInitialOrders: (savedOrders: Order[], savedPage: number, savedHasSearched: boolean) => void; clearStoredParams: () => void; clearAndNavigateToOrders: () => void; handleMultiInputChange: (field: keyof OrderSearchParams, values: string[]) => void; } // Mapeamento para converter nomes de parâmetros da URL para o estado const paramMapping: Record = { Id: "id", orderId: "orderId", Document: "Document", name: "name", sellerName: "sellerName", codfilial: "codfilial", status: "status", createDateIni: "createDateIni", createDateEnd: "createDateEnd", customerId: "customerId", document: "document", invoiceNumber: "invoiceNumber", productId: "productId" }; /** * Hook personalizado para gerenciar a consulta e exibição de pedidos * Gerencia busca, paginação, armazenamento de parâmetros e detalhes de pedidos * * @param {number} ordersPerPage - Número de pedidos a serem exibidos por página * @returns {UseOrderSearchReturn} Objeto contendo estado e funções para lidar com pedidos */ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn { const router = useRouter(); const urlSearchParams = useSearchParams(); // Inicializar searchParams com valores da URL ou do localStorage ou um objeto vazio const [searchParams, setSearchParams] = useState(() => { const params: OrderSearchParams = {}; // Tentar recuperar do localStorage primeiro const savedParams = getStorage("orderSearchParams"); if (savedParams) { return savedParams; } // Se não houver no localStorage, extrair valores da URL para o estado Object.entries(paramMapping).forEach(([urlParam, stateParam]) => { const value = urlSearchParams.get(urlParam); if (value) { // Tratando corretamente o tipo do parâmetro (params as any)[stateParam] = value; } }); // Tratamento especial para parâmetros que podem ser arrays const typeParam = urlSearchParams.get('type'); if (typeParam) { // Se o parâmetro type contém vírgulas, é um array if (typeParam.includes(',')) { params.type = typeParam.split(',').join(','); } else { params.type = typeParam; } } return params; }); const [orders, setOrders] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [hasSearched, setHasSearched] = useState(false); const [selectedOrderId, setSelectedOrderId] = useState(null); const [orderItems, setOrderItems] = useState([]); const [cutItems, setCutItems] = useState([]); const [loadingItems, setLoadingItems] = useState(false); const [itemsError, setItemsError] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [visibleOrdersCount, setVisibleOrdersCount] = useState(ordersPerPage); const [dateError, setDateError] = useState(null); // Detectar parâmetro origem=detalhe para realizar pesquisa automática ao retornar const origin = urlSearchParams.get("origem"); // Limpar todos os campos relacionados ao cliente const clearCustomerFields = useCallback(() => { setSearchParams(prev => ({ ...prev, customerId: "", document: "", name: "", selectedCustomer: null, })); }, []); // Limpar todos os campos relacionados ao produto const clearProductFields = useCallback(() => { setSearchParams(prev => ({ ...prev, productId: "", selectedProduct: null, })); }, []); // Efeito para realizar pesquisa automática quando a página é carregada // e há parâmetros de pesquisa na URL ou se estamos voltando da página de detalhes useEffect(() => { const hasSearchParams = Object.keys(searchParams).length > 0; // Se temos parâmetros de busca e vamos retornando da página de detalhes, // ou se temos parâmetros e ainda não realizamos a pesquisa if ((hasSearchParams && origin === "detalhe") || (hasSearchParams && !hasSearched)) { handleSearch(); } }, []); /** * Atualiza a URL com os parâmetros de pesquisa atuais * Salva os parâmetros no localStorage para persistência entre sessões */ const updateUrlWithSearchParams = useCallback(() => { const urlParams = new URLSearchParams(); // Adicionar apenas parâmetros com valores Object.entries(searchParams).forEach(([key, value]) => { if (value) { if ((key === 'type' || key === 'status' || key === 'deliveryType') && Array.isArray(value)) { // Para arrays, juntar com vírgula urlParams.append(key, value.join(',')); } else { urlParams.append(key, value.toString()); } } }); // Preservar parâmetro origem se existir if (origin) { urlParams.append("origem", origin); } // Salvar no localStorage setStorage("orderSearchParams", searchParams); // Atualizar URL sem causar recarregamento da página const url = window.location.pathname + (urlParams.toString() ? `?${urlParams.toString()}` : ""); window.history.replaceState({ path: url }, "", url); }, [searchParams, origin]); /** * Executa a busca de pedidos com base nos parâmetros definidos * Valida datas, formata parâmetros e faz a chamada à API * * @returns {Promise} */ const handleSearch = async () => { // Verificar se pelo menos uma data está preenchida if (!searchParams.createDateIni && !searchParams.createDateEnd) { setDateError("Por favor, informe pelo menos uma data para realizar a pesquisa"); return; } // Validate date range const dateValidation = validateDateRange( searchParams.createDateIni, searchParams.createDateEnd ); if (!dateValidation.isValid) { setDateError(dateValidation.errorMessage); return; } // Clear date error setDateError(null); try { setLoading(true); setHasSearched(true); // Transform the ALL value to empty string for the API call const paramsToSend: OrderSearchParams = { ...searchParams }; // Remover campos que não devem ser enviados para a API if (paramsToSend.selectedCustomer) { delete paramsToSend.selectedCustomer; } if (paramsToSend.selectedProduct) { delete paramsToSend.selectedProduct; } // Se tiver customerId definido, remover name e document para evitar conflito if (paramsToSend.customerId) { delete paramsToSend.name; delete paramsToSend.document; } // Se temos o nome do vendedor mas não temos o ID, tentar buscar o ID primeiro if (paramsToSend.sellerName && !paramsToSend.sellerId) { try { const sellers = await dataConsultApi.getSellers(); if (Array.isArray(sellers)) { const matchingSeller = sellers.find((seller: any) => seller.name.toLowerCase().includes(paramsToSend.sellerName?.toLowerCase() || '') ); if (matchingSeller) { // Extrair código numérico (887 de "887 - SAL-SIMONI") if (matchingSeller.name && matchingSeller.name.includes("-")) { const parts = matchingSeller.name.split("-"); if (parts.length > 0 && parts[0].trim()) { const match = parts[0].trim().match(/\d+/); if (match) { paramsToSend.sellerId = match[0]; // Após obter o ID, remover o nome para evitar conflitos delete paramsToSend.sellerName; } } } } } } catch(err) { console.error("Erro ao buscar vendedores:", err); } } // Mapear invoiceNumber para invoiceId que é o que a API espera if (paramsToSend.invoiceNumber) { paramsToSend.invoiceId = paramsToSend.invoiceNumber; delete paramsToSend.invoiceNumber; } // Remover campos que a API não espera if ('nfe' in paramsToSend) { delete paramsToSend.nfe; } if (paramsToSend.codfilial === "ALL") { paramsToSend.codfilial = ""; } if (paramsToSend.status === "ALL") { paramsToSend.status = ""; } // Remover propriedades vazias para evitar parâmetros desnecessários na URL Object.keys(paramsToSend).forEach(key => { const paramKey = key as keyof OrderSearchParams; if (paramsToSend[paramKey] === "" || paramsToSend[paramKey] === null || paramsToSend[paramKey] === undefined) { delete paramsToSend[paramKey]; } }); // Construir manualmente o queryParams para lidar com arrays const queryParams = new URLSearchParams(); Object.entries(paramsToSend).forEach(([key, value]) => { if ((key === 'type' || key === 'status' || key === 'deliveryType') && Array.isArray(value)) { // Unir os valores do array em uma única string separada por vírgula queryParams.append(key, value.join(',')); } else if (value !== undefined && value !== null && value !== "") { queryParams.append(key, String(value)); } }); const queryString = queryParams.toString(); const response = await ordersApi.findOrders(paramsToSend, queryString); setOrders(extractApiData(response)); setError(null); // Reset selected order and items when new search is performed setSelectedOrderId(null); setOrderItems([]); setCutItems([]); // Reset pagination state setCurrentPage(1); setVisibleOrdersCount(ordersPerPage); // Atualizar a URL com os parâmetros de pesquisa updateUrlWithSearchParams(); } catch (err) { setError("Erro ao buscar pedidos. Por favor, tente novamente."); console.error(err); } finally { setLoading(false); } }; /** * Manipula alterações em campos de entrada do formulário * Atualiza o estado searchParams com o novo valor * * @param {keyof OrderSearchParams} field - Nome do campo a atualizar * @param {any} value - Novo valor para o campo */ const handleInputChange = (field: keyof OrderSearchParams, value: any) => { // Garantir que valores apropriados sejam tratados como arrays if ((field === 'type' || field === 'status' || field === 'deliveryType') && typeof value === 'string' && value !== '') { // Converter string para array com um único item setSearchParams((prev) => ({ ...prev, [field]: [value], })); } else { setSearchParams((prev) => ({ ...prev, [field]: value, })); } // Clear date error when user changes dates if (field === "createDateIni" || field === "createDateEnd") { setDateError(null); } }; /** * Seleciona um cliente após busca, preenchendo campos relacionados * * @param {Cliente | null} customer - Cliente selecionado ou null para limpar */ const handleCustomerSelect = (customer: Cliente | null) => { if (!customer) { clearCustomerFields(); return; } setSearchParams((prev) => ({ ...prev, selectedCustomer: customer, customerId: customer.id ?? '', document: '', name: '', })); }; /** * Manipula clique em linha da tabela de pedidos * Carrega detalhes do pedido selecionado * * @param {string} orderId - ID do pedido selecionado * @returns {Promise} */ const handleRowClick = async (orderId: string) => { if (selectedOrderId === orderId) { // Toggle off if already selected setSelectedOrderId(null); setOrderItems([]); setCutItems([]); return; } try { setLoadingItems(true); setSelectedOrderId(orderId); setItemsError(null); // Load order items const itemsResponse = await ordersApi.getOrderItems(orderId); setOrderItems(extractApiData(itemsResponse)); // Load cut items const cutItemsResponse = await ordersApi.getCutItems(orderId); let cutItems: cutitens[] = []; if (cutItemsResponse && typeof cutItemsResponse === "object") { if ("data" in cutItemsResponse && Array.isArray(cutItemsResponse.data)) { cutItems = cutItemsResponse.data; } else if (Array.isArray(cutItemsResponse)) { cutItems = cutItemsResponse; } } setCutItems(cutItems); } catch (err) { setItemsError("Erro ao carregar itens do pedido."); console.error(err); } finally { setLoadingItems(false); } }; // Pagination calculations - corrigir para garantir 8 itens por página const totalPages = Math.ceil(orders.length / ordersPerPage); // Calcular o índice inicial baseado na página atual const indexOfFirstOrder = (currentPage - 1) * ordersPerPage; // Calcular o índice final const indexOfLastOrder = Math.min(indexOfFirstOrder + ordersPerPage, orders.length); // Selecionar apenas os itens da página atual const currentOrders = orders.slice(indexOfFirstOrder, indexOfLastOrder); // Navigate to next page const goToNextPage = useCallback(() => { if (currentPage < totalPages) { setCurrentPage(currentPage + 1); // Reset selected order when changing pages setSelectedOrderId(null); setOrderItems([]); setCutItems([]); } }, [currentPage, totalPages]); // Navigate to previous page const goToPreviousPage = useCallback(() => { if (currentPage > 1) { setCurrentPage(currentPage - 1); // Reset selected order when changing pages setSelectedOrderId(null); setOrderItems([]); setCutItems([]); } }, [currentPage]); // Load more orders function to expand the current view - não é mais necessária // Mantida por retrocompatibilidade const loadMoreOrders = useCallback(() => { // Não faz nada, pois estamos usando paginação tradicional agora }, []); // New function to go to a specific page const goToPage = useCallback((page: number) => { if (page >= 1 && page <= totalPages) { setCurrentPage(page); // Reset selected order when changing pages setSelectedOrderId(null); setOrderItems([]); setCutItems([]); } }, [totalPages]); // Atualizar visibleOrdersCount para compatibilidade com a interface useEffect(() => { setVisibleOrdersCount(indexOfLastOrder); }, [indexOfLastOrder]); /** * Identifica o tipo de entrada para busca de cliente * Diferencia entre ID, documento ou nome baseado no formato * * @param {string} value - Valor de entrada a ser analisado * @returns {'id' | 'document' | 'name'} Tipo identificado da entrada */ const identifyInputType = (value: string): 'id' | 'document' | 'name' => { if (/^\d+$/.test(value)) { return value.length <= 6 ? 'id' : 'document'; } return 'name'; }; /** * Busca cliente por ID * * @param {string} value - ID do cliente a ser buscado * @returns {Promise} */ const handleCustomerId = async (value: string) => { try { // Se value for vazio, limpar todos os campos relacionados ao cliente if (!value) { clearCustomerFields(); return; } let customerData: any = null; const inputType = identifyInputType(value); // Atualizar o campo apropriado com base no tipo de entrada setSearchParams(prev => ({ ...prev, customerId: inputType === 'id' ? value : "", document: inputType === 'document' ? value : "", name: inputType === 'name' ? value : "", })); // Buscar cliente pelo tipo correto if (inputType === 'id') { const response = await dataConsultApi.getCustomer(value); if (response.success && response.data && response.data.length > 0) { customerData = response.data[0]; } } else if (inputType === 'document' || inputType === 'name') { const response = await dataConsultApi.getCustomers(value); if (response && response.length > 0) { customerData = response[0]; } } if (customerData) { // Create customer object from response const customer: Customer = { id: customerData.id.toString(), name: customerData.name, document: customerData.document || "" }; // Update selected customer in form state setSearchParams(prev => ({ ...prev, selectedCustomer: customer, customerId: customer.id, name: "", document: "", })); } } catch (error) { console.error("Erro ao buscar dados do cliente:", error); setError("Erro ao buscar dados do cliente. Por favor, tente novamente."); } }; /** * Busca cliente por nome * * @param {string} value - Nome parcial ou completo para busca * @returns {Promise} */ const searchCustomerByName = async (value: string) => { if (!value || value.length < 2) return; try { const matches = await clientesApi.search(value); if (matches.length > 0) { const c = matches[0]; const clientId = c.id || ""; const clientName = c.name || ""; setSearchParams(prev => ({ ...prev, customerId: clientId, document: "", name: "", selectedCustomer: { id: clientId, name: clientName, document: "", }, })); } } catch (err) { console.error("Erro ao buscar cliente:", err); setError("Erro ao buscar cliente. Por favor, tente novamente."); } }; // Criar versão com debounce da função de busca const debouncedSearchCustomer = useDebounce(searchCustomerByName, 500); /** * Manipula entrada de filtro de cliente * Identifica o tipo de filtro e direciona para a busca apropriada * * @param {string} value - Termo de busca (ID, documento ou nome) * @returns {Promise} */ const handleCustomerFilter = async (value: string) => { // Se o campo estiver vazio, limpa todos os campos relacionados ao cliente if (!value) { clearCustomerFields(); return; } // Atualiza o campo de nome no estado setSearchParams(prev => ({ ...prev, name: value, customerId: "", document: "", })); // Se ainda está digitando poucas letras, apenas atualiza o estado if (value.length < 2) return; // Aciona a busca com debounce para evitar chamadas excessivas à API debouncedSearchCustomer(value); }; /** * Restaura o estado da pesquisa com dados previamente salvos * Útil para preservar o estado ao voltar da página de detalhes */ const setInitialOrders = useCallback((savedOrders: Order[], savedPage: number, savedHasSearched: boolean) => { setOrders(savedOrders); setCurrentPage(savedPage); setHasSearched(savedHasSearched); setVisibleOrdersCount(ordersPerPage * savedPage); // Calcular total de páginas const calculatedTotalPages = Math.ceil(savedOrders.length / ordersPerPage); // Como não temos acesso direto à função setTotalPages, calcular localmente const totalPages = calculatedTotalPages; // Atualizar pedidos da página atual const indexOfLastOrder = savedPage * ordersPerPage; const indexOfFirstOrder = indexOfLastOrder - ordersPerPage; const currentOrdersSlice = savedOrders.slice(indexOfFirstOrder, indexOfLastOrder); // Como não temos acesso direto à função setCurrentOrders, atualizar manualmente // Vamos fazer isso através do atualizador do estado orders, que vai disparar o useEffect // que atualiza currentOrders setOrders(savedOrders); }, [ordersPerPage]); // Função para limpar os parâmetros do localStorage const clearStoredParams = useCallback(() => { setStorage("orderSearchParams", null); }, []); // Função para limpar parâmetros e navegar para orders/find const clearAndNavigateToOrders = useCallback(() => { // Limpar parâmetros do localStorage setStorage("orderSearchParams", null); // Limpar parâmetros do estado setSearchParams({}); // Limpar URL e navegar clearUrlAndNavigate("/orders/find"); }, [setSearchParams]); /** * Manipula a seleção de múltiplos valores em filtros de seleção * * @param {string} field - Nome do campo no objeto searchParams * @param {string[]} values - Array de valores selecionados */ const handleMultiInputChange = useCallback((field: keyof OrderSearchParams, values: string[]) => { setSearchParams((prev) => ({ ...prev, [field]: values, })); }, []); return { orders, loading, error, dateError, hasSearched, selectedOrderId, orderItems, cutitens: cutItems, loadingItems, itemsError, currentPage, totalPages, currentOrders, indexOfFirstOrder, indexOfLastOrder, searchParams, setSearchParams, handleSearch, handleRowClick, goToNextPage, goToPreviousPage, goToPage, loadMoreOrders, visibleOrdersCount, handleInputChange, handleCustomerSelect, handleCustomerId, handleCustomerFilter, setInitialOrders, clearStoredParams, clearAndNavigateToOrders, handleMultiInputChange }; }