feat(pos/inventory): product search instead of ID in purchase entry modal

- Replace ID Producto input with autocomplete search by name/part number/barcode
- Support Enter key for barcode/part number exact match
- Keep hidden inventory_id field for API compatibility
- Bump inventory.js cache version
This commit is contained in:
2026-06-14 09:59:14 +00:00
parent 27358312dc
commit b78523102d
2 changed files with 134 additions and 7 deletions

View File

@@ -478,16 +478,140 @@
// PURCHASE / ENTRADA (purchaseModal)
// =====================================================================
let purchaseSearchTimeout = null;
let purchaseSelectedItem = null;
function showPurchaseModal() {
document.getElementById('purchaseModal').classList.add('is-open');
setTimeout(function() {
var el = document.getElementById('purchaseItemSearch');
if (el) el.focus();
}, 100);
}
function showPurchaseModalForItem(itemId) {
document.getElementById('purchaseItemId').value = itemId;
// Pre-fill by fetching item details
apiFetch(API + '/items?page=1&per_page=1').then(function() {
// We just need the item detail; use the existing list or fetch by id
apiFetch(API + '/items?page=1&per_page=1').then(function() {});
});
selectPurchaseItem({id: itemId, name: 'Producto #' + itemId});
showPurchaseModal();
}
function closePurchaseModal() {
document.getElementById('purchaseModal').classList.remove('is-open');
document.getElementById('purchaseResult').innerHTML = '';
clearPurchaseSelection();
}
function clearPurchaseSelection() {
purchaseSelectedItem = null;
var ids = ['purchaseItemId','purchaseItemSearch','purchaseQty','purchaseCost','purchaseInvoice','purchaseNotes'];
ids.forEach(function(id) {
var el = document.getElementById(id);
if (el) el.value = '';
});
var results = document.getElementById('purchaseItemResults');
if (results) results.style.display = 'none';
var selected = document.getElementById('purchaseItemSelected');
if (selected) selected.textContent = '';
}
function selectPurchaseItem(item) {
purchaseSelectedItem = item;
document.getElementById('purchaseItemId').value = item.id;
document.getElementById('purchaseItemSearch').value = item.name || item.part_number || item.barcode || ('#' + item.id);
document.getElementById('purchaseItemResults').style.display = 'none';
document.getElementById('purchaseItemSelected').innerHTML =
'<strong>' + esc(item.name || '') + '</strong>' +
(item.part_number ? ' · No. parte: ' + esc(item.part_number) : '') +
(item.barcode ? ' · Barcode: ' + esc(item.barcode) : '');
document.getElementById('purchaseQty').focus();
}
function searchPurchaseItems(query) {
var resultsEl = document.getElementById('purchaseItemResults');
if (!query || query.length < 2) {
resultsEl.style.display = 'none';
return;
}
apiFetch(API + '/items?q=' + encodeURIComponent(query) + '&per_page=10').then(function(res) {
var items = (res && res.items) || [];
if (!items.length) {
resultsEl.innerHTML = '<div style="padding:var(--space-3);color:var(--color-text-muted);font-size:var(--text-caption);">Sin resultados</div>';
resultsEl.style.display = 'block';
return;
}
resultsEl.innerHTML = items.map(function(it) {
return '<div class="purchase-search-result" style="padding:var(--space-3);cursor:pointer;border-bottom:1px solid var(--color-border);" ' +
'data-id="' + it.id + '">' +
'<div style="font-weight:var(--font-weight-semibold);">' + esc(it.name) + '</div>' +
'<div style="font-size:var(--text-caption);color:var(--color-text-muted);">' +
(it.part_number ? 'No. parte: ' + esc(it.part_number) + ' · ' : '') +
(it.barcode ? 'Barcode: ' + esc(it.barcode) + ' · ' : '') +
'Stock: ' + (it.stock || 0) +
'</div>' +
'</div>';
}).join('');
resultsEl.querySelectorAll('.purchase-search-result').forEach(function(row) {
row.onclick = function() {
var id = parseInt(row.dataset.id);
var item = items.find(function(x) { return x.id === id; });
if (item) selectPurchaseItem(item);
};
});
resultsEl.style.display = 'block';
}).catch(function() {
resultsEl.style.display = 'none';
});
}
function wirePurchaseSearch() {
var input = document.getElementById('purchaseItemSearch');
var resultsEl = document.getElementById('purchaseItemResults');
if (!input) return;
input.addEventListener('input', function() {
if (purchaseSelectedItem && input.value !== purchaseSelectedItem.name) {
purchaseSelectedItem = null;
document.getElementById('purchaseItemId').value = '';
document.getElementById('purchaseItemSelected').textContent = '';
}
clearTimeout(purchaseSearchTimeout);
purchaseSearchTimeout = setTimeout(function() {
searchPurchaseItems(input.value.trim());
}, 250);
});
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
// Try exact barcode match first
var query = input.value.trim();
if (!query) return;
apiFetch(API + '/items?q=' + encodeURIComponent(query) + '&per_page=20').then(function(res) {
var items = (res && res.items) || [];
var exact = items.find(function(it) {
return (it.barcode || '').toLowerCase() === query.toLowerCase() ||
(it.part_number || '').toLowerCase() === query.toLowerCase();
});
if (exact) {
selectPurchaseItem(exact);
} else if (items.length === 1) {
selectPurchaseItem(items[0]);
} else {
searchPurchaseItems(query);
}
});
} else if (e.key === 'Escape') {
if (resultsEl) resultsEl.style.display = 'none';
}
});
document.addEventListener('click', function(e) {
if (resultsEl && !input.contains(e.target) && !resultsEl.contains(e.target)) {
resultsEl.style.display = 'none';
}
});
}
function recordPurchase() {
@@ -506,10 +630,6 @@
if (result && result.operation_id) {
document.getElementById('purchaseResult').innerHTML = '<span style="color:var(--color-success);">Compra registrada (op #' + result.operation_id + ')</span>';
closePurchaseModal();
['purchaseItemId','purchaseQty','purchaseCost','purchaseInvoice','purchaseNotes'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.value = '';
});
if (window.loadInventoryStats) window.loadInventoryStats();
loadItems(currentPage);
} else {
@@ -2009,4 +2129,5 @@
loadItems(1);
renderSavedFilters();
wirePurchaseSearch();
})();

View File

@@ -760,7 +760,13 @@
</div>
<div class="inv-modal__body">
<div class="inv-form-grid">
<div class="inv-field"><label>ID Producto *</label><input type="number" id="purchaseItemId" placeholder="ID inventario" /></div>
<div class="inv-field inv-field--full" style="position:relative;">
<label>Producto *</label>
<input type="hidden" id="purchaseItemId" />
<input type="text" id="purchaseItemSearch" placeholder="Escribe nombre, No. de parte o escanea código de barras..." autocomplete="off" />
<div id="purchaseItemResults" style="position:absolute;left:0;right:0;top:100%;background:var(--color-bg-elevated);border:1px solid var(--color-border);border-radius:var(--radius-md);max-height:200px;overflow-y:auto;z-index:100;display:none;"></div>
<div id="purchaseItemSelected" style="margin-top:var(--space-1);font-size:var(--text-caption);color:var(--color-text-secondary);"></div>
</div>
<div class="inv-field"><label>Cantidad *</label><input type="number" id="purchaseQty" placeholder="Cantidad" /></div>
<div class="inv-field"><label>Costo Unitario *</label><input type="number" id="purchaseCost" step="0.01" placeholder="0.00" /></div>
<div class="inv-field"><label>Factura Proveedor</label><input type="text" id="purchaseInvoice" placeholder="No. factura" /></div>
@@ -1053,7 +1059,7 @@
<script src="/pos/static/js/pos-utils.js?v=2" defer></script>
<script src="/pos/static/js/sidebar.js" defer></script>
<script src="/pos/static/js/virtual-scroll.js?v=2" defer></script>
<script src="/pos/static/js/inventory.js?v=17" defer></script>
<script src="/pos/static/js/inventory.js?v=18" defer></script>
<script src="/pos/static/js/offline-banner.js" defer></script>
<script src="/pos/static/js/sync-engine.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>