962 lines
54 KiB
JavaScript
962 lines
54 KiB
JavaScript
// /home/Autopartes/pos/static/js/inventory.js
|
|
// 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';
|
|
|
|
var API = '/pos/api/inventory';
|
|
var token = localStorage.getItem('pos_token');
|
|
if (!token) { window.location.href = '/pos/login'; return; }
|
|
|
|
var headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
|
|
var currentPage = 1;
|
|
var currentSearch = '';
|
|
var draftCountId = null;
|
|
var inventoryVS = null;
|
|
var compatSource = 'both'; // default, loaded from config
|
|
|
|
// Load compatibility source setting
|
|
(function loadCompatSource() {
|
|
fetch('/pos/api/config/vehicle-compat-source', { headers: { 'Authorization': 'Bearer ' + token } })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(d) {
|
|
if (d.source) compatSource = d.source;
|
|
}).catch(function() {});
|
|
})();
|
|
|
|
// --- API helper ---
|
|
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();
|
|
});
|
|
}
|
|
|
|
// --- 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;
|
|
}
|
|
|
|
// =====================================================================
|
|
// TAB SWITCHING — uses design-system switchTab() already in the HTML.
|
|
// We hook into it to trigger data loads when tabs are activated.
|
|
// =====================================================================
|
|
|
|
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 renderInventoryRow(it) {
|
|
return '<tr style="cursor:pointer;" onclick="viewProductDetail(' + it.id + ')">' +
|
|
'<td class="td--mono" style="font-size:var(--text-caption);color:var(--color-text-muted);">' + it.id + '</td>' +
|
|
'<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="event.stopPropagation();viewHistory(' + it.id + ')">Historial</button> ' +
|
|
'<button class="btn btn--ghost btn--sm" style="color:var(--color-accent);" onclick="event.stopPropagation();showPurchaseModalForItem(' + it.id + ')">Entrada</button> ' +
|
|
'<button class="btn btn--ghost btn--sm" onclick="event.stopPropagation();printBarcode(\'' + esc(it.barcode) + '\',\'' + esc(it.part_number) + '\',\'' + esc(it.name) + '\')">Etiqueta</button>' +
|
|
'</td></tr>';
|
|
}
|
|
|
|
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);
|
|
|
|
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="12" style="text-align:center;padding:30px;color:var(--color-text-muted);">Sin productos</td></tr>';
|
|
document.getElementById('productPagination').innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
if (!inventoryVS) {
|
|
inventoryVS = new VirtualScroll({
|
|
container: tbody,
|
|
rowHeight: 48,
|
|
buffer: 3,
|
|
renderRow: renderInventoryRow,
|
|
emptyHtml: '<tr><td colspan="12" style="text-align:center;padding:30px;color:var(--color-text-muted);">Sin productos</td></tr>'
|
|
});
|
|
}
|
|
inventoryVS.setData(items);
|
|
|
|
// 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;
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', function () {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(function () {
|
|
loadItems(1, searchInput.value.trim());
|
|
}, 350);
|
|
});
|
|
}
|
|
|
|
// =====================================================================
|
|
// CREATE ITEM (createModal)
|
|
// =====================================================================
|
|
|
|
function showCreateModal() {
|
|
document.getElementById('createModal').classList.add('is-open');
|
|
// Attach AI classification on part number blur
|
|
var pnInput = document.getElementById('newPartNumber');
|
|
if (pnInput && !pnInput._classifyBound) {
|
|
pnInput._classifyBound = true;
|
|
pnInput.addEventListener('blur', function () {
|
|
var pn = this.value.trim();
|
|
if (pn.length < 3) return;
|
|
var nameInput = document.getElementById('newName');
|
|
// Only auto-classify if name is still empty
|
|
if (nameInput && nameInput.value.trim()) return;
|
|
classifyPartNumber(pn);
|
|
});
|
|
}
|
|
}
|
|
|
|
function classifyPartNumber(partNumber) {
|
|
var resultEl = document.getElementById('createResult');
|
|
resultEl.innerHTML = '<span style="color:var(--color-text-muted);">Consultando IA...</span>';
|
|
apiFetch(API + '/classify/' + encodeURIComponent(partNumber)).then(function (data) {
|
|
if (!data) return;
|
|
if (data.name) {
|
|
document.getElementById('newName').value = data.name;
|
|
}
|
|
if (data.brand) {
|
|
document.getElementById('newBrand').value = data.brand;
|
|
}
|
|
// Show suggestion label
|
|
var parts = [];
|
|
if (data.name) parts.push(data.name);
|
|
if (data.brand) parts.push(data.brand);
|
|
if (data.vehicle) parts.push(data.vehicle);
|
|
if (data.category) parts.push(data.category);
|
|
if (parts.length > 0) {
|
|
resultEl.innerHTML = '<span style="color:var(--color-accent);font-size:var(--text-caption);">Sugerido por IA: ' + esc(parts.join(' | ')) + '</span>';
|
|
} else {
|
|
resultEl.innerHTML = '<span style="color:var(--color-text-muted);font-size:var(--text-caption);">IA no pudo identificar este numero de parte</span>';
|
|
}
|
|
}).catch(function () {
|
|
resultEl.innerHTML = '';
|
|
});
|
|
}
|
|
|
|
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(),
|
|
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: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) {
|
|
var msg = 'Creado ID ' + result.id + ' | Barcode: ' + result.barcode;
|
|
if (result.vehicle_compatibilities_added > 0) {
|
|
msg += ' | ' + result.vehicle_compatibilities_added + ' vehiculo(s) asignado(s) por IA';
|
|
}
|
|
document.getElementById('createResult').innerHTML = '<span style="color:var(--color-success);">' + msg + '</span>';
|
|
loadItems(currentPage);
|
|
// Close modal, clear form, refresh badges
|
|
closeCreateModal();
|
|
['newPartNumber','newName','newBrand','newBarcode','newCost','newPrice1','newPrice2','newPrice3','newMinStock','newInitialStock','newLocation'].forEach(function(id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.value = '';
|
|
});
|
|
if (window.loadInventoryStats) window.loadInventoryStats();
|
|
} else {
|
|
document.getElementById('createResult').innerHTML = '<span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
|
}
|
|
});
|
|
}
|
|
|
|
// =====================================================================
|
|
// PURCHASE / ENTRADA (purchaseModal)
|
|
// =====================================================================
|
|
|
|
function showPurchaseModal() {
|
|
document.getElementById('purchaseModal').classList.add('is-open');
|
|
}
|
|
function showPurchaseModalForItem(itemId) {
|
|
document.getElementById('purchaseItemId').value = itemId;
|
|
showPurchaseModal();
|
|
}
|
|
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),
|
|
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:var(--color-error);">Complete todos los campos obligatorios</span>';
|
|
return;
|
|
}
|
|
apiFetch(API + '/purchase', { method: 'POST', body: JSON.stringify(data) }).then(function (result) {
|
|
if (result && result.operation_id) {
|
|
document.getElementById('purchaseResult').innerHTML = '<span style="color:var(--color-success);">Compra registrada (op #' + result.operation_id + ')</span>';
|
|
closePurchaseModal();
|
|
['purchaseItemId','purchaseQty','purchaseCost','purchaseInvoice','purchaseNotes'].forEach(function(id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.value = '';
|
|
});
|
|
if (window.loadInventoryStats) window.loadInventoryStats();
|
|
loadItems(currentPage);
|
|
} else {
|
|
document.getElementById('purchaseResult').innerHTML = '<span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
|
}
|
|
});
|
|
}
|
|
|
|
// =====================================================================
|
|
// 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:var(--color-error);">Complete todos los campos (razon obligatoria)</span>';
|
|
return;
|
|
}
|
|
apiFetch(API + '/adjustment', { method: 'POST', body: JSON.stringify(data) }).then(function (result) {
|
|
if (result && result.operation_id) {
|
|
document.getElementById('adjustResult').innerHTML = '<span style="color:var(--color-success);">Ajuste registrado (op #' + result.operation_id + ')</span>';
|
|
closeAdjustmentModal();
|
|
['adjustItemId','adjustQty','adjustReason'].forEach(function(id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.value = '';
|
|
});
|
|
if (window.loadInventoryStats) window.loadInventoryStats();
|
|
if (window.loadOperations) window.loadOperations('ajustes', 1);
|
|
loadItems(currentPage);
|
|
} else {
|
|
document.getElementById('adjustResult').innerHTML = '<span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
|
}
|
|
});
|
|
}
|
|
|
|
// =====================================================================
|
|
// 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),
|
|
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:var(--color-error);">Complete todos los campos</span>';
|
|
return;
|
|
}
|
|
apiFetch(API + '/transfer', { method: 'POST', body: JSON.stringify(data) }).then(function (result) {
|
|
if (result && result.out_operation_id) {
|
|
document.getElementById('transferResult').innerHTML = '<span style="color:var(--color-success);">Transferencia registrada</span>';
|
|
closeTransferModal();
|
|
['transferItemId','transferFrom','transferTo','transferQty','transferNotes'].forEach(function(id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.value = '';
|
|
});
|
|
if (window.loadInventoryStats) window.loadInventoryStats();
|
|
if (window.loadOperations) window.loadOperations('traspasos', 1);
|
|
loadItems(currentPage);
|
|
} else {
|
|
document.getElementById('transferResult').innerHTML = '<span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
|
}
|
|
});
|
|
}
|
|
|
|
// =====================================================================
|
|
// OPERATIONS LIST (Entradas, Salidas, Traspasos, Ajustes)
|
|
// =====================================================================
|
|
|
|
var opTypeMap = {
|
|
'entradas': 'PURCHASE',
|
|
'salidas': 'SALE',
|
|
'traspasos': 'TRANSFER',
|
|
'ajustes': 'ADJUST'
|
|
};
|
|
|
|
var opColspan = { entradas: 8, salidas: 7, traspasos: 8, ajustes: 7 };
|
|
|
|
function loadOperations(type, page) {
|
|
var opType = opTypeMap[type];
|
|
if (!opType) return;
|
|
page = page || 1;
|
|
var params = new URLSearchParams({ type: opType, page: page, per_page: 50 });
|
|
apiFetch(API + '/operations?' + params.toString()).then(function (data) {
|
|
if (!data) return;
|
|
var tbodyId = type + 'TableBody';
|
|
var footerId = type + 'Footer';
|
|
var pagId = type + 'Pagination';
|
|
var tbody = document.getElementById(tbodyId);
|
|
var ops = data.data || [];
|
|
if (!ops.length) {
|
|
tbody.innerHTML = '<tr><td colspan="' + (opColspan[type] || 8) + '" style="text-align:center;padding:30px;color:var(--color-text-muted);">Sin registros</td></tr>';
|
|
document.getElementById(pagId).innerHTML = '';
|
|
document.getElementById(footerId).textContent = '';
|
|
return;
|
|
}
|
|
tbody.innerHTML = ops.map(function (op) { return renderOperationRow(op, type); }).join('');
|
|
var pg = data.pagination || {};
|
|
if (pg.total_pages > 1) {
|
|
document.getElementById(pagId).innerHTML =
|
|
'<div class="pagination">' +
|
|
'<button class="page-btn" ' + (pg.page <= 1 ? 'disabled' : 'onclick="window._loadOperations(\'' + type + '\',' + (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 + ' registros)</span>' +
|
|
'<button class="page-btn" ' + (pg.page >= pg.total_pages ? 'disabled' : 'onclick="window._loadOperations(\'' + type + '\',' + (pg.page + 1) + ')"') + '>›</button>' +
|
|
'</div>';
|
|
} else {
|
|
document.getElementById(pagId).innerHTML = '';
|
|
}
|
|
document.getElementById(footerId).textContent = (pg.total || 0) + ' registros';
|
|
});
|
|
}
|
|
window.loadOperations = loadOperations;
|
|
window._loadOperations = loadOperations;
|
|
|
|
function renderOperationRow(op, type) {
|
|
var dateStr = op.created_at ? new Date(op.created_at).toLocaleString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-';
|
|
var productInfo = esc(op.part_number || op.barcode || '') + ' — ' + esc(op.product_name || '');
|
|
if (type === 'entradas') {
|
|
return '<tr>' +
|
|
'<td class="td--mono">#' + op.id + '</td>' +
|
|
'<td>' + dateStr + '</td>' +
|
|
'<td>' + productInfo + '</td>' +
|
|
'<td style="text-align:right">' + (op.quantity || 0) + '</td>' +
|
|
'<td style="text-align:right" class="td--amount">$' + fmt(op.cost_at_time || 0) + '</td>' +
|
|
'<td style="text-align:right" class="td--amount">$' + fmt(op.total || 0) + '</td>' +
|
|
'<td>' + esc(op.notes || '-') + '</td>' +
|
|
'<td>' + esc(op.employee_name || '-') + '</td>' +
|
|
'</tr>';
|
|
}
|
|
if (type === 'salidas') {
|
|
return '<tr>' +
|
|
'<td class="td--mono">#' + op.id + '</td>' +
|
|
'<td>' + dateStr + '</td>' +
|
|
'<td>' + productInfo + '</td>' +
|
|
'<td style="text-align:right">' + (op.quantity || 0) + '</td>' +
|
|
'<td style="text-align:right" class="td--amount">$' + fmt(op.total || 0) + '</td>' +
|
|
'<td>' + esc(op.notes || '-') + '</td>' +
|
|
'<td>' + esc(op.employee_name || '-') + '</td>' +
|
|
'</tr>';
|
|
}
|
|
if (type === 'traspasos') {
|
|
return '<tr>' +
|
|
'<td class="td--mono">#' + op.id + '</td>' +
|
|
'<td>' + dateStr + '</td>' +
|
|
'<td>' + productInfo + '</td>' +
|
|
'<td style="text-align:right">' + (op.quantity || 0) + '</td>' +
|
|
'<td>' + esc(op.branch_name || '-') + '</td>' +
|
|
'<td>' + esc(op.notes || '-') + '</td>' +
|
|
'<td>' + esc(op.employee_name || '-') + '</td>' +
|
|
'<td></td>' +
|
|
'</tr>';
|
|
}
|
|
if (type === 'ajustes') {
|
|
return '<tr>' +
|
|
'<td class="td--mono">#' + op.id + '</td>' +
|
|
'<td>' + dateStr + '</td>' +
|
|
'<td>' + esc(op.operation_type || 'ADJUST') + '</td>' +
|
|
'<td>' + productInfo + '</td>' +
|
|
'<td style="text-align:right">' + (op.quantity || 0) + '</td>' +
|
|
'<td>' + esc(op.notes || '-') + '</td>' +
|
|
'<td>' + esc(op.employee_name || '-') + '</td>' +
|
|
'</tr>';
|
|
}
|
|
return '<tr><td colspan="8">-</td></tr>';
|
|
}
|
|
|
|
// =====================================================================
|
|
// 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');
|
|
}
|
|
|
|
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: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);
|
|
}
|
|
|
|
function startPhysicalCount() {
|
|
var rows = document.querySelectorAll('#countLines .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; }
|
|
|
|
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 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;
|
|
});
|
|
}
|
|
|
|
function approvePhysicalCount() {
|
|
if (!draftCountId) { alert('No hay borrador activo'); return; }
|
|
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:var(--color-text-muted);">Borrador cancelado</span>';
|
|
}
|
|
|
|
// =====================================================================
|
|
// ALERTS (panel-alertas)
|
|
// =====================================================================
|
|
|
|
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;
|
|
});
|
|
}
|
|
|
|
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>';
|
|
}
|
|
|
|
// =====================================================================
|
|
// HISTORY MODAL
|
|
// =====================================================================
|
|
|
|
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
|
|
// =====================================================================
|
|
|
|
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();
|
|
}
|
|
|
|
// =====================================================================
|
|
// PRODUCT DETAIL MODAL (shows item info + movement history)
|
|
// =====================================================================
|
|
|
|
function uploadItemImage(itemId) {
|
|
var input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = 'image/jpeg,image/png,image/webp';
|
|
input.onchange = function () {
|
|
if (!input.files || !input.files[0]) return;
|
|
var file = input.files[0];
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
alert('Imagen demasiado grande (max 5 MB)');
|
|
return;
|
|
}
|
|
var fd = new FormData();
|
|
fd.append('file', file);
|
|
var statusEl = document.getElementById('imgUploadStatus');
|
|
if (statusEl) statusEl.textContent = 'Subiendo...';
|
|
fetch(API + '/items/' + itemId + '/image', {
|
|
method: 'POST',
|
|
headers: { 'Authorization': 'Bearer ' + token },
|
|
body: fd
|
|
})
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
if (result.image_url) {
|
|
// Refresh detail view
|
|
viewProductDetail(itemId);
|
|
} else {
|
|
if (statusEl) statusEl.textContent = result.error || 'Error';
|
|
}
|
|
})
|
|
.catch(function () {
|
|
if (statusEl) statusEl.textContent = 'Error de red';
|
|
});
|
|
};
|
|
input.click();
|
|
}
|
|
|
|
function deleteItemImage(itemId) {
|
|
if (!confirm('Eliminar imagen de este producto?')) return;
|
|
fetch(API + '/items/' + itemId + '/image', {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
})
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
if (result.message) {
|
|
viewProductDetail(itemId);
|
|
} else {
|
|
alert(result.error || 'Error');
|
|
}
|
|
})
|
|
.catch(function () { alert('Error de red'); });
|
|
}
|
|
|
|
function viewProductDetail(itemId) {
|
|
apiFetch(API + '/items/' + itemId).then(function (data) {
|
|
if (!data || data.error) {
|
|
alert(data ? data.error : 'Error de red');
|
|
return;
|
|
}
|
|
var history = data.history || [];
|
|
var html = '';
|
|
|
|
// Product image section
|
|
html += '<div style="text-align:center;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
|
if (data.image_url) {
|
|
html += '<img src="' + esc(data.image_url) + '?t=' + Date.now() + '" alt="' + esc(data.name) + '" loading="lazy" decoding="async" style="max-width:100%;max-height:220px;object-fit:contain;border-radius:var(--radius-sm);margin-bottom:8px;display:block;margin-left:auto;margin-right:auto;">';
|
|
html += '<div style="display:flex;gap:8px;justify-content:center;">';
|
|
html += '<button class="btn btn--ghost btn--sm" onclick="uploadItemImage(' + data.id + ')">Cambiar imagen</button>';
|
|
html += '<button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="deleteItemImage(' + data.id + ')">Eliminar imagen</button>';
|
|
html += '</div>';
|
|
} else {
|
|
html += '<div style="padding:24px;color:var(--color-text-muted);">';
|
|
html += '<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" style="opacity:0.4;"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>';
|
|
html += '<div style="margin-top:8px;">Sin imagen</div>';
|
|
html += '</div>';
|
|
html += '<button class="btn btn--primary btn--sm" onclick="uploadItemImage(' + data.id + ')">Subir imagen</button>';
|
|
}
|
|
html += '<span id="imgUploadStatus" style="display:block;margin-top:4px;font-size:var(--text-caption);color:var(--color-text-muted);"></span>';
|
|
html += '</div>';
|
|
|
|
// Product info header
|
|
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">ID Inventario</span><strong style="font-family:var(--font-mono);">' + data.id + '</strong></div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">No. Parte</span><strong>' + esc(data.part_number) + '</strong></div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Nombre</span><strong>' + esc(data.name) + '</strong></div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Marca</span>' + esc(data.brand) + '</div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Codigo de Barras</span><span style="font-family:var(--font-mono);">' + esc(data.barcode) + '</span></div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Ubicacion</span>' + esc(data.location || '-') + '</div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Stock</span><strong style="font-size:1.2em;">' + (data.stock || 0) + '</strong></div>';
|
|
html += '</div>';
|
|
|
|
// Prices
|
|
html += '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Costo</span><span class="td--amount">$' + fmt(data.cost) + '</span></div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Precio 1</span><span class="td--amount">$' + fmt(data.price_1) + '</span></div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Precio 2</span><span class="td--amount">$' + fmt(data.price_2) + '</span></div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Precio 3</span><span class="td--amount">$' + fmt(data.price_3) + '</span></div>';
|
|
html += '</div>';
|
|
|
|
// Cross-references section
|
|
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:var(--tracking-widest);margin-bottom:8px;">Cross-References / Equivalencias</div>';
|
|
html += '<div id="crossRefContent" style="margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
|
html += '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Cargando equivalencias...</p>';
|
|
html += '</div>';
|
|
|
|
// Load cross-references from catalog API
|
|
var partNumber = data.part_number;
|
|
var catalogPartId = data.catalog_part_id;
|
|
(function loadCrossRefs() {
|
|
// Try catalog part detail if we have catalog_part_id
|
|
var url = catalogPartId
|
|
? '/pos/api/catalog/part/' + catalogPartId
|
|
: '/pos/api/catalog/search?q=' + encodeURIComponent(partNumber);
|
|
|
|
fetch(url, { headers: { 'Authorization': 'Bearer ' + token } })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(d) {
|
|
var el = document.getElementById('crossRefContent');
|
|
if (!el) return;
|
|
|
|
var alternatives = d.alternatives || [];
|
|
var bodegas = d.bodegas || [];
|
|
|
|
// If it was a search, get alternatives from first result
|
|
if (!catalogPartId && d.data && d.data.length > 0) {
|
|
// Fetch detail for first match
|
|
fetch('/pos/api/catalog/part/' + d.data[0].id_part, { headers: { 'Authorization': 'Bearer ' + token } })
|
|
.then(function(r2) { return r2.json(); })
|
|
.then(function(d2) {
|
|
renderCrossRefs(el, d2.alternatives || [], d2.bodegas || []);
|
|
})
|
|
.catch(function() { el.innerHTML = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Sin conexion al catalogo.</p>'; });
|
|
return;
|
|
}
|
|
|
|
renderCrossRefs(el, alternatives, bodegas);
|
|
})
|
|
.catch(function() {
|
|
var el = document.getElementById('crossRefContent');
|
|
if (el) el.innerHTML = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Sin conexion al catalogo central.</p>';
|
|
});
|
|
})();
|
|
|
|
function renderCrossRefs(el, alternatives, bodegas) {
|
|
var html2 = '';
|
|
|
|
if (bodegas && bodegas.length > 0) {
|
|
html2 += '<div style="margin-bottom:12px;"><strong style="font-size:var(--text-body-sm);color:var(--color-text-primary);">Disponible en Bodegas:</strong></div>';
|
|
html2 += '<table class="data-table"><thead><tr><th>Bodega</th><th>Stock</th><th>Precio</th><th>Ubicacion</th></tr></thead><tbody>';
|
|
bodegas.forEach(function(b) {
|
|
html2 += '<tr><td>' + esc(b.business_name || b.bodega || '') + '</td><td>' + (b.stock || b.stock_quantity || 0) + '</td><td class="td--amount">$' + fmt(b.price || 0) + '</td><td>' + esc(b.location || b.warehouse_location || '') + '</td></tr>';
|
|
});
|
|
html2 += '</tbody></table>';
|
|
}
|
|
|
|
if (alternatives && alternatives.length > 0) {
|
|
html2 += '<div style="margin-top:12px;margin-bottom:8px;"><strong style="font-size:var(--text-body-sm);color:var(--color-text-primary);">Partes Equivalentes (Aftermarket):</strong></div>';
|
|
html2 += '<table class="data-table"><thead><tr><th>No. Parte</th><th>Fabricante</th><th>Nombre</th></tr></thead><tbody>';
|
|
alternatives.forEach(function(a) {
|
|
html2 += '<tr><td class="td--mono">' + esc(a.part_number || a.cross_reference_number || '') + '</td><td>' + esc(a.manufacturer || a.source_ref || '') + '</td><td>' + esc(a.name || a.name_aftermarket_parts || '') + '</td></tr>';
|
|
});
|
|
html2 += '</tbody></table>';
|
|
}
|
|
|
|
if (!html2) {
|
|
html2 = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">No se encontraron equivalencias para esta parte.</p>';
|
|
}
|
|
el.innerHTML = html2;
|
|
}
|
|
|
|
// Vehicle compatibility section
|
|
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:var(--tracking-widest);margin-bottom:8px;">Vehiculos Compatibles</div>';
|
|
html += '<div id="compatContent" style="margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
|
html += '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Cargando compatibilidades...</p>';
|
|
html += '</div>';
|
|
|
|
// Load vehicle compatibilities
|
|
(function loadCompat() {
|
|
fetch('/pos/api/inventory/items/' + itemId + '/vehicles', { headers: { 'Authorization': 'Bearer ' + token } })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(d) {
|
|
var el = document.getElementById('compatContent');
|
|
if (!el) return;
|
|
var list = d.vehicles || [];
|
|
var html2 = '';
|
|
if (list.length > 0) {
|
|
html2 += '<table class="data-table"><thead><tr><th>Marca</th><th>Modelo</th><th>Ano</th><th>Motor</th><th>Origen</th><th></th></tr></thead><tbody>';
|
|
list.forEach(function(c) {
|
|
var sourceLabel = c.source === 'qwen_ai' ? '<span style="background:var(--color-primary);color:#000;padding:2px 6px;border-radius:4px;font-size:10px;font-weight:600;">IA</span>' : (c.source === 'auto_match' ? '<span style="background:var(--color-success);color:#fff;padding:2px 6px;border-radius:4px;font-size:10px;font-weight:600;">TecDoc</span>' : esc(c.source || ''));
|
|
html2 += '<tr><td>' + esc(c.brand || '') + '</td><td>' + esc(c.model || '') + '</td><td>' + esc(c.year || '') + '</td><td>' + esc(c.engine || '') + '</td><td>' + sourceLabel + '</td>';
|
|
html2 += '<td><button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="removeCompat(' + itemId + ',' + c.id + ')">Quitar</button></td></tr>';
|
|
});
|
|
html2 += '</tbody></table>';
|
|
} else {
|
|
html2 += '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Sin vehiculos vinculados.</p>';
|
|
}
|
|
var btnLabel = compatSource === 'qwen' ? 'Auto-Match con IA (QWEN)' : (compatSource === 'both' ? 'Auto-Match (TecDoc + IA)' : 'Auto-Match por TecDoc');
|
|
var btnDesc = compatSource === 'qwen' ? 'Busca compatibilidad usando inteligencia artificial' : (compatSource === 'both' ? 'Busca en catalogo central y con IA' : 'Busca en catalogo central y vincula automaticamente');
|
|
html2 += '<div style="margin-top:8px;"><button class="btn btn--primary btn--sm" onclick="autoMatchCompat(' + itemId + ')">' + btnLabel + '</button> <span style="font-size:var(--text-caption);color:var(--color-text-muted);">' + btnDesc + '</span></div>';
|
|
el.innerHTML = html2;
|
|
})
|
|
.catch(function() {
|
|
var el = document.getElementById('compatContent');
|
|
if (el) el.innerHTML = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Error al cargar compatibilidades.</p>';
|
|
});
|
|
})();
|
|
|
|
// Movement history
|
|
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:var(--tracking-widest);margin-bottom:8px;">Historial de Movimientos</div>';
|
|
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) : '\u2014') + '</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');
|
|
});
|
|
}
|
|
|
|
// Vehicle compatibility actions
|
|
function autoMatchCompat(itemId) {
|
|
fetch('/pos/api/inventory/items/' + itemId + '/vehicles/auto-match', {
|
|
method: 'POST',
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
}).then(function(r) { return r.json(); })
|
|
.then(function(d) {
|
|
var msg = '';
|
|
if (d.tecdoc && d.qwen) {
|
|
var t = d.tecdoc.matched ? (d.tecdoc.matched_count || d.tecdoc.matches ? d.tecdoc.matches.length : 0) : 0;
|
|
var q = d.qwen.total_qwen || 0;
|
|
var qi = d.qwen.inserted || 0;
|
|
msg = 'Auto-match completado.\nTecDoc: ' + t + ' vehiculos.\nIA QWEN: ' + qi + ' nuevos vinculados (de ' + q + ' encontrados).';
|
|
} else if (d.myes) {
|
|
msg = 'Auto-match completado. Vehiculos encontrados: ' + (d.total_qwen || d.myes.length) + ' (nuevos vinculados: ' + (d.inserted || 0) + ')';
|
|
} else {
|
|
msg = 'Auto-match completado. Vehiculos vinculados: ' + (d.matched ? 'Si' : 'No');
|
|
}
|
|
alert(msg);
|
|
viewProductDetail(itemId);
|
|
}).catch(function() { alert('Error en auto-match'); });
|
|
}
|
|
|
|
function removeCompat(itemId, compatId) {
|
|
if (!confirm('Quitar compatibilidad con este vehiculo?')) return;
|
|
fetch('/pos/api/inventory/items/' + itemId + '/vehicles/' + compatId, {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
}).then(function(r) { return r.json(); })
|
|
.then(function() {
|
|
viewProductDetail(itemId);
|
|
}).catch(function() { alert('Error al quitar compatibilidad'); });
|
|
}
|
|
|
|
// =====================================================================
|
|
// EXPOSE GLOBALS (for onclick handlers in HTML)
|
|
// =====================================================================
|
|
|
|
window._loadItems = function (p) { loadItems(p); };
|
|
window.loadItems = function (p, q) { loadItems(p, q); };
|
|
window.viewHistory = viewHistory;
|
|
window.viewProductDetail = viewProductDetail;
|
|
window.uploadItemImage = uploadItemImage;
|
|
window.deleteItemImage = deleteItemImage;
|
|
window.closeHistoryModal = closeHistoryModal;
|
|
window.showCreateModal = showCreateModal;
|
|
window.closeCreateModal = closeCreateModal;
|
|
window.createItem = createItem;
|
|
window.showPurchaseModal = showPurchaseModal;
|
|
window.showPurchaseModalForItem = showPurchaseModalForItem;
|
|
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;
|
|
window.cancelDraft = cancelDraft;
|
|
window.loadAlerts = loadAlerts;
|
|
window.printBarcode = printBarcode;
|
|
window.autoMatchCompat = autoMatchCompat;
|
|
window.removeCompat = removeCompat;
|
|
|
|
// =====================================================================
|
|
// INIT — load stock on page load
|
|
// =====================================================================
|
|
|
|
loadItems(1);
|
|
})();
|