/* ============================================================ POS (PUNTO DE VENTA - CAJA REGISTRADORA) ============================================================ */ // CLIENT COMBOBOX let clientComboboxState = { highlightedIndex: -1, filteredClients: [], isOpen: false }; const POS_UNDO_HISTORY_KEY = 'posUndoHistory'; const POS_UNDO_MAX_ENTRIES = 10; const POS_UNDO_DEFAULT_WINDOW_MS = 5 * 60 * 1000; let posUndoHistory = []; function getPosUndoWindowMs() { const configuredWindow = Number(CONFIG?.POS_UNDO_WINDOW_MS); if (Number.isFinite(configuredWindow) && configuredWindow > 0) { return configuredWindow; } return POS_UNDO_DEFAULT_WINDOW_MS; } function persistPosUndoHistory() { try { localStorage.setItem(POS_UNDO_HISTORY_KEY, JSON.stringify(posUndoHistory)); } catch (err) { console.warn('No se pudo persistir historial de deshacer POS:', err); } } function loadPosUndoHistory() { try { const raw = localStorage.getItem(POS_UNDO_HISTORY_KEY); if (!raw) { posUndoHistory = []; return; } const parsed = JSON.parse(raw); posUndoHistory = Array.isArray(parsed) ? parsed : []; purgeExpiredUndoHistory(); } catch (err) { console.warn('No se pudo cargar historial de deshacer POS:', err); posUndoHistory = []; } } function purgeExpiredUndoHistory() { const now = Date.now(); const windowMs = getPosUndoWindowMs(); posUndoHistory = posUndoHistory.filter(entry => { if (!entry || typeof entry !== 'object') return false; if (!Array.isArray(entry.cart) || !entry.clientId) return false; if (!Number.isFinite(entry.timestamp)) return false; return now - entry.timestamp <= windowMs; }); persistPosUndoHistory(); } function savePosUndoSnapshot() { if (!STATE.adminCart.length) return; const snapshot = { clientId: STATE.adminSelectedClient || 'c1', cart: deepClone(STATE.adminCart), timestamp: Date.now(), consumed: false }; posUndoHistory.push(snapshot); if (posUndoHistory.length > POS_UNDO_MAX_ENTRIES) { posUndoHistory = posUndoHistory.slice(-POS_UNDO_MAX_ENTRIES); } purgeExpiredUndoHistory(); } function getImmediateUndoCandidate(clientId) { purgeExpiredUndoHistory(); const latest = posUndoHistory[posUndoHistory.length - 1]; if (!latest) return null; if (latest.consumed) return null; if (latest.clientId !== clientId) return null; return latest; } function focusLastCartItemQty() { if (!STATE.adminCart.length) return; const lastItem = STATE.adminCart[STATE.adminCart.length - 1]; if (!lastItem) return; setTimeout(() => { const qtyInput = document.getElementById(`admin-qty-${lastItem.id}`); if (!qtyInput) return; qtyInput.focus(); qtyInput.select(); }, 0); } function showUndoAppliedFeedback() { const container = document.getElementById('admin-cart-items'); if (container) { container.style.transition = 'box-shadow 0.25s ease'; container.style.boxShadow = '0 0 0 2px #10b981 inset'; setTimeout(() => { container.style.boxShadow = ''; }, 900); } showToast('Deshacer aplicado: se recuperó el último pedido del cliente', 'success'); } function undoLastPosOrder() { const clientId = STATE.adminSelectedClient || 'c1'; const undoCandidate = getImmediateUndoCandidate(clientId); if (!undoCandidate) { showToast('No hay un pedido inmediato para deshacer para este cliente', 'warning'); return; } // 1) Cancelar ticket actual STATE.adminCart = []; // 2) Recuperar último pedido inmediato del cliente seleccionado STATE.adminCart = deepClone(undoCandidate.cart); undoCandidate.consumed = true; undoCandidate.appliedAt = Date.now(); persistPosUndoHistory(); // 3 + 4) Rehidratar carrito y re-render renderPosCart(); renderPosProductList(); focusLastCartItemQty(); showUndoAppliedFeedback(); } function renderPosClientCombobox() { const input = document.getElementById('admin-client-input'); if (!input) return; // Initialize with first client (Consumidor Final) if (!STATE.adminSelectedClient && STATE.clients.length > 0) { STATE.adminSelectedClient = STATE.clients[0].id; } const selectedClient = STATE.clients.find(c => c.id === STATE.adminSelectedClient); if (selectedClient) { input.value = selectedClient.name; } filterAndRenderClientList(''); } function filterAndRenderClientList(query = '') { const list = document.getElementById('admin-client-list'); if (!list) return; const normalized = normalizeText(query); clientComboboxState.filteredClients = STATE.clients.filter(client => { if (!normalized) return true; return [client.name, client.cuit, client.phone, client.client_code] .filter(Boolean) .some(value => normalizeText(value).includes(normalized)); }); clientComboboxState.highlightedIndex = -1; if (clientComboboxState.filteredClients.length === 0) { list.innerHTML = '
  • No se encontraron clientes
  • '; list.classList.remove('hidden'); return; } list.innerHTML = clientComboboxState.filteredClients.map((client, index) => { const isSelected = client.id === STATE.adminSelectedClient; return `
  • ${escapeHtml(client.name)}
    ${client.cuit ? '📋 ' + client.cuit + ' · ' : ''}${client.phone ? '📞 ' + client.phone : ''}
  • `; }).join(''); list.classList.remove('hidden'); clientComboboxState.isOpen = true; } function handleClientInputChange(event) { const query = event.target.value; filterAndRenderClientList(query); } function handleClientKeydown(event) { const { key } = event; const list = document.getElementById('admin-client-list'); const maxIndex = clientComboboxState.filteredClients.length - 1; if (key === 'ArrowDown') { event.preventDefault(); clientComboboxState.highlightedIndex = Math.min(clientComboboxState.highlightedIndex + 1, maxIndex); updateClientListHighlight(); } else if (key === 'ArrowUp') { event.preventDefault(); clientComboboxState.highlightedIndex = Math.max(clientComboboxState.highlightedIndex - 1, -1); updateClientListHighlight(); } else if (key === 'Enter') { event.preventDefault(); if (clientComboboxState.highlightedIndex >= 0) { const client = clientComboboxState.filteredClients[clientComboboxState.highlightedIndex]; selectClient(client.id); } } else if (key === 'Escape') { list.classList.add('hidden'); clientComboboxState.isOpen = false; } } function updateClientListHighlight() { const items = document.querySelectorAll('#admin-client-list li[data-index]'); items.forEach((item, index) => { if (index === clientComboboxState.highlightedIndex) { item.style.backgroundColor = '#dbeafe'; item.style.borderLeft = '3px solid #3b82f6'; item.scrollIntoView({ block: 'nearest' }); } else { const client = clientComboboxState.filteredClients[index]; const isSelected = client.id === STATE.adminSelectedClient; item.style.backgroundColor = isSelected ? '#e0e7ff' : 'transparent'; item.style.borderLeft = isSelected ? '3px solid #3b82f6' : 'transparent'; } }); } function selectClient(clientId) { STATE.adminSelectedClient = clientId; const client = STATE.clients.find(c => c.id === clientId); if (client) { const input = document.getElementById('admin-client-input'); const list = document.getElementById('admin-client-list'); if (input) input.value = client.name; if (list) list.classList.add('hidden'); clientComboboxState.isOpen = false; if (client.price_list_id) { STATE.activePriceListId = client.price_list_id; renderPosListSelector(); renderPosProductList(); } } } function renderPosCategoryFilters() { const container = document.getElementById('admin-pos-categories'); if (!container) return; container.innerHTML = CONFIG.CATEGORIES.map(category => ` `).join(''); } function setPosCategory(category, btn = null) { STATE.adminPosCategory = category; STATE.posSearchActiveIndex = -1; const container = document.getElementById('admin-pos-categories'); if (container) { container.querySelectorAll('.pos-category-btn').forEach(button => { button.classList.toggle('active', button.dataset.category === category); }); } else if (btn) { document.querySelectorAll('.pos-category-btn').forEach(button => button.classList.remove('active')); btn.classList.add('active'); } renderPosProductList(); } // PRODUCT LIST function getFilteredPosProducts() { const searchInput = document.getElementById('admin-search-prod'); const query = searchInput?.value.toLowerCase() || ''; const activeCategory = STATE.adminPosCategory || 'all'; return STATE.products.filter(p => { const productName = (p.name || '').toLowerCase(); const productShort = (p.short || '').toLowerCase(); const productSku = (p.sku || '').toLowerCase(); const productId = (p.id || '').toLowerCase(); const categoryMatches = activeCategory === 'all' || p.cat === activeCategory; const searchMatches = productName.includes(query) || productShort.includes(query) || productSku.includes(query) || productId.includes(query); return categoryMatches && searchMatches; }); } function renderPosProductList() { const list = document.getElementById('admin-prod-list'); const noResults = document.getElementById('admin-pos-no-results'); if (!list) { console.error('admin-prod-list elemento no encontrado'); return; } const filtered = getFilteredPosProducts(); const lastIndex = filtered.length - 1; if (lastIndex < 0) { STATE.posSearchActiveIndex = -1; } else if (STATE.posSearchActiveIndex > lastIndex) { STATE.posSearchActiveIndex = lastIndex; } renderPosCategoryFilters(); const html = filtered.map((p, index) => ( renderPosProductCard(p, { index, activeIndex: STATE.posSearchActiveIndex }) )).join(''); list.innerHTML = html; if (noResults) { noResults.classList.toggle('hidden', filtered.length > 0); } console.log(`POS product list renderizado con ${filtered.length} productos`); } function handlePosSearchKeydown(event) { const { key } = event; if (!['ArrowDown', 'ArrowUp'].includes(key)) return; const filtered = getFilteredPosProducts(); if (!filtered.length) return; event.preventDefault(); const lastIndex = filtered.length - 1; if (key === 'ArrowDown') { STATE.posSearchActiveIndex = Math.min(STATE.posSearchActiveIndex + 1, lastIndex); } else { STATE.posSearchActiveIndex = Math.max(STATE.posSearchActiveIndex - 1, 0); } renderPosProductList(); } // ADMIN CART function addToAdminCart(productId, options = {}) { const { focusQtyInput = true } = options; const product = STATE.products.find(p => p.id === productId); if (!product || product.stock <= 0) { showToast('Producto sin stock', 'error'); return; } const existing = STATE.adminCart.find(c => c.id === productId); if (existing) { if (existing.qty < product.stock) { existing.qty++; } else { showToast('Stock insuficiente', 'warning'); } } else { const displayPrice = STATE.activePriceListId && typeof getPriceForList === 'function' ? getPriceForList(product, STATE.activePriceListId) : (typeof applyPriceListFactor === 'function' ? applyPriceListFactor(product.sale) : product.sale); STATE.adminCart.push({ id: productId, name: product.name, price: displayPrice, qty: 1, stock: product.stock }); } renderPosCart(); if (focusQtyInput) { // Focus and select the quantity input for the new/updated item setTimeout(() => { const qtyInput = document.getElementById(`admin-qty-${productId}`); if (qtyInput) { qtyInput.focus(); qtyInput.select(); } }, 0); } } function removeFromAdminCart(productId) { STATE.adminCart = STATE.adminCart.filter(c => c.id !== productId); renderPosCart(); } function updateAdminCartQty(productId, qty) { qty = parseInt(qty) || 1; const item = STATE.adminCart.find(c => c.id === productId); if (!item) return; if (qty > item.stock) { qty = item.stock; showToast('Stock máximo: ' + item.stock, 'warning'); } if (qty < 1) { removeFromAdminCart(productId); } else { item.qty = qty; } renderPosCart(); } function handleQuantityKeypress(event, productId) { if (event.key === 'Escape') { document.getElementById(`admin-qty-${productId}`).blur(); } } function isPosBillingViewActive() { return Boolean(document.getElementById('admin-search-prod')); } function isTextEditingTarget(target) { if (!target) return false; const tagName = (target.tagName || '').toLowerCase(); const isTextInput = tagName === 'input' || tagName === 'textarea' || tagName === 'select'; return isTextInput || target.isContentEditable; } function getActivePosProduct() { const filtered = getFilteredPosProducts(); if (!filtered.length || STATE.posSearchActiveIndex < 0) return null; return filtered[STATE.posSearchActiveIndex] || null; } function isPosShortcutsModalOpen() { return isModalOpen('admin-pos-shortcuts-modal'); } function openPosShortcutsModal() { openModal('admin-pos-shortcuts-modal'); setTimeout(() => { const closeBtn = document.getElementById('admin-pos-shortcuts-close-btn'); closeBtn?.focus(); }, 20); } function closePosShortcutsModal() { closeModal('admin-pos-shortcuts-modal'); const helpBtn = document.getElementById('admin-pos-help-btn'); setTimeout(() => { helpBtn?.focus(); }, 260); } function handlePosShortcutsOverlayClick(event) { if (event.target?.id === 'admin-pos-shortcuts-modal') { closePosShortcutsModal(); } } function handlePosGlobalShortcuts(event) { if (!isPosBillingViewActive()) return; const { key, target } = event; if (isPosShortcutsModalOpen()) { if (key === 'Escape') { event.preventDefault(); closePosShortcutsModal(); } return; } const searchInput = document.getElementById('admin-search-prod'); if (!searchInput) return; if (key === '/') { event.preventDefault(); searchInput.focus(); searchInput.select(); return; } if (key === '+') { event.preventDefault(); const selected = getActivePosProduct(); if (selected) { addToAdminCart(selected.id, { focusQtyInput: true }); setTimeout(() => { const qtyInput = document.getElementById(`admin-qty-${selected.id}`); const cartItem = STATE.adminCart.find(item => item.id === selected.id); if (qtyInput) { if (cartItem && cartItem.qty === 1) { qtyInput.value = '1'; } qtyInput.focus(); qtyInput.select(); } }, 0); } else { searchInput.focus(); searchInput.select(); } return; } if (key === '-') { if (isTextEditingTarget(target)) return; event.preventDefault(); undoLastPosOrder(); return; } if (key === 'Enter') { if (isTextEditingTarget(target)) return; event.preventDefault(); procesarVenta(false); } } function renderPosCart() { const container = document.getElementById('admin-cart-items'); const totalEl = document.getElementById('admin-cart-total'); const itemsCountEl = document.getElementById('admin-items-count'); if (!container || !totalEl) return; const total = STATE.adminCart.reduce((s, c) => s + c.price * c.qty, 0); const itemsCount = STATE.adminCart.reduce((s, c) => s + c.qty, 0); totalEl.textContent = fmt(total); if (itemsCountEl) { itemsCountEl.textContent = `${itemsCount} ${itemsCount === 1 ? 'artículo' : 'artículos'}`; } if (!STATE.adminCart.length) { container.innerHTML = '
    Ticket vacío
    '; return; } let html = '
    ' + 'Producto' + 'Total' + '
    '; STATE.adminCart.forEach(c => { html += `

    ${escapeHtml(c.name)}

    ${fmt(c.price)} ×

    ${fmt(c.price * c.qty)}

    `; }); container.innerHTML = html; } // DOCUMENT TYPE function setDocumentType(type) { STATE.adminDocumentType = type; updateDocumentTypeUI(); } function updateDocumentTypeUI() { document.querySelectorAll('[data-doctype]').forEach(el => { el.classList.toggle('checked', el.dataset.doctype === STATE.adminDocumentType); }); } // PROCESS SALE function procesarVenta(printPDF = false) { if (!STATE.adminCart.length) { showToast('El ticket está vacío', 'warning'); return; } const clientId = STATE.adminSelectedClient || 'c1'; const client = STATE.clients.find(c => c.id === clientId) || STATE.clients[0]; const docType = STATE.adminDocumentType; const total = STATE.adminCart.reduce((sum, item) => sum + item.price * item.qty, 0); const orderId = generateId('ord'); const invoiceId = generateId('inv'); const orderNumber = nextOrderNumber(); // Save invoice const invoice = { id: invoiceId, orderId, orderNumber, date: getCurrentDateTime(), client: client.name, clientId: client.id, docType, items: deepClone(STATE.adminCart), total, itemsCount: STATE.adminCart.reduce((s, c) => s + c.qty, 0) }; STATE.adminInvoices.unshift(invoice); const order = { id: orderId, invoiceId, orderNumber, date: getCurrentDateTime(), client: client.name, address: client.address || '—', items: deepClone(STATE.adminCart), total, source: 'pos', status: 'completed' }; STATE.adminOrders.unshift(order); // Snapshot para deshacer: carrito + cliente + timestamp (historial en memoria + localStorage) savePosUndoSnapshot(); // Deduct stock STATE.adminCart.forEach(item => { const product = STATE.products.find(p => p.id === item.id); if (product) { product.stock -= item.qty; } }); persistData(); // Capture cart snapshot before clearing (needed for PDF reprint) const cartSnapshot = deepClone(STATE.adminCart); // Print if requested if (printPDF) { generateInvoicePDF(client, docType, cartSnapshot, total, invoiceId, orderNumber); } // Clear cart STATE.adminCart = []; renderPosCart(); renderPosProductList(); renderStockTable(); // Post-sale feedback if (docType !== 'x') { // For facturas A/B/C: compact non-blocking panel with PDF + optional AFIP mostrarPanelPostVenta(invoiceId, invoice.orderId, docType, client, cartSnapshot, total, orderNumber); } else if (printPDF) { showToast('Venta procesada, impresa y stock actualizado', 'success'); } else { showActionableToast( 'Venta procesada. Stock actualizado.', 'success', { label: 'Descargar Factura', action: () => downloadOrderInvoice(invoice.orderId) } ); } return { invoiceId: invoice.id, orderId: invoice.orderId }; } // POST-SALE PANEL (non-blocking, bottom-right slide-in) function mostrarPanelPostVenta(invoiceId, orderId, docType, client, items, total, orderNumber) { document.getElementById('pos-postventa-panel')?.remove(); const typeLabel = { a: 'A', b: 'B', c: 'C' }[docType] ?? docType.toUpperCase(); const el = document.createElement('div'); el.id = 'pos-postventa-panel'; el.style.cssText = [ 'position:fixed;bottom:24px;right:24px;z-index:8000', 'background:#fff;border-radius:16px;padding:16px;width:296px', 'box-shadow:0 8px 32px rgba(0,0,0,.18);border:1px solid #e5e7eb', 'animation:pvpSlideIn .25s ease', ].join(';'); el.innerHTML = `

    Venta procesada

    Factura ${typeLabel} · ${(client.name || '').substring(0, 22)}

    `; document.body.appendChild(el); let currentInvoiceData = null; el.querySelector('#btn-pvp-cerrar').addEventListener('click', () => el.remove()); el.querySelector('#btn-pvp-pdf').addEventListener('click', () => { generateInvoicePDF(client, docType, items, total, invoiceId, orderNumber, currentInvoiceData); }); el.querySelector('#btn-pvp-afip').addEventListener('click', async () => { const btn = el.querySelector('#btn-pvp-afip'); btn.disabled = true; btn.textContent = '⏳ Solicitando CAE...'; let succeeded = false; await autorizarFactura(invoiceId, (updated) => { succeeded = true; currentInvoiceData = updated; const idx = STATE.adminInvoices.findIndex(i => i.id === invoiceId); if (idx !== -1) Object.assign(STATE.adminInvoices[idx], updated); const info = el.querySelector('#pvp-cae-info'); if (info) { info.textContent = `✔ CAE: ${updated.cae} · Vto: ${updated.cae_vencimiento || ''}`; info.style.display = 'block'; } btn.textContent = '📄 Re-descargar PDF con CAE'; btn.style.background = '#059669'; btn.disabled = false; }); if (!succeeded) { btn.disabled = false; btn.textContent = '🔏 Reintentar autorización AFIP'; } }); // Auto-remove after 60 seconds if untouched const autoClose = setTimeout(() => el.remove(), 60000); el.addEventListener('mouseenter', () => clearTimeout(autoClose)); } // SEARCH PRODUCTS IN POS document.addEventListener('DOMContentLoaded', function() { loadPosUndoHistory(); renderPosCategoryFilters(); // Initialize client combobox renderPosClientCombobox(); const clientInput = document.getElementById('admin-client-input'); const clientList = document.getElementById('admin-client-list'); if (clientInput) { clientInput.addEventListener('input', handleClientInputChange); clientInput.addEventListener('keydown', handleClientKeydown); clientInput.addEventListener('focus', () => { // Show list with all clients on focus filterAndRenderClientList(''); }); clientInput.addEventListener('blur', () => { // Hide list after brief delay to allow click on list items setTimeout(() => { if (document.activeElement !== clientList && !clientList?.contains(document.activeElement)) { clientList?.classList.add('hidden'); clientComboboxState.isOpen = false; } }, 200); }); } // Click outside combobox to close document.addEventListener('click', (e) => { const combobox = document.getElementById('admin-client-combobox'); if (combobox && !combobox.contains(e.target)) { if (clientList) { clientList.classList.add('hidden'); clientComboboxState.isOpen = false; } } }); const searchInput = document.getElementById('admin-search-prod'); if (searchInput) { searchInput.addEventListener('input', () => { STATE.posSearchActiveIndex = -1; renderPosProductList(); }); searchInput.addEventListener('keydown', handlePosSearchKeydown); } document.addEventListener('keydown', handlePosGlobalShortcuts); });