fix(pos): rewrite inventory.js to match design system HTML structure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,90 +1,133 @@
|
||||
// /home/Autopartes/pos/static/js/inventory.js
|
||||
// Inventory management UI: CRUD, purchases, adjustments, transfers, physical count, alerts
|
||||
// Inventory management UI — rewritten to match design-system HTML structure
|
||||
// Panels: panel-stock, panel-entradas, panel-salidas, panel-traspasos, panel-ajustes, panel-conteos, panel-alertas
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const API = '/pos/api/inventory';
|
||||
const token = localStorage.getItem('pos_token');
|
||||
var API = '/pos/api/inventory';
|
||||
var 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;
|
||||
var headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
|
||||
var currentPage = 1;
|
||||
var currentSearch = '';
|
||||
var 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();
|
||||
function apiFetch(url, opts) {
|
||||
return fetch(url, Object.assign({ headers: headers }, opts || {}))
|
||||
.then(function (resp) {
|
||||
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');
|
||||
// --- 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;
|
||||
}
|
||||
|
||||
if (tab.dataset.tab === 'alerts') loadAlerts();
|
||||
});
|
||||
});
|
||||
// =====================================================================
|
||||
// TAB SWITCHING — uses design-system switchTab() already in the HTML.
|
||||
// We hook into it to trigger data loads when tabs are activated.
|
||||
// =====================================================================
|
||||
|
||||
// --- Products ---
|
||||
async function loadItems(page, search) {
|
||||
var _origSwitchTab = window.switchTab;
|
||||
window.switchTab = function (name) {
|
||||
if (typeof _origSwitchTab === 'function') _origSwitchTab(name);
|
||||
if (name === 'alertas') loadAlerts();
|
||||
if (name === 'stock') loadItems(currentPage);
|
||||
};
|
||||
|
||||
// =====================================================================
|
||||
// STOCK / PRODUCTS (panel-stock)
|
||||
// =====================================================================
|
||||
|
||||
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;
|
||||
apiFetch(API + '/items?' + params.toString()).then(function (data) {
|
||||
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; }
|
||||
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:var(--color-text-muted);">Sin productos</td></tr>';
|
||||
document.getElementById('productPagination').innerHTML = '';
|
||||
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('');
|
||||
tbody.innerHTML = items.map(function (it) {
|
||||
return '<tr>' +
|
||||
'<td class="td--mono">' + esc(it.barcode) + '</td>' +
|
||||
'<td class="td--mono">' + esc(it.part_number) + '</td>' +
|
||||
'<td class="td--primary">' + esc(it.name) + '</td>' +
|
||||
'<td>' + esc(it.brand) + '</td>' +
|
||||
'<td style="text-align:right" class="td--primary">' + it.stock + '</td>' +
|
||||
'<td style="text-align:right" class="td--amount">$' + fmt(it.cost) + '</td>' +
|
||||
'<td style="text-align:right" class="td--amount">$' + fmt(it.price_1) + '</td>' +
|
||||
'<td style="text-align:right" class="td--amount">$' + fmt(it.price_2) + '</td>' +
|
||||
'<td style="text-align:right" class="td--amount">$' + fmt(it.price_3) + '</td>' +
|
||||
'<td>' + esc(it.location) + '</td>' +
|
||||
'<td>' +
|
||||
'<button class="btn btn--ghost btn--sm" onclick="viewHistory(' + it.id + ')">Historial</button> ' +
|
||||
'<button class="btn btn--ghost btn--sm" onclick="printBarcode(\'' + esc(it.barcode) + '\',\'' + esc(it.part_number) + '\',\'' + esc(it.name) + '\')">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>';
|
||||
}
|
||||
// Pagination
|
||||
var pg = data.pagination || {};
|
||||
var pgEl = document.getElementById('productPagination');
|
||||
if (pg.total_pages > 1) {
|
||||
pgEl.innerHTML =
|
||||
'<div class="pagination">' +
|
||||
'<button class="page-btn" ' + (pg.page <= 1 ? 'disabled' : 'onclick="window._loadItems(' + (pg.page - 1) + ')"') + '>‹</button>' +
|
||||
'<span style="padding:0 var(--space-2);font-size:var(--text-body-sm);color:var(--color-text-muted);">' + pg.page + ' / ' + pg.total_pages + ' (' + pg.total + ' productos)</span>' +
|
||||
'<button class="page-btn" ' + (pg.page >= pg.total_pages ? 'disabled' : 'onclick="window._loadItems(' + (pg.page + 1) + ')"') + '>›</button>' +
|
||||
'</div>';
|
||||
} else {
|
||||
pgEl.innerHTML = '<span style="font-size:var(--text-body-sm);color:var(--color-text-muted);">' + (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);
|
||||
});
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function () {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(function () {
|
||||
loadItems(1, searchInput.value.trim());
|
||||
}, 350);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Create item ---
|
||||
async function createItem() {
|
||||
// =====================================================================
|
||||
// CREATE ITEM (createModal)
|
||||
// =====================================================================
|
||||
|
||||
function showCreateModal() {
|
||||
document.getElementById('createModal').classList.add('is-open');
|
||||
}
|
||||
function closeCreateModal() {
|
||||
document.getElementById('createModal').classList.remove('is-open');
|
||||
document.getElementById('createResult').innerHTML = '';
|
||||
}
|
||||
|
||||
function createItem() {
|
||||
var data = {
|
||||
part_number: document.getElementById('newPartNumber').value.trim(),
|
||||
name: document.getElementById('newName').value.trim(),
|
||||
@@ -98,19 +141,33 @@
|
||||
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>';
|
||||
if (!data.part_number || !data.name) {
|
||||
document.getElementById('createResult').innerHTML = '<span style="color:var(--color-error);">Numero de parte y nombre son obligatorios</span>';
|
||||
return;
|
||||
}
|
||||
apiFetch(API + '/items', { method: 'POST', body: JSON.stringify(data) }).then(function (result) {
|
||||
if (result && result.id) {
|
||||
document.getElementById('createResult').innerHTML = '<span style="color:var(--color-success);">Creado ID ' + result.id + ' | Barcode: ' + result.barcode + '</span>';
|
||||
loadItems(currentPage);
|
||||
} else {
|
||||
document.getElementById('createResult').innerHTML = '<span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- Purchase ---
|
||||
async function recordPurchase() {
|
||||
// =====================================================================
|
||||
// PURCHASE / ENTRADA (purchaseModal)
|
||||
// =====================================================================
|
||||
|
||||
function showPurchaseModal() {
|
||||
document.getElementById('purchaseModal').classList.add('is-open');
|
||||
}
|
||||
function closePurchaseModal() {
|
||||
document.getElementById('purchaseModal').classList.remove('is-open');
|
||||
document.getElementById('purchaseResult').innerHTML = '';
|
||||
}
|
||||
|
||||
function recordPurchase() {
|
||||
var data = {
|
||||
inventory_id: parseInt(document.getElementById('purchaseItemId').value),
|
||||
quantity: parseInt(document.getElementById('purchaseQty').value),
|
||||
@@ -119,32 +176,58 @@
|
||||
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;
|
||||
document.getElementById('purchaseResult').innerHTML = '<span style="color:var(--color-error);">Complete todos los campos obligatorios</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>';
|
||||
apiFetch(API + '/purchase', { method: 'POST', body: JSON.stringify(data) }).then(function (result) {
|
||||
document.getElementById('purchaseResult').innerHTML = result && result.operation_id
|
||||
? '<span style="color:var(--color-success);">Compra registrada (op #' + result.operation_id + ')</span>'
|
||||
: '<span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
||||
});
|
||||
}
|
||||
|
||||
// --- Adjustment ---
|
||||
async function recordAdjustment() {
|
||||
// =====================================================================
|
||||
// ADJUSTMENT / AJUSTE (adjustmentModal)
|
||||
// =====================================================================
|
||||
|
||||
function showAdjustmentModal() {
|
||||
document.getElementById('adjustmentModal').classList.add('is-open');
|
||||
}
|
||||
function closeAdjustmentModal() {
|
||||
document.getElementById('adjustmentModal').classList.remove('is-open');
|
||||
document.getElementById('adjustResult').innerHTML = '';
|
||||
}
|
||||
|
||||
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;
|
||||
document.getElementById('adjustResult').innerHTML = '<span style="color:var(--color-error);">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>';
|
||||
apiFetch(API + '/adjustment', { method: 'POST', body: JSON.stringify(data) }).then(function (result) {
|
||||
document.getElementById('adjustResult').innerHTML = result && result.operation_id
|
||||
? '<span style="color:var(--color-success);">Ajuste registrado (op #' + result.operation_id + ')</span>'
|
||||
: '<span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
||||
});
|
||||
}
|
||||
|
||||
// --- Transfer ---
|
||||
async function recordTransfer() {
|
||||
// =====================================================================
|
||||
// TRANSFER / TRASPASO (transferModal)
|
||||
// =====================================================================
|
||||
|
||||
function showTransferModal() {
|
||||
document.getElementById('transferModal').classList.add('is-open');
|
||||
}
|
||||
function closeTransferModal() {
|
||||
document.getElementById('transferModal').classList.remove('is-open');
|
||||
document.getElementById('transferResult').innerHTML = '';
|
||||
}
|
||||
|
||||
function recordTransfer() {
|
||||
var data = {
|
||||
inventory_id: parseInt(document.getElementById('transferItemId').value),
|
||||
from_branch_id: parseInt(document.getElementById('transferFrom').value),
|
||||
@@ -153,27 +236,44 @@
|
||||
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;
|
||||
document.getElementById('transferResult').innerHTML = '<span style="color:var(--color-error);">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>';
|
||||
apiFetch(API + '/transfer', { method: 'POST', body: JSON.stringify(data) }).then(function (result) {
|
||||
document.getElementById('transferResult').innerHTML = result && result.out_operation_id
|
||||
? '<span style="color:var(--color-success);">Transferencia registrada</span>'
|
||||
: '<span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
||||
});
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// PHYSICAL COUNT / CONTEO (countModal)
|
||||
// =====================================================================
|
||||
|
||||
function showCountModal() {
|
||||
document.getElementById('countModal').classList.add('is-open');
|
||||
// Pre-add one line if empty
|
||||
if (!document.querySelectorAll('#countLines .count-row').length) {
|
||||
addCountLine();
|
||||
}
|
||||
}
|
||||
function closeCountModal() {
|
||||
document.getElementById('countModal').classList.remove('is-open');
|
||||
}
|
||||
|
||||
// --- 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>';
|
||||
row.innerHTML =
|
||||
'<input type="number" placeholder="ID producto" class="count-inv-id" style="width:140px;" />' +
|
||||
'<input type="number" placeholder="Cantidad contada" class="count-qty" style="width:160px;" />' +
|
||||
'<button class="btn btn--ghost btn--sm" onclick="this.parentElement.remove()">Quitar</button>';
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
async function startPhysicalCount() {
|
||||
var rows = document.querySelectorAll('.count-row');
|
||||
function startPhysicalCount() {
|
||||
var rows = document.querySelectorAll('#countLines .count-row');
|
||||
var items = [];
|
||||
rows.forEach(function (row) {
|
||||
var invId = parseInt(row.querySelector('.count-inv-id').value);
|
||||
@@ -182,85 +282,152 @@
|
||||
});
|
||||
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;
|
||||
}
|
||||
apiFetch(API + '/physical-count/start', { method: 'POST', body: JSON.stringify({ items: items }) }).then(function (result) {
|
||||
if (!result || !result.count_id) {
|
||||
document.getElementById('countResults').innerHTML = '<span style="color:var(--color-error);">' + (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>';
|
||||
draftCountId = result.count_id;
|
||||
var html = '<h4 style="margin-bottom:var(--space-3);">Borrador #' + result.count_id + ' — ' + esc(result.message) + '</h4>';
|
||||
html += '<table class="data-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 ? 'var(--color-success)' : (r.difference < 0 ? 'var(--color-error)' : 'var(--color-warning)');
|
||||
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 += '<div style="margin-top:var(--space-3);display:flex;gap:var(--space-2);">';
|
||||
html += '<button class="btn btn--primary btn--sm" onclick="approvePhysicalCount()">Aprobar y aplicar ajustes</button>';
|
||||
html += '<button class="btn btn--ghost btn--sm" onclick="cancelDraft()">Cancelar borrador</button>';
|
||||
html += '</div>';
|
||||
document.getElementById('countResults').innerHTML = html;
|
||||
});
|
||||
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() {
|
||||
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>';
|
||||
}
|
||||
apiFetch(API + '/physical-count/approve', { method: 'POST', body: JSON.stringify({ count_id: draftCountId }) }).then(function (result) {
|
||||
if (result && result.status === 'approved') {
|
||||
document.getElementById('countResults').innerHTML = '<span style="color:var(--color-success);">' + esc(result.message) + '</span>';
|
||||
draftCountId = null;
|
||||
} else {
|
||||
document.getElementById('countResults').innerHTML += '<br><span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cancelDraft() {
|
||||
draftCountId = null;
|
||||
document.getElementById('countResults').innerHTML = '<span style="color:#999;">Borrador cancelado</span>';
|
||||
document.getElementById('countResults').innerHTML = '<span style="color:var(--color-text-muted);">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; }
|
||||
// =====================================================================
|
||||
// ALERTS (panel-alertas)
|
||||
// =====================================================================
|
||||
|
||||
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('');
|
||||
function loadAlerts() {
|
||||
apiFetch(API + '/alerts').then(function (data) {
|
||||
if (!data) return;
|
||||
var alerts = data.data || [];
|
||||
var container = document.getElementById('alertsContent');
|
||||
if (!container) return;
|
||||
|
||||
if (!alerts.length) {
|
||||
container.innerHTML = '<p style="padding:var(--space-6);text-align:center;color:var(--color-text-muted);">Sin alertas activas</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
|
||||
// Group by severity
|
||||
var critical = alerts.filter(function (a) { return a.severity === 'critical'; });
|
||||
var warning = alerts.filter(function (a) { return a.severity === 'warning'; });
|
||||
var info = alerts.filter(function (a) { return a.severity !== 'critical' && a.severity !== 'warning'; });
|
||||
|
||||
if (critical.length) {
|
||||
html += '<div class="section-heading"><span class="section-heading__title">Criticas</span><div class="section-heading__line"></div><span class="badge badge--low">' + critical.length + '</span></div>';
|
||||
html += '<div class="alerts-grid" style="margin-bottom:var(--space-6);">';
|
||||
critical.forEach(function (a) {
|
||||
var icon = a.type === 'zero' ? 'AGOTADO' : (a.type === 'low' ? 'BAJO' : a.type.toUpperCase());
|
||||
html += buildAlertCard(a, icon, 'critical');
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (warning.length) {
|
||||
html += '<div class="section-heading"><span class="section-heading__title">Advertencias</span><div class="section-heading__line"></div><span class="badge badge--over">' + warning.length + '</span></div>';
|
||||
html += '<div class="alerts-grid" style="margin-bottom:var(--space-6);">';
|
||||
warning.forEach(function (a) {
|
||||
html += buildAlertCard(a, 'EXCESO', 'warning');
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (info.length) {
|
||||
html += '<div class="section-heading"><span class="section-heading__title">Informativas</span><div class="section-heading__line"></div><span class="badge badge--ok">' + info.length + '</span></div>';
|
||||
html += '<div class="alerts-grid" style="margin-bottom:var(--space-6);">';
|
||||
info.forEach(function (a) {
|
||||
html += buildAlertCard(a, 'INFO', 'info');
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
});
|
||||
}
|
||||
|
||||
// --- 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 buildAlertCard(a, icon, level) {
|
||||
var cls = level === 'critical' ? 'alert-card--critical' : (level === 'warning' ? 'alert-card--warning' : 'alert-card--info');
|
||||
return '<div class="alert-card ' + cls + '">' +
|
||||
'<div class="alert-card__icon"><svg viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>' +
|
||||
'<div class="alert-card__body">' +
|
||||
'<div class="alert-card__title">[' + icon + '] ' + esc(a.part_number) + ' — ' + esc(a.name) + '</div>' +
|
||||
'<div class="alert-card__desc">Stock: ' + a.stock +
|
||||
(a.min_stock ? ' (min: ' + a.min_stock + ')' : '') +
|
||||
(a.max_stock ? ' (max: ' + a.max_stock + ')' : '') +
|
||||
' · Sucursal ' + a.branch_id + '</div>' +
|
||||
'</div></div>';
|
||||
}
|
||||
|
||||
function closeHistoryModal() { document.getElementById('historyModal').classList.remove('show'); }
|
||||
// =====================================================================
|
||||
// HISTORY MODAL
|
||||
// =====================================================================
|
||||
|
||||
// --- Create modal ---
|
||||
function showCreateModal() { document.getElementById('createModal').classList.add('show'); }
|
||||
function closeCreateModal() { document.getElementById('createModal').classList.remove('show'); }
|
||||
function viewHistory(itemId) {
|
||||
apiFetch(API + '/items/' + itemId + '/history').then(function (data) {
|
||||
if (!data) return;
|
||||
var history = data.data || [];
|
||||
var html = '';
|
||||
if (!history.length) {
|
||||
html = '<p style="color:var(--color-text-muted);text-align:center;padding:var(--space-4);">Sin movimientos</p>';
|
||||
} else {
|
||||
html = '<table class="data-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 ? 'var(--color-success)' : 'var(--color-error)';
|
||||
html += '<tr>' +
|
||||
'<td style="font-size:var(--text-caption);">' + esc(h.date) + '</td>' +
|
||||
'<td>' + esc(h.type) + '</td>' +
|
||||
'<td style="color:' + qtyColor + ';font-weight:600;">' + (h.quantity > 0 ? '+' : '') + h.quantity + '</td>' +
|
||||
'<td class="td--amount">' + (h.cost ? '$' + fmt(h.cost) : '—') + '</td>' +
|
||||
'<td>' + esc(h.employee) + '</td>' +
|
||||
'<td style="font-size:var(--text-caption);">' + esc(h.notes) + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
}
|
||||
document.getElementById('historyContent').innerHTML = html;
|
||||
document.getElementById('historyModal').classList.add('is-open');
|
||||
});
|
||||
}
|
||||
|
||||
function closeHistoryModal() {
|
||||
document.getElementById('historyModal').classList.remove('is-open');
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// BARCODE LABEL PRINT
|
||||
// =====================================================================
|
||||
|
||||
// --- 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>');
|
||||
@@ -272,20 +439,27 @@
|
||||
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 (for onclick handlers in HTML)
|
||||
// =====================================================================
|
||||
|
||||
// --- Expose globals ---
|
||||
window._loadItems = function (p) { loadItems(p); };
|
||||
window.viewHistory = viewHistory;
|
||||
window.closeHistoryModal = closeHistoryModal;
|
||||
window.showCreateModal = showCreateModal;
|
||||
window.closeCreateModal = closeCreateModal;
|
||||
window.createItem = createItem;
|
||||
window.showPurchaseModal = showPurchaseModal;
|
||||
window.closePurchaseModal = closePurchaseModal;
|
||||
window.recordPurchase = recordPurchase;
|
||||
window.showAdjustmentModal = showAdjustmentModal;
|
||||
window.closeAdjustmentModal = closeAdjustmentModal;
|
||||
window.recordAdjustment = recordAdjustment;
|
||||
window.showTransferModal = showTransferModal;
|
||||
window.closeTransferModal = closeTransferModal;
|
||||
window.recordTransfer = recordTransfer;
|
||||
window.showCountModal = showCountModal;
|
||||
window.closeCountModal = closeCountModal;
|
||||
window.addCountLine = addCountLine;
|
||||
window.startPhysicalCount = startPhysicalCount;
|
||||
window.approvePhysicalCount = approvePhysicalCount;
|
||||
@@ -293,6 +467,9 @@
|
||||
window.loadAlerts = loadAlerts;
|
||||
window.printBarcode = printBarcode;
|
||||
|
||||
// --- Init ---
|
||||
// =====================================================================
|
||||
// INIT — load stock on page load
|
||||
// =====================================================================
|
||||
|
||||
loadItems(1);
|
||||
})();
|
||||
|
||||
@@ -1172,6 +1172,141 @@
|
||||
padding: var(--space-1); opacity: 0.7; color: inherit;
|
||||
}
|
||||
.banner__dismiss:hover { opacity: 1; }
|
||||
|
||||
/* =========================================================================
|
||||
INVENTORY MODALS
|
||||
========================================================================= */
|
||||
|
||||
.inv-modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9000;
|
||||
background: rgba(0,0,0,0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.inv-modal-overlay.is-open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.inv-modal {
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
width: 520px;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.inv-modal--wide {
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
.inv-modal__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.inv-modal__header h3 {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: var(--heading-weight-primary);
|
||||
font-size: var(--text-h5);
|
||||
color: var(--color-text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.inv-modal__close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
padding: 0 var(--space-1);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.inv-modal__close:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.inv-modal__body {
|
||||
padding: var(--space-4) var(--space-5);
|
||||
}
|
||||
|
||||
.inv-modal__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-5);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.inv-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.inv-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.inv-field--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.inv-field label {
|
||||
font-size: var(--text-caption);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-muted);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.inv-field input {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--color-surface-1);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.inv-field input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-muted);
|
||||
}
|
||||
|
||||
.count-row {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.count-row input {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--color-surface-1);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
/* History table inside modal */
|
||||
.inv-modal .data-table { width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -1430,7 +1565,7 @@
|
||||
<div class="toolbar">
|
||||
<div class="search-box">
|
||||
<svg viewBox="0 0 24 24" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" placeholder="Buscar por SKU, nombre, marca…" />
|
||||
<input type="text" id="productSearch" placeholder="Buscar por SKU, nombre, marca…" />
|
||||
</div>
|
||||
<select class="select-filter">
|
||||
<option value="">Todas las marcas</option>
|
||||
@@ -1455,125 +1590,34 @@
|
||||
<svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Columnas
|
||||
</button>
|
||||
<button class="btn btn--primary btn--sm" onclick="showCreateModal()">
|
||||
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Nuevo Producto
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<table class="data-table" id="stockTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>SKU</th>
|
||||
<th>Nombre del Producto</th>
|
||||
<th>Barcode</th>
|
||||
<th>No. Parte</th>
|
||||
<th>Nombre</th>
|
||||
<th>Marca</th>
|
||||
<th>Categoría</th>
|
||||
<th style="text-align:right">Stock</th>
|
||||
<th style="text-align:right">Mín</th>
|
||||
<th style="text-align:right">Máx</th>
|
||||
<th style="text-align:right">Costo</th>
|
||||
<th style="text-align:right">Precio 1</th>
|
||||
<th style="text-align:right">Precio 2</th>
|
||||
<th style="text-align:right">Precio 3</th>
|
||||
<th>Ubicación</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td--mono">GAT-50104</td>
|
||||
<td class="td--primary">Banda de Tiempo Kit</td>
|
||||
<td>Gates</td>
|
||||
<td>Motor</td>
|
||||
<td style="text-align:right" class="td--primary">142</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">20</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">200</td>
|
||||
<td>A-12-3</td>
|
||||
<td><span class="badge badge--ok">OK</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td--mono">SKF-6204</td>
|
||||
<td class="td--primary">Rodamiento Rueda Delantera</td>
|
||||
<td>SKF</td>
|
||||
<td>Suspensión</td>
|
||||
<td style="text-align:right" class="td--primary">8</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">15</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">60</td>
|
||||
<td>B-05-1</td>
|
||||
<td><span class="badge badge--low">Bajo</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td--mono">BOC-0258</td>
|
||||
<td class="td--primary">Bujía Iridium BKR6EIX</td>
|
||||
<td>Bosch</td>
|
||||
<td>Eléctrico</td>
|
||||
<td style="text-align:right" class="td--primary">3</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">10</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">100</td>
|
||||
<td>C-08-2</td>
|
||||
<td><span class="badge badge--low">Bajo</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td--mono">MON-G8114</td>
|
||||
<td class="td--primary">Amortiguador Trasero Derecho</td>
|
||||
<td>Monroe</td>
|
||||
<td>Suspensión</td>
|
||||
<td style="text-align:right" class="td--primary">67</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">10</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">40</td>
|
||||
<td>B-14-5</td>
|
||||
<td><span class="badge badge--over">Sobrestock</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td--mono">NGK-LFR6A</td>
|
||||
<td class="td--primary">Bujía Laser Platinum LFR6A</td>
|
||||
<td>NGK</td>
|
||||
<td>Eléctrico</td>
|
||||
<td style="text-align:right" class="td--primary">88</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">25</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">120</td>
|
||||
<td>C-02-4</td>
|
||||
<td><span class="badge badge--ok">OK</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td--mono">BOC-F026</td>
|
||||
<td class="td--primary">Filtro de Aceite F026407006</td>
|
||||
<td>Bosch</td>
|
||||
<td>Filtros</td>
|
||||
<td style="text-align:right" class="td--primary">5</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">20</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">80</td>
|
||||
<td>D-01-2</td>
|
||||
<td><span class="badge badge--low">Bajo</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td--mono">ATE-13046</td>
|
||||
<td class="td--primary">Pastillas de Freno ATE Power</td>
|
||||
<td>ATE</td>
|
||||
<td>Frenos</td>
|
||||
<td style="text-align:right" class="td--primary">34</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">15</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">50</td>
|
||||
<td>E-03-1</td>
|
||||
<td><span class="badge badge--ok">OK</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td--mono">TRW-GDB1</td>
|
||||
<td class="td--primary">Disco de Freno Ventilado</td>
|
||||
<td>TRW</td>
|
||||
<td>Frenos</td>
|
||||
<td style="text-align:right" class="td--primary">12</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">10</td>
|
||||
<td style="text-align:right;color:var(--color-text-muted)">30</td>
|
||||
<td>E-07-3</td>
|
||||
<td><span class="badge badge--ok">OK</span></td>
|
||||
</tr>
|
||||
<tbody id="productTableBody">
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="table-footer">
|
||||
<span>Mostrando 1–8 de 4,817 productos</span>
|
||||
<div class="pagination">
|
||||
<button class="page-btn">‹</button>
|
||||
<button class="page-btn is-active">1</button>
|
||||
<button class="page-btn">2</button>
|
||||
<button class="page-btn">3</button>
|
||||
<span style="padding:0 var(--space-1);color:var(--color-text-muted)">…</span>
|
||||
<button class="page-btn">602</button>
|
||||
<button class="page-btn">›</button>
|
||||
</div>
|
||||
<div id="productPagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1594,7 +1638,7 @@
|
||||
</select>
|
||||
<input type="date" class="select-filter" value="2026-04-01" />
|
||||
<div class="toolbar__spacer"></div>
|
||||
<button class="btn btn--primary">
|
||||
<button class="btn btn--primary" onclick="showPurchaseModal()">
|
||||
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Nueva Entrada
|
||||
</button>
|
||||
@@ -1801,7 +1845,7 @@
|
||||
<option>En tránsito</option><option>Recibido</option><option>Pendiente</option>
|
||||
</select>
|
||||
<div class="toolbar__spacer"></div>
|
||||
<button class="btn btn--primary">
|
||||
<button class="btn btn--primary" onclick="showTransferModal()">
|
||||
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Nuevo Traspaso
|
||||
</button>
|
||||
@@ -1888,7 +1932,7 @@
|
||||
<option>Merma</option><option>Daño</option><option>Corrección</option>
|
||||
</select>
|
||||
<div class="toolbar__spacer"></div>
|
||||
<button class="btn btn--primary">
|
||||
<button class="btn btn--primary" onclick="showAdjustmentModal()">
|
||||
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Nuevo Ajuste
|
||||
</button>
|
||||
@@ -1983,7 +2027,7 @@
|
||||
<option>Completado</option><option>En proceso</option><option>Programado</option>
|
||||
</select>
|
||||
<div class="toolbar__spacer"></div>
|
||||
<button class="btn btn--primary">
|
||||
<button class="btn btn--primary" onclick="showCountModal()">
|
||||
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Programar Conteo
|
||||
</button>
|
||||
@@ -2072,7 +2116,7 @@
|
||||
TAB 7 — ALERTAS
|
||||
=================================================================== -->
|
||||
<div class="tab-panel" id="panel-alertas" role="tabpanel">
|
||||
|
||||
<div id="alertsContent">
|
||||
<!-- Low Stock -->
|
||||
<div class="section-heading">
|
||||
<span class="section-heading__title">Stock Bajo</span>
|
||||
@@ -2251,6 +2295,7 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div><!-- /alertsContent -->
|
||||
</div>
|
||||
|
||||
</div><!-- /tab-panels -->
|
||||
@@ -2334,6 +2379,142 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- ===== MODALS ===== -->
|
||||
|
||||
<!-- Create Item Modal -->
|
||||
<div class="inv-modal-overlay" id="createModal">
|
||||
<div class="inv-modal">
|
||||
<div class="inv-modal__header">
|
||||
<h3>Nuevo Producto</h3>
|
||||
<button class="inv-modal__close" onclick="closeCreateModal()">×</button>
|
||||
</div>
|
||||
<div class="inv-modal__body">
|
||||
<div class="inv-form-grid">
|
||||
<div class="inv-field"><label>No. Parte *</label><input type="text" id="newPartNumber" placeholder="Ej: GAT-50104" /></div>
|
||||
<div class="inv-field"><label>Nombre *</label><input type="text" id="newName" placeholder="Nombre del producto" /></div>
|
||||
<div class="inv-field"><label>Marca</label><input type="text" id="newBrand" placeholder="Marca" /></div>
|
||||
<div class="inv-field"><label>Barcode</label><input type="text" id="newBarcode" placeholder="Auto-generado si vacío" /></div>
|
||||
<div class="inv-field"><label>Costo</label><input type="number" id="newCost" step="0.01" placeholder="0.00" /></div>
|
||||
<div class="inv-field"><label>Precio 1</label><input type="number" id="newPrice1" step="0.01" placeholder="0.00" /></div>
|
||||
<div class="inv-field"><label>Precio 2</label><input type="number" id="newPrice2" step="0.01" placeholder="0.00" /></div>
|
||||
<div class="inv-field"><label>Precio 3</label><input type="number" id="newPrice3" step="0.01" placeholder="0.00" /></div>
|
||||
<div class="inv-field"><label>Stock Mínimo</label><input type="number" id="newMinStock" placeholder="0" /></div>
|
||||
<div class="inv-field"><label>Stock Inicial</label><input type="number" id="newInitialStock" placeholder="0" /></div>
|
||||
<div class="inv-field"><label>Ubicación</label><input type="text" id="newLocation" placeholder="Ej: A-12-3" /></div>
|
||||
</div>
|
||||
<div id="createResult" style="margin-top:var(--space-3);min-height:1.5em;"></div>
|
||||
</div>
|
||||
<div class="inv-modal__footer">
|
||||
<button class="btn btn--ghost" onclick="closeCreateModal()">Cancelar</button>
|
||||
<button class="btn btn--primary" onclick="createItem()">Crear Producto</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Purchase Modal -->
|
||||
<div class="inv-modal-overlay" id="purchaseModal">
|
||||
<div class="inv-modal">
|
||||
<div class="inv-modal__header">
|
||||
<h3>Registrar Compra / Entrada</h3>
|
||||
<button class="inv-modal__close" onclick="closePurchaseModal()">×</button>
|
||||
</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"><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>
|
||||
<div class="inv-field inv-field--full"><label>Notas</label><input type="text" id="purchaseNotes" placeholder="Notas adicionales" /></div>
|
||||
</div>
|
||||
<div id="purchaseResult" style="margin-top:var(--space-3);min-height:1.5em;"></div>
|
||||
</div>
|
||||
<div class="inv-modal__footer">
|
||||
<button class="btn btn--ghost" onclick="closePurchaseModal()">Cancelar</button>
|
||||
<button class="btn btn--primary" onclick="recordPurchase()">Registrar Compra</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transfer Modal -->
|
||||
<div class="inv-modal-overlay" id="transferModal">
|
||||
<div class="inv-modal">
|
||||
<div class="inv-modal__header">
|
||||
<h3>Nuevo Traspaso</h3>
|
||||
<button class="inv-modal__close" onclick="closeTransferModal()">×</button>
|
||||
</div>
|
||||
<div class="inv-modal__body">
|
||||
<div class="inv-form-grid">
|
||||
<div class="inv-field"><label>ID Producto *</label><input type="number" id="transferItemId" placeholder="ID inventario" /></div>
|
||||
<div class="inv-field"><label>Cantidad *</label><input type="number" id="transferQty" placeholder="Cantidad" /></div>
|
||||
<div class="inv-field"><label>Sucursal Origen *</label><input type="number" id="transferFrom" placeholder="ID sucursal origen" /></div>
|
||||
<div class="inv-field"><label>Sucursal Destino *</label><input type="number" id="transferTo" placeholder="ID sucursal destino" /></div>
|
||||
<div class="inv-field inv-field--full"><label>Notas</label><input type="text" id="transferNotes" placeholder="Notas adicionales" /></div>
|
||||
</div>
|
||||
<div id="transferResult" style="margin-top:var(--space-3);min-height:1.5em;"></div>
|
||||
</div>
|
||||
<div class="inv-modal__footer">
|
||||
<button class="btn btn--ghost" onclick="closeTransferModal()">Cancelar</button>
|
||||
<button class="btn btn--primary" onclick="recordTransfer()">Registrar Traspaso</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Adjustment Modal -->
|
||||
<div class="inv-modal-overlay" id="adjustmentModal">
|
||||
<div class="inv-modal">
|
||||
<div class="inv-modal__header">
|
||||
<h3>Nuevo Ajuste</h3>
|
||||
<button class="inv-modal__close" onclick="closeAdjustmentModal()">×</button>
|
||||
</div>
|
||||
<div class="inv-modal__body">
|
||||
<div class="inv-form-grid">
|
||||
<div class="inv-field"><label>ID Producto *</label><input type="number" id="adjustItemId" placeholder="ID inventario" /></div>
|
||||
<div class="inv-field"><label>Cantidad * (negativo=salida)</label><input type="number" id="adjustQty" placeholder="Ej: -3 o +5" /></div>
|
||||
<div class="inv-field inv-field--full"><label>Razón / Motivo *</label><input type="text" id="adjustReason" placeholder="Motivo del ajuste (obligatorio)" /></div>
|
||||
</div>
|
||||
<div id="adjustResult" style="margin-top:var(--space-3);min-height:1.5em;"></div>
|
||||
</div>
|
||||
<div class="inv-modal__footer">
|
||||
<button class="btn btn--ghost" onclick="closeAdjustmentModal()">Cancelar</button>
|
||||
<button class="btn btn--primary" onclick="recordAdjustment()">Registrar Ajuste</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Physical Count Modal -->
|
||||
<div class="inv-modal-overlay" id="countModal">
|
||||
<div class="inv-modal inv-modal--wide">
|
||||
<div class="inv-modal__header">
|
||||
<h3>Conteo Físico</h3>
|
||||
<button class="inv-modal__close" onclick="closeCountModal()">×</button>
|
||||
</div>
|
||||
<div class="inv-modal__body">
|
||||
<div id="countLines"></div>
|
||||
<button class="btn btn--ghost btn--sm" onclick="addCountLine()" style="margin-top:var(--space-2);">+ Agregar Línea</button>
|
||||
<div id="countResults" style="margin-top:var(--space-4);"></div>
|
||||
</div>
|
||||
<div class="inv-modal__footer">
|
||||
<button class="btn btn--ghost" onclick="closeCountModal()">Cerrar</button>
|
||||
<button class="btn btn--primary" onclick="startPhysicalCount()">Iniciar Conteo</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History Modal -->
|
||||
<div class="inv-modal-overlay" id="historyModal">
|
||||
<div class="inv-modal inv-modal--wide">
|
||||
<div class="inv-modal__header">
|
||||
<h3>Historial de Movimientos</h3>
|
||||
<button class="inv-modal__close" onclick="closeHistoryModal()">×</button>
|
||||
</div>
|
||||
<div class="inv-modal__body" id="historyContent">
|
||||
</div>
|
||||
<div class="inv-modal__footer">
|
||||
<button class="btn btn--ghost" onclick="closeHistoryModal()">Cerrar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Offline Banner -->
|
||||
<div id="offlineBanner" class="banner banner--warning" style="display:none;position:fixed;top:0;left:0;right:0;z-index:9999;border-radius:0;animation:none;">
|
||||
<span class="banner__icon"></span>
|
||||
|
||||
Reference in New Issue
Block a user