- Gastos (Expenses): Added `/gastos` route, auto-creation of `expenses` DB table, and `gastos.html` to track net profit with split month/year dropdowns. - Sales & Filters: Overhauled `/sales` backend to use pagination. Top summary cards now accurately reflect the selected payment method filter. - Checkout Improvements: - Added "Transferencia" as a payment option with numpad shortcuts. - Built a "Pinned Products" quick-access grid using localStorage. - Implemented a global processing lock to prevent duplicate sales on double-clicks. - Burned the default HTML number arrows with custom CSS. - Global Settings & Receipts: - Created a global settings modal accessible from the navbar. - Added localStorage toggles for custom business name and auto-print. - Added "Restaurant Mode" toggle to prompt for Client Name and Pickup Time, which now dynamically prints on the receipt. - Bug Fixes: Resolved Jinja `TemplateSyntaxError` crash and removed the duplicate search bar in the checkout view.
1255 lines
57 KiB
HTML
1255 lines
57 KiB
HTML
{% extends "macros/base.html" %}
|
|
{% from 'macros/modals.html' import confirm_modal, scanner_modal, render_receipt %}
|
|
{% block title %}Caja{% endblock %}
|
|
|
|
{% block head %}
|
|
<style>
|
|
input::-webkit-outer-spin-button,
|
|
input::-webkit-inner-spin-button {
|
|
-webkit-appearance: none;
|
|
margin: 0;
|
|
}
|
|
input[type="number"] {
|
|
-moz-appearance: textfield;
|
|
appearance: textfield;
|
|
}
|
|
.crt-cursor {
|
|
animation: blink-step 1s step-end infinite;
|
|
border-bottom: 3px solid #ed4245;
|
|
display: inline-block;
|
|
width: 14px;
|
|
height: 1.2em;
|
|
vertical-align: bottom;
|
|
margin-left: 2px;
|
|
}
|
|
@keyframes blink-step { 50% { opacity: 0; } }
|
|
.boot-btn {
|
|
background: transparent;
|
|
transition: all 0.1s;
|
|
}
|
|
.boot-btn:hover {
|
|
background: #ed4245 !important;
|
|
color: #000 !important;
|
|
text-shadow: none !important;
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{{ render_receipt() }}
|
|
|
|
{% call confirm_modal('removeConfirmModal', 'Quitar Producto', 'btn-danger-discord', 'Quitar', 'executeRemoveItem()') %}
|
|
¿Estás seguro de que quieres quitar <strong id="removeItemName"></strong> del carrito?
|
|
{% endcall %}
|
|
|
|
{% call confirm_modal('clearCartModal', 'Vaciar Carrito', 'btn-danger-discord', 'Sí, vaciar', 'executeClearCart()') %}
|
|
<div class="text-center">
|
|
<i class="bi bi-cart-x text-danger" style="font-size: 3rem;"></i>
|
|
<p class="mt-3">¿Seguro que quieres eliminar todos los productos del carrito?</p>
|
|
</div>
|
|
{% endcall %}
|
|
|
|
<div class="modal fade" id="customProductModal" tabindex="-1" data-bs-backdrop="static">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Agregar Producto Manual</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label text-muted small mb-1">Descripción</label>
|
|
<input type="text" id="custom-name" class="form-control" placeholder="Ej: Varios, Bolsa, etc."
|
|
onkeydown="if(event.key === 'Enter') document.getElementById('custom-price').focus()">
|
|
</div>
|
|
<div class="row g-2">
|
|
<div class="col-8">
|
|
<label class="form-label text-muted small mb-1">Precio Unitario</label>
|
|
<input type="number" id="custom-price" class="form-control" placeholder="Ej: 1500"
|
|
onkeydown="if(event.key === 'Enter') addCustomProduct()">
|
|
</div>
|
|
<div class="col-4">
|
|
<label class="form-label text-muted small mb-1">Tipo</label>
|
|
<select id="custom-unit" class="form-select">
|
|
<option value="unit">Unidad</option>
|
|
<option value="kg">Kg</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer d-flex">
|
|
<button class="btn btn-secondary flex-grow-1" data-bs-dismiss="modal">Cancelar</button>
|
|
<button class="btn btn-accent flex-grow-1" onclick="addCustomProduct()">Agregar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal" id="variosModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header border-0 pb-0">
|
|
<h5 class="modal-title w-100 text-center text-muted text-uppercase" style="font-size: 0.8rem;">
|
|
Producto Varios
|
|
</h5>
|
|
<button type="button" class="btn-close position-absolute end-0 top-0 m-3" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body text-center pt-1 pb-4">
|
|
<div class="mb-3 text-start">
|
|
<label class="text-muted small mb-1">Precio (CLP)</label>
|
|
<input type="text" inputmode="numeric" id="varios-price-input" class="form-control form-control-lg text-center fw-bold fs-4"
|
|
placeholder="$0"
|
|
oninput="let v = this.value.replace(/\D/g, ''); this.value = v ? parseInt(v, 10).toLocaleString('es-CL') : '';"
|
|
onkeydown="if(event.key === 'Enter') addVariosToCart()">
|
|
</div>
|
|
<button class="btn btn-warning w-100 py-3 fw-bold" onclick="addVariosToCart()">
|
|
<i class="bi bi-cart-plus me-1"></i> Agregar al Carrito
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="weightModal" tabindex="-1" data-bs-backdrop="static">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5>Ingresar Peso (Gramos)</h5>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="number" id="weight-input" class="form-control form-control-lg" step="1"
|
|
placeholder="Ej: 500" onkeydown="if(event.key === 'Enter') confirmWeight()">
|
|
</div>
|
|
<div class="modal-footer d-flex">
|
|
<button class="btn btn-secondary flex-grow-1" data-bs-dismiss="modal">Cancelar</button>
|
|
<button class="btn btn-accent flex-grow-1" onclick="confirmWeight()">Agregar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="orderDetailsModal" tabindex="-1" data-bs-backdrop="static">
|
|
<div class="modal-dialog modal-dialog-centered modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header border-0 pb-0">
|
|
<h5 class="modal-title w-100 text-center text-muted text-uppercase" style="font-size: 0.8rem">
|
|
Detalles del Pedido
|
|
</h5>
|
|
<button type="button" class="btn-close position-absolute end-0 top-0 m-3" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body pt-2 pb-4">
|
|
<div class="mb-3">
|
|
<label class="form-label text-muted small mb-1">Nombre del Cliente</label>
|
|
<input type="text" id="order-client-name" class="form-control" placeholder="Ej: Juan Pérez" autocomplete="off" onkeydown="if(event.key === 'Enter') document.getElementById('order-pickup-time').focus()">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label text-muted small mb-1">Hora de Retiro (Opcional)</label>
|
|
<input type="time" id="order-pickup-time" class="form-control" onkeydown="if(event.key === 'Enter') document.getElementById('order-notes').focus()">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="form-label text-muted small mb-1">Detalles / Notas</label>
|
|
<input type="text" id="order-notes" class="form-control" placeholder="Ej: Para llevar, Sin cebolla" autocomplete="off" onkeydown="if(event.key === 'Enter') confirmOrderDetails()">
|
|
</div>
|
|
<button class="btn btn-primary w-100 py-2 fw-bold" onclick="confirmOrderDetails()">
|
|
Continuar al Pago <i class="bi bi-arrow-right ms-1"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="paymentModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header border-0 pb-0">
|
|
<h5 class="modal-title w-100 text-center text-muted text-uppercase" style="font-size: 0.8rem">
|
|
Total a Pagar
|
|
</h5>
|
|
<button type="button" class="btn-close position-absolute end-0 top-0 m-3" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body text-center pt-1 pb-4">
|
|
<h1 id="payment-modal-total" class="mb-4" style="color: var(--accent); font-weight: 800; font-size: 3rem">$0</h1>
|
|
<div class="d-grid gap-3 px-3">
|
|
<button class="btn btn-lg btn-success py-3" onclick="openVueltoModal()">
|
|
<i class="bi bi-cash-coin me-2" style="font-size: 1.5rem; vertical-align: middle"></i> Efectivo (1)
|
|
</button>
|
|
<button class="btn btn-lg btn-secondary py-3" onclick="executeCheckout('tarjeta')">
|
|
<i class="bi bi-credit-card me-2" style="font-size: 1.5rem; vertical-align: middle"></i> Tarjeta (2)
|
|
</button>
|
|
<button class="btn btn-lg btn-info py-3 text-white" onclick="executeCheckout('transferencia')">
|
|
<i class="bi bi-bank me-2" style="font-size: 1.5rem; vertical-align: middle"></i> Transferencia (3)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal" id="vueltoModal" tabindex="-1" data-bs-backdrop="static">
|
|
<div class="modal-dialog modal-dialog-centered modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header border-0 pb-0">
|
|
<h5 class="modal-title w-100 text-center text-muted text-uppercase" style="font-size: 0.8rem">Pago en Efectivo</h5>
|
|
<button type="button" class="btn-close position-absolute end-0 top-0 m-3" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body text-center pt-1 pb-4">
|
|
<div class="mb-3">
|
|
<span class="text-muted small">Total a Pagar:</span><br>
|
|
<span id="vuelto-total-display" class="fs-4 fw-bold" style="color: var(--text-main)">$0</span>
|
|
</div>
|
|
<div class="mb-3 text-start">
|
|
<label class="text-muted small mb-1">Monto Recibido</label>
|
|
<input type="text" inputmode="numeric" id="monto-recibido" class="form-control form-control-lg text-center fw-bold fs-4"
|
|
placeholder="$0"
|
|
oninput="let v = this.value.replace(/\D/g, ''); this.value = v ? parseInt(v, 10).toLocaleString('es-CL') : ''; calculateVuelto();"
|
|
onkeydown="if(event.key === 'Enter' && !document.getElementById('btn-confirm-vuelto').disabled) executeCheckout('efectivo')">
|
|
</div>
|
|
<div class="d-flex flex-wrap justify-content-center gap-2 mb-3" id="vuelto-quick-buttons"></div>
|
|
<div class="p-3 mb-3" style="background: var(--input-bg); border-radius: 8px">
|
|
<span class="text-muted small text-uppercase fw-bold">Vuelto a Entregar</span><br>
|
|
<span id="vuelto-amount" class="fs-1 fw-bold text-muted">$0</span>
|
|
</div>
|
|
<button id="btn-confirm-vuelto" class="btn btn-success w-100 py-3 fw-bold" onclick="executeCheckout('efectivo')" disabled>Confirmar Venta</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal" id="quickSaleModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header border-0 pb-0">
|
|
<h5 class="modal-title w-100 text-center text-muted text-uppercase" style="font-size: 0.8rem;">Venta Rápida</h5>
|
|
<button type="button" class="btn-close position-absolute end-0 top-0 m-3" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body text-center pt-1 pb-4">
|
|
<div class="mb-3 text-start">
|
|
<label class="text-muted small mb-1">Monto (CLP)</label>
|
|
<input type="text" inputmode="numeric" id="quick-sale-amount" class="form-control form-control-lg text-center fw-bold fs-4"
|
|
placeholder="$0"
|
|
oninput="let v = this.value.replace(/\D/g, ''); this.value = v ? parseInt(v, 10).toLocaleString('es-CL') : '';"
|
|
onkeydown="if(event.key === 'Enter') processQuickSale()">
|
|
</div>
|
|
<button class="btn btn-primary w-100 py-3 fw-bold" onclick="processQuickSale()">
|
|
<i class="bi bi-lightning-charge me-1"></i> Finalizar Venta
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="notFoundModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content border-warning">
|
|
<div class="modal-header border-0 pb-0">
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body text-center pt-0 pb-4">
|
|
<i class="bi bi-question-circle text-warning mb-3" style="font-size: 3rem"></i>
|
|
<h4 class="mb-2">Producto No Registrado</h4>
|
|
<p class="text-muted px-3 small">El código <strong id="not-found-barcode" style="color: var(--text-main);"></strong> no existe.</p>
|
|
<div class="d-flex flex-column gap-2 px-3 mt-4">
|
|
<button class="btn btn-accent w-100 py-2" onclick="goToInventory()">
|
|
<i class="bi bi-database-add me-2"></i>Registrar en Inventario
|
|
</button>
|
|
<button class="btn btn-outline-secondary w-100 py-2" onclick="openTempProduct()">
|
|
<i class="bi bi-cart-plus me-2"></i>Venta Temporal (Solo por esta vez)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="successModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content border-success">
|
|
<div class="modal-body text-center py-4">
|
|
<i class="bi bi-check-circle-fill text-success" style="font-size: 4rem;"></i>
|
|
<h4 class="mt-3">¡Venta Exitosa!</h4>
|
|
<p class="text-muted">El carrito se ha procesado correctamente.</p>
|
|
<button class="btn btn-accent px-5" data-bs-dismiss="modal">Listo</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="doomModal" tabindex="-1" data-bs-backdrop="static">
|
|
<div class="modal-dialog modal-dialog-centered" style="max-width: 90vw;">
|
|
<div class="modal-content border-danger" style="background: #000;">
|
|
<div class="modal-header border-0 pb-0 d-flex justify-content-between">
|
|
<h5 class="modal-title text-danger font-monospace">E1M1: Hangar</h5>
|
|
<div>
|
|
<span class="text-muted small me-3 align-middle">Haz clic en el juego para sonido</span>
|
|
<button class="btn btn-sm btn-outline-danger me-3" onclick="toggleDoomFullscreen()" title="Pantalla Completa">
|
|
<i class="bi bi-arrows-fullscreen"></i>
|
|
</button>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-body text-center p-0 d-flex justify-content-center bg-black position-relative" style="min-height: 80vh;">
|
|
<div id="doom-boot-screen" class="position-absolute w-100 h-100 flex-column align-items-start p-5 d-none" style="z-index: 10; background: #050505; font-family: 'Courier New', monospace; text-transform: uppercase; cursor: pointer;" onclick="startDoom()">
|
|
<div class="text-start" style="color: #4af626; text-shadow: 0 0 4px rgba(74, 246, 38, 0.5);">
|
|
<p class="mb-1">UAC_BIOS v1.9.9.3</p>
|
|
<div id="boot-loading" class="mt-3">
|
|
<span id="boot-status">CARGANDO SISTEMA MS-DOS...</span><span class="crt-cursor"></span>
|
|
</div>
|
|
<div id="boot-ready" class="d-none mt-3">
|
|
<p class="mb-1">HIMEM IS TESTING EXTENDED MEMORY... [OK]</p>
|
|
<p class="mb-1">SND_INIT: SOUNDBLASTER 16 ... [OK]</p>
|
|
<p class="mb-4 text-warning" style="text-shadow: 0 0 4px rgba(255, 193, 7, 0.5);">WARNING: NON-STANDARD SEKIPOS HARDWARE DETECTED.</p>
|
|
<div class="fs-4 text-danger d-flex align-items-center" style="text-shadow: 0 0 5px rgba(237, 66, 69, 0.6);">
|
|
<span>C:\UAC\SEKIPOS> DOOM.EXE</span><span class="crt-cursor"></span>
|
|
</div>
|
|
<p class="mt-5 text-muted small" style="animation: blink-step 2s step-end infinite;">[ HAZ CLIC PARA INICIAR ]</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<canvas id="jsdos" style="width: 100%; height: 80vh; object-fit: contain; image-rendering: pixelated;"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container-fluid">
|
|
<div class="row g-3">
|
|
<div class="col-md-8">
|
|
<div class="discord-card p-3">
|
|
<h4><i class="bi bi-cart3"></i> Carrito</h4>
|
|
|
|
<div class="position-relative mb-4">
|
|
<div class="input-group">
|
|
<span class="input-group-text border-0 position-absolute" style="background: transparent; z-index: 10">
|
|
<i class="bi bi-search text-muted"></i>
|
|
</span>
|
|
<input type="text" id="manual-search" class="form-control ps-5 py-2 rounded"
|
|
placeholder="Buscar producto por nombre o código..." autocomplete="off" onkeyup="filterSearch()">
|
|
|
|
<button class="btn btn-warning px-3 fw-bold" type="button" onclick="openVariosModal()" title="Agregar Varios rápido">
|
|
<i class="bi bi-asterisk"></i> <span class="d-none d-sm-inline ms-1">Varios</span>
|
|
</button>
|
|
|
|
<button class="btn btn-accent px-3" type="button" onclick="openCustomProductModal()" title="Agregar manual detallado">
|
|
<i class="bi bi-plus-lg"></i> <span class="d-none d-sm-inline ms-1">Manual</span>
|
|
</button>
|
|
</div>
|
|
<div id="search-results" class="dropdown-menu w-100 shadow-sm mt-1"
|
|
style="display: none; position: absolute; top: 100%; left: 0; z-index: 1000; max-height: 300px; overflow-y: auto"></div>
|
|
</div>
|
|
|
|
<div id="pinned-products-container" class="d-flex flex-wrap gap-2 mb-3"></div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table mt-3" id="cart-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Código</th>
|
|
<th>Producto</th>
|
|
<th>Precio/U</th>
|
|
<th>Cant/Peso</th>
|
|
<th>Subtotal</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="cart-items"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<div class="discord-card p-3 mb-3 text-center shadow-sm">
|
|
<p class="mb-1 fw-semibold text-uppercase" style="color:var(--text-muted); font-size:0.7rem">Último Escaneado</p>
|
|
<img id="display-img" src="./static/placeholder.png" class="mb-2" alt="product">
|
|
<h6 id="display-name" class="mb-0 text-truncate">Esperando scan...</h6>
|
|
<small id="display-barcode" class="text-muted font-monospace" style="font-size: 0.7rem"></small>
|
|
|
|
<div class="total-banner text-center mb-3 mt-3">
|
|
<h2 class="mb-0">TOTAL</h2>
|
|
<h1 id="grand-total" style="font-size: 3.5rem; font-weight: 800;">$0</h1>
|
|
</div>
|
|
|
|
<button class="btn btn-primary w-100 btn-lg mb-2" onclick="openQuickSaleModal()">
|
|
<i class="bi bi-lightning-charge"></i> VENTA RÁPIDA
|
|
</button>
|
|
<button class="btn btn-success w-100 btn-lg mb-2" onclick="processSale()">
|
|
<i class="bi bi-cash-coin"></i> COBRAR
|
|
</button>
|
|
<button class="btn btn-danger w-100 btn-lg" onclick="clearCart()">
|
|
<i class="bi bi-trash3"></i> VACIAR
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
/* =========================================
|
|
1. GLOBAL STATE & FORMATTERS
|
|
========================================= */
|
|
let cart = [];
|
|
let pendingProduct = null;
|
|
let missingProductData = null;
|
|
let tempBarcode = null;
|
|
let editingCartIndex = null;
|
|
let itemIndexToRemove = null;
|
|
|
|
let currentClientName = '';
|
|
let currentOrderNotes = '';
|
|
let currentPickupTime = '';
|
|
|
|
// The lock to prevent duplicate sales
|
|
let isProcessing = false;
|
|
|
|
// Fetch the pinned items from local storage
|
|
let pinnedBarcodes = JSON.parse(localStorage.getItem('seki_pinned_products')) || [];
|
|
|
|
let socket = io();
|
|
|
|
const clp = new Intl.NumberFormat('es-CL', {
|
|
style: 'currency',
|
|
currency: 'CLP',
|
|
minimumFractionDigits: 0
|
|
});
|
|
|
|
const allProducts = [
|
|
{% for p in products %}
|
|
{
|
|
barcode: {{ p[0] | tojson }},
|
|
name: {{ p[1] | tojson }},
|
|
price: {{ p[2] | int }},
|
|
image: {{ p[3] | tojson }},
|
|
stock: {{ p[4] | int }},
|
|
unit: {{ p[5] | tojson }}
|
|
},
|
|
{% endfor %}
|
|
];
|
|
|
|
/* =========================================
|
|
2. SOCKET LISTENERS (BARCODE / SCALE)
|
|
========================================= */
|
|
socket.on('scan_error', (data) => {
|
|
missingProductData = data;
|
|
document.getElementById('not-found-barcode').innerText = data.barcode;
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('notFoundModal')).show();
|
|
});
|
|
|
|
socket.on('new_scan', (product) => {
|
|
handleProductScan(product);
|
|
});
|
|
|
|
socket.on('scale_update', function (data) {
|
|
const weightModal = document.getElementById('weightModal');
|
|
if (weightModal.classList.contains('show')) {
|
|
document.getElementById('weight-input').value = data.grams;
|
|
}
|
|
});
|
|
|
|
/* =========================================
|
|
3. CORE CART & UI LOGIC
|
|
========================================= */
|
|
function renderCart() {
|
|
const tbody = document.getElementById('cart-items');
|
|
tbody.innerHTML = '';
|
|
let total = 0;
|
|
|
|
cart.forEach((item, index) => {
|
|
total += item.subtotal;
|
|
const row = document.createElement('tr');
|
|
let qtyControls;
|
|
|
|
if (item.unit === 'kg') {
|
|
qtyControls = `
|
|
<button class="btn btn-sm btn-outline-secondary py-0 px-2" onclick="editWeight(${index})">
|
|
${item.qty.toFixed(3)} kg <i class="bi bi-pencil ms-1" style="font-size: 0.7rem;"></i>
|
|
</button>`;
|
|
} else {
|
|
qtyControls = `
|
|
<div class="d-flex align-items-center gap-1">
|
|
<button class="btn btn-sm btn-outline-secondary py-0 px-2" onclick="updateQty(${index}, -1)">-</button>
|
|
<input type="number" class="form-control form-control-sm text-center p-0" style="width: 50px;" value="${item.qty}" onchange="manualQty(${index}, this.value)">
|
|
<button class="btn btn-sm btn-outline-secondary py-0 px-2" onclick="updateQty(${index}, 1)">+</button>
|
|
</div>`;
|
|
}
|
|
|
|
row.innerHTML = `
|
|
<td class="font-monospace small text-muted">${item.barcode}</td>
|
|
<td>${item.name}</td>
|
|
<td>${clp.format(item.price)}</td>
|
|
<td>${qtyControls}</td>
|
|
<td>${clp.format(item.subtotal)}</td>
|
|
<td>
|
|
<button class="btn btn-danger-discord btn-sm" onclick="removeItem(${index})">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>`;
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
document.getElementById('grand-total').innerText = clp.format(total);
|
|
saveCart();
|
|
}
|
|
|
|
function addToCart(product, qty) {
|
|
const existingIndex = cart.findIndex(item => item.barcode === product.barcode && item.unit !== 'kg');
|
|
if (existingIndex !== -1) {
|
|
cart[existingIndex].qty += qty;
|
|
cart[existingIndex].subtotal = calculateSubtotal(cart[existingIndex].price, cart[existingIndex].qty);
|
|
} else {
|
|
cart.push({ ...product, qty, subtotal: calculateSubtotal(product.price, qty) });
|
|
}
|
|
renderCart();
|
|
}
|
|
|
|
function updateQty(index, delta) {
|
|
if (cart[index].unit === 'kg') return;
|
|
cart[index].qty += delta;
|
|
if (cart[index].qty <= 0) {
|
|
removeItem(index, cart[index].name);
|
|
} else {
|
|
cart[index].subtotal = calculateSubtotal(cart[index].price, cart[index].qty);
|
|
renderCart();
|
|
}
|
|
}
|
|
|
|
function manualQty(index, val) {
|
|
const newQty = parseFloat(val);
|
|
if (isNaN(newQty) || newQty <= 0) return;
|
|
cart[index].qty = newQty;
|
|
cart[index].subtotal = calculateSubtotal(cart[index].price, cart[index].qty);
|
|
renderCart();
|
|
}
|
|
|
|
function removeItem(idx) {
|
|
itemIndexToRemove = idx;
|
|
document.getElementById('removeItemName').innerText = cart[idx].name;
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('removeConfirmModal')).show();
|
|
}
|
|
|
|
function executeRemoveItem() {
|
|
if (itemIndexToRemove !== null) {
|
|
cart.splice(itemIndexToRemove, 1);
|
|
renderCart();
|
|
bootstrap.Modal.getInstance(document.getElementById('removeConfirmModal')).hide();
|
|
itemIndexToRemove = null;
|
|
}
|
|
}
|
|
|
|
function clearCart() {
|
|
if (cart.length === 0) return;
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('clearCartModal')).show();
|
|
}
|
|
|
|
function executeClearCart() {
|
|
cart = [];
|
|
renderCart();
|
|
clearLastScanned();
|
|
bootstrap.Modal.getInstance(document.getElementById('clearCartModal')).hide();
|
|
}
|
|
|
|
function calculateSubtotal(price, qty) {
|
|
return Math.round((price * qty) / 10) * 10;
|
|
}
|
|
|
|
function saveCart() { localStorage.setItem('seki_cart', JSON.stringify(cart)); }
|
|
function loadCart() {
|
|
const saved = localStorage.getItem('seki_cart');
|
|
if (saved) {
|
|
try { cart = JSON.parse(saved); renderCart(); }
|
|
catch (e) { console.error(e); cart = []; }
|
|
}
|
|
}
|
|
|
|
/* =========================================
|
|
4. PRODUCT SCANNING & SEARCH
|
|
========================================= */
|
|
function handleProductScan(product) {
|
|
document.getElementById('display-name').innerText = product.name;
|
|
document.getElementById('display-barcode').innerText = product.barcode;
|
|
document.getElementById('display-img').src = product.image || './static/placeholder.png';
|
|
|
|
const actualUnit = product.unit || product.unit_type;
|
|
if (actualUnit === 'kg') {
|
|
pendingProduct = product;
|
|
pendingProduct.unit = 'kg';
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('weightModal')).show();
|
|
setTimeout(() => document.getElementById('weight-input').focus(), 500);
|
|
} else {
|
|
product.unit = 'unit';
|
|
addToCart(product, 1);
|
|
}
|
|
}
|
|
|
|
function filterSearch() {
|
|
const query = document.getElementById('manual-search').value.toLowerCase().trim();
|
|
const resultsBox = document.getElementById('search-results');
|
|
|
|
if (query.length < 2) {
|
|
resultsBox.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const matches = allProducts.filter(p => p.name.toLowerCase().includes(query) || p.barcode.includes(query)).slice(0, 10);
|
|
|
|
if (matches.length === 0) {
|
|
resultsBox.innerHTML = '<div class="p-3 text-muted text-center">No se encontraron productos</div>';
|
|
} else {
|
|
resultsBox.innerHTML = matches.map(p => {
|
|
// Check if it's pinned to color the icon
|
|
const isPinned = pinnedBarcodes.includes(p.barcode);
|
|
const pinIcon = isPinned ? 'bi-pin-angle-fill text-warning' : 'bi-pin-angle text-muted';
|
|
|
|
return `
|
|
<a href="#" class="dropdown-item d-flex justify-content-between align-items-center py-2" onclick="selectSearchResult('${p.barcode}')">
|
|
<div class="d-flex align-items-center gap-3">
|
|
<button class="btn btn-sm btn-link p-0 text-decoration-none" onclick="togglePin('${p.barcode}', event)" title="Fijar producto">
|
|
<i class="bi ${pinIcon} fs-5"></i>
|
|
</button>
|
|
<div>
|
|
<strong>${p.name}</strong><br>
|
|
<small class="text-muted font-monospace">${p.barcode}</small>
|
|
</div>
|
|
</div>
|
|
<div class="text-end">
|
|
<span style="color: var(--accent); font-weight: bold;">${clp.format(p.price)}</span><br>
|
|
<small class="text-muted">${p.unit === 'kg' ? 'Kg' : 'Unidad'}</small>
|
|
</div>
|
|
</a>
|
|
`}).join('');
|
|
}
|
|
resultsBox.style.display = 'block';
|
|
}
|
|
|
|
function togglePin(barcode, event) {
|
|
event.stopPropagation(); // Stop the row from adding to cart when clicking the pin
|
|
if (pinnedBarcodes.includes(barcode)) {
|
|
pinnedBarcodes = pinnedBarcodes.filter(b => b !== barcode);
|
|
} else {
|
|
pinnedBarcodes.push(barcode);
|
|
}
|
|
localStorage.setItem('seki_pinned_products', JSON.stringify(pinnedBarcodes));
|
|
|
|
renderPinnedProducts();
|
|
filterSearch(); // Re-render the search dropdown so the pin icon updates
|
|
document.getElementById('manual-search').focus(); // Keep focus on search input
|
|
}
|
|
|
|
function renderPinnedProducts() {
|
|
const container = document.getElementById('pinned-products-container');
|
|
const pinnedProducts = allProducts.filter(p => pinnedBarcodes.includes(p.barcode));
|
|
|
|
if (pinnedProducts.length === 0) {
|
|
container.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = pinnedProducts.map(p => `
|
|
<button class="btn btn-outline-secondary text-start p-2 shadow-sm d-flex flex-column justify-content-between"
|
|
style="width: 110px; height: 75px; border-color: var(--border); background: var(--input-bg); color: var(--text-main);"
|
|
onclick="handleProductScan(allProducts.find(x => x.barcode === '${p.barcode}'))">
|
|
<span class="small fw-bold text-truncate w-100 mb-1" title="${p.name}">${p.name}</span>
|
|
<span class="small" style="color: var(--accent); font-weight: 800;">${clp.format(p.price)}</span>
|
|
</button>
|
|
`).join('');
|
|
}
|
|
|
|
function selectSearchResult(barcode) {
|
|
const product = allProducts.find(p => p.barcode === barcode);
|
|
if (product) handleProductScan(product);
|
|
|
|
const searchInput = document.getElementById('manual-search');
|
|
searchInput.value = '';
|
|
document.getElementById('search-results').style.display = 'none';
|
|
searchInput.focus();
|
|
}
|
|
|
|
function clearLastScanned() {
|
|
document.getElementById('display-img').src = './static/placeholder.png';
|
|
document.getElementById('display-name').innerText = 'Esperando scan...';
|
|
document.getElementById('display-barcode').innerText = '';
|
|
}
|
|
|
|
/* =========================================
|
|
5. MANUAL & TEMPORARY PRODUCT INPUTS
|
|
========================================= */
|
|
function openTempProduct() {
|
|
bootstrap.Modal.getInstance(document.getElementById('notFoundModal')).hide();
|
|
tempBarcode = missingProductData.barcode;
|
|
document.getElementById('custom-name').value = missingProductData.name || '';
|
|
document.getElementById('custom-price').value = '';
|
|
document.getElementById('custom-unit').value = 'unit';
|
|
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('customProductModal')).show();
|
|
setTimeout(() => {
|
|
if (missingProductData.name) document.getElementById('custom-price').focus();
|
|
else document.getElementById('custom-name').focus();
|
|
}, 500);
|
|
}
|
|
|
|
function openCustomProductModal() {
|
|
tempBarcode = null;
|
|
document.getElementById('custom-name').value = '';
|
|
document.getElementById('custom-price').value = '';
|
|
document.getElementById('custom-unit').value = 'unit';
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('customProductModal')).show();
|
|
setTimeout(() => document.getElementById('custom-name').focus(), 500);
|
|
}
|
|
|
|
function addCustomProduct() {
|
|
const nameInput = document.getElementById('custom-name').value.trim();
|
|
const priceInput = parseInt(document.getElementById('custom-price').value, 10);
|
|
const unitInput = document.getElementById('custom-unit').value;
|
|
|
|
if (!nameInput || isNaN(priceInput) || priceInput <= 0) {
|
|
alert("Por favor ingresa un nombre y un precio válido.");
|
|
return;
|
|
}
|
|
|
|
const customProduct = {
|
|
barcode: tempBarcode ? tempBarcode : `MANUAL-${Date.now().toString().slice(-6)}`,
|
|
name: `* ${nameInput}`,
|
|
price: priceInput,
|
|
image: '',
|
|
stock: 0,
|
|
unit: unitInput
|
|
};
|
|
|
|
if (unitInput === 'kg') {
|
|
pendingProduct = customProduct;
|
|
bootstrap.Modal.getInstance(document.getElementById('customProductModal')).hide();
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('weightModal')).show();
|
|
setTimeout(() => document.getElementById('weight-input').focus(), 500);
|
|
} else {
|
|
addToCart(customProduct, 1);
|
|
bootstrap.Modal.getInstance(document.getElementById('customProductModal')).hide();
|
|
}
|
|
tempBarcode = null;
|
|
}
|
|
|
|
function openVariosModal() {
|
|
const input = document.getElementById('varios-price-input');
|
|
input.value = '';
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('variosModal')).show();
|
|
input.focus(); // Instant focus
|
|
}
|
|
|
|
function addVariosToCart() {
|
|
// Grab the value and strip the dots before parsing
|
|
const rawValue = document.getElementById('varios-price-input').value.replace(/\./g, '');
|
|
const price = parseInt(rawValue, 10);
|
|
|
|
if (isNaN(price) || price <= 0) return alert("Ingresa un precio válido.");
|
|
|
|
addToCart({
|
|
barcode: `VARIOS-${Date.now().toString().slice(-6)}`,
|
|
name: '* Varios',
|
|
price: price,
|
|
qty: 1,
|
|
subtotal: price,
|
|
image: '',
|
|
stock: 0,
|
|
unit: 'unit'
|
|
}, 1);
|
|
bootstrap.Modal.getInstance(document.getElementById('variosModal')).hide();
|
|
}
|
|
|
|
/* =========================================
|
|
6. WEIGHT LOGIC (GRAMS TO KG)
|
|
========================================= */
|
|
function editWeight(index) {
|
|
editingCartIndex = index;
|
|
const weightInput = document.getElementById('weight-input');
|
|
weightInput.value = Math.round(cart[index].qty * 1000);
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('weightModal')).show();
|
|
setTimeout(() => { weightInput.focus(); weightInput.select(); }, 500);
|
|
}
|
|
|
|
function confirmWeight() {
|
|
const weightGrams = parseInt(document.getElementById('weight-input').value, 10);
|
|
if (weightGrams > 0) {
|
|
const weightKg = weightGrams / 1000;
|
|
if (editingCartIndex !== null) {
|
|
cart[editingCartIndex].qty = weightKg;
|
|
cart[editingCartIndex].subtotal = calculateSubtotal(cart[editingCartIndex].price, weightKg);
|
|
renderCart();
|
|
} else {
|
|
addToCart(pendingProduct, weightKg);
|
|
}
|
|
bootstrap.Modal.getInstance(document.getElementById('weightModal')).hide();
|
|
editingCartIndex = null;
|
|
}
|
|
}
|
|
|
|
document.getElementById('weightModal').addEventListener('hidden.bs.modal', function () {
|
|
document.getElementById('weight-input').value = '';
|
|
pendingProduct = null;
|
|
editingCartIndex = null;
|
|
});
|
|
|
|
/* =========================================
|
|
7. CHECKOUT & PAYMENT LOGIC
|
|
========================================= */
|
|
function processSale() {
|
|
if (cart.length === 0) return;
|
|
const total = cart.reduce((sum, item) => sum + item.subtotal, 0);
|
|
|
|
if (total === 666) { triggerDoom(); return; }
|
|
|
|
// Intercept checkout if Restaurant mode is active
|
|
if (localStorage.getItem('seki_ask_order_details') === 'true') {
|
|
document.getElementById('order-client-name').value = '';
|
|
document.getElementById('order-pickup-time').value = ''; // <-- ADD THIS
|
|
document.getElementById('order-notes').value = '';
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('orderDetailsModal')).show();
|
|
setTimeout(() => document.getElementById('order-client-name').focus(), 500);
|
|
return;
|
|
}
|
|
|
|
showPaymentModal(total);
|
|
}
|
|
|
|
function confirmOrderDetails() {
|
|
currentClientName = document.getElementById('order-client-name').value.trim();
|
|
currentPickupTime = document.getElementById('order-pickup-time').value;
|
|
currentOrderNotes = document.getElementById('order-notes').value.trim();
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('orderDetailsModal')).hide();
|
|
|
|
const total = cart.reduce((sum, item) => sum + item.subtotal, 0);
|
|
showPaymentModal(total);
|
|
}
|
|
|
|
function showPaymentModal(total) {
|
|
document.getElementById('payment-modal-total').innerText = clp.format(total);
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('paymentModal')).show();
|
|
}
|
|
|
|
function openVueltoModal() {
|
|
bootstrap.Modal.getInstance(document.getElementById('paymentModal')).hide();
|
|
const total = cart.reduce((sum, item) => sum + item.subtotal, 0);
|
|
|
|
document.getElementById('vuelto-total-display').innerText = clp.format(total);
|
|
const input = document.getElementById('monto-recibido');
|
|
input.value = '';
|
|
|
|
document.getElementById('vuelto-amount').innerText = clp.format(0);
|
|
document.getElementById('vuelto-amount').className = "fs-1 fw-bold text-success";
|
|
document.getElementById('btn-confirm-vuelto').disabled = false;
|
|
|
|
const quickBox = document.getElementById('vuelto-quick-buttons');
|
|
quickBox.innerHTML = `<button class="btn btn-sm btn-outline-secondary fw-bold" onclick="setMonto(${total})">Exacto</button>`;
|
|
[2000, 5000, 10000, 20000].forEach(bill => {
|
|
if (bill > total && (bill - total) <= 20000) {
|
|
quickBox.innerHTML += `<button class="btn btn-sm btn-outline-secondary fw-bold" onclick="setMonto(${bill})">${clp.format(bill)}</button>`;
|
|
}
|
|
});
|
|
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('vueltoModal')).show();
|
|
input.focus(); // Instant focus
|
|
}
|
|
|
|
function setMonto(amount) {
|
|
// Formats the quick-select buttons so they have dots too
|
|
document.getElementById('monto-recibido').value = amount.toLocaleString('es-CL');
|
|
calculateVuelto();
|
|
}
|
|
|
|
function calculateVuelto() {
|
|
const total = cart.reduce((sum, item) => sum + item.subtotal, 0);
|
|
// Strip the dots before parsing the math
|
|
const rawRecibido = document.getElementById('monto-recibido').value.replace(/\./g, '');
|
|
const recibido = parseInt(rawRecibido, 10);
|
|
const vueltoDisplay = document.getElementById('vuelto-amount');
|
|
const confirmBtn = document.getElementById('btn-confirm-vuelto');
|
|
|
|
if (isNaN(recibido) || recibido === 0) {
|
|
vueltoDisplay.innerText = clp.format(0);
|
|
vueltoDisplay.className = "fs-1 fw-bold text-success";
|
|
confirmBtn.disabled = false;
|
|
} else if (recibido < total) {
|
|
vueltoDisplay.innerText = "Falta Dinero";
|
|
vueltoDisplay.className = "fs-4 fw-bold text-danger mt-2 d-block";
|
|
confirmBtn.disabled = true;
|
|
} else {
|
|
vueltoDisplay.innerText = clp.format(recibido - total);
|
|
vueltoDisplay.className = "fs-1 fw-bold text-success";
|
|
confirmBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function executeCheckout(method) {
|
|
if (cart.length === 0 || isProcessing) return;
|
|
isProcessing = true;
|
|
|
|
const total = cart.reduce((sum, item) => sum + item.subtotal, 0);
|
|
let paidAmount = total;
|
|
|
|
const confirmBtn = document.getElementById('btn-confirm-vuelto');
|
|
const originalBtnText = confirmBtn.innerHTML;
|
|
|
|
if (method === 'efectivo') {
|
|
// Strip the dots here too
|
|
const rawVal = document.getElementById('monto-recibido').value.replace(/\./g, '');
|
|
const inputVal = parseInt(rawVal, 10);
|
|
if (!isNaN(inputVal) && inputVal > 0) paidAmount = inputVal;
|
|
|
|
confirmBtn.disabled = true;
|
|
confirmBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Procesando...';
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/checkout', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ cart: cart, payment_method: method })
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
const pModal = bootstrap.Modal.getInstance(document.getElementById('paymentModal'));
|
|
const vModal = bootstrap.Modal.getInstance(document.getElementById('vueltoModal'));
|
|
if (pModal) pModal.hide();
|
|
if (vModal) vModal.hide();
|
|
|
|
printReceipt(total, result.sale_id, paidAmount);
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('successModal')).show();
|
|
|
|
cart = [];
|
|
renderCart();
|
|
clearLastScanned();
|
|
setTimeout(() => bootstrap.Modal.getInstance(document.getElementById('successModal')).hide(), 2000);
|
|
} else {
|
|
alert("Error: " + (result.error || "Error desconocido"));
|
|
}
|
|
} catch (err) {
|
|
alert("Error de conexión.");
|
|
} finally {
|
|
isProcessing = false;
|
|
confirmBtn.innerHTML = originalBtnText;
|
|
}
|
|
}
|
|
|
|
function openQuickSaleModal() {
|
|
const input = document.getElementById('quick-sale-amount');
|
|
input.value = '';
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('quickSaleModal')).show();
|
|
input.focus(); // Instant focus
|
|
}
|
|
|
|
async function processQuickSale() {
|
|
if (isProcessing) return;
|
|
|
|
const rawValue = document.getElementById('quick-sale-amount').value.replace(/\./g, '');
|
|
const amount = parseInt(rawValue, 10);
|
|
|
|
if (isNaN(amount) || amount <= 0) return alert("Ingresa un monto válido.");
|
|
|
|
if (amount === 666) {
|
|
bootstrap.Modal.getInstance(document.getElementById('quickSaleModal')).hide();
|
|
triggerDoom(); return;
|
|
}
|
|
|
|
isProcessing = true;
|
|
const quickBtn = document.querySelector('#quickSaleModal .btn-primary');
|
|
const originalBtnText = quickBtn.innerHTML;
|
|
quickBtn.disabled = true;
|
|
quickBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Procesando...';
|
|
|
|
const quickCart = [{
|
|
barcode: `RAPIDA-${Date.now().toString().slice(-6)}`,
|
|
name: '* Varios', price: amount, qty: 1, subtotal: amount, unit: 'unit'
|
|
}];
|
|
|
|
try {
|
|
const response = await fetch('/api/checkout', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ cart: quickCart, payment_method: 'efectivo' })
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
bootstrap.Modal.getInstance(document.getElementById('quickSaleModal')).hide();
|
|
const originalCart = [...cart];
|
|
cart = quickCart;
|
|
printReceipt(amount, result.sale_id, amount);
|
|
cart = originalCart;
|
|
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('successModal')).show();
|
|
setTimeout(() => bootstrap.Modal.getInstance(document.getElementById('successModal')).hide(), 2000);
|
|
} else {
|
|
alert("Error: " + (result.error || "Error desconocido"));
|
|
}
|
|
} catch (err) {
|
|
alert("Error de conexión.");
|
|
} finally {
|
|
isProcessing = false;
|
|
quickBtn.disabled = false;
|
|
quickBtn.innerHTML = originalBtnText;
|
|
}
|
|
}
|
|
|
|
function printReceipt(total, saleId, paidAmount = 0) {
|
|
const tbody = document.getElementById('receipt-items-print');
|
|
tbody.innerHTML = '';
|
|
|
|
cart.forEach(item => {
|
|
const qtyStr = item.unit === 'kg' ? item.qty.toFixed(3) : item.qty;
|
|
tbody.innerHTML += `
|
|
<tr>
|
|
<td>${qtyStr}</td>
|
|
<td style="padding-left: 5px; padding-right: 5px; word-break: break-word;">${item.name}</td>
|
|
<td style="text-align: right;">${clp.format(item.subtotal)}</td>
|
|
</tr>`;
|
|
});
|
|
|
|
const finalPaid = paidAmount > 0 ? paidAmount : total;
|
|
|
|
document.getElementById('receipt-ticket-id').innerText = saleId || "N/A";
|
|
document.getElementById('receipt-total-print').innerText = clp.format(total);
|
|
document.getElementById('receipt-paid-print').innerText = clp.format(finalPaid);
|
|
document.getElementById('receipt-change-print').innerText = clp.format(finalPaid - total);
|
|
document.getElementById('receipt-date').innerText = new Date().toLocaleString('es-CL');
|
|
|
|
// NEW: Fill in Restaurant Info if it exists
|
|
const orderInfoDiv = document.getElementById('receipt-order-info');
|
|
if (currentClientName || currentOrderNotes || currentPickupTime) {
|
|
orderInfoDiv.style.display = 'block';
|
|
document.getElementById('receipt-client-name').innerText = currentClientName || '-';
|
|
document.getElementById('receipt-order-notes').innerText = currentOrderNotes || '-';
|
|
|
|
const pickupContainer = document.getElementById('receipt-pickup-container');
|
|
if (currentPickupTime) {
|
|
pickupContainer.style.display = 'block';
|
|
document.getElementById('receipt-pickup-time').innerText = currentPickupTime;
|
|
} else {
|
|
pickupContainer.style.display = 'none';
|
|
}
|
|
} else {
|
|
orderInfoDiv.style.display = 'none';
|
|
}
|
|
|
|
// Wipe the memory for the next sale
|
|
currentClientName = '';
|
|
currentOrderNotes = '';
|
|
currentPickupTime = '';
|
|
|
|
|
|
// Check the setting before printing
|
|
setTimeout(() => {
|
|
if (localStorage.getItem('seki_auto_print') !== 'false') {
|
|
window.print();
|
|
}
|
|
}, 250);
|
|
}
|
|
|
|
/* =========================================
|
|
8. GLOBAL EVENT LISTENERS
|
|
========================================= */
|
|
document.addEventListener('click', function (e) {
|
|
const searchArea = document.getElementById('manual-search');
|
|
const resultsBox = document.getElementById('search-results');
|
|
if (e.target !== searchArea && !resultsBox.contains(e.target)) {
|
|
resultsBox.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keydown', function(event) {
|
|
const activeTag = document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
|
|
const isTyping = activeTag === 'input' || activeTag === 'textarea';
|
|
|
|
// Find modals that are fully open OR in the middle of their fade-in animation
|
|
const openModal = document.querySelector('.modal.show, .modal[style*="display: block"]');
|
|
|
|
// 1. The Eject Button: NumpadDecimal (.) or Delete
|
|
if (event.code === 'NumpadDecimal' || event.key === 'Delete') {
|
|
if (isTyping && event.key === 'Delete') return;
|
|
|
|
if (openModal) {
|
|
event.preventDefault();
|
|
const modalInstance = bootstrap.Modal.getInstance(openModal) || bootstrap.Modal.getOrCreateInstance(openModal);
|
|
if (modalInstance) modalInstance.hide();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 2. Payment Modal Selection: 1 for Efectivo, 2 for Tarjeta, 3 for Transferencia
|
|
if (openModal && openModal.id === 'paymentModal') {
|
|
if (event.code === 'Numpad1' || event.key === '1') {
|
|
event.preventDefault();
|
|
openVueltoModal();
|
|
return;
|
|
}
|
|
if (event.code === 'Numpad2' || event.key === '2') {
|
|
event.preventDefault();
|
|
executeCheckout('tarjeta');
|
|
return;
|
|
}
|
|
// NEW TRANSFER SHORTCUT
|
|
if (event.code === 'Numpad3' || event.key === '3') {
|
|
event.preventDefault();
|
|
executeCheckout('transferencia');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 3. The Money Button: Enter
|
|
if (event.code === 'NumpadEnter' || event.key === 'Enter') {
|
|
|
|
if (itemIndexToRemove !== null) {
|
|
event.preventDefault();
|
|
executeRemoveItem();
|
|
return;
|
|
}
|
|
|
|
const clearModal = document.getElementById('clearCartModal');
|
|
if (clearModal && (clearModal.classList.contains('show') || clearModal.style.display === 'block')) {
|
|
event.preventDefault();
|
|
executeClearCart();
|
|
return;
|
|
}
|
|
|
|
if (!openModal && !isTyping && cart.length > 0) {
|
|
event.preventDefault();
|
|
processSale();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 4. The Varios Button: +
|
|
if (event.code === 'NumpadAdd' || event.key === '+') {
|
|
if (isTyping) return;
|
|
event.preventDefault();
|
|
openVariosModal();
|
|
return;
|
|
}
|
|
|
|
// 5. The Oops Button: - (Remove last item)
|
|
if (event.code === 'NumpadSubtract' || event.key === '-') {
|
|
if (isTyping) return;
|
|
if (!openModal && cart.length > 0) {
|
|
event.preventDefault();
|
|
removeItem(cart.length - 1);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 6. The Speedrun Button: * (Venta Rápida)
|
|
if (event.code === 'NumpadMultiply' || event.key === '*') {
|
|
if (isTyping) return;
|
|
event.preventDefault();
|
|
openQuickSaleModal();
|
|
return;
|
|
}
|
|
});
|
|
|
|
// Safety cleanup: If you close the remove modal with ESC or the Eject button, clear the memory
|
|
document.getElementById('removeConfirmModal').addEventListener('hidden.bs.modal', function () {
|
|
itemIndexToRemove = null;
|
|
});
|
|
|
|
// Safety cleanup: If you close the remove modal with ESC or the Eject button, clear the memory
|
|
document.getElementById('removeConfirmModal').addEventListener('hidden.bs.modal', function () {
|
|
itemIndexToRemove = null;
|
|
});
|
|
|
|
/* =========================================
|
|
9. DOOM EASTER EGG LOGIC
|
|
========================================= */
|
|
let doomMainFn = null;
|
|
let titleDefender = null;
|
|
let dosInstance = null;
|
|
const posTitle = "SekiPOS - Caja";
|
|
|
|
function triggerDoom() {
|
|
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('doomModal'));
|
|
|
|
document.getElementById('boot-loading').classList.remove('d-none');
|
|
document.getElementById('boot-ready').classList.add('d-none');
|
|
document.getElementById('boot-status').innerHTML = "INICIANDO EMULADOR...<br>";
|
|
|
|
const bootScreen = document.getElementById('doom-boot-screen');
|
|
bootScreen.classList.remove('d-none');
|
|
bootScreen.classList.add('d-flex');
|
|
|
|
titleDefender = setInterval(() => {
|
|
if (document.title === "DOSBox") document.title = posTitle;
|
|
}, 50);
|
|
|
|
modal.show();
|
|
|
|
if (!document.getElementById('js-dos-script')) {
|
|
const script = document.createElement('script');
|
|
script.id = 'js-dos-script';
|
|
script.src = 'https://js-dos.com/6.22/current/js-dos.js';
|
|
script.onload = prepDoomEnvironment;
|
|
document.body.appendChild(script);
|
|
} else {
|
|
prepDoomEnvironment();
|
|
}
|
|
}
|
|
|
|
function prepDoomEnvironment() {
|
|
const bootText = document.getElementById('boot-status');
|
|
bootText.innerHTML += "MONTANDO UNIDAD VIRTUAL C:\\...<br>";
|
|
|
|
Dos(document.getElementById("jsdos"), {
|
|
wdosboxUrl: "https://js-dos.com/6.22/current/wdosbox.js"
|
|
}).ready((fs, main) => {
|
|
bootText.innerHTML += "DESCARGANDO ARCHIVOS WAD...<br>";
|
|
const bypassCache = new Date().getTime();
|
|
|
|
fs.extract(`/static/doom.zip?t=${bypassCache}`).then(() => {
|
|
const configData = "snd_channels 4\nsnd_musicdevice 3\nsnd_sfxdevice 3\nsnd_sbport 544\nsnd_sbirq 7\nsnd_sbdma 1\nsnd_mport 816\nuse_mouse 1\n";
|
|
const configBytes = new Uint8Array(new TextEncoder().encode(configData));
|
|
fs.createFile("fdoom.cfg", configBytes);
|
|
fs.createFile("default.cfg", configBytes);
|
|
|
|
doomMainFn = main;
|
|
document.getElementById('boot-loading').classList.add('d-none');
|
|
document.getElementById('boot-ready').classList.remove('d-none');
|
|
});
|
|
});
|
|
}
|
|
|
|
function startDoom() {
|
|
if (!doomMainFn) return;
|
|
const bootScreen = document.getElementById('doom-boot-screen');
|
|
bootScreen.classList.remove('d-flex');
|
|
bootScreen.classList.add('d-none');
|
|
|
|
if (window.SDL && window.SDL.audioContext && window.SDL.audioContext.state === 'suspended') {
|
|
window.SDL.audioContext.resume();
|
|
}
|
|
|
|
doomMainFn(["-c", "DOOM.EXE"]).then((ci) => { dosInstance = ci; });
|
|
}
|
|
|
|
function toggleDoomFullscreen() {
|
|
const canvas = document.getElementById('jsdos');
|
|
if (!document.fullscreenElement) {
|
|
if (canvas.requestFullscreen) canvas.requestFullscreen();
|
|
else if (canvas.webkitRequestFullscreen) canvas.webkitRequestFullscreen();
|
|
} else {
|
|
if (document.exitFullscreen) document.exitFullscreen();
|
|
}
|
|
}
|
|
|
|
// The Nuclear Cleanup Crew
|
|
document.getElementById('doomModal').addEventListener('hidden.bs.modal', function () {
|
|
window.location.reload();
|
|
});
|
|
|
|
/* =========================================
|
|
10. INIT
|
|
========================================= */
|
|
loadCart();
|
|
renderPinnedProducts();
|
|
|
|
|
|
|
|
</script>
|
|
{% endblock %} |