feat(pos): add inventory management UI — products, purchases, adjustments, alerts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
298
pos/static/js/inventory.js
Normal file
298
pos/static/js/inventory.js
Normal file
@@ -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 = '<tr><td colspan="11" style="text-align:center;padding:30px;color:#999;">Sin productos</td></tr>'; return; }
|
||||
|
||||
tbody.innerHTML = items.map(function (it) {
|
||||
return '<tr>' +
|
||||
'<td style="font-family:monospace;font-size:0.8rem;">' + esc(it.barcode) + '</td>' +
|
||||
'<td>' + esc(it.part_number) + '</td>' +
|
||||
'<td><strong>' + esc(it.name) + '</strong></td>' +
|
||||
'<td>' + esc(it.brand) + '</td>' +
|
||||
'<td style="font-weight:600;">' + it.stock + '</td>' +
|
||||
'<td>$' + fmt(it.cost) + '</td>' +
|
||||
'<td>$' + fmt(it.price_1) + '</td>' +
|
||||
'<td>$' + fmt(it.price_2) + '</td>' +
|
||||
'<td>$' + fmt(it.price_3) + '</td>' +
|
||||
'<td>' + esc(it.location) + '</td>' +
|
||||
'<td><button class="btn btn-secondary" onclick="viewHistory(' + it.id + ')" style="padding:4px 8px;font-size:0.75rem;">Historial</button> ' +
|
||||
'<button class="btn btn-secondary" onclick="printBarcode(\'' + esc(it.barcode) + '\',\'' + esc(it.part_number) + '\',\'' + esc(it.name) + '\')" style="padding:4px 8px;font-size:0.75rem;">Etiqueta</button></td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
|
||||
// Pagination
|
||||
var pg = data.pagination || {};
|
||||
var pgEl = document.getElementById('productPagination');
|
||||
if (pg.total_pages > 1) {
|
||||
pgEl.innerHTML = '<button class="btn btn-secondary" ' + (pg.page <= 1 ? 'disabled' : 'onclick="window._loadItems(' + (pg.page - 1) + ')"') + '>«</button>' +
|
||||
'<span style="padding:6px 12px;font-size:0.85rem;">' + pg.page + ' / ' + pg.total_pages + ' (' + pg.total + ' items)</span>' +
|
||||
'<button class="btn btn-secondary" ' + (pg.page >= pg.total_pages ? 'disabled' : 'onclick="window._loadItems(' + (pg.page + 1) + ')"') + '>»</button>';
|
||||
} else {
|
||||
pgEl.innerHTML = '<span style="font-size:0.85rem;color:#999;">' + (pg.total || 0) + ' productos</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = '<span style="color:red;">Numero de parte y nombre son obligatorios</span>'; return; }
|
||||
|
||||
var result = await apiFetch(API + '/items', { method: 'POST', body: JSON.stringify(data) });
|
||||
if (result && result.id) {
|
||||
document.getElementById('createResult').innerHTML = '<span style="color:green;">Creado ID ' + result.id + ' | Barcode: ' + result.barcode + '</span>';
|
||||
loadItems(currentPage);
|
||||
} else {
|
||||
document.getElementById('createResult').innerHTML = '<span style="color:red;">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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 = '<span style="color:red;">Complete todos los campos</span>'; return;
|
||||
}
|
||||
var result = await apiFetch(API + '/purchase', { method: 'POST', body: JSON.stringify(data) });
|
||||
document.getElementById('purchaseResult').innerHTML = result && result.operation_id
|
||||
? '<span style="color:green;">Compra registrada (op #' + result.operation_id + ')</span>'
|
||||
: '<span style="color:red;">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
||||
}
|
||||
|
||||
// --- 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 = '<span style="color:red;">Complete todos los campos (razon obligatoria)</span>'; return;
|
||||
}
|
||||
var result = await apiFetch(API + '/adjustment', { method: 'POST', body: JSON.stringify(data) });
|
||||
document.getElementById('adjustResult').innerHTML = result && result.operation_id
|
||||
? '<span style="color:green;">Ajuste registrado (op #' + result.operation_id + ')</span>'
|
||||
: '<span style="color:red;">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
||||
}
|
||||
|
||||
// --- 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 = '<span style="color:red;">Complete todos los campos</span>'; return;
|
||||
}
|
||||
var result = await apiFetch(API + '/transfer', { method: 'POST', body: JSON.stringify(data) });
|
||||
document.getElementById('transferResult').innerHTML = result && result.out_operation_id
|
||||
? '<span style="color:green;">Transferencia registrada</span>'
|
||||
: '<span style="color:red;">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
||||
}
|
||||
|
||||
// --- Physical Count (two-phase) ---
|
||||
function addCountLine() {
|
||||
var container = document.getElementById('countLines');
|
||||
var row = document.createElement('div');
|
||||
row.className = 'count-row';
|
||||
row.innerHTML = '<input type="number" placeholder="ID producto" class="count-inv-id" style="width:120px;">' +
|
||||
'<input type="number" placeholder="Cantidad contada" class="count-qty" style="width:140px;">' +
|
||||
'<button class="btn btn-secondary" onclick="this.parentElement.remove()">Quitar</button>';
|
||||
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 = '<span style="color:red;">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
draftCountId = result.count_id;
|
||||
var html = '<h4>Borrador #' + result.count_id + ' — ' + result.message + '</h4>';
|
||||
html += '<table class="inv-table"><thead><tr><th>ID</th><th>Esperado</th><th>Contado</th><th>Diferencia</th></tr></thead><tbody>';
|
||||
(result.results || []).forEach(function (r) {
|
||||
var color = r.difference === 0 ? '#16a34a' : (r.difference < 0 ? '#dc2626' : '#ca8a04');
|
||||
html += '<tr><td>' + r.inventory_id + '</td><td>' + r.expected + '</td><td>' + r.counted + '</td><td style="color:' + color + ';font-weight:600;">' + (r.difference > 0 ? '+' : '') + r.difference + '</td></tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
html += '<button class="btn btn-primary" onclick="approvePhysicalCount()" style="margin-top:12px;">Aprobar y aplicar ajustes</button>';
|
||||
html += ' <button class="btn btn-secondary" onclick="cancelDraft()" style="margin-top:12px;">Cancelar</button>';
|
||||
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 = '<span style="color:green;">' + result.message + '</span>';
|
||||
draftCountId = null;
|
||||
} else {
|
||||
document.getElementById('countResults').innerHTML += '<br><span style="color:red;">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function cancelDraft() {
|
||||
draftCountId = null;
|
||||
document.getElementById('countResults').innerHTML = '<span style="color:#999;">Borrador cancelado</span>';
|
||||
}
|
||||
|
||||
// --- 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 = '<p style="color:#999;">Sin alertas activas</p>'; 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 '<div class="alert-card ' + cls + '">' +
|
||||
'<div><strong>[' + icon + ']</strong> ' + esc(a.part_number) + ' — ' + esc(a.name) + ' | Stock: ' + a.stock +
|
||||
(a.min_stock ? ' (min: ' + a.min_stock + ')' : '') + (a.max_stock ? ' (max: ' + a.max_stock + ')' : '') + '</div>' +
|
||||
'<span style="font-size:0.8rem;color:#888;">Sucursal ' + a.branch_id + '</span></div>';
|
||||
}).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 = '<p style="color:#999;">Sin movimientos</p>'; }
|
||||
else {
|
||||
html = '<table class="inv-table"><thead><tr><th>Fecha</th><th>Tipo</th><th>Cantidad</th><th>Costo</th><th>Empleado</th><th>Notas</th></tr></thead><tbody>';
|
||||
history.forEach(function (h) {
|
||||
var qtyColor = h.quantity > 0 ? '#16a34a' : '#dc2626';
|
||||
html += '<tr><td style="font-size:0.8rem;">' + h.date + '</td><td>' + h.type + '</td><td style="color:' + qtyColor + ';font-weight:600;">' + (h.quantity > 0 ? '+' : '') + h.quantity + '</td><td>' + (h.cost ? '$' + fmt(h.cost) : '-') + '</td><td>' + esc(h.employee) + '</td><td style="font-size:0.8rem;">' + esc(h.notes) + '</td></tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
}
|
||||
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('<html><head><title>Etiqueta</title><style>body{font-family:monospace;text-align:center;padding:20px;}h1{font-size:1.5rem;margin:8px 0;}p{margin:4px 0;}</style></head><body>');
|
||||
w.document.write('<h1>' + barcode + '</h1>');
|
||||
w.document.write('<p>' + partNumber + '</p>');
|
||||
w.document.write('<p style="font-size:0.85rem;">' + name + '</p>');
|
||||
w.document.write('</body></html>');
|
||||
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);
|
||||
})();
|
||||
191
pos/templates/inventory.html
Normal file
191
pos/templates/inventory.html
Normal file
@@ -0,0 +1,191 @@
|
||||
<!-- /home/Autopartes/pos/templates/inventory.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Inventario - Nexus POS</title>
|
||||
<link rel="stylesheet" href="/pos/static/css/common.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans, 'Inter', system-ui, sans-serif); background: var(--color-bg, #f5f5f5); }
|
||||
.header { padding: 12px 20px; background: var(--color-surface, #fff); border-bottom: 1px solid var(--color-border, #e5e5e5); }
|
||||
.header h1 { margin: 0; font-size: 1.2rem; }
|
||||
.tabs { display: flex; gap: 0; border-bottom: 2px solid var(--color-border, #e5e5e5); background: var(--color-surface, #fff); padding: 0 20px; overflow-x: auto; }
|
||||
.tab { padding: 10px 18px; cursor: pointer; font-size: 0.85rem; border-bottom: 2px solid transparent; margin-bottom: -2px; white-space: nowrap; color: #666; }
|
||||
.tab:hover { color: #333; }
|
||||
.tab.active { color: var(--color-primary, #2563eb); border-bottom-color: var(--color-primary, #2563eb); font-weight: 600; }
|
||||
.tab-content { display: none; padding: 20px; }
|
||||
.tab-content.active { display: block; }
|
||||
.inv-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
||||
.inv-table th { text-align: left; padding: 8px 10px; background: #f9f9f9; border-bottom: 2px solid #e5e5e5; font-weight: 600; font-size: 0.8rem; text-transform: uppercase; color: #666; }
|
||||
.inv-table td { padding: 8px 10px; border-bottom: 1px solid #eee; }
|
||||
.inv-table tr:hover { background: #f9fafb; }
|
||||
.form-group { margin-bottom: 14px; }
|
||||
.form-group label { display: block; font-size: 0.8rem; font-weight: 600; color: #555; margin-bottom: 4px; }
|
||||
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 8px 10px; border: 1px solid var(--color-border, #ddd); border-radius: var(--radius, 6px); font-size: 0.9rem; box-sizing: border-box; }
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.btn { padding: 8px 18px; border: none; border-radius: var(--radius, 6px); cursor: pointer; font-size: 0.85rem; }
|
||||
.btn-primary { background: var(--color-primary, #2563eb); color: #fff; }
|
||||
.btn-secondary { background: #f3f4f6; color: #333; border: 1px solid #ddd; }
|
||||
.btn-danger { background: #ef4444; color: #fff; }
|
||||
.alert-card { padding: 12px 16px; border-radius: var(--radius, 6px); margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; }
|
||||
.alert-critical { background: #fef2f2; border: 1px solid #fecaca; }
|
||||
.alert-warning { background: #fefce8; border: 1px solid #fef08a; }
|
||||
.alert-info { background: #eff6ff; border: 1px solid #bfdbfe; }
|
||||
.count-results { margin-top: 16px; }
|
||||
.count-row { display: flex; gap: 12px; align-items: center; margin-bottom: 8px; padding: 8px; background: #fff; border: 1px solid #eee; border-radius: 4px; }
|
||||
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 100; }
|
||||
.modal-overlay.show { display: flex; align-items: center; justify-content: center; }
|
||||
.modal { background: #fff; border-radius: 8px; padding: 24px; width: 600px; max-width: 90vw; max-height: 80vh; overflow-y: auto; }
|
||||
.search-row { display: flex; gap: 8px; margin-bottom: 16px; }
|
||||
.search-row input { flex: 1; padding: 8px 12px; border: 1px solid #ddd; border-radius: var(--radius, 6px); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Gestion de Inventario</h1>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="products">Productos</div>
|
||||
<div class="tab" data-tab="purchases">Entradas</div>
|
||||
<div class="tab" data-tab="adjustments">Ajustes</div>
|
||||
<div class="tab" data-tab="transfers">Transferencias</div>
|
||||
<div class="tab" data-tab="count">Toma Fisica</div>
|
||||
<div class="tab" data-tab="alerts">Alertas</div>
|
||||
</div>
|
||||
|
||||
<!-- Products tab -->
|
||||
<div class="tab-content active" id="tab-products">
|
||||
<div class="search-row">
|
||||
<input type="text" id="productSearch" placeholder="Buscar productos...">
|
||||
<button class="btn btn-primary" onclick="showCreateModal()">+ Nuevo producto</button>
|
||||
</div>
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="inv-table">
|
||||
<thead>
|
||||
<tr><th>Cod. Barras</th><th>No. Parte</th><th>Nombre</th><th>Marca</th><th>Stock</th><th>Costo</th><th>P1</th><th>P2</th><th>P3</th><th>Ubicacion</th><th>Acciones</th></tr>
|
||||
</thead>
|
||||
<tbody id="productTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="productPagination" class="pagination" style="margin-top:12px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Purchases tab -->
|
||||
<div class="tab-content" id="tab-purchases">
|
||||
<h3>Registrar Entrada de Compra</h3>
|
||||
<div style="max-width:500px;">
|
||||
<div class="form-group"><label>Producto (ID)</label><input type="number" id="purchaseItemId" placeholder="ID del producto"></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Cantidad</label><input type="number" id="purchaseQty" min="1" value="1"></div>
|
||||
<div class="form-group"><label>Costo unitario</label><input type="number" id="purchaseCost" step="0.01" placeholder="0.00"></div>
|
||||
</div>
|
||||
<div class="form-group"><label>Factura proveedor</label><input type="text" id="purchaseInvoice" placeholder="FAC-001"></div>
|
||||
<div class="form-group"><label>Notas</label><textarea id="purchaseNotes" rows="2"></textarea></div>
|
||||
<button class="btn btn-primary" onclick="recordPurchase()">Registrar compra</button>
|
||||
<div id="purchaseResult" style="margin-top:12px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Adjustments tab -->
|
||||
<div class="tab-content" id="tab-adjustments">
|
||||
<h3>Ajuste Manual de Stock</h3>
|
||||
<div style="max-width:500px;">
|
||||
<div class="form-group"><label>Producto (ID)</label><input type="number" id="adjustItemId" placeholder="ID del producto"></div>
|
||||
<div class="form-group"><label>Cantidad (+/-)</label><input type="number" id="adjustQty" placeholder="-2 o +5"></div>
|
||||
<div class="form-group"><label>Razon (obligatoria)</label><textarea id="adjustReason" rows="2" placeholder="Merma, error de conteo, etc."></textarea></div>
|
||||
<button class="btn btn-primary" onclick="recordAdjustment()">Registrar ajuste</button>
|
||||
<div id="adjustResult" style="margin-top:12px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transfers tab -->
|
||||
<div class="tab-content" id="tab-transfers">
|
||||
<h3>Transferencia entre Sucursales</h3>
|
||||
<div style="max-width:500px;">
|
||||
<div class="form-group"><label>Producto (ID)</label><input type="number" id="transferItemId" placeholder="ID del producto"></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Sucursal origen (ID)</label><input type="number" id="transferFrom"></div>
|
||||
<div class="form-group"><label>Sucursal destino (ID)</label><input type="number" id="transferTo"></div>
|
||||
</div>
|
||||
<div class="form-group"><label>Cantidad</label><input type="number" id="transferQty" min="1" value="1"></div>
|
||||
<div class="form-group"><label>Notas</label><textarea id="transferNotes" rows="2"></textarea></div>
|
||||
<button class="btn btn-primary" onclick="recordTransfer()">Transferir</button>
|
||||
<div id="transferResult" style="margin-top:12px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Physical Count tab -->
|
||||
<div class="tab-content" id="tab-count">
|
||||
<h3>Toma Fisica de Inventario</h3>
|
||||
<p style="font-size:0.85rem;color:#666;">Fase 1: Ingrese los conteos. Se generara un borrador con comparacion esperado vs contado. Fase 2: Apruebe para aplicar ajustes.</p>
|
||||
<div id="countForm">
|
||||
<div id="countLines">
|
||||
<div class="count-row">
|
||||
<input type="number" placeholder="ID producto" class="count-inv-id" style="width:120px;">
|
||||
<input type="number" placeholder="Cantidad contada" class="count-qty" style="width:140px;">
|
||||
<button class="btn btn-secondary" onclick="this.parentElement.remove()">Quitar</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" onclick="addCountLine()" style="margin:8px 0;">+ Agregar linea</button>
|
||||
<br>
|
||||
<button class="btn btn-primary" onclick="startPhysicalCount()">Crear borrador</button>
|
||||
</div>
|
||||
<div id="countResults" class="count-results"></div>
|
||||
</div>
|
||||
|
||||
<!-- Alerts tab -->
|
||||
<div class="tab-content" id="tab-alerts">
|
||||
<h3>Alertas de Stock</h3>
|
||||
<button class="btn btn-secondary" onclick="loadAlerts()" style="margin-bottom:12px;">Actualizar</button>
|
||||
<div id="alertsList"></div>
|
||||
</div>
|
||||
|
||||
<!-- History modal -->
|
||||
<div class="modal-overlay" id="historyModal">
|
||||
<div class="modal">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
|
||||
<h3 style="margin:0;">Historial de movimientos</h3>
|
||||
<button onclick="closeHistoryModal()" style="background:none;border:none;font-size:1.3rem;cursor:pointer;">×</button>
|
||||
</div>
|
||||
<div id="historyContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit modal -->
|
||||
<div class="modal-overlay" id="createModal">
|
||||
<div class="modal">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
|
||||
<h3 style="margin:0;" id="createModalTitle">Nuevo Producto</h3>
|
||||
<button onclick="closeCreateModal()" style="background:none;border:none;font-size:1.3rem;cursor:pointer;">×</button>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>No. Parte *</label><input type="text" id="newPartNumber"></div>
|
||||
<div class="form-group"><label>Nombre *</label><input type="text" id="newName"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Marca (fabricante)</label><input type="text" id="newBrand" placeholder="Bosch, NGK..."></div>
|
||||
<div class="form-group"><label>Codigo de barras</label><input type="text" id="newBarcode" placeholder="Auto-generado si vacio"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Costo</label><input type="number" id="newCost" step="0.01" value="0"></div>
|
||||
<div class="form-group"><label>Precio 1</label><input type="number" id="newPrice1" step="0.01" value="0"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Precio 2</label><input type="number" id="newPrice2" step="0.01" value="0"></div>
|
||||
<div class="form-group"><label>Precio 3</label><input type="number" id="newPrice3" step="0.01" value="0"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Stock minimo</label><input type="number" id="newMinStock" value="0"></div>
|
||||
<div class="form-group"><label>Stock inicial</label><input type="number" id="newInitialStock" value="0"></div>
|
||||
</div>
|
||||
<div class="form-group"><label>Ubicacion</label><input type="text" id="newLocation" placeholder="Pasillo A, Estante 3"></div>
|
||||
<button class="btn btn-primary" onclick="createItem()">Guardar</button>
|
||||
<div id="createResult" style="margin-top:8px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/pos/static/js/inventory.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user