diff --git a/dashboard/captura.css b/dashboard/captura.css new file mode 100644 index 0000000..b8bfbe7 --- /dev/null +++ b/dashboard/captura.css @@ -0,0 +1,660 @@ +/* ============================================================ + captura.css -- Styles for Nexus Autoparts Data Entry + ============================================================ */ + +/* --- Tabs --- */ +.captura-tabs { + display: flex; + gap: 0; + border-bottom: 2px solid var(--border); + margin-bottom: 1.5rem; +} + +.captura-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; +} + +.captura-tab:hover { + color: var(--text-primary); + background: var(--bg-hover); +} + +.captura-tab.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +.captura-tab .tab-badge { + background: var(--accent); + color: #fff; + font-size: 0.7rem; + padding: 0.15rem 0.5rem; + border-radius: 10px; + margin-left: 0.5rem; + font-weight: 700; +} + +.captura-section { + display: none; +} + +.captura-section.active { + display: block; +} + +/* --- Vehicle Selector (Section 1) --- */ +.vehicle-filters { + display: flex; + gap: 1rem; + margin-bottom: 1rem; + flex-wrap: wrap; + align-items: flex-end; +} + +.vehicle-filters .filter-group { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.vehicle-filters label { + font-size: 0.75rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.vehicle-filters select, +.vehicle-filters input { + padding: 0.5rem 0.8rem; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 0.9rem; + min-width: 160px; +} + +.vehicle-filters select:focus, +.vehicle-filters input:focus { + outline: none; + border-color: var(--accent); +} + +/* --- Vehicle List --- */ +.vehicle-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 0.8rem; + max-height: 400px; + overflow-y: auto; + padding-right: 0.5rem; +} + +.vehicle-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1rem; + cursor: pointer; + transition: all 0.2s; +} + +.vehicle-card:hover { + border-color: var(--accent); + background: var(--bg-hover); +} + +.vehicle-card .vc-brand { + font-weight: 700; + font-size: 0.95rem; + color: var(--accent); +} + +.vehicle-card .vc-model { + font-size: 1.1rem; + font-weight: 600; + margin: 0.2rem 0; +} + +.vehicle-card .vc-details { + font-size: 0.8rem; + color: var(--text-secondary); +} + +.vehicle-card .vc-parts-count { + margin-top: 0.5rem; + font-size: 0.75rem; + color: var(--success); +} + +/* --- Vehicle Header (when editing) --- */ +.vehicle-header { + background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-hover) 100%); + border: 1px solid var(--accent); + border-radius: 12px; + padding: 1rem 1.5rem; + margin-bottom: 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.vehicle-header .vh-info { + display: flex; + gap: 1.5rem; + align-items: center; + flex-wrap: wrap; +} + +.vehicle-header .vh-label { + font-size: 0.7rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.vehicle-header .vh-value { + font-size: 1.1rem; + font-weight: 700; +} + +.vehicle-header .vh-brand { color: var(--accent); } + +.vehicle-header .vh-actions { + display: flex; + gap: 0.5rem; +} + +/* --- Part Groups Table --- */ +.category-section { + margin-bottom: 1.5rem; +} + +.category-header { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px 8px 0 0; + padding: 0.6rem 1rem; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + user-select: none; +} + +.category-header:hover { + background: var(--bg-hover); +} + +.category-header h3 { + font-size: 0.9rem; + font-weight: 700; + color: var(--accent); +} + +.category-header .cat-toggle { + font-size: 0.8rem; + color: var(--text-secondary); + transition: transform 0.2s; +} + +.category-header.collapsed .cat-toggle { + transform: rotate(-90deg); +} + +.category-body { + border: 1px solid var(--border); + border-top: none; + border-radius: 0 0 8px 8px; +} + +.category-body.collapsed { + display: none; +} + +.group-section { + border-bottom: 1px solid var(--border); + padding: 0.8rem 1rem; +} + +.group-section:last-child { + border-bottom: none; +} + +.group-name { + font-size: 0.85rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +/* --- Part Rows --- */ +.part-rows { + display: flex; + flex-direction: column; + gap: 0.4rem; + margin-bottom: 0.4rem; +} + +.part-row { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.part-row input { + padding: 0.4rem 0.6rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + font-size: 0.85rem; +} + +.part-row input:focus { + outline: none; + border-color: var(--accent); +} + +.part-row .pr-oem { + width: 160px; + font-family: monospace; +} + +.part-row .pr-name { + flex: 1; + min-width: 150px; +} + +.part-row .pr-qty { + width: 50px; + text-align: center; +} + +.part-row .pr-btn { + padding: 0.3rem 0.6rem; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.8rem; + font-weight: 600; + transition: all 0.2s; +} + +.part-row .pr-save { + background: var(--success); + color: #fff; +} + +.part-row .pr-save:hover { background: #1ea34e; } + +.part-row .pr-delete { + background: var(--danger); + color: #fff; +} + +.part-row .pr-delete:hover { background: #cc3333; } + +.part-row.saved { + background: rgba(34, 197, 94, 0.08); + border-radius: 6px; + padding: 0.2rem 0.4rem; +} + +.part-row.saved input { + background: transparent; + border-color: var(--success); + color: var(--success); +} + +.btn-add-part { + background: transparent; + border: 1px dashed var(--border); + border-radius: 6px; + padding: 0.3rem 0.8rem; + color: var(--text-secondary); + font-size: 0.8rem; + cursor: pointer; + transition: all 0.2s; +} + +.btn-add-part:hover { + border-color: var(--accent); + color: var(--accent); +} + +/* --- Progress Bar --- */ +.progress-bar { + background: var(--bg-secondary); + border-radius: 10px; + height: 8px; + overflow: hidden; + margin: 0.5rem 0; +} + +.progress-bar .progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--accent), var(--success)); + border-radius: 10px; + transition: width 0.3s; +} + +.progress-text { + font-size: 0.75rem; + color: var(--text-secondary); +} + +/* --- Section 2: Intercambios --- */ +.part-detail-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1rem; + margin-bottom: 1rem; +} + +.part-detail-card .pdc-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.8rem; +} + +.part-detail-card .pdc-oem { + font-family: monospace; + font-size: 1rem; + font-weight: 700; + color: var(--accent); +} + +.part-detail-card .pdc-name { + font-size: 0.85rem; + color: var(--text-secondary); +} + +.part-detail-card .pdc-group { + font-size: 0.75rem; + color: var(--text-secondary); + background: var(--bg-hover); + padding: 0.2rem 0.5rem; + border-radius: 4px; +} + +.aftermarket-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; +} + +.aftermarket-table th { + text-align: left; + padding: 0.5rem; + border-bottom: 1px solid var(--border); + color: var(--text-secondary); + font-size: 0.75rem; + text-transform: uppercase; +} + +.aftermarket-table td { + padding: 0.4rem 0.5rem; + border-bottom: 1px solid rgba(42, 42, 58, 0.5); +} + +.aftermarket-form { + display: flex; + gap: 0.5rem; + align-items: flex-end; + flex-wrap: wrap; + margin-top: 0.8rem; + padding-top: 0.8rem; + border-top: 1px dashed var(--border); +} + +.aftermarket-form .af-field { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.aftermarket-form label { + font-size: 0.7rem; + color: var(--text-secondary); + text-transform: uppercase; +} + +.aftermarket-form select, +.aftermarket-form input { + padding: 0.4rem 0.6rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + font-size: 0.85rem; +} + +.aftermarket-form select:focus, +.aftermarket-form input:focus { + outline: none; + border-color: var(--accent); +} + +/* --- Section 3: Imágenes --- */ +.image-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1rem; + display: flex; + gap: 1rem; + align-items: center; + margin-bottom: 0.8rem; +} + +.image-card .ic-preview { + width: 100px; + height: 100px; + background: var(--bg-secondary); + border: 2px dashed var(--border); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + font-size: 0.75rem; + overflow: hidden; + flex-shrink: 0; +} + +.image-card .ic-preview img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.image-card .ic-info { + flex: 1; +} + +.image-card .ic-oem { + font-family: monospace; + font-weight: 700; + color: var(--accent); +} + +.image-card .ic-name { + font-size: 0.85rem; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.image-card .ic-upload { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.image-card .ic-upload input[type="file"] { + font-size: 0.8rem; + color: var(--text-secondary); +} + +/* --- Search bar --- */ +.captura-search { + display: flex; + gap: 0.8rem; + margin-bottom: 1rem; + align-items: center; +} + +.captura-search input { + padding: 0.5rem 0.8rem; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 0.9rem; + flex: 1; + max-width: 400px; +} + +.captura-search input:focus { + outline: none; + border-color: var(--accent); +} + +/* --- Pagination --- */ +.captura-pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; + margin-top: 1rem; + padding: 0.5rem; +} + +.captura-pagination button { + padding: 0.4rem 0.8rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + cursor: pointer; + font-size: 0.85rem; +} + +.captura-pagination button:hover { + border-color: var(--accent); +} + +.captura-pagination button:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.captura-pagination .page-info { + font-size: 0.8rem; + color: var(--text-secondary); +} + +/* --- Empty state --- */ +.empty-state { + text-align: center; + padding: 3rem; + color: var(--text-secondary); +} + +.empty-state .es-icon { + font-size: 2.5rem; + margin-bottom: 0.5rem; +} + +.empty-state .es-text { + font-size: 0.9rem; +} + +/* --- Toast notifications --- */ +.toast { + position: fixed; + bottom: 2rem; + right: 2rem; + padding: 0.8rem 1.5rem; + border-radius: 10px; + color: #fff; + font-weight: 600; + font-size: 0.9rem; + z-index: 9999; + animation: toastIn 0.3s ease; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); +} + +.toast.success { background: var(--success); } +.toast.error { background: var(--danger); } + +@keyframes toastIn { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +/* --- Loading spinner --- */ +.loading { + display: flex; + justify-content: center; + padding: 2rem; +} + +.spinner { + width: 30px; + height: 30px; + border: 3px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* --- Layout --- */ +.captura-container { + max-width: 1200px; + margin: 0 auto; + padding: 5rem 2rem 2rem; +} + +/* --- Status tabs for vehicles --- */ +.status-tabs { + display: flex; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.status-tab { + padding: 0.4rem 1rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 20px; + color: var(--text-secondary); + font-size: 0.8rem; + cursor: pointer; + transition: all 0.2s; +} + +.status-tab:hover { border-color: var(--accent); } + +.status-tab.active { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} diff --git a/dashboard/captura.html b/dashboard/captura.html new file mode 100644 index 0000000..e444f2f --- /dev/null +++ b/dashboard/captura.html @@ -0,0 +1,99 @@ + + + + + + Captura de Datos — NEXUS AUTOPARTS + + + + + +
+ +
+ +
+ + + +
+ + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+ + + +
+ + + + +
+ + +
+
+
+ + + + +
+ + +
+
+
+
+ + + + + diff --git a/dashboard/captura.js b/dashboard/captura.js new file mode 100644 index 0000000..943a3dd --- /dev/null +++ b/dashboard/captura.js @@ -0,0 +1,707 @@ +/** + * captura.js — Data entry logic for Nexus Autoparts + * 3 sections: OEM Parts, Aftermarket/Interchange, Images + */ +(function () { + 'use strict'; + + var API = ''; + var currentMye = null; // selected vehicle MYE id + var currentVehicle = null; // vehicle info object + var vehicleParts = []; // existing parts for current vehicle + var manufacturers = []; // cached manufacturer list + var vehicleStatus = 'pending'; + var vehiclePage = 1; + + // ================================================================ + // Utility + // ================================================================ + + function toast(msg, type) { + var el = document.createElement('div'); + el.className = 'toast ' + (type || 'success'); + el.textContent = msg; + document.body.appendChild(el); + setTimeout(function () { el.remove(); }, 3000); + } + + function api(path, opts) { + opts = opts || {}; + return fetch(API + path, opts).then(function (r) { + if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || 'Error'); }); + return r.json(); + }); + } + + function esc(s) { + if (!s) return ''; + var d = document.createElement('div'); + d.textContent = s; + return d.innerHTML; + } + + // ================================================================ + // Tab Switching + // ================================================================ + + document.querySelectorAll('.captura-tab').forEach(function (tab) { + tab.addEventListener('click', function () { + document.querySelectorAll('.captura-tab').forEach(function (t) { t.classList.remove('active'); }); + document.querySelectorAll('.captura-section').forEach(function (s) { s.classList.remove('active'); }); + tab.classList.add('active'); + var target = tab.getAttribute('data-tab'); + document.getElementById('section-' + target).classList.add('active'); + + if (target === 'aftermarket') loadPartsWithoutAftermarket(); + if (target === 'images') loadPartsWithoutImage(); + }); + }); + + // ================================================================ + // SECTION 1: OEM Parts + // ================================================================ + + // --- Status tabs --- + document.querySelectorAll('.status-tab').forEach(function (tab) { + tab.addEventListener('click', function () { + document.querySelectorAll('.status-tab').forEach(function (t) { t.classList.remove('active'); }); + tab.classList.add('active'); + vehicleStatus = tab.getAttribute('data-status'); + vehiclePage = 1; + loadVehicles(); + }); + }); + + // --- Brand filter --- + function loadBrands() { + api('/api/brands').then(function (brands) { + var sel = document.getElementById('oem-brand-filter'); + brands.forEach(function (b) { + var opt = document.createElement('option'); + opt.value = b; + opt.textContent = b; + sel.appendChild(opt); + }); + }); + } + + document.getElementById('oem-brand-filter').addEventListener('change', function () { + vehiclePage = 1; + loadVehicles(); + }); + + var modelTimer = null; + document.getElementById('oem-model-filter').addEventListener('input', function () { + clearTimeout(modelTimer); + modelTimer = setTimeout(function () { + vehiclePage = 1; + loadVehicles(); + }, 400); + }); + + // --- Load vehicles --- + function loadVehicles() { + var brand = document.getElementById('oem-brand-filter').value; + var model = document.getElementById('oem-model-filter').value; + var list = document.getElementById('oem-vehicle-list'); + list.innerHTML = '
'; + + var endpoint = vehicleStatus === 'pending' + ? '/api/captura/vehicles/pending' + : '/api/captura/vehicles/in-progress'; + + var params = '?page=' + vehiclePage + '&per_page=30'; + if (brand) params += '&brand=' + encodeURIComponent(brand); + if (model) params += '&model=' + encodeURIComponent(model); + + api(endpoint + params).then(function (res) { + var data = res.data || []; + if (data.length === 0) { + list.innerHTML = '
📋
No hay vehiculos ' + + (vehicleStatus === 'pending' ? 'pendientes' : 'en progreso') + '
'; + document.getElementById('oem-vehicle-pagination').innerHTML = ''; + return; + } + + list.innerHTML = data.map(function (v) { + return '
' + + '
' + esc(v.brand) + '
' + + '
' + esc(v.model) + '
' + + '
' + v.year + ' · ' + esc(v.engine) + + (v.trim_level ? ' · ' + esc(v.trim_level) : '') + '
' + + (v.parts_count ? '
' + v.parts_count + ' partes registradas
' : '') + + '
'; + }).join(''); + + // Click handler for vehicle cards + list.querySelectorAll('.vehicle-card').forEach(function (card) { + card.addEventListener('click', function () { + selectVehicle(parseInt(card.getAttribute('data-mye'))); + }); + }); + + // Pagination + renderPagination('oem-vehicle-pagination', res.pagination, function (p) { + vehiclePage = p; + loadVehicles(); + }); + }); + } + + function renderPagination(containerId, pag, onPage) { + var c = document.getElementById(containerId); + if (!pag || pag.total_pages <= 1) { c.innerHTML = ''; return; } + c.innerHTML = '' + + 'Pag ' + pag.page + ' de ' + pag.total_pages + ' (' + pag.total + ' total)' + + ''; + c.querySelectorAll('button').forEach(function (btn) { + btn.addEventListener('click', function () { + onPage(parseInt(btn.getAttribute('data-p'))); + }); + }); + } + + // --- Select vehicle and show part entry --- + function selectVehicle(myeId) { + currentMye = myeId; + document.getElementById('oem-vehicle-select').style.display = 'none'; + document.getElementById('oem-part-entry').style.display = 'block'; + + // Mark as in_progress + api('/api/captura/vehicles/' + myeId + '/status', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: 'in_progress' }) + }); + + loadVehicleParts(myeId); + } + + function loadVehicleParts(myeId) { + api('/api/captura/vehicles/' + myeId + '/parts').then(function (res) { + currentVehicle = res.vehicle; + vehicleParts = res.parts || []; + + // Render vehicle header + var hdr = document.getElementById('oem-vehicle-header'); + hdr.innerHTML = '
' + + '
Marca
' + esc(currentVehicle.brand) + '
' + + '
Modelo
' + esc(currentVehicle.model) + '
' + + '
Ano
' + currentVehicle.year + '
' + + '
Motor
' + esc(currentVehicle.engine) + '
' + + (currentVehicle.trim_level ? '
Trim
' + esc(currentVehicle.trim_level) + '
' : '') + + '
' + + '
' + + '' + + '' + + '
'; + + document.getElementById('btn-back-vehicles').addEventListener('click', backToVehicles); + document.getElementById('btn-complete-vehicle').addEventListener('click', completeVehicle); + + // Build groups by category + renderGroups(res.groups, vehicleParts); + updateProgress(); + }); + } + + function backToVehicles() { + document.getElementById('oem-vehicle-select').style.display = 'block'; + document.getElementById('oem-part-entry').style.display = 'none'; + currentMye = null; + loadVehicles(); + } + + function completeVehicle() { + if (vehicleParts.length === 0) { + toast('Registra al menos una parte antes de marcar como terminado', 'error'); + return; + } + api('/api/captura/vehicles/' + currentMye + '/status', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: 'completed' }) + }).then(function () { + toast('Vehiculo completado'); + backToVehicles(); + }); + } + + // --- Render groups/categories --- + function renderGroups(groups, parts) { + var container = document.getElementById('oem-groups-container'); + // Group by category + var categories = {}; + groups.forEach(function (g) { + if (!categories[g.category]) { + categories[g.category] = { id: g.id_part_category, groups: [] }; + } + categories[g.category].groups.push(g); + }); + + var html = ''; + Object.keys(categories).forEach(function (catName) { + var cat = categories[catName]; + var catParts = parts.filter(function (p) { + return cat.groups.some(function (g) { return g.id_part_group === p.group_id; }); + }); + + html += '
' + + '
' + + '

' + esc(catName) + ' (' + catParts.length + ')

' + + '
' + + '
'; + + cat.groups.forEach(function (g) { + var groupParts = parts.filter(function (p) { return p.group_id === g.id_part_group; }); + html += '
' + + '
' + esc(g.group_name) + '
' + + '
'; + + groupParts.forEach(function (p) { + html += savedPartRow(p); + }); + + html += '
' + + '' + + '
'; + }); + + html += '
'; + }); + + container.innerHTML = html; + + // Category toggle + container.querySelectorAll('.category-header').forEach(function (ch) { + ch.addEventListener('click', function () { + var catId = ch.getAttribute('data-cat'); + var body = container.querySelector('[data-cat-body="' + catId + '"]'); + ch.classList.toggle('collapsed'); + body.classList.toggle('collapsed'); + }); + }); + + // Add part buttons + container.querySelectorAll('.btn-add-part').forEach(function (btn) { + btn.addEventListener('click', function () { + addPartRow(parseInt(btn.getAttribute('data-group-id')), btn); + }); + }); + } + + function savedPartRow(p) { + return '
' + + '' + + '' + + '' + + '' + + '
'; + } + + function addPartRow(groupId, addBtn) { + var rowsContainer = document.querySelector('[data-group-parts="' + groupId + '"]'); + var row = document.createElement('div'); + row.className = 'part-row'; + row.innerHTML = '' + + '' + + '' + + '' + + ''; + + rowsContainer.appendChild(row); + + // Focus OEM field + row.querySelector('.pr-oem').focus(); + + // OEM blur: check if exists + row.querySelector('.pr-oem').addEventListener('blur', function () { + var oem = this.value.trim(); + if (!oem) return; + api('/api/captura/parts/check-oem?oem=' + encodeURIComponent(oem)).then(function (res) { + if (res.exists) { + row.querySelector('.pr-name').value = res.part.name_part || ''; + row.querySelector('.pr-name').style.borderColor = 'var(--success)'; + row.dataset.existingPartId = res.part.id_part; + } + }); + }); + + // Save + row.querySelector('.pr-save').addEventListener('click', function () { + savePart(row, groupId); + }); + + // Delete (unsaved) + row.querySelector('.pr-delete').addEventListener('click', function () { + row.remove(); + }); + } + + function savePart(row, groupId) { + var oem = row.querySelector('.pr-oem').value.trim(); + var name = row.querySelector('.pr-name').value.trim(); + var qty = parseInt(row.querySelector('.pr-qty').value) || 1; + + if (!oem) { + toast('Ingresa el numero OEM', 'error'); + row.querySelector('.pr-oem').focus(); + return; + } + + var saveBtn = row.querySelector('.pr-save'); + saveBtn.disabled = true; + saveBtn.textContent = '...'; + + // Check if part already exists + var existingId = row.dataset.existingPartId; + + function createFitment(partId) { + api('/api/admin/fitment', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model_year_engine_id: currentMye, + part_id: partId, + quantity_required: qty + }) + }).then(function (res) { + // Replace row with saved version + var newPart = { + id_vehicle_part: res.id, + part_id: partId, + oem_part_number: oem, + name_part: name, + quantity_required: qty, + group_id: groupId + }; + vehicleParts.push(newPart); + row.outerHTML = savedPartRow(newPart); + updateProgress(); + toast('Parte guardada: ' + oem); + + // Re-attach delete handlers + attachDeleteHandlers(); + }).catch(function (err) { + toast(err.message, 'error'); + saveBtn.disabled = false; + saveBtn.textContent = '\u2713'; + }); + } + + if (existingId) { + createFitment(parseInt(existingId)); + } else { + // Create part first + api('/api/admin/parts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + oem_part_number: oem, + name: name || oem, + group_id: groupId + }) + }).then(function (res) { + createFitment(res.id); + }).catch(function (err) { + toast(err.message, 'error'); + saveBtn.disabled = false; + saveBtn.textContent = '\u2713'; + }); + } + } + + function attachDeleteHandlers() { + document.querySelectorAll('.part-row.saved .pr-delete').forEach(function (btn) { + btn.onclick = function () { + var row = btn.closest('.part-row'); + var fitmentId = row.getAttribute('data-fitment-id'); + if (!fitmentId) { row.remove(); return; } + + api('/api/admin/fitment/' + fitmentId, { method: 'DELETE' }).then(function () { + vehicleParts = vehicleParts.filter(function (p) { + return p.id_vehicle_part !== parseInt(fitmentId); + }); + row.remove(); + updateProgress(); + toast('Parte eliminada'); + }).catch(function (err) { + toast(err.message, 'error'); + }); + }; + }); + } + + function updateProgress() { + var count = vehicleParts.length; + var totalGroups = 63; + var pct = Math.min(100, Math.round((count / totalGroups) * 100)); + document.getElementById('oem-progress-fill').style.width = pct + '%'; + document.getElementById('oem-progress-text').textContent = count + ' partes registradas'; + + // Update category counts + document.querySelectorAll('.category-header h3').forEach(function (h3) { + var catSection = h3.closest('.category-section'); + var rows = catSection.querySelectorAll('.part-row.saved'); + var catName = h3.textContent.replace(/\s*\(\d+\)$/, ''); + h3.textContent = catName + ' (' + rows.length + ')'; + }); + } + + // ================================================================ + // SECTION 2: Aftermarket / Interchange + // ================================================================ + + var aftermarketPage = 1; + + function loadPartsWithoutAftermarket(page) { + page = page || 1; + aftermarketPage = page; + var search = document.getElementById('aftermarket-search').value; + var list = document.getElementById('aftermarket-list'); + list.innerHTML = '
'; + + var params = '?page=' + page + '&per_page=20'; + if (search) params += '&search=' + encodeURIComponent(search); + + api('/api/captura/parts/without-aftermarket' + params).then(function (res) { + var data = res.data || []; + if (data.length === 0) { + list.innerHTML = '
No hay piezas sin intercambios
'; + document.getElementById('aftermarket-pagination').innerHTML = ''; + return; + } + + list.innerHTML = data.map(function (p) { + return '
' + + '
' + + '
' + esc(p.oem_part_number) + '' + + ' ' + esc(p.name_part) + '
' + + '' + esc(p.category) + ' › ' + esc(p.group_name) + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '' + + '
'; + }).join(''); + + // Load existing aftermarket for each part + data.forEach(function (p) { + loadPartAftermarket(p.id_part); + }); + + // Save handlers + list.querySelectorAll('.af-save-btn').forEach(function (btn) { + btn.addEventListener('click', function () { + var card = btn.closest('.part-detail-card'); + saveAftermarket(card); + }); + }); + + renderPagination('aftermarket-pagination', res.pagination, function (p) { + loadPartsWithoutAftermarket(p); + }); + }); + } + + function manufacturerOptions() { + return manufacturers.map(function (m) { + return ''; + }).join(''); + } + + function loadPartAftermarket(partId) { + api('/api/captura/parts/' + partId + '/aftermarket').then(function (items) { + var container = document.querySelector('[data-af-list="' + partId + '"]'); + if (items.length === 0) { + container.innerHTML = '

Sin intercambios registrados

'; + return; + } + var html = '' + + ''; + items.forEach(function (a) { + html += ''; + }); + html += '
Fabricante# ParteNombreCalidadPrecioGarantia
' + esc(a.manufacturer) + '' + esc(a.part_number) + + '' + esc(a.name || '') + '' + esc(a.quality || '') + + '' + (a.price_usd ? '$' + a.price_usd : '') + + '' + (a.warranty_months || '') + '
'; + container.innerHTML = html; + }); + } + + function saveAftermarket(card) { + var partId = card.getAttribute('data-part-id'); + var manufacturer = card.querySelector('.af-manufacturer').value; + var partNumber = card.querySelector('.af-partnum').value.trim(); + var name = card.querySelector('.af-name').value.trim(); + var quality = card.querySelector('.af-quality').value; + var price = card.querySelector('.af-price').value; + var warranty = card.querySelector('.af-warranty').value; + + if (!partNumber) { + toast('Ingresa el numero de parte aftermarket', 'error'); + return; + } + + api('/api/admin/aftermarket', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + oem_part_id: parseInt(partId), + manufacturer_id: parseInt(manufacturer), + part_number: partNumber, + name: name, + quality_tier: quality, + price_usd: price ? parseFloat(price) : null, + warranty_months: warranty ? parseInt(warranty) : null + }) + }).then(function () { + toast('Intercambio guardado: ' + partNumber); + // Clear form + card.querySelector('.af-partnum').value = ''; + card.querySelector('.af-name').value = ''; + card.querySelector('.af-price').value = ''; + card.querySelector('.af-warranty').value = ''; + // Reload aftermarket list + loadPartAftermarket(parseInt(partId)); + }).catch(function (err) { + toast(err.message, 'error'); + }); + } + + // ================================================================ + // SECTION 3: Images + // ================================================================ + + var imagePage = 1; + + function loadPartsWithoutImage(page) { + page = page || 1; + imagePage = page; + var search = document.getElementById('image-search').value; + var list = document.getElementById('image-list'); + list.innerHTML = '
'; + + var params = '?page=' + page + '&per_page=20'; + if (search) params += '&search=' + encodeURIComponent(search); + + api('/api/captura/parts/without-image' + params).then(function (res) { + var data = res.data || []; + if (data.length === 0) { + list.innerHTML = '
📷
No hay piezas sin imagen
'; + document.getElementById('image-pagination').innerHTML = ''; + return; + } + + list.innerHTML = data.map(function (p) { + return '
' + + '
Sin imagen
' + + '
' + + '
' + esc(p.oem_part_number) + '
' + + '
' + esc(p.name_part) + ' · ' + esc(p.group_name) + '
' + + '
' + + '' + + '' + + '
'; + }).join(''); + + // File input change → enable upload button and show preview + list.querySelectorAll('.ic-file-input').forEach(function (input) { + input.addEventListener('change', function () { + var card = input.closest('.image-card'); + var btn = card.querySelector('.ic-upload-btn'); + var preview = card.querySelector('.ic-preview'); + + if (input.files && input.files[0]) { + btn.disabled = false; + + // Show preview + var reader = new FileReader(); + reader.onload = function (e) { + preview.innerHTML = ''; + }; + reader.readAsDataURL(input.files[0]); + } + }); + }); + + // Upload button + list.querySelectorAll('.ic-upload-btn').forEach(function (btn) { + btn.addEventListener('click', function () { + var card = btn.closest('.image-card'); + uploadImage(card); + }); + }); + + renderPagination('image-pagination', res.pagination, function (p) { + loadPartsWithoutImage(p); + }); + }); + } + + function uploadImage(card) { + var partId = card.getAttribute('data-part-id'); + var fileInput = card.querySelector('.ic-file-input'); + var btn = card.querySelector('.ic-upload-btn'); + + if (!fileInput.files || !fileInput.files[0]) return; + + btn.disabled = true; + btn.textContent = 'Subiendo...'; + + var formData = new FormData(); + formData.append('image', fileInput.files[0]); + + fetch(API + '/api/captura/parts/' + partId + '/image', { + method: 'POST', + body: formData + }).then(function (r) { return r.json(); }) + .then(function (res) { + if (res.error) throw new Error(res.error); + toast('Imagen subida correctamente'); + // Remove card from list + card.style.opacity = '0.3'; + setTimeout(function () { card.remove(); }, 500); + }).catch(function (err) { + toast(err.message, 'error'); + btn.disabled = false; + btn.textContent = 'Subir'; + }); + } + + // ================================================================ + // Init + // ================================================================ + + function init() { + loadBrands(); + loadVehicles(); + + // Pre-load manufacturers for Section 2 + api('/api/captura/manufacturers').then(function (data) { + manufacturers = data; + }); + } + + // Make functions globally accessible for inline onclick handlers + window.loadPartsWithoutAftermarket = loadPartsWithoutAftermarket; + window.loadPartsWithoutImage = loadPartsWithoutImage; + + init(); +})(); diff --git a/dashboard/cuentas.css b/dashboard/cuentas.css new file mode 100644 index 0000000..ff54c71 --- /dev/null +++ b/dashboard/cuentas.css @@ -0,0 +1,282 @@ +/* ============================================================ + cuentas.css -- Accounts receivable styles + ============================================================ */ + +.cuentas-container { + max-width: 1200px; + margin: 0 auto; + padding: 5rem 2rem 2rem; +} + +/* --- Customer list --- */ +.cuentas-search { + display: flex; + gap: 0.8rem; + margin-bottom: 1rem; +} + +.cuentas-search input { + flex: 1; + max-width: 400px; + padding: 0.5rem 0.8rem; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 0.9rem; +} + +.cuentas-search input:focus { + outline: none; + border-color: var(--accent); +} + +.customer-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 0.8rem; + margin-bottom: 1.5rem; +} + +.customer-card-item { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1rem; + cursor: pointer; + transition: all 0.2s; +} + +.customer-card-item:hover { + border-color: var(--accent); + background: var(--bg-hover); +} + +.cci-name { + font-weight: 700; + font-size: 1rem; + margin-bottom: 0.2rem; +} + +.cci-rfc { + font-family: monospace; + font-size: 0.8rem; + color: var(--text-secondary); +} + +.cci-balance-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 0.5rem; +} + +.cci-balance { + font-size: 1.1rem; + font-weight: 700; +} + +.cci-balance.positive { color: var(--danger); } +.cci-balance.zero { color: var(--success); } + +.cci-limit { + font-size: 0.75rem; + color: var(--text-secondary); +} + +/* --- Customer detail view --- */ +.detail-view { + display: none; +} + +.detail-header { + background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-hover) 100%); + border: 1px solid var(--accent); + border-radius: 12px; + padding: 1.2rem 1.5rem; + margin-bottom: 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 1rem; +} + +.dh-info { + display: flex; + gap: 2rem; + flex-wrap: wrap; +} + +.dh-field .dh-label { + font-size: 0.7rem; + color: var(--text-secondary); + text-transform: uppercase; +} + +.dh-field .dh-value { + font-size: 1rem; + font-weight: 600; +} + +.dh-field .dh-value.accent { color: var(--accent); } +.dh-field .dh-value.danger { color: var(--danger); } +.dh-field .dh-value.success { color: var(--success); } + +/* --- Two-column layout for invoices/payments --- */ +.detail-columns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; +} + +@media (max-width: 768px) { + .detail-columns { grid-template-columns: 1fr; } +} + +.detail-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; +} + +.detail-card h3 { + padding: 0.8rem 1rem; + border-bottom: 1px solid var(--border); + font-size: 0.9rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.detail-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; +} + +.detail-table th { + text-align: left; + padding: 0.5rem 0.6rem; + border-bottom: 1px solid var(--border); + color: var(--text-secondary); + font-size: 0.75rem; + text-transform: uppercase; +} + +.detail-table td { + padding: 0.4rem 0.6rem; + border-bottom: 1px solid rgba(42, 42, 58, 0.5); +} + +.status-badge { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; +} + +.status-badge.pending { background: rgba(245, 158, 11, 0.15); color: var(--warning); } +.status-badge.partial { background: rgba(59, 130, 246, 0.15); color: var(--info); } +.status-badge.paid { background: rgba(34, 197, 94, 0.15); color: var(--success); } +.status-badge.cancelled { background: rgba(255, 68, 68, 0.15); color: var(--danger); } + +/* --- Payment form --- */ +.payment-form { + padding: 1rem; + border-top: 1px solid var(--border); +} + +.payment-form h4 { + font-size: 0.85rem; + color: var(--accent); + margin-bottom: 0.8rem; +} + +.pf-row { + display: flex; + gap: 0.5rem; + margin-bottom: 0.5rem; + flex-wrap: wrap; +} + +.pf-field { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.pf-field label { + font-size: 0.7rem; + color: var(--text-secondary); + text-transform: uppercase; +} + +.pf-field input, +.pf-field select { + padding: 0.4rem 0.6rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + font-size: 0.85rem; +} + +.pf-field input:focus, +.pf-field select:focus { + outline: none; + border-color: var(--accent); +} + +/* --- Toast --- */ +.toast { + position: fixed; + bottom: 2rem; + right: 2rem; + padding: 0.8rem 1.5rem; + border-radius: 10px; + color: #fff; + font-weight: 600; + font-size: 0.9rem; + z-index: 9999; + animation: toastIn 0.3s ease; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); +} +.toast.success { background: var(--success); } +.toast.error { background: var(--danger); } +@keyframes toastIn { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +/* --- Pagination --- */ +.cuentas-pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; + margin-top: 1rem; +} + +.cuentas-pagination button { + padding: 0.4rem 0.8rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + cursor: pointer; + font-size: 0.85rem; +} + +.cuentas-pagination button:hover { border-color: var(--accent); } +.cuentas-pagination button:disabled { opacity: 0.4; cursor: not-allowed; } +.cuentas-pagination .page-info { font-size: 0.8rem; color: var(--text-secondary); } + +.empty-state { + text-align: center; + padding: 2rem; + color: var(--text-secondary); + font-size: 0.9rem; +} diff --git a/dashboard/cuentas.html b/dashboard/cuentas.html new file mode 100644 index 0000000..8b0a673 --- /dev/null +++ b/dashboard/cuentas.html @@ -0,0 +1,102 @@ + + + + + + Cuentas por Cobrar — NEXUS AUTOPARTS + + + + + +
+ +
+ +
+ +
+
+
+ + +
+
+
+
Cliente
+
RFC
+
Saldo
+
Limite
+
Plazo
+
+ +
+ +
+ +
+

Facturas

+ + + + + +
FolioFechaTotalPagadoEstado
+
+ + +
+

Pagos

+ + + + + +
FechaMontoMetodoRefFactura
+ +
+

Registrar Pago

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+ +
+
+
+
+
+
+ + + + + diff --git a/dashboard/cuentas.js b/dashboard/cuentas.js new file mode 100644 index 0000000..171a750 --- /dev/null +++ b/dashboard/cuentas.js @@ -0,0 +1,222 @@ +/** + * cuentas.js — Accounts receivable logic for Nexus Autoparts + */ +(function () { + 'use strict'; + + var API = ''; + var currentCustomerId = null; + var customerPage = 1; + + // ================================================================ + // Utility + // ================================================================ + + function toast(msg, type) { + var el = document.createElement('div'); + el.className = 'toast ' + (type || 'success'); + el.textContent = msg; + document.body.appendChild(el); + setTimeout(function () { el.remove(); }, 3000); + } + + function api(path, opts) { + opts = opts || {}; + return fetch(API + path, opts).then(function (r) { + if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || 'Error'); }); + return r.json(); + }); + } + + function esc(s) { + if (!s) return ''; + var d = document.createElement('div'); + d.textContent = s; + return d.innerHTML; + } + + function fmt(n) { + return '$' + (parseFloat(n) || 0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } + + function fmtDate(d) { + if (!d) return ''; + var dt = new Date(d); + return dt.toLocaleDateString('es-MX', { day: '2-digit', month: 'short', year: 'numeric' }); + } + + // ================================================================ + // Customer List + // ================================================================ + + var searchTimer = null; + document.getElementById('customer-search').addEventListener('input', function () { + clearTimeout(searchTimer); + searchTimer = setTimeout(function () { + customerPage = 1; + loadCustomers(); + }, 400); + }); + + function loadCustomers() { + var search = document.getElementById('customer-search').value; + var grid = document.getElementById('customer-grid'); + grid.innerHTML = '
Cargando...
'; + + var params = '?page=' + customerPage + '&per_page=30'; + if (search) params += '&search=' + encodeURIComponent(search); + + api('/api/pos/customers' + params).then(function (res) { + var data = res.data || []; + if (data.length === 0) { + grid.innerHTML = '
No se encontraron clientes
'; + document.getElementById('customer-pagination').innerHTML = ''; + return; + } + + grid.innerHTML = data.map(function (c) { + return '
' + + '
' + esc(c.name) + '
' + + '
' + esc(c.rfc || 'Sin RFC') + '
' + + '
' + + '' + fmt(c.balance) + '' + + 'Limite: ' + fmt(c.credit_limit) + '
'; + }).join(''); + + grid.querySelectorAll('.customer-card-item').forEach(function (card) { + card.addEventListener('click', function () { + showCustomerDetail(parseInt(card.getAttribute('data-id'))); + }); + }); + + // Pagination + var pag = res.pagination; + var pagEl = document.getElementById('customer-pagination'); + if (pag.total_pages <= 1) { pagEl.innerHTML = ''; return; } + pagEl.innerHTML = '' + + 'Pag ' + pag.page + '/' + pag.total_pages + '' + + ''; + pagEl.querySelectorAll('button').forEach(function (btn) { + btn.addEventListener('click', function () { + customerPage = parseInt(btn.getAttribute('data-p')); + loadCustomers(); + }); + }); + }).catch(function (err) { + console.error('Error loading customers:', err); + grid.innerHTML = '
Error al cargar clientes
'; + }); + } + + // ================================================================ + // Customer Detail + // ================================================================ + + function showCustomerDetail(customerId) { + currentCustomerId = customerId; + document.getElementById('list-view').style.display = 'none'; + document.getElementById('detail-view').style.display = 'block'; + + api('/api/pos/customers/' + customerId + '/statement').then(function (res) { + var c = res.customer; + document.getElementById('dh-name').textContent = c.name; + document.getElementById('dh-rfc').textContent = c.rfc || 'Sin RFC'; + + var balEl = document.getElementById('dh-balance'); + balEl.textContent = fmt(c.balance); + balEl.className = 'dh-value ' + (c.balance > 0 ? 'danger' : 'success'); + + document.getElementById('dh-limit').textContent = fmt(c.credit_limit); + document.getElementById('dh-terms').textContent = c.payment_terms + ' dias'; + + // Invoices + var invBody = document.getElementById('invoice-list'); + if (res.invoices.length === 0) { + invBody.innerHTML = 'Sin facturas'; + } else { + invBody.innerHTML = res.invoices.map(function (i) { + return '' + + '' + esc(i.folio) + '' + + '' + fmtDate(i.date_issued) + '' + + '' + fmt(i.total) + '' + + '' + fmt(i.amount_paid) + '' + + '' + i.status + ''; + }).join(''); + } + + // Payments + var payBody = document.getElementById('payment-list'); + if (res.payments.length === 0) { + payBody.innerHTML = 'Sin pagos'; + } else { + payBody.innerHTML = res.payments.map(function (p) { + return '' + + '' + fmtDate(p.date_payment) + '' + + '' + fmt(p.amount) + '' + + '' + esc(p.payment_method) + '' + + '' + esc(p.reference || '') + '' + + '' + esc(p.invoice_folio || 'General') + ''; + }).join(''); + } + + // Populate invoice dropdown for payment form + var invSelect = document.getElementById('pay-invoice'); + invSelect.innerHTML = ''; + res.invoices.filter(function (i) { return i.status !== 'paid' && i.status !== 'cancelled'; }) + .forEach(function (i) { + invSelect.innerHTML += ''; + }); + }); + } + + document.getElementById('btn-back-list').addEventListener('click', function () { + document.getElementById('detail-view').style.display = 'none'; + document.getElementById('list-view').style.display = 'block'; + currentCustomerId = null; + loadCustomers(); + }); + + // ================================================================ + // Register Payment + // ================================================================ + + document.getElementById('btn-pay').addEventListener('click', function () { + var amount = parseFloat(document.getElementById('pay-amount').value); + if (!amount || amount <= 0) { + toast('Ingresa un monto valido', 'error'); + return; + } + + var invoiceId = document.getElementById('pay-invoice').value; + + api('/api/pos/payments', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + customer_id: currentCustomerId, + amount: amount, + payment_method: document.getElementById('pay-method').value, + reference: document.getElementById('pay-reference').value.trim() || null, + invoice_id: invoiceId ? parseInt(invoiceId) : null, + notes: document.getElementById('pay-notes').value.trim() || null + }) + }).then(function () { + toast('Pago de ' + fmt(amount) + ' registrado'); + // Clear form + document.getElementById('pay-amount').value = ''; + document.getElementById('pay-reference').value = ''; + document.getElementById('pay-notes').value = ''; + // Refresh detail + showCustomerDetail(currentCustomerId); + }).catch(function (err) { + toast(err.message, 'error'); + }); + }); + + // ================================================================ + // Init + // ================================================================ + + loadCustomers(); +})(); diff --git a/dashboard/pos.css b/dashboard/pos.css new file mode 100644 index 0000000..99280cb --- /dev/null +++ b/dashboard/pos.css @@ -0,0 +1,418 @@ +/* ============================================================ + pos.css -- Point of Sale styles + ============================================================ */ + +.pos-container { + max-width: 1400px; + margin: 0 auto; + padding: 5rem 2rem 2rem; +} + +/* --- Layout: 2 columns --- */ +.pos-layout { + display: grid; + grid-template-columns: 1fr 360px; + gap: 1.5rem; + align-items: start; +} + +/* --- Left: Search + Cart --- */ +.pos-main { + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* --- Customer bar --- */ +.customer-bar { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1rem; + display: flex; + gap: 1rem; + align-items: center; +} + +.customer-bar .cb-search { + flex: 1; + padding: 0.5rem 0.8rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 0.9rem; +} + +.customer-bar .cb-search:focus { + outline: none; + border-color: var(--accent); +} + +.customer-bar .cb-selected { + display: flex; + align-items: center; + gap: 0.8rem; + flex: 1; +} + +.customer-bar .cb-name { + font-weight: 700; + font-size: 1rem; +} + +.customer-bar .cb-rfc { + font-size: 0.8rem; + color: var(--text-secondary); + font-family: monospace; +} + +.customer-bar .cb-balance { + font-size: 0.85rem; + padding: 0.2rem 0.6rem; + border-radius: 6px; +} + +.cb-balance.positive { background: rgba(255, 68, 68, 0.15); color: var(--danger); } +.cb-balance.zero { background: rgba(34, 197, 94, 0.15); color: var(--success); } + +/* --- Customer dropdown --- */ +.customer-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 0 0 8px 8px; + max-height: 250px; + overflow-y: auto; + z-index: 100; + box-shadow: 0 8px 30px rgba(0,0,0,0.4); +} + +.customer-dropdown-item { + padding: 0.6rem 1rem; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid var(--border); +} + +.customer-dropdown-item:hover { + background: var(--bg-hover); +} + +.customer-dropdown-item .cdi-name { font-weight: 600; } +.customer-dropdown-item .cdi-rfc { font-size: 0.8rem; color: var(--text-secondary); } + +/* --- Part search --- */ +.part-search-wrap { + position: relative; +} + +.part-search { + width: 100%; + padding: 0.7rem 1rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + color: var(--text-primary); + font-size: 1rem; +} + +.part-search:focus { + outline: none; + border-color: var(--accent); +} + +.part-results { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 0 0 10px 10px; + max-height: 300px; + overflow-y: auto; + z-index: 100; + box-shadow: 0 8px 30px rgba(0,0,0,0.4); + display: none; +} + +.part-result-item { + padding: 0.6rem 1rem; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid var(--border); +} + +.part-result-item:hover, +.part-result-item.part-result-active { + background: var(--bg-hover); + border-left: 3px solid var(--accent); +} + +.part-result-item .pri-number { + font-family: monospace; + font-weight: 600; + color: var(--accent); +} + +.part-result-item .pri-name { + font-size: 0.85rem; + color: var(--text-secondary); + margin-left: 0.5rem; +} + +.part-result-item .pri-type { + font-size: 0.7rem; + padding: 0.15rem 0.4rem; + border-radius: 4px; + text-transform: uppercase; +} + +.pri-type.oem { background: rgba(59, 130, 246, 0.15); color: var(--info); } +.pri-type.aftermarket { background: rgba(245, 158, 11, 0.15); color: var(--warning); } + +/* --- Cart table --- */ +.cart-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; +} + +.cart-card h3 { + padding: 0.8rem 1rem; + border-bottom: 1px solid var(--border); + font-size: 0.9rem; + color: var(--text-secondary); +} + +.cart-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; +} + +.cart-table th { + text-align: left; + padding: 0.5rem 0.6rem; + border-bottom: 1px solid var(--border); + color: var(--text-secondary); + font-size: 0.75rem; + text-transform: uppercase; +} + +.cart-table td { + padding: 0.5rem 0.6rem; + border-bottom: 1px solid rgba(42, 42, 58, 0.5); + vertical-align: middle; +} + +.cart-table input { + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text-primary); + padding: 0.3rem 0.4rem; + font-size: 0.85rem; + width: 70px; + text-align: right; +} + +.cart-table input:focus { + outline: none; + border-color: var(--accent); +} + +.cart-table .cart-desc { max-width: 250px; } +.cart-table .cart-qty { width: 45px; text-align: center; } +.cart-table .cart-cost { width: 80px; } +.cart-table .cart-margin { width: 55px; } +.cart-table .cart-price { width: 80px; } + +.cart-table .cart-remove { + background: none; + border: none; + color: var(--danger); + cursor: pointer; + font-size: 1rem; + padding: 0.2rem; +} + +.cart-empty { + text-align: center; + padding: 2rem; + color: var(--text-secondary); + font-size: 0.9rem; +} + +/* --- Right sidebar: Invoice summary --- */ +.pos-sidebar { + position: sticky; + top: 5rem; +} + +.invoice-summary { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1.2rem; +} + +.invoice-summary h3 { + font-size: 0.9rem; + color: var(--text-secondary); + margin-bottom: 1rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.summary-row { + display: flex; + justify-content: space-between; + padding: 0.4rem 0; + font-size: 0.9rem; +} + +.summary-row.total { + border-top: 2px solid var(--accent); + margin-top: 0.5rem; + padding-top: 0.8rem; + font-size: 1.2rem; + font-weight: 700; +} + +.summary-row .sr-label { color: var(--text-secondary); } +.summary-row .sr-value { font-weight: 600; } +.summary-row.total .sr-value { color: var(--accent); } + +.btn-facturar { + width: 100%; + margin-top: 1.2rem; + padding: 0.9rem; + font-size: 1rem; + background: linear-gradient(135deg, var(--accent), #ff4500); + border: none; + border-radius: 10px; + color: #fff; + font-weight: 700; + cursor: pointer; + transition: all 0.3s; +} + +.btn-facturar:hover { + transform: translateY(-2px); + box-shadow: 0 6px 25px var(--accent-glow); +} + +.btn-facturar:disabled { + opacity: 0.4; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.invoice-notes { + width: 100%; + margin-top: 0.8rem; + padding: 0.5rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + font-size: 0.85rem; + resize: vertical; + min-height: 60px; +} + +.invoice-notes:focus { + outline: none; + border-color: var(--accent); +} + +/* --- New customer modal --- */ +.modal-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.6); + z-index: 2000; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-content { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1.5rem; + width: 450px; + max-width: 95vw; +} + +.modal-content h3 { + margin-bottom: 1rem; + font-size: 1.1rem; +} + +.modal-field { + margin-bottom: 0.8rem; +} + +.modal-field label { + display: block; + font-size: 0.75rem; + color: var(--text-secondary); + text-transform: uppercase; + margin-bottom: 0.2rem; +} + +.modal-field input { + width: 100%; + padding: 0.5rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + font-size: 0.9rem; +} + +.modal-field input:focus { + outline: none; + border-color: var(--accent); +} + +.modal-actions { + display: flex; + gap: 0.5rem; + justify-content: flex-end; + margin-top: 1rem; +} + +/* --- Toast (reuse from captura) --- */ +.toast { + position: fixed; + bottom: 2rem; + right: 2rem; + padding: 0.8rem 1.5rem; + border-radius: 10px; + color: #fff; + font-weight: 600; + font-size: 0.9rem; + z-index: 9999; + animation: toastIn 0.3s ease; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); +} +.toast.success { background: var(--success); } +.toast.error { background: var(--danger); } +@keyframes toastIn { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} diff --git a/dashboard/pos.html b/dashboard/pos.html new file mode 100644 index 0000000..baaa15d --- /dev/null +++ b/dashboard/pos.html @@ -0,0 +1,113 @@ + + + + + + Punto de Venta — NEXUS AUTOPARTS + + + + + +
+ +
+
+ +
+ +
+
+ + +
+ + +
+ + +
+ +
+
+ + +
+

Carrito

+ + + + + + + + + + + + + + + + +
DescripcionTipoCantCostoMargen%PrecioTotal
Busca y agrega partes al carrito
+
+
+ + +
+
+

Resumen de Factura

+
+ Articulos + 0 +
+
+ Subtotal + $0.00 +
+
+ IVA (16%) + $0.00 +
+
+ Total + $0.00 +
+ + +
+
+
+
+ + + + + + + + diff --git a/dashboard/pos.js b/dashboard/pos.js new file mode 100644 index 0000000..039666b --- /dev/null +++ b/dashboard/pos.js @@ -0,0 +1,413 @@ +/** + * pos.js — Point of Sale logic for Nexus Autoparts + */ +(function () { + 'use strict'; + + var API = ''; + var selectedCustomer = null; + var cart = []; + var defaultMargin = 30; + + // ================================================================ + // Utility + // ================================================================ + + function toast(msg, type) { + var el = document.createElement('div'); + el.className = 'toast ' + (type || 'success'); + el.textContent = msg; + document.body.appendChild(el); + setTimeout(function () { el.remove(); }, 3000); + } + + function api(path, opts) { + opts = opts || {}; + return fetch(API + path, opts).then(function (r) { + if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || 'Error'); }); + return r.json(); + }); + } + + function esc(s) { + if (!s) return ''; + var d = document.createElement('div'); + d.textContent = s; + return d.innerHTML; + } + + function fmt(n) { + return '$' + (parseFloat(n) || 0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } + + // ================================================================ + // Customer Selection + // ================================================================ + + var customerSearchTimer = null; + var customerSearchEl = document.getElementById('customer-search'); + var customerDropdown = document.getElementById('customer-dropdown'); + + customerSearchEl.addEventListener('input', function () { + clearTimeout(customerSearchTimer); + var q = this.value.trim(); + if (q.length < 2) { customerDropdown.style.display = 'none'; return; } + customerSearchTimer = setTimeout(function () { + api('/api/pos/customers?search=' + encodeURIComponent(q) + '&per_page=10') + .then(function (res) { + var data = res.data || []; + if (data.length === 0) { + customerDropdown.innerHTML = '
No se encontraron clientes
'; + } else { + customerDropdown.innerHTML = data.map(function (c) { + return '
' + + '
' + esc(c.name) + '' + + (c.rfc ? ' ' + esc(c.rfc) + '' : '') + '
' + + '' + + fmt(c.balance) + '
'; + }).join(''); + + customerDropdown.querySelectorAll('.customer-dropdown-item').forEach(function (item) { + item.addEventListener('click', function () { + selectCustomer(parseInt(item.getAttribute('data-id'))); + }); + }); + } + customerDropdown.style.display = 'block'; + }); + }, 300); + }); + + customerSearchEl.addEventListener('blur', function () { + setTimeout(function () { customerDropdown.style.display = 'none'; }, 200); + }); + + function selectCustomer(id) { + api('/api/pos/customers/' + id).then(function (c) { + selectedCustomer = c; + document.getElementById('customer-select').style.display = 'none'; + var info = document.getElementById('customer-info'); + info.style.display = 'flex'; + document.getElementById('sel-customer-name').textContent = c.name; + document.getElementById('sel-customer-rfc').textContent = c.rfc || 'Sin RFC'; + var balEl = document.getElementById('sel-customer-balance'); + balEl.textContent = 'Saldo: ' + fmt(c.balance); + balEl.className = 'cb-balance ' + (c.balance > 0 ? 'positive' : 'zero'); + customerDropdown.style.display = 'none'; + updateFacturarBtn(); + }); + } + + document.getElementById('btn-change-customer').addEventListener('click', function () { + selectedCustomer = null; + document.getElementById('customer-info').style.display = 'none'; + document.getElementById('customer-select').style.display = 'block'; + customerSearchEl.value = ''; + customerSearchEl.focus(); + updateFacturarBtn(); + }); + + // ================================================================ + // New Customer Modal + // ================================================================ + + document.getElementById('btn-new-customer').addEventListener('click', function () { + document.getElementById('modal-new-customer').style.display = 'flex'; + document.getElementById('nc-name').focus(); + }); + + document.getElementById('nc-cancel').addEventListener('click', function () { + document.getElementById('modal-new-customer').style.display = 'none'; + }); + + document.getElementById('nc-save').addEventListener('click', function () { + var name = document.getElementById('nc-name').value.trim(); + if (!name) { toast('Ingresa el nombre del cliente', 'error'); return; } + + api('/api/pos/customers', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: name, + rfc: document.getElementById('nc-rfc').value.trim() || null, + business_name: document.getElementById('nc-business').value.trim() || null, + phone: document.getElementById('nc-phone').value.trim() || null, + email: document.getElementById('nc-email').value.trim() || null, + address: document.getElementById('nc-address').value.trim() || null, + credit_limit: parseFloat(document.getElementById('nc-credit').value) || 0, + payment_terms: parseInt(document.getElementById('nc-terms').value) || 30 + }) + }).then(function (res) { + toast('Cliente creado: ' + name); + document.getElementById('modal-new-customer').style.display = 'none'; + selectCustomer(res.id); + // Clear form + ['nc-name','nc-rfc','nc-business','nc-phone','nc-email','nc-address'].forEach(function(id) { + document.getElementById(id).value = ''; + }); + document.getElementById('nc-credit').value = '0'; + document.getElementById('nc-terms').value = '30'; + }).catch(function (err) { + toast(err.message, 'error'); + }); + }); + + // ================================================================ + // Part Search — Autocomplete + // ================================================================ + + var partSearchTimer = null; + var partSearchEl = document.getElementById('part-search'); + var partResults = document.getElementById('part-results'); + var searchResults = []; + var highlightIdx = -1; + + function doPartSearch() { + var q = partSearchEl.value.trim(); + if (q.length < 1) { partResults.style.display = 'none'; searchResults = []; return; } + clearTimeout(partSearchTimer); + partSearchTimer = setTimeout(function () { + api('/api/pos/search-parts?q=' + encodeURIComponent(q)).then(function (results) { + searchResults = results; + highlightIdx = -1; + renderSearchResults(); + }); + }, 150); + } + + function renderSearchResults() { + if (searchResults.length === 0 && partSearchEl.value.trim().length > 0) { + partResults.innerHTML = '
No se encontraron partes para "' + esc(partSearchEl.value) + '"
'; + partResults.style.display = 'block'; + return; + } + if (searchResults.length === 0) { partResults.style.display = 'none'; return; } + + partResults.innerHTML = searchResults.map(function (p, i) { + var active = i === highlightIdx ? ' part-result-active' : ''; + return '
' + + '
' + esc(p.oem_part_number) + '' + + '' + esc(p.name_part) + '
' + + '
' + + '' + p.part_type + '' + + (p.cost_usd ? '' + fmt(p.cost_usd) + '' : '') + + '' + esc(p.group_name || '') + '' + + '
'; + }).join(''); + + partResults.querySelectorAll('.part-result-item').forEach(function (item) { + item.addEventListener('mousedown', function (e) { + e.preventDefault(); + selectSearchResult(parseInt(item.getAttribute('data-idx'))); + }); + item.addEventListener('mouseenter', function () { + highlightIdx = parseInt(item.getAttribute('data-idx')); + updateHighlight(); + }); + }); + + partResults.style.display = 'block'; + } + + function updateHighlight() { + partResults.querySelectorAll('.part-result-item').forEach(function (el, i) { + if (i === highlightIdx) { + el.classList.add('part-result-active'); + el.scrollIntoView({ block: 'nearest' }); + } else { + el.classList.remove('part-result-active'); + } + }); + } + + function selectSearchResult(idx) { + if (idx >= 0 && idx < searchResults.length) { + addToCart(searchResults[idx]); + partSearchEl.value = ''; + partResults.style.display = 'none'; + searchResults = []; + highlightIdx = -1; + partSearchEl.focus(); + } + } + + partSearchEl.addEventListener('input', doPartSearch); + + partSearchEl.addEventListener('keydown', function (e) { + if (partResults.style.display === 'none' || searchResults.length === 0) return; + + if (e.key === 'ArrowDown') { + e.preventDefault(); + highlightIdx = Math.min(highlightIdx + 1, searchResults.length - 1); + updateHighlight(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + highlightIdx = Math.max(highlightIdx - 1, 0); + updateHighlight(); + } else if (e.key === 'Enter') { + e.preventDefault(); + if (highlightIdx >= 0) { + selectSearchResult(highlightIdx); + } else if (searchResults.length === 1) { + selectSearchResult(0); + } + } else if (e.key === 'Escape') { + partResults.style.display = 'none'; + highlightIdx = -1; + } + }); + + partSearchEl.addEventListener('focus', function () { + if (searchResults.length > 0) { + partResults.style.display = 'block'; + } + }); + + partSearchEl.addEventListener('blur', function () { + setTimeout(function () { partResults.style.display = 'none'; }, 200); + }); + + // ================================================================ + // Cart + // ================================================================ + + function addToCart(part) { + cart.push({ + part_id: part.part_type === 'oem' ? part.id_part : null, + aftermarket_id: part.part_type === 'aftermarket' ? part.id_part : null, + description: (part.oem_part_number || '') + ' - ' + (part.name_part || ''), + part_type: part.part_type, + quantity: 1, + unit_cost: part.cost_usd || 0, + margin_pct: defaultMargin, + unit_price: (part.cost_usd || 0) * (1 + defaultMargin / 100) + }); + renderCart(); + partSearchEl.focus(); + } + + function renderCart() { + var tbody = document.getElementById('cart-body'); + + if (cart.length === 0) { + tbody.innerHTML = 'Busca y agrega partes al carrito'; + updateTotals(); + return; + } + + tbody.innerHTML = cart.map(function (item, i) { + var lineTotal = item.quantity * item.unit_price; + return '' + + '' + esc(item.description) + '' + + '' + item.part_type + '' + + '' + + '' + + '%' + + '' + fmt(item.unit_price) + '' + + '' + fmt(lineTotal) + '' + + '' + + ''; + }).join(''); + + // Input change handlers + tbody.querySelectorAll('input').forEach(function (input) { + input.addEventListener('change', function () { + var idx = parseInt(input.getAttribute('data-idx')); + var field = input.getAttribute('data-field'); + var val = parseFloat(input.value) || 0; + cart[idx][field] = val; + + // Recalculate price from cost + margin + if (field === 'unit_cost' || field === 'margin_pct') { + cart[idx].unit_price = cart[idx].unit_cost * (1 + cart[idx].margin_pct / 100); + } + + renderCart(); + }); + }); + + // Remove handlers + tbody.querySelectorAll('.cart-remove').forEach(function (btn) { + btn.addEventListener('click', function () { + cart.splice(parseInt(btn.getAttribute('data-idx')), 1); + renderCart(); + }); + }); + + updateTotals(); + } + + function updateTotals() { + var itemCount = cart.reduce(function (sum, it) { return sum + it.quantity; }, 0); + var subtotal = cart.reduce(function (sum, it) { return sum + (it.quantity * it.unit_price); }, 0); + var tax = subtotal * 0.16; + var total = subtotal + tax; + + document.getElementById('sum-items').textContent = itemCount; + document.getElementById('sum-subtotal').textContent = fmt(subtotal); + document.getElementById('sum-tax').textContent = fmt(tax); + document.getElementById('sum-total').textContent = fmt(total); + + updateFacturarBtn(); + } + + function updateFacturarBtn() { + document.getElementById('btn-facturar').disabled = !(selectedCustomer && cart.length > 0); + } + + // ================================================================ + // Facturar + // ================================================================ + + document.getElementById('btn-facturar').addEventListener('click', function () { + if (!selectedCustomer || cart.length === 0) return; + + var btn = this; + btn.disabled = true; + btn.textContent = 'Generando...'; + + var items = cart.map(function (it) { + return { + part_id: it.part_id, + aftermarket_id: it.aftermarket_id, + description: it.description, + quantity: it.quantity, + unit_cost: it.unit_cost, + margin_pct: it.margin_pct, + unit_price: Math.round(it.unit_price * 100) / 100 + }; + }); + + api('/api/pos/invoices', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + customer_id: selectedCustomer.id_customer, + items: items, + notes: document.getElementById('invoice-notes').value.trim() + }) + }).then(function (res) { + toast('Factura ' + res.folio + ' creada por ' + fmt(res.total)); + + // Reset cart + cart = []; + renderCart(); + document.getElementById('invoice-notes').value = ''; + + // Refresh customer balance + selectCustomer(selectedCustomer.id_customer); + + btn.textContent = 'Facturar'; + }).catch(function (err) { + toast(err.message, 'error'); + btn.disabled = false; + btn.textContent = 'Facturar'; + }); + }); + + // ================================================================ + // Init + // ================================================================ + + renderCart(); +})(); diff --git a/dashboard/tienda.css b/dashboard/tienda.css new file mode 100644 index 0000000..403dd99 --- /dev/null +++ b/dashboard/tienda.css @@ -0,0 +1,678 @@ +/* ============================================================ + tienda.css -- Store / Tablet dashboard styles + Nexus Autoparts — tablet-first, touch-friendly + ============================================================ */ + +/* --- Base overrides for tienda page --- */ +body { + margin: 0; + padding: 0; + font-family: 'DM Sans', sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + overscroll-behavior: none; +} + +/* --- Header --- */ +.t-header { + position: fixed; + top: 0; left: 0; right: 0; + z-index: 100; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.6rem 1.2rem; + background: rgba(18, 18, 26, 0.92); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border-bottom: 1px solid var(--border); +} + +.t-header-left { + display: flex; + align-items: center; + gap: 0.6rem; + flex-shrink: 0; +} + +.t-logo-mark { + width: 36px; + height: 36px; + background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%); + border-radius: 9px; + box-shadow: 0 3px 14px var(--accent-glow); + display: flex; + align-items: center; + justify-content: center; +} + +.t-logo-mark::after { + content: '\2699\FE0F'; + font-size: 1.2rem; +} + +.t-brand { + display: flex; + flex-direction: column; + line-height: 1.1; +} + +.t-brand-name { + font-family: 'Outfit', sans-serif; + font-weight: 800; + font-size: 1.1rem; + background: linear-gradient(135deg, #fff 0%, var(--accent) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.t-brand-sub { + font-size: 0.55rem; + font-weight: 600; + color: var(--text-secondary); + letter-spacing: 0.15em; + text-transform: uppercase; +} + +/* --- Header center: search --- */ +.t-header-center { + flex: 1; + max-width: 420px; + margin: 0 1rem; +} + +.t-search-box { + position: relative; + display: flex; + align-items: center; +} + +.t-search-icon { + position: absolute; + left: 0.7rem; + width: 18px; + height: 18px; + color: var(--text-secondary); + pointer-events: none; +} + +.t-search-box input { + width: 100%; + padding: 0.55rem 0.8rem 0.55rem 2.2rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + color: var(--text-primary); + font-family: 'DM Sans', sans-serif; + font-size: 0.85rem; + outline: none; + transition: border-color 0.2s; +} + +.t-search-box input:focus { + border-color: var(--accent); +} + +.t-search-box input::placeholder { + color: var(--text-secondary); +} + +.t-search-results { + position: absolute; + top: calc(100% + 4px); + left: 0; right: 0; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + max-height: 300px; + overflow-y: auto; + box-shadow: 0 12px 40px rgba(0,0,0,0.5); + display: none; + z-index: 200; +} + +.t-search-results.active { + display: block; +} + +.t-search-result-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.6rem 0.8rem; + border-bottom: 1px solid var(--border); + cursor: pointer; + transition: background 0.15s; +} + +.t-search-result-item:last-child { + border-bottom: none; +} + +.t-search-result-item:hover, +.t-search-result-item:active { + background: var(--bg-hover); +} + +.t-search-result-item .sri-number { + font-family: 'JetBrains Mono', monospace; + font-weight: 600; + font-size: 0.85rem; + color: var(--accent); +} + +.t-search-result-item .sri-name { + font-size: 0.8rem; + color: var(--text-secondary); + margin-left: 0.4rem; +} + +/* --- Header right: clock --- */ +.t-header-right { + flex-shrink: 0; +} + +.t-clock { + font-family: 'JetBrains Mono', monospace; + font-size: 0.85rem; + font-weight: 500; + color: var(--text-secondary); + letter-spacing: 0.03em; +} + +/* --- Main --- */ +.t-main { + padding: 4.2rem 1rem 1.5rem; + max-width: 1200px; + margin: 0 auto; +} + +/* --- KPI Row --- */ +.t-kpi-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 0.8rem; + margin-bottom: 1rem; +} + +.t-kpi { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 0.9rem 1rem; + display: flex; + align-items: center; + gap: 0.8rem; + position: relative; + overflow: hidden; + transition: transform 0.2s, box-shadow 0.2s; +} + +.t-kpi:active { + transform: scale(0.98); +} + +/* Colored left accent bar */ +.t-kpi::before { + content: ''; + position: absolute; + left: 0; top: 0; bottom: 0; + width: 3px; + border-radius: 3px 0 0 3px; +} + +.t-kpi[data-color="accent"]::before { background: var(--accent); } +.t-kpi[data-color="success"]::before { background: var(--success); } +.t-kpi[data-color="info"]::before { background: var(--info); } +.t-kpi[data-color="warning"]::before { background: var(--warning); } + +.t-kpi-icon { + width: 40px; + height: 40px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.t-kpi-icon svg { + width: 22px; + height: 22px; +} + +.t-kpi[data-color="accent"] .t-kpi-icon { background: rgba(255, 107, 53, 0.12); color: var(--accent); } +.t-kpi[data-color="success"] .t-kpi-icon { background: rgba(34, 197, 94, 0.12); color: var(--success); } +.t-kpi[data-color="info"] .t-kpi-icon { background: rgba(59, 130, 246, 0.12); color: var(--info); } +.t-kpi[data-color="warning"] .t-kpi-icon { background: rgba(245, 158, 11, 0.12); color: var(--warning); } + +.t-kpi-data { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; +} + +.t-kpi-value { + font-family: 'Outfit', sans-serif; + font-weight: 700; + font-size: 1.3rem; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.t-kpi-label { + font-size: 0.72rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; + font-weight: 500; +} + +.t-kpi-count { + font-size: 0.65rem; + color: var(--text-secondary); + font-family: 'JetBrains Mono', monospace; + white-space: nowrap; + align-self: flex-start; + margin-top: 0.2rem; +} + +/* --- Content Grid --- */ +.t-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.8rem; +} + +/* --- Cards --- */ +.t-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1rem; +} + +.t-card-full { + min-height: 0; +} + +.t-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.8rem; +} + +.t-card-title { + font-family: 'DM Sans', sans-serif; + font-size: 0.85rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 0.8rem; +} + +.t-card-header .t-card-title { + margin-bottom: 0; +} + +.t-see-all { + font-size: 0.75rem; + color: var(--accent); + text-decoration: none; + font-weight: 600; + padding: 0.3rem 0.6rem; + border-radius: 6px; + transition: background 0.2s; +} + +.t-see-all:hover, +.t-see-all:active { + background: rgba(255, 107, 53, 0.1); +} + +/* --- Quick Actions Grid --- */ +.t-actions-card { + padding-bottom: 0.8rem; +} + +.t-actions-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.6rem; +} + +.t-action { + display: flex; + align-items: center; + gap: 0.7rem; + padding: 0.8rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 10px; + text-decoration: none; + color: var(--text-primary); + font-size: 0.85rem; + font-weight: 600; + transition: transform 0.15s, background 0.2s, border-color 0.2s; + -webkit-tap-highlight-color: transparent; +} + +.t-action:active { + transform: scale(0.96); +} + +.t-action:hover { + background: var(--bg-hover); +} + +.t-action-icon { + width: 36px; + height: 36px; + border-radius: 9px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.t-action-icon svg { + width: 20px; + height: 20px; +} + +.t-action[data-color="accent"] .t-action-icon { background: rgba(255, 107, 53, 0.12); color: var(--accent); } +.t-action[data-color="accent"]:hover { border-color: var(--accent); } +.t-action[data-color="info"] .t-action-icon { background: rgba(59, 130, 246, 0.12); color: var(--info); } +.t-action[data-color="info"]:hover { border-color: var(--info); } +.t-action[data-color="success"] .t-action-icon { background: rgba(34, 197, 94, 0.12); color: var(--success); } +.t-action[data-color="success"]:hover { border-color: var(--success); } +.t-action[data-color="warning"] .t-action-icon { background: rgba(245, 158, 11, 0.12); color: var(--warning); } +.t-action[data-color="warning"]:hover { border-color: var(--warning); } + +/* --- Debtors List --- */ +.t-debtors-list { + display: flex; + flex-direction: column; + gap: 0.4rem; + max-height: 280px; + overflow-y: auto; +} + +.t-debtor { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.6rem 0.7rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} + +.t-debtor:hover, +.t-debtor:active { + background: var(--bg-hover); + border-color: var(--danger); +} + +.t-debtor-name { + font-weight: 600; + font-size: 0.85rem; +} + +.t-debtor-invoices { + font-size: 0.7rem; + color: var(--text-secondary); +} + +.t-debtor-amount { + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + font-size: 0.9rem; + color: var(--danger); +} + +/* --- Invoice List --- */ +.t-invoice-list { + display: flex; + flex-direction: column; + gap: 0.4rem; + max-height: 320px; + overflow-y: auto; +} + +.t-invoice { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.6rem 0.7rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px; + transition: background 0.15s; +} + +.t-invoice:hover, +.t-invoice:active { + background: var(--bg-hover); +} + +.t-invoice-left { + display: flex; + flex-direction: column; + gap: 0.15rem; +} + +.t-invoice-folio { + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + font-size: 0.85rem; + color: var(--accent); +} + +.t-invoice-customer { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.t-invoice-right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.15rem; +} + +.t-invoice-total { + font-family: 'JetBrains Mono', monospace; + font-weight: 600; + font-size: 0.85rem; +} + +.t-invoice-status { + font-size: 0.65rem; + font-weight: 600; + padding: 0.15rem 0.45rem; + border-radius: 4px; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.t-invoice-status.paid { + background: rgba(34, 197, 94, 0.15); + color: var(--success); +} + +.t-invoice-status.pending { + background: rgba(245, 158, 11, 0.15); + color: var(--warning); +} + +.t-invoice-status.partial { + background: rgba(59, 130, 246, 0.15); + color: var(--info); +} + +.t-invoice-status.cancelled { + background: rgba(255, 68, 68, 0.15); + color: var(--danger); +} + +/* --- Today's Payments card --- */ +.t-today-payments { + text-align: center; + padding: 0.5rem 0; +} + +.t-today-amount { + font-family: 'Outfit', sans-serif; + font-weight: 800; + font-size: 2rem; + color: var(--success); + line-height: 1.2; +} + +.t-today-count { + font-size: 0.8rem; + color: var(--text-secondary); + margin-top: 0.3rem; +} + +/* --- Empty state --- */ +.t-empty { + text-align: center; + padding: 1.5rem; + color: var(--text-secondary); + font-size: 0.85rem; +} + +/* --- Scrollbar (minimal for touch) --- */ +.t-debtors-list::-webkit-scrollbar, +.t-invoice-list::-webkit-scrollbar, +.t-search-results::-webkit-scrollbar { + width: 4px; +} + +.t-debtors-list::-webkit-scrollbar-track, +.t-invoice-list::-webkit-scrollbar-track, +.t-search-results::-webkit-scrollbar-track { + background: transparent; +} + +.t-debtors-list::-webkit-scrollbar-thumb, +.t-invoice-list::-webkit-scrollbar-thumb, +.t-search-results::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 2px; +} + +/* --- Responsive --- */ + +/* Tablet landscape (default target) */ +@media (max-width: 1024px) { + .t-main { + padding: 4rem 0.8rem 1.2rem; + } + + .t-kpi-row { + grid-template-columns: repeat(2, 1fr); + } +} + +/* Tablet portrait / large phone */ +@media (max-width: 768px) { + .t-header-center { + display: none; + } + + .t-main { + padding: 3.8rem 0.6rem 1rem; + } + + .t-content { + grid-template-columns: 1fr; + } + + .t-kpi-row { + grid-template-columns: repeat(2, 1fr); + gap: 0.6rem; + } + + .t-kpi { + padding: 0.7rem 0.8rem; + } + + .t-kpi-value { + font-size: 1.1rem; + } + + .t-kpi-count { + display: none; + } + + .t-actions-grid { + grid-template-columns: 1fr 1fr; + } +} + +/* Small phone */ +@media (max-width: 480px) { + .t-kpi-row { + grid-template-columns: 1fr 1fr; + } + + .t-kpi-icon { + width: 32px; + height: 32px; + } + + .t-kpi-icon svg { + width: 18px; + height: 18px; + } + + .t-kpi-value { + font-size: 1rem; + } + + .t-actions-grid { + grid-template-columns: 1fr; + } +} + +/* --- Fade-in animation for cards --- */ +@keyframes t-fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +.t-kpi { + animation: t-fadeIn 0.4s ease both; +} + +.t-kpi:nth-child(1) { animation-delay: 0.05s; } +.t-kpi:nth-child(2) { animation-delay: 0.1s; } +.t-kpi:nth-child(3) { animation-delay: 0.15s; } +.t-kpi:nth-child(4) { animation-delay: 0.2s; } + +.t-card { + animation: t-fadeIn 0.4s ease both; + animation-delay: 0.25s; +} + +.t-content .t-col:nth-child(2) .t-card { + animation-delay: 0.3s; +} + +.t-content .t-col:nth-child(2) .t-card:nth-child(2) { + animation-delay: 0.35s; +} diff --git a/dashboard/tienda.html b/dashboard/tienda.html new file mode 100644 index 0000000..1df57fa --- /dev/null +++ b/dashboard/tienda.html @@ -0,0 +1,156 @@ + + + + + + Tienda — NEXUS AUTOPARTS + + + + + + + + + + +
+
+
+
+ NEXUS + AUTOPARTS +
+
+
+ +
+
+ +
+
+ + +
+ +
+
+
+ +
+
+ $0 + Ventas hoy +
+ 0 facturas +
+
+
+ +
+
+ $0 + Ventas del mes +
+ 0 facturas +
+
+
+ +
+
+ 0 + Clientes activos +
+ 0 partes +
+
+
+ +
+
+ $0 + Por cobrar +
+ 0 facturas +
+
+ + +
+ +
+ + + + +
+

Cuentas pendientes

+
+
Sin cuentas pendientes
+
+
+
+ + +
+ +
+
+

Ultimas facturas

+ Ver todas +
+
+
Sin facturas recientes
+
+
+ + +
+
+

Cobros de hoy

+
+
+
$0.00
+
0 pagos registrados
+
+
+
+
+
+ + + + diff --git a/dashboard/tienda.js b/dashboard/tienda.js new file mode 100644 index 0000000..993a758 --- /dev/null +++ b/dashboard/tienda.js @@ -0,0 +1,187 @@ +/** + * tienda.js — Store / Tablet dashboard logic for Nexus Autoparts + */ +(function () { + 'use strict'; + + var API = ''; + + // ================================================================ + // Utility + // ================================================================ + + function fmt(n) { + return '$' + (parseFloat(n) || 0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } + + function esc(s) { + if (!s) return ''; + var d = document.createElement('div'); + d.textContent = s; + return d.innerHTML; + } + + // ================================================================ + // Clock + // ================================================================ + + function updateClock() { + var now = new Date(); + var h = now.getHours(); + var m = String(now.getMinutes()).padStart(2, '0'); + var ampm = h >= 12 ? 'PM' : 'AM'; + h = h % 12 || 12; + document.getElementById('clock').textContent = h + ':' + m + ' ' + ampm; + } + + updateClock(); + setInterval(updateClock, 30000); + + // ================================================================ + // Load Dashboard Stats + // ================================================================ + + function loadStats() { + fetch(API + '/api/tienda/stats') + .then(function (r) { return r.json(); }) + .then(function (d) { + var st = d.sales_today || {}; + var sm = d.sales_month || {}; + var pt = d.payments_today || {}; + + // KPIs + document.getElementById('kpi-sales-today').textContent = fmt(st.total); + document.getElementById('kpi-sales-count').textContent = (st.count || 0) + ' facturas'; + document.getElementById('kpi-month').textContent = fmt(sm.total); + document.getElementById('kpi-month-count').textContent = (sm.count || 0) + ' facturas'; + document.getElementById('kpi-customers').textContent = d.total_customers || 0; + document.getElementById('kpi-parts-count').textContent = (d.total_parts || 0) + ' partes'; + document.getElementById('kpi-pending').textContent = fmt(d.pending_balance || 0); + document.getElementById('kpi-pending-count').textContent = (d.pending_invoices || 0) + ' facturas'; + + // Today's payments + document.getElementById('payments-today-amount').textContent = fmt(pt.total); + document.getElementById('payments-today-count').textContent = (pt.count || 0) + ' pagos registrados'; + + // Top debtors + renderDebtors(d.top_debtors || []); + + // Recent invoices + renderInvoices(d.recent_invoices || []); + }) + .catch(function (err) { + console.error('Error loading stats:', err); + }); + } + + // ================================================================ + // Render Debtors + // ================================================================ + + function renderDebtors(debtors) { + var el = document.getElementById('debtors-list'); + + if (debtors.length === 0) { + el.innerHTML = '
Sin cuentas pendientes
'; + return; + } + + el.innerHTML = debtors.map(function (d) { + var limitPct = d.credit_limit > 0 ? Math.round(d.balance / d.credit_limit * 100) : 0; + return '' + + '
' + + '
' + esc(d.name) + '
' + + (d.credit_limit > 0 ? '
' + limitPct + '% de l\u00edmite
' : '') + + '
' + + '' + fmt(d.balance) + '' + + '
'; + }).join(''); + } + + // ================================================================ + // Render Recent Invoices + // ================================================================ + + function renderInvoices(invoices) { + var el = document.getElementById('recent-invoices'); + + if (invoices.length === 0) { + el.innerHTML = '
Sin facturas recientes
'; + return; + } + + el.innerHTML = invoices.map(function (inv) { + var statusClass = inv.status || 'pending'; + var statusLabel = { pending: 'Pendiente', paid: 'Pagada', partial: 'Parcial', cancelled: 'Cancelada' }; + return '
' + + '
' + + '' + esc(inv.folio) + '' + + '' + esc(inv.customer_name) + '' + + '
' + + '
' + + '' + fmt(inv.total) + '' + + '' + (statusLabel[statusClass] || statusClass) + '' + + '
' + + '
'; + }).join(''); + } + + // ================================================================ + // Global Search + // ================================================================ + + var searchTimer = null; + var searchInput = document.getElementById('global-search'); + var searchResults = document.getElementById('global-results'); + + if (searchInput) { + searchInput.addEventListener('input', function () { + clearTimeout(searchTimer); + var q = this.value.trim(); + if (q.length < 2) { + searchResults.classList.remove('active'); + searchResults.innerHTML = ''; + return; + } + + searchTimer = setTimeout(function () { + fetch(API + '/api/pos/search-parts?q=' + encodeURIComponent(q)) + .then(function (r) { return r.json(); }) + .then(function (results) { + if (results.length === 0) { + searchResults.innerHTML = '
Sin resultados para "' + esc(q) + '"
'; + } else { + searchResults.innerHTML = results.slice(0, 8).map(function (p) { + return '
' + + '
' + + '' + esc(p.oem_part_number) + '' + + '' + esc(p.name_part) + '' + + '
' + + '
'; + }).join(''); + } + searchResults.classList.add('active'); + }); + }, 250); + }); + + searchInput.addEventListener('blur', function () { + setTimeout(function () { searchResults.classList.remove('active'); }, 200); + }); + + searchInput.addEventListener('focus', function () { + if (searchResults.innerHTML.trim()) { + searchResults.classList.add('active'); + } + }); + } + + // ================================================================ + // Init + // ================================================================ + + loadStats(); + + // Auto-refresh every 2 minutes + setInterval(loadStats, 120000); +})();