/** * bodega.js — Warehouse (Bodega) dashboard for Nexus Autoparts * Tabs: Mapeo de Columnas | Subir Inventario | Mi Inventario */ (function () { 'use strict'; var API = ''; var selectedFile = null; var invPage = 1; var invQuery = ''; // ================================================================ // Auth helpers // ================================================================ function getToken() { return localStorage.getItem('access_token') || ''; } function getRole() { var token = getToken(); if (!token) return null; try { var payload = JSON.parse(atob(token.split('.')[1])); return payload.role || null; } catch (e) { return null; } } function authHeaders(extra) { var h = { 'Authorization': 'Bearer ' + getToken() }; if (extra) { for (var k in extra) { h[k] = extra[k]; } } return h; } function checkAuth() { var token = getToken(); var role = getRole(); if (!token || (role !== 'BODEGA' && role !== 'ADMIN')) { window.location.href = '/login.html'; return false; } return true; } function tryRefreshToken() { var refresh = localStorage.getItem('refresh_token'); if (!refresh) return Promise.reject(new Error('No refresh token')); return fetch(API + '/api/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: refresh }) }).then(function (r) { if (!r.ok) throw new Error('Refresh failed'); return r.json(); }).then(function (data) { if (data.access_token) { localStorage.setItem('access_token', data.access_token); } return data; }); } // ================================================================ // API helper with 401 retry // ================================================================ function api(path, opts) { opts = opts || {}; if (!opts.headers) opts.headers = {}; opts.headers['Authorization'] = 'Bearer ' + getToken(); return fetch(API + path, opts).then(function (r) { if (r.status === 401) { return tryRefreshToken().then(function () { opts.headers['Authorization'] = 'Bearer ' + getToken(); return fetch(API + path, opts); }).then(function (r2) { if (!r2.ok) return r2.json().then(function (d) { throw new Error(d.error || 'Error'); }); return r2.json(); }); } if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || 'Error'); }); return r.json(); }); } // ================================================================ // Utilities // ================================================================ function esc(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } function toast(msg, type) { var container = document.getElementById('toast-container'); var el = document.createElement('div'); el.className = 'toast ' + (type || 'success'); el.textContent = msg; container.appendChild(el); setTimeout(function () { el.remove(); }, 3500); } function fmtDate(s) { if (!s) return '—'; var d = new Date(s); return d.toLocaleDateString('es-MX', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } function fmtPrice(v) { var n = parseFloat(v); if (isNaN(n)) return '—'; return '$' + n.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function statusBadge(status) { var map = { 'completed': { cls: 'badge-success', label: 'Completado' }, 'success': { cls: 'badge-success', label: 'Completado' }, 'error': { cls: 'badge-error', label: 'Error' }, 'failed': { cls: 'badge-error', label: 'Fallido' }, 'pending': { cls: 'badge-pending', label: 'Pendiente' }, 'processing': { cls: 'badge-processing', label: 'Procesando' } }; var info = map[(status || '').toLowerCase()] || { cls: 'badge-pending', label: status || 'Desconocido' }; return '' + esc(info.label) + ''; } // ================================================================ // Tab Switching // ================================================================ document.querySelectorAll('.bodega-tab').forEach(function (tab) { tab.addEventListener('click', function () { document.querySelectorAll('.bodega-tab').forEach(function (t) { t.classList.remove('active'); }); document.querySelectorAll('.bodega-section').forEach(function (s) { s.classList.remove('active'); }); tab.classList.add('active'); var section = document.getElementById('section-' + tab.getAttribute('data-tab')); if (section) section.classList.add('active'); // Load data when switching tabs var target = tab.getAttribute('data-tab'); if (target === 'subir') loadUploadHistory(); if (target === 'inventario') loadInventory(); }); }); // ================================================================ // TAB 1: Mapeo de Columnas // ================================================================ function loadMapping() { api('/api/inventory/mapping').then(function (data) { if (data.part_number) document.getElementById('map-part-number').value = data.part_number; if (data.price) document.getElementById('map-price').value = data.price; if (data.stock) document.getElementById('map-stock').value = data.stock; if (data.location) document.getElementById('map-location').value = data.location; }).catch(function () { // No mapping yet — fields stay empty }); } document.getElementById('btn-save-mapping').addEventListener('click', function () { var partNumber = document.getElementById('map-part-number').value.trim(); var price = document.getElementById('map-price').value.trim(); var stock = document.getElementById('map-stock').value.trim(); var location = document.getElementById('map-location').value.trim(); var statusEl = document.getElementById('mapping-status'); if (!partNumber || !price || !stock) { statusEl.textContent = 'Completa los campos obligatorios.'; statusEl.className = 'status-msg error'; return; } var body = { part_number: partNumber, price: price, stock: stock, location: location || null }; statusEl.textContent = 'Guardando...'; statusEl.className = 'status-msg'; api('/api/inventory/mapping', { method: 'PUT', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify(body) }).then(function () { statusEl.textContent = 'Mapeo guardado correctamente.'; statusEl.className = 'status-msg success'; toast('Mapeo guardado', 'success'); }).catch(function (err) { statusEl.textContent = err.message || 'Error al guardar.'; statusEl.className = 'status-msg error'; }); }); // ================================================================ // TAB 2: Subir Inventario // ================================================================ var dropZone = document.getElementById('drop-zone'); var fileInput = document.getElementById('file-input'); dropZone.addEventListener('click', function () { fileInput.click(); }); dropZone.addEventListener('dragover', function (e) { e.preventDefault(); dropZone.classList.add('dragover'); }); dropZone.addEventListener('dragleave', function () { dropZone.classList.remove('dragover'); }); dropZone.addEventListener('drop', function (e) { e.preventDefault(); dropZone.classList.remove('dragover'); if (e.dataTransfer.files.length) { selectFile(e.dataTransfer.files[0]); } }); fileInput.addEventListener('change', function () { if (fileInput.files.length) { selectFile(fileInput.files[0]); } }); function selectFile(file) { var validTypes = [ 'text/csv', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ]; var ext = (file.name || '').split('.').pop().toLowerCase(); if (validTypes.indexOf(file.type) === -1 && ['csv', 'xls', 'xlsx'].indexOf(ext) === -1) { toast('Formato no soportado. Usa CSV o Excel.', 'error'); return; } if (file.size > 10 * 1024 * 1024) { toast('El archivo excede 10MB.', 'error'); return; } selectedFile = file; document.getElementById('selected-file-name').textContent = file.name + ' (' + (file.size / 1024).toFixed(1) + ' KB)'; document.getElementById('selected-file').style.display = 'flex'; document.getElementById('btn-upload').disabled = false; } document.getElementById('btn-clear-file').addEventListener('click', function () { selectedFile = null; fileInput.value = ''; document.getElementById('selected-file').style.display = 'none'; document.getElementById('btn-upload').disabled = true; }); document.getElementById('btn-upload').addEventListener('click', function () { if (!selectedFile) return; var btn = document.getElementById('btn-upload'); var statusEl = document.getElementById('upload-status'); btn.disabled = true; statusEl.textContent = 'Subiendo...'; statusEl.className = 'status-msg'; var fd = new FormData(); fd.append('file', selectedFile); fetch(API + '/api/inventory/upload', { method: 'POST', headers: { 'Authorization': 'Bearer ' + getToken() }, body: fd }).then(function (r) { if (r.status === 401) { return tryRefreshToken().then(function () { return fetch(API + '/api/inventory/upload', { method: 'POST', headers: { 'Authorization': 'Bearer ' + getToken() }, body: fd }); }); } return r; }).then(function (r) { return r.json().then(function (data) { if (!r.ok) throw new Error(data.error || 'Error al subir'); return data; }); }).then(function (data) { statusEl.textContent = ''; showUploadResult(data); toast('Archivo procesado correctamente.', 'success'); loadUploadHistory(); // Clear file selection selectedFile = null; fileInput.value = ''; document.getElementById('selected-file').style.display = 'none'; btn.disabled = true; }).catch(function (err) { statusEl.textContent = err.message || 'Error al subir archivo.'; statusEl.className = 'status-msg error'; btn.disabled = false; }); }); function showUploadResult(data) { var el = document.getElementById('upload-result'); var imported = data.imported || data.imported_count || 0; var errors = data.errors || data.error_count || 0; var samples = data.error_samples || []; var html = '
' + esc(typeof s === 'string' ? s : JSON.stringify(s)) + '
'; }); html += '