/** * pos.js — Point of Sale logic for Nexus Autoparts */ (function () { 'use strict'; var API = ''; var selectedCustomer = null; var cart = []; var defaultMargin = 30; // ================================================================ // Utility // ================================================================ function toast(msg, type) { var el = document.createElement('div'); el.className = 'toast ' + (type || 'success'); el.textContent = msg; document.body.appendChild(el); setTimeout(function () { el.remove(); }, 3000); } function api(path, opts) { opts = opts || {}; return fetch(API + path, opts).then(function (r) { if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || 'Error'); }); return r.json(); }); } function esc(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } function fmt(n) { return '$' + (parseFloat(n) || 0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); } // ================================================================ // Customer Selection // ================================================================ var customerSearchTimer = null; var customerSearchEl = document.getElementById('customer-search'); var customerDropdown = document.getElementById('customer-dropdown'); customerSearchEl.addEventListener('input', function () { clearTimeout(customerSearchTimer); var q = this.value.trim(); if (q.length < 2) { customerDropdown.style.display = 'none'; return; } customerSearchTimer = setTimeout(function () { api('/api/pos/customers?search=' + encodeURIComponent(q) + '&per_page=10') .then(function (res) { var data = res.data || []; if (data.length === 0) { customerDropdown.innerHTML = '
No se encontraron clientes
'; } else { customerDropdown.innerHTML = data.map(function (c) { return '
' + '
' + esc(c.name) + '' + (c.rfc ? ' ' + esc(c.rfc) + '' : '') + '
' + '' + fmt(c.balance) + '
'; }).join(''); customerDropdown.querySelectorAll('.customer-dropdown-item').forEach(function (item) { item.addEventListener('click', function () { selectCustomer(parseInt(item.getAttribute('data-id'))); }); }); } customerDropdown.style.display = 'block'; }); }, 300); }); customerSearchEl.addEventListener('blur', function () { setTimeout(function () { customerDropdown.style.display = 'none'; }, 200); }); function selectCustomer(id) { api('/api/pos/customers/' + id).then(function (c) { selectedCustomer = c; document.getElementById('customer-select').style.display = 'none'; var info = document.getElementById('customer-info'); info.style.display = 'flex'; document.getElementById('sel-customer-name').textContent = c.name; document.getElementById('sel-customer-rfc').textContent = c.rfc || 'Sin RFC'; var balEl = document.getElementById('sel-customer-balance'); balEl.textContent = 'Saldo: ' + fmt(c.balance); balEl.className = 'cb-balance ' + (c.balance > 0 ? 'positive' : 'zero'); customerDropdown.style.display = 'none'; updateFacturarBtn(); }); } document.getElementById('btn-change-customer').addEventListener('click', function () { selectedCustomer = null; document.getElementById('customer-info').style.display = 'none'; document.getElementById('customer-select').style.display = 'block'; customerSearchEl.value = ''; customerSearchEl.focus(); updateFacturarBtn(); }); // ================================================================ // New Customer Modal // ================================================================ document.getElementById('btn-new-customer').addEventListener('click', function () { document.getElementById('modal-new-customer').style.display = 'flex'; document.getElementById('nc-name').focus(); }); document.getElementById('nc-cancel').addEventListener('click', function () { document.getElementById('modal-new-customer').style.display = 'none'; }); document.getElementById('nc-save').addEventListener('click', function () { var name = document.getElementById('nc-name').value.trim(); if (!name) { toast('Ingresa el nombre del cliente', 'error'); return; } api('/api/pos/customers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, rfc: document.getElementById('nc-rfc').value.trim() || null, business_name: document.getElementById('nc-business').value.trim() || null, phone: document.getElementById('nc-phone').value.trim() || null, email: document.getElementById('nc-email').value.trim() || null, address: document.getElementById('nc-address').value.trim() || null, credit_limit: parseFloat(document.getElementById('nc-credit').value) || 0, payment_terms: parseInt(document.getElementById('nc-terms').value) || 30 }) }).then(function (res) { toast('Cliente creado: ' + name); document.getElementById('modal-new-customer').style.display = 'none'; selectCustomer(res.id); // Clear form ['nc-name','nc-rfc','nc-business','nc-phone','nc-email','nc-address'].forEach(function(id) { document.getElementById(id).value = ''; }); document.getElementById('nc-credit').value = '0'; document.getElementById('nc-terms').value = '30'; }).catch(function (err) { toast(err.message, 'error'); }); }); // ================================================================ // Part Search — Autocomplete // ================================================================ var partSearchTimer = null; var partSearchEl = document.getElementById('part-search'); var partResults = document.getElementById('part-results'); var searchResults = []; var highlightIdx = -1; function doPartSearch() { var q = partSearchEl.value.trim(); if (q.length < 1) { partResults.style.display = 'none'; searchResults = []; return; } clearTimeout(partSearchTimer); partSearchTimer = setTimeout(function () { api('/api/pos/search-parts?q=' + encodeURIComponent(q)).then(function (results) { searchResults = results; highlightIdx = -1; renderSearchResults(); }); }, 150); } function renderSearchResults() { if (searchResults.length === 0 && partSearchEl.value.trim().length > 0) { partResults.innerHTML = '
No se encontraron partes para "' + esc(partSearchEl.value) + '"
'; partResults.style.display = 'block'; return; } if (searchResults.length === 0) { partResults.style.display = 'none'; return; } partResults.innerHTML = searchResults.map(function (p, i) { var active = i === highlightIdx ? ' part-result-active' : ''; return '
' + '
' + esc(p.oem_part_number) + '' + '' + esc(p.name_part) + '
' + '
' + '' + p.part_type + '' + (p.cost_usd ? '' + fmt(p.cost_usd) + '' : '') + '' + esc(p.group_name || '') + '' + '
'; }).join(''); partResults.querySelectorAll('.part-result-item').forEach(function (item) { item.addEventListener('mousedown', function (e) { e.preventDefault(); selectSearchResult(parseInt(item.getAttribute('data-idx'))); }); item.addEventListener('mouseenter', function () { highlightIdx = parseInt(item.getAttribute('data-idx')); updateHighlight(); }); }); partResults.style.display = 'block'; } function updateHighlight() { partResults.querySelectorAll('.part-result-item').forEach(function (el, i) { if (i === highlightIdx) { el.classList.add('part-result-active'); el.scrollIntoView({ block: 'nearest' }); } else { el.classList.remove('part-result-active'); } }); } function selectSearchResult(idx) { if (idx >= 0 && idx < searchResults.length) { addToCart(searchResults[idx]); partSearchEl.value = ''; partResults.style.display = 'none'; searchResults = []; highlightIdx = -1; partSearchEl.focus(); } } partSearchEl.addEventListener('input', doPartSearch); partSearchEl.addEventListener('keydown', function (e) { if (partResults.style.display === 'none' || searchResults.length === 0) return; if (e.key === 'ArrowDown') { e.preventDefault(); highlightIdx = Math.min(highlightIdx + 1, searchResults.length - 1); updateHighlight(); } else if (e.key === 'ArrowUp') { e.preventDefault(); highlightIdx = Math.max(highlightIdx - 1, 0); updateHighlight(); } else if (e.key === 'Enter') { e.preventDefault(); if (highlightIdx >= 0) { selectSearchResult(highlightIdx); } else if (searchResults.length === 1) { selectSearchResult(0); } } else if (e.key === 'Escape') { partResults.style.display = 'none'; highlightIdx = -1; } }); partSearchEl.addEventListener('focus', function () { if (searchResults.length > 0) { partResults.style.display = 'block'; } }); partSearchEl.addEventListener('blur', function () { setTimeout(function () { partResults.style.display = 'none'; }, 200); }); // ================================================================ // Cart // ================================================================ function addToCart(part) { cart.push({ part_id: part.part_type === 'oem' ? part.id_part : null, aftermarket_id: part.part_type === 'aftermarket' ? part.id_part : null, description: (part.oem_part_number || '') + ' - ' + (part.name_part || ''), part_type: part.part_type, quantity: 1, unit_cost: part.cost_usd || 0, margin_pct: defaultMargin, unit_price: (part.cost_usd || 0) * (1 + defaultMargin / 100) }); renderCart(); partSearchEl.focus(); } function renderCart() { var tbody = document.getElementById('cart-body'); if (cart.length === 0) { tbody.innerHTML = 'Busca y agrega partes al carrito'; updateTotals(); return; } tbody.innerHTML = cart.map(function (item, i) { var lineTotal = item.quantity * item.unit_price; return '' + '' + esc(item.description) + '' + '' + item.part_type + '' + '' + '' + '%' + '' + fmt(item.unit_price) + '' + '' + fmt(lineTotal) + '' + '' + ''; }).join(''); // Input change handlers tbody.querySelectorAll('input').forEach(function (input) { input.addEventListener('change', function () { var idx = parseInt(input.getAttribute('data-idx')); var field = input.getAttribute('data-field'); var val = parseFloat(input.value) || 0; cart[idx][field] = val; // Recalculate price from cost + margin if (field === 'unit_cost' || field === 'margin_pct') { cart[idx].unit_price = cart[idx].unit_cost * (1 + cart[idx].margin_pct / 100); } renderCart(); }); }); // Remove handlers tbody.querySelectorAll('.cart-remove').forEach(function (btn) { btn.addEventListener('click', function () { cart.splice(parseInt(btn.getAttribute('data-idx')), 1); renderCart(); }); }); updateTotals(); } function updateTotals() { var itemCount = cart.reduce(function (sum, it) { return sum + it.quantity; }, 0); var subtotal = cart.reduce(function (sum, it) { return sum + (it.quantity * it.unit_price); }, 0); var tax = subtotal * 0.16; var total = subtotal + tax; document.getElementById('sum-items').textContent = itemCount; document.getElementById('sum-subtotal').textContent = fmt(subtotal); document.getElementById('sum-tax').textContent = fmt(tax); document.getElementById('sum-total').textContent = fmt(total); updateFacturarBtn(); } function updateFacturarBtn() { document.getElementById('btn-facturar').disabled = !(selectedCustomer && cart.length > 0); } // ================================================================ // Facturar // ================================================================ document.getElementById('btn-facturar').addEventListener('click', function () { if (!selectedCustomer || cart.length === 0) return; var btn = this; btn.disabled = true; btn.textContent = 'Generando...'; var items = cart.map(function (it) { return { part_id: it.part_id, aftermarket_id: it.aftermarket_id, description: it.description, quantity: it.quantity, unit_cost: it.unit_cost, margin_pct: it.margin_pct, unit_price: Math.round(it.unit_price * 100) / 100 }; }); api('/api/pos/invoices', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ customer_id: selectedCustomer.id_customer, items: items, notes: document.getElementById('invoice-notes').value.trim() }) }).then(function (res) { toast('Factura ' + res.folio + ' creada por ' + fmt(res.total)); // Reset cart cart = []; renderCart(); document.getElementById('invoice-notes').value = ''; // Refresh customer balance selectCustomer(selectedCustomer.id_customer); btn.textContent = 'Facturar'; }).catch(function (err) { toast(err.message, 'error'); btn.disabled = false; btn.textContent = 'Facturar'; }); }); // ================================================================ // Init // ================================================================ renderCart(); })();