295 lines
11 KiB
TypeScript
295 lines
11 KiB
TypeScript
|
|
import React, { useState } from "react";
|
||
|
|
import { Product } from "../types";
|
||
|
|
import NoImagePlaceholder from "./NoImagePlaceholder";
|
||
|
|
import ImageZoomModal from "./ImageZoomModal";
|
||
|
|
import ProductStoreDeliveryModal from "./ProductStoreDeliveryModal";
|
||
|
|
import { authService } from "../src/services/auth.service";
|
||
|
|
|
||
|
|
interface ProductListItemProps {
|
||
|
|
product: Product;
|
||
|
|
quantity: number;
|
||
|
|
onQuantityChange: (quantity: number) => void;
|
||
|
|
onAddToCart: (
|
||
|
|
product: Product,
|
||
|
|
quantity: number,
|
||
|
|
stockStore?: string,
|
||
|
|
deliveryType?: string
|
||
|
|
) => void;
|
||
|
|
onViewDetails: (product: Product, quantity?: number) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* ProductListItem Component
|
||
|
|
* Componente para exibir um produto no modo lista
|
||
|
|
*/
|
||
|
|
const ProductListItem: React.FC<ProductListItemProps> = ({
|
||
|
|
product,
|
||
|
|
quantity,
|
||
|
|
onQuantityChange,
|
||
|
|
onAddToCart,
|
||
|
|
onViewDetails,
|
||
|
|
}) => {
|
||
|
|
const [imageError, setImageError] = useState(false);
|
||
|
|
const [showImageZoom, setShowImageZoom] = useState(false);
|
||
|
|
const [showStoreDeliveryModal, setShowStoreDeliveryModal] = useState(false);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex gap-6">
|
||
|
|
{/* Imagem do produto */}
|
||
|
|
<div className="flex-shrink-0 w-48 h-48 bg-slate-50 rounded-xl overflow-hidden relative flex items-center justify-center p-4 group/image">
|
||
|
|
{product.image &&
|
||
|
|
product.image !==
|
||
|
|
"https://placehold.co/200x200/f8fafc/949494/png?text=Sem+Imagem" &&
|
||
|
|
!product.image.includes("placeholder") &&
|
||
|
|
!imageError ? (
|
||
|
|
<>
|
||
|
|
<img
|
||
|
|
src={product.image}
|
||
|
|
alt={product.name}
|
||
|
|
className="max-w-full max-h-full object-contain mix-blend-multiply"
|
||
|
|
onError={() => {
|
||
|
|
setImageError(true);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
{/* Badge de Desconto */}
|
||
|
|
{typeof product.discount === "number" && product.discount > 0 && (
|
||
|
|
<div className="absolute top-3 left-3 flex items-center gap-2">
|
||
|
|
<span className="bg-orange-500 text-white px-3 py-1.5 rounded-lg text-xs font-black uppercase tracking-wider shadow-lg">
|
||
|
|
{product.discount.toFixed(2)}%
|
||
|
|
</span>
|
||
|
|
<div className="bg-orange-500 text-white p-2 rounded-full shadow-lg">
|
||
|
|
<svg
|
||
|
|
className="w-4 h-4"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth="2.5"
|
||
|
|
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{/* Botão de Zoom */}
|
||
|
|
<button
|
||
|
|
onClick={() => setShowImageZoom(true)}
|
||
|
|
className="absolute bottom-3 right-3 bg-[#002147]/80 hover:bg-[#002147] text-white p-2.5 rounded-lg transition-all opacity-0 group-hover/image:opacity-100 shadow-lg backdrop-blur-sm"
|
||
|
|
title="Ampliar imagem"
|
||
|
|
>
|
||
|
|
<svg
|
||
|
|
className="w-5 h-5"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth={2.5}
|
||
|
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<NoImagePlaceholder size="md" />
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Informações do produto */}
|
||
|
|
<div className="flex-1 flex flex-col justify-between min-w-0">
|
||
|
|
{/* Parte superior: Título, descrição, informações */}
|
||
|
|
<div>
|
||
|
|
<div className="flex items-start justify-between gap-4 mb-2">
|
||
|
|
<h3 className="text-xl font-extrabold text-[#002147] leading-tight flex-1">
|
||
|
|
{product.name}
|
||
|
|
</h3>
|
||
|
|
<button
|
||
|
|
onClick={() => onViewDetails(product)}
|
||
|
|
className="flex-shrink-0 bg-[#002147] text-white px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wide hover:bg-[#003366] transition-all shadow-md"
|
||
|
|
>
|
||
|
|
Ver mais
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Descrição */}
|
||
|
|
{product.description && (
|
||
|
|
<p className="text-sm text-slate-600 mb-3 line-clamp-2">
|
||
|
|
{product.description}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Informações: Marca - Código - EAN - Modelo */}
|
||
|
|
{(() => {
|
||
|
|
const parts: string[] = [];
|
||
|
|
if (
|
||
|
|
product.mark &&
|
||
|
|
product.mark !== "0" &&
|
||
|
|
product.mark !== "Sem marca"
|
||
|
|
) {
|
||
|
|
parts.push(product.mark);
|
||
|
|
}
|
||
|
|
if (product.code && product.code !== "0") {
|
||
|
|
parts.push(product.code);
|
||
|
|
}
|
||
|
|
if (product.ean && product.ean !== "0") {
|
||
|
|
parts.push(product.ean);
|
||
|
|
}
|
||
|
|
if (product.model && product.model !== "0") {
|
||
|
|
parts.push(product.model);
|
||
|
|
}
|
||
|
|
return parts.length > 0 ? (
|
||
|
|
<div className="mb-3">
|
||
|
|
<p className="text-xs text-slate-500 font-medium leading-relaxed">
|
||
|
|
{parts.join(" - ")}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
) : null;
|
||
|
|
})()}
|
||
|
|
|
||
|
|
{/* Preços */}
|
||
|
|
<div className="mb-4">
|
||
|
|
{product.originalPrice && product.originalPrice > product.price && (
|
||
|
|
<p className="text-sm text-slate-400 line-through mb-1">
|
||
|
|
de R${" "}
|
||
|
|
{product.originalPrice.toLocaleString("pt-BR", {
|
||
|
|
minimumFractionDigits: 2,
|
||
|
|
maximumFractionDigits: 2,
|
||
|
|
})}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
<div className="flex items-center gap-3 mb-2">
|
||
|
|
<p className="text-3xl font-black text-orange-600">
|
||
|
|
por R${" "}
|
||
|
|
{product.price.toLocaleString("pt-BR", {
|
||
|
|
minimumFractionDigits: 2,
|
||
|
|
maximumFractionDigits: 2,
|
||
|
|
})}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
{/* Parcelamento */}
|
||
|
|
<p className="text-sm text-slate-600">
|
||
|
|
ou em 10x de R${" "}
|
||
|
|
{(product.price / 10).toLocaleString("pt-BR", {
|
||
|
|
minimumFractionDigits: 2,
|
||
|
|
maximumFractionDigits: 2,
|
||
|
|
})}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Informações de Estoque */}
|
||
|
|
<div className="mb-4 space-y-2 border-t border-slate-100 pt-4">
|
||
|
|
<div className="flex justify-between items-center">
|
||
|
|
<span className="text-xs text-slate-600 font-medium">
|
||
|
|
Estoque loja
|
||
|
|
</span>
|
||
|
|
<span className="text-xs font-bold text-slate-800">
|
||
|
|
{product.stockLocal.toFixed(2) || 0} UN
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between items-center">
|
||
|
|
<span className="text-xs text-slate-600 font-medium">
|
||
|
|
Estoque disponível
|
||
|
|
</span>
|
||
|
|
<span className="text-xs font-bold text-slate-800">
|
||
|
|
{product.stockAvailable?.toFixed(2) ||
|
||
|
|
product.stockLocal.toFixed(2) ||
|
||
|
|
0}{" "}
|
||
|
|
UN
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between items-center">
|
||
|
|
<span className="text-xs text-slate-600 font-medium">
|
||
|
|
Estoque geral
|
||
|
|
</span>
|
||
|
|
<span className="text-xs font-bold text-slate-800">
|
||
|
|
{product.stockGeneral.toFixed(2) || 0} UN
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Parte inferior: Seletor de quantidade e botão adicionar */}
|
||
|
|
<div className="pt-4 border-t border-slate-100 flex items-center gap-3">
|
||
|
|
{/* Seletor de Quantidade */}
|
||
|
|
<div className="flex items-center border border-slate-300 rounded-lg overflow-hidden">
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
const newQty = Math.max(1, quantity - 1);
|
||
|
|
onQuantityChange(newQty);
|
||
|
|
}}
|
||
|
|
className="bg-[#002147] text-white px-4 py-2 hover:bg-[#003366] transition-colors font-bold"
|
||
|
|
disabled={quantity <= 1}
|
||
|
|
>
|
||
|
|
-
|
||
|
|
</button>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
min="1"
|
||
|
|
value={quantity.toFixed(2)}
|
||
|
|
onChange={(e) => {
|
||
|
|
const val = parseFloat(e.target.value) || 1;
|
||
|
|
const newQty = Math.max(1, val);
|
||
|
|
onQuantityChange(newQty);
|
||
|
|
}}
|
||
|
|
className="w-20 text-center text-sm font-bold border-0 focus:outline-none focus:ring-0"
|
||
|
|
/>
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
onQuantityChange(quantity + 1);
|
||
|
|
}}
|
||
|
|
className="bg-[#002147] text-white px-4 py-2 hover:bg-[#003366] transition-colors font-bold"
|
||
|
|
>
|
||
|
|
+
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Botão Adicionar */}
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
// Se tiver estoque na loja, adicionar direto com "Retira Imediata"
|
||
|
|
if (product.stockLocal && product.stockLocal > 0) {
|
||
|
|
const currentStore = authService.getStore() || "";
|
||
|
|
onAddToCart(product, quantity, currentStore, "RI"); // RI = Retira Imediata
|
||
|
|
} else {
|
||
|
|
// Caso contrário, abrir modal para selecionar loja e tipo de entrega
|
||
|
|
setShowStoreDeliveryModal(true);
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
className="flex-1 bg-orange-500 text-white py-2.5 rounded-lg font-bold uppercase text-xs tracking-wide hover:bg-orange-600 transition-all shadow-md"
|
||
|
|
>
|
||
|
|
Adicionar
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Modal de Zoom da Imagem */}
|
||
|
|
<ImageZoomModal
|
||
|
|
isOpen={showImageZoom}
|
||
|
|
onClose={() => setShowImageZoom(false)}
|
||
|
|
imageUrl={product.image || ""}
|
||
|
|
productName={product.name}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Modal de Seleção de Loja e Tipo de Entrega */}
|
||
|
|
<ProductStoreDeliveryModal
|
||
|
|
isOpen={showStoreDeliveryModal}
|
||
|
|
onClose={() => setShowStoreDeliveryModal(false)}
|
||
|
|
onConfirm={(stockStore, deliveryType) => {
|
||
|
|
// Adicionar ao carrinho com as informações selecionadas
|
||
|
|
onAddToCart(product, quantity, stockStore, deliveryType);
|
||
|
|
setShowStoreDeliveryModal(false);
|
||
|
|
}}
|
||
|
|
product={product}
|
||
|
|
quantity={quantity}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default ProductListItem;
|