Merge branch 'main' into desarrollo_hector
This commit is contained in:
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
2
pos/static/css/accounting.min.css
vendored
2
pos/static/css/accounting.min.css
vendored
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
2
pos/static/css/catalog.min.css
vendored
2
pos/static/css/catalog.min.css
vendored
@@ -12,8 +12,6 @@
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
2
pos/static/css/config.min.css
vendored
2
pos/static/css/config.min.css
vendored
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
font-weight: var(--font-weight-regular);
|
||||
background-color: var(--color-bg-base);
|
||||
color: var(--color-text-primary);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
2
pos/static/css/customers.min.css
vendored
2
pos/static/css/customers.min.css
vendored
@@ -20,8 +20,6 @@
|
||||
font-weight: var(--font-weight-regular);
|
||||
background-color: var(--color-bg-base);
|
||||
color: var(--color-text-primary);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
}
|
||||
|
||||
/* Modern theme dot-grid on body */
|
||||
@@ -814,6 +812,18 @@
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.chart-canvas-wrap {
|
||||
position: relative;
|
||||
height: 220px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chart-canvas-wrap canvas {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.rank-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
2
pos/static/css/dashboard.min.css
vendored
2
pos/static/css/dashboard.min.css
vendored
@@ -23,8 +23,6 @@
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
}
|
||||
|
||||
/* Modern theme dot-grid on body */
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
2
pos/static/css/diagrams.min.css
vendored
2
pos/static/css/diagrams.min.css
vendored
@@ -12,8 +12,6 @@
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
2
pos/static/css/fleet.min.css
vendored
2
pos/static/css/fleet.min.css
vendored
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -732,6 +730,20 @@
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.btn--meli {
|
||||
background: #FFE600;
|
||||
color: #2D3277;
|
||||
border-color: transparent;
|
||||
font-weight: 700;
|
||||
}
|
||||
.btn--meli:hover {
|
||||
background: #e6cf00;
|
||||
color: #1a1f5c;
|
||||
}
|
||||
.btn--meli svg {
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
DATA TABLE
|
||||
========================================================================= */
|
||||
@@ -1261,7 +1273,7 @@
|
||||
.inv-field label {
|
||||
font-size: var(--text-caption);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-muted);
|
||||
color: var(--color-text-secondary);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -1282,6 +1294,23 @@
|
||||
box-shadow: 0 0 0 2px var(--color-primary-muted);
|
||||
}
|
||||
|
||||
.inv-field select {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--color-surface-1);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-body-sm);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inv-field select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-muted);
|
||||
}
|
||||
|
||||
.count-row {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
@@ -1301,3 +1330,129 @@
|
||||
|
||||
/* History table inside modal */
|
||||
.inv-modal .data-table { width: 100%; }
|
||||
|
||||
/* ─── Virtual Scroll fixes ───────────────────────────────────────────── */
|
||||
.vs-container {
|
||||
will-change: transform;
|
||||
contain: layout paint;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.vs-container table {
|
||||
will-change: transform;
|
||||
}
|
||||
.vs-container tbody tr {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 48px;
|
||||
}
|
||||
|
||||
/* ─── MercadoLibre Publish Modal Enhancements ────────────────────────── */
|
||||
.meli-preview-card {
|
||||
display: grid;
|
||||
grid-template-columns: 56px 1fr auto auto auto;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--space-2);
|
||||
background: var(--color-surface-1);
|
||||
}
|
||||
.meli-preview-card img {
|
||||
width: 56px; height: 56px; object-fit: cover; border-radius: var(--radius-sm);
|
||||
background: var(--color-surface-2);
|
||||
}
|
||||
.meli-preview-card .meli-title-input {
|
||||
width: 100%;
|
||||
background: var(--color-surface-2);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-primary);
|
||||
padding: 4px 8px;
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
.meli-preview-card .meli-num-input {
|
||||
width: 80px;
|
||||
background: var(--color-surface-2);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-primary);
|
||||
padding: 4px 8px;
|
||||
font-size: var(--text-caption);
|
||||
text-align: right;
|
||||
}
|
||||
.meli-check { font-size: var(--text-caption); display: flex; align-items: center; gap: 4px; }
|
||||
.meli-check.ok { color: var(--color-success); }
|
||||
.meli-check.fail { color: var(--color-error); }
|
||||
.meli-checks-row {
|
||||
display: flex; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-1);
|
||||
}
|
||||
.meli-attrs-section {
|
||||
margin-top: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface-1);
|
||||
}
|
||||
.meli-attrs-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
.meli-img-upload {
|
||||
border: 2px dashed var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-3);
|
||||
text-align: center;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
.meli-img-upload:hover { border-color: var(--color-primary); }
|
||||
.meli-img-upload input { display: none; }
|
||||
|
||||
/* ─── MercadoLibre Category Autocomplete ─────────────────────────────── */
|
||||
.meli-cat-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 200;
|
||||
background: var(--color-surface-1);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg);
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.meli-cat-item {
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
transition: background var(--transition-fast);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.meli-cat-item:last-child { border-bottom: none; }
|
||||
.meli-cat-item:hover,
|
||||
.meli-cat-item.is-active {
|
||||
background: var(--color-surface-2);
|
||||
}
|
||||
.meli-cat-item .cat-id {
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-muted);
|
||||
margin-left: 8px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.meli-cat-loading,
|
||||
.meli-cat-empty {
|
||||
padding: 12px 14px;
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
2
pos/static/css/inventory.min.css
vendored
2
pos/static/css/inventory.min.css
vendored
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
2
pos/static/css/invoicing.min.css
vendored
2
pos/static/css/invoicing.min.css
vendored
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-slow) var(--ease-in-out),
|
||||
color var(--duration-slow) var(--ease-in-out);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@@ -58,8 +56,6 @@
|
||||
background-color: var(--color-bg-elevated);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: background-color var(--duration-slow) var(--ease-in-out),
|
||||
border-color var(--duration-slow) var(--ease-in-out);
|
||||
}
|
||||
|
||||
.theme-bar__label {
|
||||
@@ -657,8 +653,6 @@
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
background: var(--color-surface-1);
|
||||
transition: background-color var(--duration-slow) var(--ease-in-out),
|
||||
border-color var(--duration-slow) var(--ease-in-out);
|
||||
}
|
||||
|
||||
.footer-version {
|
||||
|
||||
6
pos/static/css/login.min.css
vendored
6
pos/static/css/login.min.css
vendored
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-slow) var(--ease-in-out),
|
||||
color var(--duration-slow) var(--ease-in-out);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@@ -58,8 +56,6 @@
|
||||
background-color: var(--color-bg-elevated);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: background-color var(--duration-slow) var(--ease-in-out),
|
||||
border-color var(--duration-slow) var(--ease-in-out);
|
||||
}
|
||||
|
||||
.theme-bar__label {
|
||||
@@ -657,8 +653,6 @@
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
background: var(--color-surface-1);
|
||||
transition: background-color var(--duration-slow) var(--ease-in-out),
|
||||
border-color var(--duration-slow) var(--ease-in-out);
|
||||
}
|
||||
|
||||
.footer-version {
|
||||
|
||||
@@ -18,14 +18,22 @@ body {
|
||||
SIDEBAR — Glass treatment
|
||||
========================================================================== */
|
||||
|
||||
/* Prevent flash/stun while sidebar.js replaces static sidebar markup */
|
||||
.sidebar,
|
||||
.pos-sidebar {
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
background: var(--glass-bg-strong) !important;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-right: 1px solid var(--glass-border) !important;
|
||||
}
|
||||
|
||||
body.sidebar-ready .sidebar,
|
||||
body.sidebar-ready .pos-sidebar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar__logo {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
861
pos/static/css/pos-ui.css
Normal file
861
pos/static/css/pos-ui.css
Normal file
@@ -0,0 +1,861 @@
|
||||
/**
|
||||
* pos-ui.css — Nexus POS UI Polish Kit
|
||||
* Backlog visual improvements: skeletons, toasts, empty states,
|
||||
* scrollbars, focus rings, badges, tooltips, compact mode, etc.
|
||||
* Load AFTER tokens.css and common.css, BEFORE page-specific CSS.
|
||||
*/
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
0. ICON BUTTONS (header bar)
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-md, 8px);
|
||||
background: var(--color-surface-2, #222);
|
||||
border: 1px solid var(--color-border, #2a2a2a);
|
||||
color: var(--color-text-secondary, #aaa);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
position: relative;
|
||||
}
|
||||
.icon-btn:hover {
|
||||
background: var(--color-surface-3, #333);
|
||||
color: var(--color-text-primary, #eee);
|
||||
border-color: var(--color-primary, #F5A623);
|
||||
}
|
||||
.notif-dot {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-error, #ef4444);
|
||||
box-shadow: 0 0 4px var(--color-error, #ef4444);
|
||||
}
|
||||
.notif-dot:empty { display: none; }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
1. CUSTOM SCROLLBAR (global)
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
* { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb, #444) var(--scrollbar-track, transparent); }
|
||||
*::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
*::-webkit-scrollbar-track { background: var(--scrollbar-track, transparent); }
|
||||
*::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb, #444); border-radius: var(--radius-full, 999px); }
|
||||
*::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover, #666); }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
2. SKELETON SCREENS
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
@keyframes skeleton-shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg,
|
||||
var(--color-surface-1, #1a1a1a) 25%,
|
||||
var(--color-surface-2, #2a2a2a) 50%,
|
||||
var(--color-surface-1, #1a1a1a) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.6s ease-in-out infinite;
|
||||
border-radius: var(--radius-md, 8px);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
.skeleton--text { height: 1em; width: 100%; }
|
||||
.skeleton--text-sm { height: 0.75em; width: 60%; }
|
||||
.skeleton--circle { width: 40px; height: 40px; border-radius: var(--radius-full, 999px); }
|
||||
.skeleton--rect { height: 80px; width: 100%; }
|
||||
.skeleton--btn { height: 36px; width: 100px; border-radius: var(--radius-md, 8px); }
|
||||
.skeleton--table-row td { border: none !important; background: transparent !important; }
|
||||
.skeleton--table-row td .skeleton { height: 16px; }
|
||||
.skeleton--table-row td:nth-child(1) .skeleton { width: 40px; }
|
||||
.skeleton--table-row td:nth-child(2) .skeleton { width: 90%; }
|
||||
.skeleton--table-row td:nth-child(3) .skeleton { width: 70%; }
|
||||
.skeleton--table-row td:nth-child(4) .skeleton { width: 50%; }
|
||||
.skeleton--table-row td:nth-child(5) .skeleton { width: 60%; }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
3. EMPTY STATES (illustrated)
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-8, 2rem);
|
||||
text-align: center;
|
||||
color: var(--color-text-muted, #888);
|
||||
}
|
||||
.empty-state__icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: var(--space-4, 1rem);
|
||||
opacity: 0.4;
|
||||
color: var(--color-text-muted, #888);
|
||||
}
|
||||
.empty-state__icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.empty-state__title {
|
||||
font-size: var(--text-lg, 1.125rem);
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
color: var(--color-text-primary, #eee);
|
||||
margin-bottom: var(--space-2, 0.5rem);
|
||||
}
|
||||
.empty-state__subtitle {
|
||||
font-size: var(--text-sm, 0.875rem);
|
||||
max-width: 360px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: var(--space-4, 1rem);
|
||||
}
|
||||
.empty-state__action {
|
||||
margin-top: var(--space-2, 0.5rem);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
4. UNIFIED INPUT FOCUS RING
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
input:focus, select:focus, textarea:focus,
|
||||
.inv-field input:focus, .inv-field select:focus, .inv-field textarea:focus,
|
||||
.search-box:focus-within, .select-filter:focus,
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary, #F5A623) !important;
|
||||
box-shadow: 0 0 0 3px var(--color-primary-muted, rgba(245,166,35,0.25)) !important;
|
||||
}
|
||||
input:disabled, select:disabled, textarea:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
5. BADGES (enhanced + new variants)
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 10px;
|
||||
border-radius: var(--radius-full, 999px);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
letter-spacing: var(--tracking-wide, 0.05em);
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.badge::before {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: var(--radius-full, 999px);
|
||||
background: currentColor;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.badge--ok { background: rgba(34, 197, 94, 0.12); color: var(--color-success, #22c55e); }
|
||||
.badge--low { background: rgba(239, 68, 68, 0.12); color: var(--color-error, #ef4444); }
|
||||
.badge--over { background: rgba(234, 179, 8, 0.12); color: var(--color-warning, #eab308); }
|
||||
.badge--pending { background: var(--color-primary-muted, rgba(245,166,35,0.15)); color: var(--color-primary, #F5A623); }
|
||||
.badge--complete { background: rgba(34, 197, 94, 0.12); color: var(--color-success, #22c55e); }
|
||||
.badge--transit { background: rgba(99, 102, 241, 0.12); color: #818cf8; }
|
||||
.badge--cancelled{ background: rgba(115, 115, 115, 0.12);color: var(--color-text-muted, #888); }
|
||||
.badge--damage { background: rgba(239, 68, 68, 0.12); color: var(--color-error, #ef4444); }
|
||||
.badge--shrinkage{ background: rgba(234, 179, 8, 0.12); color: var(--color-warning, #eab308); }
|
||||
.badge--correction{ background: var(--color-primary-muted, rgba(245,166,35,0.15)); color: var(--color-primary, #F5A623); }
|
||||
.badge--partial { background: rgba(234, 179, 8, 0.12); color: var(--color-warning, #eab308); }
|
||||
.badge--ml { background: rgba(255, 230, 0, 0.15); color: #2D3277; }
|
||||
.badge--ml::before { background: #2D3277; }
|
||||
.badge--new { background: rgba(59, 130, 246, 0.12); color: #3b82f6; }
|
||||
.badge--syncing { background: rgba(245, 166, 35, 0.12); color: var(--color-primary, #F5A623); animation: pulse-dot 1.5s ease-in-out infinite; }
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
6. HOVER STATES (table + cards)
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.data-table tbody tr {
|
||||
border-bottom: 1px solid var(--color-border, #2a2a2a);
|
||||
transition: background var(--duration-fast, 150ms) var(--ease-in-out, ease-in-out),
|
||||
transform var(--duration-fast, 150ms) var(--ease-in-out, ease-in-out);
|
||||
}
|
||||
.data-table tbody tr:hover {
|
||||
background: var(--color-surface-2, #222);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
.data-table tbody tr:active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.card, .glass-card, .stat-card, .info-card {
|
||||
transition: transform var(--duration-fast, 150ms) var(--ease-in-out, ease-in-out),
|
||||
box-shadow var(--duration-fast, 150ms) var(--ease-in-out, ease-in-out);
|
||||
}
|
||||
.card:hover, .glass-card:hover, .stat-card:hover, .info-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg, 0 8px 24px rgba(0,0,0,0.3));
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
7. TOAST NOTIFICATIONS (enhanced)
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
#toast-container {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
pointer-events: none;
|
||||
max-width: 420px;
|
||||
}
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.25);
|
||||
pointer-events: auto;
|
||||
animation: toastSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.toast--ok { background: rgba(26, 122, 58, 0.95); color: #fff; }
|
||||
.toast--error { background: rgba(192, 57, 43, 0.95); color: #fff; }
|
||||
.toast--warn { background: rgba(212, 160, 23, 0.95); color: #000; }
|
||||
.toast--info { background: rgba(40, 40, 45, 0.95); color: #eee; border-color: rgba(255,255,255,0.08); }
|
||||
.toast__icon { flex-shrink: 0; width: 20px; height: 20px; margin-top: 1px; }
|
||||
.toast__content { flex: 1; }
|
||||
.toast__title { font-weight: 700; margin-bottom: 2px; font-size: 13px; }
|
||||
.toast__msg { font-size: 13px; opacity: 0.9; }
|
||||
.toast__action { margin-top: 8px; }
|
||||
.toast__action button {
|
||||
background: rgba(255,255,255,0.15);
|
||||
border: none;
|
||||
color: inherit;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.toast__action button:hover { background: rgba(255,255,255,0.25); }
|
||||
.toast__close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
margin-left: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.toast__close:hover { opacity: 1; }
|
||||
.toast__progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
background: rgba(255,255,255,0.4);
|
||||
border-radius: 0 0 0 var(--radius-lg, 12px);
|
||||
animation: toastProgress linear forwards;
|
||||
}
|
||||
@keyframes toastSlideIn {
|
||||
from { opacity: 0; transform: translateX(40px) scale(0.95); }
|
||||
to { opacity: 1; transform: translateX(0) scale(1); }
|
||||
}
|
||||
@keyframes toastSlideOut {
|
||||
from { opacity: 1; transform: translateX(0) scale(1); }
|
||||
to { opacity: 0; transform: translateX(40px) scale(0.95); }
|
||||
}
|
||||
@keyframes toastProgress {
|
||||
from { width: 100%; }
|
||||
to { width: 0%; }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
8. TOOLTIPS
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
[data-tooltip] {
|
||||
position: relative;
|
||||
cursor: help;
|
||||
}
|
||||
[data-tooltip]::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scale(0.9);
|
||||
padding: 6px 10px;
|
||||
background: var(--color-surface-3, #333);
|
||||
color: var(--color-text-primary, #eee);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
border-radius: var(--radius-md, 8px);
|
||||
box-shadow: var(--shadow-md, 0 4px 12px rgba(0,0,0,0.2));
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s, transform 0.15s;
|
||||
z-index: 10000;
|
||||
border: 1px solid var(--color-border, #2a2a2a);
|
||||
}
|
||||
[data-tooltip]:hover::after {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
[data-tooltip-pos="bottom"]::after {
|
||||
bottom: auto;
|
||||
top: calc(100% + 6px);
|
||||
}
|
||||
[data-tooltip-pos="left"]::after {
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
right: calc(100% + 6px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%) scale(0.9);
|
||||
}
|
||||
[data-tooltip-pos="left"]:hover::after {
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
[data-tooltip-pos="right"]::after {
|
||||
bottom: auto;
|
||||
left: calc(100% + 6px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%) scale(0.9);
|
||||
}
|
||||
[data-tooltip-pos="right"]:hover::after {
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
9. COMPACT / DENSE MODE
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
[data-density="compact"] .data-table th,
|
||||
[data-density="compact"] .data-table td {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
[data-density="compact"] .data-table tbody tr {
|
||||
height: 36px;
|
||||
}
|
||||
[data-density="compact"] .sidebar__nav-link {
|
||||
padding: 8px 14px;
|
||||
}
|
||||
[data-density="compact"] .card, [data-density="compact"] .glass-card {
|
||||
padding: var(--space-3, 0.75rem);
|
||||
}
|
||||
[data-density="compact"] .form-group {
|
||||
margin-bottom: var(--space-2, 0.5rem);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
10. STICKY TABLE HEADERS
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.data-table thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.data-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--color-surface-1, #1a1a1a);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
11. RESIZABLE COLUMNS (visual cue)
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.data-table th {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
.data-table th .resize-handle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.data-table th .resize-handle:hover,
|
||||
.data-table th.is-resizing .resize-handle {
|
||||
background: var(--color-primary, #F5A623);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
12. BREADCRUMBS
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2, 0.5rem);
|
||||
padding: var(--space-2, 0.5rem) var(--space-4, 1rem);
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted, #888);
|
||||
border-bottom: 1px solid var(--color-border, #2a2a2a);
|
||||
}
|
||||
.breadcrumbs a {
|
||||
color: var(--color-text-secondary, #aaa);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.breadcrumbs a:hover {
|
||||
color: var(--color-primary, #F5A623);
|
||||
}
|
||||
.breadcrumbs__sep {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.breadcrumbs__current {
|
||||
color: var(--color-text-primary, #eee);
|
||||
font-weight: var(--font-weight-semibold, 600);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
13. USER AVATARS (initials)
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius-full, 999px);
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
color: #fff;
|
||||
background: var(--color-primary, #F5A623);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.avatar--sm { width: 28px; height: 28px; font-size: 12px; }
|
||||
.avatar--lg { width: 48px; height: 48px; font-size: 18px; }
|
||||
.avatar--circle { border-radius: var(--radius-full, 999px); }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
14. CONNECTION INDICATOR
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.connection-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-full, 999px);
|
||||
background: var(--color-surface-2, #222);
|
||||
}
|
||||
.connection-indicator::before {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: var(--radius-full, 999px);
|
||||
background: var(--color-success, #22c55e);
|
||||
box-shadow: 0 0 6px var(--color-success, #22c55e);
|
||||
}
|
||||
.connection-indicator--offline::before {
|
||||
background: var(--color-error, #ef4444);
|
||||
box-shadow: 0 0 6px var(--color-error, #ef4444);
|
||||
}
|
||||
.connection-indicator--syncing::before {
|
||||
background: var(--color-primary, #F5A623);
|
||||
animation: pulse-dot 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
15. NOTIFICATIONS DROPDOWN
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.notif-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
width: 360px;
|
||||
max-height: 480px;
|
||||
background: var(--color-surface-1, #1a1a1a);
|
||||
border: 1px solid var(--color-border, #2a2a2a);
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
box-shadow: var(--shadow-lg, 0 8px 24px rgba(0,0,0,0.3));
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.notif-dropdown__header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--color-border, #2a2a2a);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.notif-dropdown__list {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
.notif-dropdown__item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--color-border, #2a2a2a);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.notif-dropdown__item:hover { background: var(--color-surface-2, #222); }
|
||||
.notif-dropdown__item--unread { border-left: 3px solid var(--color-primary, #F5A623); }
|
||||
.notif-dropdown__icon { flex-shrink: 0; width: 32px; height: 32px; border-radius: var(--radius-md, 8px); display: flex; align-items: center; justify-content: center; background: var(--color-surface-2, #222); font-size: 14px; }
|
||||
.notif-dropdown__content { flex: 1; }
|
||||
.notif-dropdown__title { font-size: 13px; font-weight: 600; color: var(--color-text-primary, #eee); margin-bottom: 2px; }
|
||||
.notif-dropdown__time { font-size: 11px; color: var(--color-text-muted, #888); }
|
||||
.notif-dropdown__empty { padding: 24px; text-align: center; color: var(--color-text-muted, #888); font-size: 13px; }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
16. CMD+K SEARCH OVERLAY
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.cmdk-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 15vh;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
pointer-events: none;
|
||||
}
|
||||
.cmdk-overlay.is-open { opacity: 1; pointer-events: auto; }
|
||||
.cmdk-modal {
|
||||
width: 640px;
|
||||
max-width: 90vw;
|
||||
background: var(--color-surface-1, #1a1a1a);
|
||||
border: 1px solid var(--color-border, #2a2a2a);
|
||||
border-radius: var(--radius-xl, 16px);
|
||||
box-shadow: var(--shadow-xl, 0 16px 48px rgba(0,0,0,0.4));
|
||||
overflow: hidden;
|
||||
transform: scale(0.95);
|
||||
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.cmdk-overlay.is-open .cmdk-modal { transform: scale(1); }
|
||||
.cmdk-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--color-border, #2a2a2a);
|
||||
}
|
||||
.cmdk-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--color-text-primary, #eee);
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
}
|
||||
.cmdk-input::placeholder { color: var(--color-text-muted, #888); }
|
||||
.cmdk-shortcut { font-size: 11px; color: var(--color-text-muted, #888); background: var(--color-surface-2, #222); padding: 2px 6px; border-radius: 4px; }
|
||||
.cmdk-results { max-height: 400px; overflow-y: auto; }
|
||||
.cmdk-group { padding: 8px 0; }
|
||||
.cmdk-group__label { padding: 4px 20px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-muted, #888); }
|
||||
.cmdk-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
font-size: 14px;
|
||||
}
|
||||
.cmdk-item:hover, .cmdk-item.is-selected { background: var(--color-surface-2, #222); }
|
||||
.cmdk-item__icon { width: 28px; height: 28px; border-radius: var(--radius-md, 8px); display: flex; align-items: center; justify-content: center; background: var(--color-surface-2, #222); font-size: 13px; }
|
||||
.cmdk-item__meta { margin-left: auto; font-size: 11px; color: var(--color-text-muted, #888); }
|
||||
.cmdk-footer { padding: 8px 20px; border-top: 1px solid var(--color-border, #2a2a2a); font-size: 11px; color: var(--color-text-muted, #888); display: flex; justify-content: space-between; }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
17. ENTRANCE ANIMATIONS
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes scaleIn {
|
||||
from { opacity: 0; transform: scale(0.95); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
.animate-fade-in-up { animation: fadeInUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) both; }
|
||||
.animate-fade-in { animation: fadeIn 0.3s ease both; }
|
||||
.animate-scale-in { animation: scaleIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) both; }
|
||||
/* Stagger delays */
|
||||
.stagger-1 { animation-delay: 0.05s; }
|
||||
.stagger-2 { animation-delay: 0.10s; }
|
||||
.stagger-3 { animation-delay: 0.15s; }
|
||||
.stagger-4 { animation-delay: 0.20s; }
|
||||
.stagger-5 { animation-delay: 0.25s; }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
18. SPARKLINE (mini charts)
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.sparkline { display: flex; align-items: flex-end; gap: 2px; height: 24px; }
|
||||
.sparkline__bar { flex: 1; border-radius: 1px; background: var(--color-primary, #F5A623); opacity: 0.6; transition: opacity 0.15s; min-width: 3px; }
|
||||
.sparkline__bar:hover { opacity: 1; }
|
||||
.sparkline--up .sparkline__bar { background: var(--color-success, #22c55e); }
|
||||
.sparkline--down .sparkline__bar { background: var(--color-error, #ef4444); }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
19. SYNC SPINNER / ANIMATED ICONS
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
@keyframes spin-slow { to { transform: rotate(360deg); } }
|
||||
@keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
|
||||
.icon-spin { animation: spin-slow 1.2s linear infinite; }
|
||||
.icon-bounce { animation: bounce 1s ease-in-out infinite; }
|
||||
.icon-pulse { animation: pulse-dot 1.5s ease-in-out infinite; }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
20. KIOSK / TOUCH MODE
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
[data-touch="true"] .data-table th,
|
||||
[data-touch="true"] .data-table td { padding: 16px 14px; font-size: 1rem; }
|
||||
[data-touch="true"] .btn { padding: 14px 24px; font-size: 1rem; min-height: 48px; }
|
||||
[data-touch="true"] .sidebar__nav-link { padding: 16px 18px; font-size: 1rem; }
|
||||
[data-touch="true"] input, [data-touch="true"] select, [data-touch="true"] textarea { font-size: 16px; padding: 14px 12px; }
|
||||
[data-touch="true"] .search-box { min-height: 48px; }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
21. BULK ACTIONS TOOLBAR
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.bulk-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3, 0.75rem);
|
||||
padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
|
||||
background: var(--color-surface-2, #222);
|
||||
border: 1px solid var(--color-border, #2a2a2a);
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
margin-bottom: var(--space-4, 1rem);
|
||||
animation: fadeInUp 0.25s ease both;
|
||||
}
|
||||
.bulk-toolbar__count {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary, #eee);
|
||||
padding-right: var(--space-3, 0.75rem);
|
||||
border-right: 1px solid var(--color-border, #2a2a2a);
|
||||
}
|
||||
.bulk-toolbar__actions { display: flex; gap: var(--space-2, 0.5rem); }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
22. TICKET PREVIEW
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.ticket-preview {
|
||||
background: #fff;
|
||||
color: #222;
|
||||
padding: 24px;
|
||||
border-radius: var(--radius-md, 8px);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
max-width: 320px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
.ticket-preview__header { text-align: center; border-bottom: 1px dashed #ccc; padding-bottom: 12px; margin-bottom: 12px; }
|
||||
.ticket-preview__title { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
|
||||
.ticket-preview__meta { font-size: 11px; color: #666; }
|
||||
.ticket-preview__row { display: flex; justify-content: space-between; margin-bottom: 4px; }
|
||||
.ticket-preview__total { border-top: 1px dashed #ccc; padding-top: 8px; margin-top: 8px; font-size: 14px; font-weight: 700; }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
23. IMAGE COMPARATOR
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.img-compare {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
user-select: none;
|
||||
}
|
||||
.img-compare__img { width: 100%; display: block; }
|
||||
.img-compare__overlay {
|
||||
position: absolute;
|
||||
top: 0; left: 0; bottom: 0;
|
||||
width: 50%;
|
||||
overflow: hidden;
|
||||
border-right: 2px solid var(--color-primary, #F5A623);
|
||||
}
|
||||
.img-compare__overlay img { height: 100%; width: auto; max-width: none; }
|
||||
.img-compare__handle {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 36px; height: 36px;
|
||||
background: var(--color-primary, #F5A623);
|
||||
border-radius: var(--radius-full, 999px);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
cursor: ew-resize;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
24. CUSTOM LOADER / SPINNER
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.nx-loader {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
.nx-loader__ring {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: var(--color-primary, #F5A623);
|
||||
border-radius: 50%;
|
||||
animation: spin-slow 1s linear infinite;
|
||||
}
|
||||
.nx-loader__ring:nth-child(2) {
|
||||
inset: 6px;
|
||||
border-top-color: var(--color-secondary, #3b82f6);
|
||||
animation-duration: 1.3s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.nx-loader__ring:nth-child(3) {
|
||||
inset: 12px;
|
||||
border-top-color: var(--color-success, #22c55e);
|
||||
animation-duration: 0.8s;
|
||||
}
|
||||
.nx-loader--sm { width: 24px; height: 24px; }
|
||||
.nx-loader--sm .nx-loader__ring { border-width: 2px; }
|
||||
.nx-loader--sm .nx-loader__ring:nth-child(2) { inset: 4px; }
|
||||
.nx-loader--sm .nx-loader__ring:nth-child(3) { inset: 8px; }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
21. TIMELINE
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.timeline { position: relative; padding-left: 24px; }
|
||||
.timeline::before { content: ''; position: absolute; left: 7px; top: 4px; bottom: 4px; width: 2px; background: var(--color-border, #2a2a2a); }
|
||||
.timeline__item { position: relative; margin-bottom: var(--space-4, 1rem); display: flex; gap: 12px; align-items: flex-start; }
|
||||
.timeline__dot { width: 14px; height: 14px; border-radius: 50%; background: var(--color-primary, #F5A623); border: 3px solid var(--color-surface-1, #1a1a1a); flex-shrink: 0; margin-left: -24px; margin-top: 3px; z-index: 1; }
|
||||
.timeline__dot--green { background: var(--color-success, #22c55e); }
|
||||
.timeline__dot--red { background: var(--color-error, #ef4444); }
|
||||
.timeline__dot--blue { background: #3b82f6; }
|
||||
.timeline__content { flex: 1; }
|
||||
.timeline__date { font-size: 11px; color: var(--color-text-muted, #888); margin-bottom: 2px; }
|
||||
.timeline__title { font-size: 13px; font-weight: 600; color: var(--color-text-primary, #eee); }
|
||||
.timeline__desc { font-size: 12px; color: var(--color-text-secondary, #aaa); margin-top: 2px; }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
22. KANBAN BOARD
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.kanban { display: flex; gap: var(--space-4, 1rem); overflow-x: auto; padding-bottom: var(--space-2, 0.5rem); }
|
||||
.kanban__col { min-width: 280px; max-width: 320px; flex: 1; background: var(--color-surface-2, #222); border-radius: var(--radius-lg, 12px); border: 1px solid var(--color-border, #2a2a2a); display: flex; flex-direction: column; max-height: 70vh; }
|
||||
.kanban__col-header { padding: 12px 16px; border-bottom: 1px solid var(--color-border, #2a2a2a); font-size: 13px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; }
|
||||
.kanban__col-count { font-size: 11px; padding: 2px 8px; border-radius: var(--radius-full, 999px); background: var(--color-surface-3, #333); color: var(--color-text-muted, #888); }
|
||||
.kanban__cards { flex: 1; overflow-y: auto; padding: var(--space-3, 0.75rem); display: flex; flex-direction: column; gap: var(--space-2, 0.5rem); }
|
||||
.kanban__card { background: var(--color-surface-1, #1a1a1a); border: 1px solid var(--color-border, #2a2a2a); border-radius: var(--radius-md, 8px); padding: 12px; cursor: grab; transition: all 0.15s; }
|
||||
.kanban__card:hover { border-color: var(--color-primary, #F5A623); transform: translateY(-1px); }
|
||||
.kanban__card-title { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
|
||||
.kanban__card-meta { font-size: 11px; color: var(--color-text-muted, #888); }
|
||||
.kanban__card.dragging { opacity: 0.5; cursor: grabbing; }
|
||||
.kanban__col.drag-over { background: var(--color-surface-3, #333); border-color: var(--color-primary, #F5A623); }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
25. SAVED FILTERS CHIPS
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.filter-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
background: var(--color-surface-2, #222);
|
||||
border: 1px solid var(--color-border, #2a2a2a);
|
||||
border-radius: var(--radius-full, 999px);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary, #aaa);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.filter-chip:hover { border-color: var(--color-primary, #F5A623); color: var(--color-text-primary, #eee); }
|
||||
.filter-chip.is-active { background: var(--color-primary-muted, rgba(245,166,35,0.15)); border-color: var(--color-primary, #F5A623); color: var(--color-primary, #F5A623); }
|
||||
.filter-chip__remove { background: none; border: none; color: inherit; cursor: pointer; font-size: 14px; line-height: 1; padding: 0; opacity: 0.6; }
|
||||
.filter-chip__remove:hover { opacity: 1; }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
26. DARK MODE REFINEMENTS (subtle borders, depth)
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
[data-theme="modern"] .data-table thead th {
|
||||
background: linear-gradient(180deg, var(--color-surface-1, #1a1a1a) 0%, var(--color-surface-2, #222) 100%);
|
||||
}
|
||||
[data-theme="modern"] .card, [data-theme="modern"] .glass-card {
|
||||
border: 1px solid rgba(255,255,255,0.04);
|
||||
}
|
||||
[data-theme="industrial"] .data-table thead th {
|
||||
background: var(--color-surface-1, #1a1a1a);
|
||||
border-bottom: 2px solid var(--color-primary, #F5A623);
|
||||
}
|
||||
|
||||
/* Enhanced depth layers for both themes */
|
||||
[data-theme="modern"] {
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
|
||||
--shadow-md: 0 4px 12px rgba(0,0,0,0.4);
|
||||
--shadow-lg: 0 8px 24px rgba(0,0,0,0.5);
|
||||
--shadow-xl: 0 16px 48px rgba(0,0,0,0.6);
|
||||
}
|
||||
[data-theme="industrial"] {
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.4);
|
||||
--shadow-md: 0 4px 12px rgba(0,0,0,0.5);
|
||||
--shadow-lg: 0 8px 24px rgba(0,0,0,0.6);
|
||||
--shadow-xl: 0 16px 48px rgba(0,0,0,0.7);
|
||||
}
|
||||
|
||||
/* Smooth theme transition (only on properties that don't cause flash) */
|
||||
.card, .glass-card, .btn, .icon-btn, .kpi-card, .alert-card, .meli-card {
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
/* Improved text rendering in dark */
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Focus visible for accessibility */
|
||||
*:focus-visible {
|
||||
outline: 2px solid var(--color-primary, #F5A623);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Reduced motion preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,6 @@
|
||||
font-weight: var(--font-weight-regular);
|
||||
background-color: var(--color-bg-base);
|
||||
color: var(--color-text-primary);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
2
pos/static/css/pos.min.css
vendored
2
pos/static/css/pos.min.css
vendored
@@ -21,8 +21,6 @@
|
||||
font-weight: var(--font-weight-regular);
|
||||
background-color: var(--color-bg-base);
|
||||
color: var(--color-text-primary);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
2
pos/static/css/reports.min.css
vendored
2
pos/static/css/reports.min.css
vendored
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
2
pos/static/css/whatsapp.min.css
vendored
2
pos/static/css/whatsapp.min.css
vendored
@@ -19,8 +19,6 @@
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -506,4 +506,12 @@ const Accounting = (() => {
|
||||
loadIncomeStatement, loadCashFlow, loadReconciliation, loadPeriodClose,
|
||||
exportarContabilidad, showNewEntryModal, closeNewEntryModal, addEntryLine, submitNewEntry,
|
||||
};
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
registerCmdKItem({ group: "Principal", label: "POS Ventas", href: "/pos/sale", icon: "🛒" });
|
||||
registerCmdKItem({ group: "Principal", label: "Catálogo", href: "/pos/catalog", icon: "📁" });
|
||||
registerCmdKItem({ group: "Principal", label: "Clientes", href: "/pos/customers", icon: "👤" });
|
||||
registerCmdKItem({ group: "Principal", label: "Dashboard", href: "/pos/dashboard", icon: "📊" });
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -167,12 +167,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Also prevent any DOMContentLoaded theme switchers from overriding
|
||||
// by re-applying our saved theme after a tick
|
||||
setTimeout(function() {
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
}, 100);
|
||||
|
||||
// ─── Expose globally ───
|
||||
window.POS_USER = {
|
||||
name: name,
|
||||
@@ -186,4 +180,31 @@
|
||||
permissions: payload.permissions || []
|
||||
};
|
||||
|
||||
// ─── Preload enabled modules for sidebar filtering ───
|
||||
try {
|
||||
fetch('/pos/api/config/modules', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
}).then(function(r) {
|
||||
if (r.ok) return r.json();
|
||||
}).then(function(data) {
|
||||
if (data) {
|
||||
localStorage.setItem('pos_modules', JSON.stringify(data));
|
||||
window.POS_USER.modules = data;
|
||||
if (typeof window.renderSidebar === 'function') {
|
||||
window.renderSidebar(data);
|
||||
}
|
||||
}
|
||||
}).catch(function() {});
|
||||
} catch(e) {}
|
||||
|
||||
// ─── Service Worker update handler ───
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', function (event) {
|
||||
if (event.data && event.data.type === 'SW_UPDATED') {
|
||||
console.log('[AppInit] SW updated to', event.data.cacheName, '— reloading...');
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
1
pos/static/js/app-init.min.js
vendored
1
pos/static/js/app-init.min.js
vendored
@@ -1 +0,0 @@
|
||||
!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"}();
|
||||
@@ -6,6 +6,7 @@
|
||||
_offset: 0,
|
||||
_limit: 50,
|
||||
_total: 0,
|
||||
_allowedBrands: [],
|
||||
|
||||
// Navigation state
|
||||
nav: {
|
||||
@@ -71,7 +72,9 @@
|
||||
},
|
||||
|
||||
loading: function(on) {
|
||||
this.el('brandCatalogLoading').style.display = on ? 'block' : 'none';
|
||||
var el = this.el('brandCatalogLoading');
|
||||
if (on) el.classList.add('is-visible');
|
||||
else el.classList.remove('is-visible');
|
||||
},
|
||||
|
||||
setContent: function(html) {
|
||||
@@ -88,23 +91,23 @@
|
||||
|
||||
buildBreadcrumb: function() {
|
||||
var parts = [];
|
||||
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.loadBrands()" style="color:var(--color-primary);text-decoration:none;">Marcas</a>');
|
||||
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick="BrandCatalog.loadBrands()">Marcas</a>');
|
||||
if (this.nav.brand) {
|
||||
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.selectBrand(' + JSON.stringify(this.nav.brand) + ',' + this.nav.brandId + ')" style="color:var(--color-primary);text-decoration:none;">' + escapeHtml(this.nav.brand) + '</a>');
|
||||
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick=\'BrandCatalog.selectBrand(' + JSON.stringify(this.nav.brand) + ',' + this.nav.brandId + ')\'>' + escapeHtml(this.nav.brand) + '</a>');
|
||||
}
|
||||
if (this.nav.model) {
|
||||
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.selectModel(' + this.nav.modelId + ',' + JSON.stringify(this.nav.model) + ')" style="color:var(--color-primary);text-decoration:none;">' + escapeHtml(this.nav.model) + '</a>');
|
||||
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick=\'BrandCatalog.selectModel(' + this.nav.modelId + ',' + JSON.stringify(this.nav.model) + ')\'>' + escapeHtml(this.nav.model) + '</a>');
|
||||
}
|
||||
if (this.nav.year) {
|
||||
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.selectYear(' + this.nav.yearId + ',' + this.nav.year + ')" style="color:var(--color-primary);text-decoration:none;">' + this.nav.year + '</a>');
|
||||
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick=\'BrandCatalog.selectYear(' + this.nav.yearId + ',' + this.nav.year + ')\'>' + this.nav.year + '</a>');
|
||||
}
|
||||
if (this.nav.engine) {
|
||||
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.selectEngine(' + this.nav.myeId + ',' + JSON.stringify(this.nav.engine) + ')" style="color:var(--color-primary);text-decoration:none;">' + escapeHtml(this.nav.engine) + '</a>');
|
||||
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick=\'BrandCatalog.selectEngine(' + this.nav.myeId + ',' + JSON.stringify(this.nav.engine) + ')\'>' + escapeHtml(this.nav.engine) + '</a>');
|
||||
}
|
||||
if (this.nav.category) {
|
||||
parts.push('<strong>' + escapeHtml(this.nav.category) + '</strong>');
|
||||
parts.push('<span class="breadcrumb__current">' + escapeHtml(this.nav.category) + '</span>');
|
||||
}
|
||||
this.setBreadcrumb(parts.join(' › '));
|
||||
this.setBreadcrumb('<nav class="breadcrumb">' + parts.join('<span class="breadcrumb__sep">›</span>') + '</nav>');
|
||||
},
|
||||
|
||||
// ---------- BRANDS ----------
|
||||
@@ -112,12 +115,10 @@
|
||||
this.loading(true);
|
||||
this.state = 'brands';
|
||||
this.reset();
|
||||
this.setBreadcrumb('<strong>Marcas de vehiculo</strong>');
|
||||
this.setBreadcrumb('<nav class="breadcrumb"><span class="breadcrumb__current">Marcas de vehiculo</span></nav>');
|
||||
this.setSearch(
|
||||
'<input type="text" id="brandSearchInput" placeholder="Buscar marca..." ' +
|
||||
'style="width:100%;padding:10px 14px;border:1px solid var(--color-border);border-radius:var(--radius-md);' +
|
||||
'font-size:var(--text-body);background:var(--color-surface);color:var(--color-text-primary);' +
|
||||
'outline:none;" oninput="BrandCatalog.filterBrands(this.value)">'
|
||||
'class="level-filter" oninput="BrandCatalog.filterBrands(this.value)">'
|
||||
);
|
||||
var self = this;
|
||||
fetch('/pos/api/catalog/vehicle-brands', { headers: this._headers() })
|
||||
@@ -130,24 +131,25 @@
|
||||
self.loading(false);
|
||||
self._allBrands = data.brands || [];
|
||||
if (!self._allBrands.length) {
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron marcas.</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">No se encontraron marcas.</div></div>');
|
||||
return;
|
||||
}
|
||||
self.renderBrandList(self._allBrands);
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.loading(false);
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar marcas: ' + escapeHtml(err.message) + '</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">Error al cargar marcas</div><div class="empty-state__subtitle">' + escapeHtml(err.message) + '</div></div>');
|
||||
});
|
||||
},
|
||||
|
||||
renderBrandList: function(brands) {
|
||||
var html = '';
|
||||
var html = '<div class="nav-grid">';
|
||||
brands.forEach(function(b) {
|
||||
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectBrand(' + JSON.stringify(b.name) + ',' + b.id + ')">' +
|
||||
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + escapeHtml(b.name) + '</div>' +
|
||||
html += '<div class="nav-card" onclick=\'BrandCatalog.selectBrand(' + JSON.stringify(b.name) + ',' + b.id + ')\'>' +
|
||||
'<div class="nav-card__name">' + escapeHtml(b.name) + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
this.setContent(html);
|
||||
},
|
||||
|
||||
@@ -186,23 +188,28 @@
|
||||
self.loading(false);
|
||||
var models = data.data || [];
|
||||
if (!models.length) {
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron modelos.</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">No se encontraron modelos.</div></div>');
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
models.forEach(function(m) {
|
||||
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectModel(' + m.id_model + ',' + JSON.stringify(m.display_name || m.name_model) + ')">' +
|
||||
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + escapeHtml(m.display_name || m.name_model) + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
self.setContent(html);
|
||||
self.renderModelList(models);
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.loading(false);
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar modelos: ' + escapeHtml(err.message) + '</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">Error al cargar modelos</div><div class="empty-state__subtitle">' + escapeHtml(err.message) + '</div></div>');
|
||||
});
|
||||
},
|
||||
|
||||
renderModelList: function(models) {
|
||||
var html = '<div class="nav-grid">';
|
||||
models.forEach(function(m) {
|
||||
html += '<div class="nav-card" onclick=\'BrandCatalog.selectModel(' + m.id_model + ',' + JSON.stringify(m.display_name || m.name_model) + ')\'>' +
|
||||
'<div class="nav-card__name">' + escapeHtml(m.display_name || m.name_model) + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
this.setContent(html);
|
||||
},
|
||||
|
||||
selectModel: function(modelId, modelName) {
|
||||
this.nav.model = modelName;
|
||||
this.nav.modelId = modelId;
|
||||
@@ -226,23 +233,28 @@
|
||||
self.loading(false);
|
||||
var years = data.data || [];
|
||||
if (!years.length) {
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron años.</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">No se encontraron años.</div></div>');
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
years.forEach(function(y) {
|
||||
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectYear(' + y.id_year + ',' + y.year_car + ')">' +
|
||||
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + y.year_car + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
self.setContent(html);
|
||||
self.renderYearList(years);
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.loading(false);
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar años: ' + escapeHtml(err.message) + '</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">Error al cargar años</div><div class="empty-state__subtitle">' + escapeHtml(err.message) + '</div></div>');
|
||||
});
|
||||
},
|
||||
|
||||
renderYearList: function(years) {
|
||||
var html = '<div class="nav-grid nav-grid--years">';
|
||||
years.forEach(function(y) {
|
||||
html += '<div class="nav-card nav-card--year" onclick=\'BrandCatalog.selectYear(' + y.id_year + ',' + y.year_car + ')\'>' +
|
||||
'<div class="nav-card__name">' + y.year_car + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
this.setContent(html);
|
||||
},
|
||||
|
||||
selectYear: function(yearId, yearCar) {
|
||||
this.nav.year = yearCar;
|
||||
this.nav.yearId = yearId;
|
||||
@@ -266,24 +278,29 @@
|
||||
self.loading(false);
|
||||
var engines = data.data || [];
|
||||
if (!engines.length) {
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron motores.</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">No se encontraron motores.</div></div>');
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
engines.forEach(function(e) {
|
||||
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectEngine(' + e.id_mye + ',' + JSON.stringify(e.name_engine) + ')">' +
|
||||
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + escapeHtml(e.name_engine) + '</div>' +
|
||||
'<div style="font-size:var(--text-body-sm);color:var(--color-text-muted);">' + escapeHtml(e.trim_level || '') + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
self.setContent(html);
|
||||
self.renderEngineList(engines);
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.loading(false);
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar motores: ' + escapeHtml(err.message) + '</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">Error al cargar motores</div><div class="empty-state__subtitle">' + escapeHtml(err.message) + '</div></div>');
|
||||
});
|
||||
},
|
||||
|
||||
renderEngineList: function(engines) {
|
||||
var html = '<div class="nav-grid">';
|
||||
engines.forEach(function(e) {
|
||||
html += '<div class="nav-card" onclick=\'BrandCatalog.selectEngine(' + e.id_mye + ',' + JSON.stringify(e.name_engine) + ')\'>' +
|
||||
'<div class="nav-card__name">' + escapeHtml(e.name_engine) + '</div>' +
|
||||
'<div class="nav-card__sub">' + escapeHtml(e.trim_level || '') + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
this.setContent(html);
|
||||
},
|
||||
|
||||
selectEngine: function(myeId, engineName) {
|
||||
this.nav.engine = engineName;
|
||||
this.nav.myeId = myeId;
|
||||
@@ -305,26 +322,36 @@
|
||||
.then(function(data) {
|
||||
if (!data) return;
|
||||
self.loading(false);
|
||||
self._allowedBrands = data.allowed_brands || [];
|
||||
var categories = data.data || [];
|
||||
if (!categories.length) {
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron categorias.</p>');
|
||||
var msg = 'No se encontraron categorias.';
|
||||
if (self._allowedBrands.length) {
|
||||
msg = 'Este vehiculo no tiene cobertura de ' + self._allowedBrands.join(', ') + '.';
|
||||
}
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">' + msg + '</div><div class="empty-state__subtitle">Prueba con otro vehiculo o contacta a soporte para ampliar el catalogo.</div></div>');
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
categories.forEach(function(c) {
|
||||
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectCategory(' + c.id_part_category + ',' + JSON.stringify(c.name) + ')">' +
|
||||
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + escapeHtml(c.name) + '</div>' +
|
||||
'<div style="font-size:var(--text-body-sm);color:var(--color-text-muted);">' + (c.part_count || 0) + ' refacciones</div>' +
|
||||
'</div>';
|
||||
});
|
||||
self.setContent(html);
|
||||
self.renderCategoryList(categories);
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.loading(false);
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar categorias: ' + escapeHtml(err.message) + '</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">Error al cargar categorias</div><div class="empty-state__subtitle">' + escapeHtml(err.message) + '</div></div>');
|
||||
});
|
||||
},
|
||||
|
||||
renderCategoryList: function(categories) {
|
||||
var html = '<div class="nav-grid">';
|
||||
categories.forEach(function(c) {
|
||||
html += '<div class="nav-card" onclick=\'BrandCatalog.selectCategory(' + c.id_part_category + ',' + JSON.stringify(c.name) + ')\'>' +
|
||||
'<div class="nav-card__name">' + escapeHtml(c.name) + '</div>' +
|
||||
'<div class="nav-card__sub">' + (c.part_count || 0) + ' refacciones</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
this.setContent(html);
|
||||
},
|
||||
|
||||
selectCategory: function(catId, catName) {
|
||||
this.nav.category = catName;
|
||||
this.nav.categoryId = catId;
|
||||
@@ -340,11 +367,10 @@
|
||||
this.setSearch(
|
||||
'<div style="display:flex;gap:var(--space-2);flex-wrap:wrap;align-items:center;">' +
|
||||
'<input type="text" id="partsSearchInput" placeholder="Buscar refaccion..." value="' + escapeHtml(searchTerm || '') + '" ' +
|
||||
'style="flex:1;min-width:200px;padding:10px 14px;border:1px solid var(--color-border);border-radius:var(--radius-md);' +
|
||||
'font-size:var(--text-body);background:var(--color-surface);color:var(--color-text-primary);outline:none;" ' +
|
||||
'class="level-filter" ' +
|
||||
'onkeydown="if(event.key===\'Enter\')BrandCatalog.searchParts(this.value)">' +
|
||||
'<button class="btn btn--primary btn--sm" onclick="BrandCatalog.searchParts(document.getElementById(\'partsSearchInput\').value)">Buscar</button>' +
|
||||
'<button class="btn btn--secondary btn--sm" onclick="BrandCatalog.clearPartsSearch()">Limpiar</button>' +
|
||||
'<button class="btn btn-primary" onclick="BrandCatalog.searchParts(document.getElementById(\'partsSearchInput\').value)">Buscar</button>' +
|
||||
'<button class="btn btn-ghost" onclick="BrandCatalog.clearPartsSearch()">Limpiar</button>' +
|
||||
'</div>'
|
||||
);
|
||||
var url = '/pos/api/catalog/mye-parts?mye_id=' + encodeURIComponent(myeId) + '&category_id=' + encodeURIComponent(categoryId) +
|
||||
@@ -361,6 +387,7 @@
|
||||
.then(function(data) {
|
||||
if (!data) return;
|
||||
self.loading(false);
|
||||
self._allowedBrands = data.allowed_brands || [];
|
||||
self._lastItems = data.items || [];
|
||||
self._total = data.total || 0;
|
||||
self._offset = data.offset || 0;
|
||||
@@ -368,16 +395,20 @@
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.loading(false);
|
||||
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar refacciones: ' + escapeHtml(err.message) + '</p>');
|
||||
self.setContent('<div class="empty-state is-visible"><div class="empty-state__title">Error al cargar refacciones</div><div class="empty-state__subtitle">' + escapeHtml(err.message) + '</div></div>');
|
||||
});
|
||||
},
|
||||
|
||||
renderPartsList: function(items, searchTerm) {
|
||||
var html = '';
|
||||
if (!items.length) {
|
||||
html += '<div style="grid-column:1/-1;text-align:center;padding:var(--space-8);">' +
|
||||
'<p style="color:var(--color-text-muted);font-size:var(--text-body-lg);">No se encontraron refacciones.</p>' +
|
||||
'<button class="btn btn--primary" style="margin-top:var(--space-3);" onclick="BrandCatalog.loadCategories(' + this.nav.myeId + ')">Volver a categorias</button>' +
|
||||
var msg = 'No se encontraron refacciones.';
|
||||
if (this._allowedBrands.length) {
|
||||
msg = 'No hay refacciones de ' + this._allowedBrands.join(', ') + ' en esta categoria.';
|
||||
}
|
||||
html += '<div class="empty-state is-visible">' +
|
||||
'<div class="empty-state__title">' + msg + '</div>' +
|
||||
'<div class="empty-state__subtitle"><button class="btn btn-primary" onclick="BrandCatalog.loadCategories(' + this.nav.myeId + ')">Volver a categorias</button></div>' +
|
||||
'</div>';
|
||||
this.setContent(html);
|
||||
return;
|
||||
@@ -389,40 +420,55 @@
|
||||
'Mostrando ' + startIdx + '-' + endIdx + ' de ' + this._total + ' refacciones' +
|
||||
'</div>';
|
||||
|
||||
html += '<div style="grid-column:1/-1;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:var(--space-3);">';
|
||||
html += '<div class="nav-grid nav-grid--parts">';
|
||||
items.forEach(function(p) {
|
||||
var price = p.local_price ? '$' + Number(p.local_price).toFixed(2) : 'Consultar precio';
|
||||
var img = '/pos/static/images/placeholder-part.png';
|
||||
var hasAm = !!p.manufacturer;
|
||||
var price = p.local_price
|
||||
? '$' + Number(p.local_price).toFixed(2)
|
||||
: (p.price_usd ? '$' + Number(p.price_usd).toFixed(2) : 'Consultar precio');
|
||||
var stockBadge = p.local_stock > 0
|
||||
? '<span style="display:inline-block;background:var(--color-success);color:#fff;font-size:11px;padding:2px 8px;border-radius:var(--radius-sm);margin-left:6px;">' + p.local_stock + ' en stock</span>'
|
||||
: '<span style="display:inline-block;background:var(--color-text-muted);color:#fff;font-size:11px;padding:2px 8px;border-radius:var(--radius-sm);margin-left:6px;">Sin stock local</span>';
|
||||
html += '<div class="catalog-category-card" style="padding:0;overflow:hidden;display:flex;flex-direction:column;">' +
|
||||
'<div style="height:160px;background:#f5f5f5;display:flex;align-items:center;justify-content:center;">' +
|
||||
'<img src="' + escapeHtml(img) + '" alt="" style="max-width:100%;max-height:100%;object-fit:contain;">' +
|
||||
? '<span class="stock-badge stock-badge--local">En stock</span>'
|
||||
: '<span class="stock-badge stock-badge--none">Sin stock local</span>';
|
||||
var imgHtml = p.image_url
|
||||
? '<img src="' + escapeHtml(p.image_url) + '" alt="">'
|
||||
: '<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>';
|
||||
var brandLine = hasAm
|
||||
? '<div style="font-size:var(--text-caption);color:var(--color-accent);font-weight:600;">' + escapeHtml(p.manufacturer) + '</div>'
|
||||
: '';
|
||||
html += '<div class="part-card">' +
|
||||
'<div class="part-card__image">' + imgHtml + '</div>' +
|
||||
'<div class="part-card__body">' +
|
||||
brandLine +
|
||||
'<div class="part-card__oem">' + escapeHtml(p.oem_part_number || 'N/A') + '</div>' +
|
||||
'<div class="part-card__name">' + escapeHtml(p.name || '') + '</div>' +
|
||||
'</div>' +
|
||||
'<div style="padding:var(--space-3);flex:1;display:flex;flex-direction:column;">' +
|
||||
'<div style="font-weight:600;font-size:var(--text-body);margin-bottom:4px;">' + escapeHtml(p.oem_part_number || 'N/A') + stockBadge + '</div>' +
|
||||
'<div style="font-size:var(--text-body-sm);color:var(--color-text-muted);margin-bottom:8px;flex:1;">' + escapeHtml(p.name || '') + '</div>' +
|
||||
'<div style="font-size:var(--text-h5);font-weight:700;color:var(--color-primary);margin-bottom:8px;">' + price + '</div>' +
|
||||
'<button class="btn btn--primary btn--sm" style="width:100%;" onclick="BrandCatalog.addToCart(' + p.id + ', event)">Agregar</button>' +
|
||||
'<div class="part-card__footer">' +
|
||||
stockBadge +
|
||||
'<span class="part-card__price">' + price + '</span>' +
|
||||
'</div>' +
|
||||
'<button class="btn btn-primary" onclick="BrandCatalog.addToCart(' + p.id + ', event)">Agregar</button>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
html += this.renderPagination();
|
||||
|
||||
this.setContent(html);
|
||||
},
|
||||
|
||||
renderPagination: function() {
|
||||
var hasPrev = this._offset > 0;
|
||||
var hasNext = (this._offset + this._limit) < this._total;
|
||||
var pageNum = Math.floor(this._offset / this._limit) + 1;
|
||||
var totalPages = Math.ceil(this._total / this._limit) || 1;
|
||||
html += '<div style="grid-column:1/-1;display:flex;justify-content:center;align-items:center;gap:var(--space-3);padding:var(--space-4) 0;">' +
|
||||
'<button class="btn btn--secondary" ' + (hasPrev ? '' : 'disabled style="opacity:0.5;cursor:not-allowed;"') +
|
||||
var html = '<div class="pagination">' +
|
||||
'<button class="page-item" ' + (hasPrev ? '' : 'disabled') +
|
||||
' onclick="BrandCatalog.goToPage(' + (this._offset - this._limit) + ')">← Anterior</button>' +
|
||||
'<span style="font-size:var(--text-body-sm);color:var(--color-text-muted);">Pagina ' + pageNum + ' de ' + totalPages + '</span>' +
|
||||
'<button class="btn btn--secondary" ' + (hasNext ? '' : 'disabled style="opacity:0.5;cursor:not-allowed;"') +
|
||||
'<button class="page-item" ' + (hasNext ? '' : 'disabled') +
|
||||
' onclick="BrandCatalog.goToPage(' + (this._offset + this._limit) + ')">Siguiente →</button>' +
|
||||
'</div>';
|
||||
|
||||
this.setContent(html);
|
||||
return html;
|
||||
},
|
||||
|
||||
searchParts: function(term) {
|
||||
@@ -451,16 +497,17 @@
|
||||
return;
|
||||
}
|
||||
if (window.CatalogApp && CatalogApp.addToCart) {
|
||||
var isAftermarket = !!part.manufacturer;
|
||||
CatalogApp.addToCart({
|
||||
id: part.id,
|
||||
id: part.oem_id || part.id,
|
||||
part_number: part.oem_part_number || 'N/A',
|
||||
name: part.name || 'Refaccion',
|
||||
brand: '',
|
||||
price: part.local_price || 0,
|
||||
brand: part.manufacturer || '',
|
||||
price: part.local_price || part.price_usd || 0,
|
||||
tax_rate: 0.16,
|
||||
unit: 'PZA',
|
||||
stock: part.local_stock || 0,
|
||||
source: 'oem-brand',
|
||||
source: isAftermarket ? 'aftermarket' : 'oem-brand',
|
||||
inventory_id: null
|
||||
}, 1);
|
||||
var btn = event.target;
|
||||
@@ -482,4 +529,12 @@
|
||||
}
|
||||
|
||||
window.BrandCatalog = BrandCatalog;
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
registerCmdKItem({ group: "Principal", label: "POS Ventas", href: "/pos/sale", icon: "🛒" });
|
||||
registerCmdKItem({ group: "Principal", label: "Catálogo", href: "/pos/catalog", icon: "📁" });
|
||||
registerCmdKItem({ group: "Principal", label: "Clientes", href: "/pos/customers", icon: "👤" });
|
||||
registerCmdKItem({ group: "Principal", label: "Dashboard", href: "/pos/dashboard", icon: "📊" });
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -46,6 +46,11 @@
|
||||
var checkoutBtn = document.getElementById('checkoutBtn');
|
||||
var cartFab = document.getElementById('cartFab');
|
||||
var cartCloseBtn = document.getElementById('cartCloseBtn');
|
||||
// Supplier prices upload
|
||||
var uploadPricesBtn = document.getElementById('uploadPricesBtn');
|
||||
var uploadPricesModal= document.getElementById('uploadPricesModal');
|
||||
var uploadPricesFile = document.getElementById('uploadPricesFile');
|
||||
var uploadPricesStatus=document.getElementById('uploadPricesStatus');
|
||||
|
||||
// ─── Navigation State ───
|
||||
var nav = {
|
||||
@@ -195,7 +200,19 @@
|
||||
currentAbort = null;
|
||||
}
|
||||
var opts = { headers: headers };
|
||||
if (url.indexOf('/pos/api/') === 0 && url.indexOf('mode=') !== -1 || url.indexOf('/years') !== -1 || url.indexOf('/brands') !== -1 || url.indexOf('/models') !== -1 || url.indexOf('/engines') !== -1 || url.indexOf('/categories') !== -1 || url.indexOf('/groups') !== -1 || url.indexOf('/part-types') !== -1 || url.indexOf('/parts') !== -1 || url.indexOf('/search') !== -1) {
|
||||
var isCatalogNav = url.indexOf('/pos/api/') === 0 && (
|
||||
url.indexOf('mode=') !== -1 ||
|
||||
url.indexOf('/years') !== -1 ||
|
||||
url.indexOf('/brands') !== -1 ||
|
||||
url.indexOf('/models') !== -1 ||
|
||||
url.indexOf('/engines') !== -1 ||
|
||||
url.indexOf('/categories') !== -1 ||
|
||||
url.indexOf('/groups') !== -1 ||
|
||||
url.indexOf('/part-types') !== -1 ||
|
||||
url.indexOf('/parts') !== -1 ||
|
||||
url.indexOf('/search') !== -1
|
||||
);
|
||||
if (isCatalogNav) {
|
||||
currentAbort = new AbortController();
|
||||
opts.signal = currentAbort.signal;
|
||||
}
|
||||
@@ -233,7 +250,7 @@
|
||||
if (!s) return '';
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
return d.innerHTML.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ─── Breadcrumb ───
|
||||
@@ -304,9 +321,9 @@
|
||||
|
||||
function resetNav() {
|
||||
nav.level = 'brands';
|
||||
pushNavState();
|
||||
nav.brand = nav.model = nav.year = nav.engine = nav.category = nav.group = nav.partType = null;
|
||||
nav.nxGroup = nav.nxSubgroup = nav.nxPartType = null;
|
||||
pushNavState();
|
||||
}
|
||||
|
||||
function resetNavFrom(level) {
|
||||
@@ -363,10 +380,14 @@
|
||||
var cacheKey = 'nexus:brands:' + catalogMode;
|
||||
var cached = sessionStorage.getItem(cacheKey);
|
||||
if (cached) {
|
||||
hideLoading();
|
||||
var data = JSON.parse(cached);
|
||||
renderBrands(data);
|
||||
return;
|
||||
try {
|
||||
hideLoading();
|
||||
var data = JSON.parse(cached);
|
||||
renderBrands(data);
|
||||
return;
|
||||
} catch (e) {
|
||||
sessionStorage.removeItem(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
apiFetch(API + '/brands?mode=' + catalogMode).then(function (data) {
|
||||
@@ -937,9 +958,14 @@
|
||||
partsGrid.querySelectorAll('.part-card').forEach(function (card) {
|
||||
card.addEventListener('click', function () {
|
||||
var pid = this.dataset.partId;
|
||||
var src = this.dataset.source || '';
|
||||
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
|
||||
return;
|
||||
}
|
||||
if (src === 'supplier_catalog' || (typeof pid === 'string' && pid.indexOf('sc:') === 0)) {
|
||||
openSupplierDetail(pid.replace('sc:', ''));
|
||||
return;
|
||||
}
|
||||
openPartDetail(parseInt(pid));
|
||||
});
|
||||
});
|
||||
@@ -998,17 +1024,23 @@
|
||||
partsGrid.innerHTML = data.data.map(function (p) {
|
||||
// Stock badge — prefer tenant stock, then warehouse network, else fallback
|
||||
var stockBadge;
|
||||
if (p.local_stock > 0) {
|
||||
var isSupplier = p.source === 'supplier_catalog' || (typeof p.id_part === 'string' && p.id_part.indexOf('sc:') === 0);
|
||||
if (isSupplier) {
|
||||
stockBadge = '<span class="stock-badge stock-badge--none" style="background:#f59e0b;color:#fff;">Cat. Proveedor</span>';
|
||||
} else if (p.local_stock > 0) {
|
||||
stockBadge = '<span class="stock-badge stock-badge--local">En stock: ' + p.local_stock + '</span>';
|
||||
} else if (p.in_stock_network || p.bodega_count > 0) {
|
||||
stockBadge = '<span class="stock-badge stock-badge--bodega">' + p.bodega_count + ' bodega' + (p.bodega_count > 1 ? 's' : '') + '</span>';
|
||||
} else {
|
||||
stockBadge = '<span class="stock-badge stock-badge--none">Sin stock</span>';
|
||||
}
|
||||
// Local inventory native badge
|
||||
var sourceBadge = p.source === 'local_inventory'
|
||||
? '<span class="stock-badge stock-badge--local" style="margin-left:4px;background:#4f46e5;">Stock Local</span>'
|
||||
: '';
|
||||
// Source badge for local inventory or supplier catalog
|
||||
var sourceBadge = '';
|
||||
if (p.source === 'local_inventory') {
|
||||
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-left:4px;background:#4f46e5;">Stock Local</span>';
|
||||
} else if (isSupplier) {
|
||||
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-left:4px;background:#f59e0b;color:#fff;">Cat. Proveedor</span>';
|
||||
}
|
||||
|
||||
var imgHtml = p.image_url
|
||||
? '<img src="' + esc(p.image_url) + '" alt="' + esc(p.name) + '" loading="lazy" decoding="async">'
|
||||
@@ -1040,6 +1072,7 @@
|
||||
'</div>' +
|
||||
'<div class="part-card__footer">' +
|
||||
(p.local_price ? '<span class="part-card__price">$' + fmt(p.local_price) + '</span>' : '<span class="part-card__price" style="color:var(--color-text-muted);">Sin precio</span>') +
|
||||
(p.supplier_price ? '<span class="part-card__price" style="color:#2d7d46;font-size:0.85em;">Prov: $' + fmt(p.supplier_price) + '</span>' : '') +
|
||||
stockBadge +
|
||||
'</div>' +
|
||||
'</article>';
|
||||
@@ -1049,10 +1082,15 @@
|
||||
partsGrid.querySelectorAll('.part-card').forEach(function (card) {
|
||||
card.addEventListener('click', function () {
|
||||
var pid = this.dataset.partId;
|
||||
var src = this.dataset.source || '';
|
||||
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
|
||||
// local-inventory item: info already visible on card
|
||||
return;
|
||||
}
|
||||
if (src === 'supplier_catalog' || (typeof pid === 'string' && pid.indexOf('sc:') === 0)) {
|
||||
openSupplierDetail(pid.replace('sc:', ''));
|
||||
return;
|
||||
}
|
||||
openPartDetail(parseInt(pid));
|
||||
});
|
||||
});
|
||||
@@ -1195,6 +1233,73 @@
|
||||
});
|
||||
}
|
||||
|
||||
function openSupplierDetail(supplierId) {
|
||||
detailBody.innerHTML = '<div class="loading is-visible"><div class="spinner"></div></div>';
|
||||
detailFooter.style.display = 'none';
|
||||
detailPanel.classList.add('is-open');
|
||||
detailOverlay.classList.add('is-visible');
|
||||
|
||||
apiFetch('/pos/api/supplier-catalog/items/' + supplierId).then(function (data) {
|
||||
if (!data || data.error) {
|
||||
detailBody.innerHTML = '<p style="color:var(--color-error);padding:var(--space-4);">Error al cargar detalle.</p>';
|
||||
return;
|
||||
}
|
||||
var p = data;
|
||||
var html = '';
|
||||
|
||||
html += '<div class="detail-section">';
|
||||
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-1);">' + esc(p.supplier_name) + ' > ' + esc(p.category || '') + '</div>';
|
||||
html += '<div class="detail-oem">' + esc(p.sku) + '</div>';
|
||||
html += '<div class="detail-name">' + esc((p.name || '').replace(/\\n/g, ' ')) + '</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="" loading="lazy" decoding="async" style="max-width:100%;max-height:200px;object-fit:contain;border-radius:var(--radius-sm);"></div>';
|
||||
html += '</div>';
|
||||
|
||||
// Interchanges
|
||||
if (p.interchanges && p.interchanges.length) {
|
||||
html += '<div class="detail-section">';
|
||||
html += '<div class="detail-section__title">Intercambios OEM</div>';
|
||||
var seen = {};
|
||||
p.interchanges.forEach(function(ix) {
|
||||
var key = (ix.brand || '') + '|' + (ix.interchange_number || '');
|
||||
if (seen[key]) return;
|
||||
seen[key] = true;
|
||||
html += '<div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--color-border);">' +
|
||||
'<span style="font-weight:600;">' + esc(ix.brand || '') + '</span>' +
|
||||
'<span style="color:var(--color-text-muted);font-family:monospace;">' + esc(ix.interchange_number || '') + '</span>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Compatibilities — deduplicate by (make, model, year, engine)
|
||||
if (p.compatibilities && p.compatibilities.length) {
|
||||
html += '<div class="detail-section">';
|
||||
html += '<div class="detail-section__title">Vehiculos compatibles</div>';
|
||||
var seenCompat = {};
|
||||
var uniqCompat = [];
|
||||
p.compatibilities.forEach(function(c) {
|
||||
var key = (c.make || '') + '|' + (c.model || '') + '|' + (c.year || '') + '|' + (c.engine || '');
|
||||
if (seenCompat[key]) return;
|
||||
seenCompat[key] = true;
|
||||
uniqCompat.push(c);
|
||||
});
|
||||
var currentMake = '';
|
||||
uniqCompat.forEach(function(c) {
|
||||
if (c.make !== currentMake) {
|
||||
currentMake = c.make;
|
||||
html += '<div style="font-weight:600;margin-top:8px;">' + esc(c.make) + '</div>';
|
||||
}
|
||||
html += '<div style="padding-left:12px;color:var(--color-text-muted);font-size:var(--text-body-sm);">' +
|
||||
esc(c.model) + ' ' + c.year + ' ' + esc(c.engine || '') + '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
detailBody.innerHTML = html;
|
||||
});
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
detailPanel.classList.remove('is-open');
|
||||
detailOverlay.classList.remove('is-visible');
|
||||
@@ -1408,17 +1513,22 @@
|
||||
}
|
||||
searchDropdown.innerHTML = data.data.map(function (r) {
|
||||
var isLocal = r.source === 'local_inventory' || (typeof r.id_part === 'string' && r.id_part.indexOf('inv:') === 0);
|
||||
var isSupplier = r.source === 'supplier_catalog' || (typeof r.id_part === 'string' && r.id_part.indexOf('sc:') === 0);
|
||||
var stockLabel = r.local_stock > 0
|
||||
? '<span class="stock-badge stock-badge--local" style="margin-left:auto;">Stock: ' + r.local_stock + '</span>'
|
||||
: '';
|
||||
var localBadge = isLocal
|
||||
? '<span class="stock-badge stock-badge--local" style="margin-right:4px;background:#4f46e5;">Stock Local</span>'
|
||||
: '';
|
||||
var sourceBadge = '';
|
||||
if (isLocal) {
|
||||
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-right:4px;background:#4f46e5;">Stock Local</span>';
|
||||
} else if (isSupplier) {
|
||||
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-right:4px;background:#f59e0b;color:#fff;">Cat. Proveedor</span>';
|
||||
}
|
||||
var oemNum = isLocal ? (r.oem_part_number || r.part_number || '') : (r.oem_part_number || '');
|
||||
return '<div class="search-result-item" data-part-id="' + r.id_part + '" data-name="' + esc(r.name) + '" data-pn="' + esc(oemNum) + '" data-price="' + (r.local_price || '') + '" data-stock="' + (r.local_stock || 0) + '">' +
|
||||
var cleanName = (r.name || '').replace(/\\n/g, ' ');
|
||||
return '<div class="search-result-item" data-part-id="' + r.id_part + '" data-name="' + esc(cleanName) + '" data-pn="' + esc(oemNum) + '" data-price="' + (r.local_price || '') + '" data-stock="' + (r.local_stock || 0) + '" data-source="' + (r.source || '') + '">' +
|
||||
'<div style="flex:1;">' +
|
||||
'<div class="search-result__oem">' + localBadge + esc(oemNum) + '</div>' +
|
||||
'<div class="search-result__name">' + esc(r.name) + '</div>' +
|
||||
'<div class="search-result__oem">' + sourceBadge + esc(oemNum) + '</div>' +
|
||||
'<div class="search-result__name">' + esc(cleanName) + '</div>' +
|
||||
(r.vehicle_info ? '<div class="search-result__vehicle">' + esc(r.vehicle_info) + '</div>' : '') +
|
||||
'</div>' +
|
||||
stockLabel +
|
||||
@@ -1430,6 +1540,7 @@
|
||||
el.addEventListener('click', function () {
|
||||
searchDropdown.classList.remove('is-visible');
|
||||
var pid = this.dataset.partId;
|
||||
var src = this.dataset.source || '';
|
||||
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
|
||||
var info = '💠 Stock Local\n\n' +
|
||||
'Parte: ' + (this.dataset.pn || 'N/A') + '\n' +
|
||||
@@ -1439,6 +1550,10 @@
|
||||
alert(info);
|
||||
return;
|
||||
}
|
||||
if (src === 'supplier_catalog' || (typeof pid === 'string' && pid.indexOf('sc:') === 0)) {
|
||||
openSupplierDetail(pid.replace('sc:', ''));
|
||||
return;
|
||||
}
|
||||
openPartDetail(parseInt(pid));
|
||||
});
|
||||
});
|
||||
@@ -1645,8 +1760,13 @@
|
||||
var cacheKey = 'nexus:years-all';
|
||||
var cached = sessionStorage.getItem(cacheKey);
|
||||
if (cached) {
|
||||
var data = JSON.parse(cached);
|
||||
var years = data.data || data || [];
|
||||
try {
|
||||
var data = JSON.parse(cached);
|
||||
var years = data.data || data || [];
|
||||
} catch (e) {
|
||||
sessionStorage.removeItem(cacheKey);
|
||||
var years = [];
|
||||
}
|
||||
if (!years.length) {
|
||||
years = [];
|
||||
for (var y = 2026; y >= 1990; y--) years.push({ id_year: y, year_car: y });
|
||||
@@ -2046,6 +2166,53 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Supplier prices upload ─────────────────────────────────────────────
|
||||
function openUploadPricesModal() {
|
||||
if (uploadPricesModal) uploadPricesModal.style.display = 'flex';
|
||||
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '';
|
||||
if (uploadPricesFile) uploadPricesFile.value = '';
|
||||
}
|
||||
function closeUploadPricesModal() {
|
||||
if (uploadPricesModal) uploadPricesModal.style.display = 'none';
|
||||
}
|
||||
async function submitUploadPrices() {
|
||||
if (!uploadPricesFile || !uploadPricesFile.files || !uploadPricesFile.files[0]) {
|
||||
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '<span style="color:var(--color-error);">Selecciona un archivo primero.</span>';
|
||||
return;
|
||||
}
|
||||
var form = new FormData();
|
||||
form.append('file', uploadPricesFile.files[0]);
|
||||
if (uploadPricesStatus) uploadPricesStatus.innerHTML = 'Subiendo...';
|
||||
try {
|
||||
var res = await fetch('/pos/api/supplier-catalog/prices/upload', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
body: form
|
||||
});
|
||||
var data = await res.json();
|
||||
if (res.ok && data.success) {
|
||||
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '<span style="color:var(--color-success);">✓ Precios actualizados: ' + data.processed + ' (insertados: ' + data.inserted + ', actualizados: ' + data.updated + ')</span>';
|
||||
uploadPricesFile.value = '';
|
||||
} else {
|
||||
var msg = data.error || 'Error al subir precios';
|
||||
var details = (data.details || []).join('<br>');
|
||||
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '<span style="color:var(--color-error);">' + esc(msg) + '</span>' + (details ? '<div style="margin-top:4px;font-size:0.9em;">' + details + '</div>' : '');
|
||||
}
|
||||
} catch (e) {
|
||||
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '<span style="color:var(--color-error);">Error de red: ' + esc(e.message) + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function shouldShowUploadPricesButton() {
|
||||
try {
|
||||
var user = JSON.parse(localStorage.getItem('pos_employee') || '{}');
|
||||
return user.role === 'owner' || user.role === 'admin';
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
if (uploadPricesBtn && shouldShowUploadPricesButton()) {
|
||||
uploadPricesBtn.style.display = 'inline-flex';
|
||||
}
|
||||
|
||||
window.CatalogApp = {
|
||||
toggleCart: toggleCart,
|
||||
goToCheckout: goToCheckout,
|
||||
@@ -2065,6 +2232,9 @@
|
||||
togglePlate: togglePlate,
|
||||
lookupPlate: lookupPlate,
|
||||
setMode: setCatalogMode,
|
||||
openUploadPricesModal: openUploadPricesModal,
|
||||
closeUploadPricesModal: closeUploadPricesModal,
|
||||
submitUploadPrices: submitUploadPrices,
|
||||
};
|
||||
|
||||
// ─── INIT ───
|
||||
|
||||
1
pos/static/js/catalog.min.js
vendored
1
pos/static/js/catalog.min.js
vendored
File diff suppressed because one or more lines are too long
1
pos/static/js/chat.min.js
vendored
1
pos/static/js/chat.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -19,6 +19,11 @@ const Config = (() => {
|
||||
return true;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
return String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function headers() {
|
||||
return { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' };
|
||||
}
|
||||
@@ -156,7 +161,7 @@ const Config = (() => {
|
||||
|
||||
_branches.forEach(function(b, idx) {
|
||||
var statusBadge = b.is_active
|
||||
? '<span class="badge badge--ok" style="padding:0 4px;font-size:0.625rem;">' + (idx === 0 ? 'Principal' : 'Activa') + '</span>'
|
||||
? '<span class="badge badge--ok" style="padding:0 4px;font-size:0.625rem;">' + (b.is_main ? 'Principal' : 'Activa') + '</span>'
|
||||
: '<span class="badge badge--inactive" style="padding:0 4px;font-size:0.625rem;">Inactiva</span>';
|
||||
|
||||
html += '<div class="device-card">'
|
||||
@@ -165,14 +170,20 @@ const Config = (() => {
|
||||
+ '</div>'
|
||||
+ '<div class="device-card__body">'
|
||||
+ '<div class="device-card__name">' + escHtml(b.name) + '</div>'
|
||||
+ '<div class="device-card__detail">' + statusBadge + '</div>'
|
||||
+ '<div class="device-card__detail">' + statusBadge
|
||||
+ (b.rfc ? ' · RFC: ' + escHtml(b.rfc) : '')
|
||||
+ (b.cp ? ' · CP: ' + escHtml(b.cp) : '')
|
||||
+ '</div>'
|
||||
+ (b.address ? '<div class="device-card__detail">' + escHtml(b.address) + '</div>' : '')
|
||||
+ (b.phone ? '<div class="device-card__detail">' + escHtml(b.phone) + '</div>' : '')
|
||||
+ '</div>'
|
||||
+ '<div class="device-card__actions">'
|
||||
+ '<button class="btn btn--ghost btn--sm" onclick="Config.editBranch(' + b.id + ')">Editar</button>'
|
||||
+ '</div></div>';
|
||||
});
|
||||
|
||||
// "Agregar Sucursal" card
|
||||
html += '<div class="device-card" style="border-style:dashed;cursor:pointer;" onclick="Config.openModal(\'modal-branch\')">'
|
||||
html += '<div class="device-card" style="border-style:dashed;cursor:pointer;" onclick="Config.openBranchModal()">'
|
||||
+ '<div class="device-card__icon" style="background:transparent;border:2px dashed var(--color-border);">'
|
||||
+ '<svg viewBox="0 0 24 24" style="stroke:var(--color-text-muted);"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>'
|
||||
+ '</div>'
|
||||
@@ -198,9 +209,36 @@ const Config = (() => {
|
||||
});
|
||||
}
|
||||
|
||||
function openBranchModal(branch) {
|
||||
document.getElementById('branch-modal-title').textContent = branch ? 'Editar Sucursal' : 'Nueva Sucursal';
|
||||
document.getElementById('branch-id').value = branch ? branch.id : '';
|
||||
document.getElementById('branch-name').value = branch ? branch.name : '';
|
||||
document.getElementById('branch-rfc').value = branch ? (branch.rfc || '') : '';
|
||||
document.getElementById('branch-razon').value = branch ? (branch.razon_social || '') : '';
|
||||
document.getElementById('branch-regimen').value = branch ? (branch.regimen_fiscal || '') : '';
|
||||
document.getElementById('branch-cp').value = branch ? (branch.cp || '') : '';
|
||||
document.getElementById('branch-direccion-fiscal').value = branch ? (branch.direccion_fiscal || '') : '';
|
||||
document.getElementById('branch-serie').value = branch ? (branch.serie_cfdi || '') : '';
|
||||
document.getElementById('branch-folio-inicio').value = branch ? (branch.folio_inicio || '') : '';
|
||||
document.getElementById('branch-folio-actual').value = branch ? (branch.folio_actual || '') : '';
|
||||
document.getElementById('branch-email').value = branch ? (branch.email || '') : '';
|
||||
document.getElementById('branch-address').value = branch ? (branch.address || '') : '';
|
||||
document.getElementById('branch-phone').value = branch ? (branch.phone || '') : '';
|
||||
document.getElementById('branch-main').checked = branch ? !!branch.is_main : false;
|
||||
openModal('modal-branch');
|
||||
}
|
||||
|
||||
function editBranch(branchId) {
|
||||
var b = _branches.find(function(x) { return x.id === branchId; });
|
||||
if (!b) { toast('Sucursal no encontrada', 'error'); return; }
|
||||
openBranchModal(b);
|
||||
}
|
||||
|
||||
async function saveBranch(data) {
|
||||
var res = await fetch(API + '/branches', {
|
||||
method: 'POST',
|
||||
var branchId = document.getElementById('branch-id').value;
|
||||
var url = API + '/branches' + (branchId ? '/' + branchId : '');
|
||||
var res = await fetch(url, {
|
||||
method: branchId ? 'PUT' : 'POST',
|
||||
headers: headers(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
@@ -424,14 +462,36 @@ const Config = (() => {
|
||||
try {
|
||||
await saveBranch({
|
||||
name: name,
|
||||
address: document.getElementById('branch-address').value.trim(),
|
||||
phone: document.getElementById('branch-phone').value.trim()
|
||||
rfc: document.getElementById('branch-rfc').value.trim() || null,
|
||||
razon_social: document.getElementById('branch-razon').value.trim() || null,
|
||||
regimen_fiscal: document.getElementById('branch-regimen').value.trim() || null,
|
||||
cp: document.getElementById('branch-cp').value.trim() || null,
|
||||
direccion_fiscal: document.getElementById('branch-direccion-fiscal').value.trim() || null,
|
||||
serie_cfdi: document.getElementById('branch-serie').value.trim() || null,
|
||||
folio_inicio: document.getElementById('branch-folio-inicio').value ? parseInt(document.getElementById('branch-folio-inicio').value, 10) : null,
|
||||
folio_actual: document.getElementById('branch-folio-actual').value ? parseInt(document.getElementById('branch-folio-actual').value, 10) : null,
|
||||
email: document.getElementById('branch-email').value.trim() || null,
|
||||
address: document.getElementById('branch-address').value.trim() || null,
|
||||
phone: document.getElementById('branch-phone').value.trim() || null,
|
||||
is_main: document.getElementById('branch-main').checked,
|
||||
});
|
||||
toast('Sucursal creada');
|
||||
toast('Sucursal guardada');
|
||||
closeModal('modal-branch');
|
||||
// Reset form
|
||||
document.getElementById('branch-id').value = '';
|
||||
document.getElementById('branch-name').value = '';
|
||||
document.getElementById('branch-rfc').value = '';
|
||||
document.getElementById('branch-razon').value = '';
|
||||
document.getElementById('branch-regimen').value = '';
|
||||
document.getElementById('branch-cp').value = '';
|
||||
document.getElementById('branch-direccion-fiscal').value = '';
|
||||
document.getElementById('branch-serie').value = '';
|
||||
document.getElementById('branch-folio-inicio').value = '';
|
||||
document.getElementById('branch-folio-actual').value = '';
|
||||
document.getElementById('branch-email').value = '';
|
||||
document.getElementById('branch-address').value = '';
|
||||
document.getElementById('branch-phone').value = '';
|
||||
document.getElementById('branch-main').checked = false;
|
||||
await loadBranches();
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
@@ -623,6 +683,117 @@ const Config = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Allowed Part Brands
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadAllowedBrands() {
|
||||
var container = document.getElementById('allowed-brands-container');
|
||||
if (!container) return;
|
||||
try {
|
||||
var res = await fetch(API + '/available-brands', { headers: headers() });
|
||||
if (!res.ok) throw new Error('Failed to load brands');
|
||||
var d = await res.json();
|
||||
var allBrands = d.brands || [];
|
||||
|
||||
var res2 = await fetch(API + '/allowed-brands', { headers: headers() });
|
||||
if (!res2.ok) throw new Error('Failed to load allowed brands');
|
||||
var d2 = await res2.json();
|
||||
var allowed = d2.brands || [];
|
||||
|
||||
if (!allBrands.length) {
|
||||
container.innerHTML = '<p style="color:var(--color-text-muted);font-size:var(--text-body-sm);">No hay marcas disponibles.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
allBrands.forEach(function(b) {
|
||||
var checked = allowed.indexOf(b) !== -1 ? 'checked' : '';
|
||||
html += '<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer;font-size:var(--text-body-sm);color:var(--color-text-primary);padding:var(--space-1);">' +
|
||||
'<input type="checkbox" value="' + escapeHtml(b) + '" data-brand-checkbox ' + checked + ' style="width:16px;height:16px;cursor:pointer;">' +
|
||||
escapeHtml(b) + '</label>';
|
||||
});
|
||||
container.innerHTML = html;
|
||||
} catch (e) {
|
||||
console.error('Config.loadAllowedBrands:', e);
|
||||
if (container) container.innerHTML = '<p style="color:var(--color-error);font-size:var(--text-body-sm);">Error al cargar marcas.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAllowedBrands() {
|
||||
var btn = document.getElementById('btn-save-allowed-brands');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; }
|
||||
try {
|
||||
var checked = [];
|
||||
document.querySelectorAll('[data-brand-checkbox]').forEach(function(cb) {
|
||||
if (cb.checked) checked.push(cb.value);
|
||||
});
|
||||
var res = await fetch(API + '/allowed-brands', {
|
||||
method: 'PUT',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({ brands: checked })
|
||||
});
|
||||
if (!res.ok) {
|
||||
var err = await res.json().catch(function() { return { error: res.statusText }; });
|
||||
throw new Error(err.error || 'Save failed');
|
||||
}
|
||||
toast('Marcas permitidas actualizadas');
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Guardar Marcas'; }
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Modules / Integrations
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadModules() {
|
||||
try {
|
||||
var res = await fetch(API + '/modules', { headers: headers() });
|
||||
if (!res.ok) return;
|
||||
var data = await res.json();
|
||||
var cbWa = document.getElementById('cfg-module-whatsapp');
|
||||
var cbMp = document.getElementById('cfg-module-marketplace');
|
||||
var cbMeli = document.getElementById('cfg-module-meli');
|
||||
var cbCat = document.getElementById('cfg-module-catalog');
|
||||
if (cbWa) cbWa.checked = data.whatsapp !== false;
|
||||
if (cbMp) cbMp.checked = data.marketplace !== false;
|
||||
if (cbMeli) cbMeli.checked = data.meli !== false;
|
||||
if (cbCat) cbCat.checked = data.catalog !== false;
|
||||
localStorage.setItem('pos_modules', JSON.stringify(data));
|
||||
} catch (e) {
|
||||
console.error('Config.loadModules:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveModules() {
|
||||
var btn = event.target;
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; }
|
||||
try {
|
||||
var data = {
|
||||
whatsapp: document.getElementById('cfg-module-whatsapp').checked,
|
||||
marketplace: document.getElementById('cfg-module-marketplace').checked,
|
||||
meli: document.getElementById('cfg-module-meli').checked,
|
||||
catalog: document.getElementById('cfg-module-catalog').checked,
|
||||
};
|
||||
var res = await fetch(API + '/modules', {
|
||||
method: 'PUT',
|
||||
headers: headers(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!res.ok) {
|
||||
var err = await res.json().catch(function() { return { error: res.statusText }; });
|
||||
throw new Error(err.error || 'Save failed');
|
||||
}
|
||||
localStorage.setItem('pos_modules', JSON.stringify(data));
|
||||
toast('Módulos actualizados');
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Guardar módulos'; }
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Init
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -650,6 +821,12 @@ const Config = (() => {
|
||||
btnCompat.addEventListener('click', saveVehicleCompatSource);
|
||||
}
|
||||
|
||||
// Allowed brands save button
|
||||
var btnBrands = document.getElementById('btn-save-allowed-brands');
|
||||
if (btnBrands) {
|
||||
btnBrands.addEventListener('click', saveAllowedBrands);
|
||||
}
|
||||
|
||||
// Kiosk mode toggle
|
||||
var kioskToggle = document.getElementById('cfg-kiosk-mode');
|
||||
if (kioskToggle && window.NexusKiosk) {
|
||||
@@ -671,15 +848,26 @@ const Config = (() => {
|
||||
loadBusiness();
|
||||
loadCurrency();
|
||||
loadVehicleCompatSource();
|
||||
loadAllowedBrands();
|
||||
loadModules();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
return {
|
||||
init, setTheme, selectThemeOption,
|
||||
init, setTheme, selectThemeOption, loadAllowedBrands, saveAllowedBrands,
|
||||
loadBranches, loadEmployees, saveBranch, saveEmployee, editEmployee,
|
||||
loadBusiness, saveBusiness, saveTaxParams,
|
||||
loadCurrency, saveCurrency,
|
||||
openModal, closeModal
|
||||
loadModules, saveModules,
|
||||
openModal, closeModal, openBranchModal, editBranch
|
||||
};
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
registerCmdKItem({ group: "Principal", label: "POS Ventas", href: "/pos/sale", icon: "🛒" });
|
||||
registerCmdKItem({ group: "Principal", label: "Catálogo", href: "/pos/catalog", icon: "📁" });
|
||||
registerCmdKItem({ group: "Principal", label: "Clientes", href: "/pos/customers", icon: "👤" });
|
||||
registerCmdKItem({ group: "Principal", label: "Dashboard", href: "/pos/dashboard", icon: "📊" });
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
1
pos/static/js/config.min.js
vendored
1
pos/static/js/config.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -94,6 +94,8 @@ const Customers = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
var selectedCustomers = new Set();
|
||||
|
||||
function renderCustomerRow(c) {
|
||||
const tier = tierMap[c.price_tier] || 'Mostrador';
|
||||
const tClass = tierClass[c.price_tier] || 'mostrador';
|
||||
@@ -104,11 +106,13 @@ const Customers = (() => {
|
||||
const creditClass = usedPct >= 80 ? 'none' : usedPct >= 60 ? 'low' : '';
|
||||
const num = String(c.id).padStart(5, '0');
|
||||
const selClass = (currentCustomer && currentCustomer.id === c.id) ? 'selected' : '';
|
||||
return '<tr class="' + selClass + '" onclick="selectCustomer(' + c.id + ')">' +
|
||||
const isChecked = selectedCustomers.has(c.id) ? 'checked' : '';
|
||||
return '<tr class="' + selClass + '">' +
|
||||
'<td onclick="event.stopPropagation();"><input type="checkbox" ' + isChecked + ' onchange="Customers.toggleCustomerSelection(' + c.id + ')"></td>' +
|
||||
'<td class="cell-num">' + num + '</td>' +
|
||||
'<td>' +
|
||||
'<div class="cell-name">' + (c.name || '') + '</div>' +
|
||||
'<div class="cell-name-sub hide-mobile">' + (c.email || '') + '</div>' +
|
||||
'<div class="cell-name-sub hide-mobile">' + (c.razon_social || c.email || '') + '</div>' +
|
||||
'</td>' +
|
||||
'<td class="cell-rfc hide-mobile">' + (c.rfc || '-') + '</td>' +
|
||||
'<td class="hide-mobile">' + (c.phone || '-') + '</td>' +
|
||||
@@ -127,7 +131,12 @@ const Customers = (() => {
|
||||
if (!tbody) return;
|
||||
|
||||
if (!customers || customers.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;padding:var(--space-8);color:var(--color-text-muted);">Sin resultados.</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="9">' + renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
|
||||
title: 'Sin clientes',
|
||||
subtitle: 'No se encontraron clientes registrados.',
|
||||
action: '<button class="btn btn--primary btn--sm" onclick="Customers.openCreateModal()">Nuevo cliente</button>'
|
||||
}) + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,7 +146,7 @@ const Customers = (() => {
|
||||
rowHeight: 52,
|
||||
buffer: 3,
|
||||
renderRow: renderCustomerRow,
|
||||
emptyHtml: '<tr><td colspan="9" style="text-align:center;padding:var(--space-8);color:var(--color-text-muted);">Sin resultados.</td></tr>'
|
||||
emptyHtml: '<tr><td colspan="9">' + renderEmptyState({ title: 'Sin clientes', subtitle: 'No hay clientes registrados.' }) + '</td></tr>'
|
||||
});
|
||||
}
|
||||
customersVS.setData(customers);
|
||||
@@ -240,7 +249,9 @@ const Customers = (() => {
|
||||
|
||||
// Contact
|
||||
const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val || '-'; };
|
||||
set('detailRazonSocial', c.razon_social);
|
||||
set('detailAddress', c.address);
|
||||
set('detailCp', c.cp);
|
||||
set('detailPhone', c.phone);
|
||||
set('detailEmail', c.email);
|
||||
set('detailSince', formatDate(c.created_at));
|
||||
@@ -263,6 +274,13 @@ const Customers = (() => {
|
||||
bar.className = `progress-bar__fill ${pct < 40 ? 'low' : pct > 75 ? 'high' : ''}`;
|
||||
}
|
||||
|
||||
// Discount
|
||||
const discountEl = document.getElementById('detailMaxDiscount');
|
||||
if (discountEl) discountEl.textContent = (c.max_discount_pct || 0) + '%';
|
||||
|
||||
// Re-wire action buttons after detail panel is visible
|
||||
wireActionButtons();
|
||||
|
||||
// Purchase History
|
||||
const hbody = document.getElementById('historyBody');
|
||||
if (hbody) {
|
||||
@@ -363,7 +381,7 @@ const Customers = (() => {
|
||||
const btns = document.querySelectorAll('.quick-actions .action-btn');
|
||||
// Order: Nueva Venta, Editar, Estado de Cuenta, Historial
|
||||
if (btns.length >= 1) btns[0].onclick = () => {
|
||||
if (currentCustomer) window.location.href = '/pos/?customer=' + currentCustomer.id;
|
||||
if (currentCustomer) window.location.href = '/pos/sale?customer=' + currentCustomer.id;
|
||||
};
|
||||
if (btns.length >= 2) btns[1].onclick = () => editCurrent();
|
||||
if (btns.length >= 3) btns[2].onclick = () => showStatement();
|
||||
@@ -378,17 +396,19 @@ const Customers = (() => {
|
||||
if (!modal) return;
|
||||
document.getElementById('modalTitle').textContent = 'Nuevo Cliente';
|
||||
document.getElementById('editId').value = '';
|
||||
document.getElementById('fName').value = '';
|
||||
document.getElementById('fRfc').value = '';
|
||||
document.getElementById('fRazonSocial').value = '';
|
||||
document.getElementById('fRegimenFiscal').value = '';
|
||||
document.getElementById('fUsoCfdi').value = 'G03';
|
||||
document.getElementById('fCp').value = '';
|
||||
document.getElementById('fPhone').value = '';
|
||||
document.getElementById('fEmail').value = '';
|
||||
document.getElementById('fAddress').value = '';
|
||||
document.getElementById('fPriceTier').value = '1';
|
||||
document.getElementById('fCreditLimit').value = '0';
|
||||
const safeSet = (id, v) => { const el = document.getElementById(id); if (el) el.value = v; };
|
||||
safeSet('fName', '');
|
||||
safeSet('fRfc', '');
|
||||
safeSet('fRazonSocial', '');
|
||||
safeSet('fRegimenFiscal', '');
|
||||
safeSet('fUsoCfdi', 'G03');
|
||||
safeSet('fCp', '');
|
||||
safeSet('fPhone', '');
|
||||
safeSet('fEmail', '');
|
||||
safeSet('fAddress', '');
|
||||
safeSet('fPriceTier', '1');
|
||||
safeSet('fCreditLimit', '0');
|
||||
safeSet('fMaxDiscountPct', '0');
|
||||
modal.classList.add('active');
|
||||
document.getElementById('fName').focus();
|
||||
}
|
||||
@@ -400,17 +420,19 @@ const Customers = (() => {
|
||||
if (!modal) return;
|
||||
document.getElementById('modalTitle').textContent = 'Editar Cliente';
|
||||
document.getElementById('editId').value = c.id;
|
||||
document.getElementById('fName').value = c.name || '';
|
||||
document.getElementById('fRfc').value = c.rfc || '';
|
||||
document.getElementById('fRazonSocial').value = c.razon_social || '';
|
||||
document.getElementById('fRegimenFiscal').value = c.regimen_fiscal || '';
|
||||
document.getElementById('fUsoCfdi').value = c.uso_cfdi || 'G03';
|
||||
document.getElementById('fCp').value = c.cp || '';
|
||||
document.getElementById('fPhone').value = c.phone || '';
|
||||
document.getElementById('fEmail').value = c.email || '';
|
||||
document.getElementById('fAddress').value = c.address || '';
|
||||
document.getElementById('fPriceTier').value = c.price_tier || '1';
|
||||
document.getElementById('fCreditLimit').value = c.credit_limit || 0;
|
||||
const safeSet = (id, v) => { const el = document.getElementById(id); if (el) el.value = v; };
|
||||
safeSet('fName', c.name || '');
|
||||
safeSet('fRfc', c.rfc || '');
|
||||
safeSet('fRazonSocial', c.razon_social || '');
|
||||
safeSet('fRegimenFiscal', c.regimen_fiscal || '');
|
||||
safeSet('fUsoCfdi', c.uso_cfdi || 'G03');
|
||||
safeSet('fCp', c.cp || '');
|
||||
safeSet('fPhone', c.phone || '');
|
||||
safeSet('fEmail', c.email || '');
|
||||
safeSet('fAddress', c.address || '');
|
||||
safeSet('fPriceTier', c.price_tier || '1');
|
||||
safeSet('fCreditLimit', c.credit_limit || 0);
|
||||
safeSet('fMaxDiscountPct', c.max_discount_pct || 0);
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
@@ -438,6 +460,7 @@ const Customers = (() => {
|
||||
address: val('fAddress') || null,
|
||||
price_tier: parseInt(val('fPriceTier')) || 1,
|
||||
credit_limit: parseFloat(val('fCreditLimit')) || 0,
|
||||
max_discount_pct: parseFloat(val('fMaxDiscountPct')) || 0,
|
||||
};
|
||||
|
||||
const editId = val('editId');
|
||||
@@ -474,7 +497,7 @@ const Customers = (() => {
|
||||
if (nameEl) nameEl.textContent = currentCustomer.name;
|
||||
|
||||
const content = document.getElementById('statementContent');
|
||||
if (content) content.innerHTML = '<div style="text-align:center;padding:20px;color:var(--color-text-muted);">Cargando...</div>';
|
||||
if (content) content.innerHTML = '<div style="padding:20px;"><div class="skeleton skeleton--text"></div><div class="skeleton skeleton--text-sm" style="width:70%;"></div><div class="skeleton skeleton--text" style="width:50%;"></div></div>';
|
||||
modal.classList.add('active');
|
||||
|
||||
try {
|
||||
@@ -586,7 +609,9 @@ const Customers = (() => {
|
||||
|
||||
function injectModals() {
|
||||
// Customer Create/Edit Modal
|
||||
if (!document.getElementById('customerModal')) {
|
||||
// Always remove and re-inject to ensure latest fields are present
|
||||
const existingModal = document.getElementById('customerModal');
|
||||
if (existingModal) existingModal.remove();
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `
|
||||
<div id="customerModal" class="modal-overlay" style="display:none;">
|
||||
@@ -646,6 +671,7 @@ const Customers = (() => {
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group"><label>Limite de Credito</label><input type="number" id="fCreditLimit" class="form-input" value="0" min="0" step="1000" /></div>
|
||||
<div class="form-group"><label>Descuento Max (%)</label><input type="number" id="fMaxDiscountPct" class="form-input" value="0" min="0" max="100" step="0.5" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -655,9 +681,10 @@ const Customers = (() => {
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
// Statement Modal
|
||||
const existingStatement = document.getElementById('statementModal');
|
||||
if (existingStatement) existingStatement.remove();
|
||||
if (!document.getElementById('statementModal')) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `
|
||||
@@ -772,11 +799,58 @@ const Customers = (() => {
|
||||
// Run init
|
||||
init();
|
||||
|
||||
return {
|
||||
const publicApi = {
|
||||
search, goToPage, loadCustomers,
|
||||
showDetail, selectCustomer, closeDetail,
|
||||
showCreateModal, editCurrent, closeModal, save,
|
||||
showStatement, closeStatement,
|
||||
showPaymentModal, closePayment, recordPayment,
|
||||
};
|
||||
|
||||
// Bulk selection
|
||||
publicApi.toggleCustomerSelection = function(id) {
|
||||
if (selectedCustomers.has(id)) selectedCustomers.delete(id);
|
||||
else selectedCustomers.add(id);
|
||||
updateBulkToolbar();
|
||||
};
|
||||
publicApi.toggleSelectAll = function() {
|
||||
var cb = document.getElementById('selectAllCustomers');
|
||||
var allChecked = cb.checked;
|
||||
if (customersVS && customersVS.data) {
|
||||
customersVS.data.forEach(function(c) {
|
||||
if (allChecked) selectedCustomers.add(c.id);
|
||||
else selectedCustomers.delete(c.id);
|
||||
});
|
||||
customersVS.refresh();
|
||||
}
|
||||
updateBulkToolbar();
|
||||
};
|
||||
function updateBulkToolbar() {
|
||||
var container = document.getElementById('customersBulkToolbar');
|
||||
if (!container) return;
|
||||
var count = selectedCustomers.size;
|
||||
if (count === 0) { container.innerHTML = ''; return; }
|
||||
container.innerHTML = renderBulkToolbar(count,
|
||||
'<button class="btn btn--primary btn--sm" onclick="Customers.featureProximamente(\'Exportar seleccionados\')">📥 Exportar</button>' +
|
||||
'<button class="btn btn--ghost btn--sm" onclick="Customers.clearSelection()">Limpiar</button>'
|
||||
);
|
||||
}
|
||||
publicApi.clearSelection = function() {
|
||||
selectedCustomers.clear();
|
||||
document.getElementById('selectAllCustomers').checked = false;
|
||||
if (customersVS) customersVS.refresh();
|
||||
updateBulkToolbar();
|
||||
};
|
||||
|
||||
// Expose globally for inline HTML onclick handlers
|
||||
window.Customers = publicApi;
|
||||
return publicApi;
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
registerCmdKItem({ group: "Principal", label: "POS Ventas", href: "/pos/sale", icon: "🛒" });
|
||||
registerCmdKItem({ group: "Principal", label: "Catálogo", href: "/pos/catalog", icon: "📁" });
|
||||
registerCmdKItem({ group: "Principal", label: "Clientes", href: "/pos/customers", icon: "👤" });
|
||||
registerCmdKItem({ group: "Principal", label: "Dashboard", href: "/pos/dashboard", icon: "📊" });
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
1
pos/static/js/customers.min.js
vendored
1
pos/static/js/customers.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -8,6 +8,9 @@
|
||||
const token = localStorage.getItem('pos_token') || '';
|
||||
if (!token) return;
|
||||
|
||||
let hourlyChart = null;
|
||||
let topProductsChart = null;
|
||||
|
||||
function headers() {
|
||||
return { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
|
||||
}
|
||||
@@ -31,10 +34,11 @@
|
||||
function renderHourlyChart(hourly) {
|
||||
const ctx = document.getElementById('hourlySalesChart');
|
||||
if (!ctx) return;
|
||||
if (hourlyChart) { hourlyChart.destroy(); hourlyChart = null; }
|
||||
const labels = hourly.map(function (h) { return h.hour + ':00'; });
|
||||
const totals = hourly.map(function (h) { return h.total; });
|
||||
|
||||
new Chart(ctx, {
|
||||
hourlyChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
@@ -62,10 +66,38 @@
|
||||
function renderTopProductsChart(topProducts) {
|
||||
const ctx = document.getElementById('topProductsChart');
|
||||
if (!ctx) return;
|
||||
if (topProductsChart) { topProductsChart.destroy(); topProductsChart = null; }
|
||||
if (!topProducts || topProducts.length === 0) {
|
||||
// No sales today — render a friendly empty-state mini chart so the canvas
|
||||
// doesn't collapse or leave a blank hole.
|
||||
topProductsChart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Sin ventas hoy'],
|
||||
datasets: [{
|
||||
data: [1],
|
||||
backgroundColor: ['rgba(136, 136, 136, 0.25)'],
|
||||
borderWidth: 0,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: { color: '#888', font: { size: 10 }, boxWidth: 10 }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = topProducts.map(function (p) { return p.name.substring(0, 20); });
|
||||
const revenues = topProducts.map(function (p) { return p.revenue; });
|
||||
|
||||
new Chart(ctx, {
|
||||
topProductsChart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: labels,
|
||||
|
||||
@@ -208,7 +208,7 @@ const Dashboard = (() => {
|
||||
function setKpiError(valueId, metaId) {
|
||||
const v = document.getElementById(valueId);
|
||||
const m = document.getElementById(metaId);
|
||||
if (v) v.textContent = '--';
|
||||
if (v) v.innerHTML = '<span style="color:var(--color-error)">--</span>';
|
||||
if (m) m.innerHTML = '<span class="kpi-meta-text" style="color:var(--color-error)">Error al cargar</span>';
|
||||
}
|
||||
|
||||
@@ -225,7 +225,12 @@ const Dashboard = (() => {
|
||||
if (!container) return;
|
||||
|
||||
if (!registers || registers.length === 0) {
|
||||
container.innerHTML = '<div class="rank-item" style="justify-content:center;color:var(--color-text-muted);font-size:var(--text-caption);">Sin cajas registradas hoy</div>';
|
||||
container.innerHTML = renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
||||
title: 'Sin cajas hoy',
|
||||
subtitle: 'Ninguna caja ha sido abierta el día de hoy.',
|
||||
action: '<a href="/pos/sale" class="btn btn--primary btn--sm">Abrir POS</a>'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -326,7 +331,12 @@ const Dashboard = (() => {
|
||||
if (!container) return;
|
||||
|
||||
if (!data || !data.data || data.data.length === 0) {
|
||||
container.innerHTML = '<div class="rank-item" style="justify-content:center;color:var(--color-text-muted);font-size:var(--text-caption);">Sin ventas hoy</div>';
|
||||
container.innerHTML = renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
||||
title: 'Sin ventas hoy',
|
||||
subtitle: 'Aún no hay transacciones registradas el día de hoy.',
|
||||
action: '<a href="/pos/sale" class="btn btn--primary btn--sm">Nueva venta</a>'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -353,7 +363,11 @@ const Dashboard = (() => {
|
||||
const sorted = Object.values(productMap).sort((a, b) => b.revenue - a.revenue).slice(0, 5);
|
||||
|
||||
if (sorted.length === 0) {
|
||||
container.innerHTML = '<div class="rank-item" style="justify-content:center;color:var(--color-text-muted);font-size:var(--text-caption);">Sin productos vendidos</div>';
|
||||
container.innerHTML = renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>',
|
||||
title: 'Sin productos vendidos',
|
||||
subtitle: 'No hay suficiente información para mostrar el ranking.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -444,7 +458,12 @@ const Dashboard = (() => {
|
||||
if (!tbody) return;
|
||||
|
||||
if (!data || !data.data || data.data.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--color-text-muted);font-size:var(--text-caption);">Sin ventas hoy</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="5">' + renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
||||
title: 'Sin ventas hoy',
|
||||
subtitle: 'Aún no hay transacciones registradas.',
|
||||
action: '<a href="/pos/sale" class="btn btn--primary btn--sm">Nueva venta</a>'
|
||||
}) + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -526,6 +545,17 @@ const Dashboard = (() => {
|
||||
}, 120000);
|
||||
}
|
||||
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === 'function') {
|
||||
registerCmdKItem({ group: 'Principal', label: 'Dashboard', href: '/pos/dashboard', icon: '📊' });
|
||||
registerCmdKItem({ group: 'Principal', label: 'POS Ventas', href: '/pos/sale', icon: '🛒' });
|
||||
registerCmdKItem({ group: 'Principal', label: 'Catálogo', href: '/pos/catalog', icon: '📁' });
|
||||
registerCmdKItem({ group: 'Principal', label: 'Clientes', href: '/pos/customers', icon: '👤' });
|
||||
registerCmdKItem({ group: 'Principal', label: 'Facturación', href: '/pos/invoicing', icon: '📄' });
|
||||
registerCmdKItem({ group: 'Principal', label: 'Reportes', href: '/pos/reports', icon: '📈' });
|
||||
registerCmdKItem({ group: 'Principal', label: 'Configuración', href: '/pos/config', icon: '⚙️' });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
return { init, setTheme };
|
||||
|
||||
1
pos/static/js/dashboard.min.js
vendored
1
pos/static/js/dashboard.min.js
vendored
File diff suppressed because one or more lines are too long
1
pos/static/js/diagrams.min.js
vendored
1
pos/static/js/diagrams.min.js
vendored
File diff suppressed because one or more lines are too long
1
pos/static/js/fleet.min.js
vendored
1
pos/static/js/fleet.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -478,6 +478,51 @@ const Invoicing = (() => {
|
||||
alert('Nota de credito: proximamente');
|
||||
}
|
||||
|
||||
// ---- Global Invoice ----
|
||||
function openGlobalInvoiceModal() {
|
||||
const now = new Date();
|
||||
document.getElementById('global-year').value = now.getFullYear();
|
||||
document.getElementById('global-month').value = now.getMonth() + 1;
|
||||
document.getElementById('global-preview').innerHTML = 'Presiona "Vista previa" para ver ventas elegibles.';
|
||||
document.getElementById('modalGlobalInvoice').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function previewGlobalInvoice() {
|
||||
const year = document.getElementById('global-year').value;
|
||||
const month = document.getElementById('global-month').value;
|
||||
const preview = document.getElementById('global-preview');
|
||||
preview.innerHTML = 'Cargando...';
|
||||
try {
|
||||
const res = await api(`/global-invoice/eligible-sales?year=${year}&month=${month}`);
|
||||
preview.innerHTML = `<strong>${res.count} ventas elegibles</strong> — Total: $${fmt(res.total)}<br><small>${res.sales.map(s => '#' + s.id).join(', ')}</small>`;
|
||||
} catch (e) {
|
||||
preview.innerHTML = '<span style="color:var(--color-error);">Error: ' + e.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function generateGlobalInvoice() {
|
||||
const year = parseInt(document.getElementById('global-year').value, 10);
|
||||
const month = parseInt(document.getElementById('global-month').value, 10);
|
||||
const btn = document.querySelector('#modalGlobalInvoice .btn--primary');
|
||||
const originalText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Generando...';
|
||||
try {
|
||||
const res = await api('/global-invoice', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ year, month })
|
||||
});
|
||||
alert(`Factura global generada: ${res.provisional_folio} (${res.sales_count} ventas, $${fmt(res.total)})`);
|
||||
document.getElementById('modalGlobalInvoice').style.display = 'none';
|
||||
loadFacturas();
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
// Expose switchTab globally for onclick handlers in HTML
|
||||
window.switchTab = switchTab;
|
||||
window.showNewInvoiceModal = showNewInvoiceModal;
|
||||
@@ -489,5 +534,14 @@ const Invoicing = (() => {
|
||||
switchTab, loadFacturas, loadNotas, loadComplementos, loadCancelaciones,
|
||||
showDetail, showCancelModal, confirmCancel, processQueue,
|
||||
showNewInvoiceModal, closeNewInvoiceModal, submitNewInvoice, notaCreditoPlaceholder,
|
||||
openGlobalInvoiceModal, previewGlobalInvoice, generateGlobalInvoice,
|
||||
};
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
registerCmdKItem({ group: "Principal", label: "POS Ventas", href: "/pos/sale", icon: "🛒" });
|
||||
registerCmdKItem({ group: "Principal", label: "Catálogo", href: "/pos/catalog", icon: "📁" });
|
||||
registerCmdKItem({ group: "Principal", label: "Clientes", href: "/pos/customers", icon: "👤" });
|
||||
registerCmdKItem({ group: "Principal", label: "Dashboard", href: "/pos/dashboard", icon: "📊" });
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -154,6 +154,9 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Expose for other scripts
|
||||
window.isKioskEnabled = isKioskEnabled;
|
||||
|
||||
// ─── Init ───
|
||||
if (isKioskEnabled()) {
|
||||
activate();
|
||||
|
||||
608
pos/static/js/marketplace_external.js
Normal file
608
pos/static/js/marketplace_external.js
Normal file
@@ -0,0 +1,608 @@
|
||||
/**
|
||||
* marketplace_external.js — MercadoLibre integration UI
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var API = '/pos/api/marketplace-ext';
|
||||
var TOKEN = localStorage.getItem('pos_token') || '';
|
||||
|
||||
function headers() {
|
||||
return {
|
||||
'Authorization': 'Bearer ' + TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Device-Id': localStorage.getItem('pos_device_id') || 'web',
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Tabs ──────────────────────────────────────────────────────────────
|
||||
|
||||
window.switchTab = function(tab) {
|
||||
document.querySelectorAll('.tab-btn').forEach(function(b) {
|
||||
b.classList.toggle('is-active', b.dataset.tab === tab);
|
||||
b.setAttribute('aria-selected', b.dataset.tab === tab ? 'true' : 'false');
|
||||
});
|
||||
document.querySelectorAll('.tab-panel').forEach(function(p) {
|
||||
p.classList.toggle('is-active', p.id === 'panel-' + tab);
|
||||
});
|
||||
if (tab === 'listings') loadListings();
|
||||
if (tab === 'orders') loadOrders();
|
||||
if (tab === 'questions') loadQuestions();
|
||||
};
|
||||
|
||||
function closeModal(id) {
|
||||
document.getElementById(id).classList.remove('is-open');
|
||||
}
|
||||
window.closeModal = closeModal;
|
||||
|
||||
// ─── Config / Connection ───────────────────────────────────────────────
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
var res = await fetch(API + '/config', { headers: headers() });
|
||||
if (!res.ok) throw new Error('Failed to load config');
|
||||
var cfg = await res.json();
|
||||
|
||||
var statusDiv = document.getElementById('configStatus');
|
||||
var formDiv = document.getElementById('configForm');
|
||||
var connectedDiv = document.getElementById('configConnected');
|
||||
|
||||
if (cfg.connected) {
|
||||
statusDiv.innerHTML = '<span class="meli-status meli-status--active">● Conectado</span>';
|
||||
formDiv.style.display = 'none';
|
||||
connectedDiv.style.display = 'block';
|
||||
document.getElementById('connectedNickname').textContent = cfg.meli_user_id || 'Usuario ML';
|
||||
document.getElementById('connectedSite').textContent = cfg.meli_site_id || 'MLM';
|
||||
} else {
|
||||
statusDiv.innerHTML = '<span class="meli-status meli-status--pending">● No conectado</span>';
|
||||
formDiv.style.display = 'block';
|
||||
connectedDiv.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
document.getElementById('configStatus').innerHTML = '<p style="color:var(--color-danger);">Error cargando configuración</p>';
|
||||
}
|
||||
}
|
||||
|
||||
window.startOAuth = function() {
|
||||
var clientId = document.getElementById('cfgClientId').value.trim();
|
||||
var clientSecret = document.getElementById('cfgClientSecret').value.trim();
|
||||
var category = document.getElementById('cfgCategory').value.trim();
|
||||
var shipping = document.getElementById('cfgShipping').value;
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
alert('Client ID y Client Secret son requeridos');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save config locally for the callback
|
||||
localStorage.setItem('meli_client_id', clientId);
|
||||
localStorage.setItem('meli_client_secret', clientSecret);
|
||||
localStorage.setItem('meli_category', category);
|
||||
localStorage.setItem('meli_shipping', shipping);
|
||||
|
||||
var redirectUri = window.location.origin + '/pos/marketplace-external/callback';
|
||||
var authUrl = 'https://auth.mercadolibre.com.mx/authorization?response_type=code&client_id=' + encodeURIComponent(clientId) + '&redirect_uri=' + encodeURIComponent(redirectUri) + '&scope=read+write+offline_access';
|
||||
window.location.href = authUrl;
|
||||
};
|
||||
|
||||
window.disconnectMeli = async function() {
|
||||
if (!confirm('¿Desconectar MercadoLibre? Las publicaciones existentes no se eliminarán de ML.')) return;
|
||||
try {
|
||||
var res = await fetch(API + '/connect', { method: 'DELETE', headers: headers() });
|
||||
if (res.ok) {
|
||||
loadConfig();
|
||||
} else {
|
||||
alert('Error desconectando');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Listings ──────────────────────────────────────────────────────────
|
||||
|
||||
var listingsData = [];
|
||||
|
||||
window.loadListings = async function() {
|
||||
var container = document.getElementById('listingsContainer');
|
||||
container.innerHTML = '<div class="meli-card"><div class="skeleton skeleton--text"></div><div class="skeleton skeleton--text-sm" style="width:70%;"></div></div>'
|
||||
+ '<div class="meli-card"><div class="skeleton skeleton--text"></div><div class="skeleton skeleton--text-sm" style="width:60%;"></div></div>'
|
||||
+ '<div class="meli-card"><div class="skeleton skeleton--text"></div><div class="skeleton skeleton--text-sm" style="width:80%;"></div></div>';
|
||||
try {
|
||||
var res = await fetch(API + '/listings?page=1&per_page=50', { headers: headers() });
|
||||
if (!res.ok) throw new Error('Failed to load listings');
|
||||
var data = await res.json();
|
||||
listingsData = data.items || [];
|
||||
renderListings();
|
||||
} catch (e) {
|
||||
container.innerHTML = renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
||||
title: 'Error cargando publicaciones',
|
||||
subtitle: 'No se pudieron obtener las publicaciones de MercadoLibre. Intenta de nuevo.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function renderListings() {
|
||||
var container = document.getElementById('listingsContainer');
|
||||
var statusFilter = document.getElementById('listingStatusFilter').value;
|
||||
var search = document.getElementById('listingSearch').value.toLowerCase();
|
||||
|
||||
var filtered = listingsData.filter(function(l) {
|
||||
if (statusFilter && l.external_status !== statusFilter) return false;
|
||||
if (search && !((l.title || '').toLowerCase().includes(search))) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!filtered.length) {
|
||||
container.innerHTML = renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
||||
title: 'Sin publicaciones',
|
||||
subtitle: 'Aún no hay publicaciones en MercadoLibre. Ve a Inventario y publica un producto.',
|
||||
action: '<a href="/pos/inventory" class="btn btn--meli btn--sm">Ir a Inventario</a>'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = filtered.map(function(l) {
|
||||
var statusClass = 'meli-status--' + (l.external_status || 'pending');
|
||||
return '<div class="meli-card">'
|
||||
+ '<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:var(--space-3);">'
|
||||
+ '<a href="' + escapeHtml(l.external_permalink || '#') + '" target="_blank" rel="noopener" style="font-weight:700;font-size:var(--text-body-sm);line-height:1.3;color:var(--color-primary);text-decoration:none;">' + escapeHtml(l.title || l.inventory_name || 'Sin título') + ' ↗</a>'
|
||||
+ '<span class="meli-status ' + statusClass + '">' + (l.external_status || '—') + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-2);">'
|
||||
+ 'SKU: ' + escapeHtml(l.part_number || '—') + ' · ID ML: <a href="' + escapeHtml(l.external_permalink || '#') + '" target="_blank" rel="noopener" style="color:var(--color-primary);">' + escapeHtml(l.external_item_id || '—') + '</a>'
|
||||
+ '</div>'
|
||||
+ '<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3);">'
|
||||
+ '<button class="btn btn--ghost btn--xs" onclick="syncListing(' + l.id + ')">Sync</button>'
|
||||
+ (l.external_status === 'active' ? '<button class="btn btn--ghost btn--xs" onclick="pauseListing(' + l.id + ')">Pausar</button>' : '<button class="btn btn--ghost btn--xs" onclick="activateListing(' + l.id + ')">Activar</button>')
|
||||
+ (l.external_status === 'closed' || !l.is_active
|
||||
? '<button class="btn btn--danger btn--xs" onclick="deleteListingPermanently(' + l.id + ')">Eliminar</button>'
|
||||
: '<button class="btn btn--danger btn--xs" onclick="deleteListing(' + l.id + ')">Cerrar</button>')
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.filterListings = renderListings;
|
||||
|
||||
window.syncListing = async function(id) {
|
||||
try {
|
||||
var res = await fetch(API + '/listings/' + id + '/sync', { method: 'POST', headers: headers() });
|
||||
var data = await res.json();
|
||||
if (res.ok) {
|
||||
showToast('Sincronizado: $' + data.price + ' · Stock: ' + data.stock, 'ok', { title: 'Publicación actualizada' });
|
||||
loadListings();
|
||||
} else {
|
||||
showToast(data.error || 'Error desconocido', 'error', { title: 'Error de sincronización' });
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error', { title: 'Error de red' });
|
||||
}
|
||||
};
|
||||
|
||||
window.pauseListing = async function(id) {
|
||||
try {
|
||||
var res = await fetch(API + '/listings/' + id + '/pause', { method: 'POST', headers: headers() });
|
||||
if (res.ok) { loadListings(); } else { alert('Error'); }
|
||||
} catch (e) { alert('Error: ' + e.message); }
|
||||
};
|
||||
|
||||
window.activateListing = async function(id) {
|
||||
try {
|
||||
var res = await fetch(API + '/listings/' + id + '/activate', { method: 'POST', headers: headers() });
|
||||
if (res.ok) { loadListings(); } else { alert('Error'); }
|
||||
} catch (e) { alert('Error: ' + e.message); }
|
||||
};
|
||||
|
||||
window.deleteListing = async function(id) {
|
||||
if (!confirm('¿Cerrar esta publicación en MercadoLibre?')) return;
|
||||
try {
|
||||
var res = await fetch(API + '/listings/' + id, { method: 'DELETE', headers: headers() });
|
||||
if (res.ok) { loadListings(); } else { alert('Error'); }
|
||||
} catch (e) { alert('Error: ' + e.message); }
|
||||
};
|
||||
|
||||
window.deleteListingPermanently = async function(id) {
|
||||
if (!confirm('¿Eliminar permanentemente esta publicación del listado local? Esta acción no se puede deshacer.')) return;
|
||||
try {
|
||||
var res = await fetch(API + '/listings/' + id + '/permanent', { method: 'DELETE', headers: headers() });
|
||||
if (res.ok) { loadListings(); } else { alert('Error al eliminar'); }
|
||||
} catch (e) { alert('Error: ' + e.message); }
|
||||
};
|
||||
|
||||
// ─── Orders ────────────────────────────────────────────────────────────
|
||||
|
||||
var ordersData = [];
|
||||
|
||||
window.loadOrders = async function() {
|
||||
var tbody = document.getElementById('ordersTableBody');
|
||||
tbody.innerHTML = renderSkeletonRows(6, 5);
|
||||
try {
|
||||
var res = await fetch(API + '/orders?page=1&per_page=50', { headers: headers() });
|
||||
if (!res.ok) throw new Error('Failed to load orders');
|
||||
var data = await res.json();
|
||||
ordersData = data.items || [];
|
||||
renderOrders();
|
||||
} catch (e) {
|
||||
tbody.innerHTML = '<tr><td colspan="6">' + renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
||||
title: 'Error cargando órdenes',
|
||||
subtitle: 'No se pudieron obtener las órdenes de MercadoLibre.'
|
||||
}) + '</td></tr>';
|
||||
}
|
||||
};
|
||||
|
||||
function renderOrders() {
|
||||
var tbody = document.getElementById('ordersTableBody');
|
||||
var statusFilter = document.getElementById('orderStatusFilter').value;
|
||||
var search = document.getElementById('orderSearch').value.toLowerCase();
|
||||
|
||||
var filtered = ordersData.filter(function(o) {
|
||||
if (statusFilter && o.status !== statusFilter) return false;
|
||||
if (search && !((o.buyer_name || '').toLowerCase().includes(search) || (o.external_order_id || '').includes(search))) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!filtered.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="6">' + renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
||||
title: 'Sin órdenes',
|
||||
subtitle: 'No hay órdenes de MercadoLibre en este momento.'
|
||||
}) + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = filtered.map(function(o) {
|
||||
var statusClass = 'meli-status--' + (o.status || 'pending');
|
||||
return '<tr>'
|
||||
+ '<td><a href="#" onclick="showOrderDetail(' + o.id + ');return false;">' + escapeHtml(o.external_order_id) + '</a></td>'
|
||||
+ '<td>' + escapeHtml(o.buyer_name || o.buyer_nickname || '—') + '</td>'
|
||||
+ '<td style="text-align:right">$' + (o.total_amount || 0).toFixed(2) + '</td>'
|
||||
+ '<td><span class="meli-status ' + statusClass + '">' + (o.status || '—') + '</span></td>'
|
||||
+ '<td>' + (o.created_at ? o.created_at.split('T')[0] : '—') + '</td>'
|
||||
+ '<td>'
|
||||
+ (o.status === 'pending' ? '<button class="btn btn--primary btn--xs" onclick="convertOrder(' + o.id + ')">Convertir a Venta</button> ' : '')
|
||||
+ '<button class="btn btn--ghost btn--xs" onclick="showOrderDetail(' + o.id + ')">Ver</button>'
|
||||
+ '</td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.filterOrders = renderOrders;
|
||||
|
||||
window.showOrderDetail = async function(id) {
|
||||
var modal = document.getElementById('orderModal');
|
||||
var body = document.getElementById('orderModalBody');
|
||||
var footer = document.getElementById('orderModalFooter');
|
||||
body.innerHTML = 'Cargando...';
|
||||
footer.innerHTML = '';
|
||||
modal.classList.add('is-open');
|
||||
|
||||
try {
|
||||
var res = await fetch(API + '/orders/' + id, { headers: headers() });
|
||||
var o = await res.json();
|
||||
if (!res.ok) throw new Error(o.error || 'Error');
|
||||
|
||||
var itemsHtml = (o.items || []).map(function(it) {
|
||||
return '<tr><td>' + escapeHtml(it.title || '—') + '</td><td>' + it.quantity + '</td><td style="text-align:right">$' + (it.unit_price || 0).toFixed(2) + '</td></tr>';
|
||||
}).join('');
|
||||
|
||||
body.innerHTML = '<div style="margin-bottom:var(--space-4);">'
|
||||
+ '<p><strong>Comprador:</strong> ' + escapeHtml(o.buyer_name || '—') + ' (' + escapeHtml(o.buyer_nickname || '—') + ')</p>'
|
||||
+ '<p><strong>Email:</strong> ' + escapeHtml(o.buyer_email || '—') + '</p>'
|
||||
+ '<p><strong>Teléfono:</strong> ' + escapeHtml(o.buyer_phone || '—') + '</p>'
|
||||
+ '<p><strong>Total:</strong> $' + (o.total_amount || 0).toFixed(2) + '</p>'
|
||||
+ '<p><strong>Estado ML:</strong> ' + escapeHtml(o.external_status || '—') + '</p>'
|
||||
+ '<p><strong>Estado Nexus:</strong> ' + escapeHtml(o.status || '—') + '</p>'
|
||||
+ '</div>'
|
||||
+ '<h4 style="margin:var(--space-3) 0;">Items</h4>'
|
||||
+ '<table class="data-table"><thead><tr><th>Producto</th><th>Cantidad</th><th style="text-align:right">Precio</th></tr></thead><tbody>' + itemsHtml + '</tbody></table>';
|
||||
|
||||
footer.innerHTML = '';
|
||||
if (o.status === 'pending') {
|
||||
footer.innerHTML += '<button class="btn btn--primary" onclick="convertOrder(' + o.id + ');closeModal(\'orderModal\')">Convertir a Venta</button> ';
|
||||
}
|
||||
if (o.status === 'confirmed') {
|
||||
footer.innerHTML += '<button class="btn btn--primary" onclick="updateOrderStatus(' + o.id + ', \'packed\')">Marcar Empacada</button> ';
|
||||
}
|
||||
if (o.status === 'packed') {
|
||||
footer.innerHTML += '<button class="btn btn--primary" onclick="updateOrderStatus(' + o.id + ', \'shipped\')">Marcar Enviada</button> ';
|
||||
}
|
||||
footer.innerHTML += '<button class="btn btn--ghost" onclick="closeModal(\'orderModal\')">Cerrar</button>';
|
||||
} catch (e) {
|
||||
body.innerHTML = '<p style="color:var(--color-danger)">Error: ' + escapeHtml(e.message) + '</p>';
|
||||
}
|
||||
};
|
||||
|
||||
window.convertOrder = async function(id) {
|
||||
try {
|
||||
var res = await fetch(API + '/orders/' + id + '/convert', {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
var data = await res.json();
|
||||
if (res.ok) {
|
||||
showToast('Orden convertida a venta #' + data.sale_id, 'ok', { title: 'Venta creada' });
|
||||
loadOrders();
|
||||
} else {
|
||||
showToast(data.error || 'Error desconocido', 'error', { title: 'Error al convertir' });
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error', { title: 'Error de red' });
|
||||
}
|
||||
};
|
||||
|
||||
window.updateOrderStatus = async function(id, status) {
|
||||
try {
|
||||
var res = await fetch(API + '/orders/' + id + '/status', {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({ status: status })
|
||||
});
|
||||
if (res.ok) { loadOrders(); } else { alert('Error'); }
|
||||
} catch (e) { alert('Error: ' + e.message); }
|
||||
};
|
||||
|
||||
// ─── Utils ─────────────────────────────────────────────────────────────
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
var div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ─── Init ──────────────────────────────────────────────────────────────
|
||||
|
||||
// Handle OAuth callback
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var authCode = urlParams.get('code');
|
||||
if (authCode && window.location.pathname.includes('marketplace-external')) {
|
||||
(async function() {
|
||||
var clientId = localStorage.getItem('meli_client_id');
|
||||
var clientSecret = localStorage.getItem('meli_client_secret');
|
||||
var redirectUri = window.location.origin + '/pos/marketplace-external/callback';
|
||||
try {
|
||||
var res = await fetch(API + '/connect', {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({
|
||||
code: authCode,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: redirectUri,
|
||||
})
|
||||
});
|
||||
var data = await res.json();
|
||||
if (res.ok) {
|
||||
alert('¡Conectado exitosamente con MercadoLibre!');
|
||||
window.history.replaceState({}, document.title, '/pos/marketplace-external');
|
||||
loadConfig();
|
||||
} else {
|
||||
alert('Error conectando: ' + (data.error || 'Unknown'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// ML Status Cards (sparkline simulation)
|
||||
window.loadMeliStats = async function() {
|
||||
var container = document.getElementById('meliStatsBar');
|
||||
if (!container) return;
|
||||
try {
|
||||
var res = await fetch(API + '/listings?page=1&per_page=200', { headers: headers() });
|
||||
if (!res.ok) throw new Error('Failed');
|
||||
var data = await res.json();
|
||||
var items = data.items || [];
|
||||
var active = items.filter(function(l) { return l.external_status === 'active'; }).length;
|
||||
var paused = items.filter(function(l) { return l.external_status === 'paused'; }).length;
|
||||
var closed = items.filter(function(l) { return l.external_status === 'closed'; }).length;
|
||||
var total = items.length;
|
||||
|
||||
var html = '<div style="display:flex;gap:var(--space-4);flex-wrap:wrap;margin-bottom:var(--space-4);">' +
|
||||
'<div class="kpi-card" style="flex:1;min-width:160px;"><div class="kpi-card__label">Activas</div><div class="kpi-card__value" style="color:var(--color-success);">' + active + '</div></div>' +
|
||||
'<div class="kpi-card" style="flex:1;min-width:160px;"><div class="kpi-card__label">Pausadas</div><div class="kpi-card__value" style="color:var(--color-warning);">' + paused + '</div></div>' +
|
||||
'<div class="kpi-card" style="flex:1;min-width:160px;"><div class="kpi-card__label">Cerradas</div><div class="kpi-card__value" style="color:var(--color-error);">' + closed + '</div></div>' +
|
||||
'<div class="kpi-card" style="flex:1;min-width:160px;"><div class="kpi-card__label">Total</div><div class="kpi-card__value">' + total + '</div></div>';
|
||||
// Sparkline simulation
|
||||
html += '<div class="kpi-card" style="flex:1;min-width:200px;"><div class="kpi-card__label">Tendencia</div><div id="meliSparkline"></div></div>';
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
if (typeof renderSparkline === 'function') {
|
||||
renderSparkline('#meliSparkline', [active, paused, closed, total % 50, active - 2, paused + 1, closed, active], { prefix: '' });
|
||||
}
|
||||
} catch(e) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
};
|
||||
|
||||
// Kanban Order View
|
||||
var _orderViewMode = 'table';
|
||||
window.toggleOrderView = function() {
|
||||
_orderViewMode = _orderViewMode === 'table' ? 'kanban' : 'table';
|
||||
document.getElementById('ordersTableView').style.display = _orderViewMode === 'table' ? '' : 'none';
|
||||
document.getElementById('ordersKanbanView').style.display = _orderViewMode === 'kanban' ? '' : 'none';
|
||||
document.getElementById('btnKanbanView').textContent = _orderViewMode === 'table' ? '📋 Kanban' : '📄 Tabla';
|
||||
if (_orderViewMode === 'kanban') renderKanbanOrders();
|
||||
};
|
||||
|
||||
function renderKanbanOrders() {
|
||||
var container = document.getElementById('ordersKanbanView');
|
||||
if (!container) return;
|
||||
var columns = [
|
||||
{ key: 'pending', label: 'Pendientes', badge: 'badge--pending' },
|
||||
{ key: 'confirmed', label: 'Confirmadas', badge: 'badge--ok' },
|
||||
{ key: 'packed', label: 'Empacadas', badge: 'badge--transit' },
|
||||
{ key: 'shipped', label: 'Enviadas', badge: 'badge--transit' },
|
||||
{ key: 'delivered', label: 'Entregadas', badge: 'badge--complete' },
|
||||
{ key: 'cancelled', label: 'Canceladas', badge: 'badge--cancelled' },
|
||||
];
|
||||
var html = '<div class="kanban">';
|
||||
columns.forEach(function(col) {
|
||||
var items = ordersData.filter(function(o) { return o.status === col.key; });
|
||||
html += '<div class="kanban__col" data-status="' + col.key + '">';
|
||||
html += '<div class="kanban__col-header">' + col.label + '<span class="kanban__col-count ' + col.badge + '">' + items.length + '</span></div>';
|
||||
html += '<div class="kanban__cards">';
|
||||
items.slice(0, 20).forEach(function(o) {
|
||||
html += '<div class="kanban__card" draggable="true" data-id="' + o.id + '">' +
|
||||
'<div class="kanban__card-title">' + escapeHtml(o.buyer_name || o.buyer_nickname || '—') + '</div>' +
|
||||
'<div class="kanban__card-meta">$' + (o.total_amount || 0).toFixed(2) + ' · ' + escapeHtml(o.external_order_id || '') + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
if (items.length > 20) html += '<div style="text-align:center;font-size:11px;color:var(--color-text-muted);padding:8px;">+' + (items.length - 20) + ' más</div>';
|
||||
html += '</div></div>';
|
||||
});
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// ─── Questions ─────────────────────────────────────────────────────────
|
||||
|
||||
var questionsData = [];
|
||||
|
||||
window.loadQuestions = async function() {
|
||||
var container = document.getElementById('questionsContainer');
|
||||
container.innerHTML = '<div class="skeleton-grid">' + Array(6).fill('<div class="skeleton skeleton--card"></div>').join('') + '</div>';
|
||||
try {
|
||||
var res = await fetch(API + '/questions', { headers: headers() });
|
||||
if (!res.ok) throw new Error('Failed to load questions');
|
||||
var data = await res.json();
|
||||
questionsData = data.items || [];
|
||||
renderQuestions();
|
||||
} catch (e) {
|
||||
container.innerHTML = renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
||||
title: 'Sin preguntas',
|
||||
subtitle: 'No hay preguntas de compradores pendientes. Sincroniza con MercadoLibre para obtenerlas.',
|
||||
action: '<button class="btn btn--meli btn--sm" onclick="syncQuestions()">Sincronizar con ML</button>'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function renderQuestions() {
|
||||
var container = document.getElementById('questionsContainer');
|
||||
var statusFilter = document.getElementById('questionStatusFilter').value;
|
||||
var search = document.getElementById('questionSearch').value.toLowerCase();
|
||||
|
||||
var filtered = questionsData.filter(function(q) {
|
||||
if (statusFilter && q.status !== statusFilter) return false;
|
||||
if (search && !((q.question_text || '').toLowerCase().includes(search)) && !((q.listing_title || '').toLowerCase().includes(search))) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Stats bar
|
||||
var unanswered = questionsData.filter(function(q) { return q.status === 'unanswered'; }).length;
|
||||
var answered = questionsData.filter(function(q) { return q.status === 'answered'; }).length;
|
||||
var total = questionsData.length;
|
||||
var statsHtml = '<div style="display:flex;gap:var(--space-4);flex-wrap:wrap;margin-bottom:var(--space-4);">' +
|
||||
'<div class="meli-card" style="flex:1;min-width:140px;text-align:center;"><div style="font-size:28px;font-weight:800;color:var(--color-primary);">' + total + '</div><div style="font-size:var(--text-caption);color:var(--color-text-muted);">Total preguntas</div></div>' +
|
||||
'<div class="meli-card" style="flex:1;min-width:140px;text-align:center;"><div style="font-size:28px;font-weight:800;color:var(--color-error);">' + unanswered + '</div><div style="font-size:var(--text-caption);color:var(--color-text-muted);">Sin responder</div></div>' +
|
||||
'<div class="meli-card" style="flex:1;min-width:140px;text-align:center;"><div style="font-size:28px;font-weight:800;color:var(--color-success);">' + answered + '</div><div style="font-size:var(--text-caption);color:var(--color-text-muted);">Respondidas</div></div>' +
|
||||
'</div>';
|
||||
document.getElementById('questionsStatsBar').innerHTML = statsHtml;
|
||||
|
||||
if (!filtered.length) {
|
||||
container.innerHTML = renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
||||
title: 'Sin preguntas',
|
||||
subtitle: statusFilter ? 'No hay preguntas con el filtro seleccionado.' : 'No hay preguntas sincronizadas.',
|
||||
action: ''
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = filtered.map(function(q) {
|
||||
var statusClass = 'meli-status--' + (q.status || 'pending');
|
||||
var statusLabel = q.status === 'unanswered' ? 'Sin responder' : (q.status === 'answered' ? 'Respondida' : (q.status || '—'));
|
||||
var answerHtml = '';
|
||||
if (q.status === 'unanswered') {
|
||||
answerHtml = '<div style="margin-top:var(--space-2);">' +
|
||||
'<textarea class="meli-title-input" id="qAnswer-' + q.id + '" rows="2" placeholder="Escribe tu respuesta..."></textarea>' +
|
||||
'<button class="btn btn--primary btn--xs" style="margin-top:6px;" onclick="submitAnswer(' + q.id + ')">Enviar respuesta</button>' +
|
||||
'</div>';
|
||||
} else if (q.answer_text) {
|
||||
answerHtml = '<div style="margin-top:var(--space-2);padding:var(--space-2);background:var(--color-surface-0);border-radius:var(--radius-sm);font-size:var(--text-caption);color:var(--color-text-secondary);">' +
|
||||
'<strong>Respuesta:</strong> ' + escapeHtml(q.answer_text) +
|
||||
'</div>';
|
||||
}
|
||||
return '<div class="meli-card">'
|
||||
+ '<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:var(--space-3);">'
|
||||
+ '<div style="font-weight:700;font-size:var(--text-body-sm);line-height:1.3;">' + escapeHtml(q.listing_title || 'Artículo sin título') + '</div>'
|
||||
+ '<span class="meli-status ' + statusClass + '">' + statusLabel + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-2);">'
|
||||
+ 'Comprador: ' + escapeHtml(q.buyer_nickname || '—') + ' · ' + (q.question_date ? new Date(q.question_date).toLocaleString('es-MX') : '—')
|
||||
+ '</div>'
|
||||
+ '<div style="font-size:var(--text-body-sm);color:var(--color-text-primary);margin-bottom:var(--space-2);">'
|
||||
+ '<strong>Pregunta:</strong> ' + escapeHtml(q.question_text)
|
||||
+ '</div>'
|
||||
+ answerHtml
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.filterQuestions = renderQuestions;
|
||||
|
||||
window.syncQuestions = async function() {
|
||||
var btn = document.querySelector('#panel-questions .btn--primary');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Sincronizando...'; }
|
||||
try {
|
||||
var res = await fetch(API + '/questions/sync', { method: 'POST', headers: headers() });
|
||||
var data = await res.json();
|
||||
if (res.ok) {
|
||||
showToast('Sincronizadas ' + (data.synced || 0) + ' preguntas', 'ok', { title: 'Sincronización' });
|
||||
loadQuestions();
|
||||
} else {
|
||||
showToast(data.error || 'Error al sincronizar', 'error', { title: 'Error' });
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error', { title: 'Error de red' });
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '🔄 Actualizar'; }
|
||||
}
|
||||
};
|
||||
|
||||
window.submitAnswer = async function(questionId) {
|
||||
var textarea = document.getElementById('qAnswer-' + questionId);
|
||||
if (!textarea) return;
|
||||
var text = textarea.value.trim();
|
||||
if (!text) {
|
||||
showToast('Escribe una respuesta antes de enviar', 'error', { title: 'Respuesta vacía' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var res = await fetch(API + '/questions/' + questionId + '/answer', {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({ text: text })
|
||||
});
|
||||
var data = await res.json();
|
||||
if (res.ok) {
|
||||
showToast('Respuesta enviada correctamente', 'ok', { title: 'Pregunta respondida' });
|
||||
loadQuestions();
|
||||
} else {
|
||||
showToast(data.error || 'Error al enviar respuesta', 'error', { title: 'Error' });
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error', { title: 'Error de red' });
|
||||
}
|
||||
};
|
||||
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === 'function') {
|
||||
registerCmdKItem({ group: 'MercadoLibre', label: 'Configuración ML', href: '/pos/marketplace-external', icon: '⚙️' });
|
||||
registerCmdKItem({ group: 'MercadoLibre', label: 'Publicaciones ML', href: '/pos/marketplace-external#listings', icon: '📦' });
|
||||
registerCmdKItem({ group: 'MercadoLibre', label: 'Órdenes ML', href: '/pos/marketplace-external#orders', icon: '🛒' });
|
||||
registerCmdKItem({ group: 'MercadoLibre', label: 'Preguntas ML', href: '/pos/marketplace-external#questions', icon: '❓' });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadConfig();
|
||||
});
|
||||
})();
|
||||
@@ -76,38 +76,245 @@
|
||||
window.print();
|
||||
};
|
||||
|
||||
// ── Toast (simple, non-blocking notification) ──────────────────
|
||||
// Only creates its own toast if the page doesn't already have one.
|
||||
window.showToast = function(msg, type) {
|
||||
// ── Toast (enhanced with icons, progress bar, close button, actions) ──
|
||||
var _toastIcons = {
|
||||
ok: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>',
|
||||
error: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
|
||||
warn: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
||||
info: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>'
|
||||
};
|
||||
var _toastTitles = { ok: 'Éxito', error: 'Error', warn: 'Advertencia', info: 'Información' };
|
||||
|
||||
window.showToast = function(msg, type, opts) {
|
||||
type = type || 'info';
|
||||
opts = opts || {};
|
||||
var container = document.getElementById('toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
container.style.cssText = 'position:fixed;top:16px;right:16px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none;';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
var colors = {
|
||||
ok: 'background:#1a7a3a;color:#fff;',
|
||||
error: 'background:#c0392b;color:#fff;',
|
||||
warn: 'background:#d4a017;color:#000;',
|
||||
info: 'background:var(--color-surface-3,#333);color:var(--color-text-primary,#fff);',
|
||||
};
|
||||
|
||||
var toast = document.createElement('div');
|
||||
toast.style.cssText = (colors[type] || colors.info) +
|
||||
'padding:10px 20px;border-radius:8px;font-size:14px;font-weight:500;' +
|
||||
'box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;' +
|
||||
'animation:slideInRight 0.3s ease;max-width:400px;';
|
||||
toast.textContent = msg;
|
||||
toast.className = 'toast toast--' + type;
|
||||
|
||||
var iconHtml = '<div class="toast__icon">' + (_toastIcons[type] || _toastIcons.info) + '</div>';
|
||||
var titleHtml = opts.title ? '<div class="toast__title">' + opts.title + '</div>' : '';
|
||||
var actionHtml = '';
|
||||
if (opts.action && opts.action.text) {
|
||||
actionHtml = '<div class="toast__action"><button onclick="this.closest(\'.toast\').__toastAction()">' + opts.action.text + '</button></div>';
|
||||
toast.__toastAction = function() {
|
||||
if (opts.action.callback) opts.action.callback();
|
||||
_removeToast(toast);
|
||||
};
|
||||
}
|
||||
var progressHtml = '<div class="toast__progress" style="animation-duration:' + (opts.duration || 4000) + 'ms;"></div>';
|
||||
|
||||
toast.innerHTML = iconHtml +
|
||||
'<div class="toast__content">' + titleHtml + '<div class="toast__msg">' + msg + '</div>' + actionHtml + '</div>' +
|
||||
'<button class="toast__close" onclick="_removeToast(this.closest(\'.toast\'))">✕</button>' +
|
||||
progressHtml;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(function() {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transition = 'opacity 0.3s';
|
||||
setTimeout(function() { toast.remove(); }, 300);
|
||||
}, 3000);
|
||||
var timer = setTimeout(function() { _removeToast(toast); }, opts.duration || 4000);
|
||||
toast.__toastTimer = timer;
|
||||
|
||||
toast.addEventListener('mouseenter', function() { clearTimeout(timer); var p = toast.querySelector('.toast__progress'); if (p) p.style.animationPlayState = 'paused'; });
|
||||
toast.addEventListener('mouseleave', function() { var p = toast.querySelector('.toast__progress'); if (p) p.style.animationPlayState = 'running'; timer = setTimeout(function() { _removeToast(toast); }, 2000); toast.__toastTimer = timer; });
|
||||
};
|
||||
|
||||
window._removeToast = function(toast) {
|
||||
if (!toast || toast.__toastRemoved) return;
|
||||
toast.__toastRemoved = true;
|
||||
if (toast.__toastTimer) clearTimeout(toast.__toastTimer);
|
||||
toast.style.animation = 'toastSlideOut 0.25s ease forwards';
|
||||
setTimeout(function() { toast.remove(); }, 260);
|
||||
};
|
||||
|
||||
// ── Skeleton helpers ──────────────────────────────────────────
|
||||
window.renderSkeletonRows = function(cols, rows) {
|
||||
rows = rows || 6;
|
||||
var html = '';
|
||||
for (var i = 0; i < rows; i++) {
|
||||
html += '<tr class="skeleton--table-row">';
|
||||
for (var j = 0; j < cols; j++) {
|
||||
html += '<td><div class="skeleton"></div></td>';
|
||||
}
|
||||
html += '</tr>';
|
||||
}
|
||||
return html;
|
||||
};
|
||||
|
||||
window.showSkeleton = function(containerSelector, cols, rows) {
|
||||
var el = typeof containerSelector === 'string' ? document.querySelector(containerSelector) : containerSelector;
|
||||
if (!el) return;
|
||||
el.dataset.originalContent = el.innerHTML;
|
||||
el.innerHTML = renderSkeletonRows(cols || 6, rows || 6);
|
||||
};
|
||||
|
||||
window.hideSkeleton = function(containerSelector) {
|
||||
var el = typeof containerSelector === 'string' ? document.querySelector(containerSelector) : containerSelector;
|
||||
if (!el || el.dataset.originalContent === undefined) return;
|
||||
el.innerHTML = el.dataset.originalContent;
|
||||
delete el.dataset.originalContent;
|
||||
};
|
||||
|
||||
// ── Empty state helper ────────────────────────────────────────
|
||||
window.renderEmptyState = function(opts) {
|
||||
opts = opts || {};
|
||||
var icon = opts.icon || '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="9" y1="9" x2="15" y2="15"/><line x1="15" y1="9" x2="9" y2="15"/></svg>';
|
||||
var title = opts.title || 'Sin datos';
|
||||
var subtitle = opts.subtitle || 'No hay información disponible en este momento.';
|
||||
var action = opts.action ? '<div class="empty-state__action">' + opts.action + '</div>' : '';
|
||||
return '<div class="empty-state">' +
|
||||
'<div class="empty-state__icon">' + icon + '</div>' +
|
||||
'<div class="empty-state__title">' + title + '</div>' +
|
||||
'<div class="empty-state__subtitle">' + subtitle + '</div>' +
|
||||
action + '</div>';
|
||||
};
|
||||
|
||||
// ── Cmd+K Global Search ───────────────────────────────────────
|
||||
(function() {
|
||||
var cmdkOverlay = null, cmdkInput = null, cmdkResults = null, cmdkSelected = -1;
|
||||
var cmdkItems = [];
|
||||
|
||||
function buildCmdK() {
|
||||
if (cmdkOverlay) return;
|
||||
cmdkOverlay = document.createElement('div');
|
||||
cmdkOverlay.className = 'cmdk-overlay';
|
||||
cmdkOverlay.innerHTML =
|
||||
'<div class="cmdk-modal" role="dialog" aria-label="Búsqueda global">' +
|
||||
' <div class="cmdk-input-wrap">' +
|
||||
' <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="color:var(--color-text-muted);flex-shrink:0;"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>' +
|
||||
' <input type="text" class="cmdk-input" placeholder="Buscar módulos, productos, clientes..." autocomplete="off">' +
|
||||
' <span class="cmdk-shortcut">ESC</span>' +
|
||||
' </div>' +
|
||||
' <div class="cmdk-results"></div>' +
|
||||
' <div class="cmdk-footer"><span>↑↓ navegar · ↵ seleccionar</span><span>' + cmdkItems.length + ' resultados</span></div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(cmdkOverlay);
|
||||
cmdkInput = cmdkOverlay.querySelector('.cmdk-input');
|
||||
cmdkResults = cmdkOverlay.querySelector('.cmdk-results');
|
||||
|
||||
cmdkOverlay.addEventListener('click', function(e) { if (e.target === cmdkOverlay) closeCmdK(); });
|
||||
cmdkInput.addEventListener('input', function() { filterCmdK(this.value); });
|
||||
cmdkInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') { closeCmdK(); return; }
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); moveCmdK(1); }
|
||||
if (e.key === 'ArrowUp') { e.preventDefault(); moveCmdK(-1); }
|
||||
if (e.key === 'Enter') { e.preventDefault(); activateCmdK(); }
|
||||
});
|
||||
}
|
||||
|
||||
function openCmdK() {
|
||||
buildCmdK();
|
||||
cmdkOverlay.classList.add('is-open');
|
||||
cmdkInput.value = '';
|
||||
cmdkInput.focus();
|
||||
filterCmdK('');
|
||||
}
|
||||
|
||||
function closeCmdK() {
|
||||
if (cmdkOverlay) cmdkOverlay.classList.remove('is-open');
|
||||
}
|
||||
|
||||
function moveCmdK(dir) {
|
||||
var items = cmdkResults.querySelectorAll('.cmdk-item');
|
||||
if (!items.length) return;
|
||||
cmdkSelected += dir;
|
||||
if (cmdkSelected < 0) cmdkSelected = items.length - 1;
|
||||
if (cmdkSelected >= items.length) cmdkSelected = 0;
|
||||
items.forEach(function(it, i) { it.classList.toggle('is-selected', i === cmdkSelected); });
|
||||
var sel = items[cmdkSelected];
|
||||
if (sel) sel.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
function activateCmdK() {
|
||||
var items = cmdkResults.querySelectorAll('.cmdk-item');
|
||||
var sel = items[cmdkSelected];
|
||||
if (sel && sel.dataset.href) { closeCmdK(); window.location.href = sel.dataset.href; }
|
||||
}
|
||||
|
||||
function filterCmdK(q) {
|
||||
q = (q || '').toLowerCase().trim();
|
||||
var groups = {};
|
||||
cmdkItems.forEach(function(item) {
|
||||
if (!q || item.label.toLowerCase().indexOf(q) !== -1 || (item.keywords || '').toLowerCase().indexOf(q) !== -1) {
|
||||
groups[item.group] = groups[item.group] || [];
|
||||
groups[item.group].push(item);
|
||||
}
|
||||
});
|
||||
var html = '';
|
||||
var total = 0;
|
||||
Object.keys(groups).forEach(function(g) {
|
||||
html += '<div class="cmdk-group"><div class="cmdk-group__label">' + g + '</div>';
|
||||
groups[g].forEach(function(item) {
|
||||
total++;
|
||||
html += '<div class="cmdk-item" data-href="' + (item.href || '') + '">' +
|
||||
'<div class="cmdk-item__icon">' + (item.icon || '→') + '</div>' +
|
||||
'<div>' + item.label + '</div>' +
|
||||
(item.meta ? '<div class="cmdk-item__meta">' + item.meta + '</div>' : '') +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
});
|
||||
if (!total) html = '<div style="padding:24px;text-align:center;color:var(--color-text-muted);">Sin resultados</div>';
|
||||
cmdkResults.innerHTML = html;
|
||||
cmdkSelected = 0;
|
||||
var first = cmdkResults.querySelector('.cmdk-item');
|
||||
if (first) first.classList.add('is-selected');
|
||||
var footer = cmdkOverlay.querySelector('.cmdk-footer span:last-child');
|
||||
if (footer) footer.textContent = total + ' resultados';
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openCmdK(); }
|
||||
});
|
||||
|
||||
window.registerCmdKItem = function(item) {
|
||||
if (!item || !item.label) return;
|
||||
cmdkItems.push(item);
|
||||
};
|
||||
|
||||
window.openCmdK = openCmdK;
|
||||
window.closeCmdK = closeCmdK;
|
||||
})();
|
||||
|
||||
// ── Connection indicator helper ───────────────────────────────
|
||||
window.ConnectionStatus = {
|
||||
online: function() { return navigator.onLine; },
|
||||
render: function(containerId) {
|
||||
var el = document.getElementById(containerId);
|
||||
if (!el) return;
|
||||
function update() {
|
||||
var isOnline = navigator.onLine;
|
||||
el.className = 'connection-indicator' + (isOnline ? '' : ' connection-indicator--offline');
|
||||
el.innerHTML = '<span></span>' + (isOnline ? 'En línea' : 'Sin conexión');
|
||||
}
|
||||
update();
|
||||
window.addEventListener('online', update);
|
||||
window.addEventListener('offline', update);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Bulk toolbar helper ───────────────────────────────────────
|
||||
window.renderBulkToolbar = function(count, actionsHtml) {
|
||||
return '<div class="bulk-toolbar">' +
|
||||
'<div class="bulk-toolbar__count">' + count + ' seleccionado' + (count !== 1 ? 's' : '') + '</div>' +
|
||||
'<div class="bulk-toolbar__actions">' + actionsHtml + '</div>' +
|
||||
'</div>';
|
||||
};
|
||||
|
||||
// ── Entrance animation helper ─────────────────────────────────
|
||||
window.animateEntrance = function(selector, animClass, stagger) {
|
||||
animClass = animClass || 'animate-fade-in-up';
|
||||
stagger = stagger || 0.05;
|
||||
var els = document.querySelectorAll(selector);
|
||||
els.forEach(function(el, i) {
|
||||
el.style.animationDelay = (i * stagger) + 's';
|
||||
el.classList.add(animClass);
|
||||
});
|
||||
};
|
||||
|
||||
// ── "Próximamente" placeholder for features not yet built ──────
|
||||
@@ -392,4 +599,275 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Barcode Scanner Feedback ──────────────────────────────────
|
||||
window.BarcodeFeedback = {
|
||||
_audioCtx: null,
|
||||
success: function() {
|
||||
this._beep(800, 0.1, 'sine');
|
||||
this._flash('#22c55e');
|
||||
if (navigator.vibrate) navigator.vibrate(50);
|
||||
},
|
||||
error: function() {
|
||||
this._beep(200, 0.15, 'square');
|
||||
this._flash('#ef4444');
|
||||
if (navigator.vibrate) navigator.vibrate([80, 50, 80]);
|
||||
},
|
||||
_beep: function(freq, duration, type) {
|
||||
try {
|
||||
if (!this._audioCtx) this._audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
var osc = this._audioCtx.createOscillator();
|
||||
var gain = this._audioCtx.createGain();
|
||||
osc.type = type || 'sine';
|
||||
osc.frequency.value = freq;
|
||||
gain.gain.value = 0.1;
|
||||
osc.connect(gain);
|
||||
gain.connect(this._audioCtx.destination);
|
||||
osc.start();
|
||||
osc.stop(this._audioCtx.currentTime + duration);
|
||||
} catch(e) {}
|
||||
},
|
||||
_flash: function(color) {
|
||||
var el = document.createElement('div');
|
||||
el.style.cssText = 'position:fixed;inset:0;z-index:99999;opacity:0.3;background:' + color + ';pointer-events:none;transition:opacity 0.2s;';
|
||||
document.body.appendChild(el);
|
||||
setTimeout(function() { el.style.opacity = '0'; }, 50);
|
||||
setTimeout(function() { el.remove(); }, 300);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Saved Filters ─────────────────────────────────────────────
|
||||
window.SavedFilters = {
|
||||
_key: function(page) { return 'pos_filters_' + (page || window.location.pathname); },
|
||||
save: function(name, filters) {
|
||||
var key = this._key();
|
||||
var saved = this.list();
|
||||
saved.push({ name: name, filters: filters, created: Date.now() });
|
||||
localStorage.setItem(key, JSON.stringify(saved));
|
||||
},
|
||||
list: function() {
|
||||
try { return JSON.parse(localStorage.getItem(this._key()) || '[]'); } catch(e) { return []; }
|
||||
},
|
||||
remove: function(name) {
|
||||
var saved = this.list().filter(function(f) { return f.name !== name; });
|
||||
localStorage.setItem(this._key(), JSON.stringify(saved));
|
||||
},
|
||||
renderChips: function(containerId, onApply) {
|
||||
var container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
var saved = this.list();
|
||||
if (!saved.length) { container.innerHTML = ''; return; }
|
||||
var html = '';
|
||||
saved.forEach(function(f) {
|
||||
html += '<span class="filter-chip">' + esc(f.name) +
|
||||
'<button class="filter-chip__remove" onclick="SavedFilters.remove(\'' + esc(f.name) + '\');SavedFilters.renderChips(\'' + containerId + '\');">×</button></span>';
|
||||
});
|
||||
container.innerHTML = html;
|
||||
container.querySelectorAll('.filter-chip').forEach(function(chip, i) {
|
||||
chip.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('filter-chip__remove')) return;
|
||||
if (onApply) onApply(saved[i].filters);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ── Resizable Columns ─────────────────────────────────────────
|
||||
window.makeTableResizable = function(tableSelector) {
|
||||
var table = document.querySelector(tableSelector);
|
||||
if (!table) return;
|
||||
var ths = table.querySelectorAll('thead th');
|
||||
ths.forEach(function(th, i) {
|
||||
if (i >= ths.length - 1) return; // skip last column
|
||||
var handle = document.createElement('div');
|
||||
handle.className = 'resize-handle';
|
||||
th.appendChild(handle);
|
||||
|
||||
handle.addEventListener('mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
var startX = e.pageX;
|
||||
var startW = th.offsetWidth;
|
||||
th.classList.add('is-resizing');
|
||||
|
||||
function onMove(ev) {
|
||||
var newW = Math.max(60, startW + (ev.pageX - startX));
|
||||
th.style.width = newW + 'px';
|
||||
th.style.minWidth = newW + 'px';
|
||||
}
|
||||
function onUp() {
|
||||
th.classList.remove('is-resizing');
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
}
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ── Density / Touch Mode Toggles ──────────────────────────────
|
||||
window.DensityToggle = {
|
||||
set: function(density) {
|
||||
document.documentElement.setAttribute('data-density', density);
|
||||
localStorage.setItem('pos_density', density);
|
||||
},
|
||||
init: function() {
|
||||
var saved = localStorage.getItem('pos_density') || 'normal';
|
||||
document.documentElement.setAttribute('data-density', saved);
|
||||
}
|
||||
};
|
||||
window.TouchModeToggle = {
|
||||
set: function(enabled) {
|
||||
document.documentElement.setAttribute('data-touch', enabled ? 'true' : 'false');
|
||||
localStorage.setItem('pos_touch_mode', enabled ? 'true' : 'false');
|
||||
},
|
||||
init: function() {
|
||||
var saved = localStorage.getItem('pos_touch_mode') === 'true';
|
||||
document.documentElement.setAttribute('data-touch', saved ? 'true' : 'false');
|
||||
}
|
||||
};
|
||||
DensityToggle.init();
|
||||
TouchModeToggle.init();
|
||||
|
||||
// ── Notifications Dropdown (functional) ───────────────────────
|
||||
window.NotificationsDropdown = {
|
||||
_visible: false,
|
||||
_el: null,
|
||||
toggle: function() {
|
||||
if (this._visible) { this.hide(); return; }
|
||||
this.show();
|
||||
},
|
||||
show: function() {
|
||||
if (this._el) this._el.remove();
|
||||
var btn = document.getElementById('notifDropdownBtn');
|
||||
var el = document.createElement('div');
|
||||
el.className = 'notif-dropdown';
|
||||
el.innerHTML = '<div class="notif-dropdown__header">Notificaciones <button onclick="NotificationsDropdown.hide()" style="background:none;border:none;color:var(--color-text-muted);cursor:pointer;font-size:16px;">✕</button></div>' +
|
||||
'<div class="notif-dropdown__list" id="notifDropdownList"><div class="notif-dropdown__empty">Sin notificaciones nuevas</div></div>';
|
||||
if (btn) {
|
||||
btn.parentElement.style.position = 'relative';
|
||||
btn.parentElement.appendChild(el);
|
||||
} else {
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
this._el = el;
|
||||
this._visible = true;
|
||||
this._load();
|
||||
setTimeout(function() {
|
||||
document.addEventListener('click', function handler(e) {
|
||||
if (!el.contains(e.target) && e.target !== btn) {
|
||||
NotificationsDropdown.hide();
|
||||
document.removeEventListener('click', handler);
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
},
|
||||
hide: function() { if (this._el) { this._el.remove(); this._el = null; } this._visible = false; },
|
||||
_load: function() {
|
||||
// Stub: can be wired to /api/notifications endpoint
|
||||
var list = document.getElementById('notifDropdownList');
|
||||
if (!list) return;
|
||||
// Example: show inventory alerts count if available
|
||||
var token = localStorage.getItem('pos_token');
|
||||
if (!token) return;
|
||||
fetch('/pos/api/inventory/alerts', { headers: { 'Authorization': 'Bearer ' + token } })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
var count = (d.counts || {}).critical || 0;
|
||||
if (count > 0) {
|
||||
list.innerHTML = '<div class="notif-dropdown__item notif-dropdown__item--unread" onclick="window.location.href=\'/pos/inventory#alertas\'">' +
|
||||
'<div class="notif-dropdown__icon">⚠️</div>' +
|
||||
'<div class="notif-dropdown__content"><div class="notif-dropdown__title">' + count + ' producto' + (count > 1 ? 's' : '') + ' sin stock</div>' +
|
||||
'<div class="notif-dropdown__time">Stock crítico</div></div></div>';
|
||||
}
|
||||
}).catch(function() {});
|
||||
}
|
||||
};
|
||||
|
||||
// ── Ticket Preview Helper ─────────────────────────────────────
|
||||
window.previewTicket = function(ticketData) {
|
||||
var data = ticketData || {};
|
||||
var items = (data.items || []).map(function(it) {
|
||||
return '<div class="ticket-preview__row"><span>' + (it.quantity || 1) + 'x ' + esc(it.name) + '</span><span>$' + (it.subtotal || 0).toFixed(2) + '</span></div>';
|
||||
}).join('');
|
||||
var html = '<div class="ticket-preview">' +
|
||||
'<div class="ticket-preview__header"><div class="ticket-preview__title">Nexus Autoparts</div><div class="ticket-preview__meta">' + (data.store || 'Sucursal Centro') + '</div></div>' +
|
||||
'<div class="ticket-preview__meta" style="text-align:center;margin-bottom:8px;">' + new Date().toLocaleString('es-MX') + '</div>' +
|
||||
'<div class="ticket-preview__row"><span>Ticket #' + (data.id || '---') + '</span></div>' +
|
||||
'<hr style="border:none;border-top:1px dashed #ccc;margin:8px 0;">' +
|
||||
items +
|
||||
'<hr style="border:none;border-top:1px dashed #ccc;margin:8px 0;">' +
|
||||
'<div class="ticket-preview__row"><span>Subtotal</span><span>$' + (data.subtotal || 0).toFixed(2) + '</span></div>' +
|
||||
'<div class="ticket-preview__row"><span>IVA</span><span>$' + (data.tax || 0).toFixed(2) + '</span></div>' +
|
||||
'<div class="ticket-preview__total"><span>TOTAL</span><span>$' + (data.total || 0).toFixed(2) + '</span></div>' +
|
||||
'<div style="text-align:center;font-size:10px;color:#666;margin-top:12px;">Gracias por su compra</div>' +
|
||||
'</div>';
|
||||
return html;
|
||||
};
|
||||
|
||||
// ── Image Comparator Helper ───────────────────────────────────
|
||||
window.initImageComparator = function(containerSelector) {
|
||||
var container = document.querySelector(containerSelector);
|
||||
if (!container) return;
|
||||
var overlay = container.querySelector('.img-compare__overlay');
|
||||
var handle = container.querySelector('.img-compare__handle');
|
||||
if (!overlay || !handle) return;
|
||||
|
||||
function move(x) {
|
||||
var rect = container.getBoundingClientRect();
|
||||
var pct = Math.max(0, Math.min(100, ((x - rect.left) / rect.width) * 100));
|
||||
overlay.style.width = pct + '%';
|
||||
handle.style.left = pct + '%';
|
||||
}
|
||||
handle.addEventListener('mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
function onMove(ev) { move(ev.pageX); }
|
||||
function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
});
|
||||
handle.addEventListener('touchstart', function(e) {
|
||||
function onMove(ev) { move(ev.touches[0].pageX); }
|
||||
function onUp() { document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onUp); }
|
||||
document.addEventListener('touchmove', onMove);
|
||||
document.addEventListener('touchend', onUp);
|
||||
});
|
||||
};
|
||||
|
||||
// ── Infinite Scroll Helper ────────────────────────────────────
|
||||
window.InfiniteScroll = function(opts) {
|
||||
opts = opts || {};
|
||||
var container = opts.container || window;
|
||||
var threshold = opts.threshold || 200;
|
||||
var loading = false;
|
||||
var observer = new IntersectionObserver(function(entries) {
|
||||
if (entries[0].isIntersecting && !loading && opts.onLoad) {
|
||||
loading = true;
|
||||
opts.onLoad(function() { loading = false; });
|
||||
}
|
||||
}, { root: container === window ? null : container, rootMargin: threshold + 'px' });
|
||||
var sentinel = document.createElement('div');
|
||||
sentinel.style.cssText = 'height:1px;';
|
||||
(opts.sentinelParent || document.body).appendChild(sentinel);
|
||||
observer.observe(sentinel);
|
||||
return { disconnect: function() { observer.disconnect(); sentinel.remove(); } };
|
||||
};
|
||||
|
||||
// ── Sparkline Renderer ────────────────────────────────────────
|
||||
window.renderSparkline = function(containerSelector, values, opts) {
|
||||
opts = opts || {};
|
||||
var container = typeof containerSelector === 'string' ? document.querySelector(containerSelector) : containerSelector;
|
||||
if (!container || !values || !values.length) return;
|
||||
var max = Math.max.apply(null, values) || 1;
|
||||
var min = Math.min.apply(null, values);
|
||||
var range = max - min || 1;
|
||||
var cls = opts.trend === 'up' ? 'sparkline--up' : (opts.trend === 'down' ? 'sparkline--down' : '');
|
||||
var html = '<div class="sparkline ' + cls + '">';
|
||||
values.forEach(function(v) {
|
||||
var h = Math.max(4, Math.round(((v - min) / range) * 100));
|
||||
html += '<div class="sparkline__bar" style="height:' + h + '%" title="' + (opts.prefix || '') + v + '"></div>';
|
||||
});
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
@@ -123,6 +123,8 @@ const POS = (() => {
|
||||
currentRegister = null;
|
||||
document.getElementById('registerInfo').innerHTML =
|
||||
'<span style="color:var(--color-error);cursor:pointer;" onclick="POS.showOpenRegisterModal()" title="Clic para abrir caja">⚠ Sin caja abierta — Clic para abrir</span>';
|
||||
// Force open register modal on first load
|
||||
showOpenRegisterModal();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Register check failed:', e);
|
||||
@@ -240,6 +242,7 @@ const POS = (() => {
|
||||
if (existing) {
|
||||
existing.quantity += (item.quantity || 1);
|
||||
renderCart();
|
||||
if (window.BarcodeFeedback) BarcodeFeedback.success();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -253,6 +256,9 @@ const POS = (() => {
|
||||
discount_pct: parseFloat(item.discount_pct || 0),
|
||||
tax_rate: parseFloat(item.tax_rate || 0.16),
|
||||
stock: item.stock || 0,
|
||||
price_1: parseFloat(item.price_1 || 0),
|
||||
price_2: parseFloat(item.price_2 || 0),
|
||||
price_3: parseFloat(item.price_3 || 0),
|
||||
});
|
||||
|
||||
renderCart();
|
||||
@@ -265,6 +271,82 @@ const POS = (() => {
|
||||
renderCart();
|
||||
}
|
||||
|
||||
function clearCart() {
|
||||
cart.length = 0;
|
||||
selectedRow = -1;
|
||||
renderCart();
|
||||
}
|
||||
|
||||
function openCancelModal() {
|
||||
const overlay = document.getElementById('overlay-cancelar-venta');
|
||||
const dialog = document.getElementById('modal-cancelar-venta');
|
||||
if (overlay) overlay.classList.add('active');
|
||||
if (dialog) dialog.classList.add('active');
|
||||
}
|
||||
|
||||
function closeCancelModal() {
|
||||
const overlay = document.getElementById('overlay-cancelar-venta');
|
||||
const dialog = document.getElementById('modal-cancelar-venta');
|
||||
if (overlay) overlay.classList.remove('active');
|
||||
if (dialog) dialog.classList.remove('active');
|
||||
}
|
||||
|
||||
function changeQuantity() {
|
||||
if (selectedRow < 0 || selectedRow >= cart.length) {
|
||||
showToast('Selecciona un articulo primero', 'warn');
|
||||
return;
|
||||
}
|
||||
const q = prompt('Nueva cantidad:', cart[selectedRow].quantity);
|
||||
if (q !== null) {
|
||||
const n = parseInt(q);
|
||||
if (n > 0) {
|
||||
cart[selectedRow].quantity = n;
|
||||
renderCart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyDiscount() {
|
||||
if (selectedRow < 0 || selectedRow >= cart.length) {
|
||||
showToast('Selecciona un articulo primero', 'warn');
|
||||
return;
|
||||
}
|
||||
const d = prompt('Descuento %:', cart[selectedRow].discount_pct);
|
||||
if (d !== null) {
|
||||
const n = parseFloat(d);
|
||||
if (n >= 0 && n <= 100) {
|
||||
cart[selectedRow].discount_pct = n;
|
||||
renderCart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function modifyPrice() {
|
||||
if (selectedRow < 0 || selectedRow >= cart.length) {
|
||||
showToast('Selecciona un articulo primero', 'warn');
|
||||
return;
|
||||
}
|
||||
const p = prompt('Nuevo precio unitario:', cart[selectedRow].unit_price);
|
||||
if (p !== null) {
|
||||
const n = parseFloat(p);
|
||||
if (n >= 0) {
|
||||
cart[selectedRow].unit_price = n;
|
||||
renderCart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wire confirm-cancel button
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var btn = document.getElementById('btnConfirmCancel');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
clearCart();
|
||||
closeCancelModal();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function renderCart() {
|
||||
const tbody = document.getElementById('cartBody');
|
||||
const table = document.getElementById('cartTable');
|
||||
@@ -436,6 +518,19 @@ const POS = (() => {
|
||||
|
||||
if (data.data.length === 0) {
|
||||
container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--color-text-muted);">Sin resultados</div>';
|
||||
if (window.BarcodeFeedback) BarcodeFeedback.error();
|
||||
} else if (data.data.length === 1 && q.length >= 8) {
|
||||
// Auto-select single result on barcode scan (long codes)
|
||||
const item = data.data[0];
|
||||
let price = item.price_1;
|
||||
if (currentCustomer) {
|
||||
const tier = currentCustomer.price_tier || 1;
|
||||
price = tier === 3 ? item.price_3 : tier === 2 ? item.price_2 : item.price_1;
|
||||
}
|
||||
addFromSearch(item, price);
|
||||
input.value = '';
|
||||
hideSearchResults();
|
||||
return;
|
||||
} else {
|
||||
let html = '';
|
||||
data.data.forEach(item => {
|
||||
@@ -472,7 +567,11 @@ const POS = (() => {
|
||||
cost: item.cost,
|
||||
tax_rate: item.tax_rate,
|
||||
stock: item.stock,
|
||||
price_1: item.price_1,
|
||||
price_2: item.price_2,
|
||||
price_3: item.price_3,
|
||||
});
|
||||
if (window.BarcodeFeedback) BarcodeFeedback.success();
|
||||
hideSearchResults();
|
||||
document.getElementById('itemSearch').value = '';
|
||||
document.getElementById('itemSearch').focus();
|
||||
@@ -530,11 +629,22 @@ const POS = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
function recalcCartPrices() {
|
||||
const tier = currentCustomer ? (currentCustomer.price_tier || 1) : 1;
|
||||
cart.forEach(item => {
|
||||
if (item.price_1 > 0) {
|
||||
item.unit_price = tier === 3 ? item.price_3 : tier === 2 ? item.price_2 : item.price_1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function selectCustomer(customer) {
|
||||
currentCustomer = customer;
|
||||
document.getElementById('customerAutocomplete').style.display = 'none';
|
||||
document.getElementById('customerSearchWrap').querySelector('input').style.display = 'none';
|
||||
|
||||
recalcCartPrices();
|
||||
|
||||
const tiers = { 1: 'P1 Mostrador', 2: 'P2 Taller', 3: 'P3 Mayoreo' };
|
||||
document.getElementById('customerName').textContent = customer.name;
|
||||
document.getElementById('customerTier').textContent = tiers[customer.price_tier] || 'P1';
|
||||
@@ -1255,7 +1365,7 @@ const POS = (() => {
|
||||
init();
|
||||
|
||||
return {
|
||||
addToCart, removeFromCart, selectRow,
|
||||
addToCart, removeFromCart, clearCart, selectRow,
|
||||
updateQty, updateDiscount,
|
||||
addFromSearch, hideSearchResults,
|
||||
selectCustomer, clearCustomer,
|
||||
@@ -1268,5 +1378,14 @@ const POS = (() => {
|
||||
connectThermal, thermalPrint,
|
||||
showOpenRegisterModal, closeOpenRegisterModal, openRegister,
|
||||
showCutZModal, closeCutZModal, loadCutX, confirmCutZ,
|
||||
openCancelModal, closeCancelModal, changeQuantity, applyDiscount, modifyPrice,
|
||||
};
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
registerCmdKItem({ group: "Principal", label: "POS Ventas", href: "/pos/sale", icon: "🛒" });
|
||||
registerCmdKItem({ group: "Principal", label: "Catálogo", href: "/pos/catalog", icon: "📁" });
|
||||
registerCmdKItem({ group: "Principal", label: "Clientes", href: "/pos/customers", icon: "👤" });
|
||||
registerCmdKItem({ group: "Principal", label: "Dashboard", href: "/pos/dashboard", icon: "📊" });
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
2
pos/static/js/pos.min.js
vendored
2
pos/static/js/pos.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -714,6 +714,13 @@ const Reports = (() => {
|
||||
init, setTheme, switchTab,
|
||||
loadVentas, loadInventario, loadClientes, loadFinancieros, fmt
|
||||
};
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
registerCmdKItem({ group: "Principal", label: "Reportes", href: "/pos/reports", icon: "📈" });
|
||||
registerCmdKItem({ group: "Principal", label: "Dashboard", href: "/pos/dashboard", icon: "📊" });
|
||||
registerCmdKItem({ group: "Principal", label: "POS Ventas", href: "/pos/sale", icon: "🛒" });
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
// ── Global: Export visible table as CSV (Excel-compatible) ──
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Replaces existing sidebar in each page with a consistent, themed version.
|
||||
* Uses i18n t() for all labels when available.
|
||||
*/
|
||||
(function() {
|
||||
window.renderSidebar = function(modulesOverride) {
|
||||
'use strict';
|
||||
|
||||
// i18n helper — falls back to raw string if i18n.js not loaded
|
||||
@@ -17,24 +17,38 @@
|
||||
var currentTheme = localStorage.getItem('pos_theme') || 'industrial';
|
||||
var currentLang = localStorage.getItem('pos_lang') || 'es';
|
||||
|
||||
var modules = {};
|
||||
if (modulesOverride && typeof modulesOverride === 'object') {
|
||||
modules = modulesOverride;
|
||||
} else {
|
||||
try {
|
||||
modules = JSON.parse(localStorage.getItem('pos_modules') || '{}');
|
||||
} catch(e) { modules = {}; }
|
||||
}
|
||||
|
||||
function moduleEnabled(key) {
|
||||
// Default to true if not configured yet
|
||||
return modules[key] !== false;
|
||||
}
|
||||
|
||||
var navSections = [
|
||||
{ label: _t('nav_main'), items: [
|
||||
{ name: _t('dashboard'), href: '/pos/dashboard', icon: '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>' },
|
||||
{ name: _t('pos'), href: '/pos/sale', icon: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>' },
|
||||
{ name: _t('catalog'), href: '/pos/catalog', icon: '<path d="M4 6h16M4 10h16M4 14h16M4 18h16"/>' },
|
||||
moduleEnabled('catalog') ? { name: _t('catalog'), href: '/pos/catalog', icon: '<path d="M4 6h16M4 10h16M4 14h16M4 18h16"/>' } : null,
|
||||
{ name: _t('inventory'), href: '/pos/inventory', icon: '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>' },
|
||||
|
||||
]},
|
||||
].filter(Boolean)},
|
||||
{ label: _t('nav_management'), items: [
|
||||
{ name: _t('customers'), href: '/pos/customers', icon: '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/>' },
|
||||
{ name: 'Cotizaciones', href: '/pos/quotations', icon: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="15" x2="15" y2="15"/><line x1="12" y1="12" x2="12" y2="18"/>' },
|
||||
{ name: 'Marketplace', href: '/pos/marketplace', icon: '<circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>' },
|
||||
moduleEnabled('marketplace') ? { name: 'Marketplace', href: '/pos/marketplace', icon: '<circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>' } : null,
|
||||
moduleEnabled('meli') ? { name: 'MercadoLibre', href: '/pos/marketplace-external', icon: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>' } : null,
|
||||
{ name: _t('invoicing'), href: '/pos/invoicing', icon: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>' },
|
||||
{ name: _t('accounting'), href: '/pos/accounting', icon: '<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>' },
|
||||
{ name: _t('reports'), href: '/pos/reports', icon: '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>' },
|
||||
{ name: _t('fleet'), href: '/pos/fleet', icon: '<path d="M1 13h22M1 13l2-6h6l2 6M9 7h6l2 6M15 13l2-6M5 17a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM19 17a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/>' },
|
||||
{ name: _t('whatsapp'), href: '/pos/whatsapp', icon: '<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>' },
|
||||
]},
|
||||
moduleEnabled('whatsapp') ? { name: _t('whatsapp'), href: '/pos/whatsapp', icon: '<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>' } : null,
|
||||
].filter(Boolean)},
|
||||
{ label: _t('nav_system'), items: [
|
||||
{ name: _t('config'), href: '/pos/config', icon: '<circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/>' },
|
||||
]},
|
||||
@@ -124,10 +138,6 @@
|
||||
if (main) main.classList.add('pos-main-offset');
|
||||
|
||||
// ── Tablet/mobile: sidebar toggle + overlay ─────────────────────
|
||||
// Creates a hamburger button + overlay for screens < 1024px.
|
||||
// The CSS in pos-glass.css hides the sidebar by default on tablets
|
||||
// and shows it as a slide-in drawer when .open is added.
|
||||
|
||||
var sidebar = document.querySelector('.pos-sidebar, .sidebar, #sidebar');
|
||||
var overlay = document.getElementById('sidebar-overlay');
|
||||
|
||||
@@ -180,4 +190,9 @@
|
||||
window.toggleSidebar = toggleSidebar;
|
||||
window.closeSidebar = closeSidebar;
|
||||
|
||||
})();
|
||||
// Reveal sidebar smoothly after replacement to avoid flash/stun
|
||||
document.body.classList.add('sidebar-ready');
|
||||
};
|
||||
|
||||
// Initial render
|
||||
window.renderSidebar();
|
||||
|
||||
131
pos/static/js/splash-loader.js
Normal file
131
pos/static/js/splash-loader.js
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* splash-loader.js — PWA splash screen + dynamic favicon
|
||||
* Show an animated splash while the app loads, then fade out.
|
||||
* Also provides dynamic favicon updates for notifications / offline states.
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ─── Create splash element ──────────────────────────────────────────────
|
||||
var splash = document.createElement('div');
|
||||
splash.id = 'nx-splash';
|
||||
splash.style.cssText = 'position:fixed;inset:0;z-index:99999;' +
|
||||
'background:linear-gradient(135deg,#0d0d0d 0%,#1a1205 100%);' +
|
||||
'display:flex;flex-direction:column;align-items:center;justify-content:center;' +
|
||||
'transition:opacity 0.5s ease,visibility 0.5s ease;';
|
||||
|
||||
// Logo SVG (animated)
|
||||
var logoSvg = '<svg width="80" height="80" viewBox="0 0 80 80" style="margin-bottom:24px;">' +
|
||||
'<defs><linearGradient id="nxg" x1="0%" y1="0%" x2="100%" y2="100%">' +
|
||||
'<stop offset="0%" style="stop-color:#F5A623"/>' +
|
||||
'<stop offset="100%" style="stop-color:#E08E00"/>' +
|
||||
'</linearGradient></defs>' +
|
||||
'<circle cx="40" cy="40" r="36" fill="none" stroke="url(#nxg)" stroke-width="3" stroke-dasharray="226" stroke-dashoffset="226" style="animation:nxDraw 1.2s ease forwards;">' +
|
||||
'<animateTransform attributeName="transform" type="rotate" from="0 40 40" to="360 40 40" dur="8s" repeatCount="indefinite"/>' +
|
||||
'</circle>' +
|
||||
'<text x="40" y="48" text-anchor="middle" fill="#F5A623" font-family="system-ui,sans-serif" font-weight="800" font-size="28" style="animation:nxFadeIn 0.6s 0.4s ease both;">N</text>' +
|
||||
'</svg>';
|
||||
|
||||
var title = '<div style="font-family:system-ui,sans-serif;font-size:20px;font-weight:700;color:#eee;letter-spacing:2px;text-transform:uppercase;margin-bottom:8px;">Nexus</div>';
|
||||
var subtitle = '<div style="font-family:system-ui,sans-serif;font-size:13px;color:#888;letter-spacing:4px;text-transform:uppercase;">Autoparts POS</div>';
|
||||
var spinner = '<div class="nx-loader" style="margin-top:32px;width:32px;height:32px;"><div class="nx-loader__ring"></div><div class="nx-loader__ring"></div></div>';
|
||||
|
||||
splash.innerHTML = logoSvg + title + subtitle + spinner;
|
||||
|
||||
// Inject keyframes if not present
|
||||
if (!document.getElementById('nx-splash-styles')) {
|
||||
var style = document.createElement('style');
|
||||
style.id = 'nx-splash-styles';
|
||||
style.textContent = '@keyframes nxDraw { to { stroke-dashoffset:0; } } @keyframes nxFadeIn { from { opacity:0;transform:translateY(8px); } to { opacity:1;transform:translateY(0); } }';
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
document.body.appendChild(splash);
|
||||
|
||||
// Hide splash when DOM is ready + minimum display time
|
||||
var minDisplay = 800;
|
||||
var startTime = Date.now();
|
||||
|
||||
function hideSplash() {
|
||||
var elapsed = Date.now() - startTime;
|
||||
var remaining = Math.max(0, minDisplay - elapsed);
|
||||
setTimeout(function() {
|
||||
splash.style.opacity = '0';
|
||||
splash.style.visibility = 'hidden';
|
||||
setTimeout(function() { splash.remove(); }, 500);
|
||||
}, remaining);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', hideSplash);
|
||||
} else {
|
||||
hideSplash();
|
||||
}
|
||||
|
||||
// ─── Dynamic Favicon ────────────────────────────────────────────────────
|
||||
window.NexusFavicon = {
|
||||
canvas: null,
|
||||
ctx: null,
|
||||
link: null,
|
||||
baseColor: '#F5A623',
|
||||
|
||||
init: function() {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.width = 64;
|
||||
this.canvas.height = 64;
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.link = document.querySelector('link[rel*="icon"]') || document.createElement('link');
|
||||
this.link.rel = 'shortcut icon';
|
||||
this.link.type = 'image/png';
|
||||
document.head.appendChild(this.link);
|
||||
this.setNormal();
|
||||
},
|
||||
|
||||
draw: function(color, badge) {
|
||||
var ctx = this.ctx;
|
||||
var c = this.canvas;
|
||||
ctx.clearRect(0, 0, 64, 64);
|
||||
|
||||
// Background circle
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 30, 0, Math.PI * 2);
|
||||
ctx.fillStyle = color || this.baseColor;
|
||||
ctx.fill();
|
||||
|
||||
// Letter N
|
||||
ctx.fillStyle = '#0d0d0d';
|
||||
ctx.font = 'bold 36px system-ui,sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('N', 32, 33);
|
||||
|
||||
// Badge dot
|
||||
if (badge) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(52, 12, 10, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#ef4444';
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 11px system-ui,sans-serif';
|
||||
ctx.fillText(badge > 9 ? '9+' : String(badge), 52, 13);
|
||||
}
|
||||
|
||||
this.link.href = c.toDataURL('image/png');
|
||||
},
|
||||
|
||||
setNormal: function() { this.draw(this.baseColor); },
|
||||
setOffline: function() { this.draw('#666'); },
|
||||
setNotify: function(count) { this.draw(this.baseColor, count); },
|
||||
};
|
||||
|
||||
// Auto-init favicon
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() { NexusFavicon.init(); });
|
||||
} else {
|
||||
NexusFavicon.init();
|
||||
}
|
||||
|
||||
// Update favicon on online/offline
|
||||
window.addEventListener('online', function() { if (window.NexusFavicon) NexusFavicon.setNormal(); });
|
||||
window.addEventListener('offline', function() { if (window.NexusFavicon) NexusFavicon.setOffline(); });
|
||||
})();
|
||||
299
pos/static/js/supplier_catalog.js
Normal file
299
pos/static/js/supplier_catalog.js
Normal file
@@ -0,0 +1,299 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const API = '/pos/api/supplier-catalog';
|
||||
const VEHICLE_API = '/pos/api/inventory/vehicles';
|
||||
const token = localStorage.getItem('pos_token') || '';
|
||||
|
||||
let state = {
|
||||
q: '',
|
||||
category: '',
|
||||
make: '',
|
||||
model: '',
|
||||
year: '',
|
||||
engine: '',
|
||||
myeId: null,
|
||||
page: 1,
|
||||
perPage: 30,
|
||||
totalPages: 1,
|
||||
categories: [],
|
||||
items: []
|
||||
};
|
||||
|
||||
function headers() {
|
||||
return { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
|
||||
}
|
||||
|
||||
let scAbort = null;
|
||||
let scSeq = 0;
|
||||
|
||||
async function apiFetch(url) {
|
||||
if (scAbort) {
|
||||
scAbort.abort();
|
||||
scAbort = null;
|
||||
}
|
||||
const ctrl = new AbortController();
|
||||
scAbort = ctrl;
|
||||
try {
|
||||
const resp = await fetch(url, { headers: headers(), signal: ctrl.signal });
|
||||
if (resp.status === 401) { window.location.href = '/pos/login'; return null; }
|
||||
if (!resp.ok) { console.error('API error', url, resp.status); return null; }
|
||||
return resp.json();
|
||||
} catch (e) {
|
||||
if (e.name === 'AbortError') return null;
|
||||
console.error('API error', url, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function apiFetchSeq(url) {
|
||||
const mySeq = ++scSeq;
|
||||
const data = await apiFetch(url);
|
||||
if (!data || scSeq !== mySeq) return null;
|
||||
return data;
|
||||
}
|
||||
|
||||
// ─── Categories ─────────────────────────────────────────────
|
||||
async function loadCategories() {
|
||||
const data = await apiFetch(API + '/categories');
|
||||
if (!data) return;
|
||||
state.categories = data.categories || [];
|
||||
renderCategories();
|
||||
}
|
||||
|
||||
function renderCategories() {
|
||||
const el = document.getElementById('categoriesGrid');
|
||||
if (!el) return;
|
||||
let html = '<div class="sc-cat-card' + (state.category === '' ? ' active' : '') + '" onclick="selectCategory(\'\')">' +
|
||||
'<div>Todas</div><div class="count">' + state.categories.reduce((a,c)=>a+c.count,0) + ' items</div></div>';
|
||||
state.categories.forEach(function(c) {
|
||||
html += '<div class="sc-cat-card' + (state.category === c.name ? ' active' : '') + '" onclick="selectCategory(\'' + escapeHtml(c.name) + '\')">' +
|
||||
'<div>' + escapeHtml(c.name) + '</div><div class="count">' + c.count + ' items</div></div>';
|
||||
});
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
window.selectCategory = function(name) {
|
||||
state.category = name;
|
||||
state.page = 1;
|
||||
renderCategories();
|
||||
doSearch();
|
||||
};
|
||||
|
||||
// ─── Vehicle filters ────────────────────────────────────────
|
||||
async function loadMakes() {
|
||||
const data = await apiFetch(VEHICLE_API + '/makes');
|
||||
if (!data) return;
|
||||
const sel = document.getElementById('filterMake');
|
||||
sel.innerHTML = '<option value="">Marca vehiculo</option>';
|
||||
(data.data || []).forEach(function(m) {
|
||||
sel.innerHTML += '<option value="' + escapeHtml(m.name_brand) + '">' + escapeHtml(m.name_brand) + '</option>';
|
||||
});
|
||||
}
|
||||
|
||||
window.onMakeChange = async function() {
|
||||
const sel = document.getElementById('filterMake');
|
||||
state.make = sel.value;
|
||||
state.model = ''; state.year = ''; state.engine = ''; state.myeId = null;
|
||||
document.getElementById('filterModel').disabled = true;
|
||||
document.getElementById('filterYear').disabled = true;
|
||||
document.getElementById('filterEngine').disabled = true;
|
||||
if (!state.make) { doSearch(); return; }
|
||||
|
||||
const makes = await apiFetchSeq(VEHICLE_API + '/makes');
|
||||
if (!makes) return;
|
||||
const brand = (makes.data || []).find(function(m) { return m.name_brand === state.make; });
|
||||
if (!brand) { doSearch(); return; }
|
||||
|
||||
const models = await apiFetchSeq(VEHICLE_API + '/models?brand_id=' + brand.id_brand);
|
||||
if (!models) return;
|
||||
const msel = document.getElementById('filterModel');
|
||||
msel.innerHTML = '<option value="">Modelo</option>';
|
||||
(models.data || []).forEach(function(m) {
|
||||
msel.innerHTML += '<option value="' + m.id_model + '">' + escapeHtml(m.name_model) + '</option>';
|
||||
});
|
||||
msel.disabled = false;
|
||||
doSearch();
|
||||
};
|
||||
|
||||
window.onModelChange = async function() {
|
||||
const sel = document.getElementById('filterModel');
|
||||
const modelId = sel.value;
|
||||
state.model = modelId ? sel.options[sel.selectedIndex].text : '';
|
||||
state.year = ''; state.engine = ''; state.myeId = null;
|
||||
document.getElementById('filterYear').disabled = true;
|
||||
document.getElementById('filterEngine').disabled = true;
|
||||
if (!modelId) { doSearch(); return; }
|
||||
|
||||
const years = await apiFetchSeq(VEHICLE_API + '/years?model_id=' + modelId);
|
||||
if (!years) return;
|
||||
const ysel = document.getElementById('filterYear');
|
||||
ysel.innerHTML = '<option value="">Año</option>';
|
||||
(years.data || []).forEach(function(y) {
|
||||
ysel.innerHTML += '<option value="' + y.id_year + '">' + y.year_car + '</option>';
|
||||
});
|
||||
ysel.disabled = false;
|
||||
doSearch();
|
||||
};
|
||||
|
||||
window.onYearChange = async function() {
|
||||
const sel = document.getElementById('filterYear');
|
||||
const yearId = sel.value;
|
||||
const modelId = document.getElementById('filterModel').value;
|
||||
state.year = yearId ? sel.options[sel.selectedIndex].text : '';
|
||||
state.engine = ''; state.myeId = null;
|
||||
document.getElementById('filterEngine').disabled = true;
|
||||
if (!yearId || !modelId) { doSearch(); return; }
|
||||
|
||||
const engines = await apiFetchSeq(VEHICLE_API + '/engines?model_id=' + modelId + '&year_id=' + yearId);
|
||||
if (!engines) return;
|
||||
const esel = document.getElementById('filterEngine');
|
||||
esel.innerHTML = '<option value="">Motorizacion</option>';
|
||||
(engines.data || []).forEach(function(e) {
|
||||
const label = escapeHtml(e.name_engine) + (e.trim_level ? ' (' + escapeHtml(e.trim_level) + ')' : '');
|
||||
esel.innerHTML += '<option value="' + e.id_mye + '">' + label + '</option>';
|
||||
});
|
||||
esel.disabled = false;
|
||||
doSearch();
|
||||
};
|
||||
|
||||
// ─── Search ─────────────────────────────────────────────────
|
||||
window.doSearch = async function() {
|
||||
state.q = document.getElementById('searchInput').value.trim();
|
||||
const engineSel = document.getElementById('filterEngine');
|
||||
state.myeId = engineSel.value || null;
|
||||
|
||||
let url = API + '/search?page=' + state.page + '&per_page=' + state.perPage;
|
||||
if (state.q) url += '&q=' + encodeURIComponent(state.q);
|
||||
if (state.category) url += '&category=' + encodeURIComponent(state.category);
|
||||
if (state.myeId) {
|
||||
url += '&mye_id=' + state.myeId;
|
||||
} else {
|
||||
if (state.make) url += '&make=' + encodeURIComponent(state.make);
|
||||
if (state.model) url += '&model=' + encodeURIComponent(state.model);
|
||||
if (state.year) url += '&year=' + encodeURIComponent(state.year);
|
||||
}
|
||||
|
||||
const data = await apiFetch(url);
|
||||
if (!data) return;
|
||||
state.items = data.data || [];
|
||||
state.totalPages = (data.pagination || {}).total_pages || 1;
|
||||
renderItems();
|
||||
renderPagination();
|
||||
};
|
||||
|
||||
window.clearFilters = function() {
|
||||
document.getElementById('searchInput').value = '';
|
||||
document.getElementById('filterMake').value = '';
|
||||
document.getElementById('filterModel').innerHTML = '<option value="">Modelo</option>'; document.getElementById('filterModel').disabled = true;
|
||||
document.getElementById('filterYear').innerHTML = '<option value="">Año</option>'; document.getElementById('filterYear').disabled = true;
|
||||
document.getElementById('filterEngine').innerHTML = '<option value="">Motorizacion</option>'; document.getElementById('filterEngine').disabled = true;
|
||||
state.q = ''; state.category = ''; state.make = ''; state.model = ''; state.year = ''; state.engine = ''; state.myeId = null; state.page = 1;
|
||||
renderCategories();
|
||||
doSearch();
|
||||
};
|
||||
|
||||
// ─── Render results ─────────────────────────────────────────
|
||||
function renderItems() {
|
||||
const el = document.getElementById('partsGrid');
|
||||
if (!el) return;
|
||||
if (!state.items.length) {
|
||||
el.innerHTML = '<div class="sc-empty" style="grid-column:1/-1;"><div style="font-size:48px;margin-bottom:var(--space-4);">🔍</div><h3>Sin resultados</h3><p>Intenta con otros filtros o terminos de busqueda.</p></div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = state.items.map(function(it) {
|
||||
return '<div class="sc-card" onclick="openDetail(' + it.id + ')">' +
|
||||
'<div class="sc-card__sku">' + escapeHtml(it.sku) + '</div>' +
|
||||
'<div class="sc-card__name">' + escapeHtml(it.name) + '</div>' +
|
||||
'<div class="sc-card__meta">' +
|
||||
'<span class="sc-card__badge">' + escapeHtml(it.category || 'SIN CATEGORIA') + '</span>' +
|
||||
' <span>' + escapeHtml(it.supplier_name) + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderPagination() {
|
||||
const el = document.getElementById('pagination');
|
||||
if (!el) return;
|
||||
if (state.totalPages <= 1) { el.innerHTML = ''; return; }
|
||||
let html = '<button ' + (state.page <= 1 ? 'disabled' : '') + ' onclick="goPage(' + (state.page - 1) + ')">Anterior</button>';
|
||||
html += '<span>Pagina ' + state.page + ' de ' + state.totalPages + '</span>';
|
||||
html += '<button ' + (state.page >= state.totalPages ? 'disabled' : '') + ' onclick="goPage(' + (state.page + 1) + ')">Siguiente</button>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
window.goPage = function(p) {
|
||||
state.page = p;
|
||||
doSearch();
|
||||
};
|
||||
|
||||
// ─── Detail modal ───────────────────────────────────────────
|
||||
window.openDetail = async function(id) {
|
||||
const data = await apiFetch(API + '/items/' + id);
|
||||
if (!data) return;
|
||||
document.getElementById('modalTitle').textContent = escapeHtml(data.sku);
|
||||
let html = '';
|
||||
html += '<div><strong style="font-size:var(--text-h6);">' + escapeHtml(data.name) + '</strong></div>';
|
||||
html += '<div class="sc-modal__section"><h4>Informacion</h4>' +
|
||||
'<p>Proveedor: ' + escapeHtml(data.supplier_name) + '<br>Categoria: ' + escapeHtml(data.category || 'N/A') + '</p></div>';
|
||||
|
||||
if (data.interchanges && data.interchanges.length) {
|
||||
html += '<div class="sc-modal__section"><h4>Intercambios</h4><div class="sc-interchange-list">' +
|
||||
data.interchanges.map(function(ix) {
|
||||
return '<span class="sc-interchange-chip">' + escapeHtml(ix.brand) + ' — ' + escapeHtml(ix.part_number) + '</span>';
|
||||
}).join('') + '</div></div>';
|
||||
}
|
||||
|
||||
if (data.compatibilities && data.compatibilities.length) {
|
||||
var seenCompat = {};
|
||||
var uniqCompat = data.compatibilities.filter(function(c) {
|
||||
var key = (c.make || '') + '|' + (c.model || '') + '|' + (c.year || '') + '|' + (c.engine || '');
|
||||
if (seenCompat[key]) return false;
|
||||
seenCompat[key] = true;
|
||||
return true;
|
||||
});
|
||||
html += '<div class="sc-modal__section"><h4>Vehiculos compatibles (' + uniqCompat.length + ')</h4>' +
|
||||
'<div class="sc-compat-grid">' +
|
||||
uniqCompat.slice(0, 50).map(function(c) {
|
||||
return '<div class="sc-compat-item">' +
|
||||
'<strong>' + escapeHtml(c.make || '') + ' ' + escapeHtml(c.model || '') + '</strong><br>' +
|
||||
(c.year || '') + ' ' + escapeHtml(c.engine || '') +
|
||||
'</div>';
|
||||
}).join('') +
|
||||
(uniqCompat.length > 50 ? '<div style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">... y ' + (uniqCompat.length - 50) + ' mas</div>' : '') +
|
||||
'</div></div>';
|
||||
}
|
||||
|
||||
document.getElementById('modalBody').innerHTML = html;
|
||||
document.getElementById('detailModal').classList.add('open');
|
||||
};
|
||||
|
||||
window.closeModal = function() {
|
||||
document.getElementById('detailModal').classList.remove('open');
|
||||
};
|
||||
|
||||
// ─── Utils ──────────────────────────────────────────────────
|
||||
function escapeHtml(s) {
|
||||
if (s == null) return '';
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ─── Init ───────────────────────────────────────────────────
|
||||
function init() {
|
||||
if (!token) { window.location.href = '/pos/login'; return; }
|
||||
loadCategories();
|
||||
loadMakes();
|
||||
doSearch().then(function() {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var id = params.get('id');
|
||||
if (id) { openDetail(parseInt(id)); }
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -24,6 +24,8 @@
|
||||
this._scrollHandler = this._onScroll.bind(this);
|
||||
this._resizeHandler = this._onResize.bind(this);
|
||||
this._isTbody = this.container.tagName === 'TBODY';
|
||||
this._rafId = null;
|
||||
this._pendingRender = false;
|
||||
this._init();
|
||||
}
|
||||
|
||||
@@ -58,11 +60,22 @@
|
||||
};
|
||||
|
||||
VirtualScroll.prototype._onScroll = function() {
|
||||
this._render();
|
||||
this._scheduleRender();
|
||||
};
|
||||
|
||||
VirtualScroll.prototype._onResize = function() {
|
||||
this._render();
|
||||
this._scheduleRender();
|
||||
};
|
||||
|
||||
VirtualScroll.prototype._scheduleRender = function() {
|
||||
if (this._pendingRender) return;
|
||||
this._pendingRender = true;
|
||||
var self = this;
|
||||
this._rafId = requestAnimationFrame(function() {
|
||||
self._rafId = null;
|
||||
self._pendingRender = false;
|
||||
self._render();
|
||||
});
|
||||
};
|
||||
|
||||
VirtualScroll.prototype._getScrollTop = function() {
|
||||
@@ -99,11 +112,7 @@
|
||||
var buffer = this.buffer;
|
||||
|
||||
if (!data.length) {
|
||||
if (this._isTbody) {
|
||||
this.container.innerHTML = this.emptyHtml;
|
||||
} else {
|
||||
this.container.innerHTML = this.emptyHtml;
|
||||
}
|
||||
this.container.innerHTML = this.emptyHtml;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,20 +121,19 @@
|
||||
var startIdx = Math.max(0, Math.floor(scrollTop / rowH) - buffer);
|
||||
var endIdx = Math.min(data.length, Math.ceil((scrollTop + containerHeight) / rowH) + buffer);
|
||||
|
||||
// Build new HTML
|
||||
var html = '';
|
||||
if (this._isTbody) {
|
||||
// Top spacer row
|
||||
var topSpacerHeight = startIdx * rowH;
|
||||
if (topSpacerHeight > 0) {
|
||||
html += '<tr style="height:' + topSpacerHeight + 'px;"><td colspan="99" style="padding:0;border:0;"></td></tr>';
|
||||
html += '<tr style="height:' + topSpacerHeight + 'px;" aria-hidden="true"><td colspan="99" style="padding:0;border:0;"></td></tr>';
|
||||
}
|
||||
for (var i = startIdx; i < endIdx; i++) {
|
||||
html += this.renderRow(data[i], i);
|
||||
}
|
||||
// Bottom spacer row
|
||||
var bottomSpacerHeight = (data.length - endIdx) * rowH;
|
||||
if (bottomSpacerHeight > 0) {
|
||||
html += '<tr style="height:' + bottomSpacerHeight + 'px;"><td colspan="99" style="padding:0;border:0;"></td></tr>';
|
||||
html += '<tr style="height:' + bottomSpacerHeight + 'px;" aria-hidden="true"><td colspan="99" style="padding:0;border:0;"></td></tr>';
|
||||
}
|
||||
} else {
|
||||
for (var j = startIdx; j < endIdx; j++) {
|
||||
@@ -133,6 +141,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Use a DocumentFragment approach via innerHTML to avoid flicker:
|
||||
// Setting innerHTML on tbody is the fastest way, but we can reduce
|
||||
// perceived flicker by ensuring the container has contain: paint
|
||||
// and by batching via rAF (done in _scheduleRender).
|
||||
this.container.innerHTML = html;
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
var activePhone = null;
|
||||
var pollTimer = null;
|
||||
var statusPollTimer = null;
|
||||
var qrPollTimer = null;
|
||||
var connectionState = 'unknown'; // 'open', 'close', 'connecting', 'unknown'
|
||||
|
||||
// -- Helpers ---------------------------------------------------------------
|
||||
@@ -88,6 +89,10 @@
|
||||
api('GET', '/status').then(function (data) {
|
||||
var state = (data.instance || data).state || data.state || 'close';
|
||||
updateConnectionUI(state);
|
||||
// If bridge already has a QR ready, show it immediately
|
||||
if (state === 'qr' || state === 'connecting') {
|
||||
fetchQR();
|
||||
}
|
||||
}).catch(function () {
|
||||
updateConnectionUI('close');
|
||||
});
|
||||
@@ -106,7 +111,8 @@
|
||||
// Load conversations + start polling on page load / reconnect
|
||||
loadConversations();
|
||||
startPolling();
|
||||
} else if (state === 'connecting') {
|
||||
stopQRPolling();
|
||||
} else if (state === 'connecting' || state === 'qr') {
|
||||
statusDot.className = 'status-dot status-dot--warn';
|
||||
statusText.textContent = 'Escaneando QR...';
|
||||
connectSection.style.display = 'flex';
|
||||
@@ -125,6 +131,7 @@
|
||||
refreshQrBtn.style.display = 'none';
|
||||
qrImg.style.display = 'none';
|
||||
qrPlaceholder.style.display = '';
|
||||
stopQRPolling();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,8 +148,15 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Instance created, now fetch QR
|
||||
fetchQR();
|
||||
// Switch UI to connecting state immediately
|
||||
updateConnectionUI('connecting');
|
||||
qrPlaceholder.textContent = 'Iniciando conexion con WhatsApp, generando QR...';
|
||||
qrPlaceholder.style.display = '';
|
||||
qrImg.style.display = 'none';
|
||||
|
||||
// Start polling for QR; the first fetchQR may not have QR ready yet
|
||||
startStatusPolling();
|
||||
startQRPolling();
|
||||
}).catch(function () {
|
||||
connectBtn.disabled = false;
|
||||
connectBtn.textContent = 'Conectar WhatsApp';
|
||||
@@ -151,7 +165,10 @@
|
||||
}
|
||||
|
||||
function fetchQR() {
|
||||
qrPlaceholder.textContent = 'Generando QR...';
|
||||
// Only update placeholder text if we don't already have a QR image showing
|
||||
if (qrImg.style.display !== 'block') {
|
||||
qrPlaceholder.textContent = 'Generando codigo QR, espera unos segundos...';
|
||||
}
|
||||
|
||||
api('GET', '/qr').then(function (data) {
|
||||
var base64 = data.qr || data.base64 || data.qrcode || '';
|
||||
@@ -164,14 +181,18 @@
|
||||
|
||||
// Start polling for connection state while QR is shown
|
||||
startStatusPolling();
|
||||
startQRPolling();
|
||||
} else if ((data.instance && data.instance.state === 'open') || data.state === 'open') {
|
||||
// Already connected
|
||||
updateConnectionUI('open');
|
||||
loadConversations();
|
||||
} else {
|
||||
qrPlaceholder.textContent = 'No se pudo generar el QR. Intenta de nuevo.';
|
||||
qrPlaceholder.style.display = '';
|
||||
qrImg.style.display = 'none';
|
||||
// QR not ready yet — this is normal right after pressing Connect
|
||||
if (qrImg.style.display !== 'block') {
|
||||
qrPlaceholder.textContent = 'Generando codigo QR, por favor espera... (el codigo cambia cada pocos segundos, escanealo en cuanto aparezca)';
|
||||
qrPlaceholder.style.display = '';
|
||||
qrImg.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}).catch(function () {
|
||||
qrPlaceholder.textContent = 'Error al obtener QR';
|
||||
@@ -208,6 +229,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
function startQRPolling() {
|
||||
stopQRPolling();
|
||||
qrPollTimer = setInterval(function () {
|
||||
if (connectionState === 'connecting' || connectionState === 'qr') {
|
||||
fetchQR();
|
||||
} else {
|
||||
stopQRPolling();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function stopQRPolling() {
|
||||
if (qrPollTimer) {
|
||||
clearInterval(qrPollTimer);
|
||||
qrPollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
connectBtn.addEventListener('click', doConnect);
|
||||
disconnectBtn.addEventListener('click', doDisconnect);
|
||||
refreshQrBtn.addEventListener('click', fetchQR);
|
||||
@@ -486,4 +525,12 @@
|
||||
};
|
||||
} catch(e) {}
|
||||
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
registerCmdKItem({ group: "Principal", label: "POS Ventas", href: "/pos/sale", icon: "🛒" });
|
||||
registerCmdKItem({ group: "Principal", label: "Catálogo", href: "/pos/catalog", icon: "📁" });
|
||||
registerCmdKItem({ group: "Principal", label: "Clientes", href: "/pos/customers", icon: "👤" });
|
||||
registerCmdKItem({ group: "Principal", label: "Dashboard", href: "/pos/dashboard", icon: "📊" });
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
{
|
||||
"name": "Nexus POS",
|
||||
"name": "Nexus Autoparts POS",
|
||||
"short_name": "NexusPOS",
|
||||
"description": "Sistema de Punto de Venta para Refaccionarias",
|
||||
"start_url": "/pos/login",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#0d0d0d",
|
||||
"theme_color": "#F5A623",
|
||||
"categories": ["business", "productivity"],
|
||||
"icons": [
|
||||
{"src": "/pos/static/pwa/icon-192.png", "sizes": "192x192", "type": "image/png"},
|
||||
{"src": "/pos/static/pwa/icon-512.png", "sizes": "512x512", "type": "image/png"}
|
||||
]
|
||||
{"src": "/pos/static/pwa/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable"},
|
||||
{"src": "/pos/static/pwa/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable"}
|
||||
],
|
||||
"shortcuts": [
|
||||
{"name": "Nueva Venta", "short_name": "Venta", "description": "Abrir punto de venta", "url": "/pos/sale", "icons": [{"src": "/pos/static/pwa/icon-192.png", "sizes": "192x192"}]},
|
||||
{"name": "Inventario", "short_name": "Stock", "description": "Ver inventario", "url": "/pos/inventory", "icons": [{"src": "/pos/static/pwa/icon-192.png", "sizes": "192x192"}]},
|
||||
{"name": "Dashboard", "short_name": "Dashboard", "description": "Ver resumen", "url": "/pos/dashboard", "icons": [{"src": "/pos/static/pwa/icon-192.png", "sizes": "192x192"}]}
|
||||
],
|
||||
"screenshots": [],
|
||||
"related_applications": [],
|
||||
"prefer_related_applications": false
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
// /home/Autopartes/pos/static/pwa/sw.js
|
||||
// Nexus POS — Service Worker v6
|
||||
// Nexus POS — Service Worker v17
|
||||
// Self-contained vanilla JS. No external imports.
|
||||
//
|
||||
// Bump CACHE_NAME whenever static assets change significantly.
|
||||
// The fetch handler normalizes static asset URLs (strips ?v= query strings)
|
||||
// so templates can use cache-busting query params freely.
|
||||
|
||||
const CACHE_NAME = 'nexus-pos-v6';
|
||||
const CACHE_NAME = 'nexus-pos-v17';
|
||||
|
||||
const APP_SHELL = [
|
||||
'/pos/static/css/tokens.css',
|
||||
'/pos/static/css/common.css',
|
||||
'/pos/static/css/pos-ui.css',
|
||||
'/pos/static/js/app-init.js',
|
||||
'/pos/static/js/sidebar.js',
|
||||
'/pos/static/js/login.js',
|
||||
'/pos/static/js/pos.js',
|
||||
'/pos/static/js/catalog.js',
|
||||
'/pos/static/js/inventory.js',
|
||||
'/pos/static/js/customers.js',
|
||||
'/pos/static/js/invoicing.js',
|
||||
'/pos/static/js/accounting.js',
|
||||
'/pos/static/js/dashboard.js',
|
||||
'/pos/static/js/config.js',
|
||||
'/pos/static/js/reports.js',
|
||||
'/pos/static/js/offline-banner.js',
|
||||
'/pos/static/js/sync-engine.js',
|
||||
'/pos/static/js/brand-catalog.js',
|
||||
'/pos/static/js/i18n.js',
|
||||
'/pos/static/js/kiosk.js',
|
||||
'/pos/static/js/splash-loader.js',
|
||||
'/pos/static/js/pos-utils.js',
|
||||
'/pos/static/js/pwa-install.js',
|
||||
'/pos/static/js/chat.js',
|
||||
'/pos/static/pwa/manifest.json',
|
||||
'/pos/static/pwa/icon-192.png',
|
||||
'/pos/static/pwa/icon-512.png'
|
||||
@@ -103,6 +103,12 @@ self.addEventListener('activate', function (event) {
|
||||
);
|
||||
}).then(function () {
|
||||
return self.clients.claim();
|
||||
}).then(function () {
|
||||
return self.clients.matchAll({ type: 'window' }).then(function (clients) {
|
||||
clients.forEach(function (client) {
|
||||
client.postMessage({ type: 'SW_UPDATED', cacheName: CACHE_NAME });
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -117,6 +123,13 @@ self.addEventListener('fetch', function (event) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize cache key for static assets (strip query strings) so
|
||||
// catalog.js?v=5 and catalog.js?v=6 share the same cache entry.
|
||||
var cacheKey = req;
|
||||
if (/\.(js|css|png|jpg|jpeg|webp|svg|gif|ico|woff|woff2|ttf|eot|json)$/.test(url.pathname)) {
|
||||
cacheKey = new Request(url.pathname);
|
||||
}
|
||||
|
||||
// Never cache auth endpoints
|
||||
if (url.pathname.indexOf('/pos/api/auth/') !== -1) {
|
||||
return;
|
||||
@@ -172,26 +185,27 @@ self.addEventListener('fetch', function (event) {
|
||||
}
|
||||
|
||||
// Everything else (JS, CSS, images) -> cache-first
|
||||
event.respondWith(cacheFirst(req));
|
||||
event.respondWith(cacheFirst(req, cacheKey));
|
||||
});
|
||||
|
||||
function cacheFirst(request) {
|
||||
return caches.match(request).then(function (cached) {
|
||||
function cacheFirst(request, cacheKey) {
|
||||
cacheKey = cacheKey || request;
|
||||
return caches.match(cacheKey).then(function (cached) {
|
||||
if (cached) {
|
||||
fetch(request).then(function (response) {
|
||||
if (response && response.status === 200) {
|
||||
if (response && response.status === 200 && request.method === 'GET') {
|
||||
caches.open(CACHE_NAME).then(function (cache) {
|
||||
cache.put(request, response);
|
||||
cache.put(cacheKey, response);
|
||||
});
|
||||
}
|
||||
}).catch(function () {});
|
||||
return cached;
|
||||
}
|
||||
return fetch(request).then(function (response) {
|
||||
if (response && response.status === 200) {
|
||||
if (response && response.status === 200 && request.method === 'GET') {
|
||||
var clone = response.clone();
|
||||
caches.open(CACHE_NAME).then(function (cache) {
|
||||
cache.put(request, clone);
|
||||
cache.put(cacheKey, clone);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
@@ -201,7 +215,7 @@ function cacheFirst(request) {
|
||||
|
||||
function networkFirst(request) {
|
||||
return fetch(request).then(function (response) {
|
||||
if (response && response.status === 200) {
|
||||
if (response && response.status === 200 && request.method === 'GET') {
|
||||
var clone = response.clone();
|
||||
caches.open(CACHE_NAME).then(function (cache) {
|
||||
cache.put(request, clone);
|
||||
|
||||
Reference in New Issue
Block a user