feat(inventory): edit items in inventory grid
- 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:
@@ -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' : '') + '> ' + 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>';
|
||||
|
||||
Reference in New Issue
Block a user