284 lines
14 KiB
TypeScript
284 lines
14 KiB
TypeScript
'use client';
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import { ShoppingBag, Truck, CheckCircle2, Clock, Phone, MapPin, Search, MessageCircle, Send, ShieldCheck, UserCheck } from 'lucide-react';
|
||
import { Order, OrderItem } from '@/types';
|
||
import { formatCOP } from '@/lib/pricing-engine';
|
||
|
||
export default function AdminOrdersPage() {
|
||
const [orders, setOrders] = useState<Order[]>([]);
|
||
const [search, setSearch] = useState('');
|
||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||
|
||
useEffect(() => {
|
||
fetch('/api/orders')
|
||
.then((r) => r.json())
|
||
.then((d) => d.success && setOrders(d.orders))
|
||
.catch(() => {});
|
||
}, []);
|
||
|
||
const handleStatusChange = async (orderId: string, newStatus: Order['status']) => {
|
||
setOrders((prev) =>
|
||
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(() => {});
|
||
};
|
||
|
||
const handleNotifySupplier = (order: Order, item: OrderItem) => {
|
||
const supplierPhone = item.supplierPhone || '573134848203';
|
||
const supplierName = item.supplierName || 'Proveedor Mayorista';
|
||
const supplierCost = item.supplierPrice ? formatCOP(item.supplierPrice) : 'Precio Mayorista';
|
||
|
||
const message = `👋 Hola *${supplierName}*!\n\nPor favor *sepárame este par* para despacho inmediato:\n\n👟 *Modelo:* ${item.productName}\n📏 *Talla:* ${item.size}\n📦 *Cantidad:* ${item.quantity} par(es)\n💵 *Costo Base:* ${supplierCost}\n\n🏷️ *Orden Interna:* #${order.id}\n📍 *Ciudad Destino:* ${order.cityName}\n\n¿Me confirmas si lo tienes listo para recoger / enviar? ¡Muchas gracias!`;
|
||
|
||
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 getStatusBadge = (status: Order['status']) => {
|
||
switch (status) {
|
||
case 'pending':
|
||
return <span className="bg-amber-500/20 text-amber-400 border border-amber-500/30 font-bold px-2.5 py-1 rounded-full text-[10px]">Pendiente Confirmar</span>;
|
||
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>;
|
||
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>;
|
||
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':
|
||
return <span className="bg-red-500/20 text-red-400 border border-red-500/30 font-bold px-2.5 py-1 rounded-full text-[10px]">Cancelado</span>;
|
||
}
|
||
};
|
||
|
||
const filteredOrders = orders.filter((o) => {
|
||
const matchesSearch =
|
||
o.customerName.toLowerCase().includes(search.toLowerCase()) ||
|
||
o.customerPhone.includes(search) ||
|
||
o.cityName.toLowerCase().includes(search.toLowerCase()) ||
|
||
o.id.toLowerCase().includes(search.toLowerCase());
|
||
|
||
if (!matchesSearch) return false;
|
||
if (statusFilter !== 'all' && o.status !== statusFilter) return false;
|
||
return true;
|
||
});
|
||
|
||
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.
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<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>
|
||
|
||
<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>
|
||
|
||
{/* 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>
|
||
<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>
|
||
</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"
|
||
>
|
||
<MessageCircle className="w-3.5 h-3.5 text-emerald-400" />
|
||
<span>📲 Separar con {suppName} (WhatsApp)</span>
|
||
</button>
|
||
</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>
|
||
</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>
|
||
|
||
<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>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|