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...
Listado de facturas importadas desde archivos XML
' + + 'Cargando facturas...
Cargando facturas...
No se encontraron facturas.
' + + '' + escapeHtml(truncateUUID(f.uuid)) + '| Fecha | ' + + 'UUID | ' + + 'Emisor | ' + + 'Total | ' + + '# Art\u00edculos | ' + + '
|---|
Error al cargar facturas: ' + escapeHtml(err.message) + '
' + + '' + escapeHtml(c.no_identificacion || '') + '| ' + + ' | Descripci\u00f3n | ' + + 'No. Parte | ' + + 'Cantidad | ' + + 'Precio Unit. | ' + + 'Importe | ' + + 'Intercambio | ' + + '
|---|---|---|---|---|---|---|
| Sin conceptos | ||||||
Error al cargar la factura: ' + escapeHtml(err.message) + '
' + + 'Busca art\u00edculos por nombre, n\u00famero de parte o intercambio
' + + 'Ingrese un t\u00e9rmino de b\u00fasqueda para encontrar art\u00edculos.
' + + 'Buscando...
No se encontraron art\u00edculos para "' + escapeHtml(query) + '".
' + + '' + escapeHtml(a.no_identificacion || '') + '| ' + + ' | Descripci\u00f3n | ' + + 'No. Parte | ' + + 'Intercambio | ' + + 'Proveedor | ' + + 'Precio | ' + + 'Fecha | ' + + '
|---|
Error al buscar: ' + escapeHtml(err.message) + '
' + + 'Administra la correspondencia entre n\u00fameros de parte del proveedor y n\u00fameros de intercambio
' + + 'Cargando cat\u00e1logo...
No se encontraron registros en el cat\u00e1logo.
' + + '| No. Parte Proveedor | ' + + 'No. Parte Intercambio | ' + + 'Acciones | ' + + '
|---|
Error al cargar el cat\u00e1logo: ' + escapeHtml(err.message) + '
' + + 'Ajustes de la impresora Zebra y carpeta de archivos XML
' + + 'Error al cargar la configuraci\u00f3n: ' + escapeHtml(err.message) + '
' + + '