This commit is contained in:
JuruSysadmin
2025-06-17 13:41:48 -03:00
commit af154c3f7f
197 changed files with 50658 additions and 0 deletions

706
src/hooks/useOrderSearch.ts Normal file
View File

@@ -0,0 +1,706 @@
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";
/**
* 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<T extends (...args: any[]) => any>(
callback: T,
delay: number
): (...args: Parameters<T>) => void {
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
return useCallback(
(...args: Parameters<T>) => {
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<React.SetStateAction<OrderSearchParams>>;
handleSearch: () => Promise<void>;
handleRowClick: (orderId: string) => Promise<void>;
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<void>;
handleCustomerFilter: (value: string) => Promise<void>;
setInitialOrders: (savedOrders: Order[], savedPage: number, savedHasSearched: boolean) => void;
clearStoredParams: () => void;
handleMultiInputChange: (field: keyof OrderSearchParams, values: string[]) => void;
}
// Mapeamento para converter nomes de parâmetros da URL para o estado
const paramMapping: Record<string, keyof OrderSearchParams> = {
Id: "id",
orderId: "orderId",
Document: "Document",
name: "name",
sellerName: "sellerName",
codfilial: "codfilial",
status: "status",
createDateIni: "createDateIni",
createDateEnd: "createDateEnd",
customerId: "customerId",
document: "document",
invoiceNumber: "invoiceNumber"
};
/**
* 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<OrderSearchParams>(() => {
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<Order[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [hasSearched, setHasSearched] = useState(false);
const [selectedOrderId, setSelectedOrderId] = useState<string | null>(null);
const [orderItems, setOrderItems] = useState<OrderItem[]>([]);
const [cutItems, setCutItems] = useState<cutitens[]>([]);
const [loadingItems, setLoadingItems] = useState(false);
const [itemsError, setItemsError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [visibleOrdersCount, setVisibleOrdersCount] = useState(ordersPerPage);
const [dateError, setDateError] = useState<string | null>(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,
}));
}, []);
// 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<void>}
*/
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 o campo selectedCustomer antes de enviar para a API
if (paramsToSend.selectedCustomer) {
delete paramsToSend.selectedCustomer;
}
// 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<Order>(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<void>}
*/
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<OrderItem>(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<void>}
*/
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<void>}
*/
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<void>}
*/
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);
}, []);
/**
* 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,
handleMultiInputChange
};
}