470 lines
17 KiB
TypeScript
470 lines
17 KiB
TypeScript
|
|
import React, { useState, useEffect } from "react";
|
||
|
|
import { OrderItem } from "../types";
|
||
|
|
import ConfirmDialog from "./ConfirmDialog";
|
||
|
|
|
||
|
|
interface CartDrawerProps {
|
||
|
|
isOpen: boolean;
|
||
|
|
onClose: () => void;
|
||
|
|
items: OrderItem[];
|
||
|
|
onUpdateQuantity: (id: string, delta: number) => void;
|
||
|
|
onRemove: (id: string) => void;
|
||
|
|
onCheckout: () => void;
|
||
|
|
onNewOrder?: () => void;
|
||
|
|
onNavigate?: (path: string) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
const CartDrawer: React.FC<CartDrawerProps> = ({
|
||
|
|
isOpen,
|
||
|
|
onClose,
|
||
|
|
items,
|
||
|
|
onUpdateQuantity,
|
||
|
|
onRemove,
|
||
|
|
onCheckout,
|
||
|
|
onNewOrder,
|
||
|
|
onNavigate,
|
||
|
|
}) => {
|
||
|
|
const [isAnimating, setIsAnimating] = useState(false);
|
||
|
|
const [shouldRender, setShouldRender] = useState(false);
|
||
|
|
const [confirmDialog, setConfirmDialog] = useState<{
|
||
|
|
isOpen: boolean;
|
||
|
|
itemId: string;
|
||
|
|
productName: string;
|
||
|
|
}>({
|
||
|
|
isOpen: false,
|
||
|
|
itemId: "",
|
||
|
|
productName: "",
|
||
|
|
});
|
||
|
|
const [showNewOrderDialog, setShowNewOrderDialog] = useState(false);
|
||
|
|
const [showContinueOrNewDialog, setShowContinueOrNewDialog] = useState(false);
|
||
|
|
const total = items.reduce(
|
||
|
|
(acc, item) => acc + item.price * item.quantity,
|
||
|
|
0
|
||
|
|
);
|
||
|
|
|
||
|
|
// Controlar renderização e animação
|
||
|
|
useEffect(() => {
|
||
|
|
if (isOpen) {
|
||
|
|
setShouldRender(true);
|
||
|
|
// Pequeno delay para garantir que o DOM está pronto antes de iniciar a animação
|
||
|
|
setTimeout(() => setIsAnimating(true), 10);
|
||
|
|
} else {
|
||
|
|
// Iniciar animação de saída
|
||
|
|
setIsAnimating(false);
|
||
|
|
// Remover do DOM após a animação terminar
|
||
|
|
const timer = setTimeout(() => setShouldRender(false), 400); // Duração da animação
|
||
|
|
return () => clearTimeout(timer);
|
||
|
|
}
|
||
|
|
}, [isOpen]);
|
||
|
|
|
||
|
|
// Não renderizar se não deve estar visível
|
||
|
|
if (!shouldRender) return null;
|
||
|
|
|
||
|
|
const handleClose = () => {
|
||
|
|
// Chamar onClose imediatamente para atualizar o estado
|
||
|
|
// O useEffect vai cuidar da animação de saída
|
||
|
|
onClose();
|
||
|
|
};
|
||
|
|
|
||
|
|
// Verificar se há carrinho (itens no carrinho)
|
||
|
|
const hasCart = () => {
|
||
|
|
return items.length > 0 || localStorage.getItem("cart");
|
||
|
|
};
|
||
|
|
|
||
|
|
// Verificar se há dados do pedido atual
|
||
|
|
const hasOrderData = () => {
|
||
|
|
const hasCartItems = items.length > 0;
|
||
|
|
const hasCustomer = localStorage.getItem("customer");
|
||
|
|
const hasAddress = localStorage.getItem("address");
|
||
|
|
const hasPaymentPlan = localStorage.getItem("paymentPlan");
|
||
|
|
const hasBilling = localStorage.getItem("billing");
|
||
|
|
const hasDataDelivery = localStorage.getItem("dataDelivery");
|
||
|
|
const hasInvoiceStore = localStorage.getItem("invoiceStore");
|
||
|
|
const hasPartner = localStorage.getItem("partner");
|
||
|
|
|
||
|
|
return (
|
||
|
|
hasCartItems ||
|
||
|
|
hasCustomer ||
|
||
|
|
hasAddress ||
|
||
|
|
hasPaymentPlan ||
|
||
|
|
hasBilling ||
|
||
|
|
hasDataDelivery ||
|
||
|
|
hasInvoiceStore ||
|
||
|
|
hasPartner
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleNewOrderClick = () => {
|
||
|
|
// Se houver carrinho, perguntar se quer continuar ou iniciar novo
|
||
|
|
if (hasCart()) {
|
||
|
|
setShowContinueOrNewDialog(true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Se não houver carrinho mas houver outros dados, mostrar confirmação normal
|
||
|
|
if (hasOrderData()) {
|
||
|
|
setShowNewOrderDialog(true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Se não houver nenhum dado, limpar direto sem confirmação
|
||
|
|
if (onNewOrder) {
|
||
|
|
onNewOrder();
|
||
|
|
handleClose();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleContinueOrder = () => {
|
||
|
|
// Fechar o dialog e navegar para /sales/home
|
||
|
|
setShowContinueOrNewDialog(false);
|
||
|
|
handleClose(); // Fechar o drawer também
|
||
|
|
window.location.href = "/#/sales/home";
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleStartNewOrder = () => {
|
||
|
|
// Fechar o dialog de continuar/iniciar e abrir o de confirmação
|
||
|
|
setShowContinueOrNewDialog(false);
|
||
|
|
setShowNewOrderDialog(true);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleConfirmNewOrder = () => {
|
||
|
|
if (onNewOrder) {
|
||
|
|
onNewOrder();
|
||
|
|
setShowNewOrderDialog(false);
|
||
|
|
handleClose();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="fixed inset-0 z-[100] flex justify-end">
|
||
|
|
{/* Overlay */}
|
||
|
|
<div
|
||
|
|
className={`absolute inset-0 bg-[#001f3f]/40 backdrop-blur-[2px] transition-opacity duration-400 ${
|
||
|
|
isAnimating ? "opacity-100" : "opacity-0"
|
||
|
|
}`}
|
||
|
|
onClick={handleClose}
|
||
|
|
></div>
|
||
|
|
|
||
|
|
{/* Panel - Fullscreen em mobile, sidebar em desktop */}
|
||
|
|
<div
|
||
|
|
className={`relative w-full lg:max-w-[440px] bg-white h-full shadow-2xl flex flex-col transition-transform duration-400 ease-out-quart ${
|
||
|
|
isAnimating ? "translate-x-0" : "translate-x-full"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<div className="p-4 lg:p-6 bg-[#002147] text-white flex justify-between items-center relative overflow-hidden safe-area-top">
|
||
|
|
<div className="relative z-10">
|
||
|
|
<span className="text-orange-400 text-[9px] font-black uppercase tracking-[0.2em] block mb-0.5">
|
||
|
|
Seu Carrinho
|
||
|
|
</span>
|
||
|
|
<h3 className="text-lg lg:text-xl font-black">
|
||
|
|
{items.length} {items.length === 1 ? "Produto" : "Produtos"}
|
||
|
|
</h3>
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
onClick={handleClose}
|
||
|
|
className="relative z-10 w-10 h-10 lg:w-12 lg:h-12 flex items-center justify-center rounded-2xl bg-white/10 hover:bg-white/20 transition-colors touch-manipulation"
|
||
|
|
>
|
||
|
|
<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 className="absolute right-[-20%] top-[-20%] w-64 h-64 bg-blue-400/10 rounded-full blur-3xl"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex-1 overflow-auto p-4 lg:p-5 space-y-4 lg:space-y-5 custom-scrollbar">
|
||
|
|
{items.length === 0 ? (
|
||
|
|
<div className="h-full flex flex-col items-center justify-center text-center opacity-40">
|
||
|
|
<div className="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mb-3">
|
||
|
|
<svg
|
||
|
|
className="w-8 h-8"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth="2"
|
||
|
|
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
<p className="font-bold text-sm">Seu carrinho está vazio</p>
|
||
|
|
<p className="text-xs">Adicione produtos para começar.</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
items.map((item) => (
|
||
|
|
<div key={item.id} className="flex space-x-4 group">
|
||
|
|
<div className="w-20 h-20 bg-slate-50 rounded-xl p-3 mt-1.5 flex items-center justify-center shrink-0 border border-slate-100">
|
||
|
|
{item.image && item.image.trim() !== "" ? (
|
||
|
|
<img
|
||
|
|
src={item.image}
|
||
|
|
alt={item.name}
|
||
|
|
className="max-h-full mix-blend-multiply"
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<svg
|
||
|
|
className="w-8 h-8 text-slate-300"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth="2"
|
||
|
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<h5 className="text-xs font-bold text-[#002147] line-clamp-1 mb-1 group-hover:text-orange-600 transition-colors">
|
||
|
|
{item.name}
|
||
|
|
</h5>
|
||
|
|
<span className="text-lg font-black block mb-0.5">
|
||
|
|
R$ {item.price.toFixed(2)}
|
||
|
|
</span>
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="flex bg-slate-100 rounded-lg p-0.5 items-center border border-slate-200">
|
||
|
|
<button
|
||
|
|
onClick={(e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
e.stopPropagation();
|
||
|
|
console.log(
|
||
|
|
"🛒 [CartDrawer] Botão - clicado:",
|
||
|
|
item.id
|
||
|
|
);
|
||
|
|
onUpdateQuantity(item.id, -1).catch((err) => {
|
||
|
|
console.error(
|
||
|
|
"🛒 [CartDrawer] Erro ao diminuir quantidade:",
|
||
|
|
err
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}}
|
||
|
|
className="w-7 h-7 flex items-center justify-center font-bold text-slate-400 hover:text-[#002147] transition-colors text-sm touch-manipulation"
|
||
|
|
>
|
||
|
|
-
|
||
|
|
</button>
|
||
|
|
<span className="px-3 text-xs font-black min-w-[32px] text-center">
|
||
|
|
{item.quantity}
|
||
|
|
</span>
|
||
|
|
<button
|
||
|
|
onClick={(e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
e.stopPropagation();
|
||
|
|
console.log(
|
||
|
|
"🛒 [CartDrawer] Botão + clicado:",
|
||
|
|
item.id
|
||
|
|
);
|
||
|
|
onUpdateQuantity(item.id, 1).catch((err) => {
|
||
|
|
console.error(
|
||
|
|
"🛒 [CartDrawer] Erro ao aumentar quantidade:",
|
||
|
|
err
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}}
|
||
|
|
className="w-7 h-7 flex items-center justify-center font-bold text-slate-400 hover:text-[#002147] transition-colors text-sm touch-manipulation"
|
||
|
|
>
|
||
|
|
+
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
setConfirmDialog({
|
||
|
|
isOpen: true,
|
||
|
|
itemId: item.id,
|
||
|
|
productName: item.name,
|
||
|
|
});
|
||
|
|
}}
|
||
|
|
className="text-slate-300 hover:text-red-500 transition-colors p-1.5"
|
||
|
|
>
|
||
|
|
<svg
|
||
|
|
className="w-4 h-4"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth="2.5"
|
||
|
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{items.length > 0 && (
|
||
|
|
<div className="p-4 lg:p-6 bg-slate-50 border-t border-slate-200 space-y-4 safe-area-bottom">
|
||
|
|
<div className="flex justify-between items-end">
|
||
|
|
<div>
|
||
|
|
<span className="text-slate-400 font-bold uppercase text-[9px] tracking-widest block mb-0.5">
|
||
|
|
Total do Pedido
|
||
|
|
</span>
|
||
|
|
<span className="text-3xl font-black text-[#002147]">
|
||
|
|
R$ {total.toFixed(2)}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-1 gap-2">
|
||
|
|
<button
|
||
|
|
onClick={onCheckout}
|
||
|
|
className="w-full py-3.5 bg-orange-500 text-white font-black uppercase text-xs tracking-[0.2em] rounded-xl shadow-lg shadow-orange-500/20 hover:bg-orange-600 transition-all flex items-center justify-center active:scale-95"
|
||
|
|
>
|
||
|
|
Finalizar Venda
|
||
|
|
<svg
|
||
|
|
className="w-4 h-4 ml-2"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth="2.5"
|
||
|
|
d="M14 5l7 7m0 0l-7 7m7-7H3"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
{onNewOrder && (
|
||
|
|
<button
|
||
|
|
onClick={handleNewOrderClick}
|
||
|
|
className="w-full py-3 bg-slate-200 text-slate-700 font-black uppercase text-xs tracking-[0.1em] rounded-xl hover:bg-slate-300 transition-all flex items-center justify-center active:scale-95"
|
||
|
|
>
|
||
|
|
<svg
|
||
|
|
className="w-4 h-4 mr-2"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth="2.5"
|
||
|
|
d="M12 4v16m8-8H4"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
Novo Pedido
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
<button
|
||
|
|
onClick={handleClose}
|
||
|
|
className="w-full py-3 text-slate-400 font-bold uppercase text-[9px] tracking-widest hover:text-[#002147] transition-colors"
|
||
|
|
>
|
||
|
|
Continuar Comprando
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<style>{`
|
||
|
|
/* Função de easing elegante para animações suaves */
|
||
|
|
.ease-out-quart {
|
||
|
|
transition-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
|
||
|
|
}
|
||
|
|
.duration-400 {
|
||
|
|
transition-duration: 400ms;
|
||
|
|
}
|
||
|
|
`}</style>
|
||
|
|
|
||
|
|
{/* Dialog de Confirmação - Remover Item */}
|
||
|
|
<ConfirmDialog
|
||
|
|
isOpen={confirmDialog.isOpen}
|
||
|
|
onClose={() =>
|
||
|
|
setConfirmDialog({ isOpen: false, itemId: "", productName: "" })
|
||
|
|
}
|
||
|
|
onConfirm={() => {
|
||
|
|
onRemove(confirmDialog.itemId);
|
||
|
|
setConfirmDialog({ isOpen: false, itemId: "", productName: "" });
|
||
|
|
}}
|
||
|
|
type="delete"
|
||
|
|
message={
|
||
|
|
<>
|
||
|
|
Deseja remover o produto{" "}
|
||
|
|
<span className="font-black text-[#002147]">
|
||
|
|
"{confirmDialog.productName}"
|
||
|
|
</span>{" "}
|
||
|
|
do carrinho?
|
||
|
|
<br />
|
||
|
|
<span className="text-xs text-slate-400 mt-2 block">
|
||
|
|
Esta ação não pode ser desfeita.
|
||
|
|
</span>
|
||
|
|
</>
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Dialog - Continuar ou Iniciar Novo Pedido */}
|
||
|
|
<ConfirmDialog
|
||
|
|
isOpen={showContinueOrNewDialog}
|
||
|
|
onClose={handleContinueOrder}
|
||
|
|
onConfirm={handleStartNewOrder}
|
||
|
|
type="info"
|
||
|
|
title="Carrinho Existente"
|
||
|
|
message={
|
||
|
|
<>
|
||
|
|
Você já possui um carrinho com itens.
|
||
|
|
<br />
|
||
|
|
<br />
|
||
|
|
Deseja iniciar um novo pedido e limpar todos os dados do pedido
|
||
|
|
atual?
|
||
|
|
</>
|
||
|
|
}
|
||
|
|
confirmText="Iniciar Novo Pedido"
|
||
|
|
cancelText="Cancelar"
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Dialog de Confirmação - Novo Pedido */}
|
||
|
|
<ConfirmDialog
|
||
|
|
isOpen={showNewOrderDialog}
|
||
|
|
onClose={() => setShowNewOrderDialog(false)}
|
||
|
|
onConfirm={handleConfirmNewOrder}
|
||
|
|
type="warning"
|
||
|
|
title="Novo Pedido"
|
||
|
|
message={
|
||
|
|
<>
|
||
|
|
Deseja iniciar um novo pedido?
|
||
|
|
<br />
|
||
|
|
<br />
|
||
|
|
<span className="text-sm font-bold text-slate-700">
|
||
|
|
Todos os dados do pedido atual serão perdidos:
|
||
|
|
</span>
|
||
|
|
<ul className="text-xs text-slate-600 mt-2 space-y-1 list-disc list-inside">
|
||
|
|
<li>Itens do carrinho</li>
|
||
|
|
<li>Dados do cliente</li>
|
||
|
|
<li>Endereço de entrega</li>
|
||
|
|
<li>Plano de pagamento</li>
|
||
|
|
<li>Dados financeiros</li>
|
||
|
|
<li>Informações de entrega</li>
|
||
|
|
</ul>
|
||
|
|
<br />
|
||
|
|
<span className="text-xs text-slate-400 block">
|
||
|
|
Esta ação não pode ser desfeita.
|
||
|
|
</span>
|
||
|
|
</>
|
||
|
|
}
|
||
|
|
confirmText="Sim, Iniciar Novo Pedido"
|
||
|
|
cancelText="Cancelar"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default CartDrawer;
|