Tul xxx Tul
User / IP
:
216.73.216.146
Host / Server
:
45.84.207.204 / aircan.me
System
:
Linux lt-bnk-web1726.main-hosting.eu 5.14.0-611.36.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Mar 3 11:23:52 EST 2026 x86_64
Command
|
Upload
|
Create
Mass Deface
|
Jumping
|
Symlink
|
Reverse Shell
Ping
|
Port Scan
|
DNS Lookup
|
Whois
|
Header
|
cURL
:
/
home
/
u931257429
/
domains
/
aircan.me
/
public_html
/
comidarapidafran2
/
admin
/
Viewing: delivery_orders.php
<?php include '../components/connect.php'; require_once '../components/admin_roles.php'; session_start(); $admin_id = $_SESSION['admin_id']; if(!isset($admin_id)){ header('location:admin_login.php'); } ensureAdminRolesSchema($conn); $currentRole = getRoleBySession($conn); $canViewInsights = adminHasPermission($currentRole, 'orders_insights'); date_default_timezone_set('America/Bogota'); $today = new DateTime('today'); $tomorrow = (clone $today)->modify('+1 day'); $today_start = $today->format('Y-m-d H:i:s'); $tomorrow_start = $tomorrow->format('Y-m-d H:i:s'); // Obtener fechas de filtro $fecha_desde = $canViewInsights && isset($_GET['fecha_desde']) ? $_GET['fecha_desde'] : $today->format('Y-m-01'); $fecha_hasta = $canViewInsights && isset($_GET['fecha_hasta']) ? $_GET['fecha_hasta'] : $today->format('Y-m-d'); if($fecha_desde > $fecha_hasta){ $fecha_desde = $fecha_hasta; } // Obtener estadÃsticas del dÃa $select_stats_today = $conn->prepare("SELECT COUNT(*) as total_pedidos, COALESCE(SUM(total_amount), 0) as total_recaudado FROM `delivery_orders` WHERE created_at >= ? AND created_at < ?"); $select_stats_today->execute([$today_start, $tomorrow_start]); $stats_today = $select_stats_today->fetch(PDO::FETCH_ASSOC) ?: ['total_pedidos' => 0, 'total_recaudado' => 0]; // Obtener estadÃsticas del rango de fechas $select_stats_range = $conn->prepare("SELECT COUNT(*) as total_pedidos, COALESCE(SUM(total_amount), 0) as total_recaudado FROM `delivery_orders` WHERE DATE(created_at) BETWEEN ? AND ?"); $select_stats_range->execute([$fecha_desde, $fecha_hasta]); $stats_range = $select_stats_range->fetch(PDO::FETCH_ASSOC) ?: ['total_pedidos' => 0, 'total_recaudado' => 0]; // Entregados del rango (por fecha de creación) $deliveredRange = 0; try { $delRangeStmt = $conn->prepare("SELECT COUNT(*) FROM `delivery_orders` WHERE status = 'entregado' AND DATE(created_at) BETWEEN ? AND ?"); $delRangeStmt->execute([$fecha_desde, $fecha_hasta]); $deliveredRange = (int)$delRangeStmt->fetchColumn(); } catch (Throwable $e) { $deliveredRange = 0; } // Estado de pedidos de hoy $todayStatusCounts = [ 'pendiente' => 0, 'en_preparacion' => 0, 'en_camino' => 0, 'entregado' => 0, 'cancelado' => 0, ]; try { $stmt = $conn->prepare("SELECT status, COUNT(*) AS total FROM `delivery_orders` WHERE created_at >= ? AND created_at < ? GROUP BY status"); $stmt->execute([$today_start, $tomorrow_start]); foreach($stmt->fetchAll(PDO::FETCH_ASSOC) as $row){ $key = (string)($row['status'] ?? ''); if($key !== '' && array_key_exists($key, $todayStatusCounts)){ $todayStatusCounts[$key] = (int)($row['total'] ?? 0); } } } catch (Throwable $e) { } $inProgressToday = (int)$todayStatusCounts['pendiente'] + (int)$todayStatusCounts['en_preparacion'] + (int)$todayStatusCounts['en_camino']; $deliveredToday = (int)$todayStatusCounts['entregado']; $cancelledToday = (int)$todayStatusCounts['cancelado']; $deliveryRateToday = ((int)$stats_today['total_pedidos'] > 0) ? round(((float)$deliveredToday / (float)$stats_today['total_pedidos']) * 100) : 0; $avgTicketToday = ((int)$stats_today['total_pedidos'] > 0) ? ((float)$stats_today['total_recaudado'] / (float)$stats_today['total_pedidos']) : 0.0; // Métodos de pago de hoy $todayPaymentCounts = [ 'efectivo' => 0, 'transferencia' => 0, ]; try { $payStmt = $conn->prepare("SELECT payment_method, COUNT(*) AS total FROM `delivery_orders` WHERE created_at >= ? AND created_at < ? GROUP BY payment_method"); $payStmt->execute([$today_start, $tomorrow_start]); foreach($payStmt->fetchAll(PDO::FETCH_ASSOC) as $row){ $key = (string)($row['payment_method'] ?? ''); if($key !== '' && array_key_exists($key, $todayPaymentCounts)){ $todayPaymentCounts[$key] = (int)($row['total'] ?? 0); } } } catch (Throwable $e) { } // EstadÃsticas del mes actual $monthStart = (clone $today)->modify('first day of this month')->format('Y-m-d H:i:s'); $select_stats_month = $conn->prepare("SELECT COUNT(*) as total_pedidos, COALESCE(SUM(total_amount), 0) as total_recaudado FROM `delivery_orders` WHERE created_at >= ? AND created_at < ?"); $select_stats_month->execute([$monthStart, $tomorrow_start]); $stats_month = $select_stats_month->fetch(PDO::FETCH_ASSOC) ?: ['total_pedidos' => 0, 'total_recaudado' => 0]; // Obtener pedidos pendientes (no entregados) sin importar la fecha $select_orders = $conn->prepare( "SELECT o.*, (SELECT GROUP_CONCAT(DISTINCT pr.name ORDER BY pr.name SEPARATOR ', ') FROM delivery_order_preparers dop JOIN preparers pr ON pr.id = dop.preparer_id WHERE dop.order_id = o.id) AS preparer_names, (SELECT GROUP_CONCAT(DISTINCT dop2.preparer_id ORDER BY dop2.preparer_id SEPARATOR ',') FROM delivery_order_preparers dop2 WHERE dop2.order_id = o.id) AS preparer_ids, p.name AS preparer_name FROM `delivery_orders` o LEFT JOIN `preparers` p ON o.preparer_id = p.id WHERE o.status <> 'entregado' ORDER BY o.created_at ASC" ); $select_orders->execute(); $orders = $select_orders->fetchAll(PDO::FETCH_ASSOC); $productPrepMap = []; try { $productIds = []; foreach($orders as $o){ $itemsRaw = json_decode($o['order_items'] ?? '[]', true); if(!is_array($itemsRaw)){ continue; } foreach($itemsRaw as $it){ $pid = (int)($it['id'] ?? 0); if($pid > 0){ $productIds[$pid] = $pid; } } } if(!empty($productIds)){ $placeholders = implode(',', array_fill(0, count($productIds), '?')); $ppStmt = $conn->prepare( "SELECT pp.product_id, GROUP_CONCAT(DISTINCT pr.name ORDER BY pr.name SEPARATOR ', ') AS names FROM product_preparers pp JOIN preparers pr ON pr.id = pp.preparer_id WHERE pp.product_id IN ($placeholders) GROUP BY pp.product_id" ); $ppStmt->execute(array_values($productIds)); foreach($ppStmt->fetchAll(PDO::FETCH_ASSOC) as $row){ $productPrepMap[(int)$row['product_id']] = (string)($row['names'] ?? ''); } } } catch (Throwable $e) { $productPrepMap = []; } // Obtener categorÃas y productos $select_categories = $conn->prepare("SELECT * FROM `categories` ORDER BY name ASC"); $select_categories->execute(); $categorias = $select_categories->fetchAll(PDO::FETCH_ASSOC); $select_products = $conn->prepare( "SELECT p.*, c.name as category_name, (SELECT GROUP_CONCAT(DISTINCT pr.name ORDER BY pr.name SEPARATOR ', ') FROM product_preparers pp JOIN preparers pr ON pr.id = pp.preparer_id WHERE pp.product_id = p.id) AS preparer_names, (SELECT GROUP_CONCAT(DISTINCT pp2.preparer_id ORDER BY pp2.preparer_id SEPARATOR ',') FROM product_preparers pp2 WHERE pp2.product_id = p.id) AS preparer_ids FROM `products` p LEFT JOIN `categories` c ON p.category = c.id ORDER BY c.name, p.name" ); $select_products->execute(); $productos = $select_products->fetchAll(PDO::FETCH_ASSOC); // Obtener preparadores activos $select_preparers = $conn->prepare("SELECT id, name FROM `preparers` WHERE status = 'activo' ORDER BY name ASC"); $select_preparers->execute(); $preparers = $select_preparers->fetchAll(PDO::FETCH_ASSOC); function normaliza_categoria($str){ $str = mb_strtolower($str, 'UTF-8'); $map = ['á'=>'a','à '=>'a','ä'=>'a','â'=>'a','Ã'=>'a','À'=>'a','Â'=>'a','Ä'=>'a','é'=>'e','è'=>'e','ë'=>'e','ê'=>'e','É'=>'e','È'=>'e','Ê'=>'e','Ë'=>'e','Ã'=>'i','ì'=>'i','ï'=>'i','î'=>'i','Ã'=>'i','ÃŒ'=>'i','ÃŽ'=>'i','Ã'=>'i','ó'=>'o','ò'=>'o','ö'=>'o','ô'=>'o','Ó'=>'o','Ã’'=>'o','Ô'=>'o','Ö'=>'o','ú'=>'u','ù'=>'u','ü'=>'u','û'=>'u','Ú'=>'u','Ù'=>'u','Û'=>'u','Ü'=>'u','ñ'=>'n','Ñ'=>'n','ç'=>'c','Ç'=>'c']; $str = strtr($str, $map); $str = preg_replace('/[^a-z0-9]+/','-',$str); $str = trim($str,'-'); return $str; } $businessName = getBusinessName($conn); $businessLogoVersion = getBusinessLogoVersion($conn); $iconHref = '../icon.php?size=64' . ($businessLogoVersion !== '' ? '&v=' . rawurlencode($businessLogoVersion) : ''); $fecha_desde_safe = htmlspecialchars((string)$fecha_desde, ENT_QUOTES, 'UTF-8'); $fecha_hasta_safe = htmlspecialchars((string)$fecha_hasta, ENT_QUOTES, 'UTF-8'); // Build product catalog and categories for POS $productosCatalogo = []; $productosPorCategoria = []; foreach($productos as $producto){ $catName = $producto['category_name'] ?? 'Sin categorÃa'; $catKey = normaliza_categoria($catName); if(!isset($productosPorCategoria[$catKey])){ $productosPorCategoria[$catKey] = ['nombre' => $catName, 'items' => []]; } $salePrice = isset($producto['sale_price']) && (float)$producto['sale_price'] > 0 ? (float)$producto['sale_price'] : (float)$producto['price']; $costPrice = (float)$producto['price']; $prodData = [ 'id' => (int)$producto['id'], 'name' => $producto['name'], 'price' => $salePrice, 'cost_price' => $costPrice, 'image' => $producto['image'], 'category_key' => $catKey, 'category_name' => $catName, 'preparer_names' => $producto['preparer_names'] ?? '', 'preparer_ids' => $producto['preparer_ids'] ?? '' ]; $productosPorCategoria[$catKey]['items'][] = $prodData; $productosCatalogo[$prodData['id']] = $prodData; } $productosJson = json_encode($productosCatalogo, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) ?: '{}'; ?> <!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>POS Domicilios | <?= htmlspecialchars($businessName); ?></title> <link rel="icon" href="<?= htmlspecialchars($iconHref); ?>" type="image/png"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <style> :root { --pos-primary: #0ea5e9; --pos-primary-dark: #0284c7; --pos-secondary: #0f172a; --pos-bg: #f8fafc; --pos-surface: #ffffff; --pos-border: #e2e8f0; --pos-text: #1e293b; --pos-text-muted: #64748b; --pos-success: #10b981; --pos-warning: #f59e0b; --pos-danger: #ef4444; --pos-sidebar-width: 380px; --pos-header-height: 64px; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Inter', system-ui, -apple-system, sans-serif; background: var(--pos-bg); color: var(--pos-text); overflow-x: hidden; } .pos-topbar { height: var(--pos-header-height); background: var(--pos-surface); border-bottom: 1px solid var(--pos-border); display: flex; align-items: center; justify-content: space-between; padding: 0 1.5rem; position: sticky; top: 0; z-index: 100; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); } .pos-topbar-left { display: flex; align-items: center; gap: 1.5rem; } .btn-back { display: inline-flex; align-items: center; gap: 0.5rem; color: var(--pos-text-muted); text-decoration: none; font-weight: 600; padding: 0.5rem 1rem; border-radius: 8px; transition: all 0.2s; background: var(--pos-bg); border: 1px solid var(--pos-border); } .btn-back:hover { color: var(--pos-secondary); background: #f1f5f9; border-color: #cbd5e1; } .pos-title { font-size: 1.25rem; font-weight: 800; color: var(--pos-secondary); display: flex; align-items: center; gap: 0.5rem; margin:0; } .pos-title i { color: var(--pos-primary); } .pos-workspace { padding: 1.5rem; max-width: 1600px; margin: 0 auto; display: flex; flex-direction: column; gap: 1.5rem; min-height: calc(100vh - var(--pos-header-height)); } .pos-workspace-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; } .btn { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.75rem 1.5rem; border-radius: 12px; font-weight: 600; cursor: pointer; border: none; transition: all 0.2s; text-decoration: none; font-size: 0.95rem; } .btn-primary { background: linear-gradient(135deg, var(--pos-primary), var(--pos-primary-dark)); color: white; box-shadow: 0 4px 14px rgba(14, 165, 233, 0.3); } .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(14, 165, 233, 0.4); } .btn-outline { background: var(--pos-surface); color: var(--pos-text); border: 1px solid var(--pos-border); } .btn-outline:hover { background: var(--pos-bg); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.05); } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; } .stat-card { background: var(--pos-surface); border-radius: 16px; padding: 1.25rem; border: 1px solid var(--pos-border); box-shadow: 0 10px 15px -3px rgba(0,0,0,0.02); display: flex; flex-direction: column; gap: 0.5rem; } .stat-card h3 { display: flex; align-items: center; gap: 0.5rem; color: var(--pos-text-muted); font-weight: 600; font-size: 0.95rem; margin:0;} .stat-value { font-size: 1.75rem; font-weight: 800; color: var(--pos-secondary); } .stat-meta { display: flex; justify-content: space-between; font-size: 0.85rem; color: var(--pos-text-muted); font-weight: 500; flex-wrap: wrap; gap: 0.5rem; } .filters-card { background: var(--pos-surface); border-radius: 16px; padding: 1rem 1.25rem; border: 1px solid var(--pos-border); display: flex; gap: 1rem; align-items: flex-end; flex-wrap: wrap; } .form-group { display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: 0; } .form-group label { font-size: 0.85rem; font-weight: 600; color: var(--pos-text-muted); margin:0;} .orders-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 1.25rem; margin-top: 1rem; } .order-card { background: var(--pos-surface); border-radius: 16px; padding: 1.5rem; border: 1px solid var(--pos-border); box-shadow: 0 10px 25px -5px rgba(0,0,0,0.05); display: flex; flex-direction: column; gap: 1rem; transition: transform 0.2s, box-shadow 0.2s; position: relative; overflow: hidden; } .order-card:hover { transform: translateY(-2px); box-shadow: 0 15px 35px -5px rgba(0,0,0,0.08); border-color: #cbd5e1; } .order-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;} .order-number { font-size: 1.35rem; font-weight: 800; color: var(--pos-primary); display: flex; align-items: center; gap: 0.5rem; margin:0;} .order-status-badge { font-size: 0.75rem; font-weight: 700; padding: 0.3rem 0.6rem; border-radius: 99px; text-transform: uppercase; letter-spacing: 0.05em; } .badge-pendiente { background: #fef3c7; color: #92400e; } .badge-en_preparacion { background: #e0f2fe; color: #0369a1; } .badge-en_camino { background: #f3e8ff; color: #6b21a8; } .badge-entregado { background: #dcfce7; color: #166534; } .badge-cancelado { background: #fee2e2; color: #991b1b; } .order-meta { display: flex; flex-direction: column; gap: 0.4rem; font-size: 0.9rem; color: var(--pos-text-muted); font-weight: 600; } .order-meta i { width: 16px; text-align: center; } .order-items-list { background: var(--pos-bg); border-radius: 12px; padding: 1rem; border: 1px solid var(--pos-border); max-height: 200px; overflow-y: auto; } .order-item { display: flex; justify-content: space-between; font-size: 0.95rem; font-weight: 600; padding: 0.4rem 0; border-bottom: 1px solid rgba(0,0,0,0.03); color: var(--pos-secondary); } .order-item:last-child { border-bottom: none; } .item-qty { color: var(--pos-primary); margin-right: 0.25rem; } .order-notes { background: #fffbeb; border: 1px solid #fde68a; color: #92400e; padding: 0.75rem; border-radius: 10px; font-size: 0.9rem; font-weight: 600; } .order-total { display: flex; justify-content: space-between; font-size: 1.25rem; font-weight: 800; color: var(--pos-secondary); padding: 1rem; background: var(--pos-bg); border-radius: 12px; border: 1px solid var(--pos-border); align-items: center;} .order-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; margin-top: auto; } .btn-action { width: 100%; padding: 0.6rem; border-radius: 10px; font-weight: 600; font-size: 0.9rem; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 0.4rem; transition: background 0.2s, transform 0.1s; } .btn-action:active { transform: scale(0.98); } .btn-edit { background: #e0f2fe; color: #0284c7; } .btn-edit:hover { background: #bae6fd; } .btn-status-btn { background: #f1f5f9; color: #475569; } .btn-status-btn:hover { background: #e2e8f0; } .btn-print { background: #ffedd5; color: #c2410c; } .btn-print:hover { background: #fed7aa; } .btn-delete { background: #fee2e2; color: #991b1b; } .btn-delete:hover { background: #fecaca; } .btn-action.full { grid-column: 1 / -1; } .empty-state { text-align: center; padding: 4rem 2rem; background: var(--pos-surface); border-radius: 16px; border: 1px dashed var(--pos-border); color: var(--pos-text-muted); } .empty-state i { font-size: 3rem; color: #cbd5e1; margin-bottom: 1rem; } .empty-state h3 { font-size: 1.25rem; color: var(--pos-secondary); margin-bottom: 0.5rem; } /* Full-screen POS Modals */ .pos-modal { position: fixed; inset: 0; background: var(--pos-surface); z-index: 1000; display: none; flex-direction: column; overflow: hidden; opacity: 0; transition: opacity 0.25s ease; } .pos-modal.show { display: flex; opacity: 1; } .pos-modal-header { height: 48px; background: var(--pos-secondary); color: white; display: flex; justify-content: space-between; align-items: center; padding: 0 1rem; flex-shrink: 0; } .pos-modal-header-left { display: flex; align-items: center; gap: 0.75rem; } .pos-modal-title { font-size: 1rem; font-weight: 700; display: flex; align-items: center; gap: 0.4rem; margin:0; white-space: nowrap; } .pos-modal-total-badge { display: flex; align-items: center; gap: 0.5rem; background: rgba(255,255,255,0.12); padding: 0.3rem 0.75rem; border-radius: 8px; } .pos-modal-total-label { font-size: 0.7rem; font-weight: 600; color: rgba(255,255,255,0.7); text-transform: uppercase; letter-spacing: 0.05em; } .pos-modal-total-value { font-size: 1.15rem; font-weight: 900; color: #22d3ee; } .pos-modal-items-count { font-size: 0.7rem; color: rgba(255,255,255,0.5); font-weight: 600; } .btn-close-modal { background: rgba(255,255,255,0.1); color: white; border: none; padding: 0.35rem 0.75rem; border-radius: 6px; font-weight: 600; cursor: pointer; transition: background 0.2s; display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem;} .btn-close-modal:hover { background: rgba(255,255,255,0.2); } .pos-modal-body { display: flex; flex: 1; height: calc(100vh - 48px); overflow: hidden; } .pos-pane-left { flex: 1; display: flex; flex-direction: column; background: var(--pos-bg); border-right: 1px solid var(--pos-border); min-width: 0; } .pos-categories { padding: 0.5rem 1rem; display: flex; gap: 0.35rem; overflow-x: auto; border-bottom: 1px solid var(--pos-border); background: var(--pos-surface); scrollbar-width: none; flex-shrink: 0; } .pos-categories::-webkit-scrollbar { display: none; } .pos-search-bar { padding: 0.6rem 1rem; border-bottom: 1px solid var(--pos-border); background: var(--pos-surface); flex-shrink: 0; } .pos-search-input { width: 100%; padding: 0.5rem 0.75rem; border: 2px solid var(--pos-border); border-radius: 8px; font-size: 0.85rem; font-weight: 600; outline: none; background: var(--pos-bg); transition: border 0.2s; color: var(--pos-text); } .pos-search-input:focus { border-color: var(--pos-primary); background-color: var(--pos-surface); } .btn-category { white-space: nowrap; padding: 0.4rem 0.9rem; border-radius: 99px; font-weight: 700; font-size: 0.8rem; cursor: pointer; border: 2px solid var(--pos-border); background: var(--pos-surface); color: var(--pos-text-muted); transition: all 0.2s; } .btn-category.active, .btn-category:hover { background: var(--pos-primary); border-color: var(--pos-primary); color: white; box-shadow: 0 2px 8px rgba(14, 165, 233, 0.25); } .pos-products-wrap { flex: 1; overflow-y: auto; padding: 0.75rem; } .pos-products-wrap::-webkit-scrollbar { width: 6px; } .pos-products-wrap::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } .pos-products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0.6rem; } .pos-product { background: var(--pos-surface); border-radius: 10px; padding: 0.5rem; border: 1px solid var(--pos-border); cursor: pointer; display: flex; flex-direction: column; gap: 0.25rem; transition: all 0.2s; position: relative; overflow: hidden; } .pos-product:hover { border-color: var(--pos-primary); box-shadow: 0 4px 12px -2px rgba(14, 165, 233, 0.15); transform: translateY(-1px); } .pos-product:active { transform: scale(0.98); } .pos-product-img { width: 100%; height: 75px; object-fit: cover; border-radius: 8px; background: var(--pos-bg); } .pos-product-name { font-weight: 700; color: var(--pos-secondary); font-size: 0.78rem; line-height: 1.2; margin: 0; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-clamp: 2; } .pos-product-price { font-weight: 800; color: var(--pos-success); font-size: 0.85rem; margin-top: auto; } .pos-product-qty-control { display: flex; align-items: center; justify-content: space-between; gap: 0.25rem; margin-top: 0.2rem; border-top: 1px dashed var(--pos-border); padding-top: 0.3rem; } .btn-qty-pos { width: 28px; height: 28px; border-radius: 7px; border: none; background: #e0f2fe; color: var(--pos-primary-dark); font-weight: 800; font-size: 0.9rem; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s; } .btn-qty-pos:hover { background: #bae6fd; } .btn-qty-pos.minus { background: #fee2e2; color: #b91c1c; } .btn-qty-pos.minus:hover { background: #fecaca; } .qty-display-pos { font-weight: 800; font-size: 0.9rem; width: 22px; text-align: center; color: var(--pos-secondary);} .pos-pane-right { width: 380px; display: flex; flex-direction: column; background: var(--pos-surface); z-index: 10; box-shadow: -4px 0 15px rgba(0,0,0,0.04); flex-shrink: 0; border-left: 1px solid var(--pos-border);} .pos-right-scroll { flex: 1; overflow-y: auto; display: flex; flex-direction: column; } .pos-right-scroll::-webkit-scrollbar { width: 5px; } .pos-right-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } .pos-section-title { font-size: 0.7rem; font-weight: 800; color: var(--pos-text-muted); text-transform: uppercase; letter-spacing: 0.08em; padding: 0.6rem 0.85rem 0.3rem; display: flex; align-items: center; gap: 0.35rem; margin: 0; } .pos-section-title i { font-size: 0.65rem; } .pos-ticket-header { padding: 0.6rem 0.85rem; border-bottom: 1px solid var(--pos-border); display: flex; flex-direction: column; gap: 0.45rem; background: var(--pos-bg); } .pos-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.45rem; } .pos-input-wrap { display: flex; flex-direction: column; gap: 0.15rem; } .pos-input-wrap label { font-size: 0.68rem; font-weight: 700; color: var(--pos-text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin:0;} .pos-input { padding: 0.4rem 0.6rem; border: 1.5px solid var(--pos-border); border-radius: 7px; font-weight: 600; font-size: 0.82rem; background: var(--pos-surface); color: var(--pos-secondary); transition: border 0.2s; width: 100%; outline: none; font-family: inherit; } .pos-input:focus { border-color: var(--pos-primary); box-shadow: 0 0 0 2px rgba(14,165,233,0.12); } textarea.pos-input { resize: vertical; min-height: 40px; } .pos-cart { flex: 1; overflow-y: auto; padding: 0.5rem 0.85rem; display: flex; flex-direction: column; gap: 0.35rem; background: #fff; min-height: 80px; } .pos-cart::-webkit-scrollbar { width: 4px; } .pos-cart::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 2px; } .cart-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 1.5rem; color: var(--pos-text-muted); } .pos-cart-item { background: var(--pos-bg); border: 1px solid var(--pos-border); border-radius: 8px; padding: 0.5rem 0.6rem; display: flex; flex-direction: column; gap: 0.5rem; } .pos-cart-item-header { display: flex; justify-content: space-between; align-items: flex-start; } .pos-cart-item-name { font-weight: 700; color: var(--pos-secondary); font-size: 0.82rem; } .pos-cart-item-price { font-weight: 600; color: var(--pos-text-muted); font-size: 0.75rem; } .pos-cart-item-controls { display: flex; justify-content: space-between; align-items: center; } .pos-cart-qty-group { display: flex; align-items: center; gap: 0.3rem; background: var(--pos-surface); border: 1px solid var(--pos-border); border-radius: 6px; padding: 0.15rem; } .btn-cart-remove { border: none; background: #fee2e2; color: #b91c1c; border-radius: 6px; padding: 0.25rem 0.4rem; font-weight: 700; font-size: 0.7rem; cursor: pointer; transition: background 0.2s; } .btn-cart-remove:hover { background: #fecaca; } .pos-ticket-footer { padding: 0.75rem 0.85rem; display: flex; flex-direction: column; gap: 0.6rem; background: var(--pos-surface); border-top: 2px solid var(--pos-border); flex-shrink: 0; } .pos-total-row { display: flex; justify-content: space-between; align-items: flex-end; } .pos-total-label { font-size: 0.85rem; font-weight: 700; color: var(--pos-text-muted); } .pos-total-amount { font-size: 1.4rem; font-weight: 900; color: var(--pos-secondary); line-height: 1; } .pos-action-btns { display: flex; gap: 0.5rem; flex-direction: column;} .btn-pos-action { width: 100%; padding: 0.7rem; border-radius: 10px; font-weight: 800; font-size: 0.95rem; border: none; cursor: pointer; color: white; transition: transform 0.2s, box-shadow 0.2s; display: flex; align-items: center; justify-content: center; gap: 0.5rem; } .btn-pos-action:hover { transform: translateY(-1px); } .btn-pos-action:active { transform: scale(0.98); } .btn-pos-save { background: linear-gradient(135deg, var(--pos-primary), var(--pos-primary-dark)); box-shadow: 0 6px 16px rgba(14, 165, 233, 0.3); } .pos-preparers-mini { max-height: 80px; overflow-y: auto; border: 2px solid var(--pos-border); border-radius: 7px; background: var(--pos-surface); padding: 0.3rem; display: grid; grid-template-columns: 1fr 1fr; gap: 0.2rem; } .pos-prep-item { display: flex; align-items: center; gap: 0.3rem; font-size: 0.72rem; font-weight: 600; padding: 0.2rem 0.3rem; border-radius: 6px; border: 1px solid transparent; cursor: pointer; color: var(--pos-secondary);} .pos-prep-item:hover { background: var(--pos-bg); } .pos-prep-item input { accent-color: var(--pos-primary); width: 14px; height: 14px;} .pos-prep-item.is-selected { background: #f0f9ff; border-color: var(--pos-primary); color: var(--pos-primary-dark);} .pos-topbar-actions { display: flex; gap: 0.5rem; } .pos-modal-tabs { display: none; } .category-placeholder { text-align: center; color: var(--pos-text-muted); font-weight: 600; padding: 2rem; background: var(--pos-surface); border-radius: 10px; border: 2px dashed var(--pos-border); } .btn-back-to-menu { display: none; } @media (max-width: 992px) { .pos-topbar { padding: 0 1rem; } .pos-title .modal-title-text { display: none; } .btn-back .btn-text { display: none; } .pos-topbar-actions .btn-text { display: none; } .pos-topbar-actions .btn { padding: 0.6rem 0.8rem; border-radius: 8px; } .pos-workspace { padding: 1rem; } .stats-grid { grid-template-columns: 1fr 1fr; } } @media (max-width: 768px) { .pos-modal-header { height: 60px; padding: 0 0.75rem; } .pos-modal-title .modal-title-text { display: none; } .pos-modal-header-left { gap: 0.5rem; } .pos-modal-total-badge { padding: 0.25rem 0.5rem; } .pos-modal-total-value { font-size: 1rem; } .pos-modal-tabs { display: flex; background: rgba(255,255,255,0.05); border-radius: 10px; padding: 3px; margin: 0 0.5rem; } .pos-tab-btn { background: transparent; border: none; color: rgba(255,255,255,0.6); padding: 0.5rem 0.8rem; border-radius: 8px; font-weight: 700; font-size: 0.75rem; cursor: pointer; display: flex; align-items: center; gap: 0.4rem; position: relative; } .pos-tab-btn.active { background: var(--pos-primary); color: white; } .pos-tab-btn .tab-badge { background: var(--pos-danger); color: white; font-size: 0.6rem; padding: 1px 5px; border-radius: 10px; position: absolute; top: -2px; right: -2px; box-shadow: 0 2px 4px rgba(0,0,0,0.2); transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .pos-tab-btn .tab-badge.pulse { transform: scale(1.4); } .pos-tab-btn .tab-badge:empty { display: none; } .pos-tab-btn .tab-badge[data-count="0"] { display: none; } .btn-close-modal .btn-text { display: none; } .btn-close-modal { padding: 0.5rem; } .pos-modal-body { flex-direction: row; height: calc(100vh - 60px); } .pos-pane-right { width: 100%; position: absolute; inset: 0; transform: translateX(100%); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); background: white; border-left: none; } .pos-pane-left { width: 100%; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .pos-modal-body.show-order .pos-pane-right { transform: translateX(0); } .pos-modal-body.show-order .pos-pane-left { transform: translateX(-100%); } .pos-products-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); } .pos-product-img { height: 90px; } .stats-grid { grid-template-columns: 1fr; } .filters-card { flex-direction: column; align-items: stretch; } .filters-card .form-group { width: 100%; } .filters-card .btn { width: 100%; justify-content: center; } .btn-action span { display: none; } .orders-grid { grid-template-columns: 1fr; } .btn-back-to-menu { display: flex; align-items: center; gap: 0.6rem; background: rgba(14, 165, 233, 0.1); color: var(--pos-primary); border: 1px solid rgba(14, 165, 233, 0.2); padding: 0.8rem 1rem; width: 100%; font-weight: 800; margin-bottom: 1.25rem; border-radius: 12px; cursor: pointer; font-size: 0.9rem; transition: all 0.2s; } .btn-back-to-menu:active { background: var(--pos-primary); color: white; transform: scale(0.98); } } @media (max-width: 480px) { .pos-modal-total-badge .pos-modal-total-label { display: none; } .pos-modal-items-count { display: none; } .pos-tab-btn span:not(.tab-badge) { display: none; } .pos-tab-btn { padding: 0.6rem 1rem; } .pos-products-grid { grid-template-columns: 1fr 1fr; } .pos-product-img { height: 80px; } } </style> </head> <body class="pos-app"> <header class="pos-topbar"> <div class="pos-topbar-left"> <a href="dashboard.php" class="btn-back"><i class="fas fa-arrow-left"></i> <span class="btn-text">Volver al Panel</span></a> <h1 class="pos-title"><i class="fas fa-truck"></i> <span class="modal-title-text">POS Domicilios</span></h1> </div> <div class="pos-topbar-right"> <div class="pos-topbar-actions"> <a class="btn btn-outline" href="delivery_history.php"><i class="fas fa-history"></i> <span class="btn-text">Historial</span></a> <button class="btn btn-primary" id="openCreateModal"><i class="fas fa-plus"></i> <span class="btn-text">Nuevo Pedido</span></button> </div> </div> </header> <section class="pos-workspace"> <?php if ($canViewInsights): ?> <div class="stats-grid"> <div class="stat-card"> <h3><i class="fas fa-calendar-day"></i> Hoy</h3> <div class="stat-value"><?= htmlspecialchars(formatMoney((float)$stats_today['total_recaudado'], $conn)); ?></div> <div class="stat-meta"><span><i class="fas fa-receipt"></i> <?= (int)$stats_today['total_pedidos']; ?> pedidos</span><span><i class="fas fa-check-circle"></i> Entregados: <?= (int)$deliveredToday; ?></span></div> </div> <div class="stat-card"> <h3><i class="fas fa-calendar-range"></i> Rango seleccionado</h3> <div class="stat-value"><?= htmlspecialchars(formatMoney((float)$stats_range['total_recaudado'], $conn)); ?></div> <div class="stat-meta"><span><?= $fecha_desde_safe; ?> → <?= $fecha_hasta_safe; ?></span><span><i class="fas fa-receipt"></i> <?= (int)$stats_range['total_pedidos']; ?> pedidos</span></div> </div> <div class="stat-card"> <h3><i class="fas fa-check-circle"></i> Entregados</h3> <div class="stat-value"><?= number_format($deliveredRange, 0, ',', '.'); ?></div> <div class="stat-meta"><span><i class="fas fa-calendar-day"></i> Hoy: <?= number_format($deliveredToday, 0, ',', '.'); ?></span><span><i class="fas fa-history"></i> Rango: <?= number_format($deliveredRange, 0, ',', '.'); ?></span></div> </div> <div class="stat-card"> <h3><i class="fas fa-bolt"></i> En curso</h3> <div class="stat-value"><?= number_format($inProgressToday, 0, ',', '.'); ?></div> <div class="stat-meta"><span><i class="fas fa-clock"></i> Pend: <?= (int)$todayStatusCounts['pendiente']; ?></span><span><i class="fas fa-spinner"></i> Prep: <?= (int)$todayStatusCounts['en_preparacion']; ?> · <i class="fas fa-truck"></i> Camino: <?= (int)$todayStatusCounts['en_camino']; ?></span></div> </div> </div> <form class="filters-card" method="GET"> <div class="form-group"><label for="fecha_desde">Desde</label><input type="date" id="fecha_desde" name="fecha_desde" value="<?= $fecha_desde_safe; ?>"></div> <div class="form-group"><label for="fecha_hasta">Hasta</label><input type="date" id="fecha_hasta" name="fecha_hasta" value="<?= $fecha_hasta_safe; ?>"></div> <div class="form-group"><button class="btn btn-primary" type="submit"><i class="fas fa-search"></i> Aplicar</button></div> <div class="form-group"><a class="btn btn-outline" href="delivery_orders.php"><i class="fas fa-undo"></i> Restablecer</a></div> </form> <?php endif; ?> <h2 style="font-size:1.1rem;font-weight:800;color:var(--pos-secondary);margin:0;display:flex;align-items:center;gap:0.5rem;"><i class="fas fa-fire" style="color:var(--pos-primary);"></i> Pedidos en curso</h2> <?php if(empty($orders)): ?> <div class="empty-state"><i class="fas fa-box-open"></i><h3>No hay pedidos activos</h3><p>Registra un nuevo pedido para comenzar.</p></div> <?php else: ?> <div class="orders-grid" id="activeOrdersContainer"> <?php foreach($orders as $index => $order): $items = json_decode($order['order_items'], true); $displayNumber = (isset($order['order_number']) && (int)$order['order_number'] > 0) ? (int)$order['order_number'] : ($index + 1); $preparerNames = trim((string)($order['preparer_names'] ?? '')); if($preparerNames === '' && !empty($order['preparer_name'])) $preparerNames = (string)$order['preparer_name']; $preparerIdsAttr = trim((string)($order['preparer_ids'] ?? '')); if($preparerIdsAttr === '' && isset($order['preparer_id']) && $order['preparer_id'] !== null) $preparerIdsAttr = (string)(int)$order['preparer_id']; $statusLabels = ['pendiente'=>'Pendiente','en_preparacion'=>'En Preparación','en_camino'=>'En Camino','entregado'=>'Entregado','cancelado'=>'Cancelado']; $statusLabel = $statusLabels[$order['status']] ?? $order['status']; ?> <article class="order-card" data-order-id="<?= (int)$order['id']; ?>" data-customer="<?= htmlspecialchars($order['customer_name'] ?? '', ENT_QUOTES, 'UTF-8'); ?>" data-phone="<?= htmlspecialchars($order['phone'] ?? '', ENT_QUOTES, 'UTF-8'); ?>" data-address="<?= htmlspecialchars($order['address'] ?? '', ENT_QUOTES, 'UTF-8'); ?>" data-payment="<?= htmlspecialchars($order['payment_method'] ?? '', ENT_QUOTES, 'UTF-8'); ?>" data-notes="<?= htmlspecialchars($order['notes'] ?? '', ENT_QUOTES, 'UTF-8'); ?>" data-status="<?= htmlspecialchars($order['status'] ?? '', ENT_QUOTES, 'UTF-8'); ?>" data-items="<?= htmlspecialchars(base64_encode($order['order_items']), ENT_QUOTES, 'UTF-8'); ?>" data-total="<?= (float)$order['total_amount']; ?>" data-preparer-id="<?= isset($order['preparer_id']) ? (int)$order['preparer_id'] : '' ?>" data-preparer-ids="<?= htmlspecialchars($preparerIdsAttr, ENT_QUOTES, 'UTF-8'); ?>"> <div class="order-header"> <div> <h4 class="order-number"><i class="fas fa-hashtag"></i> Pedido #<?= $displayNumber; ?></h4> <span style="font-size:0.85rem;color:var(--pos-text-muted);"><i class="far fa-clock"></i> <?= date('d/m/Y h:i A', strtotime($order['created_at'])); ?></span> </div> <span class="order-status-badge badge-<?= htmlspecialchars($order['status']); ?>"><?= $statusLabel; ?></span> </div> <div class="order-meta"> <span><i class="fas fa-user"></i> <?= htmlspecialchars($order['customer_name']); ?></span> <span><i class="fas fa-phone"></i> <?= htmlspecialchars($order['phone']); ?></span> <span><i class="fas fa-map-marker-alt"></i> <?= htmlspecialchars($order['address']); ?></span> <span><i class="fas fa-money-bill-wave"></i> <?= ucfirst($order['payment_method']); ?></span> <?php if($preparerNames !== ''): ?><span><i class="fas fa-user-tie"></i> <?= htmlspecialchars($preparerNames); ?></span><?php endif; ?> </div> <div class="order-items-list"> <?php if(is_array($items)): foreach($items as $item): $pid = (int)($item['id'] ?? 0); $prepForItem = $pid > 0 ? trim((string)($productPrepMap[$pid] ?? '')) : ''; if($prepForItem === '') $prepForItem = $preparerNames; ?> <div class="order-item"> <div><span class="item-qty"><?= (int)$item['cantidad']; ?>x</span> <?= htmlspecialchars($item['nombre']); ?> <?php if($prepForItem !== ''): ?><small style="display:block;color:var(--pos-text-muted);font-weight:700;font-size:0.75rem;">Prep: <?= htmlspecialchars($prepForItem); ?></small><?php endif; ?> </div> <span><?= htmlspecialchars(formatMoney((float)($item['precio'] * $item['cantidad']), $conn)); ?></span> </div> <?php endforeach; endif; ?> </div> <?php if(!empty($order['notes'])): ?> <div class="order-notes"><i class="fas fa-sticky-note"></i> <?= nl2br(htmlspecialchars($order['notes'])); ?></div> <?php endif; ?> <div class="order-total"><span>Total</span><span><?= htmlspecialchars(formatMoney((float)$order['total_amount'], $conn)); ?></span></div> <div class="order-actions"> <button class="btn-action btn-edit" data-action="edit"><i class="fas fa-edit"></i> <span>Editar</span></button> <button class="btn-action btn-status-btn" data-action="status"><i class="fas fa-sync-alt"></i> <span>Estado</span></button> <button class="btn-action btn-print" data-action="print"><i class="fas fa-print"></i> <span>Imprimir</span></button> <button class="btn-action btn-delete" data-action="delete"><i class="fas fa-trash"></i> <span>Eliminar</span></button> </div> </article> <?php endforeach; ?> </div> <?php endif; ?> </section> <!-- Modal Crear --> <div class="pos-modal" id="createModal"> <div class="pos-modal-header"> <div class="pos-modal-header-left"> <h2 class="pos-modal-title"><i class="fas fa-plus-circle"></i> <span class="modal-title-text">Nuevo Pedido</span></h2> <div class="pos-modal-total-badge"> <div><span class="pos-modal-total-label">Total</span><br><span class="pos-modal-total-value" id="createHeaderTotal">$0</span></div> <span class="pos-modal-items-count" id="createHeaderCount">0 items</span> </div> </div> <div class="pos-modal-tabs"> <button type="button" class="pos-tab-btn active" data-tab="menu" onclick="switchPosTab('create', 'menu')"><i class="fas fa-utensils"></i> <span>Menú</span></button> <button type="button" class="pos-tab-btn" data-tab="order" onclick="switchPosTab('create', 'order')"><i class="fas fa-shopping-cart"></i> <span>Orden</span> <span class="tab-badge" id="createTabBadge">0</span></button> </div> <button class="btn-close-modal" data-close="create"><i class="fas fa-times"></i> <span class="btn-text">Salir</span></button> </div> <div class="pos-modal-body" id="createModalBody"> <div class="pos-pane-left"> <div class="pos-search-bar"><input type="text" class="pos-search-input" id="createSearchInput" placeholder="Buscar producto por nombre..." oninput="searchProducts('create', this.value)"></div> <div class="pos-categories" id="createCategoryContainer"> <button type="button" class="btn-category active" data-category="" onclick="toggleCategoryBtn('create', this)">Todas</button> <?php foreach($productosPorCategoria as $catKey => $catData): ?> <button type="button" class="btn-category" data-category="<?= htmlspecialchars($catKey); ?>" onclick="toggleCategoryBtn('create', this)"><?= htmlspecialchars($catData['nombre']); ?></button> <?php endforeach; ?> </div> <div class="pos-products-wrap"> <div class="pos-products-grid"> <?php foreach($productosPorCategoria as $catKey => $catData): ?> <?php foreach($catData['items'] as $product): $imagePath = !empty($product['image']) ? '../uploaded_img/' . htmlspecialchars($product['image']) : '../images/placeholder.png'; ?> <div class="pos-product category-item-pos" data-category="<?= htmlspecialchars($catKey); ?>" onclick="adjustQuantity('create', <?= (int)$product['id']; ?>, 1)"> <img src="<?= $imagePath; ?>" alt="<?= htmlspecialchars($product['name']); ?>" class="pos-product-img" onerror="this.src='../images/placeholder.png';"> <h3 class="pos-product-name"><?= htmlspecialchars($product['name']); ?></h3> <div class="pos-product-price"><?= htmlspecialchars(formatMoney((float)$product['price'], $conn)); ?></div> <div class="pos-product-qty-control" onclick="event.stopPropagation()"> <button type="button" class="btn-qty-pos minus" onclick="adjustQuantity('create', <?= (int)$product['id']; ?>, -1)"><i class="fas fa-minus"></i></button> <span class="qty-display-pos" id="create-qty-<?= (int)$product['id']; ?>">0</span> <button type="button" class="btn-qty-pos" onclick="adjustQuantity('create', <?= (int)$product['id']; ?>, 1)"><i class="fas fa-plus"></i></button> </div> </div> <?php endforeach; ?> <?php endforeach; ?> </div> <div id="createCategoryPlaceholder" class="category-placeholder" style="display:none;">Selecciona una categoría para continuar.</div> </div> </div> <form id="createOrderForm" class="pos-pane-right"> <div class="pos-right-scroll"> <button type="button" class="btn-back-to-menu" onclick="switchPosTab('create', 'menu')"><i class="fas fa-arrow-left"></i> Añadir más productos</button> <h4 class="pos-section-title"><i class="fas fa-user"></i> Datos del Cliente</h4> <div class="pos-ticket-header"> <div class="pos-input-wrap"><label>Nombre y Apellido *</label><input type="text" id="createCustomerName" name="customer_name" class="pos-input" required autocomplete="off"></div> <div class="pos-row"> <div class="pos-input-wrap"><label>Teléfono *</label><input type="tel" id="createPhone" name="phone" class="pos-input" required autocomplete="off"></div> <div class="pos-input-wrap"><label>Método de Pago *</label> <select id="createPayment" name="payment_method" class="pos-input" required> <option value="">Seleccione</option><option value="efectivo">Efectivo</option><option value="transferencia">Transferencia</option> </select> </div> </div> <div class="pos-input-wrap"><label>Dirección *</label><textarea id="createAddress" name="address" class="pos-input" rows="2" required></textarea></div> <div class="pos-input-wrap preparer-group"><label>Preparadores</label> <div class="pos-preparers-mini" id="createPreparerChecklist"> <?php foreach($preparers as $prep): ?> <label class="pos-prep-item check-item" data-preparer-id="<?= (int)$prep['id']; ?>"> <input type="checkbox" class="preparer-checkbox" value="<?= (int)$prep['id']; ?>"> <span><?= htmlspecialchars($prep['name']); ?></span> </label> <?php endforeach; ?> </div> </div> <div class="pos-input-wrap"><label>Notas</label><textarea id="createNotes" name="notes" class="pos-input" placeholder="Instrucciones especiales..."></textarea></div> </div> <h4 class="pos-section-title"><i class="fas fa-shopping-cart"></i> Productos Seleccionados</h4> <div class="pos-cart" id="createCartItems"><div class="cart-empty"><i class="fas fa-shopping-basket" style="font-size:1.5rem;color:#cbd5e1;margin-bottom:0.3rem;"></i><span style="font-weight:700;color:#94a3b8;">El pedido está vacío</span></div></div> </div> <div class="pos-ticket-footer"> <div class="pos-total-row"><span class="pos-total-label">Total a cobrar</span><span class="pos-total-amount" id="createCartTotal">$0</span></div> <div class="pos-action-btns"><button type="submit" form="createOrderForm" class="btn-pos-action btn-pos-save"><i class="fas fa-paper-plane"></i> Enviar Pedido</button></div> </div> </form> </div> </div> <!-- Modal Editar --> <div class="pos-modal" id="editModal"> <div class="pos-modal-header"> <div class="pos-modal-header-left"> <h2 class="pos-modal-title"><i class="fas fa-pen"></i> <span class="modal-title-text">Editar Pedido</span></h2> <div class="pos-modal-total-badge"> <div><span class="pos-modal-total-label">Total</span><br><span class="pos-modal-total-value" id="editHeaderTotal">$0</span></div> <span class="pos-modal-items-count" id="editHeaderCount">0 items</span> </div> </div> <div class="pos-modal-tabs"> <button type="button" class="pos-tab-btn active" data-tab="menu" onclick="switchPosTab('edit', 'menu')"><i class="fas fa-utensils"></i> <span>Menú</span></button> <button type="button" class="pos-tab-btn" data-tab="order" onclick="switchPosTab('edit', 'order')"><i class="fas fa-shopping-cart"></i> <span>Orden</span> <span class="tab-badge" id="editTabBadge">0</span></button> </div> <button class="btn-close-modal" data-close="edit"><i class="fas fa-times"></i> <span class="btn-text">Salir</span></button> </div> <div class="pos-modal-body" id="editModalBody"> <div class="pos-pane-left"> <div class="pos-search-bar"><input type="text" class="pos-search-input" id="editSearchInput" placeholder="Buscar producto por nombre..." oninput="searchProducts('edit', this.value)"></div> <div class="pos-categories" id="editCategoryContainer"> <button type="button" class="btn-category active" data-category="" onclick="toggleCategoryBtn('edit', this)">Todas</button> <?php foreach($productosPorCategoria as $catKey => $catData): ?> <button type="button" class="btn-category" data-category="<?= htmlspecialchars($catKey); ?>" onclick="toggleCategoryBtn('edit', this)"><?= htmlspecialchars($catData['nombre']); ?></button> <?php endforeach; ?> </div> <div class="pos-products-wrap"> <div class="pos-products-grid"> <?php foreach($productosPorCategoria as $catKey => $catData): ?> <?php foreach($catData['items'] as $product): $imagePath = !empty($product['image']) ? '../uploaded_img/' . htmlspecialchars($product['image']) : '../images/placeholder.png'; ?> <div class="pos-product category-item-edit-pos" data-category="<?= htmlspecialchars($catKey); ?>" onclick="adjustQuantity('edit', <?= (int)$product['id']; ?>, 1)"> <img src="<?= $imagePath; ?>" alt="<?= htmlspecialchars($product['name']); ?>" class="pos-product-img" onerror="this.src='../images/placeholder.png';"> <h3 class="pos-product-name"><?= htmlspecialchars($product['name']); ?></h3> <div class="pos-product-price"><?= htmlspecialchars(formatMoney((float)$product['price'], $conn)); ?></div> <div class="pos-product-qty-control" onclick="event.stopPropagation()"> <button type="button" class="btn-qty-pos minus" onclick="adjustQuantity('edit', <?= (int)$product['id']; ?>, -1)"><i class="fas fa-minus"></i></button> <span class="qty-display-pos" id="edit-qty-<?= (int)$product['id']; ?>">0</span> <button type="button" class="btn-qty-pos" onclick="adjustQuantity('edit', <?= (int)$product['id']; ?>, 1)"><i class="fas fa-plus"></i></button> </div> </div> <?php endforeach; ?> <?php endforeach; ?> </div> <div id="editCategoryPlaceholder" class="category-placeholder" style="display:none;">Selecciona una categoría para continuar.</div> </div> </div> <form id="editOrderForm" class="pos-pane-right"> <input type="hidden" id="editOrderId" name="order_id"> <div class="pos-right-scroll"> <button type="button" class="btn-back-to-menu" onclick="switchPosTab('edit', 'menu')"><i class="fas fa-arrow-left"></i> Añadir más productos</button> <h4 class="pos-section-title"><i class="fas fa-user"></i> Datos del Cliente</h4> <div class="pos-ticket-header"> <div class="pos-input-wrap"><label>Nombre y Apellido *</label><input type="text" id="editCustomerName" name="customer_name" class="pos-input" required autocomplete="off"></div> <div class="pos-row"> <div class="pos-input-wrap"><label>Teléfono *</label><input type="tel" id="editPhone" name="phone" class="pos-input" required autocomplete="off"></div> <div class="pos-input-wrap"><label>Método de Pago *</label> <select id="editPayment" name="payment_method" class="pos-input" required> <option value="">Seleccione</option><option value="efectivo">Efectivo</option><option value="transferencia">Transferencia</option> </select> </div> </div> <div class="pos-input-wrap"><label>Dirección *</label><textarea id="editAddress" name="address" class="pos-input" rows="2" required></textarea></div> <div class="pos-input-wrap"><label>Estado del Pedido</label> <select id="editStatus" name="status" class="pos-input"> <option value="pendiente">Pendiente</option><option value="en_preparacion">En Preparación</option><option value="en_camino">En Camino</option><option value="entregado">Entregado</option><option value="cancelado">Cancelado</option> </select> </div> <div class="pos-input-wrap preparer-group"><label>Preparadores</label> <div class="pos-preparers-mini" id="editPreparerChecklist"> <?php foreach($preparers as $prep): ?> <label class="pos-prep-item check-item" data-preparer-id="<?= (int)$prep['id']; ?>"> <input type="checkbox" class="preparer-checkbox" value="<?= (int)$prep['id']; ?>"> <span><?= htmlspecialchars($prep['name']); ?></span> </label> <?php endforeach; ?> </div> </div> <div class="pos-input-wrap"><label>Notas</label><textarea id="editNotes" name="notes" class="pos-input"></textarea></div> </div> <h4 class="pos-section-title"><i class="fas fa-shopping-cart"></i> Productos Seleccionados</h4> <div class="pos-cart" id="editCartItems"><div class="cart-empty"><i class="fas fa-shopping-basket" style="font-size:1.5rem;color:#cbd5e1;margin-bottom:0.3rem;"></i><span style="font-weight:700;color:#94a3b8;">Selecciona productos para editar</span></div></div> </div> <div class="pos-ticket-footer"> <div class="pos-total-row"><span class="pos-total-label">Total a cobrar</span><span class="pos-total-amount" id="editCartTotal">$0</span></div> <div class="pos-action-btns"><button type="submit" form="editOrderForm" class="btn-pos-action btn-pos-save"><i class="fas fa-save"></i> Guardar Cambios</button></div> </div> </form> </div> </div> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script> const productCatalog = <?= $productosJson; ?>; const createCart = {}; const editCart = {}; function formatCOP(amount){ if (window.formatMoneyJs) return window.formatMoneyJs(amount); return new Intl.NumberFormat('es-CO').format(amount || 0); } function renderCart(context){ const cart = context === 'create' ? createCart : editCart; const container = document.getElementById(context === 'create' ? 'createCartItems' : 'editCartItems'); const totalEl = document.getElementById(context === 'create' ? 'createCartTotal' : 'editCartTotal'); if(!container || !totalEl) return; const values = Object.values(cart); if(values.length === 0){ container.innerHTML = `<div class="cart-empty"><i class="fas fa-shopping-basket" style="font-size:2rem;color:#cbd5e1;margin-bottom:0.5rem;"></i><span style="font-weight:700;color:#94a3b8;">El pedido está vacío</span></div>`; totalEl.textContent = '$0'; updateHeaderTotal(context); return; } let html = '', total = 0; values.forEach(item => { const subtotal = item.precio * item.cantidad; total += subtotal; html += `<div class="pos-cart-item"> <div class="pos-cart-item-header"> <span class="pos-cart-item-name">${item.nombre}</span> <span class="pos-cart-item-price">${formatCOP(item.precio)} c/u</span> </div> <div class="pos-cart-item-controls"> <div class="pos-cart-qty-group"> <button type="button" class="btn-qty-pos minus" onclick="adjustQuantity('${context}', ${item.id}, -1)" style="width:28px;height:28px;font-size:1rem;"><i class="fas fa-minus"></i></button> <span style="font-weight:800;font-size:1rem;color:var(--pos-secondary);width:24px;text-align:center;">${item.cantidad}</span> <button type="button" class="btn-qty-pos" onclick="adjustQuantity('${context}', ${item.id}, 1)" style="width:28px;height:28px;font-size:1rem;"><i class="fas fa-plus"></i></button> </div> <div style="display:flex;align-items:center;gap:0.75rem;"> <span style="font-weight:900;color:var(--pos-success);">${formatCOP(subtotal)}</span> <button type="button" class="btn-cart-remove" onclick="removeFromCart('${context}', ${item.id})"><i class="fas fa-trash"></i></button> </div> </div> ${item.preparer_names ? `<div style="font-size:0.75rem;color:var(--pos-text-muted);font-weight:700;">Prep: ${item.preparer_names}</div>` : ''} </div>`; }); container.innerHTML = html; totalEl.textContent = formatCOP(total); updateHeaderTotal(context); autoPopulatePreparersFromCart(context); } function switchPosTab(context, tab){ const body = document.getElementById(context === 'create' ? 'createModalBody' : 'editModalBody'); const header = document.querySelector(`#${context}Modal .pos-modal-header`); if(!body || !header) return; header.querySelectorAll('.pos-tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === tab)); if(tab === 'order') body.classList.add('show-order'); else body.classList.remove('show-order'); } function updateHeaderTotal(context){ const cart = context === 'create' ? createCart : editCart; const hTotal = document.getElementById(context === 'create' ? 'createHeaderTotal' : 'editHeaderTotal'); const hCount = document.getElementById(context === 'create' ? 'createHeaderCount' : 'editHeaderCount'); const values = Object.values(cart); let total = 0, count = 0; values.forEach(it => { const q = parseInt(it.cantidad||0,10); total += (parseFloat(it.precio||0)*q); count += q; }); if(hTotal) hTotal.textContent = '$' + formatCOP(total); if(hCount) hCount.textContent = `${count} ${count === 1 ? 'item' : 'items'}`; const tabBadge = document.getElementById(context === 'create' ? 'createTabBadge' : 'editTabBadge'); if(tabBadge) { const prev = parseInt(tabBadge.textContent||0,10); tabBadge.textContent = count; tabBadge.dataset.count = count; if(count !== prev){ tabBadge.classList.remove('pulse'); void tabBadge.offsetWidth; tabBadge.classList.add('pulse'); setTimeout(() => tabBadge.classList.remove('pulse'), 300); } } } function adjustQuantity(context, productId, delta){ const product = productCatalog[productId]; if(!product) return; const cart = context === 'create' ? createCart : editCart; const qtySpan = document.getElementById(`${context}-qty-${productId}`); let current = cart[productId]?.cantidad || 0; current = Math.max(0, current + delta); if(current === 0) delete cart[productId]; else cart[productId] = { id: product.id, nombre: product.name, precio: parseFloat(product.price), coste: parseFloat(product.cost_price||0), cantidad: current, preparer_names: product.preparer_names||'', preparer_ids: product.preparer_ids||'' }; if(qtySpan) qtySpan.textContent = current; renderCart(context); } function removeFromCart(context, productId){ const cart = context === 'create' ? createCart : editCart; if(cart[productId]){ delete cart[productId]; const s = document.getElementById(`${context}-qty-${productId}`); if(s) s.textContent='0'; renderCart(context); } } function resetCart(context){ const cart = context === 'create' ? createCart : editCart; Object.keys(cart).forEach(k => delete cart[k]); Object.keys(productCatalog).forEach(pid => { const s = document.getElementById(`${context}-qty-${pid}`); if(s) s.textContent='0'; }); renderCart(context); } function toggleCategoryBtn(context, btn){ const container = document.getElementById(`${context}CategoryContainer`); if(container) container.querySelectorAll('.btn-category').forEach(b => b.classList.remove('active')); if(btn) btn.classList.add('active'); const value = btn ? (btn.dataset.category||'') : ''; const placeholder = document.getElementById(`${context}CategoryPlaceholder`); const productClass = context === 'create' ? '.category-item-pos' : '.category-item-edit-pos'; let activeCount = 0; document.querySelectorAll(`#${context}Modal ${productClass}`).forEach(item => { if(value === '' || item.dataset.category === value){ item.style.display='flex'; activeCount++; } else { item.style.display='none'; } }); if(placeholder) placeholder.style.display = activeCount > 0 ? 'none' : 'block'; } function searchProducts(context, query){ const q = (query||'').toLowerCase().trim(); const productClass = context === 'create' ? '.category-item-pos' : '.category-item-edit-pos'; const modal = document.getElementById(context === 'create' ? 'createModal' : 'editModal'); if(!modal) return; if(q.length > 0){ const cc = document.getElementById(context+'CategoryContainer'); if(cc){ cc.querySelectorAll('.btn-category').forEach(b => b.classList.remove('active')); const ab = cc.querySelector('.btn-category[data-category=""]'); if(ab) ab.classList.add('active'); } } modal.querySelectorAll(productClass).forEach(item => { const name = (item.querySelector('.pos-product-name')?.textContent||'').toLowerCase(); item.style.display = (q===''||name.includes(q)) ? 'flex' : 'none'; }); } // Preparer logic function getPreparerChecklist(ctx){ return document.getElementById(`${ctx}PreparerChecklist`); } function updatePreparerIndicators(ctx){ const cl = getPreparerChecklist(ctx); if(!cl) return; cl.querySelectorAll('.check-item').forEach(row => { const cb = row.querySelector('.preparer-checkbox'); row.classList.toggle('is-selected', !!cb?.checked); }); } function resetPreparerControls(ctx){ const cl = getPreparerChecklist(ctx); if(!cl) return; cl.querySelectorAll('.preparer-checkbox').forEach(cb => { cb.checked=false; }); updatePreparerIndicators(ctx); } function populatePreparerSelection(ctx, rawCsv){ resetPreparerControls(ctx); const cl = getPreparerChecklist(ctx); if(!cl) return; (rawCsv||'').split(',').map(v=>v.trim()).filter(Boolean).forEach(id => { const cb = cl.querySelector(`.preparer-checkbox[value="${id}"]`); if(cb) cb.checked=true; }); updatePreparerIndicators(ctx); } function getCombinedPreparerIds(ctx){ const cl = getPreparerChecklist(ctx); if(!cl) return ''; const ids=[]; cl.querySelectorAll('.preparer-checkbox').forEach(cb => { if(cb.checked && cb.value && !ids.includes(cb.value)) ids.push(cb.value); }); return ids.join(','); } function initPreparerChecklist(ctx){ const cl = getPreparerChecklist(ctx); if(!cl) return; cl.querySelectorAll('.preparer-checkbox').forEach(cb => cb.addEventListener('change', () => updatePreparerIndicators(ctx))); } function derivePreparerIdsFromCart(cartObj){ const ids=[]; Object.values(cartObj).forEach(item => { (item.preparer_ids||'').toString().split(',').map(v=>v.trim()).filter(Boolean).forEach(v => { if(!ids.includes(v)) ids.push(v); }); }); return ids.join(','); } function autoPopulatePreparersFromCart(ctx){ const current = getCombinedPreparerIds(ctx); if(current) return; const cart = ctx === 'create' ? createCart : editCart; const derived = derivePreparerIdsFromCart(cart); if(!derived) return; populatePreparerSelection(ctx, derived); } // Modal open/close function closeModal(which){ const m = document.getElementById(which==='create'?'createModal':'editModal'); if(m){ m.classList.remove('show'); document.body.style.overflow=''; } } function openModal(which){ const m = document.getElementById(which==='create'?'createModal':'editModal'); if(m){ m.classList.add('show'); document.body.style.overflow='hidden'; } } document.querySelectorAll('[data-close]')?.forEach(btn => btn.addEventListener('click', () => closeModal(btn.dataset.close))); document.getElementById('openCreateModal')?.addEventListener('click', () => { resetCart('create'); document.getElementById('createOrderForm').reset(); resetPreparerControls('create'); toggleCategoryBtn('create', document.querySelector('#createCategoryContainer .btn-category[data-category=""]')); switchPosTab('create','menu'); openModal('create'); }); document.addEventListener('DOMContentLoaded', () => { initPreparerChecklist('create'); initPreparerChecklist('edit'); }); // Form submissions document.getElementById('createOrderForm')?.addEventListener('submit', function(e){ e.preventDefault(); if(Object.keys(createCart).length===0){ Swal.fire('Aviso','Debes agregar al menos un producto','warning'); return; } const form = e.target; const payload = new URLSearchParams(); payload.set('customer_name', form.customer_name.value.trim()); payload.set('phone', form.phone.value.trim()); payload.set('address', form.address.value.trim()); payload.set('payment_method', form.payment_method.value); payload.set('notes', form.notes.value.trim()); const prepCsv = getCombinedPreparerIds('create'); payload.set('preparer_ids', prepCsv); payload.set('preparer_id', prepCsv.split(',').map(v=>v.trim()).filter(Boolean)[0]||''); payload.set('order_items', JSON.stringify(Object.values(createCart))); let total=0; Object.values(createCart).forEach(it => total += it.precio*it.cantidad); payload.set('total_amount', String(total)); fetch('create_delivery.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body: payload.toString() }) .then(r => r.json()).then(data => { if(data.success){ closeModal('create'); Swal.fire('¡Guardado!','El pedido ha sido registrado','success').then(()=>location.reload()); } else Swal.fire('Error', data.message||'No se pudo guardar','error'); }).catch(() => Swal.fire('Error','No se pudo guardar','error')); }); document.getElementById('editOrderForm')?.addEventListener('submit', function(e){ e.preventDefault(); if(Object.keys(editCart).length===0){ Swal.fire('Aviso','Debes agregar al menos un producto','warning'); return; } const form = e.target; const orderId = form.order_id?.value; if(!orderId){ Swal.fire('Error','No se pudo identificar el pedido','error'); return; } const payload = new URLSearchParams(); payload.set('order_id', orderId); payload.set('customer_name', form.customer_name.value.trim()); payload.set('phone', form.phone.value.trim()); payload.set('address', form.address.value.trim()); payload.set('payment_method', form.payment_method.value); payload.set('notes', form.notes.value.trim()); payload.set('status', form.status.value); payload.set('order_items', JSON.stringify(Object.values(editCart))); let editTotal=0; Object.values(editCart).forEach(it => editTotal += it.precio*it.cantidad); payload.set('total_amount', String(editTotal)); const editPrepCsv = getCombinedPreparerIds('edit'); payload.set('preparer_ids', editPrepCsv); payload.set('preparer_id', editPrepCsv.split(',').map(v=>v.trim()).filter(Boolean)[0]||''); (async () => { if(form.status.value === 'entregado'){ const ok = await confirmInventoryPreviewDelivery({ orderId, orderItemsJson: JSON.stringify(Object.values(editCart)), title:'Confirmar entrega del pedido', confirmText:'Sí, entregar', confirmColor:'#22c55e' }); if(!ok) return; } fetch('update_delivery.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body: payload.toString(), credentials:'same-origin' }) .then(r => r.json()).then(data => { if(data.success){ closeModal('edit'); Swal.fire('¡Actualizado!','El pedido ha sido modificado','success').then(()=>location.reload()); } else Swal.fire('Error', data.message||'No se pudo actualizar','error'); }).catch(() => Swal.fire('Error','No se pudo actualizar','error')); })(); }); // Fill edit modal from order card function fillEditModal(card){ resetCart('edit'); resetPreparerControls('edit'); if(!card) return; document.getElementById('editOrderId').value = card.dataset.orderId||''; document.getElementById('editCustomerName').value = card.dataset.customer||''; document.getElementById('editPhone').value = card.dataset.phone||''; document.getElementById('editAddress').value = card.dataset.address||''; document.getElementById('editPayment').value = card.dataset.payment||''; document.getElementById('editNotes').value = card.dataset.notes||''; document.getElementById('editStatus').value = card.dataset.status||'pendiente'; populatePreparerSelection('edit', card.dataset.preparerIds || card.dataset.preparerId || ''); let items = []; try { items = JSON.parse(atob(card.dataset.items||'')); } catch(e){ items=[]; } if(!Array.isArray(items)) items = []; items.filter(Boolean).forEach(item => { const product = productCatalog[item.id]; if(!product) return; editCart[item.id] = { id: item.id, nombre: product.name, precio: parseFloat(product.price), coste: parseFloat(product.cost_price||item.coste||0), cantidad: parseInt(item.cantidad||0,10), preparer_names: product.preparer_names||'', preparer_ids: product.preparer_ids||'' }; const span = document.getElementById(`edit-qty-${item.id}`); if(span) span.textContent = item.cantidad; }); renderCart('edit'); toggleCategoryBtn('edit', document.querySelector('#editCategoryContainer .btn-category[data-category=""]')); switchPosTab('edit','menu'); openModal('edit'); } // Order card actions function handleCardAction(event){ const button = event.target.closest('button'); if(!button) return; const action = button.dataset.action; if(!action) return; const card = button.closest('.order-card'); if(!card) return; if(action === 'edit') fillEditModal(card); else if(action === 'status') cambiarEstado(parseInt(card.dataset.orderId), card.dataset.status); else if(action === 'print') imprimirPedido(parseInt(card.dataset.orderId)); else if(action === 'delete'){ Swal.fire({ title:'¿Estás seguro?', text:'Esta acción no se puede deshacer', icon:'warning', showCancelButton:true, confirmButtonText:'Sí, eliminar', cancelButtonText:'Cancelar', confirmButtonColor:'#ef4444', cancelButtonColor:'#6b7280' }).then(result => { if(result.isConfirmed) window.location.href = 'delete_delivery.php?id=' + card.dataset.orderId; }); } } document.getElementById('activeOrdersContainer')?.addEventListener('click', handleCardAction); // Status change function cambiarEstado(id, estadoActual){ const estados = { 'pendiente':'Pendiente', 'en_preparacion':'En Preparación', 'en_camino':'En Camino', 'entregado':'Entregado', 'cancelado':'Cancelado' }; Swal.fire({ title:'Cambiar Estado del Pedido', input:'select', inputOptions:estados, inputValue:estadoActual, showCancelButton:true, confirmButtonText:'Cambiar', cancelButtonText:'Cancelar', confirmButtonColor:'#0ea5e9', cancelButtonColor:'#6b7280', inputValidator:(v)=>{ if(!v) return 'Debes seleccionar un estado'; } }).then(result => { if(!result.isConfirmed) return; (async () => { if(result.value === 'entregado'){ const ok = await confirmInventoryPreviewDelivery({ orderId:id, orderItemsJson:'', title:'Confirmar entrega del pedido', confirmText:'Sí, entregar', confirmColor:'#22c55e' }); if(!ok) return; } fetch('update_delivery_status.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body: `id=${id}&status=${result.value}`, credentials:'same-origin' }) .then(r => r.json()).then(data => { if(!data?.success){ Swal.fire('Error', data?.message||'No se pudo actualizar','error'); return; } if(result.value === 'entregado'){ const c = document.querySelector(`.order-card[data-order-id="${id}"]`); if(c){ c.style.transition='opacity 180ms ease'; c.style.opacity='0'; setTimeout(()=>{ c.remove(); checkEmpty(); },190); } Swal.fire('¡Entregado!','Pedido movido al historial.','success'); return; } const c2 = document.querySelector(`.order-card[data-order-id="${id}"]`); if(c2){ const se = c2.querySelector('.order-status-badge'); if(se){ se.className = `order-status-badge badge-${result.value}`; se.textContent = estados[result.value]; } c2.dataset.status = result.value; } Swal.fire('¡Actualizado!','Estado actualizado.','success'); }).catch(() => Swal.fire('Error','No se pudo actualizar','error')); })(); }); } function checkEmpty(){ const c = document.querySelector('.orders-grid'); if(!c) return; if(c.querySelectorAll('.order-card').length===0){ c.innerHTML='<div class="empty-state"><i class="fas fa-box-open"></i><h3>No hay pedidos activos</h3><p>Registra un nuevo pedido para comenzar.</p></div>'; } } function imprimirPedido(id){ if(!id){ Swal.fire('Error','No se pudo identificar el pedido','error'); return; } window.open(`delivery_order_ticket.php?id=${encodeURIComponent(id)}&print=1`, '_blank'); } // Inventory preview function escapeHtml(v){ return String(v??'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); } function formatQty(v){ const n=Number(v); if(!isFinite(n)) return '0'; return n.toLocaleString('es-CO',{minimumFractionDigits:0,maximumFractionDigits:3}); } function buildInventoryPreviewHtml(data){ const items = Array.isArray(data?.items) ? data.items : []; const summary = data?.summary || {}; const alreadyDeducted = !!data?.already_deducted; if(items.length===0) return `<div style="text-align:left;">${alreadyDeducted?'<div style="margin-bottom:.5rem;font-weight:800;color:#0f766e;">Inventario ya descontado.</div>':''}<div style="color:#374151;">${escapeHtml(data?.message||'No hay insumos para descontar.')}</div></div>`; let header=''; if(alreadyDeducted) header+='<div style="margin-bottom:.6rem;font-weight:800;color:#0f766e;">Inventario ya descontado. No se volverá a descontar.</div>'; if((summary.negatives||0)>0) header+=`<div style="margin-bottom:.35rem;font-weight:800;color:#b91c1c;">Atención: ${summary.negatives} insumo(s) quedarían en negativo.</div>`; if((summary.lows||0)>0) header+=`<div style="margin-bottom:.35rem;font-weight:800;color:#b45309;">Alerta: ${summary.lows} insumo(s) quedarían por debajo del mínimo.</div>`; const rows = items.map(it => { const neg=!!it.is_negative; const low=!!it.is_low; const bg=neg?'rgba(185,28,28,0.08)':(low?'rgba(180,83,9,0.08)':'transparent'); return `<tr style="background:${bg};"><td style="padding:.55rem .6rem;border-bottom:1px solid #eee;font-weight:800;">${escapeHtml(it.name)}</td><td style="padding:.55rem .6rem;border-bottom:1px solid #eee;text-align:right;font-weight:800;">${formatQty(it.needed)}</td><td style="padding:.55rem .6rem;border-bottom:1px solid #eee;text-align:right;">${formatQty(it.stock)}</td><td style="padding:.55rem .6rem;border-bottom:1px solid #eee;text-align:right;font-weight:900;${neg?'color:#b91c1c;':(low?'color:#b45309;':'color:#0f766e;')}">${formatQty(it.after)}</td><td style="padding:.55rem .6rem;border-bottom:1px solid #eee;text-align:center;color:#6b7280;font-weight:800;">${escapeHtml(it.unit||'u')}</td></tr>`; }).join(''); return `<div style="text-align:left;">${header}<div style="overflow:auto;border:1px solid #eee;border-radius:12px;"><table style="width:100%;border-collapse:collapse;min-width:720px;"><thead><tr style="background:#f9fafb;"><th style="text-align:left;padding:.6rem .6rem;border-bottom:1px solid #eee;">Insumo</th><th style="text-align:right;padding:.6rem;border-bottom:1px solid #eee;">Se descuenta</th><th style="text-align:right;padding:.6rem;border-bottom:1px solid #eee;">Stock actual</th><th style="text-align:right;padding:.6rem;border-bottom:1px solid #eee;">Stock después</th><th style="text-align:center;padding:.6rem;border-bottom:1px solid #eee;">Unidad</th></tr></thead><tbody>${rows}</tbody></table></div></div>`; } async function confirmInventoryPreviewDelivery({ orderId, orderItemsJson, title, confirmText, confirmColor }){ const payload = new URLSearchParams(); payload.set('context','delivery'); if(orderId) payload.set('order_id', String(orderId)); if(orderItemsJson) payload.set('order_items', orderItemsJson); const res = await fetch('preview_inventory_deduction.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body: payload.toString(), credentials:'same-origin' }); const data = await res.json().catch(()=>null); if(!data||!data.success){ await Swal.fire('Error', data?.message||'No se pudo generar el preview.','error'); return false; } const html = buildInventoryPreviewHtml(data); const result = await Swal.fire({ title: title||'Confirmar descuento', html, icon: (data.summary?.negatives>0)?'warning':'info', showCancelButton:true, confirmButtonText: confirmText||'Confirmar', cancelButtonText:'Cancelar', confirmButtonColor: confirmColor||'#22c55e', cancelButtonColor:'#6b7280', width:'980px' }); return !!result.isConfirmed; } renderCart('create'); renderCart('edit'); </script> </body> </html>
Coded With 💗 by
0x6ick