feat(inventory): edit items in inventory grid
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

- Added 'Editar' button in inventory rows.

- Reused create modal as create/edit with pre-filled data.

- Added fields: unit, max_stock, tax_rate, description, is_active.

- Added /inventory/categories/all endpoint for flat category selector.

- Price/cost inputs are disabled for users without config.edit_prices.
This commit is contained in:
2026-06-30 19:43:03 +00:00
parent b6679c6d9c
commit 603046cb09
3 changed files with 131 additions and 59 deletions

View File

@@ -17,6 +17,11 @@
var compatSource = 'both'; // default, loaded from config
var inventorySearchController = null;
var user = window.POS_USER || {};
var userRole = (user.role || '').toLowerCase();
var userPerms = user.permissions || [];
var canEditPrices = userRole === 'owner' || userRole === 'admin' || userPerms.indexOf('config.edit_prices') !== -1;
// Load compatibility source setting
(function loadCompatSource() {
fetch('/pos/api/config/vehicle-compat-source', { headers: { 'Authorization': 'Bearer ' + token } })
@@ -181,6 +186,7 @@
'<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" onclick="event.stopPropagation();showEditItemModal(' + it.id + ')">Editar</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--sm btn--meli" onclick="event.stopPropagation();publishToMeli(' + it.id + ')">ML</button> ' +
'<button class="btn btn--ghost btn--sm" onclick="event.stopPropagation();printBarcode(\'' + esc(it.barcode) + '\',\'' + esc(it.part_number) + '\',\'' + esc(it.name) + '\')">Etiqueta</button> ' +
@@ -307,39 +313,49 @@
// CREATE ITEM (createModal)
// =====================================================================
function loadCategories() {
function loadCategories(selectedId) {
var sel = document.getElementById('newCategory');
if (!sel) return;
apiFetch(API + '/categories').then(function(data) {
apiFetch(API + '/categories/all').then(function(data) {
if (!data || !data.categories) return;
sel.innerHTML = '<option value="">Selecciona categoría</option>';
data.categories.forEach(function(c) {
sel.innerHTML += '<option value="' + c.id + '">' + esc(c.name) + '</option>';
var cats = data.categories;
var top = cats.filter(function(c) { return !c.parent_id; });
var subs = cats.filter(function(c) { return c.parent_id; });
sel.innerHTML = '<option value="">Sin categoría</option>';
top.forEach(function(c) {
sel.innerHTML += '<optgroup label="' + esc(c.name) + '">' +
'<option value="' + c.id + '"' + (c.id === selectedId ? ' selected' : '') + '>' + esc(c.name) + '</option>';
subs.filter(function(s) { return s.parent_id === c.id; }).forEach(function(s) {
sel.innerHTML += '<option value="' + s.id + '"' + (s.id === selectedId ? ' selected' : '') + '>&nbsp;&nbsp;' + esc(s.name) + '</option>';
});
sel.innerHTML += '</optgroup>';
});
});
}
window.loadCategories = loadCategories;
function onCategoryChange(categoryId) {
var subSel = document.getElementById('newSubcategory');
if (!subSel) return;
if (!categoryId) {
subSel.innerHTML = '<option value="">Selecciona categoría primero</option>';
subSel.disabled = true;
return;
}
apiFetch(API + '/categories/' + categoryId + '/subcategories').then(function(data) {
if (!data || !data.subcategories) return;
subSel.innerHTML = '<option value="">Selecciona subcategoría</option>';
data.subcategories.forEach(function(s) {
subSel.innerHTML += '<option value="' + s.id + '">' + esc(s.name) + '</option>';
});
subSel.disabled = false;
});
// Kept for backwards compatibility; selector is now flat.
}
window.onCategoryChange = onCategoryChange;
function resetCreateModal() {
document.getElementById('editItemId').value = '';
document.getElementById('createModalTitle').textContent = 'Nuevo Producto';
document.getElementById('createModalBtn').textContent = 'Crear Producto';
var ids = ['newPartNumber','newName','newBrand','newBarcode','newSku2','newSku3','newUnit','newCost','newPrice1','newMinStock','newInitialStock','newMaxStock','newTaxRate','newLocation','newDescription'];
ids.forEach(function(id) {
var el = document.getElementById(id);
if (el) el.value = '';
});
document.getElementById('newCategory').innerHTML = '<option value="">Sin categoría</option>';
document.getElementById('newIsActive').value = 'true';
document.getElementById('initialStockField').style.display = '';
document.querySelectorAll('#createModal .price-field input').forEach(function(el) { el.disabled = false; });
}
function showCreateModal() {
resetCreateModal();
document.getElementById('createModal').classList.add('is-open');
loadCategories();
// Attach AI classification on part number blur
@@ -357,6 +373,41 @@
}
}
function showEditItemModal(itemId) {
resetCreateModal();
document.getElementById('editItemId').value = itemId;
document.getElementById('createModalTitle').textContent = 'Editar Producto';
document.getElementById('createModalBtn').textContent = 'Guardar Cambios';
document.getElementById('initialStockField').style.display = 'none';
document.getElementById('createModal').classList.add('is-open');
apiFetch(API + '/items/' + itemId).then(function(it) {
if (!it) return;
document.getElementById('newPartNumber').value = it.part_number || '';
document.getElementById('newName').value = it.name || '';
document.getElementById('newBrand').value = it.brand || '';
document.getElementById('newBarcode').value = it.barcode || '';
document.getElementById('newUnit').value = it.unit || '';
document.getElementById('newCost').value = it.cost != null ? it.cost : '';
document.getElementById('newPrice1').value = it.price_1 != null ? it.price_1 : '';
document.getElementById('newMinStock').value = it.min_stock != null ? it.min_stock : '';
document.getElementById('newMaxStock').value = it.max_stock != null ? it.max_stock : '';
document.getElementById('newTaxRate').value = it.tax_rate != null ? it.tax_rate : '';
document.getElementById('newLocation').value = it.location || '';
document.getElementById('newDescription').value = it.description || '';
document.getElementById('newIsActive').value = (it.is_active === false ? 'false' : 'true');
document.getElementById('newSku2').value = (it.sku_aliases && it.sku_aliases[0]) ? it.sku_aliases[0].sku : '';
document.getElementById('newSku3').value = (it.sku_aliases && it.sku_aliases[1]) ? it.sku_aliases[1].sku : '';
if (!canEditPrices) {
document.querySelectorAll('#createModal .price-field input').forEach(function(el) { el.disabled = true; });
}
loadCategories(it.category_id);
}).catch(function(e) {
alert('Error al cargar producto: ' + e.message);
closeCreateModal();
});
}
window.showEditItemModal = showEditItemModal;
function classifyPartNumber(partNumber) {
var resultEl = document.getElementById('createResult');
resultEl.innerHTML = '<span style="color:var(--color-text-muted);">Consultando IA...</span>';
@@ -387,58 +438,57 @@
function closeCreateModal() {
document.getElementById('createModal').classList.remove('is-open');
document.getElementById('createResult').innerHTML = '';
var catSel = document.getElementById('newCategory');
var subSel = document.getElementById('newSubcategory');
if (catSel) catSel.innerHTML = '<option value="">Selecciona categoría</option>';
if (subSel) { subSel.innerHTML = '<option value="">Selecciona categoría primero</option>'; subSel.disabled = true; }
resetCreateModal();
}
function createItem() {
var elPrice2 = document.getElementById('newPrice2');
var elPrice3 = document.getElementById('newPrice3');
var editId = document.getElementById('editItemId').value;
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,
unit: document.getElementById('newUnit').value.trim() || undefined,
cost: parseFloat(document.getElementById('newCost').value) || 0,
price_1: parseFloat(document.getElementById('newPrice1').value) || 0,
price_2: elPrice2 ? (parseFloat(elPrice2.value) || 0) : 0,
price_3: elPrice3 ? (parseFloat(elPrice3.value) || 0) : 0,
min_stock: parseInt(document.getElementById('newMinStock').value) || 0,
initial_stock: parseInt(document.getElementById('newInitialStock').value) || 0,
max_stock: parseInt(document.getElementById('newMaxStock').value) || 0,
tax_rate: parseFloat(document.getElementById('newTaxRate').value) || 0,
location: document.getElementById('newLocation').value.trim(),
description: document.getElementById('newDescription').value.trim(),
is_active: document.getElementById('newIsActive').value === 'true',
sku_aliases: []
};
var sku2 = document.getElementById('newSku2').value.trim();
var sku3 = document.getElementById('newSku3').value.trim();
var categoryId = document.getElementById('newCategory').value;
var subcategoryId = document.getElementById('newSubcategory').value;
if (categoryId) data.category_id = parseInt(categoryId);
if (sku2) data.sku_aliases.push({sku: sku2, label: 'Alternativo 1'});
if (sku3) data.sku_aliases.push({sku: sku3, label: 'Alternativo 2'});
if (subcategoryId) {
data.category_id = parseInt(subcategoryId);
} else if (categoryId) {
data.category_id = parseInt(categoryId);
}
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) {
var url = API + '/items';
var method = 'POST';
if (editId) {
url = API + '/items/' + editId;
method = 'PUT';
// Stock inicial solo en creación
} else {
data.initial_stock = parseInt(document.getElementById('newInitialStock').value) || 0;
}
apiFetch(url, { method: method, body: JSON.stringify(data) }).then(function (result) {
if (result && (result.id || result.message)) {
var msg = editId ? 'Producto actualizado' : ('Creado ID ' + result.id + ' | Barcode: ' + result.barcode);
if (!editId && 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','newSku2','newSku3','newCost','newPrice1','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>';