diff --git a/src/public/css/styles.css b/src/public/css/styles.css new file mode 100644 index 0000000..b0592c0 --- /dev/null +++ b/src/public/css/styles.css @@ -0,0 +1,774 @@ +/* ============================================ + Portal Refaccionaria - Styles + ============================================ */ + +:root { + --sidebar-bg: #1a2332; + --sidebar-hover: #243044; + --sidebar-text: #94a3b8; + --sidebar-active: #ffffff; + --sidebar-active-bg: #2563eb; + --sidebar-width: 240px; + + --primary: #2563eb; + --primary-hover: #1d4ed8; + --primary-light: #dbeafe; + + --bg: #f1f5f9; + --card-bg: #ffffff; + --text: #1e293b; + --text-secondary: #64748b; + --text-muted: #94a3b8; + + --border: #e2e8f0; + --border-focus: #93c5fd; + + --success: #22c55e; + --success-bg: #f0fdf4; + --error: #ef4444; + --error-bg: #fef2f2; + --warning: #f59e0b; + --warning-bg: #fffbeb; + + --radius: 8px; + --radius-sm: 4px; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); + + --transition: 150ms ease; +} + +/* ---- Reset ---- */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 14px; + line-height: 1.5; + color: var(--text); + background: var(--bg); +} + +body { + display: flex; + overflow: hidden; +} + +/* ---- Sidebar ---- */ +.sidebar { + width: var(--sidebar-width); + min-width: var(--sidebar-width); + height: 100vh; + background: var(--sidebar-bg); + display: flex; + flex-direction: column; + overflow-y: auto; + z-index: 100; +} + +.sidebar-header { + padding: 24px 20px 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.sidebar-title { + font-size: 16px; + font-weight: 700; + color: var(--sidebar-active); + letter-spacing: -0.01em; +} + +.sidebar-nav { + flex: 1; + padding: 12px 0; +} + +.nav-link { + display: flex; + align-items: center; + padding: 10px 20px; + color: var(--sidebar-text); + text-decoration: none; + font-size: 14px; + font-weight: 500; + transition: all var(--transition); + border-left: 3px solid transparent; + gap: 12px; +} + +.nav-link:hover { + color: var(--sidebar-active); + background: var(--sidebar-hover); +} + +.nav-link.active { + color: var(--sidebar-active); + background: var(--sidebar-hover); + border-left-color: var(--primary); +} + +.nav-icon { + font-size: 16px; + width: 24px; + text-align: center; + flex-shrink: 0; +} + +.nav-text { + white-space: nowrap; +} + +.sidebar-footer { + padding: 16px 20px; + border-top: 1px solid rgba(255, 255, 255, 0.08); + color: var(--text-muted); + font-size: 11px; +} + +/* ---- Content Area ---- */ +.content { + flex: 1; + height: 100vh; + overflow-y: auto; + padding: 24px 32px; + background: var(--bg); +} + +/* ---- Loading ---- */ +.loading-screen { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 50vh; + color: var(--text-secondary); +} + +.spinner { + width: 32px; + height: 32px; + border: 3px solid var(--border); + border-top-color: var(--primary); + border-radius: 50%; + animation: spin 0.7s linear infinite; + margin-bottom: 12px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ---- Page Header ---- */ +.page-header { + margin-bottom: 24px; +} + +.page-header h2 { + font-size: 22px; + font-weight: 700; + color: var(--text); + margin-bottom: 4px; +} + +.page-header p { + color: var(--text-secondary); + font-size: 14px; +} + +/* ---- Cards ---- */ +.card { + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); +} + +.card-body { + padding: 20px; +} + +.card + .card { + margin-top: 16px; +} + +/* ---- Filters Bar ---- */ +.filters-bar { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: flex-end; + padding: 16px 20px; +} + +.filter-group { + display: flex; + flex-direction: column; + gap: 4px; +} + +.filter-group label { + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.03em; +} + +/* ---- Form Elements ---- */ +input[type="text"], +input[type="date"], +input[type="number"], +input[type="file"], +select { + height: 36px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + font-size: 13px; + color: var(--text); + background: var(--card-bg); + transition: border-color var(--transition), box-shadow var(--transition); + outline: none; + font-family: inherit; +} + +input[type="text"]:focus, +input[type="date"]:focus, +input[type="number"]:focus, +select:focus { + border-color: var(--border-focus); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); +} + +input[type="file"] { + height: auto; + padding: 8px 12px; + cursor: pointer; +} + +input[type="text"]::placeholder { + color: var(--text-muted); +} + +/* ---- Buttons ---- */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + height: 36px; + padding: 0 16px; + border: none; + border-radius: var(--radius-sm); + font-size: 13px; + font-weight: 600; + font-family: inherit; + cursor: pointer; + transition: all var(--transition); + white-space: nowrap; + text-decoration: none; +} + +.btn:active { + transform: scale(0.98); +} + +.btn-primary { + background: var(--primary); + color: #ffffff; +} + +.btn-primary:hover { + background: var(--primary-hover); +} + +.btn-secondary { + background: var(--card-bg); + color: var(--text); + border: 1px solid var(--border); +} + +.btn-secondary:hover { + background: var(--bg); +} + +.btn-success { + background: var(--success); + color: #ffffff; +} + +.btn-success:hover { + background: #16a34a; +} + +.btn-sm { + height: 30px; + padding: 0 10px; + font-size: 12px; +} + +.btn-icon { + width: 30px; + height: 30px; + padding: 0; + font-size: 14px; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* ---- Tables ---- */ +.table-container { + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +thead { + background: var(--bg); + position: sticky; + top: 0; + z-index: 1; +} + +thead th { + padding: 10px 12px; + text-align: left; + font-weight: 600; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-secondary); + border-bottom: 2px solid var(--border); + white-space: nowrap; +} + +tbody td { + padding: 10px 12px; + border-bottom: 1px solid var(--border); + color: var(--text); + vertical-align: middle; +} + +tbody tr { + transition: background var(--transition); +} + +tbody tr:hover { + background: #f8fafc; +} + +tbody tr.clickable-row { + cursor: pointer; +} + +tbody tr.clickable-row:hover { + background: var(--primary-light); +} + +/* ---- Checkbox ---- */ +input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: var(--primary); +} + +/* ---- Currency ---- */ +.currency { + font-variant-numeric: tabular-nums; + text-align: right; + white-space: nowrap; +} + +/* ---- Empty State ---- */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 24px; + color: var(--text-secondary); + text-align: center; +} + +.empty-state .empty-icon { + font-size: 48px; + margin-bottom: 12px; + opacity: 0.4; +} + +.empty-state p { + font-size: 14px; + max-width: 300px; +} + +/* ---- Badge ---- */ +.badge { + display: inline-block; + padding: 2px 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; +} + +.badge-primary { + background: var(--primary-light); + color: var(--primary); +} + +/* ---- Detail Header ---- */ +.detail-header { + display: flex; + flex-wrap: wrap; + gap: 24px; + align-items: flex-start; +} + +.detail-meta { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 16px; + flex: 1; +} + +.meta-item { + display: flex; + flex-direction: column; + gap: 2px; +} + +.meta-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); +} + +.meta-value { + font-size: 15px; + font-weight: 500; + color: var(--text); + word-break: break-all; +} + +.meta-value.total { + font-size: 20px; + font-weight: 700; + color: var(--primary); +} + +/* ---- Back Button ---- */ +.back-btn { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--primary); + text-decoration: none; + font-weight: 600; + font-size: 14px; + margin-bottom: 16px; + transition: color var(--transition); + cursor: pointer; + background: none; + border: none; + padding: 0; + font-family: inherit; +} + +.back-btn:hover { + color: var(--primary-hover); +} + +/* ---- Print Section ---- */ +.print-section { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: var(--bg); + border-top: 1px solid var(--border); + border-radius: 0 0 var(--radius) var(--radius); +} + +.print-section label { + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); +} + +.print-section input[type="number"] { + width: 80px; +} + +/* ---- Config Form ---- */ +.config-form { + display: grid; + gap: 16px; + max-width: 480px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.form-group label { + font-size: 13px; + font-weight: 600; + color: var(--text); +} + +.form-group input { + width: 100%; +} + +.form-group .form-hint { + font-size: 11px; + color: var(--text-muted); +} + +/* ---- Import Section ---- */ +.import-section { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid var(--border); +} + +.import-result { + font-size: 13px; + font-weight: 600; + color: var(--success); +} + +/* ---- Inline Edit ---- */ +.inline-edit-input { + width: 100%; + max-width: 200px; +} + +/* ---- Add Form (Catalog) ---- */ +.add-form { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: flex-end; + padding: 16px 20px; + border-top: 1px solid var(--border); + background: var(--bg); + border-radius: 0 0 var(--radius) var(--radius); +} + +/* ---- Search Bar ---- */ +.search-bar { + display: flex; + gap: 8px; + max-width: 600px; + margin-bottom: 16px; +} + +.search-bar input[type="text"] { + flex: 1; + height: 40px; + font-size: 14px; +} + +.search-bar .btn { + height: 40px; +} + +/* ---- Toast Notifications ---- */ +.toast-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + display: flex; + flex-direction: column; + gap: 8px; +} + +.toast { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 20px; + border-radius: var(--radius); + box-shadow: var(--shadow-lg); + font-size: 13px; + font-weight: 500; + animation: toastIn 0.3s ease; + min-width: 280px; + max-width: 420px; +} + +.toast-success { + background: var(--success-bg); + border: 1px solid var(--success); + color: #166534; +} + +.toast-error { + background: var(--error-bg); + border: 1px solid var(--error); + color: #991b1b; +} + +.toast-warning { + background: var(--warning-bg); + border: 1px solid var(--warning); + color: #92400e; +} + +@keyframes toastIn { + from { + opacity: 0; + transform: translateX(40px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.toast-exit { + animation: toastOut 0.2s ease forwards; +} + +@keyframes toastOut { + to { + opacity: 0; + transform: translateX(40px); + } +} + +/* ---- Utility ---- */ +.text-right { + text-align: right; +} + +.text-center { + text-align: center; +} + +.text-muted { + color: var(--text-secondary); +} + +.mt-16 { + margin-top: 16px; +} + +.mb-16 { + margin-bottom: 16px; +} + +.flex-gap { + display: flex; + gap: 12px; + align-items: center; +} + +/* ---- Responsive ---- */ +@media (max-width: 768px) { + body { + flex-direction: column; + } + + .sidebar { + width: 100%; + min-width: 100%; + height: auto; + max-height: none; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + } + + .sidebar-header { + padding: 12px 16px; + border-bottom: none; + border-right: 1px solid rgba(255, 255, 255, 0.08); + } + + .sidebar-title { + font-size: 14px; + } + + .sidebar-nav { + display: flex; + flex: 1; + padding: 0; + overflow-x: auto; + } + + .nav-link { + padding: 12px 14px; + border-left: none; + border-bottom: 3px solid transparent; + font-size: 13px; + } + + .nav-link.active { + border-left-color: transparent; + border-bottom-color: var(--primary); + } + + .sidebar-footer { + display: none; + } + + .content { + height: calc(100vh - 50px); + padding: 16px; + } + + .filters-bar { + flex-direction: column; + align-items: stretch; + } + + .filter-group { + width: 100%; + } + + .filter-group input, + .filter-group select { + width: 100%; + } + + .detail-meta { + grid-template-columns: 1fr 1fr; + } + + .search-bar { + max-width: 100%; + } +} + +@media (max-width: 480px) { + .detail-meta { + grid-template-columns: 1fr; + } + + .print-section { + flex-wrap: wrap; + } +} diff --git a/src/public/js/app.js b/src/public/js/app.js new file mode 100644 index 0000000..49176e0 --- /dev/null +++ b/src/public/js/app.js @@ -0,0 +1,800 @@ +/* ============================================ + Portal Refaccionaria - app.js + Single-page application with hash-based routing + ============================================ */ + +(function () { + 'use strict'; + + // ---- Constants ---- + const contentEl = document.getElementById('content'); + const toastContainer = document.getElementById('toastContainer'); + + const currencyFmt = new Intl.NumberFormat('es-MX', { + style: 'currency', + currency: 'MXN', + }); + + // ---- API Helper ---- + async function fetchJSON(url, options) { + const res = await fetch(url, options); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error || `Error ${res.status}`); + } + return res.json(); + } + + // ---- Toast Notifications ---- + function showToast(message, type) { + // type: 'success' | 'error' | 'warning' + const toast = document.createElement('div'); + toast.className = 'toast toast-' + type; + const icons = { success: '\u2705', error: '\u274C', warning: '\u26A0\uFE0F' }; + toast.innerHTML = '' + (icons[type] || '') + '' + escapeHtml(message) + ''; + toastContainer.appendChild(toast); + setTimeout(function () { + toast.classList.add('toast-exit'); + setTimeout(function () { toast.remove(); }, 200); + }, 3500); + } + + // ---- Utility ---- + function escapeHtml(str) { + if (!str) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function truncateUUID(uuid) { + if (!uuid) return ''; + return uuid.substring(0, 8) + '...'; + } + + function formatDate(dateStr) { + if (!dateStr) return ''; + // Dates come as ISO strings, show YYYY-MM-DD + return dateStr.substring(0, 10); + } + + function showLoading() { + contentEl.innerHTML = + '

Cargando...

'; + } + + function buildQueryString(params) { + var parts = []; + for (var key in params) { + if (params[key]) { + parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); + } + } + return parts.length ? '?' + parts.join('&') : ''; + } + + // ---- Router ---- + function router() { + var hash = window.location.hash || '#facturas'; + var parts = hash.substring(1).split('/'); + var route = parts[0]; + var param = parts[1] || null; + + // Update active nav link + var links = document.querySelectorAll('.nav-link'); + links.forEach(function (link) { + var linkRoute = link.getAttribute('data-route'); + if (linkRoute === route || (route === 'factura' && linkRoute === 'facturas')) { + link.classList.add('active'); + } else { + link.classList.remove('active'); + } + }); + + showLoading(); + + switch (route) { + case 'facturas': + renderFacturas(); + break; + case 'factura': + renderFacturaDetail(param); + break; + case 'articulos': + renderArticulos(); + break; + case 'catalogo': + renderCatalogo(); + break; + case 'config': + renderConfig(); + break; + default: + renderFacturas(); + } + } + + // ============================================ + // VIEW: Facturas List + // ============================================ + async function renderFacturas() { + contentEl.innerHTML = + '' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '

Cargando facturas...

' + + '
' + + '
'; + + // Wire up filter button and enter key + document.getElementById('f-btn-buscar').addEventListener('click', loadFacturas); + ['f-fecha-inicio', 'f-fecha-fin', 'f-proveedor', 'f-buscar'].forEach(function (id) { + document.getElementById(id).addEventListener('keydown', function (e) { + if (e.key === 'Enter') loadFacturas(); + }); + }); + + await loadFacturas(); + } + + async function loadFacturas() { + var container = document.getElementById('facturas-table-container'); + if (!container) return; + container.innerHTML = '

Cargando facturas...

'; + + var params = { + fecha_inicio: document.getElementById('f-fecha-inicio').value, + fecha_fin: document.getElementById('f-fecha-fin').value, + proveedor: document.getElementById('f-proveedor').value, + q: document.getElementById('f-buscar').value, + }; + + try { + var facturas = await fetchJSON('/api/facturas' + buildQueryString(params)); + + if (!facturas.length) { + container.innerHTML = + '
' + + '
📄
' + + '

No se encontraron facturas.

' + + '
'; + return; + } + + var rows = facturas.map(function (f) { + return '' + + '' + escapeHtml(formatDate(f.fecha)) + '' + + '' + escapeHtml(truncateUUID(f.uuid)) + '' + + '' + escapeHtml(f.nombre_emisor) + '' + + '' + currencyFmt.format(f.total || 0) + '' + + '' + (f.num_conceptos || 0) + '' + + ''; + }).join(''); + + container.innerHTML = + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + rows + '' + + '
FechaUUIDEmisorTotal# Art\u00edculos
'; + + // Row click navigation + container.querySelectorAll('.clickable-row').forEach(function (row) { + row.addEventListener('click', function () { + window.location.hash = '#factura/' + row.getAttribute('data-id'); + }); + }); + } catch (err) { + container.innerHTML = + '
' + + '
⚠️
' + + '

Error al cargar facturas: ' + escapeHtml(err.message) + '

' + + '
'; + } + } + + // ============================================ + // VIEW: Factura Detail + // ============================================ + async function renderFacturaDetail(id) { + if (!id) { + window.location.hash = '#facturas'; + return; + } + + try { + var data = await fetchJSON('/api/facturas/' + encodeURIComponent(id)); + var f = data.factura; + var conceptos = data.conceptos || []; + + var conceptoRows = conceptos.map(function (c, index) { + return '' + + '' + + '' + escapeHtml(c.descripcion) + '' + + '' + escapeHtml(c.no_identificacion || '') + '' + + '' + (c.cantidad || 0) + '' + + '' + currencyFmt.format(c.valor_unitario || 0) + '' + + '' + currencyFmt.format(c.importe || 0) + '' + + '' + escapeHtml(c.intercambio || '-') + '' + + ''; + }).join(''); + + contentEl.innerHTML = + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + 'Emisor' + + '' + escapeHtml(f.nombre_emisor) + '' + + '
' + + '
' + + 'RFC' + + '' + escapeHtml(f.rfc_emisor) + '' + + '
' + + '
' + + 'UUID' + + '' + escapeHtml(f.uuid) + '' + + '
' + + '
' + + 'Fecha' + + '' + escapeHtml(formatDate(f.fecha)) + '' + + '
' + + '
' + + 'Subtotal' + + '' + currencyFmt.format(f.subtotal || 0) + '' + + '
' + + '
' + + 'Total' + + '' + currencyFmt.format(f.total || 0) + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '

Conceptos

' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + (conceptoRows || '') + '' + + '
Descripci\u00f3nNo. ParteCantidadPrecio Unit.ImporteIntercambio
Sin conceptos
' + + '
' + + '' + + '
'; + + // Store conceptos for printing + contentEl._conceptos = conceptos; + + // Back button + document.getElementById('btn-back').addEventListener('click', function () { + window.location.hash = '#facturas'; + }); + + // Select all checkbox + document.getElementById('select-all-conceptos').addEventListener('change', function () { + var checked = this.checked; + document.querySelectorAll('.concepto-check').forEach(function (cb) { + cb.checked = checked; + }); + }); + + // Print button + document.getElementById('btn-print').addEventListener('click', function () { + handlePrint(contentEl._conceptos, '.concepto-check', 'print-qty'); + }); + } catch (err) { + contentEl.innerHTML = + '' + + '
' + + '
⚠️
' + + '

Error al cargar la factura: ' + escapeHtml(err.message) + '

' + + '
'; + document.getElementById('btn-back').addEventListener('click', function () { + window.location.hash = '#facturas'; + }); + } + } + + // ============================================ + // VIEW: Articulos Search + // ============================================ + async function renderArticulos() { + contentEl.innerHTML = + '' + + '' + + '
' + + '
' + + '
' + + '
🔍
' + + '

Ingrese un t\u00e9rmino de b\u00fasqueda para encontrar art\u00edculos.

' + + '
' + + '
' + + '' + + '
'; + + document.getElementById('art-btn-buscar').addEventListener('click', loadArticulos); + document.getElementById('art-query').addEventListener('keydown', function (e) { + if (e.key === 'Enter') loadArticulos(); + }); + + document.getElementById('art-btn-print').addEventListener('click', function () { + handlePrint(contentEl._articulos, '.articulo-check', 'art-print-qty'); + }); + + // Focus the search input + document.getElementById('art-query').focus(); + } + + async function loadArticulos() { + var query = document.getElementById('art-query').value.trim(); + var container = document.getElementById('articulos-table-container'); + var printSection = document.getElementById('art-print-section'); + + if (query.length < 2) { + showToast('Ingrese al menos 2 caracteres para buscar.', 'warning'); + return; + } + + container.innerHTML = '

Buscando...

'; + printSection.style.display = 'none'; + + try { + var articulos = await fetchJSON('/api/articulos?q=' + encodeURIComponent(query)); + contentEl._articulos = articulos; + + if (!articulos.length) { + container.innerHTML = + '
' + + '
🔍
' + + '

No se encontraron art\u00edculos para "' + escapeHtml(query) + '".

' + + '
'; + return; + } + + var rows = articulos.map(function (a, index) { + return '' + + '' + + '' + escapeHtml(a.descripcion) + '' + + '' + escapeHtml(a.no_identificacion || '') + '' + + '' + escapeHtml(a.intercambio || '-') + '' + + '' + escapeHtml(a.nombre_emisor) + '' + + '' + currencyFmt.format(a.valor_unitario || 0) + '' + + '' + escapeHtml(formatDate(a.fecha)) + '' + + ''; + }).join(''); + + container.innerHTML = + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + rows + '' + + '
Descripci\u00f3nNo. ParteIntercambioProveedorPrecioFecha
'; + + printSection.style.display = 'flex'; + + // Select all + document.getElementById('select-all-articulos').addEventListener('change', function () { + var checked = this.checked; + document.querySelectorAll('.articulo-check').forEach(function (cb) { + cb.checked = checked; + }); + }); + } catch (err) { + container.innerHTML = + '
' + + '
⚠️
' + + '

Error al buscar: ' + escapeHtml(err.message) + '

' + + '
'; + } + } + + // ============================================ + // VIEW: Catalogo + // ============================================ + async function renderCatalogo() { + contentEl.innerHTML = + '' + + '
' + + '
' + + '' + + '' + + '' + + '
' + + '
' + + '' + + '
' + + '
' + + '

Cargando cat\u00e1logo...

' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
'; + + // Wire up events + document.getElementById('cat-btn-import').addEventListener('click', importCatalog); + document.getElementById('cat-btn-add').addEventListener('click', addCatalogEntry); + + var debounceTimer = null; + document.getElementById('cat-query').addEventListener('input', function () { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(loadCatalogo, 300); + }); + + await loadCatalogo(); + } + + async function loadCatalogo() { + var container = document.getElementById('catalogo-table-container'); + if (!container) return; + var query = document.getElementById('cat-query').value.trim(); + + try { + var items = await fetchJSON('/api/catalogo' + (query ? '?q=' + encodeURIComponent(query) : '')); + + if (!items.length) { + container.innerHTML = + '
' + + '
📋
' + + '

No se encontraron registros en el cat\u00e1logo.

' + + '
'; + return; + } + + var rows = items.map(function (item) { + return '' + + '' + escapeHtml(item.no_parte_proveedor) + '' + + '' + escapeHtml(item.no_parte_intercambio) + '' + + '' + + '' + + '' + + ''; + }).join(''); + + container.innerHTML = + '' + + '' + + '' + + '' + + '' + + '' + + '' + rows + '' + + '
No. Parte ProveedorNo. Parte IntercambioAcciones
'; + + // Attach edit handlers + container.querySelectorAll('.cat-btn-edit').forEach(function (btn) { + btn.addEventListener('click', function () { + startInlineEdit(btn.closest('tr')); + }); + }); + } catch (err) { + container.innerHTML = + '
' + + '
⚠️
' + + '

Error al cargar el cat\u00e1logo: ' + escapeHtml(err.message) + '

' + + '
'; + } + } + + function startInlineEdit(row) { + var parteProveedor = row.getAttribute('data-parte'); + var intercambioCell = row.querySelector('.cat-col-intercambio'); + var currentValue = intercambioCell.textContent; + var actionsCell = row.querySelector('td:last-child'); + + intercambioCell.innerHTML = + ''; + actionsCell.innerHTML = + ' ' + + ''; + + var input = intercambioCell.querySelector('input'); + input.focus(); + input.select(); + + actionsCell.querySelector('.cat-btn-save').addEventListener('click', async function () { + var newValue = input.value.trim(); + if (!newValue) { + showToast('El valor no puede estar vac\u00edo.', 'warning'); + return; + } + try { + await fetchJSON('/api/catalogo/' + encodeURIComponent(parteProveedor), { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ noParteIntercambio: newValue }), + }); + showToast('Registro actualizado correctamente.', 'success'); + loadCatalogo(); + } catch (err) { + showToast('Error al actualizar: ' + err.message, 'error'); + } + }); + + actionsCell.querySelector('.cat-btn-cancel').addEventListener('click', function () { + loadCatalogo(); + }); + + input.addEventListener('keydown', function (e) { + if (e.key === 'Enter') actionsCell.querySelector('.cat-btn-save').click(); + if (e.key === 'Escape') actionsCell.querySelector('.cat-btn-cancel').click(); + }); + } + + async function importCatalog() { + var fileInput = document.getElementById('cat-file'); + var resultSpan = document.getElementById('cat-import-result'); + + if (!fileInput.files.length) { + showToast('Seleccione un archivo para importar.', 'warning'); + return; + } + + var formData = new FormData(); + formData.append('file', fileInput.files[0]); + + try { + resultSpan.textContent = 'Importando...'; + var res = await fetchJSON('/api/catalogo/import', { + method: 'POST', + body: formData, + }); + resultSpan.textContent = res.imported + ' registros importados'; + showToast(res.imported + ' registros importados exitosamente.', 'success'); + fileInput.value = ''; + loadCatalogo(); + } catch (err) { + resultSpan.textContent = ''; + showToast('Error al importar: ' + err.message, 'error'); + } + } + + async function addCatalogEntry() { + var proveedorInput = document.getElementById('cat-new-proveedor'); + var intercambioInput = document.getElementById('cat-new-intercambio'); + var proveedor = proveedorInput.value.trim(); + var intercambio = intercambioInput.value.trim(); + + if (!proveedor || !intercambio) { + showToast('Complete ambos campos para agregar un registro.', 'warning'); + return; + } + + try { + await fetchJSON('/api/catalogo', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + no_parte_proveedor: proveedor, + no_parte_intercambio: intercambio, + }), + }); + showToast('Registro agregado correctamente.', 'success'); + proveedorInput.value = ''; + intercambioInput.value = ''; + loadCatalogo(); + } catch (err) { + showToast('Error al agregar: ' + err.message, 'error'); + } + } + + // ============================================ + // VIEW: Configuration + // ============================================ + async function renderConfig() { + showLoading(); + + try { + var config = await fetchJSON('/api/config'); + + contentEl.innerHTML = + '' + + '
' + + '
' + + '
' + + '
' + + '' + + '' + + 'Direcci\u00f3n IP de la impresora de etiquetas en la red local' + + '
' + + '
' + + '' + + '' + + 'Puerto TCP de la impresora (por defecto: 9100)' + + '
' + + '
' + + '' + + '' + + 'Ruta donde se monitorean los archivos XML de facturas' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '
'; + + document.getElementById('cfg-btn-save').addEventListener('click', saveConfig); + } catch (err) { + contentEl.innerHTML = + '' + + '
' + + '
⚠️
' + + '

Error al cargar la configuraci\u00f3n: ' + escapeHtml(err.message) + '

' + + '
'; + } + } + + async function saveConfig() { + var btn = document.getElementById('cfg-btn-save'); + btn.disabled = true; + btn.textContent = 'Guardando...'; + + var payload = { + zebra_ip: document.getElementById('cfg-zebra-ip').value.trim(), + zebra_port: document.getElementById('cfg-zebra-port').value.trim(), + xml_folder: document.getElementById('cfg-xml-folder').value.trim(), + label_width: document.getElementById('cfg-label-width').value.trim(), + label_height: document.getElementById('cfg-label-height').value.trim(), + }; + + try { + await fetchJSON('/api/config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + showToast('Configuraci\u00f3n guardada exitosamente.', 'success'); + } catch (err) { + showToast('Error al guardar: ' + err.message, 'error'); + } finally { + btn.disabled = false; + btn.textContent = 'Guardar Configuraci\u00f3n'; + } + } + + // ============================================ + // SHARED: Print Handler + // ============================================ + async function handlePrint(dataArray, checkboxSelector, qtyInputId) { + var checkboxes = document.querySelectorAll(checkboxSelector + ':checked'); + + if (!checkboxes.length) { + showToast('Seleccione al menos un art\u00edculo para imprimir.', 'warning'); + return; + } + + var qty = parseInt(document.getElementById(qtyInputId).value, 10) || 1; + + var articles = []; + checkboxes.forEach(function (cb) { + var index = parseInt(cb.getAttribute('data-index'), 10); + var item = dataArray[index]; + if (item) { + articles.push({ + descripcion: item.descripcion, + noIdentificacion: item.no_identificacion, + intercambio: item.intercambio, + quantity: qty, + }); + } + }); + + if (!articles.length) { + showToast('No se pudieron preparar los art\u00edculos seleccionados.', 'error'); + return; + } + + try { + var res = await fetchJSON('/api/print', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ articles: articles }), + }); + showToast('Se enviaron ' + res.labels + ' etiqueta(s) a la impresora.', 'success'); + } catch (err) { + showToast('Error al imprimir: ' + err.message, 'error'); + } + } + + // ---- Initialize ---- + window.addEventListener('hashchange', router); + window.addEventListener('DOMContentLoaded', function () { + if (!window.location.hash) { + window.location.hash = '#facturas'; + } else { + router(); + } + }); + +})();