559 lines
36 KiB
TypeScript
559 lines
36 KiB
TypeScript
|
|
|
||
|
|
import React, { useState } from 'react';
|
||
|
|
import { useCRM } from '../context/CRMContext';
|
||
|
|
import { Search, ShoppingCart, Trash2, Plus, Minus, CreditCard, ChevronRight, Calculator, Package, AlertTriangle, Download, History, Store, CheckSquare, Square, UserPlus, Pencil, X, Banknote, CreditCard as CardIcon } from 'lucide-react';
|
||
|
|
import { InventoryItem, Sale, SalesChannel, Customer, PaymentMethod } from '../types';
|
||
|
|
import clsx from 'clsx';
|
||
|
|
|
||
|
|
const Sales: React.FC = () => {
|
||
|
|
const { inventory, customers, registerSale, sales, importSales, updateSale, loading, addCustomer, updateProduct, isAdmin } = useCRM();
|
||
|
|
|
||
|
|
// State
|
||
|
|
const [viewMode, setViewMode] = useState<'list' | 'pos'>('list'); // 'list' = Pedidos, 'pos' = Frente de Caixa
|
||
|
|
|
||
|
|
// POS State
|
||
|
|
const [searchTerm, setSearchTerm] = useState('');
|
||
|
|
const [cart, setCart] = useState<{ item: InventoryItem; quantity: number; price: number }[]>([]);
|
||
|
|
const [selectedCustomer, setSelectedCustomer] = useState<string>('');
|
||
|
|
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>('Cash'); // New state
|
||
|
|
const [isFinalizing, setIsFinalizing] = useState(false);
|
||
|
|
|
||
|
|
// Import Modal State
|
||
|
|
const [showImportModal, setShowImportModal] = useState(false);
|
||
|
|
const [importChannel, setImportChannel] = useState<SalesChannel>('Mercado Livre');
|
||
|
|
|
||
|
|
// Quick Customer Modal
|
||
|
|
const [showCustomerModal, setShowCustomerModal] = useState(false);
|
||
|
|
const [newCustomer, setNewCustomer] = useState({ name: '', email: '', phone: '' });
|
||
|
|
|
||
|
|
// Quick Edit Product Modal
|
||
|
|
const [editingProduct, setEditingProduct] = useState<InventoryItem | null>(null);
|
||
|
|
const [editForm, setEditForm] = useState({ price: 0, stock: 0 });
|
||
|
|
|
||
|
|
// Filter Inventory
|
||
|
|
const filteredInventory = inventory.filter(i =>
|
||
|
|
i.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
|
|
i.sku.toLowerCase().includes(searchTerm.toLowerCase())
|
||
|
|
);
|
||
|
|
|
||
|
|
// Cart Actions
|
||
|
|
const addToCart = (item: InventoryItem) => {
|
||
|
|
const existing = cart.find(c => c.item.id === item.id);
|
||
|
|
if (existing) {
|
||
|
|
if (existing.quantity >= item.quantity) return; // Max stock reached
|
||
|
|
setCart(cart.map(c => c.item.id === item.id ? { ...c, quantity: c.quantity + 1 } : c));
|
||
|
|
} else {
|
||
|
|
if (item.quantity <= 0) return;
|
||
|
|
setCart([...cart, { item, quantity: 1, price: Number(item.marketPriceBRL) || 0 }]);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const updateQuantity = (id: string, delta: number) => {
|
||
|
|
setCart(cart.map(c => {
|
||
|
|
if (c.item.id === id) {
|
||
|
|
const newQty = c.quantity + delta;
|
||
|
|
if (newQty <= 0) return c; // Don't remove here, need explicit remove
|
||
|
|
if (newQty > c.item.quantity) return c;
|
||
|
|
return { ...c, quantity: newQty };
|
||
|
|
}
|
||
|
|
return c;
|
||
|
|
}));
|
||
|
|
};
|
||
|
|
|
||
|
|
const removeFromCart = (id: string) => {
|
||
|
|
setCart(cart.filter(c => c.item.id !== id));
|
||
|
|
};
|
||
|
|
|
||
|
|
const updatePrice = (id: string, newPrice: number) => {
|
||
|
|
setCart(cart.map(c => c.item.id === id ? { ...c, price: newPrice } : c));
|
||
|
|
};
|
||
|
|
|
||
|
|
// Totals
|
||
|
|
const totalAmount = cart.reduce((acc, c) => acc + (c.price * c.quantity), 0);
|
||
|
|
const totalItems = cart.reduce((acc, c) => acc + c.quantity, 0);
|
||
|
|
|
||
|
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||
|
|
const [lastSaleTotal, setLastSaleTotal] = useState(0);
|
||
|
|
|
||
|
|
const handleFinalize = async () => {
|
||
|
|
if (cart.length === 0) return;
|
||
|
|
setIsFinalizing(true);
|
||
|
|
|
||
|
|
const saleTotal = cart.reduce((acc, c) => acc + (c.price * c.quantity), 0);
|
||
|
|
setLastSaleTotal(saleTotal);
|
||
|
|
|
||
|
|
await registerSale(
|
||
|
|
cart.map(c => ({ id: c.item.id, quantity: c.quantity, salePrice: c.price })),
|
||
|
|
selectedCustomer || undefined,
|
||
|
|
paymentMethod // Pass new arg
|
||
|
|
);
|
||
|
|
|
||
|
|
setCart([]);
|
||
|
|
setSelectedCustomer('');
|
||
|
|
setPaymentMethod('Cash'); // Reset
|
||
|
|
setIsFinalizing(false);
|
||
|
|
setShowSuccessModal(true);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleNewSale = () => {
|
||
|
|
setShowSuccessModal(false);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handlePrintReceipt = () => {
|
||
|
|
alert("Impressão de Recibo iniciada... (Mock)");
|
||
|
|
// window.print();
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSendEmail = () => {
|
||
|
|
const email = customers.find(c => c.id === selectedCustomer)?.email || "cliente@exemplo.com";
|
||
|
|
alert(`Comprovante enviado para ${email} (Mock)`);
|
||
|
|
};
|
||
|
|
|
||
|
|
// --- IMPORT HANDLERS ---
|
||
|
|
const handleImport = async () => {
|
||
|
|
await importSales(importChannel);
|
||
|
|
setShowImportModal(false);
|
||
|
|
};
|
||
|
|
|
||
|
|
// --- QUICK ACTIONS ---
|
||
|
|
const handleSaveCustomer = async () => {
|
||
|
|
if (!newCustomer.name) return alert("Nome é obrigatório");
|
||
|
|
try {
|
||
|
|
const created = await addCustomer({ ...newCustomer, status: 'Active', city: 'Foz do Iguaçu' } as any);
|
||
|
|
if (created) {
|
||
|
|
setNewCustomer({ name: '', email: '', phone: '' });
|
||
|
|
setShowCustomerModal(false);
|
||
|
|
setSelectedCustomer(created.id); // Auto-select
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error(error);
|
||
|
|
alert("Erro ao cadastrar cliente. Verifique o console.");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSaveProductEdit = async () => {
|
||
|
|
if (!editingProduct) return;
|
||
|
|
await updateProduct(editingProduct.id, {
|
||
|
|
marketPriceBRL: editForm.price,
|
||
|
|
quantity: editForm.stock
|
||
|
|
});
|
||
|
|
setEditingProduct(null);
|
||
|
|
};
|
||
|
|
|
||
|
|
// --- UI RENDERERS ---
|
||
|
|
|
||
|
|
// 1. SALES LIST VIEW
|
||
|
|
const renderSalesList = () => (
|
||
|
|
<div className="space-y-6">
|
||
|
|
<div className="flex justify-between items-center bg-white/5 p-6 rounded-[32px] border border-white/5">
|
||
|
|
<div>
|
||
|
|
<h2 className="text-xl font-bold text-white tracking-tight">Histórico de Pedidos</h2>
|
||
|
|
<p className="text-sm text-slate-400">Gerencie vendas locais e importadas.</p>
|
||
|
|
</div>
|
||
|
|
<div className="flex gap-3">
|
||
|
|
<button
|
||
|
|
onClick={() => setShowImportModal(true)}
|
||
|
|
className="bg-slate-800 hover:bg-slate-700 text-white px-5 py-3 rounded-2xl font-bold text-sm transition-all flex items-center gap-2 border border-white/5"
|
||
|
|
>
|
||
|
|
<Download size={18} /> Importar Pedidos
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={() => setViewMode('pos')}
|
||
|
|
className="bg-indigo-600 hover:bg-indigo-500 text-white px-5 py-3 rounded-2xl font-bold text-sm shadow-lg shadow-indigo-900/20 transition-all flex items-center gap-2"
|
||
|
|
>
|
||
|
|
<Plus size={18} /> Nova Venda (POS)
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="glass-card rounded-3xl border border-white/5 overflow-hidden">
|
||
|
|
<table className="w-full text-left">
|
||
|
|
<thead className="bg-black/20 text-[10px] font-bold text-slate-500 uppercase tracking-widest border-b border-white/5">
|
||
|
|
<tr>
|
||
|
|
<th className="px-6 py-4">Data / ID</th>
|
||
|
|
<th className="px-6 py-4">Canal</th>
|
||
|
|
<th className="px-6 py-4">Cliente</th>
|
||
|
|
<th className="px-6 py-4 text-right">Total</th>
|
||
|
|
<th className="px-6 py-4 text-center">Status</th>
|
||
|
|
<th className="px-6 py-4 text-center">Baixar Estoque</th>
|
||
|
|
<th className="px-6 py-4 text-center">Lançar Financeiro</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody className="divide-y divide-white/5">
|
||
|
|
{sales.map(sale => (
|
||
|
|
<tr key={sale.id} className="hover:bg-white/[0.02] transition-colors">
|
||
|
|
<td className="px-6 py-4">
|
||
|
|
<div className="font-mono text-xs text-slate-400">{new Date(sale.date).toLocaleDateString()}</div>
|
||
|
|
<div className="text-[9px] text-slate-600">#{sale.externalId || sale.id.substring(0, 8)}</div>
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4">
|
||
|
|
<div className="flex items-center gap-2 text-sm text-slate-300">
|
||
|
|
{sale.channel === 'Mercado Livre' && <span className="text-yellow-400 font-bold text-xs bg-yellow-400/10 px-2 py-0.5 rounded">MeLi</span>}
|
||
|
|
{sale.channel === 'Shopee' && <span className="text-orange-400 font-bold text-xs bg-orange-400/10 px-2 py-0.5 rounded">Shopee</span>}
|
||
|
|
{sale.channel === 'Amazon' && <span className="text-white font-bold text-xs bg-slate-700 px-2 py-0.5 rounded">Amazon</span>}
|
||
|
|
{sale.channel === 'Local' && <span className="text-emerald-400 font-bold text-xs bg-emerald-400/10 px-2 py-0.5 rounded">Balcão</span>}
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4 text-sm text-slate-300 font-medium">
|
||
|
|
{sale.customerName || 'Cliente Consumidor'}
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4 text-right text-sm font-bold text-slate-200">
|
||
|
|
R$ {sale.total.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4 text-center">
|
||
|
|
<span className={`text-[10px] font-bold uppercase px-2 py-1 rounded-full ${sale.status === 'Completed' ? 'bg-emerald-500/10 text-emerald-400' :
|
||
|
|
sale.status === 'Pending' ? 'bg-amber-500/10 text-amber-400' :
|
||
|
|
'bg-slate-500/10 text-slate-400'
|
||
|
|
}`}>
|
||
|
|
{sale.status}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4 text-center">
|
||
|
|
<button
|
||
|
|
onClick={() => updateSale(sale.id, { isStockLaunched: !sale.isStockLaunched })}
|
||
|
|
disabled={sale.isStockLaunched}
|
||
|
|
className={`p-2 rounded-lg transition-all ${sale.isStockLaunched ? 'text-emerald-500 opacity-50 cursor-not-allowed' : 'text-slate-500 hover:bg-white/10 hover:text-white'}`}
|
||
|
|
>
|
||
|
|
{sale.isStockLaunched ? <CheckSquare size={18} /> : <Square size={18} />}
|
||
|
|
</button>
|
||
|
|
</td>
|
||
|
|
<td className="px-6 py-4 text-center">
|
||
|
|
<button
|
||
|
|
onClick={() => updateSale(sale.id, { isFinancialLaunched: !sale.isFinancialLaunched })}
|
||
|
|
disabled={sale.isFinancialLaunched}
|
||
|
|
className={`p-2 rounded-lg transition-all ${sale.isFinancialLaunched ? 'text-emerald-500 opacity-50 cursor-not-allowed' : 'text-slate-500 hover:bg-white/10 hover:text-white'}`}
|
||
|
|
>
|
||
|
|
{sale.isFinancialLaunched ? <CheckSquare size={18} /> : <Square size={18} />}
|
||
|
|
</button>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
))}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
{sales.length === 0 && (
|
||
|
|
<div className="py-20 text-center opacity-30">
|
||
|
|
<History size={48} className="mx-auto mb-4 text-slate-400" />
|
||
|
|
<p className="text-sm font-bold text-slate-400 uppercase tracking-widest">Nenhuma venda registrada</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* IMPORT MODAL */}
|
||
|
|
{showImportModal && (
|
||
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm">
|
||
|
|
<div className="bg-[#0F1115] border border-white/10 rounded-[32px] w-full max-w-sm shadow-2xl p-8">
|
||
|
|
<h3 className="text-xl font-bold text-white mb-6">Importar Pedidos</h3>
|
||
|
|
|
||
|
|
<div className="space-y-4 mb-8">
|
||
|
|
<div>
|
||
|
|
<label className="text-xs font-bold text-slate-500 uppercase tracking-widest mb-2 block">Marketplace</label>
|
||
|
|
<select
|
||
|
|
className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-sm text-slate-300 outline-none focus:border-indigo-500"
|
||
|
|
value={importChannel}
|
||
|
|
onChange={(e) => setImportChannel(e.target.value as SalesChannel)}
|
||
|
|
>
|
||
|
|
<option value="Mercado Livre">Mercado Livre</option>
|
||
|
|
<option value="Shopee">Shopee</option>
|
||
|
|
<option value="Amazon">Amazon</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center gap-3 p-4 bg-white/5 rounded-xl border border-white/5">
|
||
|
|
<div className="p-2 bg-indigo-500/20 rounded-lg text-indigo-400">
|
||
|
|
<Store size={16} />
|
||
|
|
</div>
|
||
|
|
<div className="text-xs text-slate-400 leading-relaxed">
|
||
|
|
Simularemos a conexão com a API do <strong>{importChannel}</strong> para buscar novos pedidos pendentes.
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex gap-3">
|
||
|
|
<button onClick={() => setShowImportModal(false)} className="flex-1 py-3 text-sm font-bold text-slate-400 hover:text-white transition-colors">Cancelar</button>
|
||
|
|
<button
|
||
|
|
onClick={handleImport}
|
||
|
|
disabled={loading}
|
||
|
|
className="flex-[2] bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl font-bold text-sm shadow-lg shadow-indigo-900/20 transition-all flex items-center justify-center gap-2"
|
||
|
|
>
|
||
|
|
{loading ? (
|
||
|
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||
|
|
) : 'Importar Agora'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="animate-in fade-in duration-500 h-[calc(100vh-140px)]">
|
||
|
|
{viewMode === 'list' ? renderSalesList() : (
|
||
|
|
<div className="h-full flex flex-col">
|
||
|
|
<button onClick={() => setViewMode('list')} className="self-start mb-4 text-xs font-bold text-slate-500 hover:text-white flex items-center gap-1 uppercase tracking-wider">
|
||
|
|
<ChevronRight className="rotate-180" size={14} /> Voltar para Histórico
|
||
|
|
</button>
|
||
|
|
<div className="grid grid-cols-1 xl:grid-cols-12 gap-8 h-full">
|
||
|
|
{/* LEFT: POS / INVENTORY */}
|
||
|
|
<div className="xl:col-span-8 flex flex-col h-full space-y-6">
|
||
|
|
{/* ... Search Bar and Grid kept as children ... */}
|
||
|
|
{/* RE-INSERTING ORIGINAL POS CONTENT WRAPPED IN FRAGMENT OR DIV IF NEEDED */}
|
||
|
|
|
||
|
|
{/* Search Bar */}
|
||
|
|
<div className="bg-white/5 p-6 rounded-[32px] border border-white/5 flex gap-4 items-center shadow-sm">
|
||
|
|
<div className="relative flex-grow">
|
||
|
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500" size={20} />
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={searchTerm}
|
||
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||
|
|
placeholder="Buscar produto por nome ou SKU..."
|
||
|
|
className="w-full bg-slate-900/50 border border-white/10 rounded-2xl py-4 pl-12 pr-4 text-white focus:border-indigo-500 outline-none transition-all placeholder:text-slate-600"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Grid */}
|
||
|
|
<div className="flex-grow overflow-y-auto pr-2 custom-scrollbar">
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
|
|
{filteredInventory.map(item => (
|
||
|
|
<div
|
||
|
|
key={item.id}
|
||
|
|
onClick={() => addToCart(item)}
|
||
|
|
className={clsx(
|
||
|
|
"p-5 rounded-[24px] border transition-all cursor-pointer group relative overflow-hidden",
|
||
|
|
item.quantity === 0 ? "opacity-50 grayscale border-white/5 bg-white/5 cursor-not-allowed" : "bg-white/5 border-white/5 hover:border-indigo-500/50 hover:bg-white/10 active:scale-95"
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
<div className="flex justify-between items-start mb-3">
|
||
|
|
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest bg-black/20 px-2 py-1 rounded-lg">{item.sku}</span>
|
||
|
|
{item.quantity <= 3 && item.quantity > 0 && (
|
||
|
|
<div className="flex items-center gap-1 text-amber-400 text-[10px] font-bold bg-amber-400/10 px-2 py-1 rounded-lg">
|
||
|
|
<AlertTriangle size={10} /> Baixo Estoque
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{isAdmin && (
|
||
|
|
<button
|
||
|
|
onClick={(e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
setEditingProduct(item);
|
||
|
|
setEditForm({ price: item.marketPriceBRL || 0, stock: item.quantity });
|
||
|
|
}}
|
||
|
|
className="text-slate-500 hover:text-white p-1 rounded hover:bg-white/10 transition-colors"
|
||
|
|
>
|
||
|
|
<Pencil size={12} />
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<h4 className="text-sm font-bold text-slate-200 leading-tight mb-4 min-h-[40px] line-clamp-2">{item.name}</h4>
|
||
|
|
<div className="flex justify-between items-end">
|
||
|
|
<div>
|
||
|
|
<p className="text-[10px] font-bold text-slate-500 uppercase">Preço Venda</p>
|
||
|
|
<p className="text-lg font-bold text-emerald-400">R$ {(Number(item.marketPriceBRL) || 0).toLocaleString('pt-BR')}</p>
|
||
|
|
</div>
|
||
|
|
<div className="text-right">
|
||
|
|
<p className="text-[10px] font-bold text-slate-500 uppercase">Estoque</p>
|
||
|
|
<p className={clsx("text-lg font-bold", item.quantity === 0 ? "text-rose-500" : "text-white")}>{item.quantity}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* RIGHT: CART / CHECKOUT */}
|
||
|
|
<div className="xl:col-span-4 flex flex-col h-full">
|
||
|
|
<div className="bg-white/5 rounded-[40px] border border-white/5 shadow-2xl flex flex-col h-full overflow-hidden backdrop-blur-xl">
|
||
|
|
|
||
|
|
{/* Header */}
|
||
|
|
<div className="p-8 border-b border-white/5 bg-slate-900/50 flex justify-between items-center">
|
||
|
|
<div className="flex items-center gap-4">
|
||
|
|
<div className="w-12 h-12 rounded-2xl bg-indigo-600 flex items-center justify-center text-white shadow-lg shadow-indigo-500/20">
|
||
|
|
<ShoppingCart size={24} />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h2 className="text-xl font-bold text-white tracking-tight">Carrinho</h2>
|
||
|
|
<p className="text-xs font-bold text-slate-500 uppercase tracking-widest">Nova Venda</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<span className="bg-white/10 text-white px-3 py-1 rounded-xl text-xs font-bold">{totalItems} itens</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Customer Select (Optional) */}
|
||
|
|
<div className="p-6 border-b border-white/5 flex items-center gap-3">
|
||
|
|
<select
|
||
|
|
value={selectedCustomer}
|
||
|
|
onChange={(e) => setSelectedCustomer(e.target.value)}
|
||
|
|
className="flex-grow bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-sm text-slate-300 focus:border-indigo-500 outline-none cursor-pointer appearance-none"
|
||
|
|
>
|
||
|
|
<option value="">Cliente Não Identificado (Balcão)</option>
|
||
|
|
{customers.map(c => (
|
||
|
|
<option key={c.id} value={c.id}>{c.name}</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
<button
|
||
|
|
onClick={() => setShowCustomerModal(true)}
|
||
|
|
className="bg-indigo-600 hover:bg-indigo-500 text-white p-3 rounded-xl transition-colors shadow-lg shadow-indigo-900/20"
|
||
|
|
title="Novo Cliente"
|
||
|
|
>
|
||
|
|
<UserPlus size={18} />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Cart Items */}
|
||
|
|
<div className="flex-grow overflow-y-auto p-6 space-y-4 custom-scrollbar bg-black/10">
|
||
|
|
{cart.length === 0 ? (
|
||
|
|
<div className="h-full flex flex-col items-center justify-center opacity-20 text-center">
|
||
|
|
<Package size={64} className="mb-4 text-slate-400" />
|
||
|
|
<p className="text-sm font-bold text-slate-400 uppercase tracking-widest">Carrinho Vazio</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
cart.map((c, idx) => (
|
||
|
|
<div key={idx} className="bg-slate-800/50 p-4 rounded-2xl border border-white/5 group hover:border-indigo-500/30 transition-all">
|
||
|
|
<div className="flex justify-between items-start mb-2">
|
||
|
|
<h4 className="text-xs font-bold text-slate-200 line-clamp-1 pr-4">{c.item.name}</h4>
|
||
|
|
<button onClick={() => removeFromCart(c.item.id)} className="text-slate-600 hover:text-rose-500 transition-colors"><Trash2 size={14} /></button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between gap-4 mt-3">
|
||
|
|
<div className="flex items-center gap-2 bg-black/30 rounded-lg p-1 border border-white/5">
|
||
|
|
<button onClick={() => updateQuantity(c.item.id, -1)} className="p-1.5 hover:bg-white/10 rounded-md text-slate-400"><Minus size={12} /></button>
|
||
|
|
<span className="text-xs font-bold text-white w-6 text-center">{c.quantity}</span>
|
||
|
|
<button onClick={() => updateQuantity(c.item.id, 1)} className="p-1.5 hover:bg-white/10 rounded-md text-slate-400"><Plus size={12} /></button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex-grow">
|
||
|
|
<div className="relative">
|
||
|
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-[10px] mobile:text-[10px] text-slate-500 font-bold">R$</span>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
value={c.price}
|
||
|
|
onChange={(e) => updatePrice(c.item.id, parseFloat(e.target.value))}
|
||
|
|
className="w-full bg-black/30 border border-white/10 rounded-lg py-1.5 pl-8 pr-2 text-right text-xs font-bold text-white focus:border-indigo-500 outline-none"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="text-right mt-2">
|
||
|
|
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Subtotal:</span>
|
||
|
|
<span className="text-xs font-bold text-indigo-400 ml-2">R$ {(c.price * c.quantity).toLocaleString('pt-BR')}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Summary & Action */}
|
||
|
|
<div className="p-8 bg-slate-900 border-t border-white/10 space-y-6">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<div className="flex justify-between text-xs text-slate-500 font-medium">
|
||
|
|
<span>Itens</span>
|
||
|
|
<span>{totalItems} UN</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between items-end">
|
||
|
|
<span className="text-sm font-bold text-white uppercase tracking-widest">Total Geral</span>
|
||
|
|
<span className="text-3xl font-bold text-emerald-400 tracking-tight">R$ {totalAmount.toLocaleString('pt-BR')}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Payment Method Selector */}
|
||
|
|
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
|
||
|
|
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-3">Forma de Pagamento</p>
|
||
|
|
<div className="grid grid-cols-3 gap-2">
|
||
|
|
{['Cash', 'Pix', 'Credit Card'].map(method => (
|
||
|
|
<button
|
||
|
|
key={method}
|
||
|
|
onClick={() => setPaymentMethod(method as PaymentMethod)}
|
||
|
|
className={clsx(
|
||
|
|
"py-2 px-1 rounded-lg text-xs font-bold transition-all flex flex-col items-center gap-1",
|
||
|
|
paymentMethod === method
|
||
|
|
? "bg-indigo-600 text-white shadow-lg"
|
||
|
|
: "bg-black/20 text-slate-400 hover:bg-white/10 hover:text-white"
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{method === 'Cash' && <Banknote size={14} />}
|
||
|
|
{method === 'Pix' && <img src="https://icongr.am/entypo/flash.svg?size=14&color=currentColor" className={paymentMethod === 'Pix' ? "invert" : "opacity-50"} alt="Pix" />}
|
||
|
|
{method === 'Credit Card' && <CardIcon size={14} />}
|
||
|
|
<span>{method === 'Credit Card' ? 'Cartão' : method}</span>
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<button
|
||
|
|
onClick={handleFinalize}
|
||
|
|
disabled={cart.length === 0 || isFinalizing}
|
||
|
|
className="w-full py-4 bg-emerald-600 hover:bg-emerald-500 text-white rounded-2xl font-bold text-sm shadow-xl shadow-emerald-900/40 transition-all flex items-center justify-center gap-3 disabled:opacity-50 disabled:cursor-not-allowed active:scale-95"
|
||
|
|
>
|
||
|
|
{isFinalizing ? (
|
||
|
|
<div className="animate-spin w-5 h-5 border-2 border-white/30 border-t-white rounded-full"></div>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<CreditCard size={18} /> FINALIZAR VENDA
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{/* SUCCESS MODAL */}
|
||
|
|
{showSuccessModal && (
|
||
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-300">
|
||
|
|
<div className="bg-[#0F1115] border border-white/10 rounded-[32px] w-full max-w-md shadow-2xl p-8 relative overflow-hidden">
|
||
|
|
{/* Confetti or success decoration could go here */}
|
||
|
|
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-emerald-500 to-indigo-500"></div>
|
||
|
|
|
||
|
|
<div className="flex flex-col items-center text-center mb-8">
|
||
|
|
<div className="w-20 h-20 bg-emerald-500/10 rounded-full flex items-center justify-center mb-6 border border-emerald-500/20 shadow-[0_0_30px_rgba(16,185,129,0.2)]">
|
||
|
|
<CreditCard size={40} className="text-emerald-400" />
|
||
|
|
</div>
|
||
|
|
<h2 className="text-2xl font-bold text-white mb-2">Venda Realizada!</h2>
|
||
|
|
<p className="text-slate-400">Total processado com sucesso.</p>
|
||
|
|
<div className="mt-4 text-3xl font-bold text-emerald-400 font-mono tracking-tight">R$ {lastSaleTotal.toLocaleString('pt-BR')}</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-3">
|
||
|
|
<button
|
||
|
|
onClick={handlePrintReceipt}
|
||
|
|
className="w-full py-4 bg-white/5 hover:bg-white/10 border border-white/10 rounded-2xl flex items-center justify-between px-6 text-slate-300 hover:text-white transition-all group"
|
||
|
|
>
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<div className="p-2 rounded-lg bg-black/40 text-indigo-400"><Calculator size={20} /></div> {/* Printer Icon replacement */}
|
||
|
|
<span className="font-bold text-sm">Imprimir Recibo</span>
|
||
|
|
</div>
|
||
|
|
<ChevronRight size={16} className="text-slate-600 group-hover:text-white" />
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<button
|
||
|
|
onClick={handleSendEmail}
|
||
|
|
className="w-full py-4 bg-white/5 hover:bg-white/10 border border-white/10 rounded-2xl flex items-center justify-between px-6 text-slate-300 hover:text-white transition-all group"
|
||
|
|
>
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<div className="p-2 rounded-lg bg-black/40 text-blue-400"><AlertTriangle size={20} /></div> {/* Mail Icon replacement */}
|
||
|
|
<span className="font-bold text-sm">Enviar por Email</span>
|
||
|
|
</div>
|
||
|
|
<ChevronRight size={16} className="text-slate-600 group-hover:text-white" />
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<button
|
||
|
|
onClick={handleNewSale}
|
||
|
|
className="w-full py-4 bg-indigo-600 hover:bg-indigo-500 text-white rounded-2xl font-bold text-sm shadow-xl shadow-indigo-900/20 transition-all flex items-center justify-center gap-2 mt-6 active:scale-95"
|
||
|
|
>
|
||
|
|
<Plus size={18} /> NOVA VENDA
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default Sales;
|