Vendaweb-portal/views/ProductSearchView.tsx

1272 lines
47 KiB
TypeScript
Raw Permalink Normal View History

2026-01-08 12:09:16 +00:00
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<ProductSearchViewProps> = ({
onAddToCart,
}) => {
const [searchTerm, setSearchTerm] = useState("");
const [showResults, setShowResults] = useState(false);
const [loading, setLoading] = useState(false);
const [searchResults, setSearchResults] = useState<Product[]>([]);
const [error, setError] = useState<string | null>(null);
const [selectedDeliveryDate, setSelectedDeliveryDate] = useState("");
const [selectedProduct, setSelectedProduct] = useState<Product | null>(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<string, number>
>({}); // 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<string[]>([]);
const [availableBrands, setAvailableBrands] = useState<string[]>([]); // 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: (
<div className="flex flex-col items-center">
<div className="w-12 h-12 rounded-full border-4 border-orange-500 flex items-center justify-center font-black text-orange-600 text-2xl bg-white shadow-inner">
$
</div>
<svg
className="w-8 h-8 text-orange-500 -mt-2 drop-shadow-sm"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</div>
),
onClick: handleFilterPromotion,
},
{
label: "BAIXARAM DE PREÇO",
icon: (
<div className="relative">
<svg
className="w-16 h-16 text-orange-500 drop-shadow-md"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12.76 2.24A2 2 0 0 0 11.34 2H4a2 2 0 0 0-2 2v7.34a2 2 0 0 0 .59 1.42l9.41 9.41a2 2 0 0 0 2.83 0l7.34-7.34a2 2 0 0 0 0-2.83l-9.41-9.41zM7 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm4.24 6l1.42 1.42L9.42 19.66 8 18.24l3.24-3.24zM16 11l-1.42-1.42 3.24-3.24L19.24 7.76 16 11z" />
</svg>
</div>
),
onClick: handleFilterMarkdown,
},
{
label: "OPORTUNIDADE",
icon: (
<div className="text-7xl drop-shadow-xl animate-pulse-slow">🤑</div>
),
onClick: handleFilterOpportunity,
},
{
label: "OFERTAS IMPERDÍVEIS",
icon: (
<div className="p-4 bg-orange-50 rounded-3xl group-hover:bg-orange-100 transition-colors">
<svg
className="w-14 h-14 text-orange-600 drop-shadow-sm"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.79l.05.025A4 4 0 008.943 18h5.416a2 2 0 001.962-1.608l1.2-6A2 2 0 0015.56 8H12V4a2 2 0 00-2-2 1 1 0 00-1 1v.667a4 4 0 01-.8 2.4L6.8 7.933a4 4 0 00-.8 2.4z" />
</svg>
</div>
),
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 (
<div className="h-full flex bg-[#f8fafc] relative">
{/* FilterSidebar - Desktop (sidebar fixa) */}
<div className="hidden xl:block">
<FilterSidebar
selectedDepartment={selectedDepartment}
onDepartmentChange={(url) => {
setSelectedDepartment(url);
handleDepartmentChange(url);
}}
selectedBrands={selectedBrands}
onBrandsChange={setSelectedBrands}
filters={filters}
onFiltersChange={setFilters}
onApplyFilters={handleApplyFilters}
brands={availableBrands}
/>
</div>
{/* Overlay para mobile quando filtros estão abertos */}
{isFiltersOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 xl:hidden"
onClick={() => setIsFiltersOpen(false)}
/>
)}
{/* FilterSidebar - Mobile/Tablet (drawer) */}
<div
className={`fixed xl:hidden inset-y-0 left-0 z-50 w-full max-w-sm bg-white border-r border-slate-200 flex flex-col overflow-hidden transform transition-transform duration-300 ease-out ${
isFiltersOpen ? "translate-x-0" : "-translate-x-full"
}`}
>
<div className="p-safe-top p-4 border-b border-slate-200 flex items-center justify-between bg-[#002147] text-white flex-shrink-0">
<h3 className="text-lg font-black">Filtros</h3>
<button
onClick={() => setIsFiltersOpen(false)}
className="p-2 text-white/70 hover:text-white hover:bg-white/10 rounded-lg transition-colors touch-manipulation"
title="Fechar filtros"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div className="flex-1 overflow-y-auto p-safe-bottom">
<FilterSidebar
selectedDepartment={selectedDepartment}
onDepartmentChange={(url) => {
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}
/>
</div>
</div>
<main className="flex-1 flex flex-col overflow-hidden h-full">
{!showResults ? (
<div className="flex-1 p-4 lg:p-10 overflow-auto custom-scrollbar h-full">
<div className="max-w-[1400px] mx-auto py-4 lg:py-8">
{/* Barra superior com botão de filtros e input de busca - Mobile/Tablet */}
<div className="lg:hidden mb-6 space-y-3">
<div className="flex items-center gap-3">
<button
onClick={() => setIsFiltersOpen(true)}
className="p-3 bg-white rounded-xl shadow-sm border border-slate-200 text-slate-600 hover:bg-slate-50 transition-all touch-manipulation flex items-center justify-center"
title="Filtros"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
/>
</svg>
</button>
<div className="flex-1">
<SearchInput
value={searchTerm}
onChange={setSearchTerm}
onSearch={handleSearch}
loading={loading}
placeholder="Ex: Cimento, Tijolo, Furadeira..."
/>
</div>
</div>
</div>
{/* Desktop: Título e input centralizado */}
<div className="hidden lg:block text-center mb-10">
<h2 className="text-3xl font-black text-[#002147] mb-2 tracking-tight">
O que vamos vender hoje?
</h2>
<p className="text-slate-500 text-sm font-medium mb-6">
Pesquise por nome, código ou categoria do produto.
</p>
<div className="max-w-2xl mx-auto">
<SearchInput
value={searchTerm}
onChange={setSearchTerm}
onSearch={handleSearch}
loading={loading}
placeholder="Ex: Cimento, Tijolo, Furadeira..."
/>
</div>
</div>
<div className="mb-4 ml-1 flex items-center justify-between">
<h3 className="text-base font-black text-[#002147] uppercase tracking-tight">
Categorias em Destaque
</h3>
<span className="text-[10px] font-black text-orange-500 uppercase tracking-widest bg-orange-50 px-3 py-1 rounded-full border border-orange-100 animate-pulse">
Preços baixos hoje
</span>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 lg:gap-4 mb-8 lg:mb-12">
{categories.map((item, idx) => (
<CategoryCard
key={idx}
label={item.label}
icon={item.icon}
onClick={item.onClick}
/>
))}
</div>
{/* Componente "Baldinho" - Versão Evoluída (Painel de Logística) */}
<Baldinho
selectedDeliveryDate={selectedDeliveryDate}
onDateChange={setSelectedDeliveryDate}
/>
</div>
</div>
) : (
<div className="flex flex-col h-full w-full max-w-7xl mx-auto overflow-hidden flex-1">
{/* Header Fixo */}
<header className="flex-shrink-0 p-4 lg:p-10 pb-4 bg-[#f8fafc] border-b border-slate-200 w-full">
<div className="flex flex-col lg:flex-row justify-between items-start max-w-7xl mx-auto gap-4">
<div className="flex-1 w-full">
{/* Mobile/Tablet: Barra superior com botão de filtros, voltar e input */}
<div className="lg:hidden space-y-3 mb-4">
<div className="flex items-center gap-2">
<button
onClick={() => {
setShowResults(false);
setSearchResults([]);
setError(null);
setSearchTerm("");
setSelectedDepartment("");
}}
className="p-2.5 text-slate-600 hover:bg-slate-100 rounded-lg transition-colors touch-manipulation"
title="Voltar"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
<button
onClick={() => setIsFiltersOpen(true)}
className="p-2.5 bg-white rounded-lg shadow-sm border border-slate-200 text-slate-600 hover:bg-slate-50 transition-all touch-manipulation flex items-center justify-center"
title="Filtros"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
/>
</svg>
</button>
<div className="flex-1">
<SearchInput
value={searchTerm}
onChange={setSearchTerm}
onSearch={handleSearch}
loading={loading}
placeholder="Ex: Cimento, Tijolo, Furadeira..."
/>
</div>
</div>
</div>
{/* Desktop: Layout original */}
<div className="hidden lg:block">
<button
onClick={() => {
setShowResults(false);
setSearchResults([]);
setError(null);
setSearchTerm("");
setSelectedDepartment("");
}}
className="text-slate-400 hover:text-slate-600 flex items-center mb-2 font-bold text-sm"
>
<svg
className="w-4 h-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="M15 19l-7-7 7-7"
/>
</svg>
Voltar para busca
</button>
<h2 className="text-2xl font-black text-[#002147]">
{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"}
</h2>
{searchResults.length > 0 && (
<p className="text-sm text-slate-500 mt-1">
{searchResults.length} produto
{searchResults.length !== 1 ? "s" : ""} encontrado
{searchResults.length !== 1 ? "s" : ""}
</p>
)}
{/* Input de busca para nova pesquisa - Desktop */}
<div className="mt-4 mb-[-50px] w-full">
<SearchInput
value={searchTerm}
onChange={setSearchTerm}
onSearch={handleSearch}
loading={loading}
placeholder="Ex: Cimento, Tijolo, Furadeira..."
/>
</div>
</div>
{/* Mobile/Tablet: Título dos resultados */}
<div className="lg:hidden">
<h2 className="text-lg font-black text-[#002147] mb-1">
{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"}
</h2>
{searchResults.length > 0 && (
<p className="text-xs text-slate-500">
{searchResults.length} produto
{searchResults.length !== 1 ? "s" : ""} encontrado
{searchResults.length !== 1 ? "s" : ""}
</p>
)}
</div>
</div>
{/* Botões de alternância de visualização */}
{searchResults.length > 0 && (
<div className="flex items-center gap-2 bg-white rounded-xl p-1 border border-slate-200 shadow-sm">
<button
onClick={() => setViewMode("grid")}
className={`p-2 rounded-lg transition-all ${
viewMode === "grid"
? "bg-[#002147] text-white shadow-md"
: "text-slate-600 hover:bg-slate-100"
}`}
title="Visualização em grade"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"
/>
</svg>
</button>
<button
onClick={() => setViewMode("list")}
className={`p-2 rounded-lg transition-all ${
viewMode === "list"
? "bg-[#002147] text-white shadow-md"
: "text-slate-600 hover:bg-slate-100"
}`}
title="Visualização em lista"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
</div>
)}
</div>
</header>
{/* Área de Scroll - Apenas produtos */}
<div className="flex-1 overflow-y-auto overflow-x-hidden scrollbar-hide p-4 lg:p-10 pt-4 lg:pt-6 min-h-0">
<div className="max-w-7xl mx-auto w-full lg:min-w-[600px]">
{/* Loading State */}
{loading && (
<LoadingSpinner
message="Carregando produtos..."
subMessage="Aguarde enquanto buscamos os melhores produtos para você"
/>
)}
{/* Error State - apenas erros reais (não "nenhum produto encontrado") */}
{error && !loading && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-2xl">
<p className="text-red-600 text-sm font-medium">{error}</p>
</div>
)}
{/* Empty State - quando não há resultados e não há erro */}
{!loading && searchResults.length === 0 && !error && (
<div className="bg-white rounded-2xl border border-slate-100 overflow-hidden w-full">
<NoData
title={
filters.onPromotion
? "Nenhum produto em promoção encontrado"
: filters.priceDropped
? "Nenhum produto que baixou de preço encontrado"
: filters.opportunities
? "Nenhuma oportunidade encontrada"
: filters.unmissableOffers
? "Nenhuma oferta imperdível encontrada"
: selectedDepartment
? `Nenhum produto encontrado para "${selectedDepartment}"`
: searchTerm
? `Nenhum produto encontrado para "${searchTerm}"`
: "Nenhum produto encontrado"
}
description={
filters.onPromotion
? "Não há produtos em promoção no momento. Tente novamente mais tarde ou explore outras categorias."
: filters.priceDropped
? "Não foram encontrados produtos que baixaram de preço. Tente ajustar os filtros ou verificar em outro período."
: filters.opportunities
? "Não foram encontradas oportunidades no momento. Tente ajustar os filtros ou verificar em outro período."
: filters.unmissableOffers
? "Não foram encontradas ofertas imperdíveis no momento. Tente novamente mais tarde ou explore outras categorias."
: selectedDepartment
? `Não foram encontrados produtos para o departamento "${selectedDepartment}". Tente selecionar outro departamento ou usar termos de busca.`
: "Não foram encontrados produtos com os filtros selecionados. Tente ajustar os filtros, usar outros termos de busca ou selecione um departamento."
}
icon={
filters.onPromotion ||
filters.priceDropped ||
filters.opportunities ||
filters.unmissableOffers
? "file"
: "search"
}
variant="outline"
/>
</div>
)}
{/* Results - apenas quando não está carregando e há resultados */}
{!loading && searchResults.length > 0 && (
<div
className={
viewMode === "grid"
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 lg:gap-6 w-full items-stretch"
: "space-y-4 w-full"
}
>
{searchResults.map((product) => (
<div
key={product.id}
className={
viewMode === "list"
? "bg-white rounded-xl border border-slate-200 p-4 hover:shadow-lg transition-all"
: "h-full"
}
>
{viewMode === "list" ? (
<ProductListItem
product={product}
quantity={productQuantities[product.id] || 1}
onQuantityChange={(newQty) => {
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);
}}
/>
) : (
<ProductCard
product={product}
onAddToCart={onAddToCart}
onViewDetails={(p, qty) => {
setSelectedProduct(p);
setInitialQuantity(qty || 1);
setIsDetailModalOpen(true);
}}
/>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
)}
</main>
<style>{`
@keyframes pulse-slow {
0%, 100% { transform: scale(1); filter: drop-shadow(0 0 0px orange); }
50% { transform: scale(1.05); filter: drop-shadow(0 0 10px rgba(249, 115, 22, 0.3)); }
}
.animate-pulse-slow { animation: pulse-slow 3s infinite ease-in-out; }
/* Esconder scrollbar vertical */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
`}</style>
{/* Modal de Detalhamento do Produto */}
<ProductDetailModal
product={selectedProduct}
isOpen={isDetailModalOpen}
onClose={() => {
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);
}}
/>
</div>
);
};
export default ProductSearchView;