diff --git a/almeira-store-app/src/app/admin/layout.tsx b/almeira-store-app/src/app/admin/layout.tsx index ae22bc5..ff0624d 100644 --- a/almeira-store-app/src/app/admin/layout.tsx +++ b/almeira-store-app/src/app/admin/layout.tsx @@ -15,6 +15,7 @@ import { Store, Users, MessageSquare, + Building2, } from 'lucide-react'; export default function AdminLayout({ children }: { children: React.ReactNode }) { @@ -22,6 +23,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) const NAV_ITEMS = [ { href: '/admin', label: 'Resumen & KPIs', icon: LayoutDashboard }, + { href: '/admin/proveedores', label: 'Proveedores & Bodegas', icon: Building2, badge: 'Activos/Inactivos' }, { href: '/admin/grupos-whatsapp', label: 'Grupos WhatsApp', icon: MessageSquare, badge: 'Monitoreo 24/7' }, { href: '/admin/usuarios', label: 'Control de Usuarios', icon: Users, badge: 'Roles & Accesos' }, { href: '/admin/configuracion', label: 'Datos de la Tienda', icon: Store }, diff --git a/almeira-store-app/src/app/admin/pedidos/page.tsx b/almeira-store-app/src/app/admin/pedidos/page.tsx index c0203a2..b08d541 100644 --- a/almeira-store-app/src/app/admin/pedidos/page.tsx +++ b/almeira-store-app/src/app/admin/pedidos/page.tsx @@ -20,25 +20,90 @@ import { Sparkles, Compass, Check, - AlertCircle + AlertCircle, + Edit3, + Trash2, + Plus, + X, + CreditCard, + User, + FileText } from 'lucide-react'; -import { Order, OrderItem } from '@/types'; +import { Order, OrderItem, Supplier, City } from '@/types'; import { formatCOP } from '@/lib/pricing-engine'; export default function AdminOrdersPage() { const [orders, setOrders] = useState([]); + const [suppliers, setSuppliers] = useState([]); + const [cities, setCities] = useState([]); const [activeTab, setActiveTab] = useState<'individual' | 'suppliers' | 'routes'>('individual'); const [search, setSearch] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const [toastMessage, setToastMessage] = useState(null); + // Edit Order Modal State + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [editingOrder, setEditingOrder] = useState(null); + const [editCustomerName, setEditCustomerName] = useState(''); + const [editCustomerPhone, setEditCustomerPhone] = useState(''); + const [editAddress, setEditAddress] = useState(''); + const [editCityId, setEditCityId] = useState('city-cali'); + const [editCityName, setEditCityName] = useState('Cali'); + const [editStatus, setEditStatus] = useState('pending'); + const [editPaymentMethodName, setEditPaymentMethodName] = useState(''); + const [editTotalAmount, setEditTotalAmount] = useState(0); + const [editDepositAmount, setEditDepositAmount] = useState(10000); + const [editShippingCost, setEditShippingCost] = useState(0); + const [editNotes, setEditNotes] = useState(''); + const [editItems, setEditItems] = useState([]); + const [isSavingOrder, setIsSavingOrder] = useState(false); + + // Delete Order Confirmation Modal State + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [orderToDelete, setOrderToDelete] = useState(null); + useEffect(() => { - fetch('/api/orders') - .then((r) => r.json()) - .then((d) => d.success && setOrders(d.orders)) - .catch(() => {}); + fetchOrders(); + fetchSuppliers(); + fetchCities(); }, []); + const fetchOrders = async () => { + try { + const r = await fetch('/api/orders'); + const d = await r.json(); + if (d.success && d.orders) { + setOrders(d.orders); + } + } catch (e) { + console.error('Error loading orders:', e); + } + }; + + const fetchSuppliers = async () => { + try { + const r = await fetch('/api/suppliers?all=true'); + const d = await r.json(); + if (d.success && d.suppliers) { + setSuppliers(d.suppliers); + } + } catch (e) { + console.error('Error loading suppliers:', e); + } + }; + + const fetchCities = async () => { + try { + const r = await fetch('/api/cities'); + const d = await r.json(); + if (d.success && d.cities) { + setCities(d.cities); + } + } catch (e) { + console.error('Error loading cities:', e); + } + }; + const handleStatusChange = async (orderId: string, newStatus: Order['status']) => { setOrders((prev) => prev.map((o) => (o.id === orderId ? { ...o, status: newStatus } : o)) @@ -51,13 +116,158 @@ export default function AdminOrdersPage() { body: JSON.stringify({ id: orderId, status: newStatus }), }); setToastMessage(`Estado de la orden #${orderId} actualizado a "${newStatus}".`); + setTimeout(() => setToastMessage(null), 4000); } catch (e) { console.error(e); } }; + // Open Edit Order Modal + const handleOpenEditModal = (order: Order) => { + setEditingOrder(order); + setEditCustomerName(order.customerName); + setEditCustomerPhone(order.customerPhone); + setEditAddress(order.address); + setEditCityId(order.cityId); + setEditCityName(order.cityName); + setEditStatus(order.status); + setEditPaymentMethodName(order.paymentMethodName || 'Contraentrega'); + setEditTotalAmount(order.totalAmount); + setEditDepositAmount(order.depositAmount); + setEditShippingCost(order.shippingCost || 0); + setEditNotes(order.notes || ''); + setEditItems([...order.items]); + setIsEditModalOpen(true); + }; + + // Handle Edit Item in Modal + const handleItemChange = (index: number, field: keyof OrderItem, value: any) => { + const updated = [...editItems]; + updated[index] = { ...updated[index], [field]: value }; + + // If changing supplierId, auto update supplierName and supplierPhone + if (field === 'supplierName') { + const foundSup = suppliers.find((s) => s.name === value || s.id === value); + if (foundSup) { + updated[index].supplierName = foundSup.name; + updated[index].supplierPhone = foundSup.phone; + } + } + + setEditItems(updated); + + // Auto update total amount if prices/quantities change + if (field === 'price' || field === 'quantity') { + const newSubtotal = updated.reduce((acc, curr) => acc + (curr.price || 0) * (curr.quantity || 1), 0); + setEditTotalAmount(newSubtotal + editShippingCost); + } + }; + + const handleRemoveItemFromEdit = (index: number) => { + const updated = editItems.filter((_, idx) => idx !== index); + setEditItems(updated); + const newSubtotal = updated.reduce((acc, curr) => acc + (curr.price || 0) * (curr.quantity || 1), 0); + setEditTotalAmount(newSubtotal + editShippingCost); + }; + + const handleAddItemToEdit = () => { + const defaultSup = suppliers[0] || { name: 'DIFUSIÓN JOVIC STORE', phone: '573167455877' }; + const newItem: OrderItem = { + productId: `prod-manual-${Date.now()}`, + productName: 'Nuevo Modelo / Zapatilla', + brand: 'Nike', + size: '39', + quantity: 1, + price: 125000, + supplierPrice: 90000, + supplierName: defaultSup.name, + supplierPhone: defaultSup.phone, + image: 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80', + }; + const updated = [...editItems, newItem]; + setEditItems(updated); + const newSubtotal = updated.reduce((acc, curr) => acc + (curr.price || 0) * (curr.quantity || 1), 0); + setEditTotalAmount(newSubtotal + editShippingCost); + }; + + // Save Order Updates + const handleSaveOrderEdits = async (e: React.FormEvent) => { + e.preventDefault(); + if (!editingOrder) return; + setIsSavingOrder(true); + + const balanceDue = editTotalAmount - editDepositAmount; + + const payload: Partial = { + id: editingOrder.id, + customerName: editCustomerName, + customerPhone: editCustomerPhone, + address: editAddress, + cityId: editCityId, + cityName: editCityName, + status: editStatus, + paymentMethodName: editPaymentMethodName, + totalAmount: editTotalAmount, + depositAmount: editDepositAmount, + shippingCost: editShippingCost, + balanceDue: balanceDue > 0 ? balanceDue : 0, + items: editItems, + notes: editNotes, + }; + + try { + const res = await fetch('/api/orders', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const data = await res.json(); + if (data.success) { + setOrders((prev) => + prev.map((o) => (o.id === editingOrder.id ? { ...o, ...payload } as Order : o)) + ); + setToastMessage(`✅ Orden #${editingOrder.id} modificada y guardada correctamente.`); + setTimeout(() => setToastMessage(null), 4000); + setIsEditModalOpen(false); + } + } catch (err) { + console.error('Error saving order edits:', err); + } finally { + setIsSavingOrder(false); + } + }; + + // Delete Order + const handleOpenDeleteModal = (order: Order) => { + setOrderToDelete(order); + setIsDeleteModalOpen(true); + }; + + const handleConfirmDeleteOrder = async () => { + if (!orderToDelete) return; + const targetId = orderToDelete.id; + + try { + const res = await fetch(`/api/orders?id=${targetId}`, { + method: 'DELETE', + }); + const data = await res.json(); + if (data.success) { + setOrders((prev) => prev.filter((o) => o.id !== targetId)); + setToastMessage(`🗑️ Orden #${targetId} eliminada correctamente.`); + setTimeout(() => setToastMessage(null), 4000); + } + } catch (err) { + console.error('Error deleting order:', err); + } finally { + setIsDeleteModalOpen(false); + setOrderToDelete(null); + } + }; + + // 1. Notificar separación individual a proveedor const handleNotifySingleSupplier = (order: Order, item: OrderItem) => { - const supplierPhone = item.supplierPhone || '573134848203'; + const supplierPhone = item.supplierPhone || '573167455877'; const supplierName = item.supplierName || 'Proveedor Mayorista'; const supplierCost = item.supplierPrice ? formatCOP(item.supplierPrice) : 'Precio Mayorista'; @@ -71,6 +281,7 @@ 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); @@ -145,14 +356,19 @@ export default function AdminOrdersPage() { return true; }); + // Consolidado por Proveedor const supplierGroups = orders.reduce((acc, order) => { order.items.forEach((item) => { - const suppName = item.supplierName || 'DIFUSION JOVIC STORE'; - const suppPhone = item.supplierPhone || '573134848203'; + const suppName = item.supplierName || '💎 DIFUSIÓN JOVIC STORE COLOMBIA'; + const suppPhone = item.supplierPhone || '573167455877'; + const matchingSupplier = suppliers.find((s) => s.name.toLowerCase() === suppName.toLowerCase()); + const isActiveSup = matchingSupplier ? matchingSupplier.isActive !== false : true; + if (!acc[suppName]) { acc[suppName] = { name: suppName, phone: suppPhone, + isActive: isActiveSup, entries: [], totalPairs: 0, totalCost: 0, @@ -163,7 +379,7 @@ export default function AdminOrdersPage() { acc[suppName].totalCost += (item.supplierPrice || 0) * item.quantity; }); return acc; - }, {} as Record); + }, {} as Record); const pendingDeliveries = orders.filter((o) => o.status !== 'delivered' && o.status !== 'cancelled'); const totalBalanceToCollect = pendingDeliveries.reduce((acc, curr) => acc + curr.balanceDue, 0); @@ -174,17 +390,17 @@ export default function AdminOrdersPage() {

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

+ Gestión completa de pedidos (editar datos, cambiar tallas, precios o eliminar), consolidación con proveedores y navegación Waze en 1 clic.

{toastMessage && ( -
+
{toastMessage} @@ -222,11 +438,11 @@ export default function AdminOrdersPage() { 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' + ? 'bg-gradient-to-r from-cyan-500 to-emerald-500 text-white shadow-lg shadow-emerald-500/30' + : 'text-slate-400 hover:text-white' }`} > - + 3. Rutas de Entrega & Waze ({pendingDeliveries.length})
@@ -235,13 +451,13 @@ export default function AdminOrdersPage() { {activeTab === 'individual' && (
-
+
{['all', 'pending', 'deposit_received', 'supplier_notified', 'dispatched', 'delivered'].map((st) => (
{filteredOrders.length === 0 ? ( -
+
No hay pedidos que coincidan con los filtros.
) : ( filteredOrders.map((order) => (
-
+ {/* Top bar */} +
Orden #{order.id} {getStatusBadge(order.status)} - {order.createdAt} + {order.createdAt}
- Estado: + + + + setEditCustomerName(e.target.value)} + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-cyan-400 font-bold" + /> +
+ +
+ + setEditCustomerPhone(e.target.value)} + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-emerald-300 outline-hidden focus:border-emerald-400 font-mono font-bold" + /> +
+ +
+ + setEditAddress(e.target.value)} + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-cyan-400" + /> +
+ +
+ + +
+ +
+ + +
+
+ + {/* Items Management */} +
+
+

Zapatillas & Proveedores en la Orden

+ +
+ +
+ {editItems.map((item, idx) => ( +
+
+ Par #{idx + 1} + {editItems.length > 1 && ( + + )} +
+ +
+
+ + handleItemChange(idx, 'productName', e.target.value)} + className="w-full bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1.5 text-white outline-hidden" + /> +
+ +
+ + handleItemChange(idx, 'size', e.target.value)} + className="w-full bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1.5 text-white font-mono font-bold outline-hidden" + /> +
+
+ +
+
+ + handleItemChange(idx, 'quantity', parseInt(e.target.value, 10) || 1)} + className="w-full bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1.5 text-white outline-hidden" + /> +
+ +
+ + handleItemChange(idx, 'price', parseInt(e.target.value, 10) || 0)} + className="w-full bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1.5 text-white font-mono outline-hidden" + /> +
+ +
+ + handleItemChange(idx, 'supplierPrice', parseInt(e.target.value, 10) || 0)} + className="w-full bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1.5 text-emerald-300 font-mono outline-hidden" + /> +
+
+ +
+ + +
+
+ ))} +
+
+ + {/* Financial calculations */} +
+
+ + setEditTotalAmount(parseInt(e.target.value, 10) || 0)} + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white font-mono font-bold outline-hidden focus:border-cyan-400" + /> +
+ +
+ + setEditDepositAmount(parseInt(e.target.value, 10) || 0)} + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-blue-300 font-mono font-bold outline-hidden focus:border-cyan-400" + /> +
+ +
+ +
+ {formatCOP(editTotalAmount - editDepositAmount)} +
+
+
+ +
+ +