/* ============================================================
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 = '';
STATE.adminCart.forEach(c => {
html += `
${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);
});