diff --git a/almeira-store-app/src/app/admin/pedidos/page.tsx b/almeira-store-app/src/app/admin/pedidos/page.tsx index 82562a2..c0203a2 100644 --- a/almeira-store-app/src/app/admin/pedidos/page.tsx +++ b/almeira-store-app/src/app/admin/pedidos/page.tsx @@ -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([]); + const [activeTab, setActiveTab] = useState<'individual' | 'suppliers' | 'routes'>('individual'); const [search, setSearch] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); + const [toastMessage, setToastMessage] = useState(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 Separación $10k Pagada; case 'supplier_notified': - return Proveedor Notificado; + return Proveedor Notificado; case 'dispatched': - return En Ruta con Mensajero; + return En Ruta con Mensajero; case 'delivered': return Entregado & Cobrado; 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); + + const pendingDeliveries = orders.filter((o) => o.status !== 'delivered' && o.status !== 'cancelled'); + const totalBalanceToCollect = pendingDeliveries.reduce((acc, curr) => acc + curr.balanceDue, 0); + return (
{/* Header */} -
-

- - Gestión de Pedidos & Separación con Proveedores ({orders.length}) -

-

- Control logístico de pedidos contraentrega, cobro de anticipos de $10.000 COP y notificación inmediata de separación al proveedor vía WhatsApp. -

-
- - {/* Toolbar */} -
-
- - - - -
- -
- 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" - /> -
-
- - {filteredOrders.length === 0 ? ( -
-
- -
-

No hay pedidos en esta sección

-

- 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. +

+
+

+ + Centro Logístico: Pedidos, Proveedores & Rutas Waze +

+

+ Consolidación automática para separación con proveedores y hoja de ruta para mensajeros con navegación Waze en 1 clic.

- ) : ( +
+ + {toastMessage && ( +
+ {toastMessage} + +
+ )} + + {/* Main Tabs Navigation */} +
+ + + + + +
+ + {/* TAB 1: INDIVIDUAL ORDERS LIST */} + {activeTab === 'individual' && (
- {filteredOrders.map((order) => ( -
- {/* Top Order bar */} -
-
- Orden #{order.id} - • {new Date(order.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} -
- {getStatusBadge(order.status)} +
+
+ {['all', 'pending', 'deposit_received', 'supplier_notified', 'dispatched', 'delivered'].map((st) => ( + + ))} +
+ +
+ 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" + /> +
+
+ +
+ {filteredOrders.length === 0 ? ( +
+ No hay pedidos que coincidan con los filtros.
+ ) : ( + filteredOrders.map((order) => ( +
+
+
+ Orden #{order.id} + {getStatusBadge(order.status)} + {order.createdAt} +
-
- {/* Customer Details */} -
- - Cliente & Entrega (24 Horas) - -

{order.customerName}

-

- {order.customerPhone} -

-

- {order.address} ({order.cityName}) -

-
+
+ Estado: + +
+
- {/* Items and Supplier Separation Action */} -
- - Zapatillas & Proveedor Asignado: - -
- {order.items.map((it, idx) => { - const suppName = it.supplierName || 'DIFUSION JOVIC STORE'; - return ( -
-
+
+
+

Cliente & Destino

+

{order.customerName}

+

+ {order.customerPhone} +

+

+ + {order.address} ({order.cityName}) +

+
+ +
+

Resumen de Cobro

+
+ Total Venta: + {formatCOP(order.totalAmount)} +
+
+ Anticipo / Separación: + -{formatCOP(order.depositAmount)} +
+
+ Saldo Contraentrega: + {formatCOP(order.balanceDue)} +
+
+ +
+ + + +
+
+ +
+

Pares en esta orden:

+
+ {order.items.map((item, idx) => ( +
+
+
-

{it.productName}

-

- Talla {it.size} • Cant: {it.quantity} +

{item.productName}

+

+ Talla: {item.size} | Cantidad: {item.quantity} | Proveedor: {item.supplierName || 'Bodega Principal'}

- - Prov: {suppName} -
- {/* 1-Click WhatsApp Separation Button */}
- ); - })} + ))} +
+
+
+ )) + )} +
+
+ )} + + {/* TAB 2: CONSOLIDADO AGRUPADO POR PROVEEDOR */} + {activeTab === 'suppliers' && ( +
+
+
+ + Consolidado Automático de Compras & Separaciones +
+

+ Aquí se agrupan todos los pares pendientes clasificados por cada Proveedor Mayorista. Puedes enviar el manifiesto consolidado en 1 solo mensaje de WhatsApp para que el proveedor prepare y empaque todos los pedidos de la jornada juntos. +

+
+ +
+ {Object.values(supplierGroups).map((group) => ( +
+
+
+ + Proveedor Mayorista + +

{group.name}

+

+ + WhatsApp Proveedor: {group.phone} +

+
+ +
+
+

Total Pares

+

{group.totalPairs} par(es)

+
+
+

Total a Pagar

+

{formatCOP(group.totalCost)}

+
- {/* Payment Breakdown */} -
- - Cobro & Pagos Contraentrega - -
-

- Total de la Orden: - {formatCOP(order.totalAmount)} -

- {order.depositAmount > 0 ? ( - <> -

- Anticipo / Separación: - {formatCOP(order.depositAmount)} -

-

- Saldo al Mensajero: - {formatCOP(order.balanceDue)} -

- - ) : ( -

- 100% Contraentrega al recibir -

- )} -
+
+ + + + + + + + + + + + + + {group.entries.map((entry, idx) => ( + + + + + + + + + + ))} + +
Modelo / ZapatillaTallaCant.Costo BaseOrden ClienteDestinoEstado Orden
+ + {entry.item.productName} + {entry.item.size}{entry.item.quantity}{formatCOP(entry.item.supplierPrice || 0)}#{entry.order.id}{entry.order.cityName}{getStatusBadge(entry.order.status)}
+
+ +
+
+ ))} +
+
+ )} - {/* Order Status Action Buttons */} -
- Método: {order.paymentMethodName} + {/* TAB 3: RUTAS DE ENTREGA ORGANIZADAS POR DIRECCIONES & WAZE */} + {activeTab === 'routes' && ( +
+
+
+
+ + Hoja de Ruta & Navegación GPS (Waze / Google Maps) +
+

+ Entregas contraentrega secuenciadas por zona geográfica. El mensajero puede presionar "Abrir en Waze" para iniciar la navegación instantánea a cada portería/casa. +

+
-
- - - - -
+
+
+

Paradas / Entregas

+

{pendingDeliveries.length} Paradas

+
+ +
+

Total a Recaudar

+

{formatCOP(totalBalanceToCollect)}

- ))} +
+ +
+ {pendingDeliveries.length === 0 ? ( +
+ 🎉 No hay entregas pendientes por despachar en este momento. +
+ ) : ( + pendingDeliveries.map((delivery, index) => ( +
+
+
+ Parada + #{index + 1} +
+ +
+
+ {delivery.customerName} + • Orden #{delivery.id} + {getStatusBadge(delivery.status)} +
+ +

+ + {delivery.address} ({delivery.cityName}) +

+ +
+ + {delivery.customerPhone} + + • + + {delivery.items.map((i) => `${i.productName} (Talla ${i.size})`).join(', ')} + +
+ + {delivery.notes && ( +

+ Nota: {delivery.notes} +

+ )} +
+
+ +
+
+

Cobrar Contraentrega

+

{formatCOP(delivery.balanceDue)}

+
+ + + + + + +
+
+ )) + )} +
)}
diff --git a/dripzone-app/src/app/admin/pedidos/page.tsx b/dripzone-app/src/app/admin/pedidos/page.tsx index 25ca97f..72fe76d 100644 --- a/dripzone-app/src/app/admin/pedidos/page.tsx +++ b/dripzone-app/src/app/admin/pedidos/page.tsx @@ -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([]); + const [activeTab, setActiveTab] = useState<'individual' | 'suppliers' | 'routes'>('individual'); const [search, setSearch] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); + const [toastMessage, setToastMessage] = useState(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 Proveedor Notificado; case 'dispatched': - return En Ruta con Mensajero; + return En Ruta con Mensajero; case 'delivered': return Entregado & Cobrado; 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); + + // 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 (
{/* Header */} -
-

- - Gestión de Pedidos & Separación con Proveedores ({orders.length}) -

-

- Control logístico de pedidos contraentrega, cobro de anticipos de $10.000 COP y notificación inmediata de separación al proveedor vía WhatsApp. -

-
- - {/* Toolbar */} -
-
- - - - -
- -
- 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" - /> -
-
- - {filteredOrders.length === 0 ? ( -
-
- -
-

No hay pedidos en esta sección

-

- 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. +

+
+

+ + Centro Logístico: Pedidos, Proveedores & Rutas Waze +

+

+ Consolidación automática para separación con proveedores y hoja de ruta para mensajeros con navegación Waze en 1 clic.

- ) : ( +
+ + {toastMessage && ( +
+ {toastMessage} + +
+ )} + + {/* Main Tabs Navigation */} +
+ + + + + +
+ + {/* TAB 1: INDIVIDUAL ORDERS LIST */} + {activeTab === 'individual' && (
- {filteredOrders.map((order) => ( -
-
-
- Orden #{order.id} - • {new Date(order.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} -
- {getStatusBadge(order.status)} + {/* Toolbar */} +
+
+ {['all', 'pending', 'deposit_received', 'supplier_notified', 'dispatched', 'delivered'].map((st) => ( + + ))} +
+ +
+ 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" + /> +
+
+ + {/* Orders Cards Grid */} +
+ {filteredOrders.length === 0 ? ( +
+ No hay pedidos que coincidan con los filtros.
+ ) : ( + filteredOrders.map((order) => ( +
+ {/* Top Bar: Order ID, Status, Actions */} +
+
+ Orden #{order.id} + {getStatusBadge(order.status)} + {order.createdAt} +
-
-
- - Cliente & Entrega (24 Horas) - -

{order.customerName}

-

- {order.customerPhone} -

-

- {order.address} ({order.cityName}) -

-
+ {/* Status Dropdown */} +
+ Estado: + +
+
-
- - Zapatillas & Proveedor Asignado: - -
- {order.items.map((it, idx) => { - const suppName = it.supplierName || 'DIFUSION JOVIC STORE'; - return ( -
-
+ {/* Customer Info & Financials */} +
+ {/* Customer */} +
+

Cliente & Destino

+

{order.customerName}

+

+ {order.customerPhone} +

+

+ + {order.address} ({order.cityName}) +

+
+ + {/* Financial Balance */} +
+

Resumen de Cobro

+
+ Total Venta: + {formatCOP(order.totalAmount)} +
+
+ Anticipo / Separación: + -{formatCOP(order.depositAmount)} +
+
+ Saldo Contraentrega: + {formatCOP(order.balanceDue)} +
+
+ + {/* Quick Dispatch Actions */} +
+ + + +
+
+ + {/* Items breakdown with Supplier notification button */} +
+

Pares en esta orden:

+
+ {order.items.map((item, idx) => ( +
+
+
-

{it.productName}

+

{item.productName}

- Talla {it.size} • Cant: {it.quantity} + Talla: {item.size} | Cantidad: {item.quantity} | Proveedor: {item.supplierName || 'Bodega Principal'}

- - Prov: {suppName} -
- ); - })} + ))} +
+
+
+ )) + )} +
+
+ )} + + {/* TAB 2: CONSOLIDADO AGRUPADO POR PROVEEDOR */} + {activeTab === 'suppliers' && ( +
+
+
+ + Consolidado Automático de Compras & Separaciones +
+

+ Aquí se agrupan todos los pares pendientes clasificados por cada Proveedor Mayorista. Puedes enviar el manifiesto consolidado en 1 solo mensaje de WhatsApp para que el proveedor prepare y empaque todos los pedidos de la jornada juntos. +

+
+ +
+ {Object.values(supplierGroups).map((group) => ( +
+ {/* Supplier Header */} +
+
+ + Proveedor Mayorista + +

{group.name}

+

+ + WhatsApp Proveedor: {group.phone} +

+
+ +
+
+

Total Pares

+

{group.totalPairs} par(es)

+
+
+

Total a Pagar

+

{formatCOP(group.totalCost)}

+
-
- - Cobro & Pagos Contraentrega - -
-

- Total de la Orden: - {formatCOP(order.totalAmount)} -

- {order.depositAmount > 0 ? ( - <> -

- Anticipo / Separación: - {formatCOP(order.depositAmount)} -

-

- Saldo al Mensajero: - {formatCOP(order.balanceDue)} -

- - ) : ( -

- 100% Contraentrega al recibir -

- )} -
+ {/* Items Table for this supplier */} +
+ + + + + + + + + + + + + + {group.entries.map((entry, idx) => ( + + + + + + + + + + ))} + +
Modelo / ZapatillaTallaCant.Costo BaseOrden ClienteDestinoEstado Orden
+ + {entry.item.productName} + {entry.item.size}{entry.item.quantity}{formatCOP(entry.item.supplierPrice || 0)}#{entry.order.id}{entry.order.cityName}{getStatusBadge(entry.order.status)}
+
+ + {/* Send Batch WhatsApp Manifest Button */} +
+
+ ))} +
+
+ )} -
- Método: {order.paymentMethodName} + {/* TAB 3: RUTAS DE ENTREGA ORGANIZADAS POR DIRECCIONES & WAZE */} + {activeTab === 'routes' && ( +
+ {/* KPI Route Header */} +
+
+
+ + Hoja de Ruta & Navegación GPS (Waze / Google Maps) +
+

+ Entregas contraentrega secuenciadas por zona geográfica. El mensajero puede presionar "Abrir en Waze" para iniciar la navegación instantánea a cada portería/casa. +

+
-
- - - - -
+
+
+

Paradas / Entregas

+

{pendingDeliveries.length} Paradas

+
+ +
+

Total a Recaudar

+

{formatCOP(totalBalanceToCollect)}

- ))} +
+ + {/* Delivery Stops Sequential List */} +
+ {pendingDeliveries.length === 0 ? ( +
+ 🎉 No hay entregas pendientes por despachar en este momento. +
+ ) : ( + pendingDeliveries.map((delivery, index) => ( +
+ {/* Left: Sequence Number & Stop Details */} +
+ {/* Stop Number Badge */} +
+ Parada + #{index + 1} +
+ +
+
+ {delivery.customerName} + • Orden #{delivery.id} + {getStatusBadge(delivery.status)} +
+ + {/* Exact Address Highlight */} +

+ + {delivery.address} ({delivery.cityName}) +

+ +
+ + {delivery.customerPhone} + + • + + {delivery.items.map((i) => `${i.productName} (Talla ${i.size})`).join(', ')} + +
+ + {delivery.notes && ( +

+ Nota: {delivery.notes} +

+ )} +
+
+ + {/* Right: Balance & 1-Click Waze / WhatsApp Actions */} +
+ {/* Cash to collect badge */} +
+

Cobrar Contraentrega

+

{formatCOP(delivery.balanceDue)}

+
+ + {/* Waze GPS Button */} + + + {/* Google Maps GPS Button */} + + + {/* WhatsApp Alert */} + +
+
+ )) + )} +
)}
diff --git a/dripzone-app/src/lib/data-service.ts b/dripzone-app/src/lib/data-service.ts index 4050f2f..11adc8c 100644 --- a/dripzone-app/src/lib/data-service.ts +++ b/dripzone-app/src/lib/data-service.ts @@ -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[] { diff --git a/sneaky-app/src/app/admin/pedidos/page.tsx b/sneaky-app/src/app/admin/pedidos/page.tsx index f28a975..c0203a2 100644 --- a/sneaky-app/src/app/admin/pedidos/page.tsx +++ b/sneaky-app/src/app/admin/pedidos/page.tsx @@ -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([]); + const [activeTab, setActiveTab] = useState<'individual' | 'suppliers' | 'routes'>('individual'); const [search, setSearch] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); + const [toastMessage, setToastMessage] = useState(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 Separación $10k Pagada; case 'supplier_notified': - return Proveedor Notificado; + return Proveedor Notificado; case 'dispatched': - return En Ruta con Mensajero; + return En Ruta con Mensajero; case 'delivered': return Entregado & Cobrado; 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); + + const pendingDeliveries = orders.filter((o) => o.status !== 'delivered' && o.status !== 'cancelled'); + const totalBalanceToCollect = pendingDeliveries.reduce((acc, curr) => acc + curr.balanceDue, 0); + return (
{/* Header */} -
-

- - Gestión de Pedidos & Separación con Proveedores ({orders.length}) -

-

- Control logístico de pedidos contraentrega, cobro de anticipos de $10.000 COP y notificación inmediata de separación al proveedor vía WhatsApp. -

-
- - {/* Toolbar */} -
-
- - - - -
- -
- 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" - /> -
-
- - {filteredOrders.length === 0 ? ( -
-
- -
-

No hay pedidos en esta sección

-

- 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. +

+
+

+ + Centro Logístico: Pedidos, Proveedores & Rutas Waze +

+

+ Consolidación automática para separación con proveedores y hoja de ruta para mensajeros con navegación Waze en 1 clic.

- ) : ( +
+ + {toastMessage && ( +
+ {toastMessage} + +
+ )} + + {/* Main Tabs Navigation */} +
+ + + + + +
+ + {/* TAB 1: INDIVIDUAL ORDERS LIST */} + {activeTab === 'individual' && (
- {filteredOrders.map((order) => ( -
-
-
- Orden #{order.id} - • {new Date(order.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} -
- {getStatusBadge(order.status)} +
+
+ {['all', 'pending', 'deposit_received', 'supplier_notified', 'dispatched', 'delivered'].map((st) => ( + + ))} +
+ +
+ 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" + /> +
+
+ +
+ {filteredOrders.length === 0 ? ( +
+ No hay pedidos que coincidan con los filtros.
+ ) : ( + filteredOrders.map((order) => ( +
+
+
+ Orden #{order.id} + {getStatusBadge(order.status)} + {order.createdAt} +
-
-
- - Cliente & Entrega (24 Horas) - -

{order.customerName}

-

- {order.customerPhone} -

-

- {order.address} ({order.cityName}) -

-
+
+ Estado: + +
+
-
- - Zapatillas & Proveedor Asignado: - -
- {order.items.map((it, idx) => { - const suppName = it.supplierName || 'DIFUSION JOVIC STORE'; - return ( -
-
+
+
+

Cliente & Destino

+

{order.customerName}

+

+ {order.customerPhone} +

+

+ + {order.address} ({order.cityName}) +

+
+ +
+

Resumen de Cobro

+
+ Total Venta: + {formatCOP(order.totalAmount)} +
+
+ Anticipo / Separación: + -{formatCOP(order.depositAmount)} +
+
+ Saldo Contraentrega: + {formatCOP(order.balanceDue)} +
+
+ +
+ + + +
+
+ +
+

Pares en esta orden:

+
+ {order.items.map((item, idx) => ( +
+
+
-

{it.productName}

-

- Talla {it.size} • Cant: {it.quantity} +

{item.productName}

+

+ Talla: {item.size} | Cantidad: {item.quantity} | Proveedor: {item.supplierName || 'Bodega Principal'}

- - Prov: {suppName} -
- ); - })} + ))} +
+
+
+ )) + )} +
+
+ )} + + {/* TAB 2: CONSOLIDADO AGRUPADO POR PROVEEDOR */} + {activeTab === 'suppliers' && ( +
+
+
+ + Consolidado Automático de Compras & Separaciones +
+

+ Aquí se agrupan todos los pares pendientes clasificados por cada Proveedor Mayorista. Puedes enviar el manifiesto consolidado en 1 solo mensaje de WhatsApp para que el proveedor prepare y empaque todos los pedidos de la jornada juntos. +

+
+ +
+ {Object.values(supplierGroups).map((group) => ( +
+
+
+ + Proveedor Mayorista + +

{group.name}

+

+ + WhatsApp Proveedor: {group.phone} +

+
+ +
+
+

Total Pares

+

{group.totalPairs} par(es)

+
+
+

Total a Pagar

+

{formatCOP(group.totalCost)}

+
-
- - Cobro & Pagos Contraentrega - -
-

- Total de la Orden: - {formatCOP(order.totalAmount)} -

- {order.depositAmount > 0 ? ( - <> -

- Anticipo / Separación: - {formatCOP(order.depositAmount)} -

-

- Saldo al Mensajero: - {formatCOP(order.balanceDue)} -

- - ) : ( -

- 100% Contraentrega al recibir -

- )} -
+
+ + + + + + + + + + + + + + {group.entries.map((entry, idx) => ( + + + + + + + + + + ))} + +
Modelo / ZapatillaTallaCant.Costo BaseOrden ClienteDestinoEstado Orden
+ + {entry.item.productName} + {entry.item.size}{entry.item.quantity}{formatCOP(entry.item.supplierPrice || 0)}#{entry.order.id}{entry.order.cityName}{getStatusBadge(entry.order.status)}
+
+ +
+
+ ))} +
+
+ )} -
- Método: {order.paymentMethodName} + {/* TAB 3: RUTAS DE ENTREGA ORGANIZADAS POR DIRECCIONES & WAZE */} + {activeTab === 'routes' && ( +
+
+
+
+ + Hoja de Ruta & Navegación GPS (Waze / Google Maps) +
+

+ Entregas contraentrega secuenciadas por zona geográfica. El mensajero puede presionar "Abrir en Waze" para iniciar la navegación instantánea a cada portería/casa. +

+
-
- - - - -
+
+
+

Paradas / Entregas

+

{pendingDeliveries.length} Paradas

+
+ +
+

Total a Recaudar

+

{formatCOP(totalBalanceToCollect)}

- ))} +
+ +
+ {pendingDeliveries.length === 0 ? ( +
+ 🎉 No hay entregas pendientes por despachar en este momento. +
+ ) : ( + pendingDeliveries.map((delivery, index) => ( +
+
+
+ Parada + #{index + 1} +
+ +
+
+ {delivery.customerName} + • Orden #{delivery.id} + {getStatusBadge(delivery.status)} +
+ +

+ + {delivery.address} ({delivery.cityName}) +

+ +
+ + {delivery.customerPhone} + + • + + {delivery.items.map((i) => `${i.productName} (Talla ${i.size})`).join(', ')} + +
+ + {delivery.notes && ( +

+ Nota: {delivery.notes} +

+ )} +
+
+ +
+
+

Cobrar Contraentrega

+

{formatCOP(delivery.balanceDue)}

+
+ + + + + + +
+
+ )) + )} +
)}
diff --git a/zapply-app/src/app/admin/pedidos/page.tsx b/zapply-app/src/app/admin/pedidos/page.tsx index 3f01444..c0203a2 100644 --- a/zapply-app/src/app/admin/pedidos/page.tsx +++ b/zapply-app/src/app/admin/pedidos/page.tsx @@ -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([]); + const [activeTab, setActiveTab] = useState<'individual' | 'suppliers' | 'routes'>('individual'); const [search, setSearch] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); + const [toastMessage, setToastMessage] = useState(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 Separación $10k Pagada; case 'supplier_notified': - return Proveedor Notificado; + return Proveedor Notificado; case 'dispatched': - return En Ruta con Mensajero; + return En Ruta con Mensajero; case 'delivered': return Entregado & Cobrado; 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); + + const pendingDeliveries = orders.filter((o) => o.status !== 'delivered' && o.status !== 'cancelled'); + const totalBalanceToCollect = pendingDeliveries.reduce((acc, curr) => acc + curr.balanceDue, 0); + return (
{/* Header */} -
-

- - Gestión de Pedidos & Separación con Proveedores ({orders.length}) -

-

- Control logístico de pedidos contraentrega, cobro de anticipos de $10.000 COP y notificación inmediata de separación al proveedor vía WhatsApp. -

-
- - {/* Toolbar */} -
-
- - - - -
- -
- 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" - /> -
-
- - {filteredOrders.length === 0 ? ( -
-
- -
-

No hay pedidos en esta sección

-

- 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. +

+
+

+ + Centro Logístico: Pedidos, Proveedores & Rutas Waze +

+

+ Consolidación automática para separación con proveedores y hoja de ruta para mensajeros con navegación Waze en 1 clic.

- ) : ( +
+ + {toastMessage && ( +
+ {toastMessage} + +
+ )} + + {/* Main Tabs Navigation */} +
+ + + + + +
+ + {/* TAB 1: INDIVIDUAL ORDERS LIST */} + {activeTab === 'individual' && (
- {filteredOrders.map((order) => ( -
-
-
- Orden #{order.id} - • {new Date(order.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} -
- {getStatusBadge(order.status)} +
+
+ {['all', 'pending', 'deposit_received', 'supplier_notified', 'dispatched', 'delivered'].map((st) => ( + + ))} +
+ +
+ 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" + /> +
+
+ +
+ {filteredOrders.length === 0 ? ( +
+ No hay pedidos que coincidan con los filtros.
+ ) : ( + filteredOrders.map((order) => ( +
+
+
+ Orden #{order.id} + {getStatusBadge(order.status)} + {order.createdAt} +
-
-
- - Cliente & Entrega (24 Horas) - -

{order.customerName}

-

- {order.customerPhone} -

-

- {order.address} ({order.cityName}) -

-
+
+ Estado: + +
+
-
- - Zapatillas & Proveedor Asignado: - -
- {order.items.map((it, idx) => { - const suppName = it.supplierName || 'DIFUSION JOVIC STORE'; - return ( -
-
+
+
+

Cliente & Destino

+

{order.customerName}

+

+ {order.customerPhone} +

+

+ + {order.address} ({order.cityName}) +

+
+ +
+

Resumen de Cobro

+
+ Total Venta: + {formatCOP(order.totalAmount)} +
+
+ Anticipo / Separación: + -{formatCOP(order.depositAmount)} +
+
+ Saldo Contraentrega: + {formatCOP(order.balanceDue)} +
+
+ +
+ + + +
+
+ +
+

Pares en esta orden:

+
+ {order.items.map((item, idx) => ( +
+
+
-

{it.productName}

-

- Talla {it.size} • Cant: {it.quantity} +

{item.productName}

+

+ Talla: {item.size} | Cantidad: {item.quantity} | Proveedor: {item.supplierName || 'Bodega Principal'}

- - Prov: {suppName} -
- ); - })} + ))} +
+
+
+ )) + )} +
+
+ )} + + {/* TAB 2: CONSOLIDADO AGRUPADO POR PROVEEDOR */} + {activeTab === 'suppliers' && ( +
+
+
+ + Consolidado Automático de Compras & Separaciones +
+

+ Aquí se agrupan todos los pares pendientes clasificados por cada Proveedor Mayorista. Puedes enviar el manifiesto consolidado en 1 solo mensaje de WhatsApp para que el proveedor prepare y empaque todos los pedidos de la jornada juntos. +

+
+ +
+ {Object.values(supplierGroups).map((group) => ( +
+
+
+ + Proveedor Mayorista + +

{group.name}

+

+ + WhatsApp Proveedor: {group.phone} +

+
+ +
+
+

Total Pares

+

{group.totalPairs} par(es)

+
+
+

Total a Pagar

+

{formatCOP(group.totalCost)}

+
-
- - Cobro & Pagos Contraentrega - -
-

- Total de la Orden: - {formatCOP(order.totalAmount)} -

- {order.depositAmount > 0 ? ( - <> -

- Anticipo / Separación: - {formatCOP(order.depositAmount)} -

-

- Saldo al Mensajero: - {formatCOP(order.balanceDue)} -

- - ) : ( -

- 100% Contraentrega al recibir -

- )} -
+
+ + + + + + + + + + + + + + {group.entries.map((entry, idx) => ( + + + + + + + + + + ))} + +
Modelo / ZapatillaTallaCant.Costo BaseOrden ClienteDestinoEstado Orden
+ + {entry.item.productName} + {entry.item.size}{entry.item.quantity}{formatCOP(entry.item.supplierPrice || 0)}#{entry.order.id}{entry.order.cityName}{getStatusBadge(entry.order.status)}
+
+ +
+
+ ))} +
+
+ )} -
- Método: {order.paymentMethodName} + {/* TAB 3: RUTAS DE ENTREGA ORGANIZADAS POR DIRECCIONES & WAZE */} + {activeTab === 'routes' && ( +
+
+
+
+ + Hoja de Ruta & Navegación GPS (Waze / Google Maps) +
+

+ Entregas contraentrega secuenciadas por zona geográfica. El mensajero puede presionar "Abrir en Waze" para iniciar la navegación instantánea a cada portería/casa. +

+
-
- - - - -
+
+
+

Paradas / Entregas

+

{pendingDeliveries.length} Paradas

+
+ +
+

Total a Recaudar

+

{formatCOP(totalBalanceToCollect)}

- ))} +
+ +
+ {pendingDeliveries.length === 0 ? ( +
+ 🎉 No hay entregas pendientes por despachar en este momento. +
+ ) : ( + pendingDeliveries.map((delivery, index) => ( +
+
+
+ Parada + #{index + 1} +
+ +
+
+ {delivery.customerName} + • Orden #{delivery.id} + {getStatusBadge(delivery.status)} +
+ +

+ + {delivery.address} ({delivery.cityName}) +

+ +
+ + {delivery.customerPhone} + + • + + {delivery.items.map((i) => `${i.productName} (Talla ${i.size})`).join(', ')} + +
+ + {delivery.notes && ( +

+ Nota: {delivery.notes} +

+ )} +
+
+ +
+
+

Cobrar Contraentrega

+

{formatCOP(delivery.balanceDue)}

+
+ + + + + + +
+
+ )) + )} +
)}