- Agregar dependencias: i18next, react-i18next, i18next-browser-languagedetector - Eliminar imports no utilizados (Search, Filter) - Eliminar variables no utilizadas (refetch, generateCodeMutation) - Renombrar params no usados con underscore (_supplier, _onRemove) Build ahora pasa sin errores. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
731 lines
26 KiB
TypeScript
731 lines
26 KiB
TypeScript
import { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import {
|
|
Store,
|
|
Search,
|
|
Star,
|
|
Heart,
|
|
ShoppingCart,
|
|
Package,
|
|
Truck,
|
|
Clock,
|
|
CheckCircle,
|
|
XCircle,
|
|
MapPin,
|
|
Phone,
|
|
ChevronRight,
|
|
} from 'lucide-react';
|
|
import clsx from 'clsx';
|
|
import { marketplaceApi } from '../lib/api';
|
|
|
|
const categories = [
|
|
{ id: 'bebidas', name: 'Bebidas', icon: '🥤' },
|
|
{ id: 'botanas', name: 'Botanas', icon: '🍿' },
|
|
{ id: 'lacteos', name: 'Lacteos', icon: '🥛' },
|
|
{ id: 'pan', name: 'Pan', icon: '🍞' },
|
|
{ id: 'abarrotes', name: 'Abarrotes', icon: '🛒' },
|
|
{ id: 'limpieza', name: 'Limpieza', icon: '🧹' },
|
|
];
|
|
|
|
const orderStatusConfig = {
|
|
pending: { label: 'Pendiente', color: 'yellow', icon: Clock },
|
|
confirmed: { label: 'Confirmado', color: 'blue', icon: CheckCircle },
|
|
preparing: { label: 'Preparando', color: 'indigo', icon: Package },
|
|
shipped: { label: 'En camino', color: 'purple', icon: Truck },
|
|
delivered: { label: 'Entregado', color: 'green', icon: CheckCircle },
|
|
cancelled: { label: 'Cancelado', color: 'red', icon: XCircle },
|
|
rejected: { label: 'Rechazado', color: 'red', icon: XCircle },
|
|
};
|
|
|
|
export function Marketplace() {
|
|
const [view, setView] = useState<'suppliers' | 'orders' | 'favorites'>('suppliers');
|
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [selectedSupplier, setSelectedSupplier] = useState<any>(null);
|
|
const [showCart, setShowCart] = useState(false);
|
|
const [cart, setCart] = useState<{ product: any; quantity: number }[]>([]);
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data: suppliers = [], isLoading: loadingSuppliers } = useQuery({
|
|
queryKey: ['suppliers', selectedCategory, searchQuery],
|
|
queryFn: async () => {
|
|
const response = await marketplaceApi.getSuppliers({
|
|
category: selectedCategory || undefined,
|
|
search: searchQuery || undefined,
|
|
});
|
|
return response.data;
|
|
},
|
|
});
|
|
|
|
const { data: orders = [], isLoading: loadingOrders } = useQuery({
|
|
queryKey: ['marketplace-orders'],
|
|
queryFn: async () => {
|
|
const response = await marketplaceApi.getOrders();
|
|
return response.data;
|
|
},
|
|
enabled: view === 'orders',
|
|
});
|
|
|
|
const { data: favorites = [] } = useQuery({
|
|
queryKey: ['supplier-favorites'],
|
|
queryFn: async () => {
|
|
const response = await marketplaceApi.getFavorites();
|
|
return response.data;
|
|
},
|
|
enabled: view === 'favorites',
|
|
});
|
|
|
|
const addToCart = (product: any) => {
|
|
const existing = cart.find((item) => item.product.id === product.id);
|
|
if (existing) {
|
|
setCart(
|
|
cart.map((item) =>
|
|
item.product.id === product.id
|
|
? { ...item, quantity: item.quantity + 1 }
|
|
: item
|
|
)
|
|
);
|
|
} else {
|
|
setCart([...cart, { product, quantity: 1 }]);
|
|
}
|
|
};
|
|
|
|
const removeFromCart = (productId: string) => {
|
|
setCart(cart.filter((item) => item.product.id !== productId));
|
|
};
|
|
|
|
const updateCartQuantity = (productId: string, quantity: number) => {
|
|
if (quantity <= 0) {
|
|
removeFromCart(productId);
|
|
} else {
|
|
setCart(
|
|
cart.map((item) =>
|
|
item.product.id === productId ? { ...item, quantity } : item
|
|
)
|
|
);
|
|
}
|
|
};
|
|
|
|
const cartTotal = cart.reduce(
|
|
(sum, item) => sum + Number(item.product.unitPrice) * item.quantity,
|
|
0
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Marketplace</h1>
|
|
<p className="text-gray-500">Encuentra proveedores para tu negocio</p>
|
|
</div>
|
|
|
|
{cart.length > 0 && (
|
|
<button
|
|
onClick={() => setShowCart(true)}
|
|
className="btn-primary flex items-center gap-2"
|
|
>
|
|
<ShoppingCart className="h-4 w-4" />
|
|
Carrito ({cart.length})
|
|
<span className="ml-2 font-bold">
|
|
${cartTotal.toLocaleString('es-MX', { minimumFractionDigits: 2 })}
|
|
</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="flex gap-2 border-b">
|
|
<button
|
|
onClick={() => setView('suppliers')}
|
|
className={clsx(
|
|
'px-4 py-2 font-medium border-b-2 -mb-px transition-colors',
|
|
view === 'suppliers'
|
|
? 'border-primary-600 text-primary-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
|
)}
|
|
>
|
|
<Store className="h-4 w-4 inline mr-2" />
|
|
Proveedores
|
|
</button>
|
|
<button
|
|
onClick={() => setView('orders')}
|
|
className={clsx(
|
|
'px-4 py-2 font-medium border-b-2 -mb-px transition-colors',
|
|
view === 'orders'
|
|
? 'border-primary-600 text-primary-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
|
)}
|
|
>
|
|
<Package className="h-4 w-4 inline mr-2" />
|
|
Mis Pedidos
|
|
</button>
|
|
<button
|
|
onClick={() => setView('favorites')}
|
|
className={clsx(
|
|
'px-4 py-2 font-medium border-b-2 -mb-px transition-colors',
|
|
view === 'favorites'
|
|
? 'border-primary-600 text-primary-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
|
)}
|
|
>
|
|
<Heart className="h-4 w-4 inline mr-2" />
|
|
Favoritos
|
|
</button>
|
|
</div>
|
|
|
|
{view === 'suppliers' && (
|
|
<>
|
|
{/* Search & Filter */}
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Buscar proveedores..."
|
|
className="input pl-10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Categories */}
|
|
<div className="flex gap-2 overflow-x-auto pb-2">
|
|
<button
|
|
onClick={() => setSelectedCategory(null)}
|
|
className={clsx(
|
|
'px-4 py-2 rounded-lg font-medium whitespace-nowrap transition-colors',
|
|
!selectedCategory
|
|
? 'bg-primary-100 text-primary-700'
|
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
|
)}
|
|
>
|
|
Todos
|
|
</button>
|
|
{categories.map((category) => (
|
|
<button
|
|
key={category.id}
|
|
onClick={() => setSelectedCategory(category.id)}
|
|
className={clsx(
|
|
'px-4 py-2 rounded-lg font-medium whitespace-nowrap transition-colors flex items-center gap-2',
|
|
selectedCategory === category.id
|
|
? 'bg-primary-100 text-primary-700'
|
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
|
)}
|
|
>
|
|
<span>{category.icon}</span>
|
|
{category.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Suppliers Grid */}
|
|
{loadingSuppliers ? (
|
|
<div className="text-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto"></div>
|
|
<p className="text-gray-500 mt-2">Buscando proveedores...</p>
|
|
</div>
|
|
) : suppliers.length === 0 ? (
|
|
<div className="text-center py-12 card">
|
|
<Store className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-900">No hay proveedores</h3>
|
|
<p className="text-gray-500">Pronto habra mas proveedores disponibles</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{suppliers.map((supplier: any) => (
|
|
<SupplierCard
|
|
key={supplier.id}
|
|
supplier={supplier}
|
|
onClick={() => setSelectedSupplier(supplier)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{view === 'orders' && (
|
|
<div className="space-y-4">
|
|
{loadingOrders ? (
|
|
<div className="text-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto"></div>
|
|
</div>
|
|
) : orders.length === 0 ? (
|
|
<div className="text-center py-12 card">
|
|
<Package className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-900">No hay pedidos</h3>
|
|
<p className="text-gray-500">Tus pedidos a proveedores apareceran aqui</p>
|
|
</div>
|
|
) : (
|
|
orders.map((order: any) => (
|
|
<OrderCard key={order.id} order={order} />
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{view === 'favorites' && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{favorites.length === 0 ? (
|
|
<div className="col-span-full text-center py-12 card">
|
|
<Heart className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-900">Sin favoritos</h3>
|
|
<p className="text-gray-500">Agrega proveedores a tus favoritos</p>
|
|
</div>
|
|
) : (
|
|
favorites.map((supplier: any) => (
|
|
<SupplierCard
|
|
key={supplier.id}
|
|
supplier={supplier}
|
|
onClick={() => setSelectedSupplier(supplier)}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Supplier Detail Modal */}
|
|
{selectedSupplier && (
|
|
<SupplierDetailModal
|
|
supplier={selectedSupplier}
|
|
onClose={() => setSelectedSupplier(null)}
|
|
onAddToCart={addToCart}
|
|
cart={cart}
|
|
/>
|
|
)}
|
|
|
|
{/* Cart Modal */}
|
|
{showCart && (
|
|
<CartModal
|
|
cart={cart}
|
|
supplier={selectedSupplier}
|
|
onClose={() => setShowCart(false)}
|
|
onUpdateQuantity={updateCartQuantity}
|
|
onRemove={removeFromCart}
|
|
onOrderSuccess={() => {
|
|
setCart([]);
|
|
setShowCart(false);
|
|
queryClient.invalidateQueries({ queryKey: ['marketplace-orders'] });
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SupplierCard({
|
|
supplier,
|
|
onClick,
|
|
}: {
|
|
supplier: any;
|
|
onClick: () => void;
|
|
}) {
|
|
return (
|
|
<div
|
|
onClick={onClick}
|
|
className="card cursor-pointer hover:shadow-md transition-shadow"
|
|
>
|
|
<div className="flex items-start gap-4">
|
|
<div className="w-16 h-16 rounded-lg bg-gray-100 flex items-center justify-center overflow-hidden">
|
|
{supplier.logoUrl ? (
|
|
<img src={supplier.logoUrl} alt={supplier.name} className="w-full h-full object-cover" />
|
|
) : (
|
|
<Store className="h-8 w-8 text-gray-400" />
|
|
)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="font-bold text-gray-900 truncate">{supplier.name}</h3>
|
|
{supplier.verified && (
|
|
<CheckCircle className="h-4 w-4 text-blue-500 flex-shrink-0" />
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<div className="flex items-center text-yellow-500">
|
|
<Star className="h-4 w-4 fill-current" />
|
|
<span className="ml-1 text-sm font-medium">{Number(supplier.rating).toFixed(1)}</span>
|
|
</div>
|
|
<span className="text-gray-400">·</span>
|
|
<span className="text-sm text-gray-500">{supplier.totalReviews} resenas</span>
|
|
</div>
|
|
<div className="flex flex-wrap gap-1 mt-2">
|
|
{supplier.categories?.slice(0, 3).map((cat: string) => (
|
|
<span key={cat} className="px-2 py-0.5 text-xs bg-gray-100 rounded-full text-gray-600">
|
|
{cat}
|
|
</span>
|
|
))}
|
|
</div>
|
|
<div className="flex items-center gap-4 mt-2 text-sm text-gray-500">
|
|
<span>Min: ${Number(supplier.minOrderAmount).toFixed(0)}</span>
|
|
{Number(supplier.deliveryFee) > 0 ? (
|
|
<span>Envio: ${Number(supplier.deliveryFee).toFixed(0)}</span>
|
|
) : (
|
|
<span className="text-green-600">Envio gratis</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<ChevronRight className="h-5 w-5 text-gray-400 flex-shrink-0" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function OrderCard({ order }: { order: any }) {
|
|
const status = orderStatusConfig[order.status as keyof typeof orderStatusConfig] || orderStatusConfig.pending;
|
|
const StatusIcon = status.icon;
|
|
|
|
return (
|
|
<div className="card">
|
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
|
<div className="flex items-start gap-4">
|
|
<div className={`p-3 rounded-full bg-${status.color}-100`}>
|
|
<StatusIcon className={`h-6 w-6 text-${status.color}-600`} />
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="font-bold">Pedido #{order.orderNumber}</h3>
|
|
<span className={`px-2 py-0.5 text-xs rounded-full bg-${status.color}-100 text-${status.color}-700`}>
|
|
{status.label}
|
|
</span>
|
|
</div>
|
|
<p className="text-gray-600">{order.supplier?.name}</p>
|
|
<p className="text-sm text-gray-500">
|
|
{order.items?.length} productos
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-right">
|
|
<p className="text-2xl font-bold">
|
|
${Number(order.total).toLocaleString('es-MX', { minimumFractionDigits: 2 })}
|
|
</p>
|
|
<p className="text-sm text-gray-500">
|
|
{new Date(order.createdAt).toLocaleDateString('es-MX')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SupplierDetailModal({
|
|
supplier,
|
|
onClose,
|
|
onAddToCart,
|
|
cart,
|
|
}: {
|
|
supplier: any;
|
|
onClose: () => void;
|
|
onAddToCart: (product: any) => void;
|
|
cart: { product: any; quantity: number }[];
|
|
}) {
|
|
const [search, setSearch] = useState('');
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data: products = [], isLoading } = useQuery({
|
|
queryKey: ['supplier-products', supplier.id, search],
|
|
queryFn: async () => {
|
|
const response = await marketplaceApi.getSupplierProducts(supplier.id, {
|
|
search: search || undefined,
|
|
});
|
|
return response.data;
|
|
},
|
|
});
|
|
|
|
const favoriteMutation = useMutation({
|
|
mutationFn: () => marketplaceApi.addFavorite(supplier.id),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['supplier-favorites'] });
|
|
},
|
|
});
|
|
|
|
const getCartQuantity = (productId: string) => {
|
|
const item = cart.find((i) => i.product.id === productId);
|
|
return item?.quantity || 0;
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
|
<div className="bg-white rounded-xl max-w-3xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
|
{/* Header */}
|
|
<div className="p-6 border-b">
|
|
<div className="flex items-start gap-4">
|
|
<div className="w-20 h-20 rounded-lg bg-gray-100 flex items-center justify-center overflow-hidden">
|
|
{supplier.logoUrl ? (
|
|
<img src={supplier.logoUrl} alt={supplier.name} className="w-full h-full object-cover" />
|
|
) : (
|
|
<Store className="h-10 w-10 text-gray-400" />
|
|
)}
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-xl font-bold">{supplier.name}</h2>
|
|
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
|
<XCircle className="h-6 w-6" />
|
|
</button>
|
|
</div>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<Star className="h-4 w-4 text-yellow-500 fill-current" />
|
|
<span className="font-medium">{Number(supplier.rating).toFixed(1)}</span>
|
|
<span className="text-gray-400">·</span>
|
|
<span className="text-gray-500">{supplier.totalReviews} resenas</span>
|
|
</div>
|
|
{supplier.address && (
|
|
<p className="text-sm text-gray-500 mt-1 flex items-center gap-1">
|
|
<MapPin className="h-4 w-4" />
|
|
{supplier.city}, {supplier.state}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2 mt-4">
|
|
<button
|
|
onClick={() => favoriteMutation.mutate()}
|
|
className="btn-secondary flex items-center gap-2"
|
|
>
|
|
<Heart className="h-4 w-4" />
|
|
Agregar a favoritos
|
|
</button>
|
|
{supplier.contactPhone && (
|
|
<a href={`tel:${supplier.contactPhone}`} className="btn-secondary flex items-center gap-2">
|
|
<Phone className="h-4 w-4" />
|
|
Llamar
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Search */}
|
|
<div className="p-4 border-b">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
placeholder="Buscar productos..."
|
|
className="input pl-10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Products */}
|
|
<div className="flex-1 overflow-y-auto p-4">
|
|
{isLoading ? (
|
|
<div className="text-center py-8">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto"></div>
|
|
</div>
|
|
) : products.length === 0 ? (
|
|
<div className="text-center py-8 text-gray-500">
|
|
No hay productos disponibles
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
{products.map((product: any) => {
|
|
const inCart = getCartQuantity(product.id);
|
|
return (
|
|
<div key={product.id} className="flex gap-3 p-3 bg-gray-50 rounded-lg">
|
|
<div className="w-16 h-16 rounded bg-white flex items-center justify-center overflow-hidden">
|
|
{product.imageUrl ? (
|
|
<img src={product.imageUrl} alt={product.name} className="w-full h-full object-cover" />
|
|
) : (
|
|
<Package className="h-6 w-6 text-gray-300" />
|
|
)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h4 className="font-medium text-gray-900 truncate">{product.name}</h4>
|
|
<p className="text-sm text-gray-500">
|
|
Min: {product.minQuantity} {product.unitType}
|
|
</p>
|
|
<div className="flex items-center justify-between mt-2">
|
|
<span className="font-bold text-primary-600">
|
|
${Number(product.unitPrice).toFixed(2)}
|
|
</span>
|
|
{inCart > 0 ? (
|
|
<span className="text-sm text-green-600">
|
|
{inCart} en carrito
|
|
</span>
|
|
) : (
|
|
<button
|
|
onClick={() => onAddToCart(product)}
|
|
className="text-sm text-primary-600 font-medium"
|
|
>
|
|
+ Agregar
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CartModal({
|
|
cart,
|
|
supplier: _supplier,
|
|
onClose,
|
|
onUpdateQuantity,
|
|
onRemove: _onRemove,
|
|
onOrderSuccess,
|
|
}: {
|
|
cart: { product: any; quantity: number }[];
|
|
supplier: any;
|
|
onClose: () => void;
|
|
onUpdateQuantity: (productId: string, quantity: number) => void;
|
|
onRemove: (productId: string) => void;
|
|
onOrderSuccess: () => void;
|
|
}) {
|
|
const [deliveryAddress, setDeliveryAddress] = useState('');
|
|
const [deliveryPhone, setDeliveryPhone] = useState('');
|
|
const [notes, setNotes] = useState('');
|
|
|
|
const subtotal = cart.reduce(
|
|
(sum, item) => sum + Number(item.product.unitPrice) * item.quantity,
|
|
0
|
|
);
|
|
|
|
const orderMutation = useMutation({
|
|
mutationFn: (data: any) => marketplaceApi.createOrder(data),
|
|
onSuccess: onOrderSuccess,
|
|
});
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
const supplierId = cart[0]?.product?.supplierId;
|
|
if (!supplierId) return;
|
|
|
|
orderMutation.mutate({
|
|
supplierId,
|
|
items: cart.map((item) => ({
|
|
productId: item.product.id,
|
|
quantity: item.quantity,
|
|
})),
|
|
deliveryAddress,
|
|
deliveryPhone,
|
|
notes,
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
|
<div className="bg-white rounded-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
|
|
<div className="p-6 border-b flex items-center justify-between">
|
|
<h2 className="text-xl font-bold">Tu Carrito</h2>
|
|
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
|
<XCircle className="h-6 w-6" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
|
{/* Items */}
|
|
<div className="space-y-3">
|
|
{cart.map((item) => (
|
|
<div key={item.product.id} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
|
|
<div className="flex-1">
|
|
<p className="font-medium">{item.product.name}</p>
|
|
<p className="text-sm text-gray-500">
|
|
${Number(item.product.unitPrice).toFixed(2)} x {item.quantity}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => onUpdateQuantity(item.product.id, item.quantity - 1)}
|
|
className="w-8 h-8 rounded-full bg-gray-200 text-gray-600 flex items-center justify-center"
|
|
>
|
|
-
|
|
</button>
|
|
<span className="w-8 text-center font-medium">{item.quantity}</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => onUpdateQuantity(item.product.id, item.quantity + 1)}
|
|
className="w-8 h-8 rounded-full bg-gray-200 text-gray-600 flex items-center justify-center"
|
|
>
|
|
+
|
|
</button>
|
|
</div>
|
|
<p className="font-bold w-20 text-right">
|
|
${(Number(item.product.unitPrice) * item.quantity).toFixed(2)}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Delivery Info */}
|
|
<div className="pt-4 border-t space-y-4">
|
|
<h3 className="font-medium">Datos de entrega</h3>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Direccion de entrega
|
|
</label>
|
|
<textarea
|
|
value={deliveryAddress}
|
|
onChange={(e) => setDeliveryAddress(e.target.value)}
|
|
className="input"
|
|
rows={2}
|
|
required
|
|
placeholder="Calle, numero, colonia, CP"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Telefono de contacto
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={deliveryPhone}
|
|
onChange={(e) => setDeliveryPhone(e.target.value)}
|
|
className="input"
|
|
required
|
|
placeholder="5512345678"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Notas (opcional)
|
|
</label>
|
|
<textarea
|
|
value={notes}
|
|
onChange={(e) => setNotes(e.target.value)}
|
|
className="input"
|
|
rows={2}
|
|
placeholder="Instrucciones especiales..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Total */}
|
|
<div className="pt-4 border-t">
|
|
<div className="flex justify-between text-lg font-bold">
|
|
<span>Total</span>
|
|
<span>${subtotal.toLocaleString('es-MX', { minimumFractionDigits: 2 })}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-3 pt-4">
|
|
<button type="button" onClick={onClose} className="btn-secondary flex-1">
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={orderMutation.isPending || cart.length === 0}
|
|
className="btn-primary flex-1"
|
|
>
|
|
{orderMutation.isPending ? 'Enviando...' : 'Enviar Pedido'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|