diff --git a/dashboard/bodega.css b/dashboard/bodega.css
new file mode 100644
index 0000000..3626c73
--- /dev/null
+++ b/dashboard/bodega.css
@@ -0,0 +1,485 @@
+/* ============================================================
+ bodega.css -- Styles for Nexus Autoparts Warehouse (Bodega)
+ ============================================================ */
+
+/* --- Layout --- */
+.bodega-container {
+ max-width: 1100px;
+ margin: 0 auto;
+ padding: 5.5rem 2rem 3rem;
+}
+
+/* --- Tabs --- */
+.bodega-tabs {
+ display: flex;
+ gap: 0;
+ border-bottom: 2px solid var(--border);
+ margin-bottom: 1.5rem;
+}
+
+.bodega-tab {
+ padding: 0.8rem 1.8rem;
+ background: transparent;
+ border: none;
+ color: var(--text-secondary);
+ font-size: 0.95rem;
+ font-weight: 600;
+ cursor: pointer;
+ border-bottom: 3px solid transparent;
+ transition: all 0.2s;
+ position: relative;
+ bottom: -2px;
+}
+
+.bodega-tab:hover {
+ color: var(--text-primary);
+ background: var(--bg-hover);
+}
+
+.bodega-tab.active {
+ color: var(--accent);
+ border-bottom-color: var(--accent);
+}
+
+.bodega-section {
+ display: none;
+}
+
+.bodega-section.active {
+ display: block;
+}
+
+/* --- Section Intro --- */
+.section-intro {
+ margin-bottom: 1.5rem;
+}
+
+.section-intro h2 {
+ font-size: 1.3rem;
+ font-weight: 700;
+ margin-bottom: 0.5rem;
+}
+
+.section-intro p {
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ line-height: 1.5;
+}
+
+/* --- Mapping Form --- */
+.mapping-form {
+ max-width: 550px;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 1.5rem;
+}
+
+.mapping-form .form-group {
+ margin-bottom: 1.25rem;
+}
+
+.mapping-form .form-label {
+ display: block;
+ margin-bottom: 0.4rem;
+ font-weight: 500;
+ font-size: 0.9rem;
+ color: var(--text-primary);
+}
+
+.required {
+ color: var(--danger);
+ font-weight: 700;
+}
+
+.optional {
+ color: var(--text-secondary);
+ font-size: 0.8rem;
+ font-weight: 400;
+}
+
+.form-hint {
+ display: block;
+ margin-top: 0.3rem;
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+}
+
+.form-actions {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-top: 1.25rem;
+}
+
+.status-msg {
+ font-size: 0.85rem;
+ font-weight: 500;
+}
+
+.status-msg.success {
+ color: var(--success);
+}
+
+.status-msg.error {
+ color: var(--danger);
+}
+
+/* --- Upload Zone --- */
+.upload-zone {
+ border: 2px dashed var(--border);
+ border-radius: 12px;
+ padding: 3rem 2rem;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.3s;
+ background: var(--bg-card);
+ margin-bottom: 1rem;
+}
+
+.upload-zone:hover,
+.upload-zone.dragover {
+ border-color: var(--accent);
+ background: rgba(255, 107, 53, 0.05);
+}
+
+.upload-icon {
+ font-size: 3rem;
+ margin-bottom: 0.75rem;
+}
+
+.upload-text {
+ font-size: 1rem;
+ font-weight: 500;
+ margin-bottom: 0.3rem;
+}
+
+.upload-hint {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+}
+
+/* --- Selected File --- */
+.selected-file {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ background: var(--bg-card);
+ border: 1px solid var(--accent);
+ border-radius: 8px;
+ padding: 0.6rem 1rem;
+ margin-bottom: 1rem;
+ font-size: 0.9rem;
+}
+
+.btn-icon {
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ font-size: 1.2rem;
+ cursor: pointer;
+ padding: 0.1rem 0.3rem;
+ line-height: 1;
+ transition: color 0.2s;
+}
+
+.btn-icon:hover {
+ color: var(--danger);
+}
+
+/* --- Upload Result --- */
+.upload-result {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 1.25rem;
+ margin-bottom: 1.5rem;
+}
+
+.upload-result h4 {
+ margin-bottom: 0.75rem;
+ font-size: 1rem;
+}
+
+.result-stats {
+ display: flex;
+ gap: 1.5rem;
+ margin-bottom: 0.75rem;
+}
+
+.result-stat {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ font-size: 0.9rem;
+}
+
+.result-stat.ok {
+ color: var(--success);
+}
+
+.result-stat.err {
+ color: var(--danger);
+}
+
+.error-samples {
+ margin-top: 0.75rem;
+ padding: 0.75rem;
+ background: var(--bg-secondary);
+ border-radius: 8px;
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ max-height: 150px;
+ overflow-y: auto;
+}
+
+.error-samples p {
+ margin-bottom: 0.3rem;
+}
+
+/* --- History --- */
+.history-section {
+ margin-top: 2rem;
+}
+
+.history-section h3 {
+ font-size: 1rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
+}
+
+/* --- Tables --- */
+.table-wrap {
+ overflow-x: auto;
+}
+
+.data-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.88rem;
+}
+
+.data-table thead th {
+ background: var(--bg-secondary);
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ font-size: 0.72rem;
+ font-weight: 600;
+ letter-spacing: 0.05em;
+ padding: 0.75rem 1rem;
+ text-align: left;
+ border-bottom: 1px solid var(--border);
+ white-space: nowrap;
+}
+
+.data-table tbody td {
+ padding: 0.7rem 1rem;
+ border-bottom: 1px solid var(--border);
+ color: var(--text-primary);
+}
+
+.data-table tbody tr:hover {
+ background: var(--bg-hover);
+}
+
+.empty-row {
+ text-align: center;
+ color: var(--text-secondary);
+ padding: 2rem 1rem !important;
+}
+
+/* --- Status Badges --- */
+.badge {
+ display: inline-block;
+ padding: 0.2rem 0.6rem;
+ border-radius: 10px;
+ font-size: 0.72rem;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+
+.badge-success {
+ background: rgba(34, 197, 94, 0.15);
+ color: var(--success);
+}
+
+.badge-error {
+ background: rgba(255, 68, 68, 0.15);
+ color: var(--danger);
+}
+
+.badge-pending {
+ background: rgba(245, 158, 11, 0.15);
+ color: var(--warning);
+}
+
+.badge-processing {
+ background: rgba(59, 130, 246, 0.15);
+ color: var(--info);
+}
+
+/* --- Inventory Toolbar --- */
+.inventory-toolbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 1rem;
+ flex-wrap: wrap;
+}
+
+.search-box {
+ display: flex;
+ gap: 0.5rem;
+ flex: 1;
+ max-width: 500px;
+}
+
+.search-box .form-input {
+ flex: 1;
+}
+
+.btn-danger {
+ background: rgba(255, 68, 68, 0.15);
+ border: 1px solid var(--danger);
+ color: var(--danger);
+ padding: 0.7rem 1.5rem;
+ border-radius: 10px;
+ font-weight: 600;
+ cursor: pointer;
+ font-size: 0.9rem;
+ transition: all 0.3s;
+}
+
+.btn-danger:hover {
+ background: var(--danger);
+ color: white;
+}
+
+/* --- Pagination --- */
+.bodega-pagination {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 0.5rem;
+ margin-top: 1.25rem;
+ flex-wrap: wrap;
+}
+
+.bodega-pagination button {
+ padding: 0.4rem 0.8rem;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--text-secondary);
+ cursor: pointer;
+ font-size: 0.85rem;
+ transition: all 0.2s;
+}
+
+.bodega-pagination button:hover:not(:disabled) {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+.bodega-pagination button.active {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: white;
+}
+
+.bodega-pagination button:disabled {
+ opacity: 0.4;
+ cursor: default;
+}
+
+/* --- Confirm Modal --- */
+.confirm-box {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ padding: 2rem;
+ max-width: 420px;
+ width: 100%;
+}
+
+.confirm-box h3 {
+ margin-bottom: 0.75rem;
+ font-size: 1.1rem;
+}
+
+.confirm-box p {
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ margin-bottom: 1.5rem;
+ line-height: 1.5;
+}
+
+.confirm-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.75rem;
+}
+
+/* --- Toast --- */
+#toast-container {
+ position: fixed;
+ bottom: 2rem;
+ right: 2rem;
+ z-index: 3000;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.toast {
+ padding: 0.8rem 1.2rem;
+ border-radius: 8px;
+ font-size: 0.85rem;
+ font-weight: 500;
+ animation: fadeIn 0.3s ease;
+ max-width: 350px;
+}
+
+.toast.success {
+ background: rgba(34, 197, 94, 0.15);
+ border: 1px solid var(--success);
+ color: var(--success);
+}
+
+.toast.error {
+ background: rgba(255, 68, 68, 0.15);
+ border: 1px solid var(--danger);
+ color: var(--danger);
+}
+
+/* --- Responsive --- */
+@media (max-width: 768px) {
+ .bodega-container {
+ padding: 5rem 1rem 2rem;
+ }
+
+ .bodega-tabs {
+ overflow-x: auto;
+ }
+
+ .bodega-tab {
+ padding: 0.7rem 1.2rem;
+ font-size: 0.85rem;
+ white-space: nowrap;
+ }
+
+ .inventory-toolbar {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .search-box {
+ max-width: none;
+ }
+
+ .result-stats {
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+}
diff --git a/dashboard/bodega.html b/dashboard/bodega.html
new file mode 100644
index 0000000..1003b76
--- /dev/null
+++ b/dashboard/bodega.html
@@ -0,0 +1,164 @@
+
+
+
+
+
+ Bodega — NEXUS AUTOPARTS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Mapeo de Columnas
+
Configura como se mapean las columnas de tu archivo CSV/Excel a los campos del sistema. Escribe el nombre exacto de la columna en tu archivo.
+
+
+
+
+
+
+
+
+
+
+
Subir Inventario
+
Sube un archivo CSV o Excel con tu inventario. Asegurate de haber configurado el mapeo de columnas primero.
+
+
+
+
📦
+
Arrastra tu archivo aqui o haz clic para seleccionar
+
CSV, XLS, XLSX — Max 10MB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Historial de Cargas
+
+
+
+
+ | Archivo |
+ Estado |
+ Importados |
+ Errores |
+ Fecha |
+
+
+
+ | Cargando... |
+
+
+
+
+
+
+
+
+
+
+
+
Mi Inventario
+
+
+
+
+
+
+
+
+ | Numero de Parte |
+ Nombre |
+ Precio |
+ Existencias |
+ Ubicacion |
+ Actualizado |
+
+
+
+ | Cargando... |
+
+
+
+
+
+
+
+
+
+
+
+
Confirmar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/bodega.js b/dashboard/bodega.js
new file mode 100644
index 0000000..2899b26
--- /dev/null
+++ b/dashboard/bodega.js
@@ -0,0 +1,522 @@
+/**
+ * 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();
+ }
+
+})();