/** * 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 = '

Resultado de la Carga

'; html += '
'; html += 'Importados: ' + imported + ''; html += 'Errores: ' + errors + ''; html += '
'; if (samples.length) { html += '
'; html += 'Ejemplos de errores:'; samples.forEach(function (s) { html += '

' + esc(typeof s === 'string' ? s : JSON.stringify(s)) + '

'; }); html += '
'; } el.innerHTML = html; el.style.display = 'block'; } function loadUploadHistory() { api('/api/inventory/uploads').then(function (data) { var rows = data.uploads || data.data || data || []; var tbody = document.getElementById('history-body'); if (!Array.isArray(rows) || rows.length === 0) { tbody.innerHTML = 'Sin cargas previas'; return; } tbody.innerHTML = rows.map(function (r) { return '' + '' + esc(r.filename || r.archivo || '—') + '' + '' + statusBadge(r.status || r.estado) + '' + '' + (r.imported_count != null ? r.imported_count : (r.importados != null ? r.importados : '—')) + '' + '' + (r.error_count != null ? r.error_count : (r.errores != null ? r.errores : '—')) + '' + '' + fmtDate(r.created_at || r.fecha) + '' + ''; }).join(''); }).catch(function () { document.getElementById('history-body').innerHTML = 'Error al cargar historial'; }); } // ================================================================ // TAB 3: Mi Inventario // ================================================================ function loadInventory() { var params = '?page=' + invPage; if (invQuery) params += '&q=' + encodeURIComponent(invQuery); api('/api/inventory/items' + params).then(function (data) { var items = data.data || data.items || []; var pagination = data.pagination || {}; var tbody = document.getElementById('inv-body'); if (!Array.isArray(items) || items.length === 0) { tbody.innerHTML = 'Sin articulos en inventario'; renderPagination(pagination); return; } tbody.innerHTML = items.map(function (it) { return '' + '' + esc(it.part_number) + '' + '' + esc(it.name || it.nombre || '—') + '' + '' + fmtPrice(it.price || it.precio) + '' + '' + (it.stock != null ? it.stock : (it.existencias != null ? it.existencias : '—')) + '' + '' + esc(it.location || it.ubicacion || '—') + '' + '' + fmtDate(it.updated_at || it.actualizado) + '' + ''; }).join(''); renderPagination(pagination); }).catch(function () { document.getElementById('inv-body').innerHTML = 'Error al cargar inventario'; }); } function renderPagination(pg) { var container = document.getElementById('inv-pagination'); if (!pg || !pg.total_pages || pg.total_pages <= 1) { container.innerHTML = ''; return; } var current = pg.page || pg.current_page || 1; var total = pg.total_pages; var html = ''; html += ''; var start = Math.max(1, current - 2); var end = Math.min(total, current + 2); if (start > 1) { html += ''; if (start > 2) html += ''; } for (var i = start; i <= end; i++) { html += ''; } if (end < total) { if (end < total - 1) html += ''; html += ''; } html += ''; container.innerHTML = html; container.querySelectorAll('button[data-page]').forEach(function (btn) { btn.addEventListener('click', function () { invPage = parseInt(btn.getAttribute('data-page'), 10); loadInventory(); }); }); } // Search document.getElementById('btn-inv-search').addEventListener('click', function () { invQuery = document.getElementById('inv-search').value.trim(); invPage = 1; loadInventory(); }); document.getElementById('inv-search').addEventListener('keydown', function (e) { if (e.key === 'Enter') { invQuery = this.value.trim(); invPage = 1; loadInventory(); } }); // Clear All document.getElementById('btn-clear-all').addEventListener('click', function () { showConfirm( 'Limpiar Inventario', 'Se eliminaran todos los articulos de tu inventario. Esta accion no se puede deshacer.', function () { api('/api/inventory/items', { method: 'DELETE', headers: authHeaders() }).then(function () { toast('Inventario limpiado correctamente.', 'success'); loadInventory(); }).catch(function (err) { toast(err.message || 'Error al limpiar inventario.', 'error'); }); } ); }); // ================================================================ // Confirm Modal // ================================================================ var confirmCallback = null; function showConfirm(title, msg, onConfirm) { document.getElementById('confirm-title').textContent = title; document.getElementById('confirm-msg').textContent = msg; document.getElementById('confirm-modal').classList.add('active'); confirmCallback = onConfirm; } document.getElementById('confirm-cancel').addEventListener('click', function () { document.getElementById('confirm-modal').classList.remove('active'); confirmCallback = null; }); document.getElementById('confirm-ok').addEventListener('click', function () { document.getElementById('confirm-modal').classList.remove('active'); if (confirmCallback) { confirmCallback(); confirmCallback = null; } }); document.getElementById('confirm-modal').addEventListener('click', function (e) { if (e.target === this) { this.classList.remove('active'); confirmCallback = null; } }); // ================================================================ // Init // ================================================================ if (checkAuth()) { loadMapping(); } })();