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.

+
+ +
+
+ + + Campo del sistema: part_number +
+
+ + + Campo del sistema: price +
+
+ + + Campo del sistema: stock +
+
+ + + Campo del sistema: location +
+
+ + +
+
+
+ + + + +
+
+

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

+
+ + + + + + + + + + + + + +
ArchivoEstadoImportadosErroresFecha
Cargando...
+
+
+
+ + + + +
+
+

Mi Inventario

+
+ +
+ + +
+ +
+ + + + + + + + + + + + + + +
Numero de ParteNombrePrecioExistenciasUbicacionActualizado
Cargando...
+
+ +
+
+
+ + + + + +
+ + + + + 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(); + } + +})();