Skip to content

Instantly share code, notes, and snippets.

@iDavidMorales
Created November 28, 2025 19:53
Show Gist options
  • Select an option

  • Save iDavidMorales/790d325b0d1a48cdbb7496085619d41f to your computer and use it in GitHub Desktop.

Select an option

Save iDavidMorales/790d325b0d1a48cdbb7496085619d41f to your computer and use it in GitHub Desktop.
<?php
// Este bloque PHP se ejecutaría en tu servidor.
// Asegúrate de que tu lógica de inicio de sesión y base de datos establezca $_SESSION['plan'] y $_SESSION['loggedIn'].
session_start();
// --- SIMULACIÓN DE DATOS DE SESIÓN PHP ---
// Para probar:
// - Cambia 'free' a 'premium' para ver la interfaz premium.
// - Cambia '1' a '0' para simular un usuario no loggeado (aunque el sistema asume loggeado para funcionalidades).\
// NOTA: '1' para premium, '0' para gratuito
$_SESSION['plan'] = '1'; // <-- ¡CAMBIA ESTO A 'premium' PARA PROBAR EL PLAN PREMIUM!
$_SESSION['userID'] = $_SESSION['user_id'];
$_SESSION['loggedIn'] = '1';
// --- FIN DE SIMULACIÓN ---
// Determinar si el usuario es premium
// Aseguramos que el valor sea booleano para evitar cualquier problema de tipo en JavaScript.
$is_premium_user_php = (isset($_SESSION['plan']) && $_SESSION['plan'] === '1' && $_SESSION['loggedIn'] === '1');
// Convertimos el booleano PHP directamente a su representación de cadena JavaScript 'true' o 'false'.
$is_premium_user_js_value = $is_premium_user_php ? 'true' : 'false';
?>
<?php
if($_SESSION['user_id']!=''){
// header("Location: https://routicket.com/app/?href=index?redirect_session=1&id_profile=".$_SESSION['user_id']);
}else{
header("Location: https://routicket.com/login/v2/?punto_de_venta_login_requerido=1");
}
?>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-N6JNVM4NVD"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-N6JNVM4NVD');
</script>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Routicket | Punto de Venta</title>
<script src="../../chart.js"></script>
<script src="excl.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="funciones.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
--><!--<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
-->
<link rel="stylesheet" href="stylepdv.css">
</head>
<body>
<div id="messageContainer"></div>
<div id="customConfirmModal" class="custom-modal-overlay">
<div class="custom-modal-content">
<h5 id="customConfirmMessage">¿Estás seguro de que quieres realizar esta acción?</h5>
<div class="modal-buttons">
<button class="btn btn-danger" id="customConfirmYes">Sí</button>
<button class="btn btn-secondary" id="customConfirmNo">No</button>
</div>
</div>
</div>
<div id="loadingModal" class="custom-modal-overlay">
<div class="custom-modal-content">
<div class="spinner-border" role="status">
<span class="sr-only">Cargando...</span>
</div>
<div class="loading-message">Cargando...</div>
</div>
</div>
<!-- New Modal for Weight-based Product Entry -->
<div id="weightEntryModal" class="custom-modal-overlay">
<div class="custom-modal-content">
<h5>Agregar Producto por Peso (kg/g)</h5>
<div class="form-group">
<label for="weightProductCode">Código o Nombre del Producto:</label>
<input type="text" class="form-control" id="weightProductCode" placeholder="Código o nombre del producto" autofocus>
<small class="form-text text-muted text-left">Ej: 7501234567890 o Manzanas</small>
<div id="weightProductSearchResults" class="list-group product-search-results"></div>
</div>
<div class="form-group">
<label for="weightInput">Peso (kg):</label>
<input type="number" class="form-control" id="weightInput" step="0.001" min="0.001" value="0.000" placeholder="0.000">
<small class="form-text text-muted text-left">Ej: 1.5 para 1.5 kg, 0.25 para 250 gramos</small>
</div>
<div class="form-group">
<label>Producto Seleccionado:</label>
<p id="selectedWeightedProductInfo" class="text-left font-weight-bold"></p>
<p id="calculatedWeightedPrice" class="text-left font-weight-bold text-success"></p>
</div>
<div class="modal-buttons">
<button class="btn btn-success" id="addWeightedProductBtn"><i class="fas fa-cart-plus"></i> Añadir al Carrito</button>
<button class="btn btn-secondary" id="closeWeightEntryModalBtn">Cancelar</button>
</div>
</div>
</div>
<div class="app-wrapper">
<header class="app-header">
<a class="navbar-brand" href="#">
Routicket | Punto de Venta
</a>
<ul class="navbar-nav ml-auto d-flex flex-row align-items-center"> <!-- Added d-flex flex-row align-items-center -->
<!-- Botón de Nuevos Pedidos en la cabecera (siempre visible) -->
<li class="nav-item position-relative">
<a class="nav-link" href="#" id="newOrdersNotification" data-page="ordersPage">
<i class="fas fa-bell"></i> Nuevos Pedidos
<span class="order-notification-badge d-none" id="orderCountBadge">0</span>
</a>
</li>
</ul>
</header>
<aside class="app-sidebar">
<div class="sidebar-section">
<div class="sidebar-header">Menú Principal</div>
<ul class="sidebar-nav">
<li><a href="#" class="sidebar-link active" data-page="puntoVentaPage" data-section="puntoVenta"><i class="fas fa-cash-register"></i> Punto de Venta</a></li>
<li><a href="#" class="sidebar-link" data-page="ordersPage" data-section="orders"><i class="fas fa-box-open"></i> Pedidos</a></li>
<li><a href="index.php"><i class="fas fa-file-excel"></i> Cargar Excel</a></li>
<!-- Botón de Atajos movido a la barra lateral -->
<li><a href="#" class="sidebar-link" id="shortcutsLinkSidebar" data-page="shortcutsPage" data-section="shortcuts"><i class="fas fa-keyboard"></i> Atajos</a></li>
</ul>
</div>
<div class="sidebar-section" id="reportes-section">
<div class="sidebar-header">Reportes</div>
<ul class="sidebar-nav">
<li><a href="#" class="sidebar-link" data-page="stockReportsPage" data-section="stockReports"><i class="fas fa-chart-bar"></i> Reportes de Stock</a></li>
<li><a href="#" class="sidebar-link" data-page="salesHistoryPage" data-section="salesHistory"><i class="fas fa-history"></i> Historial de Ventas</a></li>
<li><a href="#" class="sidebar-link" data-page="analyticsPage" data-section="analytics"><i class="fas fa-chart-line"></i> Análisis de Ventas</a></li>
</ul>
</div>
<div class="sidebar-section" id="gestion-section">
<div class="sidebar-header">Gestión</div>
<ul class="sidebar-nav">
<li><a href="#" class="sidebar-link" data-page="productManagementPage" data-section="productManagement"><i class="fas fa-cubes"></i> Gestión de Productos</a></li>
<li><a href="#" class="sidebar-link" data-page="grammageProductsPage" data-section="grammageProducts"><i class="fas fa-weight-hanging"></i> Productos por Peso</a></li>
<li><a href="#" class="sidebar-link" data-page="expensesPage" data-section="expenses"><i class="fas fa-money-check-alt"></i> Gastos</a></li>
<li><a href="#" class="sidebar-link" data-page="notesPage" data-section="notes"><i class="fas fa-clipboard"></i> Notas</a></li>
</ul>
</div>
<div class="sidebar-section" id="configuraciones-section">
<div class="sidebar-header">Configuraciones</div>
<ul class="sidebar-nav">
<li><a href="#" class="sidebar-link" data-page="subscriptionPage" data-section="subscription"><i class="fas fa-star"></i> Suscripción</a></li>
<li><a href="#" class="sidebar-link" data-page="settingsPage" data-section="settings"><i class="fas fa-cogs"></i> Opciones Generales</a></li>
<li><a href="#" class="sidebar-link" data-page="userManagementPage" data-section="userManagement"><i class="fas fa-user-shield"></i> Gestión de Usuarios</a></li>
</ul>
</div>
<div class="sidebar-section sidebar-premium-features">
<div class="sidebar-header">Características Premium <i class="fas fa-star text-warning ml-1"></i></div>
<ul class="sidebar-nav">
<li class="premium-feature-item" data-premium-feature="grammage_products"><i class="fas fa-check-circle"></i> Productos por Gramaje</li>
<li class="premium-feature-item" data-premium-feature="currency_management"><i class="fas fa-check-circle"></i> Gestión de Moneda</li>
<li class="premium-feature-item" data-premium-feature="advanced_analytics"><i class="fas fa-check-circle"></i> Análisis Avanzado (Ventas y Pedidos)</li>
<li class="premium-feature-item" data-premium-feature="orders"><i class="fas fa-check-circle"></i> Gestión de Pedidos</li>
<li class="premium-feature-item" data-premium-feature="advanced_pricing"><i class="fas fa-check-circle"></i> Gestión de Precios Avanzada</li>
<!--
<li class="premium-feature-item" data-premium-feature="multi_warehouse"><i class="fas fa-check-circle"></i> Múltiples Almacenes</li>
<li class="premium-feature-item" data-premium-feature="cloud_sync"><i class="fas fa-check-circle"></i> Sincronización en la Nube</li>
<li class="premium-feature-item" data-premium-feature="e_invoicing"><i class="fas fa-check-circle"></i> Facturación Electrónica</li>
-->
</ul>
<button class="btn btn-info btn-sm mt-3 mx-auto d-block" id="upgradePremiumBtn">Actualizar a Premium</button>
</div>
</aside>
<main class="app-main-content">
<div id="puntoVentaPage" class="main-content-page active">
<div class="form-group mb-3">
<input type="text" class="form-control" id="product-search" placeholder="Escanear código o buscar producto por nombre/código/departamento..." autofocus>
</div>
<div class="main-columns">
<div class="cart-panel panel-windows">
<h2>Carrito de Compras</h2>
<ul id="cart-list">
<li class="text-center text-muted">Tu carrito está vacío.</li>
</ul>
</div>
<div class="right-column-wrapper" style="height:650px;">
<div class="payment-info-panel panel-windows" >
<h2>Información de Pago</h2>
<div class="payment-section">
<div class="form-group mt-3">
<label>Método de Pago:</label>
<div class="payment-method-options">
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="paymentMethod" id="paymentCash" value="Efectivo" checked>
<label class="form-check-label" for="paymentCash"><i class="fas fa-money-bill-wave"></i> Efectivo</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="paymentMethod" id="paymentCard" value="Tarjeta">
<label class="form-check-label" for="paymentCard"><i class="fas fa-credit-card"></i> Tarjeta</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="paymentMethod" id="paymentTransfer" value="Transferencia">
<label class="form-check-label" for="paymentTransfer"><i class="fas fa-exchange-alt"></i> Transferencia</label>
</div>
</div>
</div>
<div class="form-group">
<label for="amountReceived">Monto Recibido:</label>
<input type="number" class="form-control" id="amountReceived" placeholder="0.00" step="0.01">
</div>
<div class="summary-item text-right">
<strong>Cambio a devolver:</strong> <span id="cambioDevolver">0.00</span>
</div>
</div>
</div>
<div class="product-panel panel-windows" id="productCatalogPanel">
<h2>Catálogo de Productos</h2>
<div class="form-group mb-3">
<input type="text" class="form-control" id="catalog-filter-search" placeholder="Filtrar catálogo por nombre, código o departamento...">
</div>
<div id="product-list" class="mt-3">
<p class="text-center text-muted">Carga un Excel desde "Cargar Excel" para ver los productos.</p>
</div>
</div>
</div>
</div>
</div>
<div id="stockReportsPage" class="main-content-page">
<div class="report-panel">
<h2><i class="fas fa-chart-bar"></i> Reportes de Stock</h2>
<div class="d-flex justify-content-between mb-3 flex-wrap">
<button class="btn btn-primary btn-sm mb-2 mb-md-0" onclick="downloadStockReport('csv')"><i class="fas fa-download"></i> Descargar Reporte de Stock (CSV)</button>
<button class="btn btn-primary btn-sm mb-2 mb-md-0" onclick="downloadStockReport('xlsx')"><i class="fas fa-file-excel"></i> Descargar Reporte de Stock (Excel)</button>
<button class="btn btn-info btn-sm" onclick="downloadAllProductsReport('csv')"><i class="fas fa-download"></i> Descargar Inventario Completo (CSV)</button>
<button class="btn btn-info btn-sm" onclick="downloadAllProductsReport('xlsx')"><i class="fas fa-file-excel"></i> Descargar Inventario Completo (Excel)</button>
</div>
<h3 class="mt-3 text-danger"><i class="fas fa-exclamation-triangle"></i> Productos Agotados (Existencia &le; 0)</h3>
<div class="form-group">
<input type="text" class="form-control form-control-sm" id="stock-search-out-of-stock" placeholder="Filtrar agotados por código, producto o departamento...">
</div>
<button class="btn btn-secondary btn-sm mb-2" onclick="copyTableContentToClipboard('outOfStockList', 'csv')"><i class="fas fa-copy"></i> Copiar Agotados (CSV)</button>
<div id="outOfStockList" class="report-table-container mb-4">
<p class="text-center text-muted">No hay productos agotados.</p>
</div>
<h3 class="mt-4 text-warning"><i class="fas fa-warehouse"></i> Productos con Stock Bajo (Existencia &lt; 10)</h3>
<div class="form-group">
<input type="text" class="form-control form-control-sm" id="stock-search-low-stock" placeholder="Filtrar stock bajo por código, producto o departamento...">
</div>
<button class="btn btn-secondary btn-sm mb-2" onclick="copyTableContentToClipboard('lowStockList', 'csv')"><i class="fas fa-copy"></i> Copiar Stock Bajo (CSV)</button>
<div id="lowStockList" class="report-table-container">
<p class="text-center text-muted">No hay productos con stock bajo.</p>
</div>
</div>
</div>
<div id="salesHistoryPage" class="main-content-page">
<div class="report-panel">
<h2><i class="fas fa-history"></i> Historial de Ventas y Pedidos</h2>
<div class="form-group row">
<div class="col-md-5">
<label for="startDate">Fecha Inicio:</label>
<input type="date" class="form-control" id="startDate">
</div>
<div class="col-md-5">
<label for="endDate">Fecha Fin:</label>
<input type="date" class="form-control" id="endDate">
</div>
<div class="col-md-2 d-flex align-items-end">
<button class="btn btn-primary btn-block" id="filterSalesBtn"><i class="fas fa-filter"></i> Filtrar</button>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
<h5 class="mb-0">Total Generado: <span id="filteredSalesTotal" class="text-success font-weight-bold">0.00</span></h5>
<div>
<button class="btn btn-primary btn-sm mb-2 mb-md-0" onclick="downloadSalesHistoryReport('csv')"><i class="fas fa-download"></i> Descargar Historial (CSV)</button>
<button class="btn btn-primary btn-sm" onclick="downloadSalesHistoryReport('xlsx')"><i class="fas fa-file-excel"></i> Descargar Historial (Excel)</button>
<button class="btn btn-secondary btn-sm mb-2" onclick="copyTableContentToClipboard('salesHistoryList', 'csv')"><i class="fas fa-copy"></i> Copiar Historial (CSV)</button>
</div>
</div>
<div id="salesHistoryList" class="report-table-container mt-3">
<p class="text-center text-muted">Aún no hay ventas o pedidos registrados.</p>
</div>
</div>
</div>
<div id="analyticsPage" class="main-content-page">
<div class="report-panel">
<h2><i class="fas fa-chart-line"></i> Análisis de Ventas y Pedidos</h2>
<p class="text-muted text-center">Este análisis muestra el rendimiento combinado de tus ventas y pedidos.</p>
<h3 class="mt-4"><i class="fas fa-chart-area"></i> Ventas y Pedidos por Día (Últimos 7 Días)</h3>
<div id="salesChartContainer">
<canvas id="salesChart"></canvas>
</div>
<h3 class="mt-4"><i class="fas fa-info-circle"></i> Métricas Clave</h3>
<div class="analytics-summary">
<div class="analytics-card">
<h4>Total de Transacciones</h4>
<span class="value neutral" id="totalSalesCount">0</span>
</div>
<div class="analytics-card">
<h4>Ingresos Totales</h4>
<span class="value positive" id="totalRevenue">0.00</span>
</div>
<div class="analytics-card">
<h4>Ticket Promedio</h4>
<span class="value neutral" id="averageTicket">0.00</span>
</div>
<div class="analytics-card">
<h4>Producto Más Vendido (Cantidad)</h4>
<span class="value neutral" id="mostSoldProductQuantity">N/A</span>
</div>
<div class="analytics-card">
<h4>Producto Más Vendido (Ingresos)</h4>
<span class="value neutral" id="mostSoldProductRevenue">N/A</span>
</div>
<div class="analytics-card">
<h4>Departamento Más Rentable (Ingresos)</h4>
<span class="value neutral" id="mostProfitableDeptRevenue">N/A</span>
</div>
<div class="analytics-card">
<h4>Método de Pago Preferido</h4>
<span class="value neutral" id="preferredPaymentMethod">N/A</span>
</div>
<div class="analytics-card">
<h4>Total de Artículos Vendidos</h4>
<span class="value neutral" id="totalItemsSold">0</span>
</div>
<div class="analytics-card">
<h4>Clientes Atendidos</h4>
<span class="value neutral" id="uniqueCustomers">0</span>
</div>
<div class="analytics-card">
<h4>Transacciones por Hora (Promedio)</h4>
<span class="value neutral" id="averageSalesPerHour">N/A</span>
</div>
</div>
</div>
</div>
<div id="productManagementPage" class="main-content-page">
<div class="report-panel">
<h2><i class="fas fa-cubes"></i> Gestión de Productos</h2>
<div class="product-form">
<h3>Agregar/Editar Producto</h3>
<div class="form-group">
<label for="mgmtProductCode">Código de Producto:</label>
<input type="text" class="form-control" id="mgmtProductCode" placeholder="Código único del producto" required>
</div>
<div class="form-group">
<label for="mgmtProductName">Nombre del Producto:</label>
<input type="text" class="form-control" id="mgmtProductName" placeholder="Nombre del producto" required>
</div>
<div class="form-group">
<label for="mgmtProductDepartment">Departamento:</label>
<input type="text" class="form-control" id="mgmtProductDepartment" placeholder="Ej: Abarrotes, Lácteos">
</div>
<div class="form-group">
<label for="mgmtProductPrice">Precio de Venta (<span id="mgmtProductPriceCurrency"></span>):</label>
<input type="number" class="form-control" id="mgmtProductPrice" step="0.01" min="0" value="0.00" required>
</div>
<div class="form-group">
<label for="mgmtProductStock">Existencia:</label>
<input type="number" class="form-control" id="mgmtProductStock" step="1" min="0" value="0" required>
</div>
<div class="form-group">
<label for="mgmtProductUnitType">Tipo de Unidad:</label>
<select class="form-control" id="mgmtProductUnitType" required>
<option value="unit">Unidad</option>
<option value="kg">Kilogramo (kg)</option>
</select>
</div>
<div class="d-flex justify-content-center">
<button class="btn btn-primary mr-2" id="btnSaveProduct"><i class="fas fa-save"></i> Guardar Producto</button>
<button class="btn btn-secondary" id="btnClearProductForm"><i class="fas fa-redo"></i> Limpiar Formulario</button>
</div>
</div>
<h3 class="mt-4"><i class="fas fa-list-alt"></i> Listado de Productos</h3>
<div class="form-group">
<input type="text" class="form-control" id="product-management-search" placeholder="Buscar por código, nombre o departamento para editar/eliminar...">
</div>
<div id="productManagementList" class="product-management-list-container">
<p class="text-center text-muted">Carga un Excel o agrega productos para verlos aquí.</p>
</div>
</div>
</div>
<div id="grammageProductsPage" class="main-content-page">
<div class="report-panel">
<h2><i class="fas fa-weight-hanging"></i> Productos por Peso (kg/g)</h2>
<p class="text-muted text-center">Aquí se muestran todos los productos configurados para venta por peso.</p>
<div class="form-group">
<input type="text" class="form-control form-control-sm" id="grammage-product-search" placeholder="Filtrar por código, producto o departamento...">
</div>
<button class="btn btn-secondary btn-sm mb-2" onclick="copyTableContentToClipboard('grammageProductList', 'csv')"><i class="fas fa-copy"></i> Copiar Productos por Peso (CSV)</button>
<div id="grammageProductList" class="report-table-container">
<p class="text-center text-muted">No hay productos configurados para venta por peso.</p>
</div>
</div>
</div>
<div id="expensesPage" class="main-content-page">
<div class="report-panel">
<h2><i class="fas fa-money-check-alt"></i> Gestión de Gastos</h2>
<div class="expense-form">
<h3>Registrar Nuevo Gasto</h3>
<div class="form-group">
<label for="expenseDate">Fecha:</label>
<input type="date" class="form-control" id="expenseDate" required>
</div>
<div class="form-group">
<label for="expenseDescription">Descripción:</label>
<input type="text" class="form-control" id="expenseDescription" placeholder="Ej: Pago de luz, Compra de insumos" required>
</div>
<div class="form-group">
<label for="expenseAmount">Monto (<span id="expenseAmountCurrency"></span>):</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text" id="expenseCurrencySymbol"></span>
</div>
<input type="number" class="form-control" id="expenseAmount" step="0.01" min="0" placeholder="0.00" required>
</div>
</div>
<div class="form-group">
<label for="expenseCategory">Categoría:</label>
<input type="text" class="form-control" id="expenseCategory" placeholder="Ej: Servicios, Materiales, Salarios">
</div>
<div class="d-flex justify-content-center">
<button class="btn btn-primary mr-2" id="btnAddExpense"><i class="fas fa-plus-circle"></i> Añadir Gasto</button>
<button class="btn btn-secondary" id="btnClearExpenseForm"><i class="fas fa-redo"></i> Limpiar Formulario</button>
</div>
</div>
<h3 class="mt-4"><i class="fas fa-file-invoice-dollar"></i> Reporte de Gastos</h3>
<div class="d-flex justify-content-between mb-3 flex-wrap">
<button class="btn btn-primary btn-sm mb-2 mb-md-0" onclick="downloadExpensesReport('csv')"><i class="fas fa-download"></i> Descargar Gastos (CSV)</button>
<button class="btn btn-primary btn-sm" onclick="downloadExpensesReport('xlsx')"><i class="fas fa-file-excel"></i> Descargar Gastos (Excel)</button>
</div>
<div class="form-group">
<input type="text" class="form-control form-control-sm" id="expense-search" placeholder="Filtrar gastos por descripción o categoría...">
</div>
<button class="btn btn-secondary btn-sm mb-2" onclick="copyTableContentToClipboard('expenseList', 'csv')"><i class="fas fa-copy"></i> Copiar Gastos (CSV)</button>
<div id="expenseList" class="expense-list-container report-table-container">
<p class="text-center text-muted">No hay gastos registrados.</p>
</div>
<div class="total-expenses-summary">
Total de Gastos Filtrados: <span id="filteredTotalExpenses">0.00</span>
</div>
</div>
</div>
<div id="notesPage" class="main-content-page">
<div class="report-panel">
<h2><i class="fas fa-clipboard"></i> Mis Notas</h2>
<div class="note-form">
<h3>Añadir Nueva Nota</h3>
<div class="form-group">
<label for="noteTitle">Título:</label>
<input type="text" class="form-control" id="noteTitle" placeholder="Título de la nota" required>
</div>
<div class="form-group">
<label for="noteContent">Contenido:</label>
<textarea class="form-control" id="noteContent" rows="5" placeholder="Escribe tu nota aquí..." required></textarea>
</div>
<div class="d-flex justify-content-center">
<button class="btn btn-primary mr-2" id="btnAddNote"><i class="fas fa-plus-square"></i> Guardar Nota</button>
<button class="btn btn-secondary" id="btnClearNoteForm"><i class="fas fa-redo"></i> Limpiar Formulario</button>
</div>
</div>
<h3 class="mt-4"><i class="fas fa-list-ul"></i> Listado de Notas</h3>
<div id="notesList" class="notes-list-container">
<p class="text-center text-muted">No hay notas guardadas.</p>
</div>
</div>
</div>
<div id="ordersPage" class="main-content-page">
<div class="report-panel">
<h2><i class="fas fa-box-open"></i> Gestión de Pedidos</h2>
<p class="text-center text-muted">Aquí se mostrarán los pedidos pendientes o en proceso.</p>
<div class="form-group">
<input type="text" class="form-control" id="order-search" placeholder="Buscar pedido por ID, producto o WhatsApp del cliente...">
</div>
<div id="ordersList" class="orders-list-container">
<p class="text-center text-muted">No hay pedidos pendientes.</p>
</div>
</div>
</div>
<div id="shortcutsPage" class="main-content-page">
<div class="report-panel">
<h2><i class="fas fa-keyboard"></i> Atajos de Teclado</h2>
<p class="text-muted text-center">Aquí puedes consultar los atajos de teclado para una mayor eficiencia.</p>
<ul class="list-group text-left">
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">F3</strong></span>
<span>Limpiar Carrito y Enfocar Scanner</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">F7</strong></span>
<span>Enfocar el campo "Escanear código o buscar producto"</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">F8</strong></span>
<span>Enfocar el campo "Monto Recibido" (Caja)</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">F9</strong></span>
<span>Pre-cargar el total en "Monto Recibido" y enfocar para confirmar venta (Cobrar)</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">F6</strong></span>
<span>Abrir el modal para agregar productos por peso (kg/g) y enfocar el campo de código/nombre</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">F5</strong></span>
<span>Abrir el modal para agregar productos por peso (kg/g) y enfocar el campo de peso</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">F4</strong></span>
<span>Regresar al Punto de Venta y enfocar el scanner</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">F1</strong></span>
<span>Ir a la página de Atajos de Teclado</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">F10</strong></span>
<span>Enfocar el campo "Filtrar catálogo por nombre, código o departamento"</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">Enter</strong> (en campo de scanner)</span>
<span>Añadir producto al carrito</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">Enter</strong> (en "Monto Recibido")</span>
<span>Finalizar la venta</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">↑ ↓</strong> (en catálogo de productos)</span>
<span>Navegar por productos</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">+</strong> (en catálogo de productos)</span>
<span>Añadir producto resaltado al carrito</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">-</strong> (en catálogo de productos)</span>
<span>Disminuir cantidad del producto resaltado en el carrito</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">↑ ↓</strong> (en carrito de compras)</span>
<span>Navegar por productos en el carrito</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">+</strong> (en carrito de compras)</span>
<span>Aumentar cantidad del producto resaltado en el carrito</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">-</strong> (en carrito de compras)</span>
<span>Disminuir cantidad del producto resaltado en el carrito</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong class="text-primary">Esc</strong></span>
<span>Cerrar cualquier modal abierto</span>
</li>
</ul>
</div>
</div>
<div id="subscriptionPage" class="main-content-page">
<div class="report-panel">
<h2><i class="fas fa-star"></i> Estado de la Suscripción</h2>
<div class="subscription-panel">
<h3 class="mb-4">Tu Plan Actual</h3>
<span id="subscriptionStatus" class="subscription-status">Cargando...</span>
<h3 class="mt-5 mb-3">Ventajas del Plan Premium</h3>
<ul class="advantages-list">
<li><i class="fas fa-check-circle"></i> Gestión de Precios Avanzada</li>
<li><i class="fas fa-check-circle"></i> Múltiples Almacenes</li>
<li><i class="fas fa-check-circle"></i> Sincronización en la Nube (Backup automático)</li>
<li><i class="fas fa-check-circle"></i> Facturación Electrónica Integrada</li>
<li><i class="fas fa-check-circle"></i> Soporte Prioritario 24/7</li>
<li><i class="fas fa-check-circle"></i> Reportes Personalizados y Avanzados</li>
<li><i class="fas fa-check-circle"></i> Acceso a Nuevas Características Exclusivas</li>
</ul>
<button class="btn btn-info upgrade-button" id="upgradePremiumBtn">Actualizar a Premium</button>
</div>
</div>
</div>
<div id="settingsPage" class="main-content-page">
<div class="report-panel">
<h2><i class="fas fa-cogs"></i> Opciones Generales</h2>
<div class="settings-panel">
<h3>Visibilidad de Secciones</h3>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="toggleProductCatalog" data-setting-target="productCatalogPanel">
<label class="form-check-label" for="toggleProductCatalog">
Mostrar Catálogo de Productos en Punto de Venta
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="toggleReportsSection" data-setting-target="reportes-section">
<label class="form-check-label" for="toggleReportsSection">
Mostrar Sección de Reportes
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="toggleManagementSection" data-setting-target="gestion-section">
<label class="form-check-label" for="toggleManagementSection">
Mostrar Sección de Gestión
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="toggleConfigSection" data-setting-target="configuraciones-section">
<label class="form-check-label" for="toggleConfigSection">
Mostrar Sección de Configuraciones
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="toggleSubscriptionPage" data-setting-target="subscriptionPage-link">
<label class="form-check-label" for="toggleSubscriptionPage">
Mostrar Página de Suscripción
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="toggleUserManagementPage" data-setting-target="userManagementPage-link">
<label class="form-check-label" for="toggleUserManagementPage">
Mostrar Gestión de Usuarios
</label>
</div>
</div>
<h3 class="mt-4">Características Premium (Visibilidad)</h3>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="toggleGrammageProductsPage">
<label class="form-check-label" for="toggleGrammageProductsPage">
Mostrar "Productos por Peso"
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="toggleCurrencyManagement">
<label class="form-check-label" for="toggleCurrencyManagement">
Habilitar Gestión de Moneda
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="toggleAdvancedAnalytics">
<label class="form-check-label" for="toggleAdvancedAnalytics">
Mostrar "Análisis de Ventas"
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="toggleOrdersPage">
<label class="form-check-label" for="toggleOrdersPage">
Mostrar "Pedidos" y Notificaciones
</label>
</div>
</div>
<h3 class="mt-4">Configuración de Moneda</h3>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="radio" name="currencyOption" id="currencyMXN" value="MXN">
<label class="form-check-label" for="currencyMXN">MXN ($)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="currencyOption" id="currencyUSD" value="USD">
<label class="form-check-label" for="currencyUSD">USD (US$)</label>
</div>
</div>
<div class="form-group mt-3">
<label for="exchangeRateInput">Tipo de Cambio (1 USD = MXN):</label>
<input type="number" class="form-control" id="exchangeRateInput" step="0.01" min="1" value="17.00">
<small class="form-text text-muted">Ajusta el valor del dólar.</small>
</div>
<button class="btn btn-primary" id="saveSettingsBtn"><i class="fas fa-save"></i> Guardar Ajustes</button>
</div>
</div>
</div>
<div id="userManagementPage" class="main-content-page">
<div class="report-panel">
<h2><i class="fas fa-user-shield"></i> Gestión de Usuarios</h2>
<p class="text-center text-muted">Esta es una característica premium que te permitirá gestionar usuarios y roles.</p>
<p class="text-center text-muted">¡Actualiza a Premium para desbloquearla!</p>
</div>
</div>
</main>
</div>
<div class="fixed-checkout-bar">
<div class="total-section">
Total: <span id="cart-total-currency-symbol"></span><span id="cart-total">0.00</span>
<span class="small-total-usd" id="cart-total-usd"></span>
</div>
<div class="btn-group">
<button class="btn btn-success btn-lg" id="btn-cobrar" disabled><i class="fas fa-dollar-sign"></i> Cobrar</button>
<button class="btn btn-danger btn-lg" id="btn-limpiar-carrito" disabled><i class="fas fa-trash-alt"></i> Limpiar Carrito</button>
</div>
</div>
<br>
<footer class="app-footer">
<p>&copy; 2025 Routicket.com por David Morales. Todos los derechos reservados.</p>
</footer>
<!-- <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
-->
<script src="jquery-3.5.1.slim.min.js"></script>
<script src="bootstrap.bundle.min.js"></script>
<script>
// Captura errores globales para depuración
window.onerror = function(message, source, lineno, colno, error) {
console.error("Uncaught JavaScript Error:", message, "at", source, "line", lineno, "col", colno, "Error object:", error);
// mostrarMensaje(`❌ Error de JavaScript: ${message} (Línea: ${lineno})`, 'danger');
return true; // Evita el manejo de errores predeterminado del navegador
};
// --- PHP-JS Bridge: Esta variable es establecida por el bloque PHP al cargar la página ---
const isPremiumUser = <?php echo $is_premium_user_js_value; ?>;
console.log('isPremiumUser (from PHP):', isPremiumUser);
let allProducts = [];
let productLookupMap = new Map(); // For O(1) product lookup by code
let cart = [];
let salesHistory = [];
let expenses = []; // New array for expenses
let notes = []; // New array for notes
let orders = []; // Array to store fetched orders
let barcodeScanBuffer = ''; // To accumulate characters during a scan
let lastScanTime = 0; // To detect end of scan
let salesChartInstance = null; // To store the Chart.js instance
let newOrderCount = 0; // For notification badge
let previousOrderIds = new Set(); // To store IDs of orders seen in the last fetch
// Almacenar el CÓDIGO del producto/item resaltado, no el elemento DOM directamente.
// Esto permite que el resaltado persista si la lista se re-renderiza.
let highlightedProductCode = null;
let highlightedCartItemCode = null;
// --- Currency Configuration ---
let currentCurrency = 'MXN'; // Default currency
let currentCurrencySymbol = '$'; // Default symbol for MXN
let exchangeRateUSDToMXN = 17.0; // Default example rate, will be loaded from settings
// --- Local Storage Keys ---
const PRODUCTS_STORAGE_KEY = 'datosExcel';
const CART_STORAGE_KEY = 'puntoDeVentaCart';
const SALES_HISTORY_STORAGE_KEY = 'puntoDeVentaSalesHistory';
const EXPENSES_STORAGE_KEY = 'puntoDeVentaExpenses'; // New key for expenses
const NOTES_STORAGE_KEY = 'puntoDeVentaNotes'; // New key for notes
const ORDERS_STORAGE_KEY = 'puntoDeVentaOrders'; // Key for locally stored orders (for comparison)
const SETTINGS_STORAGE_KEY = 'puntoDeVentaSettings'; // New key for settings
// Default settings for the application.
const defaultSettings = {
showProductCatalog: true,
showReportsSection: true,
showManagementSection: true,
showConfigSection: true,
showSubscriptionPage: true,
showUserManagementPage: true,
showGrammageProductsPage: true, // New premium feature
enableCurrencyManagement: true, // New premium feature
showAnalyticsPage: true, // New premium feature
showOrdersPage: true, // New premium feature
currency: 'MXN',
exchangeRateUSDToMXN: 17.00
};
let currentSettings = {}; // Will be populated from localStorage or defaultSettings
/**
* Muestra un modal de confirmación personalizado.
* @param {string} message - El mensaje a mostrar en el modal.
* @param {number} timeout - Tiempo en segundos para auto-confirmar (0 para no auto-confirmar).
* @returns {Promise<boolean>} Una promesa que se resuelve a true si el usuario confirma o si se auto-confirma, false si cancela.
*/
function showCustomConfirm(message, timeout = 0) {
console.log('showCustomConfirm called with message:', message);
const customConfirmModal = document.getElementById('customConfirmModal');
const customConfirmMessage = document.getElementById('customConfirmMessage');
const customConfirmYes = document.getElementById('customConfirmYes');
const customConfirmNo = document.getElementById('customConfirmNo');
customConfirmMessage.textContent = message;
customConfirmModal.classList.add('show');
return new Promise((resolve) => {
let timer;
let resolved = false;
const cleanup = () => {
if (timer) clearTimeout(timer);
customConfirmYes.removeEventListener('click', onYesClick);
customConfirmNo.removeEventListener('click', onNoClick);
document.removeEventListener('keydown', onKeyDown); // Eliminar el listener de teclado
customConfirmModal.classList.remove('show');
};
const onYesClick = () => {
if (!resolved) {
resolved = true;
cleanup();
resolve(true);
}
};
const onNoClick = () => {
if (!resolved) {
resolved = true;
cleanup();
resolve(false);
}
};
// Nuevo: Listener de teclado para Enter (confirmar) y Escape (cancelar)
const onKeyDown = (event) => {
if (!resolved) { // Solo manejar si la promesa no ha sido resuelta
if (event.key === 'Enter') {
event.preventDefault(); // Prevenir el comportamiento por defecto (ej. envío de formulario)
onYesClick(); // Tratar Enter como confirmación ("Sí")
} else if (event.key === 'Escape') {
event.preventDefault(); // Prevenir el comportamiento por defecto
onNoClick(); // Tratar Escape como cancelación ("No")
}
}
};
customConfirmYes.addEventListener('click', onYesClick);
customConfirmNo.addEventListener('click', onNoClick);
document.addEventListener('keydown', onKeyDown); // Añadir el listener de teclado al documento
if (timeout > 0) {
timer = setTimeout(() => {
if (!resolved) {
mostrarMensaje('Alerta de cambio ignorada. Procediendo con la venta.', 'info');
resolved = true;
cleanup();
resolve(true);
}
}, timeout * 1000);
}
});
}
/**
* Muestra u oculta el modal de carga.
* @param {boolean} show - True para mostrar, false para ocultar.
* @param {string} message - Mensaje a mostrar en el modal (opcional).
* @param {boolean} isOrderFetch - Si es true, muestra una alerta tipo "toast" en lugar del modal completo.
*/
function toggleLoadingModal(show, message = 'Cargando...', isOrderFetch = false) {
console.log('toggleLoadingModal called:', show, message, 'isOrderFetch:', isOrderFetch);
const loadingModal = document.getElementById('loadingModal');
const loadingMessage = loadingModal.querySelector('.loading-message');
if (isOrderFetch) {
if (show) {
mostrarMensaje(message, 'info');
}
// For order fetch, the 'hide' part is handled by explicit success/error messages
return;
}
// Default modal behavior for other loading scenarios
loadingMessage.textContent = message;
if (show) {
loadingModal.classList.add('show');
} else {
loadingModal.classList.remove('show');
}
}
/**
* Reproduce un sonido de notificación.
*/
function playNotificationSound() {
console.log('playNotificationSound called');
const audio = new Audio('https://routicket.com/plugins/points/audios/venta1.mp3');
audio.play().catch(e => console.error("Error playing sound:", e));
}
/**
* Cambia la página principal visible y actualiza el enlace activo en la barra lateral.
* @param {string} pageId - El ID de la página a mostrar.
*/
function showPage(pageId) {
console.log('showPage called with:', pageId);
document.querySelectorAll('.main-content-page').forEach(page => {
page.classList.remove('active');
});
document.getElementById(pageId).classList.add('active');
// Actualiza los enlaces activos en la barra lateral
document.querySelectorAll('.sidebar-link').forEach(link => {
link.classList.remove('active');
});
const activeLink = document.querySelector(`.sidebar-link[data-page="${pageId}"]`);
if (activeLink) {
activeLink.classList.add('active');
}
// Acciones específicas al cambiar de página
if (pageId === 'stockReportsPage') {
generateStockReports();
// Asegurarse de que los event listeners se adjunten solo una vez o se remuevan antes de re-adjuntar
document.getElementById('stock-search-out-of-stock').removeEventListener('input', (e) => filterStockTable('outOfStockList', e.target.value)); // Remove previous
document.getElementById('stock-search-low-stock').removeEventListener('input', (e) => filterStockTable('lowStockList', e.target.value)); // Remove previous
document.getElementById('stock-search-out-of-stock').addEventListener('input', (e) => filterStockTable('outOfStockList', e.target.value));
document.getElementById('stock-search-low-stock').addEventListener('input', (e) => filterStockTable('lowStockList', e.target.value));
} else if (pageId === 'salesHistoryPage') {
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
const formatForInput = (date) => {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
};
document.getElementById('endDate').value = formatForInput(today);
document.getElementById('startDate').value = formatForInput(yesterday);
displaySalesHistory();
} else if (pageId === 'analyticsPage') {
renderAnalytics();
} else if (pageId === 'productManagementPage') {
toggleLoadingModal(true, 'Cargando productos...');
setTimeout(() => {
renderProductManagementList(allProducts);
document.getElementById('product-management-search').removeEventListener('input', handleProductManagementSearch); // Remove previous
document.getElementById('product-management-search').addEventListener('input', handleProductManagementSearch);
toggleLoadingModal(false);
}, 100);
} else if (pageId === 'grammageProductsPage') {
toggleLoadingModal(true, 'Cargando productos por peso...');
setTimeout(() => {
renderGrammageProductsList();
document.getElementById('grammage-product-search').removeEventListener('input', (e) => filterGrammageProductsTable(e.target.value)); // Remove previous
document.getElementById('grammage-product-search').addEventListener('input', (e) => filterGrammageProductsTable(e.target.value));
toggleLoadingModal(false);
}, 100);
} else if (pageId === 'expensesPage') {
renderExpensesList();
document.getElementById('expense-search').removeEventListener('input', (e) => filterExpensesTable(e.target.value)); // Remove previous
document.getElementById('expense-search').addEventListener('input', (e) => filterExpensesTable(e.target.value));
} else if (pageId === 'notesPage') {
renderNotesList();
} else if (pageId === 'ordersPage') {
fetchOrdersFromAPI();
document.getElementById('order-search').removeEventListener('input', handleOrderSearch); // Remove previous
document.getElementById('order-search').addEventListener('input', handleOrderSearch);
}
else if (pageId === 'subscriptionPage') {
updateSubscriptionStatusUI();
} else if (pageId === 'settingsPage') {
renderSettingsPage();
} else if (pageId === 'puntoVentaPage') {
document.getElementById('product-search').focus();
handleCatalogFilterInput();
}
}
// --- Inicialización al cargar la página ---
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM Content Loaded. Initializing app...');
// Cargar productos
const storedProducts = localStorage.getItem(PRODUCTS_STORAGE_KEY);
if (storedProducts) {
try {
allProducts = JSON.parse(storedProducts);
addDefaultAndDummyProducts();
buildProductLookupMap();
if (allProducts.length > 0) {
displayProducts(allProducts);
mostrarMensaje(`✔️ Se cargaron ${allProducts.length} productos de la última sesión.`, 'success');
} else {
document.getElementById('product-list').innerHTML = '<p class="text-center text-muted">No hay productos disponibles. Carga un Excel desde "Cargar Excel".</p>';
mostrarMensaje('⚠️ No se encontraron productos guardados.', 'warning');
}
} catch (e) {
console.error("Error parsing localStorage data for products:", e);
mostrarMensaje("❌ Error al cargar productos guardados. Por favor, vuelve a cargar el Excel.", "danger");
localStorage.removeItem(PRODUCTS_STORAGE_KEY);
document.getElementById('product-list').innerHTML = '<p class="text-center text-muted">Error al cargar productos. Carga un Excel desde "Cargar Excel".</p>';
}
} else {
addDefaultAndDummyProducts();
buildProductLookupMap();
displayProducts(allProducts);
mostrarMensaje('ℹ️ No hay productos guardados. Se agregaron productos de ejemplo. Carga un archivo Excel desde la página anterior.', 'info');
}
// Cargar carrito guardado
const storedCart = localStorage.getItem(CART_STORAGE_KEY);
if (storedCart) {
try {
cart = JSON.parse(storedCart);
updateCartDisplay();
mostrarMensaje('🛒 Carrito restaurado de la última sesión.', 'info');
} catch (e) {
console.error("Error parsing localStorage data for cart:", e);
mostrarMensaje("❌ Error al cargar el carrito guardado.", "danger");
localStorage.removeItem(CART_STORAGE_KEY);
}
}
// Cargar historial de ventas
const storedSalesHistory = localStorage.getItem(SALES_HISTORY_STORAGE_KEY);
if (storedSalesHistory) {
try {
salesHistory = JSON.parse(storedSalesHistory);
} catch (e) {
console.error("Error parsing localStorage data for sales history:", e);
mostrarMensaje("❌ Error al cargar el historial de ventas.", "danger");
localStorage.removeItem(SALES_HISTORY_STORAGE_KEY);
}
}
// Cargar gastos
const storedExpenses = localStorage.getItem(EXPENSES_STORAGE_KEY);
if (storedExpenses) {
try {
expenses = JSON.parse(storedExpenses);
} catch (e) {
console.error("Error parsing localStorage data for expenses:", e);
mostrarMensaje("❌ Error al cargar los gastos guardados.", "danger");
localStorage.removeItem(EXPENSES_STORAGE_KEY);
}
}
// Cargar notas
const storedNotes = localStorage.getItem(NOTES_STORAGE_KEY);
if (storedNotes) {
try {
notes = JSON.parse(storedNotes);
} catch (e) {
console.error("Error parsing localStorage data for notes:", e);
mostrarMensaje("❌ Error al cargar las notas guardadas.", "danger");
localStorage.removeItem(NOTES_STORAGE_KEY);
}
}
// Load previous order IDs for comparison
const storedPreviousOrderIds = localStorage.getItem(ORDERS_STORAGE_KEY);
if (storedPreviousOrderIds) {
try {
previousOrderIds = new Set(JSON.parse(storedPreviousOrderIds));
} catch (e) {
console.error("Error parsing localStorage data for previous order IDs:", e);
previousOrderIds = new Set();
}
}
// Load and apply settings
loadAndApplySettings();
// --- Event Listeners Globales ---
const productSearchInput = document.getElementById('product-search');
productSearchInput.addEventListener('keydown', handleBarcodeScan);
productSearchInput.addEventListener('input', handleScannerInputDisplay);
console.log('Event listener attached to product-search (keydown, input)');
const catalogFilterSearchInput = document.getElementById('catalog-filter-search');
if (catalogFilterSearchInput) {
catalogFilterSearchInput.addEventListener('input', handleCatalogFilterInput);
console.log('Event listener attached to catalog-filter-search (input)');
}
document.getElementById('btn-limpiar-carrito').addEventListener('click', async () => {
console.log('btn-limpiar-carrito clicked');
const confirmed = await showCustomConfirm("¿Estás seguro de que quieres limpiar todo el carrito?");
if (confirmed) {
clearCart();
}
});
console.log('Event listener attached to btn-limpiar-carrito (click)');
document.getElementById('amountReceived').addEventListener('input', calculateChange);
console.log('Event listener attached to amountReceived (input)');
document.getElementById('btn-cobrar').addEventListener('click', () => {
console.log('btn-cobrar clicked');
const totalAmount = parseFloat(document.getElementById('cart-total').textContent);
const amountReceivedInput = document.getElementById('amountReceived');
if (cart.length === 0) {
mostrarMensaje('El carrito está vacío. No se puede cobrar.', 'warning');
return;
}
amountReceivedInput.value = totalAmount.toFixed(2);
calculateChange();
amountReceivedInput.focus();
mostrarMensaje('Monto total pre-cargado. Presiona Enter para confirmar la venta.', 'info');
});
console.log('Event listener attached to btn-cobrar (click)');
document.getElementById('amountReceived').addEventListener('keydown', (event) => {
console.log('amountReceived keydown:', event.key);
if (event.key === 'Enter') {
event.preventDefault();
finalizeSale();
}
});
console.log('Event listener attached to amountReceived (keydown)');
// NEW: Global keydown listener for F3 to clear cart and focus scanner
document.addEventListener('keydown', async (event) => {
if (event.key === 'F3') {
event.preventDefault();
console.log('F3 pressed, clearing cart and focusing scanner');
const confirmed = await showCustomConfirm("¿Estás seguro de que quieres limpiar todo el carrito?");
if (confirmed) {
clearCart();
document.getElementById('product-search').focus();
mostrarMensaje('Carrito limpiado. Scanner listo.', 'info');
}
}
});
// NEW: Global keydown listener for ArrowUp, ArrowDown, +, -, and Enter when product-search is focused
document.addEventListener('keydown', (event) => {
const productSearchInput = document.getElementById('product-search');
const currentPageId = document.querySelector('.main-content-page.active')?.id;
const rawSearchTerm = productSearchInput.value.trim(); // Get search term here for key logic
// Only apply this complex navigation if on the Punto de Venta page
if (currentPageId === 'puntoVentaPage' && document.activeElement === productSearchInput) {
const productItems = Array.from(document.querySelectorAll('#product-list .product-item')); // Current filtered catalog items
const cartItems = Array.from(document.querySelectorAll('#cart-list .cart-item'));
// Handle ArrowUp/ArrowDown for navigation: ONLY navigate cart if search input is EMPTY
if (['ArrowDown', 'ArrowUp'].includes(event.key) && rawSearchTerm === '') {
event.preventDefault(); // Prevent page scrolling
// If search input is empty, and cart has items, navigate the cart
if (cartItems.length > 0) {
let currentIndex = -1;
if (highlightedCartItemCode) {
currentIndex = cartItems.findIndex(item => item.dataset.code === highlightedCartItemCode);
}
let nextIndex = currentIndex;
if (event.key === 'ArrowDown') {
nextIndex = (currentIndex + 1) % cartItems.length;
} else if (event.key === 'ArrowUp') {
nextIndex = (currentIndex - 1 + cartItems.length) % cartItems.length;
}
if (cartItems[nextIndex]) {
highlightCartItem(cartItems[nextIndex].dataset.code);
cartItems[nextIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} else {
highlightCartItem(null); // Clear highlight
}
highlightProduct(null); // Ensure product catalog highlight is cleared
} else {
mostrarMensaje('No hay productos en el carrito para navegar.', 'info');
}
}
// Handle '+' and '-' for quantity adjustment / adding to cart
// This will prioritize adding/modifying the HIGHLIGHTED product/cart item
else if (['+', '-'].includes(event.key)) {
event.preventDefault();
if (highlightedProductCode) {
// If a product in the catalog is highlighted (e.g., by initial type-ahead)
const productCode = highlightedProductCode;
if (event.key === '+') {
addProductToCart(productCode);
} else if (event.key === '-') {
const itemInCart = cart.find(item => item.Cdigo === productCode);
if (itemInCart) {
updateCartQuantity(productCode, -1);
} else {
mostrarMensaje(`El producto "${productLookupMap.get(productCode)?.Producto || productCode}" no está en el carrito para restar.`, 'info');
}
}
} else if (highlightedCartItemCode) {
// If a product in the cart is highlighted
const productCode = highlightedCartItemCode;
if (event.key === '+') {
updateCartQuantity(productCode, 1);
} else if (event.key === '-') {
updateCartQuantity(productCode, -1);
}
} else {
mostrarMensaje('Primero selecciona un producto en el catálogo o en el carrito con las flechas.', 'warning');
}
}
// --- MODIFIED: Handle Enter key. Primarily for adding from scanner/manual input. ---
else if (event.key === 'Enter') {
event.preventDefault(); // Prevent form submission
if (rawSearchTerm.length > 0) {
// Determine the primary type of the search term
const isLikelyNumeric = /^\d+$/.test(rawSearchTerm);
const isLikelyAlphabetic = /^[a-zA-ZáéíóúÁÉÍÓÚñÑ\s]+$/.test(rawSearchTerm);
let foundProduct = null;
// Search `allProducts` for the best match based on the input field's content
for (const producto of allProducts) {
const nombreProducto = normalizeString(producto.Producto);
const codigoProducto = normalizeString(producto.Cdigo);
const departamentoProducto = normalizeString(producto.Departamento);
if (isLikelyNumeric) {
// For numeric, prefer exact code match, then startsWith
if (codigoProducto === normalizedSearchTerm) { // Exact match is strongest
foundProduct = producto;
break;
}
if (codigoProducto.startsWith(normalizedSearchTerm)) { // Then startsWith
if (!foundProduct) foundProduct = producto; // Take the first startsWith match if no exact
}
} else if (isLikelyAlphabetic) {
// For alphabetic, prefer startsWith for names
if (nombreProducto.startsWith(normalizedSearchTerm)) {
foundProduct = producto;
break;
}
if (departamentoProducto.startsWith(normalizedSearchTerm)) { // Also check department if name doesn't match
if (!foundProduct) foundProduct = producto;
}
} else {
// Mixed or unclear input: search all fields with startsWith, prioritizing code/name
if (codigoProducto.startsWith(normalizedSearchTerm) ||
nombreProducto.startsWith(normalizedSearchTerm) ||
departamentoProducto.startsWith(normalizedSearchTerm)) {
foundProduct = producto;
break; // Take the first match encountered
}
}
}
if (foundProduct) {
addProductToCart(foundProduct.Cdigo);
productSearchInput.value = ''; // Clear input after adding
highlightProduct(null); // Clear catalog highlight
highlightCartItem(foundProduct.Cdigo); // Optionally highlight the *newly added* item in cart
// Scroll cart to the new item
const addedCartItemElement = document.querySelector(`#cart-list .cart-item[data-code="${foundProduct.Cdigo}"]`);
if (addedCartItemElement) {
addedCartItemElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
} else {
mostrarMensaje('Producto no encontrado con el término de búsqueda actual.', 'warning');
productSearchInput.value = ''; // Clear input even if not found, to reset
highlightProduct(null);
}
} else {
// If Enter is pressed with an empty search input, and there's a highlighted cart item, increase its quantity
if (highlightedCartItemCode) {
updateCartQuantity(highlightedCartItemCode, 1);
} else {
// mostrarMensaje('Por favor, ingresa un código o nombre de producto para buscar, o selecciona un item del carrito para ajustar.', 'info');
}
}
}
}
});
// Event listeners for Weight Entry Modal
document.getElementById('addWeightedProductBtn').addEventListener('click', addWeightedProductToCart);
document.getElementById('closeWeightEntryModalBtn').addEventListener('click', closeWeightEntryModal);
console.log('Event listeners attached to weight entry modal buttons');
document.getElementById('weightProductCode').addEventListener('input', updateWeightedProductInfo);
document.getElementById('weightProductCode').addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
console.log('weightProductCode Enter pressed');
const productCode = document.getElementById('weightProductCode').value.trim();
const product = productLookupMap.get(productCode);
// --- INICIO DE LA LÓGICA AGREGADA (ya existente) ---
const searchResultsContainer = document.getElementById('weightProductSearchResults');
const searchResults = searchResultsContainer.querySelectorAll('[data-code]');
if (searchResults.length === 1) {
// Si hay un único resultado, lo seleccionamos automáticamente
const uniqueProductCode = searchResults[0].dataset.code;
document.getElementById('weightProductCode').value = uniqueProductCode;
searchResultsContainer.innerHTML = ''; // Limpiamos los resultados de búsqueda
updateWeightedProductInfo(); // Actualizamos la información del producto seleccionado
// *** MODIFICACIÓN AQUÍ ***
document.getElementById('weightInput').focus();
document.getElementById('weightInput').select(); // <--- ¡Esta es la clave!
// ************************
return; // Salimos de la función para no procesar el resto de la lógica
}
// --- FIN DE LA LÓGICA AGREGADA ---
if (product && product.UnitType === 'kg') {
// *** MODIFICACIÓN AQUÍ ***
document.getElementById('weightInput').focus();
document.getElementById('weightInput').select(); // <--- ¡Y aquí también!
// ************************
} else {
if (!product) {
mostrarMensaje('Producto no encontrado. Selecciona de la lista o verifica el código.', 'warning');
} else if (product.UnitType !== 'kg') {
mostrarMensaje(`"${product.Producto}" no es un producto por peso.`, 'warning');
}
}
}
});
console.log('Event listeners attached to weightProductCode (input, keydown)');
document.getElementById('weightInput').addEventListener('input', updateWeightedProductInfo);
document.getElementById('weightInput').addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
console.log('weightInput Enter pressed');
addWeightedProductToCart();
}
});
console.log('Event listeners attached to weightInput (input, keydown)');
document.getElementById('weightProductSearchResults').addEventListener('click', (e) => {
console.log('weightProductSearchResults clicked');
const selectedCode = e.target.dataset.code;
if (selectedCode) {
document.getElementById('weightProductCode').value = selectedCode;
document.getElementById('weightProductSearchResults').innerHTML = '';
updateWeightedProductInfo();
// *** MODIFICACIÓN AQUÍ ***
document.getElementById('weightInput').focus();
document.getElementById('weightInput').select(); // <--- ¡Y también aquí!
// ************************
}
});
console.log('Event listener attached to weightProductSearchResults (click)');
// Product Management Form Buttons
document.getElementById('btnSaveProduct').addEventListener('click', saveProduct);
document.getElementById('btnClearProductForm').addEventListener('click', clearProductForm);
console.log('Event listeners attached to product management form buttons');
// Expense Form Buttons
document.getElementById('btnAddExpense').addEventListener('click', addExpense);
document.getElementById('btnClearExpenseForm').addEventListener('click', clearExpenseForm);
// Moved expense search listener to showPage for dynamic attachment
console.log('Event listeners attached to expense form buttons');
// Notes Form Buttons
document.getElementById('btnAddNote').addEventListener('click', addNote);
document.getElementById('btnClearNoteForm').addEventListener('click', clearNoteForm);
console.log('Event listeners attached to notes form buttons');
// Sales History Filter Button
document.getElementById('filterSalesBtn').addEventListener('click', () => {
console.log('filterSalesBtn clicked');
displaySalesHistory();
});
console.log('Event listener attached to filterSalesBtn');
// Handle clicks on the sidebar to change pages
document.querySelectorAll('.sidebar-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
console.log('Sidebar link clicked:', e.currentTarget.dataset.page);
showPage(e.currentTarget.dataset.page);
});
});
console.log('Event listeners attached to sidebar links');
// Settings page save button
document.getElementById('saveSettingsBtn').addEventListener('click', saveSettings);
console.log('Event listener attached to saveSettingsBtn');
// Shortcuts sidebar link
document.getElementById('shortcutsLinkSidebar').addEventListener('click', (e) => {
e.preventDefault();
console.log('shortcutsLinkSidebar clicked');
showPage('shortcutsPage');
});
console.log('Event listener attached to shortcutsLinkSidebar');
// Header "Nuevos Pedidos" link
document.getElementById('newOrdersNotification').addEventListener('click', (e) => {
e.preventDefault();
console.log('newOrdersNotification (header) clicked');
showPage('ordersPage'); // Navigate to orders page
});
updateSubscriptionStatusUI();
// Ensure product search input is focused after all initializations
productSearchInput.focus();
// Set default payment method
document.getElementById('paymentCash').checked = true;
// Initial fetch for orders and set interval
fetchOrdersFromAPI();
setInterval(fetchOrdersFromAPI, 100000);
console.log('Order fetching interval set.');
console.log('App initialization complete.');
});
/**
* Builds a map for quick product lookup by code.
*/
function buildProductLookupMap() {
console.log('buildProductLookupMap called');
productLookupMap.clear();
allProducts.forEach(p => {
productLookupMap.set(p.Cdigo, p);
});
}
/**
* Muestra los productos en el catálogo del Punto de Venta.
* @param {Array<Object>} productsToDisplay - Array de productos a mostrar.
*/
function displayProducts(productsToDisplay) {
console.log('displayProducts called with', productsToDisplay.length, 'products.');
const productListDiv = document.getElementById('product-list');
let html = '';
if (productsToDisplay.length === 0) {
productListDiv.innerHTML = '<p class="text-center text-muted">No se encontraron productos con esa búsqueda.</p>';
highlightProduct(null); // Clear any existing highlight
return;
}
productsToDisplay.forEach((producto, index) => {
const nombreProducto = String(producto.Producto || producto.Cdigo || 'Producto Desconocido').trim();
const precioVenta = typeof producto.PVenta === 'number' ? producto.PVenta : parseFloat(producto.PVenta) || 0;
const existencia = typeof producto.Existencia === 'number' ? producto.Existencia : parseFloat(producto.Existencia) || 0;
const unitType = producto.UnitType || 'unit';
html += `
<div class="product-item" data-code="${producto.Cdigo}" data-index="${index}">
<div class="product-info">
<div class="product-name">${nombreProducto}</div>
<div class="product-code text-muted">Cód: ${producto.Cdigo || 'N/A'}</div>
<div class="product-price">Precio: ${currentCurrencySymbol}${precioVenta.toFixed(2)} ${unitType === 'kg' ? '/ kg' : ''}</div>
<div class="product-stock ${existencia <= 0 ? 'text-danger font-weight-bold' : (existencia < 10 ? 'text-warning' : '')}">
Exist: ${existencia.toFixed(unitType === 'kg' ? 3 : 0)} ${unitType === 'kg' ? 'kg' : 'uds'} ${existencia <= 0 ? '(AGOTADO)' : (existencia < 10 ? '(BAJO)' : '')}
</div>
</div>
<button class="btn btn-primary btn-sm add-to-cart-btn" data-code="${producto.Cdigo}">
<i class="fas fa-cart-plus"></i>
</button>
</div>
`;
});
productListDiv.innerHTML = html;
// After rendering, try to re-highlight the previously highlighted product if it's still in the list
if (highlightedProductCode) {
const currentHighlightedElement = document.querySelector(`.product-item[data-code="${highlightedProductCode}"]`);
if (currentHighlightedElement) {
highlightProduct(highlightedProductCode); // Re-apply highlight to the new DOM element
} else {
highlightProduct(null); // Clear if no longer in displayed list
}
}
// Remove existing listeners before adding new ones to prevent duplicates
document.querySelectorAll('.add-to-cart-btn').forEach(button => {
const newButton = button.cloneNode(true);
button.parentNode.replaceChild(newButton, button);
});
document.querySelectorAll('.add-to-cart-btn').forEach(button => {
button.addEventListener('click', (e) => {
console.log('Add to cart button clicked for code:', e.currentTarget.dataset.code);
const productCode = e.currentTarget.dataset.code;
addProductToCart(productCode);
});
});
console.log('Event listeners attached to .add-to-cart-btn');
}
/**
* Highlights a product element in the list and scrolls it into view.
* Now takes a productCode (string) and finds the corresponding element.
* @param {string | null} productCode - The code of the product to highlight, or null to clear.
*/
function highlightProduct(productCode) {
// Clear previous highlight
if (highlightedProductCode) {
const prevHighlightedElement = document.querySelector(`.product-item[data-code="${highlightedProductCode}"]`);
if (prevHighlightedElement) {
prevHighlightedElement.classList.remove('highlighted');
}
}
// Apply new highlight
if (productCode) {
const newHighlightedElement = document.querySelector(`.product-item[data-code="${productCode}"]`);
if (newHighlightedElement) {
newHighlightedElement.classList.add('highlighted');
newHighlightedElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
highlightedProductCode = productCode;
} else {
highlightedProductCode = null; // Product not found in current view
}
} else {
highlightedProductCode = null; // Clear highlight
}
}
/**
* NEW: Highlights a cart item element in the list and scrolls it into view.
* Now takes an itemCode (string) and finds the corresponding element.
* @param {string | null} itemCode - The code of the cart item to highlight, or null to clear.
*/
function highlightCartItem(itemCode) {
// Clear previous highlight
if (highlightedCartItemCode) {
const prevHighlightedElement = document.querySelector(`.cart-item[data-code="${highlightedCartItemCode}"]`);
if (prevHighlightedElement) {
prevHighlightedElement.classList.remove('highlighted');
}
}
// Apply new highlight
if (itemCode) {
const newHighlightedElement = document.querySelector(`.cart-item[data-code="${itemCode}"]`);
if (newHighlightedElement) {
newHighlightedElement.classList.add('highlighted');
newHighlightedElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
highlightedCartItemCode = itemCode;
} else {
highlightedCartItemCode = null; // Item not found in current cart view
}
} else {
highlightedCartItemCode = null; // Clear highlight
}
}
/**
* Handles keyboard input to simulate barcode scanning.
* Uses productLookupMap for O(1) lookup, which is very fast.
* @param {Event} event - The keyboard event.
*/
function handleBarcodeScan(event) {
console.log('handleBarcodeScan called, key:', event.key);
const searchInput = document.getElementById('product-search');
const currentTime = Date.now();
// Reset buffer if significant time has passed since last key press
if (currentTime - lastScanTime > 50 && barcodeScanBuffer.length > 0) {
barcodeScanBuffer = '';
}
lastScanTime = currentTime;
if (event.key === 'Enter') {
event.preventDefault();
const scannedCode = searchInput.value.trim();
console.log('Enter pressed, scannedCode:', scannedCode);
if (scannedCode.length > 0) {
const productFound = productLookupMap.get(scannedCode);
if (productFound) {
if (productFound.UnitType === 'kg') {
openWeightEntryModal(productFound.Cdigo, false);
} else {
addProductToCart(productFound.Cdigo);
}
searchInput.value = '';
// No focus to catalog filter here, focus remains on product-search
searchInput.focus();
} else {
mostrarMensaje(`Producto no encontrado con el código exacto: "${scannedCode}".`, 'danger');
searchInput.value = '';
searchInput.focus();
}
}
barcodeScanBuffer = ''; // Clear buffer after processing Enter
} else {
barcodeScanBuffer += event.key;
}
}
/**
* Handles input in the scanner search field to update its display.
*/
function handleScannerInputDisplay() {
// console.log('handleScannerInputDisplay called'); // Too verbose, keep commented
}
/**
* Handles input in the new catalog filter search field.
*/
function handleCatalogFilterInput() {
console.log('handleCatalogFilterInput called');
const catalogFilterInput = document.getElementById('catalog-filter-search');
const rawSearchTerm = catalogFilterInput.value.trim();
const normalizedSearchTerm = normalizeString(rawSearchTerm); // Assuming normalizeString handles accents, case, etc.
if (rawSearchTerm.length > 0) {
// Determine the primary type of the search term
const isLikelyNumeric = /^\d+$/.test(rawSearchTerm); // Entirely digits
const isLikelyAlphabetic = /^[a-zA-ZáéíóúÁÉÍÓÚñÑ\s]+$/.test(rawSearchTerm); // Primarily letters and spaces
const filteredProducts = allProducts.filter(producto => {
const nombreProducto = normalizeString(producto.Producto);
const codigoProducto = normalizeString(producto.Cdigo);
const departamentoProducto = normalizeString(producto.Departamento);
// --- Prioritize search based on detected input type ---
if (isLikelyNumeric) {
// If the input is primarily numeric, prioritize searching in Cdigo
// and ensure it starts with the search term.
return codigoProducto.startsWith(normalizedSearchTerm);
} else if (isLikelyAlphabetic) {
// If the input is primarily alphabetic, prioritize searching in Nombre and Departamento
// and ensure it starts with the search term.
return nombreProducto.startsWith(normalizedSearchTerm) ||
departamentoProducto.startsWith(normalizedSearchTerm);
} else {
// If it's mixed or hard to classify (e.g., "AB123"), search across all fields
// using startsWith, giving preference to the beginning of the string.
return nombreProducto.startsWith(normalizedSearchTerm) ||
codigoProducto.startsWith(normalizedSearchTerm) ||
departamentoProducto.startsWith(normalizedSearchTerm);
}
});
displayProducts(filteredProducts);
} else {
// If the search term is empty, display all products
displayProducts(allProducts);
}
}
/**
* Adds a product to the cart or increments its quantity if it already exists.
* This function handles unit-based products. For 'kg' products, it directs to the weight entry modal.
* @param {string} productCode - The code of the product to add.
* @param {number} [quantityToAdd=1] - The quantity to add (defaults to 1 for unit products).
*/
function addProductToCart(productCode, quantityToAdd = 1) { // Added quantityToAdd for flexibility
console.log('addProductToCart called with code:', productCode, 'quantity:', quantityToAdd);
const productToAdd = productLookupMap.get(productCode);
// 1. Validar si el producto existe
if (!productToAdd) {
mostrarMensaje('Producto no encontrado. Asegúrate de que el código sea exacto.', 'danger');
return;
}
// 2. Manejar productos por peso (redirigir al modal específico)
if (productToAdd.UnitType === 'kg') {
mostrarMensaje('Este producto se vende por peso. Abriendo la ventana de peso...', 'info');
// Usar 'true' para enfocar directamente el input de peso, más fluido para escaner/atajos
openWeightEntryModal(productCode, true);
return;
}
// Asegurar que la cantidad a añadir sea válida y positiva
if (isNaN(quantityToAdd) || quantityToAdd <= 0) {
console.warn('Intento de añadir cantidad inválida:', quantityToAdd);
mostrarMensaje('Cantidad a añadir no válida.', 'warning');
return;
}
const precioVenta = typeof productToAdd.PVenta === 'number' ? productToAdd.PVenta : parseFloat(productToAdd.PVenta) || 0;
const existenciaActual = typeof productToAdd.Existencia === 'number' ? productToAdd.Existencia : parseFloat(productToAdd.Existencia) || 0;
const existingCartItemIndex = cart.findIndex(item => item.Cdigo === productCode);
let currentItemQuantityInCart = 0;
if (existingCartItemIndex !== -1) {
currentItemQuantityInCart = cart[existingCartItemIndex].quantity;
}
const newQuantityInCart = currentItemQuantityInCart + quantityToAdd;
// 3. **Consolidar la validación y mensajes de stock**
// Esto evita múltiples llamadas a mostrarMensaje y asegura un único mensaje relevante.
let finalMessage = '';
let messageType = 'success';
if (newQuantityInCart > existenciaActual) {
if (existenciaActual <= 0) {
finalMessage = `⚠️ ¡Alerta de Stock 0! El producto "${String(productToAdd.Producto || productToAdd.Cdigo).trim()}" no tiene existencia disponible.`;
messageType = 'warning';
} else {
finalMessage = `¡Sobregiro! Solo hay ${existenciaActual.toFixed(0)} unidades de "${String(productToAdd.Producto || productToAdd.Cdigo).trim()}" disponibles (ya tienes ${currentItemQuantityInCart.toFixed(0)} en el carrito).`;
messageType = 'warning';
}
} else if (existenciaActual < 10) {
finalMessage = `🔔 "${String(productToAdd.Producto || productToAdd.Cdigo).trim()}" añadido. ¡Stock bajo: ${existenciaActual.toFixed(0)} unidades restantes!`;
messageType = 'info';
} else {
// Mensaje estándar si no hay alertas de stock
finalMessage = `"${String(productToAdd.Producto || productToAdd.Cdigo).trim()}" añadido al carrito.`;
messageType = 'success';
}
// 4. Actualizar el array del carrito (añadir nuevo ítem o actualizar existente)
if (existingCartItemIndex !== -1) {
cart[existingCartItemIndex].quantity = newQuantityInCart;
} else {
cart.unshift({
Cdigo: productToAdd.Cdigo,
Producto: productToAdd.Producto,
Departamento: productToAdd.Departamento,
quantity: newQuantityInCart,
precioUnitario: precioVenta,
UnitType: productToAdd.UnitType
});
}
// 5. **Actualizar el stock en `allProducts` (para feedback visual inmediato)**
// Esta es la parte que decrementa el stock de tu inventario en memoria.
// Como hemos hablado, si deseas una gestión de inventario estricta solo al finalizar la venta,
// esta sección debería ir en la función `finalizeSale`.
// Si la mantienes aquí, se verá reflejado inmediatamente en el catálogo.
const productInAllProductsIndex = allProducts.findIndex(p => p.Cdigo === productCode);
if (productInAllProductsIndex !== -1) {
allProducts[productInAllProductsIndex].Existencia -= quantityToAdd;
if (allProducts[productInAllProductsIndex].Existencia < 0) {
allProducts[productInAllProductsIndex].Existencia = 0; // Evitar stock negativo en la interfaz
}
}
// Mostrar el mensaje consolidado al final
mostrarMensaje(finalMessage, messageType);
// 6. **Actualizaciones de la Interfaz de Usuario (UI)**
// Estas son las llamadas más críticas para la fluidez.
// Es vital que estas funciones (especialmente `updateCartDisplay` y `handleCatalogFilterInput`)
// estén optimizadas para minimizar el redibujado del DOM.
updateCartDisplay();
handleCatalogFilterInput(); // Re-renderiza el catálogo para reflejar cambios de stock
generateStockReports(); // Asegura que los reportes de stock también se actualicen
renderGrammageProductsList(); // Actualiza la lista de productos por peso si aplica
}
/**
* Updates the quantity of a product in the cart.
* @param {string} productCode - The product code.
* @param {number} change - The quantity to add or subtract (e.g., 1 or -1).
*/
function updateCartQuantity(productCode, change) {
console.log('updateCartQuantity called for:', productCode, 'change:', change);
// Find the item in the cart and the product in the lookup map
const itemIndex = cart.findIndex(item => item.Cdigo === productCode);
const productInAllProducts = productLookupMap.get(productCode);
// If product not found in allProducts, we can't update stock accurately.
// Log an error and exit, or handle as appropriate for your application.
if (!productInAllProducts) {
console.error(`Product with code ${productCode} not found in productLookupMap.`);
return; // Exit if product data is missing
}
// Ensure Existencia is a number
let existenciaActual = typeof productInAllProducts.Existencia === 'number'
? productInAllProducts.Existencia
: parseFloat(productInAllProducts.Existencia) || 0;
let oldQuantity = 0; // Initialize oldQuantity for new items
let newQuantity;
if (itemIndex > -1) {
const item = cart[itemIndex];
oldQuantity = item.quantity;
newQuantity = oldQuantity + change;
// Prevent quantity from going below 0, especially for manual changes
if (newQuantity < 0) {
newQuantity = 0;
}
// Calculate the proposed stock after this change
const stockAfterChange = existenciaActual + (oldQuantity - newQuantity);
// *** MODIFICACIÓN CLAVE AQUÍ: Permite la adición, pero alerta si el stock es negativo ***
if (change > 0 && stockAfterChange < 0) {
mostrarMensaje(`¡Atención! Has añadido "${item.Nombre}" y el stock disponible es: ${existenciaActual}. La venta se realizará sin inventario físico.`, 'warning');
// No retornamos, la operación continúa y el stock se volverá negativo.
}
// Apply the changes to stock and quantity
if (newQuantity <= 0) {
// Item fully removed
removeFromCart(productCode); // This function should also update UI/DOM
// Add back the old quantity to stock
productInAllProducts.Existencia = existenciaActual + oldQuantity;
} else {
// Update quantity
item.quantity = newQuantity;
// Update stock based on the calculated stockAfterChange
productInAllProducts.Existencia = stockAfterChange;
// No forzamos que el stock sea 0 aquí si se desea permitir stock negativo.
// Si realmente quieres que el stock no baje de 0, vuelve a añadir:
// if (productInAllProducts.Existencia < 0) { productInAllProducts.Existencia = 0; }
// Pero el requisito es permitir la venta sin stock, lo que implica stock negativo.
}
} else if (change > 0) {
// If the item isn't in the cart but we're trying to add (change > 0)
// This scenario typically handled by addProductToCart, but if this function
// is also used for initial adds, we need to handle it.
// Assuming 'change' here is typically 1 for an initial add.
newQuantity = change; // Starting quantity for a new item is 'change' (e.g., 1)
// Calculate proposed stock for a new item being added
const stockAfterChange = existenciaActual - newQuantity;
// *** MODIFICACIÓN CLAVE AQUÍ PARA NUEVOS PRODUCTOS: Alerta si el stock es negativo ***
if (stockAfterChange < 0) {
mostrarMensaje(`¡Atención! Estás añadiendo "${productInAllProducts.Nombre}" y el stock disponible es: ${existenciaActual}. La venta se realizará sin inventario físico.`, 'warning');
}
// Add the new item to the cart
cart.push({
Cdigo: productCode,
Nombre: productInAllProducts.Nombre, // Assuming productInAllProducts has Nombre
Precio: productInAllProducts.Precio, // Assuming productInAllProducts has Precio
quantity: newQuantity
});
// Update stock based on the calculated stockAfterChange
productInAllProducts.Existencia = stockAfterChange;
} else {
// If item not found and change is negative (e.g., trying to remove a non-existent item)
console.warn(`Attempted to remove non-existent item ${productCode} from cart.`);
return;
}
// --- Single UI Update Point ---
// Only update the display and storage once per function call.
updateCartDisplay(); // Re-render the cart display
localStorage.setItem(PRODUCTS_STORAGE_KEY, JSON.stringify(allProducts)); // Save updated stock
handleCatalogFilterInput(); // Re-render catalog to show stock changes (if necessary)
}
/**
* Handles cell editing in stock tables.
* @param {Event} event - The blur or keydown event.
*/
function handleStockCellEdit(event) {
console.log('handleStockCellEdit called');
const cell = event.target;
const row = cell.closest('tr');
const productCode = row.dataset.code;
const field = cell.dataset.field;
let newValue = cell.textContent.trim();
const productIndex = allProducts.findIndex(p => p.Cdigo === productCode);
if (productIndex === -1) {
mostrarMensaje('Error: Producto no encontrado para actualizar.', 'danger');
return;
}
if (field === 'Existencia') {
newValue = parseFloat(newValue);
if (isNaN(newValue) || newValue < 0) {
mostrarMensaje('Error: La existencia debe ser un número positivo.', 'danger');
cell.textContent = allProducts[productIndex].Existencia.toFixed(allProducts[productIndex].UnitType === 'kg' ? 3 : 0);
return;
}
}
allProducts[productIndex][field] = newValue;
localStorage.setItem(PRODUCTS_STORAGE_KEY, JSON.stringify(allProducts));
buildProductLookupMap();
mostrarMensaje(`Producto "${allProducts[productIndex].Producto}" actualizado: ${field} a ${newValue}.`, 'success');
generateStockReports();
handleCatalogFilterInput();
renderProductManagementList(allProducts);
renderGrammageProductsList();
}
function renderProductManagementList(productsToDisplay) {
console.log('renderProductManagementList called with', productsToDisplay.length, 'products.');
const productManagementListDiv = document.getElementById('productManagementList');
let html = '';
if (productsToDisplay.length === 0) {
productManagementListDiv.innerHTML = '<p class="text-center text-muted">No se encontraron productos.</p>';
return;
}
productsToDisplay.forEach(producto => {
const nombreProducto = String(producto.Producto || 'Producto Desconocido').trim();
const precioVenta = typeof producto.PVenta === 'number' ? producto.PVenta : parseFloat(producto.PVenta) || 0;
const existencia = typeof producto.Existencia === 'number' ? producto.Existencia : parseFloat(producto.Existencia) || 0;
const departamento = String(producto.Departamento || 'N/A').trim();
const unitType = producto.UnitType || 'unit';
html += `
<div class="product-management-item" data-code="${producto.Cdigo}">
<div class="product-info">
<div class="product-name">
<input type="text" value="${nombreProducto}" class="form-control-plaintext product-name-input" data-field="Producto" readonly>
</div>
<div class="product-code text-muted">
Cód: <input type="text" value="${producto.Cdigo || 'N/A'}" class="form-control-plaintext product-code-input" data-field="Cdigo" readonly>
</div>
<div class="product-department text-muted">
Depto: <input type="text" value="${departamento}" class="form-control-plaintext product-department-input" data-field="Departamento" readonly>
</div>
<div class="product-price">
Precio: ${currentCurrencySymbol}<input type="number" value="${precioVenta.toFixed(2)}" step="0.01" class="form-control-plaintext product-price-input" data-field="PVenta" readonly>
</div>
<div class="product-stock">
Exist: <input type="number" value="${existencia.toFixed(unitType === 'kg' ? 3 : 0)}" step="${unitType === 'kg' ? '0.001' : '1'}" class="form-control-plaintext product-stock-input ${existencia <= 0 ? 'text-danger font-weight-bold' : (existencia < 10 ? 'text-warning' : '')}" data-field="Existencia" readonly>
</div>
<div class="product-unit-type">
Unidad:
<select class="form-control-plaintext product-unit-type-select" data-field="UnitType" disabled>
<option value="unit" ${unitType === 'unit' ? 'selected' : ''}>Unidad</option>
<option value="kg" ${unitType === 'kg' ? 'selected' : ''}>Kilogramo (kg)</option>
</select>
</div>
</div>
<div class="product-actions">
<button class="btn btn-sm btn-info edit-product-btn" data-code="${producto.Cdigo}">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-success save-product-changes-btn d-none" data-code="${producto.Cdigo}">
<i class="fas fa-check"></i>
</button>
<button class="btn btn-sm btn-danger delete-product-btn" data-code="${producto.Cdigo}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
});
productManagementListDiv.innerHTML = html;
// Remove existing listeners before adding new ones to prevent duplicates
document.querySelectorAll('.edit-product-btn').forEach(button => {
const newButton = button.cloneNode(true);
button.parentNode.replaceChild(newButton, button);
});
document.querySelectorAll('.save-product-changes-btn').forEach(button => {
const newButton = button.cloneNode(true);
button.parentNode.replaceChild(newButton, button);
});
document.querySelectorAll('.delete-product-btn').forEach(button => {
const newButton = button.cloneNode(true);
button.parentNode.replaceChild(newButton, button);
});
document.querySelectorAll('.edit-product-btn').forEach(button => {
button.addEventListener('click', (e) => {
console.log('Edit product button clicked for code:', e.currentTarget.dataset.code);
editProductInList(e.currentTarget.dataset.code);
});
});
document.querySelectorAll('.save-product-changes-btn').forEach(button => {
button.addEventListener('click', (e) => {
console.log('Save product changes button clicked for code:', e.currentTarget.dataset.code);
saveProductChangesInList(e.currentTarget.dataset.code);
});
});
document.querySelectorAll('.delete-product-btn').forEach(button => {
button.addEventListener('click', async (e) => {
console.log('Delete product button clicked for code:', e.currentTarget.dataset.code);
const productCode = e.currentTarget.dataset.code;
const confirmed = await showCustomConfirm(`¿Estás seguro de que quieres eliminar el producto con código "${productCode}"? Esta acción no se puede deshacer.`);
if (confirmed) {
deleteProduct(productCode);
}
});
});
}
// --- Functions for Grammage Products List ---
function renderGrammageProductsList(filteredProducts = null) {
console.log('renderGrammageProductsList called');
const grammageProductListDiv = document.getElementById('grammageProductList');
if (!isPremiumUser || !currentSettings.showGrammageProductsPage) {
grammageProductListDiv.innerHTML = '<p class="text-center text-muted">La función de "Productos por Peso" es una característica Premium. ¡Actualiza tu suscripción!</p>';
return;
}
const kgProducts = allProducts.filter(p => p.UnitType === 'kg');
let productsToDisplay = filteredProducts || kgProducts;
let html = '<table class="report-table"><thead><tr><th>Código</th><th>Producto</th><th>Departamento</th><th>Precio/kg</th><th>Existencia (kg)</th></tr></thead><tbody>';
if (productsToDisplay.length === 0) {
html += '<tr><td colspan="5" class="text-center text-muted">No hay productos configurados para venta por peso que coincidan con la búsqueda.</td></tr>';
} else {
productsToDisplay.forEach(p => {
const existenciaClass = p.Existencia <= 0 ? 'text-danger font-weight-bold' : (p.Existencia < 10 ? 'text-warning' : '');
html += `
<tr data-code="${p.Cdigo}">
<td>${p.Cdigo || 'N/A'}</td>
<td>${String(p.Producto || 'Producto Desconocido').trim()}</td>
<td>${p.Departamento || 'N/A'}</td>
<td>${currentCurrencySymbol}${p.PVenta.toFixed(2)}</td>
<td class="${existenciaClass}">${p.Existencia.toFixed(3)}</td>
</tr>
`;
});
}
html += '</tbody></table>';
grammageProductListDiv.innerHTML = html;
}
function filterGrammageProductsTable(searchTerm) {
console.log('filterGrammageProductsTable called with term:', searchTerm);
const normalizedSearchTerm = normalizeString(searchTerm);
const kgProducts = allProducts.filter(p => p.UnitType === 'kg');
if (normalizedSearchTerm) {
const filtered = kgProducts.filter(p => {
const nombreProducto = normalizeString(p.Producto);
const codigoProducto = normalizeString(p.Cdigo);
const departamentoProducto = normalizeString(p.Departamento);
return nombreProducto.includes(normalizedSearchTerm) ||
codigoProducto.includes(normalizedSearchTerm) ||
departamentoProducto.includes(normalizedSearchTerm);
});
renderGrammageProductsList(filtered);
} else {
renderGrammageProductsList(kgProducts);
}
}
function downloadExpensesReport(format = 'csv') {
console.log('downloadExpensesReport called, format:', format);
if (expenses.length === 0) {
mostrarMensaje('No hay gastos para descargar.', 'warning');
return;
}
const dataToDownload = expenses.map(e => ({
"Fecha": e.date,
"Descripción": e.description,
"Categoría": e.category,
"Monto": `${e.currency === 'USD' ? 'US$' : '$'}${parseFloat(e.amount).toFixed(2)}`,
"Moneda": e.currency
}));
if (format === 'csv') {
downloadCSV(dataToDownload, 'reporte_gastos.csv');
} else if (format === 'xlsx') {
downloadXLSX(dataToDownload, 'reporte_gastos.xlsx');
}
}
function renderSettingsPage() {
console.log('renderSettingsPage called');
document.getElementById('toggleProductCatalog').checked = currentSettings.showProductCatalog;
document.getElementById('toggleReportsSection').checked = currentSettings.showReportsSection;
document.getElementById('toggleManagementSection').checked = currentSettings.showManagementSection;
document.getElementById('toggleConfigSection').checked = currentSettings.showConfigSection;
document.getElementById('toggleSubscriptionPage').checked = currentSettings.showSubscriptionPage;
document.getElementById('toggleUserManagementPage').checked = currentSettings.showUserManagementPage;
const toggleGrammageProductsPage = document.getElementById('toggleGrammageProductsPage');
if (toggleGrammageProductsPage) {
toggleGrammageProductsPage.checked = currentSettings.showGrammageProductsPage;
toggleGrammageProductsPage.disabled = !isPremiumUser; // Disable if not premium
}
const toggleCurrencyManagement = document.getElementById('toggleCurrencyManagement');
if (toggleCurrencyManagement) {
toggleCurrencyManagement.checked = currentSettings.enableCurrencyManagement;
toggleCurrencyManagement.disabled = !isPremiumUser; // Disable if not premium
}
const toggleAdvancedAnalytics = document.getElementById('toggleAdvancedAnalytics');
if (toggleAdvancedAnalytics) {
toggleAdvancedAnalytics.checked = currentSettings.showAnalyticsPage;
toggleAdvancedAnalytics.disabled = !isPremiumUser; // Disable if not premium
}
const toggleOrdersPage = document.getElementById('toggleOrdersPage');
if (toggleOrdersPage) {
toggleOrdersPage.checked = currentSettings.showOrdersPage;
toggleOrdersPage.disabled = !isPremiumUser; // Disable if not premium
}
// Disable currency options if currency management is not enabled or not premium
document.getElementById('currencyMXN').disabled = !(currentSettings.enableCurrencyManagement && isPremiumUser);
document.getElementById('currencyUSD').disabled = !(currentSettings.enableCurrencyManagement && isPremiumUser);
document.getElementById('exchangeRateInput').disabled = !(currentSettings.enableCurrencyManagement && isPremiumUser);
document.getElementById('currencyMXN').checked = currentSettings.currency === 'MXN';
document.getElementById('currencyUSD').checked = currentSettings.currency === 'USD';
document.getElementById('exchangeRateInput').value = currentSettings.exchangeRateUSDToMXN.toFixed(2);
}
const style = getComputedStyle(document.body);
const varOfficePrimaryBlue = style.getPropertyValue('--office-primary-blue').trim();
function copyToClipboard(text) {
console.log('copyToClipboard called');
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
mostrarMensaje('Copiado al portapapeles.', 'success');
} catch (err) {
console.error('Error al copiar al portapapeles:', err);
mostrarMensaje('Error al copiar al portapapeles. Intenta copiar manualmente.', 'danger');
} finally {
document.body.removeChild(textarea);
}
}
/**
* Opens the modal for weight-based product entry.
* @param {string} [initialProductCode=''] - Optional initial product code to pre-fill.
* @param {boolean} [focusWeightInputDirectly=false] - If true, focuses directly on the weight input.
*/
function openWeightEntryModal(initialProductCode = '', focusWeightInputDirectly = false) {
console.log('openWeightEntryModal called with code:', initialProductCode, 'focusWeight:', focusWeightInputDirectly);
if (!isPremiumUser || !currentSettings.showGrammageProductsPage) {
mostrarMensaje('La función de "Productos por Gramaje" es una característica Premium. ¡Actualiza tu suscripción!', 'warning');
return;
}
const weightEntryModal = document.getElementById('weightEntryModal');
const weightProductCodeInput = document.getElementById('weightProductCode');
const weightInput = document.getElementById('weightInput');
const selectedWeightedProductInfo = document.getElementById('selectedWeightedProductInfo');
const calculatedWeightedPrice = document.getElementById('calculatedWeightedPrice');
const addWeightedProductBtn = document.getElementById('addWeightedProductBtn');
const weightProductSearchResults = document.getElementById('weightProductSearchResults');
weightProductCodeInput.value = initialProductCode;
weightInput.value = '0.000';
selectedWeightedProductInfo.textContent = '';
calculatedWeightedPrice.textContent = '';
weightProductSearchResults.innerHTML = ''; // Clear previous search results
weightEntryModal.classList.add('show');
updateWeightedProductInfo(); // Update info based on initial product code
if (focusWeightInputDirectly) {
const product = productLookupMap.get(initialProductCode);
if (product && product.UnitType === 'kg') {
weightInput.focus();
weightInput.select();
} else {
weightProductCodeInput.focus();
weightProductCodeInput.select();
if (!product) mostrarMensaje('Producto no encontrado con el código proporcionado.', 'warning');
else mostrarMensaje(`"${product.Producto}" no es un producto por peso.`, 'warning');
}
} else {
weightProductCodeInput.focus();
weightProductCodeInput.select();
}
}
/**
* Closes the modal for weight-based product entry.
*/
function closeWeightEntryModal() {
console.log('closeWeightEntryModal called');
document.getElementById('weightEntryModal').classList.remove('show');
document.getElementById('product-search').focus(); // Return focus to main scanner input
}
/**
* Displays search results for weighted products in the modal.
* @param {Array<Object>} results - Array of product objects to display.
*/
function displayWeightedSearchResults(results) {
const weightProductSearchResults = document.getElementById('weightProductSearchResults');
let html = '';
if (results.length > 0) {
html += '<ul class="list-group mt-2">';
results.forEach(p => {
html += `<li class="list-group-item list-group-item-action" data-code="${p.Cdigo}">${p.Producto} (${p.Cdigo}) - ${currentCurrencySymbol}${p.PVenta.toFixed(2)}/kg</li>`;
});
html += '</ul>';
} else {
html += '<p class="text-muted text-center mt-2">No se encontraron productos por peso.</p>';
}
weightProductSearchResults.innerHTML = html;
}
/**
* Updates product info and calculated price in the weight entry modal.
*/
function updateWeightedProductInfo() {
console.log('updateWeightedProductInfo called');
const productCodeInput = document.getElementById('weightProductCode');
const productCode = productCodeInput.value.trim();
const weight = parseFloat(document.getElementById('weightInput').value);
const selectedWeightedProductInfo = document.getElementById('selectedWeightedProductInfo');
const calculatedWeightedPrice = document.getElementById('calculatedWeightedPrice');
const addWeightedProductBtn = document.getElementById('addWeightedProductBtn');
const weightProductSearchResults = document.getElementById('weightProductSearchResults');
selectedWeightedProductInfo.textContent = '';
calculatedWeightedPrice.textContent = '';
addWeightedProductBtn.disabled = true;
weightProductSearchResults.innerHTML = ''; // Clear previous search results
if (productCode) {
const product = productLookupMap.get(productCode);
if (product) {
if (product.UnitType !== 'kg') {
selectedWeightedProductInfo.textContent = `Error: "${product.Producto}" no es un producto por peso.`;
calculatedWeightedPrice.textContent = '';
return;
}
selectedWeightedProductInfo.textContent = `Producto: ${product.Producto} (Precio/kg: ${currentCurrencySymbol}${product.PVenta.toFixed(2)})`;
if (!isNaN(weight) && weight > 0) {
const totalPrice = product.PVenta * weight;
calculatedWeightedPrice.textContent = `Precio Total: ${currentCurrencySymbol}${totalPrice.toFixed(2)}`;
addWeightedProductBtn.disabled = false;
} else {
calculatedWeightedPrice.textContent = 'Ingrese un peso válido (Ej: 0.5 para 500g).';
}
} else {
// If product not found by exact code, search for it
const normalizedSearchTerm = normalizeString(productCode);
const results = allProducts.filter(p =>
p.UnitType === 'kg' && // Only show kg products
(normalizeString(p.Cdigo).includes(normalizedSearchTerm) || normalizeString(p.Producto).includes(normalizedSearchTerm))
);
displayWeightedSearchResults(results);
selectedWeightedProductInfo.textContent = 'Producto no encontrado. Selecciona de la lista o verifica el código.';
}
}
}
/**
* Adds the weighted product to the cart.
*/
function addWeightedProductToCart() {
console.log('addWeightedProductToCart called');
const productCode = document.getElementById('weightProductCode').value.trim();
const weight = parseFloat(document.getElementById('weightInput').value);
if (!productCode || isNaN(weight) || weight <= 0) {
mostrarMensaje('Por favor, ingresa un código de producto y un peso válido.', 'danger');
return;
}
const productToAdd = productLookupMap.get(productCode);
if (!productToAdd) {
mostrarMensaje('Producto no encontrado.', 'danger');
return;
}
if (productToAdd.UnitType !== 'kg') {
mostrarMensaje(`El producto "${productToAdd.Producto}" no está configurado para venta por peso.`, 'warning');
return;
}
const totalPrice = productToAdd.PVenta * weight;
const existenciaActual = typeof productToAdd.Existencia === 'number' ? productToAdd.Existencia : parseFloat(productToAdd.Existencia) || 0;
if (existenciaActual < weight) {
mostrarMensaje(`No hay suficiente existencia de "${productToAdd.Producto}". Disponible: ${existenciaActual.toFixed(3)} kg.`, 'danger');
return;
}
const existingCartItemIndex = cart.findIndex(item => item.Cdigo === productCode);
if (existingCartItemIndex !== -1) {
const existingItem = cart[existingCartItemIndex];
existingItem.quantity += weight;
mostrarMensaje(`"${String(productToAdd.Producto || productToAdd.Cdigo).trim()}" cantidad aumentada por ${weight.toFixed(3)} kg.`, 'info');
} else {
// Insert at the beginning of the cart array for most recent on top
cart.unshift({
Cdigo: productToAdd.Cdigo,
Producto: productToAdd.Producto,
Departamento: productToAdd.Departamento,
quantity: weight,
precioUnitario: productToAdd.PVenta,
UnitType: 'kg'
});
mostrarMensaje(`"${String(productToAdd.Producto || productToAdd.Cdigo).trim()}" (${weight.toFixed(3)} kg) añadido al carrito.`, 'success');
}
// Update stock
const productInAllProductsIndex = allProducts.findIndex(p => p.Cdigo === productCode);
if (productInAllProductsIndex !== -1) {
allProducts[productInAllProductsIndex].Existencia -= weight;
if (allProducts[productInAllProductsIndex].Existencia < 0) {
allProducts[productInAllProductsIndex].Existencia = 0;
}
}
updateCartDisplay();
localStorage.setItem(PRODUCTS_STORAGE_KEY, JSON.stringify(allProducts)); // Save updated stock
handleCatalogFilterInput(); // Re-render products to reflect updated stock in catalog
closeWeightEntryModal();
}
</script>
</body>
</html>
<?php
// Flush the output buffer and send content to the browser
ob_end_flush();
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment