FASE 7d: Lazy Loading + Minificación + Auto-serve minified

Cambios implementados:

1. Lazy loading de imágenes:
   - catalog.js: loading="lazy" decoding="async" en part cards y detail panel
   - inventory.js: lazy loading en imagen de detalle de item

2. Minificación de assets:
   - scripts/minify-assets.sh: minifica JS (terser) y CSS para POS y Dashboard
   - 25 archivos .min.js + 5 .min.css generados en pos/static/
   - 14 archivos .min.js + 8 .min.css generados en dashboard/

3. Nginx auto-serve minified:
   - try_files $1.min.js antes de servir .js original
   - try_files $1.min.css antes de servir .css original
   - Transparente para los templates HTML (cero cambios en HTML)

4. Cache warming script:
   - scripts/warm_vehicle_cache.py: pobla Redis con vehicle info por batches
   - Mitiga DISTINCT ON + 4 JOINs sobre 2B filas
   - Corre en background, procesa ~1.5M parts

Tests: 73/73 pasando
This commit is contained in:
2026-04-27 08:34:24 +00:00
parent e21722a3a9
commit 21959f1b37
56 changed files with 5629 additions and 4 deletions

1
dashboard/admin.min.js vendored Normal file

File diff suppressed because one or more lines are too long

485
dashboard/bodega.min.css vendored Normal file
View File

@@ -0,0 +1,485 @@
/* ============================================================
bodega.css -- Styles for Nexus Autoparts Warehouse (Bodega)
============================================================ */
/* --- Layout --- */
.bodega-container {
max-width: 1100px;
margin: 0 auto;
padding: 5.5rem 2rem 3rem;
}
/* --- Tabs --- */
.bodega-tabs {
display: flex;
gap: 0;
border-bottom: 2px solid var(--border);
margin-bottom: 1.5rem;
}
.bodega-tab {
padding: 0.8rem 1.8rem;
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.2s;
position: relative;
bottom: -2px;
}
.bodega-tab:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
.bodega-tab.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
.bodega-section {
display: none;
}
.bodega-section.active {
display: block;
}
/* --- Section Intro --- */
.section-intro {
margin-bottom: 1.5rem;
}
.section-intro h2 {
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.section-intro p {
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.5;
}
/* --- Mapping Form --- */
.mapping-form {
max-width: 550px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
}
.mapping-form .form-group {
margin-bottom: 1.25rem;
}
.mapping-form .form-label {
display: block;
margin-bottom: 0.4rem;
font-weight: 500;
font-size: 0.9rem;
color: var(--text-primary);
}
.required {
color: var(--danger);
font-weight: 700;
}
.optional {
color: var(--text-secondary);
font-size: 0.8rem;
font-weight: 400;
}
.form-hint {
display: block;
margin-top: 0.3rem;
font-size: 0.75rem;
color: var(--text-secondary);
}
.form-actions {
display: flex;
align-items: center;
gap: 1rem;
margin-top: 1.25rem;
}
.status-msg {
font-size: 0.85rem;
font-weight: 500;
}
.status-msg.success {
color: var(--success);
}
.status-msg.error {
color: var(--danger);
}
/* --- Upload Zone --- */
.upload-zone {
border: 2px dashed var(--border);
border-radius: 12px;
padding: 3rem 2rem;
text-align: center;
cursor: pointer;
transition: all 0.3s;
background: var(--bg-card);
margin-bottom: 1rem;
}
.upload-zone:hover,
.upload-zone.dragover {
border-color: var(--accent);
background: rgba(255, 107, 53, 0.05);
}
.upload-icon {
font-size: 3rem;
margin-bottom: 0.75rem;
}
.upload-text {
font-size: 1rem;
font-weight: 500;
margin-bottom: 0.3rem;
}
.upload-hint {
font-size: 0.8rem;
color: var(--text-secondary);
}
/* --- Selected File --- */
.selected-file {
display: flex;
align-items: center;
gap: 0.75rem;
background: var(--bg-card);
border: 1px solid var(--accent);
border-radius: 8px;
padding: 0.6rem 1rem;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.btn-icon {
background: none;
border: none;
color: var(--text-secondary);
font-size: 1.2rem;
cursor: pointer;
padding: 0.1rem 0.3rem;
line-height: 1;
transition: color 0.2s;
}
.btn-icon:hover {
color: var(--danger);
}
/* --- Upload Result --- */
.upload-result {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1.25rem;
margin-bottom: 1.5rem;
}
.upload-result h4 {
margin-bottom: 0.75rem;
font-size: 1rem;
}
.result-stats {
display: flex;
gap: 1.5rem;
margin-bottom: 0.75rem;
}
.result-stat {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.9rem;
}
.result-stat.ok {
color: var(--success);
}
.result-stat.err {
color: var(--danger);
}
.error-samples {
margin-top: 0.75rem;
padding: 0.75rem;
background: var(--bg-secondary);
border-radius: 8px;
font-size: 0.8rem;
color: var(--text-secondary);
max-height: 150px;
overflow-y: auto;
}
.error-samples p {
margin-bottom: 0.3rem;
}
/* --- History --- */
.history-section {
margin-top: 2rem;
}
.history-section h3 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 1rem;
}
/* --- Tables --- */
.table-wrap {
overflow-x: auto;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
.data-table thead th {
background: var(--bg-secondary);
color: var(--text-secondary);
text-transform: uppercase;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.05em;
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
.data-table tbody td {
padding: 0.7rem 1rem;
border-bottom: 1px solid var(--border);
color: var(--text-primary);
}
.data-table tbody tr:hover {
background: var(--bg-hover);
}
.empty-row {
text-align: center;
color: var(--text-secondary);
padding: 2rem 1rem !important;
}
/* --- Status Badges --- */
.badge {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 10px;
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-success {
background: rgba(34, 197, 94, 0.15);
color: var(--success);
}
.badge-error {
background: rgba(255, 68, 68, 0.15);
color: var(--danger);
}
.badge-pending {
background: rgba(245, 158, 11, 0.15);
color: var(--warning);
}
.badge-processing {
background: rgba(59, 130, 246, 0.15);
color: var(--info);
}
/* --- Inventory Toolbar --- */
.inventory-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.search-box {
display: flex;
gap: 0.5rem;
flex: 1;
max-width: 500px;
}
.search-box .form-input {
flex: 1;
}
.btn-danger {
background: rgba(255, 68, 68, 0.15);
border: 1px solid var(--danger);
color: var(--danger);
padding: 0.7rem 1.5rem;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s;
}
.btn-danger:hover {
background: var(--danger);
color: white;
}
/* --- Pagination --- */
.bodega-pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
margin-top: 1.25rem;
flex-wrap: wrap;
}
.bodega-pagination button {
padding: 0.4rem 0.8rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-secondary);
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.bodega-pagination button:hover:not(:disabled) {
border-color: var(--accent);
color: var(--accent);
}
.bodega-pagination button.active {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.bodega-pagination button:disabled {
opacity: 0.4;
cursor: default;
}
/* --- Confirm Modal --- */
.confirm-box {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 2rem;
max-width: 420px;
width: 100%;
}
.confirm-box h3 {
margin-bottom: 0.75rem;
font-size: 1.1rem;
}
.confirm-box p {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 1.5rem;
line-height: 1.5;
}
.confirm-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
/* --- Toast --- */
#toast-container {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 3000;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.toast {
padding: 0.8rem 1.2rem;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 500;
animation: fadeIn 0.3s ease;
max-width: 350px;
}
.toast.success {
background: rgba(34, 197, 94, 0.15);
border: 1px solid var(--success);
color: var(--success);
}
.toast.error {
background: rgba(255, 68, 68, 0.15);
border: 1px solid var(--danger);
color: var(--danger);
}
/* --- Responsive --- */
@media (max-width: 768px) {
.bodega-container {
padding: 5rem 1rem 2rem;
}
.bodega-tabs {
overflow-x: auto;
}
.bodega-tab {
padding: 0.7rem 1.2rem;
font-size: 0.85rem;
white-space: nowrap;
}
.inventory-toolbar {
flex-direction: column;
align-items: stretch;
}
.search-box {
max-width: none;
}
.result-stats {
flex-direction: column;
gap: 0.5rem;
}
}

1
dashboard/bodega.min.js vendored Normal file

File diff suppressed because one or more lines are too long

660
dashboard/captura.min.css vendored Normal file
View File

@@ -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);
}

1
dashboard/captura.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dashboard/catalog-public.min.js vendored Normal file

File diff suppressed because one or more lines are too long

237
dashboard/chat-public.min.css vendored Normal file
View File

@@ -0,0 +1,237 @@
/* ==========================================================================
NEXUS — Public Catalog Chat Widget
Reuses design tokens from tokens.css
========================================================================== */
.chat-fab {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 8000;
width: 52px;
height: 52px;
border-radius: var(--radius-full, 50%);
border: none;
cursor: pointer;
background: var(--color-accent, #F5A623);
color: #fff;
font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow-lg, 0 4px 12px rgba(0,0,0,0.3));
transition: transform 0.2s ease, background 0.2s ease;
}
.chat-fab:hover {
transform: scale(1.08);
background: var(--color-primary-hover, #e5952f);
}
.chat-panel {
position: fixed;
bottom: 90px;
right: 24px;
z-index: 8001;
width: 400px;
height: 520px;
max-height: calc(100vh - 100px);
display: flex;
flex-direction: column;
background: var(--color-bg-elevated, #1a1a1a);
border: 1px solid var(--color-border, #333);
border-radius: var(--radius-xl, 16px);
box-shadow: var(--shadow-xl, 0 8px 32px rgba(0,0,0,0.4));
overflow: hidden;
transform: translateY(20px) scale(0.95);
opacity: 0;
pointer-events: none;
transition: transform 0.25s ease, opacity 0.25s ease;
}
.chat-panel.open {
transform: translateY(0) scale(1);
opacity: 1;
pointer-events: all;
}
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--color-accent, #F5A623);
color: #fff;
flex-shrink: 0;
}
.chat-header h3 {
font-family: var(--font-heading, sans-serif);
font-size: 0.95rem;
font-weight: 600;
margin: 0;
}
.chat-header-close {
background: none;
border: none;
color: #fff;
font-size: 1.2rem;
cursor: pointer;
padding: 4px;
line-height: 1;
opacity: 0.8;
}
.chat-header-close:hover { opacity: 1; }
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.chat-msg {
max-width: 85%;
padding: 8px 12px;
border-radius: 12px;
font-size: 0.875rem;
line-height: 1.45;
word-wrap: break-word;
}
.chat-msg.user {
align-self: flex-end;
background: var(--color-accent, #F5A623);
color: #fff;
border-bottom-right-radius: 4px;
}
.chat-msg.ai {
align-self: flex-start;
background: var(--color-surface-2, rgba(255,255,255,0.06));
color: var(--color-text-primary, #fff);
border-bottom-left-radius: 4px;
}
.chat-typing {
align-self: flex-start;
display: none;
gap: 4px;
padding: 8px 12px;
background: var(--color-surface-2, rgba(255,255,255,0.06));
border-radius: 12px;
border-bottom-left-radius: 4px;
}
.chat-typing.visible { display: flex; }
.chat-typing span {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--color-text-muted, #888);
animation: chatBounce 1.2s infinite;
}
.chat-typing span:nth-child(2) { animation-delay: 0.2s; }
.chat-typing span:nth-child(3) { animation-delay: 0.4s; }
@keyframes chatBounce {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-5px); }
}
.chat-parts {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
}
.chat-part-card {
background: var(--color-bg-elevated, #1a1a1a);
border: 1px solid var(--color-border, #333);
border-radius: 8px;
padding: 8px 12px;
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease;
}
.chat-part-card:hover {
border-color: var(--color-accent, #F5A623);
background: var(--color-bg-base, #111);
}
.chat-part-card .part-number {
font-family: var(--font-mono, monospace);
font-size: 0.75rem;
color: var(--color-accent, #F5A623);
font-weight: 600;
}
.chat-part-card .part-name {
font-size: 0.875rem;
color: var(--color-text-primary, #fff);
margin-top: 2px;
}
.chat-input-area {
display: flex;
gap: 8px;
padding: 12px;
border-top: 1px solid var(--color-border, #333);
flex-shrink: 0;
}
.chat-input {
flex: 1;
padding: 8px 12px;
border: 1px solid var(--color-border, #333);
border-radius: 8px;
background: var(--color-bg-base, #111);
color: var(--color-text-primary, #fff);
font-size: 0.875rem;
font-family: var(--font-body, sans-serif);
resize: none;
outline: none;
min-height: 38px;
max-height: 80px;
}
.chat-input:focus {
border-color: var(--color-accent, #F5A623);
}
.chat-input::placeholder {
color: var(--color-text-muted, #888);
}
.chat-send-btn {
width: 38px;
height: 38px;
border-radius: 8px;
border: none;
background: var(--color-accent, #F5A623);
color: #fff;
font-size: 1.1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: background 0.15s ease;
}
.chat-send-btn:hover { background: var(--color-primary-hover, #e5952f); }
.chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
@media (max-width: 480px) {
.chat-panel {
width: calc(100vw - 16px);
right: 8px;
height: 60vh;
}
}

1
dashboard/chat-public.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var e=!1,t=!1,n=[];function a(){var e=document.createElement("button");e.className="chat-fab",e.id="chatFab",e.title="Asistente IA",e.innerHTML="&#x1F4AC;",e.setAttribute("aria-label","Abrir asistente IA");var t=document.createElement("div");t.className="chat-panel",t.id="chatPanel",t.innerHTML='<div class="chat-header"><h3>Asistente — Buscar partes</h3><button class="chat-header-close" id="chatClose" aria-label="Cerrar">&times;</button></div><div class="chat-messages" id="chatMessages"><div class="chat-msg ai">Hola, soy el asistente de Nexus Autoparts. Dime que refaccion buscas y te ayudo a encontrarla en el catalogo.</div><div class="chat-typing" id="chatTyping"><span></span><span></span><span></span></div></div><div class="chat-input-area"><textarea class="chat-input" id="chatInput" placeholder="Ej: Balatas para Tsuru 2015..." rows="1"></textarea><button class="chat-send-btn" id="chatSend" aria-label="Enviar">&#9654;</button></div>',document.body.appendChild(e),document.body.appendChild(t),e.addEventListener("click",s),document.getElementById("chatClose").addEventListener("click",s),document.getElementById("chatSend").addEventListener("click",c),document.getElementById("chatInput").addEventListener("keydown",(function(e){"Enter"!==e.key||e.shiftKey||(e.preventDefault(),c())})),document.getElementById("chatInput").addEventListener("input",(function(){this.style.height="auto",this.style.height=Math.min(this.scrollHeight,80)+"px"}))}function s(){e=!e;var t=document.getElementById("chatPanel"),n=document.getElementById("chatFab");e?(t.classList.add("open"),n.style.display="none",document.getElementById("chatInput").focus()):(t.classList.remove("open"),n.style.display="flex")}function c(){if(!t){var e=document.getElementById("chatInput"),a=e.value.trim();a&&(e.value="",e.style.height="auto",i(a,"user"),n.push({role:"user",content:a}),n.length>20&&n.splice(0,2),t=!0,document.getElementById("chatSend").disabled=!0,d(!0),fetch("/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:a,history:n.slice(-10)})}).then((function(e){return e.json()})).then((function(e){if(e.error)i("Error: "+e.error,"ai");else{var t,a,c,d,l=e.response||"Sin respuesta.";i(l,"ai"),n.push({role:"assistant",content:l}),e.search_results&&e.search_results.length>0&&(t=e.search_results,a=document.getElementById("chatMessages"),c=document.getElementById("chatTyping"),(d=document.createElement("div")).className="chat-parts",t.slice(0,8).forEach((function(e){var t=document.createElement("div");t.className="chat-part-card";var n=e.name_es||e.name_part||"",a=e.oem_part_number||e.part_number||"",c=e.brand||"";t.innerHTML='<div class="part-number">'+o(a)+'</div><div class="part-name">'+o(n)+(c?' <span style="color:var(--color-text-muted);">('+o(c)+")</span>":"")+"</div>",t.style.cursor="pointer",t.addEventListener("click",(function(){var e=document.getElementById("searchInput");e&&a&&(e.value=a,"function"==typeof window.doSearch&&window.doSearch(),s())})),d.appendChild(t)})),a.insertBefore(d,c),r())}})).catch((function(e){i("Error de conexion: "+e.message,"ai")})).finally((function(){t=!1,document.getElementById("chatSend").disabled=!1,d(!1)})))}}function i(e,t){var n=document.getElementById("chatMessages"),a=document.getElementById("chatTyping"),s=document.createElement("div");s.className="chat-msg "+t,s.textContent=e,n.insertBefore(s,a),r()}function d(e){var t=document.getElementById("chatTyping");t&&t.classList.toggle("visible",e),e&&r()}function r(){var e=document.getElementById("chatMessages");e&&(e.scrollTop=e.scrollHeight)}function o(e){if(!e)return"";var t=document.createElement("div");return t.textContent=e,t.innerHTML}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",a):a()}();

282
dashboard/cuentas.min.css vendored Normal file
View File

@@ -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;
}

1
dashboard/cuentas.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dashboard/dashboard.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dashboard/enhanced-search.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dashboard/landing.min.js vendored Normal file

File diff suppressed because one or more lines are too long

211
dashboard/login.min.css vendored Normal file
View File

@@ -0,0 +1,211 @@
/* ============================================================
login.css -- Login / Register page styles
============================================================ */
.login-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: var(--bg-primary);
padding: 2rem;
}
/* --- Card --- */
.login-card {
width: 100%;
max-width: 440px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 16px;
padding: 2.5rem;
animation: fadeIn 0.4s ease;
}
/* --- Brand header --- */
.login-brand {
text-align: center;
margin-bottom: 2rem;
}
.login-brand .logo-icon {
width: 56px;
height: 56px;
background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%);
border-radius: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.6rem;
margin-bottom: 1rem;
box-shadow: 0 4px 20px var(--accent-glow);
}
.login-brand h1 {
font-family: 'Orbitron', sans-serif;
font-size: 1.5rem;
font-weight: 700;
letter-spacing: 2px;
color: var(--text-primary);
margin-bottom: 0.4rem;
}
.login-brand h1 span {
color: var(--accent);
}
.login-brand .slogan {
font-size: 0.85rem;
color: var(--text-secondary);
font-weight: 400;
}
/* --- Form panel visibility --- */
.form-panel {
display: none;
}
.form-panel.active {
display: block;
animation: fadeIn 0.3s ease;
}
/* --- Form title --- */
.form-title {
font-size: 1.15rem;
font-weight: 600;
margin-bottom: 1.5rem;
text-align: center;
color: var(--text-primary);
}
/* --- Select (dropdown) --- */
.form-select {
width: 100%;
padding: 0.75rem 1rem;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 0.95rem;
transition: border-color 0.2s;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a0a0b0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 1rem center;
cursor: pointer;
}
.form-select:focus {
outline: none;
border-color: var(--accent);
}
.form-select option {
background: var(--bg-secondary);
color: var(--text-primary);
}
/* --- Submit button (full width) --- */
.btn-submit {
width: 100%;
padding: 0.85rem;
margin-top: 0.5rem;
font-size: 1rem;
}
/* --- Toggle link --- */
.toggle-link {
text-align: center;
margin-top: 1.5rem;
font-size: 0.9rem;
color: var(--text-secondary);
}
.toggle-link a {
color: var(--accent);
text-decoration: none;
font-weight: 600;
cursor: pointer;
transition: color 0.2s;
}
.toggle-link a:hover {
color: var(--accent-hover);
text-decoration: underline;
}
/* --- Alert messages --- */
.login-alert {
padding: 0.85rem 1rem;
border-radius: 8px;
margin-bottom: 1.25rem;
font-size: 0.9rem;
display: none;
align-items: center;
gap: 0.5rem;
line-height: 1.4;
}
.login-alert.show {
display: flex;
}
.login-alert.error {
background: rgba(255, 68, 68, 0.1);
border: 1px solid var(--danger);
color: var(--danger);
}
.login-alert.success {
background: rgba(0, 214, 143, 0.1);
border: 1px solid var(--success);
color: var(--success);
}
/* --- Loading spinner on button --- */
.btn-submit.loading {
pointer-events: none;
opacity: 0.7;
}
.btn-submit .spinner {
display: none;
width: 18px;
height: 18px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
.btn-submit.loading .spinner {
display: inline-block;
}
.btn-submit.loading .btn-label {
display: none;
}
/* --- Row layout for two fields side by side --- */
.form-row {
display: flex;
gap: 1rem;
}
.form-row .form-group {
flex: 1;
}
/* --- Responsive --- */
@media (max-width: 500px) {
.login-card {
padding: 1.75rem 1.5rem;
}
.form-row {
flex-direction: column;
gap: 0;
}
}

1
dashboard/login.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";const e=document.getElementById("loginPanel"),t=document.getElementById("registerPanel"),o=document.getElementById("loginForm"),n=document.getElementById("registerForm"),r=document.getElementById("alert"),a={ADMIN:"/demo",OWNER:"/demo",BODEGA:"/bodega",TALLER:"/demo"};function s(e,t){r.textContent=e,r.className="login-alert show "+t}function c(){r.className="login-alert",r.textContent=""}function l(e,t){e.classList.toggle("loading",t)}!function(){const e=localStorage.getItem("access_token"),t=localStorage.getItem("user_role");if(e&&t){const e=a[t]||"/index.html";window.location.replace(e)}}(),window.showPanel=function(o){c(),"register"===o?(e.classList.remove("active"),t.classList.add("active")):(t.classList.remove("active"),e.classList.add("active"))},o.addEventListener("submit",(async function(e){e.preventDefault(),c();const t=document.getElementById("loginEmail").value.trim(),n=document.getElementById("loginPassword").value,r=o.querySelector(".btn-submit");if(t&&n){l(r,!0);try{const e=await fetch("/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:t,password:n})}),o=await e.json();if(!e.ok)return void s(o.error||o.message||"Credenciales incorrectas.","error");localStorage.setItem("access_token",o.access_token),localStorage.setItem("refresh_token",o.refresh_token||""),localStorage.setItem("user_role",o.role||o.user?.role||""),localStorage.setItem("user_name",o.name||o.user?.name||"");const r=(o.role||o.user?.role||"").toUpperCase(),c=a[r]||"/index.html";window.location.replace(c)}catch(e){s("Error de conexion. Intenta de nuevo.","error")}finally{l(r,!1)}}else s("Completa todos los campos.","error")})),n.addEventListener("submit",(async function(e){e.preventDefault(),c();const t=document.getElementById("regName").value.trim(),o=document.getElementById("regEmail").value.trim(),r=document.getElementById("regPassword").value,a=document.getElementById("regConfirm").value,i=document.getElementById("regBusiness").value.trim(),m=document.getElementById("regPhone").value.trim(),d=document.getElementById("regRole").value,u=n.querySelector(".btn-submit");if(t&&o&&r&&a&&i&&m)if(r.length<8)s("La contrasena debe tener al menos 8 caracteres.","error");else if(r===a){l(u,!0);try{const e=await fetch("/api/auth/register",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t,email:o,password:r,role:d,business_name:i,phone:m})}),a=await e.json();if(!e.ok)return void s(a.error||a.message||"Error al crear la cuenta.","error");s("Cuenta creada. Pendiente de aprobacion por administrador.","success"),n.reset()}catch(e){s("Error de conexion. Intenta de nuevo.","error")}finally{l(u,!1)}}else s("Las contrasenas no coinciden.","error");else s("Completa todos los campos.","error")})),window.authFetch=async function(e,t={}){const o=localStorage.getItem("access_token");if(!o)return void window.location.replace("/login.html");const n=Object.assign({},t.headers||{},{Authorization:"Bearer "+o});let r=await fetch(e,Object.assign({},t,{headers:n}));if(401===r.status){const o=await async function(){const e=localStorage.getItem("refresh_token");if(!e)return!1;try{const t=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refresh_token:e})});if(!t.ok)return!1;const o=await t.json();return localStorage.setItem("access_token",o.access_token),o.refresh_token&&localStorage.setItem("refresh_token",o.refresh_token),!0}catch(e){return!1}}();if(!o)return void logout();n.Authorization="Bearer "+localStorage.getItem("access_token"),r=await fetch(e,Object.assign({},t,{headers:n}))}return r},window.logout=function(){localStorage.removeItem("access_token"),localStorage.removeItem("refresh_token"),localStorage.removeItem("user_role"),localStorage.removeItem("user_name"),window.location.replace("/login.html")}}();

1
dashboard/nav.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var e=window.location.pathname;function t(t){var a=t.replace(/\/+$/,"")||"/",n=e.replace(/\/+$/,"")||"/";return a===n||(!("/"!==a&&"/index.html"!==a||"/"!==n&&"/index.html"!==n)||(!("/admin.html"!==a&&"/admin"!==a||"/admin.html"!==n&&"/admin"!==n)||(!("/diagramas"!==a&&"/diagrams.html"!==a||"/diagramas"!==n&&"/diagrams.html"!==n)||("/customer-landing.html"===a&&"/customer-landing.html"===n||("/captura"===a&&"/captura"===n||("/pos"===a&&"/pos"===n||("/cuentas"===a&&"/cuentas"===n||("/tienda"===a&&"/tienda"===n||("/bodega"===a&&"/bodega"===n||!("/demo"!==a&&"/demo.html"!==a||"/demo"!==n&&"/demo.html"!==n))))))))))}var a='<header id="shared-nav-header" style="background: rgba(18, 18, 26, 0.95);backdrop-filter: blur(20px);-webkit-backdrop-filter: blur(20px);border-bottom: 1px solid var(--border);padding: 1rem 2rem;position: fixed;top: 0; left: 0; right: 0;z-index: 1000;"><div style="max-width: 1600px;margin: 0 auto;display: flex;justify-content: space-between;align-items: center;gap: 2rem;"><a href="/" style="display: flex;align-items: center;gap: 0.75rem;text-decoration: none;flex-shrink: 0;"><div style="width: 42px; height: 42px;background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%);border-radius: 10px;display: flex; align-items: center; justify-content: center;font-size: 1.5rem;box-shadow: 0 4px 20px var(--accent-glow);">⚙️</div><span style="font-family: Orbitron, sans-serif;font-size: 1.3rem;font-weight: 700;background: linear-gradient(135deg, #fff 0%, var(--accent) 100%);-webkit-background-clip: text;-webkit-text-fill-color: transparent;background-clip: text;">NEXUS AUTOPARTS</span></a><div id="shared-nav-extra" style="display: contents;"></div><nav id="shared-nav-links" style="display: flex;gap: 1.5rem;align-items: center;flex-shrink: 0;">'+[{label:"Demo",href:"/demo"},{label:"Tienda",href:"/tienda"},{label:"Catálogo",href:"/index.html"},{label:"Captura",href:"/captura"},{label:"POS",href:"/pos"},{label:"Cuentas",href:"/cuentas"},{label:"Bodega",href:"/bodega"},{label:"Admin",href:"/admin"}].map((function(e){var a="text-decoration: none; font-size: 0.9rem; font-weight: 500; transition: color 0.2s;";return t(e.href)?a+=" color: var(--accent);":a+=" color: var(--text-secondary);",'<a href="'+e.href+'" style="'+a+'" onmouseover="this.style.color=\'var(--accent)\'" onmouseout="'+(t(e.href)?"":"this.style.color='var(--text-secondary)'")+'">'+e.label+"</a>"})).join("")+'</nav><div id="nav-auth" style="display:flex;align-items:center;gap:0.75rem;flex-shrink:0;"><span id="nav-user-name" style="color:var(--text-secondary);font-size:0.85rem;"></span><a id="nav-auth-btn" href="/login.html" style="text-decoration:none;font-size:0.85rem;font-weight:500;color:var(--bg);background:var(--accent);padding:0.4rem 1rem;border-radius:6px;transition:opacity 0.2s;">Iniciar Sesión</a></div></div></header>',n=document.getElementById("shared-nav");n&&(n.innerHTML=a);var r=localStorage.getItem("access_token");if(r)try{var i=JSON.parse(atob(r.split(".")[1])),o=document.getElementById("nav-user-name"),l=document.getElementById("nav-auth-btn");o&&i.business_name?o.textContent=i.business_name:o&&(o.textContent=i.role||""),l&&(l.textContent="Salir",l.href="#",l.onclick=function(e){e.preventDefault(),localStorage.removeItem("access_token"),localStorage.removeItem("refresh_token"),window.location.href="/login.html"})}catch(e){}}();

418
dashboard/pos.min.css vendored Normal file
View File

@@ -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; }
}

1
dashboard/pos.min.js vendored Normal file

File diff suppressed because one or more lines are too long

262
dashboard/shared.min.css vendored Normal file
View File

@@ -0,0 +1,262 @@
/* ============================================================
shared.css -- Common styles for all Nexus Autoparts pages
============================================================ */
/* --- Reset --- */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* --- CSS Variables (union of all pages) --- */
:root {
--bg-primary: #0a0a0f;
--bg-secondary: #12121a;
--bg-card: #1a1a24;
--bg-hover: #252532;
--bg-tertiary: #1a1a25;
--accent: #ff6b35;
--accent-hover: #ff8555;
--accent-glow: rgba(255, 107, 53, 0.3);
--text-primary: #ffffff;
--text-secondary: #a0a0b0;
--border: #2a2a3a;
--success: #22c55e;
--warning: #f59e0b;
--info: #3b82f6;
--danger: #ff4444;
}
/* --- Base body --- */
body {
font-family: 'Inter', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
}
/* --- Shared Button Styles --- */
.btn {
padding: 0.7rem 1.5rem;
border-radius: 10px;
border: none;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
font-size: 0.9rem;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%);
color: white;
box-shadow: 0 4px 15px var(--accent-glow);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 25px var(--accent-glow);
}
.btn-secondary {
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-primary);
}
.btn-secondary:hover {
border-color: var(--accent);
color: var(--accent);
}
.btn-back {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1.2rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text-primary);
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 1.5rem;
}
.btn-back:hover {
border-color: var(--accent);
color: var(--accent);
}
/* --- Shared Animations --- */
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* --- Loading & Empty States --- */
.state-container {
text-align: center;
padding: 4rem 2rem;
color: var(--text-secondary);
}
.state-container i {
font-size: 4rem;
margin-bottom: 1rem;
color: var(--text-secondary);
}
.state-container h4 {
color: var(--text-primary);
margin-bottom: 0.5rem;
}
/* --- Scrollbar Styling --- */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}
/* --- Skip Link (accessibility) --- */
.skip-link {
position: absolute;
top: -50px;
left: 0;
background: var(--accent);
color: white;
padding: 0.75rem 1.5rem;
z-index: 3000;
text-decoration: none;
font-weight: 600;
border-radius: 0 0 8px 0;
}
.skip-link:focus {
top: 0;
}
/* --- Screen Reader Only --- */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* --- Alert / Toast Styles --- */
.alert {
padding: 1rem 1.5rem;
border-radius: 8px;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.alert-success {
background: rgba(0, 214, 143, 0.1);
border: 1px solid var(--success);
color: var(--success);
}
.alert-error {
background: rgba(255, 68, 68, 0.1);
border: 1px solid var(--danger);
color: var(--danger);
}
/* --- Modal Base Styles --- */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 2000;
align-items: center;
justify-content: center;
padding: 2rem;
}
.modal-overlay.active {
display: flex;
}
/* --- Form Styles --- */
.form-group {
margin-bottom: 1.25rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
font-size: 0.9rem;
color: var(--text-secondary);
}
.form-input {
width: 100%;
padding: 0.75rem 1rem;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 0.95rem;
transition: border-color 0.2s;
}
.form-input:focus {
outline: none;
border-color: var(--accent);
}
.form-input::placeholder {
color: var(--text-secondary);
}
/* --- Quality Badges --- */
.quality-badge {
display: inline-block;
padding: 0.25rem 0.6rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.quality-economy { background: var(--warning); color: #000; }
.quality-standard { background: var(--info); color: white; }
.quality-premium { background: var(--success); color: white; }
.quality-oem { background: #9b59b6; color: white; }

678
dashboard/tienda.min.css vendored Normal file
View File

@@ -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;
}

1
dashboard/tienda.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";function t(t){return"$"+(parseFloat(t)||0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g,",")}function e(t){if(!t)return"";var e=document.createElement("div");return e.textContent=t,e.innerHTML}function n(){var t=new Date,e=t.getHours(),n=String(t.getMinutes()).padStart(2,"0"),i=e>=12?"PM":"AM";e=e%12||12,document.getElementById("clock").textContent=e+":"+n+" "+i}function i(){fetch("/api/tienda/stats").then((function(t){return t.json()})).then((function(n){var i=n.sales_today||{},a=n.sales_month||{},o=n.payments_today||{};document.getElementById("kpi-sales-today").textContent=t(i.total),document.getElementById("kpi-sales-count").textContent=(i.count||0)+" facturas",document.getElementById("kpi-month").textContent=t(a.total),document.getElementById("kpi-month-count").textContent=(a.count||0)+" facturas",document.getElementById("kpi-customers").textContent=n.total_customers||0,document.getElementById("kpi-parts-count").textContent=(n.total_parts||0)+" partes",document.getElementById("kpi-pending").textContent=t(n.pending_balance||0),document.getElementById("kpi-pending-count").textContent=(n.pending_invoices||0)+" facturas",document.getElementById("payments-today-amount").textContent=t(o.total),document.getElementById("payments-today-count").textContent=(o.count||0)+" pagos registrados",function(n){var i=document.getElementById("debtors-list");if(0===n.length)return void(i.innerHTML='<div class="t-empty">Sin cuentas pendientes</div>');i.innerHTML=n.map((function(n){var i=n.credit_limit>0?Math.round(n.balance/n.credit_limit*100):0;return'<a href="/cuentas" class="t-debtor"><div><div class="t-debtor-name">'+e(n.name)+"</div>"+(n.credit_limit>0?'<div class="t-debtor-invoices">'+i+"% de límite</div>":"")+'</div><span class="t-debtor-amount">'+t(n.balance)+"</span></a>"})).join("")}(n.top_debtors||[]),function(n){var i=document.getElementById("recent-invoices");if(0===n.length)return void(i.innerHTML='<div class="t-empty">Sin facturas recientes</div>');i.innerHTML=n.map((function(n){var i=n.status||"pending",a={pending:"Pendiente",paid:"Pagada",partial:"Parcial",cancelled:"Cancelada"};return'<div class="t-invoice"><div class="t-invoice-left"><span class="t-invoice-folio">'+e(n.folio)+'</span><span class="t-invoice-customer">'+e(n.customer_name)+'</span></div><div class="t-invoice-right"><span class="t-invoice-total">'+t(n.total)+'</span><span class="t-invoice-status '+i+'">'+(a[i]||i)+"</span></div></div>"})).join("")}(n.recent_invoices||[])})).catch((function(t){console.error("Error loading stats:",t)}))}n(),setInterval(n,3e4);var a=null,o=document.getElementById("global-search"),s=document.getElementById("global-results");o&&(o.addEventListener("input",(function(){clearTimeout(a);var t=this.value.trim();if(t.length<2)return s.classList.remove("active"),void(s.innerHTML="");a=setTimeout((function(){fetch("/api/pos/search-parts?q="+encodeURIComponent(t)).then((function(t){return t.json()})).then((function(n){0===n.length?s.innerHTML='<div style="padding:0.8rem;color:var(--text-secondary);font-size:0.85rem">Sin resultados para "'+e(t)+'"</div>':s.innerHTML=n.slice(0,8).map((function(t){return'<div class="t-search-result-item"><div><span class="sri-number">'+e(t.oem_part_number)+'</span><span class="sri-name">'+e(t.name_part)+"</span></div></div>"})).join(""),s.classList.add("active")}))}),250)})),o.addEventListener("blur",(function(){setTimeout((function(){s.classList.remove("active")}),200)})),o.addEventListener("focus",(function(){s.innerHTML.trim()&&s.classList.add("active")}))),i(),setInterval(i,12e4)}();

View File

@@ -32,6 +32,21 @@ server {
add_header X-Content-Type-Options nosniff always; add_header X-Content-Type-Options nosniff always;
} }
# Auto-serve minified JS/CSS when available (transparent to templates)
location ~* ^(.+)\.js$ {
try_files $1.min.js $uri =404;
expires 6M;
add_header Cache-Control "public, immutable";
add_header X-Content-Type-Options nosniff always;
}
location ~* ^(.+)\.css$ {
try_files $1.min.css $uri =404;
expires 6M;
add_header Cache-Control "public, immutable";
add_header X-Content-Type-Options nosniff always;
}
location / { location / {
proxy_pass http://nexus_main; proxy_pass http://nexus_main;
proxy_set_header Host $host; proxy_set_header Host $host;

385
pos/static/css/chat.min.css vendored Normal file
View File

@@ -0,0 +1,385 @@
/* ==========================================================================
NEXUS POS — AI Chat Widget
Uses design system tokens from tokens.css
========================================================================== */
/* ─── Floating Button ─── */
.chat-fab {
position: fixed;
bottom: 140px; /* above cart FAB and F-keys footer */
right: var(--space-5);
z-index: 8000;
width: 52px;
height: 52px;
border-radius: var(--radius-full);
border: none;
cursor: pointer;
background: var(--color-accent);
color: #fff;
font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow-lg);
transition: transform var(--duration-fast) var(--ease-in-out),
background var(--duration-fast) var(--ease-in-out);
}
.chat-fab:hover {
transform: scale(1.08);
background: var(--color-primary-hover, #e5952f);
}
.chat-fab.has-unread::after {
content: '';
position: absolute;
top: 4px;
right: 4px;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--color-error);
}
/* ─── Chat Panel ─── */
.chat-panel {
position: fixed;
bottom: 200px;
right: var(--space-5);
z-index: 8001;
width: 400px;
height: 520px;
max-height: calc(100vh - 100px);
display: flex;
flex-direction: column;
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl);
overflow: hidden;
transform: translateY(20px) scale(0.95);
opacity: 0;
pointer-events: none;
transition: transform var(--duration-normal) var(--ease-in-out),
opacity var(--duration-normal) var(--ease-in-out);
}
.chat-panel.open {
transform: translateY(0) scale(1);
opacity: 1;
pointer-events: all;
}
/* ─── Header ─── */
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
background: var(--color-accent);
color: #fff;
flex-shrink: 0;
}
.chat-header h3 {
font-family: var(--font-heading);
font-size: var(--text-body);
font-weight: var(--font-weight-semibold);
margin: 0;
}
.chat-header-close {
background: none;
border: none;
color: #fff;
font-size: 1.2rem;
cursor: pointer;
padding: var(--space-1);
line-height: 1;
opacity: 0.8;
}
.chat-header-close:hover { opacity: 1; }
/* ─── Messages Area ─── */
.chat-messages {
flex: 1;
overflow-y: auto;
padding: var(--space-3);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
/* ─── Message Bubbles ─── */
.chat-msg {
max-width: 85%;
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-lg);
font-size: var(--text-body-sm, 0.875rem);
line-height: 1.45;
word-wrap: break-word;
}
.chat-msg.user {
align-self: flex-end;
background: var(--color-accent);
color: #fff;
border-bottom-right-radius: var(--radius-sm, 4px);
}
.chat-msg.ai {
align-self: flex-start;
background: var(--color-surface-2, rgba(255,255,255,0.06));
color: var(--color-text-primary);
border-bottom-left-radius: var(--radius-sm, 4px);
}
/* ─── Typing Indicator ─── */
.chat-typing {
align-self: flex-start;
display: none;
gap: 4px;
padding: var(--space-2) var(--space-3);
background: var(--color-surface-2, rgba(255,255,255,0.06));
border-radius: var(--radius-lg);
border-bottom-left-radius: var(--radius-sm, 4px);
}
.chat-typing.visible { display: flex; }
.chat-typing span {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--color-text-muted);
animation: chatBounce 1.2s infinite;
}
.chat-typing span:nth-child(2) { animation-delay: 0.2s; }
.chat-typing span:nth-child(3) { animation-delay: 0.4s; }
@keyframes chatBounce {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-5px); }
}
/* ─── Part Result Cards ─── */
.chat-parts {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-top: var(--space-2);
}
.chat-part-card {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3);
cursor: pointer;
transition: border-color var(--duration-fast) var(--ease-in-out),
background var(--duration-fast) var(--ease-in-out);
}
.chat-part-card:hover {
border-color: var(--color-accent);
background: var(--color-bg-base);
}
.chat-part-card .part-number {
font-family: var(--font-mono);
font-size: var(--text-caption, 0.75rem);
color: var(--color-accent);
font-weight: var(--font-weight-semibold);
}
.chat-part-card .part-name {
font-size: var(--text-body-sm, 0.875rem);
color: var(--color-text-primary);
margin-top: 2px;
}
.chat-part-card .part-stock {
font-size: var(--text-caption, 0.75rem);
color: var(--color-text-muted);
margin-top: 2px;
}
.chat-part-card .part-stock.in-stock {
color: var(--color-success);
}
/* ─── Input Area ─── */
.chat-input-area {
display: flex;
gap: var(--space-2);
padding: var(--space-3);
border-top: 1px solid var(--color-border);
flex-shrink: 0;
}
.chat-input {
flex: 1;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-base);
color: var(--color-text-primary);
font-size: var(--text-body-sm, 0.875rem);
font-family: var(--font-body);
resize: none;
outline: none;
min-height: 38px;
max-height: 80px;
}
.chat-input:focus {
border-color: var(--color-accent);
}
.chat-input::placeholder {
color: var(--color-text-muted);
}
.chat-send-btn {
width: 38px;
height: 38px;
border-radius: var(--radius-md);
border: none;
background: var(--color-accent);
color: #fff;
font-size: 1.1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: background var(--duration-fast) var(--ease-in-out);
}
.chat-send-btn:hover { background: var(--color-primary-hover, #e5952f); }
.chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* ─── Camera Button (Photo identification) ─── */
.chat-cam-btn {
width: 38px;
height: 38px;
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
background: var(--color-bg-base);
color: var(--color-text-secondary);
font-size: 1.1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: background var(--duration-fast) var(--ease-in-out),
color var(--duration-fast) var(--ease-in-out),
border-color var(--duration-fast) var(--ease-in-out);
}
.chat-cam-btn:hover {
border-color: var(--color-accent);
color: var(--color-accent);
}
.chat-msg-image img {
border: 1px solid var(--color-border);
}
/* ─── Mic Button (Voice Input) ─── */
.chat-mic-btn {
width: 38px;
height: 38px;
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
background: var(--color-bg-base);
color: var(--color-text-secondary);
font-size: 1.1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: background var(--duration-fast) var(--ease-in-out),
color var(--duration-fast) var(--ease-in-out),
border-color var(--duration-fast) var(--ease-in-out);
}
.chat-mic-btn:hover {
border-color: var(--color-accent);
color: var(--color-accent);
}
.chat-mic-btn.listening {
background: #f85149;
border-color: #f85149;
color: #fff;
animation: micPulse 1.4s infinite;
}
@keyframes micPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(248, 81, 73, 0.4); }
50% { box-shadow: 0 0 0 10px rgba(248, 81, 73, 0); }
}
/* ─── Voice Toast ─── */
.chat-voice-toast {
position: fixed;
bottom: 160px;
left: 50%;
transform: translateX(-50%) translateY(10px);
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 8px 18px;
border-radius: var(--radius-md, 8px);
font-size: 0.85rem;
z-index: 9999;
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
pointer-events: none;
}
.chat-voice-toast.visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ─── Vehicle Info Banner ─── */
.chat-vehicle-banner {
margin-top: var(--space-2);
padding: var(--space-2) var(--space-3);
background: var(--color-bg-base);
border: 1px solid var(--color-accent);
border-left: 3px solid var(--color-accent);
border-radius: var(--radius-md);
font-size: var(--text-caption, 0.75rem);
color: var(--color-text-secondary);
}
.chat-vehicle-banner strong {
color: var(--color-text-primary);
}
/* ─── Responsive ─── */
@media (max-width: 480px) {
.chat-panel {
width: calc(100vw - var(--space-4));
right: var(--space-2);
height: 60vh;
}
}

85
pos/static/css/common.min.css vendored Normal file
View File

@@ -0,0 +1,85 @@
/* /home/Autopartes/pos/static/css/common.css */
/* Theme variables — overridden by tenant theme */
:root {
--color-primary: #1a73e8;
--color-secondary: #5f6368;
--color-accent: #ff6b35;
--color-bg: #ffffff;
--color-surface: #f8f9fa;
--color-text: #202124;
--color-text-secondary: #5f6368;
--color-border: #dadce0;
--color-success: #34a853;
--color-warning: #f9ab00;
--color-error: #ea4335;
--font-display: 'Sora', sans-serif;
--font-body: 'Plus Jakarta Sans', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--radius: 8px;
--shadow: 0 1px 3px rgba(0,0,0,0.12);
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body);
background: var(--color-bg);
color: var(--color-text);
line-height: 1.6;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
border: 1px solid var(--color-border);
border-radius: var(--radius);
font-family: var(--font-body);
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
background: var(--color-surface);
color: var(--color-text);
}
.btn:hover { background: var(--color-border); }
.btn--primary { background: var(--color-primary); color: white; border-color: var(--color-primary); }
.btn--primary:hover { opacity: 0.9; }
.btn--accent { background: var(--color-accent); color: white; border-color: var(--color-accent); }
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: 24px;
}
/* Catalog grid */
.catalog-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; }
.catalog-card { cursor: pointer; transition: all 0.2s; }
.catalog-card:hover { border-color: var(--color-primary); transform: translateY(-2px); box-shadow: var(--shadow); }
.stock-badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; font-weight: 600; }
.stock-badge--ok { background: #dcfce7; color: #166534; }
.stock-badge--low { background: #fef9c3; color: #854d0e; }
.stock-badge--zero { background: #fecaca; color: #991b1b; }
/* Cart sidebar */
.cart-sidebar { position: fixed; right: 0; top: 0; bottom: 0; width: 360px; background: var(--color-surface); border-left: 1px solid var(--color-border); padding: 20px; overflow-y: auto; transform: translateX(100%); transition: transform 0.3s; z-index: 50; }
.cart-sidebar.open { transform: translateX(0); }
.cart-item { display: flex; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--color-border); }
.cart-total { font-family: var(--font-mono); font-size: 1.3rem; font-weight: 700; }
/* Search bar */
.search-bar { display: flex; gap: 8px; margin-bottom: 20px; }
.search-bar input { flex: 1; padding: 10px 16px; border: 1px solid var(--color-border); border-radius: var(--radius); font-size: 1rem; }
.search-bar input:focus { outline: none; border-color: var(--color-primary); }
/* Filter chips */
.filter-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
.chip { padding: 4px 12px; border-radius: 20px; border: 1px solid var(--color-border); font-size: 0.8rem; cursor: pointer; background: transparent; }
.chip.active { background: var(--color-primary); color: white; border-color: var(--color-primary); }
/* External availability */
.external-results { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: var(--radius); padding: 16px; margin-top: 12px; }

428
pos/static/css/onboarding.min.css vendored Normal file
View File

@@ -0,0 +1,428 @@
/* ==========================================================================
NEXUS POS — Onboarding Wizard
Uses design system tokens (works with both industrial + modern themes)
========================================================================== */
/* Overlay backdrop */
.onboarding-overlay {
position: fixed;
inset: 0;
z-index: var(--z-modal);
display: flex;
align-items: center;
justify-content: center;
background: var(--overlay-backdrop);
opacity: 0;
animation: onb-fade-in var(--duration-normal) var(--ease-out) forwards;
}
@keyframes onb-fade-in {
to { opacity: 1; }
}
/* Modal card */
.onboarding-modal {
width: 92vw;
max-width: 500px;
max-height: 90vh;
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-xl);
display: flex;
flex-direction: column;
overflow: hidden;
transform: translateY(20px);
animation: onb-slide-up var(--duration-normal) var(--ease-out) forwards;
}
[data-theme="industrial"] .onboarding-modal {
border-radius: 0;
clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 0 100%);
}
[data-theme="modern"] .onboarding-modal {
border-radius: var(--radius-lg);
}
@keyframes onb-slide-up {
to { transform: translateY(0); }
}
/* Step content area */
.onboarding-body {
padding: var(--space-8) var(--space-6);
overflow-y: auto;
flex: 1;
}
/* Step icon */
.onb-icon {
width: 64px;
height: 64px;
margin: 0 auto var(--space-5);
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
background: var(--color-primary-muted);
color: var(--color-primary);
}
[data-theme="industrial"] .onb-icon {
border-radius: 0;
clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);
}
[data-theme="modern"] .onb-icon {
border-radius: var(--radius-lg);
}
/* Titles */
.onb-title {
font-family: var(--font-heading);
font-weight: var(--heading-weight-primary);
font-size: var(--text-h4);
color: var(--color-text-primary);
text-align: center;
margin-bottom: var(--space-2);
letter-spacing: var(--tracking-snug);
}
[data-theme="industrial"] .onb-title {
text-transform: uppercase;
}
.onb-desc {
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
text-align: center;
line-height: var(--leading-body-sm);
margin-bottom: var(--space-6);
}
/* Form fields inside wizard */
.onb-form {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.onb-field {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.onb-label {
font-size: var(--text-label);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
letter-spacing: var(--tracking-wide);
}
[data-theme="industrial"] .onb-label {
text-transform: uppercase;
font-size: var(--text-caption);
letter-spacing: var(--tracking-wider);
}
.onb-input {
height: 40px;
padding: 0 var(--space-3);
background: var(--color-bg-base);
border: 1px solid var(--color-border);
color: var(--color-text-primary);
font-family: var(--font-body);
font-size: var(--text-body-sm);
transition: var(--transition-fast);
outline: none;
}
[data-theme="industrial"] .onb-input {
border-radius: 0;
}
[data-theme="modern"] .onb-input {
border-radius: var(--radius-md);
}
.onb-input:focus {
border-color: var(--color-border-focus);
box-shadow: var(--shadow-focus);
}
.onb-input::placeholder {
color: var(--color-text-muted);
}
.onb-select {
height: 40px;
padding: 0 var(--space-3);
background: var(--color-bg-base);
border: 1px solid var(--color-border);
color: var(--color-text-primary);
font-family: var(--font-body);
font-size: var(--text-body-sm);
transition: var(--transition-fast);
outline: none;
cursor: pointer;
}
[data-theme="industrial"] .onb-select {
border-radius: 0;
}
[data-theme="modern"] .onb-select {
border-radius: var(--radius-md);
}
.onb-select:focus {
border-color: var(--color-border-focus);
box-shadow: var(--shadow-focus);
}
/* Inline error */
.onb-error {
font-size: var(--text-caption);
color: var(--color-error);
min-height: 18px;
margin-top: var(--space-1);
}
/* Success message in step */
.onb-success {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3);
background: var(--color-success-light);
color: var(--color-success-dark);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
margin-top: var(--space-3);
}
[data-theme="industrial"] .onb-success {
border-radius: 0;
}
[data-theme="modern"] .onb-success {
border-radius: var(--radius-md);
}
/* Footer: buttons + progress */
.onboarding-footer {
padding: var(--space-4) var(--space-6);
border-top: 1px solid var(--color-border);
display: flex;
flex-direction: column;
gap: var(--space-4);
background: var(--color-bg-elevated);
}
.onb-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
}
/* Buttons */
.onb-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: 0 var(--space-5);
height: 40px;
font-family: var(--font-body);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
border: 1px solid transparent;
cursor: pointer;
transition: var(--transition-fast);
white-space: nowrap;
letter-spacing: var(--tracking-wide);
}
[data-theme="industrial"] .onb-btn {
border-radius: 0;
clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%);
text-transform: uppercase;
}
[data-theme="modern"] .onb-btn {
border-radius: var(--radius-md);
}
.onb-btn--primary {
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
}
.onb-btn--primary:hover {
background: var(--btn-primary-bg-hover);
}
.onb-btn--primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.onb-btn--secondary {
background: var(--btn-secondary-bg);
color: var(--btn-secondary-text);
border-color: var(--btn-secondary-border);
}
.onb-btn--secondary:hover {
background: var(--btn-secondary-bg-hover);
}
.onb-btn--ghost {
background: transparent;
color: var(--color-text-muted);
border-color: transparent;
}
.onb-btn--ghost:hover {
color: var(--color-text-primary);
}
/* Progress dots */
.onb-progress {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
}
.onb-dot {
width: 10px;
height: 10px;
border: 2px solid var(--color-border-strong);
background: transparent;
transition: var(--transition-normal);
}
[data-theme="industrial"] .onb-dot {
border-radius: 0;
}
[data-theme="modern"] .onb-dot {
border-radius: var(--radius-full);
}
.onb-dot.is-active {
background: var(--color-primary);
border-color: var(--color-primary);
transform: scale(1.2);
}
.onb-dot.is-done {
background: var(--color-primary-muted);
border-color: var(--color-primary);
}
/* Step counter label */
.onb-step-label {
font-size: var(--text-caption);
color: var(--color-text-muted);
text-align: center;
letter-spacing: var(--tracking-wide);
}
/* "No mostrar de nuevo" checkbox row */
.onb-dismiss-row {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding-top: var(--space-2);
}
.onb-dismiss-row label {
font-size: var(--text-caption);
color: var(--color-text-muted);
cursor: pointer;
}
.onb-dismiss-row input[type="checkbox"] {
accent-color: var(--color-primary);
cursor: pointer;
}
/* Final step link grid */
.onb-links {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-3);
margin-top: var(--space-4);
}
.onb-link-card {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
padding: var(--space-4) var(--space-3);
background: var(--color-bg-base);
border: 1px solid var(--color-border);
text-decoration: none;
color: var(--color-text-primary);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
transition: var(--transition-fast);
}
[data-theme="industrial"] .onb-link-card {
border-radius: 0;
}
[data-theme="modern"] .onb-link-card {
border-radius: var(--radius-md);
}
.onb-link-card:hover {
border-color: var(--color-primary);
color: var(--color-primary);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.onb-link-card span.onb-link-icon {
font-size: 1.5rem;
}
/* Step transition */
.onb-step-enter {
animation: onb-step-in var(--duration-normal) var(--ease-out) forwards;
}
@keyframes onb-step-in {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Responsive */
@media (max-width: 480px) {
.onboarding-modal {
width: 96vw;
max-height: 95vh;
}
.onboarding-body {
padding: var(--space-6) var(--space-4);
}
.onboarding-footer {
padding: var(--space-3) var(--space-4);
}
.onb-links {
grid-template-columns: 1fr;
}
}

683
pos/static/css/pos-glass.min.css vendored Normal file
View File

@@ -0,0 +1,683 @@
/* ==========================================================================
POS-GLASS.CSS — Pixel-Perfect glassmorphism overlay for Nexus POS
Load AFTER tokens.css. Applies glass effects, glow, 3D buttons,
and animations to all POS pages without modifying inline styles.
========================================================================== */
/* ── Hidden scrollbar (global) ── */
html { scrollbar-width: none; }
html::-webkit-scrollbar { width: 0; }
/* ── Smooth font rendering ── */
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ==========================================================================
SIDEBAR — Glass treatment
========================================================================== */
.sidebar,
.pos-sidebar {
background: var(--glass-bg-strong) !important;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-right: 1px solid var(--glass-border) !important;
}
.sidebar__logo {
position: relative;
}
.sidebar__logo-text {
position: relative;
}
/* Glow under logo text */
.sidebar__logo-text::after {
content: '';
position: absolute;
bottom: -4px;
left: 0;
right: 0;
height: 2px;
background: var(--gradient-accent);
border-radius: 1px;
opacity: 0.4;
filter: blur(2px);
}
/* Nav items — hover glow */
.sidebar__nav a,
.sidebar__nav-item,
.sidebar .nav-item {
transition: all 0.25s var(--ease-out) !important;
border-radius: var(--radius-md);
}
.sidebar__nav a:hover,
.sidebar__nav-item:hover,
.sidebar .nav-item:hover {
box-shadow: 0 0 12px var(--glow-color-soft);
}
.sidebar__nav a.active,
.sidebar__nav-item.active,
.sidebar .nav-item.active {
box-shadow: 0 0 16px var(--glow-color-soft), inset 0 0 0 1px var(--glass-border);
}
/* ==========================================================================
THEME BAR — Glass
========================================================================== */
.theme-bar {
background: var(--glass-bg-strong) !important;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid var(--glass-border) !important;
}
/* ==========================================================================
CARDS — Glass with glow hover
========================================================================== */
.kpi-card,
.table-card,
.card,
.stat-card,
.chart-card,
.alert-card,
.config-card,
.fleet-card,
.report-card,
.invoice-card,
.customer-card,
.panel {
background: var(--glass-bg) !important;
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border) !important;
transition: all 0.3s var(--ease-out) !important;
position: relative;
overflow: hidden;
}
/* Accent top-line on hover */
.kpi-card::before,
.table-card::before,
.chart-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--gradient-accent);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.4s var(--ease-out);
z-index: 1;
}
.kpi-card:hover::before,
.table-card:hover::before,
.chart-card:hover::before {
transform: scaleX(1);
}
.kpi-card:hover,
.table-card:hover,
.card:hover,
.stat-card:hover,
.chart-card:hover,
.config-card:hover,
.fleet-card:hover,
.report-card:hover {
border-color: var(--color-border-accent) !important;
box-shadow: 0 4px 20px var(--glow-color-soft);
}
/* KPI card accent bar — add glow */
.kpi-card__accent-bar {
box-shadow: 0 0 8px var(--glow-color-soft);
}
/* ==========================================================================
BUTTONS — 3D depth effect
========================================================================== */
/* Primary buttons */
.btn--primary,
button.primary,
.btn-primary,
input[type="submit"],
button[type="submit"] {
background: var(--gradient-accent) !important;
border: none !important;
box-shadow: 0 3px 0 var(--color-primary-active),
0 4px 10px var(--glow-color-soft) !important;
transition: all 0.25s var(--ease-out) !important;
position: relative;
overflow: hidden;
}
.btn--primary:hover,
button.primary:hover,
.btn-primary:hover,
input[type="submit"]:hover,
button[type="submit"]:hover {
transform: translateY(-1px);
box-shadow: 0 4px 0 var(--color-primary-active),
0 8px 20px var(--glow-color) !important;
}
.btn--primary:active,
button.primary:active,
.btn-primary:active,
input[type="submit"]:active,
button[type="submit"]:active {
transform: translateY(1px);
box-shadow: 0 1px 0 var(--color-primary-active) !important;
}
/* Ghost / secondary buttons — glass */
.btn--ghost,
.btn--secondary,
.btn-secondary,
.btn-ghost,
button.secondary {
background: var(--glass-bg) !important;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid var(--glass-border) !important;
transition: all 0.25s var(--ease-out) !important;
}
.btn--ghost:hover,
.btn--secondary:hover,
.btn-secondary:hover,
.btn-ghost:hover,
button.secondary:hover {
border-color: var(--color-border-accent) !important;
box-shadow: 0 0 16px var(--glow-color-soft);
}
/* ==========================================================================
INPUTS — Glass with focus glow
========================================================================== */
input[type="text"],
input[type="number"],
input[type="email"],
input[type="password"],
input[type="search"],
input[type="tel"],
input[type="date"],
input[type="url"],
textarea,
select,
.search-input,
.filter-input {
background: var(--glass-bg) !important;
border: 1px solid var(--glass-border) !important;
transition: all 0.25s var(--ease-out) !important;
}
input[type="text"]:focus,
input[type="number"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
input[type="search"]:focus,
input[type="tel"]:focus,
input[type="date"]:focus,
input[type="url"]:focus,
textarea:focus,
select:focus,
.search-input:focus,
.filter-input:focus {
border-color: var(--color-border-focus) !important;
box-shadow: 0 0 0 3px var(--glow-color-soft), 0 0 16px var(--glow-color-soft) !important;
outline: none;
}
/* ==========================================================================
TABLES — Subtle glass rows
========================================================================== */
table thead th {
background: var(--glass-bg) !important;
backdrop-filter: blur(8px);
font-family: var(--font-mono);
font-size: var(--text-caption);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
}
table tbody tr {
transition: all 0.2s ease !important;
}
table tbody tr:hover {
background: var(--glass-highlight) !important;
box-shadow: inset 0 0 0 1px var(--glass-border);
}
/* ==========================================================================
MODALS — Glass overlay + glass content
========================================================================== */
.modal-overlay,
.overlay,
.modal-backdrop {
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.modal,
.modal-content,
.modal-dialog,
.dialog {
background: var(--glass-bg-strong) !important;
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid var(--glass-border) !important;
box-shadow: 0 24px 48px rgba(0,0,0,0.3) !important;
}
/* ==========================================================================
TABS — Glass active state
========================================================================== */
.tab,
.tab-btn,
.tabs button {
transition: all 0.25s var(--ease-out) !important;
border-radius: var(--radius-md);
}
.tab.active,
.tab-btn.active,
.tabs button.active {
background: var(--color-primary-muted) !important;
box-shadow: 0 0 12px var(--glow-color-soft);
border-color: var(--color-border-accent) !important;
}
/* ==========================================================================
BADGES / TAGS — Subtle glow
========================================================================== */
.badge,
.tag,
.status-badge,
.pill {
backdrop-filter: blur(4px);
transition: all 0.2s ease;
}
/* ==========================================================================
SCROLL REVEAL — Available for any POS page that wants it
========================================================================== */
.nx-reveal {
opacity: 0;
transform: translateY(24px);
transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out);
}
.nx-reveal.is-visible {
opacity: 1;
transform: translateY(0);
}
.nx-stagger > .nx-reveal:nth-child(1) { transition-delay: 0ms; }
.nx-stagger > .nx-reveal:nth-child(2) { transition-delay: 80ms; }
.nx-stagger > .nx-reveal:nth-child(3) { transition-delay: 160ms; }
.nx-stagger > .nx-reveal:nth-child(4) { transition-delay: 240ms; }
.nx-stagger > .nx-reveal:nth-child(5) { transition-delay: 320ms; }
.nx-stagger > .nx-reveal:nth-child(6) { transition-delay: 400ms; }
/* ==========================================================================
TOAST / NOTIFICATIONS — Glass
========================================================================== */
.toast,
.notification,
.snackbar,
.alert {
background: var(--glass-bg-strong) !important;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border) !important;
}
/* ==========================================================================
DROPDOWN / POPOVER — Glass
========================================================================== */
.dropdown-menu,
.popover,
.autocomplete-list,
.suggestion-list {
background: var(--glass-bg-strong) !important;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border) !important;
box-shadow: 0 8px 32px rgba(0,0,0,0.2) !important;
}
/* ==========================================================================
STATUS BAR (POS) — Glass
========================================================================== */
.status-bar,
.pos-status-bar {
background: var(--glass-bg-strong) !important;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid var(--glass-border) !important;
}
/* ==========================================================================
LOADING SPINNER — Glow animation
========================================================================== */
.spinner,
.loading-spinner {
animation: nx-glow-pulse 1.5s ease-in-out infinite;
}
/* ==========================================================================
ANIMATIONS — Available keyframes
========================================================================== */
@keyframes pos-fade-in {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
/* Apply subtle entry animation to main content area */
.content,
.main-content,
main {
animation: pos-fade-in 0.4s var(--ease-out) both;
}
/* ==========================================================================
DASHED BORDER ACCENTS (Pixel-Perfect style)
========================================================================== */
.section-divider,
hr {
border: none;
border-top: 1px dashed var(--glass-border);
margin: var(--space-4) 0;
}
/* ==========================================================================
TABLET RESPONSIVE — Adaptive layout for 768px-1024px screens
Applied globally to all POS pages via pos-glass.css.
Targets iPad (768×1024), Android tablets (800×1280), and similar.
========================================================================== */
/* ── Tablet portrait (768-1023px) — sidebar collapses, grids reflow ── */
@media (max-width: 1023px) {
/* Sidebar collapses to an overlay drawer */
.sidebar,
.pos-sidebar {
position: fixed !important;
top: 0 !important;
left: 0 !important;
bottom: 0 !important;
z-index: var(--z-modal) !important;
transform: translateX(-100%) !important;
transition: transform 0.3s var(--ease-out) !important;
width: 260px !important;
}
.sidebar.open,
.pos-sidebar.open {
transform: translateX(0) !important;
box-shadow: 0 0 40px rgba(0,0,0,0.3) !important;
}
.sidebar-overlay {
display: none !important;
position: fixed !important;
inset: 0 !important;
z-index: calc(var(--z-modal) - 1) !important;
background: rgba(0,0,0,0.5) !important;
}
.sidebar-overlay.open {
display: block !important;
}
/* App shell: full width when sidebar is hidden */
.app-shell {
flex-direction: column !important;
}
.app-shell > main,
.app-shell > .main-content,
.app-shell > .content,
.main-content,
.content {
margin-left: 0 !important;
width: 100% !important;
}
/* Show hamburger button */
.hamburger-btn {
display: flex !important;
}
/* Touch-friendly targets — minimum 44px tap area */
button,
.btn,
.nav-card,
.tab-btn,
.tab,
.part-card,
.search-result-item,
table tbody tr,
.kpi-card {
min-height: 44px;
}
/* Larger text for readability on tablets */
.kpi-card__value {
font-size: 1.5rem !important;
}
/* Grid reflow: 2 columns instead of 3-4 */
.kpi-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
.nav-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
/* Tables: horizontal scroll wrapper on narrow screens */
.table-wrap,
.table-card {
overflow-x: auto !important;
-webkit-overflow-scrolling: touch;
}
/* POS-specific: if the POS has a side panel (cart), stack vertically */
.pos-layout {
flex-direction: column !important;
}
.pos-layout .pos-cart,
.pos-layout .cart-panel {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
max-height: 40vh !important;
}
/* Content headers: tighter padding */
.content-header,
.header,
.page-header {
padding: var(--space-3) var(--space-4) !important;
}
/* Search bar: full width */
.search-bar,
.search-wrapper {
width: 100% !important;
max-width: 100% !important;
}
/* Mode toggle: slightly larger buttons for touch */
.mode-toggle button {
padding: 6px 14px !important;
font-size: 12px !important;
}
/* Vehicle selector dropdowns: stack on smaller tablets */
.vehicle-selector__inner,
.vehicle-selector .vs-group {
flex-wrap: wrap !important;
}
.vehicle-selector .vs-arrow {
display: none !important;
}
.vehicle-selector .vs-select {
min-width: 130px !important;
}
}
/* ── Phone portrait (< 768px) — single column, max simplification ── */
@media (max-width: 767px) {
.sidebar {
width: 85vw !important;
max-width: 300px !important;
}
.kpi-grid,
.nav-grid,
.results-grid {
grid-template-columns: 1fr !important;
}
.kpi-card__value {
font-size: 1.3rem !important;
}
/* Stack the mode toggle buttons vertically if tight */
.mode-toggle {
flex-wrap: wrap !important;
}
/* Hide non-essential UI to save space */
.header__store-badge,
.vs-vin-divider {
display: none !important;
}
/* Full-width modals */
.modal-content {
max-width: 95vw !important;
margin: var(--space-3) !important;
padding: var(--space-4) !important;
}
/* Tables: force readable font size */
table {
font-size: 12px !important;
}
table th,
table td {
padding: var(--space-2) var(--space-2) !important;
}
}
/* ── Landscape tablet (height < 600px with wide screen) ── */
@media (max-height: 600px) and (min-width: 768px) {
/* Reduce vertical padding for landscape tablet use */
.kpi-grid {
gap: var(--space-2) !important;
}
.dashboard,
.main-content,
.content {
padding: var(--space-3) !important;
}
}
/* ── Touch device hints ── */
@media (hover: none) and (pointer: coarse) {
/* Remove hover-only effects on touch devices — they cause sticky states */
.kpi-card:hover,
.nav-card:hover,
.part-card:hover,
.table-card:hover,
.card:hover {
transform: none !important;
}
/* Larger touch targets for interactive elements */
.sidebar__nav a,
.sidebar__nav-item,
.sidebar .nav-item {
padding: 12px 16px !important;
min-height: 48px !important;
display: flex !important;
align-items: center !important;
}
/* Scroll momentum on iOS */
.table-wrap,
.main-content,
.content,
.parts-grid,
.nav-grid {
-webkit-overflow-scrolling: touch;
}
/* Disable text selection on buttons (prevents accidental blue highlight on long tap) */
button,
.btn,
.nav-card,
.tab-btn {
-webkit-user-select: none;
user-select: none;
}
}
/* ==========================================================================
PRINT — Disable glass effects for printing
========================================================================== */
@media print {
.sidebar,
.theme-bar,
.kpi-card,
.table-card,
.card,
.modal,
.modal-content,
table thead th,
input,
select,
textarea {
background: #fff !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
box-shadow: none !important;
border-color: #ccc !important;
color: #000 !important;
}
}

627
pos/static/css/tokens.min.css vendored Normal file
View File

@@ -0,0 +1,627 @@
/* ==========================================================================
NEXUS AUTOPARTS — Design Tokens
POS System for Auto Parts Stores
Version: 1.0.0
==========================================================================
Themes:
- [data-theme="industrial"] — Industrial Robusto (Dark)
- [data-theme="modern"] — Técnico Moderno (Light)
========================================================================== */
/* --------------------------------------------------------------------------
GOOGLE FONTS IMPORTS
-------------------------------------------------------------------------- */
@import url('https://fonts.googleapis.com/css2?family=Barlow:wght@400;700&family=Barlow+Condensed:wght@600;800&family=Poppins:wght@300;400;600;700&display=swap');
/* ==========================================================================
GLOBAL TOKENS — Theme-independent, shared across both themes
========================================================================== */
:root {
/* ------------------------------------------------------------------------
SEMANTIC COLORS — Status / Feedback (shared)
------------------------------------------------------------------------ */
--color-success: #22c55e;
--color-success-light: #bbf7d0;
--color-success-dark: #15803d;
--color-warning: #eab308;
--color-warning-light: #fef08a;
--color-warning-dark: #a16207;
--color-error: #ef4444;
--color-error-light: #fecaca;
--color-error-dark: #b91c1c;
/* ------------------------------------------------------------------------
NEUTRAL SCALE — Grey ramp (50900)
------------------------------------------------------------------------ */
--color-neutral-50: #fafafa;
--color-neutral-100: #f5f5f5;
--color-neutral-200: #e5e5e5;
--color-neutral-300: #d4d4d4;
--color-neutral-400: #a3a3a3;
--color-neutral-500: #737373;
--color-neutral-600: #525252;
--color-neutral-700: #404040;
--color-neutral-800: #262626;
--color-neutral-900: #171717;
/* ------------------------------------------------------------------------
SPACING — 4px base grid
------------------------------------------------------------------------ */
/* --space-N = N × 4px */
--space-1: 4px; /* 4px */
--space-2: 8px; /* 8px */
--space-3: 12px; /* 12px */
--space-4: 16px; /* 16px */
--space-5: 20px; /* 20px */
--space-6: 24px; /* 24px */
--space-7: 28px; /* 28px */
--space-8: 32px; /* 32px */
--space-9: 36px; /* 36px */
--space-10: 40px; /* 40px */
--space-11: 44px; /* 44px */
--space-12: 48px; /* 48px */
--space-14: 56px; /* 56px */
--space-16: 64px; /* 64px */
/* ------------------------------------------------------------------------
BORDER RADIUS
------------------------------------------------------------------------ */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 20px;
--radius-full: 9999px;
/* ------------------------------------------------------------------------
TRANSITIONS
------------------------------------------------------------------------ */
--transition-fast: all 0.10s ease;
--transition-normal: all 0.20s ease;
--transition-slow: all 0.40s ease;
/* Easing functions for fine-grained control */
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--duration-fast: 100ms;
--duration-normal: 200ms;
--duration-slow: 400ms;
/* ------------------------------------------------------------------------
Z-INDEX SCALE
------------------------------------------------------------------------ */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-modal: 1050;
--z-toast: 1080;
/* ------------------------------------------------------------------------
BREAKPOINTS — Reference only (use in media queries, not calc())
sm: 640px
md: 768px
lg: 1024px
xl: 1280px
------------------------------------------------------------------------ */
}
/* ==========================================================================
THEME A — Industrial Robusto (Dark)
Usage: <html data-theme="industrial"> or <body data-theme="industrial">
Style: Industrial, robust, high-contrast amber accents, clip-path diagonals
========================================================================== */
[data-theme="industrial"] {
/* ------------------------------------------------------------------------
PRIMITIVE COLORS
------------------------------------------------------------------------ */
--color-primary: #F5A623; /* Amber gold — main brand accent */
--color-primary-hover: #e8951a; /* Darker amber on hover */
--color-primary-active: #d4850f; /* Pressed state */
--color-primary-muted: rgba(245, 166, 35, 0.15); /* Subtle tint */
--color-secondary: #333333; /* Mid-dark border / secondary bg */
--color-secondary-hover: #444444;
--color-accent: #F5A623; /* Same as primary in this theme */
/* ------------------------------------------------------------------------
BACKGROUNDS
------------------------------------------------------------------------ */
--color-bg-base: #0d0d0d; /* Page / app shell background */
--color-bg-elevated: #1a1a1a; /* Cards, panels, sidebars */
--color-bg-overlay: #252525; /* Modals, dropdowns, tooltips */
/* Surface levels (for layered UI) */
--color-surface-1: #1a1a1a; /* Lowest raised surface */
--color-surface-2: #252525; /* Mid-level surface */
--color-surface-3: #303030; /* Highest raised surface */
/* ------------------------------------------------------------------------
TEXT
------------------------------------------------------------------------ */
--color-text-primary: #FFFFFF;
--color-text-secondary: #CCCCCC;
--color-text-muted: #888888;
--color-text-disabled: #555555;
--color-text-inverse: #000000; /* Text on amber background */
--color-text-accent: #F5A623;
/* ------------------------------------------------------------------------
BORDERS
------------------------------------------------------------------------ */
--color-border: #333333;
--color-border-strong: #555555;
--color-border-accent: #F5A623;
--color-border-focus: #F5A623;
/* ------------------------------------------------------------------------
BUTTONS
------------------------------------------------------------------------ */
/* Primary button */
--btn-primary-bg: #F5A623;
--btn-primary-bg-hover: #e8951a;
--btn-primary-bg-active: #d4850f;
--btn-primary-text: #000000;
--btn-primary-border: transparent;
/* Secondary button */
--btn-secondary-bg: transparent;
--btn-secondary-bg-hover: rgba(245, 166, 35, 0.10);
--btn-secondary-text: #F5A623;
--btn-secondary-border: #F5A623;
/* Ghost / Danger */
--btn-ghost-bg: transparent;
--btn-ghost-text: #CCCCCC;
--btn-ghost-border: #333333;
--btn-danger-bg: #ef4444;
--btn-danger-text: #FFFFFF;
/* ------------------------------------------------------------------------
TYPOGRAPHY
------------------------------------------------------------------------ */
/* Font families */
--font-heading: 'Barlow Condensed', 'Arial Narrow', sans-serif;
--font-body: 'Barlow', 'Arial', sans-serif;
--font-mono: 'Courier New', 'Consolas', monospace; /* prices / SKUs */
/* Font weights */
--font-weight-light: 300; /* n/a in Barlow — falls to 400 */
--font-weight-regular: 400;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-extrabold: 800;
/* Heading weights (Barlow Condensed) */
--heading-weight-primary: 800;
--heading-weight-secondary: 600;
/* ------------------------------------------------------------------------
SHADOWS / ELEVATION
Tinted with amber to feel cohesive with the theme
------------------------------------------------------------------------ */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.60),
0 1px 2px rgba(0, 0, 0, 0.40);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.60),
0 2px 4px rgba(0, 0, 0, 0.40);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.70),
0 4px 6px rgba(0, 0, 0, 0.50);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.80),
0 10px 10px rgba(0, 0, 0, 0.50);
/* Accent glow — use on focused/highlighted elements */
--shadow-accent: 0 0 0 3px rgba(245, 166, 35, 0.40);
--shadow-focus: 0 0 0 3px rgba(245, 166, 35, 0.50);
/* ------------------------------------------------------------------------
MISC UI
------------------------------------------------------------------------ */
--scrollbar-track: #1a1a1a;
--scrollbar-thumb: #444444;
--scrollbar-thumb-hover: #F5A623;
--overlay-backdrop: rgba(0, 0, 0, 0.75);
/* Industrial clip-path angle (use in clip-path: polygon(...) utilities) */
--clip-diagonal-angle: 6deg;
}
/* ==========================================================================
THEME B — Técnico Moderno (Light)
Usage: <html data-theme="modern"> or <body data-theme="modern">
Style: Clean, modern, Poppins typography, subtle dot-grid background
========================================================================== */
[data-theme="modern"] {
/* ------------------------------------------------------------------------
PRIMITIVE COLORS
------------------------------------------------------------------------ */
--color-primary: #FF6B35; /* Orange — main brand accent */
--color-primary-hover: #f05a22; /* Darker on hover */
--color-primary-active: #dc4a12; /* Pressed state */
--color-primary-muted: rgba(255, 107, 53, 0.10); /* Subtle tint */
--color-secondary: #1a1a2e; /* Deep navy — used for strong text */
--color-secondary-hover: #252545;
--color-accent: #FF6B35; /* Same as primary in this theme */
/* ------------------------------------------------------------------------
BACKGROUNDS
------------------------------------------------------------------------ */
--color-bg-base: #FFFFFF; /* Page / app shell background */
--color-bg-elevated: #F8F9FF; /* Cards, panels — very subtle blue */
--color-bg-overlay: #FFFFFF; /* Modals, dropdowns */
/* Surface levels */
--color-surface-1: #F8F9FF;
--color-surface-2: #F0F2FF;
--color-surface-3: #E8EBFF;
/* Dot-grid background pattern (apply via background-image on body/shell) */
/* background-image: radial-gradient(circle, var(--dot-grid-color) 1px, transparent 1px); */
/* background-size: var(--dot-grid-size) var(--dot-grid-size); */
--dot-grid-color: rgba(26, 26, 46, 0.07);
--dot-grid-size: 24px;
/* ------------------------------------------------------------------------
TEXT
------------------------------------------------------------------------ */
--color-text-primary: #1a1a2e;
--color-text-secondary: #4a4a6a;
--color-text-muted: #8080a0;
--color-text-disabled: #b0b0c8;
--color-text-inverse: #FFFFFF; /* Text on orange background */
--color-text-accent: #FF6B35;
/* ------------------------------------------------------------------------
BORDERS
------------------------------------------------------------------------ */
--color-border: #e2e4f0;
--color-border-strong: #c8cadc;
--color-border-accent: #FF6B35;
--color-border-focus: #FF6B35;
/* ------------------------------------------------------------------------
BUTTONS
------------------------------------------------------------------------ */
/* Primary button */
--btn-primary-bg: #FF6B35;
--btn-primary-bg-hover: #f05a22;
--btn-primary-bg-active: #dc4a12;
--btn-primary-text: #FFFFFF;
--btn-primary-border: transparent;
/* Secondary button */
--btn-secondary-bg: transparent;
--btn-secondary-bg-hover: rgba(255, 107, 53, 0.08);
--btn-secondary-text: #FF6B35;
--btn-secondary-border: #FF6B35;
/* Ghost / Danger */
--btn-ghost-bg: transparent;
--btn-ghost-text: #4a4a6a;
--btn-ghost-border: #e2e4f0;
--btn-danger-bg: #ef4444;
--btn-danger-text: #FFFFFF;
/* ------------------------------------------------------------------------
TYPOGRAPHY
------------------------------------------------------------------------ */
/* Font families */
--font-heading: 'Poppins', 'Segoe UI', sans-serif;
--font-body: 'Poppins', 'Segoe UI', sans-serif;
--font-mono: 'Courier New', 'Consolas', monospace; /* prices / SKUs */
/* Font weights */
--font-weight-light: 300;
--font-weight-regular: 400;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-extrabold: 800; /* falls to 700 in Poppins */
/* Heading weights (Poppins) */
--heading-weight-primary: 700;
--heading-weight-secondary: 600;
/* ------------------------------------------------------------------------
SHADOWS / ELEVATION
Softer, cooler tints for the light theme
------------------------------------------------------------------------ */
--shadow-sm: 0 1px 3px rgba(26, 26, 46, 0.08),
0 1px 2px rgba(26, 26, 46, 0.05);
--shadow-md: 0 4px 6px rgba(26, 26, 46, 0.08),
0 2px 4px rgba(26, 26, 46, 0.05);
--shadow-lg: 0 10px 15px rgba(26, 26, 46, 0.10),
0 4px 6px rgba(26, 26, 46, 0.06);
--shadow-xl: 0 20px 25px rgba(26, 26, 46, 0.12),
0 10px 10px rgba(26, 26, 46, 0.06);
/* Accent glow — use on focused/highlighted elements */
--shadow-accent: 0 0 0 3px rgba(255, 107, 53, 0.25);
--shadow-focus: 0 0 0 3px rgba(255, 107, 53, 0.30);
/* ------------------------------------------------------------------------
MISC UI
------------------------------------------------------------------------ */
--scrollbar-track: #F8F9FF;
--scrollbar-thumb: #c8cadc;
--scrollbar-thumb-hover: #FF6B35;
--overlay-backdrop: rgba(26, 26, 46, 0.50);
/* No diagonal clip in modern theme — set to 0 for override-safe utilities */
--clip-diagonal-angle: 0deg;
}
/* ==========================================================================
TYPOGRAPHY SCALE — Token definitions
Resolved at theme level because font families differ between themes.
These tokens map to semantic roles and should be consumed directly.
========================================================================== */
/* Shared scale values (dimensionless, theme-independent) */
:root {
/* --- Type scale (font-size) --- */
--text-h1: clamp(2.25rem, 5vw, 3.5rem); /* 36px → 56px */
--text-h2: clamp(1.875rem, 4vw, 2.75rem); /* 30px → 44px */
--text-h3: clamp(1.5rem, 3vw, 2.125rem); /* 24px → 34px */
--text-h4: clamp(1.25rem, 2vw, 1.625rem); /* 20px → 26px */
--text-h5: 1.125rem; /* 18px */
--text-h6: 1rem; /* 16px */
--text-body-lg: 1.125rem; /* 18px */
--text-body: 1rem; /* 16px */
--text-body-sm: 0.875rem; /* 14px */
--text-caption: 0.75rem; /* 12px */
--text-label: 0.8125rem; /* 13px */
--text-mono: 1rem; /* 16px — prices, SKUs */
/* --- Line heights --- */
--leading-h1: 1.10;
--leading-h2: 1.12;
--leading-h3: 1.15;
--leading-h4: 1.20;
--leading-h5: 1.25;
--leading-h6: 1.30;
--leading-body-lg: 1.65;
--leading-body: 1.60;
--leading-body-sm: 1.55;
--leading-caption: 1.45;
--leading-label: 1.40;
--leading-mono: 1.50;
/* --- Letter spacing --- */
--tracking-tight: -0.03em;
--tracking-snug: -0.01em;
--tracking-normal: 0em;
--tracking-wide: 0.03em;
--tracking-wider: 0.06em;
--tracking-widest: 0.12em; /* Use for ALL-CAPS labels / badges */
}
/* Heading letter-spacing per theme */
[data-theme="industrial"] {
--heading-tracking-h1: -0.02em;
--heading-tracking-h2: -0.02em;
--heading-tracking-h3: -0.01em;
--heading-tracking-h4: 0em;
--heading-tracking-h5: 0.02em;
--heading-tracking-h6: 0.04em;
}
[data-theme="modern"] {
--heading-tracking-h1: -0.03em;
--heading-tracking-h2: -0.02em;
--heading-tracking-h3: -0.01em;
--heading-tracking-h4: 0em;
--heading-tracking-h5: 0em;
--heading-tracking-h6: 0.01em;
}
/* ==========================================================================
COMPONENT SHORTHAND TOKENS
Convenience aliases that combine multiple primitives. Components should
reference these rather than the primitives above.
========================================================================== */
:root {
/* --- Input / form fields --- */
/* These are intentionally left as CSS variable references so they resolve
correctly within whichever theme is active at runtime. */
/* (No :root overrides needed — components consume --color-* directly.) */
/* --- Focus ring --- */
--focus-ring: 0 0 0 3px var(--shadow-focus, rgba(245,166,35,0.40));
/* --- Content max widths --- */
--content-xs: 480px;
--content-sm: 640px;
--content-md: 768px;
--content-lg: 1024px;
--content-xl: 1280px;
--content-full: 100%;
}
/* ==========================================================================
UTILITY — Scrollbar styles (opt-in via class)
========================================================================== */
.themed-scrollbar {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
.themed-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.themed-scrollbar::-webkit-scrollbar-track {
background: var(--scrollbar-track);
}
.themed-scrollbar::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb);
border-radius: var(--radius-full);
border: 2px solid var(--scrollbar-track);
}
.themed-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: var(--scrollbar-thumb-hover);
}
/* ==========================================================================
UTILITY — Dot-grid background (Theme B helper)
Apply class .bg-dot-grid to body or layout shell when using modern theme.
========================================================================== */
[data-theme="modern"] .bg-dot-grid {
background-color: var(--color-bg-base);
background-image: radial-gradient(
circle,
var(--dot-grid-color) 1px,
transparent 1px
);
background-size: var(--dot-grid-size) var(--dot-grid-size);
}
/* ==========================================================================
UTILITY — Industrial diagonal clip helpers (Theme A)
========================================================================== */
[data-theme="industrial"] .clip-top-right {
clip-path: polygon(0 0, calc(100% - 24px) 0, 100% 24px, 100% 100%, 0 100%);
}
[data-theme="industrial"] .clip-bottom-left {
clip-path: polygon(0 0, 100% 0, 100% 100%, 24px 100%, 0 calc(100% - 24px));
}
[data-theme="industrial"] .clip-corner {
clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 0 100%);
}
/* ==========================================================================
GLASSMORPHISM TOKENS
========================================================================== */
[data-theme="industrial"] {
--glass-bg: rgba(26, 26, 26, 0.70);
--glass-bg-strong: rgba(26, 26, 26, 0.85);
--glass-border: rgba(255, 255, 255, 0.08);
--glass-blur: 16px;
--glass-highlight: rgba(245, 166, 35, 0.06);
--glow-color: rgba(245, 166, 35, 0.40);
--glow-color-soft: rgba(245, 166, 35, 0.15);
--glow-color-strong: rgba(245, 166, 35, 0.60);
--gradient-accent: linear-gradient(135deg, #F5A623 0%, #e8951a 50%, #d4850f 100%);
--gradient-text: linear-gradient(135deg, #F5A623 0%, #FFD080 50%, #F5A623 100%);
--canvas-grid-color: rgba(255, 255, 255, 0.06);
--canvas-star-color: rgba(245, 166, 35, 0.30);
--canvas-glow-color: rgba(245, 166, 35, 0.08);
}
[data-theme="modern"] {
--glass-bg: rgba(248, 249, 255, 0.70);
--glass-bg-strong: rgba(248, 249, 255, 0.85);
--glass-border: rgba(26, 26, 46, 0.08);
--glass-blur: 16px;
--glass-highlight: rgba(255, 107, 53, 0.04);
--glow-color: rgba(255, 107, 53, 0.35);
--glow-color-soft: rgba(255, 107, 53, 0.12);
--glow-color-strong: rgba(255, 107, 53, 0.55);
--gradient-accent: linear-gradient(135deg, #FF6B35 0%, #FF8F65 50%, #FF6B35 100%);
--gradient-text: linear-gradient(135deg, #FF6B35 0%, #FF8F65 50%, #e85520 100%);
--canvas-grid-color: rgba(26, 26, 46, 0.05);
--canvas-star-color: rgba(255, 107, 53, 0.20);
--canvas-glow-color: rgba(255, 107, 53, 0.06);
}
/* ==========================================================================
ANIMATION KEYFRAMES
========================================================================== */
@keyframes nx-fade-up {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes nx-glow-pulse {
0%, 100% { box-shadow: 0 0 20px var(--glow-color-soft); }
50% { box-shadow: 0 0 40px var(--glow-color); }
}
@keyframes nx-shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* ==========================================================================
END OF TOKENS FILE
nexus-autoparts-design/tokens/tokens.css
========================================================================== */

1
pos/static/js/accounting.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/app-init.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var e=localStorage.getItem("pos_token");if(e){try{var t=JSON.parse(atob(e.split(".")[1]));if(1e3*t.exp<Date.now())return localStorage.removeItem("pos_token"),localStorage.removeItem("pos_employee"),void(window.location.href="/pos/login")}catch(e){return localStorage.removeItem("pos_token"),void(window.location.href="/pos/login")}var o={};try{o=JSON.parse(localStorage.getItem("pos_employee")||"{}")}catch(e){}var n=o.name||t.name||"Usuario",a=o.role||t.role||"",r="function"==typeof window.t?window.t:function(e){return e},i={owner:r("role_owner"),admin:r("role_admin"),cashier:r("role_cashier"),warehouse:r("role_warehouse"),accountant:r("role_accountant")}[a]||a,c=n.split(" ").map((function(e){return e[0]})).join("").toUpperCase().substring(0,2);document.querySelectorAll(".sidebar__user-name").forEach((function(e){e.textContent=n})),document.querySelectorAll(".sidebar__user-role").forEach((function(e){e.textContent=i})),document.querySelectorAll(".sidebar__user-avatar, .sidebar__avatar").forEach((function(e){e.textContent=c})),document.querySelectorAll(".profile-info__name").forEach((function(e){e.textContent=n})),document.querySelectorAll(".profile-info__role").forEach((function(e){e.textContent=i})),document.querySelectorAll(".theme-bar__label").forEach((function(e){-1===e.textContent.indexOf("Usuario:")&&-1===e.textContent.indexOf("Sucursal")||(e.textContent="Sucursal Principal — "+n)})),document.querySelectorAll(".status-bar .user-name, .status-info span").forEach((function(e){var t=e.textContent;["Hugo M.","Hugo García","J. Ramírez","José Ramírez","Carlos M.","Admin"].forEach((function(o){-1!==t.indexOf(o)&&(e.textContent=t.replace(o,n))}))}));var l=window.location.pathname;document.querySelectorAll(".nav-item, .nav-link").forEach((function(e){e.classList.remove("is-active","active"),(e.getAttribute("href")||"")===l&&(e.classList.add("is-active"),e.classList.add("active"))})),window.posLogout=function(){localStorage.removeItem("pos_token"),localStorage.removeItem("pos_employee"),localStorage.removeItem("pos_tenant_id"),localStorage.removeItem("pos_cart"),window.location.href="/pos/login"},document.querySelectorAll('[data-action="logout"], .btn-logout, .logout-btn').forEach((function(e){e.addEventListener("click",(function(e){e.preventDefault(),posLogout()}))}));var s=localStorage.getItem("pos_theme");s||(s=window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"industrial":"modern"),document.documentElement.setAttribute("data-theme",s),document.querySelectorAll(".theme-bar").forEach((function(e){e.style.display="none"})),window.posSetTheme=function(e){document.documentElement.setAttribute("data-theme",e),localStorage.setItem("pos_theme",e)},window.setTheme=window.posSetTheme,window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",(function(e){if(!localStorage.getItem("pos_theme")){var t=e.matches?"industrial":"modern";document.documentElement.setAttribute("data-theme",t)}})),setTimeout((function(){document.documentElement.setAttribute("data-theme",s)}),100),window.POS_USER={name:n,role:a,roleLabel:i,initials:c,token:e,tenantId:t.tenant_id,employeeId:t.employee_id,branchId:t.branch_id,permissions:t.permissions||[]}}else window.location.href="/pos/login"}();

View File

@@ -893,7 +893,7 @@
} }
var imgHtml = p.image_url var imgHtml = p.image_url
? '<img src="' + esc(p.image_url) + '" alt="' + esc(p.name) + '">' ? '<img src="' + esc(p.image_url) + '" alt="' + esc(p.name) + '" loading="lazy" decoding="async">'
: '<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 7h8M8 12h8M8 17h5"/></svg>'; : '<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 7h8M8 12h8M8 17h5"/></svg>';
var tierClass = p.priority_tier === 1 ? ' part-card--tier1' : (p.priority_tier === 2 ? ' part-card--tier2' : ''); var tierClass = p.priority_tier === 1 ? ' part-card--tier1' : (p.priority_tier === 2 ? ' part-card--tier2' : '');
@@ -997,7 +997,7 @@
: ''; : '';
var imgHtml = p.image_url var imgHtml = p.image_url
? '<img src="' + esc(p.image_url) + '" alt="' + esc(p.name) + '">' ? '<img src="' + esc(p.image_url) + '" alt="' + esc(p.name) + '" loading="lazy" decoding="async">'
: '<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 7h8M8 12h8M8 17h5"/></svg>'; : '<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 7h8M8 12h8M8 17h5"/></svg>';
// Local-mode extras: manufacturer badge + priority tier indicator // Local-mode extras: manufacturer badge + priority tier indicator
@@ -1125,7 +1125,7 @@
html += '<div class="detail-oem">' + esc(p.oem_part_number) + '</div>'; html += '<div class="detail-oem">' + esc(p.oem_part_number) + '</div>';
html += '<div class="detail-name">' + esc(p.name) + '</div>'; html += '<div class="detail-name">' + esc(p.name) + '</div>';
if (p.description) html += '<div class="detail-desc">' + esc(p.description) + '</div>'; if (p.description) html += '<div class="detail-desc">' + esc(p.description) + '</div>';
if (p.image_url) html += '<div style="margin-top:var(--space-3);text-align:center;"><img src="' + esc(p.image_url) + '" alt="" style="max-width:100%;max-height:200px;object-fit:contain;border-radius:var(--radius-sm);"></div>'; if (p.image_url) html += '<div style="margin-top:var(--space-3);text-align:center;"><img src="' + esc(p.image_url) + '" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:200px;object-fit:contain;border-radius:var(--radius-sm);"></div>';
html += '</div>'; html += '</div>';
// Local stock // Local stock

1
pos/static/js/catalog.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/chat.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/config.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/customers.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/dashboard.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/diagrams.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/fleet.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/i18n.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -549,7 +549,7 @@
// Product image section // Product image section
html += '<div style="text-align:center;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">'; html += '<div style="text-align:center;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
if (data.image_url) { if (data.image_url) {
html += '<img src="' + esc(data.image_url) + '?t=' + Date.now() + '" alt="' + esc(data.name) + '" style="max-width:100%;max-height:220px;object-fit:contain;border-radius:var(--radius-sm);margin-bottom:8px;display:block;margin-left:auto;margin-right:auto;">'; html += '<img src="' + esc(data.image_url) + '?t=' + Date.now() + '" alt="' + esc(data.name) + '" loading="lazy" decoding="async" style="max-width:100%;max-height:220px;object-fit:contain;border-radius:var(--radius-sm);margin-bottom:8px;display:block;margin-left:auto;margin-right:auto;">';
html += '<div style="display:flex;gap:8px;justify-content:center;">'; html += '<div style="display:flex;gap:8px;justify-content:center;">';
html += '<button class="btn btn--ghost btn--sm" onclick="uploadItemImage(' + data.id + ')">Cambiar imagen</button>'; html += '<button class="btn btn--ghost btn--sm" onclick="uploadItemImage(' + data.id + ')">Cambiar imagen</button>';
html += '<button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="deleteItemImage(' + data.id + ')">Eliminar imagen</button>'; html += '<button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="deleteItemImage(' + data.id + ')">Eliminar imagen</button>';

1
pos/static/js/inventory.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/invoicing.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/kiosk.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var e="pos_kiosk_mode";function n(){return window.matchMedia("(display-mode: standalone)").matches||!0===window.navigator.standalone}function t(){return void 0!==window.Capacitor&&window.Capacitor.isNativePlatform&&window.Capacitor.isNativePlatform()}function o(){var o=localStorage.getItem(e);return"true"===o||"false"!==o&&(n()||t())}var a=!1;function i(){if(!a){var e=document.documentElement,n=e.requestFullscreen||e.webkitRequestFullscreen||e.mozRequestFullScreen||e.msRequestFullscreen;n&&(n.call(e).catch((function(){})),a=!0)}}var c=null;async function r(){if("wakeLock"in navigator)try{(c=await navigator.wakeLock.request("screen")).addEventListener("release",(function(){c=null}))}catch(e){}}function u(){window.addEventListener("beforeunload",(function(e){o()&&(e.preventDefault(),e.returnValue="")})),document.addEventListener("contextmenu",(function(e){o()&&e.preventDefault()}));var e=["click","touchstart","keydown"];function n(){o()&&(i(),r()),e.forEach((function(e){document.removeEventListener(e,n)}))}e.forEach((function(e){document.addEventListener(e,n,{once:!1})})),function(){var e=localStorage.getItem("pos_token");if(e&&-1!==window.location.pathname.indexOf("/pos/login"))try{var n=e.split(".");if(3!==n.length)return;var t=JSON.parse(atob(n[1].replace(/-/g,"+").replace(/_/g,"/"))).exp;t&&1e3*t>Date.now()&&(window.location.href="/pos/")}catch(e){}}()}document.addEventListener("visibilitychange",(function(){"visible"===document.visibilityState&&o()&&!c&&r()})),window.NexusKiosk={isEnabled:o,isPWA:n,isCapacitor:t,enable:function(){localStorage.setItem(e,"true"),i(),r()},disable:function(){localStorage.setItem(e,"false"),c&&(c.release(),c=null),document.fullscreenElement&&document.exitFullscreen().catch((function(){}))},toggle:function(){return o()?window.NexusKiosk.disable():window.NexusKiosk.enable(),o()}},o()&&u(),window.addEventListener("storage",(function(n){n.key===e&&"true"===n.newValue&&u()}))}();

1
pos/static/js/login.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var t="",e=document.querySelectorAll("#pinDots .pin-dot"),n=document.getElementById("loginError"),o=new URLSearchParams(window.location.search).get("tenant")||localStorage.getItem("pos_tenant_id"),a=localStorage.getItem("pos_device_id");function i(){e.forEach((function(e,n){e.classList.toggle("filled",n<t.length)}))}a||(a="dev-"+Date.now()+"-"+Math.random().toString(36).substr(2,9),localStorage.setItem("pos_device_id",a)),window.addDigit=function(e){t.length>=4||(t+=e,i(),n.textContent="",4===t.length&&submitPin())},window.clearPin=function(){t="",i(),n.textContent=""},window.submitPin=function(){4===t.length&&(n.textContent="",fetch("/pos/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tenant_id:parseInt(o),pin:t,device_id:a})}).then((function(t){return t.json().then((function(e){return{ok:t.ok,data:e}}))})).then((function(t){if(!t.ok)return n.textContent=t.data.error||"Error de autenticacion",void clearPin();localStorage.setItem("pos_token",t.data.token),localStorage.setItem("pos_employee",JSON.stringify(t.data.employee)),localStorage.setItem("pos_tenant_id",o),window.location.href="/pos/catalog"})).catch((function(){n.textContent="Error de conexion",clearPin()})))},document.addEventListener("keydown",(function(t){t.key>="0"&&t.key<="9"?addDigit(t.key):"Backspace"===t.key?clearPin():"Enter"===t.key&&submitPin()}));var r=localStorage.getItem("pos_token");r&&o&&(!function(t){try{var e=t.split(".");if(3!==e.length)return!1;var n=e[1].replace(/-/g,"+").replace(/_/g,"/"),o=JSON.parse(atob(n));return!!o.exp&&1e3*o.exp>Date.now()+3e4}catch(t){return!1}}(r)?(localStorage.removeItem("pos_token"),localStorage.removeItem("pos_employee")):window.location.href="/pos/catalog")}();

1
pos/static/js/native-bridge.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";window.NexusNative={isNative:"undefined"!=typeof Capacitor,_scanStream:null,_scanVideo:null,async scanBarcode(){if(this.isNative)try{const{Camera:e}=await import("@capacitor/camera");return await e.getPhoto({quality:90,resultType:"base64"})}catch(e){return null}return"BarcodeDetector"in window?new Promise((async e=>{try{const t=await navigator.mediaDevices.getUserMedia({video:{facingMode:"environment",width:{ideal:1280},height:{ideal:720}}});this._scanStream=t;const a=document.createElement("div");a.id="barcode-scan-overlay",a.style.cssText="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:99999;display:flex;flex-direction:column;align-items:center;justify-content:center;";const i=document.createElement("video");i.autoplay=!0,i.playsInline=!0,i.style.cssText="width:90%;max-width:500px;border-radius:12px;border:3px solid #F5A623;",i.srcObject=t,this._scanVideo=i;const o=document.createElement("p");o.textContent="Apunta al codigo de barras...",o.style.cssText="color:#fff;font-size:16px;margin-top:16px;font-family:sans-serif;";const s=document.createElement("button");s.textContent="Cancelar",s.style.cssText="margin-top:16px;padding:10px 24px;background:#F5A623;color:#000;border:none;border-radius:6px;font-size:15px;cursor:pointer;font-weight:bold;",s.onclick=()=>{this.stopScan(),a.remove(),e(null)},a.appendChild(i),a.appendChild(o),a.appendChild(s),document.body.appendChild(a);const n=new BarcodeDetector({formats:["ean_13","ean_8","code_128","code_39","qr_code","upc_a","upc_e"]}),r=async()=>{if(this._scanStream){try{const t=await n.detect(i);if(t.length>0){const i=t[0].rawValue;return o.textContent="Codigo detectado: "+i,o.style.color="#4CAF50",void setTimeout((()=>{this.stopScan(),a.remove(),e(i)}),400)}}catch(e){}requestAnimationFrame(r)}};i.onloadedmetadata=()=>{i.play(),requestAnimationFrame(r)}}catch(t){console.error("Camera access error:",t),alert("No se pudo acceder a la camara: "+t.message),e(null)}})):(alert("Tu navegador no soporta escaneo de codigos de barras. Usa Chrome 83+ o un dispositivo movil."),null)},stopScan(){this._scanStream&&(this._scanStream.getTracks().forEach((e=>e.stop())),this._scanStream=null),this._scanVideo=null;var e=document.getElementById("barcode-scan-overlay");e&&e.remove()},async registerPush(){if(!this.isNative)return null;try{const{PushNotifications:e}=await import("@capacitor/push-notifications");"granted"===(await e.requestPermissions()).receive&&await e.register(),e.addListener("registration",(e=>{console.log("Push token:",e.value)})),e.addListener("pushNotificationReceived",(e=>{console.log("Push received:",e)}))}catch(e){console.log("Push not available:",e)}},async vibrate(){if(this.isNative)try{const{Haptics:e,ImpactStyle:t}=await import("@capacitor/haptics");await e.impact({style:t.Light})}catch(e){}},async setupStatusBar(){if(this.isNative)try{const{StatusBar:e,Style:t}=await import("@capacitor/status-bar");await e.setStyle({style:t.Dark}),await e.setBackgroundColor({color:"#0d0d0d"})}catch(e){}}},window.NexusNative.isNative&&(window.NexusNative.setupStatusBar(),window.NexusNative.registerPush())}();

1
pos/static/js/offline-banner.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var n=document.getElementById("offlineBanner"),e=document.getElementById("offlineBannerText");if(n&&e){var t=null;navigator.onLine||o(),window.addEventListener("offline",o),window.addEventListener("online",(function(){clearTimeout(t),n.className="banner banner--success",n.style.display="flex",n.style.animation="slideDown 0.35s ease-out forwards",e.innerHTML="<strong>Conexion restaurada</strong> — Sincronizando datos...",t=setTimeout((function(){n.style.animation="slideUp 0.3s ease-in forwards",n.addEventListener("animationend",(function e(){n.style.display="none",n.removeEventListener("animationend",e)}),{once:!0})}),3e3)}))}function o(){clearTimeout(t),n.className="banner banner--error",n.style.display="flex",n.style.animation="slideDown 0.35s ease-out forwards",e.innerHTML="<strong>Conexion perdida</strong> — Intentando reconectar..."}}();

1
pos/static/js/onboarding.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/pos-utils.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/pos.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/printer.min.js vendored Normal file
View File

@@ -0,0 +1 @@
window.NexusPrinter=function(){"use strict";let e=null,t=null,r=null,n=null,a=null;async function i(){try{"usb"===a&&e&&await e.close(),"serial"===a&&(n&&(n.releaseLock(),n=null),r&&(await r.close(),r=null))}catch(e){}e=null,t=null,a=null,localStorage.removeItem("nexus_printer")}async function o(r){if(!a)return!1;try{if("usb"===a&&e&&null!==t){const n=512;for(let a=0;a<r.length;a+=n)await e.transferOut(t,r.slice(a,a+n));return!0}if("serial"===a&&n)return await n.write(r),!0}catch(e){console.error("[NexusPrinter] send error:",e),await i()}return!1}function l(){try{localStorage.setItem("nexus_printer",a||"")}catch(e){}}return{connect:async function(){if("usb"in navigator)try{e=await navigator.usb.requestDevice({filters:[]}),await e.open(),null===e.configuration&&await e.selectConfiguration(1);const r=e.configuration.interfaces[0];await e.claimInterface(r.interfaceNumber);const n=r.alternates[0].endpoints.find((e=>"out"===e.direction&&"bulk"===e.type));if(n)return t=n.endpointNumber,a="usb",l(),{ok:!0,type:"usb",name:e.productName||"USB Printer"}}catch(t){e=null}if("serial"in navigator)try{return r=await navigator.serial.requestPort(),await r.open({baudRate:9600}),n=r.writable.getWriter(),a="serial",l(),{ok:!0,type:"serial",name:"Serial Printer"}}catch(e){r=null,n=null}return{ok:!1,error:"No printer connected. Use a browser that supports WebUSB or Web Serial (Chrome/Edge)."}},disconnect:i,isConnected:function(){return null!==a},sendRaw:o,printSale:async function(e,t){t=t||80;const r=localStorage.getItem("pos_token"),n=await fetch("/pos/api/sales/"+e+"/print",{method:"POST",headers:{Authorization:"Bearer "+r,"Content-Type":"application/json"},body:JSON.stringify({printer_type:"escpos_raw",width:t})});if(!n.ok)return console.error("[NexusPrinter] backend error",n.status),!1;const a=await n.arrayBuffer();return o(new Uint8Array(a))}}}();

1
pos/static/js/push.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var e={};try{e=JSON.parse(localStorage.getItem("pos_employee")||"{}")}catch(e){}var o=e.role||"";if("owner"===o||"admin"===o)if("serviceWorker"in navigator&&"PushManager"in window){var r=localStorage.getItem("pos_token");r&&("complete"===document.readyState?setTimeout(i,2e3):window.addEventListener("load",(function(){setTimeout(i,2e3)})))}else console.log("[Push] Browser does not support push notifications");function t(e){for(var o=(e+"=".repeat((4-e.length%4)%4)).replace(/-/g,"+").replace(/_/g,"/"),r=window.atob(o),t=new Uint8Array(r.length),i=0;i<r.length;++i)t[i]=r.charCodeAt(i);return t}async function i(){try{var e=await navigator.serviceWorker.register("/pos/static/sw-push.js",{scope:"/pos/"});console.log("[Push] Service worker registered");var o=await fetch("/pos/api/push/vapid-key",{headers:{Authorization:"Bearer "+r}});if(!o.ok)return void console.log("[Push] VAPID key not available:",o.status);var i=(await o.json()).public_key;if(!i)return;if("granted"!==await Notification.requestPermission())return void console.log("[Push] Permission denied");var a=await e.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:t(i)});(await fetch("/pos/api/push/subscribe",{method:"POST",headers:{"Content-Type":"application/json",Authorization:"Bearer "+r},body:JSON.stringify({subscription:a.toJSON()})})).ok&&console.log("[Push] Subscribed successfully")}catch(e){console.log("[Push] Setup error:",e)}}}();

1
pos/static/js/reports.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/sidebar.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pos/static/js/sync-engine.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var n="sync_queue",e="inventory_cache",t="cached_parts",r=null;function o(){return new Promise((function(o,c){if(r)o(r);else{var i=indexedDB.open("nexus_pos_offline",2);i.onupgradeneeded=function(r){var o=r.target.result;if(o.objectStoreNames.contains(n)||o.createObjectStore(n,{keyPath:"id",autoIncrement:!0}),!o.objectStoreNames.contains(e)){var c=o.createObjectStore(e,{keyPath:"item_id"});c.createIndex("sku","sku",{unique:!1}),c.createIndex("name","name",{unique:!1})}if(!o.objectStoreNames.contains(t)){var i=o.createObjectStore(t,{keyPath:"part_number"});i.createIndex("name","name",{unique:!1}),i.createIndex("category","category",{unique:!1})}},i.onsuccess=function(n){r=n.target.result,o(r)},i.onerror=function(){c(i.error)}}}))}function c(){return o().then((function(e){return new Promise((function(t,r){var o=e.transaction(n,"readonly").objectStore(n).getAll();o.onsuccess=function(){t(o.result)},o.onerror=function(){r(o.error)}}))})).then((function(e){if(!e.length)return Promise.resolve({synced:0});var t=Promise.resolve(),r=0,c=0;return e.forEach((function(e){t=t.then((function(){var t={method:e.method,headers:{"Content-Type":"application/json"}};return e.body&&(t.body=JSON.stringify(e.body)),fetch(e.url,t).then((function(t){if(t.ok)return r++,i=e.id,o().then((function(e){return new Promise((function(t,r){var o=e.transaction(n,"readwrite");o.objectStore(n).delete(i),o.oncomplete=function(){t()},o.onerror=function(){r(o.error)}}))}));var i;c++})).catch((function(){c++}))}))})),t.then((function(){return{synced:r,failed:c,total:e.length}}))}))}window.addEventListener("online",(function(){console.log("[SyncEngine] Online — processing queue..."),c().then((function(n){n.synced>0&&console.log("[SyncEngine] Synced "+n.synced+" operations")})).catch((function(n){console.error("[SyncEngine] Queue processing error:",n)}))})),window.addEventListener("offline",(function(){console.log("[SyncEngine] Offline — operations will be queued")})),"serviceWorker"in navigator&&navigator.serviceWorker.addEventListener("message",(function(n){n.data&&"SYNC_REQUESTED"===n.data.type&&c()})),o().catch((function(n){console.error("[SyncEngine] Failed to open IndexedDB:",n)})),window.SyncEngine={queueOperation:function(e,t,r){return o().then((function(o){return new Promise((function(c,i){var a=o.transaction(n,"readwrite");a.objectStore(n).add({url:e,method:t,body:r||null,timestamp:Date.now()}),a.oncomplete=function(){c()},a.onerror=function(){i(a.error)}}))}))},processQueue:c,getQueueCount:function(){return o().then((function(e){return new Promise((function(t,r){var o=e.transaction(n,"readonly").objectStore(n).count();o.onsuccess=function(){t(o.result)},o.onerror=function(){r(o.error)}}))}))},isOnline:function(){return navigator.onLine},cacheInventory:function(){return fetch("/pos/api/sync/inventory").then((function(n){if(!n.ok)throw new Error("Sync inventory failed: "+n.status);return n.json()})).then((function(n){var t=n.items||[];return o().then((function(n){return new Promise((function(r,o){var c=n.transaction(e,"readwrite"),i=c.objectStore(e);i.clear(),t.forEach((function(n){i.put(n)})),c.oncomplete=function(){r(t.length)},c.onerror=function(){o(c.error)}}))}))}))},getCachedInventory:function(n){return o().then((function(t){return new Promise((function(r,o){var c=t.transaction(e,"readonly").objectStore(e).getAll();c.onsuccess=function(){var e=c.result;if(n){var t=n.toLowerCase(),o=e.filter((function(n){return n.sku&&-1!==n.sku.toLowerCase().indexOf(t)||n.name&&-1!==n.name.toLowerCase().indexOf(t)||n.barcode&&-1!==n.barcode.toLowerCase().indexOf(t)}));r(o)}else r(e)},c.onerror=function(){o(c.error)}}))}))},cacheTopParts:function(){return fetch("/pos/api/sync/top-parts").then((function(n){if(!n.ok)throw new Error("Sync top-parts failed: "+n.status);return n.json()})).then((function(n){var e=n.parts||[];return o().then((function(n){return new Promise((function(r,o){var c=n.transaction(t,"readwrite"),i=c.objectStore(t);i.clear(),e.forEach((function(n){i.put(n)})),c.oncomplete=function(){console.log("[SyncEngine] Cached "+e.length+" top parts"),r(e.length)},c.onerror=function(){o(c.error)}}))}))}))},searchCachedParts:function(n){return o().then((function(e){return new Promise((function(r,o){var c=e.transaction(t,"readonly").objectStore(t).getAll();c.onsuccess=function(){var e=c.result;if(n){var t=n.toLowerCase(),o=e.filter((function(n){return n.part_number&&-1!==n.part_number.toLowerCase().indexOf(t)||n.name&&-1!==n.name.toLowerCase().indexOf(t)||n.category&&-1!==n.category.toLowerCase().indexOf(t)||n.brand&&-1!==n.brand.toLowerCase().indexOf(t)}));r(o)}else r(e)},c.onerror=function(){o(c.error)}}))}))}}}();

1
pos/static/js/whatsapp.min.js vendored Normal file

File diff suppressed because one or more lines are too long

47
scripts/minify-assets.sh Executable file
View File

@@ -0,0 +1,47 @@
#!/bin/bash
# Minify JS and CSS assets for production deploy
# Run this before restarting nginx / gunicorn
set -e
echo "=== Minifying JS assets ==="
for f in /home/Autopartes/pos/static/js/*.js; do
base=$(basename "$f" .js)
out="/home/Autopartes/pos/static/js/${base}.min.js"
echo " $base.js -> ${base}.min.js"
terser "$f" -o "$out" --compress --mangle 2>/dev/null || cp "$f" "$out"
done
echo "=== Minifying CSS assets ==="
for f in /home/Autopartes/pos/static/css/*.css; do
base=$(basename "$f" .css)
out="/home/Autopartes/pos/static/css/${base}.min.css"
echo " $base.css -> ${base}.min.css"
if command -v csso >/dev/null 2>&1; then
csso "$f" -o "$out" 2>/dev/null || cp "$f" "$out"
else
cp "$f" "$out"
fi
done
echo "=== Minifying Dashboard assets ==="
for f in /home/Autopartes/dashboard/*.js; do
base=$(basename "$f" .js)
out="/home/Autopartes/dashboard/${base}.min.js"
echo " $base.js -> ${base}.min.js"
terser "$f" -o "$out" --compress --mangle 2>/dev/null || cp "$f" "$out"
done
for f in /home/Autopartes/dashboard/*.css; do
base=$(basename "$f" .css)
out="/home/Autopartes/dashboard/${base}.min.css"
echo " $base.css -> ${base}.min.css"
if command -v csso >/dev/null 2>&1; then
csso "$f" -o "$out" 2>/dev/null || cp "$f" "$out"
else
cp "$f" "$out"
fi
done
echo "=== Done ==="
echo "Minified files ready. Nginx will serve .min.js/.min.css when available."

84
scripts/warm_vehicle_cache.py Executable file
View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Warm Redis cache for vehicle info (part_vehicle_preview alternative).
Runs in batches over all parts in the catalog, populating
nexus:vehicle:{part_id} keys in Redis. This eliminates the
DISTINCT ON + 4 JOINs query on vehicle_parts (2B rows) for
cached parts.
Usage:
export MASTER_DB_URL="postgresql://..."
export REDIS_URL="redis://localhost:6379/0"
python3 warm_vehicle_cache.py
"""
import os, sys, json, time
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'pos'))
import psycopg2
import redis
def _fix_dsn(dsn):
if dsn and 'host=' not in dsn and '@/' in dsn:
dsn = dsn.replace('@/', '@localhost/')
return dsn
MASTER_DB_URL = _fix_dsn(os.environ.get('MASTER_DB_URL', 'postgresql://postgres@localhost/nexus_autoparts'))
REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
BATCH_SIZE = 5000
TTL_SECONDS = 3600
def main():
print("Connecting to master DB and Redis...")
conn = psycopg2.connect(MASTER_DB_URL)
cur = conn.cursor()
r = redis.from_url(REDIS_URL, decode_responses=True)
r.ping()
# Get all part_ids
cur.execute("SELECT id_part FROM parts WHERE oem_part_number IS NOT NULL ORDER BY id_part")
all_ids = [r[0] for r in cur.fetchall()]
total = len(all_ids)
print(f"Total parts to warm: {total}")
processed = 0
cached = 0
start = time.time()
for i in range(0, total, BATCH_SIZE):
batch = all_ids[i:i + BATCH_SIZE]
cur.execute("""
SELECT DISTINCT ON (vp.part_id)
vp.part_id, b.name_brand, m.name_model, y.year_car
FROM vehicle_parts vp
JOIN model_year_engine mye ON mye.id_mye = vp.model_year_engine_id
JOIN models m ON m.id_model = mye.model_id
JOIN brands b ON b.id_brand = m.brand_id
JOIN years y ON y.id_year = mye.year_id
WHERE vp.part_id = ANY(%s)
ORDER BY vp.part_id, y.year_car DESC
""", (batch,))
pipe = r.pipeline()
batch_cached = 0
for row in cur.fetchall():
info = f"{row[1]} {row[2]} {row[3]}"
pipe.setex(f'nexus:vehicle:{row[0]}', TTL_SECONDS, info)
batch_cached += 1
pipe.execute()
processed += len(batch)
cached += batch_cached
elapsed = time.time() - start
rate = processed / elapsed if elapsed > 0 else 0
print(f" [{processed}/{total}] cached={batch_cached} ({rate:.0f}/s)")
cur.close()
conn.close()
print(f"\nDone. Cached {cached} vehicle entries in {elapsed:.0f}s")
if __name__ == '__main__':
main()