import React, { useState, useEffect, useCallback } from "react"; import { Product, OrderItem } from "../types"; import FilterSidebar from "../components/FilterSidebar"; import Baldinho from "../components/Baldinho"; import ProductDetailModal from "../components/ProductDetailModal"; import ProductCard from "../components/ProductCard"; import ProductListItem from "../components/ProductListItem"; import SearchInput from "../components/SearchInput"; import CategoryCard from "../components/CategoryCard"; import LoadingSpinner from "../components/LoadingSpinner"; import NoImagePlaceholder from "../components/NoImagePlaceholder"; import NoData from "../components/NoData"; import { productService, SaleProduct, FilterProduct, } from "../src/services/product.service"; import { authService } from "../src/services/auth.service"; interface ProductSearchViewProps { onAddToCart: (p: Product | OrderItem) => void; } const ProductSearchView: React.FC = ({ onAddToCart, }) => { const [searchTerm, setSearchTerm] = useState(""); const [showResults, setShowResults] = useState(false); const [loading, setLoading] = useState(false); const [searchResults, setSearchResults] = useState([]); const [error, setError] = useState(null); const [selectedDeliveryDate, setSelectedDeliveryDate] = useState(""); const [selectedProduct, setSelectedProduct] = useState(null); const [isDetailModalOpen, setIsDetailModalOpen] = useState(false); const [initialQuantity, setInitialQuantity] = useState(1); const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); // Modo de visualização const [productQuantities, setProductQuantities] = useState< Record >({}); // Quantidades por produto no modo lista const [isFiltersOpen, setIsFiltersOpen] = useState(false); // Estado para drawer de filtros em mobile // Inicializar quantidades quando os produtos mudam useEffect(() => { setProductQuantities((prev) => { const newQuantities = { ...prev }; searchResults.forEach((product) => { if (!newQuantities[product.id]) { newQuantities[product.id] = 1; } }); return newQuantities; }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchResults.length, searchResults.map((p) => p.id).join(",")]); // Estados para filtros const [selectedDepartment, setSelectedDepartment] = useState(""); const [selectedBrands, setSelectedBrands] = useState([]); const [availableBrands, setAvailableBrands] = useState([]); // Marcas disponíveis extraídas dos produtos const [filters, setFilters] = useState({ onlyInStock: false, stockBranch: "", onPromotion: false, discountRange: [0, 100] as [number, number], priceDropped: false, opportunities: false, unmissableOffers: false, }); /** * Converte SaleProduct da API para Product do componente * Segue o mesmo padrão do Angular */ const mapSaleProductToProduct = (saleProduct: SaleProduct): Product => { // Garantir que temos pelo menos um ID válido const productId = saleProduct.idProduct?.toString() || saleProduct.id?.toString() || ""; if (!productId) { console.warn("Produto sem ID válido:", saleProduct); } // Calcular desconto se houver diferença entre preço original e promocional let discount: number | undefined = saleProduct.offPercent; const listPrice = saleProduct.listPrice || 0; const salePrice = saleProduct.salePrice || saleProduct.salePromotion || 0; if (!discount && listPrice > 0 && salePrice > 0 && salePrice < listPrice) { discount = Math.round(((listPrice - salePrice) / listPrice) * 100); } return { id: productId || `product-${Math.random()}`, code: productId || "", name: saleProduct.title || saleProduct.smallDescription || saleProduct.description || "Produto sem nome", description: saleProduct.description || saleProduct.smallDescription || undefined, price: salePrice || listPrice || 0, originalPrice: listPrice > 0 && salePrice > 0 && salePrice < listPrice ? listPrice : undefined, discount: discount, mark: saleProduct.brand || "Sem marca", image: saleProduct.urlImage || "", stockLocal: saleProduct.store_stock || saleProduct.stock || 0, stockAvailable: saleProduct.stock || saleProduct.store_stock || 0, stockGeneral: saleProduct.full_stock || saleProduct.stock || 0, ean: saleProduct.ean || undefined, model: saleProduct.idProduct?.toString() || undefined, }; }; /** * Busca produtos por departamento/categoria * Segue o padrão do Angular: getProductByFilter com POST e urlCategory no body * Request: POST /sales/products * Body: { urlCategory: "ferragens/cadeados", brands: [], promotion: false, ... } */ const handleDepartmentChange = useCallback(async (urlDepartment: string) => { try { setLoading(true); setError(null); setShowResults(true); // Mostrar resultados ao clicar em departamento setSearchTerm(""); // Limpar termo de busca const store = authService.getStore(); if (!store) { throw new Error("Loja não encontrada. Faça login novamente."); } // Criar FilterProduct seguindo o padrão do Angular // No Angular: { brands: [], text: null, promotion: true, markdown: false, oportunity: false, offers: false, urlCategory: "ferragens/cadeados" } const filterProduct = { brands: [] as string[], text: null as string | null, promotion: false, markdown: false, oportunity: false, offers: false, urlCategory: urlDepartment, // URL do departamento/categoria }; // Buscar produtos usando POST /sales/products com urlCategory no body // Segue exatamente o padrão do Angular: getProductByFilter(store, page, size, filterProduct) const saleProducts = await productService.getProductByFilter( store, 1, // page 100, // size filterProduct ); if (!Array.isArray(saleProducts) || saleProducts.length === 0) { setSearchResults([]); setError(null); return; } // Converter SaleProduct[] para Product[] const mappedProducts = saleProducts.map(mapSaleProductToProduct); setSearchResults(mappedProducts); // Extrair marcas dos produtos retornados (seguindo padrão do Angular) const uniqueBrands = Array.from( new Set( mappedProducts .map((p) => p.mark) .filter((b) => b && b !== "Sem marca") ) ).sort((a, b) => { // Ordenar como no Angular: remover '#' e ordenar alfabeticamente const brandA = a.replace("#", "").toLowerCase(); const brandB = b.replace("#", "").toLowerCase(); return brandA < brandB ? -1 : brandA > brandB ? 1 : 0; }); // Atualizar marcas disponíveis setAvailableBrands(uniqueBrands); } catch (err: any) { console.error("Erro ao buscar produtos por departamento:", err); setError(err.message || "Erro ao buscar produtos. Tente novamente."); setSearchResults([]); } finally { setLoading(false); } }, []); /** * Aplica os filtros selecionados e busca produtos * Segue EXATAMENTE o padrão do Angular: getFilterProducts() * No Angular (linha 739-781): * 1. Atualiza this.filters com os valores dos checkboxes * 2. Limpa produtos atuais * 3. Chama getProductByFilter com os filtros * 4. Extrai marcas dos produtos retornados */ const handleApplyFilters = useCallback(async () => { try { setLoading(true); setError(null); setShowResults(true); // Mostrar resultados ao aplicar filtros const store = authService.getStore(); if (!store) { throw new Error("Loja não encontrada. Faça login novamente."); } // Construir FilterProduct seguindo EXATAMENTE o padrão do Angular // No Angular (linha 740-746): // this.filters.promotion = this.productPromotion; // this.filters.markdown = this.markdown; // this.filters.oportunity = this.oportunity; // this.filters.offers = this.offers; // this.filters.onlyWithStock = this.onlyWithStock; // this.filters.percentOffMin = this.percentOff[0]; // this.filters.percentOffMax = this.percentOff[1]; const filterProduct: FilterProduct = { brands: selectedBrands.length > 0 ? selectedBrands : [], text: searchTerm.trim() || null, urlCategory: selectedDepartment || null, promotion: filters.onPromotion, markdown: filters.priceDropped, oportunity: filters.opportunities, offers: filters.unmissableOffers, onlyWithStock: filters.onlyInStock, storeStock: filters.stockBranch || null, percentOffMin: filters.discountRange[0], percentOffMax: filters.discountRange[1], }; // Limpar produtos atuais (seguindo padrão do Angular: linha 747-748) setSearchResults([]); // Buscar produtos usando POST /sales/products com filtros no body // Segue exatamente o padrão do Angular: getProductByFilter(store, page, size, filterProduct) const saleProducts = await productService.getProductByFilter( store, 1, // page 100, // size filterProduct ); if (!Array.isArray(saleProducts) || saleProducts.length === 0) { setSearchResults([]); setError(null); return; } // Converter SaleProduct[] para Product[] const mappedProducts = saleProducts.map(mapSaleProductToProduct); setSearchResults(mappedProducts); // Extrair marcas dos produtos retornados (seguindo padrão do Angular: linha 760-779) // No Angular: ordena produtos por marca e extrai marcas únicas const uniqueBrands = Array.from( new Set( mappedProducts .map((p) => p.mark) .filter((b) => b && b !== "Sem marca") ) ).sort((a, b) => { // Ordenar como no Angular: remover '#' e ordenar alfabeticamente const brandA = a.replace("#", "").toLowerCase(); const brandB = b.replace("#", "").toLowerCase(); return brandA < brandB ? -1 : brandA > brandB ? 1 : 0; }); // Atualizar marcas disponíveis (seguindo padrão do Angular: linha 760) // No Angular: this.brands = []; e depois popula com os produtos setAvailableBrands(uniqueBrands); // Limpar seleção de marcas quando novos produtos são carregados // (opcional, seguindo padrão do Angular onde marcas são resetadas) // setSelectedBrands([]); } catch (err: any) { console.error("Erro ao aplicar filtros:", err); setError(err.message || "Erro ao aplicar filtros. Tente novamente."); setSearchResults([]); } finally { setLoading(false); } }, [ selectedDepartment, selectedBrands, filters, searchTerm, mapSaleProductToProduct, ]); /** * Filtra produtos por promoção ("DE" / "POR") * Segue o padrão do Angular: filterPromotion() * No Angular: define promotion: true e os outros como false, depois navega para product-list */ const handleFilterPromotion = useCallback(async () => { try { setLoading(true); setError(null); setShowResults(true); setSearchTerm(""); // Limpar termo de busca setSelectedDepartment(""); // Limpar departamento selecionado setSelectedBrands([]); // Limpar marcas selecionadas // Atualizar filtros: promotion = true, outros = false setFilters({ onlyInStock: false, stockBranch: "", onPromotion: true, // "DE" / "POR" discountRange: [0, 100], priceDropped: false, opportunities: false, unmissableOffers: false, }); const store = authService.getStore(); if (!store) { throw new Error("Loja não encontrada. Faça login novamente."); } // Construir FilterProduct com promotion: true const filterProduct: FilterProduct = { brands: [], text: null, urlCategory: null, promotion: true, // "DE" / "POR" markdown: false, oportunity: false, offers: false, onlyWithStock: false, storeStock: null, percentOffMin: 0, percentOffMax: 100, }; setSearchResults([]); const saleProducts = await productService.getProductByFilter( store, 1, 100, filterProduct ); if (!Array.isArray(saleProducts) || saleProducts.length === 0) { setSearchResults([]); setError(null); // Não é um erro, apenas estado vazio return; } const mappedProducts = saleProducts.map(mapSaleProductToProduct); setSearchResults(mappedProducts); // Extrair marcas const uniqueBrands = Array.from( new Set( mappedProducts .map((p) => p.mark) .filter((b) => b && b !== "Sem marca") ) ).sort((a, b) => { const brandA = a.replace("#", "").toLowerCase(); const brandB = b.replace("#", "").toLowerCase(); return brandA < brandB ? -1 : brandA > brandB ? 1 : 0; }); setAvailableBrands(uniqueBrands); } catch (err: any) { console.error("Erro ao filtrar produtos em promoção:", err); setError(err.message || "Erro ao buscar produtos. Tente novamente."); setSearchResults([]); } finally { setLoading(false); } }, [mapSaleProductToProduct]); /** * Filtra produtos que baixaram de preço * Segue o padrão do Angular: filterMarkdown() */ const handleFilterMarkdown = useCallback(async () => { try { setLoading(true); setError(null); setShowResults(true); setSearchTerm(""); setSelectedDepartment(""); setSelectedBrands([]); setFilters({ onlyInStock: false, stockBranch: "", onPromotion: false, discountRange: [0, 100], priceDropped: true, // BAIXARAM DE PREÇO opportunities: false, unmissableOffers: false, }); const store = authService.getStore(); if (!store) { throw new Error("Loja não encontrada. Faça login novamente."); } const filterProduct: FilterProduct = { brands: [], text: null, urlCategory: null, promotion: false, markdown: true, // BAIXARAM DE PREÇO oportunity: false, offers: false, onlyWithStock: false, storeStock: null, percentOffMin: 0, percentOffMax: 100, }; setSearchResults([]); const saleProducts = await productService.getProductByFilter( store, 1, 100, filterProduct ); if (!Array.isArray(saleProducts) || saleProducts.length === 0) { setSearchResults([]); setError(null); return; } const mappedProducts = saleProducts.map(mapSaleProductToProduct); setSearchResults(mappedProducts); const uniqueBrands = Array.from( new Set( mappedProducts .map((p) => p.mark) .filter((b) => b && b !== "Sem marca") ) ).sort((a, b) => { const brandA = a.replace("#", "").toLowerCase(); const brandB = b.replace("#", "").toLowerCase(); return brandA < brandB ? -1 : brandA > brandB ? 1 : 0; }); setAvailableBrands(uniqueBrands); } catch (err: any) { console.error("Erro ao filtrar produtos que baixaram de preço:", err); setError(err.message || "Erro ao buscar produtos. Tente novamente."); setSearchResults([]); } finally { setLoading(false); } }, [mapSaleProductToProduct]); /** * Filtra produtos em oportunidade * Segue o padrão do Angular: filterOportunity() */ const handleFilterOpportunity = useCallback(async () => { try { setLoading(true); setError(null); setShowResults(true); setSearchTerm(""); setSelectedDepartment(""); setSelectedBrands([]); setFilters({ onlyInStock: false, stockBranch: "", onPromotion: false, discountRange: [0, 100], priceDropped: false, opportunities: true, // OPORTUNIDADE unmissableOffers: false, }); const store = authService.getStore(); if (!store) { throw new Error("Loja não encontrada. Faça login novamente."); } const filterProduct: FilterProduct = { brands: [], text: null, urlCategory: null, promotion: false, markdown: false, oportunity: true, // OPORTUNIDADE offers: false, onlyWithStock: false, storeStock: null, percentOffMin: 0, percentOffMax: 100, }; setSearchResults([]); const saleProducts = await productService.getProductByFilter( store, 1, 100, filterProduct ); if (!Array.isArray(saleProducts) || saleProducts.length === 0) { setSearchResults([]); setError(null); return; } const mappedProducts = saleProducts.map(mapSaleProductToProduct); setSearchResults(mappedProducts); const uniqueBrands = Array.from( new Set( mappedProducts .map((p) => p.mark) .filter((b) => b && b !== "Sem marca") ) ).sort((a, b) => { const brandA = a.replace("#", "").toLowerCase(); const brandB = b.replace("#", "").toLowerCase(); return brandA < brandB ? -1 : brandA > brandB ? 1 : 0; }); setAvailableBrands(uniqueBrands); } catch (err: any) { console.error("Erro ao filtrar oportunidades:", err); setError(err.message || "Erro ao buscar produtos. Tente novamente."); setSearchResults([]); } finally { setLoading(false); } }, [mapSaleProductToProduct]); /** * Filtra ofertas imperdíveis * Segue o padrão do Angular: filterOffers() */ const handleFilterOffers = useCallback(async () => { try { setLoading(true); setError(null); setShowResults(true); setSearchTerm(""); setSelectedDepartment(""); setSelectedBrands([]); setFilters({ onlyInStock: false, stockBranch: "", onPromotion: false, discountRange: [0, 100], priceDropped: false, opportunities: false, unmissableOffers: true, // OFERTAS IMPERDÍVEIS }); const store = authService.getStore(); if (!store) { throw new Error("Loja não encontrada. Faça login novamente."); } const filterProduct: FilterProduct = { brands: [], text: null, urlCategory: null, promotion: false, markdown: false, oportunity: false, offers: true, // OFERTAS IMPERDÍVEIS onlyWithStock: false, storeStock: null, percentOffMin: 0, percentOffMax: 100, }; setSearchResults([]); const saleProducts = await productService.getProductByFilter( store, 1, 100, filterProduct ); if (!Array.isArray(saleProducts) || saleProducts.length === 0) { setSearchResults([]); setError(null); return; } const mappedProducts = saleProducts.map(mapSaleProductToProduct); setSearchResults(mappedProducts); const uniqueBrands = Array.from( new Set( mappedProducts .map((p) => p.mark) .filter((b) => b && b !== "Sem marca") ) ).sort((a, b) => { const brandA = a.replace("#", "").toLowerCase(); const brandB = b.replace("#", "").toLowerCase(); return brandA < brandB ? -1 : brandA > brandB ? 1 : 0; }); setAvailableBrands(uniqueBrands); } catch (err: any) { console.error("Erro ao filtrar ofertas imperdíveis:", err); setError(err.message || "Erro ao buscar produtos. Tente novamente."); setSearchResults([]); } finally { setLoading(false); } }, [mapSaleProductToProduct]); const categories = [ { label: '"DE" / "POR"', icon: (
$
), onClick: handleFilterPromotion, }, { label: "BAIXARAM DE PREÇO", icon: (
), onClick: handleFilterMarkdown, }, { label: "OPORTUNIDADE", icon: (
🤑
), onClick: handleFilterOpportunity, }, { label: "OFERTAS IMPERDÍVEIS", icon: (
), onClick: handleFilterOffers, }, ]; /** * Realiza a busca de produtos por termo de pesquisa * Segue o mesmo padrão do Angular: searchProduct(store, page, size, search) */ const handleSearch = useCallback(async () => { const trimmedSearch = searchTerm.trim(); // Validação: mínimo de 3 caracteres (seguindo padrão do Angular) if (trimmedSearch.length < 3) { setError("Digite pelo menos 3 caracteres para buscar"); return; } try { setLoading(true); setError(null); setShowResults(false); const store = authService.getStore(); if (!store) { throw new Error("Loja não encontrada. Faça login novamente."); } // Buscar produtos usando o método searchProduct (GET /sales/products/{search}) // Seguindo o padrão do Angular: searchProduct(store, page, size, search) const saleProducts = await productService.searchProduct( store, 1, // page 100, // size (no Angular usa 50 ou 5000 dependendo do contexto) trimmedSearch.toUpperCase() // No Angular converte para uppercase ); if (!Array.isArray(saleProducts) || saleProducts.length === 0) { setSearchResults([]); setError(null); setShowResults(true); return; } // Converter SaleProduct[] para Product[] const mappedProducts = saleProducts.map(mapSaleProductToProduct); setSearchResults(mappedProducts); setShowResults(true); } catch (err: any) { console.error("Erro ao buscar produtos:", err); setError(err.message || "Erro ao buscar produtos. Tente novamente."); setSearchResults([]); setShowResults(true); } finally { setLoading(false); } }, [searchTerm]); return (
{/* FilterSidebar - Desktop (sidebar fixa) */}
{ setSelectedDepartment(url); handleDepartmentChange(url); }} selectedBrands={selectedBrands} onBrandsChange={setSelectedBrands} filters={filters} onFiltersChange={setFilters} onApplyFilters={handleApplyFilters} brands={availableBrands} />
{/* Overlay para mobile quando filtros estão abertos */} {isFiltersOpen && (
setIsFiltersOpen(false)} /> )} {/* FilterSidebar - Mobile/Tablet (drawer) */}

Filtros

{ setSelectedDepartment(url); handleDepartmentChange(url); setIsFiltersOpen(false); // Fechar drawer após seleção }} selectedBrands={selectedBrands} onBrandsChange={setSelectedBrands} filters={filters} onFiltersChange={setFilters} onApplyFilters={() => { handleApplyFilters(); setIsFiltersOpen(false); // Fechar drawer após aplicar filtros }} brands={availableBrands} />
{!showResults ? (
{/* Barra superior com botão de filtros e input de busca - Mobile/Tablet */}
{/* Desktop: Título e input centralizado */}

O que vamos vender hoje?

Pesquise por nome, código ou categoria do produto.

Categorias em Destaque

Preços baixos hoje
{categories.map((item, idx) => ( ))}
{/* Componente "Baldinho" - Versão Evoluída (Painel de Logística) */}
) : (
{/* Header Fixo */}
{/* Mobile/Tablet: Barra superior com botão de filtros, voltar e input */}
{/* Desktop: Layout original */}

{searchTerm ? `Resultados para "${searchTerm}"` : filters.onPromotion ? 'Produtos "DE" / "POR"' : filters.priceDropped ? "Produtos que Baixaram de Preço" : filters.opportunities ? "Oportunidades" : filters.unmissableOffers ? "Ofertas Imperdíveis" : selectedDepartment ? `Produtos em "${selectedDepartment}"` : "Produtos Encontrados"}

{searchResults.length > 0 && (

{searchResults.length} produto {searchResults.length !== 1 ? "s" : ""} encontrado {searchResults.length !== 1 ? "s" : ""}

)} {/* Input de busca para nova pesquisa - Desktop */}
{/* Mobile/Tablet: Título dos resultados */}

{searchTerm ? `Resultados para "${searchTerm}"` : filters.onPromotion ? 'Produtos "DE" / "POR"' : filters.priceDropped ? "Produtos que Baixaram de Preço" : filters.opportunities ? "Oportunidades" : filters.unmissableOffers ? "Ofertas Imperdíveis" : selectedDepartment ? `Produtos em "${selectedDepartment}"` : "Produtos Encontrados"}

{searchResults.length > 0 && (

{searchResults.length} produto {searchResults.length !== 1 ? "s" : ""} encontrado {searchResults.length !== 1 ? "s" : ""}

)}
{/* Botões de alternância de visualização */} {searchResults.length > 0 && (
)}
{/* Área de Scroll - Apenas produtos */}
{/* Loading State */} {loading && ( )} {/* Error State - apenas erros reais (não "nenhum produto encontrado") */} {error && !loading && (

{error}

)} {/* Empty State - quando não há resultados e não há erro */} {!loading && searchResults.length === 0 && !error && (
)} {/* Results - apenas quando não está carregando e há resultados */} {!loading && searchResults.length > 0 && (
{searchResults.map((product) => (
{viewMode === "list" ? ( { setProductQuantities((prev) => ({ ...prev, [product.id]: newQty, })); }} onAddToCart={( prod, qty, stockStore, deliveryType ) => { onAddToCart({ ...prod, quantity: qty, stockStore, deliveryType, } as OrderItem); }} onViewDetails={(prod, qty) => { setSelectedProduct(prod); setInitialQuantity(qty || 1); setIsDetailModalOpen(true); }} /> ) : ( { setSelectedProduct(p); setInitialQuantity(qty || 1); setIsDetailModalOpen(true); }} /> )}
))}
)}
)}
{/* Modal de Detalhamento do Produto */} { setIsDetailModalOpen(false); setSelectedProduct(null); setInitialQuantity(1); }} onAddToCart={onAddToCart} initialQuantity={initialQuantity} onProductChange={(newProduct) => { // Atualizar o produto exibido no modal sem fechá-lo setSelectedProduct(newProduct); setInitialQuantity(1); }} />
); }; export default ProductSearchView;