diff --git a/pos/blueprints/config_bp.py b/pos/blueprints/config_bp.py index 06e3101..ddf4167 100644 --- a/pos/blueprints/config_bp.py +++ b/pos/blueprints/config_bp.py @@ -409,3 +409,45 @@ def upgrade_billing(): if 'error' in result: return jsonify(result), 400 return jsonify(result) + + +# ─── Vehicle Compatibility Source ──────────────────── + +@config_bp.route('/vehicle-compat-source', methods=['GET']) +@require_auth() +def get_vehicle_compat_source(): + """Get the configured vehicle compatibility source. + + Returns: {'source': 'tecdoc' | 'qwen' | 'both'} + """ + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + cur.execute("SELECT value FROM tenant_config WHERE key = 'vehicle_compat_source'") + row = cur.fetchone() + cur.close() + conn.close() + source = row[0] if row else 'both' + if source not in ('tecdoc', 'qwen', 'both'): + source = 'both' + return jsonify({'source': source}) + + +@config_bp.route('/vehicle-compat-source', methods=['PUT']) +@require_auth('config.edit') +def update_vehicle_compat_source(): + """Set the vehicle compatibility source.""" + data = request.get_json() or {} + source = data.get('source', 'both') + if source not in ('tecdoc', 'qwen', 'both'): + return jsonify({'error': 'source must be tecdoc, qwen, or both'}), 400 + + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + cur.execute(""" + INSERT INTO tenant_config (key, value) VALUES ('vehicle_compat_source', %s) + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value + """, (source,)) + conn.commit() + cur.close() + conn.close() + return jsonify({'message': 'Vehicle compatibility source updated', 'source': source}) diff --git a/pos/blueprints/inventory_bp.py b/pos/blueprints/inventory_bp.py index b5a932d..a68483e 100644 --- a/pos/blueprints/inventory_bp.py +++ b/pos/blueprints/inventory_bp.py @@ -18,7 +18,7 @@ from services.audit import log_action from tenant_db import get_master_conn from services.inventory_vehicle_compat import ( auto_match_vehicle_compatibility, add_compatibility, remove_compatibility, - remove_all_compatibility, get_compatibility, search_mye, + remove_all_compatibility, get_compatibility, search_mye, get_compat_source, ) inventory_bp = Blueprint('inventory', __name__, url_prefix='/pos/api/inventory') @@ -283,38 +283,43 @@ def create_item(): conn.commit() cur.close() - # Auto-match vehicle compatibility via TecDoc - try: - master = get_master_conn() - auto_match_vehicle_compatibility(master, conn, item_id, data['part_number'], - brand=data.get('brand'), name=data.get('name')) - master.close() - except Exception as am_err: - print(f"[auto_match] Error for item {item_id}: {am_err}") - - # QWEN AI fitment (complementa TecDoc mientras se termina) + # ── Vehicle compatibility (respects tenant config) ──────────────── + compat_source = get_compat_source(g.tenant_id) qwen_added = 0 - try: - from services.qwen_fitment import get_vehicle_fitment - fitment = get_vehicle_fitment( - data['part_number'], - data['name'], - data.get('brand', '') - ) - for v in fitment.get('vehicles', []): - if v.get('mye_id'): - cur = conn.cursor() - cur.execute(""" - INSERT INTO inventory_vehicle_compat - (inventory_id, model_year_engine_id, source, confidence, created_at) - VALUES (%s, %s, %s, %s, NOW()) - ON CONFLICT (inventory_id, model_year_engine_id) DO NOTHING - """, (item_id, v['mye_id'], 'qwen_ai', fitment.get('confidence', 0))) - cur.close() - qwen_added += 1 - conn.commit() - except Exception as qwen_err: - print(f"[qwen_fitment] Error for item {item_id}: {qwen_err}") + + # TecDoc auto-match + if compat_source in ('tecdoc', 'both'): + try: + master = get_master_conn() + auto_match_vehicle_compatibility(master, conn, item_id, data['part_number'], + brand=data.get('brand'), name=data.get('name')) + master.close() + except Exception as am_err: + print(f"[auto_match] Error for item {item_id}: {am_err}") + + # QWEN AI fitment + if compat_source in ('qwen', 'both'): + try: + from services.qwen_fitment import get_vehicle_fitment + fitment = get_vehicle_fitment( + data['part_number'], + data['name'], + data.get('brand', '') + ) + for v in fitment.get('vehicles', []): + if v.get('mye_id'): + cur = conn.cursor() + cur.execute(""" + INSERT INTO inventory_vehicle_compat + (inventory_id, model_year_engine_id, source, confidence, created_at) + VALUES (%s, %s, %s, %s, NOW()) + ON CONFLICT (inventory_id, model_year_engine_id) DO NOTHING + """, (item_id, v['mye_id'], 'qwen_ai', fitment.get('confidence', 0))) + cur.close() + qwen_added += 1 + conn.commit() + except Exception as qwen_err: + print(f"[qwen_fitment] Error for item {item_id}: {qwen_err}") conn.close() return jsonify({ @@ -1363,14 +1368,51 @@ def auto_match_item_vehicles(item_id): return jsonify({'error': 'Item not found'}), 404 part_number, brand, name = row - master = get_master_conn() - try: - result = auto_match_vehicle_compatibility(master, conn, item_id, part_number, - brand=brand, name=name) - return jsonify(result) - finally: - master.close() - conn.close() + compat_source = get_compat_source(g.tenant_id) + + # TecDoc auto-match + if compat_source in ('tecdoc', 'both'): + master = get_master_conn() + try: + result = auto_match_vehicle_compatibility(master, conn, item_id, part_number, + brand=brand, name=name) + return jsonify(result) + finally: + master.close() + conn.close() + + # QWEN AI auto-match + if compat_source == 'qwen': + try: + from services.qwen_fitment import get_vehicle_fitment + fitment = get_vehicle_fitment(part_number, name, brand) + # Insert results + inserted = 0 + cur2 = conn.cursor() + for v in fitment.get('vehicles', []): + if v.get('mye_id'): + cur2.execute(""" + INSERT INTO inventory_vehicle_compat + (inventory_id, model_year_engine_id, source, confidence, created_at) + VALUES (%s, %s, %s, %s, NOW()) + ON CONFLICT (inventory_id, model_year_engine_id) DO NOTHING + """, (item_id, v['mye_id'], 'qwen_ai', fitment.get('confidence', 0))) + if cur2.rowcount > 0: + inserted += 1 + conn.commit() + cur2.close() + return jsonify({ + 'matched': inserted > 0, + 'matches': [], + 'myes': [v['mye_id'] for v in fitment.get('vehicles', []) if v.get('mye_id')], + 'inserted': inserted, + }) + except Exception as e: + conn.close() + return jsonify({'error': str(e)}), 500 + + conn.close() + return jsonify({'error': 'No compatibility source configured'}), 400 @inventory_bp.route('/mye/search', methods=['GET']) diff --git a/pos/services/inventory_vehicle_compat.py b/pos/services/inventory_vehicle_compat.py index 6f8cefd..ae87da8 100644 --- a/pos/services/inventory_vehicle_compat.py +++ b/pos/services/inventory_vehicle_compat.py @@ -12,6 +12,29 @@ Features: from typing import List, Dict, Optional +def get_compat_source(tenant_id): + """Return the configured compatibility source: 'tecdoc', 'qwen', or 'both'. + + Reads from tenant_config table. Defaults to 'both'. + """ + from tenant_db import get_tenant_conn + try: + conn = get_tenant_conn(tenant_id) + cur = conn.cursor() + cur.execute( + "SELECT value FROM tenant_config WHERE key = 'vehicle_compat_source'" + ) + row = cur.fetchone() + cur.close() + conn.close() + source = row[0] if row else 'both' + if source in ('tecdoc', 'qwen', 'both'): + return source + except Exception: + pass + return 'both' + + def auto_match_vehicle_compatibility(master_conn, tenant_conn, inventory_id, part_number, brand=None, name=None): """Find vehicle compatibility for an inventory item by part_number. diff --git a/pos/static/js/config.js b/pos/static/js/config.js index 5b2d329..134f157 100644 --- a/pos/static/js/config.js +++ b/pos/static/js/config.js @@ -580,6 +580,49 @@ const Config = (() => { } } + // ------------------------------------------------------------------------- + // Vehicle Compatibility Source + // ------------------------------------------------------------------------- + async function loadVehicleCompatSource() { + try { + var res = await fetch(API + '/vehicle-compat-source', { headers: headers() }); + if (!res.ok) return; + var d = await res.json(); + var sel = document.getElementById('cfg-compat-source'); + if (sel) sel.value = d.source || 'both'; + } catch (e) { + console.error('Config.loadVehicleCompatSource:', e); + } + } + + async function saveVehicleCompatSource() { + var sel = document.getElementById('cfg-compat-source'); + var btn = document.getElementById('btn-save-compat-source'); + if (!sel) return; + + if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; } + + try { + var res = await fetch(API + '/vehicle-compat-source', { + method: 'PUT', + headers: headers(), + body: JSON.stringify({ source: sel.value }) + }); + if (!res.ok) { + var err = await res.json().catch(function() { return { error: res.statusText }; }); + throw new Error(err.error || 'Save failed'); + } + toast('Fuente de compatibilidad actualizada'); + } catch (e) { + toast(e.message, 'error'); + } finally { + if (btn) { + btn.disabled = false; + btn.textContent = 'Guardar'; + } + } + } + // ------------------------------------------------------------------------- // Init // ------------------------------------------------------------------------- @@ -601,6 +644,12 @@ const Config = (() => { // Bind UI events bindEvents(); + // Vehicle compat source save button + var btnCompat = document.getElementById('btn-save-compat-source'); + if (btnCompat) { + btnCompat.addEventListener('click', saveVehicleCompatSource); + } + // Kiosk mode toggle var kioskToggle = document.getElementById('cfg-kiosk-mode'); if (kioskToggle && window.NexusKiosk) { @@ -621,6 +670,7 @@ const Config = (() => { loadEmployees(); loadBusiness(); loadCurrency(); + loadVehicleCompatSource(); } document.addEventListener('DOMContentLoaded', init); diff --git a/pos/static/js/config.min.js b/pos/static/js/config.min.js index 514813b..ea4814b 100644 --- a/pos/static/js/config.min.js +++ b/pos/static/js/config.min.js @@ -1 +1 @@ -const Config=(()=>{const e="/pos/api/config";let t=[];function a(){return localStorage.getItem("pos_token")||""}function n(){return!!a()||(window.location.href="/pos/login",!1)}function r(){return{Authorization:`Bearer ${a()}`,"Content-Type":"application/json"}}function o(e,t){var a=document.createElement("div");a.className="cfg-toast cfg-toast--"+(t||"ok"),a.textContent=e,document.body.appendChild(a),setTimeout((function(){a.remove()}),3e3)}function d(e){var t=document.getElementById(e);t&&(t.style.display="flex")}function c(e){var t=document.getElementById(e);t&&(t.style.display="none")}function i(e){document.documentElement.setAttribute("data-theme",e);try{localStorage.setItem("nexus-theme",e)}catch(e){}document.querySelectorAll(".theme-btn").forEach((function(t){t.classList.toggle("is-active",t.dataset.themeTarget===e)})),document.querySelectorAll(".theme-option").forEach((function(e){e.classList.remove("is-selected")}));var t="industrial"===e?0:1,a=document.querySelectorAll(".theme-option");a[t]&&a[t].classList.add("is-selected")}function l(e){i(e)}function s(){var e=new Date,t=String(e.getHours()).padStart(2,"0"),a=String(e.getMinutes()).padStart(2,"0"),n=document.getElementById("live-clock");n&&(n.textContent=t+":"+a)}window.setTheme=i,window.selectThemeOption=l;var u={owner:"Dueno",admin:"Admin",cashier:"Cajero",warehouse:"Almacenista",accountant:"Contador"},m={owner:"badge--owner",admin:"badge--blue",cashier:"badge--green",warehouse:"badge--yellow",accountant:"badge--purple"};function v(e){if(!e)return"";var t=document.createElement("div");return t.textContent=e,t.innerHTML}async function y(){var a=document.getElementById("branches-grid");if(!a)return[];try{var n=await fetch(e+"/branches",{headers:r()});if(!n.ok)throw new Error("Failed");var o=await n.json();t=o.data||[]}catch(e){return console.error("Config.loadBranches:",e),t=[],a.innerHTML='
Error al cargar sucursales
',t}return function(){var e=document.getElementById("branches-grid");if(!e)return;var a="";t.forEach((function(e,t){var n=e.is_active?''+(0===t?"Principal":"Activa")+"":'Inactiva';a+='
'+v(e.name)+'
'+n+"
"+(e.address?'
'+v(e.address)+"
":"")+(e.phone?'
'+v(e.phone)+"
":"")+"
"})),a+='
Agregar Sucursal
Configura una nueva ubicacion
',e.innerHTML=a}(),function(){var e=document.getElementById("emp-branch");if(!e)return;e.innerHTML='',t.forEach((function(t){if(t.is_active){var a=document.createElement("option");a.value=t.id,a.textContent=t.name,e.appendChild(a)}}))}(),t}async function g(t){var a=await fetch(e+"/branches",{method:"POST",headers:r(),body:JSON.stringify(t)});if(!a.ok){var n=await a.json().catch((function(){return{error:a.statusText}}));throw new Error(n.error||"Save failed")}return a.json()}async function f(){var t=document.getElementById("employees-tbody"),a=document.getElementById("employees-count");if(!t)return[];var n=[];try{var o=await fetch(e+"/employees",{headers:r()});if(!o.ok)throw new Error("Failed");n=(await o.json()).data||[]}catch(e){return console.error("Config.loadEmployees:",e),t.innerHTML='Error al cargar empleados',[]}var d=n.filter((function(e){return e.is_active})).length;if(a&&(a.textContent=d+" empleado"+(1!==d?"s":"")+" activo"+(1!==d?"s":"")),0===n.length)return t.innerHTML='Sin empleados registrados',n;var c="";return n.forEach((function(e){var t,a=function(e){if(!e)return"??";var t=e.trim().split(/\s+/);return t.length>=2?(t[0][0]+t[t.length-1][0]).toUpperCase():t[0].substring(0,2).toUpperCase()}(e.name),n=e.is_active?'Activo':'Inactivo';c+='
'+a+'
'+v(e.name)+"
"+v(e.email||"-")+""+(t=e.role,''+v(u[t]||t)+"")+v(e.branch_name||"Todas")+""+n+""+(e.max_discount_pct||0)+'%'})),t.innerHTML=c,n}async function p(t){var a=document.getElementById("employee-modal"),n=a?a.dataset.editId:null,o=e+"/employees",d="POST";n&&(o=e+"/employees/"+n,d="PUT",delete a.dataset.editId);var c=await fetch(o,{method:d,headers:r(),body:JSON.stringify(t)});if(!c.ok){var i=await c.json().catch((function(){return{error:c.statusText}}));throw new Error(i.error||"Save failed")}return c.json()}async function h(){try{var t=await fetch(e+"/business",{headers:r()});if(!t.ok)return;var a=await t.json();b("biz-razon-social",a.razon_social),b("biz-nombre",a.nombre),b("biz-rfc",a.rfc),b("biz-regimen",a.regimen_fiscal),b("biz-direccion",a.direccion),b("biz-telefono",a.telefono),b("biz-email",a.email)}catch(e){console.error("Config.loadBusiness:",e)}}function b(e,t){var a=document.getElementById(e);a&&(a.value=t||"")}function E(e){var t=document.getElementById(e);return t?t.value.trim():""}async function w(){try{var t=await fetch(e+"/currency",{headers:r()});if(!t.ok)return;var a=await t.json(),n=document.getElementById("cfg-currency"),o=document.getElementById("cfg-exchange-rate");n&&(n.value=a.currency||"MXN"),o&&(o.value=a.exchange_rate||17.5),localStorage.setItem("pos_currency",a.currency||"MXN"),localStorage.setItem("pos_exchange_rate",a.exchange_rate||17.5)}catch(e){console.error("Config.loadCurrency:",e)}}function I(){if(n()){try{var e=localStorage.getItem("nexus-theme");"industrial"!==e&&"modern"!==e||i(e)}catch(e){}s(),setInterval(s,3e4),function(){document.querySelector("#branches-grid");var e=document.getElementById("btn-save-branch");e&&e.addEventListener("click",(async function(){var t=document.getElementById("branch-name").value.trim();if(t){e.disabled=!0,e.textContent="Guardando...";try{await g({name:t,address:document.getElementById("branch-address").value.trim(),phone:document.getElementById("branch-phone").value.trim()}),o("Sucursal creada"),c("modal-branch"),document.getElementById("branch-name").value="",document.getElementById("branch-address").value="",document.getElementById("branch-phone").value="",await y()}catch(e){o(e.message,"error")}finally{e.disabled=!1,e.textContent="Guardar Sucursal"}}else o("Nombre de sucursal requerido","error")}));var t=document.getElementById("btn-new-employee");t&&t.addEventListener("click",(function(){d("modal-employee")}));var a=document.getElementById("btn-save-employee");a&&a.addEventListener("click",(async function(){var e=document.getElementById("emp-name").value.trim(),t=document.getElementById("emp-role").value,n=document.getElementById("emp-pin").value.trim();if(e)if(t)if(n&&4===n.length&&/^\d{4}$/.test(n)){var r=document.getElementById("emp-branch").value;a.disabled=!0,a.textContent="Guardando...";try{await p({name:e,email:document.getElementById("emp-email").value.trim()||null,phone:document.getElementById("emp-phone").value.trim()||null,role:t,pin:n,branch_id:r?parseInt(r,10):null,max_discount_pct:parseFloat(document.getElementById("emp-discount").value)||0}),o("Empleado creado"),c("modal-employee"),document.getElementById("emp-name").value="",document.getElementById("emp-email").value="",document.getElementById("emp-phone").value="",document.getElementById("emp-role").value="",document.getElementById("emp-pin").value="",document.getElementById("emp-branch").value="",document.getElementById("emp-discount").value="0",await f()}catch(e){o(e.message,"error")}finally{a.disabled=!1,a.textContent="Guardar Empleado"}}else o("PIN debe ser 4 digitos","error");else o("Selecciona un rol","error");else o("Nombre requerido","error")})),document.querySelectorAll(".cfg-modal-overlay").forEach((function(e){e.addEventListener("click",(function(t){t.target===e&&(e.style.display="none")}))})),document.addEventListener("keydown",(function(e){"Escape"===e.key&&document.querySelectorAll(".cfg-modal-overlay").forEach((function(e){e.style.display="none"}))}))}();var t=document.getElementById("cfg-kiosk-mode");t&&window.NexusKiosk&&(t.checked=window.NexusKiosk.isEnabled(),t.addEventListener("change",(function(){this.checked?(window.NexusKiosk.enable(),o("Modo Kiosko activado")):(window.NexusKiosk.disable(),o("Modo Kiosko desactivado"))}))),y(),f(),h(),w()}}return document.addEventListener("DOMContentLoaded",I),{init:I,setTheme:i,selectThemeOption:l,loadBranches:y,loadEmployees:f,saveBranch:g,saveEmployee:p,editEmployee:async function(t){if(n())try{var a=await fetch(e+"/employees",{headers:r()});if(!a.ok)throw new Error("Failed to load employees");var c=((await a.json()).data||[]).find((function(e){return e.id===t}));if(!c)return void o("Empleado no encontrado","error");b("new-emp-name",c.name),b("new-emp-email",c.email||"");var i=document.getElementById("new-emp-role");i&&(i.value=c.role||"cashier");var l=document.getElementById("new-emp-branch");l&&(l.value=c.branch_id||""),b("new-emp-discount",c.max_discount_pct||""),b("new-emp-pin","");var s=document.getElementById("employee-modal");if(s){s.dataset.editId=t;var u=s.querySelector(".modal-title, h3");u&&(u.textContent="Editar Empleado")}d("employee-modal")}catch(e){o("Error: "+e.message,"error")}},loadBusiness:h,saveBusiness:async function(){if(n()){var t={razon_social:E("biz-razon-social"),nombre:E("biz-nombre"),rfc:E("biz-rfc"),regimen_fiscal:E("biz-regimen"),direccion:E("biz-direccion"),telefono:E("biz-telefono"),email:E("biz-email")};try{var a=await fetch(e+"/business",{method:"PUT",headers:r(),body:JSON.stringify(t)});if(!a.ok){var d=await a.json().catch((function(){return{error:a.statusText}}));throw new Error(d.error||"Error al guardar")}o("Datos de empresa guardados","ok")}catch(e){o(e.message,"error")}}},saveTaxParams:async function(){if(n()){var t={tax_iva:E("tax-iva")||"16",tax_ieps:E("tax-ieps")||"0",invoice_serie:E("tax-serie")||"FA",invoice_folio:E("tax-folio")||"1",default_currency:document.getElementById("tax-moneda")?document.getElementById("tax-moneda").value:"MXN",default_payment_method:document.getElementById("tax-forma-pago")?document.getElementById("tax-forma-pago").value:"01"};try{if(!(await fetch(e+"/business",{method:"PUT",headers:r(),body:JSON.stringify(t)})).ok)throw new Error("Error al guardar");o("Parámetros de impuestos guardados","ok")}catch(e){o(e.message,"error")}}},loadCurrency:w,saveCurrency:async function(){var t=document.getElementById("cfg-currency"),a=document.getElementById("cfg-exchange-rate"),n=document.getElementById("currency-status"),d=document.getElementById("btn-save-currency");if(t&&a){var c=t.value,i=parseFloat(a.value);if(!i||i<=0)o("Tipo de cambio invalido","error");else{d&&(d.disabled=!0,d.textContent="Guardando...");try{var l=await fetch(e+"/currency",{method:"PUT",headers:r(),body:JSON.stringify({currency:c,exchange_rate:i})});if(!l.ok){var s=await l.json().catch((function(){return{error:l.statusText}}));throw new Error(s.error||"Save failed")}localStorage.setItem("pos_currency",c),localStorage.setItem("pos_exchange_rate",i),o("Moneda actualizada"),n&&(n.textContent=c+" — TC: "+i)}catch(e){o(e.message,"error")}finally{d&&(d.disabled=!1,d.textContent="Guardar Moneda")}}}},openModal:d,closeModal:c}})(); \ No newline at end of file +const Config=(()=>{const e="/pos/api/config";let t=[];function a(){return localStorage.getItem("pos_token")||""}function n(){return!!a()||(window.location.href="/pos/login",!1)}function r(){return{Authorization:`Bearer ${a()}`,"Content-Type":"application/json"}}function o(e,t){var a=document.createElement("div");a.className="cfg-toast cfg-toast--"+(t||"ok"),a.textContent=e,document.body.appendChild(a),setTimeout((function(){a.remove()}),3e3)}function c(e){var t=document.getElementById(e);t&&(t.style.display="flex")}function d(e){var t=document.getElementById(e);t&&(t.style.display="none")}function i(e){document.documentElement.setAttribute("data-theme",e);try{localStorage.setItem("nexus-theme",e)}catch(e){}document.querySelectorAll(".theme-btn").forEach((function(t){t.classList.toggle("is-active",t.dataset.themeTarget===e)})),document.querySelectorAll(".theme-option").forEach((function(e){e.classList.remove("is-selected")}));var t="industrial"===e?0:1,a=document.querySelectorAll(".theme-option");a[t]&&a[t].classList.add("is-selected")}function s(e){i(e)}function l(){var e=new Date,t=String(e.getHours()).padStart(2,"0"),a=String(e.getMinutes()).padStart(2,"0"),n=document.getElementById("live-clock");n&&(n.textContent=t+":"+a)}window.setTheme=i,window.selectThemeOption=s;var u={owner:"Dueno",admin:"Admin",cashier:"Cajero",warehouse:"Almacenista",accountant:"Contador"},m={owner:"badge--owner",admin:"badge--blue",cashier:"badge--green",warehouse:"badge--yellow",accountant:"badge--purple"};function v(e){if(!e)return"";var t=document.createElement("div");return t.textContent=e,t.innerHTML}async function y(){var a=document.getElementById("branches-grid");if(!a)return[];try{var n=await fetch(e+"/branches",{headers:r()});if(!n.ok)throw new Error("Failed");var o=await n.json();t=o.data||[]}catch(e){return console.error("Config.loadBranches:",e),t=[],a.innerHTML='
Error al cargar sucursales
',t}return function(){var e=document.getElementById("branches-grid");if(!e)return;var a="";t.forEach((function(e,t){var n=e.is_active?''+(0===t?"Principal":"Activa")+"":'Inactiva';a+='
'+v(e.name)+'
'+n+"
"+(e.address?'
'+v(e.address)+"
":"")+(e.phone?'
'+v(e.phone)+"
":"")+"
"})),a+='
Agregar Sucursal
Configura una nueva ubicacion
',e.innerHTML=a}(),function(){var e=document.getElementById("emp-branch");if(!e)return;e.innerHTML='',t.forEach((function(t){if(t.is_active){var a=document.createElement("option");a.value=t.id,a.textContent=t.name,e.appendChild(a)}}))}(),t}async function g(t){var a=await fetch(e+"/branches",{method:"POST",headers:r(),body:JSON.stringify(t)});if(!a.ok){var n=await a.json().catch((function(){return{error:a.statusText}}));throw new Error(n.error||"Save failed")}return a.json()}async function f(){var t=document.getElementById("employees-tbody"),a=document.getElementById("employees-count");if(!t)return[];var n=[];try{var o=await fetch(e+"/employees",{headers:r()});if(!o.ok)throw new Error("Failed");n=(await o.json()).data||[]}catch(e){return console.error("Config.loadEmployees:",e),t.innerHTML='Error al cargar empleados',[]}var c=n.filter((function(e){return e.is_active})).length;if(a&&(a.textContent=c+" empleado"+(1!==c?"s":"")+" activo"+(1!==c?"s":"")),0===n.length)return t.innerHTML='Sin empleados registrados',n;var d="";return n.forEach((function(e){var t,a=function(e){if(!e)return"??";var t=e.trim().split(/\s+/);return t.length>=2?(t[0][0]+t[t.length-1][0]).toUpperCase():t[0].substring(0,2).toUpperCase()}(e.name),n=e.is_active?'Activo':'Inactivo';d+='
'+a+'
'+v(e.name)+"
"+v(e.email||"-")+""+(t=e.role,''+v(u[t]||t)+"")+v(e.branch_name||"Todas")+""+n+""+(e.max_discount_pct||0)+'%'})),t.innerHTML=d,n}async function p(t){var a=document.getElementById("employee-modal"),n=a?a.dataset.editId:null,o=e+"/employees",c="POST";n&&(o=e+"/employees/"+n,c="PUT",delete a.dataset.editId);var d=await fetch(o,{method:c,headers:r(),body:JSON.stringify(t)});if(!d.ok){var i=await d.json().catch((function(){return{error:d.statusText}}));throw new Error(i.error||"Save failed")}return d.json()}async function h(){try{var t=await fetch(e+"/business",{headers:r()});if(!t.ok)return;var a=await t.json();b("biz-razon-social",a.razon_social),b("biz-nombre",a.nombre),b("biz-rfc",a.rfc),b("biz-regimen",a.regimen_fiscal),b("biz-direccion",a.direccion),b("biz-telefono",a.telefono),b("biz-email",a.email)}catch(e){console.error("Config.loadBusiness:",e)}}function b(e,t){var a=document.getElementById(e);a&&(a.value=t||"")}function E(e){var t=document.getElementById(e);return t?t.value.trim():""}async function w(){try{var t=await fetch(e+"/currency",{headers:r()});if(!t.ok)return;var a=await t.json(),n=document.getElementById("cfg-currency"),o=document.getElementById("cfg-exchange-rate");n&&(n.value=a.currency||"MXN"),o&&(o.value=a.exchange_rate||17.5),localStorage.setItem("pos_currency",a.currency||"MXN"),localStorage.setItem("pos_exchange_rate",a.exchange_rate||17.5)}catch(e){console.error("Config.loadCurrency:",e)}}async function I(){var t=document.getElementById("cfg-compat-source"),a=document.getElementById("btn-save-compat-source");if(t){a&&(a.disabled=!0,a.textContent="Guardando...");try{var n=await fetch(e+"/vehicle-compat-source",{method:"PUT",headers:r(),body:JSON.stringify({source:t.value})});if(!n.ok){var c=await n.json().catch((function(){return{error:n.statusText}}));throw new Error(c.error||"Save failed")}o("Fuente de compatibilidad actualizada")}catch(e){o(e.message,"error")}finally{a&&(a.disabled=!1,a.textContent="Guardar")}}}function x(){if(n()){try{var t=localStorage.getItem("nexus-theme");"industrial"!==t&&"modern"!==t||i(t)}catch(e){}l(),setInterval(l,3e4),function(){document.querySelector("#branches-grid");var e=document.getElementById("btn-save-branch");e&&e.addEventListener("click",(async function(){var t=document.getElementById("branch-name").value.trim();if(t){e.disabled=!0,e.textContent="Guardando...";try{await g({name:t,address:document.getElementById("branch-address").value.trim(),phone:document.getElementById("branch-phone").value.trim()}),o("Sucursal creada"),d("modal-branch"),document.getElementById("branch-name").value="",document.getElementById("branch-address").value="",document.getElementById("branch-phone").value="",await y()}catch(e){o(e.message,"error")}finally{e.disabled=!1,e.textContent="Guardar Sucursal"}}else o("Nombre de sucursal requerido","error")}));var t=document.getElementById("btn-new-employee");t&&t.addEventListener("click",(function(){c("modal-employee")}));var a=document.getElementById("btn-save-employee");a&&a.addEventListener("click",(async function(){var e=document.getElementById("emp-name").value.trim(),t=document.getElementById("emp-role").value,n=document.getElementById("emp-pin").value.trim();if(e)if(t)if(n&&4===n.length&&/^\d{4}$/.test(n)){var r=document.getElementById("emp-branch").value;a.disabled=!0,a.textContent="Guardando...";try{await p({name:e,email:document.getElementById("emp-email").value.trim()||null,phone:document.getElementById("emp-phone").value.trim()||null,role:t,pin:n,branch_id:r?parseInt(r,10):null,max_discount_pct:parseFloat(document.getElementById("emp-discount").value)||0}),o("Empleado creado"),d("modal-employee"),document.getElementById("emp-name").value="",document.getElementById("emp-email").value="",document.getElementById("emp-phone").value="",document.getElementById("emp-role").value="",document.getElementById("emp-pin").value="",document.getElementById("emp-branch").value="",document.getElementById("emp-discount").value="0",await f()}catch(e){o(e.message,"error")}finally{a.disabled=!1,a.textContent="Guardar Empleado"}}else o("PIN debe ser 4 digitos","error");else o("Selecciona un rol","error");else o("Nombre requerido","error")})),document.querySelectorAll(".cfg-modal-overlay").forEach((function(e){e.addEventListener("click",(function(t){t.target===e&&(e.style.display="none")}))})),document.addEventListener("keydown",(function(e){"Escape"===e.key&&document.querySelectorAll(".cfg-modal-overlay").forEach((function(e){e.style.display="none"}))}))}();var a=document.getElementById("btn-save-compat-source");a&&a.addEventListener("click",I);var s=document.getElementById("cfg-kiosk-mode");s&&window.NexusKiosk&&(s.checked=window.NexusKiosk.isEnabled(),s.addEventListener("change",(function(){this.checked?(window.NexusKiosk.enable(),o("Modo Kiosko activado")):(window.NexusKiosk.disable(),o("Modo Kiosko desactivado"))}))),y(),f(),h(),w(),async function(){try{var t=await fetch(e+"/vehicle-compat-source",{headers:r()});if(!t.ok)return;var a=await t.json(),n=document.getElementById("cfg-compat-source");n&&(n.value=a.source||"both")}catch(e){console.error("Config.loadVehicleCompatSource:",e)}}()}}return document.addEventListener("DOMContentLoaded",x),{init:x,setTheme:i,selectThemeOption:s,loadBranches:y,loadEmployees:f,saveBranch:g,saveEmployee:p,editEmployee:async function(t){if(n())try{var a=await fetch(e+"/employees",{headers:r()});if(!a.ok)throw new Error("Failed to load employees");var d=((await a.json()).data||[]).find((function(e){return e.id===t}));if(!d)return void o("Empleado no encontrado","error");b("new-emp-name",d.name),b("new-emp-email",d.email||"");var i=document.getElementById("new-emp-role");i&&(i.value=d.role||"cashier");var s=document.getElementById("new-emp-branch");s&&(s.value=d.branch_id||""),b("new-emp-discount",d.max_discount_pct||""),b("new-emp-pin","");var l=document.getElementById("employee-modal");if(l){l.dataset.editId=t;var u=l.querySelector(".modal-title, h3");u&&(u.textContent="Editar Empleado")}c("employee-modal")}catch(e){o("Error: "+e.message,"error")}},loadBusiness:h,saveBusiness:async function(){if(n()){var t={razon_social:E("biz-razon-social"),nombre:E("biz-nombre"),rfc:E("biz-rfc"),regimen_fiscal:E("biz-regimen"),direccion:E("biz-direccion"),telefono:E("biz-telefono"),email:E("biz-email")};try{var a=await fetch(e+"/business",{method:"PUT",headers:r(),body:JSON.stringify(t)});if(!a.ok){var c=await a.json().catch((function(){return{error:a.statusText}}));throw new Error(c.error||"Error al guardar")}o("Datos de empresa guardados","ok")}catch(e){o(e.message,"error")}}},saveTaxParams:async function(){if(n()){var t={tax_iva:E("tax-iva")||"16",tax_ieps:E("tax-ieps")||"0",invoice_serie:E("tax-serie")||"FA",invoice_folio:E("tax-folio")||"1",default_currency:document.getElementById("tax-moneda")?document.getElementById("tax-moneda").value:"MXN",default_payment_method:document.getElementById("tax-forma-pago")?document.getElementById("tax-forma-pago").value:"01"};try{if(!(await fetch(e+"/business",{method:"PUT",headers:r(),body:JSON.stringify(t)})).ok)throw new Error("Error al guardar");o("Parámetros de impuestos guardados","ok")}catch(e){o(e.message,"error")}}},loadCurrency:w,saveCurrency:async function(){var t=document.getElementById("cfg-currency"),a=document.getElementById("cfg-exchange-rate"),n=document.getElementById("currency-status"),c=document.getElementById("btn-save-currency");if(t&&a){var d=t.value,i=parseFloat(a.value);if(!i||i<=0)o("Tipo de cambio invalido","error");else{c&&(c.disabled=!0,c.textContent="Guardando...");try{var s=await fetch(e+"/currency",{method:"PUT",headers:r(),body:JSON.stringify({currency:d,exchange_rate:i})});if(!s.ok){var l=await s.json().catch((function(){return{error:s.statusText}}));throw new Error(l.error||"Save failed")}localStorage.setItem("pos_currency",d),localStorage.setItem("pos_exchange_rate",i),o("Moneda actualizada"),n&&(n.textContent=d+" — TC: "+i)}catch(e){o(e.message,"error")}finally{c&&(c.disabled=!1,c.textContent="Guardar Moneda")}}}},openModal:c,closeModal:d}})(); \ No newline at end of file diff --git a/pos/templates/config.html b/pos/templates/config.html index 634874a..d2e8482 100644 --- a/pos/templates/config.html +++ b/pos/templates/config.html @@ -551,7 +551,40 @@ +
+
+
+ +
+
+
Compatibilidad de Vehículos
+
Elige la fuente para asignar vehículos compatibles a tus productos
+
+
+ +
+
Fuente de Datos
+
+
+ + + Afecta la creación de productos y el botón "Auto-Match" +
+
+
+ +
+
+
+ +