feat(logistics): consolidado de compras por proveedor y hojas de ruta con navegacion Waze para mensajeros
This commit is contained in:
parent
e49317f221
commit
d562598b01
@ -1,14 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ShoppingBag, Truck, CheckCircle2, Clock, Phone, MapPin, Search, MessageCircle, Send, ShieldCheck, UserCheck } from 'lucide-react';
|
||||
import {
|
||||
ShoppingBag,
|
||||
Truck,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Phone,
|
||||
MapPin,
|
||||
Search,
|
||||
MessageCircle,
|
||||
Send,
|
||||
ShieldCheck,
|
||||
Building2,
|
||||
Navigation,
|
||||
ExternalLink,
|
||||
DollarSign,
|
||||
Layers,
|
||||
Sparkles,
|
||||
Compass,
|
||||
Check,
|
||||
AlertCircle
|
||||
} from 'lucide-react';
|
||||
import { Order, OrderItem } from '@/types';
|
||||
import { formatCOP } from '@/lib/pricing-engine';
|
||||
|
||||
export default function AdminOrdersPage() {
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<'individual' | 'suppliers' | 'routes'>('individual');
|
||||
const [search, setSearch] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/orders')
|
||||
@ -22,14 +44,19 @@ export default function AdminOrdersPage() {
|
||||
prev.map((o) => (o.id === orderId ? { ...o, status: newStatus } : o))
|
||||
);
|
||||
|
||||
await fetch('/api/orders', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: orderId, status: newStatus }),
|
||||
}).catch(() => {});
|
||||
try {
|
||||
await fetch('/api/orders', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: orderId, status: newStatus }),
|
||||
});
|
||||
setToastMessage(`Estado de la orden #${orderId} actualizado a "${newStatus}".`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotifySupplier = (order: Order, item: OrderItem) => {
|
||||
const handleNotifySingleSupplier = (order: Order, item: OrderItem) => {
|
||||
const supplierPhone = item.supplierPhone || '573134848203';
|
||||
const supplierName = item.supplierName || 'Proveedor Mayorista';
|
||||
const supplierCost = item.supplierPrice ? formatCOP(item.supplierPrice) : 'Precio Mayorista';
|
||||
@ -39,12 +66,55 @@ export default function AdminOrdersPage() {
|
||||
const url = `https://wa.me/${supplierPhone}?text=${encodeURIComponent(message)}`;
|
||||
window.open(url, '_blank');
|
||||
|
||||
// Auto-update status to supplier_notified if still pending
|
||||
if (order.status === 'pending' || order.status === 'deposit_received') {
|
||||
handleStatusChange(order.id, 'supplier_notified');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendSupplierBatchManifest = (supplierName: string, supplierPhone: string, itemsList: { order: Order; item: OrderItem }[]) => {
|
||||
const totalPairs = itemsList.reduce((acc, curr) => acc + curr.item.quantity, 0);
|
||||
const totalCost = itemsList.reduce((acc, curr) => acc + (curr.item.supplierPrice || 0) * curr.item.quantity, 0);
|
||||
|
||||
let text = `📦 *MANIFIESTO CONSOLIDADO DE SEPARACIÓN*\n`;
|
||||
text += `🏢 *Proveedor:* ${supplierName}\n`;
|
||||
text += `📅 *Fecha:* ${new Date().toLocaleDateString('es-CO')} | *Total Pares:* ${totalPairs}\n`;
|
||||
text += `💰 *Valor Total a Pagar Proveedor:* ${formatCOP(totalCost)}\n\n`;
|
||||
text += `👟 *DETALLE DE PARES A SEPARAR:*\n`;
|
||||
|
||||
itemsList.forEach((entry, idx) => {
|
||||
text += `\n${idx + 1}. *${entry.item.productName}*\n`;
|
||||
text += ` • Talla: *${entry.item.size}* (x${entry.item.quantity})\n`;
|
||||
text += ` • Costo Base: ${formatCOP(entry.item.supplierPrice || 0)}\n`;
|
||||
text += ` • Orden Cliente: #${entry.order.id} (${entry.order.cityName})\n`;
|
||||
});
|
||||
|
||||
text += `\n🤝 *Por favor confirmar disponibilidad y empaque para recogida del mensajero.* ¡Muchas gracias!`;
|
||||
|
||||
const cleanPhone = supplierPhone.startsWith('57') ? supplierPhone : `57${supplierPhone}`;
|
||||
window.open(`https://wa.me/${cleanPhone}?text=${encodeURIComponent(text)}`, '_blank');
|
||||
setToastMessage(`📲 Manifiesto de ${totalPairs} pares enviado a ${supplierName}.`);
|
||||
};
|
||||
|
||||
const handleOpenWaze = (address: string, city: string) => {
|
||||
const query = encodeURIComponent(`${address}, ${city}, Colombia`);
|
||||
const wazeUrl = `https://waze.com/ul?q=${query}&navigate=yes`;
|
||||
window.open(wazeUrl, '_blank');
|
||||
};
|
||||
|
||||
const handleOpenGoogleMaps = (address: string, city: string) => {
|
||||
const query = encodeURIComponent(`${address}, ${city}, Colombia`);
|
||||
const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${query}`;
|
||||
window.open(mapsUrl, '_blank');
|
||||
};
|
||||
|
||||
const handleNotifyCustomerOnTheWay = (order: Order) => {
|
||||
const cleanPhone = order.customerPhone.startsWith('57') ? order.customerPhone : `57${order.customerPhone}`;
|
||||
const text = `👋 ¡Hola *${order.customerName}*!\n\n🛵 Tu pedido ya va *EN CAMINO* con nuestro mensajero.\n\n📍 *Dirección de Entrega:* ${order.address}\n🏙️ *Ciudad:* ${order.cityName}\n💵 *Saldo a Pagar Contraentrega:* *${formatCOP(order.balanceDue)}*\n💳 *Medio de Pago:* Efectivo o Transferencia Nequi\n\nPor favor estar atento a la portería o celular. ¡Gracias por tu compra! 🔥`;
|
||||
|
||||
window.open(`https://wa.me/${cleanPhone}?text=${encodeURIComponent(text)}`, '_blank');
|
||||
handleStatusChange(order.id, 'dispatched');
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: Order['status']) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
@ -52,9 +122,9 @@ export default function AdminOrdersPage() {
|
||||
case 'deposit_received':
|
||||
return <span className="bg-blue-500/20 text-blue-400 border border-blue-500/30 font-bold px-2.5 py-1 rounded-full text-[10px]">Separación $10k Pagada</span>;
|
||||
case 'supplier_notified':
|
||||
return <span className="bg-cyan-500/20 text-cyan-300 border border-cyan-500/30 font-bold px-2.5 py-1 rounded-full text-[10px] flex items-center gap-1"><CheckCircle2 className="w-3 h-3 text-cyan-400" /> Proveedor Notificado</span>;
|
||||
return <span className="bg-purple-500/20 text-purple-300 border border-purple-500/30 font-bold px-2.5 py-1 rounded-full text-[10px] flex items-center gap-1"><CheckCircle2 className="w-3 h-3 text-purple-400" /> Proveedor Notificado</span>;
|
||||
case 'dispatched':
|
||||
return <span className="bg-purple-500/20 text-purple-400 border border-purple-500/30 font-bold px-2.5 py-1 rounded-full text-[10px]">En Ruta con Mensajero</span>;
|
||||
return <span className="bg-fuchsia-500/20 text-fuchsia-400 border border-fuchsia-500/30 font-bold px-2.5 py-1 rounded-full text-[10px] flex items-center gap-1"><Truck className="w-3 h-3" /> En Ruta con Mensajero</span>;
|
||||
case 'delivered':
|
||||
return <span className="bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 font-bold px-2.5 py-1 rounded-full text-[10px]">Entregado & Cobrado</span>;
|
||||
case 'cancelled':
|
||||
@ -66,6 +136,7 @@ export default function AdminOrdersPage() {
|
||||
const matchesSearch =
|
||||
o.customerName.toLowerCase().includes(search.toLowerCase()) ||
|
||||
o.customerPhone.includes(search) ||
|
||||
o.address.toLowerCase().includes(search.toLowerCase()) ||
|
||||
o.cityName.toLowerCase().includes(search.toLowerCase()) ||
|
||||
o.id.toLowerCase().includes(search.toLowerCase());
|
||||
|
||||
@ -74,208 +145,440 @@ export default function AdminOrdersPage() {
|
||||
return true;
|
||||
});
|
||||
|
||||
const supplierGroups = orders.reduce((acc, order) => {
|
||||
order.items.forEach((item) => {
|
||||
const suppName = item.supplierName || 'DIFUSION JOVIC STORE';
|
||||
const suppPhone = item.supplierPhone || '573134848203';
|
||||
if (!acc[suppName]) {
|
||||
acc[suppName] = {
|
||||
name: suppName,
|
||||
phone: suppPhone,
|
||||
entries: [],
|
||||
totalPairs: 0,
|
||||
totalCost: 0,
|
||||
};
|
||||
}
|
||||
acc[suppName].entries.push({ order, item });
|
||||
acc[suppName].totalPairs += item.quantity;
|
||||
acc[suppName].totalCost += (item.supplierPrice || 0) * item.quantity;
|
||||
});
|
||||
return acc;
|
||||
}, {} as Record<string, { name: string; phone: string; entries: { order: Order; item: OrderItem }[]; totalPairs: number; totalCost: number }>);
|
||||
|
||||
const pendingDeliveries = orders.filter((o) => o.status !== 'delivered' && o.status !== 'cancelled');
|
||||
const totalBalanceToCollect = pendingDeliveries.reduce((acc, curr) => acc + curr.balanceDue, 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white flex items-center gap-2">
|
||||
<ShoppingBag className="w-6 h-6 text-cyan-400" />
|
||||
<span>Gestión de Pedidos & Separación con Proveedores ({orders.length})</span>
|
||||
</h1>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
Control logístico de pedidos contraentrega, cobro de anticipos de $10.000 COP y <strong>notificación inmediata de separación al proveedor</strong> vía WhatsApp.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||
<div className="flex items-center space-x-1.5 bg-slate-900 border border-slate-800 p-1 rounded-2xl overflow-x-auto">
|
||||
<button
|
||||
onClick={() => setStatusFilter('all')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'all' ? 'bg-white text-slate-950 shadow' : 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Todos ({orders.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('pending')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'pending' ? 'bg-amber-500/30 text-amber-300' : 'text-slate-400'
|
||||
}`}
|
||||
>
|
||||
Pendientes
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('supplier_notified')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'supplier_notified' ? 'bg-cyan-500/30 text-cyan-300' : 'text-slate-400'
|
||||
}`}
|
||||
>
|
||||
Proveedor Notificado
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('dispatched')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'dispatched' ? 'bg-purple-500/30 text-purple-300' : 'text-slate-400'
|
||||
}`}
|
||||
>
|
||||
En Ruta
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-w-xs w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar por cliente, ciudad o # orden..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-white outline-hidden focus:border-cyan-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredOrders.length === 0 ? (
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-3xl p-12 text-center space-y-3">
|
||||
<div className="w-16 h-16 rounded-full bg-slate-950 text-slate-500 flex items-center justify-center mx-auto border border-slate-800">
|
||||
<ShoppingBag className="w-8 h-8 text-cyan-400" />
|
||||
</div>
|
||||
<h3 className="font-bold text-white text-base">No hay pedidos en esta sección</h3>
|
||||
<p className="text-xs text-slate-400 max-w-sm mx-auto">
|
||||
Cuando los clientes hagan un pedido en la tienda web o por WhatsApp, aparecerán aquí con el botón para separar inmediatamente con el proveedor correspondiente.
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white flex items-center gap-2">
|
||||
<ShoppingBag className="w-6 h-6 text-purple-400" />
|
||||
<span>Centro Logístico: Pedidos, Proveedores & Rutas Waze</span>
|
||||
</h1>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Consolidación automática para <strong>separación con proveedores</strong> y <strong>hoja de ruta para mensajeros con navegación Waze en 1 clic</strong>.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
</div>
|
||||
|
||||
{toastMessage && (
|
||||
<div className="bg-purple-950/80 border border-purple-500 text-purple-300 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-md">
|
||||
<span>{toastMessage}</span>
|
||||
<button onClick={() => setToastMessage(null)} className="font-bold text-white cursor-pointer ml-3">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Tabs Navigation */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 p-1 bg-neutral-900 border border-neutral-800 rounded-2xl gap-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('individual')}
|
||||
className={`py-3 px-4 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 cursor-pointer ${
|
||||
activeTab === 'individual'
|
||||
? 'bg-purple-600 text-white shadow-lg shadow-purple-600/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Layers className="w-4 h-4" />
|
||||
<span>1. Todos los Pedidos ({orders.length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('suppliers')}
|
||||
className={`py-3 px-4 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 cursor-pointer ${
|
||||
activeTab === 'suppliers'
|
||||
? 'bg-purple-600 text-white shadow-lg shadow-purple-600/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Building2 className="w-4 h-4 text-fuchsia-300" />
|
||||
<span>2. Consolidado por Proveedor ({Object.keys(supplierGroups).length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('routes')}
|
||||
className={`py-3 px-4 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 cursor-pointer ${
|
||||
activeTab === 'routes'
|
||||
? 'bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg shadow-pink-600/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Compass className="w-4 h-4 text-cyan-300" />
|
||||
<span>3. Rutas de Entrega & Waze ({pendingDeliveries.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* TAB 1: INDIVIDUAL ORDERS LIST */}
|
||||
{activeTab === 'individual' && (
|
||||
<div className="space-y-4">
|
||||
{filteredOrders.map((order) => (
|
||||
<div
|
||||
key={order.id}
|
||||
className="bg-slate-900 border border-slate-800 rounded-3xl p-5 space-y-4 shadow-xl"
|
||||
>
|
||||
{/* Top Order bar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-800 pb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-black text-white text-sm">Orden #{order.id}</span>
|
||||
<span className="text-xs text-slate-500">• {new Date(order.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</div>
|
||||
{getStatusBadge(order.status)}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||
<div className="flex items-center space-x-1.5 bg-neutral-900 border border-neutral-800 p-1 rounded-2xl overflow-x-auto">
|
||||
{['all', 'pending', 'deposit_received', 'supplier_notified', 'dispatched', 'delivered'].map((st) => (
|
||||
<button
|
||||
key={st}
|
||||
onClick={() => setStatusFilter(st)}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === st ? 'bg-white text-neutral-950 shadow' : 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{st === 'all' ? 'Todos' : st === 'pending' ? 'Pendientes' : st === 'deposit_received' ? 'Anticipo Pagado' : st === 'supplier_notified' ? 'Prov. Notificado' : st === 'dispatched' ? 'En Ruta' : 'Entregados'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="max-w-xs w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar por cliente, celular, ciudad o #..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3.5 py-2 text-xs text-white outline-hidden focus:border-purple-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{filteredOrders.length === 0 ? (
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl p-12 text-center text-neutral-500">
|
||||
No hay pedidos que coincidan con los filtros.
|
||||
</div>
|
||||
) : (
|
||||
filteredOrders.map((order) => (
|
||||
<div
|
||||
key={order.id}
|
||||
className="bg-neutral-900 border border-neutral-800 hover:border-purple-500/40 rounded-3xl p-5 shadow-xl transition-all space-y-4"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-neutral-800 pb-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="font-black text-white text-base">Orden #{order.id}</span>
|
||||
{getStatusBadge(order.status)}
|
||||
<span className="text-[11px] text-neutral-500 font-mono">{order.createdAt}</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
||||
{/* Customer Details */}
|
||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3.5 space-y-1.5">
|
||||
<span className="text-[10px] text-cyan-400 font-bold uppercase block tracking-wider">
|
||||
Cliente & Entrega (24 Horas)
|
||||
</span>
|
||||
<p className="font-bold text-white text-sm">{order.customerName}</p>
|
||||
<p className="text-slate-300 flex items-center gap-1.5">
|
||||
<Phone className="w-3.5 h-3.5 text-cyan-400" /> {order.customerPhone}
|
||||
</p>
|
||||
<p className="text-slate-300 flex items-center gap-1.5">
|
||||
<MapPin className="w-3.5 h-3.5 text-cyan-400" /> {order.address} ({order.cityName})
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-neutral-400 font-bold">Estado:</span>
|
||||
<select
|
||||
value={order.status}
|
||||
onChange={(e) => handleStatusChange(order.id, e.target.value as any)}
|
||||
className="bg-neutral-950 border border-neutral-800 text-xs text-white rounded-xl px-3 py-1.5 outline-hidden font-bold cursor-pointer"
|
||||
>
|
||||
<option value="pending">⏳ Pendiente Confirmar</option>
|
||||
<option value="deposit_received">💳 Separación $10k Pagada</option>
|
||||
<option value="supplier_notified">🏢 Proveedor Notificado</option>
|
||||
<option value="dispatched">🛵 En Ruta con Mensajero</option>
|
||||
<option value="delivered">✅ Entregado & Cobrado</option>
|
||||
<option value="cancelled">❌ Cancelado</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Items and Supplier Separation Action */}
|
||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3.5 space-y-2">
|
||||
<span className="text-[10px] text-slate-400 font-bold uppercase block tracking-wider">
|
||||
Zapatillas & Proveedor Asignado:
|
||||
</span>
|
||||
<div className="space-y-2.5">
|
||||
{order.items.map((it, idx) => {
|
||||
const suppName = it.supplierName || 'DIFUSION JOVIC STORE';
|
||||
return (
|
||||
<div key={idx} className="bg-slate-900 border border-slate-800 rounded-xl p-2.5 space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
||||
<div className="space-y-1 bg-neutral-950/60 p-3 rounded-2xl border border-neutral-800/80">
|
||||
<p className="font-bold text-neutral-400 uppercase text-[10px]">Cliente & Destino</p>
|
||||
<p className="text-white font-bold text-sm">{order.customerName}</p>
|
||||
<p className="text-emerald-400 font-mono font-semibold flex items-center gap-1">
|
||||
<Phone className="w-3 h-3" /> {order.customerPhone}
|
||||
</p>
|
||||
<p className="text-neutral-300 flex items-start gap-1">
|
||||
<MapPin className="w-3 h-3 shrink-0 text-purple-400 mt-0.5" />
|
||||
<span>{order.address} ({order.cityName})</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 bg-neutral-950/60 p-3 rounded-2xl border border-neutral-800/80">
|
||||
<p className="font-bold text-neutral-400 uppercase text-[10px]">Resumen de Cobro</p>
|
||||
<div className="flex justify-between text-neutral-300">
|
||||
<span>Total Venta:</span>
|
||||
<strong className="text-white">{formatCOP(order.totalAmount)}</strong>
|
||||
</div>
|
||||
<div className="flex justify-between text-neutral-300">
|
||||
<span>Anticipo / Separación:</span>
|
||||
<strong className="text-blue-400">-{formatCOP(order.depositAmount)}</strong>
|
||||
</div>
|
||||
<div className="flex justify-between text-neutral-300 pt-1 border-t border-neutral-800 font-bold">
|
||||
<span className="text-purple-300">Saldo Contraentrega:</span>
|
||||
<span className="text-emerald-400 text-sm">{formatCOP(order.balanceDue)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 bg-neutral-950/60 p-3 rounded-2xl border border-neutral-800/80 flex flex-col justify-center">
|
||||
<button
|
||||
onClick={() => handleOpenWaze(order.address, order.cityName)}
|
||||
className="w-full bg-cyan-600 hover:bg-cyan-500 text-white font-bold py-1.5 px-3 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer shadow transition-all text-xs"
|
||||
>
|
||||
<Compass className="w-3.5 h-3.5" />
|
||||
<span>Navegar con Waze</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleNotifyCustomerOnTheWay(order)}
|
||||
className="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-1.5 px-3 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer shadow transition-all text-xs"
|
||||
>
|
||||
<MessageCircle className="w-3.5 h-3.5" />
|
||||
<span>Avisar "Voy en camino"</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pt-2">
|
||||
<p className="font-bold text-neutral-400 uppercase text-[10px]">Pares en esta orden:</p>
|
||||
<div className="space-y-2">
|
||||
{order.items.map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 bg-neutral-950 p-3 rounded-2xl border border-neutral-800"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<img src={item.image} alt="" className="w-12 h-12 rounded-xl object-cover border border-neutral-800 shrink-0" />
|
||||
<div>
|
||||
<p className="font-bold text-white text-xs">{it.productName}</p>
|
||||
<p className="text-[11px] text-slate-400">
|
||||
Talla <strong className="text-cyan-400 font-bold">{it.size}</strong> • Cant: {it.quantity}
|
||||
<p className="font-bold text-white text-xs">{item.productName}</p>
|
||||
<p className="text-[11px] text-neutral-400">
|
||||
Talla: <strong className="text-purple-400">{item.size}</strong> | Cantidad: <strong>{item.quantity}</strong> | Proveedor: <strong className="text-purple-300">{item.supplierName || 'Bodega Principal'}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-[10px] bg-slate-950 text-slate-300 border border-slate-800 px-2 py-0.5 rounded font-mono">
|
||||
Prov: {suppName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 1-Click WhatsApp Separation Button */}
|
||||
<button
|
||||
onClick={() => handleNotifySupplier(order, it)}
|
||||
className="w-full bg-emerald-600/20 hover:bg-emerald-600/30 text-emerald-300 border border-emerald-500/30 font-bold text-xs py-1.5 px-2.5 rounded-lg flex items-center justify-center space-x-1.5 transition-all cursor-pointer shadow-xs"
|
||||
title="Enviar mensaje preformateado al WhatsApp del proveedor para separar este par"
|
||||
onClick={() => handleNotifySingleSupplier(order, item)}
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-3.5 py-2 rounded-xl flex items-center space-x-1.5 shadow cursor-pointer shrink-0 self-end sm:self-auto"
|
||||
>
|
||||
<MessageCircle className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>📲 Separar con {suppName} (WhatsApp)</span>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
<span>Separar con Proveedor</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 2: CONSOLIDADO AGRUPADO POR PROVEEDOR */}
|
||||
{activeTab === 'suppliers' && (
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
<div className="bg-purple-950/40 border border-purple-500/30 rounded-3xl p-5 text-xs text-neutral-300 space-y-2 shadow-lg">
|
||||
<div className="flex items-center space-x-2 text-purple-300 font-bold text-sm">
|
||||
<Building2 className="w-5 h-5 text-purple-400" />
|
||||
<span>Consolidado Automático de Compras & Separaciones</span>
|
||||
</div>
|
||||
<p>
|
||||
Aquí se agrupan todos los pares pendientes clasificados por cada <strong>Proveedor Mayorista</strong>. Puedes enviar el <strong>manifiesto consolidado en 1 solo mensaje de WhatsApp</strong> para que el proveedor prepare y empaque todos los pedidos de la jornada juntos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{Object.values(supplierGroups).map((group) => (
|
||||
<div
|
||||
key={group.name}
|
||||
className="bg-neutral-900 border border-neutral-800 rounded-3xl p-6 shadow-xl space-y-4"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-neutral-800 pb-4">
|
||||
<div>
|
||||
<span className="bg-purple-500/20 text-purple-300 border border-purple-500/40 text-[10px] font-bold px-2.5 py-0.5 rounded-full uppercase tracking-wider">
|
||||
Proveedor Mayorista
|
||||
</span>
|
||||
<h3 className="text-xl font-black text-white mt-1">{group.name}</h3>
|
||||
<p className="text-xs text-neutral-400 flex items-center gap-1 mt-0.5">
|
||||
<Phone className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>WhatsApp Proveedor: <strong>{group.phone}</strong></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-neutral-950 border border-neutral-800 px-4 py-2 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 uppercase font-bold">Total Pares</p>
|
||||
<p className="text-lg font-black text-purple-400">{group.totalPairs} par(es)</p>
|
||||
</div>
|
||||
<div className="bg-neutral-950 border border-neutral-800 px-4 py-2 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 uppercase font-bold">Total a Pagar</p>
|
||||
<p className="text-lg font-black text-emerald-400">{formatCOP(group.totalCost)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Breakdown */}
|
||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3.5 space-y-2">
|
||||
<span className="text-[10px] text-slate-400 font-bold uppercase block tracking-wider">
|
||||
Cobro & Pagos Contraentrega
|
||||
</span>
|
||||
<div className="space-y-1 text-xs">
|
||||
<p className="text-slate-400 flex justify-between">
|
||||
<span>Total de la Orden:</span>
|
||||
<strong className="text-white font-mono">{formatCOP(order.totalAmount)}</strong>
|
||||
</p>
|
||||
{order.depositAmount > 0 ? (
|
||||
<>
|
||||
<p className="text-amber-300 flex justify-between">
|
||||
<span>Anticipo / Separación:</span>
|
||||
<strong className="font-mono">{formatCOP(order.depositAmount)}</strong>
|
||||
</p>
|
||||
<p className="text-emerald-400 font-bold flex justify-between pt-1 border-t border-slate-800">
|
||||
<span>Saldo al Mensajero:</span>
|
||||
<strong className="font-mono">{formatCOP(order.balanceDue)}</strong>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-emerald-400 font-bold pt-1 border-t border-slate-800">
|
||||
100% Contraentrega al recibir
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs text-neutral-300">
|
||||
<thead className="bg-neutral-950 text-neutral-400 font-bold uppercase text-[10px]">
|
||||
<tr>
|
||||
<th className="py-2.5 px-3">Modelo / Zapatilla</th>
|
||||
<th className="py-2.5 px-3">Talla</th>
|
||||
<th className="py-2.5 px-3">Cant.</th>
|
||||
<th className="py-2.5 px-3">Costo Base</th>
|
||||
<th className="py-2.5 px-3">Orden Cliente</th>
|
||||
<th className="py-2.5 px-3">Destino</th>
|
||||
<th className="py-2.5 px-3">Estado Orden</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-neutral-800">
|
||||
{group.entries.map((entry, idx) => (
|
||||
<tr key={idx} className="hover:bg-neutral-800/40">
|
||||
<td className="py-2.5 px-3 font-bold text-white flex items-center gap-2">
|
||||
<img src={entry.item.image} alt="" className="w-8 h-8 rounded-lg object-cover border border-neutral-800" />
|
||||
<span>{entry.item.productName}</span>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 font-bold text-purple-400 font-mono">{entry.item.size}</td>
|
||||
<td className="py-2.5 px-3 font-bold">{entry.item.quantity}</td>
|
||||
<td className="py-2.5 px-3 font-mono font-semibold">{formatCOP(entry.item.supplierPrice || 0)}</td>
|
||||
<td className="py-2.5 px-3 font-mono text-neutral-400">#{entry.order.id}</td>
|
||||
<td className="py-2.5 px-3">{entry.order.cityName}</td>
|
||||
<td className="py-2.5 px-3">{getStatusBadge(entry.order.status)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 flex justify-end">
|
||||
<button
|
||||
onClick={() => handleSendSupplierBatchManifest(group.name, group.phone, group.entries)}
|
||||
className="bg-gradient-to-r from-purple-600 via-fuchsia-600 to-pink-600 hover:opacity-95 text-white font-black text-xs px-5 py-3 rounded-2xl shadow-lg flex items-center gap-2 cursor-pointer transition-all active:scale-95"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
<span>Enviar Manifiesto de {group.totalPairs} Pares por WhatsApp a {group.name}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Order Status Action Buttons */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-slate-800">
|
||||
<span className="text-[11px] text-slate-500">Método: {order.paymentMethodName}</span>
|
||||
{/* TAB 3: RUTAS DE ENTREGA ORGANIZADAS POR DIRECCIONES & WAZE */}
|
||||
{activeTab === 'routes' && (
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
<div className="bg-gradient-to-r from-neutral-900 via-neutral-900 to-purple-950/60 border border-purple-500/30 rounded-3xl p-6 shadow-xl flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 text-cyan-300 font-bold text-sm">
|
||||
<Compass className="w-5 h-5 text-cyan-400" />
|
||||
<span>Hoja de Ruta & Navegación GPS (Waze / Google Maps)</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-300 mt-1 max-w-xl">
|
||||
Entregas contraentrega secuenciadas por zona geográfica. El mensajero puede presionar <strong>"Abrir en Waze"</strong> para iniciar la navegación instantánea a cada portería/casa.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'deposit_received')}
|
||||
className="bg-blue-600/20 hover:bg-blue-600/30 text-blue-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-blue-500/20"
|
||||
>
|
||||
Confirmar $10k
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'supplier_notified')}
|
||||
className="bg-cyan-600/20 hover:bg-cyan-600/30 text-cyan-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-cyan-500/20"
|
||||
>
|
||||
Marcar Proveedor Notificado
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'dispatched')}
|
||||
className="bg-purple-600/20 hover:bg-purple-600/30 text-purple-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-purple-500/20"
|
||||
>
|
||||
Despachar en Ruta
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'delivered')}
|
||||
className="bg-emerald-600/20 hover:bg-emerald-600/30 text-emerald-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-emerald-500/20"
|
||||
>
|
||||
Marcar Entregado
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-neutral-950 border border-neutral-800 p-3 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 uppercase font-bold">Paradas / Entregas</p>
|
||||
<p className="text-xl font-black text-white">{pendingDeliveries.length} Paradas</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-neutral-950 border border-emerald-500/30 p-3 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-emerald-400 uppercase font-bold">Total a Recaudar</p>
|
||||
<p className="text-xl font-black text-emerald-400">{formatCOP(totalBalanceToCollect)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{pendingDeliveries.length === 0 ? (
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl p-12 text-center text-neutral-500">
|
||||
🎉 No hay entregas pendientes por despachar en este momento.
|
||||
</div>
|
||||
) : (
|
||||
pendingDeliveries.map((delivery, index) => (
|
||||
<div
|
||||
key={delivery.id}
|
||||
className="bg-neutral-900 border border-neutral-800 hover:border-cyan-500/40 rounded-3xl p-5 shadow-lg transition-all flex flex-col lg:flex-row lg:items-center justify-between gap-4"
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-cyan-500/20 border border-cyan-500/40 flex flex-col items-center justify-center text-cyan-300 font-black shrink-0 shadow-sm">
|
||||
<span className="text-[10px] uppercase font-bold text-neutral-400 leading-none">Parada</span>
|
||||
<span className="text-lg leading-tight">#{index + 1}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-black text-white text-base">{delivery.customerName}</span>
|
||||
<span className="text-xs text-neutral-400">• Orden #{delivery.id}</span>
|
||||
{getStatusBadge(delivery.status)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs font-bold text-white flex items-center gap-1.5 bg-neutral-950 px-3 py-1 rounded-xl border border-neutral-800 w-fit">
|
||||
<MapPin className="w-4 h-4 text-cyan-400 shrink-0" />
|
||||
<span>{delivery.address} ({delivery.cityName})</span>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center space-x-3 text-xs text-neutral-400 pt-0.5">
|
||||
<span className="flex items-center gap-1 text-emerald-400 font-semibold">
|
||||
<Phone className="w-3 h-3" /> {delivery.customerPhone}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>
|
||||
{delivery.items.map((i) => `${i.productName} (Talla ${i.size})`).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{delivery.notes && (
|
||||
<p className="text-[11px] text-amber-300/90 italic bg-amber-500/10 px-2.5 py-0.5 rounded-lg border border-amber-500/20 w-fit">
|
||||
Nota: {delivery.notes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 self-end lg:self-auto shrink-0">
|
||||
<div className="bg-neutral-950 border border-neutral-800 px-3.5 py-2 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 font-bold uppercase">Cobrar Contraentrega</p>
|
||||
<p className="text-base font-black text-emerald-400">{formatCOP(delivery.balanceDue)}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleOpenWaze(delivery.address, delivery.cityName)}
|
||||
className="bg-cyan-600 hover:bg-cyan-500 text-white font-black text-xs px-4 py-3 rounded-2xl shadow-lg flex items-center gap-1.5 cursor-pointer active:scale-95 transition-all"
|
||||
title="Abrir ubicación directamente en Waze"
|
||||
>
|
||||
<Compass className="w-4 h-4" />
|
||||
<span>Waze</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleOpenGoogleMaps(delivery.address, delivery.cityName)}
|
||||
className="bg-neutral-800 hover:bg-neutral-700 text-white font-bold text-xs p-3 rounded-2xl shadow cursor-pointer transition-all"
|
||||
title="Abrir en Google Maps"
|
||||
>
|
||||
<Navigation className="w-4 h-4 text-emerald-400" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleNotifyCustomerOnTheWay(delivery)}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs px-3.5 py-3 rounded-2xl shadow flex items-center gap-1.5 cursor-pointer active:scale-95 transition-all"
|
||||
title="Enviar mensaje de WhatsApp notificando que el mensajero va en camino"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">En Camino</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,14 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ShoppingBag, Truck, CheckCircle2, Clock, Phone, MapPin, Search, MessageCircle, Send, ShieldCheck, UserCheck } from 'lucide-react';
|
||||
import {
|
||||
ShoppingBag,
|
||||
Truck,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Phone,
|
||||
MapPin,
|
||||
Search,
|
||||
MessageCircle,
|
||||
Send,
|
||||
ShieldCheck,
|
||||
Building2,
|
||||
Navigation,
|
||||
ExternalLink,
|
||||
DollarSign,
|
||||
Layers,
|
||||
Sparkles,
|
||||
Compass,
|
||||
Check,
|
||||
AlertCircle
|
||||
} from 'lucide-react';
|
||||
import { Order, OrderItem } from '@/types';
|
||||
import { formatCOP } from '@/lib/pricing-engine';
|
||||
|
||||
export default function AdminOrdersPage() {
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<'individual' | 'suppliers' | 'routes'>('individual');
|
||||
const [search, setSearch] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/orders')
|
||||
@ -22,14 +44,20 @@ export default function AdminOrdersPage() {
|
||||
prev.map((o) => (o.id === orderId ? { ...o, status: newStatus } : o))
|
||||
);
|
||||
|
||||
await fetch('/api/orders', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: orderId, status: newStatus }),
|
||||
}).catch(() => {});
|
||||
try {
|
||||
await fetch('/api/orders', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: orderId, status: newStatus }),
|
||||
});
|
||||
setToastMessage(`Estado de la orden #${orderId} actualizado a "${newStatus}".`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotifySupplier = (order: Order, item: OrderItem) => {
|
||||
// 1. Notificar separación individual a proveedor
|
||||
const handleNotifySingleSupplier = (order: Order, item: OrderItem) => {
|
||||
const supplierPhone = item.supplierPhone || '573134848203';
|
||||
const supplierName = item.supplierName || 'Proveedor Mayorista';
|
||||
const supplierCost = item.supplierPrice ? formatCOP(item.supplierPrice) : 'Precio Mayorista';
|
||||
@ -44,6 +72,55 @@ export default function AdminOrdersPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Enviar Manifiesto Consolidado por Proveedor vía WhatsApp
|
||||
const handleSendSupplierBatchManifest = (supplierName: string, supplierPhone: string, itemsList: { order: Order; item: OrderItem }[]) => {
|
||||
const totalPairs = itemsList.reduce((acc, curr) => acc + curr.item.quantity, 0);
|
||||
const totalCost = itemsList.reduce((acc, curr) => acc + (curr.item.supplierPrice || 0) * curr.item.quantity, 0);
|
||||
|
||||
let text = `📦 *MANIFIESTO CONSOLIDADO DE SEPARACIÓN*\n`;
|
||||
text += `🏢 *Proveedor:* ${supplierName}\n`;
|
||||
text += `📅 *Fecha:* ${new Date().toLocaleDateString('es-CO')} | *Total Pares:* ${totalPairs}\n`;
|
||||
text += `💰 *Valor Total a Pagar Proveedor:* ${formatCOP(totalCost)}\n\n`;
|
||||
text += `👟 *DETALLE DE PARES A SEPARAR:*\n`;
|
||||
|
||||
itemsList.forEach((entry, idx) => {
|
||||
text += `\n${idx + 1}. *${entry.item.productName}*\n`;
|
||||
text += ` • Talla: *${entry.item.size}* (x${entry.item.quantity})\n`;
|
||||
text += ` • Costo Base: ${formatCOP(entry.item.supplierPrice || 0)}\n`;
|
||||
text += ` • Orden Cliente: #${entry.order.id} (${entry.order.cityName})\n`;
|
||||
});
|
||||
|
||||
text += `\n🤝 *Por favor confirmar disponibilidad y empaque para recogida del mensajero.* ¡Muchas gracias!`;
|
||||
|
||||
const cleanPhone = supplierPhone.startsWith('57') ? supplierPhone : `57${supplierPhone}`;
|
||||
window.open(`https://wa.me/${cleanPhone}?text=${encodeURIComponent(text)}`, '_blank');
|
||||
setToastMessage(`📲 Manifiesto de ${totalPairs} pares enviado a ${supplierName}.`);
|
||||
};
|
||||
|
||||
// 3. Abrir en Waze
|
||||
const handleOpenWaze = (address: string, city: string) => {
|
||||
const query = encodeURIComponent(`${address}, ${city}, Colombia`);
|
||||
// Waze Universal Deep Link
|
||||
const wazeUrl = `https://waze.com/ul?q=${query}&navigate=yes`;
|
||||
window.open(wazeUrl, '_blank');
|
||||
};
|
||||
|
||||
// 4. Abrir en Google Maps
|
||||
const handleOpenGoogleMaps = (address: string, city: string) => {
|
||||
const query = encodeURIComponent(`${address}, ${city}, Colombia`);
|
||||
const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${query}`;
|
||||
window.open(mapsUrl, '_blank');
|
||||
};
|
||||
|
||||
// 5. Avisar al cliente que el mensajero va en camino
|
||||
const handleNotifyCustomerOnTheWay = (order: Order) => {
|
||||
const cleanPhone = order.customerPhone.startsWith('57') ? order.customerPhone : `57${order.customerPhone}`;
|
||||
const text = `👋 ¡Hola *${order.customerName}*!\n\n🛵 Tu pedido de *DripZone* ya va *EN CAMINO* con nuestro mensajero.\n\n📍 *Dirección de Entrega:* ${order.address}\n🏙️ *Ciudad:* ${order.cityName}\n💵 *Saldo a Pagar Contraentrega:* *${formatCOP(order.balanceDue)}*\n💳 *Medio de Pago:* Efectivo o Transferencia Nequi\n\nPor favor estar atento a la portería o celular. ¡Gracias por tu compra! 🔥`;
|
||||
|
||||
window.open(`https://wa.me/${cleanPhone}?text=${encodeURIComponent(text)}`, '_blank');
|
||||
handleStatusChange(order.id, 'dispatched');
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: Order['status']) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
@ -53,7 +130,7 @@ export default function AdminOrdersPage() {
|
||||
case 'supplier_notified':
|
||||
return <span className="bg-purple-500/20 text-purple-300 border border-purple-500/30 font-bold px-2.5 py-1 rounded-full text-[10px] flex items-center gap-1"><CheckCircle2 className="w-3 h-3 text-purple-400" /> Proveedor Notificado</span>;
|
||||
case 'dispatched':
|
||||
return <span className="bg-purple-500/20 text-purple-400 border border-purple-500/30 font-bold px-2.5 py-1 rounded-full text-[10px]">En Ruta con Mensajero</span>;
|
||||
return <span className="bg-fuchsia-500/20 text-fuchsia-400 border border-fuchsia-500/30 font-bold px-2.5 py-1 rounded-full text-[10px] flex items-center gap-1"><Truck className="w-3 h-3" /> En Ruta con Mensajero</span>;
|
||||
case 'delivered':
|
||||
return <span className="bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 font-bold px-2.5 py-1 rounded-full text-[10px]">Entregado & Cobrado</span>;
|
||||
case 'cancelled':
|
||||
@ -61,10 +138,12 @@ export default function AdminOrdersPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Filtrado de pedidos
|
||||
const filteredOrders = orders.filter((o) => {
|
||||
const matchesSearch =
|
||||
o.customerName.toLowerCase().includes(search.toLowerCase()) ||
|
||||
o.customerPhone.includes(search) ||
|
||||
o.address.toLowerCase().includes(search.toLowerCase()) ||
|
||||
o.cityName.toLowerCase().includes(search.toLowerCase()) ||
|
||||
o.id.toLowerCase().includes(search.toLowerCase());
|
||||
|
||||
@ -73,201 +152,464 @@ export default function AdminOrdersPage() {
|
||||
return true;
|
||||
});
|
||||
|
||||
// Agrupación por Proveedor
|
||||
const supplierGroups = orders.reduce((acc, order) => {
|
||||
order.items.forEach((item) => {
|
||||
const suppName = item.supplierName || 'DIFUSION JOVIC STORE';
|
||||
const suppPhone = item.supplierPhone || '573134848203';
|
||||
if (!acc[suppName]) {
|
||||
acc[suppName] = {
|
||||
name: suppName,
|
||||
phone: suppPhone,
|
||||
entries: [],
|
||||
totalPairs: 0,
|
||||
totalCost: 0,
|
||||
};
|
||||
}
|
||||
acc[suppName].entries.push({ order, item });
|
||||
acc[suppName].totalPairs += item.quantity;
|
||||
acc[suppName].totalCost += (item.supplierPrice || 0) * item.quantity;
|
||||
});
|
||||
return acc;
|
||||
}, {} as Record<string, { name: string; phone: string; entries: { order: Order; item: OrderItem }[]; totalPairs: number; totalCost: number }>);
|
||||
|
||||
// Agrupación de Pendientes por Entregar (Zonas / Direcciones)
|
||||
const pendingDeliveries = orders.filter((o) => o.status !== 'delivered' && o.status !== 'cancelled');
|
||||
const totalBalanceToCollect = pendingDeliveries.reduce((acc, curr) => acc + curr.balanceDue, 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white flex items-center gap-2">
|
||||
<ShoppingBag className="w-6 h-6 text-purple-400" />
|
||||
<span>Gestión de Pedidos & Separación con Proveedores ({orders.length})</span>
|
||||
</h1>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Control logístico de pedidos contraentrega, cobro de anticipos de $10.000 COP y <strong>notificación inmediata de separación al proveedor</strong> vía WhatsApp.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||
<div className="flex items-center space-x-1.5 bg-neutral-900 border border-neutral-800 p-1 rounded-2xl overflow-x-auto">
|
||||
<button
|
||||
onClick={() => setStatusFilter('all')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'all' ? 'bg-white text-neutral-950 shadow' : 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Todos ({orders.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('pending')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'pending' ? 'bg-amber-500/30 text-amber-300' : 'text-neutral-400'
|
||||
}`}
|
||||
>
|
||||
Pendientes
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('supplier_notified')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'supplier_notified' ? 'bg-purple-500/30 text-purple-300' : 'text-neutral-400'
|
||||
}`}
|
||||
>
|
||||
Proveedor Notificado
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('dispatched')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'dispatched' ? 'bg-purple-500/30 text-purple-300' : 'text-neutral-400'
|
||||
}`}
|
||||
>
|
||||
En Ruta
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-w-xs w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar por cliente, ciudad o # orden..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3.5 py-2 text-xs text-white outline-hidden focus:border-purple-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredOrders.length === 0 ? (
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl p-12 text-center space-y-3">
|
||||
<div className="w-16 h-16 rounded-full bg-neutral-950 text-neutral-500 flex items-center justify-center mx-auto border border-neutral-800">
|
||||
<ShoppingBag className="w-8 h-8 text-purple-400" />
|
||||
</div>
|
||||
<h3 className="font-bold text-white text-base">No hay pedidos en esta sección</h3>
|
||||
<p className="text-xs text-neutral-400 max-w-sm mx-auto">
|
||||
Cuando los clientes hagan un pedido en la tienda web o por WhatsApp, aparecerán aquí con el botón para separar inmediatamente con el proveedor correspondiente.
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white flex items-center gap-2">
|
||||
<ShoppingBag className="w-6 h-6 text-purple-400" />
|
||||
<span>Centro Logístico: Pedidos, Proveedores & Rutas Waze</span>
|
||||
</h1>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Consolidación automática para <strong>separación con proveedores</strong> y <strong>hoja de ruta para mensajeros con navegación Waze en 1 clic</strong>.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
</div>
|
||||
|
||||
{toastMessage && (
|
||||
<div className="bg-purple-950/80 border border-purple-500 text-purple-300 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-md">
|
||||
<span>{toastMessage}</span>
|
||||
<button onClick={() => setToastMessage(null)} className="font-bold text-white cursor-pointer ml-3">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Tabs Navigation */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 p-1 bg-neutral-900 border border-neutral-800 rounded-2xl gap-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('individual')}
|
||||
className={`py-3 px-4 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 cursor-pointer ${
|
||||
activeTab === 'individual'
|
||||
? 'bg-purple-600 text-white shadow-lg shadow-purple-600/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Layers className="w-4 h-4" />
|
||||
<span>1. Todos los Pedidos ({orders.length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('suppliers')}
|
||||
className={`py-3 px-4 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 cursor-pointer ${
|
||||
activeTab === 'suppliers'
|
||||
? 'bg-purple-600 text-white shadow-lg shadow-purple-600/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Building2 className="w-4 h-4 text-fuchsia-300" />
|
||||
<span>2. Consolidado por Proveedor ({Object.keys(supplierGroups).length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('routes')}
|
||||
className={`py-3 px-4 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 cursor-pointer ${
|
||||
activeTab === 'routes'
|
||||
? 'bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg shadow-pink-600/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Compass className="w-4 h-4 text-cyan-300" />
|
||||
<span>3. Rutas de Entrega & Waze ({pendingDeliveries.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* TAB 1: INDIVIDUAL ORDERS LIST */}
|
||||
{activeTab === 'individual' && (
|
||||
<div className="space-y-4">
|
||||
{filteredOrders.map((order) => (
|
||||
<div
|
||||
key={order.id}
|
||||
className="bg-neutral-900 border border-neutral-800 rounded-3xl p-5 space-y-4 shadow-xl"
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-neutral-800 pb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-black text-white text-sm">Orden #{order.id}</span>
|
||||
<span className="text-xs text-neutral-500">• {new Date(order.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</div>
|
||||
{getStatusBadge(order.status)}
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||
<div className="flex items-center space-x-1.5 bg-neutral-900 border border-neutral-800 p-1 rounded-2xl overflow-x-auto">
|
||||
{['all', 'pending', 'deposit_received', 'supplier_notified', 'dispatched', 'delivered'].map((st) => (
|
||||
<button
|
||||
key={st}
|
||||
onClick={() => setStatusFilter(st)}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === st ? 'bg-white text-neutral-950 shadow' : 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{st === 'all' ? 'Todos' : st === 'pending' ? 'Pendientes' : st === 'deposit_received' ? 'Anticipo Pagado' : st === 'supplier_notified' ? 'Prov. Notificado' : st === 'dispatched' ? 'En Ruta' : 'Entregados'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="max-w-xs w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar por cliente, celular, ciudad o #..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3.5 py-2 text-xs text-white outline-hidden focus:border-purple-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Orders Cards Grid */}
|
||||
<div className="space-y-4">
|
||||
{filteredOrders.length === 0 ? (
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl p-12 text-center text-neutral-500">
|
||||
No hay pedidos que coincidan con los filtros.
|
||||
</div>
|
||||
) : (
|
||||
filteredOrders.map((order) => (
|
||||
<div
|
||||
key={order.id}
|
||||
className="bg-neutral-900 border border-neutral-800 hover:border-purple-500/40 rounded-3xl p-5 shadow-xl transition-all space-y-4"
|
||||
>
|
||||
{/* Top Bar: Order ID, Status, Actions */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-neutral-800 pb-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="font-black text-white text-base">Orden #{order.id}</span>
|
||||
{getStatusBadge(order.status)}
|
||||
<span className="text-[11px] text-neutral-500 font-mono">{order.createdAt}</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
||||
<div className="bg-neutral-950/60 border border-neutral-800/80 rounded-2xl p-3.5 space-y-1.5">
|
||||
<span className="text-[10px] text-purple-400 font-bold uppercase block tracking-wider">
|
||||
Cliente & Entrega (24 Horas)
|
||||
</span>
|
||||
<p className="font-bold text-white text-sm">{order.customerName}</p>
|
||||
<p className="text-neutral-300 flex items-center gap-1.5">
|
||||
<Phone className="w-3.5 h-3.5 text-purple-400" /> {order.customerPhone}
|
||||
</p>
|
||||
<p className="text-neutral-300 flex items-center gap-1.5">
|
||||
<MapPin className="w-3.5 h-3.5 text-purple-400" /> {order.address} ({order.cityName})
|
||||
</p>
|
||||
</div>
|
||||
{/* Status Dropdown */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-neutral-400 font-bold">Estado:</span>
|
||||
<select
|
||||
value={order.status}
|
||||
onChange={(e) => handleStatusChange(order.id, e.target.value as any)}
|
||||
className="bg-neutral-950 border border-neutral-800 text-xs text-white rounded-xl px-3 py-1.5 outline-hidden font-bold cursor-pointer"
|
||||
>
|
||||
<option value="pending">⏳ Pendiente Confirmar</option>
|
||||
<option value="deposit_received">💳 Separación $10k Pagada</option>
|
||||
<option value="supplier_notified">🏢 Proveedor Notificado</option>
|
||||
<option value="dispatched">🛵 En Ruta con Mensajero</option>
|
||||
<option value="delivered">✅ Entregado & Cobrado</option>
|
||||
<option value="cancelled">❌ Cancelado</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-neutral-950/60 border border-neutral-800/80 rounded-2xl p-3.5 space-y-2">
|
||||
<span className="text-[10px] text-neutral-400 font-bold uppercase block tracking-wider">
|
||||
Zapatillas & Proveedor Asignado:
|
||||
</span>
|
||||
<div className="space-y-2.5">
|
||||
{order.items.map((it, idx) => {
|
||||
const suppName = it.supplierName || 'DIFUSION JOVIC STORE';
|
||||
return (
|
||||
<div key={idx} className="bg-neutral-900 border border-neutral-800 rounded-xl p-2.5 space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Customer Info & Financials */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
||||
{/* Customer */}
|
||||
<div className="space-y-1 bg-neutral-950/60 p-3 rounded-2xl border border-neutral-800/80">
|
||||
<p className="font-bold text-neutral-400 uppercase text-[10px]">Cliente & Destino</p>
|
||||
<p className="text-white font-bold text-sm">{order.customerName}</p>
|
||||
<p className="text-emerald-400 font-mono font-semibold flex items-center gap-1">
|
||||
<Phone className="w-3 h-3" /> {order.customerPhone}
|
||||
</p>
|
||||
<p className="text-neutral-300 flex items-start gap-1">
|
||||
<MapPin className="w-3 h-3 shrink-0 text-purple-400 mt-0.5" />
|
||||
<span>{order.address} ({order.cityName})</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Financial Balance */}
|
||||
<div className="space-y-1 bg-neutral-950/60 p-3 rounded-2xl border border-neutral-800/80">
|
||||
<p className="font-bold text-neutral-400 uppercase text-[10px]">Resumen de Cobro</p>
|
||||
<div className="flex justify-between text-neutral-300">
|
||||
<span>Total Venta:</span>
|
||||
<strong className="text-white">{formatCOP(order.totalAmount)}</strong>
|
||||
</div>
|
||||
<div className="flex justify-between text-neutral-300">
|
||||
<span>Anticipo / Separación:</span>
|
||||
<strong className="text-blue-400">-{formatCOP(order.depositAmount)}</strong>
|
||||
</div>
|
||||
<div className="flex justify-between text-neutral-300 pt-1 border-t border-neutral-800 font-bold">
|
||||
<span className="text-purple-300">Saldo Contraentrega:</span>
|
||||
<span className="text-emerald-400 text-sm">{formatCOP(order.balanceDue)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Dispatch Actions */}
|
||||
<div className="space-y-2 bg-neutral-950/60 p-3 rounded-2xl border border-neutral-800/80 flex flex-col justify-center">
|
||||
<button
|
||||
onClick={() => handleOpenWaze(order.address, order.cityName)}
|
||||
className="w-full bg-cyan-600 hover:bg-cyan-500 text-white font-bold py-1.5 px-3 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer shadow transition-all text-xs"
|
||||
>
|
||||
<Compass className="w-3.5 h-3.5" />
|
||||
<span>Navegar con Waze</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleNotifyCustomerOnTheWay(order)}
|
||||
className="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-1.5 px-3 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer shadow transition-all text-xs"
|
||||
>
|
||||
<MessageCircle className="w-3.5 h-3.5" />
|
||||
<span>Avisar "Voy en camino"</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Items breakdown with Supplier notification button */}
|
||||
<div className="space-y-2 pt-2">
|
||||
<p className="font-bold text-neutral-400 uppercase text-[10px]">Pares en esta orden:</p>
|
||||
<div className="space-y-2">
|
||||
{order.items.map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 bg-neutral-950 p-3 rounded-2xl border border-neutral-800"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<img src={item.image} alt="" className="w-12 h-12 rounded-xl object-cover border border-neutral-800 shrink-0" />
|
||||
<div>
|
||||
<p className="font-bold text-white text-xs">{it.productName}</p>
|
||||
<p className="font-bold text-white text-xs">{item.productName}</p>
|
||||
<p className="text-[11px] text-neutral-400">
|
||||
Talla <strong className="text-purple-400 font-bold">{it.size}</strong> • Cant: {it.quantity}
|
||||
Talla: <strong className="text-purple-400">{item.size}</strong> | Cantidad: <strong>{item.quantity}</strong> | Proveedor: <strong className="text-purple-300">{item.supplierName || 'Bodega Principal'}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-[10px] bg-neutral-950 text-neutral-300 border border-neutral-800 px-2 py-0.5 rounded font-mono">
|
||||
Prov: {suppName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleNotifySupplier(order, it)}
|
||||
className="w-full bg-emerald-600/20 hover:bg-emerald-600/30 text-emerald-300 border border-emerald-500/30 font-bold text-xs py-1.5 px-2.5 rounded-lg flex items-center justify-center space-x-1.5 transition-all cursor-pointer shadow-xs"
|
||||
onClick={() => handleNotifySingleSupplier(order, item)}
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-3.5 py-2 rounded-xl flex items-center space-x-1.5 shadow cursor-pointer shrink-0 self-end sm:self-auto"
|
||||
>
|
||||
<MessageCircle className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>📲 Separar con {suppName} (WhatsApp)</span>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
<span>Separar con Proveedor</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 2: CONSOLIDADO AGRUPADO POR PROVEEDOR */}
|
||||
{activeTab === 'suppliers' && (
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
<div className="bg-purple-950/40 border border-purple-500/30 rounded-3xl p-5 text-xs text-neutral-300 space-y-2 shadow-lg">
|
||||
<div className="flex items-center space-x-2 text-purple-300 font-bold text-sm">
|
||||
<Building2 className="w-5 h-5 text-purple-400" />
|
||||
<span>Consolidado Automático de Compras & Separaciones</span>
|
||||
</div>
|
||||
<p>
|
||||
Aquí se agrupan todos los pares pendientes clasificados por cada <strong>Proveedor Mayorista</strong>. Puedes enviar el <strong>manifiesto consolidado en 1 solo mensaje de WhatsApp</strong> para que el proveedor prepare y empaque todos los pedidos de la jornada juntos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{Object.values(supplierGroups).map((group) => (
|
||||
<div
|
||||
key={group.name}
|
||||
className="bg-neutral-900 border border-neutral-800 rounded-3xl p-6 shadow-xl space-y-4"
|
||||
>
|
||||
{/* Supplier Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-neutral-800 pb-4">
|
||||
<div>
|
||||
<span className="bg-purple-500/20 text-purple-300 border border-purple-500/40 text-[10px] font-bold px-2.5 py-0.5 rounded-full uppercase tracking-wider">
|
||||
Proveedor Mayorista
|
||||
</span>
|
||||
<h3 className="text-xl font-black text-white mt-1">{group.name}</h3>
|
||||
<p className="text-xs text-neutral-400 flex items-center gap-1 mt-0.5">
|
||||
<Phone className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>WhatsApp Proveedor: <strong>{group.phone}</strong></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-neutral-950 border border-neutral-800 px-4 py-2 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 uppercase font-bold">Total Pares</p>
|
||||
<p className="text-lg font-black text-purple-400">{group.totalPairs} par(es)</p>
|
||||
</div>
|
||||
<div className="bg-neutral-950 border border-neutral-800 px-4 py-2 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 uppercase font-bold">Total a Pagar</p>
|
||||
<p className="text-lg font-black text-emerald-400">{formatCOP(group.totalCost)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-neutral-950/60 border border-neutral-800/80 rounded-2xl p-3.5 space-y-2">
|
||||
<span className="text-[10px] text-neutral-400 font-bold uppercase block tracking-wider">
|
||||
Cobro & Pagos Contraentrega
|
||||
</span>
|
||||
<div className="space-y-1 text-xs">
|
||||
<p className="text-neutral-400 flex justify-between">
|
||||
<span>Total de la Orden:</span>
|
||||
<strong className="text-white font-mono">{formatCOP(order.totalAmount)}</strong>
|
||||
</p>
|
||||
{order.depositAmount > 0 ? (
|
||||
<>
|
||||
<p className="text-amber-300 flex justify-between">
|
||||
<span>Anticipo / Separación:</span>
|
||||
<strong className="font-mono">{formatCOP(order.depositAmount)}</strong>
|
||||
</p>
|
||||
<p className="text-emerald-400 font-bold flex justify-between pt-1 border-t border-neutral-800">
|
||||
<span>Saldo al Mensajero:</span>
|
||||
<strong className="font-mono">{formatCOP(order.balanceDue)}</strong>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-emerald-400 font-bold pt-1 border-t border-neutral-800">
|
||||
100% Contraentrega al recibir
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Items Table for this supplier */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs text-neutral-300">
|
||||
<thead className="bg-neutral-950 text-neutral-400 font-bold uppercase text-[10px]">
|
||||
<tr>
|
||||
<th className="py-2.5 px-3">Modelo / Zapatilla</th>
|
||||
<th className="py-2.5 px-3">Talla</th>
|
||||
<th className="py-2.5 px-3">Cant.</th>
|
||||
<th className="py-2.5 px-3">Costo Base</th>
|
||||
<th className="py-2.5 px-3">Orden Cliente</th>
|
||||
<th className="py-2.5 px-3">Destino</th>
|
||||
<th className="py-2.5 px-3">Estado Orden</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-neutral-800">
|
||||
{group.entries.map((entry, idx) => (
|
||||
<tr key={idx} className="hover:bg-neutral-800/40">
|
||||
<td className="py-2.5 px-3 font-bold text-white flex items-center gap-2">
|
||||
<img src={entry.item.image} alt="" className="w-8 h-8 rounded-lg object-cover border border-neutral-800" />
|
||||
<span>{entry.item.productName}</span>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 font-bold text-purple-400 font-mono">{entry.item.size}</td>
|
||||
<td className="py-2.5 px-3 font-bold">{entry.item.quantity}</td>
|
||||
<td className="py-2.5 px-3 font-mono font-semibold">{formatCOP(entry.item.supplierPrice || 0)}</td>
|
||||
<td className="py-2.5 px-3 font-mono text-neutral-400">#{entry.order.id}</td>
|
||||
<td className="py-2.5 px-3">{entry.order.cityName}</td>
|
||||
<td className="py-2.5 px-3">{getStatusBadge(entry.order.status)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Send Batch WhatsApp Manifest Button */}
|
||||
<div className="pt-2 flex justify-end">
|
||||
<button
|
||||
onClick={() => handleSendSupplierBatchManifest(group.name, group.phone, group.entries)}
|
||||
className="bg-gradient-to-r from-purple-600 via-fuchsia-600 to-pink-600 hover:opacity-95 text-white font-black text-xs px-5 py-3 rounded-2xl shadow-lg flex items-center gap-2 cursor-pointer transition-all active:scale-95"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
<span>Enviar Manifiesto de {group.totalPairs} Pares por WhatsApp a {group.name}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-neutral-800">
|
||||
<span className="text-[11px] text-neutral-500">Método: {order.paymentMethodName}</span>
|
||||
{/* TAB 3: RUTAS DE ENTREGA ORGANIZADAS POR DIRECCIONES & WAZE */}
|
||||
{activeTab === 'routes' && (
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
{/* KPI Route Header */}
|
||||
<div className="bg-gradient-to-r from-neutral-900 via-neutral-900 to-purple-950/60 border border-purple-500/30 rounded-3xl p-6 shadow-xl flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 text-cyan-300 font-bold text-sm">
|
||||
<Compass className="w-5 h-5 text-cyan-400" />
|
||||
<span>Hoja de Ruta & Navegación GPS (Waze / Google Maps)</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-300 mt-1 max-w-xl">
|
||||
Entregas contraentrega secuenciadas por zona geográfica. El mensajero puede presionar <strong>"Abrir en Waze"</strong> para iniciar la navegación instantánea a cada portería/casa.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'deposit_received')}
|
||||
className="bg-blue-600/20 hover:bg-blue-600/30 text-blue-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-blue-500/20"
|
||||
>
|
||||
Confirmar $10k
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'supplier_notified')}
|
||||
className="bg-purple-600/20 hover:bg-purple-600/30 text-purple-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-purple-500/20"
|
||||
>
|
||||
Marcar Proveedor Notificado
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'dispatched')}
|
||||
className="bg-purple-600/20 hover:bg-purple-600/30 text-purple-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-purple-500/20"
|
||||
>
|
||||
Despachar en Ruta
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'delivered')}
|
||||
className="bg-emerald-600/20 hover:bg-emerald-600/30 text-emerald-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-emerald-500/20"
|
||||
>
|
||||
Marcar Entregado
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-neutral-950 border border-neutral-800 p-3 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 uppercase font-bold">Paradas / Entregas</p>
|
||||
<p className="text-xl font-black text-white">{pendingDeliveries.length} Paradas</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-neutral-950 border border-emerald-500/30 p-3 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-emerald-400 uppercase font-bold">Total a Recaudar</p>
|
||||
<p className="text-xl font-black text-emerald-400">{formatCOP(totalBalanceToCollect)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Delivery Stops Sequential List */}
|
||||
<div className="space-y-3">
|
||||
{pendingDeliveries.length === 0 ? (
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl p-12 text-center text-neutral-500">
|
||||
🎉 No hay entregas pendientes por despachar en este momento.
|
||||
</div>
|
||||
) : (
|
||||
pendingDeliveries.map((delivery, index) => (
|
||||
<div
|
||||
key={delivery.id}
|
||||
className="bg-neutral-900 border border-neutral-800 hover:border-cyan-500/40 rounded-3xl p-5 shadow-lg transition-all flex flex-col lg:flex-row lg:items-center justify-between gap-4"
|
||||
>
|
||||
{/* Left: Sequence Number & Stop Details */}
|
||||
<div className="flex items-start space-x-4">
|
||||
{/* Stop Number Badge */}
|
||||
<div className="w-12 h-12 rounded-2xl bg-cyan-500/20 border border-cyan-500/40 flex flex-col items-center justify-center text-cyan-300 font-black shrink-0 shadow-sm">
|
||||
<span className="text-[10px] uppercase font-bold text-neutral-400 leading-none">Parada</span>
|
||||
<span className="text-lg leading-tight">#{index + 1}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-black text-white text-base">{delivery.customerName}</span>
|
||||
<span className="text-xs text-neutral-400">• Orden #{delivery.id}</span>
|
||||
{getStatusBadge(delivery.status)}
|
||||
</div>
|
||||
|
||||
{/* Exact Address Highlight */}
|
||||
<p className="text-xs font-bold text-white flex items-center gap-1.5 bg-neutral-950 px-3 py-1 rounded-xl border border-neutral-800 w-fit">
|
||||
<MapPin className="w-4 h-4 text-cyan-400 shrink-0" />
|
||||
<span>{delivery.address} ({delivery.cityName})</span>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center space-x-3 text-xs text-neutral-400 pt-0.5">
|
||||
<span className="flex items-center gap-1 text-emerald-400 font-semibold">
|
||||
<Phone className="w-3 h-3" /> {delivery.customerPhone}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>
|
||||
{delivery.items.map((i) => `${i.productName} (Talla ${i.size})`).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{delivery.notes && (
|
||||
<p className="text-[11px] text-amber-300/90 italic bg-amber-500/10 px-2.5 py-0.5 rounded-lg border border-amber-500/20 w-fit">
|
||||
Nota: {delivery.notes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Balance & 1-Click Waze / WhatsApp Actions */}
|
||||
<div className="flex items-center gap-3 self-end lg:self-auto shrink-0">
|
||||
{/* Cash to collect badge */}
|
||||
<div className="bg-neutral-950 border border-neutral-800 px-3.5 py-2 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 font-bold uppercase">Cobrar Contraentrega</p>
|
||||
<p className="text-base font-black text-emerald-400">{formatCOP(delivery.balanceDue)}</p>
|
||||
</div>
|
||||
|
||||
{/* Waze GPS Button */}
|
||||
<button
|
||||
onClick={() => handleOpenWaze(delivery.address, delivery.cityName)}
|
||||
className="bg-cyan-600 hover:bg-cyan-500 text-white font-black text-xs px-4 py-3 rounded-2xl shadow-lg flex items-center gap-1.5 cursor-pointer active:scale-95 transition-all"
|
||||
title="Abrir ubicación directamente en Waze"
|
||||
>
|
||||
<Compass className="w-4 h-4" />
|
||||
<span>Waze</span>
|
||||
</button>
|
||||
|
||||
{/* Google Maps GPS Button */}
|
||||
<button
|
||||
onClick={() => handleOpenGoogleMaps(delivery.address, delivery.cityName)}
|
||||
className="bg-neutral-800 hover:bg-neutral-700 text-white font-bold text-xs p-3 rounded-2xl shadow cursor-pointer transition-all"
|
||||
title="Abrir en Google Maps"
|
||||
>
|
||||
<Navigation className="w-4 h-4 text-emerald-400" />
|
||||
</button>
|
||||
|
||||
{/* WhatsApp Alert */}
|
||||
<button
|
||||
onClick={() => handleNotifyCustomerOnTheWay(delivery)}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs px-3.5 py-3 rounded-2xl shadow flex items-center gap-1.5 cursor-pointer active:scale-95 transition-all"
|
||||
title="Enviar mensaje de WhatsApp notificando que el mensajero va en camino"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">En Camino</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -2138,12 +2138,174 @@ export const INITIAL_MONITORED_GROUPS: MonitoredWhatsAppGroup[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const INITIAL_ORDERS: Order[] = [
|
||||
{
|
||||
id: 'ORD-8921',
|
||||
customerName: 'Mateo Valencia',
|
||||
customerPhone: '3168945120',
|
||||
address: 'Av 6N # 24N-32 Edificio Granada Real',
|
||||
cityId: 'city-cali',
|
||||
cityName: 'Cali (Norte - Granada)',
|
||||
items: [
|
||||
{
|
||||
productId: 'snk-jordan4-1',
|
||||
productName: 'Nike Air Jordan 4 Retro White Cement',
|
||||
brand: 'Jordan',
|
||||
size: '41',
|
||||
quantity: 1,
|
||||
price: 135000,
|
||||
supplierPrice: 95000,
|
||||
supplierName: '💎 2️⃣ DIFUSIÓN JOVIC STORE COLOMBIA🍀',
|
||||
supplierPhone: '573134848203',
|
||||
image: 'https://images.unsplash.com/photo-1584735935682-2f2b69dff9d2?auto=format&fit=crop&w=800&q=80',
|
||||
},
|
||||
],
|
||||
subtotal: 135000,
|
||||
shippingCost: 0,
|
||||
totalAmount: 135000,
|
||||
depositAmount: 10000,
|
||||
balanceDue: 125000,
|
||||
paymentMethodId: 'pay-contraentrega',
|
||||
paymentMethodName: 'Contraentrega Efectivo / Nequi',
|
||||
status: 'pending',
|
||||
createdAt: 'Hoy, 06:45 AM',
|
||||
notes: 'Timbrar en apto 302. Paga con billete de 100 y 50.',
|
||||
},
|
||||
{
|
||||
id: 'ORD-8922',
|
||||
customerName: 'Valentina Restrepo',
|
||||
customerPhone: '3157829401',
|
||||
address: 'Calle 52N # 4BN-15 Casa 4 La Flora',
|
||||
cityId: 'city-cali',
|
||||
cityName: 'Cali (Norte - La Flora)',
|
||||
items: [
|
||||
{
|
||||
productId: 'snk-vomero5-1',
|
||||
productName: 'Nike Zoom Vomero 5 Photon Dust Metallic Silver',
|
||||
brand: 'Nike',
|
||||
size: '39',
|
||||
quantity: 1,
|
||||
price: 130000,
|
||||
supplierPrice: 90000,
|
||||
supplierName: '💎 2️⃣ DIFUSIÓN JOVIC STORE COLOMBIA🍀',
|
||||
supplierPhone: '573134848203',
|
||||
image: 'https://images.unsplash.com/photo-1600185365926-3a2ce3cdb9eb?auto=format&fit=crop&w=800&q=80',
|
||||
},
|
||||
],
|
||||
subtotal: 130000,
|
||||
shippingCost: 0,
|
||||
totalAmount: 130000,
|
||||
depositAmount: 10000,
|
||||
balanceDue: 120000,
|
||||
paymentMethodId: 'pay-contraentrega',
|
||||
paymentMethodName: 'Contraentrega Nequi',
|
||||
status: 'pending',
|
||||
createdAt: 'Hoy, 07:15 AM',
|
||||
notes: 'Dejar en portería si no contesta.',
|
||||
},
|
||||
{
|
||||
id: 'ORD-8923',
|
||||
customerName: 'Santiago Henao',
|
||||
customerPhone: '3186523910',
|
||||
address: 'Cra 105 # 14-45 Condominio Portal de la Viga',
|
||||
cityId: 'city-cali',
|
||||
cityName: 'Cali (Sur - Ciudad Jardín)',
|
||||
items: [
|
||||
{
|
||||
productId: 'snk-campus-1',
|
||||
productName: 'Adidas Campus 00s Core Black White',
|
||||
brand: 'Adidas',
|
||||
size: '40',
|
||||
quantity: 1,
|
||||
price: 125000,
|
||||
supplierPrice: 85000,
|
||||
supplierName: 'EXCLUSIVE CC MÍO 3B',
|
||||
supplierPhone: '573159876543',
|
||||
image: 'https://images.unsplash.com/photo-1525966222134-fcfa99b8ae77?auto=format&fit=crop&w=800&q=80',
|
||||
},
|
||||
],
|
||||
subtotal: 125000,
|
||||
shippingCost: 0,
|
||||
totalAmount: 125000,
|
||||
depositAmount: 10000,
|
||||
balanceDue: 115000,
|
||||
paymentMethodId: 'pay-contraentrega',
|
||||
paymentMethodName: 'Contraentrega',
|
||||
status: 'pending',
|
||||
createdAt: 'Hoy, 07:30 AM',
|
||||
notes: 'Recibe en horas de la tarde después de las 2 PM.',
|
||||
},
|
||||
{
|
||||
id: 'ORD-8924',
|
||||
customerName: 'Camila Ospina',
|
||||
customerPhone: '3114958203',
|
||||
address: 'Cra 85E # 14-80 Apto 501 El Ingenio',
|
||||
cityId: 'city-cali',
|
||||
cityName: 'Cali (Sur - El Ingenio)',
|
||||
items: [
|
||||
{
|
||||
productId: 'snk-coach-1',
|
||||
productName: 'Coach Low Top Monogram Signature Brown',
|
||||
brand: 'Coach',
|
||||
size: '38',
|
||||
quantity: 1,
|
||||
price: 145000,
|
||||
supplierPrice: 100000,
|
||||
supplierName: 'EXCLUSIVE CC MÍO 3B',
|
||||
supplierPhone: '573159876543',
|
||||
image: 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80',
|
||||
},
|
||||
],
|
||||
subtotal: 145000,
|
||||
shippingCost: 0,
|
||||
totalAmount: 145000,
|
||||
depositAmount: 10000,
|
||||
balanceDue: 135000,
|
||||
paymentMethodId: 'pay-contraentrega',
|
||||
paymentMethodName: 'Separación Pagada Bold',
|
||||
status: 'deposit_received',
|
||||
createdAt: 'Hoy, 08:00 AM',
|
||||
notes: 'Cliente ya pagó $10.000 de anticipo.',
|
||||
},
|
||||
{
|
||||
id: 'ORD-8925',
|
||||
customerName: 'Daniel Caicedo',
|
||||
customerPhone: '3174829105',
|
||||
address: 'Cra 10 # 3-45 San Antonio',
|
||||
cityId: 'city-cali',
|
||||
cityName: 'Cali (Oeste - San Antonio)',
|
||||
items: [
|
||||
{
|
||||
productId: 'snk-af1-1',
|
||||
productName: 'Nike Air Force 1 07 Low Triple White',
|
||||
brand: 'Nike',
|
||||
size: '42',
|
||||
quantity: 1,
|
||||
price: 120000,
|
||||
supplierPrice: 80000,
|
||||
supplierName: '💎 2️⃣ DIFUSIÓN JOVIC STORE COLOMBIA🍀',
|
||||
supplierPhone: '573134848203',
|
||||
image: 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80',
|
||||
},
|
||||
],
|
||||
subtotal: 120000,
|
||||
shippingCost: 0,
|
||||
totalAmount: 120000,
|
||||
depositAmount: 10000,
|
||||
balanceDue: 110000,
|
||||
paymentMethodId: 'pay-contraentrega',
|
||||
paymentMethodName: 'Contraentrega',
|
||||
status: 'pending',
|
||||
createdAt: 'Hoy, 08:20 AM',
|
||||
},
|
||||
];
|
||||
|
||||
class DataService {
|
||||
private products: Product[] = [...INITIAL_PRODUCTS];
|
||||
private cities: City[] = [...INITIAL_CITIES];
|
||||
private paymentMethods: PaymentMethod[] = [...INITIAL_PAYMENT_METHODS];
|
||||
private suppliers: Supplier[] = [...INITIAL_SUPPLIERS];
|
||||
private orders: Order[] = [];
|
||||
private orders: Order[] = [...INITIAL_ORDERS];
|
||||
private supplierPosts: SupplierPost[] = [...INITIAL_SUPPLIER_POSTS];
|
||||
|
||||
getProducts(): Product[] {
|
||||
|
||||
@ -1,14 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ShoppingBag, Truck, CheckCircle2, Clock, Phone, MapPin, Search, MessageCircle, Send, ShieldCheck, UserCheck } from 'lucide-react';
|
||||
import {
|
||||
ShoppingBag,
|
||||
Truck,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Phone,
|
||||
MapPin,
|
||||
Search,
|
||||
MessageCircle,
|
||||
Send,
|
||||
ShieldCheck,
|
||||
Building2,
|
||||
Navigation,
|
||||
ExternalLink,
|
||||
DollarSign,
|
||||
Layers,
|
||||
Sparkles,
|
||||
Compass,
|
||||
Check,
|
||||
AlertCircle
|
||||
} from 'lucide-react';
|
||||
import { Order, OrderItem } from '@/types';
|
||||
import { formatCOP } from '@/lib/pricing-engine';
|
||||
|
||||
export default function AdminOrdersPage() {
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<'individual' | 'suppliers' | 'routes'>('individual');
|
||||
const [search, setSearch] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/orders')
|
||||
@ -22,14 +44,19 @@ export default function AdminOrdersPage() {
|
||||
prev.map((o) => (o.id === orderId ? { ...o, status: newStatus } : o))
|
||||
);
|
||||
|
||||
await fetch('/api/orders', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: orderId, status: newStatus }),
|
||||
}).catch(() => {});
|
||||
try {
|
||||
await fetch('/api/orders', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: orderId, status: newStatus }),
|
||||
});
|
||||
setToastMessage(`Estado de la orden #${orderId} actualizado a "${newStatus}".`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotifySupplier = (order: Order, item: OrderItem) => {
|
||||
const handleNotifySingleSupplier = (order: Order, item: OrderItem) => {
|
||||
const supplierPhone = item.supplierPhone || '573134848203';
|
||||
const supplierName = item.supplierName || 'Proveedor Mayorista';
|
||||
const supplierCost = item.supplierPrice ? formatCOP(item.supplierPrice) : 'Precio Mayorista';
|
||||
@ -44,6 +71,50 @@ export default function AdminOrdersPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendSupplierBatchManifest = (supplierName: string, supplierPhone: string, itemsList: { order: Order; item: OrderItem }[]) => {
|
||||
const totalPairs = itemsList.reduce((acc, curr) => acc + curr.item.quantity, 0);
|
||||
const totalCost = itemsList.reduce((acc, curr) => acc + (curr.item.supplierPrice || 0) * curr.item.quantity, 0);
|
||||
|
||||
let text = `📦 *MANIFIESTO CONSOLIDADO DE SEPARACIÓN*\n`;
|
||||
text += `🏢 *Proveedor:* ${supplierName}\n`;
|
||||
text += `📅 *Fecha:* ${new Date().toLocaleDateString('es-CO')} | *Total Pares:* ${totalPairs}\n`;
|
||||
text += `💰 *Valor Total a Pagar Proveedor:* ${formatCOP(totalCost)}\n\n`;
|
||||
text += `👟 *DETALLE DE PARES A SEPARAR:*\n`;
|
||||
|
||||
itemsList.forEach((entry, idx) => {
|
||||
text += `\n${idx + 1}. *${entry.item.productName}*\n`;
|
||||
text += ` • Talla: *${entry.item.size}* (x${entry.item.quantity})\n`;
|
||||
text += ` • Costo Base: ${formatCOP(entry.item.supplierPrice || 0)}\n`;
|
||||
text += ` • Orden Cliente: #${entry.order.id} (${entry.order.cityName})\n`;
|
||||
});
|
||||
|
||||
text += `\n🤝 *Por favor confirmar disponibilidad y empaque para recogida del mensajero.* ¡Muchas gracias!`;
|
||||
|
||||
const cleanPhone = supplierPhone.startsWith('57') ? supplierPhone : `57${supplierPhone}`;
|
||||
window.open(`https://wa.me/${cleanPhone}?text=${encodeURIComponent(text)}`, '_blank');
|
||||
setToastMessage(`📲 Manifiesto de ${totalPairs} pares enviado a ${supplierName}.`);
|
||||
};
|
||||
|
||||
const handleOpenWaze = (address: string, city: string) => {
|
||||
const query = encodeURIComponent(`${address}, ${city}, Colombia`);
|
||||
const wazeUrl = `https://waze.com/ul?q=${query}&navigate=yes`;
|
||||
window.open(wazeUrl, '_blank');
|
||||
};
|
||||
|
||||
const handleOpenGoogleMaps = (address: string, city: string) => {
|
||||
const query = encodeURIComponent(`${address}, ${city}, Colombia`);
|
||||
const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${query}`;
|
||||
window.open(mapsUrl, '_blank');
|
||||
};
|
||||
|
||||
const handleNotifyCustomerOnTheWay = (order: Order) => {
|
||||
const cleanPhone = order.customerPhone.startsWith('57') ? order.customerPhone : `57${order.customerPhone}`;
|
||||
const text = `👋 ¡Hola *${order.customerName}*!\n\n🛵 Tu pedido ya va *EN CAMINO* con nuestro mensajero.\n\n📍 *Dirección de Entrega:* ${order.address}\n🏙️ *Ciudad:* ${order.cityName}\n💵 *Saldo a Pagar Contraentrega:* *${formatCOP(order.balanceDue)}*\n💳 *Medio de Pago:* Efectivo o Transferencia Nequi\n\nPor favor estar atento a la portería o celular. ¡Gracias por tu compra! 🔥`;
|
||||
|
||||
window.open(`https://wa.me/${cleanPhone}?text=${encodeURIComponent(text)}`, '_blank');
|
||||
handleStatusChange(order.id, 'dispatched');
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: Order['status']) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
@ -51,9 +122,9 @@ export default function AdminOrdersPage() {
|
||||
case 'deposit_received':
|
||||
return <span className="bg-blue-500/20 text-blue-400 border border-blue-500/30 font-bold px-2.5 py-1 rounded-full text-[10px]">Separación $10k Pagada</span>;
|
||||
case 'supplier_notified':
|
||||
return <span className="bg-orange-500/20 text-orange-300 border border-orange-500/30 font-bold px-2.5 py-1 rounded-full text-[10px] flex items-center gap-1"><CheckCircle2 className="w-3 h-3 text-orange-400" /> Proveedor Notificado</span>;
|
||||
return <span className="bg-purple-500/20 text-purple-300 border border-purple-500/30 font-bold px-2.5 py-1 rounded-full text-[10px] flex items-center gap-1"><CheckCircle2 className="w-3 h-3 text-purple-400" /> Proveedor Notificado</span>;
|
||||
case 'dispatched':
|
||||
return <span className="bg-purple-500/20 text-purple-400 border border-purple-500/30 font-bold px-2.5 py-1 rounded-full text-[10px]">En Ruta con Mensajero</span>;
|
||||
return <span className="bg-fuchsia-500/20 text-fuchsia-400 border border-fuchsia-500/30 font-bold px-2.5 py-1 rounded-full text-[10px] flex items-center gap-1"><Truck className="w-3 h-3" /> En Ruta con Mensajero</span>;
|
||||
case 'delivered':
|
||||
return <span className="bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 font-bold px-2.5 py-1 rounded-full text-[10px]">Entregado & Cobrado</span>;
|
||||
case 'cancelled':
|
||||
@ -65,6 +136,7 @@ export default function AdminOrdersPage() {
|
||||
const matchesSearch =
|
||||
o.customerName.toLowerCase().includes(search.toLowerCase()) ||
|
||||
o.customerPhone.includes(search) ||
|
||||
o.address.toLowerCase().includes(search.toLowerCase()) ||
|
||||
o.cityName.toLowerCase().includes(search.toLowerCase()) ||
|
||||
o.id.toLowerCase().includes(search.toLowerCase());
|
||||
|
||||
@ -73,201 +145,440 @@ export default function AdminOrdersPage() {
|
||||
return true;
|
||||
});
|
||||
|
||||
const supplierGroups = orders.reduce((acc, order) => {
|
||||
order.items.forEach((item) => {
|
||||
const suppName = item.supplierName || 'DIFUSION JOVIC STORE';
|
||||
const suppPhone = item.supplierPhone || '573134848203';
|
||||
if (!acc[suppName]) {
|
||||
acc[suppName] = {
|
||||
name: suppName,
|
||||
phone: suppPhone,
|
||||
entries: [],
|
||||
totalPairs: 0,
|
||||
totalCost: 0,
|
||||
};
|
||||
}
|
||||
acc[suppName].entries.push({ order, item });
|
||||
acc[suppName].totalPairs += item.quantity;
|
||||
acc[suppName].totalCost += (item.supplierPrice || 0) * item.quantity;
|
||||
});
|
||||
return acc;
|
||||
}, {} as Record<string, { name: string; phone: string; entries: { order: Order; item: OrderItem }[]; totalPairs: number; totalCost: number }>);
|
||||
|
||||
const pendingDeliveries = orders.filter((o) => o.status !== 'delivered' && o.status !== 'cancelled');
|
||||
const totalBalanceToCollect = pendingDeliveries.reduce((acc, curr) => acc + curr.balanceDue, 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white flex items-center gap-2">
|
||||
<ShoppingBag className="w-6 h-6 text-orange-500" />
|
||||
<span>Gestión de Pedidos & Separación con Proveedores ({orders.length})</span>
|
||||
</h1>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Control logístico de pedidos contraentrega, cobro de anticipos de $10.000 COP y <strong>notificación inmediata de separación al proveedor</strong> vía WhatsApp.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||
<div className="flex items-center space-x-1.5 bg-gray-900 border border-gray-800 p-1 rounded-2xl overflow-x-auto">
|
||||
<button
|
||||
onClick={() => setStatusFilter('all')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'all' ? 'bg-white text-gray-950 shadow' : 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Todos ({orders.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('pending')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'pending' ? 'bg-amber-500/30 text-amber-300' : 'text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Pendientes
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('supplier_notified')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'supplier_notified' ? 'bg-orange-500/30 text-orange-300' : 'text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Proveedor Notificado
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('dispatched')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'dispatched' ? 'bg-purple-500/30 text-purple-300' : 'text-gray-400'
|
||||
}`}
|
||||
>
|
||||
En Ruta
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-w-xs w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar por cliente, ciudad o # orden..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full bg-gray-900 border border-gray-800 rounded-xl px-3.5 py-2 text-xs text-white outline-hidden focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredOrders.length === 0 ? (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-3xl p-12 text-center space-y-3">
|
||||
<div className="w-16 h-16 rounded-full bg-gray-950 text-gray-500 flex items-center justify-center mx-auto border border-gray-800">
|
||||
<ShoppingBag className="w-8 h-8 text-orange-500" />
|
||||
</div>
|
||||
<h3 className="font-bold text-white text-base">No hay pedidos en esta sección</h3>
|
||||
<p className="text-xs text-gray-400 max-w-sm mx-auto">
|
||||
Cuando los clientes hagan un pedido en la tienda web o por WhatsApp, aparecerán aquí con el botón para separar inmediatamente con el proveedor correspondiente.
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white flex items-center gap-2">
|
||||
<ShoppingBag className="w-6 h-6 text-purple-400" />
|
||||
<span>Centro Logístico: Pedidos, Proveedores & Rutas Waze</span>
|
||||
</h1>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Consolidación automática para <strong>separación con proveedores</strong> y <strong>hoja de ruta para mensajeros con navegación Waze en 1 clic</strong>.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
</div>
|
||||
|
||||
{toastMessage && (
|
||||
<div className="bg-purple-950/80 border border-purple-500 text-purple-300 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-md">
|
||||
<span>{toastMessage}</span>
|
||||
<button onClick={() => setToastMessage(null)} className="font-bold text-white cursor-pointer ml-3">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Tabs Navigation */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 p-1 bg-neutral-900 border border-neutral-800 rounded-2xl gap-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('individual')}
|
||||
className={`py-3 px-4 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 cursor-pointer ${
|
||||
activeTab === 'individual'
|
||||
? 'bg-purple-600 text-white shadow-lg shadow-purple-600/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Layers className="w-4 h-4" />
|
||||
<span>1. Todos los Pedidos ({orders.length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('suppliers')}
|
||||
className={`py-3 px-4 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 cursor-pointer ${
|
||||
activeTab === 'suppliers'
|
||||
? 'bg-purple-600 text-white shadow-lg shadow-purple-600/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Building2 className="w-4 h-4 text-fuchsia-300" />
|
||||
<span>2. Consolidado por Proveedor ({Object.keys(supplierGroups).length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('routes')}
|
||||
className={`py-3 px-4 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 cursor-pointer ${
|
||||
activeTab === 'routes'
|
||||
? 'bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg shadow-pink-600/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Compass className="w-4 h-4 text-cyan-300" />
|
||||
<span>3. Rutas de Entrega & Waze ({pendingDeliveries.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* TAB 1: INDIVIDUAL ORDERS LIST */}
|
||||
{activeTab === 'individual' && (
|
||||
<div className="space-y-4">
|
||||
{filteredOrders.map((order) => (
|
||||
<div
|
||||
key={order.id}
|
||||
className="bg-gray-900 border border-gray-800 rounded-3xl p-5 space-y-4 shadow-xl"
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-gray-800 pb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-black text-white text-sm">Orden #{order.id}</span>
|
||||
<span className="text-xs text-gray-500">• {new Date(order.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</div>
|
||||
{getStatusBadge(order.status)}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||
<div className="flex items-center space-x-1.5 bg-neutral-900 border border-neutral-800 p-1 rounded-2xl overflow-x-auto">
|
||||
{['all', 'pending', 'deposit_received', 'supplier_notified', 'dispatched', 'delivered'].map((st) => (
|
||||
<button
|
||||
key={st}
|
||||
onClick={() => setStatusFilter(st)}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === st ? 'bg-white text-neutral-950 shadow' : 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{st === 'all' ? 'Todos' : st === 'pending' ? 'Pendientes' : st === 'deposit_received' ? 'Anticipo Pagado' : st === 'supplier_notified' ? 'Prov. Notificado' : st === 'dispatched' ? 'En Ruta' : 'Entregados'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="max-w-xs w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar por cliente, celular, ciudad o #..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3.5 py-2 text-xs text-white outline-hidden focus:border-purple-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{filteredOrders.length === 0 ? (
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl p-12 text-center text-neutral-500">
|
||||
No hay pedidos que coincidan con los filtros.
|
||||
</div>
|
||||
) : (
|
||||
filteredOrders.map((order) => (
|
||||
<div
|
||||
key={order.id}
|
||||
className="bg-neutral-900 border border-neutral-800 hover:border-purple-500/40 rounded-3xl p-5 shadow-xl transition-all space-y-4"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-neutral-800 pb-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="font-black text-white text-base">Orden #{order.id}</span>
|
||||
{getStatusBadge(order.status)}
|
||||
<span className="text-[11px] text-neutral-500 font-mono">{order.createdAt}</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
||||
<div className="bg-gray-950/60 border border-gray-800/80 rounded-2xl p-3.5 space-y-1.5">
|
||||
<span className="text-[10px] text-orange-400 font-bold uppercase block tracking-wider">
|
||||
Cliente & Entrega (24 Horas)
|
||||
</span>
|
||||
<p className="font-bold text-white text-sm">{order.customerName}</p>
|
||||
<p className="text-gray-300 flex items-center gap-1.5">
|
||||
<Phone className="w-3.5 h-3.5 text-orange-400" /> {order.customerPhone}
|
||||
</p>
|
||||
<p className="text-gray-300 flex items-center gap-1.5">
|
||||
<MapPin className="w-3.5 h-3.5 text-orange-400" /> {order.address} ({order.cityName})
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-neutral-400 font-bold">Estado:</span>
|
||||
<select
|
||||
value={order.status}
|
||||
onChange={(e) => handleStatusChange(order.id, e.target.value as any)}
|
||||
className="bg-neutral-950 border border-neutral-800 text-xs text-white rounded-xl px-3 py-1.5 outline-hidden font-bold cursor-pointer"
|
||||
>
|
||||
<option value="pending">⏳ Pendiente Confirmar</option>
|
||||
<option value="deposit_received">💳 Separación $10k Pagada</option>
|
||||
<option value="supplier_notified">🏢 Proveedor Notificado</option>
|
||||
<option value="dispatched">🛵 En Ruta con Mensajero</option>
|
||||
<option value="delivered">✅ Entregado & Cobrado</option>
|
||||
<option value="cancelled">❌ Cancelado</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-950/60 border border-gray-800/80 rounded-2xl p-3.5 space-y-2">
|
||||
<span className="text-[10px] text-gray-400 font-bold uppercase block tracking-wider">
|
||||
Zapatillas & Proveedor Asignado:
|
||||
</span>
|
||||
<div className="space-y-2.5">
|
||||
{order.items.map((it, idx) => {
|
||||
const suppName = it.supplierName || 'DIFUSION JOVIC STORE';
|
||||
return (
|
||||
<div key={idx} className="bg-gray-900 border border-gray-800 rounded-xl p-2.5 space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
||||
<div className="space-y-1 bg-neutral-950/60 p-3 rounded-2xl border border-neutral-800/80">
|
||||
<p className="font-bold text-neutral-400 uppercase text-[10px]">Cliente & Destino</p>
|
||||
<p className="text-white font-bold text-sm">{order.customerName}</p>
|
||||
<p className="text-emerald-400 font-mono font-semibold flex items-center gap-1">
|
||||
<Phone className="w-3 h-3" /> {order.customerPhone}
|
||||
</p>
|
||||
<p className="text-neutral-300 flex items-start gap-1">
|
||||
<MapPin className="w-3 h-3 shrink-0 text-purple-400 mt-0.5" />
|
||||
<span>{order.address} ({order.cityName})</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 bg-neutral-950/60 p-3 rounded-2xl border border-neutral-800/80">
|
||||
<p className="font-bold text-neutral-400 uppercase text-[10px]">Resumen de Cobro</p>
|
||||
<div className="flex justify-between text-neutral-300">
|
||||
<span>Total Venta:</span>
|
||||
<strong className="text-white">{formatCOP(order.totalAmount)}</strong>
|
||||
</div>
|
||||
<div className="flex justify-between text-neutral-300">
|
||||
<span>Anticipo / Separación:</span>
|
||||
<strong className="text-blue-400">-{formatCOP(order.depositAmount)}</strong>
|
||||
</div>
|
||||
<div className="flex justify-between text-neutral-300 pt-1 border-t border-neutral-800 font-bold">
|
||||
<span className="text-purple-300">Saldo Contraentrega:</span>
|
||||
<span className="text-emerald-400 text-sm">{formatCOP(order.balanceDue)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 bg-neutral-950/60 p-3 rounded-2xl border border-neutral-800/80 flex flex-col justify-center">
|
||||
<button
|
||||
onClick={() => handleOpenWaze(order.address, order.cityName)}
|
||||
className="w-full bg-cyan-600 hover:bg-cyan-500 text-white font-bold py-1.5 px-3 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer shadow transition-all text-xs"
|
||||
>
|
||||
<Compass className="w-3.5 h-3.5" />
|
||||
<span>Navegar con Waze</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleNotifyCustomerOnTheWay(order)}
|
||||
className="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-1.5 px-3 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer shadow transition-all text-xs"
|
||||
>
|
||||
<MessageCircle className="w-3.5 h-3.5" />
|
||||
<span>Avisar "Voy en camino"</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pt-2">
|
||||
<p className="font-bold text-neutral-400 uppercase text-[10px]">Pares en esta orden:</p>
|
||||
<div className="space-y-2">
|
||||
{order.items.map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 bg-neutral-950 p-3 rounded-2xl border border-neutral-800"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<img src={item.image} alt="" className="w-12 h-12 rounded-xl object-cover border border-neutral-800 shrink-0" />
|
||||
<div>
|
||||
<p className="font-bold text-white text-xs">{it.productName}</p>
|
||||
<p className="text-[11px] text-gray-400">
|
||||
Talla <strong className="text-orange-400 font-bold">{it.size}</strong> • Cant: {it.quantity}
|
||||
<p className="font-bold text-white text-xs">{item.productName}</p>
|
||||
<p className="text-[11px] text-neutral-400">
|
||||
Talla: <strong className="text-purple-400">{item.size}</strong> | Cantidad: <strong>{item.quantity}</strong> | Proveedor: <strong className="text-purple-300">{item.supplierName || 'Bodega Principal'}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-[10px] bg-gray-950 text-gray-300 border border-gray-800 px-2 py-0.5 rounded font-mono">
|
||||
Prov: {suppName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleNotifySupplier(order, it)}
|
||||
className="w-full bg-emerald-600/20 hover:bg-emerald-600/30 text-emerald-300 border border-emerald-500/30 font-bold text-xs py-1.5 px-2.5 rounded-lg flex items-center justify-center space-x-1.5 transition-all cursor-pointer shadow-xs"
|
||||
onClick={() => handleNotifySingleSupplier(order, item)}
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-3.5 py-2 rounded-xl flex items-center space-x-1.5 shadow cursor-pointer shrink-0 self-end sm:self-auto"
|
||||
>
|
||||
<MessageCircle className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>📲 Separar con {suppName} (WhatsApp)</span>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
<span>Separar con Proveedor</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 2: CONSOLIDADO AGRUPADO POR PROVEEDOR */}
|
||||
{activeTab === 'suppliers' && (
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
<div className="bg-purple-950/40 border border-purple-500/30 rounded-3xl p-5 text-xs text-neutral-300 space-y-2 shadow-lg">
|
||||
<div className="flex items-center space-x-2 text-purple-300 font-bold text-sm">
|
||||
<Building2 className="w-5 h-5 text-purple-400" />
|
||||
<span>Consolidado Automático de Compras & Separaciones</span>
|
||||
</div>
|
||||
<p>
|
||||
Aquí se agrupan todos los pares pendientes clasificados por cada <strong>Proveedor Mayorista</strong>. Puedes enviar el <strong>manifiesto consolidado en 1 solo mensaje de WhatsApp</strong> para que el proveedor prepare y empaque todos los pedidos de la jornada juntos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{Object.values(supplierGroups).map((group) => (
|
||||
<div
|
||||
key={group.name}
|
||||
className="bg-neutral-900 border border-neutral-800 rounded-3xl p-6 shadow-xl space-y-4"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-neutral-800 pb-4">
|
||||
<div>
|
||||
<span className="bg-purple-500/20 text-purple-300 border border-purple-500/40 text-[10px] font-bold px-2.5 py-0.5 rounded-full uppercase tracking-wider">
|
||||
Proveedor Mayorista
|
||||
</span>
|
||||
<h3 className="text-xl font-black text-white mt-1">{group.name}</h3>
|
||||
<p className="text-xs text-neutral-400 flex items-center gap-1 mt-0.5">
|
||||
<Phone className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>WhatsApp Proveedor: <strong>{group.phone}</strong></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-neutral-950 border border-neutral-800 px-4 py-2 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 uppercase font-bold">Total Pares</p>
|
||||
<p className="text-lg font-black text-purple-400">{group.totalPairs} par(es)</p>
|
||||
</div>
|
||||
<div className="bg-neutral-950 border border-neutral-800 px-4 py-2 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 uppercase font-bold">Total a Pagar</p>
|
||||
<p className="text-lg font-black text-emerald-400">{formatCOP(group.totalCost)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-950/60 border border-gray-800/80 rounded-2xl p-3.5 space-y-2">
|
||||
<span className="text-[10px] text-gray-400 font-bold uppercase block tracking-wider">
|
||||
Cobro & Pagos Contraentrega
|
||||
</span>
|
||||
<div className="space-y-1 text-xs">
|
||||
<p className="text-gray-400 flex justify-between">
|
||||
<span>Total de la Orden:</span>
|
||||
<strong className="text-white font-mono">{formatCOP(order.totalAmount)}</strong>
|
||||
</p>
|
||||
{order.depositAmount > 0 ? (
|
||||
<>
|
||||
<p className="text-amber-300 flex justify-between">
|
||||
<span>Anticipo / Separación:</span>
|
||||
<strong className="font-mono">{formatCOP(order.depositAmount)}</strong>
|
||||
</p>
|
||||
<p className="text-emerald-400 font-bold flex justify-between pt-1 border-t border-gray-800">
|
||||
<span>Saldo al Mensajero:</span>
|
||||
<strong className="font-mono">{formatCOP(order.balanceDue)}</strong>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-emerald-400 font-bold pt-1 border-t border-gray-800">
|
||||
100% Contraentrega al recibir
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs text-neutral-300">
|
||||
<thead className="bg-neutral-950 text-neutral-400 font-bold uppercase text-[10px]">
|
||||
<tr>
|
||||
<th className="py-2.5 px-3">Modelo / Zapatilla</th>
|
||||
<th className="py-2.5 px-3">Talla</th>
|
||||
<th className="py-2.5 px-3">Cant.</th>
|
||||
<th className="py-2.5 px-3">Costo Base</th>
|
||||
<th className="py-2.5 px-3">Orden Cliente</th>
|
||||
<th className="py-2.5 px-3">Destino</th>
|
||||
<th className="py-2.5 px-3">Estado Orden</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-neutral-800">
|
||||
{group.entries.map((entry, idx) => (
|
||||
<tr key={idx} className="hover:bg-neutral-800/40">
|
||||
<td className="py-2.5 px-3 font-bold text-white flex items-center gap-2">
|
||||
<img src={entry.item.image} alt="" className="w-8 h-8 rounded-lg object-cover border border-neutral-800" />
|
||||
<span>{entry.item.productName}</span>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 font-bold text-purple-400 font-mono">{entry.item.size}</td>
|
||||
<td className="py-2.5 px-3 font-bold">{entry.item.quantity}</td>
|
||||
<td className="py-2.5 px-3 font-mono font-semibold">{formatCOP(entry.item.supplierPrice || 0)}</td>
|
||||
<td className="py-2.5 px-3 font-mono text-neutral-400">#{entry.order.id}</td>
|
||||
<td className="py-2.5 px-3">{entry.order.cityName}</td>
|
||||
<td className="py-2.5 px-3">{getStatusBadge(entry.order.status)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 flex justify-end">
|
||||
<button
|
||||
onClick={() => handleSendSupplierBatchManifest(group.name, group.phone, group.entries)}
|
||||
className="bg-gradient-to-r from-purple-600 via-fuchsia-600 to-pink-600 hover:opacity-95 text-white font-black text-xs px-5 py-3 rounded-2xl shadow-lg flex items-center gap-2 cursor-pointer transition-all active:scale-95"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
<span>Enviar Manifiesto de {group.totalPairs} Pares por WhatsApp a {group.name}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-gray-800">
|
||||
<span className="text-[11px] text-gray-500">Método: {order.paymentMethodName}</span>
|
||||
{/* TAB 3: RUTAS DE ENTREGA ORGANIZADAS POR DIRECCIONES & WAZE */}
|
||||
{activeTab === 'routes' && (
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
<div className="bg-gradient-to-r from-neutral-900 via-neutral-900 to-purple-950/60 border border-purple-500/30 rounded-3xl p-6 shadow-xl flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 text-cyan-300 font-bold text-sm">
|
||||
<Compass className="w-5 h-5 text-cyan-400" />
|
||||
<span>Hoja de Ruta & Navegación GPS (Waze / Google Maps)</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-300 mt-1 max-w-xl">
|
||||
Entregas contraentrega secuenciadas por zona geográfica. El mensajero puede presionar <strong>"Abrir en Waze"</strong> para iniciar la navegación instantánea a cada portería/casa.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'deposit_received')}
|
||||
className="bg-blue-600/20 hover:bg-blue-600/30 text-blue-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-blue-500/20"
|
||||
>
|
||||
Confirmar $10k
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'supplier_notified')}
|
||||
className="bg-orange-600/20 hover:bg-orange-600/30 text-orange-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-orange-500/20"
|
||||
>
|
||||
Marcar Proveedor Notificado
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'dispatched')}
|
||||
className="bg-purple-600/20 hover:bg-purple-600/30 text-purple-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-purple-500/20"
|
||||
>
|
||||
Despachar en Ruta
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'delivered')}
|
||||
className="bg-emerald-600/20 hover:bg-emerald-600/30 text-emerald-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-emerald-500/20"
|
||||
>
|
||||
Marcar Entregado
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-neutral-950 border border-neutral-800 p-3 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 uppercase font-bold">Paradas / Entregas</p>
|
||||
<p className="text-xl font-black text-white">{pendingDeliveries.length} Paradas</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-neutral-950 border border-emerald-500/30 p-3 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-emerald-400 uppercase font-bold">Total a Recaudar</p>
|
||||
<p className="text-xl font-black text-emerald-400">{formatCOP(totalBalanceToCollect)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{pendingDeliveries.length === 0 ? (
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl p-12 text-center text-neutral-500">
|
||||
🎉 No hay entregas pendientes por despachar en este momento.
|
||||
</div>
|
||||
) : (
|
||||
pendingDeliveries.map((delivery, index) => (
|
||||
<div
|
||||
key={delivery.id}
|
||||
className="bg-neutral-900 border border-neutral-800 hover:border-cyan-500/40 rounded-3xl p-5 shadow-lg transition-all flex flex-col lg:flex-row lg:items-center justify-between gap-4"
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-cyan-500/20 border border-cyan-500/40 flex flex-col items-center justify-center text-cyan-300 font-black shrink-0 shadow-sm">
|
||||
<span className="text-[10px] uppercase font-bold text-neutral-400 leading-none">Parada</span>
|
||||
<span className="text-lg leading-tight">#{index + 1}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-black text-white text-base">{delivery.customerName}</span>
|
||||
<span className="text-xs text-neutral-400">• Orden #{delivery.id}</span>
|
||||
{getStatusBadge(delivery.status)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs font-bold text-white flex items-center gap-1.5 bg-neutral-950 px-3 py-1 rounded-xl border border-neutral-800 w-fit">
|
||||
<MapPin className="w-4 h-4 text-cyan-400 shrink-0" />
|
||||
<span>{delivery.address} ({delivery.cityName})</span>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center space-x-3 text-xs text-neutral-400 pt-0.5">
|
||||
<span className="flex items-center gap-1 text-emerald-400 font-semibold">
|
||||
<Phone className="w-3 h-3" /> {delivery.customerPhone}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>
|
||||
{delivery.items.map((i) => `${i.productName} (Talla ${i.size})`).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{delivery.notes && (
|
||||
<p className="text-[11px] text-amber-300/90 italic bg-amber-500/10 px-2.5 py-0.5 rounded-lg border border-amber-500/20 w-fit">
|
||||
Nota: {delivery.notes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 self-end lg:self-auto shrink-0">
|
||||
<div className="bg-neutral-950 border border-neutral-800 px-3.5 py-2 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 font-bold uppercase">Cobrar Contraentrega</p>
|
||||
<p className="text-base font-black text-emerald-400">{formatCOP(delivery.balanceDue)}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleOpenWaze(delivery.address, delivery.cityName)}
|
||||
className="bg-cyan-600 hover:bg-cyan-500 text-white font-black text-xs px-4 py-3 rounded-2xl shadow-lg flex items-center gap-1.5 cursor-pointer active:scale-95 transition-all"
|
||||
title="Abrir ubicación directamente en Waze"
|
||||
>
|
||||
<Compass className="w-4 h-4" />
|
||||
<span>Waze</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleOpenGoogleMaps(delivery.address, delivery.cityName)}
|
||||
className="bg-neutral-800 hover:bg-neutral-700 text-white font-bold text-xs p-3 rounded-2xl shadow cursor-pointer transition-all"
|
||||
title="Abrir en Google Maps"
|
||||
>
|
||||
<Navigation className="w-4 h-4 text-emerald-400" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleNotifyCustomerOnTheWay(delivery)}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs px-3.5 py-3 rounded-2xl shadow flex items-center gap-1.5 cursor-pointer active:scale-95 transition-all"
|
||||
title="Enviar mensaje de WhatsApp notificando que el mensajero va en camino"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">En Camino</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,14 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ShoppingBag, Truck, CheckCircle2, Clock, Phone, MapPin, Search, MessageCircle, Send, ShieldCheck, UserCheck } from 'lucide-react';
|
||||
import {
|
||||
ShoppingBag,
|
||||
Truck,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Phone,
|
||||
MapPin,
|
||||
Search,
|
||||
MessageCircle,
|
||||
Send,
|
||||
ShieldCheck,
|
||||
Building2,
|
||||
Navigation,
|
||||
ExternalLink,
|
||||
DollarSign,
|
||||
Layers,
|
||||
Sparkles,
|
||||
Compass,
|
||||
Check,
|
||||
AlertCircle
|
||||
} from 'lucide-react';
|
||||
import { Order, OrderItem } from '@/types';
|
||||
import { formatCOP } from '@/lib/pricing-engine';
|
||||
|
||||
export default function AdminOrdersPage() {
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<'individual' | 'suppliers' | 'routes'>('individual');
|
||||
const [search, setSearch] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/orders')
|
||||
@ -22,14 +44,19 @@ export default function AdminOrdersPage() {
|
||||
prev.map((o) => (o.id === orderId ? { ...o, status: newStatus } : o))
|
||||
);
|
||||
|
||||
await fetch('/api/orders', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: orderId, status: newStatus }),
|
||||
}).catch(() => {});
|
||||
try {
|
||||
await fetch('/api/orders', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: orderId, status: newStatus }),
|
||||
});
|
||||
setToastMessage(`Estado de la orden #${orderId} actualizado a "${newStatus}".`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotifySupplier = (order: Order, item: OrderItem) => {
|
||||
const handleNotifySingleSupplier = (order: Order, item: OrderItem) => {
|
||||
const supplierPhone = item.supplierPhone || '573134848203';
|
||||
const supplierName = item.supplierName || 'Proveedor Mayorista';
|
||||
const supplierCost = item.supplierPrice ? formatCOP(item.supplierPrice) : 'Precio Mayorista';
|
||||
@ -44,6 +71,50 @@ export default function AdminOrdersPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendSupplierBatchManifest = (supplierName: string, supplierPhone: string, itemsList: { order: Order; item: OrderItem }[]) => {
|
||||
const totalPairs = itemsList.reduce((acc, curr) => acc + curr.item.quantity, 0);
|
||||
const totalCost = itemsList.reduce((acc, curr) => acc + (curr.item.supplierPrice || 0) * curr.item.quantity, 0);
|
||||
|
||||
let text = `📦 *MANIFIESTO CONSOLIDADO DE SEPARACIÓN*\n`;
|
||||
text += `🏢 *Proveedor:* ${supplierName}\n`;
|
||||
text += `📅 *Fecha:* ${new Date().toLocaleDateString('es-CO')} | *Total Pares:* ${totalPairs}\n`;
|
||||
text += `💰 *Valor Total a Pagar Proveedor:* ${formatCOP(totalCost)}\n\n`;
|
||||
text += `👟 *DETALLE DE PARES A SEPARAR:*\n`;
|
||||
|
||||
itemsList.forEach((entry, idx) => {
|
||||
text += `\n${idx + 1}. *${entry.item.productName}*\n`;
|
||||
text += ` • Talla: *${entry.item.size}* (x${entry.item.quantity})\n`;
|
||||
text += ` • Costo Base: ${formatCOP(entry.item.supplierPrice || 0)}\n`;
|
||||
text += ` • Orden Cliente: #${entry.order.id} (${entry.order.cityName})\n`;
|
||||
});
|
||||
|
||||
text += `\n🤝 *Por favor confirmar disponibilidad y empaque para recogida del mensajero.* ¡Muchas gracias!`;
|
||||
|
||||
const cleanPhone = supplierPhone.startsWith('57') ? supplierPhone : `57${supplierPhone}`;
|
||||
window.open(`https://wa.me/${cleanPhone}?text=${encodeURIComponent(text)}`, '_blank');
|
||||
setToastMessage(`📲 Manifiesto de ${totalPairs} pares enviado a ${supplierName}.`);
|
||||
};
|
||||
|
||||
const handleOpenWaze = (address: string, city: string) => {
|
||||
const query = encodeURIComponent(`${address}, ${city}, Colombia`);
|
||||
const wazeUrl = `https://waze.com/ul?q=${query}&navigate=yes`;
|
||||
window.open(wazeUrl, '_blank');
|
||||
};
|
||||
|
||||
const handleOpenGoogleMaps = (address: string, city: string) => {
|
||||
const query = encodeURIComponent(`${address}, ${city}, Colombia`);
|
||||
const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${query}`;
|
||||
window.open(mapsUrl, '_blank');
|
||||
};
|
||||
|
||||
const handleNotifyCustomerOnTheWay = (order: Order) => {
|
||||
const cleanPhone = order.customerPhone.startsWith('57') ? order.customerPhone : `57${order.customerPhone}`;
|
||||
const text = `👋 ¡Hola *${order.customerName}*!\n\n🛵 Tu pedido ya va *EN CAMINO* con nuestro mensajero.\n\n📍 *Dirección de Entrega:* ${order.address}\n🏙️ *Ciudad:* ${order.cityName}\n💵 *Saldo a Pagar Contraentrega:* *${formatCOP(order.balanceDue)}*\n💳 *Medio de Pago:* Efectivo o Transferencia Nequi\n\nPor favor estar atento a la portería o celular. ¡Gracias por tu compra! 🔥`;
|
||||
|
||||
window.open(`https://wa.me/${cleanPhone}?text=${encodeURIComponent(text)}`, '_blank');
|
||||
handleStatusChange(order.id, 'dispatched');
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: Order['status']) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
@ -51,9 +122,9 @@ export default function AdminOrdersPage() {
|
||||
case 'deposit_received':
|
||||
return <span className="bg-blue-500/20 text-blue-400 border border-blue-500/30 font-bold px-2.5 py-1 rounded-full text-[10px]">Separación $10k Pagada</span>;
|
||||
case 'supplier_notified':
|
||||
return <span className="bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 font-bold px-2.5 py-1 rounded-full text-[10px] flex items-center gap-1"><CheckCircle2 className="w-3 h-3 text-emerald-400" /> Proveedor Notificado</span>;
|
||||
return <span className="bg-purple-500/20 text-purple-300 border border-purple-500/30 font-bold px-2.5 py-1 rounded-full text-[10px] flex items-center gap-1"><CheckCircle2 className="w-3 h-3 text-purple-400" /> Proveedor Notificado</span>;
|
||||
case 'dispatched':
|
||||
return <span className="bg-purple-500/20 text-purple-400 border border-purple-500/30 font-bold px-2.5 py-1 rounded-full text-[10px]">En Ruta con Mensajero</span>;
|
||||
return <span className="bg-fuchsia-500/20 text-fuchsia-400 border border-fuchsia-500/30 font-bold px-2.5 py-1 rounded-full text-[10px] flex items-center gap-1"><Truck className="w-3 h-3" /> En Ruta con Mensajero</span>;
|
||||
case 'delivered':
|
||||
return <span className="bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 font-bold px-2.5 py-1 rounded-full text-[10px]">Entregado & Cobrado</span>;
|
||||
case 'cancelled':
|
||||
@ -65,6 +136,7 @@ export default function AdminOrdersPage() {
|
||||
const matchesSearch =
|
||||
o.customerName.toLowerCase().includes(search.toLowerCase()) ||
|
||||
o.customerPhone.includes(search) ||
|
||||
o.address.toLowerCase().includes(search.toLowerCase()) ||
|
||||
o.cityName.toLowerCase().includes(search.toLowerCase()) ||
|
||||
o.id.toLowerCase().includes(search.toLowerCase());
|
||||
|
||||
@ -73,201 +145,440 @@ export default function AdminOrdersPage() {
|
||||
return true;
|
||||
});
|
||||
|
||||
const supplierGroups = orders.reduce((acc, order) => {
|
||||
order.items.forEach((item) => {
|
||||
const suppName = item.supplierName || 'DIFUSION JOVIC STORE';
|
||||
const suppPhone = item.supplierPhone || '573134848203';
|
||||
if (!acc[suppName]) {
|
||||
acc[suppName] = {
|
||||
name: suppName,
|
||||
phone: suppPhone,
|
||||
entries: [],
|
||||
totalPairs: 0,
|
||||
totalCost: 0,
|
||||
};
|
||||
}
|
||||
acc[suppName].entries.push({ order, item });
|
||||
acc[suppName].totalPairs += item.quantity;
|
||||
acc[suppName].totalCost += (item.supplierPrice || 0) * item.quantity;
|
||||
});
|
||||
return acc;
|
||||
}, {} as Record<string, { name: string; phone: string; entries: { order: Order; item: OrderItem }[]; totalPairs: number; totalCost: number }>);
|
||||
|
||||
const pendingDeliveries = orders.filter((o) => o.status !== 'delivered' && o.status !== 'cancelled');
|
||||
const totalBalanceToCollect = pendingDeliveries.reduce((acc, curr) => acc + curr.balanceDue, 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white flex items-center gap-2">
|
||||
<ShoppingBag className="w-6 h-6 text-emerald-400" />
|
||||
<span>Gestión de Pedidos & Separación con Proveedores ({orders.length})</span>
|
||||
</h1>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
Control logístico de pedidos contraentrega, cobro de anticipos de $10.000 COP y <strong>notificación inmediata de separación al proveedor</strong> vía WhatsApp.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||
<div className="flex items-center space-x-1.5 bg-slate-900 border border-slate-800 p-1 rounded-2xl overflow-x-auto">
|
||||
<button
|
||||
onClick={() => setStatusFilter('all')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'all' ? 'bg-white text-slate-950 shadow' : 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Todos ({orders.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('pending')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'pending' ? 'bg-amber-500/30 text-amber-300' : 'text-slate-400'
|
||||
}`}
|
||||
>
|
||||
Pendientes
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('supplier_notified')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'supplier_notified' ? 'bg-emerald-500/30 text-emerald-300' : 'text-slate-400'
|
||||
}`}
|
||||
>
|
||||
Proveedor Notificado
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('dispatched')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === 'dispatched' ? 'bg-purple-500/30 text-purple-300' : 'text-slate-400'
|
||||
}`}
|
||||
>
|
||||
En Ruta
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-w-xs w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar por cliente, ciudad o # orden..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-white outline-hidden focus:border-emerald-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredOrders.length === 0 ? (
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-3xl p-12 text-center space-y-3">
|
||||
<div className="w-16 h-16 rounded-full bg-slate-950 text-slate-500 flex items-center justify-center mx-auto border border-slate-800">
|
||||
<ShoppingBag className="w-8 h-8 text-emerald-400" />
|
||||
</div>
|
||||
<h3 className="font-bold text-white text-base">No hay pedidos en esta sección</h3>
|
||||
<p className="text-xs text-slate-400 max-w-sm mx-auto">
|
||||
Cuando los clientes hagan un pedido en la tienda web o por WhatsApp, aparecerán aquí con el botón para separar inmediatamente con el proveedor correspondiente.
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white flex items-center gap-2">
|
||||
<ShoppingBag className="w-6 h-6 text-purple-400" />
|
||||
<span>Centro Logístico: Pedidos, Proveedores & Rutas Waze</span>
|
||||
</h1>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Consolidación automática para <strong>separación con proveedores</strong> y <strong>hoja de ruta para mensajeros con navegación Waze en 1 clic</strong>.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
</div>
|
||||
|
||||
{toastMessage && (
|
||||
<div className="bg-purple-950/80 border border-purple-500 text-purple-300 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-md">
|
||||
<span>{toastMessage}</span>
|
||||
<button onClick={() => setToastMessage(null)} className="font-bold text-white cursor-pointer ml-3">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Tabs Navigation */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 p-1 bg-neutral-900 border border-neutral-800 rounded-2xl gap-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('individual')}
|
||||
className={`py-3 px-4 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 cursor-pointer ${
|
||||
activeTab === 'individual'
|
||||
? 'bg-purple-600 text-white shadow-lg shadow-purple-600/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Layers className="w-4 h-4" />
|
||||
<span>1. Todos los Pedidos ({orders.length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('suppliers')}
|
||||
className={`py-3 px-4 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 cursor-pointer ${
|
||||
activeTab === 'suppliers'
|
||||
? 'bg-purple-600 text-white shadow-lg shadow-purple-600/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Building2 className="w-4 h-4 text-fuchsia-300" />
|
||||
<span>2. Consolidado por Proveedor ({Object.keys(supplierGroups).length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('routes')}
|
||||
className={`py-3 px-4 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 cursor-pointer ${
|
||||
activeTab === 'routes'
|
||||
? 'bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg shadow-pink-600/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Compass className="w-4 h-4 text-cyan-300" />
|
||||
<span>3. Rutas de Entrega & Waze ({pendingDeliveries.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* TAB 1: INDIVIDUAL ORDERS LIST */}
|
||||
{activeTab === 'individual' && (
|
||||
<div className="space-y-4">
|
||||
{filteredOrders.map((order) => (
|
||||
<div
|
||||
key={order.id}
|
||||
className="bg-slate-900 border border-slate-800 rounded-3xl p-5 space-y-4 shadow-xl"
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-800 pb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-black text-white text-sm">Orden #{order.id}</span>
|
||||
<span className="text-xs text-slate-500">• {new Date(order.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</div>
|
||||
{getStatusBadge(order.status)}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||
<div className="flex items-center space-x-1.5 bg-neutral-900 border border-neutral-800 p-1 rounded-2xl overflow-x-auto">
|
||||
{['all', 'pending', 'deposit_received', 'supplier_notified', 'dispatched', 'delivered'].map((st) => (
|
||||
<button
|
||||
key={st}
|
||||
onClick={() => setStatusFilter(st)}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer transition-all ${
|
||||
statusFilter === st ? 'bg-white text-neutral-950 shadow' : 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{st === 'all' ? 'Todos' : st === 'pending' ? 'Pendientes' : st === 'deposit_received' ? 'Anticipo Pagado' : st === 'supplier_notified' ? 'Prov. Notificado' : st === 'dispatched' ? 'En Ruta' : 'Entregados'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="max-w-xs w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar por cliente, celular, ciudad o #..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3.5 py-2 text-xs text-white outline-hidden focus:border-purple-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{filteredOrders.length === 0 ? (
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl p-12 text-center text-neutral-500">
|
||||
No hay pedidos que coincidan con los filtros.
|
||||
</div>
|
||||
) : (
|
||||
filteredOrders.map((order) => (
|
||||
<div
|
||||
key={order.id}
|
||||
className="bg-neutral-900 border border-neutral-800 hover:border-purple-500/40 rounded-3xl p-5 shadow-xl transition-all space-y-4"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-neutral-800 pb-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="font-black text-white text-base">Orden #{order.id}</span>
|
||||
{getStatusBadge(order.status)}
|
||||
<span className="text-[11px] text-neutral-500 font-mono">{order.createdAt}</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3.5 space-y-1.5">
|
||||
<span className="text-[10px] text-emerald-400 font-bold uppercase block tracking-wider">
|
||||
Cliente & Entrega (24 Horas)
|
||||
</span>
|
||||
<p className="font-bold text-white text-sm">{order.customerName}</p>
|
||||
<p className="text-slate-300 flex items-center gap-1.5">
|
||||
<Phone className="w-3.5 h-3.5 text-emerald-400" /> {order.customerPhone}
|
||||
</p>
|
||||
<p className="text-slate-300 flex items-center gap-1.5">
|
||||
<MapPin className="w-3.5 h-3.5 text-emerald-400" /> {order.address} ({order.cityName})
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-neutral-400 font-bold">Estado:</span>
|
||||
<select
|
||||
value={order.status}
|
||||
onChange={(e) => handleStatusChange(order.id, e.target.value as any)}
|
||||
className="bg-neutral-950 border border-neutral-800 text-xs text-white rounded-xl px-3 py-1.5 outline-hidden font-bold cursor-pointer"
|
||||
>
|
||||
<option value="pending">⏳ Pendiente Confirmar</option>
|
||||
<option value="deposit_received">💳 Separación $10k Pagada</option>
|
||||
<option value="supplier_notified">🏢 Proveedor Notificado</option>
|
||||
<option value="dispatched">🛵 En Ruta con Mensajero</option>
|
||||
<option value="delivered">✅ Entregado & Cobrado</option>
|
||||
<option value="cancelled">❌ Cancelado</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3.5 space-y-2">
|
||||
<span className="text-[10px] text-slate-400 font-bold uppercase block tracking-wider">
|
||||
Zapatillas & Proveedor Asignado:
|
||||
</span>
|
||||
<div className="space-y-2.5">
|
||||
{order.items.map((it, idx) => {
|
||||
const suppName = it.supplierName || 'DIFUSION JOVIC STORE';
|
||||
return (
|
||||
<div key={idx} className="bg-slate-900 border border-slate-800 rounded-xl p-2.5 space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
||||
<div className="space-y-1 bg-neutral-950/60 p-3 rounded-2xl border border-neutral-800/80">
|
||||
<p className="font-bold text-neutral-400 uppercase text-[10px]">Cliente & Destino</p>
|
||||
<p className="text-white font-bold text-sm">{order.customerName}</p>
|
||||
<p className="text-emerald-400 font-mono font-semibold flex items-center gap-1">
|
||||
<Phone className="w-3 h-3" /> {order.customerPhone}
|
||||
</p>
|
||||
<p className="text-neutral-300 flex items-start gap-1">
|
||||
<MapPin className="w-3 h-3 shrink-0 text-purple-400 mt-0.5" />
|
||||
<span>{order.address} ({order.cityName})</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 bg-neutral-950/60 p-3 rounded-2xl border border-neutral-800/80">
|
||||
<p className="font-bold text-neutral-400 uppercase text-[10px]">Resumen de Cobro</p>
|
||||
<div className="flex justify-between text-neutral-300">
|
||||
<span>Total Venta:</span>
|
||||
<strong className="text-white">{formatCOP(order.totalAmount)}</strong>
|
||||
</div>
|
||||
<div className="flex justify-between text-neutral-300">
|
||||
<span>Anticipo / Separación:</span>
|
||||
<strong className="text-blue-400">-{formatCOP(order.depositAmount)}</strong>
|
||||
</div>
|
||||
<div className="flex justify-between text-neutral-300 pt-1 border-t border-neutral-800 font-bold">
|
||||
<span className="text-purple-300">Saldo Contraentrega:</span>
|
||||
<span className="text-emerald-400 text-sm">{formatCOP(order.balanceDue)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 bg-neutral-950/60 p-3 rounded-2xl border border-neutral-800/80 flex flex-col justify-center">
|
||||
<button
|
||||
onClick={() => handleOpenWaze(order.address, order.cityName)}
|
||||
className="w-full bg-cyan-600 hover:bg-cyan-500 text-white font-bold py-1.5 px-3 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer shadow transition-all text-xs"
|
||||
>
|
||||
<Compass className="w-3.5 h-3.5" />
|
||||
<span>Navegar con Waze</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleNotifyCustomerOnTheWay(order)}
|
||||
className="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-1.5 px-3 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer shadow transition-all text-xs"
|
||||
>
|
||||
<MessageCircle className="w-3.5 h-3.5" />
|
||||
<span>Avisar "Voy en camino"</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pt-2">
|
||||
<p className="font-bold text-neutral-400 uppercase text-[10px]">Pares en esta orden:</p>
|
||||
<div className="space-y-2">
|
||||
{order.items.map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 bg-neutral-950 p-3 rounded-2xl border border-neutral-800"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<img src={item.image} alt="" className="w-12 h-12 rounded-xl object-cover border border-neutral-800 shrink-0" />
|
||||
<div>
|
||||
<p className="font-bold text-white text-xs">{it.productName}</p>
|
||||
<p className="text-[11px] text-slate-400">
|
||||
Talla <strong className="text-emerald-400 font-bold">{it.size}</strong> • Cant: {it.quantity}
|
||||
<p className="font-bold text-white text-xs">{item.productName}</p>
|
||||
<p className="text-[11px] text-neutral-400">
|
||||
Talla: <strong className="text-purple-400">{item.size}</strong> | Cantidad: <strong>{item.quantity}</strong> | Proveedor: <strong className="text-purple-300">{item.supplierName || 'Bodega Principal'}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-[10px] bg-slate-950 text-slate-300 border border-slate-800 px-2 py-0.5 rounded font-mono">
|
||||
Prov: {suppName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleNotifySupplier(order, it)}
|
||||
className="w-full bg-emerald-600/20 hover:bg-emerald-600/30 text-emerald-300 border border-emerald-500/30 font-bold text-xs py-1.5 px-2.5 rounded-lg flex items-center justify-center space-x-1.5 transition-all cursor-pointer shadow-xs"
|
||||
onClick={() => handleNotifySingleSupplier(order, item)}
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-3.5 py-2 rounded-xl flex items-center space-x-1.5 shadow cursor-pointer shrink-0 self-end sm:self-auto"
|
||||
>
|
||||
<MessageCircle className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>📲 Separar con {suppName} (WhatsApp)</span>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
<span>Separar con Proveedor</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 2: CONSOLIDADO AGRUPADO POR PROVEEDOR */}
|
||||
{activeTab === 'suppliers' && (
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
<div className="bg-purple-950/40 border border-purple-500/30 rounded-3xl p-5 text-xs text-neutral-300 space-y-2 shadow-lg">
|
||||
<div className="flex items-center space-x-2 text-purple-300 font-bold text-sm">
|
||||
<Building2 className="w-5 h-5 text-purple-400" />
|
||||
<span>Consolidado Automático de Compras & Separaciones</span>
|
||||
</div>
|
||||
<p>
|
||||
Aquí se agrupan todos los pares pendientes clasificados por cada <strong>Proveedor Mayorista</strong>. Puedes enviar el <strong>manifiesto consolidado en 1 solo mensaje de WhatsApp</strong> para que el proveedor prepare y empaque todos los pedidos de la jornada juntos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{Object.values(supplierGroups).map((group) => (
|
||||
<div
|
||||
key={group.name}
|
||||
className="bg-neutral-900 border border-neutral-800 rounded-3xl p-6 shadow-xl space-y-4"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-neutral-800 pb-4">
|
||||
<div>
|
||||
<span className="bg-purple-500/20 text-purple-300 border border-purple-500/40 text-[10px] font-bold px-2.5 py-0.5 rounded-full uppercase tracking-wider">
|
||||
Proveedor Mayorista
|
||||
</span>
|
||||
<h3 className="text-xl font-black text-white mt-1">{group.name}</h3>
|
||||
<p className="text-xs text-neutral-400 flex items-center gap-1 mt-0.5">
|
||||
<Phone className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>WhatsApp Proveedor: <strong>{group.phone}</strong></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-neutral-950 border border-neutral-800 px-4 py-2 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 uppercase font-bold">Total Pares</p>
|
||||
<p className="text-lg font-black text-purple-400">{group.totalPairs} par(es)</p>
|
||||
</div>
|
||||
<div className="bg-neutral-950 border border-neutral-800 px-4 py-2 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 uppercase font-bold">Total a Pagar</p>
|
||||
<p className="text-lg font-black text-emerald-400">{formatCOP(group.totalCost)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3.5 space-y-2">
|
||||
<span className="text-[10px] text-slate-400 font-bold uppercase block tracking-wider">
|
||||
Cobro & Pagos Contraentrega
|
||||
</span>
|
||||
<div className="space-y-1 text-xs">
|
||||
<p className="text-slate-400 flex justify-between">
|
||||
<span>Total de la Orden:</span>
|
||||
<strong className="text-white font-mono">{formatCOP(order.totalAmount)}</strong>
|
||||
</p>
|
||||
{order.depositAmount > 0 ? (
|
||||
<>
|
||||
<p className="text-amber-300 flex justify-between">
|
||||
<span>Anticipo / Separación:</span>
|
||||
<strong className="font-mono">{formatCOP(order.depositAmount)}</strong>
|
||||
</p>
|
||||
<p className="text-emerald-400 font-bold flex justify-between pt-1 border-t border-slate-800">
|
||||
<span>Saldo al Mensajero:</span>
|
||||
<strong className="font-mono">{formatCOP(order.balanceDue)}</strong>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-emerald-400 font-bold pt-1 border-t border-slate-800">
|
||||
100% Contraentrega al recibir
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs text-neutral-300">
|
||||
<thead className="bg-neutral-950 text-neutral-400 font-bold uppercase text-[10px]">
|
||||
<tr>
|
||||
<th className="py-2.5 px-3">Modelo / Zapatilla</th>
|
||||
<th className="py-2.5 px-3">Talla</th>
|
||||
<th className="py-2.5 px-3">Cant.</th>
|
||||
<th className="py-2.5 px-3">Costo Base</th>
|
||||
<th className="py-2.5 px-3">Orden Cliente</th>
|
||||
<th className="py-2.5 px-3">Destino</th>
|
||||
<th className="py-2.5 px-3">Estado Orden</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-neutral-800">
|
||||
{group.entries.map((entry, idx) => (
|
||||
<tr key={idx} className="hover:bg-neutral-800/40">
|
||||
<td className="py-2.5 px-3 font-bold text-white flex items-center gap-2">
|
||||
<img src={entry.item.image} alt="" className="w-8 h-8 rounded-lg object-cover border border-neutral-800" />
|
||||
<span>{entry.item.productName}</span>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 font-bold text-purple-400 font-mono">{entry.item.size}</td>
|
||||
<td className="py-2.5 px-3 font-bold">{entry.item.quantity}</td>
|
||||
<td className="py-2.5 px-3 font-mono font-semibold">{formatCOP(entry.item.supplierPrice || 0)}</td>
|
||||
<td className="py-2.5 px-3 font-mono text-neutral-400">#{entry.order.id}</td>
|
||||
<td className="py-2.5 px-3">{entry.order.cityName}</td>
|
||||
<td className="py-2.5 px-3">{getStatusBadge(entry.order.status)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 flex justify-end">
|
||||
<button
|
||||
onClick={() => handleSendSupplierBatchManifest(group.name, group.phone, group.entries)}
|
||||
className="bg-gradient-to-r from-purple-600 via-fuchsia-600 to-pink-600 hover:opacity-95 text-white font-black text-xs px-5 py-3 rounded-2xl shadow-lg flex items-center gap-2 cursor-pointer transition-all active:scale-95"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
<span>Enviar Manifiesto de {group.totalPairs} Pares por WhatsApp a {group.name}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-slate-800">
|
||||
<span className="text-[11px] text-slate-500">Método: {order.paymentMethodName}</span>
|
||||
{/* TAB 3: RUTAS DE ENTREGA ORGANIZADAS POR DIRECCIONES & WAZE */}
|
||||
{activeTab === 'routes' && (
|
||||
<div className="space-y-6 animate-in fade-in">
|
||||
<div className="bg-gradient-to-r from-neutral-900 via-neutral-900 to-purple-950/60 border border-purple-500/30 rounded-3xl p-6 shadow-xl flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 text-cyan-300 font-bold text-sm">
|
||||
<Compass className="w-5 h-5 text-cyan-400" />
|
||||
<span>Hoja de Ruta & Navegación GPS (Waze / Google Maps)</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-300 mt-1 max-w-xl">
|
||||
Entregas contraentrega secuenciadas por zona geográfica. El mensajero puede presionar <strong>"Abrir en Waze"</strong> para iniciar la navegación instantánea a cada portería/casa.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'deposit_received')}
|
||||
className="bg-blue-600/20 hover:bg-blue-600/30 text-blue-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-blue-500/20"
|
||||
>
|
||||
Confirmar $10k
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'supplier_notified')}
|
||||
className="bg-emerald-600/20 hover:bg-emerald-600/30 text-emerald-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-emerald-500/20"
|
||||
>
|
||||
Marcar Proveedor Notificado
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'dispatched')}
|
||||
className="bg-purple-600/20 hover:bg-purple-600/30 text-purple-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-purple-500/20"
|
||||
>
|
||||
Despachar en Ruta
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStatusChange(order.id, 'delivered')}
|
||||
className="bg-emerald-600/20 hover:bg-emerald-600/30 text-emerald-300 text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer border border-emerald-500/20"
|
||||
>
|
||||
Marcar Entregado
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-neutral-950 border border-neutral-800 p-3 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 uppercase font-bold">Paradas / Entregas</p>
|
||||
<p className="text-xl font-black text-white">{pendingDeliveries.length} Paradas</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-neutral-950 border border-emerald-500/30 p-3 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-emerald-400 uppercase font-bold">Total a Recaudar</p>
|
||||
<p className="text-xl font-black text-emerald-400">{formatCOP(totalBalanceToCollect)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{pendingDeliveries.length === 0 ? (
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl p-12 text-center text-neutral-500">
|
||||
🎉 No hay entregas pendientes por despachar en este momento.
|
||||
</div>
|
||||
) : (
|
||||
pendingDeliveries.map((delivery, index) => (
|
||||
<div
|
||||
key={delivery.id}
|
||||
className="bg-neutral-900 border border-neutral-800 hover:border-cyan-500/40 rounded-3xl p-5 shadow-lg transition-all flex flex-col lg:flex-row lg:items-center justify-between gap-4"
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-cyan-500/20 border border-cyan-500/40 flex flex-col items-center justify-center text-cyan-300 font-black shrink-0 shadow-sm">
|
||||
<span className="text-[10px] uppercase font-bold text-neutral-400 leading-none">Parada</span>
|
||||
<span className="text-lg leading-tight">#{index + 1}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-black text-white text-base">{delivery.customerName}</span>
|
||||
<span className="text-xs text-neutral-400">• Orden #{delivery.id}</span>
|
||||
{getStatusBadge(delivery.status)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs font-bold text-white flex items-center gap-1.5 bg-neutral-950 px-3 py-1 rounded-xl border border-neutral-800 w-fit">
|
||||
<MapPin className="w-4 h-4 text-cyan-400 shrink-0" />
|
||||
<span>{delivery.address} ({delivery.cityName})</span>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center space-x-3 text-xs text-neutral-400 pt-0.5">
|
||||
<span className="flex items-center gap-1 text-emerald-400 font-semibold">
|
||||
<Phone className="w-3 h-3" /> {delivery.customerPhone}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>
|
||||
{delivery.items.map((i) => `${i.productName} (Talla ${i.size})`).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{delivery.notes && (
|
||||
<p className="text-[11px] text-amber-300/90 italic bg-amber-500/10 px-2.5 py-0.5 rounded-lg border border-amber-500/20 w-fit">
|
||||
Nota: {delivery.notes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 self-end lg:self-auto shrink-0">
|
||||
<div className="bg-neutral-950 border border-neutral-800 px-3.5 py-2 rounded-2xl text-right">
|
||||
<p className="text-[10px] text-neutral-400 font-bold uppercase">Cobrar Contraentrega</p>
|
||||
<p className="text-base font-black text-emerald-400">{formatCOP(delivery.balanceDue)}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleOpenWaze(delivery.address, delivery.cityName)}
|
||||
className="bg-cyan-600 hover:bg-cyan-500 text-white font-black text-xs px-4 py-3 rounded-2xl shadow-lg flex items-center gap-1.5 cursor-pointer active:scale-95 transition-all"
|
||||
title="Abrir ubicación directamente en Waze"
|
||||
>
|
||||
<Compass className="w-4 h-4" />
|
||||
<span>Waze</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleOpenGoogleMaps(delivery.address, delivery.cityName)}
|
||||
className="bg-neutral-800 hover:bg-neutral-700 text-white font-bold text-xs p-3 rounded-2xl shadow cursor-pointer transition-all"
|
||||
title="Abrir en Google Maps"
|
||||
>
|
||||
<Navigation className="w-4 h-4 text-emerald-400" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleNotifyCustomerOnTheWay(delivery)}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs px-3.5 py-3 rounded-2xl shadow flex items-center gap-1.5 cursor-pointer active:scale-95 transition-all"
|
||||
title="Enviar mensaje de WhatsApp notificando que el mensajero va en camino"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">En Camino</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user