From ab655e2ff1c8b6a1c01a37ff5179b8596e771000 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Tue, 31 Mar 2026 02:13:03 +0000 Subject: [PATCH] =?UTF-8?q?feat(pos):=20add=20inventory=20management=20UI?= =?UTF-8?q?=20=E2=80=94=20products,=20purchases,=20adjustments,=20alerts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- pos/static/js/inventory.js | 298 +++++++++++++++++++++++++++++++++++ pos/templates/inventory.html | 191 ++++++++++++++++++++++ 2 files changed, 489 insertions(+) create mode 100644 pos/static/js/inventory.js create mode 100644 pos/templates/inventory.html diff --git a/pos/static/js/inventory.js b/pos/static/js/inventory.js new file mode 100644 index 0000000..c0a0228 --- /dev/null +++ b/pos/static/js/inventory.js @@ -0,0 +1,298 @@ +// /home/Autopartes/pos/static/js/inventory.js +// Inventory management UI: CRUD, purchases, adjustments, transfers, physical count, alerts + +(function () { + 'use strict'; + + const API = '/pos/api/inventory'; + const token = localStorage.getItem('pos_token'); + if (!token) { window.location.href = '/pos/login'; return; } + + const headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }; + let currentPage = 1; + let currentSearch = ''; + let draftCountId = null; + + // --- API helper --- + async function apiFetch(url, opts) { + const resp = await fetch(url, Object.assign({ headers: headers }, opts || {})); + if (resp.status === 401) { localStorage.removeItem('pos_token'); window.location.href = '/pos/login'; return null; } + return resp.json(); + } + + // --- Tab switching --- + document.querySelectorAll('.tab').forEach(function (tab) { + tab.addEventListener('click', function () { + document.querySelectorAll('.tab').forEach(function (t) { t.classList.remove('active'); }); + document.querySelectorAll('.tab-content').forEach(function (c) { c.classList.remove('active'); }); + tab.classList.add('active'); + document.getElementById('tab-' + tab.dataset.tab).classList.add('active'); + + if (tab.dataset.tab === 'alerts') loadAlerts(); + }); + }); + + // --- Products --- + async function loadItems(page, search) { + currentPage = page || 1; + currentSearch = search !== undefined ? search : currentSearch; + var params = new URLSearchParams({ page: currentPage, per_page: 50 }); + if (currentSearch) params.set('q', currentSearch); + + var data = await apiFetch(API + '/items?' + params.toString()); + if (!data) return; + + var tbody = document.getElementById('productTableBody'); + var items = data.data || []; + if (!items.length) { tbody.innerHTML = 'Sin productos'; return; } + + tbody.innerHTML = items.map(function (it) { + return '' + + '' + esc(it.barcode) + '' + + '' + esc(it.part_number) + '' + + '' + esc(it.name) + '' + + '' + esc(it.brand) + '' + + '' + it.stock + '' + + '$' + fmt(it.cost) + '' + + '$' + fmt(it.price_1) + '' + + '$' + fmt(it.price_2) + '' + + '$' + fmt(it.price_3) + '' + + '' + esc(it.location) + '' + + ' ' + + '' + + ''; + }).join(''); + + // Pagination + var pg = data.pagination || {}; + var pgEl = document.getElementById('productPagination'); + if (pg.total_pages > 1) { + pgEl.innerHTML = '' + + '' + pg.page + ' / ' + pg.total_pages + ' (' + pg.total + ' items)' + + ''; + } else { + pgEl.innerHTML = '' + (pg.total || 0) + ' productos'; + } + } + + // Search + var searchInput = document.getElementById('productSearch'); + var searchTimeout; + searchInput.addEventListener('input', function () { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(function () { loadItems(1, searchInput.value.trim()); }, 350); + }); + + // --- Create item --- + async function createItem() { + var data = { + part_number: document.getElementById('newPartNumber').value.trim(), + name: document.getElementById('newName').value.trim(), + brand: document.getElementById('newBrand').value.trim(), + barcode: document.getElementById('newBarcode').value.trim() || undefined, + cost: parseFloat(document.getElementById('newCost').value) || 0, + price_1: parseFloat(document.getElementById('newPrice1').value) || 0, + price_2: parseFloat(document.getElementById('newPrice2').value) || 0, + price_3: parseFloat(document.getElementById('newPrice3').value) || 0, + min_stock: parseInt(document.getElementById('newMinStock').value) || 0, + initial_stock: parseInt(document.getElementById('newInitialStock').value) || 0, + location: document.getElementById('newLocation').value.trim() + }; + if (!data.part_number || !data.name) { document.getElementById('createResult').innerHTML = 'Numero de parte y nombre son obligatorios'; return; } + + var result = await apiFetch(API + '/items', { method: 'POST', body: JSON.stringify(data) }); + if (result && result.id) { + document.getElementById('createResult').innerHTML = 'Creado ID ' + result.id + ' | Barcode: ' + result.barcode + ''; + loadItems(currentPage); + } else { + document.getElementById('createResult').innerHTML = '' + (result ? result.error || 'Error' : 'Error de red') + ''; + } + } + + // --- Purchase --- + async function recordPurchase() { + var data = { + inventory_id: parseInt(document.getElementById('purchaseItemId').value), + quantity: parseInt(document.getElementById('purchaseQty').value), + unit_cost: parseFloat(document.getElementById('purchaseCost').value), + supplier_invoice: document.getElementById('purchaseInvoice').value.trim(), + notes: document.getElementById('purchaseNotes').value.trim() + }; + if (!data.inventory_id || !data.quantity || !data.unit_cost) { + document.getElementById('purchaseResult').innerHTML = 'Complete todos los campos'; return; + } + var result = await apiFetch(API + '/purchase', { method: 'POST', body: JSON.stringify(data) }); + document.getElementById('purchaseResult').innerHTML = result && result.operation_id + ? 'Compra registrada (op #' + result.operation_id + ')' + : '' + (result ? result.error || 'Error' : 'Error de red') + ''; + } + + // --- Adjustment --- + async function recordAdjustment() { + var data = { + inventory_id: parseInt(document.getElementById('adjustItemId').value), + quantity: parseInt(document.getElementById('adjustQty').value), + reason: document.getElementById('adjustReason').value.trim() + }; + if (!data.inventory_id || data.quantity === undefined || !data.reason) { + document.getElementById('adjustResult').innerHTML = 'Complete todos los campos (razon obligatoria)'; return; + } + var result = await apiFetch(API + '/adjustment', { method: 'POST', body: JSON.stringify(data) }); + document.getElementById('adjustResult').innerHTML = result && result.operation_id + ? 'Ajuste registrado (op #' + result.operation_id + ')' + : '' + (result ? result.error || 'Error' : 'Error de red') + ''; + } + + // --- Transfer --- + async function recordTransfer() { + var data = { + inventory_id: parseInt(document.getElementById('transferItemId').value), + from_branch_id: parseInt(document.getElementById('transferFrom').value), + to_branch_id: parseInt(document.getElementById('transferTo').value), + quantity: parseInt(document.getElementById('transferQty').value), + notes: document.getElementById('transferNotes').value.trim() + }; + if (!data.inventory_id || !data.from_branch_id || !data.to_branch_id || !data.quantity) { + document.getElementById('transferResult').innerHTML = 'Complete todos los campos'; return; + } + var result = await apiFetch(API + '/transfer', { method: 'POST', body: JSON.stringify(data) }); + document.getElementById('transferResult').innerHTML = result && result.out_operation_id + ? 'Transferencia registrada' + : '' + (result ? result.error || 'Error' : 'Error de red') + ''; + } + + // --- Physical Count (two-phase) --- + function addCountLine() { + var container = document.getElementById('countLines'); + var row = document.createElement('div'); + row.className = 'count-row'; + row.innerHTML = '' + + '' + + ''; + container.appendChild(row); + } + + async function startPhysicalCount() { + var rows = document.querySelectorAll('.count-row'); + var items = []; + rows.forEach(function (row) { + var invId = parseInt(row.querySelector('.count-inv-id').value); + var qty = parseInt(row.querySelector('.count-qty').value); + if (invId && !isNaN(qty)) items.push({ inventory_id: invId, counted_quantity: qty }); + }); + if (!items.length) { alert('Agregue al menos una linea'); return; } + + var result = await apiFetch(API + '/physical-count/start', { method: 'POST', body: JSON.stringify({ items: items }) }); + if (!result || !result.count_id) { + document.getElementById('countResults').innerHTML = '' + (result ? result.error || 'Error' : 'Error de red') + ''; + return; + } + + draftCountId = result.count_id; + var html = '

Borrador #' + result.count_id + ' — ' + result.message + '

'; + html += ''; + (result.results || []).forEach(function (r) { + var color = r.difference === 0 ? '#16a34a' : (r.difference < 0 ? '#dc2626' : '#ca8a04'); + html += ''; + }); + html += '
IDEsperadoContadoDiferencia
' + r.inventory_id + '' + r.expected + '' + r.counted + '' + (r.difference > 0 ? '+' : '') + r.difference + '
'; + html += ''; + html += ' '; + document.getElementById('countResults').innerHTML = html; + } + + async function approvePhysicalCount() { + if (!draftCountId) { alert('No hay borrador activo'); return; } + var result = await apiFetch(API + '/physical-count/approve', { method: 'POST', body: JSON.stringify({ count_id: draftCountId }) }); + if (result && result.status === 'approved') { + document.getElementById('countResults').innerHTML = '' + result.message + ''; + draftCountId = null; + } else { + document.getElementById('countResults').innerHTML += '
' + (result ? result.error || 'Error' : 'Error de red') + ''; + } + } + + function cancelDraft() { + draftCountId = null; + document.getElementById('countResults').innerHTML = 'Borrador cancelado'; + } + + // --- Alerts --- + async function loadAlerts() { + var data = await apiFetch(API + '/alerts'); + if (!data) return; + var el = document.getElementById('alertsList'); + var alerts = data.data || []; + if (!alerts.length) { el.innerHTML = '

Sin alertas activas

'; return; } + + el.innerHTML = alerts.map(function (a) { + var cls = a.severity === 'critical' ? 'alert-critical' : (a.severity === 'warning' ? 'alert-warning' : 'alert-info'); + var icon = a.type === 'zero' ? 'AGOTADO' : (a.type === 'low' ? 'BAJO' : 'EXCESO'); + return '
' + + '
[' + icon + '] ' + esc(a.part_number) + ' — ' + esc(a.name) + ' | Stock: ' + a.stock + + (a.min_stock ? ' (min: ' + a.min_stock + ')' : '') + (a.max_stock ? ' (max: ' + a.max_stock + ')' : '') + '
' + + 'Sucursal ' + a.branch_id + '
'; + }).join(''); + } + + // --- History modal --- + async function viewHistory(itemId) { + var data = await apiFetch(API + '/items/' + itemId + '/history'); + if (!data) return; + var history = data.data || []; + var html = ''; + if (!history.length) { html = '

Sin movimientos

'; } + else { + html = ''; + history.forEach(function (h) { + var qtyColor = h.quantity > 0 ? '#16a34a' : '#dc2626'; + html += ''; + }); + html += '
FechaTipoCantidadCostoEmpleadoNotas
' + h.date + '' + h.type + '' + (h.quantity > 0 ? '+' : '') + h.quantity + '' + (h.cost ? '$' + fmt(h.cost) : '-') + '' + esc(h.employee) + '' + esc(h.notes) + '
'; + } + document.getElementById('historyContent').innerHTML = html; + document.getElementById('historyModal').classList.add('show'); + } + + function closeHistoryModal() { document.getElementById('historyModal').classList.remove('show'); } + + // --- Create modal --- + function showCreateModal() { document.getElementById('createModal').classList.add('show'); } + function closeCreateModal() { document.getElementById('createModal').classList.remove('show'); } + + // --- Barcode label --- + function printBarcode(barcode, partNumber, name) { + var w = window.open('', '_blank', 'width=400,height=250'); + w.document.write('Etiqueta'); + w.document.write('

' + barcode + '

'); + w.document.write('

' + partNumber + '

'); + w.document.write('

' + name + '

'); + w.document.write(''); + w.document.close(); + w.print(); + } + + // --- Helpers --- + function fmt(n) { return (parseFloat(n) || 0).toFixed(2); } + function esc(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } + + // --- Expose globals --- + window._loadItems = function (p) { loadItems(p); }; + window.viewHistory = viewHistory; + window.closeHistoryModal = closeHistoryModal; + window.showCreateModal = showCreateModal; + window.closeCreateModal = closeCreateModal; + window.createItem = createItem; + window.recordPurchase = recordPurchase; + window.recordAdjustment = recordAdjustment; + window.recordTransfer = recordTransfer; + window.addCountLine = addCountLine; + window.startPhysicalCount = startPhysicalCount; + window.approvePhysicalCount = approvePhysicalCount; + window.cancelDraft = cancelDraft; + window.loadAlerts = loadAlerts; + window.printBarcode = printBarcode; + + // --- Init --- + loadItems(1); +})(); diff --git a/pos/templates/inventory.html b/pos/templates/inventory.html new file mode 100644 index 0000000..44e6707 --- /dev/null +++ b/pos/templates/inventory.html @@ -0,0 +1,191 @@ + + + + + + + Inventario - Nexus POS + + + + +
+

Gestion de Inventario

+
+ +
+
Productos
+
Entradas
+
Ajustes
+
Transferencias
+
Toma Fisica
+
Alertas
+
+ + +
+
+ + +
+
+ + + + + +
Cod. BarrasNo. ParteNombreMarcaStockCostoP1P2P3UbicacionAcciones
+
+ +
+ + +
+

Registrar Entrada de Compra

+
+
+
+
+
+
+
+
+ +
+
+
+ + +
+

Ajuste Manual de Stock

+
+
+
+
+ +
+
+
+ + +
+

Transferencia entre Sucursales

+
+
+
+
+
+
+
+
+ +
+
+
+ + +
+

Toma Fisica de Inventario

+

Fase 1: Ingrese los conteos. Se generara un borrador con comparacion esperado vs contado. Fase 2: Apruebe para aplicar ajustes.

+
+
+
+ + + +
+
+ +
+ +
+
+
+ + +
+

Alertas de Stock

+ +
+
+ + + + + + + + + +