Merge branch 'main' into desarrollo_hector

This commit is contained in:
2026-06-15 12:56:54 -06:00
168 changed files with 22773 additions and 1730 deletions

View File

@@ -46,6 +46,11 @@
var checkoutBtn = document.getElementById('checkoutBtn');
var cartFab = document.getElementById('cartFab');
var cartCloseBtn = document.getElementById('cartCloseBtn');
// Supplier prices upload
var uploadPricesBtn = document.getElementById('uploadPricesBtn');
var uploadPricesModal= document.getElementById('uploadPricesModal');
var uploadPricesFile = document.getElementById('uploadPricesFile');
var uploadPricesStatus=document.getElementById('uploadPricesStatus');
// ─── Navigation State ───
var nav = {
@@ -195,7 +200,19 @@
currentAbort = null;
}
var opts = { headers: headers };
if (url.indexOf('/pos/api/') === 0 && url.indexOf('mode=') !== -1 || url.indexOf('/years') !== -1 || url.indexOf('/brands') !== -1 || url.indexOf('/models') !== -1 || url.indexOf('/engines') !== -1 || url.indexOf('/categories') !== -1 || url.indexOf('/groups') !== -1 || url.indexOf('/part-types') !== -1 || url.indexOf('/parts') !== -1 || url.indexOf('/search') !== -1) {
var isCatalogNav = url.indexOf('/pos/api/') === 0 && (
url.indexOf('mode=') !== -1 ||
url.indexOf('/years') !== -1 ||
url.indexOf('/brands') !== -1 ||
url.indexOf('/models') !== -1 ||
url.indexOf('/engines') !== -1 ||
url.indexOf('/categories') !== -1 ||
url.indexOf('/groups') !== -1 ||
url.indexOf('/part-types') !== -1 ||
url.indexOf('/parts') !== -1 ||
url.indexOf('/search') !== -1
);
if (isCatalogNav) {
currentAbort = new AbortController();
opts.signal = currentAbort.signal;
}
@@ -233,7 +250,7 @@
if (!s) return '';
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
return d.innerHTML.replace(/"/g, '"');
}
// ─── Breadcrumb ───
@@ -304,9 +321,9 @@
function resetNav() {
nav.level = 'brands';
pushNavState();
nav.brand = nav.model = nav.year = nav.engine = nav.category = nav.group = nav.partType = null;
nav.nxGroup = nav.nxSubgroup = nav.nxPartType = null;
pushNavState();
}
function resetNavFrom(level) {
@@ -363,10 +380,14 @@
var cacheKey = 'nexus:brands:' + catalogMode;
var cached = sessionStorage.getItem(cacheKey);
if (cached) {
hideLoading();
var data = JSON.parse(cached);
renderBrands(data);
return;
try {
hideLoading();
var data = JSON.parse(cached);
renderBrands(data);
return;
} catch (e) {
sessionStorage.removeItem(cacheKey);
}
}
apiFetch(API + '/brands?mode=' + catalogMode).then(function (data) {
@@ -937,9 +958,14 @@
partsGrid.querySelectorAll('.part-card').forEach(function (card) {
card.addEventListener('click', function () {
var pid = this.dataset.partId;
var src = this.dataset.source || '';
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
return;
}
if (src === 'supplier_catalog' || (typeof pid === 'string' && pid.indexOf('sc:') === 0)) {
openSupplierDetail(pid.replace('sc:', ''));
return;
}
openPartDetail(parseInt(pid));
});
});
@@ -998,17 +1024,23 @@
partsGrid.innerHTML = data.data.map(function (p) {
// Stock badge — prefer tenant stock, then warehouse network, else fallback
var stockBadge;
if (p.local_stock > 0) {
var isSupplier = p.source === 'supplier_catalog' || (typeof p.id_part === 'string' && p.id_part.indexOf('sc:') === 0);
if (isSupplier) {
stockBadge = '<span class="stock-badge stock-badge--none" style="background:#f59e0b;color:#fff;">Cat. Proveedor</span>';
} else if (p.local_stock > 0) {
stockBadge = '<span class="stock-badge stock-badge--local">En stock: ' + p.local_stock + '</span>';
} else if (p.in_stock_network || p.bodega_count > 0) {
stockBadge = '<span class="stock-badge stock-badge--bodega">' + p.bodega_count + ' bodega' + (p.bodega_count > 1 ? 's' : '') + '</span>';
} else {
stockBadge = '<span class="stock-badge stock-badge--none">Sin stock</span>';
}
// Local inventory native badge
var sourceBadge = p.source === 'local_inventory'
? '<span class="stock-badge stock-badge--local" style="margin-left:4px;background:#4f46e5;">Stock Local</span>'
: '';
// Source badge for local inventory or supplier catalog
var sourceBadge = '';
if (p.source === 'local_inventory') {
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-left:4px;background:#4f46e5;">Stock Local</span>';
} else if (isSupplier) {
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-left:4px;background:#f59e0b;color:#fff;">Cat. Proveedor</span>';
}
var imgHtml = p.image_url
? '<img src="' + esc(p.image_url) + '" alt="' + esc(p.name) + '" loading="lazy" decoding="async">'
@@ -1040,6 +1072,7 @@
'</div>' +
'<div class="part-card__footer">' +
(p.local_price ? '<span class="part-card__price">$' + fmt(p.local_price) + '</span>' : '<span class="part-card__price" style="color:var(--color-text-muted);">Sin precio</span>') +
(p.supplier_price ? '<span class="part-card__price" style="color:#2d7d46;font-size:0.85em;">Prov: $' + fmt(p.supplier_price) + '</span>' : '') +
stockBadge +
'</div>' +
'</article>';
@@ -1049,10 +1082,15 @@
partsGrid.querySelectorAll('.part-card').forEach(function (card) {
card.addEventListener('click', function () {
var pid = this.dataset.partId;
var src = this.dataset.source || '';
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
// local-inventory item: info already visible on card
return;
}
if (src === 'supplier_catalog' || (typeof pid === 'string' && pid.indexOf('sc:') === 0)) {
openSupplierDetail(pid.replace('sc:', ''));
return;
}
openPartDetail(parseInt(pid));
});
});
@@ -1195,6 +1233,73 @@
});
}
function openSupplierDetail(supplierId) {
detailBody.innerHTML = '<div class="loading is-visible"><div class="spinner"></div></div>';
detailFooter.style.display = 'none';
detailPanel.classList.add('is-open');
detailOverlay.classList.add('is-visible');
apiFetch('/pos/api/supplier-catalog/items/' + supplierId).then(function (data) {
if (!data || data.error) {
detailBody.innerHTML = '<p style="color:var(--color-error);padding:var(--space-4);">Error al cargar detalle.</p>';
return;
}
var p = data;
var html = '';
html += '<div class="detail-section">';
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-1);">' + esc(p.supplier_name) + ' &gt; ' + esc(p.category || '') + '</div>';
html += '<div class="detail-oem">' + esc(p.sku) + '</div>';
html += '<div class="detail-name">' + esc((p.name || '').replace(/\\n/g, ' ')) + '</div>';
if (p.description) html += '<div class="detail-desc">' + esc(p.description) + '</div>';
if (p.image_url) html += '<div style="margin-top:var(--space-3);text-align:center;"><img src="' + esc(p.image_url) + '" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:200px;object-fit:contain;border-radius:var(--radius-sm);"></div>';
html += '</div>';
// Interchanges
if (p.interchanges && p.interchanges.length) {
html += '<div class="detail-section">';
html += '<div class="detail-section__title">Intercambios OEM</div>';
var seen = {};
p.interchanges.forEach(function(ix) {
var key = (ix.brand || '') + '|' + (ix.interchange_number || '');
if (seen[key]) return;
seen[key] = true;
html += '<div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--color-border);">' +
'<span style="font-weight:600;">' + esc(ix.brand || '') + '</span>' +
'<span style="color:var(--color-text-muted);font-family:monospace;">' + esc(ix.interchange_number || '') + '</span>' +
'</div>';
});
html += '</div>';
}
// Compatibilities — deduplicate by (make, model, year, engine)
if (p.compatibilities && p.compatibilities.length) {
html += '<div class="detail-section">';
html += '<div class="detail-section__title">Vehiculos compatibles</div>';
var seenCompat = {};
var uniqCompat = [];
p.compatibilities.forEach(function(c) {
var key = (c.make || '') + '|' + (c.model || '') + '|' + (c.year || '') + '|' + (c.engine || '');
if (seenCompat[key]) return;
seenCompat[key] = true;
uniqCompat.push(c);
});
var currentMake = '';
uniqCompat.forEach(function(c) {
if (c.make !== currentMake) {
currentMake = c.make;
html += '<div style="font-weight:600;margin-top:8px;">' + esc(c.make) + '</div>';
}
html += '<div style="padding-left:12px;color:var(--color-text-muted);font-size:var(--text-body-sm);">' +
esc(c.model) + ' ' + c.year + ' ' + esc(c.engine || '') + '</div>';
});
html += '</div>';
}
detailBody.innerHTML = html;
});
}
function closeDetail() {
detailPanel.classList.remove('is-open');
detailOverlay.classList.remove('is-visible');
@@ -1408,17 +1513,22 @@
}
searchDropdown.innerHTML = data.data.map(function (r) {
var isLocal = r.source === 'local_inventory' || (typeof r.id_part === 'string' && r.id_part.indexOf('inv:') === 0);
var isSupplier = r.source === 'supplier_catalog' || (typeof r.id_part === 'string' && r.id_part.indexOf('sc:') === 0);
var stockLabel = r.local_stock > 0
? '<span class="stock-badge stock-badge--local" style="margin-left:auto;">Stock: ' + r.local_stock + '</span>'
: '';
var localBadge = isLocal
? '<span class="stock-badge stock-badge--local" style="margin-right:4px;background:#4f46e5;">Stock Local</span>'
: '';
var sourceBadge = '';
if (isLocal) {
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-right:4px;background:#4f46e5;">Stock Local</span>';
} else if (isSupplier) {
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-right:4px;background:#f59e0b;color:#fff;">Cat. Proveedor</span>';
}
var oemNum = isLocal ? (r.oem_part_number || r.part_number || '') : (r.oem_part_number || '');
return '<div class="search-result-item" data-part-id="' + r.id_part + '" data-name="' + esc(r.name) + '" data-pn="' + esc(oemNum) + '" data-price="' + (r.local_price || '') + '" data-stock="' + (r.local_stock || 0) + '">' +
var cleanName = (r.name || '').replace(/\\n/g, ' ');
return '<div class="search-result-item" data-part-id="' + r.id_part + '" data-name="' + esc(cleanName) + '" data-pn="' + esc(oemNum) + '" data-price="' + (r.local_price || '') + '" data-stock="' + (r.local_stock || 0) + '" data-source="' + (r.source || '') + '">' +
'<div style="flex:1;">' +
'<div class="search-result__oem">' + localBadge + esc(oemNum) + '</div>' +
'<div class="search-result__name">' + esc(r.name) + '</div>' +
'<div class="search-result__oem">' + sourceBadge + esc(oemNum) + '</div>' +
'<div class="search-result__name">' + esc(cleanName) + '</div>' +
(r.vehicle_info ? '<div class="search-result__vehicle">' + esc(r.vehicle_info) + '</div>' : '') +
'</div>' +
stockLabel +
@@ -1430,6 +1540,7 @@
el.addEventListener('click', function () {
searchDropdown.classList.remove('is-visible');
var pid = this.dataset.partId;
var src = this.dataset.source || '';
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
var info = '💠 Stock Local\n\n' +
'Parte: ' + (this.dataset.pn || 'N/A') + '\n' +
@@ -1439,6 +1550,10 @@
alert(info);
return;
}
if (src === 'supplier_catalog' || (typeof pid === 'string' && pid.indexOf('sc:') === 0)) {
openSupplierDetail(pid.replace('sc:', ''));
return;
}
openPartDetail(parseInt(pid));
});
});
@@ -1645,8 +1760,13 @@
var cacheKey = 'nexus:years-all';
var cached = sessionStorage.getItem(cacheKey);
if (cached) {
var data = JSON.parse(cached);
var years = data.data || data || [];
try {
var data = JSON.parse(cached);
var years = data.data || data || [];
} catch (e) {
sessionStorage.removeItem(cacheKey);
var years = [];
}
if (!years.length) {
years = [];
for (var y = 2026; y >= 1990; y--) years.push({ id_year: y, year_car: y });
@@ -2046,6 +2166,53 @@
});
}
// ─── Supplier prices upload ─────────────────────────────────────────────
function openUploadPricesModal() {
if (uploadPricesModal) uploadPricesModal.style.display = 'flex';
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '';
if (uploadPricesFile) uploadPricesFile.value = '';
}
function closeUploadPricesModal() {
if (uploadPricesModal) uploadPricesModal.style.display = 'none';
}
async function submitUploadPrices() {
if (!uploadPricesFile || !uploadPricesFile.files || !uploadPricesFile.files[0]) {
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '<span style="color:var(--color-error);">Selecciona un archivo primero.</span>';
return;
}
var form = new FormData();
form.append('file', uploadPricesFile.files[0]);
if (uploadPricesStatus) uploadPricesStatus.innerHTML = 'Subiendo...';
try {
var res = await fetch('/pos/api/supplier-catalog/prices/upload', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token },
body: form
});
var data = await res.json();
if (res.ok && data.success) {
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '<span style="color:var(--color-success);">✓ Precios actualizados: ' + data.processed + ' (insertados: ' + data.inserted + ', actualizados: ' + data.updated + ')</span>';
uploadPricesFile.value = '';
} else {
var msg = data.error || 'Error al subir precios';
var details = (data.details || []).join('<br>');
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '<span style="color:var(--color-error);">' + esc(msg) + '</span>' + (details ? '<div style="margin-top:4px;font-size:0.9em;">' + details + '</div>' : '');
}
} catch (e) {
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '<span style="color:var(--color-error);">Error de red: ' + esc(e.message) + '</span>';
}
}
function shouldShowUploadPricesButton() {
try {
var user = JSON.parse(localStorage.getItem('pos_employee') || '{}');
return user.role === 'owner' || user.role === 'admin';
} catch (e) { return false; }
}
if (uploadPricesBtn && shouldShowUploadPricesButton()) {
uploadPricesBtn.style.display = 'inline-flex';
}
window.CatalogApp = {
toggleCart: toggleCart,
goToCheckout: goToCheckout,
@@ -2065,6 +2232,9 @@
togglePlate: togglePlate,
lookupPlate: lookupPlate,
setMode: setCatalogMode,
openUploadPricesModal: openUploadPricesModal,
closeUploadPricesModal: closeUploadPricesModal,
submitUploadPrices: submitUploadPrices,
};
// ─── INIT ───