almeira-web/almeira-store-app/src/app/admin/pedidos/page.tsx

284 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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