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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0 partes registradas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 += '' +
+ '' +
+ '
';
+
+ 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 '';
+ }).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 = '' +
+ '| Fabricante | # Parte | Nombre | Calidad | Precio | Garantia |
';
+ items.forEach(function (a) {
+ html += '| ' + esc(a.manufacturer) + ' | ' + esc(a.part_number) +
+ ' | ' + esc(a.name || '') + ' | ' + esc(a.quality || '') +
+ ' | ' + (a.price_usd ? '$' + a.price_usd : '') +
+ ' | ' + (a.warranty_months || '') + ' |
';
+ });
+ html += '
';
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Facturas
+
+
+ | Folio | Fecha | Total | Pagado | Estado |
+
+
+
+
+
+
+
+
Pagos
+
+
+ | Fecha | Monto | Metodo | Ref | Factura |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+ | Descripcion |
+ Tipo |
+ Cant |
+ Costo |
+ Margen% |
+ Precio |
+ Total |
+ |
+
+
+
+ | Busca y agrega partes al carrito |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $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
+
+
+
+
+
+
+
+
+
+
+
Sin facturas recientes
+
+
+
+
+
+
+
+
$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);
+})();