- i18n.js with 130+ translation keys for es/en, loaded in all 11 templates - sidebar.js uses t() for all nav labels, adds MX/US language toggle - app-init.js role labels use i18n - currency.py service with convert() and format_currency() - config.py adds DEFAULT_CURRENCY and EXCHANGE_RATE_USD_MXN settings - config_bp.py adds GET/PUT /pos/api/config/currency endpoints - config.html adds currency/exchange-rate section (Section 8) - config.js adds loadCurrency/saveCurrency with localStorage sync - pos.js fmt() reads pos_currency from localStorage for USD/MXN display Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,3 +36,7 @@ SMTP_FROM = os.environ.get('SMTP_FROM', 'noreply@nexusautoparts.com')
|
||||
WHATSAPP_TOKEN = os.environ.get("WHATSAPP_TOKEN", "")
|
||||
WHATSAPP_PHONE_ID = os.environ.get("WHATSAPP_PHONE_ID", "")
|
||||
WHATSAPP_VERIFY_TOKEN = os.environ.get("WHATSAPP_VERIFY_TOKEN", "nexus-wa-verify-2026")
|
||||
|
||||
# Multi-currency
|
||||
DEFAULT_CURRENCY = os.environ.get('DEFAULT_CURRENCY', 'MXN')
|
||||
EXCHANGE_RATE_USD_MXN = float(os.environ.get('EXCHANGE_RATE_USD_MXN', '17.5'))
|
||||
|
||||
55
pos/services/currency.py
Normal file
55
pos/services/currency.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Multi-currency support for border refaccionarias.
|
||||
|
||||
Supports MXN and USD with configurable exchange rate.
|
||||
"""
|
||||
|
||||
from config import DEFAULT_CURRENCY, EXCHANGE_RATE_USD_MXN
|
||||
|
||||
CURRENCIES = {
|
||||
'MXN': {'symbol': '$', 'name': 'Peso Mexicano', 'name_en': 'Mexican Peso', 'decimals': 2},
|
||||
'USD': {'symbol': 'US$', 'name': 'Dolar Estadounidense', 'name_en': 'US Dollar', 'decimals': 2},
|
||||
}
|
||||
|
||||
|
||||
def convert(amount, from_currency, to_currency, rate=None):
|
||||
"""Convert an amount between currencies.
|
||||
|
||||
Args:
|
||||
amount: The numeric amount to convert.
|
||||
from_currency: Source currency code ('MXN' or 'USD').
|
||||
to_currency: Target currency code ('MXN' or 'USD').
|
||||
rate: Optional custom exchange rate (USD->MXN). Defaults to config value.
|
||||
|
||||
Returns:
|
||||
The converted amount, rounded to 2 decimals.
|
||||
"""
|
||||
if from_currency == to_currency:
|
||||
return amount
|
||||
if rate is None:
|
||||
rate = EXCHANGE_RATE_USD_MXN
|
||||
if from_currency == 'USD' and to_currency == 'MXN':
|
||||
return round(amount * rate, 2)
|
||||
if from_currency == 'MXN' and to_currency == 'USD':
|
||||
return round(amount / rate, 2)
|
||||
return amount
|
||||
|
||||
|
||||
def format_currency(amount, currency='MXN'):
|
||||
"""Format an amount with the appropriate currency symbol.
|
||||
|
||||
Args:
|
||||
amount: Numeric value.
|
||||
currency: Currency code.
|
||||
|
||||
Returns:
|
||||
Formatted string like '$1,234.56' or 'US$1,234.56'.
|
||||
"""
|
||||
info = CURRENCIES.get(currency, CURRENCIES['MXN'])
|
||||
return f"{info['symbol']}{amount:,.{info['decimals']}f}"
|
||||
|
||||
|
||||
def get_currency_info(code=None):
|
||||
"""Return currency metadata dict. If code is None, return all."""
|
||||
if code:
|
||||
return CURRENCIES.get(code)
|
||||
return CURRENCIES
|
||||
@@ -40,9 +40,10 @@
|
||||
|
||||
var name = employee.name || payload.name || 'Usuario';
|
||||
var role = employee.role || payload.role || '';
|
||||
var _t = typeof window.t === 'function' ? window.t : function(k) { return k; };
|
||||
var roleLabels = {
|
||||
'owner': 'Dueño', 'admin': 'Administrador', 'cashier': 'Cajero',
|
||||
'warehouse': 'Almacén', 'accountant': 'Contador'
|
||||
'owner': _t('role_owner'), 'admin': _t('role_admin'), 'cashier': _t('role_cashier'),
|
||||
'warehouse': _t('role_warehouse'), 'accountant': _t('role_accountant')
|
||||
};
|
||||
var roleLabel = roleLabels[role] || role;
|
||||
var initials = name.split(' ').map(function(p) { return p[0]; }).join('').toUpperCase().substring(0, 2);
|
||||
|
||||
@@ -416,6 +416,68 @@ const Config = (() => {
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Currency
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadCurrency() {
|
||||
try {
|
||||
var res = await fetch(API + '/currency', { headers: headers() });
|
||||
if (!res.ok) return;
|
||||
var d = await res.json();
|
||||
var selCurrency = document.getElementById('cfg-currency');
|
||||
var inpRate = document.getElementById('cfg-exchange-rate');
|
||||
if (selCurrency) selCurrency.value = d.currency || 'MXN';
|
||||
if (inpRate) inpRate.value = d.exchange_rate || 17.5;
|
||||
// Store in localStorage for POS fmt() to pick up
|
||||
localStorage.setItem('pos_currency', d.currency || 'MXN');
|
||||
localStorage.setItem('pos_exchange_rate', d.exchange_rate || 17.5);
|
||||
} catch (e) {
|
||||
console.error('Config.loadCurrency:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCurrency() {
|
||||
var selCurrency = document.getElementById('cfg-currency');
|
||||
var inpRate = document.getElementById('cfg-exchange-rate');
|
||||
var statusEl = document.getElementById('currency-status');
|
||||
var btn = document.getElementById('btn-save-currency');
|
||||
|
||||
if (!selCurrency || !inpRate) return;
|
||||
|
||||
var currency = selCurrency.value;
|
||||
var rate = parseFloat(inpRate.value);
|
||||
|
||||
if (!rate || rate <= 0) {
|
||||
toast('Tipo de cambio invalido', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; }
|
||||
|
||||
try {
|
||||
var res = await fetch(API + '/currency', {
|
||||
method: 'PUT',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({ currency: currency, exchange_rate: rate })
|
||||
});
|
||||
if (!res.ok) {
|
||||
var err = await res.json().catch(function() { return { error: res.statusText }; });
|
||||
throw new Error(err.error || 'Save failed');
|
||||
}
|
||||
localStorage.setItem('pos_currency', currency);
|
||||
localStorage.setItem('pos_exchange_rate', rate);
|
||||
toast('Moneda actualizada');
|
||||
if (statusEl) statusEl.textContent = currency + ' — TC: ' + rate;
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
} finally {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Guardar Moneda';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Init
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -441,6 +503,7 @@ const Config = (() => {
|
||||
loadBranches();
|
||||
loadEmployees();
|
||||
loadBusiness();
|
||||
loadCurrency();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
@@ -448,6 +511,7 @@ const Config = (() => {
|
||||
return {
|
||||
init, setTheme, selectThemeOption,
|
||||
loadBranches, loadEmployees, saveBranch, saveEmployee,
|
||||
loadCurrency, saveCurrency,
|
||||
openModal, closeModal
|
||||
};
|
||||
})();
|
||||
|
||||
342
pos/static/js/i18n.js
Normal file
342
pos/static/js/i18n.js
Normal file
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* i18n.js — Simple internationalization for Nexus Autoparts POS
|
||||
* Supports: es (Spanish/Mexico) and en (English/USA)
|
||||
*/
|
||||
var I18N = {
|
||||
es: {
|
||||
// Sidebar nav
|
||||
'dashboard': 'Dashboard',
|
||||
'pos': 'Punto de Venta',
|
||||
'catalog': 'Catalogo',
|
||||
'inventory': 'Inventario',
|
||||
'customers': 'Clientes',
|
||||
'invoicing': 'Facturacion',
|
||||
'accounting': 'Contabilidad',
|
||||
'reports': 'Reportes',
|
||||
'fleet': 'Flotillas',
|
||||
'whatsapp': 'WhatsApp',
|
||||
'config': 'Configuracion',
|
||||
// Sidebar sections
|
||||
'nav_main': 'Principal',
|
||||
'nav_management': 'Gestion',
|
||||
'nav_system': 'Sistema',
|
||||
// Common actions
|
||||
'search': 'Buscar',
|
||||
'save': 'Guardar',
|
||||
'cancel': 'Cancelar',
|
||||
'delete': 'Eliminar',
|
||||
'edit': 'Editar',
|
||||
'new': 'Nuevo',
|
||||
'close': 'Cerrar',
|
||||
'confirm': 'Confirmar',
|
||||
'back': 'Regresar',
|
||||
'next': 'Siguiente',
|
||||
'print': 'Imprimir',
|
||||
'export': 'Exportar',
|
||||
'import': 'Importar',
|
||||
'refresh': 'Actualizar',
|
||||
'loading': 'Cargando...',
|
||||
'no_results': 'Sin resultados',
|
||||
'error': 'Error',
|
||||
'success': 'Exito',
|
||||
'warning': 'Advertencia',
|
||||
// Financial
|
||||
'total': 'Total',
|
||||
'subtotal': 'Subtotal',
|
||||
'tax': 'IVA',
|
||||
'price': 'Precio',
|
||||
'unit_price': 'Precio Unitario',
|
||||
'cost': 'Costo',
|
||||
'discount': 'Descuento',
|
||||
'margin': 'Margen',
|
||||
'profit': 'Utilidad',
|
||||
'balance': 'Saldo',
|
||||
'amount': 'Monto',
|
||||
// Inventory
|
||||
'quantity': 'Cantidad',
|
||||
'stock': 'Existencias',
|
||||
'min_stock': 'Minimo',
|
||||
'max_stock': 'Maximo',
|
||||
'sku': 'SKU',
|
||||
'barcode': 'Codigo de Barras',
|
||||
'brand': 'Marca',
|
||||
'category': 'Categoria',
|
||||
'description': 'Descripcion',
|
||||
'location': 'Ubicacion',
|
||||
// Table headers
|
||||
'name': 'Nombre',
|
||||
'date': 'Fecha',
|
||||
'status': 'Estado',
|
||||
'actions': 'Acciones',
|
||||
'id': 'ID',
|
||||
'type': 'Tipo',
|
||||
'notes': 'Notas',
|
||||
// POS
|
||||
'charge': 'Cobrar',
|
||||
'quote': 'Cotizacion',
|
||||
'layaway': 'Apartado',
|
||||
'credit': 'Credito',
|
||||
'cash': 'Efectivo',
|
||||
'transfer': 'Transferencia',
|
||||
'card': 'Tarjeta',
|
||||
'mixed': 'Mixto',
|
||||
'change': 'Cambio',
|
||||
'customer': 'Cliente',
|
||||
'general_public': 'Publico General',
|
||||
'sale': 'Venta',
|
||||
'sales': 'Ventas',
|
||||
'ticket': 'Ticket',
|
||||
'receipt': 'Recibo',
|
||||
'payment': 'Pago',
|
||||
'payment_method': 'Metodo de Pago',
|
||||
'add_to_cart': 'Agregar',
|
||||
'clear_cart': 'Limpiar Carrito',
|
||||
'hold_sale': 'Pausar Venta',
|
||||
'recall_sale': 'Retomar Venta',
|
||||
'cancel_sale': 'Cancelar Venta',
|
||||
'confirm_payment': 'Confirmar Pago',
|
||||
'cash_received': 'Efectivo Recibido',
|
||||
'amount_due': 'Total a Pagar',
|
||||
'remaining': 'Faltante',
|
||||
// Customers
|
||||
'phone': 'Telefono',
|
||||
'email': 'Correo',
|
||||
'address': 'Direccion',
|
||||
'rfc': 'RFC',
|
||||
'credit_limit': 'Limite de Credito',
|
||||
'credit_balance': 'Saldo de Credito',
|
||||
'price_tier': 'Nivel de Precio',
|
||||
// Invoicing
|
||||
'invoice': 'Factura',
|
||||
'cfdi': 'CFDI',
|
||||
'stamp': 'Timbrar',
|
||||
'cancel_invoice': 'Cancelar Factura',
|
||||
// Config
|
||||
'appearance': 'Apariencia',
|
||||
'business_data': 'Datos de la Empresa',
|
||||
'employees': 'Empleados',
|
||||
'printers': 'Impresoras',
|
||||
'branches': 'Sucursales',
|
||||
'fiscal_params': 'Parametros Fiscales',
|
||||
'system_prefs': 'Preferencias del Sistema',
|
||||
'currency_config': 'Moneda',
|
||||
'language': 'Idioma',
|
||||
'theme': 'Tema',
|
||||
'dark_theme': 'Tema oscuro',
|
||||
'light_theme': 'Tema claro',
|
||||
'logout': 'Cerrar sesion',
|
||||
// Currency
|
||||
'currency': 'Moneda',
|
||||
'exchange_rate': 'Tipo de Cambio',
|
||||
'default_currency': 'Moneda Predeterminada',
|
||||
'mxn': 'Peso Mexicano',
|
||||
'usd': 'Dolar Estadounidense',
|
||||
// Roles
|
||||
'role_owner': 'Dueno',
|
||||
'role_admin': 'Administrador',
|
||||
'role_cashier': 'Cajero',
|
||||
'role_warehouse': 'Almacen',
|
||||
'role_accountant': 'Contador',
|
||||
// Reports
|
||||
'daily_sales': 'Ventas del Dia',
|
||||
'weekly_sales': 'Ventas de la Semana',
|
||||
'monthly_sales': 'Ventas del Mes',
|
||||
'top_products': 'Productos Mas Vendidos',
|
||||
'low_stock': 'Bajo Stock',
|
||||
// Fleet
|
||||
'vehicle': 'Vehiculo',
|
||||
'plate': 'Placa',
|
||||
'vin': 'VIN',
|
||||
'mileage': 'Kilometraje',
|
||||
// Misc
|
||||
'yes': 'Si',
|
||||
'no': 'No',
|
||||
'all': 'Todos',
|
||||
'active': 'Activo',
|
||||
'inactive': 'Inactivo',
|
||||
'pending': 'Pendiente',
|
||||
'completed': 'Completado',
|
||||
'cancelled': 'Cancelado',
|
||||
},
|
||||
en: {
|
||||
// Sidebar nav
|
||||
'dashboard': 'Dashboard',
|
||||
'pos': 'Point of Sale',
|
||||
'catalog': 'Catalog',
|
||||
'inventory': 'Inventory',
|
||||
'customers': 'Customers',
|
||||
'invoicing': 'Invoicing',
|
||||
'accounting': 'Accounting',
|
||||
'reports': 'Reports',
|
||||
'fleet': 'Fleet',
|
||||
'whatsapp': 'WhatsApp',
|
||||
'config': 'Settings',
|
||||
// Sidebar sections
|
||||
'nav_main': 'Main',
|
||||
'nav_management': 'Management',
|
||||
'nav_system': 'System',
|
||||
// Common actions
|
||||
'search': 'Search',
|
||||
'save': 'Save',
|
||||
'cancel': 'Cancel',
|
||||
'delete': 'Delete',
|
||||
'edit': 'Edit',
|
||||
'new': 'New',
|
||||
'close': 'Close',
|
||||
'confirm': 'Confirm',
|
||||
'back': 'Back',
|
||||
'next': 'Next',
|
||||
'print': 'Print',
|
||||
'export': 'Export',
|
||||
'import': 'Import',
|
||||
'refresh': 'Refresh',
|
||||
'loading': 'Loading...',
|
||||
'no_results': 'No results',
|
||||
'error': 'Error',
|
||||
'success': 'Success',
|
||||
'warning': 'Warning',
|
||||
// Financial
|
||||
'total': 'Total',
|
||||
'subtotal': 'Subtotal',
|
||||
'tax': 'Tax',
|
||||
'price': 'Price',
|
||||
'unit_price': 'Unit Price',
|
||||
'cost': 'Cost',
|
||||
'discount': 'Discount',
|
||||
'margin': 'Margin',
|
||||
'profit': 'Profit',
|
||||
'balance': 'Balance',
|
||||
'amount': 'Amount',
|
||||
// Inventory
|
||||
'quantity': 'Quantity',
|
||||
'stock': 'Stock',
|
||||
'min_stock': 'Minimum',
|
||||
'max_stock': 'Maximum',
|
||||
'sku': 'SKU',
|
||||
'barcode': 'Barcode',
|
||||
'brand': 'Brand',
|
||||
'category': 'Category',
|
||||
'description': 'Description',
|
||||
'location': 'Location',
|
||||
// Table headers
|
||||
'name': 'Name',
|
||||
'date': 'Date',
|
||||
'status': 'Status',
|
||||
'actions': 'Actions',
|
||||
'id': 'ID',
|
||||
'type': 'Type',
|
||||
'notes': 'Notes',
|
||||
// POS
|
||||
'charge': 'Charge',
|
||||
'quote': 'Quote',
|
||||
'layaway': 'Layaway',
|
||||
'credit': 'Credit',
|
||||
'cash': 'Cash',
|
||||
'transfer': 'Transfer',
|
||||
'card': 'Card',
|
||||
'mixed': 'Mixed',
|
||||
'change': 'Change',
|
||||
'customer': 'Customer',
|
||||
'general_public': 'Walk-in Customer',
|
||||
'sale': 'Sale',
|
||||
'sales': 'Sales',
|
||||
'ticket': 'Ticket',
|
||||
'receipt': 'Receipt',
|
||||
'payment': 'Payment',
|
||||
'payment_method': 'Payment Method',
|
||||
'add_to_cart': 'Add',
|
||||
'clear_cart': 'Clear Cart',
|
||||
'hold_sale': 'Hold Sale',
|
||||
'recall_sale': 'Recall Sale',
|
||||
'cancel_sale': 'Cancel Sale',
|
||||
'confirm_payment': 'Confirm Payment',
|
||||
'cash_received': 'Cash Received',
|
||||
'amount_due': 'Amount Due',
|
||||
'remaining': 'Remaining',
|
||||
// Customers
|
||||
'phone': 'Phone',
|
||||
'email': 'Email',
|
||||
'address': 'Address',
|
||||
'rfc': 'Tax ID (RFC)',
|
||||
'credit_limit': 'Credit Limit',
|
||||
'credit_balance': 'Credit Balance',
|
||||
'price_tier': 'Price Tier',
|
||||
// Invoicing
|
||||
'invoice': 'Invoice',
|
||||
'cfdi': 'CFDI',
|
||||
'stamp': 'Stamp',
|
||||
'cancel_invoice': 'Cancel Invoice',
|
||||
// Config
|
||||
'appearance': 'Appearance',
|
||||
'business_data': 'Business Info',
|
||||
'employees': 'Employees',
|
||||
'printers': 'Printers',
|
||||
'branches': 'Branches',
|
||||
'fiscal_params': 'Tax Settings',
|
||||
'system_prefs': 'System Preferences',
|
||||
'currency_config': 'Currency',
|
||||
'language': 'Language',
|
||||
'theme': 'Theme',
|
||||
'dark_theme': 'Dark theme',
|
||||
'light_theme': 'Light theme',
|
||||
'logout': 'Log out',
|
||||
// Currency
|
||||
'currency': 'Currency',
|
||||
'exchange_rate': 'Exchange Rate',
|
||||
'default_currency': 'Default Currency',
|
||||
'mxn': 'Mexican Peso',
|
||||
'usd': 'US Dollar',
|
||||
// Roles
|
||||
'role_owner': 'Owner',
|
||||
'role_admin': 'Administrator',
|
||||
'role_cashier': 'Cashier',
|
||||
'role_warehouse': 'Warehouse',
|
||||
'role_accountant': 'Accountant',
|
||||
// Reports
|
||||
'daily_sales': 'Daily Sales',
|
||||
'weekly_sales': 'Weekly Sales',
|
||||
'monthly_sales': 'Monthly Sales',
|
||||
'top_products': 'Top Products',
|
||||
'low_stock': 'Low Stock',
|
||||
// Fleet
|
||||
'vehicle': 'Vehicle',
|
||||
'plate': 'Plate',
|
||||
'vin': 'VIN',
|
||||
'mileage': 'Mileage',
|
||||
// Misc
|
||||
'yes': 'Yes',
|
||||
'no': 'No',
|
||||
'all': 'All',
|
||||
'active': 'Active',
|
||||
'inactive': 'Inactive',
|
||||
'pending': 'Pending',
|
||||
'completed': 'Completed',
|
||||
'cancelled': 'Cancelled',
|
||||
}
|
||||
};
|
||||
|
||||
var currentLang = localStorage.getItem('pos_lang') || 'es';
|
||||
|
||||
/**
|
||||
* Translate a key to the current language.
|
||||
* Falls back to Spanish, then to the raw key.
|
||||
*/
|
||||
window.t = function(key) {
|
||||
return (I18N[currentLang] && I18N[currentLang][key]) || (I18N['es'] && I18N['es'][key]) || key;
|
||||
};
|
||||
|
||||
/**
|
||||
* Switch the UI language and reload.
|
||||
*/
|
||||
window.setLang = function(lang) {
|
||||
currentLang = lang;
|
||||
localStorage.setItem('pos_lang', lang);
|
||||
location.reload();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current language code.
|
||||
*/
|
||||
window.getLang = function() {
|
||||
return currentLang;
|
||||
};
|
||||
@@ -1,34 +1,39 @@
|
||||
/**
|
||||
* sidebar.js — Shared sidebar matching the design system style
|
||||
* Replaces existing sidebar in each page with a consistent, themed version.
|
||||
* Uses i18n t() for all labels when available.
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// i18n helper — falls back to raw string if i18n.js not loaded
|
||||
var _t = typeof window.t === 'function' ? window.t : function(k) { return k; };
|
||||
|
||||
var u = window.POS_USER || {};
|
||||
var name = u.name || 'Usuario';
|
||||
var roleLabel = u.roleLabel || '';
|
||||
var initials = u.initials || '?';
|
||||
var currentPath = window.location.pathname;
|
||||
var currentTheme = localStorage.getItem('pos_theme') || 'industrial';
|
||||
var currentLang = localStorage.getItem('pos_lang') || 'es';
|
||||
|
||||
var navSections = [
|
||||
{ label: 'Principal', items: [
|
||||
{ name: '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: 'POS', href: '/pos/sale', icon: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>' },
|
||||
{ name: 'Catálogo', href: '/pos/catalog', icon: '<path d="M4 6h16M4 10h16M4 14h16M4 18h16"/>' },
|
||||
{ name: 'Inventario', 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"/>' },
|
||||
{ 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"/>' },
|
||||
{ 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"/>' },
|
||||
]},
|
||||
{ label: 'Gestión', items: [
|
||||
{ name: 'Clientes', 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: 'Facturación', 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: 'Contabilidad', 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: 'Reportes', 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: 'Flotillas', 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: '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"/>' },
|
||||
{ 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: _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"/>' },
|
||||
]},
|
||||
{ label: 'Sistema', items: [
|
||||
{ name: 'Configuración', 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"/>' },
|
||||
{ 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"/>' },
|
||||
]},
|
||||
];
|
||||
|
||||
@@ -51,14 +56,24 @@
|
||||
|
||||
// Theme toggle buttons
|
||||
var themeHtml = '<div class="sidebar__theme-toggle">'
|
||||
+ '<button class="theme-toggle-btn' + (currentTheme === 'industrial' ? ' is-active' : '') + '" onclick="posSetTheme(\'industrial\');updateThemeButtons()" title="Tema oscuro">'
|
||||
+ '<button class="theme-toggle-btn' + (currentTheme === 'industrial' ? ' is-active' : '') + '" onclick="posSetTheme(\'industrial\');updateThemeButtons()" title="' + _t('dark_theme') + '">'
|
||||
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>'
|
||||
+ '</button>'
|
||||
+ '<button class="theme-toggle-btn' + (currentTheme === 'modern' ? ' is-active' : '') + '" onclick="posSetTheme(\'modern\');updateThemeButtons()" title="Tema claro">'
|
||||
+ '<button class="theme-toggle-btn' + (currentTheme === 'modern' ? ' is-active' : '') + '" onclick="posSetTheme(\'modern\');updateThemeButtons()" title="' + _t('light_theme') + '">'
|
||||
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>'
|
||||
+ '</button>'
|
||||
+ '</div>';
|
||||
|
||||
// Language toggle buttons
|
||||
var langHtml = '<div class="sidebar__lang-toggle">'
|
||||
+ '<button class="lang-toggle-btn' + (currentLang === 'es' ? ' is-active' : '') + '" onclick="setLang(\'es\')" title="Espanol">'
|
||||
+ '<span class="lang-flag">MX</span> ES'
|
||||
+ '</button>'
|
||||
+ '<button class="lang-toggle-btn' + (currentLang === 'en' ? ' is-active' : '') + '" onclick="setLang(\'en\')" title="English">'
|
||||
+ '<span class="lang-flag">US</span> EN'
|
||||
+ '</button>'
|
||||
+ '</div>';
|
||||
|
||||
window.updateThemeButtons = function() {
|
||||
var t = localStorage.getItem('pos_theme') || 'industrial';
|
||||
document.querySelectorAll('.theme-toggle-btn').forEach(function(b, i) {
|
||||
@@ -76,13 +91,14 @@
|
||||
+ '</div>'
|
||||
+ '<nav class="sidebar__nav">' + navHtml + '</nav>'
|
||||
+ themeHtml
|
||||
+ langHtml
|
||||
+ '<div class="sidebar__footer">'
|
||||
+ ' <div class="sidebar__user-avatar">' + initials + '</div>'
|
||||
+ ' <div class="sidebar__user-info">'
|
||||
+ ' <div class="sidebar__user-name">' + name + '</div>'
|
||||
+ ' <div class="sidebar__user-role">' + roleLabel + '</div>'
|
||||
+ ' </div>'
|
||||
+ ' <button class="sidebar__logout-btn" onclick="posLogout()" title="Cerrar sesión">'
|
||||
+ ' <button class="sidebar__logout-btn" onclick="posLogout()" title="' + _t('logout') + '">'
|
||||
+ ' <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>'
|
||||
+ ' </button>'
|
||||
+ '</div>';
|
||||
@@ -110,10 +126,11 @@
|
||||
'.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{display:flex;gap:4px;padding:8px 16px;border-top:1px solid var(--color-border)}',
|
||||
'.theme-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{color:var(--color-text-primary);background:var(--color-surface-2,rgba(255,255,255,0.04))}',
|
||||
'.theme-toggle-btn.is-active{background:var(--color-primary-muted,rgba(245,166,35,0.12));color:var(--color-primary);border-color:var(--color-primary)}',
|
||||
'.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}',
|
||||
@@ -124,7 +141,7 @@
|
||||
'.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{display:none}.sidebar__brand{justify-content:center;padding:12px 8px}.sidebar__footer{flex-direction:column;padding:8px}.pos-main-offset{margin-left:56px}}',
|
||||
'@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);
|
||||
|
||||
|
||||
@@ -2301,6 +2301,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/pos/static/js/i18n.js"></script>
|
||||
<script src="/pos/static/js/app-init.js"></script>
|
||||
<script src="/pos/static/js/sidebar.js"></script>
|
||||
<script src="/pos/static/js/accounting.js"></script>
|
||||
|
||||
@@ -737,6 +737,7 @@
|
||||
<button class="banner__dismiss" onclick="document.getElementById('offlineBanner').style.display='none'" aria-label="Cerrar">×</button>
|
||||
</div>
|
||||
|
||||
<script src="/pos/static/js/i18n.js"></script>
|
||||
<script src="/pos/static/js/app-init.js"></script>
|
||||
<script src="/pos/static/js/sidebar.js"></script>
|
||||
<script src="/pos/static/js/catalog.js"></script>
|
||||
|
||||
@@ -1702,6 +1702,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===============================================================
|
||||
SECTION 8: MONEDA / CURRENCY
|
||||
=============================================================== -->
|
||||
<div class="settings-section">
|
||||
<div class="settings-section__header">
|
||||
<div class="settings-section__icon">
|
||||
<svg viewBox="0 0 24 24"><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"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="settings-section__title" data-i18n="currency_config">Moneda</div>
|
||||
<div class="settings-section__desc">Configura la moneda y tipo de cambio para ventas</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card" style="padding:var(--space-5);">
|
||||
<div class="form-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-4);">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="default_currency">Moneda Predeterminada</label>
|
||||
<select class="form-input" id="cfg-currency" style="padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-sm,4px);background:var(--color-bg-base);color:var(--color-text-primary);font-size:var(--text-body-sm);">
|
||||
<option value="MXN">$ MXN — Peso Mexicano</option>
|
||||
<option value="USD">US$ USD — US Dollar</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="exchange_rate">Tipo de Cambio USD/MXN</label>
|
||||
<input class="form-input" id="cfg-exchange-rate" type="number" step="0.01" min="0.01" value="17.50"
|
||||
style="padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-sm,4px);background:var(--color-bg-base);color:var(--color-text-primary);font-size:var(--text-body-sm);font-family:var(--font-mono);" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:var(--space-4);display:flex;gap:var(--space-3);align-items:center;">
|
||||
<button class="btn btn--primary" id="btn-save-currency" style="padding:var(--space-2) var(--space-4);font-size:var(--text-body-sm);" onclick="Config.saveCurrency()">
|
||||
Guardar Moneda
|
||||
</button>
|
||||
<span id="currency-status" style="font-size:var(--text-caption);color:var(--color-text-muted);"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /content-scroll -->
|
||||
</main>
|
||||
</div><!-- /app-shell -->
|
||||
@@ -1876,6 +1914,7 @@
|
||||
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
||||
</style>
|
||||
|
||||
<script src="/pos/static/js/i18n.js"></script>
|
||||
<script src="/pos/static/js/app-init.js"></script>
|
||||
<script src="/pos/static/js/sidebar.js"></script>
|
||||
<script src="/pos/static/js/config.js"></script>
|
||||
|
||||
@@ -2147,6 +2147,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/pos/static/js/i18n.js"></script>
|
||||
<script src="/pos/static/js/app-init.js"></script>
|
||||
<script src="/pos/static/js/sidebar.js"></script>
|
||||
<script src="/pos/static/js/customers.js"></script>
|
||||
|
||||
@@ -962,6 +962,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/pos/static/js/i18n.js"></script>
|
||||
<script src="/pos/static/js/app-init.js"></script>
|
||||
<script src="/pos/static/js/sidebar.js"></script>
|
||||
<script src="/pos/static/js/fleet.js"></script>
|
||||
|
||||
@@ -2525,6 +2525,7 @@
|
||||
<button class="banner__dismiss" onclick="document.getElementById('offlineBanner').style.display='none'" aria-label="Cerrar">×</button>
|
||||
</div>
|
||||
|
||||
<script src="/pos/static/js/i18n.js"></script>
|
||||
<script src="/pos/static/js/app-init.js"></script>
|
||||
<script src="/pos/static/js/sidebar.js"></script>
|
||||
<script src="/pos/static/js/inventory.js"></script>
|
||||
|
||||
@@ -2723,6 +2723,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/pos/static/js/i18n.js"></script>
|
||||
<script src="/pos/static/js/app-init.js"></script>
|
||||
<script src="/pos/static/js/sidebar.js"></script>
|
||||
<script src="/pos/static/js/invoicing.js"></script>
|
||||
|
||||
@@ -1846,6 +1846,7 @@
|
||||
</div>
|
||||
<!-- End app-shell -->
|
||||
|
||||
<script src="/pos/static/js/i18n.js"></script>
|
||||
<script src="/pos/static/js/app-init.js"></script>
|
||||
<script src="/pos/static/js/sidebar.js"></script>
|
||||
<script src="/pos/static/js/reports.js"></script>
|
||||
|
||||
@@ -531,6 +531,7 @@ function posLogout(){localStorage.removeItem('pos_token');window.location.href='
|
||||
</script>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<script src="/pos/static/js/i18n.js"></script>
|
||||
<script src="/pos/static/js/whatsapp.js"></script>
|
||||
<script src="/pos/static/js/sidebar.js"></script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user