michangarrito-frontend-v2/src/pages/Marketplace.tsx
rckrdmrd 2ae94a679f fix: Corregir errores TypeScript y agregar dependencias i18n
- 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>
2026-01-17 05:21:30 -06:00

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>
);
}