Files
Autoparts-DB/dashboard/pos.js
2026-03-18 22:25:32 +00:00

414 lines
17 KiB
JavaScript

/**
* 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 = '<div style="padding:0.8rem;color:var(--text-secondary)">No se encontraron clientes</div>';
} else {
customerDropdown.innerHTML = data.map(function (c) {
return '<div class="customer-dropdown-item" data-id="' + c.id_customer + '">' +
'<div><span class="cdi-name">' + esc(c.name) + '</span>' +
(c.rfc ? ' <span class="cdi-rfc">' + esc(c.rfc) + '</span>' : '') + '</div>' +
'<span style="font-size:0.8rem;color:' + (c.balance > 0 ? 'var(--danger)' : 'var(--success)') + '">' +
fmt(c.balance) + '</span></div>';
}).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 = '<div style="padding:0.8rem;color:var(--text-secondary)">No se encontraron partes para "' + esc(partSearchEl.value) + '"</div>';
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 '<div class="part-result-item' + active + '" data-idx="' + i + '">' +
'<div><span class="pri-number">' + esc(p.oem_part_number) + '</span>' +
'<span class="pri-name">' + esc(p.name_part) + '</span></div>' +
'<div style="display:flex;align-items:center;gap:0.4rem">' +
'<span class="pri-type ' + p.part_type + '">' + p.part_type + '</span>' +
(p.cost_usd ? '<span style="font-size:0.8rem;color:var(--text-secondary)">' + fmt(p.cost_usd) + '</span>' : '') +
'<span style="font-size:0.75rem;color:var(--text-secondary)">' + esc(p.group_name || '') + '</span>' +
'</div></div>';
}).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 = '<tr><td colspan="8" class="cart-empty">Busca y agrega partes al carrito</td></tr>';
updateTotals();
return;
}
tbody.innerHTML = cart.map(function (item, i) {
var lineTotal = item.quantity * item.unit_price;
return '<tr>' +
'<td class="cart-desc">' + esc(item.description) + '</td>' +
'<td><span class="pri-type ' + item.part_type + '">' + item.part_type + '</span></td>' +
'<td><input class="cart-qty" type="number" min="1" value="' + item.quantity + '" data-idx="' + i + '" data-field="quantity"></td>' +
'<td><input class="cart-cost" type="number" step="0.01" value="' + item.unit_cost.toFixed(2) + '" data-idx="' + i + '" data-field="unit_cost"></td>' +
'<td><input class="cart-margin" type="number" step="1" value="' + item.margin_pct.toFixed(0) + '" data-idx="' + i + '" data-field="margin_pct">%</td>' +
'<td>' + fmt(item.unit_price) + '</td>' +
'<td>' + fmt(lineTotal) + '</td>' +
'<td><button class="cart-remove" data-idx="' + i + '">&times;</button></td>' +
'</tr>';
}).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();
})();